/** * 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 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} - 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;