import disnake from disnake.ext import commands import sqlite3 import os from datetime import datetime, timedelta import re class ModReasonModal(disnake.ui.Modal): def __init__(self, cog, action_type, member): self.cog = cog self.action_type = action_type self.member = member self.reason_input = disnake.ui.TextInput( label="Raison", placeholder="Entrez la raison ici...", required=True, max_length=500, custom_id="reason" ) components = [self.reason_input] if action_type == "TIMEOUT": self.duration_input = disnake.ui.TextInput( label="Durée (ex: 1h, 30m, 1d)", placeholder="1h", required=True, max_length=10, custom_id="duration" ) components.append(self.duration_input) super().__init__(title=f"Raison pour {action_type}", components=components) async def callback(self, interaction: disnake.ModalInteraction): reason = interaction.text_values["reason"] member = self.member if self.action_type == "WARN": await self.cog.warn(interaction, member, reason) elif self.action_type == "TIMEOUT": duration = interaction.text_values["duration"] await self.cog.timeout(interaction, member, duration, reason) elif self.action_type == "KICK": await self.cog.kick(interaction, member, reason) elif self.action_type == "BAN": await self.cog.ban(interaction, member, reason) await self.cog.send_modpanel_v2(interaction, member, edit=True) class ModConfigModal(disnake.ui.Modal): def __init__(self, field_name, current_value): self.field_name = field_name self.input_field = disnake.ui.TextInput( label=field_name, value=str(current_value), placeholder="Entrez une valeur numérique...", required=True, custom_id="value" ) super().__init__(title=f"Modifier {field_name}", components=[self.input_field]) async def callback(self, interaction: disnake.ModalInteraction): value = interaction.text_values["value"] cog = interaction.bot.get_cog("Moderation") mapping = { "Limite Timeout": "warn_limit_timeout", "Limite Kick": "warn_limit_kick", "Limite Ban": "warn_limit_ban", "Durée Timeout (s)": "timeout_duration" } if not value.isdigit(): return await interaction.response.send_message("❌ Veuillez entrer un nombre valide.", ephemeral=True) db_field = mapping.get(self.field_name) conn = sqlite3.connect(cog.db_path) cursor = conn.cursor() cursor.execute(f"UPDATE mod_config SET {db_field} = ? WHERE guild_id = ?", (int(value), interaction.guild_id)) conn.commit() conn.close() await interaction.response.send_message(f"✅ {self.field_name} mis à jour.", ephemeral=True) await cog.send_modconfig_v2(interaction, edit=True) class Moderation(commands.Cog): def __init__(self, bot): self.bot = bot self.db_path = os.path.join(os.path.dirname(__file__), "..", "config.db") self._init_db() def _init_db(self): conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute('''CREATE TABLE IF NOT EXISTS sanctions (id INTEGER PRIMARY KEY AUTOINCREMENT, guild_id INTEGER, user_id INTEGER, moderator_id INTEGER, type TEXT, reason TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, duration INTEGER, status TEXT DEFAULT 'ACTIVE')''') cursor.execute('''CREATE TABLE IF NOT EXISTS mod_config ( guild_id INTEGER PRIMARY KEY, log_channel_id INTEGER, board_channel_id INTEGER, warn_limit_timeout INTEGER DEFAULT 3, warn_limit_kick INTEGER DEFAULT 5, warn_limit_ban INTEGER DEFAULT 10, timeout_duration INTEGER DEFAULT 3600 )''') try: cursor.execute("ALTER TABLE mod_config ADD COLUMN board_channel_id INTEGER") except: pass conn.commit(); conn.close() def parse_duration(self, duration_str: str) -> int: if not duration_str: return 0 units = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400, 'w': 604800} total_seconds = 0 matches = re.findall(r'(\d+)([smhdw])', duration_str.lower()) for value, unit in matches: total_seconds += int(value) * units[unit] return total_seconds def _type_to_emoji(self, s_type: str) -> str: return {"WARN": "⚠️", "TIMEOUT": "⏳", "KICK": "👢", "BAN": "🔨"}.get(s_type, "🛡️") def build_sanction_board_components(self, *, s_type: str, user, moderator, reason, timestamp, s_id, duration=None) -> list: ts_int = int(datetime.fromisoformat(str(timestamp)).timestamp()) if not isinstance(timestamp, datetime) else int(timestamp.timestamp()) emoji = self._type_to_emoji(s_type) children = [ disnake.ui.Section(f"{emoji} **SANCTION : {s_type}**", accessory=disnake.ui.Thumbnail(user.display_avatar.url)), disnake.ui.Separator(divider=True), disnake.ui.TextDisplay(f"- **Membre sanctionné** : {user.mention}"), disnake.ui.TextDisplay(f"- **Modérateur** : {moderator.mention}"), disnake.ui.TextDisplay(f"- **Raison** : “ *{reason if reason else 'Aucune raison spécifiée'}* ”"), disnake.ui.TextDisplay(f"- **Date** : ") ] if s_type == "TIMEOUT" and duration: children.append(disnake.ui.TextDisplay(f"- **Durée** : `{duration}s`")) children.extend([ disnake.ui.Separator(divider=True), disnake.ui.TextDisplay(f"🆔 **ID** : `{s_id}`") ]) return [disnake.ui.Container(*children)] async def send_mod_log(self, guild, components): conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('SELECT log_channel_id FROM mod_config WHERE guild_id = ?', (guild.id,)) res = cursor.fetchone(); conn.close() if res and res[0]: chan = guild.get_channel(res[0]) if chan: await chan.send(components=components) async def send_sanction_board(self, guild, components): conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('SELECT board_channel_id FROM mod_config WHERE guild_id = ?', (guild.id,)) res = cursor.fetchone(); conn.close() if res and res[0]: chan = guild.get_channel(res[0]) if chan: await chan.send(components=components) async def send_modpanel_v2(self, interaction: disnake.Interaction, member: disnake.Member, edit=False): conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('SELECT COUNT(*) FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "WARN" AND status = "ACTIVE"', (interaction.guild_id, member.id)) warn_count = cursor.fetchone()[0] cursor.execute('SELECT type, reason, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND status = "ACTIVE" ORDER BY timestamp DESC LIMIT 3', (interaction.guild_id, member.id)) rows = cursor.fetchall(); conn.close() children = [ disnake.ui.Section(f"🛡️ Panel : {member.name}", accessory=disnake.ui.Thumbnail(member.display_avatar.url)), disnake.ui.Separator(divider=True), disnake.ui.TextDisplay(f"👤 **Membre :** {member.mention} (`{member.id}`)"), disnake.ui.TextDisplay(f"⚠️ **Avertissements actifs :** {warn_count}"), disnake.ui.Separator(divider=True), disnake.ui.Section("⚠️ Infliger un avertissement", accessory=disnake.ui.Button(label="Warn", style=disnake.ButtonStyle.secondary, custom_id=f"modpan_warn:{member.id}")), disnake.ui.Section("⏳ Mettre en sourdine", accessory=disnake.ui.Button(label="Timeout", style=disnake.ButtonStyle.secondary, custom_id=f"modpan_timeout:{member.id}")), disnake.ui.Section("👢 Expulser du serveur", accessory=disnake.ui.Button(label="Kick", style=disnake.ButtonStyle.secondary, custom_id=f"modpan_kick:{member.id}")), disnake.ui.Section("🔨 Bannir définitivement", accessory=disnake.ui.Button(label="Ban", style=disnake.ButtonStyle.danger, custom_id=f"modpan_ban:{member.id}")), disnake.ui.Separator(divider=True), disnake.ui.Section("🧹 Réinitialiser les warns", accessory=disnake.ui.Button(label="Clear", style=disnake.ButtonStyle.danger, custom_id=f"modpan_clear:{member.id}")) ] if rows: children.append(disnake.ui.Separator(divider=True)) children.append(disnake.ui.TextDisplay("📌 **Dernières sanctions :**")) for t, r, ts in rows: ts_int = int(datetime.fromisoformat(str(ts)).timestamp()) if not isinstance(ts, datetime) else int(ts.timestamp()) children.append(disnake.ui.TextDisplay(f"• {self._type_to_emoji(t)} **{t}** — {r} ()")) components = [disnake.ui.Container(*children)] if edit: if not interaction.response.is_done(): await interaction.response.edit_message(content=None, components=components) else: await interaction.edit_original_response(content=None, components=components) else: await interaction.response.send_message(components=components, ephemeral=True) @commands.slash_command(name="modpanel", description="Ouvre le panel de modération") @commands.has_permissions(moderate_members=True) async def modpanel(self, interaction: disnake.ApplicationCommandInteraction, member: disnake.Member): if member.top_role >= interaction.user.top_role and interaction.user.id != interaction.guild.owner_id: return await interaction.response.send_message("❌ Permissions insuffisantes.", ephemeral=True) await self.send_modpanel_v2(interaction, member) async def send_modconfig_v2(self, interaction: disnake.Interaction, edit=False): conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('SELECT log_channel_id, board_channel_id, warn_limit_timeout, warn_limit_kick, warn_limit_ban, timeout_duration FROM mod_config WHERE guild_id = ?', (interaction.guild_id,)) res = cursor.fetchone(); conn.close() if not res: return children = [ disnake.ui.Section("⚙️ Configuration Modération", accessory=disnake.ui.Thumbnail(interaction.guild.icon.url if interaction.guild.icon else None)), disnake.ui.Separator(divider=True), disnake.ui.TextDisplay(f"📁 **Logs internes :** <#{res[0]}>" if res[0] else "📁 **Logs :** Non défini"), disnake.ui.TextDisplay(f"📢 **Affichage public :** <#{res[1]}>" if res[1] else "📢 **Affichage :** Non défini"), disnake.ui.TextDisplay(f"⚖️ **Paliers :** T:{res[2]} | K:{res[3]} | B:{res[4]}"), disnake.ui.TextDisplay(f"⏱️ **Timeout auto :** {res[5]}s"), disnake.ui.Separator(divider=True), disnake.ui.Section("📁 Salon des logs", accessory=disnake.ui.Button(label="Logs", style=disnake.ButtonStyle.primary, custom_id="modcfg_logs")), disnake.ui.Section("📢 Salon d'affichage", accessory=disnake.ui.Button(label="Public", style=disnake.ButtonStyle.primary, custom_id="modcfg_board")), disnake.ui.Section("⚖️ Modifier paliers", accessory=disnake.ui.Button(label="Paliers", style=disnake.ButtonStyle.secondary, custom_id="modcfg_limits")), disnake.ui.Section("⏱️ Durée timeout", accessory=disnake.ui.Button(label="Durée", style=disnake.ButtonStyle.secondary, custom_id="modcfg_duration")) ] components = [disnake.ui.Container(*children)] if edit: if not interaction.response.is_done(): await interaction.response.edit_message(content=None, components=components) else: await interaction.edit_original_response(content=None, components=components) else: await interaction.response.send_message(components=components, ephemeral=True) @commands.slash_command(name="modconfig", description="Configuration de la modération") @commands.has_permissions(administrator=True) async def modconfig(self, interaction: disnake.ApplicationCommandInteraction): conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('INSERT OR IGNORE INTO mod_config (guild_id) VALUES (?)', (interaction.guild_id,)) conn.commit(); conn.close() await self.send_modconfig_v2(interaction) @commands.Cog.listener("on_message_interaction") async def on_mod_interaction(self, interaction: disnake.MessageInteraction): cid = interaction.data.custom_id if not cid: return if cid.startswith("modpan_"): action, mid = cid.replace("modpan_", "").split(":") member = interaction.guild.get_member(int(mid)) if not member: return await interaction.response.send_message("❌ Membre introuvable.", ephemeral=True) # Vérifications des permissions et de la hiérarchie if member.top_role >= interaction.user.top_role and interaction.user.id != interaction.guild.owner_id: return await interaction.response.send_message("❌ Permissions insuffisantes : ce membre a un rôle supérieur ou égal au vôtre.", ephemeral=True) perms = interaction.user.guild_permissions if action == "ban" and not perms.ban_members: return await interaction.response.send_message("❌ Vous n'avez pas la permission de bannir des membres.", ephemeral=True) elif action == "kick" and not perms.kick_members: return await interaction.response.send_message("❌ Vous n'avez pas la permission d'expulser des membres.", ephemeral=True) elif action in ["timeout", "warn", "clear"] and not perms.moderate_members: return await interaction.response.send_message("❌ Vous n'avez pas la permission de gérer les sanctions (moderate_members).", ephemeral=True) if action == "clear": await self.clearwarns(interaction, member, "Nettoyage Panel") await self.send_modpanel_v2(interaction, member, edit=True) else: await interaction.response.send_modal(ModReasonModal(self, action.upper(), member)) elif cid.startswith("modcfg_"): if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌ Vous devez être administrateur pour modifier cette configuration.", ephemeral=True) act = cid.replace("modcfg_", "") if act in ["logs", "board"]: col = "log_channel_id" if act == "logs" else "board_channel_id" view = disnake.ui.View() sel = disnake.ui.ChannelSelect(placeholder="Choisir salon...", channel_types=[disnake.ChannelType.text]) async def sel_cb(i): c = sel.values[0] conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute(f"UPDATE mod_config SET {col} = ? WHERE guild_id = ?", (c.id, i.guild_id)) conn.commit(); conn.close() await i.response.send_message(f"✅ Salon mis à jour.", ephemeral=True) await self.send_modconfig_v2(interaction, edit=True) sel.callback = sel_cb; view.add_item(sel) await interaction.response.send_message(f"Sélectionnez le salon pour {act} :", view=view, ephemeral=True) elif act == "limits": conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute("SELECT warn_limit_timeout, warn_limit_kick, warn_limit_ban FROM mod_config WHERE guild_id = ?", (interaction.guild_id,)) res = cursor.fetchone(); conn.close() view = disnake.ui.View() b1 = disnake.ui.Button(label=f"Timeout ({res[0]})"); b1.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Timeout", res[0])) b2 = disnake.ui.Button(label=f"Kick ({res[1]})"); b2.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Kick", res[1])) b3 = disnake.ui.Button(label=f"Ban ({res[2]})"); b3.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Ban", res[2])) view.add_item(b1); view.add_item(b2); view.add_item(b3) await interaction.response.send_message("Modifier quel palier ?", view=view, ephemeral=True) elif act == "duration": conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute("SELECT timeout_duration FROM mod_config WHERE guild_id = ?", (interaction.guild_id,)) res = cursor.fetchone(); conn.close() await interaction.response.send_modal(ModConfigModal("Durée Timeout (s)", res[0])) async def warn(self, interaction, member, reason): conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?)', (interaction.guild_id, member.id, interaction.user.id, 'WARN', reason)) cursor.execute('SELECT COUNT(*) FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "WARN" AND status = "ACTIVE"', (interaction.guild_id, member.id)) count = cursor.fetchone()[0] cursor.execute('SELECT warn_limit_timeout, warn_limit_kick, warn_limit_ban, timeout_duration FROM mod_config WHERE guild_id = ?', (interaction.guild_id,)) cfg = cursor.fetchone() cursor.execute('SELECT id, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "WARN" ORDER BY id DESC LIMIT 1', (interaction.guild_id, member.id)) s_id, ts = cursor.fetchone(); conn.commit(); conn.close() if not interaction.response.is_done(): await interaction.response.send_message(f"✅ {member.mention} averti ({count}).", ephemeral=True) await self.send_mod_log(interaction.guild, [disnake.ui.Container(disnake.ui.Section(f"⚠️ Warn : {member}", accessory=disnake.ui.Thumbnail(member.display_avatar.url)), disnake.ui.Separator(divider=True), disnake.ui.TextDisplay(f"Raison: {reason}"), disnake.ui.TextDisplay(f"Total: {count}"))]) await self.send_sanction_board(interaction.guild, self.build_sanction_board_components(s_type="WARN", user=member, moderator=interaction.user, reason=reason, timestamp=ts, s_id=s_id)) try: await member.send(f"⚠️ Avertissement sur **{interaction.guild.name}**\nRaison: {reason}") except: pass if cfg: lt, lk, lb, dur = cfg if count >= lb: await member.ban(reason=f"Auto: {count} warns") elif count >= lk: await member.kick(reason=f"Auto: {count} warns") elif count >= lt: await member.timeout(duration=timedelta(seconds=dur), reason=f"Auto: {count} warns") async def timeout(self, interaction, member, duration_str, reason): secs = self.parse_duration(duration_str) if secs <= 0: return await interaction.response.send_message("❌ Durée invalide.", ephemeral=True) await member.timeout(duration=timedelta(seconds=secs), reason=reason) conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason, duration) VALUES (?, ?, ?, ?, ?, ?)', (interaction.guild_id, member.id, interaction.user.id, 'TIMEOUT', reason, secs)) cursor.execute('SELECT id, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "TIMEOUT" ORDER BY id DESC LIMIT 1', (interaction.guild_id, member.id)) s_id, ts = cursor.fetchone(); conn.commit(); conn.close() if not interaction.response.is_done(): await interaction.response.send_message(f"✅ {member.mention} timeout.", ephemeral=True) await self.send_sanction_board(interaction.guild, self.build_sanction_board_components(s_type="TIMEOUT", user=member, moderator=interaction.user, reason=reason, timestamp=ts, s_id=s_id, duration=secs)) async def kick(self, interaction, member, reason): await member.kick(reason=reason) conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?)', (interaction.guild_id, member.id, interaction.user.id, 'KICK', reason)) cursor.execute('SELECT id, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "KICK" ORDER BY id DESC LIMIT 1', (interaction.guild_id, member.id)) s_id, ts = cursor.fetchone(); conn.commit(); conn.close() if not interaction.response.is_done(): await interaction.response.send_message(f"✅ {member.name} expulsé.", ephemeral=True) await self.send_sanction_board(interaction.guild, self.build_sanction_board_components(s_type="KICK", user=member, moderator=interaction.user, reason=reason, timestamp=ts, s_id=s_id)) async def ban(self, interaction, user, reason): await interaction.guild.ban(user, reason=reason) conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?)', (interaction.guild_id, user.id, interaction.user.id, 'BAN', reason)) cursor.execute('SELECT id, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "BAN" ORDER BY id DESC LIMIT 1', (interaction.guild_id, user.id)) s_id, ts = cursor.fetchone(); conn.commit(); conn.close() if not interaction.response.is_done(): await interaction.response.send_message(f"✅ {user.name} banni.", ephemeral=True) await self.send_sanction_board(interaction.guild, self.build_sanction_board_components(s_type="BAN", user=user, moderator=interaction.user, reason=reason, timestamp=ts, s_id=s_id)) async def clearwarns(self, interaction, member, reason): conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('UPDATE sanctions SET status = "REVOKED" WHERE guild_id = ? AND user_id = ? AND type = "WARN"', (interaction.guild_id, member.id)) conn.commit(); conn.close() if not interaction.response.is_done(): await interaction.response.send_message(f"✅ Warns effacés.", ephemeral=True) def setup(bot): bot.add_cog(Moderation(bot))