|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const ConfigManager = require('./ConfigManager'); |
|
|
|
|
|
class ModerationSystem { |
|
|
constructor(logger = null) { |
|
|
this.config = ConfigManager.getInstance(); |
|
|
this.logger = logger || console; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.blacklistPath = '/tmp/akira_data/datauser/blacklist.json'; |
|
|
|
|
|
|
|
|
this.mutedUsers = new Map(); |
|
|
this.antiLinkGroups = new Set(); |
|
|
this.muteCounts = new Map(); |
|
|
this.bannedUsers = new Map(); |
|
|
this.spamCache = new Map(); |
|
|
|
|
|
|
|
|
this.userRateLimit = new Map(); |
|
|
this.hourlyLimit = 100; |
|
|
this.hourlyWindow = 60 * 60 * 1000; |
|
|
this.blockDuration = 60 * 60 * 1000; |
|
|
this.maxAttemptsBeforeBlacklist = 3; |
|
|
|
|
|
|
|
|
this.HOURLY_LIMIT = 300; |
|
|
this.HOURLY_WINDOW_MS = 60 * 60 * 1000; |
|
|
this.SPAM_THRESHOLD = 3; |
|
|
this.SPAM_WINDOW_MS = 3000; |
|
|
|
|
|
|
|
|
this.enableDetailedLogging = true; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
checkAndLimitHourlyMessages(userId, userName, userNumber, messageText, quotedMessage = null, ehDono = false) { |
|
|
|
|
|
if (ehDono) { |
|
|
return { allowed: true, reason: 'DONO_ISENTO' }; |
|
|
} |
|
|
|
|
|
const now = Date.now(); |
|
|
let userData = this.userRateLimit.get(userId) || { |
|
|
windowStart: now, |
|
|
count: 0, |
|
|
blockedUntil: 0, |
|
|
overAttempts: 0, |
|
|
warnings: 0, |
|
|
blocked_at: null, |
|
|
blocked_by_warning: false |
|
|
}; |
|
|
|
|
|
|
|
|
if (userData.blockedUntil && now < userData.blockedUntil) { |
|
|
userData.overAttempts++; |
|
|
|
|
|
const timePassedMs = now - userData.blocked_at; |
|
|
const timePassedSec = Math.floor(timePassedMs / 1000); |
|
|
const timeRemainingMs = userData.blockedUntil - now; |
|
|
const timeRemainingSec = Math.ceil(timeRemainingMs / 1000); |
|
|
const blockExpireTime = new Date(userData.blockedUntil).toLocaleTimeString('pt-BR'); |
|
|
|
|
|
this._logRateLimitAttempt( |
|
|
'BLOQUEADO_REINCIDΓNCIA', |
|
|
userId, |
|
|
userName, |
|
|
userNumber, |
|
|
messageText, |
|
|
quotedMessage, |
|
|
`Tentativa ${userData.overAttempts}/${this.maxAttemptsBeforeBlacklist}`, |
|
|
`Passou: ${timePassedSec}s | Falta: ${timeRemainingSec}s | Desbloqueio: ${blockExpireTime}` |
|
|
); |
|
|
|
|
|
|
|
|
if (userData.overAttempts >= this.maxAttemptsBeforeBlacklist) { |
|
|
this._logRateLimitAttempt( |
|
|
'π¨ AUTO-BLACKLIST ACIONADO', |
|
|
userId, |
|
|
userName, |
|
|
userNumber, |
|
|
messageText, |
|
|
quotedMessage, |
|
|
`MΓLTIPLAS REINCIDΓNCIAS (${userData.overAttempts})`, |
|
|
'USUΓRIO ADICIONADO Γ BLACKLIST PERMANENTE' |
|
|
); |
|
|
|
|
|
this.userRateLimit.set(userId, userData); |
|
|
return { |
|
|
allowed: false, |
|
|
reason: 'AUTO_BLACKLIST_TRIGGERED', |
|
|
overAttempts: userData.overAttempts, |
|
|
action: 'ADD_TO_BLACKLIST' |
|
|
}; |
|
|
} |
|
|
|
|
|
this.userRateLimit.set(userId, userData); |
|
|
return { |
|
|
allowed: false, |
|
|
reason: 'BLOQUEADO_REINCIDΓNCIA', |
|
|
timePassedSec, |
|
|
timeRemainingSec, |
|
|
blockExpireTime, |
|
|
overAttempts: userData.overAttempts |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
if (now - userData.windowStart >= this.hourlyWindow) { |
|
|
userData.windowStart = now; |
|
|
userData.count = 0; |
|
|
userData.blockedUntil = 0; |
|
|
userData.overAttempts = 0; |
|
|
userData.warnings = 0; |
|
|
userData.blocked_at = null; |
|
|
userData.blocked_by_warning = false; |
|
|
} |
|
|
|
|
|
|
|
|
userData.count++; |
|
|
|
|
|
|
|
|
if (userData.count > this.hourlyLimit) { |
|
|
userData.blockedUntil = now + this.blockDuration; |
|
|
userData.blocked_at = now; |
|
|
userData.blocked_by_warning = true; |
|
|
userData.warnings++; |
|
|
|
|
|
this._logRateLimitAttempt( |
|
|
'β οΈ LIMITE EXCEDIDO', |
|
|
userId, |
|
|
userName, |
|
|
userNumber, |
|
|
messageText, |
|
|
quotedMessage, |
|
|
`Mensagens: ${userData.count}/${this.hourlyLimit}`, |
|
|
`Bloqueado por 1 hora` |
|
|
); |
|
|
|
|
|
this.userRateLimit.set(userId, userData); |
|
|
return { |
|
|
allowed: false, |
|
|
reason: 'LIMITE_HORARIO_EXCEDIDO', |
|
|
messagesCount: userData.count, |
|
|
limit: this.hourlyLimit, |
|
|
blockDurationMinutes: 60 |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
const percentualUso = (userData.count / this.hourlyLimit) * 100; |
|
|
if (percentualUso >= 80 && userData.count > 0) { |
|
|
this._logRateLimitAttempt( |
|
|
'β‘ AVISO: PROXIMIDADE DO LIMITE', |
|
|
userId, |
|
|
userName, |
|
|
userNumber, |
|
|
messageText, |
|
|
quotedMessage, |
|
|
`${userData.count}/${this.hourlyLimit} (${percentualUso.toFixed(1)}%)`, |
|
|
`Faltam ${this.hourlyLimit - userData.count} mensagens` |
|
|
); |
|
|
} |
|
|
|
|
|
this.userRateLimit.set(userId, userData); |
|
|
|
|
|
return { |
|
|
allowed: true, |
|
|
reason: 'OK', |
|
|
messagesCount: userData.count, |
|
|
limit: this.hourlyLimit, |
|
|
percentualUso |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_logRateLimitAttempt(status, userId, userName, userNumber, messageText, quotedMessage, details, action) { |
|
|
if (!this.enableDetailedLogging) return; |
|
|
|
|
|
const timestamp = new Date().toLocaleString('pt-BR', { |
|
|
year: 'numeric', |
|
|
month: '2-digit', |
|
|
day: '2-digit', |
|
|
hour: '2-digit', |
|
|
minute: '2-digit', |
|
|
second: '2-digit', |
|
|
hour12: false |
|
|
}); |
|
|
|
|
|
const separator = 'β'.repeat(100); |
|
|
const border = 'β'.repeat(100); |
|
|
|
|
|
|
|
|
console.log(`\n${separator}`); |
|
|
console.log(`π [${timestamp}] ${status}`); |
|
|
console.log(border); |
|
|
|
|
|
console.log(`π€ USUΓRIO`); |
|
|
console.log(` ββ Nome: ${userName || 'Desconhecido'}`); |
|
|
console.log(` ββ NΓΊmero: ${userNumber || 'N/A'}`); |
|
|
console.log(` ββ JID: ${userId || 'N/A'}`); |
|
|
|
|
|
console.log(`π¬ MENSAGEM`); |
|
|
console.log(` ββ Texto: "${messageText.substring(0, 150)}${messageText.length > 150 ? '...' : ''}"`); |
|
|
console.log(` ββ Comprimento: ${messageText.length} caracteres`); |
|
|
if (quotedMessage) { |
|
|
console.log(` ββ Citada: "${quotedMessage.substring(0, 100)}${quotedMessage.length > 100 ? '...' : ''}"`); |
|
|
} |
|
|
console.log(` ββ Tipo: ${messageText.startsWith('#') ? 'COMANDO' : 'MENSAGEM'}`); |
|
|
|
|
|
console.log(`π DETALHES`); |
|
|
console.log(` ββ ${details}`); |
|
|
|
|
|
if (action) { |
|
|
console.log(`β‘ AΓΓO`); |
|
|
console.log(` ββ ${action}`); |
|
|
} |
|
|
|
|
|
console.log(separator); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getHourlyLimitStatus(userId) { |
|
|
const userData = this.userRateLimit.get(userId); |
|
|
if (!userData) { |
|
|
return { allowed: true, reason: 'Novo usuΓ‘rio' }; |
|
|
} |
|
|
|
|
|
const now = Date.now(); |
|
|
const timePassedMs = now - userData.windowStart; |
|
|
const timePassedMin = Math.floor(timePassedMs / 60000); |
|
|
|
|
|
return { |
|
|
messagesCount: userData.count, |
|
|
limit: this.hourlyLimit, |
|
|
blocked: now < userData.blockedUntil, |
|
|
blockedUntil: userData.blockedUntil, |
|
|
overAttempts: userData.overAttempts, |
|
|
warnings: userData.warnings, |
|
|
timePassedMinutes: timePassedMin |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
isUserMuted(groupId, userId) { |
|
|
const key = `${groupId}_${userId}`; |
|
|
const muteData = this.mutedUsers.get(key); |
|
|
|
|
|
if (!muteData) return false; |
|
|
|
|
|
if (Date.now() > muteData.expires) { |
|
|
this.mutedUsers.delete(key); |
|
|
return false; |
|
|
} |
|
|
|
|
|
return true; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
muteUser(groupId, userId, minutes = null) { |
|
|
minutes = minutes || this.config.MUTE_DEFAULT_MINUTES; |
|
|
const key = `${groupId}_${userId}`; |
|
|
|
|
|
const muteCount = this.incrementMuteCount(groupId, userId); |
|
|
|
|
|
|
|
|
if (muteCount > 1) { |
|
|
minutes = minutes * Math.pow(2, muteCount - 1); |
|
|
} |
|
|
|
|
|
const expires = Date.now() + (minutes * 60 * 1000); |
|
|
this.mutedUsers.set(key, { |
|
|
expires, |
|
|
mutedAt: Date.now(), |
|
|
minutes, |
|
|
muteCount |
|
|
}); |
|
|
|
|
|
return { expires, minutes, muteCount }; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
unmuteUser(groupId, userId) { |
|
|
const key = `${groupId}_${userId}`; |
|
|
return this.mutedUsers.delete(key); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
incrementMuteCount(groupId, userId) { |
|
|
const key = `${groupId}_${userId}`; |
|
|
const today = new Date().toDateString(); |
|
|
const countData = this.muteCounts.get(key) || { count: 0, lastMuteDate: today }; |
|
|
|
|
|
if (countData.lastMuteDate !== today) { |
|
|
countData.count = 0; |
|
|
countData.lastMuteDate = today; |
|
|
} |
|
|
|
|
|
countData.count += 1; |
|
|
this.muteCounts.set(key, countData); |
|
|
|
|
|
return countData.count; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getMuteCount(groupId, userId) { |
|
|
const key = `${groupId}_${userId}`; |
|
|
const today = new Date().toDateString(); |
|
|
const countData = this.muteCounts.get(key); |
|
|
|
|
|
if (!countData || countData.lastMuteDate !== today) { |
|
|
return 0; |
|
|
} |
|
|
|
|
|
return countData.count || 0; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
toggleAntiLink(groupId, enable = true) { |
|
|
if (enable) { |
|
|
this.antiLinkGroups.add(groupId); |
|
|
} else { |
|
|
this.antiLinkGroups.delete(groupId); |
|
|
} |
|
|
return enable; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
isAntiLinkActive(groupId) { |
|
|
return this.antiLinkGroups.has(groupId); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
containsLink(text) { |
|
|
if (!text) return false; |
|
|
|
|
|
const linkRegex = /(https?:\/\/[^\s]+)|(www\.[^\s]+)|(bit\.ly\/[^\s]+)|(t\.me\/[^\s]+)|(wa\.me\/[^\s]+)|(chat\.whatsapp\.com\/[^\s]+)/gi; |
|
|
return linkRegex.test(text); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
banUser(userId, reason = 'violaΓ§Γ£o de regras', expiresIn = null) { |
|
|
const key = String(userId); |
|
|
let expiresAt = 'PERMANENT'; |
|
|
|
|
|
if (expiresIn) { |
|
|
expiresAt = Date.now() + expiresIn; |
|
|
} |
|
|
|
|
|
this.bannedUsers.set(key, { |
|
|
reason, |
|
|
bannedAt: Date.now(), |
|
|
expiresAt |
|
|
}); |
|
|
|
|
|
return { userId, reason, expiresAt }; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
unbanUser(userId) { |
|
|
return this.bannedUsers.delete(String(userId)); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
isBanned(userId) { |
|
|
const key = String(userId); |
|
|
const banData = this.bannedUsers.get(key); |
|
|
|
|
|
if (!banData) return false; |
|
|
|
|
|
if (banData.expiresAt !== 'PERMANENT' && Date.now() > banData.expiresAt) { |
|
|
this.bannedUsers.delete(key); |
|
|
return false; |
|
|
} |
|
|
|
|
|
return true; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
checkSpam(userId) { |
|
|
const now = Date.now(); |
|
|
const userData = this.spamCache.get(userId) || []; |
|
|
|
|
|
const filtered = userData.filter(t => (now - t) < this.SPAM_WINDOW_MS); |
|
|
|
|
|
if (filtered.length >= this.SPAM_THRESHOLD) { |
|
|
return true; |
|
|
} |
|
|
|
|
|
filtered.push(now); |
|
|
this.spamCache.set(userId, filtered); |
|
|
|
|
|
|
|
|
if (this.spamCache.size > 1000) { |
|
|
const oldestKey = this.spamCache.keys().next().value; |
|
|
this.spamCache.delete(oldestKey); |
|
|
} |
|
|
|
|
|
return false; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
clearSpamCache() { |
|
|
this.spamCache.clear(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
isUserBlacklisted(userId) { |
|
|
const list = this.loadBlacklistData(); |
|
|
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.loadBlacklistData(); |
|
|
const arr = Array.isArray(list) ? list : []; |
|
|
|
|
|
|
|
|
if (arr.find(x => x && x.id === userId)) { |
|
|
return { success: false, message: 'JΓ‘ estava na blacklist' }; |
|
|
} |
|
|
|
|
|
let expiresAt = 'PERMANENT'; |
|
|
if (expiryMs) { |
|
|
expiresAt = Date.now() + expiryMs; |
|
|
} |
|
|
|
|
|
const entry = { |
|
|
id: userId, |
|
|
name: userName, |
|
|
number: userNumber, |
|
|
reason, |
|
|
addedAt: Date.now(), |
|
|
expiresAt, |
|
|
severity: reason === 'abuse' ? 'CRΓTICO' : reason === 'spam' ? 'ALTO' : 'NORMAL' |
|
|
}; |
|
|
|
|
|
arr.push(entry); |
|
|
|
|
|
try { |
|
|
require('fs').writeFileSync( |
|
|
this.blacklistPath || './database/datauser/blacklist.json', |
|
|
JSON.stringify(arr, null, 2) |
|
|
); |
|
|
|
|
|
|
|
|
const timestamp = new Date().toLocaleString('pt-BR'); |
|
|
const severity = entry.severity; |
|
|
const expiresStr = expiresAt === 'PERMANENT' ? 'PERMANENTE' : new Date(expiresAt).toLocaleString('pt-BR'); |
|
|
|
|
|
console.log(`\n${'β'.repeat(100)}`); |
|
|
console.log(`π« [${timestamp}] BLACKLIST ADICIONADO - SEVERIDADE: ${severity}`); |
|
|
console.log(`${'β'.repeat(100)}`); |
|
|
console.log(`π€ USUΓRIO`); |
|
|
console.log(` ββ Nome: ${userName}`); |
|
|
console.log(` ββ NΓΊmero: ${userNumber}`); |
|
|
console.log(` ββ JID: ${userId}`); |
|
|
console.log(`π RAZΓO: ${reason}`); |
|
|
console.log(`β° EXPIRAΓΓO: ${expiresStr}`); |
|
|
console.log(`π STATUS: Agora serΓ‘ ignorado completamente`); |
|
|
console.log(`${'β'.repeat(100)}\n`); |
|
|
|
|
|
return { success: true, entry }; |
|
|
} catch (e) { |
|
|
console.error('Erro ao adicionar Γ blacklist:', e); |
|
|
return { success: false, message: e.message }; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
removeFromBlacklist(userId) { |
|
|
const list = this.loadBlacklistData(); |
|
|
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 { |
|
|
require('fs').writeFileSync( |
|
|
this.blacklistPath || './database/datauser/blacklist.json', |
|
|
JSON.stringify(arr, null, 2) |
|
|
); |
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
loadBlacklistData() { |
|
|
try { |
|
|
const fs = require('fs'); |
|
|
const path = this.blacklistPath || './database/datauser/blacklist.json'; |
|
|
|
|
|
if (!fs.existsSync(path)) { |
|
|
return []; |
|
|
} |
|
|
|
|
|
const data = fs.readFileSync(path, 'utf8'); |
|
|
if (!data || !data.trim()) { |
|
|
return []; |
|
|
} |
|
|
|
|
|
return JSON.parse(data); |
|
|
} catch (e) { |
|
|
console.error('Erro ao carregar blacklist:', e); |
|
|
return []; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getBlacklistReport() { |
|
|
const list = this.loadBlacklistData(); |
|
|
if (!Array.isArray(list) || list.length === 0) { |
|
|
return { total: 0, entries: [] }; |
|
|
} |
|
|
|
|
|
const 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') |
|
|
})); |
|
|
|
|
|
return { total: entries.length, entries }; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getStats() { |
|
|
return { |
|
|
mutedUsers: this.mutedUsers.size, |
|
|
bannedUsers: this.bannedUsers.size, |
|
|
antiLinkGroups: this.antiLinkGroups.size, |
|
|
spamCacheSize: this.spamCache.size, |
|
|
hourlyBlockedUsers: Array.from(this.userRateLimit.entries()).filter(([_, data]) => data.blockedUntil > Date.now()).length, |
|
|
blacklistedUsers: this.loadBlacklistData().length |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
reset() { |
|
|
this.mutedUsers.clear(); |
|
|
this.antiLinkGroups.clear(); |
|
|
this.muteCounts.clear(); |
|
|
this.bannedUsers.clear(); |
|
|
this.spamCache.clear(); |
|
|
this.userRateLimit.clear(); |
|
|
this.logger.info('π Sistema de moderaΓ§Γ£o resetado'); |
|
|
} |
|
|
} |
|
|
|
|
|
module.exports = ModerationSystem; |
|
|
|