Files
nabeh/whatsapp-gateway/throttle-manager.js
T

382 lines
14 KiB
JavaScript

/**
* ThrottleManager — Smart Anti-Ban Message Throttling Engine for Nabeh WhatsApp Gateway
*
* Simulates human-like messaging behavior to prevent WhatsApp bans:
* - Per-session message queues with intelligent delays
* - Typing indicators before sending
* - Random jitter to avoid pattern detection
* - Rate limiting per minute/hour/day
* - Automatic slowdown on warnings
* - Emergency pause on critical risk
*/
class ThrottleManager {
constructor() {
// Per-session message queues
this.queues = new Map(); // sessionKey -> Array<QueueItem>
this.processing = new Map(); // sessionKey -> boolean (is currently processing)
this.stats = new Map(); // sessionKey -> { sentLastMinute, sentLastHour, sentToday, warnings }
this.slowdownLevel = new Map(); // sessionKey -> 0 (normal), 1 (caution), 2 (danger)
// 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
};
// Cleanup old stats every minute
this._cleanupInterval = setInterval(() => this._cleanupStats(), 60000);
}
/**
* Get or initialize stats for a session
*/
_getStats(sessionKey) {
if (!this.stats.has(sessionKey)) {
this.stats.set(sessionKey, {
sentTimestamps: [], // Array of timestamps for rate tracking
warnings: 0,
lastWarningAt: 0,
burstCount: 0, // Messages sent in quick succession
lastSendAt: 0,
dailyReset: this._todayKey()
});
}
const stats = this.stats.get(sessionKey);
// Reset daily counter at midnight
if (stats.dailyReset !== this._todayKey()) {
stats.sentTimestamps = stats.sentTimestamps.filter(
ts => Date.now() - ts < 3600000
);
stats.dailyReset = this._todayKey();
stats.warnings = Math.max(0, stats.warnings - 1); // Decay warnings daily
}
return stats;
}
_todayKey() {
return new Date().toISOString().slice(0, 10);
}
/**
* Calculate messages sent within a time window
*/
_countInWindow(timestamps, windowMs) {
const cutoff = Date.now() - windowMs;
return timestamps.filter(ts => ts > cutoff).length;
}
/**
* Clean up old timestamps from stats to prevent memory bloat
*/
_cleanupStats() {
const oneDayAgo = Date.now() - 86400000;
for (const [key, stats] of this.stats) {
stats.sentTimestamps = stats.sentTimestamps.filter(ts => ts > oneDayAgo);
}
}
/**
* Random integer between min and max (inclusive)
*/
_randomBetween(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* Calculate human-like typing delay based on message length
* 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));
}
/**
* Calculate delay before reading/responding to simulate human behavior
*/
_calculateReadDelay(messageType) {
switch (messageType) {
case 'auto_reply':
return this._randomBetween(1500, 4000); // 1.5-4s for AI replies
case 'voice_reply':
return this._randomBetween(3000, 7000); // 3-7s for voice (longer processing feel)
case 'broadcast':
return this._randomBetween(15000, 45000); // 15-45s between broadcast messages
case 'reminder':
return this._randomBetween(3000, 8000); // 3-8s for reminders
default:
return this._randomBetween(1500, 3500);
}
}
/**
* Check if we're within rate limits for this session
*/
_isWithinLimits(sessionKey) {
const stats = this._getStats(sessionKey);
const slowdown = this.slowdownLevel.get(sessionKey) || 0;
// 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);
const sentLastMinute = this._countInWindow(stats.sentTimestamps, 60000);
const sentLastHour = this._countInWindow(stats.sentTimestamps, 3600000);
const sentToday = this._countInWindow(stats.sentTimestamps, 86400000);
if (sentLastMinute >= perMinLimit) {
console.log(`[Throttle] ${sessionKey} — Rate limit: ${sentLastMinute}/${perMinLimit} per minute. Waiting...`);
return false;
}
if (sentLastHour >= perHourLimit) {
console.log(`[Throttle] ${sessionKey} — Rate limit: ${sentLastHour}/${perHourLimit} per hour. Waiting...`);
return false;
}
if (sentToday >= perDayLimit) {
console.log(`[Throttle] ${sessionKey} — Rate limit: ${sentToday}/${perDayLimit} per day. PAUSED.`);
return false;
}
return true;
}
/**
* Enqueue a message for throttled sending
* @param {string} sessionKey - Session identifier
* @param {object} messageData - { jid, content, type, sendFn }
* @returns {Promise<object>} - Resolves when the message is actually sent
*/
enqueue(sessionKey, messageData) {
return new Promise((resolve, reject) => {
if (!this.queues.has(sessionKey)) {
this.queues.set(sessionKey, []);
}
const queueItem = {
...messageData,
enqueuedAt: Date.now(),
resolve,
reject
};
this.queues.get(sessionKey).push(queueItem);
console.log(`[Throttle] ${sessionKey} — Message queued. Queue size: ${this.queues.get(sessionKey).length}`);
// Start processing if not already running
if (!this.processing.get(sessionKey)) {
this._processQueue(sessionKey);
}
});
}
/**
* Process the message queue for a session sequentially
*/
async _processQueue(sessionKey) {
if (this.processing.get(sessionKey)) return;
this.processing.set(sessionKey, true);
const queue = this.queues.get(sessionKey);
while (queue && queue.length > 0) {
const item = queue[0];
// Check rate limits
if (!this._isWithinLimits(sessionKey)) {
// Wait and retry
await this._sleep(this._randomBetween(5000, 15000));
continue;
}
// Check burst cooldown
const stats = this._getStats(sessionKey);
if (stats.burstCount >= this.limits.burstCooldownAfter) {
const cooldown = this._randomBetween(
this.limits.burstCooldownMs.min,
this.limits.burstCooldownMs.max
);
console.log(`[Throttle] ${sessionKey} — Burst cooldown: ${Math.round(cooldown / 1000)}s pause after ${stats.burstCount} quick messages.`);
stats.burstCount = 0;
await this._sleep(cooldown);
}
// 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
}
}
// 2. Wait (simulating reading the message)
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
}
}
// 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);
// 5. Stop typing indicator
if (item.sock && item.jid) {
try {
await item.sock.sendPresenceUpdate('paused', item.jid);
} catch (e) {
// Non-critical
}
}
// 6. Actually send the message
try {
const result = await item.sendFn();
stats.sentTimestamps.push(Date.now());
stats.lastSendAt = Date.now();
stats.burstCount++;
// Reset burst if last send was more than 10 seconds ago
if (Date.now() - stats.lastSendAt > 10000) {
stats.burstCount = 1;
}
queue.shift(); // Remove processed item
item.resolve(result);
console.log(`[Throttle] ${sessionKey} — Message sent successfully. Queue remaining: ${queue.length}`);
} catch (err) {
queue.shift();
item.reject(err);
console.error(`[Throttle] ${sessionKey} — Send failed:`, err.message);
// Check if it's a rate-limit related error
if (err.message?.includes('rate') || err.message?.includes('429') || err.message?.includes('too many')) {
this.onWarning(sessionKey);
}
}
// Small random gap between processing queue items
if (queue.length > 0) {
await this._sleep(this._randomBetween(500, 2000));
}
}
this.processing.set(sessionKey, false);
}
/**
* Called when a warning signal is detected (rate limit, 429, etc.)
*/
onWarning(sessionKey) {
const stats = this._getStats(sessionKey);
stats.warnings++;
stats.lastWarningAt = Date.now();
const currentLevel = this.slowdownLevel.get(sessionKey) || 0;
if (stats.warnings >= 5) {
// EMERGENCY: Pause all sending for this session
this.slowdownLevel.set(sessionKey, 2);
console.warn(`[Throttle] ⛔ ${sessionKey} — EMERGENCY SLOWDOWN! ${stats.warnings} warnings detected. All sending severely throttled.`);
} else if (stats.warnings >= 2) {
this.slowdownLevel.set(sessionKey, 1);
console.warn(`[Throttle] ⚠️ ${sessionKey} — CAUTION slowdown. ${stats.warnings} warnings. Reducing send rate by 50%.`);
}
// Auto-recover after 30 minutes of no warnings
setTimeout(() => {
const current = this._getStats(sessionKey);
if (Date.now() - current.lastWarningAt > 1800000) {
const level = this.slowdownLevel.get(sessionKey) || 0;
if (level > 0) {
this.slowdownLevel.set(sessionKey, Math.max(0, level - 1));
console.log(`[Throttle] ✅ ${sessionKey} — Slowdown level decreased to ${level - 1} after 30min recovery.`);
}
}
}, 1800000);
}
/**
* Get queue status for monitoring
*/
getStatus(sessionKey) {
const stats = this._getStats(sessionKey);
const queue = this.queues.get(sessionKey) || [];
return {
queueSize: queue.length,
isProcessing: this.processing.get(sessionKey) || false,
slowdownLevel: this.slowdownLevel.get(sessionKey) || 0,
sentLastMinute: this._countInWindow(stats.sentTimestamps, 60000),
sentLastHour: this._countInWindow(stats.sentTimestamps, 3600000),
sentToday: this._countInWindow(stats.sentTimestamps, 86400000),
warnings: stats.warnings
};
}
/**
* Get status for all sessions
*/
getAllStatus() {
const result = {};
for (const sessionKey of this.stats.keys()) {
result[sessionKey] = this.getStatus(sessionKey);
}
return result;
}
/**
* Clear queue for a session (e.g., when disconnecting)
*/
clearQueue(sessionKey) {
const queue = this.queues.get(sessionKey) || [];
for (const item of queue) {
item.reject(new Error('Queue cleared — session disconnecting'));
}
this.queues.delete(sessionKey);
this.processing.delete(sessionKey);
console.log(`[Throttle] ${sessionKey} — Queue cleared.`);
}
/**
* Promise-based sleep
*/
_sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Cleanup on shutdown
*/
destroy() {
if (this._cleanupInterval) {
clearInterval(this._cleanupInterval);
}
for (const sessionKey of this.queues.keys()) {
this.clearQueue(sessionKey);
}
}
}
// Export singleton instance
const throttleManager = new ThrottleManager();
module.exports = throttleManager;