|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const fs = require('fs'); |
|
|
const path = require('path'); |
|
|
|
|
|
class RateLimiter { |
|
|
constructor(config = {}) { |
|
|
|
|
|
this.HOURLY_LIMIT = config.hourlyLimit || 100; |
|
|
this.HOURLY_WINDOW = config.hourlyWindow || (60 * 60 * 1000); |
|
|
this.BLOCK_DURATION = config.blockDuration || (60 * 60 * 1000); |
|
|
this.MAX_ATTEMPTS_BLACKLIST = config.maxAttemptsBlacklist || 3; |
|
|
|
|
|
|
|
|
this.userLimits = new Map(); |
|
|
this.logBuffer = []; |
|
|
this.maxLogBufferSize = 1000; |
|
|
|
|
|
|
|
|
this.dbPath = config.dbPath || './database/datauser'; |
|
|
this.blacklistPath = path.join(this.dbPath, 'blacklist.json'); |
|
|
this.logsPath = path.join(this.dbPath, 'rate_limit_logs'); |
|
|
|
|
|
|
|
|
this._initDirectories(); |
|
|
|
|
|
|
|
|
this.colors = { |
|
|
reset: '\x1b[0m', |
|
|
bright: '\x1b[1m', |
|
|
red: '\x1b[31m', |
|
|
green: '\x1b[32m', |
|
|
yellow: '\x1b[33m', |
|
|
blue: '\x1b[34m', |
|
|
magenta: '\x1b[35m', |
|
|
cyan: '\x1b[36m' |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_initDirectories() { |
|
|
try { |
|
|
if (!fs.existsSync(this.dbPath)) { |
|
|
fs.mkdirSync(this.dbPath, { recursive: true }); |
|
|
} |
|
|
if (!fs.existsSync(this.logsPath)) { |
|
|
fs.mkdirSync(this.logsPath, { recursive: true }); |
|
|
} |
|
|
} catch (e) { |
|
|
console.error('Erro ao criar diretΓ³rios:', e); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
checkLimit(userId, userName, userNumber, messageText, quotedMessage = null, ehDono = false) { |
|
|
|
|
|
if (ehDono) { |
|
|
this._log('PERMITIDO', userId, userName, userNumber, messageText, quotedMessage, 'DONO_ISENTO', 'Nenhuma limitaΓ§Γ£o'); |
|
|
return { allowed: true, reason: 'OWNER_EXEMPT' }; |
|
|
} |
|
|
|
|
|
|
|
|
if (this.isBlacklisted(userId)) { |
|
|
this._log('BLOQUEADO', userId, userName, userNumber, messageText, quotedMessage, 'BLACKLIST', 'UsuΓ‘rio estΓ‘ em blacklist permanente'); |
|
|
return { allowed: false, reason: 'BLACKLIST', severity: 'CRΓTICO' }; |
|
|
} |
|
|
|
|
|
const now = Date.now(); |
|
|
let userData = this.userLimits.get(userId); |
|
|
|
|
|
|
|
|
if (!userData) { |
|
|
userData = { |
|
|
windowStart: now, |
|
|
count: 0, |
|
|
blockedUntil: 0, |
|
|
overAttempts: 0, |
|
|
warnings: 0, |
|
|
firstMessageTime: now |
|
|
}; |
|
|
this.userLimits.set(userId, userData); |
|
|
} |
|
|
|
|
|
|
|
|
if (userData.blockedUntil && now < userData.blockedUntil) { |
|
|
userData.overAttempts++; |
|
|
|
|
|
const timePassedMs = now - userData.blockedUntil + this.BLOCK_DURATION; |
|
|
const timePassedSec = Math.floor(timePassedMs / 1000); |
|
|
const timeRemainingSec = Math.ceil((userData.blockedUntil - now) / 1000); |
|
|
const blockExpireTime = new Date(userData.blockedUntil).toLocaleTimeString('pt-BR', { |
|
|
hour: '2-digit', |
|
|
minute: '2-digit', |
|
|
second: '2-digit' |
|
|
}); |
|
|
|
|
|
this._log( |
|
|
'β οΈ BLOQUEADO REINCIDΓNCIA', |
|
|
userId, |
|
|
userName, |
|
|
userNumber, |
|
|
messageText, |
|
|
quotedMessage, |
|
|
`TENTATIVA ${userData.overAttempts}/${this.MAX_ATTEMPTS_BLACKLIST}`, |
|
|
`Passou: ${timePassedSec}s | Falta: ${timeRemainingSec}s | Desbloqueio: ${blockExpireTime}` |
|
|
); |
|
|
|
|
|
|
|
|
if (userData.overAttempts >= this.MAX_ATTEMPTS_BLACKLIST) { |
|
|
this.addToBlacklist(userId, userName, userNumber, 'SPAM_REINCIDΓNCIA'); |
|
|
|
|
|
this._log( |
|
|
'π¨ AUTO-BLACKLIST ACIONADO', |
|
|
userId, |
|
|
userName, |
|
|
userNumber, |
|
|
messageText, |
|
|
quotedMessage, |
|
|
`MΓLTIPLAS REINCIDΓNCIAS (${userData.overAttempts})`, |
|
|
'ADICIONADO Γ BLACKLIST PERMANENTE' |
|
|
); |
|
|
|
|
|
return { |
|
|
allowed: false, |
|
|
reason: 'AUTO_BLACKLIST_TRIGGERED', |
|
|
overAttempts: userData.overAttempts, |
|
|
severity: 'CRΓTICO' |
|
|
}; |
|
|
} |
|
|
|
|
|
this.userLimits.set(userId, userData); |
|
|
return { |
|
|
allowed: false, |
|
|
reason: 'BLOCKED_TEMPORARY', |
|
|
timePassedSec, |
|
|
timeRemainingSec, |
|
|
blockExpireTime, |
|
|
overAttempts: userData.overAttempts, |
|
|
severity: 'ALTO' |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
if (now - userData.windowStart >= this.HOURLY_WINDOW) { |
|
|
userData.windowStart = now; |
|
|
userData.count = 0; |
|
|
userData.blockedUntil = 0; |
|
|
userData.overAttempts = 0; |
|
|
userData.warnings = 0; |
|
|
} |
|
|
|
|
|
|
|
|
userData.count++; |
|
|
|
|
|
|
|
|
if (userData.count > this.HOURLY_LIMIT) { |
|
|
userData.blockedUntil = now + this.BLOCK_DURATION; |
|
|
userData.warnings++; |
|
|
|
|
|
const blockExpireTime = new Date(userData.blockedUntil).toLocaleTimeString('pt-BR', { |
|
|
hour: '2-digit', |
|
|
minute: '2-digit', |
|
|
second: '2-digit' |
|
|
}); |
|
|
|
|
|
this._log( |
|
|
'π« LIMITE EXCEDIDO', |
|
|
userId, |
|
|
userName, |
|
|
userNumber, |
|
|
messageText, |
|
|
quotedMessage, |
|
|
`MENSAGENS: ${userData.count}/${this.HOURLY_LIMIT}`, |
|
|
`Bloqueado atΓ© ${blockExpireTime} (1 hora)` |
|
|
); |
|
|
|
|
|
this.userLimits.set(userId, userData); |
|
|
return { |
|
|
allowed: false, |
|
|
reason: 'LIMIT_EXCEEDED', |
|
|
messagesCount: userData.count, |
|
|
limit: this.HOURLY_LIMIT, |
|
|
blockExpireTime, |
|
|
severity: 'ALTO' |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
const percentualUso = (userData.count / this.HOURLY_LIMIT) * 100; |
|
|
if (percentualUso >= 90 && userData.count > 0) { |
|
|
const remaining = this.HOURLY_LIMIT - userData.count; |
|
|
this._log( |
|
|
'β‘ AVISO: PROXIMIDADE CRΓTICA DO LIMITE', |
|
|
userId, |
|
|
userName, |
|
|
userNumber, |
|
|
messageText, |
|
|
quotedMessage, |
|
|
`${userData.count}/${this.HOURLY_LIMIT} (${percentualUso.toFixed(1)}%)`, |
|
|
`β οΈ Apenas ${remaining} mensagens restantes` |
|
|
); |
|
|
} else if (percentualUso >= 75) { |
|
|
this._log( |
|
|
'β‘ AVISO: PROXIMIDADE DO LIMITE', |
|
|
userId, |
|
|
userName, |
|
|
userNumber, |
|
|
messageText, |
|
|
quotedMessage, |
|
|
`${userData.count}/${this.HOURLY_LIMIT} (${percentualUso.toFixed(1)}%)`, |
|
|
`Faltam ${this.HOURLY_LIMIT - userData.count} mensagens` |
|
|
); |
|
|
} |
|
|
|
|
|
this.userLimits.set(userId, userData); |
|
|
|
|
|
return { |
|
|
allowed: true, |
|
|
reason: 'OK', |
|
|
messagesCount: userData.count, |
|
|
limit: this.HOURLY_LIMIT, |
|
|
percentualUso: percentualUso.toFixed(1) |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_log(status, userId, userName, userNumber, messageText, quotedMessage, details, action) { |
|
|
const timestamp = new Date(); |
|
|
const timestampFormatted = timestamp.toLocaleString('pt-BR', { |
|
|
year: 'numeric', |
|
|
month: '2-digit', |
|
|
day: '2-digit', |
|
|
hour: '2-digit', |
|
|
minute: '2-digit', |
|
|
second: '2-digit', |
|
|
hour12: false |
|
|
}); |
|
|
|
|
|
|
|
|
const logHash = `${userId}|${status}|${details}`; |
|
|
const lastLogIndex = this.logBuffer.findIndex(l => l.hash === logHash && (timestamp - l.timestamp) < 5000); |
|
|
|
|
|
if (lastLogIndex !== -1) { |
|
|
|
|
|
this.logBuffer[lastLogIndex].count++; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
this.logBuffer.push({ |
|
|
hash: logHash, |
|
|
timestamp, |
|
|
count: 1 |
|
|
}); |
|
|
|
|
|
|
|
|
if (this.logBuffer.length > this.maxLogBufferSize) { |
|
|
this.logBuffer.shift(); |
|
|
} |
|
|
|
|
|
|
|
|
const separator = 'β'.repeat(120); |
|
|
const border = 'β'.repeat(120); |
|
|
|
|
|
let statusColor = this.colors.cyan; |
|
|
if (status.includes('BLOQUEADO')) statusColor = this.colors.red; |
|
|
else if (status.includes('AUTO-BLACKLIST')) statusColor = this.colors.red + this.colors.bright; |
|
|
else if (status.includes('LIMITE')) statusColor = this.colors.yellow; |
|
|
else if (status.includes('AVISO')) statusColor = this.colors.yellow; |
|
|
else if (status.includes('PERMITIDO')) statusColor = this.colors.green; |
|
|
|
|
|
|
|
|
console.log(`\n${this.colors.cyan}${separator}${this.colors.reset}`); |
|
|
console.log(`${statusColor}π [${timestampFormatted}] ${status}${this.colors.reset}`); |
|
|
console.log(`${this.colors.cyan}${border}${this.colors.reset}`); |
|
|
|
|
|
console.log(`${this.colors.bright}π€ USUΓRIO${this.colors.reset}`); |
|
|
console.log(` ${this.colors.cyan}ββ${this.colors.reset} Nome: ${this.colors.bright}${userName}${this.colors.reset}`); |
|
|
console.log(` ${this.colors.cyan}ββ${this.colors.reset} NΓΊmero: ${this.colors.bright}${userNumber}${this.colors.reset}`); |
|
|
console.log(` ${this.colors.cyan}ββ${this.colors.reset} JID: ${this.colors.bright}${userId}${this.colors.reset}`); |
|
|
|
|
|
console.log(`${this.colors.bright}π¬ MENSAGEM${this.colors.reset}`); |
|
|
const msgPreview = messageText.substring(0, 100) + (messageText.length > 100 ? '...' : ''); |
|
|
console.log(` ${this.colors.cyan}ββ${this.colors.reset} Texto: "${this.colors.magenta}${msgPreview}${this.colors.reset}"`); |
|
|
console.log(` ${this.colors.cyan}ββ${this.colors.reset} Comprimento: ${this.colors.bright}${messageText.length}${this.colors.reset} caracteres`); |
|
|
|
|
|
if (quotedMessage && quotedMessage.trim()) { |
|
|
const quotedPreview = quotedMessage.substring(0, 80) + (quotedMessage.length > 80 ? '...' : ''); |
|
|
console.log(` ${this.colors.cyan}ββ${this.colors.reset} Citada: "${this.colors.blue}${quotedPreview}${this.colors.reset}"`); |
|
|
} |
|
|
|
|
|
console.log(` ${this.colors.cyan}ββ${this.colors.reset} Tipo: ${this.colors.bright}${messageText.startsWith('#') ? 'COMANDO' : 'MENSAGEM'}${this.colors.reset}`); |
|
|
|
|
|
console.log(`${this.colors.bright}π DETALHES${this.colors.reset}`); |
|
|
console.log(` ${this.colors.cyan}ββ${this.colors.reset} ${this.colors.yellow}${details}${this.colors.reset}`); |
|
|
|
|
|
if (action) { |
|
|
console.log(`${this.colors.bright}β‘ AΓΓO${this.colors.reset}`); |
|
|
console.log(` ${this.colors.cyan}ββ${this.colors.reset} ${this.colors.bright}${action}${this.colors.reset}`); |
|
|
} |
|
|
|
|
|
console.log(`${this.colors.cyan}${separator}${this.colors.reset}`); |
|
|
|
|
|
|
|
|
this._saveLogToFile(timestampFormatted, status, userId, userName, userNumber, messageText, quotedMessage, details, action); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_saveLogToFile(timestamp, status, userId, userName, userNumber, messageText, quotedMessage, details, action) { |
|
|
try { |
|
|
const date = new Date(); |
|
|
const dateStr = date.toISOString().split('T')[0]; |
|
|
const logFile = path.join(this.logsPath, `rate_limit_${dateStr}.log`); |
|
|
|
|
|
const logEntry = { |
|
|
timestamp, |
|
|
status, |
|
|
userId, |
|
|
userName, |
|
|
userNumber, |
|
|
messagePreview: messageText.substring(0, 150), |
|
|
quotedPreview: quotedMessage ? quotedMessage.substring(0, 100) : null, |
|
|
details, |
|
|
action |
|
|
}; |
|
|
|
|
|
const logLine = JSON.stringify(logEntry) + '\n'; |
|
|
|
|
|
fs.appendFileSync(logFile, logLine, 'utf8'); |
|
|
} catch (e) { |
|
|
console.error('Erro ao salvar log:', e); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
isBlacklisted(userId) { |
|
|
const list = this.loadBlacklist(); |
|
|
if (!Array.isArray(list)) return false; |
|
|
|
|
|
const found = list.find(entry => entry && entry.id === userId); |
|
|
|
|
|
if (found) { |
|
|
|
|
|
if (found.expiresAt && found.expiresAt !== 'PERMANENT') { |
|
|
if (Date.now() > found.expiresAt) { |
|
|
this.removeFromBlacklist(userId); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
return true; |
|
|
} |
|
|
|
|
|
return false; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
addToBlacklist(userId, userName, userNumber, reason = 'spam', expiryMs = null) { |
|
|
const list = this.loadBlacklist(); |
|
|
const arr = Array.isArray(list) ? list : []; |
|
|
|
|
|
|
|
|
if (arr.find(x => x && x.id === userId)) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
let expiresAt = 'PERMANENT'; |
|
|
if (expiryMs) { |
|
|
expiresAt = Date.now() + expiryMs; |
|
|
} |
|
|
|
|
|
const entry = { |
|
|
id: userId, |
|
|
name: userName, |
|
|
number: userNumber, |
|
|
reason, |
|
|
addedAt: Date.now(), |
|
|
expiresAt, |
|
|
severity: reason === 'SPAM_REINCIDΓNCIA' ? 'π¨ CRΓTICO' : 'ALTO' |
|
|
}; |
|
|
|
|
|
arr.push(entry); |
|
|
|
|
|
try { |
|
|
fs.writeFileSync(this.blacklistPath, JSON.stringify(arr, null, 2), 'utf8'); |
|
|
|
|
|
const timestamp = new Date().toLocaleString('pt-BR'); |
|
|
console.log(`\n${'β'.repeat(120)}`); |
|
|
console.log(`${this.colors.red}${this.colors.bright}π« [${timestamp}] BLACKLIST ADICIONADO - SEVERIDADE: ${entry.severity}${this.colors.reset}`); |
|
|
console.log(`${'β'.repeat(120)}`); |
|
|
console.log(`${this.colors.bright}π€ USUΓRIO${this.colors.reset}`); |
|
|
console.log(` ββ Nome: ${userName}`); |
|
|
console.log(` ββ NΓΊmero: ${userNumber}`); |
|
|
console.log(` ββ JID: ${userId}`); |
|
|
console.log(`π RAZΓO: ${reason}`); |
|
|
console.log(`β° EXPIRAΓΓO: ${expiresAt === 'PERMANENT' ? 'PERMANENTE' : new Date(expiresAt).toLocaleString('pt-BR')}`); |
|
|
console.log(`π STATUS: Todas as mensagens e comandos serΓ£o ignorados`); |
|
|
console.log(`${'β'.repeat(120)}\n`); |
|
|
|
|
|
return true; |
|
|
} catch (e) { |
|
|
console.error('Erro ao adicionar Γ blacklist:', e); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
removeFromBlacklist(userId) { |
|
|
const list = this.loadBlacklist(); |
|
|
const arr = Array.isArray(list) ? list : []; |
|
|
const index = arr.findIndex(x => x && x.id === userId); |
|
|
|
|
|
if (index !== -1) { |
|
|
const removed = arr[index]; |
|
|
arr.splice(index, 1); |
|
|
|
|
|
try { |
|
|
fs.writeFileSync(this.blacklistPath, JSON.stringify(arr, null, 2), 'utf8'); |
|
|
console.log(`β
[BLACKLIST] ${removed.name} (${removed.number}) removido da blacklist`); |
|
|
return true; |
|
|
} catch (e) { |
|
|
console.error('Erro ao remover da blacklist:', e); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
return false; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
loadBlacklist() { |
|
|
try { |
|
|
if (!fs.existsSync(this.blacklistPath)) { |
|
|
return []; |
|
|
} |
|
|
|
|
|
const data = fs.readFileSync(this.blacklistPath, 'utf8'); |
|
|
if (!data || !data.trim()) { |
|
|
return []; |
|
|
} |
|
|
|
|
|
return JSON.parse(data); |
|
|
} catch (e) { |
|
|
console.error('Erro ao carregar blacklist:', e); |
|
|
return []; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getBlacklistReport() { |
|
|
const list = this.loadBlacklist(); |
|
|
if (!Array.isArray(list) || list.length === 0) { |
|
|
return { total: 0, entries: [] }; |
|
|
} |
|
|
|
|
|
return { |
|
|
total: list.length, |
|
|
entries: list.map(entry => ({ |
|
|
name: entry.name || 'Desconhecido', |
|
|
number: entry.number || 'N/A', |
|
|
reason: entry.reason || 'indefinida', |
|
|
severity: entry.severity || 'NORMAL', |
|
|
addedAt: new Date(entry.addedAt).toLocaleString('pt-BR'), |
|
|
expiresAt: entry.expiresAt === 'PERMANENT' ? 'PERMANENTE' : new Date(entry.expiresAt).toLocaleString('pt-BR') |
|
|
})) |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getStatusUser(userId) { |
|
|
const userData = this.userLimits.get(userId); |
|
|
const isBlacklisted = this.isBlacklisted(userId); |
|
|
|
|
|
if (isBlacklisted) { |
|
|
return { blocked: true, reason: 'BLACKLIST' }; |
|
|
} |
|
|
|
|
|
if (!userData) { |
|
|
return { blocked: false, messagesCount: 0, limit: this.HOURLY_LIMIT }; |
|
|
} |
|
|
|
|
|
const now = Date.now(); |
|
|
const blocked = userData.blockedUntil && now < userData.blockedUntil; |
|
|
const timeRemaining = blocked ? Math.ceil((userData.blockedUntil - now) / 1000) : 0; |
|
|
|
|
|
return { |
|
|
blocked, |
|
|
messagesCount: userData.count, |
|
|
limit: this.HOURLY_LIMIT, |
|
|
overAttempts: userData.overAttempts, |
|
|
timeRemainingSec: timeRemaining |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getStats() { |
|
|
const activeUsers = Array.from(this.userLimits.entries()).filter(([_, data]) => data.blockedUntil > Date.now()); |
|
|
|
|
|
return { |
|
|
totalBlockedUsers: activeUsers.length, |
|
|
totalBlacklistedUsers: this.loadBlacklist().length, |
|
|
logBufferSize: this.logBuffer.length |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
reset() { |
|
|
this.userLimits.clear(); |
|
|
this.logBuffer = []; |
|
|
} |
|
|
} |
|
|
|
|
|
module.exports = RateLimiter; |
|
|
|