baileys-antiban 3.8.4 → 3.8.6
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/CHANGELOG.md +11 -0
- package/dist/cjs/antiban.js +578 -0
- package/dist/cjs/cli.js +160 -0
- package/dist/cjs/contactGraph.js +240 -0
- package/dist/cjs/contentVariator.js +154 -0
- package/dist/cjs/credsSnapshot.js +157 -0
- package/dist/cjs/deviceFingerprint.js +110 -0
- package/dist/cjs/health.js +211 -0
- package/dist/cjs/index.js +121 -0
- package/dist/cjs/jidCanonicalizer.js +260 -0
- package/dist/cjs/lidFirstResolver.js +212 -0
- package/dist/cjs/lidResolver.js +328 -0
- package/dist/cjs/messageQueue.js +191 -0
- package/dist/cjs/messageRecovery.js +335 -0
- package/dist/cjs/observability.js +151 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/persist.js +116 -0
- package/dist/cjs/presenceChoreographer.js +435 -0
- package/dist/cjs/presets.js +71 -0
- package/dist/cjs/profiles.js +38 -0
- package/dist/cjs/proxyRotator.js +310 -0
- package/dist/cjs/rateLimiter.js +202 -0
- package/dist/cjs/readReceiptVariance.js +91 -0
- package/dist/cjs/reconnectThrottle.js +184 -0
- package/dist/cjs/replyRatio.js +165 -0
- package/dist/cjs/retryReason.js +97 -0
- package/dist/cjs/retryTracker.js +176 -0
- package/dist/cjs/scheduler.js +115 -0
- package/dist/cjs/sessionFingerprint.js +258 -0
- package/dist/cjs/sessionStability.js +337 -0
- package/dist/cjs/stateAdapter.js +110 -0
- package/dist/cjs/stealthConnect.js +136 -0
- package/dist/cjs/timelockGuard.js +185 -0
- package/dist/cjs/warmup.js +113 -0
- package/dist/cjs/webhooks.js +84 -0
- package/dist/cjs/wrapper.js +278 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/observability.d.ts +85 -0
- package/dist/observability.js +145 -0
- package/dist/proxyRotator.js +18 -6
- package/package.json +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.8.5] - 2026-05-09
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Observability module** (`src/observability.ts`) — Prometheus metrics export and pluggable structured logging.
|
|
12
|
+
- `exportPrometheusMetrics(stats, labels?)` — exports 27 metrics (3 counters, 24 gauges) in Prometheus text exposition format v0.0.4. Covers health score/risk, warmup progress, rate limiter windows, known chats, reply ratio, contact graph, retry spirals, reconnect throttle. Accepts custom labels (`instance`, `region`, etc.).
|
|
13
|
+
- `createMetricsHandler(getStats, labels?)` — returns Express/Fastify-compatible `handle(req, res)` + `text()` helpers for a `/metrics` endpoint.
|
|
14
|
+
- `createPeriodicExporter(getStats, config)` — push-based exporter that calls `onMetrics(text)` on a configurable interval (default 30s). Returns `stop()` handle.
|
|
15
|
+
- `createConsoleLogger(prefix?)` — structured console logger compatible with winston/pino interface (`debug`, `info`, `warn`, `error` with ISO timestamps and JSON meta).
|
|
16
|
+
- `AntiBanLogger` interface — plug in any logger: `winston`, `pino`, or the built-in console logger.
|
|
17
|
+
- New exports: `createConsoleLogger`, `exportPrometheusMetrics`, `createMetricsHandler`, `createPeriodicExporter`, `AntiBanLogger`, `PeriodicExporterConfig`, `PeriodicExporterHandle`.
|
|
18
|
+
|
|
8
19
|
## [3.8.4] - 2026-05-09
|
|
9
20
|
|
|
10
21
|
### Added
|
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AntiBan — Main orchestrator combining rate limiting, warm-up, and health monitoring
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* import { AntiBan } from 'baileys-antiban';
|
|
7
|
+
* const antiban = new AntiBan();
|
|
8
|
+
*
|
|
9
|
+
* // Before sending a message:
|
|
10
|
+
* const result = await antiban.beforeSend(recipient, content);
|
|
11
|
+
* if (result.allowed) {
|
|
12
|
+
* await new Promise(r => setTimeout(r, result.delayMs));
|
|
13
|
+
* await sock.sendMessage(recipient, { text: content });
|
|
14
|
+
* antiban.afterSend(recipient, content);
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.AntiBan = void 0;
|
|
19
|
+
const rateLimiter_js_1 = require("./rateLimiter.js");
|
|
20
|
+
const warmup_js_1 = require("./warmup.js");
|
|
21
|
+
const health_js_1 = require("./health.js");
|
|
22
|
+
const timelockGuard_js_1 = require("./timelockGuard.js");
|
|
23
|
+
const replyRatio_js_1 = require("./replyRatio.js");
|
|
24
|
+
const contactGraph_js_1 = require("./contactGraph.js");
|
|
25
|
+
const presenceChoreographer_js_1 = require("./presenceChoreographer.js");
|
|
26
|
+
const retryTracker_js_1 = require("./retryTracker.js");
|
|
27
|
+
const reconnectThrottle_js_1 = require("./reconnectThrottle.js");
|
|
28
|
+
const lidResolver_js_1 = require("./lidResolver.js");
|
|
29
|
+
const jidCanonicalizer_js_1 = require("./jidCanonicalizer.js");
|
|
30
|
+
const sessionStability_js_1 = require("./sessionStability.js");
|
|
31
|
+
const presets_js_1 = require("./presets.js");
|
|
32
|
+
const persist_js_1 = require("./persist.js");
|
|
33
|
+
const profiles_js_1 = require("./profiles.js");
|
|
34
|
+
function isLegacyConfig(cfg) {
|
|
35
|
+
if (typeof cfg !== 'object' || cfg === null)
|
|
36
|
+
return false;
|
|
37
|
+
return 'rateLimiter' in cfg || 'warmUp' in cfg || 'health' in cfg || 'timelock' in cfg ||
|
|
38
|
+
'replyRatio' in cfg || 'contactGraph' in cfg || 'presence' in cfg || 'retryTracker' in cfg ||
|
|
39
|
+
'reconnectThrottle' in cfg || 'lidResolver' in cfg || 'jidCanonicalizer' in cfg ||
|
|
40
|
+
'sessionStability' in cfg;
|
|
41
|
+
}
|
|
42
|
+
function mapLegacyToFlat(legacy) {
|
|
43
|
+
console.warn('[baileys-antiban] DEPRECATED: Nested config (v2 style) detected. ' +
|
|
44
|
+
'Migrate to flat config: new AntiBan({ maxPerMinute: 8 }). ' +
|
|
45
|
+
'See: https://github.com/kobie3717/baileys-antiban#migration');
|
|
46
|
+
const flat = {};
|
|
47
|
+
if (legacy.rateLimiter?.maxPerMinute !== undefined)
|
|
48
|
+
flat.maxPerMinute = legacy.rateLimiter.maxPerMinute;
|
|
49
|
+
if (legacy.rateLimiter?.maxPerHour !== undefined)
|
|
50
|
+
flat.maxPerHour = legacy.rateLimiter.maxPerHour;
|
|
51
|
+
if (legacy.rateLimiter?.maxPerDay !== undefined)
|
|
52
|
+
flat.maxPerDay = legacy.rateLimiter.maxPerDay;
|
|
53
|
+
if (legacy.rateLimiter?.minDelayMs !== undefined)
|
|
54
|
+
flat.minDelayMs = legacy.rateLimiter.minDelayMs;
|
|
55
|
+
if (legacy.rateLimiter?.maxDelayMs !== undefined)
|
|
56
|
+
flat.maxDelayMs = legacy.rateLimiter.maxDelayMs;
|
|
57
|
+
if (legacy.rateLimiter?.newChatDelayMs !== undefined)
|
|
58
|
+
flat.newChatDelayMs = legacy.rateLimiter.newChatDelayMs;
|
|
59
|
+
if (legacy.warmUp?.warmUpDays !== undefined)
|
|
60
|
+
flat.warmupDays = legacy.warmUp.warmUpDays;
|
|
61
|
+
if (legacy.warmUp?.day1Limit !== undefined)
|
|
62
|
+
flat.day1Limit = legacy.warmUp.day1Limit;
|
|
63
|
+
if (legacy.warmUp?.growthFactor !== undefined)
|
|
64
|
+
flat.growthFactor = legacy.warmUp.growthFactor;
|
|
65
|
+
if (legacy.logging !== undefined)
|
|
66
|
+
flat.logging = legacy.logging;
|
|
67
|
+
return flat;
|
|
68
|
+
}
|
|
69
|
+
class AntiBan {
|
|
70
|
+
rateLimiter;
|
|
71
|
+
warmUp;
|
|
72
|
+
health;
|
|
73
|
+
timelockGuard;
|
|
74
|
+
replyRatioGuard;
|
|
75
|
+
contactGraphWarmer;
|
|
76
|
+
presenceChoreographer;
|
|
77
|
+
retryTrackerModule;
|
|
78
|
+
reconnectThrottleModule;
|
|
79
|
+
lidResolverModule = null;
|
|
80
|
+
jidCanonicalizerModule = null;
|
|
81
|
+
sessionStabilityMonitor = null;
|
|
82
|
+
stateManager = null;
|
|
83
|
+
resolvedConfig;
|
|
84
|
+
logging;
|
|
85
|
+
stats = {
|
|
86
|
+
messagesAllowed: 0,
|
|
87
|
+
messagesBlocked: 0,
|
|
88
|
+
totalDelayMs: 0,
|
|
89
|
+
};
|
|
90
|
+
constructor(input, warmUpStateArg) {
|
|
91
|
+
let flatConfig;
|
|
92
|
+
let legacyPassthrough = null;
|
|
93
|
+
let warmUpState = warmUpStateArg;
|
|
94
|
+
if (isLegacyConfig(input)) {
|
|
95
|
+
legacyPassthrough = input;
|
|
96
|
+
flatConfig = mapLegacyToFlat(legacyPassthrough);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
flatConfig = {};
|
|
100
|
+
legacyPassthrough = null;
|
|
101
|
+
}
|
|
102
|
+
const cfg = isLegacyConfig(input)
|
|
103
|
+
? (0, presets_js_1.resolveConfig)(flatConfig)
|
|
104
|
+
: (0, presets_js_1.resolveConfig)(input);
|
|
105
|
+
this.resolvedConfig = cfg;
|
|
106
|
+
// Initialize persistence — load state before constructing modules
|
|
107
|
+
let savedState = null;
|
|
108
|
+
if (cfg.persist) {
|
|
109
|
+
this.stateManager = new persist_js_1.StateManager(cfg.persist);
|
|
110
|
+
savedState = this.stateManager.load();
|
|
111
|
+
if (savedState) {
|
|
112
|
+
warmUpState = savedState.warmup;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
this.logging = cfg.logging ?? true;
|
|
116
|
+
this.rateLimiter = new rateLimiter_js_1.RateLimiter({
|
|
117
|
+
maxPerMinute: cfg.maxPerMinute,
|
|
118
|
+
maxPerHour: cfg.maxPerHour,
|
|
119
|
+
maxPerDay: cfg.maxPerDay,
|
|
120
|
+
minDelayMs: cfg.minDelayMs,
|
|
121
|
+
maxDelayMs: cfg.maxDelayMs,
|
|
122
|
+
newChatDelayMs: cfg.newChatDelayMs,
|
|
123
|
+
...(legacyPassthrough?.rateLimiter || {}),
|
|
124
|
+
});
|
|
125
|
+
// Restore knownChats from persisted state after rateLimiter is constructed
|
|
126
|
+
if (savedState?.knownChats) {
|
|
127
|
+
this.rateLimiter.restoreKnownChats(savedState.knownChats);
|
|
128
|
+
}
|
|
129
|
+
this.warmUp = new warmup_js_1.WarmUp({
|
|
130
|
+
warmUpDays: cfg.warmupDays,
|
|
131
|
+
day1Limit: cfg.day1Limit,
|
|
132
|
+
growthFactor: cfg.growthFactor,
|
|
133
|
+
inactivityThresholdHours: cfg.inactivityThresholdHours,
|
|
134
|
+
...(legacyPassthrough?.warmUp || {}),
|
|
135
|
+
}, warmUpState);
|
|
136
|
+
this.health = new health_js_1.HealthMonitor({
|
|
137
|
+
autoPauseAt: cfg.autoPauseAt,
|
|
138
|
+
...(legacyPassthrough?.health || {}),
|
|
139
|
+
onRiskChange: (status) => {
|
|
140
|
+
if (this.logging) {
|
|
141
|
+
const emoji = { low: '🟢', medium: '🟡', high: '🟠', critical: '🔴' };
|
|
142
|
+
console.log(`[baileys-antiban] ${emoji[status.risk]} Risk level: ${status.risk.toUpperCase()} (score: ${status.score})`);
|
|
143
|
+
console.log(`[baileys-antiban] ${status.recommendation}`);
|
|
144
|
+
status.reasons.forEach(r => console.log(`[baileys-antiban] → ${r}`));
|
|
145
|
+
}
|
|
146
|
+
// Call original callback if present
|
|
147
|
+
legacyPassthrough?.health?.onRiskChange?.(status);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
this.timelockGuard = new timelockGuard_js_1.TimelockGuard({
|
|
151
|
+
...(legacyPassthrough?.timelock || {}),
|
|
152
|
+
onTimelockDetected: (state) => {
|
|
153
|
+
this.health.recordReachoutTimelock(state.enforcementType);
|
|
154
|
+
if (this.logging) {
|
|
155
|
+
console.log(`[baileys-antiban] REACHOUT TIMELOCKED — ${state.enforcementType || 'unknown'}, expires ${state.expiresAt?.toISOString() || 'unknown'}`);
|
|
156
|
+
}
|
|
157
|
+
legacyPassthrough?.timelock?.onTimelockDetected?.(state);
|
|
158
|
+
},
|
|
159
|
+
onTimelockLifted: (state) => {
|
|
160
|
+
if (this.logging) {
|
|
161
|
+
console.log(`[baileys-antiban] Timelock lifted — resuming new contact messages`);
|
|
162
|
+
}
|
|
163
|
+
legacyPassthrough?.timelock?.onTimelockLifted?.(state);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
this.replyRatioGuard = new replyRatio_js_1.ReplyRatioGuard(legacyPassthrough?.replyRatio);
|
|
167
|
+
this.contactGraphWarmer = new contactGraph_js_1.ContactGraphWarmer(legacyPassthrough?.contactGraph);
|
|
168
|
+
this.presenceChoreographer = new presenceChoreographer_js_1.PresenceChoreographer(legacyPassthrough?.presence);
|
|
169
|
+
this.retryTrackerModule = new retryTracker_js_1.RetryReasonTracker({
|
|
170
|
+
...(legacyPassthrough?.retryTracker || {}),
|
|
171
|
+
onSpiral: (msgId, reason) => {
|
|
172
|
+
if (this.logging) {
|
|
173
|
+
console.log(`[baileys-antiban] ⚠️ Message ${msgId} stuck in retry spiral (${reason})`);
|
|
174
|
+
}
|
|
175
|
+
legacyPassthrough?.retryTracker?.onSpiral?.(msgId, reason);
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
this.reconnectThrottleModule = new reconnectThrottle_js_1.PostReconnectThrottle({
|
|
179
|
+
...(legacyPassthrough?.reconnectThrottle || {}),
|
|
180
|
+
baselineRatePerMinute: () => this.rateLimiter.getStats().limits.perMinute,
|
|
181
|
+
});
|
|
182
|
+
// Initialize LID resolver and canonicalizer if configured
|
|
183
|
+
// If jidCanonicalizer is enabled but no resolver provided, create standalone resolver
|
|
184
|
+
if (legacyPassthrough?.jidCanonicalizer?.enabled) {
|
|
185
|
+
// Create or use provided resolver
|
|
186
|
+
if (legacyPassthrough.jidCanonicalizer.resolver) {
|
|
187
|
+
// User provided their own resolver
|
|
188
|
+
this.jidCanonicalizerModule = new jidCanonicalizer_js_1.JidCanonicalizer(legacyPassthrough.jidCanonicalizer);
|
|
189
|
+
this.lidResolverModule = legacyPassthrough.jidCanonicalizer.resolver;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
// Create new resolver using lidResolver config if provided
|
|
193
|
+
const resolverConfig = legacyPassthrough.lidResolver || legacyPassthrough.jidCanonicalizer.resolverConfig;
|
|
194
|
+
const resolver = new lidResolver_js_1.LidResolver(resolverConfig);
|
|
195
|
+
this.lidResolverModule = resolver;
|
|
196
|
+
this.jidCanonicalizerModule = new jidCanonicalizer_js_1.JidCanonicalizer({
|
|
197
|
+
...legacyPassthrough.jidCanonicalizer,
|
|
198
|
+
resolver,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else if (legacyPassthrough?.lidResolver) {
|
|
203
|
+
// Standalone resolver without canonicalizer
|
|
204
|
+
this.lidResolverModule = new lidResolver_js_1.LidResolver(legacyPassthrough.lidResolver);
|
|
205
|
+
}
|
|
206
|
+
// Initialize session stability monitor if enabled
|
|
207
|
+
if (legacyPassthrough?.sessionStability?.enabled) {
|
|
208
|
+
const healthConfig = {
|
|
209
|
+
badMacThreshold: legacyPassthrough.sessionStability.badMacThreshold,
|
|
210
|
+
badMacWindowMs: legacyPassthrough.sessionStability.badMacWindowMs,
|
|
211
|
+
onDegraded: (stats) => {
|
|
212
|
+
if (this.logging) {
|
|
213
|
+
console.log(`[baileys-antiban] 🔴 SESSION DEGRADED — Bad MAC rate: ${stats.badMacCount} in last ${legacyPassthrough?.sessionStability?.badMacWindowMs || 60000}ms`);
|
|
214
|
+
console.log(`[baileys-antiban] Consider restarting session or switching to LID-based canonical form`);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
onRecovered: () => {
|
|
218
|
+
if (this.logging) {
|
|
219
|
+
console.log(`[baileys-antiban] 🟢 SESSION RECOVERED — decrypt success rate improved`);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
this.sessionStabilityMonitor = new sessionStability_js_1.SessionHealthMonitor(healthConfig);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Check if a message can be sent and get required delay.
|
|
228
|
+
* Call this BEFORE every sendMessage().
|
|
229
|
+
*/
|
|
230
|
+
async beforeSend(recipient, content) {
|
|
231
|
+
const healthStatus = this.health.getStatus();
|
|
232
|
+
// Health monitor says stop
|
|
233
|
+
if (this.health.isPaused()) {
|
|
234
|
+
this.stats.messagesBlocked++;
|
|
235
|
+
if (this.logging) {
|
|
236
|
+
console.log(`[baileys-antiban] ⛔ BLOCKED — health risk too high (${healthStatus.risk})`);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
allowed: false,
|
|
240
|
+
delayMs: 0,
|
|
241
|
+
reason: `Health risk ${healthStatus.risk}: ${healthStatus.recommendation}`,
|
|
242
|
+
health: healthStatus,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
// Timelock guard (allows existing chats, blocks new contacts)
|
|
246
|
+
const timelockDecision = this.timelockGuard.canSend(recipient);
|
|
247
|
+
if (!timelockDecision.allowed) {
|
|
248
|
+
this.stats.messagesBlocked++;
|
|
249
|
+
if (this.logging) {
|
|
250
|
+
console.log(`[baileys-antiban] TIMELOCKED — ${timelockDecision.reason}`);
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
allowed: false,
|
|
254
|
+
delayMs: 0,
|
|
255
|
+
reason: timelockDecision.reason,
|
|
256
|
+
health: healthStatus,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
// Warm-up limit check
|
|
260
|
+
if (!this.warmUp.canSend()) {
|
|
261
|
+
this.stats.messagesBlocked++;
|
|
262
|
+
const warmUpStatus = this.warmUp.getStatus();
|
|
263
|
+
if (this.logging) {
|
|
264
|
+
console.log(`[baileys-antiban] ⏳ BLOCKED — warm-up day ${warmUpStatus.day}/${warmUpStatus.totalDays}, limit reached (${warmUpStatus.todaySent}/${warmUpStatus.todayLimit})`);
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
allowed: false,
|
|
268
|
+
delayMs: 0,
|
|
269
|
+
reason: `Warm-up limit: ${warmUpStatus.todaySent}/${warmUpStatus.todayLimit} messages today (day ${warmUpStatus.day})`,
|
|
270
|
+
health: healthStatus,
|
|
271
|
+
warmUpDay: warmUpStatus.day,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
// Contact graph check
|
|
275
|
+
const contactGraphDecision = this.contactGraphWarmer.canMessage(recipient);
|
|
276
|
+
if (!contactGraphDecision.allowed) {
|
|
277
|
+
this.stats.messagesBlocked++;
|
|
278
|
+
if (this.logging) {
|
|
279
|
+
console.log(`[baileys-antiban] 📊 BLOCKED — contact graph: ${contactGraphDecision.reason}`);
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
allowed: false,
|
|
283
|
+
delayMs: 0,
|
|
284
|
+
reason: `Contact graph: ${contactGraphDecision.reason}`,
|
|
285
|
+
health: healthStatus,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
// Reply ratio check
|
|
289
|
+
const replyRatioDecision = this.replyRatioGuard.beforeSend(recipient);
|
|
290
|
+
if (!replyRatioDecision.allowed) {
|
|
291
|
+
this.stats.messagesBlocked++;
|
|
292
|
+
if (this.logging) {
|
|
293
|
+
console.log(`[baileys-antiban] 💬 BLOCKED — reply ratio: ${replyRatioDecision.reason}`);
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
allowed: false,
|
|
297
|
+
delayMs: 0,
|
|
298
|
+
reason: `Reply ratio: ${replyRatioDecision.reason}`,
|
|
299
|
+
health: healthStatus,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
// Reconnect throttle check
|
|
303
|
+
const reconnectThrottleDecision = this.reconnectThrottleModule.beforeSend();
|
|
304
|
+
if (!reconnectThrottleDecision.allowed) {
|
|
305
|
+
this.stats.messagesBlocked++;
|
|
306
|
+
if (this.logging) {
|
|
307
|
+
console.log(`[baileys-antiban] 🔄 BLOCKED — reconnect throttle: ${reconnectThrottleDecision.reason}`);
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
allowed: false,
|
|
311
|
+
delayMs: reconnectThrottleDecision.retryAfterMs || 0,
|
|
312
|
+
reason: reconnectThrottleDecision.reason || 'Post-reconnect throttle',
|
|
313
|
+
health: healthStatus,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
// Group profile rate check (runs before rateLimiter.getDelay for timing)
|
|
317
|
+
if (this.resolvedConfig.groupProfiles && (0, profiles_js_1.shouldUseGroupProfile)(recipient)) {
|
|
318
|
+
const groupLimits = (0, profiles_js_1.applyGroupMultiplier)({
|
|
319
|
+
maxPerMinute: this.resolvedConfig.maxPerMinute,
|
|
320
|
+
maxPerHour: this.resolvedConfig.maxPerHour,
|
|
321
|
+
maxPerDay: this.resolvedConfig.maxPerDay,
|
|
322
|
+
}, this.resolvedConfig.groupMultiplier);
|
|
323
|
+
const stats = this.rateLimiter.getStats();
|
|
324
|
+
if (stats.lastMinute >= groupLimits.maxPerMinute ||
|
|
325
|
+
stats.lastHour >= groupLimits.maxPerHour ||
|
|
326
|
+
stats.lastDay >= groupLimits.maxPerDay) {
|
|
327
|
+
this.stats.messagesBlocked++;
|
|
328
|
+
if (this.logging) {
|
|
329
|
+
console.log(`[baileys-antiban] 🚫 BLOCKED — group rate limit exceeded for ${recipient}`);
|
|
330
|
+
}
|
|
331
|
+
return { allowed: false, delayMs: 0, reason: 'Group rate limit exceeded', health: healthStatus };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Rate limiter delay
|
|
335
|
+
let delay = await this.rateLimiter.getDelay(recipient, content);
|
|
336
|
+
if (delay === -1) {
|
|
337
|
+
this.stats.messagesBlocked++;
|
|
338
|
+
if (this.logging) {
|
|
339
|
+
console.log(`[baileys-antiban] 🚫 BLOCKED — rate limit or identical message spam`);
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
allowed: false,
|
|
343
|
+
delayMs: 0,
|
|
344
|
+
reason: 'Rate limit exceeded or identical message spam detected',
|
|
345
|
+
health: healthStatus,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
// Apply circadian rhythm multiplier to delay
|
|
349
|
+
const activityFactor = this.presenceChoreographer.getCurrentActivityFactor();
|
|
350
|
+
if (activityFactor < 1.0) {
|
|
351
|
+
// Lower activity = longer delays (cap at 5x)
|
|
352
|
+
const multiplier = Math.min(5, 1 / activityFactor);
|
|
353
|
+
delay = Math.floor(delay * multiplier);
|
|
354
|
+
}
|
|
355
|
+
// Roll for distraction pause
|
|
356
|
+
const distractionCheck = this.presenceChoreographer.shouldPauseForDistraction();
|
|
357
|
+
if (distractionCheck.pause) {
|
|
358
|
+
delay += distractionCheck.durationMs;
|
|
359
|
+
if (this.logging) {
|
|
360
|
+
console.log(`[baileys-antiban] ⏸️ Distraction pause: +${Math.floor(distractionCheck.durationMs / 60000)}min`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Roll for offline gap
|
|
364
|
+
const offlineCheck = this.presenceChoreographer.shouldTakeOfflineGap();
|
|
365
|
+
if (offlineCheck.offline) {
|
|
366
|
+
delay += offlineCheck.durationMs;
|
|
367
|
+
if (this.logging) {
|
|
368
|
+
console.log(`[baileys-antiban] 📴 Offline gap: +${Math.floor(offlineCheck.durationMs / 60000)}min`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
this.stats.totalDelayMs += delay;
|
|
372
|
+
return {
|
|
373
|
+
allowed: true,
|
|
374
|
+
delayMs: delay,
|
|
375
|
+
health: healthStatus,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Record a successfully sent message.
|
|
380
|
+
* Call this AFTER every successful sendMessage().
|
|
381
|
+
*/
|
|
382
|
+
afterSend(recipient, content) {
|
|
383
|
+
this.rateLimiter.record(recipient, content);
|
|
384
|
+
this.warmUp.record();
|
|
385
|
+
this.replyRatioGuard.recordSent(recipient);
|
|
386
|
+
this.stats.messagesAllowed++;
|
|
387
|
+
this.persistStateDebounced();
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Record a failed message send
|
|
391
|
+
*/
|
|
392
|
+
afterSendFailed(error) {
|
|
393
|
+
this.health.recordMessageFailed(error);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Record a disconnection (call from connection.update handler)
|
|
397
|
+
*/
|
|
398
|
+
onDisconnect(reason) {
|
|
399
|
+
this.health.recordDisconnect(reason);
|
|
400
|
+
this.reconnectThrottleModule.onDisconnect();
|
|
401
|
+
const reasonStr = String(reason);
|
|
402
|
+
if (reasonStr === '403' || reasonStr === '401' || reasonStr === 'forbidden' || reasonStr === 'loggedOut') {
|
|
403
|
+
this.persistStateImmediate();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Record a successful reconnection
|
|
408
|
+
*/
|
|
409
|
+
onReconnect() {
|
|
410
|
+
this.health.recordReconnect();
|
|
411
|
+
this.reconnectThrottleModule.onReconnect();
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Handle incoming message — record in reply ratio + contact graph.
|
|
415
|
+
* Returns suggested reply if reply ratio suggests auto-reply.
|
|
416
|
+
*/
|
|
417
|
+
onIncomingMessage(jid, msgText) {
|
|
418
|
+
this.replyRatioGuard.recordReceived(jid);
|
|
419
|
+
this.contactGraphWarmer.onIncomingMessage(jid);
|
|
420
|
+
return this.replyRatioGuard.suggestReply(jid, msgText);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Get comprehensive stats
|
|
424
|
+
*/
|
|
425
|
+
getStats() {
|
|
426
|
+
const stats = {
|
|
427
|
+
...this.stats,
|
|
428
|
+
health: this.health.getStatus(),
|
|
429
|
+
warmUp: this.warmUp.getStatus(),
|
|
430
|
+
rateLimiter: this.rateLimiter.getStats(),
|
|
431
|
+
};
|
|
432
|
+
// Only include new stats if enabled
|
|
433
|
+
if (this.replyRatioGuard['config']?.enabled) {
|
|
434
|
+
stats.replyRatio = this.replyRatioGuard.getStats();
|
|
435
|
+
}
|
|
436
|
+
if (this.contactGraphWarmer['config']?.enabled) {
|
|
437
|
+
stats.contactGraph = this.contactGraphWarmer.getStats();
|
|
438
|
+
}
|
|
439
|
+
if (this.presenceChoreographer['config']?.enabled) {
|
|
440
|
+
stats.presence = this.presenceChoreographer.getStats();
|
|
441
|
+
}
|
|
442
|
+
if (this.retryTrackerModule['config']?.enabled) {
|
|
443
|
+
stats.retryTracker = this.retryTrackerModule.getStats();
|
|
444
|
+
}
|
|
445
|
+
if (this.reconnectThrottleModule['config']?.enabled) {
|
|
446
|
+
stats.reconnectThrottle = this.reconnectThrottleModule.getStats();
|
|
447
|
+
}
|
|
448
|
+
if (this.lidResolverModule) {
|
|
449
|
+
stats.lidResolver = this.lidResolverModule.getStats();
|
|
450
|
+
}
|
|
451
|
+
if (this.jidCanonicalizerModule) {
|
|
452
|
+
stats.jidCanonicalizer = this.jidCanonicalizerModule.getStats();
|
|
453
|
+
}
|
|
454
|
+
if (this.sessionStabilityMonitor) {
|
|
455
|
+
stats.sessionStability = this.sessionStabilityMonitor.getStats();
|
|
456
|
+
}
|
|
457
|
+
return stats;
|
|
458
|
+
}
|
|
459
|
+
/** Get the timelock guard for direct access */
|
|
460
|
+
get timelock() {
|
|
461
|
+
return this.timelockGuard;
|
|
462
|
+
}
|
|
463
|
+
/** Get the reply ratio guard for direct access */
|
|
464
|
+
get replyRatio() {
|
|
465
|
+
return this.replyRatioGuard;
|
|
466
|
+
}
|
|
467
|
+
/** Get the contact graph warmer for direct access */
|
|
468
|
+
get contactGraph() {
|
|
469
|
+
return this.contactGraphWarmer;
|
|
470
|
+
}
|
|
471
|
+
/** Get the presence choreographer for direct access */
|
|
472
|
+
get presence() {
|
|
473
|
+
return this.presenceChoreographer;
|
|
474
|
+
}
|
|
475
|
+
/** Get the retry tracker for direct access */
|
|
476
|
+
get retryTracker() {
|
|
477
|
+
return this.retryTrackerModule;
|
|
478
|
+
}
|
|
479
|
+
/** Get the reconnect throttle for direct access */
|
|
480
|
+
get reconnectThrottle() {
|
|
481
|
+
return this.reconnectThrottleModule;
|
|
482
|
+
}
|
|
483
|
+
/** Get the LID resolver for direct access */
|
|
484
|
+
get lidResolver() {
|
|
485
|
+
return this.lidResolverModule;
|
|
486
|
+
}
|
|
487
|
+
/** Get the JID canonicalizer for direct access */
|
|
488
|
+
get jidCanonicalizer() {
|
|
489
|
+
return this.jidCanonicalizerModule;
|
|
490
|
+
}
|
|
491
|
+
/** Get the session stability monitor for direct access */
|
|
492
|
+
get sessionStability() {
|
|
493
|
+
return this.sessionStabilityMonitor;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Export warm-up state for persistence between restarts
|
|
497
|
+
*/
|
|
498
|
+
exportWarmUpState() {
|
|
499
|
+
return this.warmUp.exportState();
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Force pause all sending
|
|
503
|
+
*/
|
|
504
|
+
pause() {
|
|
505
|
+
this.health.setPaused(true);
|
|
506
|
+
if (this.logging) {
|
|
507
|
+
console.log('[baileys-antiban] ⏸️ Sending paused manually');
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Resume sending
|
|
512
|
+
*/
|
|
513
|
+
resume() {
|
|
514
|
+
this.health.setPaused(false);
|
|
515
|
+
if (this.logging) {
|
|
516
|
+
console.log('[baileys-antiban] ▶️ Sending resumed');
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Reset everything (use after a ban period)
|
|
521
|
+
*/
|
|
522
|
+
reset() {
|
|
523
|
+
this.timelockGuard.reset();
|
|
524
|
+
this.health.reset();
|
|
525
|
+
this.warmUp.reset();
|
|
526
|
+
this.replyRatioGuard.reset();
|
|
527
|
+
this.contactGraphWarmer.reset();
|
|
528
|
+
this.presenceChoreographer.reset();
|
|
529
|
+
this.retryTrackerModule.destroy();
|
|
530
|
+
this.reconnectThrottleModule.destroy();
|
|
531
|
+
this.stats = { messagesAllowed: 0, messagesBlocked: 0, totalDelayMs: 0 };
|
|
532
|
+
if (this.logging) {
|
|
533
|
+
console.log('[baileys-antiban] 🔄 Reset — starting fresh warm-up');
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
persistStateDebounced() {
|
|
537
|
+
if (!this.stateManager)
|
|
538
|
+
return;
|
|
539
|
+
const state = {
|
|
540
|
+
warmup: this.warmUp.exportState(),
|
|
541
|
+
knownChats: Array.from(this.rateLimiter.getKnownChats()),
|
|
542
|
+
savedAt: Date.now(),
|
|
543
|
+
version: 3,
|
|
544
|
+
};
|
|
545
|
+
this.stateManager.saveDebounced(state);
|
|
546
|
+
}
|
|
547
|
+
persistStateImmediate() {
|
|
548
|
+
if (!this.stateManager)
|
|
549
|
+
return;
|
|
550
|
+
const state = {
|
|
551
|
+
warmup: this.warmUp.exportState(),
|
|
552
|
+
knownChats: Array.from(this.rateLimiter.getKnownChats()),
|
|
553
|
+
savedAt: Date.now(),
|
|
554
|
+
version: 3,
|
|
555
|
+
};
|
|
556
|
+
this.stateManager.saveImmediate(state);
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Clean up all timers and resources.
|
|
560
|
+
* Call this when disposing of the AntiBan instance or when the socket closes.
|
|
561
|
+
*/
|
|
562
|
+
destroy() {
|
|
563
|
+
this.stateManager?.destroy();
|
|
564
|
+
this.timelockGuard.reset(); // Clears the resumeTimer
|
|
565
|
+
this.replyRatioGuard.reset();
|
|
566
|
+
this.contactGraphWarmer.reset();
|
|
567
|
+
this.presenceChoreographer.reset();
|
|
568
|
+
this.retryTrackerModule.destroy();
|
|
569
|
+
this.reconnectThrottleModule.destroy();
|
|
570
|
+
this.jidCanonicalizerModule?.destroy();
|
|
571
|
+
this.lidResolverModule?.destroy();
|
|
572
|
+
this.sessionStabilityMonitor?.reset();
|
|
573
|
+
if (this.logging) {
|
|
574
|
+
console.log('[baileys-antiban] 🧹 Destroyed — all timers cleared');
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
exports.AntiBan = AntiBan;
|