baileys-antiban 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +64 -3
- package/dist/antiban.d.ts +8 -8
- package/dist/antiban.js +157 -27
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +125 -0
- package/dist/health.d.ts +2 -0
- package/dist/health.js +20 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/persist.d.ts +28 -0
- package/dist/persist.js +79 -0
- package/dist/presets.d.ts +24 -0
- package/dist/presets.js +67 -0
- package/dist/profiles.d.ts +22 -0
- package/dist/profiles.js +31 -0
- package/dist/rateLimiter.d.ts +4 -0
- package/dist/rateLimiter.js +10 -0
- package/dist/warmup.d.ts +0 -2
- package/package.json +19 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ 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.0.0] — 2026-04-25
|
|
9
|
+
|
|
10
|
+
### Breaking Changes
|
|
11
|
+
- Constructor now accepts `string | FlatConfig | undefined` — nested v2 config still works but logs deprecation warning
|
|
12
|
+
- `WarmUpConfig.statePath` removed (use `persist` in AntiBanConfig instead)
|
|
13
|
+
|
|
14
|
+
### New Features
|
|
15
|
+
- **Zero-config:** `new AntiBan()` works with conservative defaults
|
|
16
|
+
- **Presets:** `conservative` / `moderate` / `aggressive`
|
|
17
|
+
- **State persistence:** `persist: './state.json'` — warmup + knownChats survive restarts
|
|
18
|
+
- **Group profiles:** `groupProfiles: true` — stricter rate limits for @g.us and @newsletter JIDs
|
|
19
|
+
- **Health decay:** Score recovers automatically (2pts/min severe, 5pts/min normal)
|
|
20
|
+
- **CLI:** `npx baileys-antiban status|reset|warmup`
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
- `statePath` in WarmUpConfig was declared but never implemented — replaced with working `persist` option
|
|
24
|
+
- Health score never recovered after ban signals — fixed with time-based decay
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
8
28
|
## [2.1.0] - 2026-04-19
|
|
9
29
|
|
|
10
30
|
### Added
|
package/README.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
# baileys-antiban
|
|
1
|
+
# baileys-antiban — Anti-Ban Middleware for Baileys & WhatsApp Bots
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/baileys-antiban)
|
|
4
4
|
[](https://nodejs.org/)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**Drop-in anti-ban middleware for Baileys WhatsApp bots. Free, self-hosted, TypeScript-first. Whapi.Cloud alternative — zero monthly fees.**
|
|
8
|
+
|
|
9
|
+
> Rate limiting with Gaussian jitter, 7-day warmup, session health monitoring, LID resolver, disconnect classification, contact graph enforcement — all in one `npm install`. Works with [Baileys](https://github.com/WhiskeySockets/Baileys) and [@oxidezap/baileyrs](https://github.com/oxidezap/baileyrs) (Rust/WASM).
|
|
8
10
|
|
|
9
11
|
## v2.0 New Features — Session Stability Module
|
|
10
12
|
|
|
@@ -323,6 +325,25 @@ const antiban = new AntiBan({
|
|
|
323
325
|
|
|
324
326
|
**Why these features?** 2025-2026 ban research showed WhatsApp's ML models heavily weight reply-ratio (<10% = high risk), contact-graph distance (strangers = high risk), and temporal patterns (robotic timing = high risk). These modules address the three largest gaps in existing anti-ban libraries.
|
|
325
327
|
|
|
328
|
+
## baileys-antiban vs Whapi.Cloud vs DIY rate limiting
|
|
329
|
+
|
|
330
|
+
| Feature | baileys-antiban | Whapi.Cloud | DIY snippets |
|
|
331
|
+
|---|---|---|---|
|
|
332
|
+
| Price | **Free, MIT** | $49–$99/mo | Free |
|
|
333
|
+
| WhatsApp API | Unofficial (Baileys) | Unofficial underneath | Unofficial (Baileys) |
|
|
334
|
+
| Rate limiting | ✅ Gaussian jitter | ✅ Black box | ⚠️ Basic only |
|
|
335
|
+
| Warmup schedule | ✅ 7-day ramp | ✅ Managed | ❌ None |
|
|
336
|
+
| Session health monitor | ✅ Built-in | ✅ Managed | ❌ None |
|
|
337
|
+
| LID/PN resolver | ✅ v2.0 | ❌ Unknown | ❌ None |
|
|
338
|
+
| Disconnect classifier | ✅ Typed reasons | ❌ None | ❌ None |
|
|
339
|
+
| Contact graph enforcement | ✅ v1.3 | ❌ None | ❌ None |
|
|
340
|
+
| Self-hosted | ✅ Yes | ❌ No | ✅ Yes |
|
|
341
|
+
| TypeScript | ✅ Full types | N/A | ❌ Rarely |
|
|
342
|
+
| Customisable | ✅ Full control | ❌ None | ⚠️ Copy-paste |
|
|
343
|
+
| Drop-in (existing bot) | ✅ One-line wrapper | ❌ Full migration | ❌ Rewrite |
|
|
344
|
+
|
|
345
|
+
**Bottom line:** Whapi.Cloud charges $99/mo for managed Baileys under the hood — same unofficial API, same ban risk, zero customisation. baileys-antiban gives you more protection, free, with full source access.
|
|
346
|
+
|
|
326
347
|
## Why?
|
|
327
348
|
|
|
328
349
|
WhatsApp bans numbers that behave like bots. This library makes your Baileys bot behave like a human:
|
|
@@ -364,7 +385,47 @@ npm install @oxidezap/baileyrs baileys-antiban
|
|
|
364
385
|
|
|
365
386
|
Requires Node.js ≥16.
|
|
366
387
|
|
|
367
|
-
## Quick Start
|
|
388
|
+
## Quick Start (v3)
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
npm install baileys-antiban
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
import { AntiBan } from 'baileys-antiban';
|
|
396
|
+
|
|
397
|
+
// Zero config — works immediately
|
|
398
|
+
const ab = new AntiBan();
|
|
399
|
+
|
|
400
|
+
// Or pick a preset
|
|
401
|
+
const ab = new AntiBan('moderate');
|
|
402
|
+
|
|
403
|
+
// Full control
|
|
404
|
+
const ab = new AntiBan({
|
|
405
|
+
preset: 'moderate',
|
|
406
|
+
persist: './antiban-state.json', // survives restarts
|
|
407
|
+
groupProfiles: true, // stricter limits for groups
|
|
408
|
+
maxPerMinute: 15, // override any value
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Usage unchanged
|
|
412
|
+
const result = await ab.beforeSend(jid, text);
|
|
413
|
+
if (result.allowed) {
|
|
414
|
+
await new Promise(r => setTimeout(r, result.delayMs));
|
|
415
|
+
await sock.sendMessage(jid, { text });
|
|
416
|
+
ab.afterSend(jid, text);
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### CLI
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
npx baileys-antiban status --state ./antiban-state.json
|
|
424
|
+
npx baileys-antiban warmup --simulate 7 --preset moderate
|
|
425
|
+
npx baileys-antiban reset --state ./antiban-state.json
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Quick Start (Legacy)
|
|
368
429
|
|
|
369
430
|
### Option 1: Wrap Your Socket (Easiest)
|
|
370
431
|
|
package/dist/antiban.d.ts
CHANGED
|
@@ -25,7 +25,8 @@ import { PostReconnectThrottle, type ReconnectThrottleConfig, type ReconnectThro
|
|
|
25
25
|
import { LidResolver, type LidResolverConfig, type LidResolverStats } from './lidResolver.js';
|
|
26
26
|
import { JidCanonicalizer, type JidCanonicalizerConfig, type JidCanonicalizerStats } from './jidCanonicalizer.js';
|
|
27
27
|
import { SessionHealthMonitor, type SessionHealthStats } from './sessionStability.js';
|
|
28
|
-
|
|
28
|
+
import { type AntiBanInput } from './presets.js';
|
|
29
|
+
export interface AntiBanConfigLegacy {
|
|
29
30
|
rateLimiter?: Partial<RateLimiterConfig>;
|
|
30
31
|
warmUp?: Partial<WarmUpConfig>;
|
|
31
32
|
health?: Partial<HealthMonitorConfig>;
|
|
@@ -37,21 +38,16 @@ export interface AntiBanConfig {
|
|
|
37
38
|
reconnectThrottle?: Partial<ReconnectThrottleConfig>;
|
|
38
39
|
lidResolver?: LidResolverConfig;
|
|
39
40
|
jidCanonicalizer?: JidCanonicalizerConfig;
|
|
40
|
-
/** Session stability features (v2.0) — default disabled for backward compatibility */
|
|
41
41
|
sessionStability?: {
|
|
42
42
|
enabled: boolean;
|
|
43
|
-
/** Enable canonical JID normalization before sendMessage (default: true if enabled) */
|
|
44
43
|
canonicalJidNormalization?: boolean;
|
|
45
|
-
/** Enable session health monitoring (default: true if enabled) */
|
|
46
44
|
healthMonitoring?: boolean;
|
|
47
|
-
/** Bad MAC threshold before declaring session degraded (default: 3) */
|
|
48
45
|
badMacThreshold?: number;
|
|
49
|
-
/** Time window for Bad MAC threshold in ms (default: 60000) */
|
|
50
46
|
badMacWindowMs?: number;
|
|
51
47
|
};
|
|
52
|
-
/** Log warnings and blocks to console (default: true) */
|
|
53
48
|
logging?: boolean;
|
|
54
49
|
}
|
|
50
|
+
export type AntiBanConfig = AntiBanInput | AntiBanConfigLegacy;
|
|
55
51
|
export interface SendDecision {
|
|
56
52
|
allowed: boolean;
|
|
57
53
|
delayMs: number;
|
|
@@ -88,9 +84,11 @@ export declare class AntiBan {
|
|
|
88
84
|
private lidResolverModule;
|
|
89
85
|
private jidCanonicalizerModule;
|
|
90
86
|
private sessionStabilityMonitor;
|
|
87
|
+
private stateManager;
|
|
88
|
+
private resolvedConfig;
|
|
91
89
|
private logging;
|
|
92
90
|
private stats;
|
|
93
|
-
constructor(
|
|
91
|
+
constructor(input?: AntiBanInput | AntiBanConfigLegacy, warmUpStateArg?: WarmUpState);
|
|
94
92
|
/**
|
|
95
93
|
* Check if a message can be sent and get required delay.
|
|
96
94
|
* Call this BEFORE every sendMessage().
|
|
@@ -159,6 +157,8 @@ export declare class AntiBan {
|
|
|
159
157
|
* Reset everything (use after a ban period)
|
|
160
158
|
*/
|
|
161
159
|
reset(): void;
|
|
160
|
+
private persistStateDebounced;
|
|
161
|
+
private persistStateImmediate;
|
|
162
162
|
/**
|
|
163
163
|
* Clean up all timers and resources.
|
|
164
164
|
* Call this when disposing of the AntiBan instance or when the socket closes.
|
package/dist/antiban.js
CHANGED
|
@@ -25,6 +25,44 @@ import { PostReconnectThrottle } from './reconnectThrottle.js';
|
|
|
25
25
|
import { LidResolver } from './lidResolver.js';
|
|
26
26
|
import { JidCanonicalizer } from './jidCanonicalizer.js';
|
|
27
27
|
import { SessionHealthMonitor } from './sessionStability.js';
|
|
28
|
+
import { resolveConfig } from './presets.js';
|
|
29
|
+
import { StateManager } from './persist.js';
|
|
30
|
+
import { shouldUseGroupProfile, applyGroupMultiplier } from './profiles.js';
|
|
31
|
+
function isLegacyConfig(cfg) {
|
|
32
|
+
if (typeof cfg !== 'object' || cfg === null)
|
|
33
|
+
return false;
|
|
34
|
+
return 'rateLimiter' in cfg || 'warmUp' in cfg || 'health' in cfg || 'timelock' in cfg ||
|
|
35
|
+
'replyRatio' in cfg || 'contactGraph' in cfg || 'presence' in cfg || 'retryTracker' in cfg ||
|
|
36
|
+
'reconnectThrottle' in cfg || 'lidResolver' in cfg || 'jidCanonicalizer' in cfg ||
|
|
37
|
+
'sessionStability' in cfg;
|
|
38
|
+
}
|
|
39
|
+
function mapLegacyToFlat(legacy) {
|
|
40
|
+
console.warn('[baileys-antiban] DEPRECATED: Nested config (v2 style) detected. ' +
|
|
41
|
+
'Migrate to flat config: new AntiBan({ maxPerMinute: 8 }). ' +
|
|
42
|
+
'See: https://github.com/kobie3717/baileys-antiban#migration');
|
|
43
|
+
const flat = {};
|
|
44
|
+
if (legacy.rateLimiter?.maxPerMinute !== undefined)
|
|
45
|
+
flat.maxPerMinute = legacy.rateLimiter.maxPerMinute;
|
|
46
|
+
if (legacy.rateLimiter?.maxPerHour !== undefined)
|
|
47
|
+
flat.maxPerHour = legacy.rateLimiter.maxPerHour;
|
|
48
|
+
if (legacy.rateLimiter?.maxPerDay !== undefined)
|
|
49
|
+
flat.maxPerDay = legacy.rateLimiter.maxPerDay;
|
|
50
|
+
if (legacy.rateLimiter?.minDelayMs !== undefined)
|
|
51
|
+
flat.minDelayMs = legacy.rateLimiter.minDelayMs;
|
|
52
|
+
if (legacy.rateLimiter?.maxDelayMs !== undefined)
|
|
53
|
+
flat.maxDelayMs = legacy.rateLimiter.maxDelayMs;
|
|
54
|
+
if (legacy.rateLimiter?.newChatDelayMs !== undefined)
|
|
55
|
+
flat.newChatDelayMs = legacy.rateLimiter.newChatDelayMs;
|
|
56
|
+
if (legacy.warmUp?.warmUpDays !== undefined)
|
|
57
|
+
flat.warmupDays = legacy.warmUp.warmUpDays;
|
|
58
|
+
if (legacy.warmUp?.day1Limit !== undefined)
|
|
59
|
+
flat.day1Limit = legacy.warmUp.day1Limit;
|
|
60
|
+
if (legacy.warmUp?.growthFactor !== undefined)
|
|
61
|
+
flat.growthFactor = legacy.warmUp.growthFactor;
|
|
62
|
+
if (legacy.logging !== undefined)
|
|
63
|
+
flat.logging = legacy.logging;
|
|
64
|
+
return flat;
|
|
65
|
+
}
|
|
28
66
|
export class AntiBan {
|
|
29
67
|
rateLimiter;
|
|
30
68
|
warmUp;
|
|
@@ -38,17 +76,63 @@ export class AntiBan {
|
|
|
38
76
|
lidResolverModule = null;
|
|
39
77
|
jidCanonicalizerModule = null;
|
|
40
78
|
sessionStabilityMonitor = null;
|
|
79
|
+
stateManager = null;
|
|
80
|
+
resolvedConfig;
|
|
41
81
|
logging;
|
|
42
82
|
stats = {
|
|
43
83
|
messagesAllowed: 0,
|
|
44
84
|
messagesBlocked: 0,
|
|
45
85
|
totalDelayMs: 0,
|
|
46
86
|
};
|
|
47
|
-
constructor(
|
|
48
|
-
|
|
49
|
-
|
|
87
|
+
constructor(input, warmUpStateArg) {
|
|
88
|
+
let flatConfig;
|
|
89
|
+
let legacyPassthrough = null;
|
|
90
|
+
let warmUpState = warmUpStateArg;
|
|
91
|
+
if (isLegacyConfig(input)) {
|
|
92
|
+
legacyPassthrough = input;
|
|
93
|
+
flatConfig = mapLegacyToFlat(legacyPassthrough);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
flatConfig = {};
|
|
97
|
+
legacyPassthrough = null;
|
|
98
|
+
}
|
|
99
|
+
const cfg = isLegacyConfig(input)
|
|
100
|
+
? resolveConfig(flatConfig)
|
|
101
|
+
: resolveConfig(input);
|
|
102
|
+
this.resolvedConfig = cfg;
|
|
103
|
+
// Initialize persistence — load state before constructing modules
|
|
104
|
+
let savedState = null;
|
|
105
|
+
if (cfg.persist) {
|
|
106
|
+
this.stateManager = new StateManager(cfg.persist);
|
|
107
|
+
savedState = this.stateManager.load();
|
|
108
|
+
if (savedState) {
|
|
109
|
+
warmUpState = savedState.warmup;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
this.logging = cfg.logging ?? true;
|
|
113
|
+
this.rateLimiter = new RateLimiter({
|
|
114
|
+
maxPerMinute: cfg.maxPerMinute,
|
|
115
|
+
maxPerHour: cfg.maxPerHour,
|
|
116
|
+
maxPerDay: cfg.maxPerDay,
|
|
117
|
+
minDelayMs: cfg.minDelayMs,
|
|
118
|
+
maxDelayMs: cfg.maxDelayMs,
|
|
119
|
+
newChatDelayMs: cfg.newChatDelayMs,
|
|
120
|
+
...(legacyPassthrough?.rateLimiter || {}),
|
|
121
|
+
});
|
|
122
|
+
// Restore knownChats from persisted state after rateLimiter is constructed
|
|
123
|
+
if (savedState?.knownChats) {
|
|
124
|
+
this.rateLimiter.restoreKnownChats(savedState.knownChats);
|
|
125
|
+
}
|
|
126
|
+
this.warmUp = new WarmUp({
|
|
127
|
+
warmUpDays: cfg.warmupDays,
|
|
128
|
+
day1Limit: cfg.day1Limit,
|
|
129
|
+
growthFactor: cfg.growthFactor,
|
|
130
|
+
inactivityThresholdHours: cfg.inactivityThresholdHours,
|
|
131
|
+
...(legacyPassthrough?.warmUp || {}),
|
|
132
|
+
}, warmUpState);
|
|
50
133
|
this.health = new HealthMonitor({
|
|
51
|
-
|
|
134
|
+
autoPauseAt: cfg.autoPauseAt,
|
|
135
|
+
...(legacyPassthrough?.health || {}),
|
|
52
136
|
onRiskChange: (status) => {
|
|
53
137
|
if (this.logging) {
|
|
54
138
|
const emoji = { low: '🟢', medium: '🟡', high: '🟠', critical: '🔴' };
|
|
@@ -56,74 +140,74 @@ export class AntiBan {
|
|
|
56
140
|
console.log(`[baileys-antiban] ${status.recommendation}`);
|
|
57
141
|
status.reasons.forEach(r => console.log(`[baileys-antiban] → ${r}`));
|
|
58
142
|
}
|
|
59
|
-
|
|
143
|
+
// Call original callback if present
|
|
144
|
+
legacyPassthrough?.health?.onRiskChange?.(status);
|
|
60
145
|
},
|
|
61
146
|
});
|
|
62
|
-
this.logging = config.logging ?? true;
|
|
63
147
|
this.timelockGuard = new TimelockGuard({
|
|
64
|
-
...
|
|
148
|
+
...(legacyPassthrough?.timelock || {}),
|
|
65
149
|
onTimelockDetected: (state) => {
|
|
66
150
|
this.health.recordReachoutTimelock(state.enforcementType);
|
|
67
151
|
if (this.logging) {
|
|
68
152
|
console.log(`[baileys-antiban] REACHOUT TIMELOCKED — ${state.enforcementType || 'unknown'}, expires ${state.expiresAt?.toISOString() || 'unknown'}`);
|
|
69
153
|
}
|
|
70
|
-
|
|
154
|
+
legacyPassthrough?.timelock?.onTimelockDetected?.(state);
|
|
71
155
|
},
|
|
72
156
|
onTimelockLifted: (state) => {
|
|
73
157
|
if (this.logging) {
|
|
74
158
|
console.log(`[baileys-antiban] Timelock lifted — resuming new contact messages`);
|
|
75
159
|
}
|
|
76
|
-
|
|
160
|
+
legacyPassthrough?.timelock?.onTimelockLifted?.(state);
|
|
77
161
|
},
|
|
78
162
|
});
|
|
79
|
-
this.replyRatioGuard = new ReplyRatioGuard(
|
|
80
|
-
this.contactGraphWarmer = new ContactGraphWarmer(
|
|
81
|
-
this.presenceChoreographer = new PresenceChoreographer(
|
|
163
|
+
this.replyRatioGuard = new ReplyRatioGuard(legacyPassthrough?.replyRatio);
|
|
164
|
+
this.contactGraphWarmer = new ContactGraphWarmer(legacyPassthrough?.contactGraph);
|
|
165
|
+
this.presenceChoreographer = new PresenceChoreographer(legacyPassthrough?.presence);
|
|
82
166
|
this.retryTrackerModule = new RetryReasonTracker({
|
|
83
|
-
...
|
|
167
|
+
...(legacyPassthrough?.retryTracker || {}),
|
|
84
168
|
onSpiral: (msgId, reason) => {
|
|
85
169
|
if (this.logging) {
|
|
86
170
|
console.log(`[baileys-antiban] ⚠️ Message ${msgId} stuck in retry spiral (${reason})`);
|
|
87
171
|
}
|
|
88
|
-
|
|
172
|
+
legacyPassthrough?.retryTracker?.onSpiral?.(msgId, reason);
|
|
89
173
|
},
|
|
90
174
|
});
|
|
91
175
|
this.reconnectThrottleModule = new PostReconnectThrottle({
|
|
92
|
-
...
|
|
176
|
+
...(legacyPassthrough?.reconnectThrottle || {}),
|
|
93
177
|
baselineRatePerMinute: () => this.rateLimiter.getStats().limits.perMinute,
|
|
94
178
|
});
|
|
95
179
|
// Initialize LID resolver and canonicalizer if configured
|
|
96
180
|
// If jidCanonicalizer is enabled but no resolver provided, create standalone resolver
|
|
97
|
-
if (
|
|
181
|
+
if (legacyPassthrough?.jidCanonicalizer?.enabled) {
|
|
98
182
|
// Create or use provided resolver
|
|
99
|
-
if (
|
|
183
|
+
if (legacyPassthrough.jidCanonicalizer.resolver) {
|
|
100
184
|
// User provided their own resolver
|
|
101
|
-
this.jidCanonicalizerModule = new JidCanonicalizer(
|
|
102
|
-
this.lidResolverModule =
|
|
185
|
+
this.jidCanonicalizerModule = new JidCanonicalizer(legacyPassthrough.jidCanonicalizer);
|
|
186
|
+
this.lidResolverModule = legacyPassthrough.jidCanonicalizer.resolver;
|
|
103
187
|
}
|
|
104
188
|
else {
|
|
105
189
|
// Create new resolver using lidResolver config if provided
|
|
106
|
-
const resolverConfig =
|
|
190
|
+
const resolverConfig = legacyPassthrough.lidResolver || legacyPassthrough.jidCanonicalizer.resolverConfig;
|
|
107
191
|
const resolver = new LidResolver(resolverConfig);
|
|
108
192
|
this.lidResolverModule = resolver;
|
|
109
193
|
this.jidCanonicalizerModule = new JidCanonicalizer({
|
|
110
|
-
...
|
|
194
|
+
...legacyPassthrough.jidCanonicalizer,
|
|
111
195
|
resolver,
|
|
112
196
|
});
|
|
113
197
|
}
|
|
114
198
|
}
|
|
115
|
-
else if (
|
|
199
|
+
else if (legacyPassthrough?.lidResolver) {
|
|
116
200
|
// Standalone resolver without canonicalizer
|
|
117
|
-
this.lidResolverModule = new LidResolver(
|
|
201
|
+
this.lidResolverModule = new LidResolver(legacyPassthrough.lidResolver);
|
|
118
202
|
}
|
|
119
203
|
// Initialize session stability monitor if enabled
|
|
120
|
-
if (
|
|
204
|
+
if (legacyPassthrough?.sessionStability?.enabled) {
|
|
121
205
|
const healthConfig = {
|
|
122
|
-
badMacThreshold:
|
|
123
|
-
badMacWindowMs:
|
|
206
|
+
badMacThreshold: legacyPassthrough.sessionStability.badMacThreshold,
|
|
207
|
+
badMacWindowMs: legacyPassthrough.sessionStability.badMacWindowMs,
|
|
124
208
|
onDegraded: (stats) => {
|
|
125
209
|
if (this.logging) {
|
|
126
|
-
console.log(`[baileys-antiban] 🔴 SESSION DEGRADED — Bad MAC rate: ${stats.badMacCount} in last ${
|
|
210
|
+
console.log(`[baileys-antiban] 🔴 SESSION DEGRADED — Bad MAC rate: ${stats.badMacCount} in last ${legacyPassthrough?.sessionStability?.badMacWindowMs || 60000}ms`);
|
|
127
211
|
console.log(`[baileys-antiban] Consider restarting session or switching to LID-based canonical form`);
|
|
128
212
|
}
|
|
129
213
|
},
|
|
@@ -226,6 +310,24 @@ export class AntiBan {
|
|
|
226
310
|
health: healthStatus,
|
|
227
311
|
};
|
|
228
312
|
}
|
|
313
|
+
// Group profile rate check (runs before rateLimiter.getDelay for timing)
|
|
314
|
+
if (this.resolvedConfig.groupProfiles && shouldUseGroupProfile(recipient)) {
|
|
315
|
+
const groupLimits = applyGroupMultiplier({
|
|
316
|
+
maxPerMinute: this.resolvedConfig.maxPerMinute,
|
|
317
|
+
maxPerHour: this.resolvedConfig.maxPerHour,
|
|
318
|
+
maxPerDay: this.resolvedConfig.maxPerDay,
|
|
319
|
+
}, this.resolvedConfig.groupMultiplier);
|
|
320
|
+
const stats = this.rateLimiter.getStats();
|
|
321
|
+
if (stats.lastMinute >= groupLimits.maxPerMinute ||
|
|
322
|
+
stats.lastHour >= groupLimits.maxPerHour ||
|
|
323
|
+
stats.lastDay >= groupLimits.maxPerDay) {
|
|
324
|
+
this.stats.messagesBlocked++;
|
|
325
|
+
if (this.logging) {
|
|
326
|
+
console.log(`[baileys-antiban] 🚫 BLOCKED — group rate limit exceeded for ${recipient}`);
|
|
327
|
+
}
|
|
328
|
+
return { allowed: false, delayMs: 0, reason: 'Group rate limit exceeded', health: healthStatus };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
229
331
|
// Rate limiter delay
|
|
230
332
|
let delay = await this.rateLimiter.getDelay(recipient, content);
|
|
231
333
|
if (delay === -1) {
|
|
@@ -279,6 +381,7 @@ export class AntiBan {
|
|
|
279
381
|
this.warmUp.record();
|
|
280
382
|
this.replyRatioGuard.recordSent(recipient);
|
|
281
383
|
this.stats.messagesAllowed++;
|
|
384
|
+
this.persistStateDebounced();
|
|
282
385
|
}
|
|
283
386
|
/**
|
|
284
387
|
* Record a failed message send
|
|
@@ -292,6 +395,10 @@ export class AntiBan {
|
|
|
292
395
|
onDisconnect(reason) {
|
|
293
396
|
this.health.recordDisconnect(reason);
|
|
294
397
|
this.reconnectThrottleModule.onDisconnect();
|
|
398
|
+
const reasonStr = String(reason);
|
|
399
|
+
if (reasonStr === '403' || reasonStr === '401' || reasonStr === 'forbidden' || reasonStr === 'loggedOut') {
|
|
400
|
+
this.persistStateImmediate();
|
|
401
|
+
}
|
|
295
402
|
}
|
|
296
403
|
/**
|
|
297
404
|
* Record a successful reconnection
|
|
@@ -423,11 +530,34 @@ export class AntiBan {
|
|
|
423
530
|
console.log('[baileys-antiban] 🔄 Reset — starting fresh warm-up');
|
|
424
531
|
}
|
|
425
532
|
}
|
|
533
|
+
persistStateDebounced() {
|
|
534
|
+
if (!this.stateManager)
|
|
535
|
+
return;
|
|
536
|
+
const state = {
|
|
537
|
+
warmup: this.warmUp.exportState(),
|
|
538
|
+
knownChats: Array.from(this.rateLimiter.getKnownChats()),
|
|
539
|
+
savedAt: Date.now(),
|
|
540
|
+
version: 3,
|
|
541
|
+
};
|
|
542
|
+
this.stateManager.saveDebounced(state);
|
|
543
|
+
}
|
|
544
|
+
persistStateImmediate() {
|
|
545
|
+
if (!this.stateManager)
|
|
546
|
+
return;
|
|
547
|
+
const state = {
|
|
548
|
+
warmup: this.warmUp.exportState(),
|
|
549
|
+
knownChats: Array.from(this.rateLimiter.getKnownChats()),
|
|
550
|
+
savedAt: Date.now(),
|
|
551
|
+
version: 3,
|
|
552
|
+
};
|
|
553
|
+
this.stateManager.saveImmediate(state);
|
|
554
|
+
}
|
|
426
555
|
/**
|
|
427
556
|
* Clean up all timers and resources.
|
|
428
557
|
* Call this when disposing of the AntiBan instance or when the socket closes.
|
|
429
558
|
*/
|
|
430
559
|
destroy() {
|
|
560
|
+
this.stateManager?.destroy();
|
|
431
561
|
this.timelockGuard.reset(); // Clears the resumeTimer
|
|
432
562
|
this.replyRatioGuard.reset();
|
|
433
563
|
this.contactGraphWarmer.reset();
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* baileys-antiban CLI
|
|
4
|
+
* Usage: npx baileys-antiban <command> [options]
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import { StateManager } from './persist.js';
|
|
8
|
+
import { resolveConfig } from './presets.js';
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const command = args[0];
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const result = {};
|
|
13
|
+
for (let i = 1; i < argv.length; i++) {
|
|
14
|
+
const arg = argv[i];
|
|
15
|
+
if (arg.startsWith('--')) {
|
|
16
|
+
const key = arg.slice(2);
|
|
17
|
+
const next = argv[i + 1];
|
|
18
|
+
if (next && !next.startsWith('--')) {
|
|
19
|
+
result[key] = next;
|
|
20
|
+
i++;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
result[key] = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
function cmdStatus(opts) {
|
|
30
|
+
const statePath = opts['state'];
|
|
31
|
+
let warmupInfo = 'No state file (in-memory mode)';
|
|
32
|
+
let savedAt = 'N/A';
|
|
33
|
+
if (statePath) {
|
|
34
|
+
const mgr = new StateManager(statePath);
|
|
35
|
+
const state = mgr.load();
|
|
36
|
+
if (state) {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
const dayMs = 86400000;
|
|
39
|
+
const currentDay = Math.floor((now - state.warmup.startedAt) / dayMs);
|
|
40
|
+
warmupInfo = state.warmup.graduated
|
|
41
|
+
? 'Graduated (warmup complete)'
|
|
42
|
+
: `Day ${currentDay + 1}, sent today: ${state.warmup.dailyCounts[currentDay] || 0}`;
|
|
43
|
+
savedAt = new Date(state.savedAt).toISOString();
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
warmupInfo = 'State file missing or corrupt';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const output = {
|
|
50
|
+
warmup: warmupInfo,
|
|
51
|
+
savedAt,
|
|
52
|
+
statePath: statePath || null,
|
|
53
|
+
};
|
|
54
|
+
if (opts['json']) {
|
|
55
|
+
console.log(JSON.stringify(output, null, 2));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.log('═══ baileys-antiban status ═══');
|
|
59
|
+
console.log(`Warmup: ${output.warmup}`);
|
|
60
|
+
console.log(`Saved: ${output.savedAt}`);
|
|
61
|
+
console.log(`State: ${output.statePath || 'none'}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function cmdReset(opts) {
|
|
65
|
+
const statePath = opts['state'];
|
|
66
|
+
if (!statePath) {
|
|
67
|
+
console.error('Error: --state <path> required for reset');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
if (!fs.existsSync(statePath)) {
|
|
71
|
+
console.log('State file does not exist — nothing to reset');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
fs.unlinkSync(statePath);
|
|
75
|
+
console.log(`✅ State file deleted: ${statePath}`);
|
|
76
|
+
}
|
|
77
|
+
function cmdWarmupSimulate(opts) {
|
|
78
|
+
const days = parseInt(opts['simulate'] || '7', 10);
|
|
79
|
+
const presetName = opts['preset'] || 'conservative';
|
|
80
|
+
const cfg = resolveConfig(presetName);
|
|
81
|
+
console.log(`\nWarmup simulation — preset: ${presetName}, days: ${days}`);
|
|
82
|
+
console.log('─'.repeat(50));
|
|
83
|
+
const startDate = new Date();
|
|
84
|
+
for (let day = 0; day < days; day++) {
|
|
85
|
+
const date = new Date(startDate);
|
|
86
|
+
date.setDate(date.getDate() + day);
|
|
87
|
+
const dayName = date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
|
88
|
+
const limit = Math.round(cfg.day1Limit * Math.pow(cfg.growthFactor, day));
|
|
89
|
+
const bar = '█'.repeat(Math.min(30, Math.round(limit / 10)));
|
|
90
|
+
console.log(`Day ${String(day + 1).padStart(2)} ${dayName.padEnd(15)} ${String(limit).padStart(5)} msgs/day ${bar}`);
|
|
91
|
+
}
|
|
92
|
+
console.log('─'.repeat(50));
|
|
93
|
+
console.log(`Day ${days + 1}+: graduated (unlimited by warmup)\n`);
|
|
94
|
+
}
|
|
95
|
+
// Main
|
|
96
|
+
const opts = parseArgs(args);
|
|
97
|
+
switch (command) {
|
|
98
|
+
case 'status':
|
|
99
|
+
cmdStatus(opts);
|
|
100
|
+
break;
|
|
101
|
+
case 'reset':
|
|
102
|
+
cmdReset(opts);
|
|
103
|
+
break;
|
|
104
|
+
case 'warmup':
|
|
105
|
+
if (opts['simulate']) {
|
|
106
|
+
cmdWarmupSimulate(opts);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
console.error('Usage: npx baileys-antiban warmup --simulate <days> [--preset conservative|moderate|aggressive]');
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
console.log('baileys-antiban v3.0');
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log('Commands:');
|
|
117
|
+
console.log(' status [--state <path>] [--json] Show warmup and health status');
|
|
118
|
+
console.log(' reset --state <path> Delete state file');
|
|
119
|
+
console.log(' warmup --simulate <days> [--preset] Show warmup schedule');
|
|
120
|
+
console.log('');
|
|
121
|
+
console.log('Examples:');
|
|
122
|
+
console.log(' npx baileys-antiban status --state ./antiban-state.json');
|
|
123
|
+
console.log(' npx baileys-antiban warmup --simulate 7 --preset moderate');
|
|
124
|
+
console.log(' npx baileys-antiban reset --state ./antiban-state.json');
|
|
125
|
+
}
|
package/dist/health.d.ts
CHANGED
package/dist/health.js
CHANGED
|
@@ -24,6 +24,8 @@ export class HealthMonitor {
|
|
|
24
24
|
startTime = Date.now();
|
|
25
25
|
paused = false;
|
|
26
26
|
lastRisk = 'low';
|
|
27
|
+
lastBadEventTime = Date.now();
|
|
28
|
+
lastEventWasSevere = false;
|
|
27
29
|
constructor(config = {}) {
|
|
28
30
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
29
31
|
}
|
|
@@ -32,16 +34,20 @@ export class HealthMonitor {
|
|
|
32
34
|
*/
|
|
33
35
|
recordDisconnect(reason) {
|
|
34
36
|
const reasonStr = String(reason);
|
|
35
|
-
// 403 = Forbidden (WhatsApp blocking)
|
|
36
37
|
if (reasonStr === '403' || reasonStr === 'forbidden') {
|
|
37
38
|
this.events.push({ type: 'forbidden', timestamp: Date.now(), detail: reasonStr });
|
|
39
|
+
this.lastBadEventTime = Date.now();
|
|
40
|
+
this.lastEventWasSevere = true;
|
|
38
41
|
}
|
|
39
|
-
// 401 = Logged out (possible temp ban)
|
|
40
42
|
else if (reasonStr === '401' || reasonStr === 'loggedOut') {
|
|
41
43
|
this.events.push({ type: 'loggedOut', timestamp: Date.now(), detail: reasonStr });
|
|
44
|
+
this.lastBadEventTime = Date.now();
|
|
45
|
+
this.lastEventWasSevere = true;
|
|
42
46
|
}
|
|
43
47
|
else {
|
|
44
48
|
this.events.push({ type: 'disconnect', timestamp: Date.now(), detail: reasonStr });
|
|
49
|
+
this.lastBadEventTime = Date.now();
|
|
50
|
+
this.lastEventWasSevere = false;
|
|
45
51
|
}
|
|
46
52
|
this.checkAndNotify();
|
|
47
53
|
}
|
|
@@ -56,6 +62,8 @@ export class HealthMonitor {
|
|
|
56
62
|
*/
|
|
57
63
|
recordMessageFailed(error) {
|
|
58
64
|
this.events.push({ type: 'messageFailed', timestamp: Date.now(), detail: error });
|
|
65
|
+
this.lastBadEventTime = Date.now();
|
|
66
|
+
this.lastEventWasSevere = false;
|
|
59
67
|
this.checkAndNotify();
|
|
60
68
|
}
|
|
61
69
|
/**
|
|
@@ -63,6 +71,8 @@ export class HealthMonitor {
|
|
|
63
71
|
*/
|
|
64
72
|
recordReachoutTimelock(detail) {
|
|
65
73
|
this.events.push({ type: 'reachoutTimelocked', timestamp: Date.now(), detail });
|
|
74
|
+
this.lastBadEventTime = Date.now();
|
|
75
|
+
this.lastEventWasSevere = false;
|
|
66
76
|
this.checkAndNotify();
|
|
67
77
|
}
|
|
68
78
|
/**
|
|
@@ -110,6 +120,12 @@ export class HealthMonitor {
|
|
|
110
120
|
}
|
|
111
121
|
// Determine risk level
|
|
112
122
|
score = Math.min(100, score);
|
|
123
|
+
// Tiered decay: recover based on time since last bad event
|
|
124
|
+
// Severe (403/401): 2pts/min — ~50min to clear 100pts
|
|
125
|
+
// Normal: 5pts/min — ~20min to clear 100pts
|
|
126
|
+
const minutesSinceLastBad = (now - this.lastBadEventTime) / 60000;
|
|
127
|
+
const decayRate = this.lastEventWasSevere ? 2 : 5;
|
|
128
|
+
score = Math.max(0, score - Math.floor(minutesSinceLastBad * decayRate));
|
|
113
129
|
let risk;
|
|
114
130
|
if (score >= 85)
|
|
115
131
|
risk = 'critical';
|
|
@@ -174,6 +190,8 @@ export class HealthMonitor {
|
|
|
174
190
|
this.startTime = Date.now();
|
|
175
191
|
this.paused = false;
|
|
176
192
|
this.lastRisk = 'low';
|
|
193
|
+
this.lastBadEventTime = Date.now();
|
|
194
|
+
this.lastEventWasSevere = false;
|
|
177
195
|
}
|
|
178
196
|
cleanup(now) {
|
|
179
197
|
// Keep last 6 hours of events
|
package/dist/index.d.ts
CHANGED
|
@@ -28,3 +28,6 @@ export { ContentVariator, type VariatorConfig } from './contentVariator.js';
|
|
|
28
28
|
export { WebhookAlerts, type WebhookConfig } from './webhooks.js';
|
|
29
29
|
export { Scheduler, type SchedulerConfig } from './scheduler.js';
|
|
30
30
|
export { type StateAdapter, FileStateAdapter } from './stateAdapter.js';
|
|
31
|
+
export { resolveConfig, PRESETS, type AntiBanInput, type ResolvedConfig, type PresetName } from './presets.js';
|
|
32
|
+
export { StateManager, type PersistedState } from './persist.js';
|
|
33
|
+
export { isGroup, isNewsletter, isBroadcast, shouldUseGroupProfile, applyGroupMultiplier, type RateLimits } from './profiles.js';
|
package/dist/index.js
CHANGED
|
@@ -37,3 +37,7 @@ export { WebhookAlerts } from './webhooks.js';
|
|
|
37
37
|
export { Scheduler } from './scheduler.js';
|
|
38
38
|
// State persistence
|
|
39
39
|
export { FileStateAdapter } from './stateAdapter.js';
|
|
40
|
+
// v3.0 new modules
|
|
41
|
+
export { resolveConfig, PRESETS } from './presets.js';
|
|
42
|
+
export { StateManager } from './persist.js';
|
|
43
|
+
export { isGroup, isNewsletter, isBroadcast, shouldUseGroupProfile, applyGroupMultiplier } from './profiles.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { WarmUpState } from './warmup.js';
|
|
2
|
+
export interface PersistedState {
|
|
3
|
+
warmup: WarmUpState;
|
|
4
|
+
knownChats: string[];
|
|
5
|
+
savedAt: number;
|
|
6
|
+
version: 3;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Manages persisted state for a single baileys-antiban instance.
|
|
10
|
+
*
|
|
11
|
+
* **Single-writer assumption:** No file lock is used. Two processes sharing
|
|
12
|
+
* the same state file will race on concurrent writes. Use separate state
|
|
13
|
+
* files per process to avoid data corruption.
|
|
14
|
+
*/
|
|
15
|
+
export declare class StateManager {
|
|
16
|
+
private path;
|
|
17
|
+
private debounceTimer;
|
|
18
|
+
constructor(filePath: string);
|
|
19
|
+
load(): PersistedState | null;
|
|
20
|
+
/** Debounced save — called after every send (5s delay) */
|
|
21
|
+
saveDebounced(state: PersistedState): void;
|
|
22
|
+
/** Immediate save — called after health events (ban/restriction) */
|
|
23
|
+
saveImmediate(state: PersistedState): void;
|
|
24
|
+
/** Flush/cancel pending debounced write (for tests and process exit) */
|
|
25
|
+
flush(): void;
|
|
26
|
+
destroy(): void;
|
|
27
|
+
private writeFile;
|
|
28
|
+
}
|
package/dist/persist.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
const KNOWN_CHATS_MAX = 1000;
|
|
3
|
+
const DEBOUNCE_MS = 5000;
|
|
4
|
+
/**
|
|
5
|
+
* Manages persisted state for a single baileys-antiban instance.
|
|
6
|
+
*
|
|
7
|
+
* **Single-writer assumption:** No file lock is used. Two processes sharing
|
|
8
|
+
* the same state file will race on concurrent writes. Use separate state
|
|
9
|
+
* files per process to avoid data corruption.
|
|
10
|
+
*/
|
|
11
|
+
export class StateManager {
|
|
12
|
+
path;
|
|
13
|
+
debounceTimer = null;
|
|
14
|
+
constructor(filePath) {
|
|
15
|
+
this.path = filePath;
|
|
16
|
+
}
|
|
17
|
+
load() {
|
|
18
|
+
try {
|
|
19
|
+
const raw = fs.readFileSync(this.path, 'utf-8');
|
|
20
|
+
const parsed = JSON.parse(raw);
|
|
21
|
+
if (parsed.version !== 3) {
|
|
22
|
+
console.warn('[baileys-antiban] WARN: corrupt state file or version mismatch, starting fresh');
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Missing file = silent null. Corrupt JSON = warn.
|
|
29
|
+
if (fs.existsSync(this.path)) {
|
|
30
|
+
console.warn('[baileys-antiban] WARN: corrupt state file, starting fresh');
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Debounced save — called after every send (5s delay) */
|
|
36
|
+
saveDebounced(state) {
|
|
37
|
+
if (this.debounceTimer) {
|
|
38
|
+
clearTimeout(this.debounceTimer);
|
|
39
|
+
}
|
|
40
|
+
this.debounceTimer = setTimeout(() => {
|
|
41
|
+
this.writeFile(state);
|
|
42
|
+
this.debounceTimer = null;
|
|
43
|
+
}, DEBOUNCE_MS);
|
|
44
|
+
}
|
|
45
|
+
/** Immediate save — called after health events (ban/restriction) */
|
|
46
|
+
saveImmediate(state) {
|
|
47
|
+
if (this.debounceTimer) {
|
|
48
|
+
clearTimeout(this.debounceTimer);
|
|
49
|
+
this.debounceTimer = null;
|
|
50
|
+
}
|
|
51
|
+
this.writeFile(state);
|
|
52
|
+
}
|
|
53
|
+
/** Flush/cancel pending debounced write (for tests and process exit) */
|
|
54
|
+
flush() {
|
|
55
|
+
if (this.debounceTimer) {
|
|
56
|
+
clearTimeout(this.debounceTimer);
|
|
57
|
+
this.debounceTimer = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
destroy() {
|
|
61
|
+
this.flush();
|
|
62
|
+
}
|
|
63
|
+
writeFile(state) {
|
|
64
|
+
const toSave = {
|
|
65
|
+
...state,
|
|
66
|
+
savedAt: Date.now(),
|
|
67
|
+
// LRU eviction: keep last KNOWN_CHATS_MAX entries
|
|
68
|
+
knownChats: state.knownChats.length > KNOWN_CHATS_MAX
|
|
69
|
+
? state.knownChats.slice(-KNOWN_CHATS_MAX)
|
|
70
|
+
: state.knownChats,
|
|
71
|
+
};
|
|
72
|
+
try {
|
|
73
|
+
fs.writeFileSync(this.path, JSON.stringify(toSave, null, 2), 'utf-8');
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
console.warn(`[baileys-antiban] WARN: failed to write state to ${this.path}:`, err);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BanRiskLevel } from './health.js';
|
|
2
|
+
export interface ResolvedConfig {
|
|
3
|
+
maxPerMinute: number;
|
|
4
|
+
maxPerHour: number;
|
|
5
|
+
maxPerDay: number;
|
|
6
|
+
minDelayMs: number;
|
|
7
|
+
maxDelayMs: number;
|
|
8
|
+
newChatDelayMs: number;
|
|
9
|
+
warmupDays: number;
|
|
10
|
+
day1Limit: number;
|
|
11
|
+
growthFactor: number;
|
|
12
|
+
inactivityThresholdHours: number;
|
|
13
|
+
autoPauseAt: BanRiskLevel;
|
|
14
|
+
groupMultiplier: number;
|
|
15
|
+
groupProfiles: boolean;
|
|
16
|
+
persist?: string;
|
|
17
|
+
logging: boolean;
|
|
18
|
+
}
|
|
19
|
+
export type PresetName = 'conservative' | 'moderate' | 'aggressive';
|
|
20
|
+
export type AntiBanInput = PresetName | Partial<ResolvedConfig & {
|
|
21
|
+
preset?: PresetName;
|
|
22
|
+
}> | undefined;
|
|
23
|
+
export declare const PRESETS: Record<PresetName, ResolvedConfig>;
|
|
24
|
+
export declare function resolveConfig(input: AntiBanInput): ResolvedConfig;
|
package/dist/presets.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export const PRESETS = {
|
|
2
|
+
conservative: {
|
|
3
|
+
maxPerMinute: 5,
|
|
4
|
+
maxPerHour: 100,
|
|
5
|
+
maxPerDay: 800,
|
|
6
|
+
minDelayMs: 2500,
|
|
7
|
+
maxDelayMs: 7000,
|
|
8
|
+
newChatDelayMs: 4000,
|
|
9
|
+
warmupDays: 10,
|
|
10
|
+
day1Limit: 15,
|
|
11
|
+
growthFactor: 1.8,
|
|
12
|
+
inactivityThresholdHours: 72,
|
|
13
|
+
autoPauseAt: 'medium',
|
|
14
|
+
groupMultiplier: 0.5,
|
|
15
|
+
groupProfiles: false,
|
|
16
|
+
logging: true,
|
|
17
|
+
},
|
|
18
|
+
moderate: {
|
|
19
|
+
maxPerMinute: 10,
|
|
20
|
+
maxPerHour: 300,
|
|
21
|
+
maxPerDay: 1500,
|
|
22
|
+
minDelayMs: 1500,
|
|
23
|
+
maxDelayMs: 5000,
|
|
24
|
+
newChatDelayMs: 3000,
|
|
25
|
+
warmupDays: 7,
|
|
26
|
+
day1Limit: 20,
|
|
27
|
+
growthFactor: 1.8,
|
|
28
|
+
inactivityThresholdHours: 72,
|
|
29
|
+
autoPauseAt: 'high',
|
|
30
|
+
groupMultiplier: 0.7,
|
|
31
|
+
groupProfiles: false,
|
|
32
|
+
logging: true,
|
|
33
|
+
},
|
|
34
|
+
aggressive: {
|
|
35
|
+
maxPerMinute: 20,
|
|
36
|
+
maxPerHour: 800,
|
|
37
|
+
maxPerDay: 4000,
|
|
38
|
+
minDelayMs: 800,
|
|
39
|
+
maxDelayMs: 3000,
|
|
40
|
+
newChatDelayMs: 2000,
|
|
41
|
+
warmupDays: 4,
|
|
42
|
+
day1Limit: 35,
|
|
43
|
+
growthFactor: 2.0,
|
|
44
|
+
inactivityThresholdHours: 48,
|
|
45
|
+
autoPauseAt: 'critical',
|
|
46
|
+
groupMultiplier: 0.9,
|
|
47
|
+
groupProfiles: false,
|
|
48
|
+
logging: true,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
export function resolveConfig(input) {
|
|
52
|
+
if (input === undefined) {
|
|
53
|
+
return { ...PRESETS.conservative };
|
|
54
|
+
}
|
|
55
|
+
if (typeof input === 'string') {
|
|
56
|
+
if (!(input in PRESETS)) {
|
|
57
|
+
throw new Error(`Unknown preset "${input}". Valid: ${Object.keys(PRESETS).join(', ')}`);
|
|
58
|
+
}
|
|
59
|
+
return { ...PRESETS[input] };
|
|
60
|
+
}
|
|
61
|
+
// Object form — extract preset base, merge overrides
|
|
62
|
+
const { preset = 'conservative', ...overrides } = input;
|
|
63
|
+
if (!(preset in PRESETS)) {
|
|
64
|
+
throw new Error(`Unknown preset "${preset}". Valid: ${Object.keys(PRESETS).join(', ')}`);
|
|
65
|
+
}
|
|
66
|
+
return { ...PRESETS[preset], ...overrides };
|
|
67
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface RateLimits {
|
|
2
|
+
maxPerMinute: number;
|
|
3
|
+
maxPerHour: number;
|
|
4
|
+
maxPerDay: number;
|
|
5
|
+
}
|
|
6
|
+
/** @g.us = WhatsApp group */
|
|
7
|
+
export declare function isGroup(jid: string): boolean;
|
|
8
|
+
/** @newsletter = WhatsApp newsletter/channel */
|
|
9
|
+
export declare function isNewsletter(jid: string): boolean;
|
|
10
|
+
/** status@broadcast = broadcast list */
|
|
11
|
+
export declare function isBroadcast(jid: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Returns true if the JID should use stricter (group) rate limits.
|
|
14
|
+
* Groups and newsletters both get the group multiplier in v3.
|
|
15
|
+
* v4: separate newsletter profile.
|
|
16
|
+
*/
|
|
17
|
+
export declare function shouldUseGroupProfile(jid: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Scale rate limits by multiplier for group/newsletter JIDs.
|
|
20
|
+
* Floors to integer, minimum 1 per limit.
|
|
21
|
+
*/
|
|
22
|
+
export declare function applyGroupMultiplier(limits: RateLimits, multiplier: number): RateLimits;
|
package/dist/profiles.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** @g.us = WhatsApp group */
|
|
2
|
+
export function isGroup(jid) {
|
|
3
|
+
return jid.endsWith('@g.us');
|
|
4
|
+
}
|
|
5
|
+
/** @newsletter = WhatsApp newsletter/channel */
|
|
6
|
+
export function isNewsletter(jid) {
|
|
7
|
+
return jid.endsWith('@newsletter');
|
|
8
|
+
}
|
|
9
|
+
/** status@broadcast = broadcast list */
|
|
10
|
+
export function isBroadcast(jid) {
|
|
11
|
+
return jid === 'status@broadcast' || jid.endsWith('@broadcast');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Returns true if the JID should use stricter (group) rate limits.
|
|
15
|
+
* Groups and newsletters both get the group multiplier in v3.
|
|
16
|
+
* v4: separate newsletter profile.
|
|
17
|
+
*/
|
|
18
|
+
export function shouldUseGroupProfile(jid) {
|
|
19
|
+
return isGroup(jid) || isNewsletter(jid);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Scale rate limits by multiplier for group/newsletter JIDs.
|
|
23
|
+
* Floors to integer, minimum 1 per limit.
|
|
24
|
+
*/
|
|
25
|
+
export function applyGroupMultiplier(limits, multiplier) {
|
|
26
|
+
return {
|
|
27
|
+
maxPerMinute: Math.max(1, Math.floor(limits.maxPerMinute * multiplier)),
|
|
28
|
+
maxPerHour: Math.max(1, Math.floor(limits.maxPerHour * multiplier)),
|
|
29
|
+
maxPerDay: Math.max(1, Math.floor(limits.maxPerDay * multiplier)),
|
|
30
|
+
};
|
|
31
|
+
}
|
package/dist/rateLimiter.d.ts
CHANGED
|
@@ -61,6 +61,10 @@ export declare class RateLimiter {
|
|
|
61
61
|
* Get current usage stats
|
|
62
62
|
*/
|
|
63
63
|
getStats(): RateLimiterStats;
|
|
64
|
+
/** Get the set of known chat JIDs (for state persistence) */
|
|
65
|
+
getKnownChats(): Set<string>;
|
|
66
|
+
/** Restore known chats from persisted state */
|
|
67
|
+
restoreKnownChats(chats: string[]): void;
|
|
64
68
|
private cleanup;
|
|
65
69
|
/** Random delay between min and max (gaussian-ish distribution) */
|
|
66
70
|
private jitter;
|
package/dist/rateLimiter.js
CHANGED
|
@@ -154,6 +154,16 @@ export class RateLimiter {
|
|
|
154
154
|
knownChats: this.knownChats.size,
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
|
+
/** Get the set of known chat JIDs (for state persistence) */
|
|
158
|
+
getKnownChats() {
|
|
159
|
+
return this.knownChats;
|
|
160
|
+
}
|
|
161
|
+
/** Restore known chats from persisted state */
|
|
162
|
+
restoreKnownChats(chats) {
|
|
163
|
+
for (const jid of chats) {
|
|
164
|
+
this.knownChats.add(jid);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
157
167
|
cleanup(now) {
|
|
158
168
|
// Remove messages older than 24 hours
|
|
159
169
|
this.messages = this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_DAY);
|
package/dist/warmup.d.ts
CHANGED
|
@@ -18,8 +18,6 @@ export interface WarmUpConfig {
|
|
|
18
18
|
growthFactor: number;
|
|
19
19
|
/** Hours of inactivity before re-entering warm-up (default: 72) */
|
|
20
20
|
inactivityThresholdHours: number;
|
|
21
|
-
/** Persist state to this file path (optional) */
|
|
22
|
-
statePath?: string;
|
|
23
21
|
}
|
|
24
22
|
export interface WarmUpState {
|
|
25
23
|
/** When warm-up started */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baileys-antiban",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Anti-ban middleware for Baileys WhatsApp bots. Rate limiting, warmup, health monitor, LID resolver, disconnect classifier. Free Whapi.Cloud alternative.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"type": "module",
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
"test:check": "tsc --noEmit && npm test",
|
|
28
28
|
"prepublishOnly": "npm run build"
|
|
29
29
|
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"baileys-antiban": "./dist/cli.js"
|
|
32
|
+
},
|
|
30
33
|
"keywords": [
|
|
31
34
|
"baileys",
|
|
32
35
|
"baileyrs",
|
|
@@ -38,7 +41,20 @@
|
|
|
38
41
|
"bot-protection",
|
|
39
42
|
"whatsapp-api",
|
|
40
43
|
"nodejs",
|
|
41
|
-
"transport-agnostic"
|
|
44
|
+
"transport-agnostic",
|
|
45
|
+
"whapi-alternative",
|
|
46
|
+
"whatsapp-ban",
|
|
47
|
+
"baileys-middleware",
|
|
48
|
+
"warmup",
|
|
49
|
+
"session-health",
|
|
50
|
+
"lid-resolver",
|
|
51
|
+
"disconnect-classifier",
|
|
52
|
+
"gaussian-jitter",
|
|
53
|
+
"whatsapp-automation",
|
|
54
|
+
"typescript",
|
|
55
|
+
"self-hosted",
|
|
56
|
+
"free",
|
|
57
|
+
"open-source"
|
|
42
58
|
],
|
|
43
59
|
"author": "Kobus Wentzel <kobie@pop.co.za>",
|
|
44
60
|
"license": "MIT",
|