@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,922 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AntiBan — Main orchestrator combining rate limiting, warm-up, and health monitoring
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { AntiBan } from 'baileys-antiban';
|
|
6
|
+
* const antiban = new AntiBan();
|
|
7
|
+
*
|
|
8
|
+
* // Before sending a message:
|
|
9
|
+
* const result = await antiban.beforeSend(recipient, content);
|
|
10
|
+
* if (result.allowed) {
|
|
11
|
+
* await new Promise(r => setTimeout(r, result.delayMs));
|
|
12
|
+
* await sock.sendMessage(recipient, { text: content });
|
|
13
|
+
* antiban.afterSend(recipient, content);
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
import { RateLimiter } from './rateLimiter.js';
|
|
17
|
+
import { WarmUp } from './warmup.js';
|
|
18
|
+
import { HealthMonitor } from './health.js';
|
|
19
|
+
import { TimelockGuard } from './timelockGuard.js';
|
|
20
|
+
import { ReplyRatioGuard } from './replyRatio.js';
|
|
21
|
+
import { ContactGraphWarmer } from './contactGraph.js';
|
|
22
|
+
import { PresenceChoreographer } from './presenceChoreographer.js';
|
|
23
|
+
import { RetryReasonTracker } from './retryTracker.js';
|
|
24
|
+
import { TopologyThrottler } from './topologyThrottler.js';
|
|
25
|
+
import { PostReconnectThrottle } from './reconnectThrottle.js';
|
|
26
|
+
import { LidResolver } from './lidResolver.js';
|
|
27
|
+
import { JidCanonicalizer } from './jidCanonicalizer.js';
|
|
28
|
+
import { SessionHealthMonitor } from './sessionStability.js';
|
|
29
|
+
import { BanRecoveryOrchestrator } from './banRecoveryOrchestrator.js';
|
|
30
|
+
import { resolveConfig } from './presets.js';
|
|
31
|
+
import { StateManager } from './persist.js';
|
|
32
|
+
import { shouldUseGroupProfile, applyGroupMultiplier } from './profiles.js';
|
|
33
|
+
import { DeliveryTracker } from './deliveryTracker.js';
|
|
34
|
+
import { InstanceCoordinator } from './instanceCoordinator.js';
|
|
35
|
+
import { MessageTypeRegistry } from './messageTypeRegistry.js';
|
|
36
|
+
import { exportAntibanState, importAntibanState } from './stateExport.js';
|
|
37
|
+
import { JidCircuitBreaker } from './jidCircuitBreaker.js';
|
|
38
|
+
function isLegacyConfig(cfg) {
|
|
39
|
+
if (typeof cfg !== 'object' || cfg === null)
|
|
40
|
+
return false;
|
|
41
|
+
return 'rateLimiter' in cfg || 'warmUp' in cfg || 'health' in cfg || 'timelock' in cfg ||
|
|
42
|
+
'replyRatio' in cfg || 'contactGraph' in cfg || 'presence' in cfg || 'retryTracker' in cfg ||
|
|
43
|
+
'reconnectThrottle' in cfg || 'lidResolver' in cfg || 'jidCanonicalizer' in cfg ||
|
|
44
|
+
'sessionStability' in cfg;
|
|
45
|
+
}
|
|
46
|
+
function mapLegacyToFlat(legacy) {
|
|
47
|
+
console.warn('[baileys-antiban] DEPRECATED: Nested config (v2 style) detected. ' +
|
|
48
|
+
'Migrate to flat config: new AntiBan({ maxPerMinute: 8 }). ' +
|
|
49
|
+
'See: https://github.com/kobie3717/baileys-antiban#migration');
|
|
50
|
+
const flat = {};
|
|
51
|
+
if (legacy.rateLimiter?.maxPerMinute !== undefined)
|
|
52
|
+
flat.maxPerMinute = legacy.rateLimiter.maxPerMinute;
|
|
53
|
+
if (legacy.rateLimiter?.maxPerHour !== undefined)
|
|
54
|
+
flat.maxPerHour = legacy.rateLimiter.maxPerHour;
|
|
55
|
+
if (legacy.rateLimiter?.maxPerDay !== undefined)
|
|
56
|
+
flat.maxPerDay = legacy.rateLimiter.maxPerDay;
|
|
57
|
+
if (legacy.rateLimiter?.minDelayMs !== undefined)
|
|
58
|
+
flat.minDelayMs = legacy.rateLimiter.minDelayMs;
|
|
59
|
+
if (legacy.rateLimiter?.maxDelayMs !== undefined)
|
|
60
|
+
flat.maxDelayMs = legacy.rateLimiter.maxDelayMs;
|
|
61
|
+
if (legacy.rateLimiter?.newChatDelayMs !== undefined)
|
|
62
|
+
flat.newChatDelayMs = legacy.rateLimiter.newChatDelayMs;
|
|
63
|
+
if (legacy.warmUp?.warmUpDays !== undefined)
|
|
64
|
+
flat.warmupDays = legacy.warmUp.warmUpDays;
|
|
65
|
+
if (legacy.warmUp?.day1Limit !== undefined)
|
|
66
|
+
flat.day1Limit = legacy.warmUp.day1Limit;
|
|
67
|
+
if (legacy.warmUp?.growthFactor !== undefined)
|
|
68
|
+
flat.growthFactor = legacy.warmUp.growthFactor;
|
|
69
|
+
if (legacy.logging !== undefined)
|
|
70
|
+
flat.logging = legacy.logging;
|
|
71
|
+
// Preserve flat top-level fields that coexist with nested keys
|
|
72
|
+
const legacyAsFlat = legacy;
|
|
73
|
+
if (flat.warmupDays === undefined && typeof legacyAsFlat.warmUpDays === 'number')
|
|
74
|
+
flat.warmupDays = legacyAsFlat.warmUpDays;
|
|
75
|
+
if (flat.warmupDays === undefined && typeof legacyAsFlat.warmupDays === 'number')
|
|
76
|
+
flat.warmupDays = legacyAsFlat.warmupDays;
|
|
77
|
+
if (flat.day1Limit === undefined && typeof legacyAsFlat.day1Limit === 'number')
|
|
78
|
+
flat.day1Limit = legacyAsFlat.day1Limit;
|
|
79
|
+
if (flat.growthFactor === undefined && typeof legacyAsFlat.growthFactor === 'number')
|
|
80
|
+
flat.growthFactor = legacyAsFlat.growthFactor;
|
|
81
|
+
if (flat.inactivityThresholdHours === undefined && typeof legacyAsFlat.inactivityThresholdHours === 'number')
|
|
82
|
+
flat.inactivityThresholdHours = legacyAsFlat.inactivityThresholdHours;
|
|
83
|
+
if (flat.maxIdenticalMessages === undefined && typeof legacyAsFlat.maxIdenticalMessages === 'number')
|
|
84
|
+
flat.maxIdenticalMessages = legacyAsFlat.maxIdenticalMessages;
|
|
85
|
+
if (flat.identicalMessageWindowMs === undefined && typeof legacyAsFlat.identicalMessageWindowMs === 'number')
|
|
86
|
+
flat.identicalMessageWindowMs = legacyAsFlat.identicalMessageWindowMs;
|
|
87
|
+
if (flat.burstAllowance === undefined && typeof legacyAsFlat.burstAllowance === 'number')
|
|
88
|
+
flat.burstAllowance = legacyAsFlat.burstAllowance;
|
|
89
|
+
// v3 fields that may coexist with legacy nested keys
|
|
90
|
+
if (flat.autoPauseAt === undefined && typeof legacyAsFlat.autoPauseAt === 'string')
|
|
91
|
+
flat.autoPauseAt = legacyAsFlat.autoPauseAt;
|
|
92
|
+
if (flat.groupMultiplier === undefined && typeof legacyAsFlat.groupMultiplier === 'number')
|
|
93
|
+
flat.groupMultiplier = legacyAsFlat.groupMultiplier;
|
|
94
|
+
if (flat.groupProfiles === undefined && typeof legacyAsFlat.groupProfiles === 'boolean')
|
|
95
|
+
flat.groupProfiles = legacyAsFlat.groupProfiles;
|
|
96
|
+
if (flat.persist === undefined && typeof legacyAsFlat.persist === 'string')
|
|
97
|
+
flat.persist = legacyAsFlat.persist;
|
|
98
|
+
return flat;
|
|
99
|
+
}
|
|
100
|
+
export class AntiBan {
|
|
101
|
+
rateLimiter;
|
|
102
|
+
warmUp;
|
|
103
|
+
health;
|
|
104
|
+
timelockGuard;
|
|
105
|
+
replyRatioGuard;
|
|
106
|
+
contactGraphWarmer;
|
|
107
|
+
presenceChoreographer;
|
|
108
|
+
retryTrackerModule;
|
|
109
|
+
reconnectThrottleModule;
|
|
110
|
+
topologyThrottlerModule = null;
|
|
111
|
+
lidResolverModule = null;
|
|
112
|
+
jidCanonicalizerModule = null;
|
|
113
|
+
sessionStabilityMonitor = null;
|
|
114
|
+
banRecovery;
|
|
115
|
+
deliveryTracker;
|
|
116
|
+
instanceCoordinator = null;
|
|
117
|
+
messageTypeRegistry = null;
|
|
118
|
+
jidCircuitBreakerModule = null;
|
|
119
|
+
stateManager = null;
|
|
120
|
+
resolvedConfig;
|
|
121
|
+
logging;
|
|
122
|
+
hasDisconnected = false; // BUG FIX 3: Track if we've ever disconnected
|
|
123
|
+
/** Optional reputation voucher (standalone — caller manages separate voucher sockets) */
|
|
124
|
+
reputationVoucher;
|
|
125
|
+
stats = {
|
|
126
|
+
messagesAllowed: 0,
|
|
127
|
+
messagesBlocked: 0,
|
|
128
|
+
totalDelayMs: 0,
|
|
129
|
+
};
|
|
130
|
+
constructor(input, warmUpStateArg) {
|
|
131
|
+
let flatConfig;
|
|
132
|
+
let legacyPassthrough = null;
|
|
133
|
+
let warmUpState = warmUpStateArg;
|
|
134
|
+
if (isLegacyConfig(input)) {
|
|
135
|
+
legacyPassthrough = input;
|
|
136
|
+
flatConfig = mapLegacyToFlat(legacyPassthrough);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
flatConfig = {};
|
|
140
|
+
legacyPassthrough = null;
|
|
141
|
+
}
|
|
142
|
+
const cfg = isLegacyConfig(input)
|
|
143
|
+
? resolveConfig(flatConfig)
|
|
144
|
+
: resolveConfig(input);
|
|
145
|
+
this.resolvedConfig = cfg;
|
|
146
|
+
// Initialize persistence — load state before constructing modules
|
|
147
|
+
let savedState = null;
|
|
148
|
+
if (cfg.persist) {
|
|
149
|
+
this.stateManager = new StateManager(cfg.persist);
|
|
150
|
+
savedState = this.stateManager.load();
|
|
151
|
+
if (savedState) {
|
|
152
|
+
warmUpState = savedState.warmup;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
this.logging = cfg.logging ?? true;
|
|
156
|
+
this.rateLimiter = new RateLimiter({
|
|
157
|
+
maxPerMinute: cfg.maxPerMinute,
|
|
158
|
+
maxPerHour: cfg.maxPerHour,
|
|
159
|
+
maxPerDay: cfg.maxPerDay,
|
|
160
|
+
minDelayMs: cfg.minDelayMs,
|
|
161
|
+
maxDelayMs: cfg.maxDelayMs,
|
|
162
|
+
newChatDelayMs: cfg.newChatDelayMs,
|
|
163
|
+
maxIdenticalMessages: cfg.maxIdenticalMessages,
|
|
164
|
+
identicalMessageWindowMs: cfg.identicalMessageWindowMs,
|
|
165
|
+
burstAllowance: cfg.burstAllowance,
|
|
166
|
+
...(legacyPassthrough?.rateLimiter || {}),
|
|
167
|
+
});
|
|
168
|
+
// Restore knownChats from persisted state after rateLimiter is constructed
|
|
169
|
+
if (savedState?.knownChats) {
|
|
170
|
+
this.rateLimiter.restoreKnownChats(savedState.knownChats);
|
|
171
|
+
}
|
|
172
|
+
this.warmUp = new WarmUp({
|
|
173
|
+
warmUpDays: cfg.warmupDays,
|
|
174
|
+
day1Limit: cfg.day1Limit,
|
|
175
|
+
growthFactor: cfg.growthFactor,
|
|
176
|
+
inactivityThresholdHours: cfg.inactivityThresholdHours,
|
|
177
|
+
...(legacyPassthrough?.warmUp || {}),
|
|
178
|
+
}, warmUpState);
|
|
179
|
+
this.health = new HealthMonitor({
|
|
180
|
+
autoPauseAt: cfg.autoPauseAt,
|
|
181
|
+
...(legacyPassthrough?.health || {}),
|
|
182
|
+
onRiskChange: (status) => {
|
|
183
|
+
if (this.logging) {
|
|
184
|
+
const emoji = { low: '🟢', medium: '🟡', high: '🟠', critical: '🔴' };
|
|
185
|
+
console.log(`[baileys-antiban] ${emoji[status.risk]} Risk level: ${status.risk.toUpperCase()} (score: ${status.score})`);
|
|
186
|
+
console.log(`[baileys-antiban] ${status.recommendation}`);
|
|
187
|
+
status.reasons.forEach(r => console.log(`[baileys-antiban] → ${r}`));
|
|
188
|
+
}
|
|
189
|
+
if ((status.risk === 'high' || status.risk === 'critical') && cfg.onAtRisk) {
|
|
190
|
+
cfg.onAtRisk(status);
|
|
191
|
+
}
|
|
192
|
+
// Trigger recovery orchestrator on critical risk
|
|
193
|
+
if (status.risk === 'critical') {
|
|
194
|
+
this.banRecovery.recordBanEvent('soft_ban');
|
|
195
|
+
}
|
|
196
|
+
cfg.onRiskChange?.(status);
|
|
197
|
+
legacyPassthrough?.health?.onRiskChange?.(status);
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
// Initialize ban recovery orchestrator
|
|
201
|
+
this.banRecovery = new BanRecoveryOrchestrator({
|
|
202
|
+
onPhaseChange: (phase, plan) => {
|
|
203
|
+
if (this.logging) {
|
|
204
|
+
console.log(`[baileys-antiban] 🔄 Recovery phase: ${phase} — ${plan.description}`);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
onHardBan: () => {
|
|
208
|
+
if (this.logging) {
|
|
209
|
+
console.log(`[baileys-antiban] 💀 HARD BAN detected — account likely dead, replace SIM`);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
// Initialize delivery tracker
|
|
214
|
+
this.deliveryTracker = new DeliveryTracker({
|
|
215
|
+
onLowDeliveryRate: (rate) => {
|
|
216
|
+
if (this.logging) {
|
|
217
|
+
console.log(`[baileys-antiban] ⚠️ Low delivery rate detected: ${Math.round(rate * 100)}% — possible soft ban`);
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
this.timelockGuard = new TimelockGuard({
|
|
222
|
+
...(legacyPassthrough?.timelock || {}),
|
|
223
|
+
onTimelockDetected: (state) => {
|
|
224
|
+
this.health.recordReachoutTimelock(state.enforcementType);
|
|
225
|
+
this.banRecovery.recordBanEvent('timelock');
|
|
226
|
+
if (this.logging) {
|
|
227
|
+
console.log(`[baileys-antiban] REACHOUT TIMELOCKED — ${state.enforcementType || 'unknown'}, expires ${state.expiresAt?.toISOString() || 'unknown'}`);
|
|
228
|
+
}
|
|
229
|
+
cfg.onTimelockDetected?.(state);
|
|
230
|
+
legacyPassthrough?.timelock?.onTimelockDetected?.(state);
|
|
231
|
+
},
|
|
232
|
+
onTimelockLifted: (state) => {
|
|
233
|
+
if (this.logging) {
|
|
234
|
+
console.log(`[baileys-antiban] Timelock lifted — resuming new contact messages`);
|
|
235
|
+
}
|
|
236
|
+
cfg.onTimelockLifted?.(state);
|
|
237
|
+
legacyPassthrough?.timelock?.onTimelockLifted?.(state);
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
this.replyRatioGuard = new ReplyRatioGuard(legacyPassthrough?.replyRatio);
|
|
241
|
+
this.contactGraphWarmer = new ContactGraphWarmer(legacyPassthrough?.contactGraph);
|
|
242
|
+
this.presenceChoreographer = new PresenceChoreographer(legacyPassthrough?.presence);
|
|
243
|
+
this.retryTrackerModule = new RetryReasonTracker({
|
|
244
|
+
...(legacyPassthrough?.retryTracker || {}),
|
|
245
|
+
onSpiral: (msgId, reason) => {
|
|
246
|
+
if (this.logging) {
|
|
247
|
+
console.log(`[baileys-antiban] ⚠️ Message ${msgId} stuck in retry spiral (${reason})`);
|
|
248
|
+
}
|
|
249
|
+
legacyPassthrough?.retryTracker?.onSpiral?.(msgId, reason);
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
this.reconnectThrottleModule = new PostReconnectThrottle({
|
|
253
|
+
...(legacyPassthrough?.reconnectThrottle || {}),
|
|
254
|
+
baselineRatePerMinute: () => this.rateLimiter.getStats().limits.perMinute,
|
|
255
|
+
});
|
|
256
|
+
// Initialize topology throttler if configured
|
|
257
|
+
if (legacyPassthrough?.topologyThrottler) {
|
|
258
|
+
this.topologyThrottlerModule = new TopologyThrottler(legacyPassthrough.topologyThrottler);
|
|
259
|
+
if (this.logging) {
|
|
260
|
+
console.log(`[baileys-antiban] 🌐 Topology throttler enabled — max ${legacyPassthrough.topologyThrottler.maxNewContactsPerHour || 5}/hr, ${legacyPassthrough.topologyThrottler.maxNewContactsPerDay || 20}/day new contacts`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Initialize LID resolver and canonicalizer if configured
|
|
264
|
+
// If jidCanonicalizer is enabled but no resolver provided, create standalone resolver
|
|
265
|
+
if (legacyPassthrough?.jidCanonicalizer?.enabled) {
|
|
266
|
+
// Create or use provided resolver
|
|
267
|
+
if (legacyPassthrough.jidCanonicalizer.resolver) {
|
|
268
|
+
// User provided their own resolver
|
|
269
|
+
this.jidCanonicalizerModule = new JidCanonicalizer(legacyPassthrough.jidCanonicalizer);
|
|
270
|
+
this.lidResolverModule = legacyPassthrough.jidCanonicalizer.resolver;
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
// Create new resolver using lidResolver config if provided
|
|
274
|
+
const resolverConfig = legacyPassthrough.lidResolver || legacyPassthrough.jidCanonicalizer.resolverConfig;
|
|
275
|
+
const resolver = new LidResolver(resolverConfig);
|
|
276
|
+
this.lidResolverModule = resolver;
|
|
277
|
+
this.jidCanonicalizerModule = new JidCanonicalizer({
|
|
278
|
+
...legacyPassthrough.jidCanonicalizer,
|
|
279
|
+
resolver,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else if (legacyPassthrough?.lidResolver) {
|
|
284
|
+
// Standalone resolver without canonicalizer
|
|
285
|
+
this.lidResolverModule = new LidResolver(legacyPassthrough.lidResolver);
|
|
286
|
+
}
|
|
287
|
+
// Initialize session stability monitor if enabled
|
|
288
|
+
if (legacyPassthrough?.sessionStability?.enabled) {
|
|
289
|
+
const healthConfig = {
|
|
290
|
+
badMacThreshold: legacyPassthrough.sessionStability.badMacThreshold,
|
|
291
|
+
badMacWindowMs: legacyPassthrough.sessionStability.badMacWindowMs,
|
|
292
|
+
onDegraded: (stats) => {
|
|
293
|
+
if (this.logging) {
|
|
294
|
+
console.log(`[baileys-antiban] 🔴 SESSION DEGRADED — Bad MAC rate: ${stats.badMacCount} in last ${legacyPassthrough?.sessionStability?.badMacWindowMs || 60000}ms`);
|
|
295
|
+
console.log(`[baileys-antiban] Consider restarting session or switching to LID-based canonical form`);
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
onRecovered: () => {
|
|
299
|
+
if (this.logging) {
|
|
300
|
+
console.log(`[baileys-antiban] 🟢 SESSION RECOVERED — decrypt success rate improved`);
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
this.sessionStabilityMonitor = new SessionHealthMonitor(healthConfig);
|
|
305
|
+
}
|
|
306
|
+
// Initialize instance coordinator if configured
|
|
307
|
+
if (cfg.instanceCoordinator) {
|
|
308
|
+
this.instanceCoordinator = new InstanceCoordinator({
|
|
309
|
+
sharedFilePath: cfg.instanceCoordinator,
|
|
310
|
+
poolMaxPerMinute: cfg.instancePoolMaxPerMinute,
|
|
311
|
+
poolMaxPerHour: cfg.instancePoolMaxPerHour,
|
|
312
|
+
});
|
|
313
|
+
if (this.logging) {
|
|
314
|
+
console.log(`[baileys-antiban] 🌐 Instance coordination enabled: ${cfg.instanceCoordinator}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Initialize message type registry if configured
|
|
318
|
+
if (cfg.messageTypeRegistry) {
|
|
319
|
+
this.messageTypeRegistry = new MessageTypeRegistry();
|
|
320
|
+
if (this.logging) {
|
|
321
|
+
console.log(`[baileys-antiban] 📝 Message type registry enabled`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Initialize JID circuit breaker if configured (BUG FIX 2)
|
|
325
|
+
if (cfg.circuitBreaker) {
|
|
326
|
+
const cbConfig = typeof cfg.circuitBreaker === 'object'
|
|
327
|
+
? cfg.circuitBreaker
|
|
328
|
+
: {};
|
|
329
|
+
this.jidCircuitBreakerModule = new JidCircuitBreaker({
|
|
330
|
+
...cbConfig,
|
|
331
|
+
logger: this.logging ? { warn: console.warn.bind(console), info: console.info.bind(console) } : undefined,
|
|
332
|
+
});
|
|
333
|
+
if (this.logging) {
|
|
334
|
+
console.log(`[baileys-antiban] 🔌 JID circuit breaker enabled`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Check if a message can be sent and get required delay.
|
|
340
|
+
* Call this BEFORE every sendMessage().
|
|
341
|
+
*/
|
|
342
|
+
async beforeSend(recipient, content) {
|
|
343
|
+
const healthStatus = this.health.getStatus();
|
|
344
|
+
// Health monitor says stop
|
|
345
|
+
if (this.health.isPaused()) {
|
|
346
|
+
this.stats.messagesBlocked++;
|
|
347
|
+
if (this.logging) {
|
|
348
|
+
console.log(`[baileys-antiban] ⛔ BLOCKED — health risk too high (${healthStatus.risk})`);
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
allowed: false,
|
|
352
|
+
delayMs: 0,
|
|
353
|
+
reason: `Health risk ${healthStatus.risk}: ${healthStatus.recommendation}`,
|
|
354
|
+
health: healthStatus,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
// Recovery orchestrator rate multiplier
|
|
358
|
+
const recoveryStatus = this.banRecovery.getStatus();
|
|
359
|
+
if (recoveryStatus.phase === 'paused') {
|
|
360
|
+
this.stats.messagesBlocked++;
|
|
361
|
+
return {
|
|
362
|
+
allowed: false,
|
|
363
|
+
delayMs: recoveryStatus.pauseRemainingMs || 0,
|
|
364
|
+
reason: `Ban recovery: ${recoveryStatus.recommendation}`,
|
|
365
|
+
health: healthStatus,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
// Timelock guard (allows existing chats, blocks new contacts)
|
|
369
|
+
const timelockDecision = this.timelockGuard.canSend(recipient);
|
|
370
|
+
if (!timelockDecision.allowed) {
|
|
371
|
+
this.stats.messagesBlocked++;
|
|
372
|
+
if (this.logging) {
|
|
373
|
+
console.log(`[baileys-antiban] TIMELOCKED — ${timelockDecision.reason}`);
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
allowed: false,
|
|
377
|
+
delayMs: 0,
|
|
378
|
+
reason: timelockDecision.reason,
|
|
379
|
+
health: healthStatus,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
// Warm-up limit check
|
|
383
|
+
if (!this.warmUp.canSend()) {
|
|
384
|
+
this.stats.messagesBlocked++;
|
|
385
|
+
const warmUpStatus = this.warmUp.getStatus();
|
|
386
|
+
if (this.logging) {
|
|
387
|
+
console.log(`[baileys-antiban] ⏳ BLOCKED — warm-up day ${warmUpStatus.day}/${warmUpStatus.totalDays}, limit reached (${warmUpStatus.todaySent}/${warmUpStatus.todayLimit})`);
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
allowed: false,
|
|
391
|
+
delayMs: 0,
|
|
392
|
+
reason: `Warm-up limit: ${warmUpStatus.todaySent}/${warmUpStatus.todayLimit} messages today (day ${warmUpStatus.day})`,
|
|
393
|
+
health: healthStatus,
|
|
394
|
+
warmUpDay: warmUpStatus.day,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
// Contact graph check
|
|
398
|
+
const contactGraphDecision = this.contactGraphWarmer.canMessage(recipient);
|
|
399
|
+
if (!contactGraphDecision.allowed) {
|
|
400
|
+
this.stats.messagesBlocked++;
|
|
401
|
+
if (this.logging) {
|
|
402
|
+
console.log(`[baileys-antiban] 📊 BLOCKED — contact graph: ${contactGraphDecision.reason}`);
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
allowed: false,
|
|
406
|
+
delayMs: 0,
|
|
407
|
+
reason: `Contact graph: ${contactGraphDecision.reason}`,
|
|
408
|
+
health: healthStatus,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
// Topology throttler check — only applies to DMs to new/unknown contacts
|
|
412
|
+
if (this.topologyThrottlerModule && !this.isGroupJid(recipient)) {
|
|
413
|
+
const knownChats = this.rateLimiter.getKnownChats();
|
|
414
|
+
const isNewContact = !knownChats.has(recipient);
|
|
415
|
+
if (isNewContact) {
|
|
416
|
+
// Check if we can send to new contact based on topology limits
|
|
417
|
+
const topologyDecision = this.topologyThrottlerModule.canSendToNewContact();
|
|
418
|
+
if (!topologyDecision.allowed) {
|
|
419
|
+
this.stats.messagesBlocked++;
|
|
420
|
+
if (this.logging) {
|
|
421
|
+
console.log(`[baileys-antiban] 🌐 BLOCKED — topology: ${topologyDecision.reason}`);
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
allowed: false,
|
|
425
|
+
delayMs: topologyDecision.retryAfterMs || 0,
|
|
426
|
+
reason: `Topology: ${topologyDecision.reason}`,
|
|
427
|
+
health: healthStatus,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
// Assess contact risk
|
|
431
|
+
const riskAssessment = this.topologyThrottlerModule.assessContact(recipient, {
|
|
432
|
+
messageType: 'dm',
|
|
433
|
+
hasReplied: false,
|
|
434
|
+
knownGroups: [],
|
|
435
|
+
});
|
|
436
|
+
if (riskAssessment.recommendation === 'abort') {
|
|
437
|
+
this.stats.messagesBlocked++;
|
|
438
|
+
if (this.logging) {
|
|
439
|
+
console.log(`[baileys-antiban] ⛔ BLOCKED — contact risk ${riskAssessment.risk} (score ${riskAssessment.score}): ${riskAssessment.reasons.join(', ')}`);
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
allowed: false,
|
|
443
|
+
delayMs: 0,
|
|
444
|
+
reason: `Contact risk too high: ${riskAssessment.reasons.join(', ')}`,
|
|
445
|
+
health: healthStatus,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
// If delay recommended, we'll add it to the total delay later
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// Reply ratio check
|
|
452
|
+
const replyRatioDecision = this.replyRatioGuard.beforeSend(recipient);
|
|
453
|
+
if (!replyRatioDecision.allowed) {
|
|
454
|
+
this.stats.messagesBlocked++;
|
|
455
|
+
if (this.logging) {
|
|
456
|
+
console.log(`[baileys-antiban] 💬 BLOCKED — reply ratio: ${replyRatioDecision.reason}`);
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
allowed: false,
|
|
460
|
+
delayMs: 0,
|
|
461
|
+
reason: `Reply ratio: ${replyRatioDecision.reason}`,
|
|
462
|
+
health: healthStatus,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
// Reconnect throttle check
|
|
466
|
+
const reconnectThrottleDecision = this.reconnectThrottleModule.beforeSend();
|
|
467
|
+
if (!reconnectThrottleDecision.allowed) {
|
|
468
|
+
this.stats.messagesBlocked++;
|
|
469
|
+
if (this.logging) {
|
|
470
|
+
console.log(`[baileys-antiban] 🔄 BLOCKED — reconnect throttle: ${reconnectThrottleDecision.reason}`);
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
allowed: false,
|
|
474
|
+
delayMs: reconnectThrottleDecision.retryAfterMs || 0,
|
|
475
|
+
reason: reconnectThrottleDecision.reason || 'Post-reconnect throttle',
|
|
476
|
+
health: healthStatus,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
// Cross-instance coordination — check shared IP-level pool
|
|
480
|
+
if (this.instanceCoordinator) {
|
|
481
|
+
const slot = this.instanceCoordinator.tryAcquireSlot();
|
|
482
|
+
if (!slot.allowed) {
|
|
483
|
+
this.stats.messagesBlocked++;
|
|
484
|
+
if (this.logging) {
|
|
485
|
+
console.log(`[baileys-antiban] 🌐 BLOCKED — instance pool exhausted (shared IP limit), retry in ${slot.retryAfterMs}ms`);
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
allowed: false,
|
|
489
|
+
delayMs: slot.retryAfterMs || 5000,
|
|
490
|
+
reason: 'Cross-instance rate pool exhausted',
|
|
491
|
+
health: healthStatus,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// Group profile rate check (runs before rateLimiter.getDelay for timing)
|
|
496
|
+
if (this.resolvedConfig.groupProfiles && shouldUseGroupProfile(recipient)) {
|
|
497
|
+
const groupLimits = applyGroupMultiplier({
|
|
498
|
+
maxPerMinute: this.resolvedConfig.maxPerMinute,
|
|
499
|
+
maxPerHour: this.resolvedConfig.maxPerHour,
|
|
500
|
+
maxPerDay: this.resolvedConfig.maxPerDay,
|
|
501
|
+
}, this.resolvedConfig.groupMultiplier);
|
|
502
|
+
const stats = this.rateLimiter.getStats();
|
|
503
|
+
if (stats.lastMinute >= groupLimits.maxPerMinute ||
|
|
504
|
+
stats.lastHour >= groupLimits.maxPerHour ||
|
|
505
|
+
stats.lastDay >= groupLimits.maxPerDay) {
|
|
506
|
+
this.stats.messagesBlocked++;
|
|
507
|
+
if (this.logging) {
|
|
508
|
+
console.log(`[baileys-antiban] 🚫 BLOCKED — group rate limit exceeded for ${recipient}`);
|
|
509
|
+
}
|
|
510
|
+
return { allowed: false, delayMs: 0, reason: 'Group rate limit exceeded', health: healthStatus };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// Rate limiter delay
|
|
514
|
+
let delay = await this.rateLimiter.getDelay(recipient, content);
|
|
515
|
+
if (delay === -1) {
|
|
516
|
+
this.stats.messagesBlocked++;
|
|
517
|
+
if (this.logging) {
|
|
518
|
+
console.log(`[baileys-antiban] 🚫 BLOCKED — rate limit or identical message spam`);
|
|
519
|
+
}
|
|
520
|
+
return {
|
|
521
|
+
allowed: false,
|
|
522
|
+
delayMs: 0,
|
|
523
|
+
reason: 'Rate limit exceeded or identical message spam detected',
|
|
524
|
+
health: healthStatus,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
// Apply circadian rhythm multiplier to delay
|
|
528
|
+
const activityFactor = this.presenceChoreographer.getCurrentActivityFactor();
|
|
529
|
+
if (activityFactor < 1.0) {
|
|
530
|
+
// Lower activity = longer delays (cap at 5x)
|
|
531
|
+
const multiplier = Math.min(5, 1 / activityFactor);
|
|
532
|
+
delay = Math.floor(delay * multiplier);
|
|
533
|
+
}
|
|
534
|
+
// Per-contact risk multiplier — cold contacts need longer delays
|
|
535
|
+
// Only apply when contact graph is enabled, otherwise all contacts appear as 'stranger'
|
|
536
|
+
if (this.contactGraphWarmer['config']?.enabled) {
|
|
537
|
+
const contactState = this.contactGraphWarmer.getContactState(recipient);
|
|
538
|
+
const coldMultiplier = {
|
|
539
|
+
stranger: 2.5,
|
|
540
|
+
handshake_sent: 1.8,
|
|
541
|
+
handshake_complete: 1.3,
|
|
542
|
+
known: 1.0,
|
|
543
|
+
};
|
|
544
|
+
const contactRiskMult = coldMultiplier[contactState] ?? 1.0;
|
|
545
|
+
if (contactRiskMult > 1.0) {
|
|
546
|
+
delay = Math.floor(delay * contactRiskMult);
|
|
547
|
+
if (this.logging && contactRiskMult >= 2.0) {
|
|
548
|
+
console.log(`[baileys-antiban] ⚠️ Cold contact ${recipient} — ${contactState}, delay ×${contactRiskMult}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
// Topology throttler recommended delay
|
|
553
|
+
if (this.topologyThrottlerModule && !this.isGroupJid(recipient)) {
|
|
554
|
+
const knownChats = this.rateLimiter.getKnownChats();
|
|
555
|
+
const isNewContact = !knownChats.has(recipient);
|
|
556
|
+
if (isNewContact) {
|
|
557
|
+
const riskAssessment = this.topologyThrottlerModule.assessContact(recipient, {
|
|
558
|
+
messageType: 'dm',
|
|
559
|
+
hasReplied: false,
|
|
560
|
+
knownGroups: [],
|
|
561
|
+
});
|
|
562
|
+
if (riskAssessment.recommendation === 'delay' && riskAssessment.suggestedDelayMs) {
|
|
563
|
+
delay += riskAssessment.suggestedDelayMs;
|
|
564
|
+
if (this.logging) {
|
|
565
|
+
console.log(`[baileys-antiban] ⚠️ Topology risk ${riskAssessment.risk} — adding ${Math.floor(riskAssessment.suggestedDelayMs / 60000)}min delay`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
// Roll for distraction pause
|
|
571
|
+
const distractionCheck = this.presenceChoreographer.shouldPauseForDistraction();
|
|
572
|
+
if (distractionCheck.pause) {
|
|
573
|
+
delay += distractionCheck.durationMs;
|
|
574
|
+
if (this.logging) {
|
|
575
|
+
console.log(`[baileys-antiban] ⏸️ Distraction pause: +${Math.floor(distractionCheck.durationMs / 60000)}min`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// Roll for offline gap
|
|
579
|
+
const offlineCheck = this.presenceChoreographer.shouldTakeOfflineGap();
|
|
580
|
+
if (offlineCheck.offline) {
|
|
581
|
+
delay += offlineCheck.durationMs;
|
|
582
|
+
if (this.logging) {
|
|
583
|
+
console.log(`[baileys-antiban] 📴 Offline gap: +${Math.floor(offlineCheck.durationMs / 60000)}min`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
this.stats.totalDelayMs += delay;
|
|
587
|
+
return {
|
|
588
|
+
allowed: true,
|
|
589
|
+
delayMs: delay,
|
|
590
|
+
health: healthStatus,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Record a successfully sent message.
|
|
595
|
+
* Call this AFTER every successful sendMessage().
|
|
596
|
+
*/
|
|
597
|
+
afterSend(recipient, content, msgId) {
|
|
598
|
+
this.rateLimiter.record(recipient, content);
|
|
599
|
+
this.warmUp.record();
|
|
600
|
+
this.replyRatioGuard.recordSent(recipient);
|
|
601
|
+
this.topologyThrottlerModule?.recordSent(recipient);
|
|
602
|
+
this.stats.messagesAllowed++;
|
|
603
|
+
if (msgId) {
|
|
604
|
+
this.deliveryTracker.onMessageSent(msgId);
|
|
605
|
+
}
|
|
606
|
+
this.runAdaptiveCheck();
|
|
607
|
+
this.persistStateDebounced();
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Record a failed message send
|
|
611
|
+
*/
|
|
612
|
+
afterSendFailed(error) {
|
|
613
|
+
this.health.recordMessageFailed(error);
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Record a disconnection (call from connection.update handler)
|
|
617
|
+
*/
|
|
618
|
+
onDisconnect(reason) {
|
|
619
|
+
this.hasDisconnected = true; // BUG FIX 3: Mark that we've disconnected
|
|
620
|
+
this.health.recordDisconnect(reason);
|
|
621
|
+
this.reconnectThrottleModule.onDisconnect();
|
|
622
|
+
const reasonStr = String(reason);
|
|
623
|
+
if (reasonStr === '403' || reasonStr === '401' || reasonStr === 'forbidden' || reasonStr === 'loggedOut') {
|
|
624
|
+
this.persistStateImmediate();
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Record a successful reconnection
|
|
629
|
+
*/
|
|
630
|
+
onReconnect() {
|
|
631
|
+
this.health.recordReconnect();
|
|
632
|
+
this.reconnectThrottleModule.onReconnect();
|
|
633
|
+
// BUG FIX 3: Sync local rate limiter with shared pool after reconnect
|
|
634
|
+
// This prevents the double-spend window where DeafSessionDetector triggers
|
|
635
|
+
// reconnect → in-memory rate limiter resets to 0 → thinks it has full budget
|
|
636
|
+
// → sends 20 messages → THEN reads shared file and discovers 18/20 already used
|
|
637
|
+
// Only sync if this is a TRUE reconnect (after disconnect), not first connect
|
|
638
|
+
if (this.hasDisconnected && this.instanceCoordinator) {
|
|
639
|
+
this.instanceCoordinator.syncLocalLimiter(this.rateLimiter);
|
|
640
|
+
}
|
|
641
|
+
this.rateLimiter.adaptLimits(1.0);
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Handle incoming message — record in reply ratio + contact graph.
|
|
645
|
+
* Returns suggested reply if reply ratio suggests auto-reply.
|
|
646
|
+
*/
|
|
647
|
+
onIncomingMessage(jid, msgText) {
|
|
648
|
+
this.replyRatioGuard.recordReceived(jid);
|
|
649
|
+
this.contactGraphWarmer.onIncomingMessage(jid);
|
|
650
|
+
this.topologyThrottlerModule?.recordReplied(jid);
|
|
651
|
+
return this.replyRatioGuard.suggestReply(jid, msgText);
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Record a delivery receipt (status 3 = DELIVERY_ACK, status 4 = READ).
|
|
655
|
+
* Call from messages.update handler when delivery status is received.
|
|
656
|
+
*/
|
|
657
|
+
onDeliveryReceipt(msgId) {
|
|
658
|
+
this.deliveryTracker.onDeliveryReceipt(msgId);
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Get the resolved configuration
|
|
662
|
+
*/
|
|
663
|
+
getConfig() {
|
|
664
|
+
return { ...this.resolvedConfig };
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Get comprehensive stats
|
|
668
|
+
*/
|
|
669
|
+
getStats() {
|
|
670
|
+
const stats = {
|
|
671
|
+
...this.stats,
|
|
672
|
+
health: this.health.getStatus(),
|
|
673
|
+
warmUp: this.warmUp.getStatus(),
|
|
674
|
+
rateLimiter: this.rateLimiter.getStats(),
|
|
675
|
+
banRecovery: this.banRecovery.getStatus(),
|
|
676
|
+
deliveryTracker: this.deliveryTracker.getStats(),
|
|
677
|
+
};
|
|
678
|
+
// Only include new stats if enabled
|
|
679
|
+
if (this.replyRatioGuard['config']?.enabled) {
|
|
680
|
+
stats.replyRatio = this.replyRatioGuard.getStats();
|
|
681
|
+
}
|
|
682
|
+
if (this.contactGraphWarmer['config']?.enabled) {
|
|
683
|
+
stats.contactGraph = this.contactGraphWarmer.getStats();
|
|
684
|
+
}
|
|
685
|
+
if (this.presenceChoreographer['config']?.enabled) {
|
|
686
|
+
stats.presence = this.presenceChoreographer.getStats();
|
|
687
|
+
}
|
|
688
|
+
if (this.retryTrackerModule['config']?.enabled) {
|
|
689
|
+
stats.retryTracker = this.retryTrackerModule.getStats();
|
|
690
|
+
}
|
|
691
|
+
if (this.reconnectThrottleModule['config']?.enabled) {
|
|
692
|
+
stats.reconnectThrottle = this.reconnectThrottleModule.getStats();
|
|
693
|
+
}
|
|
694
|
+
if (this.topologyThrottlerModule) {
|
|
695
|
+
stats.topologyThrottler = this.topologyThrottlerModule.getTopologyStats();
|
|
696
|
+
}
|
|
697
|
+
if (this.lidResolverModule) {
|
|
698
|
+
stats.lidResolver = this.lidResolverModule.getStats();
|
|
699
|
+
}
|
|
700
|
+
if (this.jidCanonicalizerModule) {
|
|
701
|
+
stats.jidCanonicalizer = this.jidCanonicalizerModule.getStats();
|
|
702
|
+
}
|
|
703
|
+
if (this.sessionStabilityMonitor) {
|
|
704
|
+
stats.sessionStability = this.sessionStabilityMonitor.getStats();
|
|
705
|
+
}
|
|
706
|
+
if (this.instanceCoordinator) {
|
|
707
|
+
stats.instanceCoordinator = this.instanceCoordinator.getStats();
|
|
708
|
+
}
|
|
709
|
+
if (this.messageTypeRegistry) {
|
|
710
|
+
const warnings = this.messageTypeRegistry.getWarnings();
|
|
711
|
+
stats.messageRegistry = {
|
|
712
|
+
typeCount: Array.from(this.messageTypeRegistry.types.keys()).length,
|
|
713
|
+
warningCount: warnings.length,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
return stats;
|
|
717
|
+
}
|
|
718
|
+
/** Get the timelock guard for direct access */
|
|
719
|
+
get timelock() {
|
|
720
|
+
return this.timelockGuard;
|
|
721
|
+
}
|
|
722
|
+
/** Get the reply ratio guard for direct access */
|
|
723
|
+
get replyRatio() {
|
|
724
|
+
return this.replyRatioGuard;
|
|
725
|
+
}
|
|
726
|
+
/** Get the contact graph warmer for direct access */
|
|
727
|
+
get contactGraph() {
|
|
728
|
+
return this.contactGraphWarmer;
|
|
729
|
+
}
|
|
730
|
+
/** Get the presence choreographer for direct access */
|
|
731
|
+
get presence() {
|
|
732
|
+
return this.presenceChoreographer;
|
|
733
|
+
}
|
|
734
|
+
/** Get the retry tracker for direct access */
|
|
735
|
+
get retryTracker() {
|
|
736
|
+
return this.retryTrackerModule;
|
|
737
|
+
}
|
|
738
|
+
/** Get the reconnect throttle for direct access */
|
|
739
|
+
get reconnectThrottle() {
|
|
740
|
+
return this.reconnectThrottleModule;
|
|
741
|
+
}
|
|
742
|
+
/** Get the topology throttler for direct access */
|
|
743
|
+
get topologyThrottler() {
|
|
744
|
+
return this.topologyThrottlerModule;
|
|
745
|
+
}
|
|
746
|
+
/** Get the topology throttler for direct access (alias) */
|
|
747
|
+
get topology() {
|
|
748
|
+
return this.topologyThrottlerModule;
|
|
749
|
+
}
|
|
750
|
+
/** Get the LID resolver for direct access */
|
|
751
|
+
get lidResolver() {
|
|
752
|
+
return this.lidResolverModule;
|
|
753
|
+
}
|
|
754
|
+
/** Get the JID canonicalizer for direct access */
|
|
755
|
+
get jidCanonicalizer() {
|
|
756
|
+
return this.jidCanonicalizerModule;
|
|
757
|
+
}
|
|
758
|
+
/** Get the session stability monitor for direct access */
|
|
759
|
+
get sessionStability() {
|
|
760
|
+
return this.sessionStabilityMonitor;
|
|
761
|
+
}
|
|
762
|
+
/** Get the ban recovery orchestrator for direct access */
|
|
763
|
+
get recoveryOrchestrator() {
|
|
764
|
+
return this.banRecovery;
|
|
765
|
+
}
|
|
766
|
+
/** Get the message type registry for direct access */
|
|
767
|
+
get messageRegistry() {
|
|
768
|
+
return this.messageTypeRegistry;
|
|
769
|
+
}
|
|
770
|
+
/** Get the JID circuit breaker for direct access (BUG FIX 2) */
|
|
771
|
+
get circuitBreaker() {
|
|
772
|
+
return this.jidCircuitBreakerModule;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Export warm-up state for persistence between restarts
|
|
776
|
+
*/
|
|
777
|
+
exportWarmUpState() {
|
|
778
|
+
return this.warmUp.exportState();
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Force pause all sending
|
|
782
|
+
*/
|
|
783
|
+
pause() {
|
|
784
|
+
this.health.setPaused(true);
|
|
785
|
+
if (this.logging) {
|
|
786
|
+
console.log('[baileys-antiban] ⏸️ Sending paused manually');
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Resume sending
|
|
791
|
+
*/
|
|
792
|
+
resume() {
|
|
793
|
+
this.health.setPaused(false);
|
|
794
|
+
if (this.logging) {
|
|
795
|
+
console.log('[baileys-antiban] ▶️ Sending resumed');
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Reset everything (use after a ban period)
|
|
800
|
+
*/
|
|
801
|
+
reset() {
|
|
802
|
+
this.timelockGuard.reset();
|
|
803
|
+
this.health.reset();
|
|
804
|
+
this.warmUp.reset();
|
|
805
|
+
this.replyRatioGuard.reset();
|
|
806
|
+
this.contactGraphWarmer.reset();
|
|
807
|
+
this.presenceChoreographer.reset();
|
|
808
|
+
this.retryTrackerModule.destroy();
|
|
809
|
+
this.reconnectThrottleModule.destroy();
|
|
810
|
+
this.stats = { messagesAllowed: 0, messagesBlocked: 0, totalDelayMs: 0 };
|
|
811
|
+
if (this.logging) {
|
|
812
|
+
console.log('[baileys-antiban] 🔄 Reset — starting fresh warm-up');
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
isGroupJid(jid) {
|
|
816
|
+
return jid.endsWith('@g.us') || jid.endsWith('@newsletter');
|
|
817
|
+
}
|
|
818
|
+
runAdaptiveCheck() {
|
|
819
|
+
const delivery = this.deliveryTracker.getStats();
|
|
820
|
+
// Need min sample to be meaningful
|
|
821
|
+
if (delivery.deliveryRate === null)
|
|
822
|
+
return;
|
|
823
|
+
const rate = delivery.deliveryRate;
|
|
824
|
+
let targetFactor;
|
|
825
|
+
if (rate >= 0.85) {
|
|
826
|
+
targetFactor = 1.0; // Excellent — full speed
|
|
827
|
+
}
|
|
828
|
+
else if (rate >= 0.70) {
|
|
829
|
+
targetFactor = 0.75; // Good — slight reduction
|
|
830
|
+
}
|
|
831
|
+
else if (rate >= 0.55) {
|
|
832
|
+
targetFactor = 0.50; // Poor — halve throughput
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
targetFactor = 0.25; // Bad — severe throttle (soft ban likely)
|
|
836
|
+
}
|
|
837
|
+
const current = this.rateLimiter.getCurrentFactor();
|
|
838
|
+
// Only log + adjust when factor changes meaningfully (>5% delta)
|
|
839
|
+
if (Math.abs(targetFactor - current) > 0.05) {
|
|
840
|
+
if (this.logging) {
|
|
841
|
+
const dir = targetFactor > current ? '📈' : '📉';
|
|
842
|
+
console.log(`[baileys-antiban] ${dir} Adaptive rate: delivery=${(rate * 100).toFixed(0)}% → factor ${current.toFixed(2)}→${targetFactor.toFixed(2)}`);
|
|
843
|
+
}
|
|
844
|
+
this.rateLimiter.adaptLimits(targetFactor);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
persistStateDebounced() {
|
|
848
|
+
if (!this.stateManager)
|
|
849
|
+
return;
|
|
850
|
+
const state = {
|
|
851
|
+
warmup: this.warmUp.exportState(),
|
|
852
|
+
knownChats: Array.from(this.rateLimiter.getKnownChats()),
|
|
853
|
+
savedAt: Date.now(),
|
|
854
|
+
version: 3,
|
|
855
|
+
};
|
|
856
|
+
this.stateManager.saveDebounced(state);
|
|
857
|
+
}
|
|
858
|
+
persistStateImmediate() {
|
|
859
|
+
if (!this.stateManager)
|
|
860
|
+
return;
|
|
861
|
+
const state = {
|
|
862
|
+
warmup: this.warmUp.exportState(),
|
|
863
|
+
knownChats: Array.from(this.rateLimiter.getKnownChats()),
|
|
864
|
+
savedAt: Date.now(),
|
|
865
|
+
version: 3,
|
|
866
|
+
};
|
|
867
|
+
this.stateManager.saveImmediate(state);
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Export unified state snapshot for Redis failover or cross-instance migration.
|
|
871
|
+
* Returns snapshot of all module states (warmup, health, rate limiter, circuits, etc.)
|
|
872
|
+
*/
|
|
873
|
+
exportState() {
|
|
874
|
+
return exportAntibanState({
|
|
875
|
+
warmup: this.warmUp,
|
|
876
|
+
health: this.health,
|
|
877
|
+
rateLimiter: this.rateLimiter,
|
|
878
|
+
timelockGuard: this.timelockGuard,
|
|
879
|
+
messageRegistry: this.messageTypeRegistry || undefined,
|
|
880
|
+
topologyThrottler: this.topologyThrottlerModule || undefined,
|
|
881
|
+
reputationVoucher: this.reputationVoucher || undefined,
|
|
882
|
+
circuits: this.jidCircuitBreakerModule || undefined, // BUG FIX 2
|
|
883
|
+
instanceId: this.resolvedConfig.instanceId,
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Import unified state snapshot.
|
|
888
|
+
* CRDT-safe for rate limiters (never overwrites higher counts).
|
|
889
|
+
*/
|
|
890
|
+
importState(snapshot) {
|
|
891
|
+
importAntibanState(snapshot, {
|
|
892
|
+
warmup: this.warmUp,
|
|
893
|
+
health: this.health,
|
|
894
|
+
rateLimiter: this.rateLimiter,
|
|
895
|
+
timelockGuard: this.timelockGuard,
|
|
896
|
+
messageRegistry: this.messageTypeRegistry || undefined,
|
|
897
|
+
topologyThrottler: this.topologyThrottlerModule || undefined,
|
|
898
|
+
reputationVoucher: this.reputationVoucher || undefined,
|
|
899
|
+
circuits: this.jidCircuitBreakerModule || undefined, // BUG FIX 2
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Clean up all timers and resources.
|
|
904
|
+
* Call this when disposing of the AntiBan instance or when the socket closes.
|
|
905
|
+
*/
|
|
906
|
+
destroy() {
|
|
907
|
+
this.stateManager?.destroy();
|
|
908
|
+
this.timelockGuard.reset(); // Clears the resumeTimer
|
|
909
|
+
this.replyRatioGuard.reset();
|
|
910
|
+
this.contactGraphWarmer.reset();
|
|
911
|
+
this.presenceChoreographer.reset();
|
|
912
|
+
this.retryTrackerModule.destroy();
|
|
913
|
+
this.reconnectThrottleModule.destroy();
|
|
914
|
+
this.jidCanonicalizerModule?.destroy();
|
|
915
|
+
this.lidResolverModule?.destroy();
|
|
916
|
+
this.sessionStabilityMonitor?.reset();
|
|
917
|
+
this.messageTypeRegistry?.cleanup();
|
|
918
|
+
if (this.logging) {
|
|
919
|
+
console.log('[baileys-antiban] 🧹 Destroyed — all timers cleared');
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|