2026-05-06 17:07:09 +02:00
|
|
|
|
import disnake
|
|
|
|
|
|
from disnake.ext import commands
|
2026-01-01 18:48:25 +01:00
|
|
|
|
import json
|
|
|
|
|
|
import os
|
2026-01-02 19:04:05 +01:00
|
|
|
|
from typing import Optional
|
2026-01-01 18:48:25 +01:00
|
|
|
|
|
2026-05-25 19:08:45 +02:00
|
|
|
|
# Chemins ABSOLUS des fichiers (basés sur le fichier courant)
|
|
|
|
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
DATA_DIR = os.path.join(BASE_DIR, "..", "data")
|
|
|
|
|
|
SETTINGS_FILE = os.path.join(DATA_DIR, "security_settings.json")
|
|
|
|
|
|
WHITELIST_FILE = os.path.join(DATA_DIR, "whitelist.json")
|
|
|
|
|
|
|
|
|
|
|
|
# Créer le dossier data/ s'il n'existe pas
|
|
|
|
|
|
os.makedirs(DATA_DIR, exist_ok=True)
|
2026-01-01 18:48:25 +01:00
|
|
|
|
|
|
|
|
|
|
# --- UTILITAIRES ---
|
2026-01-02 19:04:05 +01:00
|
|
|
|
def load_settings(guild_id: int) -> dict:
|
|
|
|
|
|
"""Charge les paramètres de sécurité pour un serveur"""
|
|
|
|
|
|
if not os.path.exists(SETTINGS_FILE):
|
|
|
|
|
|
return {}
|
2026-01-01 18:48:25 +01:00
|
|
|
|
try:
|
|
|
|
|
|
with open(SETTINGS_FILE, "r", encoding='utf-8') as f:
|
|
|
|
|
|
return json.load(f).get(str(guild_id), {})
|
2026-01-02 19:04:05 +01:00
|
|
|
|
except (json.JSONDecodeError, FileNotFoundError):
|
|
|
|
|
|
return {}
|
2026-01-01 18:48:25 +01:00
|
|
|
|
|
2026-01-02 19:04:05 +01:00
|
|
|
|
def save_settings(guild_id: int, settings: dict) -> None:
|
|
|
|
|
|
"""Sauvegarde les paramètres de sécurité"""
|
2026-01-01 18:48:25 +01:00
|
|
|
|
data = {}
|
|
|
|
|
|
if os.path.exists(SETTINGS_FILE):
|
|
|
|
|
|
try:
|
2026-01-02 19:04:05 +01:00
|
|
|
|
with open(SETTINGS_FILE, "r", encoding='utf-8') as f:
|
|
|
|
|
|
data = json.load(f)
|
|
|
|
|
|
except (json.JSONDecodeError, FileNotFoundError):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2026-01-01 18:48:25 +01:00
|
|
|
|
data[str(guild_id)] = settings
|
2026-01-02 19:04:05 +01:00
|
|
|
|
|
2026-05-25 19:08:45 +02:00
|
|
|
|
os.makedirs(DATA_DIR, exist_ok=True)
|
2026-01-01 18:48:25 +01:00
|
|
|
|
with open(SETTINGS_FILE, "w", encoding='utf-8') as f:
|
|
|
|
|
|
json.dump(data, f, indent=4, ensure_ascii=False)
|
|
|
|
|
|
|
2026-01-02 19:04:05 +01:00
|
|
|
|
def load_whitelist(guild_id: int) -> list:
|
|
|
|
|
|
"""Charge la whitelist pour un serveur"""
|
|
|
|
|
|
if not os.path.exists(WHITELIST_FILE):
|
|
|
|
|
|
return []
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(WHITELIST_FILE, "r", encoding='utf-8') as f:
|
|
|
|
|
|
data = json.load(f)
|
|
|
|
|
|
return data.get(str(guild_id), [])
|
|
|
|
|
|
except (json.JSONDecodeError, FileNotFoundError):
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
def is_whitelisted(guild_id: int, user_id: int) -> bool:
|
|
|
|
|
|
"""Vérifie si un utilisateur est whitelisté"""
|
|
|
|
|
|
if not os.path.exists(WHITELIST_FILE):
|
|
|
|
|
|
return False
|
2026-01-01 18:48:25 +01:00
|
|
|
|
try:
|
|
|
|
|
|
with open(WHITELIST_FILE, "r", encoding='utf-8') as f:
|
|
|
|
|
|
data = json.load(f)
|
|
|
|
|
|
return user_id in data.get(str(guild_id), [])
|
2026-01-02 19:04:05 +01:00
|
|
|
|
except (json.JSONDecodeError, FileNotFoundError):
|
|
|
|
|
|
return False
|
2026-01-01 18:48:25 +01:00
|
|
|
|
|
2026-01-02 19:04:05 +01:00
|
|
|
|
def is_immune(guild, user) -> bool:
|
|
|
|
|
|
"""Vérifie l'immunité (propriétaire, admin, ou whitelist)"""
|
|
|
|
|
|
if user.id == guild.owner_id:
|
|
|
|
|
|
return True
|
|
|
|
|
|
if user.guild_permissions.administrator:
|
|
|
|
|
|
return True
|
|
|
|
|
|
if is_whitelisted(guild.id, user.id):
|
|
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
# --- MODALS DE CONFIGURATION ---
|
|
|
|
|
|
|
2026-05-09 18:42:42 +02:00
|
|
|
|
class SpamThresholdModal(disnake.ui.Modal):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Modal pour configurer le seuil anti-spam"""
|
2026-05-06 17:07:09 +02:00
|
|
|
|
threshold_input = disnake.ui.TextInput(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label="Messages max autorisés",
|
|
|
|
|
|
placeholder="Entre 3 et 10 (défaut: 5)",
|
2026-01-01 18:48:25 +01:00
|
|
|
|
min_length=1,
|
|
|
|
|
|
max_length=2,
|
|
|
|
|
|
required=True
|
|
|
|
|
|
)
|
2026-01-02 19:04:05 +01:00
|
|
|
|
|
|
|
|
|
|
def __init__(self, guild_id: int, settings: dict, view: 'SecurityView'):
|
2026-05-09 18:42:42 +02:00
|
|
|
|
super().__init__(title="🛡️ Seuil Anti-Spam", components=[self.threshold_input])
|
2026-01-02 19:04:05 +01:00
|
|
|
|
self.guild_id = guild_id
|
|
|
|
|
|
self.settings = settings
|
|
|
|
|
|
self.parent_view = view
|
|
|
|
|
|
|
2026-05-09 18:42:42 +02:00
|
|
|
|
async def callback(self, interaction: disnake.Interaction):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
if not self.threshold_input.value.isdigit():
|
|
|
|
|
|
return await interaction.response.send_message(
|
|
|
|
|
|
"❌ Erreur : Veuillez entrer un nombre entier.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
value = int(self.threshold_input.value)
|
|
|
|
|
|
if value < 1 or value > 10:
|
|
|
|
|
|
return await interaction.response.send_message(
|
|
|
|
|
|
"❌ Erreur : La valeur doit être entre 1 et 10.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self.settings["spam_threshold"] = value
|
|
|
|
|
|
save_settings(self.guild_id, self.settings)
|
|
|
|
|
|
self.parent_view.settings = self.settings
|
|
|
|
|
|
self.parent_view.build_view()
|
|
|
|
|
|
|
|
|
|
|
|
await interaction.response.edit_message(view=self.parent_view)
|
|
|
|
|
|
await interaction.followup.send(
|
|
|
|
|
|
f"✅ Seuil anti-spam configuré sur **{value}** messages/5s",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
2026-01-01 18:48:25 +01:00
|
|
|
|
|
2026-05-09 18:42:42 +02:00
|
|
|
|
class RaidThresholdModal(disnake.ui.Modal):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Modal pour configurer le seuil anti-raid"""
|
2026-05-06 17:07:09 +02:00
|
|
|
|
threshold_input = disnake.ui.TextInput(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label="Joins max en 10 secondes",
|
|
|
|
|
|
placeholder="Entre 2 et 10 (défaut: 3)",
|
|
|
|
|
|
min_length=1,
|
|
|
|
|
|
max_length=2,
|
|
|
|
|
|
required=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, guild_id: int, settings: dict, view: 'SecurityView'):
|
2026-05-09 18:42:42 +02:00
|
|
|
|
super().__init__(title="⚔️ Seuil Anti-Raid", components=[self.threshold_input])
|
2026-01-01 18:48:25 +01:00
|
|
|
|
self.guild_id = guild_id
|
|
|
|
|
|
self.settings = settings
|
|
|
|
|
|
self.parent_view = view
|
|
|
|
|
|
|
2026-05-09 18:42:42 +02:00
|
|
|
|
async def callback(self, interaction: disnake.Interaction):
|
2026-01-01 18:48:25 +01:00
|
|
|
|
if not self.threshold_input.value.isdigit():
|
2026-01-02 19:04:05 +01:00
|
|
|
|
return await interaction.response.send_message(
|
|
|
|
|
|
"❌ Erreur : Veuillez entrer un nombre entier.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
value = int(self.threshold_input.value)
|
|
|
|
|
|
if value < 1 or value > 20:
|
|
|
|
|
|
return await interaction.response.send_message(
|
|
|
|
|
|
"❌ Erreur : La valeur doit être entre 1 et 20.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
2026-01-01 18:48:25 +01:00
|
|
|
|
|
2026-01-02 19:04:05 +01:00
|
|
|
|
self.settings["raid_threshold"] = value
|
2026-01-01 18:48:25 +01:00
|
|
|
|
save_settings(self.guild_id, self.settings)
|
2026-01-02 19:04:05 +01:00
|
|
|
|
self.parent_view.settings = self.settings
|
|
|
|
|
|
self.parent_view.build_view()
|
2026-01-01 18:48:25 +01:00
|
|
|
|
|
2026-01-02 19:04:05 +01:00
|
|
|
|
await interaction.response.edit_message(view=self.parent_view)
|
|
|
|
|
|
await interaction.followup.send(
|
|
|
|
|
|
f"✅ Seuil anti-raid configuré sur **{value}** joins/10s",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-03 01:15:10 +01:00
|
|
|
|
# --- MODALS DE CONFIGURATION WHITELIST ---
|
|
|
|
|
|
|
2026-05-09 18:42:42 +02:00
|
|
|
|
class WhitelistModal(disnake.ui.Modal):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Modal pour ajouter un utilisateur à la whitelist"""
|
2026-05-06 17:07:09 +02:00
|
|
|
|
user_input = disnake.ui.TextInput(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label="ID de l'utilisateur",
|
|
|
|
|
|
placeholder="Entrez l'ID Discord de l'utilisateur",
|
|
|
|
|
|
min_length=15,
|
|
|
|
|
|
max_length=20,
|
|
|
|
|
|
required=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, guild_id: int, view: 'SecurityView'):
|
2026-05-09 18:42:42 +02:00
|
|
|
|
super().__init__(title="👤 Ajouter à la Whitelist", components=[self.user_input])
|
2026-01-02 19:04:05 +01:00
|
|
|
|
self.guild_id = guild_id
|
|
|
|
|
|
self.parent_view = view
|
|
|
|
|
|
|
2026-05-09 18:42:42 +02:00
|
|
|
|
async def callback(self, interaction: disnake.Interaction):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
if not self.user_input.value.isdigit():
|
|
|
|
|
|
return await interaction.response.send_message(
|
|
|
|
|
|
"❌ Erreur : ID invalide.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
user_id = int(self.user_input.value)
|
|
|
|
|
|
|
|
|
|
|
|
# Charger et modifier la whitelist
|
|
|
|
|
|
data = {}
|
|
|
|
|
|
if os.path.exists(WHITELIST_FILE):
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(WHITELIST_FILE, "r", encoding='utf-8') as f:
|
|
|
|
|
|
data = json.load(f)
|
|
|
|
|
|
except (json.JSONDecodeError, FileNotFoundError):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
guild_whitelist = data.get(str(self.guild_id), [])
|
|
|
|
|
|
|
|
|
|
|
|
if user_id in guild_whitelist:
|
|
|
|
|
|
return await interaction.response.send_message(
|
|
|
|
|
|
"❌ Cet utilisateur est déjà dans la whitelist.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
guild_whitelist.append(user_id)
|
|
|
|
|
|
data[str(self.guild_id)] = guild_whitelist
|
|
|
|
|
|
|
|
|
|
|
|
os.makedirs(os.path.dirname(WHITELIST_FILE), exist_ok=True)
|
|
|
|
|
|
with open(WHITELIST_FILE, "w", encoding='utf-8') as f:
|
|
|
|
|
|
json.dump(data, f, indent=4, ensure_ascii=False)
|
|
|
|
|
|
|
|
|
|
|
|
self.parent_view.whitelist_count = len(guild_whitelist)
|
|
|
|
|
|
self.parent_view.build_view()
|
|
|
|
|
|
|
|
|
|
|
|
await interaction.response.edit_message(view=self.parent_view)
|
|
|
|
|
|
await interaction.followup.send(
|
|
|
|
|
|
f"✅ Utilisateur ajouté à la whitelist.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# --- VUES DE L'INTERFACE ---
|
2026-01-01 18:48:25 +01:00
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
class SecurityView(disnake.ui.View):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Vue principale du panneau de sécurité"""
|
|
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
def __init__(self, interaction: disnake.Interaction, settings: dict, category: Optional[str] = None, bot=None):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
super().__init__(timeout=300) # 5 minutes timeout
|
2026-01-01 18:48:25 +01:00
|
|
|
|
self.interaction = interaction
|
|
|
|
|
|
self.guild = interaction.guild
|
|
|
|
|
|
self.settings = settings
|
|
|
|
|
|
self.category = category
|
2026-01-03 01:15:10 +01:00
|
|
|
|
self.bot = bot
|
2026-01-02 19:04:05 +01:00
|
|
|
|
self.whitelist_count = len(load_whitelist(self.guild.id))
|
2026-01-01 18:48:25 +01:00
|
|
|
|
self.build_view()
|
|
|
|
|
|
|
|
|
|
|
|
def build_view(self):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Construit les boutons selon la catégorie"""
|
2026-01-01 18:48:25 +01:00
|
|
|
|
self.clear_items()
|
|
|
|
|
|
|
|
|
|
|
|
if self.category is None:
|
2026-01-02 19:04:05 +01:00
|
|
|
|
# Menu principal
|
|
|
|
|
|
self.add_item(MainCategorySelect())
|
|
|
|
|
|
elif self.category == "spam":
|
|
|
|
|
|
self.build_spam_view()
|
|
|
|
|
|
elif self.category == "raid":
|
|
|
|
|
|
self.build_raid_view()
|
|
|
|
|
|
elif self.category == "nuke":
|
|
|
|
|
|
self.build_nuke_view()
|
|
|
|
|
|
elif self.category == "logs":
|
|
|
|
|
|
self.build_logs_view()
|
|
|
|
|
|
elif self.category == "whitelist":
|
|
|
|
|
|
self.build_whitelist_view()
|
2026-01-03 01:15:10 +01:00
|
|
|
|
elif self.category == "rules":
|
|
|
|
|
|
self.build_rules_view()
|
2026-06-05 17:42:07 +02:00
|
|
|
|
elif self.category == "membres":
|
|
|
|
|
|
self.build_members_view()
|
2026-01-02 19:04:05 +01:00
|
|
|
|
|
|
|
|
|
|
def build_spam_view(self):
|
|
|
|
|
|
"""Construit la vue Anti-Spam"""
|
|
|
|
|
|
# Toggle Anti-Spam
|
|
|
|
|
|
anti_spam = self.settings.get("anti_spam", False)
|
|
|
|
|
|
btn_antispam = ToggleButton(
|
|
|
|
|
|
label="Anti-Spam",
|
|
|
|
|
|
emoji="🛡️",
|
|
|
|
|
|
is_enabled=anti_spam,
|
|
|
|
|
|
custom_id="anti_spam"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.add_item(btn_antispam)
|
|
|
|
|
|
|
|
|
|
|
|
# Toggle Anti-Lien
|
|
|
|
|
|
anti_link = self.settings.get("anti_link", False)
|
|
|
|
|
|
btn_antilink = ToggleButton(
|
|
|
|
|
|
label="Anti-Liens",
|
|
|
|
|
|
emoji="🔗",
|
|
|
|
|
|
is_enabled=anti_link,
|
|
|
|
|
|
custom_id="anti_link"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.add_item(btn_antilink)
|
|
|
|
|
|
|
|
|
|
|
|
# Bouton de configuration du seuil
|
|
|
|
|
|
threshold = self.settings.get("spam_threshold", 5)
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_threshold = disnake.ui.Button(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label=f"🔢 Seuil: {threshold} msg/5s",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.blurple,
|
2026-01-02 19:04:05 +01:00
|
|
|
|
custom_id="spam_threshold"
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_threshold.callback = self.open_spam_threshold_modal
|
|
|
|
|
|
self.add_item(btn_threshold)
|
|
|
|
|
|
|
|
|
|
|
|
# Bouton retour
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_back = disnake.ui.Button(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label="Retour",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.gray,
|
2026-01-02 19:04:05 +01:00
|
|
|
|
emoji="⬅️"
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_back.callback = self.back_to_main
|
|
|
|
|
|
self.add_item(btn_back)
|
|
|
|
|
|
|
|
|
|
|
|
def build_raid_view(self):
|
|
|
|
|
|
"""Construit la vue Anti-Raid"""
|
|
|
|
|
|
# Toggle Anti-Raid
|
|
|
|
|
|
anti_raid = self.settings.get("anti_raid", False)
|
|
|
|
|
|
btn_antiraid = ToggleButton(
|
|
|
|
|
|
label="Anti-Raid",
|
|
|
|
|
|
emoji="⚔️",
|
|
|
|
|
|
is_enabled=anti_raid,
|
|
|
|
|
|
custom_id="anti_raid"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.add_item(btn_antiraid)
|
|
|
|
|
|
|
|
|
|
|
|
# Toggle Auto-Ban
|
|
|
|
|
|
auto_ban = self.settings.get("auto_ban", False)
|
|
|
|
|
|
btn_autoban = ToggleButton(
|
|
|
|
|
|
label="Auto-Ban",
|
|
|
|
|
|
emoji="🔨",
|
|
|
|
|
|
is_enabled=auto_ban,
|
|
|
|
|
|
custom_id="auto_ban"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.add_item(btn_autoban)
|
|
|
|
|
|
|
|
|
|
|
|
# Bouton de configuration du seuil
|
|
|
|
|
|
threshold = self.settings.get("raid_threshold", 3)
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_threshold = disnake.ui.Button(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label=f"🔢 Seuil: {threshold} join/10s",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.blurple,
|
2026-01-02 19:04:05 +01:00
|
|
|
|
custom_id="raid_threshold"
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_threshold.callback = self.open_raid_threshold_modal
|
|
|
|
|
|
self.add_item(btn_threshold)
|
|
|
|
|
|
|
|
|
|
|
|
# Bouton retour
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_back = disnake.ui.Button(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label="Retour",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.gray,
|
2026-01-02 19:04:05 +01:00
|
|
|
|
emoji="⬅️"
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_back.callback = self.back_to_main
|
|
|
|
|
|
self.add_item(btn_back)
|
|
|
|
|
|
|
|
|
|
|
|
def build_nuke_view(self):
|
|
|
|
|
|
"""Construit la vue Anti-Nuke"""
|
|
|
|
|
|
# Toggle Anti-Salon
|
|
|
|
|
|
anti_channel = self.settings.get("anti_channel_delete", False)
|
|
|
|
|
|
btn_channel = ToggleButton(
|
|
|
|
|
|
label="Anti-Salon",
|
|
|
|
|
|
emoji="📁",
|
|
|
|
|
|
is_enabled=anti_channel,
|
|
|
|
|
|
custom_id="anti_channel_delete"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.add_item(btn_channel)
|
|
|
|
|
|
|
|
|
|
|
|
# Toggle Anti-Rôle
|
|
|
|
|
|
anti_role = self.settings.get("anti_role_create", False)
|
|
|
|
|
|
btn_role = ToggleButton(
|
|
|
|
|
|
label="Anti-Rôle",
|
|
|
|
|
|
emoji="🎭",
|
|
|
|
|
|
is_enabled=anti_role,
|
|
|
|
|
|
custom_id="anti_role_create"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.add_item(btn_role)
|
|
|
|
|
|
|
|
|
|
|
|
# Bouton retour
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_back = disnake.ui.Button(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label="Retour",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.gray,
|
2026-01-02 19:04:05 +01:00
|
|
|
|
emoji="⬅️"
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_back.callback = self.back_to_main
|
|
|
|
|
|
self.add_item(btn_back)
|
|
|
|
|
|
|
|
|
|
|
|
def build_logs_view(self):
|
|
|
|
|
|
"""Construit la vue Logs"""
|
|
|
|
|
|
# Toggle Logs
|
|
|
|
|
|
logs_enabled = self.settings.get("server_logs", False)
|
|
|
|
|
|
btn_logs = ToggleButton(
|
|
|
|
|
|
label="Logs Serveur",
|
|
|
|
|
|
emoji="📋",
|
|
|
|
|
|
is_enabled=logs_enabled,
|
|
|
|
|
|
custom_id="server_logs"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.add_item(btn_logs)
|
|
|
|
|
|
|
|
|
|
|
|
# ID du channel de logs (affichage)
|
|
|
|
|
|
log_channel_id = self.settings.get("log_channel_id")
|
|
|
|
|
|
if log_channel_id:
|
|
|
|
|
|
channel = self.guild.get_channel(int(log_channel_id))
|
|
|
|
|
|
channel_mention = channel.mention if channel else f"`{log_channel_id}`"
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_channel = disnake.ui.Button(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label=f"📌 Channel: {channel.name if channel else 'Invalide'}",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.green,
|
2026-01-02 19:04:05 +01:00
|
|
|
|
disabled=True
|
|
|
|
|
|
)
|
2026-01-01 18:48:25 +01:00
|
|
|
|
else:
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_channel = disnake.ui.Button(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label="📌 Aucun channel configuré",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.red,
|
2026-01-02 19:04:05 +01:00
|
|
|
|
disabled=True
|
|
|
|
|
|
)
|
|
|
|
|
|
self.add_item(btn_channel)
|
|
|
|
|
|
|
|
|
|
|
|
# Bouton retour
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_back = disnake.ui.Button(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label="Retour",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.gray,
|
2026-01-02 19:04:05 +01:00
|
|
|
|
emoji="⬅️"
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_back.callback = self.back_to_main
|
|
|
|
|
|
self.add_item(btn_back)
|
|
|
|
|
|
|
|
|
|
|
|
def build_whitelist_view(self):
|
|
|
|
|
|
"""Construit la vue Whitelist"""
|
|
|
|
|
|
# Affichage du nombre avec style
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_count = disnake.ui.Button(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label=f"👥 Whitelist: {self.whitelist_count} utilisateur(s)",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.blurple,
|
2026-01-02 19:04:05 +01:00
|
|
|
|
disabled=True
|
|
|
|
|
|
)
|
|
|
|
|
|
self.add_item(btn_count)
|
|
|
|
|
|
|
|
|
|
|
|
# Menu dropdown pour gérer la whitelist
|
|
|
|
|
|
if self.whitelist_count > 0:
|
|
|
|
|
|
self.add_item(WhitelistSelect(self.guild, self))
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Message si whitelist vide
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_empty = disnake.ui.Button(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label="📭 Aucun utilisateur dans la whitelist",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.gray,
|
2026-01-02 19:04:05 +01:00
|
|
|
|
disabled=True
|
|
|
|
|
|
)
|
|
|
|
|
|
self.add_item(btn_empty)
|
|
|
|
|
|
|
|
|
|
|
|
# Bouton ajouter
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_add = disnake.ui.Button(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label="➕ Ajouter",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.green,
|
2026-01-02 19:04:05 +01:00
|
|
|
|
emoji="➕",
|
|
|
|
|
|
custom_id="whitelist_add"
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_add.callback = self.open_whitelist_modal
|
|
|
|
|
|
self.add_item(btn_add)
|
|
|
|
|
|
|
|
|
|
|
|
# Bouton retour
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_back = disnake.ui.Button(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label="Retour",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.gray,
|
2026-01-02 19:04:05 +01:00
|
|
|
|
emoji="⬅️"
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_back.callback = self.back_to_main
|
|
|
|
|
|
self.add_item(btn_back)
|
|
|
|
|
|
|
2026-01-03 01:15:10 +01:00
|
|
|
|
def build_rules_view(self):
|
|
|
|
|
|
"""Construit la vue Règlement"""
|
|
|
|
|
|
from commandes.modules_security.rules import (
|
|
|
|
|
|
RulesTitleModal, RulesContentModal, RulesChannelModal,
|
|
|
|
|
|
RulesRoleModal, ToggleRulesButton, save_settings
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Toggle Activer/Désactiver
|
|
|
|
|
|
rules_enabled = self.settings.get("rules_enabled", False)
|
|
|
|
|
|
btn_toggle = ToggleButton(
|
|
|
|
|
|
label="Règlement",
|
|
|
|
|
|
emoji="📜",
|
|
|
|
|
|
is_enabled=rules_enabled,
|
|
|
|
|
|
custom_id="rules_enabled"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Bouton pour modifier le titre
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_title = disnake.ui.Button(
|
2026-01-03 01:15:10 +01:00
|
|
|
|
label="Éditer le titre",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.blurple,
|
2026-01-03 01:15:10 +01:00
|
|
|
|
emoji="📝"
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_title.callback = self.open_rules_title_modal
|
|
|
|
|
|
self.add_item(btn_title)
|
|
|
|
|
|
|
|
|
|
|
|
# Bouton pour modifier le contenu
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_content = disnake.ui.Button(
|
2026-01-03 01:15:10 +01:00
|
|
|
|
label="Éditer le contenu",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.blurple,
|
2026-01-03 01:15:10 +01:00
|
|
|
|
emoji="📄"
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_content.callback = self.open_rules_content_modal
|
|
|
|
|
|
self.add_item(btn_content)
|
|
|
|
|
|
|
|
|
|
|
|
# Bouton pour choisir le salon
|
|
|
|
|
|
rules_channel_id = self.settings.get("rules_channel_id")
|
|
|
|
|
|
if rules_channel_id:
|
|
|
|
|
|
channel = self.guild.get_channel(int(rules_channel_id))
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_channel = disnake.ui.Button(
|
2026-01-03 01:15:10 +01:00
|
|
|
|
label=f"Salon: {channel.name if channel else 'Invalide'}",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.green,
|
2026-01-03 01:15:10 +01:00
|
|
|
|
custom_id="rules_channel"
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_channel = disnake.ui.Button(
|
2026-01-03 01:15:10 +01:00
|
|
|
|
label="Aucun salon configuré",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.red,
|
2026-01-03 01:15:10 +01:00
|
|
|
|
custom_id="rules_channel"
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_channel.callback = self.open_rules_channel_modal
|
|
|
|
|
|
self.add_item(btn_channel)
|
|
|
|
|
|
|
|
|
|
|
|
# Bouton pour choisir le rôle d'acceptation
|
|
|
|
|
|
rules_role_id = self.settings.get("rules_accept_role_id")
|
|
|
|
|
|
if rules_role_id:
|
|
|
|
|
|
role = self.guild.get_role(int(rules_role_id))
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_role = disnake.ui.Button(
|
2026-01-03 01:15:10 +01:00
|
|
|
|
label=f"Rôle: {role.name if role else 'Invalide'}",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.green,
|
2026-01-03 01:15:10 +01:00
|
|
|
|
custom_id="rules_role"
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_role = disnake.ui.Button(
|
2026-01-03 01:15:10 +01:00
|
|
|
|
label="Aucun rôle configuré",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.red,
|
2026-01-03 01:15:10 +01:00
|
|
|
|
custom_id="rules_role"
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_role.callback = self.open_rules_role_modal
|
|
|
|
|
|
self.add_item(btn_role)
|
|
|
|
|
|
|
|
|
|
|
|
# Bouton pour choisir le type de message
|
|
|
|
|
|
message_type = self.settings.get("rules_message_type", "embed")
|
|
|
|
|
|
type_label = "Embed" if message_type == "embed" else "Message simple"
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_message_type = disnake.ui.Button(
|
2026-01-03 01:15:10 +01:00
|
|
|
|
label=f"Type: {type_label}",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.blurple,
|
2026-01-03 01:15:10 +01:00
|
|
|
|
emoji="📝",
|
|
|
|
|
|
custom_id="rules_message_type"
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_message_type.callback = self.open_rules_message_type_modal
|
|
|
|
|
|
self.add_item(btn_message_type)
|
|
|
|
|
|
|
|
|
|
|
|
# Bouton pour envoyer le règlement
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_send = disnake.ui.Button(
|
2026-01-03 01:15:10 +01:00
|
|
|
|
label="Envoyer le règlement",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.green,
|
2026-01-03 01:15:10 +01:00
|
|
|
|
emoji="🚀"
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_send.callback = self.send_rules
|
|
|
|
|
|
self.add_item(btn_send)
|
|
|
|
|
|
|
|
|
|
|
|
# Bouton retour
|
2026-05-06 17:07:09 +02:00
|
|
|
|
btn_back = disnake.ui.Button(
|
2026-01-03 01:15:10 +01:00
|
|
|
|
label="Retour",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.gray,
|
2026-01-03 01:15:10 +01:00
|
|
|
|
emoji="⬅️"
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_back.callback = self.back_to_main
|
|
|
|
|
|
self.add_item(btn_back)
|
|
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
async def toggle_setting(self, interaction: disnake.Interaction, key: str):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Bascule un paramètre"""
|
2026-01-01 18:48:25 +01:00
|
|
|
|
self.settings[key] = not self.settings.get(key, False)
|
|
|
|
|
|
save_settings(self.guild.id, self.settings)
|
|
|
|
|
|
self.build_view()
|
|
|
|
|
|
await interaction.response.edit_message(view=self)
|
|
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
async def open_spam_threshold_modal(self, interaction: disnake.Interaction):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Ouvre le modal de configuration du seuil spam"""
|
|
|
|
|
|
await interaction.response.send_modal(
|
|
|
|
|
|
SpamThresholdModal(self.guild.id, self.settings, self)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
async def open_raid_threshold_modal(self, interaction: disnake.Interaction):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Ouvre le modal de configuration du seuil raid"""
|
|
|
|
|
|
await interaction.response.send_modal(
|
|
|
|
|
|
RaidThresholdModal(self.guild.id, self.settings, self)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
async def open_whitelist_modal(self, interaction: disnake.Interaction):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Ouvre le modal d'ajout à la whitelist"""
|
|
|
|
|
|
await interaction.response.send_modal(
|
|
|
|
|
|
WhitelistModal(self.guild.id, self)
|
|
|
|
|
|
)
|
2026-01-01 18:48:25 +01:00
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
async def open_rules_title_modal(self, interaction: disnake.Interaction):
|
2026-01-03 01:15:10 +01:00
|
|
|
|
"""Ouvre le modal de configuration du titre du règlement"""
|
|
|
|
|
|
from .modules_security.rules import RulesTitleModal
|
|
|
|
|
|
await interaction.response.send_modal(
|
|
|
|
|
|
RulesTitleModal(self.guild.id, self.settings)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
async def open_rules_content_modal(self, interaction: disnake.Interaction):
|
2026-01-03 01:15:10 +01:00
|
|
|
|
"""Ouvre le modal de configuration du contenu du règlement"""
|
|
|
|
|
|
from .modules_security.rules import RulesContentModal
|
|
|
|
|
|
await interaction.response.send_modal(
|
|
|
|
|
|
RulesContentModal(self.guild.id, self.settings)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
async def open_rules_channel_modal(self, interaction: disnake.Interaction):
|
2026-01-03 01:15:10 +01:00
|
|
|
|
"""Ouvre le modal de sélection du salon du règlement"""
|
|
|
|
|
|
from .modules_security.rules import RulesChannelModal
|
|
|
|
|
|
await interaction.response.send_modal(
|
|
|
|
|
|
RulesChannelModal(self.guild.id, self.settings)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
async def open_rules_role_modal(self, interaction: disnake.Interaction):
|
2026-01-03 01:15:10 +01:00
|
|
|
|
"""Ouvre le modal de configuration du rôle d'acceptation"""
|
|
|
|
|
|
from .modules_security.rules import RulesRoleModal
|
|
|
|
|
|
await interaction.response.send_modal(
|
|
|
|
|
|
RulesRoleModal(self.guild.id, self.settings)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
async def open_rules_message_type_modal(self, interaction: disnake.Interaction):
|
2026-01-03 01:15:10 +01:00
|
|
|
|
"""Ouvre le modal de sélection du type de message"""
|
|
|
|
|
|
from .modules_security.rules import RulesMessageTypeModal
|
|
|
|
|
|
await interaction.response.send_modal(
|
|
|
|
|
|
RulesMessageTypeModal(self.guild.id, self.settings)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
async def send_rules(self, interaction: disnake.Interaction):
|
2026-01-03 01:15:10 +01:00
|
|
|
|
"""Envoie le règlement dans le salon configuré"""
|
|
|
|
|
|
from commandes.modules_security.rules import RulesCog
|
|
|
|
|
|
|
|
|
|
|
|
# Vérifier que tous les paramètres sont configurés
|
|
|
|
|
|
rules_channel_id = self.settings.get("rules_channel_id")
|
|
|
|
|
|
rules_accept_role_id = self.settings.get("rules_accept_role_id")
|
|
|
|
|
|
|
|
|
|
|
|
if not rules_channel_id:
|
|
|
|
|
|
return await interaction.response.send_message(
|
|
|
|
|
|
"❌ Aucun salon configuré pour l'envoi du règlement.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if not rules_accept_role_id:
|
|
|
|
|
|
return await interaction.response.send_message(
|
|
|
|
|
|
"❌ Aucun rôle d'acceptation configuré.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Vérifier le salon
|
|
|
|
|
|
channel = self.guild.get_channel(int(rules_channel_id))
|
|
|
|
|
|
if not channel:
|
|
|
|
|
|
return await interaction.response.send_message(
|
|
|
|
|
|
"❌ Le salon configuré n'existe plus.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Vérifier le rôle
|
|
|
|
|
|
role = self.guild.get_role(int(rules_accept_role_id))
|
|
|
|
|
|
if not role:
|
|
|
|
|
|
return await interaction.response.send_message(
|
|
|
|
|
|
"❌ Le rôle d'acceptation n'existe plus.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Envoyer le règlement
|
|
|
|
|
|
try:
|
|
|
|
|
|
rules_cog = self.bot.get_cog("RulesCog")
|
|
|
|
|
|
if rules_cog:
|
|
|
|
|
|
message = await rules_cog.send_rules_message(channel, self.settings)
|
|
|
|
|
|
if message:
|
|
|
|
|
|
# Activer le règlement
|
|
|
|
|
|
self.settings["rules_enabled"] = True
|
|
|
|
|
|
save_settings(self.guild.id, self.settings)
|
|
|
|
|
|
self.settings = load_settings(self.guild.id)
|
|
|
|
|
|
self.build_view()
|
|
|
|
|
|
|
|
|
|
|
|
await interaction.response.edit_message(view=self)
|
|
|
|
|
|
await interaction.followup.send(
|
|
|
|
|
|
f"✅ Règlement envoyé avec succès dans {channel.mention} !",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
await interaction.followup.send(
|
|
|
|
|
|
"❌ Erreur lors de l'envoi du règlement.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
await interaction.followup.send(
|
|
|
|
|
|
"❌ Le cog Rules n'est pas chargé.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
await interaction.followup.send(
|
|
|
|
|
|
f"❌ Erreur: {e}",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-05 17:42:07 +02:00
|
|
|
|
def build_members_view(self):
|
|
|
|
|
|
"""Construit la vue Paramètres Membres"""
|
|
|
|
|
|
# Toggle Restauration des rôles
|
|
|
|
|
|
role_restore = self.settings.get("role_restore_enabled", True)
|
|
|
|
|
|
btn_restore = ToggleButton(
|
|
|
|
|
|
label="Restauration des rôles",
|
|
|
|
|
|
emoji="🔄",
|
|
|
|
|
|
is_enabled=role_restore,
|
|
|
|
|
|
custom_id="role_restore_enabled"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.add_item(btn_restore)
|
|
|
|
|
|
|
|
|
|
|
|
# Bouton retour
|
|
|
|
|
|
btn_back = disnake.ui.Button(
|
|
|
|
|
|
label="Retour",
|
|
|
|
|
|
style=disnake.ButtonStyle.gray,
|
|
|
|
|
|
emoji="⬅️"
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_back.callback = self.back_to_main
|
|
|
|
|
|
self.add_item(btn_back)
|
|
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
async def back_to_main(self, interaction: disnake.Interaction):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Retour au menu principal"""
|
2026-01-01 18:48:25 +01:00
|
|
|
|
self.category = None
|
|
|
|
|
|
self.build_view()
|
2026-01-02 19:04:05 +01:00
|
|
|
|
embed = self.create_main_embed(interaction.user)
|
2026-01-01 18:48:25 +01:00
|
|
|
|
await interaction.response.edit_message(embed=embed, view=self)
|
|
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
def create_main_embed(self, user: disnake.Member = None) -> disnake.Embed:
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Crée l'embed du menu principal"""
|
2026-05-06 17:07:09 +02:00
|
|
|
|
embed = disnake.Embed(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
title="🛡️ Centre de Sécurité Kuby",
|
|
|
|
|
|
description="Gérez la protection de votre serveur",
|
2026-01-03 01:15:10 +01:00
|
|
|
|
color=0x7289DA
|
2026-01-02 19:04:05 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if self.guild.icon:
|
|
|
|
|
|
embed.set_thumbnail(url=self.guild.icon.url)
|
|
|
|
|
|
|
|
|
|
|
|
# Informations du serveur
|
|
|
|
|
|
embed.add_field(
|
|
|
|
|
|
name="🏠 Serveur",
|
|
|
|
|
|
value=self.guild.name,
|
|
|
|
|
|
inline=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
embed.add_field(
|
|
|
|
|
|
name="👤 Propriétaire",
|
|
|
|
|
|
value=self.guild.owner.mention if self.guild.owner else "Inconnu",
|
|
|
|
|
|
inline=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Statut global
|
|
|
|
|
|
active_count = sum([
|
|
|
|
|
|
self.settings.get("anti_spam", False),
|
|
|
|
|
|
self.settings.get("anti_link", False),
|
|
|
|
|
|
self.settings.get("anti_raid", False),
|
|
|
|
|
|
self.settings.get("auto_ban", False),
|
|
|
|
|
|
self.settings.get("anti_channel_delete", False),
|
|
|
|
|
|
self.settings.get("anti_role_create", False),
|
|
|
|
|
|
self.settings.get("server_logs", False)
|
|
|
|
|
|
])
|
|
|
|
|
|
total_options = 7
|
|
|
|
|
|
status = "🟢 Actif" if active_count > 0 else "🔴 Inactif"
|
|
|
|
|
|
|
|
|
|
|
|
embed.add_field(
|
|
|
|
|
|
name="⚡ Protection",
|
|
|
|
|
|
value=f"{status} ({active_count}/{total_options})",
|
|
|
|
|
|
inline=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Avertissement si pas admin
|
|
|
|
|
|
if user and not is_immune(self.guild, user):
|
|
|
|
|
|
embed.set_footer(text="⚠️ Vous n'êtes pas administrateur. Accès restreint.")
|
|
|
|
|
|
|
|
|
|
|
|
return embed
|
|
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
def create_category_embed(self, title: str, description: str) -> disnake.Embed:
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Crée un embed pour une catégorie"""
|
2026-05-06 17:07:09 +02:00
|
|
|
|
embed = disnake.Embed(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
title=title,
|
|
|
|
|
|
description=description,
|
2026-01-03 01:15:10 +01:00
|
|
|
|
color=0x7289DA
|
2026-01-02 19:04:05 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if self.guild.icon:
|
|
|
|
|
|
embed.set_thumbnail(url=self.guild.icon.url)
|
|
|
|
|
|
|
2026-01-01 18:48:25 +01:00
|
|
|
|
return embed
|
|
|
|
|
|
|
2026-01-02 19:04:05 +01:00
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
class ToggleButton(disnake.ui.Button):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Bouton de bascule avec indicateur de statut"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, label: str, emoji: str, is_enabled: bool, custom_id: str):
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style = disnake.ButtonStyle.green if is_enabled else disnake.ButtonStyle.red
|
2026-01-02 19:04:05 +01:00
|
|
|
|
status = "✅" if is_enabled else "❌"
|
|
|
|
|
|
super().__init__(
|
|
|
|
|
|
label=f"{label} [{status}]",
|
|
|
|
|
|
style=style,
|
|
|
|
|
|
custom_id=custom_id,
|
|
|
|
|
|
emoji=emoji
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
async def callback(self, interaction: disnake.Interaction):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
view: SecurityView = self.view
|
|
|
|
|
|
key = interaction.data['custom_id']
|
|
|
|
|
|
|
|
|
|
|
|
# Vérifier les permissions
|
|
|
|
|
|
if not is_immune(view.guild, interaction.user):
|
|
|
|
|
|
return await interaction.response.send_message(
|
|
|
|
|
|
"❌ Permissions insuffisantes.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Basculer le paramètre
|
|
|
|
|
|
view.settings[key] = not view.settings.get(key, False)
|
|
|
|
|
|
save_settings(view.guild.id, view.settings)
|
|
|
|
|
|
view.build_view()
|
|
|
|
|
|
await interaction.response.edit_message(view=view)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
class MainCategorySelect(disnake.ui.Select):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Menu de sélection des catégories"""
|
|
|
|
|
|
|
2026-01-01 18:48:25 +01:00
|
|
|
|
def __init__(self):
|
|
|
|
|
|
options = [
|
2026-05-06 17:07:09 +02:00
|
|
|
|
disnake.SelectOption(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label="Anti-Spam",
|
|
|
|
|
|
emoji="🛡️",
|
|
|
|
|
|
value="spam",
|
|
|
|
|
|
description="Protection contre le spam et les liens"
|
|
|
|
|
|
),
|
2026-05-06 17:07:09 +02:00
|
|
|
|
disnake.SelectOption(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label="Anti-Raid",
|
|
|
|
|
|
emoji="⚔️",
|
|
|
|
|
|
value="raid",
|
|
|
|
|
|
description="Protection contre les raids"
|
|
|
|
|
|
),
|
2026-05-06 17:07:09 +02:00
|
|
|
|
disnake.SelectOption(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label="Anti-Nuke",
|
|
|
|
|
|
emoji="☢️",
|
|
|
|
|
|
value="nuke",
|
|
|
|
|
|
description="Protection contre la destruction du serveur"
|
|
|
|
|
|
),
|
2026-05-06 17:07:09 +02:00
|
|
|
|
disnake.SelectOption(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label="Logs",
|
|
|
|
|
|
emoji="📋",
|
|
|
|
|
|
value="logs",
|
|
|
|
|
|
description="Journalisation des événements"
|
|
|
|
|
|
),
|
2026-05-06 17:07:09 +02:00
|
|
|
|
disnake.SelectOption(
|
2026-01-03 01:15:10 +01:00
|
|
|
|
label="Règlement",
|
|
|
|
|
|
emoji="📜",
|
|
|
|
|
|
value="rules",
|
|
|
|
|
|
description="Créer et envoyer un règlement"
|
|
|
|
|
|
),
|
2026-05-06 17:07:09 +02:00
|
|
|
|
disnake.SelectOption(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label="Whitelist",
|
|
|
|
|
|
emoji="👥",
|
|
|
|
|
|
value="whitelist",
|
|
|
|
|
|
description="Gérer les utilisateurs immunisés"
|
2026-06-05 17:42:07 +02:00
|
|
|
|
),
|
|
|
|
|
|
disnake.SelectOption(
|
|
|
|
|
|
label="Membres",
|
|
|
|
|
|
emoji="👤",
|
|
|
|
|
|
value="membres",
|
|
|
|
|
|
description="Paramètres liés aux membres"
|
2026-01-02 19:04:05 +01:00
|
|
|
|
)
|
2026-01-01 18:48:25 +01:00
|
|
|
|
]
|
2026-01-02 19:04:05 +01:00
|
|
|
|
super().__init__(
|
|
|
|
|
|
placeholder="Sélectionnez une catégorie...",
|
|
|
|
|
|
options=options,
|
|
|
|
|
|
min_values=1,
|
|
|
|
|
|
max_values=1
|
|
|
|
|
|
)
|
2026-01-01 18:48:25 +01:00
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
async def callback(self, interaction: disnake.Interaction):
|
2026-01-01 18:48:25 +01:00
|
|
|
|
view: SecurityView = self.view
|
|
|
|
|
|
view.category = self.values[0]
|
|
|
|
|
|
view.build_view()
|
2026-01-02 19:04:05 +01:00
|
|
|
|
|
|
|
|
|
|
# Créer l'embed de la catégorie
|
|
|
|
|
|
category_embeds = {
|
|
|
|
|
|
"spam": view.create_category_embed(
|
|
|
|
|
|
"Configuration Anti-Spam",
|
|
|
|
|
|
"Paramètres de protection contre le spam et les liens.\n\n"
|
|
|
|
|
|
"• **Anti-Spam** : Détecte les messages répétitifs\n"
|
|
|
|
|
|
"• **Anti-Liens** : Bloque les liens Discord et URLs"
|
|
|
|
|
|
),
|
|
|
|
|
|
"raid": view.create_category_embed(
|
|
|
|
|
|
"Configuration Anti-Raid",
|
|
|
|
|
|
"Paramètres de protection contre les raids.\n\n"
|
|
|
|
|
|
"• **Anti-Raid** : Détecte les vagues de join\n"
|
|
|
|
|
|
"• **Auto-Ban** : Banne automatiquement les raiders"
|
|
|
|
|
|
),
|
|
|
|
|
|
"nuke": view.create_category_embed(
|
|
|
|
|
|
"Configuration Anti-Nuke",
|
|
|
|
|
|
"Paramètres de protection contre la destruction.\n\n"
|
|
|
|
|
|
"• **Anti-Salon** : Protège les salons\n"
|
|
|
|
|
|
"• **Anti-Rôle** : Protège les rôles"
|
|
|
|
|
|
),
|
|
|
|
|
|
"logs": view.create_category_embed(
|
|
|
|
|
|
"Configuration Logs",
|
|
|
|
|
|
"Paramètres de journalisation des événements.\n\n"
|
|
|
|
|
|
"• **Logs Serveur** : Active la journalisation\n"
|
|
|
|
|
|
"• **Channel** : Salon où seront envoyés les logs"
|
|
|
|
|
|
),
|
2026-01-03 01:15:10 +01:00
|
|
|
|
"rules": view.create_category_embed(
|
|
|
|
|
|
"Configuration Règlement",
|
|
|
|
|
|
"Créez et envoyez un règlement pour votre serveur.\n\n"
|
|
|
|
|
|
"• **Titre** : Personnalisez le titre du règlement\n"
|
|
|
|
|
|
"• **Contenu** : Rédigez vos règles\n"
|
|
|
|
|
|
"• **Salon** : Choisissez où l'envoyer\n"
|
|
|
|
|
|
"• **Rôle** : Rôle attribué à l'acceptation\n\n"
|
|
|
|
|
|
"Les membres devront accepter le règlement pour accéder au serveur."
|
|
|
|
|
|
),
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"whitelist": view.create_category_embed(
|
|
|
|
|
|
"Gestion Whitelist",
|
|
|
|
|
|
"Gérez les utilisateurs immunisés aux protections.\n\n"
|
|
|
|
|
|
"Ces utilisateurs ne seront jamais sanctionnés."
|
2026-06-05 17:42:07 +02:00
|
|
|
|
),
|
|
|
|
|
|
"membres": view.create_category_embed(
|
|
|
|
|
|
"Paramètres Membres",
|
|
|
|
|
|
"Paramètres liés à la gestion des membres.\n\n"
|
|
|
|
|
|
"• **Restauration des rôles** : Restaurer automatiquement les rôles des membres de retour"
|
2026-01-02 19:04:05 +01:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
embed = category_embeds.get(view.category, view.create_main_embed())
|
2026-01-01 18:48:25 +01:00
|
|
|
|
await interaction.response.edit_message(embed=embed, view=view)
|
|
|
|
|
|
|
2026-01-02 19:04:05 +01:00
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
class WhitelistSelect(disnake.ui.Select):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Menu dropdown pour voir et gérer les utilisateurs whitelistés"""
|
|
|
|
|
|
|
2026-05-09 18:42:42 +02:00
|
|
|
|
def __init__(self, guild: disnake.Guild, view: 'SecurityView'):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
self.guild = guild
|
|
|
|
|
|
self.parent_view = view
|
|
|
|
|
|
whitelist = load_whitelist(guild.id)
|
|
|
|
|
|
|
|
|
|
|
|
options = []
|
|
|
|
|
|
for user_id in whitelist:
|
|
|
|
|
|
# Essayer de trouver l'utilisateur
|
|
|
|
|
|
member = guild.get_member(user_id)
|
|
|
|
|
|
if member:
|
|
|
|
|
|
label = f"{member.name} ({member.id})"
|
|
|
|
|
|
emoji = "👤"
|
|
|
|
|
|
else:
|
|
|
|
|
|
label = f"ID: {user_id} (non trouvé)"
|
|
|
|
|
|
emoji = "❓"
|
|
|
|
|
|
options.append(
|
2026-05-06 17:07:09 +02:00
|
|
|
|
disnake.SelectOption(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
label=label[:100], # Discord limite à 100 chars
|
|
|
|
|
|
value=str(user_id),
|
|
|
|
|
|
emoji=emoji,
|
|
|
|
|
|
description="Cliquez pour retirer de la whitelist"
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
super().__init__(
|
|
|
|
|
|
placeholder="👥 Gérer la whitelist (cliquez pour retirer)...",
|
|
|
|
|
|
options=options,
|
|
|
|
|
|
min_values=1,
|
|
|
|
|
|
max_values=1
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
async def callback(self, interaction: disnake.Interaction):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Gère la sélection d'un utilisateur à retirer"""
|
|
|
|
|
|
user_id = int(self.values[0])
|
|
|
|
|
|
|
|
|
|
|
|
# Charger et modifier la whitelist
|
|
|
|
|
|
data = {}
|
|
|
|
|
|
if os.path.exists(WHITELIST_FILE):
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(WHITELIST_FILE, "r", encoding='utf-8') as f:
|
|
|
|
|
|
data = json.load(f)
|
|
|
|
|
|
except (json.JSONDecodeError, FileNotFoundError):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
guild_whitelist = data.get(str(self.guild.id), [])
|
|
|
|
|
|
|
|
|
|
|
|
if user_id not in guild_whitelist:
|
|
|
|
|
|
return await interaction.response.send_message(
|
|
|
|
|
|
"❌ Utilisateur non trouvé dans la whitelist.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Retirer l'utilisateur
|
|
|
|
|
|
guild_whitelist.remove(user_id)
|
|
|
|
|
|
data[str(self.guild.id)] = guild_whitelist
|
|
|
|
|
|
|
|
|
|
|
|
os.makedirs(os.path.dirname(WHITELIST_FILE), exist_ok=True)
|
|
|
|
|
|
with open(WHITELIST_FILE, "w", encoding='utf-8') as f:
|
|
|
|
|
|
json.dump(data, f, indent=4, ensure_ascii=False)
|
|
|
|
|
|
|
|
|
|
|
|
# Mettre à jour l'affichage
|
|
|
|
|
|
self.parent_view.whitelist_count = len(guild_whitelist)
|
|
|
|
|
|
self.parent_view.build_view()
|
|
|
|
|
|
|
|
|
|
|
|
await interaction.response.edit_message(view=self.parent_view)
|
|
|
|
|
|
await interaction.followup.send(
|
|
|
|
|
|
"✅ Utilisateur retiré de la whitelist.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-01 18:48:25 +01:00
|
|
|
|
class Security(commands.Cog):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Cog principal de gestion de la sécurité"""
|
|
|
|
|
|
|
2026-01-01 18:48:25 +01:00
|
|
|
|
def __init__(self, bot):
|
|
|
|
|
|
self.bot = bot
|
|
|
|
|
|
|
2026-05-09 18:42:42 +02:00
|
|
|
|
@commands.slash_command(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
name="security",
|
|
|
|
|
|
description="🛡️ Ouvrir le panneau de sécurité Kuby"
|
|
|
|
|
|
)
|
2026-05-09 18:42:42 +02:00
|
|
|
|
async def security(self, interaction: disnake.ApplicationCommandInteraction):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""
|
|
|
|
|
|
Commande principale pour ouvrir l'interface de sécurité.
|
|
|
|
|
|
Accessible uniquement aux administrateurs et propriétaires whitelistés.
|
|
|
|
|
|
"""
|
2026-03-29 19:29:17 +02:00
|
|
|
|
# On diffère la réponse immédiatement car le chargement des réglages peut prendre du temps
|
|
|
|
|
|
await interaction.response.defer(ephemeral=True)
|
|
|
|
|
|
|
2026-01-02 19:04:05 +01:00
|
|
|
|
# Vérification des permissions
|
|
|
|
|
|
if not is_immune(interaction.guild, interaction.user):
|
2026-03-29 19:29:17 +02:00
|
|
|
|
return await interaction.followup.send(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"❌ **Accès refusé**\n\nVous devez être administrateur, propriétaire du serveur, ou être dans la whitelist pour accéder à ce panneau.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
2026-01-01 18:48:25 +01:00
|
|
|
|
|
2026-03-29 19:29:17 +02:00
|
|
|
|
try:
|
|
|
|
|
|
# Chargement des paramètres
|
|
|
|
|
|
settings = load_settings(interaction.guild.id)
|
2026-01-02 19:04:05 +01:00
|
|
|
|
|
2026-03-29 19:29:17 +02:00
|
|
|
|
# Création de la vue
|
|
|
|
|
|
view = SecurityView(interaction, settings, bot=self.bot)
|
|
|
|
|
|
embed = view.create_main_embed(interaction.user)
|
2026-01-02 19:04:05 +01:00
|
|
|
|
|
|
|
|
|
|
await interaction.followup.send(
|
|
|
|
|
|
embed=embed,
|
|
|
|
|
|
view=view,
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
2026-03-29 19:29:17 +02:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
await interaction.followup.send(
|
|
|
|
|
|
f"❌ Une erreur est survenue lors de l'ouverture du panneau : {e}",
|
2026-01-02 19:04:05 +01:00
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@security.error
|
2026-05-09 18:42:42 +02:00
|
|
|
|
async def security_error(self, interaction: disnake.Interaction, error: Exception):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Gestion des erreurs de la commande security"""
|
2026-03-29 19:29:17 +02:00
|
|
|
|
# Utilisation de followup si l'interaction est déjà différée ou répondue
|
|
|
|
|
|
send_method = interaction.followup.send if interaction.response.is_done() else interaction.response.send_message
|
2026-01-02 19:04:05 +01:00
|
|
|
|
|
2026-05-09 18:42:42 +02:00
|
|
|
|
if isinstance(error, commands.MissingPermissions):
|
2026-03-29 19:29:17 +02:00
|
|
|
|
await send_method(
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"❌ Permissions insuffisantes.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
2026-03-29 19:29:17 +02:00
|
|
|
|
await send_method(
|
|
|
|
|
|
f"❌ Une erreur est survenue lors de l'exécution de la commande : {error}",
|
2026-01-02 19:04:05 +01:00
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-01 18:48:25 +01:00
|
|
|
|
|
2026-05-09 18:42:42 +02:00
|
|
|
|
def setup(bot):
|
2026-01-02 19:04:05 +01:00
|
|
|
|
"""Setup du cog de sécurité"""
|
2026-05-09 18:42:42 +02:00
|
|
|
|
bot.add_cog(Security(bot))
|
2026-01-02 19:04:05 +01:00
|
|
|
|
|