baileys-antiban 3.2.0 → 3.4.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 +69 -0
- package/README.md +38 -0
- package/dist/index.d.ts +1 -1
- package/dist/jidCanonicalizer.d.ts +19 -0
- package/dist/jidCanonicalizer.js +70 -0
- package/dist/presenceChoreographer.d.ts +57 -0
- package/dist/presenceChoreographer.js +135 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,75 @@ 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.4.0] — 2026-04-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **WPM-based typing duration model** — Realistic typing indicator patterns based on human typing speed
|
|
12
|
+
- `PresenceChoreographer.computeTypingPlan(messageLength)` — Generates realistic typing plan with Gaussian WPM variance
|
|
13
|
+
- `PresenceChoreographer.executeTypingPlan(sock, jid, plan)` — Executes multi-step typing/pause cycle
|
|
14
|
+
- Gaussian sampling (Box-Muller) for WPM variance (default: 45 WPM ± 15 stdDev, clamped 10-120)
|
|
15
|
+
- Think-pause injection: 8% probability per 10 chars, 0.8-3.5s pauses (humans pause mid-thought)
|
|
16
|
+
- Intermittent `paused` state (40% probability) before send for realism
|
|
17
|
+
- Configurable min/max typing duration caps (default: 0.6s - 90s)
|
|
18
|
+
- AbortSignal support for mid-plan cancellation
|
|
19
|
+
- New stats: `typingPlansComputed`, `typingPlansExecuted`, `totalTypingTimeMs`
|
|
20
|
+
- Zero new dependencies — pure TypeScript with Box-Muller transform
|
|
21
|
+
|
|
22
|
+
### Why v3.4
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
### Usage
|
|
26
|
+
```ts
|
|
27
|
+
import { PresenceChoreographer } from 'baileys-antiban';
|
|
28
|
+
|
|
29
|
+
const choreo = new PresenceChoreographer({
|
|
30
|
+
enabled: true,
|
|
31
|
+
enableTypingModel: true,
|
|
32
|
+
typingWPM: 45, // Average human typing speed
|
|
33
|
+
typingWPMStdDev: 15, // Variance (slow/fast days)
|
|
34
|
+
thinkPauseProbability: 0.08,
|
|
35
|
+
thinkPauseMinMs: 800,
|
|
36
|
+
thinkPauseMaxMs: 3500,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Before sending a message
|
|
40
|
+
const messageText = "Hello, how are you doing today?";
|
|
41
|
+
const plan = choreo.computeTypingPlan(messageText.length);
|
|
42
|
+
|
|
43
|
+
// Execute typing plan
|
|
44
|
+
await choreo.executeTypingPlan(sock, jid, plan);
|
|
45
|
+
|
|
46
|
+
// Send actual message
|
|
47
|
+
await sock.sendMessage(jid, { text: messageText });
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Technical Details
|
|
51
|
+
- Plan structure: `Array<{ state: 'composing' | 'paused', durationMs: number }>`
|
|
52
|
+
- WPM → chars/sec conversion: `(WPM × 5) / 60` (industry standard: 5 chars/word)
|
|
53
|
+
- Think pauses are extras, not subtracted from base typing time (20% budget slack)
|
|
54
|
+
- Composing chunks coalesced when no pause injected between them
|
|
55
|
+
- AbortSignal cleanup: sets presence to `paused` before throwing
|
|
56
|
+
- All existing PresenceChoreographer features remain unchanged and backward compatible
|
|
57
|
+
|
|
58
|
+
## [3.3.0] — 2026-04-26
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
- **`JidCanonicalizer.canonicalKey(jid)`** — Returns stable thread key for DB storage/indexing
|
|
62
|
+
- Solves the split-thread bug from Baileys v7 LID migration ([#1832](https://github.com/WhiskeySockets/Baileys/issues/1832))
|
|
63
|
+
- Always returns same key regardless of whether message arrives as `@lid` or `@s.whatsapp.net`
|
|
64
|
+
- Format: `thread:<digits>` for known contacts, `thread:lid:<digits>` for unknown, `thread:group:<id>` for groups
|
|
65
|
+
- Uses learned LID↔PN mappings when available, falls back to LID form when not
|
|
66
|
+
- Handles edge cases: groups, broadcasts, newsletters, empty/null inputs
|
|
67
|
+
- Tracks stats: `canonicalKeyHits` (PN known) vs `canonicalKeyMisses` (LID only)
|
|
68
|
+
- **`docs/lid-migration.md`** — Comprehensive guide for surviving Baileys v7's LID migration
|
|
69
|
+
- Explains the three major bugs LID causes (#1832 split-thread, #1718 phone lookup, #2030 call routing)
|
|
70
|
+
- Full integration examples: learning from events, canonicalizing sends, stable DB keys
|
|
71
|
+
- Production setup with persistence, stats logging, cleanup
|
|
72
|
+
- Limitations and best practices
|
|
73
|
+
|
|
74
|
+
### Why v3.3
|
|
75
|
+
Baileys v7 made `@lid` the default JID format, but many apps still use `remoteJid` as their database thread key. This causes the same conversation to appear as two separate threads when messages arrive under different forms. `canonicalKey()` provides a stable, form-independent identifier that prevents this split-thread bug. The LID migration doc owns the narrative for the v7 transition.
|
|
76
|
+
|
|
8
77
|
## [3.2.0] — 2026-04-26
|
|
9
78
|
|
|
10
79
|
### New Features
|
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
> 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).
|
|
11
11
|
|
|
12
|
+
> **New in v3.3:** [LID Migration Guide](./docs/lid-migration.md) — survive Baileys v7's @lid default with stable thread keys.
|
|
13
|
+
|
|
12
14
|
## v2.0 New Features — Session Stability Module
|
|
13
15
|
|
|
14
16
|
### What's New in v2.0
|
|
@@ -324,6 +326,42 @@ const antiban = new AntiBan({
|
|
|
324
326
|
// No manual intervention needed
|
|
325
327
|
```
|
|
326
328
|
|
|
329
|
+
#### WPM Typing Model (v3.4+)
|
|
330
|
+
|
|
331
|
+
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.
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import { PresenceChoreographer } from 'baileys-antiban';
|
|
335
|
+
|
|
336
|
+
const choreo = new PresenceChoreographer({
|
|
337
|
+
enabled: true,
|
|
338
|
+
enableTypingModel: true,
|
|
339
|
+
typingWPM: 45, // Average human typing speed
|
|
340
|
+
typingWPMStdDev: 15, // Variance (slow/fast days)
|
|
341
|
+
thinkPauseProbability: 0.08, // 8% chance of mid-typing pause per 10 chars
|
|
342
|
+
thinkPauseMinMs: 800,
|
|
343
|
+
thinkPauseMaxMs: 3500,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Before sending
|
|
347
|
+
const messageText = "Hello, how are you doing today?";
|
|
348
|
+
const plan = choreo.computeTypingPlan(messageText.length);
|
|
349
|
+
|
|
350
|
+
// Execute typing plan (sends composing/paused updates)
|
|
351
|
+
await choreo.executeTypingPlan(sock, jid, plan);
|
|
352
|
+
|
|
353
|
+
// Send message
|
|
354
|
+
await sock.sendMessage(jid, { text: messageText });
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
**How it works:**
|
|
358
|
+
1. Samples WPM from Gaussian distribution (default: 45 WPM ± 15 stdDev)
|
|
359
|
+
2. Converts to realistic typing duration: `(messageLength / charsPerSec) * 1000`
|
|
360
|
+
3. Injects "think pauses" (0.8-3.5s) mid-typing at 8% probability per 10 chars
|
|
361
|
+
4. Returns plan: `[{ state: 'composing', durationMs: 4200 }, { state: 'paused', durationMs: 950 }, ...]`
|
|
362
|
+
5. Executes plan: fires `sendPresenceUpdate('composing'/'paused')` + sleeps for each step
|
|
363
|
+
6. Supports AbortSignal for mid-plan cancellation
|
|
364
|
+
|
|
327
365
|
**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.
|
|
328
366
|
|
|
329
367
|
## baileys-antiban vs Whapi.Cloud vs DIY rate limiting
|
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';
|
|
@@ -39,6 +39,8 @@ export interface JidCanonicalizerStats {
|
|
|
39
39
|
outboundCanonicalized: number;
|
|
40
40
|
outboundPassthrough: number;
|
|
41
41
|
inboundLearned: number;
|
|
42
|
+
canonicalKeyHits: number;
|
|
43
|
+
canonicalKeyMisses: number;
|
|
42
44
|
}
|
|
43
45
|
export declare class JidCanonicalizer {
|
|
44
46
|
private config;
|
|
@@ -54,6 +56,23 @@ export declare class JidCanonicalizer {
|
|
|
54
56
|
* Called by wrapper on every outbound send. Returns canonical JID.
|
|
55
57
|
*/
|
|
56
58
|
canonicalizeTarget(jid: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Returns a stable, canonical thread key for storage / DB indexing.
|
|
61
|
+
*
|
|
62
|
+
* Different from `canonicalizeTarget()` (which picks the right send target):
|
|
63
|
+
* - canonicalizeTarget('1234@lid') → '+27...@s.whatsapp.net' (best send target)
|
|
64
|
+
* - canonicalKey('1234@lid') → 'thread:27...' (stable thread identifier)
|
|
65
|
+
*
|
|
66
|
+
* If LID has known PN mapping → use phone-number form
|
|
67
|
+
* If only LID known → use LID stripped of suffix
|
|
68
|
+
* Always lowercase, no @-suffix, prefixed with `thread:`
|
|
69
|
+
*
|
|
70
|
+
* Apps using this as their DB key won't double-thread on LID/PN drift.
|
|
71
|
+
*
|
|
72
|
+
* @param jid - WhatsApp JID (can be PN, LID, group, or broadcast)
|
|
73
|
+
* @returns Stable thread key for DB indexing
|
|
74
|
+
*/
|
|
75
|
+
canonicalKey(jid: string): string;
|
|
57
76
|
/**
|
|
58
77
|
* Called by wrapper on messages.upsert event. Learns mappings.
|
|
59
78
|
*/
|
package/dist/jidCanonicalizer.js
CHANGED
|
@@ -35,6 +35,8 @@ export class JidCanonicalizer {
|
|
|
35
35
|
outboundCanonicalized: 0,
|
|
36
36
|
outboundPassthrough: 0,
|
|
37
37
|
inboundLearned: 0,
|
|
38
|
+
canonicalKeyHits: 0,
|
|
39
|
+
canonicalKeyMisses: 0,
|
|
38
40
|
};
|
|
39
41
|
constructor(config = {}) {
|
|
40
42
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
@@ -70,6 +72,72 @@ export class JidCanonicalizer {
|
|
|
70
72
|
}
|
|
71
73
|
return canonical;
|
|
72
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Returns a stable, canonical thread key for storage / DB indexing.
|
|
77
|
+
*
|
|
78
|
+
* Different from `canonicalizeTarget()` (which picks the right send target):
|
|
79
|
+
* - canonicalizeTarget('1234@lid') → '+27...@s.whatsapp.net' (best send target)
|
|
80
|
+
* - canonicalKey('1234@lid') → 'thread:27...' (stable thread identifier)
|
|
81
|
+
*
|
|
82
|
+
* If LID has known PN mapping → use phone-number form
|
|
83
|
+
* If only LID known → use LID stripped of suffix
|
|
84
|
+
* Always lowercase, no @-suffix, prefixed with `thread:`
|
|
85
|
+
*
|
|
86
|
+
* Apps using this as their DB key won't double-thread on LID/PN drift.
|
|
87
|
+
*
|
|
88
|
+
* @param jid - WhatsApp JID (can be PN, LID, group, or broadcast)
|
|
89
|
+
* @returns Stable thread key for DB indexing
|
|
90
|
+
*/
|
|
91
|
+
canonicalKey(jid) {
|
|
92
|
+
// Defensive: handle null/undefined/empty
|
|
93
|
+
if (!jid || typeof jid !== 'string' || jid.trim() === '') {
|
|
94
|
+
return 'thread:invalid';
|
|
95
|
+
}
|
|
96
|
+
const normalized = jid.trim().toLowerCase();
|
|
97
|
+
// Extract parts: user@domain
|
|
98
|
+
const atIndex = normalized.indexOf('@');
|
|
99
|
+
if (atIndex === -1) {
|
|
100
|
+
return 'thread:invalid';
|
|
101
|
+
}
|
|
102
|
+
const user = normalized.substring(0, atIndex);
|
|
103
|
+
const domain = normalized.substring(atIndex + 1);
|
|
104
|
+
// Handle special domains
|
|
105
|
+
if (domain === 'g.us') {
|
|
106
|
+
// Group chat
|
|
107
|
+
return `thread:group:${user}`;
|
|
108
|
+
}
|
|
109
|
+
if (domain === 'broadcast') {
|
|
110
|
+
// Broadcast list
|
|
111
|
+
return `thread:broadcast:${user}`;
|
|
112
|
+
}
|
|
113
|
+
if (domain === 'newsletter') {
|
|
114
|
+
// Newsletter (WA Channels)
|
|
115
|
+
return `thread:newsletter:${user}`;
|
|
116
|
+
}
|
|
117
|
+
// Handle @s.whatsapp.net (PN form)
|
|
118
|
+
if (domain === 's.whatsapp.net') {
|
|
119
|
+
this.stats.canonicalKeyHits++;
|
|
120
|
+
return `thread:${user}`;
|
|
121
|
+
}
|
|
122
|
+
// Handle @lid form
|
|
123
|
+
if (domain === 'lid') {
|
|
124
|
+
// Try to resolve to PN via learned mappings
|
|
125
|
+
const mapping = this.lidResolver.getMapping(normalized);
|
|
126
|
+
if (mapping?.pn) {
|
|
127
|
+
// We have a PN mapping — use it
|
|
128
|
+
const pnUser = mapping.pn.split('@')[0];
|
|
129
|
+
this.stats.canonicalKeyHits++;
|
|
130
|
+
return `thread:${pnUser}`;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// No PN known yet — use LID form
|
|
134
|
+
this.stats.canonicalKeyMisses++;
|
|
135
|
+
return `thread:lid:${user}`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Unknown domain — return generic form
|
|
139
|
+
return `thread:${domain}:${user}`;
|
|
140
|
+
}
|
|
73
141
|
/**
|
|
74
142
|
* Called by wrapper on messages.upsert event. Learns mappings.
|
|
75
143
|
*/
|
|
@@ -102,6 +170,8 @@ export class JidCanonicalizer {
|
|
|
102
170
|
outboundCanonicalized: this.stats.outboundCanonicalized,
|
|
103
171
|
outboundPassthrough: this.stats.outboundPassthrough,
|
|
104
172
|
inboundLearned: this.stats.inboundLearned,
|
|
173
|
+
canonicalKeyHits: this.stats.canonicalKeyHits,
|
|
174
|
+
canonicalKeyMisses: this.stats.canonicalKeyMisses,
|
|
105
175
|
};
|
|
106
176
|
}
|
|
107
177
|
destroy() {
|
|
@@ -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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baileys-antiban",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.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",
|