baileys-antiban 1.2.0 → 1.3.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 +15 -0
- package/README.md +76 -0
- package/dist/antiban.d.ts +26 -0
- package/dist/antiban.js +101 -2
- package/dist/contactGraph.d.ts +102 -0
- package/dist/contactGraph.js +236 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -0
- package/dist/presenceChoreographer.d.ts +96 -0
- package/dist/presenceChoreographer.js +187 -0
- package/dist/replyRatio.d.ts +91 -0
- package/dist/replyRatio.js +161 -0
- package/dist/wrapper.d.ts +15 -5
- package/dist/wrapper.js +35 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ 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
|
+
## [1.3.0] - 2026-04-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **ReplyRatioGuard** — tracks outbound:inbound ratio per contact, blocks sends to non-responsive contacts, suggests auto-replies to incoming messages
|
|
12
|
+
- **ContactGraphWarmer** — requires 1:1 handshake before bulk/group send, enforces group lurk period, daily stranger quota
|
|
13
|
+
- **PresenceChoreographer** — circadian rhythm enforcement, distraction pauses, realistic read-receipt timing
|
|
14
|
+
- All three features are **opt-in** via config and backward compatible
|
|
15
|
+
- New wrapSocket option: `autoRespondToIncoming` for hands-off reply-ratio maintenance
|
|
16
|
+
- New config fields: `replyRatio`, `contactGraph`, `presence` in `AntiBanConfig`
|
|
17
|
+
- New public methods: `onIncomingMessage()`, getters for new modules
|
|
18
|
+
- Enhanced `AntiBanStats` with optional `replyRatio`, `contactGraph`, `presence` stats
|
|
19
|
+
|
|
20
|
+
### Why
|
|
21
|
+
Based on 2025-2026 ban detection research: WhatsApp's ML models weight reply-ratio, contact-graph distance, and temporal patterns more heavily than raw volume. These modules address the three largest gaps in existing anti-ban libraries.
|
|
22
|
+
|
|
8
23
|
## [1.2.0] - 2026-04-13
|
|
9
24
|
|
|
10
25
|
### Added
|
package/README.md
CHANGED
|
@@ -6,6 +6,79 @@
|
|
|
6
6
|
|
|
7
7
|
Anti-ban middleware for [Baileys](https://github.com/WhiskeySockets/Baileys) — protect your WhatsApp number with human-like messaging patterns.
|
|
8
8
|
|
|
9
|
+
## v1.3 New Features
|
|
10
|
+
|
|
11
|
+
### ReplyRatioGuard
|
|
12
|
+
Tracks outbound:inbound message ratio per contact. Blocks sends to non-responsive contacts to avoid "spray-and-pray" ban patterns. Optionally suggests auto-replies to maintain healthy engagement.
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { AntiBan } from 'baileys-antiban';
|
|
16
|
+
|
|
17
|
+
const antiban = new AntiBan({
|
|
18
|
+
replyRatio: {
|
|
19
|
+
enabled: true,
|
|
20
|
+
minRatio: 0.10, // Block sends to contacts with <10% reply rate
|
|
21
|
+
minMessagesBeforeEnforce: 5, // Enforce after 5 outbound messages
|
|
22
|
+
cooldownHoursOnViolation: 24, // 24h cooldown on ratio violation
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Handle incoming messages to track replies
|
|
27
|
+
sock.ev.on('messages.upsert', ({ messages }) => {
|
|
28
|
+
for (const msg of messages) {
|
|
29
|
+
if (!msg.key.fromMe) {
|
|
30
|
+
const suggestion = antiban.onIncomingMessage(msg.key.remoteJid);
|
|
31
|
+
if (suggestion.shouldReply) {
|
|
32
|
+
// Optionally auto-reply with suggestion.suggestedText
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### ContactGraphWarmer
|
|
40
|
+
Requires 1:1 handshake before bulk/group sends. Enforces group lurk period (don't spam immediately after joining). Caps daily new-contact messaging.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
const antiban = new AntiBan({
|
|
44
|
+
contactGraph: {
|
|
45
|
+
enabled: true,
|
|
46
|
+
requireHandshakeBeforeGroupSend: true,
|
|
47
|
+
handshakeMinDelayMs: 3600000, // 1h between handshake and first real message
|
|
48
|
+
groupLurkPeriodMs: 43200000, // 12h lurk before first group send
|
|
49
|
+
maxStrangerMessagesPerDay: 5, // Max 5 new contacts per day
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Mark handshake sent/complete manually
|
|
54
|
+
antiban.contactGraph.markHandshakeSent(jid);
|
|
55
|
+
antiban.contactGraph.markHandshakeComplete(jid);
|
|
56
|
+
|
|
57
|
+
// Or auto-register known contacts on incoming messages
|
|
58
|
+
// (enabled by default with autoRegisterOnIncoming: true)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### PresenceChoreographer
|
|
62
|
+
Adds circadian rhythm to sending patterns (slower at night, faster during business hours). Injects realistic distraction pauses, offline gaps, and read-receipt timing variations.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
const antiban = new AntiBan({
|
|
66
|
+
presence: {
|
|
67
|
+
enabled: true,
|
|
68
|
+
enableCircadianRhythm: true,
|
|
69
|
+
timezone: 'Africa/Johannesburg',
|
|
70
|
+
activityCurve: 'office', // 'office' | 'social' | 'global'
|
|
71
|
+
distractionPauseProbability: 0.05, // 5% chance per send to pause 5-20min
|
|
72
|
+
offlineGapProbability: 0.03, // 3% chance to go offline 5-15min
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Delays are automatically adjusted based on local time-of-day
|
|
77
|
+
// No manual intervention needed
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**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.
|
|
81
|
+
|
|
9
82
|
## Why?
|
|
10
83
|
|
|
11
84
|
WhatsApp bans numbers that behave like bots. This library makes your Baileys bot behave like a human:
|
|
@@ -16,6 +89,9 @@ WhatsApp bans numbers that behave like bots. This library makes your Baileys bot
|
|
|
16
89
|
- **Timelock handling** for 463 reachout errors
|
|
17
90
|
- **Auto-pause** when risk gets too high
|
|
18
91
|
- **Drop-in wrapper** — one line to protect your existing bot
|
|
92
|
+
- **Reply ratio tracking** (v1.3) — blocks sends to non-responsive contacts
|
|
93
|
+
- **Contact graph enforcement** (v1.3) — requires handshakes before bulk/group sends
|
|
94
|
+
- **Circadian rhythm** (v1.3) — realistic time-of-day activity patterns
|
|
19
95
|
|
|
20
96
|
## Installation
|
|
21
97
|
|
package/dist/antiban.d.ts
CHANGED
|
@@ -17,11 +17,17 @@ import { type RateLimiterConfig, type RateLimiterStats } from './rateLimiter.js'
|
|
|
17
17
|
import { type WarmUpConfig, type WarmUpState, type WarmUpStatus } from './warmup.js';
|
|
18
18
|
import { type HealthMonitorConfig, type HealthStatus } from './health.js';
|
|
19
19
|
import { TimelockGuard, type TimelockGuardConfig } from './timelockGuard.js';
|
|
20
|
+
import { ReplyRatioGuard, type ReplyRatioConfig, type ReplyRatioStats } from './replyRatio.js';
|
|
21
|
+
import { ContactGraphWarmer, type ContactGraphConfig, type ContactGraphStats } from './contactGraph.js';
|
|
22
|
+
import { PresenceChoreographer, type PresenceChoreographerConfig, type PresenceChoreographerStats } from './presenceChoreographer.js';
|
|
20
23
|
export interface AntiBanConfig {
|
|
21
24
|
rateLimiter?: Partial<RateLimiterConfig>;
|
|
22
25
|
warmUp?: Partial<WarmUpConfig>;
|
|
23
26
|
health?: Partial<HealthMonitorConfig>;
|
|
24
27
|
timelock?: Partial<TimelockGuardConfig>;
|
|
28
|
+
replyRatio?: Partial<ReplyRatioConfig>;
|
|
29
|
+
contactGraph?: Partial<ContactGraphConfig>;
|
|
30
|
+
presence?: Partial<PresenceChoreographerConfig>;
|
|
25
31
|
/** Log warnings and blocks to console (default: true) */
|
|
26
32
|
logging?: boolean;
|
|
27
33
|
}
|
|
@@ -39,12 +45,18 @@ export interface AntiBanStats {
|
|
|
39
45
|
health: HealthStatus;
|
|
40
46
|
warmUp: WarmUpStatus;
|
|
41
47
|
rateLimiter: RateLimiterStats;
|
|
48
|
+
replyRatio?: ReplyRatioStats;
|
|
49
|
+
contactGraph?: ContactGraphStats;
|
|
50
|
+
presence?: PresenceChoreographerStats;
|
|
42
51
|
}
|
|
43
52
|
export declare class AntiBan {
|
|
44
53
|
private rateLimiter;
|
|
45
54
|
private warmUp;
|
|
46
55
|
private health;
|
|
47
56
|
private timelockGuard;
|
|
57
|
+
private replyRatioGuard;
|
|
58
|
+
private contactGraphWarmer;
|
|
59
|
+
private presenceChoreographer;
|
|
48
60
|
private logging;
|
|
49
61
|
private stats;
|
|
50
62
|
constructor(config?: AntiBanConfig, warmUpState?: WarmUpState);
|
|
@@ -70,12 +82,26 @@ export declare class AntiBan {
|
|
|
70
82
|
* Record a successful reconnection
|
|
71
83
|
*/
|
|
72
84
|
onReconnect(): void;
|
|
85
|
+
/**
|
|
86
|
+
* Handle incoming message — record in reply ratio + contact graph.
|
|
87
|
+
* Returns suggested reply if reply ratio suggests auto-reply.
|
|
88
|
+
*/
|
|
89
|
+
onIncomingMessage(jid: string, msgText?: string): {
|
|
90
|
+
shouldReply: boolean;
|
|
91
|
+
suggestedText?: string;
|
|
92
|
+
};
|
|
73
93
|
/**
|
|
74
94
|
* Get comprehensive stats
|
|
75
95
|
*/
|
|
76
96
|
getStats(): AntiBanStats;
|
|
77
97
|
/** Get the timelock guard for direct access */
|
|
78
98
|
get timelock(): TimelockGuard;
|
|
99
|
+
/** Get the reply ratio guard for direct access */
|
|
100
|
+
get replyRatio(): ReplyRatioGuard;
|
|
101
|
+
/** Get the contact graph warmer for direct access */
|
|
102
|
+
get contactGraph(): ContactGraphWarmer;
|
|
103
|
+
/** Get the presence choreographer for direct access */
|
|
104
|
+
get presence(): PresenceChoreographer;
|
|
79
105
|
/**
|
|
80
106
|
* Export warm-up state for persistence between restarts
|
|
81
107
|
*/
|
package/dist/antiban.js
CHANGED
|
@@ -17,11 +17,17 @@ import { RateLimiter } from './rateLimiter.js';
|
|
|
17
17
|
import { WarmUp } from './warmup.js';
|
|
18
18
|
import { HealthMonitor } from './health.js';
|
|
19
19
|
import { TimelockGuard } from './timelockGuard.js';
|
|
20
|
+
import { ReplyRatioGuard } from './replyRatio.js';
|
|
21
|
+
import { ContactGraphWarmer } from './contactGraph.js';
|
|
22
|
+
import { PresenceChoreographer } from './presenceChoreographer.js';
|
|
20
23
|
export class AntiBan {
|
|
21
24
|
rateLimiter;
|
|
22
25
|
warmUp;
|
|
23
26
|
health;
|
|
24
27
|
timelockGuard;
|
|
28
|
+
replyRatioGuard;
|
|
29
|
+
contactGraphWarmer;
|
|
30
|
+
presenceChoreographer;
|
|
25
31
|
logging;
|
|
26
32
|
stats = {
|
|
27
33
|
messagesAllowed: 0,
|
|
@@ -60,6 +66,9 @@ export class AntiBan {
|
|
|
60
66
|
config.timelock?.onTimelockLifted?.(state);
|
|
61
67
|
},
|
|
62
68
|
});
|
|
69
|
+
this.replyRatioGuard = new ReplyRatioGuard(config.replyRatio);
|
|
70
|
+
this.contactGraphWarmer = new ContactGraphWarmer(config.contactGraph);
|
|
71
|
+
this.presenceChoreographer = new PresenceChoreographer(config.presence);
|
|
63
72
|
}
|
|
64
73
|
/**
|
|
65
74
|
* Check if a message can be sent and get required delay.
|
|
@@ -109,8 +118,36 @@ export class AntiBan {
|
|
|
109
118
|
warmUpDay: warmUpStatus.day,
|
|
110
119
|
};
|
|
111
120
|
}
|
|
121
|
+
// Contact graph check
|
|
122
|
+
const contactGraphDecision = this.contactGraphWarmer.canMessage(recipient);
|
|
123
|
+
if (!contactGraphDecision.allowed) {
|
|
124
|
+
this.stats.messagesBlocked++;
|
|
125
|
+
if (this.logging) {
|
|
126
|
+
console.log(`[baileys-antiban] 📊 BLOCKED — contact graph: ${contactGraphDecision.reason}`);
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
allowed: false,
|
|
130
|
+
delayMs: 0,
|
|
131
|
+
reason: `Contact graph: ${contactGraphDecision.reason}`,
|
|
132
|
+
health: healthStatus,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// Reply ratio check
|
|
136
|
+
const replyRatioDecision = this.replyRatioGuard.beforeSend(recipient);
|
|
137
|
+
if (!replyRatioDecision.allowed) {
|
|
138
|
+
this.stats.messagesBlocked++;
|
|
139
|
+
if (this.logging) {
|
|
140
|
+
console.log(`[baileys-antiban] 💬 BLOCKED — reply ratio: ${replyRatioDecision.reason}`);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
allowed: false,
|
|
144
|
+
delayMs: 0,
|
|
145
|
+
reason: `Reply ratio: ${replyRatioDecision.reason}`,
|
|
146
|
+
health: healthStatus,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
112
149
|
// Rate limiter delay
|
|
113
|
-
|
|
150
|
+
let delay = await this.rateLimiter.getDelay(recipient, content);
|
|
114
151
|
if (delay === -1) {
|
|
115
152
|
this.stats.messagesBlocked++;
|
|
116
153
|
if (this.logging) {
|
|
@@ -123,6 +160,29 @@ export class AntiBan {
|
|
|
123
160
|
health: healthStatus,
|
|
124
161
|
};
|
|
125
162
|
}
|
|
163
|
+
// Apply circadian rhythm multiplier to delay
|
|
164
|
+
const activityFactor = this.presenceChoreographer.getCurrentActivityFactor();
|
|
165
|
+
if (activityFactor < 1.0) {
|
|
166
|
+
// Lower activity = longer delays (cap at 5x)
|
|
167
|
+
const multiplier = Math.min(5, 1 / activityFactor);
|
|
168
|
+
delay = Math.floor(delay * multiplier);
|
|
169
|
+
}
|
|
170
|
+
// Roll for distraction pause
|
|
171
|
+
const distractionCheck = this.presenceChoreographer.shouldPauseForDistraction();
|
|
172
|
+
if (distractionCheck.pause) {
|
|
173
|
+
delay += distractionCheck.durationMs;
|
|
174
|
+
if (this.logging) {
|
|
175
|
+
console.log(`[baileys-antiban] ⏸️ Distraction pause: +${Math.floor(distractionCheck.durationMs / 60000)}min`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Roll for offline gap
|
|
179
|
+
const offlineCheck = this.presenceChoreographer.shouldTakeOfflineGap();
|
|
180
|
+
if (offlineCheck.offline) {
|
|
181
|
+
delay += offlineCheck.durationMs;
|
|
182
|
+
if (this.logging) {
|
|
183
|
+
console.log(`[baileys-antiban] 📴 Offline gap: +${Math.floor(offlineCheck.durationMs / 60000)}min`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
126
186
|
this.stats.totalDelayMs += delay;
|
|
127
187
|
return {
|
|
128
188
|
allowed: true,
|
|
@@ -137,6 +197,7 @@ export class AntiBan {
|
|
|
137
197
|
afterSend(recipient, content) {
|
|
138
198
|
this.rateLimiter.record(recipient, content);
|
|
139
199
|
this.warmUp.record();
|
|
200
|
+
this.replyRatioGuard.recordSent(recipient);
|
|
140
201
|
this.stats.messagesAllowed++;
|
|
141
202
|
}
|
|
142
203
|
/**
|
|
@@ -157,21 +218,53 @@ export class AntiBan {
|
|
|
157
218
|
onReconnect() {
|
|
158
219
|
this.health.recordReconnect();
|
|
159
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Handle incoming message — record in reply ratio + contact graph.
|
|
223
|
+
* Returns suggested reply if reply ratio suggests auto-reply.
|
|
224
|
+
*/
|
|
225
|
+
onIncomingMessage(jid, msgText) {
|
|
226
|
+
this.replyRatioGuard.recordReceived(jid);
|
|
227
|
+
this.contactGraphWarmer.onIncomingMessage(jid);
|
|
228
|
+
return this.replyRatioGuard.suggestReply(jid, msgText);
|
|
229
|
+
}
|
|
160
230
|
/**
|
|
161
231
|
* Get comprehensive stats
|
|
162
232
|
*/
|
|
163
233
|
getStats() {
|
|
164
|
-
|
|
234
|
+
const stats = {
|
|
165
235
|
...this.stats,
|
|
166
236
|
health: this.health.getStatus(),
|
|
167
237
|
warmUp: this.warmUp.getStatus(),
|
|
168
238
|
rateLimiter: this.rateLimiter.getStats(),
|
|
169
239
|
};
|
|
240
|
+
// Only include new stats if enabled
|
|
241
|
+
if (this.replyRatioGuard['config']?.enabled) {
|
|
242
|
+
stats.replyRatio = this.replyRatioGuard.getStats();
|
|
243
|
+
}
|
|
244
|
+
if (this.contactGraphWarmer['config']?.enabled) {
|
|
245
|
+
stats.contactGraph = this.contactGraphWarmer.getStats();
|
|
246
|
+
}
|
|
247
|
+
if (this.presenceChoreographer['config']?.enabled) {
|
|
248
|
+
stats.presence = this.presenceChoreographer.getStats();
|
|
249
|
+
}
|
|
250
|
+
return stats;
|
|
170
251
|
}
|
|
171
252
|
/** Get the timelock guard for direct access */
|
|
172
253
|
get timelock() {
|
|
173
254
|
return this.timelockGuard;
|
|
174
255
|
}
|
|
256
|
+
/** Get the reply ratio guard for direct access */
|
|
257
|
+
get replyRatio() {
|
|
258
|
+
return this.replyRatioGuard;
|
|
259
|
+
}
|
|
260
|
+
/** Get the contact graph warmer for direct access */
|
|
261
|
+
get contactGraph() {
|
|
262
|
+
return this.contactGraphWarmer;
|
|
263
|
+
}
|
|
264
|
+
/** Get the presence choreographer for direct access */
|
|
265
|
+
get presence() {
|
|
266
|
+
return this.presenceChoreographer;
|
|
267
|
+
}
|
|
175
268
|
/**
|
|
176
269
|
* Export warm-up state for persistence between restarts
|
|
177
270
|
*/
|
|
@@ -203,6 +296,9 @@ export class AntiBan {
|
|
|
203
296
|
this.timelockGuard.reset();
|
|
204
297
|
this.health.reset();
|
|
205
298
|
this.warmUp.reset();
|
|
299
|
+
this.replyRatioGuard.reset();
|
|
300
|
+
this.contactGraphWarmer.reset();
|
|
301
|
+
this.presenceChoreographer.reset();
|
|
206
302
|
this.stats = { messagesAllowed: 0, messagesBlocked: 0, totalDelayMs: 0 };
|
|
207
303
|
if (this.logging) {
|
|
208
304
|
console.log('[baileys-antiban] 🔄 Reset — starting fresh warm-up');
|
|
@@ -214,6 +310,9 @@ export class AntiBan {
|
|
|
214
310
|
*/
|
|
215
311
|
destroy() {
|
|
216
312
|
this.timelockGuard.reset(); // Clears the resumeTimer
|
|
313
|
+
this.replyRatioGuard.reset();
|
|
314
|
+
this.contactGraphWarmer.reset();
|
|
315
|
+
this.presenceChoreographer.reset();
|
|
217
316
|
if (this.logging) {
|
|
218
317
|
console.log('[baileys-antiban] 🧹 Destroyed — all timers cleared');
|
|
219
318
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contact Graph Warmer — Requires 1:1 handshake before group/bulk sends
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp's ML models weight "social graph distance" heavily. Accounts that
|
|
5
|
+
* message strangers (contacts who never replied) have higher ban risk.
|
|
6
|
+
*
|
|
7
|
+
* This module:
|
|
8
|
+
* - Tracks contact state: stranger → handshake_sent → handshake_complete → known
|
|
9
|
+
* - Blocks sends to strangers unless handshake completed
|
|
10
|
+
* - Enforces group lurk period (don't send immediately after joining)
|
|
11
|
+
* - Caps daily new-contact messaging (prevent spray-and-pray patterns)
|
|
12
|
+
* - Auto-registers inbound senders as "known contacts"
|
|
13
|
+
*
|
|
14
|
+
* Research: 2025 ban waves correlated with accounts joining groups + spamming
|
|
15
|
+
* instantly. 12-24h lurk period significantly reduced bans.
|
|
16
|
+
*/
|
|
17
|
+
export interface ContactGraphConfig {
|
|
18
|
+
/** Enable contact graph enforcement (default: false — opt-in) */
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
/** Require handshake completion before group/bulk sends (default: true) */
|
|
21
|
+
requireHandshakeBeforeGroupSend?: boolean;
|
|
22
|
+
/** Min wait time (ms) between handshake and first real message (default: 3600000 = 1h) */
|
|
23
|
+
handshakeMinDelayMs?: number;
|
|
24
|
+
/** Group lurk period (ms) before first send (default: 43200000 = 12h) */
|
|
25
|
+
groupLurkPeriodMs?: number;
|
|
26
|
+
/** Max new-contact messages per day (default: 5) */
|
|
27
|
+
maxStrangerMessagesPerDay?: number;
|
|
28
|
+
/** Auto-register inbound senders as known contacts (default: true) */
|
|
29
|
+
autoRegisterOnIncoming?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export type ContactState = 'stranger' | 'handshake_sent' | 'handshake_complete' | 'known';
|
|
32
|
+
export interface ContactGraphStats {
|
|
33
|
+
knownContacts: number;
|
|
34
|
+
pendingHandshakes: number;
|
|
35
|
+
strangersToday: number;
|
|
36
|
+
groupsJoined: Array<{
|
|
37
|
+
groupJid: string;
|
|
38
|
+
joinedAt: number;
|
|
39
|
+
firstSendUnlocksAt: number;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
export declare class ContactGraphWarmer {
|
|
43
|
+
private config;
|
|
44
|
+
private contacts;
|
|
45
|
+
private groups;
|
|
46
|
+
private strangerMessagesToday;
|
|
47
|
+
private lastStrangerResetDay;
|
|
48
|
+
constructor(config?: ContactGraphConfig);
|
|
49
|
+
/**
|
|
50
|
+
* Check if message can be sent to this contact/group.
|
|
51
|
+
* Returns { allowed: false, needsHandshake: true } if handshake required.
|
|
52
|
+
*/
|
|
53
|
+
canMessage(jid: string): {
|
|
54
|
+
allowed: boolean;
|
|
55
|
+
reason?: string;
|
|
56
|
+
needsHandshake?: boolean;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Mark handshake as sent to this contact.
|
|
60
|
+
*/
|
|
61
|
+
markHandshakeSent(jid: string): void;
|
|
62
|
+
/**
|
|
63
|
+
* Mark handshake as complete with this contact.
|
|
64
|
+
*/
|
|
65
|
+
markHandshakeComplete(jid: string): void;
|
|
66
|
+
/**
|
|
67
|
+
* Register a contact as known (skip handshake requirement).
|
|
68
|
+
*/
|
|
69
|
+
registerKnownContact(jid: string): void;
|
|
70
|
+
/**
|
|
71
|
+
* Register a group join event.
|
|
72
|
+
*/
|
|
73
|
+
registerGroupJoin(groupJid: string): void;
|
|
74
|
+
/**
|
|
75
|
+
* Get contact state.
|
|
76
|
+
*/
|
|
77
|
+
getContactState(jid: string): ContactState;
|
|
78
|
+
/**
|
|
79
|
+
* Handle incoming message — auto-register if enabled.
|
|
80
|
+
*/
|
|
81
|
+
onIncomingMessage(jid: string): void;
|
|
82
|
+
/**
|
|
83
|
+
* Get statistics.
|
|
84
|
+
*/
|
|
85
|
+
getStats(): ContactGraphStats;
|
|
86
|
+
/**
|
|
87
|
+
* Reset all state.
|
|
88
|
+
*/
|
|
89
|
+
reset(): void;
|
|
90
|
+
/**
|
|
91
|
+
* Export state for persistence.
|
|
92
|
+
*/
|
|
93
|
+
exportState(): object;
|
|
94
|
+
/**
|
|
95
|
+
* Restore state from persistence.
|
|
96
|
+
*/
|
|
97
|
+
restoreState(state: any): void;
|
|
98
|
+
private isGroup;
|
|
99
|
+
private getCurrentDay;
|
|
100
|
+
private checkGroupMessage;
|
|
101
|
+
private checkIndividualMessage;
|
|
102
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contact Graph Warmer — Requires 1:1 handshake before group/bulk sends
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp's ML models weight "social graph distance" heavily. Accounts that
|
|
5
|
+
* message strangers (contacts who never replied) have higher ban risk.
|
|
6
|
+
*
|
|
7
|
+
* This module:
|
|
8
|
+
* - Tracks contact state: stranger → handshake_sent → handshake_complete → known
|
|
9
|
+
* - Blocks sends to strangers unless handshake completed
|
|
10
|
+
* - Enforces group lurk period (don't send immediately after joining)
|
|
11
|
+
* - Caps daily new-contact messaging (prevent spray-and-pray patterns)
|
|
12
|
+
* - Auto-registers inbound senders as "known contacts"
|
|
13
|
+
*
|
|
14
|
+
* Research: 2025 ban waves correlated with accounts joining groups + spamming
|
|
15
|
+
* instantly. 12-24h lurk period significantly reduced bans.
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_CONFIG = {
|
|
18
|
+
enabled: false,
|
|
19
|
+
requireHandshakeBeforeGroupSend: true,
|
|
20
|
+
handshakeMinDelayMs: 3600000, // 1 hour
|
|
21
|
+
groupLurkPeriodMs: 43200000, // 12 hours
|
|
22
|
+
maxStrangerMessagesPerDay: 5,
|
|
23
|
+
autoRegisterOnIncoming: true,
|
|
24
|
+
};
|
|
25
|
+
export class ContactGraphWarmer {
|
|
26
|
+
config;
|
|
27
|
+
contacts = new Map();
|
|
28
|
+
groups = new Map();
|
|
29
|
+
strangerMessagesToday = 0;
|
|
30
|
+
lastStrangerResetDay = this.getCurrentDay();
|
|
31
|
+
constructor(config = {}) {
|
|
32
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if message can be sent to this contact/group.
|
|
36
|
+
* Returns { allowed: false, needsHandshake: true } if handshake required.
|
|
37
|
+
*/
|
|
38
|
+
canMessage(jid) {
|
|
39
|
+
if (!this.config.enabled) {
|
|
40
|
+
return { allowed: true };
|
|
41
|
+
}
|
|
42
|
+
// Reset daily stranger counter at UTC midnight
|
|
43
|
+
const currentDay = this.getCurrentDay();
|
|
44
|
+
if (currentDay !== this.lastStrangerResetDay) {
|
|
45
|
+
this.strangerMessagesToday = 0;
|
|
46
|
+
this.lastStrangerResetDay = currentDay;
|
|
47
|
+
}
|
|
48
|
+
// Handle groups
|
|
49
|
+
if (this.isGroup(jid)) {
|
|
50
|
+
return this.checkGroupMessage(jid);
|
|
51
|
+
}
|
|
52
|
+
// Handle individual contacts
|
|
53
|
+
return this.checkIndividualMessage(jid);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Mark handshake as sent to this contact.
|
|
57
|
+
*/
|
|
58
|
+
markHandshakeSent(jid) {
|
|
59
|
+
if (!this.config.enabled)
|
|
60
|
+
return;
|
|
61
|
+
if (this.isGroup(jid))
|
|
62
|
+
return;
|
|
63
|
+
const record = this.contacts.get(jid) || { state: 'stranger' };
|
|
64
|
+
record.state = 'handshake_sent';
|
|
65
|
+
record.handshakeSentAt = Date.now();
|
|
66
|
+
this.contacts.set(jid, record);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Mark handshake as complete with this contact.
|
|
70
|
+
*/
|
|
71
|
+
markHandshakeComplete(jid) {
|
|
72
|
+
if (!this.config.enabled)
|
|
73
|
+
return;
|
|
74
|
+
if (this.isGroup(jid))
|
|
75
|
+
return;
|
|
76
|
+
const record = this.contacts.get(jid) || { state: 'stranger' };
|
|
77
|
+
record.state = 'handshake_complete';
|
|
78
|
+
this.contacts.set(jid, record);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Register a contact as known (skip handshake requirement).
|
|
82
|
+
*/
|
|
83
|
+
registerKnownContact(jid) {
|
|
84
|
+
if (!this.config.enabled)
|
|
85
|
+
return;
|
|
86
|
+
if (this.isGroup(jid))
|
|
87
|
+
return;
|
|
88
|
+
const record = this.contacts.get(jid) || { state: 'stranger' };
|
|
89
|
+
record.state = 'known';
|
|
90
|
+
this.contacts.set(jid, record);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Register a group join event.
|
|
94
|
+
*/
|
|
95
|
+
registerGroupJoin(groupJid) {
|
|
96
|
+
if (!this.config.enabled)
|
|
97
|
+
return;
|
|
98
|
+
if (!this.isGroup(groupJid))
|
|
99
|
+
return;
|
|
100
|
+
this.groups.set(groupJid, { joinedAt: Date.now() });
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Get contact state.
|
|
104
|
+
*/
|
|
105
|
+
getContactState(jid) {
|
|
106
|
+
if (this.isGroup(jid))
|
|
107
|
+
return 'known'; // Groups don't have handshake states
|
|
108
|
+
return this.contacts.get(jid)?.state || 'stranger';
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Handle incoming message — auto-register if enabled.
|
|
112
|
+
*/
|
|
113
|
+
onIncomingMessage(jid) {
|
|
114
|
+
if (!this.config.enabled)
|
|
115
|
+
return;
|
|
116
|
+
if (this.isGroup(jid))
|
|
117
|
+
return;
|
|
118
|
+
if (this.config.autoRegisterOnIncoming) {
|
|
119
|
+
this.registerKnownContact(jid);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Get statistics.
|
|
124
|
+
*/
|
|
125
|
+
getStats() {
|
|
126
|
+
const knownContacts = Array.from(this.contacts.values()).filter(c => c.state === 'known').length;
|
|
127
|
+
const pendingHandshakes = Array.from(this.contacts.values()).filter(c => c.state === 'handshake_sent').length;
|
|
128
|
+
const groupsJoined = Array.from(this.groups.entries()).map(([groupJid, record]) => ({
|
|
129
|
+
groupJid,
|
|
130
|
+
joinedAt: record.joinedAt,
|
|
131
|
+
firstSendUnlocksAt: record.joinedAt + this.config.groupLurkPeriodMs,
|
|
132
|
+
}));
|
|
133
|
+
return {
|
|
134
|
+
knownContacts,
|
|
135
|
+
pendingHandshakes,
|
|
136
|
+
strangersToday: this.strangerMessagesToday,
|
|
137
|
+
groupsJoined,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Reset all state.
|
|
142
|
+
*/
|
|
143
|
+
reset() {
|
|
144
|
+
this.contacts.clear();
|
|
145
|
+
this.groups.clear();
|
|
146
|
+
this.strangerMessagesToday = 0;
|
|
147
|
+
this.lastStrangerResetDay = this.getCurrentDay();
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Export state for persistence.
|
|
151
|
+
*/
|
|
152
|
+
exportState() {
|
|
153
|
+
return {
|
|
154
|
+
contacts: Array.from(this.contacts.entries()),
|
|
155
|
+
groups: Array.from(this.groups.entries()),
|
|
156
|
+
strangerMessagesToday: this.strangerMessagesToday,
|
|
157
|
+
lastStrangerResetDay: this.lastStrangerResetDay,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Restore state from persistence.
|
|
162
|
+
*/
|
|
163
|
+
restoreState(state) {
|
|
164
|
+
if (state?.contacts && Array.isArray(state.contacts)) {
|
|
165
|
+
this.contacts = new Map(state.contacts);
|
|
166
|
+
}
|
|
167
|
+
if (state?.groups && Array.isArray(state.groups)) {
|
|
168
|
+
this.groups = new Map(state.groups);
|
|
169
|
+
}
|
|
170
|
+
if (typeof state?.strangerMessagesToday === 'number') {
|
|
171
|
+
this.strangerMessagesToday = state.strangerMessagesToday;
|
|
172
|
+
}
|
|
173
|
+
if (typeof state?.lastStrangerResetDay === 'number') {
|
|
174
|
+
this.lastStrangerResetDay = state.lastStrangerResetDay;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Private helpers
|
|
178
|
+
isGroup(jid) {
|
|
179
|
+
return jid.endsWith('@g.us');
|
|
180
|
+
}
|
|
181
|
+
getCurrentDay() {
|
|
182
|
+
return Math.floor(Date.now() / 86400000);
|
|
183
|
+
}
|
|
184
|
+
checkGroupMessage(groupJid) {
|
|
185
|
+
const record = this.groups.get(groupJid);
|
|
186
|
+
if (!record) {
|
|
187
|
+
// Group not registered — allow (assume old membership)
|
|
188
|
+
return { allowed: true };
|
|
189
|
+
}
|
|
190
|
+
const lurkEndsAt = record.joinedAt + this.config.groupLurkPeriodMs;
|
|
191
|
+
if (Date.now() < lurkEndsAt) {
|
|
192
|
+
const minutesLeft = Math.ceil((lurkEndsAt - Date.now()) / 60000);
|
|
193
|
+
return {
|
|
194
|
+
allowed: false,
|
|
195
|
+
reason: `Group lurk period not elapsed — wait ${minutesLeft} minutes`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return { allowed: true };
|
|
199
|
+
}
|
|
200
|
+
checkIndividualMessage(jid) {
|
|
201
|
+
const record = this.contacts.get(jid);
|
|
202
|
+
// Unknown contact (stranger)
|
|
203
|
+
if (!record || record.state === 'stranger') {
|
|
204
|
+
if (this.config.requireHandshakeBeforeGroupSend) {
|
|
205
|
+
// Check daily stranger quota
|
|
206
|
+
if (this.strangerMessagesToday >= this.config.maxStrangerMessagesPerDay) {
|
|
207
|
+
return {
|
|
208
|
+
allowed: false,
|
|
209
|
+
reason: `Daily new-contact limit reached (${this.config.maxStrangerMessagesPerDay})`,
|
|
210
|
+
needsHandshake: true,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
// Allow but increment counter
|
|
214
|
+
this.strangerMessagesToday++;
|
|
215
|
+
}
|
|
216
|
+
return { allowed: true, needsHandshake: true };
|
|
217
|
+
}
|
|
218
|
+
// Handshake sent — check delay
|
|
219
|
+
if (record.state === 'handshake_sent') {
|
|
220
|
+
if (!record.handshakeSentAt) {
|
|
221
|
+
// No timestamp — shouldn't happen, but allow
|
|
222
|
+
return { allowed: true };
|
|
223
|
+
}
|
|
224
|
+
const elapsed = Date.now() - record.handshakeSentAt;
|
|
225
|
+
if (elapsed < this.config.handshakeMinDelayMs) {
|
|
226
|
+
const minutesLeft = Math.ceil((this.config.handshakeMinDelayMs - elapsed) / 60000);
|
|
227
|
+
return {
|
|
228
|
+
allowed: false,
|
|
229
|
+
reason: `Handshake too recent — wait ${minutesLeft} minutes`,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// handshake_complete or known — allow
|
|
234
|
+
return { allowed: true };
|
|
235
|
+
}
|
|
236
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,10 @@ export { RateLimiter, type RateLimiterConfig, type RateLimiterStats } from './ra
|
|
|
12
12
|
export { WarmUp, type WarmUpConfig, type WarmUpState, type WarmUpStatus } from './warmup.js';
|
|
13
13
|
export { HealthMonitor, type HealthStatus, type HealthMonitorConfig, type BanRiskLevel } from './health.js';
|
|
14
14
|
export { TimelockGuard, type TimelockGuardConfig, type TimelockState } from './timelockGuard.js';
|
|
15
|
-
export {
|
|
15
|
+
export { ReplyRatioGuard, type ReplyRatioConfig, type ReplyRatioStats } from './replyRatio.js';
|
|
16
|
+
export { ContactGraphWarmer, type ContactGraphConfig, type ContactGraphStats, type ContactState } from './contactGraph.js';
|
|
17
|
+
export { PresenceChoreographer, type PresenceChoreographerConfig, type PresenceChoreographerStats } from './presenceChoreographer.js';
|
|
18
|
+
export { wrapSocket, type WrappedSocket, type WrapSocketOptions } from './wrapper.js';
|
|
16
19
|
export { MessageQueue, type QueuedMessage, type MessageQueueConfig } from './messageQueue.js';
|
|
17
20
|
export { ContentVariator, type VariatorConfig } from './contentVariator.js';
|
|
18
21
|
export { WebhookAlerts, type WebhookConfig } from './webhooks.js';
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,10 @@ export { RateLimiter } from './rateLimiter.js';
|
|
|
13
13
|
export { WarmUp } from './warmup.js';
|
|
14
14
|
export { HealthMonitor } from './health.js';
|
|
15
15
|
export { TimelockGuard } from './timelockGuard.js';
|
|
16
|
+
// v1.3 new modules
|
|
17
|
+
export { ReplyRatioGuard } from './replyRatio.js';
|
|
18
|
+
export { ContactGraphWarmer } from './contactGraph.js';
|
|
19
|
+
export { PresenceChoreographer } from './presenceChoreographer.js';
|
|
16
20
|
// Socket wrapper
|
|
17
21
|
export { wrapSocket } from './wrapper.js';
|
|
18
22
|
// Optional features
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presence Choreographer — Circadian rhythm, distraction pauses, realistic read-receipts
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp's ML models detect accounts with perfect, robotic timing patterns.
|
|
5
|
+
* This module adds realistic temporal variations:
|
|
6
|
+
* - Circadian rhythm: slower at night, faster during business hours
|
|
7
|
+
* - Distraction pauses: random 5-20min pauses (phone put down)
|
|
8
|
+
* - Offline gaps: occasional 5-15min offline periods
|
|
9
|
+
* - Read receipt timing: 3-45s delay, 15% chance to skip
|
|
10
|
+
*
|
|
11
|
+
* Research: 2025 ban analysis showed accounts with <10% timing variance were
|
|
12
|
+
* flagged at 3x rate vs accounts with circadian patterns. Human users have
|
|
13
|
+
* 40-60% variance in hourly activity.
|
|
14
|
+
*/
|
|
15
|
+
export interface PresenceChoreographerConfig {
|
|
16
|
+
/** Enable presence choreography (default: false — opt-in) */
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
/** Enable circadian rhythm enforcement (default: true when enabled) */
|
|
19
|
+
enableCircadianRhythm?: boolean;
|
|
20
|
+
/** IANA timezone for local hour calculation (default: 'UTC') */
|
|
21
|
+
timezone?: string;
|
|
22
|
+
/** Activity curve preset (default: 'office') */
|
|
23
|
+
activityCurve?: 'office' | 'social' | 'global';
|
|
24
|
+
/** Probability (0-1) of distraction pause per send (default: 0.05 = 5%) */
|
|
25
|
+
distractionPauseProbability?: number;
|
|
26
|
+
/** Min distraction pause duration in ms (default: 300000 = 5min) */
|
|
27
|
+
distractionPauseMinMs?: number;
|
|
28
|
+
/** Max distraction pause duration in ms (default: 1200000 = 20min) */
|
|
29
|
+
distractionPauseMaxMs?: number;
|
|
30
|
+
/** Min read receipt delay in ms (default: 3000 = 3s) */
|
|
31
|
+
readReceiptDelayMinMs?: number;
|
|
32
|
+
/** Max read receipt delay in ms (default: 45000 = 45s) */
|
|
33
|
+
readReceiptDelayMaxMs?: number;
|
|
34
|
+
/** Probability (0-1) of skipping read receipt (default: 0.15 = 15%) */
|
|
35
|
+
readReceiptSkipProbability?: number;
|
|
36
|
+
/** Probability (0-1) of offline gap per send (default: 0.03 = 3%) */
|
|
37
|
+
offlineGapProbability?: number;
|
|
38
|
+
/** Min offline gap duration in ms (default: 300000 = 5min) */
|
|
39
|
+
offlineGapMinMs?: number;
|
|
40
|
+
/** Max offline gap duration in ms (default: 900000 = 15min) */
|
|
41
|
+
offlineGapMaxMs?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface PresenceChoreographerStats {
|
|
44
|
+
currentActivityFactor: number;
|
|
45
|
+
distractionPausesInjected: number;
|
|
46
|
+
offlineGapsInjected: number;
|
|
47
|
+
readReceiptsDelayed: number;
|
|
48
|
+
readReceiptsSkipped: number;
|
|
49
|
+
currentHourLocal: number;
|
|
50
|
+
}
|
|
51
|
+
export declare class PresenceChoreographer {
|
|
52
|
+
private config;
|
|
53
|
+
private stats;
|
|
54
|
+
constructor(config?: PresenceChoreographerConfig);
|
|
55
|
+
/**
|
|
56
|
+
* Get current activity factor (0.1 to 1.0).
|
|
57
|
+
* Higher = more active = shorter delays.
|
|
58
|
+
* If circadian disabled, returns 1.0.
|
|
59
|
+
*/
|
|
60
|
+
getCurrentActivityFactor(): number;
|
|
61
|
+
/**
|
|
62
|
+
* Check if should pause for distraction.
|
|
63
|
+
* Returns { pause: true, durationMs: 600000 } if probability check passes.
|
|
64
|
+
*/
|
|
65
|
+
shouldPauseForDistraction(): {
|
|
66
|
+
pause: boolean;
|
|
67
|
+
durationMs: number;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Check if should take offline gap.
|
|
71
|
+
* Returns { offline: true, durationMs: 600000 } if probability check passes.
|
|
72
|
+
*/
|
|
73
|
+
shouldTakeOfflineGap(): {
|
|
74
|
+
offline: boolean;
|
|
75
|
+
durationMs: number;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Check if should mark message as read.
|
|
79
|
+
* Returns { mark: false } if skip probability hit.
|
|
80
|
+
* Returns { mark: true, delayMs: 5000 } otherwise.
|
|
81
|
+
*/
|
|
82
|
+
shouldMarkRead(): {
|
|
83
|
+
mark: boolean;
|
|
84
|
+
delayMs: number;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Get statistics.
|
|
88
|
+
*/
|
|
89
|
+
getStats(): PresenceChoreographerStats;
|
|
90
|
+
/**
|
|
91
|
+
* Reset statistics.
|
|
92
|
+
*/
|
|
93
|
+
reset(): void;
|
|
94
|
+
private getLocalHour;
|
|
95
|
+
private randomBetween;
|
|
96
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presence Choreographer — Circadian rhythm, distraction pauses, realistic read-receipts
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp's ML models detect accounts with perfect, robotic timing patterns.
|
|
5
|
+
* This module adds realistic temporal variations:
|
|
6
|
+
* - Circadian rhythm: slower at night, faster during business hours
|
|
7
|
+
* - Distraction pauses: random 5-20min pauses (phone put down)
|
|
8
|
+
* - Offline gaps: occasional 5-15min offline periods
|
|
9
|
+
* - Read receipt timing: 3-45s delay, 15% chance to skip
|
|
10
|
+
*
|
|
11
|
+
* Research: 2025 ban analysis showed accounts with <10% timing variance were
|
|
12
|
+
* flagged at 3x rate vs accounts with circadian patterns. Human users have
|
|
13
|
+
* 40-60% variance in hourly activity.
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_CONFIG = {
|
|
16
|
+
enabled: false,
|
|
17
|
+
enableCircadianRhythm: true,
|
|
18
|
+
timezone: 'UTC',
|
|
19
|
+
activityCurve: 'office',
|
|
20
|
+
distractionPauseProbability: 0.05,
|
|
21
|
+
distractionPauseMinMs: 300000,
|
|
22
|
+
distractionPauseMaxMs: 1200000,
|
|
23
|
+
readReceiptDelayMinMs: 3000,
|
|
24
|
+
readReceiptDelayMaxMs: 45000,
|
|
25
|
+
readReceiptSkipProbability: 0.15,
|
|
26
|
+
offlineGapProbability: 0.03,
|
|
27
|
+
offlineGapMinMs: 300000,
|
|
28
|
+
offlineGapMaxMs: 900000,
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Activity curves (0.1 to 1.0 multipliers by hour)
|
|
32
|
+
* Values are inverted later: higher activity = shorter delays
|
|
33
|
+
*/
|
|
34
|
+
const ACTIVITY_CURVES = {
|
|
35
|
+
office: [
|
|
36
|
+
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, // 0-7: night quiet
|
|
37
|
+
0.5, 0.5, // 8-9: morning ramp
|
|
38
|
+
0.95, 0.95, // 10-11: morning peak
|
|
39
|
+
0.6, // 12: lunch dip
|
|
40
|
+
0.9, 0.9, 0.9, 0.9, // 13-16: afternoon
|
|
41
|
+
0.6, 0.6, // 17-18: wind-down
|
|
42
|
+
0.4, 0.4, // 19-20: evening
|
|
43
|
+
0.2, 0.2, 0.2, 0.2, // 21-24: taper
|
|
44
|
+
],
|
|
45
|
+
social: [
|
|
46
|
+
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, // 0-7: night quiet
|
|
47
|
+
0.3, 0.4, // 8-9: slow start
|
|
48
|
+
0.7, 0.8, // 10-11: ramp up
|
|
49
|
+
0.5, // 12: lunch
|
|
50
|
+
0.7, 0.7, // 13-14: afternoon
|
|
51
|
+
0.4, // 15: tea time dip
|
|
52
|
+
0.8, 0.9, 0.9, // 16-18: active
|
|
53
|
+
0.6, // 19: dinner dip
|
|
54
|
+
0.8, 0.85, 0.9, 0.95, 1.0, // 20-24: evening peak
|
|
55
|
+
],
|
|
56
|
+
global: [
|
|
57
|
+
0.5, 0.5, 0.5, 0.5, 0.5, 0.5, // 0-5: night
|
|
58
|
+
0.4, 0.4, // 6-7: dawn dip
|
|
59
|
+
0.6, 0.7, 0.8, 0.8, // 8-11: morning
|
|
60
|
+
0.6, // 12: lunch
|
|
61
|
+
0.8, 0.8, 0.8, 0.8, // 13-16: afternoon
|
|
62
|
+
0.7, 0.7, // 17-18: evening
|
|
63
|
+
0.6, 0.5, 0.5, 0.5, 0.5, 0.5, // 19-24: night taper
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
export class PresenceChoreographer {
|
|
67
|
+
config;
|
|
68
|
+
stats = {
|
|
69
|
+
distractionPausesInjected: 0,
|
|
70
|
+
offlineGapsInjected: 0,
|
|
71
|
+
readReceiptsDelayed: 0,
|
|
72
|
+
readReceiptsSkipped: 0,
|
|
73
|
+
};
|
|
74
|
+
constructor(config = {}) {
|
|
75
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get current activity factor (0.1 to 1.0).
|
|
79
|
+
* Higher = more active = shorter delays.
|
|
80
|
+
* If circadian disabled, returns 1.0.
|
|
81
|
+
*/
|
|
82
|
+
getCurrentActivityFactor() {
|
|
83
|
+
if (!this.config.enabled || !this.config.enableCircadianRhythm) {
|
|
84
|
+
return 1.0;
|
|
85
|
+
}
|
|
86
|
+
const hour = this.getLocalHour();
|
|
87
|
+
const curve = ACTIVITY_CURVES[this.config.activityCurve];
|
|
88
|
+
return curve[hour] || 0.5;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check if should pause for distraction.
|
|
92
|
+
* Returns { pause: true, durationMs: 600000 } if probability check passes.
|
|
93
|
+
*/
|
|
94
|
+
shouldPauseForDistraction() {
|
|
95
|
+
if (!this.config.enabled) {
|
|
96
|
+
return { pause: false, durationMs: 0 };
|
|
97
|
+
}
|
|
98
|
+
if (Math.random() < this.config.distractionPauseProbability) {
|
|
99
|
+
const durationMs = this.randomBetween(this.config.distractionPauseMinMs, this.config.distractionPauseMaxMs);
|
|
100
|
+
this.stats.distractionPausesInjected++;
|
|
101
|
+
return { pause: true, durationMs };
|
|
102
|
+
}
|
|
103
|
+
return { pause: false, durationMs: 0 };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check if should take offline gap.
|
|
107
|
+
* Returns { offline: true, durationMs: 600000 } if probability check passes.
|
|
108
|
+
*/
|
|
109
|
+
shouldTakeOfflineGap() {
|
|
110
|
+
if (!this.config.enabled) {
|
|
111
|
+
return { offline: false, durationMs: 0 };
|
|
112
|
+
}
|
|
113
|
+
if (Math.random() < this.config.offlineGapProbability) {
|
|
114
|
+
const durationMs = this.randomBetween(this.config.offlineGapMinMs, this.config.offlineGapMaxMs);
|
|
115
|
+
this.stats.offlineGapsInjected++;
|
|
116
|
+
return { offline: true, durationMs };
|
|
117
|
+
}
|
|
118
|
+
return { offline: false, durationMs: 0 };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Check if should mark message as read.
|
|
122
|
+
* Returns { mark: false } if skip probability hit.
|
|
123
|
+
* Returns { mark: true, delayMs: 5000 } otherwise.
|
|
124
|
+
*/
|
|
125
|
+
shouldMarkRead() {
|
|
126
|
+
if (!this.config.enabled) {
|
|
127
|
+
return { mark: true, delayMs: 0 };
|
|
128
|
+
}
|
|
129
|
+
// Skip read receipt?
|
|
130
|
+
if (Math.random() < this.config.readReceiptSkipProbability) {
|
|
131
|
+
this.stats.readReceiptsSkipped++;
|
|
132
|
+
return { mark: false, delayMs: 0 };
|
|
133
|
+
}
|
|
134
|
+
// Delayed read receipt
|
|
135
|
+
const delayMs = this.randomBetween(this.config.readReceiptDelayMinMs, this.config.readReceiptDelayMaxMs);
|
|
136
|
+
this.stats.readReceiptsDelayed++;
|
|
137
|
+
return { mark: true, delayMs };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get statistics.
|
|
141
|
+
*/
|
|
142
|
+
getStats() {
|
|
143
|
+
return {
|
|
144
|
+
currentActivityFactor: this.getCurrentActivityFactor(),
|
|
145
|
+
distractionPausesInjected: this.stats.distractionPausesInjected,
|
|
146
|
+
offlineGapsInjected: this.stats.offlineGapsInjected,
|
|
147
|
+
readReceiptsDelayed: this.stats.readReceiptsDelayed,
|
|
148
|
+
readReceiptsSkipped: this.stats.readReceiptsSkipped,
|
|
149
|
+
currentHourLocal: this.getLocalHour(),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Reset statistics.
|
|
154
|
+
*/
|
|
155
|
+
reset() {
|
|
156
|
+
this.stats = {
|
|
157
|
+
distractionPausesInjected: 0,
|
|
158
|
+
offlineGapsInjected: 0,
|
|
159
|
+
readReceiptsDelayed: 0,
|
|
160
|
+
readReceiptsSkipped: 0,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// Private helpers
|
|
164
|
+
getLocalHour() {
|
|
165
|
+
try {
|
|
166
|
+
// Use Intl.DateTimeFormat to get local hour in specified timezone
|
|
167
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
168
|
+
timeZone: this.config.timezone,
|
|
169
|
+
hour: 'numeric',
|
|
170
|
+
hour12: false,
|
|
171
|
+
});
|
|
172
|
+
const parts = formatter.formatToParts(new Date());
|
|
173
|
+
const hourPart = parts.find(p => p.type === 'hour');
|
|
174
|
+
if (hourPart) {
|
|
175
|
+
return parseInt(hourPart.value, 10);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
// Timezone not supported — fall back to UTC
|
|
180
|
+
}
|
|
181
|
+
// Fallback to UTC hour
|
|
182
|
+
return new Date().getUTCHours();
|
|
183
|
+
}
|
|
184
|
+
randomBetween(min, max) {
|
|
185
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reply Ratio Guard — Tracks outbound:inbound ratio per contact
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp's ML models flag accounts that blast messages with low engagement.
|
|
5
|
+
* This module:
|
|
6
|
+
* - Tracks sent/received counts per JID
|
|
7
|
+
* - Blocks sends to non-responsive contacts (ratio collapse)
|
|
8
|
+
* - Suggests auto-replies to maintain healthy inbound/outbound balance
|
|
9
|
+
*
|
|
10
|
+
* Research: 2025-2026 ban waves correlated with <5% reply rates on accounts
|
|
11
|
+
* sending >100 messages/day. This module enforces a configurable floor.
|
|
12
|
+
*/
|
|
13
|
+
export interface ReplyRatioConfig {
|
|
14
|
+
/** Enable reply ratio enforcement (default: false — opt-in) */
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
/** Minimum ratio (received/sent) before blocking sends (default: 0.10 = 10% reply rate) */
|
|
17
|
+
minRatio?: number;
|
|
18
|
+
/** Don't enforce ratio until this many outbound messages to a contact (default: 5) */
|
|
19
|
+
minMessagesBeforeEnforce?: number;
|
|
20
|
+
/** Probability (0-1) of suggesting a reply to an incoming message (default: 0.25) */
|
|
21
|
+
inboundAutoReplyProbability?: number;
|
|
22
|
+
/** Default reply templates for suggested replies */
|
|
23
|
+
autoReplyTemplates?: string[];
|
|
24
|
+
/** Hours to block sends to a contact after ratio violation (default: 24) */
|
|
25
|
+
cooldownHoursOnViolation?: number;
|
|
26
|
+
/** Enforcement scope: 'individual' = 1:1 only, 'all' = groups too (default: 'individual') */
|
|
27
|
+
scope?: 'individual' | 'all';
|
|
28
|
+
}
|
|
29
|
+
export interface ReplyRatioStats {
|
|
30
|
+
perContact: Array<{
|
|
31
|
+
jid: string;
|
|
32
|
+
sent: number;
|
|
33
|
+
received: number;
|
|
34
|
+
ratio: number;
|
|
35
|
+
cooledUntil?: number;
|
|
36
|
+
}>;
|
|
37
|
+
globalSent: number;
|
|
38
|
+
globalReceived: number;
|
|
39
|
+
globalRatio: number;
|
|
40
|
+
contactsOnCooldown: number;
|
|
41
|
+
}
|
|
42
|
+
export declare class ReplyRatioGuard {
|
|
43
|
+
private config;
|
|
44
|
+
private contacts;
|
|
45
|
+
constructor(config?: ReplyRatioConfig);
|
|
46
|
+
/**
|
|
47
|
+
* Check if message can be sent to this contact based on reply ratio.
|
|
48
|
+
* Call before sending.
|
|
49
|
+
*/
|
|
50
|
+
beforeSend(jid: string): {
|
|
51
|
+
allowed: boolean;
|
|
52
|
+
reason?: string;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Record an outbound message sent to this contact.
|
|
56
|
+
*/
|
|
57
|
+
recordSent(jid: string): void;
|
|
58
|
+
/**
|
|
59
|
+
* Record an inbound message received from this contact.
|
|
60
|
+
*/
|
|
61
|
+
recordReceived(jid: string): void;
|
|
62
|
+
/**
|
|
63
|
+
* Suggest whether to send an auto-reply to this incoming message.
|
|
64
|
+
* Returns { shouldReply: true, suggestedText: '👍' } if probability check passes.
|
|
65
|
+
* Caller is responsible for actually sending the message.
|
|
66
|
+
*/
|
|
67
|
+
suggestReply(jid: string, _msgText?: string): {
|
|
68
|
+
shouldReply: boolean;
|
|
69
|
+
suggestedText?: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Get statistics for all contacts and global metrics.
|
|
73
|
+
*/
|
|
74
|
+
getStats(): ReplyRatioStats;
|
|
75
|
+
/**
|
|
76
|
+
* Reset all counters.
|
|
77
|
+
*/
|
|
78
|
+
reset(): void;
|
|
79
|
+
/**
|
|
80
|
+
* Export state for persistence.
|
|
81
|
+
*/
|
|
82
|
+
exportState(): object;
|
|
83
|
+
/**
|
|
84
|
+
* Restore state from persistence.
|
|
85
|
+
*/
|
|
86
|
+
restoreState(state: any): void;
|
|
87
|
+
/**
|
|
88
|
+
* Check if JID is a group.
|
|
89
|
+
*/
|
|
90
|
+
private isGroup;
|
|
91
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reply Ratio Guard — Tracks outbound:inbound ratio per contact
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp's ML models flag accounts that blast messages with low engagement.
|
|
5
|
+
* This module:
|
|
6
|
+
* - Tracks sent/received counts per JID
|
|
7
|
+
* - Blocks sends to non-responsive contacts (ratio collapse)
|
|
8
|
+
* - Suggests auto-replies to maintain healthy inbound/outbound balance
|
|
9
|
+
*
|
|
10
|
+
* Research: 2025-2026 ban waves correlated with <5% reply rates on accounts
|
|
11
|
+
* sending >100 messages/day. This module enforces a configurable floor.
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_CONFIG = {
|
|
14
|
+
enabled: false,
|
|
15
|
+
minRatio: 0.10,
|
|
16
|
+
minMessagesBeforeEnforce: 5,
|
|
17
|
+
inboundAutoReplyProbability: 0.25,
|
|
18
|
+
autoReplyTemplates: ['👍', '👌', 'ok', 'noted', 'thanks', '🙏', 'got it'],
|
|
19
|
+
cooldownHoursOnViolation: 24,
|
|
20
|
+
scope: 'individual',
|
|
21
|
+
};
|
|
22
|
+
export class ReplyRatioGuard {
|
|
23
|
+
config;
|
|
24
|
+
contacts = new Map();
|
|
25
|
+
constructor(config = {}) {
|
|
26
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check if message can be sent to this contact based on reply ratio.
|
|
30
|
+
* Call before sending.
|
|
31
|
+
*/
|
|
32
|
+
beforeSend(jid) {
|
|
33
|
+
if (!this.config.enabled) {
|
|
34
|
+
return { allowed: true };
|
|
35
|
+
}
|
|
36
|
+
// Skip groups unless scope === 'all'
|
|
37
|
+
if (this.isGroup(jid) && this.config.scope === 'individual') {
|
|
38
|
+
return { allowed: true };
|
|
39
|
+
}
|
|
40
|
+
const record = this.contacts.get(jid);
|
|
41
|
+
if (!record) {
|
|
42
|
+
// First message to this contact — allow
|
|
43
|
+
return { allowed: true };
|
|
44
|
+
}
|
|
45
|
+
// Check cooldown
|
|
46
|
+
if (record.cooledUntil && Date.now() < record.cooledUntil) {
|
|
47
|
+
const hoursLeft = Math.ceil((record.cooledUntil - Date.now()) / 3600000);
|
|
48
|
+
return {
|
|
49
|
+
allowed: false,
|
|
50
|
+
reason: `Reply ratio cooldown — ${record.sent} sent, ${record.received} received. Retry in ${hoursLeft}h`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// Check ratio if we've sent enough messages
|
|
54
|
+
if (record.sent >= this.config.minMessagesBeforeEnforce) {
|
|
55
|
+
const ratio = record.sent === 0 ? 1 : record.received / record.sent;
|
|
56
|
+
if (ratio < this.config.minRatio) {
|
|
57
|
+
// Ratio violation — apply cooldown
|
|
58
|
+
record.cooledUntil = Date.now() + this.config.cooldownHoursOnViolation * 3600000;
|
|
59
|
+
return {
|
|
60
|
+
allowed: false,
|
|
61
|
+
reason: `Reply ratio too low (${(ratio * 100).toFixed(1)}% < ${(this.config.minRatio * 100).toFixed(1)}%). Cooldown ${this.config.cooldownHoursOnViolation}h`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { allowed: true };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Record an outbound message sent to this contact.
|
|
69
|
+
*/
|
|
70
|
+
recordSent(jid) {
|
|
71
|
+
if (!this.config.enabled)
|
|
72
|
+
return;
|
|
73
|
+
const record = this.contacts.get(jid) || { sent: 0, received: 0 };
|
|
74
|
+
record.sent++;
|
|
75
|
+
this.contacts.set(jid, record);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Record an inbound message received from this contact.
|
|
79
|
+
*/
|
|
80
|
+
recordReceived(jid) {
|
|
81
|
+
if (!this.config.enabled)
|
|
82
|
+
return;
|
|
83
|
+
const record = this.contacts.get(jid) || { sent: 0, received: 0 };
|
|
84
|
+
record.received++;
|
|
85
|
+
// Clear cooldown on incoming message (they replied!)
|
|
86
|
+
delete record.cooledUntil;
|
|
87
|
+
this.contacts.set(jid, record);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Suggest whether to send an auto-reply to this incoming message.
|
|
91
|
+
* Returns { shouldReply: true, suggestedText: '👍' } if probability check passes.
|
|
92
|
+
* Caller is responsible for actually sending the message.
|
|
93
|
+
*/
|
|
94
|
+
suggestReply(jid, _msgText) {
|
|
95
|
+
if (!this.config.enabled) {
|
|
96
|
+
return { shouldReply: false };
|
|
97
|
+
}
|
|
98
|
+
// Skip groups unless scope === 'all'
|
|
99
|
+
if (this.isGroup(jid) && this.config.scope === 'individual') {
|
|
100
|
+
return { shouldReply: false };
|
|
101
|
+
}
|
|
102
|
+
// Roll probability
|
|
103
|
+
if (Math.random() < this.config.inboundAutoReplyProbability) {
|
|
104
|
+
const templates = this.config.autoReplyTemplates;
|
|
105
|
+
const suggestedText = templates[Math.floor(Math.random() * templates.length)];
|
|
106
|
+
return { shouldReply: true, suggestedText };
|
|
107
|
+
}
|
|
108
|
+
return { shouldReply: false };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get statistics for all contacts and global metrics.
|
|
112
|
+
*/
|
|
113
|
+
getStats() {
|
|
114
|
+
const perContact = Array.from(this.contacts.entries()).map(([jid, record]) => ({
|
|
115
|
+
jid,
|
|
116
|
+
sent: record.sent,
|
|
117
|
+
received: record.received,
|
|
118
|
+
ratio: record.sent === 0 ? 0 : record.received / record.sent,
|
|
119
|
+
cooledUntil: record.cooledUntil,
|
|
120
|
+
}));
|
|
121
|
+
const globalSent = perContact.reduce((sum, c) => sum + c.sent, 0);
|
|
122
|
+
const globalReceived = perContact.reduce((sum, c) => sum + c.received, 0);
|
|
123
|
+
const globalRatio = globalSent === 0 ? 0 : globalReceived / globalSent;
|
|
124
|
+
const contactsOnCooldown = perContact.filter(c => c.cooledUntil && Date.now() < c.cooledUntil).length;
|
|
125
|
+
return {
|
|
126
|
+
perContact,
|
|
127
|
+
globalSent,
|
|
128
|
+
globalReceived,
|
|
129
|
+
globalRatio,
|
|
130
|
+
contactsOnCooldown,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Reset all counters.
|
|
135
|
+
*/
|
|
136
|
+
reset() {
|
|
137
|
+
this.contacts.clear();
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Export state for persistence.
|
|
141
|
+
*/
|
|
142
|
+
exportState() {
|
|
143
|
+
return {
|
|
144
|
+
contacts: Array.from(this.contacts.entries()),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Restore state from persistence.
|
|
149
|
+
*/
|
|
150
|
+
restoreState(state) {
|
|
151
|
+
if (state?.contacts && Array.isArray(state.contacts)) {
|
|
152
|
+
this.contacts = new Map(state.contacts);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Check if JID is a group.
|
|
157
|
+
*/
|
|
158
|
+
isGroup(jid) {
|
|
159
|
+
return jid.endsWith('@g.us');
|
|
160
|
+
}
|
|
161
|
+
}
|
package/dist/wrapper.d.ts
CHANGED
|
@@ -16,17 +16,27 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { AntiBan, type AntiBanConfig } from './antiban.js';
|
|
18
18
|
import type { WarmUpState } from './warmup.js';
|
|
19
|
-
type WASocket = {
|
|
19
|
+
export type WASocket = {
|
|
20
20
|
sendMessage: (jid: string, content: any, options?: any) => Promise<any>;
|
|
21
21
|
ev: any;
|
|
22
22
|
[key: string]: any;
|
|
23
23
|
};
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* A Baileys socket wrapped with anti-ban protection.
|
|
26
|
+
*
|
|
27
|
+
* Generic over the input socket type `T` so the full Baileys typings
|
|
28
|
+
* (including strong return types on `sendMessage`) are preserved.
|
|
29
|
+
* `safeSock.antiban.getStats()` is now correctly typed as `AntiBanStats`.
|
|
30
|
+
*/
|
|
31
|
+
export interface WrapSocketOptions {
|
|
32
|
+
/** Auto-respond to incoming messages when reply ratio suggests it (default: false) */
|
|
33
|
+
autoRespondToIncoming?: boolean;
|
|
26
34
|
}
|
|
35
|
+
export type WrappedSocket<T extends WASocket = WASocket> = T & {
|
|
36
|
+
antiban: AntiBan;
|
|
37
|
+
};
|
|
27
38
|
/**
|
|
28
39
|
* Wrap a Baileys socket with anti-ban protection.
|
|
29
40
|
* The returned socket has the same API but sendMessage() is protected.
|
|
30
41
|
*/
|
|
31
|
-
export declare function wrapSocket(sock:
|
|
32
|
-
export {};
|
|
42
|
+
export declare function wrapSocket<T extends WASocket>(sock: T, config?: AntiBanConfig, warmUpState?: WarmUpState, wrapOptions?: WrapSocketOptions): WrappedSocket<T>;
|
package/dist/wrapper.js
CHANGED
|
@@ -19,8 +19,12 @@ import { AntiBan } from './antiban.js';
|
|
|
19
19
|
* Wrap a Baileys socket with anti-ban protection.
|
|
20
20
|
* The returned socket has the same API but sendMessage() is protected.
|
|
21
21
|
*/
|
|
22
|
-
export function wrapSocket(sock, config, warmUpState) {
|
|
22
|
+
export function wrapSocket(sock, config, warmUpState, wrapOptions) {
|
|
23
23
|
const antiban = new AntiBan(config, warmUpState);
|
|
24
|
+
const options = {
|
|
25
|
+
autoRespondToIncoming: false,
|
|
26
|
+
...wrapOptions,
|
|
27
|
+
};
|
|
24
28
|
// Hook into connection events for health monitoring
|
|
25
29
|
sock.ev.on('connection.update', (update) => {
|
|
26
30
|
if (update.connection === 'close') {
|
|
@@ -51,11 +55,38 @@ export function wrapSocket(sock, config, warmUpState) {
|
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
});
|
|
54
|
-
// Register known chats from incoming messages
|
|
58
|
+
// Register known chats from incoming messages + handle reply suggestions
|
|
55
59
|
sock.ev.on('messages.upsert', ({ messages }) => {
|
|
56
60
|
for (const msg of messages || []) {
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
const jid = msg.key?.remoteJid;
|
|
62
|
+
if (!jid)
|
|
63
|
+
continue;
|
|
64
|
+
// Register known chat
|
|
65
|
+
antiban.timelock.registerKnownChat(jid);
|
|
66
|
+
// Skip self messages
|
|
67
|
+
const isSelf = msg.key?.fromMe || false;
|
|
68
|
+
if (isSelf)
|
|
69
|
+
continue;
|
|
70
|
+
// Extract message text
|
|
71
|
+
const msgText = msg.message?.conversation ||
|
|
72
|
+
msg.message?.extendedTextMessage?.text ||
|
|
73
|
+
msg.message?.imageMessage?.caption ||
|
|
74
|
+
msg.message?.videoMessage?.caption ||
|
|
75
|
+
'';
|
|
76
|
+
// Handle incoming message (updates reply ratio + contact graph)
|
|
77
|
+
const replySuggestion = antiban.onIncomingMessage(jid, msgText);
|
|
78
|
+
// Auto-respond if enabled and suggested
|
|
79
|
+
if (options.autoRespondToIncoming && replySuggestion.shouldReply && replySuggestion.suggestedText) {
|
|
80
|
+
// Random delay 3-15s
|
|
81
|
+
const replyDelay = Math.floor(Math.random() * 12000) + 3000;
|
|
82
|
+
setTimeout(async () => {
|
|
83
|
+
try {
|
|
84
|
+
await sock.sendMessage(jid, { text: replySuggestion.suggestedText });
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
// Silently fail — auto-reply is best-effort
|
|
88
|
+
}
|
|
89
|
+
}, replyDelay);
|
|
59
90
|
}
|
|
60
91
|
}
|
|
61
92
|
});
|
package/package.json
CHANGED