@prototypevip/baileys 0.0.8 → 0.1.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.
Files changed (54) hide show
  1. package/lib/AntiBan/antiban.js +922 -0
  2. package/lib/AntiBan/banRecoveryOrchestrator.js +284 -0
  3. package/lib/AntiBan/cli.js +177 -0
  4. package/lib/AntiBan/contactGraph.js +236 -0
  5. package/lib/AntiBan/contentVariator.js +150 -0
  6. package/lib/AntiBan/credsSnapshot.js +121 -0
  7. package/lib/AntiBan/deliveryTracker.js +107 -0
  8. package/lib/AntiBan/deviceFingerprint.js +106 -0
  9. package/lib/AntiBan/fleetEventStore.js +157 -0
  10. package/lib/AntiBan/groupOperationGuard.js +116 -0
  11. package/lib/AntiBan/health.js +207 -0
  12. package/lib/AntiBan/humanEntropy.js +337 -0
  13. package/lib/AntiBan/index.js +77 -0
  14. package/lib/AntiBan/instanceCoordinator.js +175 -0
  15. package/lib/AntiBan/jidCanonicalizer.js +256 -0
  16. package/lib/AntiBan/jidCircuitBreaker.js +186 -0
  17. package/lib/AntiBan/legitimacySignalInjector.js +231 -0
  18. package/lib/AntiBan/lidFirstResolver.js +174 -0
  19. package/lib/AntiBan/lidResolver.js +324 -0
  20. package/lib/AntiBan/messageQueue.js +187 -0
  21. package/lib/AntiBan/messageRecovery.js +283 -0
  22. package/lib/AntiBan/messageTypeRegistry.js +369 -0
  23. package/lib/AntiBan/observability.js +145 -0
  24. package/lib/AntiBan/patch.js +302 -0
  25. package/lib/AntiBan/persist.js +87 -0
  26. package/lib/AntiBan/presenceChoreographer.js +430 -0
  27. package/lib/AntiBan/presets.js +105 -0
  28. package/lib/AntiBan/profiles.js +31 -0
  29. package/lib/AntiBan/proxyRotator.js +306 -0
  30. package/lib/AntiBan/rateLimiter.js +261 -0
  31. package/lib/AntiBan/readReceiptVariance.js +88 -0
  32. package/lib/AntiBan/reconnectThrottle.js +180 -0
  33. package/lib/AntiBan/replyRatio.js +161 -0
  34. package/lib/AntiBan/reputationVoucher.js +373 -0
  35. package/lib/AntiBan/retryReason.js +91 -0
  36. package/lib/AntiBan/retryTracker.js +172 -0
  37. package/lib/AntiBan/scheduler.js +111 -0
  38. package/lib/AntiBan/sessionFingerprint.js +248 -0
  39. package/lib/AntiBan/sessionStability.js +330 -0
  40. package/lib/AntiBan/stateAdapter.js +73 -0
  41. package/lib/AntiBan/stateExport.js +152 -0
  42. package/lib/AntiBan/stealthConnect.js +130 -0
  43. package/lib/AntiBan/timelockGuard.js +181 -0
  44. package/lib/AntiBan/topologyThrottler.js +339 -0
  45. package/lib/AntiBan/warmup.js +140 -0
  46. package/lib/AntiBan/webhooks.js +80 -0
  47. package/lib/AntiBan/wrapper.js +456 -0
  48. package/lib/Socket/groups.js +26 -21
  49. package/lib/Socket/index.js +132 -7
  50. package/lib/Socket/messages-send.js +68 -5
  51. package/lib/Utils/ai-rich-formatter.js +256 -0
  52. package/lib/Utils/index.js +1 -0
  53. package/lib/index.js +1 -1
  54. package/package.json +1 -1
@@ -0,0 +1,306 @@
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
+ }
@@ -0,0 +1,261 @@
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
+ }
@@ -0,0 +1,88 @@
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
+ }