@prototypevip/baileys 0.0.8 → 0.0.9
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/lib/AntiBan/antiban.js +922 -0
- package/lib/AntiBan/banRecoveryOrchestrator.js +284 -0
- package/lib/AntiBan/cli.js +177 -0
- package/lib/AntiBan/contactGraph.js +236 -0
- package/lib/AntiBan/contentVariator.js +150 -0
- package/lib/AntiBan/credsSnapshot.js +121 -0
- package/lib/AntiBan/deliveryTracker.js +107 -0
- package/lib/AntiBan/deviceFingerprint.js +106 -0
- package/lib/AntiBan/fleetEventStore.js +157 -0
- package/lib/AntiBan/groupOperationGuard.js +116 -0
- package/lib/AntiBan/health.js +207 -0
- package/lib/AntiBan/humanEntropy.js +337 -0
- package/lib/AntiBan/index.js +77 -0
- package/lib/AntiBan/instanceCoordinator.js +175 -0
- package/lib/AntiBan/jidCanonicalizer.js +256 -0
- package/lib/AntiBan/jidCircuitBreaker.js +186 -0
- package/lib/AntiBan/legitimacySignalInjector.js +231 -0
- package/lib/AntiBan/lidFirstResolver.js +174 -0
- package/lib/AntiBan/lidResolver.js +324 -0
- package/lib/AntiBan/messageQueue.js +187 -0
- package/lib/AntiBan/messageRecovery.js +283 -0
- package/lib/AntiBan/messageTypeRegistry.js +369 -0
- package/lib/AntiBan/observability.js +145 -0
- package/lib/AntiBan/patch.js +302 -0
- package/lib/AntiBan/persist.js +87 -0
- package/lib/AntiBan/presenceChoreographer.js +430 -0
- package/lib/AntiBan/presets.js +105 -0
- package/lib/AntiBan/profiles.js +31 -0
- package/lib/AntiBan/proxyRotator.js +306 -0
- package/lib/AntiBan/rateLimiter.js +261 -0
- package/lib/AntiBan/readReceiptVariance.js +88 -0
- package/lib/AntiBan/reconnectThrottle.js +180 -0
- package/lib/AntiBan/replyRatio.js +161 -0
- package/lib/AntiBan/reputationVoucher.js +373 -0
- package/lib/AntiBan/retryReason.js +91 -0
- package/lib/AntiBan/retryTracker.js +172 -0
- package/lib/AntiBan/scheduler.js +111 -0
- package/lib/AntiBan/sessionFingerprint.js +248 -0
- package/lib/AntiBan/sessionStability.js +330 -0
- package/lib/AntiBan/stateAdapter.js +73 -0
- package/lib/AntiBan/stateExport.js +152 -0
- package/lib/AntiBan/stealthConnect.js +130 -0
- package/lib/AntiBan/timelockGuard.js +181 -0
- package/lib/AntiBan/topologyThrottler.js +339 -0
- package/lib/AntiBan/warmup.js +140 -0
- package/lib/AntiBan/webhooks.js +80 -0
- package/lib/AntiBan/wrapper.js +456 -0
- package/lib/Socket/groups.js +26 -21
- package/lib/Socket/index.js +139 -7
- package/lib/Socket/messages-send.js +68 -5
- package/lib/Utils/ai-rich-formatter.js +256 -0
- package/lib/Utils/index.js +1 -0
- package/lib/index.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BanRecoveryOrchestrator — Structured recovery after ban/restriction events
|
|
3
|
+
*
|
|
4
|
+
* When WhatsApp restricts your account, the worst thing to do is immediately
|
|
5
|
+
* resume normal activity. This module provides a evidence-based recovery protocol:
|
|
6
|
+
*
|
|
7
|
+
* - Timelocked (reachout_restricted): 24h pause, resume at 10% rate, ramp 15%/week
|
|
8
|
+
* - Rate-overlimit (429): 4h pause, resume at 25% rate, ramp 25%/week
|
|
9
|
+
* - Soft-ban (repeated disconnects): 48h pause, resume at 5% rate, ramp 10%/week
|
|
10
|
+
* - Hard-ban (loggedOut): account is dead, signal for replacement
|
|
11
|
+
*
|
|
12
|
+
* Based on observed recovery times from community reports. Not guaranteed —
|
|
13
|
+
* WA's enforcement is non-deterministic. Treat as best-effort guidance.
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_PLANS = {
|
|
16
|
+
timelock: {
|
|
17
|
+
eventType: 'timelock',
|
|
18
|
+
pauseDurationMs: 24 * 60 * 60 * 1000, // 24 hours
|
|
19
|
+
resumeRateMultiplier: 0.10, // 10% of normal
|
|
20
|
+
weeklyRampPercent: 15, // +15% per week
|
|
21
|
+
estimatedRecoveryDays: 14,
|
|
22
|
+
description: 'WA reachout timelock — 24h pause then slow ramp',
|
|
23
|
+
},
|
|
24
|
+
rate_overlimit: {
|
|
25
|
+
eventType: 'rate_overlimit',
|
|
26
|
+
pauseDurationMs: 4 * 60 * 60 * 1000, // 4 hours
|
|
27
|
+
resumeRateMultiplier: 0.25, // 25% of normal
|
|
28
|
+
weeklyRampPercent: 25, // +25% per week
|
|
29
|
+
estimatedRecoveryDays: 7,
|
|
30
|
+
description: 'Rate limit hit — 4h pause then moderate ramp',
|
|
31
|
+
},
|
|
32
|
+
soft_ban: {
|
|
33
|
+
eventType: 'soft_ban',
|
|
34
|
+
pauseDurationMs: 48 * 60 * 60 * 1000, // 48 hours
|
|
35
|
+
resumeRateMultiplier: 0.05, // 5% of normal
|
|
36
|
+
weeklyRampPercent: 10, // +10% per week
|
|
37
|
+
estimatedRecoveryDays: 21,
|
|
38
|
+
description: 'Soft ban detected — 48h pause then very slow ramp',
|
|
39
|
+
},
|
|
40
|
+
hard_ban: {
|
|
41
|
+
eventType: 'hard_ban',
|
|
42
|
+
pauseDurationMs: Infinity,
|
|
43
|
+
resumeRateMultiplier: 0,
|
|
44
|
+
weeklyRampPercent: 0,
|
|
45
|
+
estimatedRecoveryDays: Infinity,
|
|
46
|
+
description: 'Hard ban — number is dead, replace SIM',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
export class BanRecoveryOrchestrator {
|
|
50
|
+
config;
|
|
51
|
+
state;
|
|
52
|
+
plans;
|
|
53
|
+
constructor(config = {}) {
|
|
54
|
+
this.config = {
|
|
55
|
+
plans: config.plans || {},
|
|
56
|
+
onPhaseChange: config.onPhaseChange || (() => { }),
|
|
57
|
+
onHardBan: config.onHardBan || (() => { }),
|
|
58
|
+
maxRecoveryWeeks: config.maxRecoveryWeeks ?? 8,
|
|
59
|
+
};
|
|
60
|
+
// Merge custom plans with defaults
|
|
61
|
+
this.plans = { ...DEFAULT_PLANS };
|
|
62
|
+
if (config.plans) {
|
|
63
|
+
for (const eventType in config.plans) {
|
|
64
|
+
const customPlan = config.plans[eventType];
|
|
65
|
+
if (customPlan) {
|
|
66
|
+
this.plans[eventType] = {
|
|
67
|
+
...this.plans[eventType],
|
|
68
|
+
...customPlan,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
this.state = {
|
|
74
|
+
active: false,
|
|
75
|
+
phase: 'graduated',
|
|
76
|
+
currentRateMultiplier: 1.0,
|
|
77
|
+
weeksSinceResume: 0,
|
|
78
|
+
banCount30d: 0,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Record a ban event and start recovery protocol
|
|
83
|
+
*/
|
|
84
|
+
recordBanEvent(eventType) {
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
const plan = this.plans[eventType];
|
|
87
|
+
// Update ban count (reset if >30 days since last ban)
|
|
88
|
+
if (this.state.lastBanAt) {
|
|
89
|
+
const daysSinceLastBan = (now - this.state.lastBanAt) / (86400000);
|
|
90
|
+
if (daysSinceLastBan > 30) {
|
|
91
|
+
this.state.banCount30d = 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
this.state.banCount30d++;
|
|
95
|
+
this.state.lastBanAt = now;
|
|
96
|
+
// Upgrade to hard_ban if 3+ bans in 30 days (unless already hard_ban)
|
|
97
|
+
if (this.state.banCount30d >= 3 && eventType !== 'hard_ban') {
|
|
98
|
+
eventType = 'hard_ban';
|
|
99
|
+
}
|
|
100
|
+
this.state.active = true;
|
|
101
|
+
this.state.eventType = eventType;
|
|
102
|
+
this.state.banDetectedAt = now;
|
|
103
|
+
this.state.pauseUntil = plan.pauseDurationMs === Infinity ? Infinity : now + plan.pauseDurationMs;
|
|
104
|
+
this.state.currentRateMultiplier = plan.resumeRateMultiplier;
|
|
105
|
+
this.state.weeksSinceResume = 0;
|
|
106
|
+
this.state.phase = eventType === 'hard_ban' ? 'dead' : 'paused';
|
|
107
|
+
this.config.onPhaseChange(this.state.phase, plan);
|
|
108
|
+
if (eventType === 'hard_ban') {
|
|
109
|
+
this.config.onHardBan();
|
|
110
|
+
}
|
|
111
|
+
return this.getStatus();
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get current recovery status (call before sending to check rate)
|
|
115
|
+
*/
|
|
116
|
+
getStatus() {
|
|
117
|
+
if (!this.state.active || !this.state.eventType) {
|
|
118
|
+
return {
|
|
119
|
+
phase: 'graduated',
|
|
120
|
+
rateMultiplier: 1.0,
|
|
121
|
+
recommendation: 'No active recovery — operating normally',
|
|
122
|
+
shouldReplaceNumber: false,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
const plan = this.plans[this.state.eventType];
|
|
127
|
+
// Check if pause period has ended
|
|
128
|
+
if (this.state.phase === 'paused' && this.state.pauseUntil !== undefined && this.state.pauseUntil !== Infinity && now >= this.state.pauseUntil) {
|
|
129
|
+
this.state.phase = 'recovering';
|
|
130
|
+
this.config.onPhaseChange(this.state.phase, plan);
|
|
131
|
+
}
|
|
132
|
+
// Check if we've exceeded max recovery weeks (give up)
|
|
133
|
+
if (this.state.phase === 'recovering' || this.state.phase === 'ramping') {
|
|
134
|
+
if (this.state.weeksSinceResume >= this.config.maxRecoveryWeeks) {
|
|
135
|
+
this.state.phase = 'dead';
|
|
136
|
+
this.config.onPhaseChange(this.state.phase, plan);
|
|
137
|
+
this.config.onHardBan();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const shouldReplaceNumber = this.state.phase === 'dead' || this.state.banCount30d >= 3;
|
|
141
|
+
let recommendation;
|
|
142
|
+
switch (this.state.phase) {
|
|
143
|
+
case 'dead':
|
|
144
|
+
recommendation = 'Account is permanently restricted. Replace number and start fresh.';
|
|
145
|
+
break;
|
|
146
|
+
case 'paused':
|
|
147
|
+
if (this.state.pauseUntil === Infinity) {
|
|
148
|
+
recommendation = 'Account is dead. Do not attempt to send messages.';
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const remainingMs = this.state.pauseUntil - now;
|
|
152
|
+
const remainingHours = Math.ceil(remainingMs / 3600000);
|
|
153
|
+
recommendation = `Pause period active. Wait ${remainingHours}h before resuming. ${plan.description}`;
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
case 'recovering':
|
|
157
|
+
recommendation = `Recovery phase. Operating at ${Math.round(this.state.currentRateMultiplier * 100)}% capacity. Ramp: ${plan.weeklyRampPercent}%/week.`;
|
|
158
|
+
break;
|
|
159
|
+
case 'ramping':
|
|
160
|
+
recommendation = `Ramping phase. Operating at ${Math.round(this.state.currentRateMultiplier * 100)}% capacity. Ramp: ${plan.weeklyRampPercent}%/week.`;
|
|
161
|
+
break;
|
|
162
|
+
case 'graduated':
|
|
163
|
+
recommendation = 'Recovery complete. Operating at full capacity.';
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
const pauseRemainingMs = this.state.pauseUntil !== undefined && this.state.pauseUntil !== Infinity && now < this.state.pauseUntil
|
|
167
|
+
? this.state.pauseUntil - now
|
|
168
|
+
: undefined;
|
|
169
|
+
let estimatedFullRecoveryDate;
|
|
170
|
+
if (this.state.phase === 'recovering' || this.state.phase === 'ramping') {
|
|
171
|
+
const rateToGain = 1.0 - this.state.currentRateMultiplier;
|
|
172
|
+
const weeksNeeded = Math.ceil((rateToGain / (plan.weeklyRampPercent / 100)) * (1 / this.state.currentRateMultiplier));
|
|
173
|
+
estimatedFullRecoveryDate = now + weeksNeeded * 7 * 86400000;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
phase: this.state.phase,
|
|
177
|
+
rateMultiplier: this.state.currentRateMultiplier,
|
|
178
|
+
pauseRemainingMs,
|
|
179
|
+
estimatedFullRecoveryDate,
|
|
180
|
+
recommendation,
|
|
181
|
+
shouldReplaceNumber,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Current rate multiplier — multiply your normal limits by this
|
|
186
|
+
*/
|
|
187
|
+
getRateMultiplier() {
|
|
188
|
+
return this.state.currentRateMultiplier;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Should be called daily/weekly to advance the ramp
|
|
192
|
+
*/
|
|
193
|
+
tick() {
|
|
194
|
+
if (!this.state.active || !this.state.eventType) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
const plan = this.plans[this.state.eventType];
|
|
199
|
+
// Transition from paused to recovering
|
|
200
|
+
if (this.state.phase === 'paused' && this.state.pauseUntil !== undefined && this.state.pauseUntil !== Infinity && now >= this.state.pauseUntil) {
|
|
201
|
+
this.state.phase = 'recovering';
|
|
202
|
+
this.config.onPhaseChange(this.state.phase, plan);
|
|
203
|
+
}
|
|
204
|
+
// Advance the ramp if in recovering/ramping phase
|
|
205
|
+
if (this.state.phase === 'recovering' || this.state.phase === 'ramping') {
|
|
206
|
+
this.state.weeksSinceResume++;
|
|
207
|
+
// Check if exceeded max recovery weeks
|
|
208
|
+
if (this.state.weeksSinceResume >= this.config.maxRecoveryWeeks) {
|
|
209
|
+
this.state.phase = 'dead';
|
|
210
|
+
this.state.currentRateMultiplier = 0;
|
|
211
|
+
this.config.onPhaseChange(this.state.phase, plan);
|
|
212
|
+
this.config.onHardBan();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// Apply weekly ramp
|
|
216
|
+
const rampMultiplier = 1 + (plan.weeklyRampPercent / 100);
|
|
217
|
+
const newMultiplier = this.state.currentRateMultiplier * rampMultiplier;
|
|
218
|
+
if (newMultiplier >= 1.0) {
|
|
219
|
+
// Graduated
|
|
220
|
+
this.state.currentRateMultiplier = 1.0;
|
|
221
|
+
this.state.phase = 'graduated';
|
|
222
|
+
this.state.active = false;
|
|
223
|
+
this.config.onPhaseChange(this.state.phase, plan);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
// Still ramping
|
|
227
|
+
this.state.currentRateMultiplier = newMultiplier;
|
|
228
|
+
this.state.phase = 'ramping';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Classify a raw error into a BanEventType
|
|
234
|
+
*/
|
|
235
|
+
static classifyError(err) {
|
|
236
|
+
if (!err)
|
|
237
|
+
return null;
|
|
238
|
+
const errorStr = String(err).toLowerCase();
|
|
239
|
+
const errorMsg = err.message?.toLowerCase() || '';
|
|
240
|
+
const errorCode = err.code || '';
|
|
241
|
+
// Timelock patterns
|
|
242
|
+
if (errorStr.includes('reachout') ||
|
|
243
|
+
errorStr.includes('account_reachout_restricted') ||
|
|
244
|
+
errorMsg.includes('reachout') ||
|
|
245
|
+
errorCode === 463 ||
|
|
246
|
+
errorCode === '463') {
|
|
247
|
+
return 'timelock';
|
|
248
|
+
}
|
|
249
|
+
// Rate overlimit patterns
|
|
250
|
+
if (errorStr.includes('rate-overlimit') ||
|
|
251
|
+
errorStr.includes('rate_overlimit') ||
|
|
252
|
+
errorStr.includes('429') ||
|
|
253
|
+
errorCode === 429 ||
|
|
254
|
+
errorCode === '429') {
|
|
255
|
+
return 'rate_overlimit';
|
|
256
|
+
}
|
|
257
|
+
// Hard ban patterns
|
|
258
|
+
if (errorStr.includes('loggedout') ||
|
|
259
|
+
errorStr.includes('logged_out') ||
|
|
260
|
+
errorStr.includes('logged out') ||
|
|
261
|
+
errorMsg.includes('loggedout') ||
|
|
262
|
+
errorCode === 401 ||
|
|
263
|
+
errorCode === '401') {
|
|
264
|
+
return 'hard_ban';
|
|
265
|
+
}
|
|
266
|
+
// Note: soft_ban detection requires context (multiple disconnects)
|
|
267
|
+
// and should be determined by the caller using HealthMonitor
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Serializable state for persistence
|
|
272
|
+
*/
|
|
273
|
+
getState() {
|
|
274
|
+
return { ...this.state };
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Restore from persisted state
|
|
278
|
+
*/
|
|
279
|
+
static fromState(state, config) {
|
|
280
|
+
const orchestrator = new BanRecoveryOrchestrator(config);
|
|
281
|
+
orchestrator.state = { ...state };
|
|
282
|
+
return orchestrator;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* baileys-antiban CLI
|
|
4
|
+
* Usage: npx baileys-antiban <command> [options]
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import { StateManager } from './persist.js';
|
|
8
|
+
import { resolveConfig } from './presets.js';
|
|
9
|
+
import { applyPatch, unpatchFile } from './patch.js';
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const command = args[0];
|
|
12
|
+
function parseArgs(argv) {
|
|
13
|
+
const result = {};
|
|
14
|
+
for (let i = 1; i < argv.length; i++) {
|
|
15
|
+
const arg = argv[i];
|
|
16
|
+
if (arg.startsWith('--')) {
|
|
17
|
+
const key = arg.slice(2);
|
|
18
|
+
const next = argv[i + 1];
|
|
19
|
+
if (next && !next.startsWith('--')) {
|
|
20
|
+
result[key] = next;
|
|
21
|
+
i++;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
result[key] = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
function cmdStatus(opts) {
|
|
31
|
+
const statePath = opts['state'];
|
|
32
|
+
let warmupInfo = 'No state file (in-memory mode)';
|
|
33
|
+
let savedAt = 'N/A';
|
|
34
|
+
if (statePath) {
|
|
35
|
+
const mgr = new StateManager(statePath);
|
|
36
|
+
const state = mgr.load();
|
|
37
|
+
if (state) {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const dayMs = 86400000;
|
|
40
|
+
const currentDay = Math.floor((now - state.warmup.startedAt) / dayMs);
|
|
41
|
+
warmupInfo = state.warmup.graduated
|
|
42
|
+
? 'Graduated (warmup complete)'
|
|
43
|
+
: `Day ${currentDay + 1}, sent today: ${state.warmup.dailyCounts[currentDay] || 0}`;
|
|
44
|
+
savedAt = new Date(state.savedAt).toISOString();
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
warmupInfo = 'State file missing or corrupt';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const output = {
|
|
51
|
+
warmup: warmupInfo,
|
|
52
|
+
savedAt,
|
|
53
|
+
statePath: statePath || null,
|
|
54
|
+
};
|
|
55
|
+
if (opts['json']) {
|
|
56
|
+
console.log(JSON.stringify(output, null, 2));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log('═══ baileys-antiban status ═══');
|
|
60
|
+
console.log(`Warmup: ${output.warmup}`);
|
|
61
|
+
console.log(`Saved: ${output.savedAt}`);
|
|
62
|
+
console.log(`State: ${output.statePath || 'none'}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function cmdReset(opts) {
|
|
66
|
+
const statePath = opts['state'];
|
|
67
|
+
if (!statePath) {
|
|
68
|
+
console.error('Error: --state <path> required for reset');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
if (!fs.existsSync(statePath)) {
|
|
72
|
+
console.log('State file does not exist — nothing to reset');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
fs.unlinkSync(statePath);
|
|
76
|
+
console.log(`✅ State file deleted: ${statePath}`);
|
|
77
|
+
}
|
|
78
|
+
function cmdWarmupSimulate(opts) {
|
|
79
|
+
const days = parseInt(opts['simulate'] || '7', 10);
|
|
80
|
+
const presetName = opts['preset'] || 'conservative';
|
|
81
|
+
const cfg = resolveConfig(presetName);
|
|
82
|
+
console.log(`\nWarmup simulation — preset: ${presetName}, days: ${days}`);
|
|
83
|
+
console.log('─'.repeat(50));
|
|
84
|
+
const startDate = new Date();
|
|
85
|
+
for (let day = 0; day < days; day++) {
|
|
86
|
+
const date = new Date(startDate);
|
|
87
|
+
date.setDate(date.getDate() + day);
|
|
88
|
+
const dayName = date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
|
89
|
+
const limit = Math.round(cfg.day1Limit * Math.pow(cfg.growthFactor, day));
|
|
90
|
+
const bar = '█'.repeat(Math.min(30, Math.round(limit / 10)));
|
|
91
|
+
console.log(`Day ${String(day + 1).padStart(2)} ${dayName.padEnd(15)} ${String(limit).padStart(5)} msgs/day ${bar}`);
|
|
92
|
+
}
|
|
93
|
+
console.log('─'.repeat(50));
|
|
94
|
+
console.log(`Day ${days + 1}+: graduated (unlimited by warmup)\n`);
|
|
95
|
+
}
|
|
96
|
+
function cmdPatch(opts) {
|
|
97
|
+
const searchPaths = opts['path'] ? [opts['path']] : undefined;
|
|
98
|
+
const preset = opts['preset'] || 'conservative';
|
|
99
|
+
const minDelay = opts['min-delay'] ? parseInt(opts['min-delay']) : 1500;
|
|
100
|
+
const maxDelay = opts['max-delay'] ? parseInt(opts['max-delay']) : 4000;
|
|
101
|
+
const typingIndicator = opts['no-typing'] ? false : true;
|
|
102
|
+
const dryRun = !!opts['dry-run'];
|
|
103
|
+
const force = !!opts['force'];
|
|
104
|
+
const result = applyPatch({
|
|
105
|
+
baileysPaths: searchPaths,
|
|
106
|
+
preset,
|
|
107
|
+
minDelay,
|
|
108
|
+
maxDelay,
|
|
109
|
+
typingIndicator,
|
|
110
|
+
dryRun,
|
|
111
|
+
});
|
|
112
|
+
if (result.alreadyPatched && !force) {
|
|
113
|
+
console.log(result.message);
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
if (!result.success) {
|
|
117
|
+
console.error('✗ ' + result.message);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
console.log(result.message);
|
|
121
|
+
}
|
|
122
|
+
function cmdUnpatch(opts) {
|
|
123
|
+
const targetFile = opts['file'];
|
|
124
|
+
if (!targetFile) {
|
|
125
|
+
console.error('Usage: npx baileys-antiban unpatch --file <path-to-patched-file>');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
const result = unpatchFile(targetFile);
|
|
129
|
+
if (!result.success) {
|
|
130
|
+
console.error('✗ ' + result.message);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
console.log(result.message);
|
|
134
|
+
}
|
|
135
|
+
// Main
|
|
136
|
+
const opts = parseArgs(args);
|
|
137
|
+
switch (command) {
|
|
138
|
+
case 'status':
|
|
139
|
+
cmdStatus(opts);
|
|
140
|
+
break;
|
|
141
|
+
case 'reset':
|
|
142
|
+
cmdReset(opts);
|
|
143
|
+
break;
|
|
144
|
+
case 'warmup':
|
|
145
|
+
if (opts['simulate']) {
|
|
146
|
+
cmdWarmupSimulate(opts);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
console.error('Usage: npx baileys-antiban warmup --simulate <days> [--preset conservative|moderate|aggressive]');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
case 'patch':
|
|
154
|
+
cmdPatch(opts);
|
|
155
|
+
break;
|
|
156
|
+
case 'unpatch':
|
|
157
|
+
cmdUnpatch(opts);
|
|
158
|
+
break;
|
|
159
|
+
default:
|
|
160
|
+
console.log('baileys-antiban v3.0');
|
|
161
|
+
console.log('');
|
|
162
|
+
console.log('Commands:');
|
|
163
|
+
console.log(' status [--state <path>] [--json] Show warmup and health status');
|
|
164
|
+
console.log(' reset --state <path> Delete state file');
|
|
165
|
+
console.log(' warmup --simulate <days> [--preset] Show warmup schedule');
|
|
166
|
+
console.log(' patch [--path <baileys-dir>] [--preset] [--min-delay] [--max-delay] [--no-typing] [--dry-run]');
|
|
167
|
+
console.log(' unpatch --file <patched-file> Restore file from backup');
|
|
168
|
+
console.log('');
|
|
169
|
+
console.log('Examples:');
|
|
170
|
+
console.log(' npx baileys-antiban status --state ./antiban-state.json');
|
|
171
|
+
console.log(' npx baileys-antiban warmup --simulate 7 --preset moderate');
|
|
172
|
+
console.log(' npx baileys-antiban reset --state ./antiban-state.json');
|
|
173
|
+
console.log(' npx baileys-antiban patch');
|
|
174
|
+
console.log(' npx baileys-antiban patch --path ./node_modules/@openclaw/whatsapp/node_modules/baileys');
|
|
175
|
+
console.log(' npx baileys-antiban patch --preset moderate --min-delay 1000 --max-delay 3000');
|
|
176
|
+
console.log(' npx baileys-antiban unpatch --file ./node_modules/baileys/lib/socket/index.js');
|
|
177
|
+
}
|
|
@@ -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
|
+
}
|