Correction complète des systèmes anti-spam et anti-liens + optimisations des performances et de la mémoire

This commit is contained in:
Mathis 2026-01-01 22:07:48 +01:00
parent 59e738c0d4
commit 343a4f879c
4 changed files with 434 additions and 110 deletions

View file

@ -2,154 +2,184 @@ import discord
from discord.ext import commands from discord.ext import commands
import json import json
import os import os
import asyncio from collections import deque
from datetime import datetime from datetime import datetime, timedelta
# Chemins des fichiers
SETTINGS_FILE = "data/security_settings.json" SETTINGS_FILE = "data/security_settings.json"
WHITELIST_FILE = "data/whitelist.json" WHITELIST_FILE = "data/whitelist.json"
# Cache TTL en secondes (5 secondes pour avoir une réactivité acceptable)
CACHE_TTL = 5
# Triggers pour détection de liens
LINK_TRIGGERS = ("discord.gg/", "http://", "https://", "www.")
class AntiSpam(commands.Cog): class AntiSpam(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
# Compteurs pour le Spam # Optimisation: deque avec maxlen pour O(1) au lieu de O(n) list filtering
self.message_counts = {} self.message_counts = {}
self.spam_warnings = {} self.spam_warnings = {}
self.link_warnings = {}
# Cache avec timestamps pour éviter la lecture fichier à chaque message
self._settings_cache = {} # {guild_id: (timestamp, settings_dict)}
self._whitelist_cache = {} # {guild_id: (timestamp, whitelist_set)}
def _get_cached_settings(self, guild_id):
"""Récupère les paramètres avec cache (TTL de 5 secondes)"""
now = discord.utils.utcnow().timestamp()
cached = self._settings_cache.get(guild_id)
# Compteurs pour les Liens if cached:
self.link_warnings = {} cached_time, settings = cached
if now - cached_time < CACHE_TTL:
return settings
# Cache expiré ou absent - lire le fichier
try:
if os.path.exists(SETTINGS_FILE):
with open(SETTINGS_FILE, "r", encoding='utf-8') as f:
all_settings = json.load(f)
settings = all_settings.get(str(guild_id), {})
else:
settings = {}
except:
settings = {}
self._settings_cache[guild_id] = (now, settings)
return settings
def _get_cached_whitelist(self, guild_id):
"""Récupère la whitelist avec cache (TTL de 5 secondes)"""
now = discord.utils.utcnow().timestamp()
cached = self._whitelist_cache.get(guild_id)
if cached:
cached_time, whitelist_set = cached
if now - cached_time < CACHE_TTL:
return whitelist_set
# Cache expiré ou absent - lire le fichier
whitelist_set = set()
try:
if os.path.exists(WHITELIST_FILE):
with open(WHITELIST_FILE, "r", encoding='utf-8') as f:
data = json.load(f)
server_id_str = str(guild_id)
whitelist_ids = data.get(server_id_str, [])
whitelist_set = set(str(uid) for uid in whitelist_ids)
except:
pass
self._whitelist_cache[guild_id] = (now, whitelist_set)
return whitelist_set
def is_whitelisted(self, guild_id, user_id): def is_whitelisted(self, guild_id, user_id):
"""Vérifie si l'utilisateur est protégé (Whitelist ou Owner)""" """Vérifie l'immunité (Propriétaire ou Whitelist JSON avec cache)"""
# 1. Le propriétaire est toujours whitelist
guild = self.bot.get_guild(guild_id) guild = self.bot.get_guild(guild_id)
if guild and user_id == guild.owner_id: if guild and user_id == guild.owner_id:
return True return True
# 2. Vérification fichier Whitelist # Utilisation du cache whitelist
if not os.path.exists(WHITELIST_FILE): return False whitelist = self._get_cached_whitelist(guild_id)
try: return str(user_id) in whitelist
with open(WHITELIST_FILE, "r", encoding='utf-8') as f:
data = json.load(f)
return user_id in data.get(str(guild_id), [])
except: return False
@commands.Cog.listener() @commands.Cog.listener()
async def on_message(self, message): async def on_message(self, message):
if message.author.bot or not message.guild: return # 1. Filtres de base (early return pour performance)
if message.author.bot or not message.guild:
return
guild_id = message.guild.id
user_id = message.author.id
# --- 1. IMMUNITÉ TOTALE --- # 2. VÉRIFICATION WHITELIST (Immunité totale)
# Si Whitelisté, on ignore tout (Spam et Liens) if self.is_whitelisted(guild_id, user_id):
if self.is_whitelisted(message.guild.id, message.author.id):
return return
# --- 2. CHARGEMENT CONFIG --- # --- LECTURE CACHÉ (dynamique mais rapide) ---
if not os.path.exists(SETTINGS_FILE): return settings = self._get_cached_settings(guild_id)
with open(SETTINGS_FILE, "r", encoding='utf-8') as f:
settings = json.load(f).get(str(message.guild.id), {}) # --- TEST ANTI-LIENS ---
# Bloque tout le monde sauf whitelist ET manage_messages
anti_link_enabled = settings.get("anti_link", False) if settings else False
has_perm = message.author.guild_permissions.manage_messages
if anti_link_enabled and not has_perm:
content_lower = message.content.lower()
if any(trigger in content_lower for trigger in LINK_TRIGGERS):
await self.handle_violation(message, "link")
return
# ================================================================= # Sortir si pas de settings
# MODULE ANTI-LINK (3 Avertissements -> Mute) if not settings:
# ================================================================= return
if settings.get("anti_link"):
# Liste des déclencheurs interdits # --- TEST ANTI-SPAM (deque pour O(1)) ---
forbidden = ["discord.gg/", "http://", "https://", "www."] if settings.get("anti_spam") is True:
now = discord.utils.utcnow().timestamp()
# On vérifie si le message contient un lien interdit # Initialiser ou récupérer le deque pour cet utilisateur
if any(trigger in message.content.lower() for trigger in forbidden):
# Si l'auteur a la permission de gérer les messages, on laisse passer (optionnel, sinon retirer cette ligne)
if not message.author.guild_permissions.manage_messages:
await self.handle_link_violation(message)
return # On arrête là, pas besoin de vérifier le spam si le message est supprimé
# =================================================================
# MODULE ANTI-SPAM (3 Avertissements -> Mute)
# =================================================================
if settings.get("anti_spam"):
user_id = message.author.id
now = datetime.now().timestamp()
if user_id not in self.message_counts: if user_id not in self.message_counts:
self.message_counts[user_id] = [] # maxlen=10 suffit pour la fenêtre de 5 secondes avec seuil typique
self.message_counts[user_id] = deque(maxlen=10)
user_deque = self.message_counts[user_id]
user_deque.append(now)
# Nettoyer les timestamps expirés (plus vieux que 5 secondes)
# Le deque supprime automatiquement les anciens éléments quand il dépasse maxlen
# Mais on doit aussi filtrer par temps pour être précis
while user_deque and now - user_deque[0] >= 5:
user_deque.popleft()
threshold = settings.get("spam_threshold", 5)
if len(user_deque) > threshold:
await self.handle_violation(message, "spam")
self.message_counts[user_id].append(now) async def handle_violation(self, message, type_violation):
# Fenêtre de temps : 5 secondes
self.message_counts[user_id] = [t for t in self.message_counts[user_id] if now - t < 5]
# Seuil dynamique (défaut: 5)
limit = settings.get("spam_threshold", 5)
if len(self.message_counts[user_id]) > limit:
await self.handle_spam_violation(message)
# --- GESTION DES VIOLATIONS LIENS ---
async def handle_link_violation(self, message):
try:
await message.delete()
except:
pass # Message déjà supprimé ou manque de perms
user_id = message.author.id
warnings = self.link_warnings.get(user_id, 0) + 1
self.link_warnings[user_id] = warnings
if warnings <= 3:
embed = discord.Embed(
title="🔗 Liens Interdits",
description=f"{message.author.mention}, les liens ne sont pas autorisés.\n**Avertissement : {warnings}/3**",
color=0xFFA500
)
await message.channel.send(embed=embed, delete_after=5)
else:
await self.sanction_user(message, "Publication de liens interdits")
self.link_warnings[user_id] = 0 # Reset après sanction
# --- GESTION DES VIOLATIONS SPAM ---
async def handle_spam_violation(self, message):
user_id = message.author.id user_id = message.author.id
# Nettoyage visuel du spam if type_violation == "link":
try: try: await message.delete()
await message.channel.purge(limit=5, check=lambda m: m.author == message.author) except: pass
except: pass self.link_warnings[user_id] = self.link_warnings.get(user_id, 0) + 1
current_warns = self.link_warnings[user_id]
reason = "Envoi de liens interdits"
else:
try: await message.channel.purge(limit=5, check=lambda m: m.author == message.author)
except: pass
self.spam_warnings[user_id] = self.spam_warnings.get(user_id, 0) + 1
current_warns = self.spam_warnings[user_id]
reason = "Spam excessif"
# Réinitialiser le deque après spam détecté
self.message_counts[user_id] = deque(maxlen=10)
warnings = self.spam_warnings.get(user_id, 0) + 1 if current_warns <= 3:
self.spam_warnings[user_id] = warnings
# Reset le compteur de messages pour éviter le spam d'avertissements
self.message_counts[user_id] = []
if warnings <= 3:
embed = discord.Embed( embed = discord.Embed(
title="🛡️ Anti-Spam", title="🛡️ Sécurité Kuby",
description=f"{message.author.mention}, veuillez cesser de spammer.\n**Avertissement : {warnings}/3**", description=f"{message.author.mention}, {reason}.\n**Avertissement : {current_warns}/3**",
color=0xFFA500 color=0xFFA500
) )
await message.channel.send(embed=embed, delete_after=5) await message.channel.send(embed=embed, delete_after=5)
else: else:
await self.sanction_user(message, "Spam excessif") await self.apply_mute(message, reason)
self.spam_warnings[user_id] = 0 # Reset après sanction if type_violation == "link": self.link_warnings[user_id] = 0
else: self.spam_warnings[user_id] = 0
# --- SANCTION COMMUNE (TIMEOUT 5 MIN) --- async def apply_mute(self, message, reason):
async def sanction_user(self, message, reason):
try: try:
# Durée : 5 minutes until = discord.utils.utcnow() + timedelta(minutes=5)
duration = discord.utils.utcnow() + asyncio.timedelta(minutes=5) await message.author.timeout(until, reason=f"Kuby Security: {reason} (3 Warns)")
# Application du Timeout (Exclusion temporaire)
await message.author.timeout(duration, reason=f"Kuby Security: {reason} (3 Warns)")
embed = discord.Embed( embed = discord.Embed(
title="⛔ Exclusion Temporaire", title="⛔ Exclusion Temporaire",
description=f"**Utilisateur :** {message.author.mention}\n**Durée :** 5 minutes\n**Raison :** {reason} (Limite atteinte).", description=f"**Membre :** {message.author.mention}\n**Durée :** 5 minutes\n**Raison :** {reason} répétitif.",
color=0xFF0000 color=0xFF0000
) )
await message.channel.send(embed=embed) await message.channel.send(embed=embed)
except discord.Forbidden:
await message.channel.send("⚠️ Je n'ai pas la permission d'exclure ce membre (Vérifiez ma hiérarchie).")
except Exception as e: except Exception as e:
print(f"Erreur Sanction: {e}") print(f"Erreur lors de la sanction : {e}")
async def setup(bot): async def setup(bot):
await bot.add_cog(AntiSpam(bot)) await bot.add_cog(AntiSpam(bot))

View file

@ -0,0 +1,10 @@
[
{
"timestamp": "2026-01-01T22:00:26.255907",
"event_type": "member_join",
"member_id": 1456390746167967932,
"member_name": "juibu_66388",
"member_discriminator": "0",
"member_avatar": null
}
]

View file

@ -0,0 +1,284 @@
[
{
"timestamp": "2026-01-01T21:05:28.276062",
"event_type": "deleted",
"message_id": 1456377795595730994,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:12:06.156108",
"event_type": "deleted",
"message_id": 1456379476211531970,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:12:06.462625",
"event_type": "deleted",
"message_id": 1456379478606348421,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "gg",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:12:12.104726",
"event_type": "deleted",
"message_id": 1456379484264595700,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:35:44.759055",
"event_type": "deleted",
"message_id": 1456385391564296297,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:35:51.064634",
"event_type": "deleted",
"message_id": 1456385392482844682,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:40:18.016792",
"event_type": "deleted",
"message_id": 1456386513947463855,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:40:21.147093",
"event_type": "deleted",
"message_id": 1456386515096703242,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:40:24.322926",
"event_type": "deleted",
"message_id": 1456386516439007288,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:43:53.352930",
"event_type": "deleted",
"message_id": 1456387431472435231,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:44:15.896248",
"event_type": "deleted",
"message_id": 1456387423477956670,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:44:32.743792",
"event_type": "deleted",
"message_id": 1456387525080776788,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "https://github.com/Omega-Kube/Kuby",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:44:43.533530",
"event_type": "deleted",
"message_id": 1456387651883237437,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "https://github.com/Omega-Kube/Kuby",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:51:23.069501",
"event_type": "deleted",
"message_id": 1456389356301582459,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "[DEBUG] anti_link_enabled: True, settings: {'anti_spam': True, 'anti_perm': True, 'anti_salon': True, 'anti_raid': True, 'anti_nuke': True, 'anti_link': True, 'anti_mention': True, 'verification_enabled': True, 'spam_threshold': 5, 'raid_threshold': 3, 'nuke_threshold': 3, 'log_channel_id': 1396088860412346428, 'lockdown_enabled': False, 'auto_ban': True, 'webhook_protection': True, 'mass_delete_protection': True, 'invite_filter': True, 'file_scanning': True, 'ai_moderation': True, 'channel_block_enabled': False, 'server_logs': True, 'anti_channel_delete': True, 'anti_role_create': True}",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T22:01:31.237385",
"event_type": "deleted",
"message_id": 1456391913694433361,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 1456390746167967932,
"author_name": "juibu_66388",
"author_roles": [
1369669999345537145
],
"content": "https://github.com/Omega-Kube/Kuby",
"attachments": [],
"embeds": [],
"details": {}
}
]

View file

@ -8,7 +8,7 @@
"anti_link": true, "anti_link": true,
"anti_mention": true, "anti_mention": true,
"verification_enabled": true, "verification_enabled": true,
"spam_threshold": 1, "spam_threshold": 5,
"raid_threshold": 3, "raid_threshold": 3,
"nuke_threshold": 3, "nuke_threshold": 3,
"log_channel_id": 1396088860412346428, "log_channel_id": 1396088860412346428,