diff --git a/bot.py b/bot.py index 7f18c8f..c4e162e 100755 --- a/bot.py +++ b/bot.py @@ -90,6 +90,7 @@ class MyBot(commands.Bot): "commandes.convocation", "commandes.absence_staff", "commandes.website_sync", + "commandes.moderation", ] for extension in extensions: diff --git a/commandes/moderation.py b/commandes/moderation.py new file mode 100644 index 0000000..379b05f --- /dev/null +++ b/commandes/moderation.py @@ -0,0 +1,325 @@ +import discord +from discord.ext import commands +from discord import app_commands +import sqlite3 +import os +from datetime import datetime, timedelta +import re + +class ModReasonModal(discord.ui.Modal): + def __init__(self, parent_view, action_type): + super().__init__(title=f"Raison pour {action_type}") + self.parent_view = parent_view + self.action_type = action_type + + self.reason = discord.ui.TextInput( + label="Raison", + placeholder="Entrez la raison ici...", + required=True, + max_length=500 + ) + self.add_item(self.reason) + + if action_type == "TIMEOUT": + self.duration = discord.ui.TextInput( + label="Durée (ex: 1h, 30m, 1d)", + placeholder="1h", + required=True, + max_length=10 + ) + self.add_item(self.duration) + + async def on_submit(self, interaction: discord.Interaction): + reason = self.reason.value + member = self.parent_view.member + + cog = interaction.client.get_cog("Moderation") + if not cog: + return await interaction.response.send_message("❌ Erreur interne.", ephemeral=True) + + if self.action_type == "WARN": + await cog.warn(interaction, member, reason) + elif self.action_type == "TIMEOUT": + await cog.timeout(interaction, member, self.duration.value, reason) + elif self.action_type == "KICK": + await cog.kick(interaction, member, reason) + elif self.action_type == "BAN": + await cog.ban(interaction, member, reason) + + await self.parent_view.update_panel(interaction) + +class ModPanelView(discord.ui.View): + def __init__(self, member, moderator, db_path): + super().__init__(timeout=120) + self.member = member + self.moderator = moderator + self.db_path = db_path + + async def update_panel(self, interaction: discord.Interaction): + 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, self.member.id)) + warn_count = cursor.fetchone()[0] + conn.close() + + embed = discord.Embed( + title=f"🛡️ Panel de Modération - {self.member.name}", + description=f"Gestion de l'utilisateur {self.member.mention}", + color=discord.Color.blue(), + timestamp=datetime.now() + ) + embed.set_thumbnail(url=self.member.display_avatar.url) + embed.add_field(name="👤 Utilisateur", value=f"ID: `{self.member.id}`\nTag: `{self.member}`", inline=True) + embed.add_field(name="⚠️ Avertissements", value=f"Actifs: **{warn_count}**", inline=True) + embed.add_field(name="📅 Dates", value=f"Rejoint: \nCréé: ", inline=False) + + if not interaction.response.is_done(): + await interaction.response.edit_message(embed=embed, view=self) + else: + await interaction.edit_original_response(embed=embed, view=self) + + @discord.ui.button(label="Avertir", style=discord.ButtonStyle.secondary, emoji="⚠️") + async def warn_btn(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_modal(ModReasonModal(self, "WARN")) + + @discord.ui.button(label="Timeout", style=discord.ButtonStyle.secondary, emoji="⏳") + async def timeout_btn(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_modal(ModReasonModal(self, "TIMEOUT")) + + @discord.ui.button(label="Expulser", style=discord.ButtonStyle.secondary, emoji="👢") + async def kick_btn(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_modal(ModReasonModal(self, "KICK")) + + @discord.ui.button(label="Bannir", style=discord.ButtonStyle.danger, emoji="🔨") + async def ban_btn(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_modal(ModReasonModal(self, "BAN")) + + @discord.ui.button(label="Historique", style=discord.ButtonStyle.primary, emoji="📜") + async def history_btn(self, interaction: discord.Interaction, button: discord.ui.Button): + cog = interaction.client.get_cog("Moderation") + await cog.history(interaction, self.member) + + @discord.ui.button(label="Clear Warns", style=discord.ButtonStyle.danger, emoji="🧹") + async def clear_btn(self, interaction: discord.Interaction, button: discord.ui.Button): + cog = interaction.client.get_cog("Moderation") + await cog.clearwarns(interaction, self.member, reason="Nettoyage via Panel") + await self.update_panel(interaction) + +class ModConfigModal(discord.ui.Modal): + def __init__(self, field_name, current_value): + super().__init__(title=f"Modifier {field_name}") + self.field_name = field_name + self.input = discord.ui.TextInput( + label=field_name, + default=str(current_value), + placeholder="Entrez une valeur numérique...", + required=True + ) + self.add_item(self.input) + + async def on_submit(self, interaction: discord.Interaction): + value = self.input.value + cog = interaction.client.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 sur **{value}**.", ephemeral=True) + +class ModConfigView(discord.ui.View): + def __init__(self, guild_id, db_path): + super().__init__(timeout=60) + self.guild_id = guild_id + self.db_path = db_path + + @discord.ui.button(label="Log Channel", style=discord.ButtonStyle.primary, emoji="📁") + async def log_btn(self, interaction: discord.Interaction, button: discord.ui.Button): + view = discord.ui.View() + select = discord.ui.ChannelSelect( + placeholder="Sélectionnez le salon de logs...", + channel_types=[discord.ChannelType.text] + ) + + async def select_callback(interaction: discord.Interaction): + channel = select.values[0] + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute("UPDATE mod_config SET log_channel_id = ? WHERE guild_id = ?", (channel.id, interaction.guild_id)) + conn.commit(); conn.close() + await interaction.response.send_message(f"✅ Salon de logs défini sur {channel.mention}", ephemeral=True) + + select.callback = select_callback + view.add_item(select) + await interaction.response.send_message("Choisissez le salon de logs :", view=view, ephemeral=True) + + @discord.ui.button(label="Paliers Warns", style=discord.ButtonStyle.secondary, emoji="⚖️") + async def limits_btn(self, interaction: discord.Interaction, button: discord.ui.Button): + 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 = ?", (self.guild_id,)) + res = cursor.fetchone(); conn.close() + + view = discord.ui.View() + + btn_t = discord.ui.Button(label=f"Timeout ({res[0]})", style=discord.ButtonStyle.secondary) + btn_t.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Timeout", res[0])) + + btn_k = discord.ui.Button(label=f"Kick ({res[1]})", style=discord.ButtonStyle.secondary) + btn_k.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Kick", res[1])) + + btn_b = discord.ui.Button(label=f"Ban ({res[2]})", style=discord.ButtonStyle.secondary) + btn_b.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Ban", res[2])) + + view.add_item(btn_t); view.add_item(btn_k); view.add_item(btn_b) + await interaction.response.send_message("Quelle limite modifier ?", view=view, ephemeral=True) + + @discord.ui.button(label="Durée Timeout", style=discord.ButtonStyle.secondary, emoji="⏱️") + async def duration_btn(self, interaction: discord.Interaction, button: discord.ui.Button): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute("SELECT timeout_duration FROM mod_config WHERE guild_id = ?", (self.guild_id,)) + res = cursor.fetchone(); conn.close() + await interaction.response.send_modal(ModConfigModal("Durée Timeout (s)", res[0])) + +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, warn_limit_timeout INTEGER DEFAULT 3, warn_limit_kick INTEGER DEFAULT 5, warn_limit_ban INTEGER DEFAULT 10, timeout_duration INTEGER DEFAULT 3600)''') + 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 + + async def send_mod_log(self, guild, embed): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute('SELECT log_channel_id FROM mod_config WHERE guild_id = ?', (guild.id,)) + result = cursor.fetchone(); conn.close() + if result and result[0]: + log_channel = guild.get_channel(result[0]) + if log_channel: await log_channel.send(embed=embed) + + @app_commands.command(name="modpanel", description="Ouvre le panel de modération pour un membre") + @app_commands.checks.has_permissions(moderate_members=True) + async def modpanel(self, interaction: discord.Interaction, member: discord.Member): + if member.top_role >= interaction.user.top_role and interaction.user.id != interaction.guild.owner_id: + return await interaction.response.send_message("❌ Impossible de gérer ce membre.", ephemeral=True) + 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]; conn.close() + embed = discord.Embed(title=f"🛡️ Panel de Modération - {member.name}", description=f"Gestion de {member.mention}", color=discord.Color.blue(), timestamp=datetime.now()) + embed.set_thumbnail(url=member.display_avatar.url) + embed.add_field(name="👤 Utilisateur", value=f"ID: `{member.id}`", inline=True) + embed.add_field(name="⚠️ Avertissements", value=f"Actifs: **{warn_count}**", inline=True) + view = ModPanelView(member, interaction.user, self.db_path) + await interaction.response.send_message(embed=embed, view=view, ephemeral=True) + + @app_commands.command(name="modconfig", description="Configuration de la modération") + @app_commands.checks.has_permissions(administrator=True) + async def modconfig(self, interaction: discord.Interaction): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute('INSERT OR IGNORE INTO mod_config (guild_id) VALUES (?)', (interaction.guild_id,)) + cursor.execute('SELECT log_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.commit(); conn.close() + embed = discord.Embed(title="⚙️ Configuration Modération", description="Modifiez les paramètres ci-dessous.", color=discord.Color.blue()) + embed.add_field(name="📁 Logs", value=f"<#{res[0]}>" if res[0] else "Non défini", inline=False) + embed.add_field(name="⚖️ Paliers", value=f"Timeout: **{res[1]}**, Kick: **{res[2]}**, Ban: **{res[3]}**", inline=False) + embed.add_field(name="⏱️ Durée Timeout Auto", value=f"**{res[4]}s**", inline=False) + view = ModConfigView(interaction.guild_id, self.db_path) + await interaction.response.send_message(embed=embed, view=view, ephemeral=True) + + async def warn(self, interaction: discord.Interaction, member: discord.Member, reason: str): + 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)) + warn_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,)) + config = cursor.fetchone(); conn.commit(); conn.close() + if not interaction.response.is_done(): await interaction.response.send_message(f"✅ {member.mention} averti ({warn_count}).", ephemeral=True) + else: await interaction.followup.send(f"✅ {member.mention} averti ({warn_count}).", ephemeral=True) + await self.send_mod_log(interaction.guild, discord.Embed(title="⚠️ Avertissement", description=f"Membre: {member.mention}\nRaison: {reason}\nTotal: {warn_count}", color=discord.Color.orange())) + try: await member.send(f"⚠️ Avertissement sur **{interaction.guild.name}**\nRaison: {reason}") + except: pass + if config: + l_t, l_k, l_b, dur = config + if warn_count >= l_b: await member.ban(reason=f"Auto: {warn_count} warns") + elif warn_count >= l_k: await member.kick(reason=f"Auto: {warn_count} warns") + elif warn_count >= l_t: await member.timeout(timedelta(seconds=dur), reason=f"Auto: {warn_count} warns") + + async def timeout(self, interaction: discord.Interaction, member: discord.Member, duration_str: str, reason: str): + secs = self.parse_duration(duration_str) + if secs <= 0: return await interaction.response.send_message("❌ Durée invalide.", ephemeral=True) + await member.timeout(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)) + conn.commit(); conn.close() + if not interaction.response.is_done(): await interaction.response.send_message(f"✅ {member.mention} mis en sourdine.", ephemeral=True) + else: await interaction.followup.send(f"✅ {member.mention} mis en sourdine.", ephemeral=True) + + async def kick(self, interaction: discord.Interaction, member: discord.Member, reason: str): + 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)) + conn.commit(); conn.close() + if not interaction.response.is_done(): await interaction.response.send_message(f"✅ {member.name} expulsé.", ephemeral=True) + else: await interaction.followup.send(f"✅ {member.name} expulsé.", ephemeral=True) + + async def ban(self, interaction: discord.Interaction, user: discord.User, reason: str): + 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)) + conn.commit(); conn.close() + if not interaction.response.is_done(): await interaction.response.send_message(f"✅ {user.name} banni.", ephemeral=True) + else: await interaction.followup.send(f"✅ {user.name} banni.", ephemeral=True) + + async def history(self, interaction: discord.Interaction, user: discord.User): + conn = sqlite3.connect(self.db_path); cursor = conn.cursor() + cursor.execute('SELECT id, type, reason, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? ORDER BY timestamp DESC', (interaction.guild_id, user.id)) + results = cursor.fetchall(); conn.close() + if not results: + if not interaction.response.is_done(): return await interaction.response.send_message("ℹ️ Aucun historique.", ephemeral=True) + else: return await interaction.followup.send("ℹ️ Aucun historique.", ephemeral=True) + embed = discord.Embed(title=f"📜 Historique de {user.name}", color=discord.Color.blue()) + for s_id, s_type, reason, ts in results[:10]: + embed.add_field(name=f"#{s_id} - {s_type}", value=f"**Date:** {ts}\n**Raison:** {reason}", inline=False) + if not interaction.response.is_done(): await interaction.response.send_message(embed=embed, ephemeral=True) + else: await interaction.followup.send(embed=embed, ephemeral=True) + + async def clearwarns(self, interaction: discord.Interaction, member: discord.Member, reason: str): + 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 de {member.name} effacés.", ephemeral=True) + else: await interaction.followup.send(f"✅ Warns de {member.name} effacés.", ephemeral=True) + +async def setup(bot): + await bot.add_cog(Moderation(bot)) diff --git a/data/json-export-history.json b/data/json-export-history.json index 262fa2b..d5d9c2e 100644 --- a/data/json-export-history.json +++ b/data/json-export-history.json @@ -1,4 +1,31 @@ [ + { + "date": "2026-05-01T14:42:53.534230+00:00", + "trigger": "auto", + "serversCount": 1, + "membersCount": 12, + "guildName": "Omega Kube Bêta Test", + "errors": [], + "success": true + }, + { + "date": "2026-05-01T14:34:33.777534+00:00", + "trigger": "auto", + "serversCount": 1, + "membersCount": 12, + "guildName": "Omega Kube Bêta Test", + "errors": [], + "success": true + }, + { + "date": "2026-05-01T14:31:01.918316+00:00", + "trigger": "auto", + "serversCount": 1, + "membersCount": 12, + "guildName": "Omega Kube Bêta Test", + "errors": [], + "success": true + }, { "date": "2026-05-01T14:11:08.947818+00:00", "trigger": "auto", @@ -151,32 +178,5 @@ "guildName": "Omega Kube Bêta Test", "errors": [], "success": true - }, - { - "date": "2026-04-01T12:07:50.185260+00:00", - "trigger": "auto", - "serversCount": 1, - "membersCount": 12, - "guildName": "Omega Kube Bêta Test", - "errors": [], - "success": true - }, - { - "date": "2026-04-01T11:07:49.769763+00:00", - "trigger": "auto", - "serversCount": 1, - "membersCount": 12, - "guildName": "Omega Kube Bêta Test", - "errors": [], - "success": true - }, - { - "date": "2026-04-01T10:07:49.758778+00:00", - "trigger": "auto", - "serversCount": 1, - "membersCount": 12, - "guildName": "Omega Kube Bêta Test", - "errors": [], - "success": true } ] \ No newline at end of file diff --git a/scratch/check_db.py b/scratch/check_db.py new file mode 100644 index 0000000..566bc3e --- /dev/null +++ b/scratch/check_db.py @@ -0,0 +1,16 @@ +import sqlite3 +import os + +db_path = "config.db" +if os.path.exists(db_path): + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") + tables = cursor.fetchall() + print(f"Tables: {tables}") + for table in tables: + cursor.execute(f"PRAGMA table_info({table[0]});") + print(f"Schema for {table[0]}: {cursor.fetchall()}") + conn.close() +else: + print("Database not found.")