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 = {}
# Compteurs pour les Liens
self.link_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)
if cached:
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:
# --- 1. IMMUNITÉ TOTALE ---
# Si Whitelisté, on ignore tout (Spam et Liens)
if self.is_whitelisted(message.guild.id, message.author.id):
return return
# --- 2. CHARGEMENT CONFIG --- guild_id = message.guild.id
if not os.path.exists(SETTINGS_FILE): return user_id = message.author.id
with open(SETTINGS_FILE, "r", encoding='utf-8') as f:
settings = json.load(f).get(str(message.guild.id), {})
# ================================================================= # 2. VÉRIFICATION WHITELIST (Immunité totale)
# MODULE ANTI-LINK (3 Avertissements -> Mute) if self.is_whitelisted(guild_id, user_id):
# ================================================================= return
if settings.get("anti_link"):
# Liste des déclencheurs interdits
forbidden = ["discord.gg/", "http://", "https://", "www."]
# On vérifie si le message contient un lien interdit # --- LECTURE CACHÉ (dynamique mais rapide) ---
if any(trigger in message.content.lower() for trigger in forbidden): settings = self._get_cached_settings(guild_id)
# 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é
# ================================================================= # --- TEST ANTI-LIENS ---
# MODULE ANTI-SPAM (3 Avertissements -> Mute) # Bloque tout le monde sauf whitelist ET manage_messages
# ================================================================= anti_link_enabled = settings.get("anti_link", False) if settings else False
if settings.get("anti_spam"): has_perm = message.author.guild_permissions.manage_messages
user_id = message.author.id if anti_link_enabled and not has_perm:
now = datetime.now().timestamp() 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
if not settings:
return
# --- TEST ANTI-SPAM (deque pour O(1)) ---
if settings.get("anti_spam") is True:
now = discord.utils.utcnow().timestamp()
# Initialiser ou récupérer le deque pour cet utilisateur
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)
self.message_counts[user_id].append(now) user_deque = self.message_counts[user_id]
# Fenêtre de temps : 5 secondes user_deque.append(now)
self.message_counts[user_id] = [t for t in self.message_counts[user_id] if now - t < 5]
# Seuil dynamique (défaut: 5) # Nettoyer les timestamps expirés (plus vieux que 5 secondes)
limit = settings.get("spam_threshold", 5) # 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()
if len(self.message_counts[user_id]) > limit: threshold = settings.get("spam_threshold", 5)
await self.handle_spam_violation(message) if len(user_deque) > threshold:
await self.handle_violation(message, "spam")
# --- 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
async def handle_violation(self, message, type_violation):
user_id = message.author.id user_id = message.author.id
warnings = self.link_warnings.get(user_id, 0) + 1
self.link_warnings[user_id] = warnings
if warnings <= 3: if type_violation == "link":
try: await message.delete()
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)
if current_warns <= 3:
embed = discord.Embed( embed = discord.Embed(
title="🔗 Liens Interdits", title="🛡️ Sécurité Kuby",
description=f"{message.author.mention}, les liens ne sont pas autorisés.\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, "Publication de liens interdits") await self.apply_mute(message, reason)
self.link_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
# --- GESTION DES VIOLATIONS SPAM --- async def apply_mute(self, message, reason):
async def handle_spam_violation(self, message):
user_id = message.author.id
# Nettoyage visuel du spam
try: try:
await message.channel.purge(limit=5, check=lambda m: m.author == message.author) until = discord.utils.utcnow() + timedelta(minutes=5)
except: pass await message.author.timeout(until, reason=f"Kuby Security: {reason} (3 Warns)")
warnings = self.spam_warnings.get(user_id, 0) + 1
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(
title="🛡️ Anti-Spam",
description=f"{message.author.mention}, veuillez cesser de spammer.\n**Avertissement : {warnings}/3**",
color=0xFFA500
)
await message.channel.send(embed=embed, delete_after=5)
else:
await self.sanction_user(message, "Spam excessif")
self.spam_warnings[user_id] = 0 # Reset après sanction
# --- SANCTION COMMUNE (TIMEOUT 5 MIN) ---
async def sanction_user(self, message, reason):
try:
# Durée : 5 minutes
duration = discord.utils.utcnow() + asyncio.timedelta(minutes=5)
# 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,