baileys-antiban 3.3.0 → 3.5.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 +120 -0
- package/README.md +121 -0
- package/dist/cli.js +0 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -0
- package/dist/presenceChoreographer.d.ts +57 -0
- package/dist/presenceChoreographer.js +135 -0
- package/dist/proxyRotator.d.ts +79 -0
- package/dist/proxyRotator.js +295 -0
- package/package.json +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,126 @@ 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.5.0] — 2026-04-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **proxyRotator** — Native proxy injection with multi-strategy rotation and health tracking
|
|
12
|
+
- Closes the datacenter IP ban vector — WhatsApp's ML flags VPS IPs, residential/4G proxies stay alive
|
|
13
|
+
- Supports SOCKS5, SOCKS5H, HTTP, HTTPS proxies with auth
|
|
14
|
+
- 4 rotation strategies: round-robin, random, least-recently-used, weighted (by health)
|
|
15
|
+
- Auto-failover on endpoint failure with configurable dead thresholds (default: 3 failures)
|
|
16
|
+
- Health tracking: failure counters, dead-marking, auto-resurrection after cooldown (default: 10min)
|
|
17
|
+
- Per-endpoint cooldown periods to avoid hammering proxy providers
|
|
18
|
+
- Scheduled rotation for proactive IP rotation (configurable interval)
|
|
19
|
+
- Rotation triggers: manual, disconnect, ban-warning, scheduled (user-wired)
|
|
20
|
+
- Lazy-loaded proxy agent dependencies (optional peerDeps: socks-proxy-agent, http-proxy-agent, https-proxy-agent)
|
|
21
|
+
- Agent caching for performance (avoids re-creating agents on every request)
|
|
22
|
+
- Comprehensive stats: total rotations, per-trigger breakdowns, endpoint health dashboard
|
|
23
|
+
- Production-ready error handling: graceful fallback when peer deps missing
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **proxyRotator**: Fixed ESM `require()` regression by using `createRequire()` from `node:module` for ESM-compatible synchronous module loading (caught by live SOCKS5 smoke test before publish)
|
|
27
|
+
|
|
28
|
+
### Why v3.5
|
|
29
|
+
Per GapHunter analysis, WhatsApp's ban detection includes IP reputation scoring. Datacenter IPs (VPS) are flagged. Residential/4G proxies stay alive. Every Baileys implementation uses DIY proxy hacks — no library handles native proxy injection. `proxyRotator` closes that gap with production-grade rotation strategies, health tracking, and auto-failover.
|
|
30
|
+
|
|
31
|
+
### Usage
|
|
32
|
+
```ts
|
|
33
|
+
import { proxyRotator } from 'baileys-antiban';
|
|
34
|
+
import { makeWASocket } from 'baileys';
|
|
35
|
+
|
|
36
|
+
const rotator = proxyRotator({
|
|
37
|
+
pool: [
|
|
38
|
+
{ type: 'socks5', host: 'proxy1.example.com', port: 1080, username: 'user', password: 'pass', label: 'Proxy1' },
|
|
39
|
+
{ type: 'socks5', host: 'proxy2.example.com', port: 1080, username: 'user', password: 'pass', label: 'Proxy2', cooldownMs: 300_000 },
|
|
40
|
+
],
|
|
41
|
+
strategy: 'weighted', // Prefer healthier endpoints
|
|
42
|
+
rotateOn: ['disconnect', 'ban-warning'],
|
|
43
|
+
maxFailures: 3,
|
|
44
|
+
deadCooldownMs: 600_000, // 10 minutes
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const sock = makeWASocket({
|
|
48
|
+
auth: state,
|
|
49
|
+
fetchAgent: rotator.currentAgent(), // Inject proxy into Baileys fetch
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Wire disconnect rotation
|
|
53
|
+
sock.ev.on('connection.update', ({ connection }) => {
|
|
54
|
+
if (connection === 'close') {
|
|
55
|
+
rotator.rotate('disconnect');
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Wire ban-warning rotation (from sessionStability)
|
|
60
|
+
monitor.onDegraded = () => {
|
|
61
|
+
rotator.rotate('ban-warning');
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Check stats
|
|
65
|
+
console.log(rotator.getStats());
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Technical Details
|
|
69
|
+
- Agent caching: agents are created once per endpoint and reused until rotation
|
|
70
|
+
- Cooldown logic: endpoints are skipped if `Date.now() - lastUsedAt < cooldownMs`
|
|
71
|
+
- Dead resurrection: auto-checks on rotation if `Date.now() - lastUsedAt >= deadCooldownMs`
|
|
72
|
+
- Weighted strategy: `weight = 1 / (failures + 1)` for probabilistic health-biased selection
|
|
73
|
+
- LRU strategy: prioritizes never-used endpoints, then oldest `lastUsedAt`
|
|
74
|
+
- Peer dep handling: uses `require()` with try/catch, logs clear error on missing deps
|
|
75
|
+
- Pool size 1: logs warning once, rotation becomes no-op
|
|
76
|
+
- All endpoints dead: `currentAgent()` returns `null`, user code must handle
|
|
77
|
+
|
|
78
|
+
## [3.4.0] — 2026-04-26
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
- **WPM-based typing duration model** — Realistic typing indicator patterns based on human typing speed
|
|
82
|
+
- `PresenceChoreographer.computeTypingPlan(messageLength)` — Generates realistic typing plan with Gaussian WPM variance
|
|
83
|
+
- `PresenceChoreographer.executeTypingPlan(sock, jid, plan)` — Executes multi-step typing/pause cycle
|
|
84
|
+
- Gaussian sampling (Box-Muller) for WPM variance (default: 45 WPM ± 15 stdDev, clamped 10-120)
|
|
85
|
+
- Think-pause injection: 8% probability per 10 chars, 0.8-3.5s pauses (humans pause mid-thought)
|
|
86
|
+
- Intermittent `paused` state (40% probability) before send for realism
|
|
87
|
+
- Configurable min/max typing duration caps (default: 0.6s - 90s)
|
|
88
|
+
- AbortSignal support for mid-plan cancellation
|
|
89
|
+
- New stats: `typingPlansComputed`, `typingPlansExecuted`, `totalTypingTimeMs`
|
|
90
|
+
- Zero new dependencies — pure TypeScript with Box-Muller transform
|
|
91
|
+
|
|
92
|
+
### Why v3.4
|
|
93
|
+
WhatsApp's ML models flag accounts that fire `composing` then immediately send, or never fire typing indicators at all. Real humans typing a 200-character message take 30-60 seconds with multiple typing/paused cycles. This is the missing signal layer that completes PresenceChoreographer's anti-detection coverage. The WPM model is the final piece of the presence choreography puzzle — realistic read receipts (v1.3), distraction pauses (v1.3), circadian rhythm (v1.3), and now typing duration.
|
|
94
|
+
|
|
95
|
+
### Usage
|
|
96
|
+
```ts
|
|
97
|
+
import { PresenceChoreographer } from 'baileys-antiban';
|
|
98
|
+
|
|
99
|
+
const choreo = new PresenceChoreographer({
|
|
100
|
+
enabled: true,
|
|
101
|
+
enableTypingModel: true,
|
|
102
|
+
typingWPM: 45, // Average human typing speed
|
|
103
|
+
typingWPMStdDev: 15, // Variance (slow/fast days)
|
|
104
|
+
thinkPauseProbability: 0.08,
|
|
105
|
+
thinkPauseMinMs: 800,
|
|
106
|
+
thinkPauseMaxMs: 3500,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Before sending a message
|
|
110
|
+
const messageText = "Hello, how are you doing today?";
|
|
111
|
+
const plan = choreo.computeTypingPlan(messageText.length);
|
|
112
|
+
|
|
113
|
+
// Execute typing plan
|
|
114
|
+
await choreo.executeTypingPlan(sock, jid, plan);
|
|
115
|
+
|
|
116
|
+
// Send actual message
|
|
117
|
+
await sock.sendMessage(jid, { text: messageText });
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Technical Details
|
|
121
|
+
- Plan structure: `Array<{ state: 'composing' | 'paused', durationMs: number }>`
|
|
122
|
+
- WPM → chars/sec conversion: `(WPM × 5) / 60` (industry standard: 5 chars/word)
|
|
123
|
+
- Think pauses are extras, not subtracted from base typing time (20% budget slack)
|
|
124
|
+
- Composing chunks coalesced when no pause injected between them
|
|
125
|
+
- AbortSignal cleanup: sets presence to `paused` before throwing
|
|
126
|
+
- All existing PresenceChoreographer features remain unchanged and backward compatible
|
|
127
|
+
|
|
8
128
|
## [3.3.0] — 2026-04-26
|
|
9
129
|
|
|
10
130
|
### Added
|
package/README.md
CHANGED
|
@@ -203,6 +203,91 @@ console.log(stats.throttledSendCount); // Sends gated since reconnect
|
|
|
203
203
|
|
|
204
204
|
**Why?** When WhatsApp reconnects after a disconnection, sending messages at full rate immediately can trigger rate limit alarms. The reconnect throttle gradually ramps up sending rate over 60 seconds, mimicking how a human would resume messaging after their internet came back.
|
|
205
205
|
|
|
206
|
+
## Proxy Rotation (v3.5)
|
|
207
|
+
|
|
208
|
+
WhatsApp's ban detection includes **IP reputation scoring**. Datacenter IPs (VPS) are flagged. Residential/4G proxies stay alive. No Baileys library handles native proxy injection — every implementation uses DIY hacks. `proxyRotator` closes that gap.
|
|
209
|
+
|
|
210
|
+
### Features
|
|
211
|
+
- Multi-strategy rotation: round-robin, random, least-recently-used, weighted (by health)
|
|
212
|
+
- Auto-failover on endpoint failure
|
|
213
|
+
- Health tracking with auto-resurrection after cooldown
|
|
214
|
+
- Per-endpoint cooldown periods
|
|
215
|
+
- Scheduled rotation for proactive IP rotation
|
|
216
|
+
- Supports SOCKS5, SOCKS5H, HTTP, HTTPS proxies with auth
|
|
217
|
+
|
|
218
|
+
### Basic Usage
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import { proxyRotator } from 'baileys-antiban';
|
|
222
|
+
import { makeWASocket } from 'baileys';
|
|
223
|
+
|
|
224
|
+
const rotator = proxyRotator({
|
|
225
|
+
pool: [
|
|
226
|
+
{
|
|
227
|
+
type: 'socks5',
|
|
228
|
+
host: 'proxy1.example.com',
|
|
229
|
+
port: 1080,
|
|
230
|
+
username: 'user',
|
|
231
|
+
password: 'pass',
|
|
232
|
+
label: 'Proxy1',
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
type: 'socks5',
|
|
236
|
+
host: 'proxy2.example.com',
|
|
237
|
+
port: 1080,
|
|
238
|
+
username: 'user',
|
|
239
|
+
password: 'pass',
|
|
240
|
+
label: 'Proxy2',
|
|
241
|
+
cooldownMs: 300_000, // 5-minute cooldown
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
strategy: 'weighted', // Prefer healthier endpoints
|
|
245
|
+
rotateOn: ['disconnect', 'ban-warning'],
|
|
246
|
+
maxFailures: 3,
|
|
247
|
+
deadCooldownMs: 600_000, // 10 minutes
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const sock = makeWASocket({
|
|
251
|
+
auth: state,
|
|
252
|
+
fetchAgent: rotator.currentAgent(), // Inject proxy
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Wire disconnect rotation
|
|
256
|
+
sock.ev.on('connection.update', ({ connection }) => {
|
|
257
|
+
if (connection === 'close') {
|
|
258
|
+
rotator.rotate('disconnect');
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Check stats
|
|
263
|
+
console.log(rotator.getStats());
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Advanced: Scheduled Rotation
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
const rotator = proxyRotator({
|
|
270
|
+
pool: [...proxies],
|
|
271
|
+
rotateOn: ['scheduled', 'disconnect'],
|
|
272
|
+
scheduledIntervalMs: 3_600_000, // Rotate every hour
|
|
273
|
+
strategy: 'least-recently-used',
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Auto-rotates every hour + on disconnects
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Peer Dependencies
|
|
280
|
+
|
|
281
|
+
Install proxy agent libraries for the protocols you use:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
npm install socks-proxy-agent # For SOCKS5/SOCKS5H
|
|
285
|
+
npm install http-proxy-agent # For HTTP
|
|
286
|
+
npm install https-proxy-agent # For HTTPS
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
All are optional peerDeps — only install what you need.
|
|
290
|
+
|
|
206
291
|
## LID / Phone Number Canonicalization
|
|
207
292
|
|
|
208
293
|
WhatsApp migrated to **Linked Identity (LID)** in 2024. A contact now has two JID forms:
|
|
@@ -326,6 +411,42 @@ const antiban = new AntiBan({
|
|
|
326
411
|
// No manual intervention needed
|
|
327
412
|
```
|
|
328
413
|
|
|
414
|
+
#### WPM Typing Model (v3.4+)
|
|
415
|
+
|
|
416
|
+
Real humans typing a 200-character message take 30-60 seconds with multiple `composing`/`paused` cycles. Bots that fire `composing` then immediately send (or never fire it) are detectable.
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
import { PresenceChoreographer } from 'baileys-antiban';
|
|
420
|
+
|
|
421
|
+
const choreo = new PresenceChoreographer({
|
|
422
|
+
enabled: true,
|
|
423
|
+
enableTypingModel: true,
|
|
424
|
+
typingWPM: 45, // Average human typing speed
|
|
425
|
+
typingWPMStdDev: 15, // Variance (slow/fast days)
|
|
426
|
+
thinkPauseProbability: 0.08, // 8% chance of mid-typing pause per 10 chars
|
|
427
|
+
thinkPauseMinMs: 800,
|
|
428
|
+
thinkPauseMaxMs: 3500,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Before sending
|
|
432
|
+
const messageText = "Hello, how are you doing today?";
|
|
433
|
+
const plan = choreo.computeTypingPlan(messageText.length);
|
|
434
|
+
|
|
435
|
+
// Execute typing plan (sends composing/paused updates)
|
|
436
|
+
await choreo.executeTypingPlan(sock, jid, plan);
|
|
437
|
+
|
|
438
|
+
// Send message
|
|
439
|
+
await sock.sendMessage(jid, { text: messageText });
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**How it works:**
|
|
443
|
+
1. Samples WPM from Gaussian distribution (default: 45 WPM ± 15 stdDev)
|
|
444
|
+
2. Converts to realistic typing duration: `(messageLength / charsPerSec) * 1000`
|
|
445
|
+
3. Injects "think pauses" (0.8-3.5s) mid-typing at 8% probability per 10 chars
|
|
446
|
+
4. Returns plan: `[{ state: 'composing', durationMs: 4200 }, { state: 'paused', durationMs: 950 }, ...]`
|
|
447
|
+
5. Executes plan: fires `sendPresenceUpdate('composing'/'paused')` + sleeps for each step
|
|
448
|
+
6. Supports AbortSignal for mid-plan cancellation
|
|
449
|
+
|
|
329
450
|
**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.
|
|
330
451
|
|
|
331
452
|
## baileys-antiban vs Whapi.Cloud vs DIY rate limiting
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { HealthMonitor, type HealthStatus, type HealthMonitorConfig, type BanRis
|
|
|
14
14
|
export { TimelockGuard, type TimelockGuardConfig, type TimelockState } from './timelockGuard.js';
|
|
15
15
|
export { ReplyRatioGuard, type ReplyRatioConfig, type ReplyRatioStats } from './replyRatio.js';
|
|
16
16
|
export { ContactGraphWarmer, type ContactGraphConfig, type ContactGraphStats, type ContactState } from './contactGraph.js';
|
|
17
|
-
export { PresenceChoreographer, type PresenceChoreographerConfig, type PresenceChoreographerStats } from './presenceChoreographer.js';
|
|
17
|
+
export { PresenceChoreographer, type PresenceChoreographerConfig, type PresenceChoreographerStats, type TypingPlanStep } from './presenceChoreographer.js';
|
|
18
18
|
export { RetryReasonTracker, type RetryTrackerConfig, type RetryStats, type RetryReason } from './retryTracker.js';
|
|
19
19
|
export { PostReconnectThrottle, type ReconnectThrottleConfig, type ReconnectThrottleStats } from './reconnectThrottle.js';
|
|
20
20
|
export { LidResolver, type LidResolverConfig, type LidResolverStats, type LidMapping } from './lidResolver.js';
|
|
@@ -35,3 +35,4 @@ export { messageRecovery, type MessageRecoveryConfig, type MessageRecoveryStats,
|
|
|
35
35
|
export { generateFingerprint, applyFingerprint, type DeviceFingerprint, type DeviceFingerprintConfig, } from './deviceFingerprint.js';
|
|
36
36
|
export { credsSnapshot, type CredsSnapshot, type CredsSnapshotConfig, } from './credsSnapshot.js';
|
|
37
37
|
export { readReceiptVariance, type ReadReceiptVariance, type ReadReceiptVarianceConfig, } from './readReceiptVariance.js';
|
|
38
|
+
export { proxyRotator, type ProxyEndpoint, type ProxyRotatorConfig, type ProxyRotatorStats, type ProxyRotatorHandle, } from './proxyRotator.js';
|
package/dist/index.js
CHANGED
|
@@ -47,3 +47,5 @@ export { messageRecovery } from './messageRecovery.js';
|
|
|
47
47
|
export { generateFingerprint, applyFingerprint, } from './deviceFingerprint.js';
|
|
48
48
|
export { credsSnapshot, } from './credsSnapshot.js';
|
|
49
49
|
export { readReceiptVariance, } from './readReceiptVariance.js';
|
|
50
|
+
// v3.5 new modules
|
|
51
|
+
export { proxyRotator, } from './proxyRotator.js';
|
|
@@ -39,6 +39,24 @@ export interface PresenceChoreographerConfig {
|
|
|
39
39
|
offlineGapMinMs?: number;
|
|
40
40
|
/** Max offline gap duration in ms (default: 900000 = 15min) */
|
|
41
41
|
offlineGapMaxMs?: number;
|
|
42
|
+
/** Enable WPM-based typing duration model (default: true when enabled) */
|
|
43
|
+
enableTypingModel?: boolean;
|
|
44
|
+
/** Mean typing speed in words per minute (default: 45) */
|
|
45
|
+
typingWPM?: number;
|
|
46
|
+
/** Std dev around the mean WPM (default: 15) — humans vary a lot */
|
|
47
|
+
typingWPMStdDev?: number;
|
|
48
|
+
/** Probability of "thinking pause" mid-typing per 10 chars (default: 0.08) */
|
|
49
|
+
thinkPauseProbability?: number;
|
|
50
|
+
/** Min think pause ms (default: 800) */
|
|
51
|
+
thinkPauseMinMs?: number;
|
|
52
|
+
/** Max think pause ms (default: 3500) */
|
|
53
|
+
thinkPauseMaxMs?: number;
|
|
54
|
+
/** Probability bot ALSO fires 'paused' state mid-cycle (default: 0.4) */
|
|
55
|
+
intermittentPausedProbability?: number;
|
|
56
|
+
/** Cap on total typing duration regardless of message length (default: 90_000 = 90s) */
|
|
57
|
+
typingMaxMs?: number;
|
|
58
|
+
/** Min typing duration even for short messages (default: 600 = 0.6s) */
|
|
59
|
+
typingMinMs?: number;
|
|
42
60
|
}
|
|
43
61
|
export interface PresenceChoreographerStats {
|
|
44
62
|
currentActivityFactor: number;
|
|
@@ -47,6 +65,13 @@ export interface PresenceChoreographerStats {
|
|
|
47
65
|
readReceiptsDelayed: number;
|
|
48
66
|
readReceiptsSkipped: number;
|
|
49
67
|
currentHourLocal: number;
|
|
68
|
+
typingPlansComputed: number;
|
|
69
|
+
typingPlansExecuted: number;
|
|
70
|
+
totalTypingTimeMs: number;
|
|
71
|
+
}
|
|
72
|
+
export interface TypingPlanStep {
|
|
73
|
+
state: 'composing' | 'paused';
|
|
74
|
+
durationMs: number;
|
|
50
75
|
}
|
|
51
76
|
export declare class PresenceChoreographer {
|
|
52
77
|
private config;
|
|
@@ -83,6 +108,31 @@ export declare class PresenceChoreographer {
|
|
|
83
108
|
mark: boolean;
|
|
84
109
|
delayMs: number;
|
|
85
110
|
};
|
|
111
|
+
/**
|
|
112
|
+
* Compute realistic typing duration for a message of given length.
|
|
113
|
+
* Includes Gaussian WPM variance + think-pause injection.
|
|
114
|
+
* Returns a "typing plan": array of { state, durationMs } steps the caller should execute sequentially.
|
|
115
|
+
*
|
|
116
|
+
* plan = [
|
|
117
|
+
* { state: 'composing', durationMs: 4200 },
|
|
118
|
+
* { state: 'paused', durationMs: 950 }, // think pause
|
|
119
|
+
* { state: 'composing', durationMs: 6800 },
|
|
120
|
+
* { state: 'paused', durationMs: 600 }, // brief stop before send
|
|
121
|
+
* ]
|
|
122
|
+
*/
|
|
123
|
+
computeTypingPlan(messageLength: number): TypingPlanStep[];
|
|
124
|
+
/**
|
|
125
|
+
* Execute a typing plan against a Baileys-shaped sock with sendPresenceUpdate(state, jid).
|
|
126
|
+
* Awaits each step's duration. Updates stats.
|
|
127
|
+
*
|
|
128
|
+
* await choreo.executeTypingPlan(sock, jid, plan);
|
|
129
|
+
* await sock.sendMessage(jid, content);
|
|
130
|
+
*/
|
|
131
|
+
executeTypingPlan(sock: {
|
|
132
|
+
sendPresenceUpdate: (state: string, jid: string) => Promise<void> | void;
|
|
133
|
+
}, jid: string, plan: TypingPlanStep[], options?: {
|
|
134
|
+
signal?: AbortSignal;
|
|
135
|
+
}): Promise<void>;
|
|
86
136
|
/**
|
|
87
137
|
* Get statistics.
|
|
88
138
|
*/
|
|
@@ -93,4 +143,11 @@ export declare class PresenceChoreographer {
|
|
|
93
143
|
reset(): void;
|
|
94
144
|
private getLocalHour;
|
|
95
145
|
private randomBetween;
|
|
146
|
+
private clamp;
|
|
147
|
+
/**
|
|
148
|
+
* Generate Gaussian sample using Box-Muller transform.
|
|
149
|
+
* Returns a sample from N(mean, stdDev).
|
|
150
|
+
*/
|
|
151
|
+
private gaussianSample;
|
|
152
|
+
private sleep;
|
|
96
153
|
}
|
|
@@ -26,6 +26,15 @@ const DEFAULT_CONFIG = {
|
|
|
26
26
|
offlineGapProbability: 0.03,
|
|
27
27
|
offlineGapMinMs: 300000,
|
|
28
28
|
offlineGapMaxMs: 900000,
|
|
29
|
+
enableTypingModel: true,
|
|
30
|
+
typingWPM: 45,
|
|
31
|
+
typingWPMStdDev: 15,
|
|
32
|
+
thinkPauseProbability: 0.08,
|
|
33
|
+
thinkPauseMinMs: 800,
|
|
34
|
+
thinkPauseMaxMs: 3500,
|
|
35
|
+
intermittentPausedProbability: 0.4,
|
|
36
|
+
typingMaxMs: 90000,
|
|
37
|
+
typingMinMs: 600,
|
|
29
38
|
};
|
|
30
39
|
/**
|
|
31
40
|
* Activity curves (0.1 to 1.0 multipliers by hour)
|
|
@@ -70,6 +79,9 @@ export class PresenceChoreographer {
|
|
|
70
79
|
offlineGapsInjected: 0,
|
|
71
80
|
readReceiptsDelayed: 0,
|
|
72
81
|
readReceiptsSkipped: 0,
|
|
82
|
+
typingPlansComputed: 0,
|
|
83
|
+
typingPlansExecuted: 0,
|
|
84
|
+
totalTypingTimeMs: 0,
|
|
73
85
|
};
|
|
74
86
|
constructor(config = {}) {
|
|
75
87
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
@@ -136,6 +148,106 @@ export class PresenceChoreographer {
|
|
|
136
148
|
this.stats.readReceiptsDelayed++;
|
|
137
149
|
return { mark: true, delayMs };
|
|
138
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Compute realistic typing duration for a message of given length.
|
|
153
|
+
* Includes Gaussian WPM variance + think-pause injection.
|
|
154
|
+
* Returns a "typing plan": array of { state, durationMs } steps the caller should execute sequentially.
|
|
155
|
+
*
|
|
156
|
+
* plan = [
|
|
157
|
+
* { state: 'composing', durationMs: 4200 },
|
|
158
|
+
* { state: 'paused', durationMs: 950 }, // think pause
|
|
159
|
+
* { state: 'composing', durationMs: 6800 },
|
|
160
|
+
* { state: 'paused', durationMs: 600 }, // brief stop before send
|
|
161
|
+
* ]
|
|
162
|
+
*/
|
|
163
|
+
computeTypingPlan(messageLength) {
|
|
164
|
+
if (!this.config.enabled || !this.config.enableTypingModel) {
|
|
165
|
+
return [{ state: 'composing', durationMs: this.config.typingMinMs }];
|
|
166
|
+
}
|
|
167
|
+
this.stats.typingPlansComputed++;
|
|
168
|
+
// Handle empty message
|
|
169
|
+
if (messageLength === 0) {
|
|
170
|
+
return [{ state: 'composing', durationMs: this.config.typingMinMs }];
|
|
171
|
+
}
|
|
172
|
+
// 1. Sample WPM from Gaussian distribution
|
|
173
|
+
const wpmSample = this.clamp(this.gaussianSample(this.config.typingWPM, this.config.typingWPMStdDev), 10, 120);
|
|
174
|
+
// 2. Convert to chars/sec (WPM standard: 5 chars/word)
|
|
175
|
+
const cps = (wpmSample * 5) / 60;
|
|
176
|
+
// 3. Base typing time
|
|
177
|
+
const baseMs = (messageLength / cps) * 1000;
|
|
178
|
+
// 4. Clamp to min/max
|
|
179
|
+
const targetMs = this.clamp(baseMs, this.config.typingMinMs, this.config.typingMaxMs);
|
|
180
|
+
// 5. Build plan with think pauses
|
|
181
|
+
const plan = [];
|
|
182
|
+
let remainingBudget = targetMs;
|
|
183
|
+
let position = 0;
|
|
184
|
+
// Walk through message in chunks of 10 chars
|
|
185
|
+
const chunkSize = 10;
|
|
186
|
+
const numChunks = Math.max(1, Math.ceil(messageLength / chunkSize));
|
|
187
|
+
for (let i = 0; i < numChunks && remainingBudget > 0; i++) {
|
|
188
|
+
const charsInChunk = Math.min(chunkSize, messageLength - position);
|
|
189
|
+
// Distribute remaining budget proportionally across remaining chunks
|
|
190
|
+
const remainingChunks = numChunks - i;
|
|
191
|
+
const chunkBudget = remainingBudget / remainingChunks;
|
|
192
|
+
const chunkTypingMs = Math.floor(Math.min(chunkBudget, remainingBudget));
|
|
193
|
+
if (chunkTypingMs <= 0)
|
|
194
|
+
break;
|
|
195
|
+
// Should we inject a think pause after this chunk?
|
|
196
|
+
if (i > 0 && i < numChunks - 1 && Math.random() < this.config.thinkPauseProbability) {
|
|
197
|
+
// Add composing step
|
|
198
|
+
plan.push({ state: 'composing', durationMs: chunkTypingMs });
|
|
199
|
+
remainingBudget -= chunkTypingMs;
|
|
200
|
+
// Add think pause (don't subtract from typing budget)
|
|
201
|
+
const pauseMs = this.randomBetween(this.config.thinkPauseMinMs, this.config.thinkPauseMaxMs);
|
|
202
|
+
plan.push({ state: 'paused', durationMs: pauseMs });
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
// Accumulate typing time
|
|
206
|
+
if (plan.length === 0 || plan[plan.length - 1].state === 'paused') {
|
|
207
|
+
plan.push({ state: 'composing', durationMs: chunkTypingMs });
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
plan[plan.length - 1].durationMs += chunkTypingMs;
|
|
211
|
+
}
|
|
212
|
+
remainingBudget -= chunkTypingMs;
|
|
213
|
+
}
|
|
214
|
+
position += charsInChunk;
|
|
215
|
+
}
|
|
216
|
+
// 6. Optional final pause before send
|
|
217
|
+
if (Math.random() < this.config.intermittentPausedProbability) {
|
|
218
|
+
const finalPauseMs = this.randomBetween(200, 800);
|
|
219
|
+
plan.push({ state: 'paused', durationMs: finalPauseMs });
|
|
220
|
+
}
|
|
221
|
+
// Ensure we have at least one composing step
|
|
222
|
+
if (plan.length === 0 || !plan.some(step => step.state === 'composing')) {
|
|
223
|
+
return [{ state: 'composing', durationMs: this.config.typingMinMs }];
|
|
224
|
+
}
|
|
225
|
+
return plan;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Execute a typing plan against a Baileys-shaped sock with sendPresenceUpdate(state, jid).
|
|
229
|
+
* Awaits each step's duration. Updates stats.
|
|
230
|
+
*
|
|
231
|
+
* await choreo.executeTypingPlan(sock, jid, plan);
|
|
232
|
+
* await sock.sendMessage(jid, content);
|
|
233
|
+
*/
|
|
234
|
+
async executeTypingPlan(sock, jid, plan, options) {
|
|
235
|
+
this.stats.typingPlansExecuted++;
|
|
236
|
+
for (const step of plan) {
|
|
237
|
+
// Check abort signal
|
|
238
|
+
if (options?.signal?.aborted) {
|
|
239
|
+
// Restore presence to paused before throwing
|
|
240
|
+
await Promise.resolve(sock.sendPresenceUpdate('paused', jid));
|
|
241
|
+
throw new Error('Typing plan aborted');
|
|
242
|
+
}
|
|
243
|
+
// Update presence
|
|
244
|
+
await Promise.resolve(sock.sendPresenceUpdate(step.state, jid));
|
|
245
|
+
// Sleep for duration
|
|
246
|
+
await this.sleep(step.durationMs);
|
|
247
|
+
// Track total typing time
|
|
248
|
+
this.stats.totalTypingTimeMs += step.durationMs;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
139
251
|
/**
|
|
140
252
|
* Get statistics.
|
|
141
253
|
*/
|
|
@@ -147,6 +259,9 @@ export class PresenceChoreographer {
|
|
|
147
259
|
readReceiptsDelayed: this.stats.readReceiptsDelayed,
|
|
148
260
|
readReceiptsSkipped: this.stats.readReceiptsSkipped,
|
|
149
261
|
currentHourLocal: this.getLocalHour(),
|
|
262
|
+
typingPlansComputed: this.stats.typingPlansComputed,
|
|
263
|
+
typingPlansExecuted: this.stats.typingPlansExecuted,
|
|
264
|
+
totalTypingTimeMs: this.stats.totalTypingTimeMs,
|
|
150
265
|
};
|
|
151
266
|
}
|
|
152
267
|
/**
|
|
@@ -158,6 +273,9 @@ export class PresenceChoreographer {
|
|
|
158
273
|
offlineGapsInjected: 0,
|
|
159
274
|
readReceiptsDelayed: 0,
|
|
160
275
|
readReceiptsSkipped: 0,
|
|
276
|
+
typingPlansComputed: 0,
|
|
277
|
+
typingPlansExecuted: 0,
|
|
278
|
+
totalTypingTimeMs: 0,
|
|
161
279
|
};
|
|
162
280
|
}
|
|
163
281
|
// Private helpers
|
|
@@ -184,4 +302,21 @@ export class PresenceChoreographer {
|
|
|
184
302
|
randomBetween(min, max) {
|
|
185
303
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
186
304
|
}
|
|
305
|
+
clamp(value, min, max) {
|
|
306
|
+
return Math.max(min, Math.min(max, value));
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Generate Gaussian sample using Box-Muller transform.
|
|
310
|
+
* Returns a sample from N(mean, stdDev).
|
|
311
|
+
*/
|
|
312
|
+
gaussianSample(mean, stdDev) {
|
|
313
|
+
// Box-Muller transform
|
|
314
|
+
const u1 = Math.random();
|
|
315
|
+
const u2 = Math.random();
|
|
316
|
+
const z0 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
317
|
+
return mean + z0 * stdDev;
|
|
318
|
+
}
|
|
319
|
+
sleep(ms) {
|
|
320
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
321
|
+
}
|
|
187
322
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy Rotation — Native proxy injection for Baileys with health tracking
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp's ban detection includes IP reputation scoring. Datacenter IPs (VPS)
|
|
5
|
+
* are flagged, while residential/4G proxies stay alive. No Baileys library handles
|
|
6
|
+
* native proxy injection — every implementation is DIY hacks. We close that gap.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Multi-strategy rotation (round-robin, random, LRU, weighted)
|
|
10
|
+
* - Auto-failover on endpoint failure
|
|
11
|
+
* - Scheduled rotation for proactive IP rotation
|
|
12
|
+
* - Cooldown periods between endpoint reuse
|
|
13
|
+
* - Health tracking and auto-resurrection
|
|
14
|
+
* - Lazy-loaded proxy agent dependencies (optional peerDeps)
|
|
15
|
+
*
|
|
16
|
+
* @author Kobus Wentzel <kobie@pop.co.za>
|
|
17
|
+
* @license MIT
|
|
18
|
+
*/
|
|
19
|
+
import type { Agent } from 'node:http';
|
|
20
|
+
export interface ProxyEndpoint {
|
|
21
|
+
type: 'socks5' | 'socks5h' | 'http' | 'https';
|
|
22
|
+
host: string;
|
|
23
|
+
port: number;
|
|
24
|
+
username?: string;
|
|
25
|
+
password?: string;
|
|
26
|
+
/** Optional health label — humans use this in logs */
|
|
27
|
+
label?: string;
|
|
28
|
+
/** Cooldown after last use, in ms (default: 0) */
|
|
29
|
+
cooldownMs?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface ProxyRotatorConfig {
|
|
32
|
+
/** Pool of proxy endpoints. Required. */
|
|
33
|
+
pool: ProxyEndpoint[];
|
|
34
|
+
/** Strategy for picking next proxy (default: 'round-robin') */
|
|
35
|
+
strategy?: 'round-robin' | 'random' | 'least-recently-used' | 'weighted';
|
|
36
|
+
/** Auto-rotate on these triggers (default: ['disconnect', 'ban-warning']) */
|
|
37
|
+
rotateOn?: Array<'disconnect' | 'ban-warning' | 'scheduled' | 'manual'>;
|
|
38
|
+
/** Scheduled rotation interval in ms (only if rotateOn includes 'scheduled') */
|
|
39
|
+
scheduledIntervalMs?: number;
|
|
40
|
+
/** Max consecutive failures before marking endpoint dead (default: 3) */
|
|
41
|
+
maxFailures?: number;
|
|
42
|
+
/** How long a "dead" endpoint stays out of rotation (default: 600_000 = 10min) */
|
|
43
|
+
deadCooldownMs?: number;
|
|
44
|
+
/** Logger */
|
|
45
|
+
logger?: {
|
|
46
|
+
info?: Function;
|
|
47
|
+
warn?: Function;
|
|
48
|
+
error?: Function;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export interface ProxyRotatorStats {
|
|
52
|
+
totalRotations: number;
|
|
53
|
+
rotationsByTrigger: Record<string, number>;
|
|
54
|
+
endpointHealth: Array<{
|
|
55
|
+
label: string;
|
|
56
|
+
inUse: boolean;
|
|
57
|
+
failures: number;
|
|
58
|
+
lastUsedAt: Date | null;
|
|
59
|
+
isDead: boolean;
|
|
60
|
+
}>;
|
|
61
|
+
currentEndpoint: string | null;
|
|
62
|
+
}
|
|
63
|
+
export interface ProxyRotatorHandle {
|
|
64
|
+
/** Get an Agent for the current endpoint. Use this in fetchOptions.agent or makeWASocket's options.agent */
|
|
65
|
+
currentAgent(): Agent | null;
|
|
66
|
+
/** Get the current endpoint's metadata */
|
|
67
|
+
current(): ProxyEndpoint | null;
|
|
68
|
+
/** Force rotate to next endpoint. Reason logged in stats. */
|
|
69
|
+
rotate(reason?: 'manual' | 'disconnect' | 'ban-warning' | 'scheduled'): ProxyEndpoint | null;
|
|
70
|
+
/** Mark current endpoint as failed (increments failure counter, may auto-rotate) */
|
|
71
|
+
markFailure(): void;
|
|
72
|
+
/** Clear all dead-flags (e.g. for cooldown override) */
|
|
73
|
+
resurrectAll(): void;
|
|
74
|
+
/** Stop scheduled rotation timer + dispose */
|
|
75
|
+
stop(): void;
|
|
76
|
+
/** Stats */
|
|
77
|
+
getStats(): ProxyRotatorStats;
|
|
78
|
+
}
|
|
79
|
+
export declare function proxyRotator(config: ProxyRotatorConfig): ProxyRotatorHandle;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy Rotation — Native proxy injection for Baileys with health tracking
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp's ban detection includes IP reputation scoring. Datacenter IPs (VPS)
|
|
5
|
+
* are flagged, while residential/4G proxies stay alive. No Baileys library handles
|
|
6
|
+
* native proxy injection — every implementation is DIY hacks. We close that gap.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Multi-strategy rotation (round-robin, random, LRU, weighted)
|
|
10
|
+
* - Auto-failover on endpoint failure
|
|
11
|
+
* - Scheduled rotation for proactive IP rotation
|
|
12
|
+
* - Cooldown periods between endpoint reuse
|
|
13
|
+
* - Health tracking and auto-resurrection
|
|
14
|
+
* - Lazy-loaded proxy agent dependencies (optional peerDeps)
|
|
15
|
+
*
|
|
16
|
+
* @author Kobus Wentzel <kobie@pop.co.za>
|
|
17
|
+
* @license MIT
|
|
18
|
+
*/
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
// Create require for optional peer dependency loading in ESM
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const NoopLogger = {
|
|
23
|
+
info: () => { },
|
|
24
|
+
warn: () => { },
|
|
25
|
+
error: () => { },
|
|
26
|
+
};
|
|
27
|
+
export function proxyRotator(config) {
|
|
28
|
+
const { pool, strategy = 'round-robin', rotateOn = ['disconnect', 'ban-warning'], scheduledIntervalMs = 0, maxFailures = 3, deadCooldownMs = 600_000, // 10 minutes
|
|
29
|
+
logger = NoopLogger, } = config;
|
|
30
|
+
if (!pool || pool.length === 0) {
|
|
31
|
+
throw new Error('proxyRotator: pool cannot be empty');
|
|
32
|
+
}
|
|
33
|
+
// Warn once for pool size 1
|
|
34
|
+
if (pool.length === 1) {
|
|
35
|
+
logger.warn?.('proxyRotator: pool size is 1. Rotation is a no-op.');
|
|
36
|
+
}
|
|
37
|
+
// Warn for aggressive scheduled rotation
|
|
38
|
+
if (scheduledIntervalMs > 0 && scheduledIntervalMs < 60_000) {
|
|
39
|
+
logger.warn?.(`proxyRotator: scheduledIntervalMs (${scheduledIntervalMs}ms) is < 60s. May hammer proxy provider.`);
|
|
40
|
+
}
|
|
41
|
+
// Internal state
|
|
42
|
+
const states = pool.map((endpoint) => ({
|
|
43
|
+
endpoint,
|
|
44
|
+
failures: 0,
|
|
45
|
+
lastUsedAt: null,
|
|
46
|
+
isDead: false,
|
|
47
|
+
}));
|
|
48
|
+
let currentIndex = 0;
|
|
49
|
+
let totalRotations = 0;
|
|
50
|
+
const rotationsByTrigger = {};
|
|
51
|
+
let scheduledTimer = null;
|
|
52
|
+
// Agent cache: map endpoint -> Agent (cleared on rotation)
|
|
53
|
+
const agentCache = new Map();
|
|
54
|
+
// Module cache for lazy-loaded proxy agents
|
|
55
|
+
const moduleCache = {};
|
|
56
|
+
function buildProxyUrl(endpoint) {
|
|
57
|
+
const { type, host, port, username, password } = endpoint;
|
|
58
|
+
const auth = username && password ? `${username}:${password}@` : '';
|
|
59
|
+
return `${type}://${auth}${host}:${port}`;
|
|
60
|
+
}
|
|
61
|
+
function createAgentForEndpointSync(endpoint) {
|
|
62
|
+
// Check cache first
|
|
63
|
+
if (agentCache.has(endpoint)) {
|
|
64
|
+
return agentCache.get(endpoint);
|
|
65
|
+
}
|
|
66
|
+
const url = buildProxyUrl(endpoint);
|
|
67
|
+
let agent = null;
|
|
68
|
+
try {
|
|
69
|
+
if (endpoint.type === 'socks5' || endpoint.type === 'socks5h') {
|
|
70
|
+
if (!moduleCache['socks-proxy-agent']) {
|
|
71
|
+
try {
|
|
72
|
+
moduleCache['socks-proxy-agent'] = require('socks-proxy-agent');
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
logger.error?.('socks-proxy-agent not installed. Run: npm install socks-proxy-agent');
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
agent = new moduleCache['socks-proxy-agent'].SocksProxyAgent(url);
|
|
80
|
+
}
|
|
81
|
+
else if (endpoint.type === 'http') {
|
|
82
|
+
if (!moduleCache['http-proxy-agent']) {
|
|
83
|
+
try {
|
|
84
|
+
moduleCache['http-proxy-agent'] = require('http-proxy-agent');
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
logger.error?.('http-proxy-agent not installed. Run: npm install http-proxy-agent');
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
agent = new moduleCache['http-proxy-agent'].HttpProxyAgent(url);
|
|
92
|
+
}
|
|
93
|
+
else if (endpoint.type === 'https') {
|
|
94
|
+
if (!moduleCache['https-proxy-agent']) {
|
|
95
|
+
try {
|
|
96
|
+
moduleCache['https-proxy-agent'] = require('https-proxy-agent');
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
logger.error?.('https-proxy-agent not installed. Run: npm install https-proxy-agent');
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
agent = new moduleCache['https-proxy-agent'].HttpsProxyAgent(url);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
logger.error?.(`Unknown proxy type: ${endpoint.type}`);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
// Cache the agent
|
|
110
|
+
if (agent) {
|
|
111
|
+
agentCache.set(endpoint, agent);
|
|
112
|
+
}
|
|
113
|
+
return agent;
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
logger.error?.(`Failed to create agent for ${endpoint.label || endpoint.host}: ${err}`);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function getAliveEndpoints() {
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
return states
|
|
123
|
+
.map((s, idx) => {
|
|
124
|
+
// Dead check with auto-resurrection
|
|
125
|
+
if (s.isDead && s.lastUsedAt) {
|
|
126
|
+
if (now - s.lastUsedAt.getTime() >= deadCooldownMs) {
|
|
127
|
+
s.isDead = false;
|
|
128
|
+
s.failures = 0;
|
|
129
|
+
logger.info?.(`Resurrected endpoint ${s.endpoint.label || s.endpoint.host} after cooldown`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Cooldown check
|
|
133
|
+
const cooldown = s.endpoint.cooldownMs || 0;
|
|
134
|
+
if (cooldown > 0 && s.lastUsedAt) {
|
|
135
|
+
if (now - s.lastUsedAt.getTime() < cooldown) {
|
|
136
|
+
return -1; // Still in cooldown
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return !s.isDead ? idx : -1;
|
|
140
|
+
})
|
|
141
|
+
.filter((idx) => idx !== -1);
|
|
142
|
+
}
|
|
143
|
+
function selectNextIndex(alive) {
|
|
144
|
+
if (alive.length === 0)
|
|
145
|
+
return currentIndex; // All dead, stay on current
|
|
146
|
+
if (strategy === 'round-robin') {
|
|
147
|
+
// Pick next after currentIndex in circular fashion
|
|
148
|
+
const afterCurrent = alive.filter((idx) => idx > currentIndex);
|
|
149
|
+
if (afterCurrent.length > 0)
|
|
150
|
+
return afterCurrent[0];
|
|
151
|
+
return alive[0]; // Wrap around
|
|
152
|
+
}
|
|
153
|
+
if (strategy === 'random') {
|
|
154
|
+
return alive[Math.floor(Math.random() * alive.length)];
|
|
155
|
+
}
|
|
156
|
+
if (strategy === 'least-recently-used') {
|
|
157
|
+
// Pick the one with oldest lastUsedAt (never-used = highest priority)
|
|
158
|
+
const neverUsed = alive.filter((idx) => states[idx].lastUsedAt === null);
|
|
159
|
+
if (neverUsed.length > 0) {
|
|
160
|
+
return neverUsed[0];
|
|
161
|
+
}
|
|
162
|
+
let oldestIdx = alive[0];
|
|
163
|
+
let oldestTime = states[oldestIdx].lastUsedAt.getTime();
|
|
164
|
+
for (const idx of alive) {
|
|
165
|
+
const time = states[idx].lastUsedAt.getTime();
|
|
166
|
+
if (time < oldestTime) {
|
|
167
|
+
oldestTime = time;
|
|
168
|
+
oldestIdx = idx;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return oldestIdx;
|
|
172
|
+
}
|
|
173
|
+
if (strategy === 'weighted') {
|
|
174
|
+
// Weighted by inverse failure count (healthier = more likely)
|
|
175
|
+
const weights = alive.map((idx) => {
|
|
176
|
+
const failures = states[idx].failures;
|
|
177
|
+
return 1 / (failures + 1); // Avoid divide-by-zero
|
|
178
|
+
});
|
|
179
|
+
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
180
|
+
let rand = Math.random() * totalWeight;
|
|
181
|
+
for (let i = 0; i < alive.length; i++) {
|
|
182
|
+
rand -= weights[i];
|
|
183
|
+
if (rand <= 0)
|
|
184
|
+
return alive[i];
|
|
185
|
+
}
|
|
186
|
+
return alive[alive.length - 1]; // Fallback
|
|
187
|
+
}
|
|
188
|
+
return alive[0]; // Default fallback
|
|
189
|
+
}
|
|
190
|
+
function rotateImpl(reason = 'manual') {
|
|
191
|
+
if (pool.length === 1) {
|
|
192
|
+
// No-op for single endpoint
|
|
193
|
+
return states[0].endpoint;
|
|
194
|
+
}
|
|
195
|
+
const alive = getAliveEndpoints();
|
|
196
|
+
if (alive.length === 0) {
|
|
197
|
+
logger.warn?.('All endpoints are dead. Cannot rotate.');
|
|
198
|
+
return states[currentIndex].endpoint;
|
|
199
|
+
}
|
|
200
|
+
const nextIdx = selectNextIndex(alive);
|
|
201
|
+
if (nextIdx === currentIndex && alive.length > 1) {
|
|
202
|
+
// Try to pick a different one if possible
|
|
203
|
+
const others = alive.filter((idx) => idx !== currentIndex);
|
|
204
|
+
if (others.length > 0) {
|
|
205
|
+
currentIndex = others[0];
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
currentIndex = nextIdx;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
currentIndex = nextIdx;
|
|
213
|
+
}
|
|
214
|
+
states[currentIndex].lastUsedAt = new Date();
|
|
215
|
+
totalRotations++;
|
|
216
|
+
rotationsByTrigger[reason] = (rotationsByTrigger[reason] || 0) + 1;
|
|
217
|
+
const label = states[currentIndex].endpoint.label || states[currentIndex].endpoint.host;
|
|
218
|
+
logger.info?.(`Rotated to endpoint ${label} (reason: ${reason})`);
|
|
219
|
+
return states[currentIndex].endpoint;
|
|
220
|
+
}
|
|
221
|
+
function markFailureImpl() {
|
|
222
|
+
const state = states[currentIndex];
|
|
223
|
+
state.failures++;
|
|
224
|
+
const label = state.endpoint.label || state.endpoint.host;
|
|
225
|
+
logger.warn?.(`Endpoint ${label} failed (${state.failures}/${maxFailures})`);
|
|
226
|
+
if (state.failures >= maxFailures) {
|
|
227
|
+
state.isDead = true;
|
|
228
|
+
logger.error?.(`Endpoint ${label} marked DEAD after ${maxFailures} failures`);
|
|
229
|
+
// Auto-rotate to next alive endpoint
|
|
230
|
+
const alive = getAliveEndpoints();
|
|
231
|
+
if (alive.length > 0) {
|
|
232
|
+
rotateImpl('manual'); // Trigger rotation as recovery
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function resurrectAllImpl() {
|
|
237
|
+
let count = 0;
|
|
238
|
+
for (const state of states) {
|
|
239
|
+
if (state.isDead) {
|
|
240
|
+
state.isDead = false;
|
|
241
|
+
state.failures = 0;
|
|
242
|
+
count++;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (count > 0) {
|
|
246
|
+
logger.info?.(`Resurrected ${count} dead endpoint(s)`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function stopImpl() {
|
|
250
|
+
if (scheduledTimer) {
|
|
251
|
+
clearInterval(scheduledTimer);
|
|
252
|
+
scheduledTimer = null;
|
|
253
|
+
logger.info?.('Stopped scheduled rotation timer');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function getStatsImpl() {
|
|
257
|
+
return {
|
|
258
|
+
totalRotations,
|
|
259
|
+
rotationsByTrigger: { ...rotationsByTrigger },
|
|
260
|
+
endpointHealth: states.map((s) => ({
|
|
261
|
+
label: s.endpoint.label || s.endpoint.host,
|
|
262
|
+
inUse: states[currentIndex] === s,
|
|
263
|
+
failures: s.failures,
|
|
264
|
+
lastUsedAt: s.lastUsedAt,
|
|
265
|
+
isDead: s.isDead,
|
|
266
|
+
})),
|
|
267
|
+
currentEndpoint: states[currentIndex].endpoint.label || states[currentIndex].endpoint.host,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function currentAgentImpl() {
|
|
271
|
+
const endpoint = states[currentIndex].endpoint;
|
|
272
|
+
return createAgentForEndpointSync(endpoint);
|
|
273
|
+
}
|
|
274
|
+
function currentImpl() {
|
|
275
|
+
return states[currentIndex].endpoint;
|
|
276
|
+
}
|
|
277
|
+
// Setup scheduled rotation if enabled
|
|
278
|
+
if (rotateOn.includes('scheduled') && scheduledIntervalMs > 0) {
|
|
279
|
+
scheduledTimer = setInterval(() => {
|
|
280
|
+
rotateImpl('scheduled');
|
|
281
|
+
}, scheduledIntervalMs);
|
|
282
|
+
logger.info?.(`Scheduled rotation enabled (every ${scheduledIntervalMs}ms)`);
|
|
283
|
+
}
|
|
284
|
+
// Initialize: select first endpoint
|
|
285
|
+
states[0].lastUsedAt = new Date();
|
|
286
|
+
return {
|
|
287
|
+
currentAgent: currentAgentImpl,
|
|
288
|
+
current: currentImpl,
|
|
289
|
+
rotate: rotateImpl,
|
|
290
|
+
markFailure: markFailureImpl,
|
|
291
|
+
resurrectAll: resurrectAllImpl,
|
|
292
|
+
stop: stopImpl,
|
|
293
|
+
getStats: getStatsImpl,
|
|
294
|
+
};
|
|
295
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baileys-antiban",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
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",
|
|
@@ -76,6 +76,15 @@
|
|
|
76
76
|
},
|
|
77
77
|
"@oxidezap/baileyrs": {
|
|
78
78
|
"optional": true
|
|
79
|
+
},
|
|
80
|
+
"socks-proxy-agent": {
|
|
81
|
+
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"http-proxy-agent": {
|
|
84
|
+
"optional": true
|
|
85
|
+
},
|
|
86
|
+
"https-proxy-agent": {
|
|
87
|
+
"optional": true
|
|
79
88
|
}
|
|
80
89
|
},
|
|
81
90
|
"devDependencies": {
|
|
@@ -86,5 +95,10 @@
|
|
|
86
95
|
"tsx": "^4.21.0",
|
|
87
96
|
"typescript": "^5.0.0",
|
|
88
97
|
"vitest": "^4.1.5"
|
|
98
|
+
},
|
|
99
|
+
"dependencies": {
|
|
100
|
+
"http-proxy-agent": "^9.0.0",
|
|
101
|
+
"https-proxy-agent": "^9.0.0",
|
|
102
|
+
"socks-proxy-agent": "^10.0.0"
|
|
89
103
|
}
|
|
90
104
|
}
|