@prototypevip/baileys 0.1.0 → 0.1.2
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/Defaults/index.js +3 -3
- package/lib/Defaults/index.js.bak-1788476907801 +187 -0
- package/lib/Defaults/index.js.bak-1788479166288 +187 -0
- package/lib/Socket/groups.js.bak-1788476973954 +475 -0
- package/lib/Socket/index.js +42 -126
- package/lib/Socket/index.js.bak-1788479281092 +38 -0
- package/lib/Socket/messages-send.js +88 -51
- package/lib/Store/prototype-store.js +34 -22
- package/lib/Store/prototype-store.js.bak-1788477006895 +983 -0
- package/lib/Utils/index.js +2 -0
- package/lib/Utils/integrity-guard.js +281 -0
- package/lib/Utils/interactive-message-handler.js +28 -8
- package/lib/Utils/interactive-message-handler.js.bak-1788476898661 +478 -0
- package/lib/Utils/socket-guard.js +154 -0
- package/package.json +2 -2
- package/lib/AntiBan/antiban.js +0 -922
- package/lib/AntiBan/banRecoveryOrchestrator.js +0 -284
- package/lib/AntiBan/cli.js +0 -177
- package/lib/AntiBan/contactGraph.js +0 -236
- package/lib/AntiBan/contentVariator.js +0 -150
- package/lib/AntiBan/credsSnapshot.js +0 -121
- package/lib/AntiBan/deliveryTracker.js +0 -107
- package/lib/AntiBan/deviceFingerprint.js +0 -106
- package/lib/AntiBan/fleetEventStore.js +0 -157
- package/lib/AntiBan/groupOperationGuard.js +0 -116
- package/lib/AntiBan/health.js +0 -207
- package/lib/AntiBan/humanEntropy.js +0 -337
- package/lib/AntiBan/index.js +0 -77
- package/lib/AntiBan/instanceCoordinator.js +0 -175
- package/lib/AntiBan/jidCanonicalizer.js +0 -256
- package/lib/AntiBan/jidCircuitBreaker.js +0 -186
- package/lib/AntiBan/legitimacySignalInjector.js +0 -231
- package/lib/AntiBan/lidFirstResolver.js +0 -174
- package/lib/AntiBan/lidResolver.js +0 -324
- package/lib/AntiBan/messageQueue.js +0 -187
- package/lib/AntiBan/messageRecovery.js +0 -283
- package/lib/AntiBan/messageTypeRegistry.js +0 -369
- package/lib/AntiBan/observability.js +0 -145
- package/lib/AntiBan/patch.js +0 -302
- package/lib/AntiBan/persist.js +0 -87
- package/lib/AntiBan/presenceChoreographer.js +0 -430
- package/lib/AntiBan/presets.js +0 -105
- package/lib/AntiBan/profiles.js +0 -31
- package/lib/AntiBan/proxyRotator.js +0 -306
- package/lib/AntiBan/rateLimiter.js +0 -261
- package/lib/AntiBan/readReceiptVariance.js +0 -88
- package/lib/AntiBan/reconnectThrottle.js +0 -180
- package/lib/AntiBan/replyRatio.js +0 -161
- package/lib/AntiBan/reputationVoucher.js +0 -373
- package/lib/AntiBan/retryReason.js +0 -91
- package/lib/AntiBan/retryTracker.js +0 -172
- package/lib/AntiBan/scheduler.js +0 -111
- package/lib/AntiBan/sessionFingerprint.js +0 -248
- package/lib/AntiBan/sessionStability.js +0 -330
- package/lib/AntiBan/stateAdapter.js +0 -73
- package/lib/AntiBan/stateExport.js +0 -152
- package/lib/AntiBan/stealthConnect.js +0 -130
- package/lib/AntiBan/timelockGuard.js +0 -181
- package/lib/AntiBan/topologyThrottler.js +0 -339
- package/lib/AntiBan/warmup.js +0 -140
- package/lib/AntiBan/webhooks.js +0 -80
- package/lib/AntiBan/wrapper.js +0 -456
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Proxy Rotation — Native proxy injection for Baileys with health tracking
|
|
3
|
-
*
|
|
4
|
-
* WhatsApp's ban detection includes IP reputation scoring. Datacenter IPs (VPS)
|
|
5
|
-
* are flagged, while residential/4G proxies stay alive. No Baileys library handles
|
|
6
|
-
* native proxy injection — every implementation is DIY hacks. We close that gap.
|
|
7
|
-
*
|
|
8
|
-
* Features:
|
|
9
|
-
* - Multi-strategy rotation (round-robin, random, LRU, weighted)
|
|
10
|
-
* - Auto-failover on endpoint failure
|
|
11
|
-
* - Scheduled rotation for proactive IP rotation
|
|
12
|
-
* - Cooldown periods between endpoint reuse
|
|
13
|
-
* - Health tracking and auto-resurrection
|
|
14
|
-
* - Lazy-loaded proxy agent dependencies (optional peerDeps)
|
|
15
|
-
*
|
|
16
|
-
* @author Kobus Wentzel <kobie@pop.co.za>
|
|
17
|
-
* @license MIT
|
|
18
|
-
*/
|
|
19
|
-
// Load optional peer dependencies synchronously in both ESM and CJS builds.
|
|
20
|
-
// CJS: native require is available. ESM: new Function() reads import.meta.url
|
|
21
|
-
// without causing TypeScript parse errors in CJS compilation. The string passed
|
|
22
|
-
// to new Function is a static literal — not user-controlled.
|
|
23
|
-
function lazyRequire(moduleName) {
|
|
24
|
-
if (typeof require !== 'undefined') {
|
|
25
|
-
return require(moduleName);
|
|
26
|
-
}
|
|
27
|
-
// ESM path: new Function with static literal string avoids TS CJS-parse error on import.meta.
|
|
28
|
-
// Not user-controlled — both strings are compile-time constants.
|
|
29
|
-
const { createRequire } = (new Function('return require')())('node:module');
|
|
30
|
-
const metaUrl = new Function('return import.meta.url')();
|
|
31
|
-
return createRequire(metaUrl)(moduleName);
|
|
32
|
-
}
|
|
33
|
-
const NoopLogger = {
|
|
34
|
-
info: () => { },
|
|
35
|
-
warn: () => { },
|
|
36
|
-
error: () => { },
|
|
37
|
-
};
|
|
38
|
-
export function proxyRotator(config) {
|
|
39
|
-
const { pool, strategy = 'round-robin', rotateOn = ['disconnect', 'ban-warning'], scheduledIntervalMs = 0, maxFailures = 3, deadCooldownMs = 600_000, // 10 minutes
|
|
40
|
-
logger = NoopLogger, } = config;
|
|
41
|
-
if (!pool || pool.length === 0) {
|
|
42
|
-
throw new Error('proxyRotator: pool cannot be empty');
|
|
43
|
-
}
|
|
44
|
-
// Warn once for pool size 1
|
|
45
|
-
if (pool.length === 1) {
|
|
46
|
-
logger.warn?.('proxyRotator: pool size is 1. Rotation is a no-op.');
|
|
47
|
-
}
|
|
48
|
-
// Warn for aggressive scheduled rotation
|
|
49
|
-
if (scheduledIntervalMs > 0 && scheduledIntervalMs < 60_000) {
|
|
50
|
-
logger.warn?.(`proxyRotator: scheduledIntervalMs (${scheduledIntervalMs}ms) is < 60s. May hammer proxy provider.`);
|
|
51
|
-
}
|
|
52
|
-
// Internal state
|
|
53
|
-
const states = pool.map((endpoint) => ({
|
|
54
|
-
endpoint,
|
|
55
|
-
failures: 0,
|
|
56
|
-
lastUsedAt: null,
|
|
57
|
-
isDead: false,
|
|
58
|
-
}));
|
|
59
|
-
let currentIndex = 0;
|
|
60
|
-
let totalRotations = 0;
|
|
61
|
-
const rotationsByTrigger = {};
|
|
62
|
-
let scheduledTimer = null;
|
|
63
|
-
// Agent cache: map endpoint -> Agent (cleared on rotation)
|
|
64
|
-
const agentCache = new Map();
|
|
65
|
-
// Module cache for lazy-loaded proxy agents
|
|
66
|
-
const moduleCache = {};
|
|
67
|
-
function buildProxyUrl(endpoint) {
|
|
68
|
-
const { type, host, port, username, password } = endpoint;
|
|
69
|
-
const auth = username && password ? `${username}:${password}@` : '';
|
|
70
|
-
return `${type}://${auth}${host}:${port}`;
|
|
71
|
-
}
|
|
72
|
-
function createAgentForEndpointSync(endpoint) {
|
|
73
|
-
// Check cache first
|
|
74
|
-
if (agentCache.has(endpoint)) {
|
|
75
|
-
return agentCache.get(endpoint);
|
|
76
|
-
}
|
|
77
|
-
const url = buildProxyUrl(endpoint);
|
|
78
|
-
let agent = null;
|
|
79
|
-
try {
|
|
80
|
-
if (endpoint.type === 'socks5' || endpoint.type === 'socks5h') {
|
|
81
|
-
if (!moduleCache['socks-proxy-agent']) {
|
|
82
|
-
try {
|
|
83
|
-
moduleCache['socks-proxy-agent'] = lazyRequire('socks-proxy-agent');
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
logger.error?.('socks-proxy-agent not installed. Run: npm install socks-proxy-agent');
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
agent = new moduleCache['socks-proxy-agent'].SocksProxyAgent(url);
|
|
91
|
-
}
|
|
92
|
-
else if (endpoint.type === 'http') {
|
|
93
|
-
if (!moduleCache['http-proxy-agent']) {
|
|
94
|
-
try {
|
|
95
|
-
moduleCache['http-proxy-agent'] = lazyRequire('http-proxy-agent');
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
logger.error?.('http-proxy-agent not installed. Run: npm install http-proxy-agent');
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
agent = new moduleCache['http-proxy-agent'].HttpProxyAgent(url);
|
|
103
|
-
}
|
|
104
|
-
else if (endpoint.type === 'https') {
|
|
105
|
-
if (!moduleCache['https-proxy-agent']) {
|
|
106
|
-
try {
|
|
107
|
-
moduleCache['https-proxy-agent'] = lazyRequire('https-proxy-agent');
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
logger.error?.('https-proxy-agent not installed. Run: npm install https-proxy-agent');
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
agent = new moduleCache['https-proxy-agent'].HttpsProxyAgent(url);
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
logger.error?.(`Unknown proxy type: ${endpoint.type}`);
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
// Cache the agent
|
|
121
|
-
if (agent) {
|
|
122
|
-
agentCache.set(endpoint, agent);
|
|
123
|
-
}
|
|
124
|
-
return agent;
|
|
125
|
-
}
|
|
126
|
-
catch (err) {
|
|
127
|
-
logger.error?.(`Failed to create agent for ${endpoint.label || endpoint.host}: ${err}`);
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
function getAliveEndpoints() {
|
|
132
|
-
const now = Date.now();
|
|
133
|
-
return states
|
|
134
|
-
.map((s, idx) => {
|
|
135
|
-
// Dead check with auto-resurrection
|
|
136
|
-
if (s.isDead && s.lastUsedAt) {
|
|
137
|
-
if (now - s.lastUsedAt.getTime() >= deadCooldownMs) {
|
|
138
|
-
s.isDead = false;
|
|
139
|
-
s.failures = 0;
|
|
140
|
-
logger.info?.(`Resurrected endpoint ${s.endpoint.label || s.endpoint.host} after cooldown`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
// Cooldown check
|
|
144
|
-
const cooldown = s.endpoint.cooldownMs || 0;
|
|
145
|
-
if (cooldown > 0 && s.lastUsedAt) {
|
|
146
|
-
if (now - s.lastUsedAt.getTime() < cooldown) {
|
|
147
|
-
return -1; // Still in cooldown
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return !s.isDead ? idx : -1;
|
|
151
|
-
})
|
|
152
|
-
.filter((idx) => idx !== -1);
|
|
153
|
-
}
|
|
154
|
-
function selectNextIndex(alive) {
|
|
155
|
-
if (alive.length === 0)
|
|
156
|
-
return currentIndex; // All dead, stay on current
|
|
157
|
-
if (strategy === 'round-robin') {
|
|
158
|
-
// Pick next after currentIndex in circular fashion
|
|
159
|
-
const afterCurrent = alive.filter((idx) => idx > currentIndex);
|
|
160
|
-
if (afterCurrent.length > 0)
|
|
161
|
-
return afterCurrent[0];
|
|
162
|
-
return alive[0]; // Wrap around
|
|
163
|
-
}
|
|
164
|
-
if (strategy === 'random') {
|
|
165
|
-
return alive[Math.floor(Math.random() * alive.length)];
|
|
166
|
-
}
|
|
167
|
-
if (strategy === 'least-recently-used') {
|
|
168
|
-
// Pick the one with oldest lastUsedAt (never-used = highest priority)
|
|
169
|
-
const neverUsed = alive.filter((idx) => states[idx].lastUsedAt === null);
|
|
170
|
-
if (neverUsed.length > 0) {
|
|
171
|
-
return neverUsed[0];
|
|
172
|
-
}
|
|
173
|
-
let oldestIdx = alive[0];
|
|
174
|
-
let oldestTime = states[oldestIdx].lastUsedAt.getTime();
|
|
175
|
-
for (const idx of alive) {
|
|
176
|
-
const time = states[idx].lastUsedAt.getTime();
|
|
177
|
-
if (time < oldestTime) {
|
|
178
|
-
oldestTime = time;
|
|
179
|
-
oldestIdx = idx;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return oldestIdx;
|
|
183
|
-
}
|
|
184
|
-
if (strategy === 'weighted') {
|
|
185
|
-
// Weighted by inverse failure count (healthier = more likely)
|
|
186
|
-
const weights = alive.map((idx) => {
|
|
187
|
-
const failures = states[idx].failures;
|
|
188
|
-
return 1 / (failures + 1); // Avoid divide-by-zero
|
|
189
|
-
});
|
|
190
|
-
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
191
|
-
let rand = Math.random() * totalWeight;
|
|
192
|
-
for (let i = 0; i < alive.length; i++) {
|
|
193
|
-
rand -= weights[i];
|
|
194
|
-
if (rand <= 0)
|
|
195
|
-
return alive[i];
|
|
196
|
-
}
|
|
197
|
-
return alive[alive.length - 1]; // Fallback
|
|
198
|
-
}
|
|
199
|
-
return alive[0]; // Default fallback
|
|
200
|
-
}
|
|
201
|
-
function rotateImpl(reason = 'manual') {
|
|
202
|
-
if (pool.length === 1) {
|
|
203
|
-
// No-op for single endpoint
|
|
204
|
-
return states[0].endpoint;
|
|
205
|
-
}
|
|
206
|
-
const alive = getAliveEndpoints();
|
|
207
|
-
if (alive.length === 0) {
|
|
208
|
-
logger.warn?.('All endpoints are dead. Cannot rotate.');
|
|
209
|
-
return states[currentIndex].endpoint;
|
|
210
|
-
}
|
|
211
|
-
const nextIdx = selectNextIndex(alive);
|
|
212
|
-
if (nextIdx === currentIndex && alive.length > 1) {
|
|
213
|
-
// Try to pick a different one if possible
|
|
214
|
-
const others = alive.filter((idx) => idx !== currentIndex);
|
|
215
|
-
if (others.length > 0) {
|
|
216
|
-
currentIndex = others[0];
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
currentIndex = nextIdx;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
currentIndex = nextIdx;
|
|
224
|
-
}
|
|
225
|
-
states[currentIndex].lastUsedAt = new Date();
|
|
226
|
-
totalRotations++;
|
|
227
|
-
rotationsByTrigger[reason] = (rotationsByTrigger[reason] || 0) + 1;
|
|
228
|
-
const label = states[currentIndex].endpoint.label || states[currentIndex].endpoint.host;
|
|
229
|
-
logger.info?.(`Rotated to endpoint ${label} (reason: ${reason})`);
|
|
230
|
-
return states[currentIndex].endpoint;
|
|
231
|
-
}
|
|
232
|
-
function markFailureImpl() {
|
|
233
|
-
const state = states[currentIndex];
|
|
234
|
-
state.failures++;
|
|
235
|
-
const label = state.endpoint.label || state.endpoint.host;
|
|
236
|
-
logger.warn?.(`Endpoint ${label} failed (${state.failures}/${maxFailures})`);
|
|
237
|
-
if (state.failures >= maxFailures) {
|
|
238
|
-
state.isDead = true;
|
|
239
|
-
logger.error?.(`Endpoint ${label} marked DEAD after ${maxFailures} failures`);
|
|
240
|
-
// Auto-rotate to next alive endpoint
|
|
241
|
-
const alive = getAliveEndpoints();
|
|
242
|
-
if (alive.length > 0) {
|
|
243
|
-
rotateImpl('manual'); // Trigger rotation as recovery
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
function resurrectAllImpl() {
|
|
248
|
-
let count = 0;
|
|
249
|
-
for (const state of states) {
|
|
250
|
-
if (state.isDead) {
|
|
251
|
-
state.isDead = false;
|
|
252
|
-
state.failures = 0;
|
|
253
|
-
count++;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
if (count > 0) {
|
|
257
|
-
logger.info?.(`Resurrected ${count} dead endpoint(s)`);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
function stopImpl() {
|
|
261
|
-
if (scheduledTimer) {
|
|
262
|
-
clearInterval(scheduledTimer);
|
|
263
|
-
scheduledTimer = null;
|
|
264
|
-
logger.info?.('Stopped scheduled rotation timer');
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
function getStatsImpl() {
|
|
268
|
-
return {
|
|
269
|
-
totalRotations,
|
|
270
|
-
rotationsByTrigger: { ...rotationsByTrigger },
|
|
271
|
-
endpointHealth: states.map((s) => ({
|
|
272
|
-
label: s.endpoint.label || s.endpoint.host,
|
|
273
|
-
inUse: states[currentIndex] === s,
|
|
274
|
-
failures: s.failures,
|
|
275
|
-
lastUsedAt: s.lastUsedAt,
|
|
276
|
-
isDead: s.isDead,
|
|
277
|
-
})),
|
|
278
|
-
currentEndpoint: states[currentIndex].endpoint.label || states[currentIndex].endpoint.host,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
function currentAgentImpl() {
|
|
282
|
-
const endpoint = states[currentIndex].endpoint;
|
|
283
|
-
return createAgentForEndpointSync(endpoint);
|
|
284
|
-
}
|
|
285
|
-
function currentImpl() {
|
|
286
|
-
return states[currentIndex].endpoint;
|
|
287
|
-
}
|
|
288
|
-
// Setup scheduled rotation if enabled
|
|
289
|
-
if (rotateOn.includes('scheduled') && scheduledIntervalMs > 0) {
|
|
290
|
-
scheduledTimer = setInterval(() => {
|
|
291
|
-
rotateImpl('scheduled');
|
|
292
|
-
}, scheduledIntervalMs);
|
|
293
|
-
logger.info?.(`Scheduled rotation enabled (every ${scheduledIntervalMs}ms)`);
|
|
294
|
-
}
|
|
295
|
-
// Initialize: select first endpoint
|
|
296
|
-
states[0].lastUsedAt = new Date();
|
|
297
|
-
return {
|
|
298
|
-
currentAgent: currentAgentImpl,
|
|
299
|
-
current: currentImpl,
|
|
300
|
-
rotate: rotateImpl,
|
|
301
|
-
markFailure: markFailureImpl,
|
|
302
|
-
resurrectAll: resurrectAllImpl,
|
|
303
|
-
stop: stopImpl,
|
|
304
|
-
getStats: getStatsImpl,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rate Limiter — Enforces human-like message pacing
|
|
3
|
-
*
|
|
4
|
-
* WhatsApp's detection looks for:
|
|
5
|
-
* - Too many messages per minute/hour
|
|
6
|
-
* - Identical messages to multiple recipients
|
|
7
|
-
* - No variation in timing between messages
|
|
8
|
-
* - Sudden spikes in activity
|
|
9
|
-
* - Messages sent at inhuman speed
|
|
10
|
-
*/
|
|
11
|
-
// Time constants for clarity
|
|
12
|
-
const TIME_CONSTANTS = {
|
|
13
|
-
MS_PER_SECOND: 1000,
|
|
14
|
-
MS_PER_MINUTE: 60000,
|
|
15
|
-
MS_PER_HOUR: 3600000,
|
|
16
|
-
MS_PER_DAY: 86400000,
|
|
17
|
-
BURST_RESET_MS: 30000,
|
|
18
|
-
IDENTICAL_WINDOW_MS: 3600000, // 1 hour
|
|
19
|
-
};
|
|
20
|
-
const DEFAULT_CONFIG = {
|
|
21
|
-
maxPerMinute: 8,
|
|
22
|
-
maxPerHour: 200,
|
|
23
|
-
maxPerDay: 1500,
|
|
24
|
-
minDelayMs: 1500,
|
|
25
|
-
maxDelayMs: 5000,
|
|
26
|
-
newChatDelayMs: 3000,
|
|
27
|
-
maxIdenticalMessages: 3,
|
|
28
|
-
burstAllowance: 3,
|
|
29
|
-
identicalMessageWindowMs: TIME_CONSTANTS.IDENTICAL_WINDOW_MS,
|
|
30
|
-
};
|
|
31
|
-
export class RateLimiter {
|
|
32
|
-
config;
|
|
33
|
-
originalConfig;
|
|
34
|
-
messages = [];
|
|
35
|
-
identicalCount = new Map();
|
|
36
|
-
knownChats = new Set();
|
|
37
|
-
burstCount = 0;
|
|
38
|
-
lastMessageTime = 0;
|
|
39
|
-
constructor(config = {}) {
|
|
40
|
-
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
41
|
-
this.originalConfig = { ...this.config };
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Calculate delay before next message can be sent.
|
|
45
|
-
* Returns 0 if message can be sent immediately.
|
|
46
|
-
* Returns -1 if message should be blocked entirely.
|
|
47
|
-
*/
|
|
48
|
-
async getDelay(recipient, content) {
|
|
49
|
-
const now = Date.now();
|
|
50
|
-
this.cleanup(now);
|
|
51
|
-
const contentHash = this.hashContent(content);
|
|
52
|
-
// Check daily limit
|
|
53
|
-
const dayMessages = this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_DAY);
|
|
54
|
-
if (dayMessages.length >= this.config.maxPerDay) {
|
|
55
|
-
return -1; // Hard block — daily limit reached
|
|
56
|
-
}
|
|
57
|
-
// Check hourly limit
|
|
58
|
-
const hourMessages = this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_HOUR);
|
|
59
|
-
if (hourMessages.length >= this.config.maxPerHour) {
|
|
60
|
-
// Sort by timestamp to find the oldest message in the window
|
|
61
|
-
hourMessages.sort((a, b) => a.timestamp - b.timestamp);
|
|
62
|
-
const oldestInHour = hourMessages[0];
|
|
63
|
-
const delay = oldestInHour ? (oldestInHour.timestamp + TIME_CONSTANTS.MS_PER_HOUR) - now : TIME_CONSTANTS.MS_PER_HOUR;
|
|
64
|
-
// Return a proper blocking delay (at least the time until the oldest message expires)
|
|
65
|
-
return Math.max(delay, TIME_CONSTANTS.MS_PER_MINUTE);
|
|
66
|
-
}
|
|
67
|
-
// Check per-minute limit
|
|
68
|
-
const minuteMessages = this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_MINUTE);
|
|
69
|
-
if (minuteMessages.length >= this.config.maxPerMinute) {
|
|
70
|
-
// Sort by timestamp to find the oldest message in the window
|
|
71
|
-
minuteMessages.sort((a, b) => a.timestamp - b.timestamp);
|
|
72
|
-
const oldestInMinute = minuteMessages[0];
|
|
73
|
-
const delay = oldestInMinute ? (oldestInMinute.timestamp + TIME_CONSTANTS.MS_PER_MINUTE) - now : TIME_CONSTANTS.MS_PER_MINUTE;
|
|
74
|
-
return Math.max(delay, TIME_CONSTANTS.MS_PER_SECOND);
|
|
75
|
-
}
|
|
76
|
-
// Check identical message limit (within time window)
|
|
77
|
-
const tracker = this.identicalCount.get(contentHash);
|
|
78
|
-
if (tracker) {
|
|
79
|
-
// Check if tracker is still within the time window
|
|
80
|
-
if (now - tracker.firstSeen < this.config.identicalMessageWindowMs) {
|
|
81
|
-
if (tracker.count >= this.config.maxIdenticalMessages) {
|
|
82
|
-
return -1; // Block identical spam within time window
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
// Calculate human-like delay
|
|
87
|
-
let delay = 0;
|
|
88
|
-
// Burst allowance — first few messages can be faster
|
|
89
|
-
if (this.burstCount < this.config.burstAllowance) {
|
|
90
|
-
this.burstCount++;
|
|
91
|
-
delay = this.jitter(this.config.minDelayMs * 0.5, this.config.minDelayMs);
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
delay = this.jitter(this.config.minDelayMs, this.config.maxDelayMs);
|
|
95
|
-
}
|
|
96
|
-
// Extra delay for new chats (first message to this recipient)
|
|
97
|
-
if (!this.knownChats.has(recipient)) {
|
|
98
|
-
delay += this.jitter(this.config.newChatDelayMs * 0.5, this.config.newChatDelayMs);
|
|
99
|
-
}
|
|
100
|
-
// Ensure minimum time since last message
|
|
101
|
-
const timeSinceLast = now - this.lastMessageTime;
|
|
102
|
-
if (timeSinceLast < this.config.minDelayMs) {
|
|
103
|
-
delay = Math.max(delay, this.config.minDelayMs - timeSinceLast);
|
|
104
|
-
}
|
|
105
|
-
// Add "typing simulation" delay based on content length
|
|
106
|
-
const typingDelay = Math.min(content.length * 30, 3000); // ~30ms per char, max 3s
|
|
107
|
-
delay += this.jitter(typingDelay * 0.5, typingDelay);
|
|
108
|
-
return Math.round(delay);
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Record a sent message
|
|
112
|
-
*/
|
|
113
|
-
record(recipient, content) {
|
|
114
|
-
const now = Date.now();
|
|
115
|
-
const contentHash = this.hashContent(content);
|
|
116
|
-
// BUG FIX 1: Check burst reset BEFORE updating lastMessageTime
|
|
117
|
-
const timeSinceLast = now - this.lastMessageTime;
|
|
118
|
-
if (timeSinceLast > TIME_CONSTANTS.BURST_RESET_MS) {
|
|
119
|
-
this.burstCount = 0;
|
|
120
|
-
}
|
|
121
|
-
this.messages.push({ timestamp: now, recipient, contentHash });
|
|
122
|
-
this.knownChats.add(recipient);
|
|
123
|
-
this.lastMessageTime = now;
|
|
124
|
-
// Track identical messages with time window
|
|
125
|
-
const tracker = this.identicalCount.get(contentHash);
|
|
126
|
-
if (tracker) {
|
|
127
|
-
// Check if within same window
|
|
128
|
-
if (now - tracker.firstSeen < this.config.identicalMessageWindowMs) {
|
|
129
|
-
tracker.count++;
|
|
130
|
-
tracker.lastSeen = now;
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
// Start new window
|
|
134
|
-
this.identicalCount.set(contentHash, { count: 1, firstSeen: now, lastSeen: now });
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
this.identicalCount.set(contentHash, { count: 1, firstSeen: now, lastSeen: now });
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Get current usage stats
|
|
143
|
-
*/
|
|
144
|
-
getStats() {
|
|
145
|
-
const now = Date.now();
|
|
146
|
-
this.cleanup(now);
|
|
147
|
-
return {
|
|
148
|
-
lastMinute: this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_MINUTE).length,
|
|
149
|
-
lastHour: this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_HOUR).length,
|
|
150
|
-
lastDay: this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_DAY).length,
|
|
151
|
-
limits: {
|
|
152
|
-
perMinute: this.config.maxPerMinute,
|
|
153
|
-
perHour: this.config.maxPerHour,
|
|
154
|
-
perDay: this.config.maxPerDay,
|
|
155
|
-
},
|
|
156
|
-
knownChats: this.knownChats.size,
|
|
157
|
-
currentFactor: this.getCurrentFactor(),
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Dynamically scale rate limits by a factor (0.1–1.0).
|
|
162
|
-
* factor=1.0 = original config limits (full speed)
|
|
163
|
-
* factor=0.5 = 50% of configured limits
|
|
164
|
-
* Floors: perMinute >= 1, perHour >= 5, perDay >= 20
|
|
165
|
-
* Also scales minDelayMs/maxDelayMs inversely (slower sends when throttled).
|
|
166
|
-
*/
|
|
167
|
-
adaptLimits(factor) {
|
|
168
|
-
const f = Math.max(0.1, Math.min(1.0, factor));
|
|
169
|
-
this.config.maxPerMinute = Math.max(1, Math.floor(this.originalConfig.maxPerMinute * f));
|
|
170
|
-
this.config.maxPerHour = Math.max(5, Math.floor(this.originalConfig.maxPerHour * f));
|
|
171
|
-
this.config.maxPerDay = Math.max(20, Math.floor(this.originalConfig.maxPerDay * f));
|
|
172
|
-
// Inverse: lower factor = longer delays (more human when throttled)
|
|
173
|
-
const delayScale = 1 + (1 - f) * 2; // f=1.0→1x, f=0.5→2x, f=0.1→3.8x
|
|
174
|
-
this.config.minDelayMs = Math.floor(this.originalConfig.minDelayMs * delayScale);
|
|
175
|
-
this.config.maxDelayMs = Math.floor(this.originalConfig.maxDelayMs * delayScale);
|
|
176
|
-
}
|
|
177
|
-
/** Return current effective factor (0.1–1.0) */
|
|
178
|
-
getCurrentFactor() {
|
|
179
|
-
return this.config.maxPerMinute / this.originalConfig.maxPerMinute;
|
|
180
|
-
}
|
|
181
|
-
/** Get the set of known chat JIDs (for state persistence) */
|
|
182
|
-
getKnownChats() {
|
|
183
|
-
return this.knownChats;
|
|
184
|
-
}
|
|
185
|
-
/** Restore known chats from persisted state */
|
|
186
|
-
restoreKnownChats(chats) {
|
|
187
|
-
for (const jid of chats) {
|
|
188
|
-
this.knownChats.add(jid);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* BUG FIX 3: Inject historical timestamps into the sliding window
|
|
193
|
-
* Used by InstanceCoordinator to sync local limiter with shared pool after reconnect.
|
|
194
|
-
* This prevents the double-spend window where local limiter thinks it has full budget
|
|
195
|
-
* but the shared pool already shows most slots used.
|
|
196
|
-
*/
|
|
197
|
-
injectTimestamps(timestamps) {
|
|
198
|
-
const now = Date.now();
|
|
199
|
-
// Filter to last 24 hours only (our cleanup window)
|
|
200
|
-
const recentTimestamps = timestamps.filter(ts => now - ts < TIME_CONSTANTS.MS_PER_DAY);
|
|
201
|
-
// Merge with existing messages, avoiding duplicates by timestamp
|
|
202
|
-
const existingTimes = new Set(this.messages.map(m => m.timestamp));
|
|
203
|
-
for (const ts of recentTimestamps) {
|
|
204
|
-
if (!existingTimes.has(ts)) {
|
|
205
|
-
// Create synthetic message record with placeholder data
|
|
206
|
-
this.messages.push({
|
|
207
|
-
timestamp: ts,
|
|
208
|
-
recipient: '__injected__', // placeholder - we don't know the actual recipient
|
|
209
|
-
contentHash: '__injected__', // placeholder - we don't know the content
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
// Update lastMessageTime if injected timestamps are more recent
|
|
214
|
-
const maxInjected = Math.max(...recentTimestamps, 0);
|
|
215
|
-
if (maxInjected > this.lastMessageTime) {
|
|
216
|
-
this.lastMessageTime = maxInjected;
|
|
217
|
-
}
|
|
218
|
-
// Sort messages by timestamp to maintain sliding window integrity
|
|
219
|
-
this.messages.sort((a, b) => a.timestamp - b.timestamp);
|
|
220
|
-
}
|
|
221
|
-
cleanup(now) {
|
|
222
|
-
// Remove messages older than 24 hours
|
|
223
|
-
this.messages = this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_DAY);
|
|
224
|
-
// Clean up identicalCount Map based on time windows (not just message presence)
|
|
225
|
-
// Remove trackers where the window has expired
|
|
226
|
-
for (const [hash, tracker] of this.identicalCount.entries()) {
|
|
227
|
-
if (now - tracker.lastSeen > this.config.identicalMessageWindowMs) {
|
|
228
|
-
this.identicalCount.delete(hash);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
// LRU size cap — prevents unbounded growth when sending many unique messages.
|
|
232
|
-
// Evict oldest-by-lastSeen entries when map exceeds 10,000 entries.
|
|
233
|
-
const IDENTICAL_COUNT_MAX = 10_000;
|
|
234
|
-
if (this.identicalCount.size > IDENTICAL_COUNT_MAX) {
|
|
235
|
-
const sorted = [...this.identicalCount.entries()].sort(([, a], [, b]) => a.lastSeen - b.lastSeen);
|
|
236
|
-
const excess = this.identicalCount.size - IDENTICAL_COUNT_MAX;
|
|
237
|
-
for (let i = 0; i < excess; i++)
|
|
238
|
-
this.identicalCount.delete(sorted[i][0]);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
/** Random delay between min and max (gaussian-ish distribution) */
|
|
242
|
-
jitter(min, max) {
|
|
243
|
-
// Use Box-Muller for more human-like distribution (clustered around middle)
|
|
244
|
-
const u1 = Math.random();
|
|
245
|
-
const u2 = Math.random();
|
|
246
|
-
const normal = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
247
|
-
const normalized = (normal + 3) / 6; // Map to ~0-1 range
|
|
248
|
-
const clamped = Math.max(0, Math.min(1, normalized));
|
|
249
|
-
return Math.round(min + clamped * (max - min));
|
|
250
|
-
}
|
|
251
|
-
/** Simple hash for content dedup */
|
|
252
|
-
hashContent(content) {
|
|
253
|
-
let hash = 0;
|
|
254
|
-
for (let i = 0; i < content.length; i++) {
|
|
255
|
-
const char = content.charCodeAt(i);
|
|
256
|
-
hash = ((hash << 5) - hash) + char;
|
|
257
|
-
hash |= 0;
|
|
258
|
-
}
|
|
259
|
-
return hash.toString(36);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read Receipt Timing Variance
|
|
3
|
-
*
|
|
4
|
-
* Extends presence choreography to randomize read-receipt delay.
|
|
5
|
-
* Instant reads = bot signal. Gaussian jitter makes reads feel human.
|
|
6
|
-
*
|
|
7
|
-
* @author Kobus Wentzel <kobie@pop.co.za>
|
|
8
|
-
* @license MIT
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* Box-Muller transform for Gaussian random samples
|
|
12
|
-
* Returns a value from normal distribution (mean=0, stdDev=1)
|
|
13
|
-
*/
|
|
14
|
-
function gaussianRandom() {
|
|
15
|
-
let u = 0;
|
|
16
|
-
let v = 0;
|
|
17
|
-
while (u === 0)
|
|
18
|
-
u = Math.random(); // Avoid log(0)
|
|
19
|
-
while (v === 0)
|
|
20
|
-
v = Math.random();
|
|
21
|
-
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
|
22
|
-
}
|
|
23
|
-
export function readReceiptVariance(config = {}) {
|
|
24
|
-
const { meanMs = 1500, stdDevMs = 800, minMs = 200, maxMs = 8000, skipIfOlderThanMs = 60_000, } = config;
|
|
25
|
-
const pendingTimers = new Set();
|
|
26
|
-
function delayMs() {
|
|
27
|
-
// Generate Gaussian sample and scale to configured mean/stdDev
|
|
28
|
-
const gaussian = gaussianRandom();
|
|
29
|
-
const value = meanMs + gaussian * stdDevMs;
|
|
30
|
-
// Clamp to min/max
|
|
31
|
-
return Math.max(minMs, Math.min(maxMs, value));
|
|
32
|
-
}
|
|
33
|
-
function wrap(sock) {
|
|
34
|
-
const originalReadMessages = sock.readMessages.bind(sock);
|
|
35
|
-
// Proxy the readMessages method
|
|
36
|
-
const wrappedReadMessages = async (keys) => {
|
|
37
|
-
// Check if messages are too old (backlog)
|
|
38
|
-
const now = Date.now();
|
|
39
|
-
const oldMessages = keys.every((key) => {
|
|
40
|
-
if (!key.messageTimestamp)
|
|
41
|
-
return false;
|
|
42
|
-
const msgTime = typeof key.messageTimestamp === 'number'
|
|
43
|
-
? key.messageTimestamp * 1000 // Baileys uses seconds
|
|
44
|
-
: parseInt(key.messageTimestamp, 10) * 1000;
|
|
45
|
-
return now - msgTime > skipIfOlderThanMs;
|
|
46
|
-
});
|
|
47
|
-
if (oldMessages) {
|
|
48
|
-
// Skip delay for backlog messages
|
|
49
|
-
return originalReadMessages(keys);
|
|
50
|
-
}
|
|
51
|
-
// Apply jittered delay
|
|
52
|
-
const delay = delayMs();
|
|
53
|
-
return new Promise((resolve, reject) => {
|
|
54
|
-
const timer = setTimeout(async () => {
|
|
55
|
-
pendingTimers.delete(timer);
|
|
56
|
-
try {
|
|
57
|
-
const result = await originalReadMessages(keys);
|
|
58
|
-
resolve(result);
|
|
59
|
-
}
|
|
60
|
-
catch (err) {
|
|
61
|
-
reject(err);
|
|
62
|
-
}
|
|
63
|
-
}, delay);
|
|
64
|
-
pendingTimers.add(timer);
|
|
65
|
-
});
|
|
66
|
-
};
|
|
67
|
-
// Return proxy with wrapped readMessages
|
|
68
|
-
return new Proxy(sock, {
|
|
69
|
-
get(target, prop) {
|
|
70
|
-
if (prop === 'readMessages') {
|
|
71
|
-
return wrappedReadMessages;
|
|
72
|
-
}
|
|
73
|
-
return target[prop];
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
function stop() {
|
|
78
|
-
for (const timer of pendingTimers) {
|
|
79
|
-
clearTimeout(timer);
|
|
80
|
-
}
|
|
81
|
-
pendingTimers.clear();
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
wrap,
|
|
85
|
-
delayMs,
|
|
86
|
-
stop,
|
|
87
|
-
};
|
|
88
|
-
}
|