Deploy: 2026-07-30 11:05:21

This commit is contained in:
Hamza-Ayed
2026-07-30 11:05:21 +03:00
parent e6417f2883
commit f38d01a0e0
3 changed files with 85 additions and 54 deletions
+66 -48
View File
@@ -18,13 +18,17 @@ class ThrottleManager {
this.stats = new Map(); // sessionKey -> { sentLastMinute, sentLastHour, sentToday, warnings }
this.slowdownLevel = new Map(); // sessionKey -> 0 (normal), 1 (caution), 2 (danger)
// Per-session randomized limits, computed once so they don't flap between checks
this.sessionLimits = new Map(); // sessionKey -> { perMinute, perHour, perDay }
// Default limits (randomized per session to avoid fingerprinting)
this.limits = {
perMinute: { min: 6, max: 10 },
perHour: { min: 80, max: 120 },
perDay: { min: 400, max: 600 },
burstCooldownAfter: 5, // After 5 quick messages, take a break
burstCooldownMs: { min: 30000, max: 90000 } // 30-90 second break
burstCooldownAfter: 10, // After 10 back-to-back messages, take a break
burstCooldownMs: { min: 8000, max: 20000 }, // 8-20 second break
burstGapMs: 3000 // Sends more than 3s apart don't count as a burst
};
// Cleanup old stats every minute
@@ -91,11 +95,14 @@ class ThrottleManager {
* Average human types ~40 words/minute = ~200 chars/minute
* We simulate faster (AI is "quick") but still believable: ~400 chars/min
*/
_calculateTypingDelay(messageLength) {
if (!messageLength) return this._randomBetween(800, 1500);
// Base: 150ms per character, capped between 1-5 seconds
const baseDelay = Math.min(messageLength * 40, 5000);
return Math.max(800, baseDelay + this._randomBetween(-300, 500));
_calculateTypingDelay(messageLength, messageType) {
// Transactional sends (OTP codes, order confirmations, explicit API sends) are
// expected to be instant by the user — no typing simulation at all.
if (messageType === 'transactional') return 0;
if (!messageLength) return this._randomBetween(300, 700);
// ~15ms per character, capped at 2s — still reads as human without stalling the API.
const baseDelay = Math.min(messageLength * 15, 2000);
return Math.max(400, baseDelay + this._randomBetween(-150, 250));
}
/**
@@ -103,16 +110,18 @@ class ThrottleManager {
*/
_calculateReadDelay(messageType) {
switch (messageType) {
case 'transactional':
return 0; // OTP / codes / explicit API sends — send now
case 'auto_reply':
return this._randomBetween(1500, 4000); // 1.5-4s for AI replies
return this._randomBetween(400, 1200); // AI replies
case 'voice_reply':
return this._randomBetween(3000, 7000); // 3-7s for voice (longer processing feel)
return this._randomBetween(800, 2000); // voice notes
case 'broadcast':
return this._randomBetween(15000, 45000); // 15-45s between broadcast messages
case 'reminder':
return this._randomBetween(3000, 8000); // 3-8s for reminders
return this._randomBetween(1000, 2500); // reminders
default:
return this._randomBetween(1500, 3500);
return this._randomBetween(400, 1200);
}
}
@@ -126,9 +135,21 @@ class ThrottleManager {
// Apply multiplier based on slowdown level
const multiplier = slowdown === 0 ? 1 : (slowdown === 1 ? 0.5 : 0.2);
const perMinLimit = Math.floor(this._randomBetween(this.limits.perMinute.min, this.limits.perMinute.max) * multiplier);
const perHourLimit = Math.floor(this._randomBetween(this.limits.perHour.min, this.limits.perHour.max) * multiplier);
const perDayLimit = Math.floor(this._randomBetween(this.limits.perDay.min, this.limits.perDay.max) * multiplier);
// Limits are randomized ONCE per session. Re-rolling them on every check made the
// effective ceiling jump around (a low roll would block a send that the previous
// check had allowed), producing unpredictable multi-second stalls.
if (!this.sessionLimits.has(sessionKey)) {
this.sessionLimits.set(sessionKey, {
perMinute: this._randomBetween(this.limits.perMinute.min, this.limits.perMinute.max),
perHour: this._randomBetween(this.limits.perHour.min, this.limits.perHour.max),
perDay: this._randomBetween(this.limits.perDay.min, this.limits.perDay.max)
});
}
const base = this.sessionLimits.get(sessionKey);
const perMinLimit = Math.max(1, Math.floor(base.perMinute * multiplier));
const perHourLimit = Math.max(1, Math.floor(base.perHour * multiplier));
const perDayLimit = Math.max(1, Math.floor(base.perDay * multiplier));
const sentLastMinute = this._countInWindow(stats.sentTimestamps, 60000);
const sentLastHour = this._countInWindow(stats.sentTimestamps, 3600000);
@@ -197,9 +218,15 @@ class ThrottleManager {
continue;
}
const messageType = item.type || 'auto_reply';
// Transactional traffic (OTP codes, confirmations) skips the human-behaviour
// simulation entirely: no read pause, no typing indicator, no burst cooldown.
// It is still counted against the rate limits above.
const simulateHuman = messageType !== 'transactional';
// Check burst cooldown
const stats = this._getStats(sessionKey);
if (stats.burstCount >= this.limits.burstCooldownAfter) {
if (simulateHuman && stats.burstCount >= this.limits.burstCooldownAfter) {
const cooldown = this._randomBetween(
this.limits.burstCooldownMs.min,
this.limits.burstCooldownMs.max
@@ -210,55 +237,46 @@ class ThrottleManager {
}
// Calculate pre-send delays
const messageType = item.type || 'auto_reply';
const readDelay = this._calculateReadDelay(messageType);
// 1. Send "read" receipt if applicable
if (item.sock && item.incomingMsgKey) {
try {
await item.sock.readMessages([item.incomingMsgKey]);
} catch (e) {
// Non-critical, continue
}
// 1. Send "read" receipt if applicable. Fire-and-forget — awaiting this added a
// full network roundtrip to every send for a receipt nobody blocks on.
if (simulateHuman && item.sock && item.incomingMsgKey) {
item.sock.readMessages([item.incomingMsgKey]).catch(() => { });
}
// 2. Wait (simulating reading the message)
await this._sleep(readDelay);
if (readDelay > 0) await this._sleep(readDelay);
// 3. Send typing indicator
if (item.sock && item.jid) {
try {
await item.sock.sendPresenceUpdate('composing', item.jid);
} catch (e) {
// Non-critical
}
// 3. Send typing indicator (also fire-and-forget)
if (simulateHuman && item.sock && item.jid) {
item.sock.sendPresenceUpdate('composing', item.jid).catch(() => { });
}
// 4. Wait for typing delay
const textLength = item.content?.text?.length || item.content?.caption?.length || 20;
const typingDelay = this._calculateTypingDelay(textLength);
await this._sleep(typingDelay);
const typingDelay = this._calculateTypingDelay(textLength, messageType);
if (typingDelay > 0) await this._sleep(typingDelay);
// 5. Stop typing indicator
if (item.sock && item.jid) {
try {
await item.sock.sendPresenceUpdate('paused', item.jid);
} catch (e) {
// Non-critical
}
// 5. Stop typing indicator (fire-and-forget)
if (simulateHuman && item.sock && item.jid) {
item.sock.sendPresenceUpdate('paused', item.jid).catch(() => { });
}
// 6. Actually send the message
try {
const result = await item.sendFn();
stats.sentTimestamps.push(Date.now());
stats.lastSendAt = Date.now();
stats.burstCount++;
const now = Date.now();
// Reset burst if last send was more than 10 seconds ago
if (Date.now() - stats.lastSendAt > 10000) {
stats.burstCount = 1;
}
// Reset the burst counter when the previous send was a while ago — this must be
// evaluated against the OLD lastSendAt. Comparing after overwriting lastSendAt
// made the condition always false, so burstCount never reset and the 30-90s
// burst cooldown fired on every 5th message.
const idleSincePreviousSend = stats.lastSendAt ? now - stats.lastSendAt : Infinity;
stats.burstCount = idleSincePreviousSend > this.limits.burstGapMs ? 1 : stats.burstCount + 1;
stats.sentTimestamps.push(now);
stats.lastSendAt = now;
queue.shift(); // Remove processed item
item.resolve(result);
@@ -276,7 +294,7 @@ class ThrottleManager {
// Small random gap between processing queue items
if (queue.length > 0) {
await this._sleep(this._randomBetween(500, 2000));
await this._sleep(simulateHuman ? this._randomBetween(300, 800) : this._randomBetween(80, 200));
}
}