From 2b8b725bbb9ad2e5630f69e50b88199462f183a1 Mon Sep 17 00:00:00 2001 From: Mow910 Date: Sun, 3 May 2026 14:36:44 +0200 Subject: [PATCH] Refactor moderation commands in Discord bot - Removed deprecated timeout and ban command handling from the message processing logic. - Added new moderation slash commands for ban, kick, and timeout functionalities. - Updated the command tree initialization to include the new moderation commands for better organization and usability. --- discordbot/__init__.py | 37 +-- discordbot/moderation.py | 697 ++++++++++++++++++++++----------------- 2 files changed, 420 insertions(+), 314 deletions(-) diff --git a/discordbot/__init__.py b/discordbot/__init__.py index 64ac489..0affe89 100644 --- a/discordbot/__init__.py +++ b/discordbot/__init__.py @@ -14,16 +14,19 @@ from discordbot.moderation import ( handle_warning_command, handle_remove_warning_command, handle_list_warnings_command, - handle_ban_command, - handle_kick_command, handle_unban_command, handle_inspect_command, handle_ban_list_command, handle_staff_help_command, - handle_timeout_command, handle_say_command, handle_transfer_command, - transfer_message_context_menu + transfer_message_context_menu, + moderation_slash_ban, + moderation_slash_kick, + moderation_slash_timeout, + moderation_ctx_ban_author, + moderation_ctx_kick_author, + moderation_ctx_timeout_author, ) from discordbot.welcome import sendWelcomeMessage, sendLeaveMessage, updateInviteCache from discordbot.patreon import checkPatreonPosts @@ -38,8 +41,17 @@ class DiscordBot(discord.Client): self.synced = False async def setup_hook(self): - self.tree.add_command(transfer_message_context_menu) - logging.info("Commande contextuelle 'Déplacer le message' ajoutée au CommandTree") + for cmd in ( + transfer_message_context_menu, + moderation_slash_ban, + moderation_slash_kick, + moderation_slash_timeout, + moderation_ctx_ban_author, + moderation_ctx_kick_author, + moderation_ctx_timeout_author, + ): + self.tree.add_command(cmd) + logging.info("Commandes d'application (transfert, ban, kick, timeout) ajoutées au CommandTree") async def on_ready(self): logging.info(f'Connecté en tant que {self.user} (ID: {self.user.id})') @@ -177,10 +189,6 @@ async def on_message(message: Message): await handle_warning_command(message, bot) return - if command_name in ['!to', '!timeout']: - await handle_timeout_command(message, bot) - return - if command_name in ['!delaverto', '!removewarn', '!unwarn']: await handle_remove_warning_command(message, bot) return @@ -190,10 +198,6 @@ async def on_message(message: Message): return if ConfigurationHelper().getValue('moderation_ban_enable'): - if command_name == '!ban': - await handle_ban_command(message, bot) - return - if command_name == '!unban': await handle_unban_command(message, bot) return @@ -201,11 +205,6 @@ async def on_message(message: Message): await handle_ban_list_command(message, bot) return - if ConfigurationHelper().getValue('moderation_kick_enable'): - if command_name == '!kick': - await handle_kick_command(message, bot) - return - if ConfigurationHelper().getValue('moderation_enable'): if command_name == '!inspect': await handle_inspect_command(message, bot) diff --git a/discordbot/moderation.py b/discordbot/moderation.py index 182f444..fc3d9e6 100644 --- a/discordbot/moderation.py +++ b/discordbot/moderation.py @@ -5,6 +5,7 @@ import os import re import discord import io +from typing import Optional from datetime import datetime, timezone, timedelta from zoneinfo import ZoneInfo from database import db @@ -293,18 +294,6 @@ async def _process_warning_success(message: Message, target_user, reason: str, b await send_warning_confirmation(message.channel, target_user, reason, message, bot, timeout_info) -async def send_timeout_usage(channel): - embed = discord.Embed( - title="📋 Utilisation de la commande", - description="**Syntaxe :** `!to @utilisateur durée raison` ou `!timeout @utilisateur durée raison`", - color=discord.Color.blue() - ) - embed.add_field(name="Exemples", value="• `!to @User 10m Spam`\n• `!timeout @User 1h Comportement inapproprié`\n• `!to @User 30s Flood`\n• `!timeout @User 1j Toxicité`", inline=False) - embed.add_field(name="Durées", value="`s` = secondes, `m` = minutes (défaut), `h` = heures, `j` = jours\nExemple: `10m`, `1h`, `60s`", inline=False) - embed.add_field(name="Aliases", value="`!to`, `!timeout`", inline=False) - msg = await channel.send(embed=embed) - asyncio.create_task(delete_after_delay(msg)) - def parse_timeout_from_args(duration_str: str): match = re.match(r'^(\d+)([smhj])?$', duration_str.lower()) if not match: @@ -323,118 +312,6 @@ def parse_timeout_from_args(duration_str: str): return value * 86400 return None -async def parse_timeout_target_and_params(message, bot, parts: list): - if len(parts) < 3: - return None, None, None - - if message.mentions: - target_user = message.mentions[0] - timeout_seconds = parse_timeout_from_args(parts[2]) - reason = " ".join(parts[3:]) if len(parts) > 3 else "Sans raison" - return target_user, timeout_seconds, reason - - try: - user_id = int(parts[1]) - target_user = await bot.fetch_user(user_id) - timeout_seconds = parse_timeout_from_args(parts[2]) - reason = " ".join(parts[3:]) if len(parts) > 3 else "Sans raison" - return target_user, timeout_seconds, reason - except (ValueError, discord.NotFound): - return None, None, None - -async def send_timeout_confirmation(channel, target_user, reason: str, timeout_seconds: int, original_message: Message, bot): - local_now = _to_local(datetime.now(timezone.utc)) - - embed = discord.Embed( - title="⏱️ Time out", - description=f"**{target_user.name}** (`{target_user.name}`) a été exclu temporairement ({format_timeout_duration(timeout_seconds)})", - color=discord.Color.orange(), - timestamp=datetime.now(timezone.utc) - ) - embed.add_field(name="👤 Utilisateur", value=f"**{target_user.name}**\n`{target_user.id}`", inline=True) - embed.add_field(name="🛡️ Modérateur", value=f"**{original_message.author.name}**", inline=True) - embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True) - embed.add_field(name="⏱️ Durée", value=format_timeout_duration(timeout_seconds), inline=True) - if reason != "Sans raison": - embed.add_field(name="📝 Raison", value=reason, inline=False) - - embed.set_footer(text=f"ID: {target_user.id} • Serveur: {original_message.guild.name}") - - await send_to_moderation_log_channel(bot, embed) - await safe_delete_message(original_message) - -async def send_invalid_timeout_duration(channel): - embed = discord.Embed( - title="❌ Erreur", - description="Durée invalide. Utilisez un format valide comme `10m`, `1h`, `60s`, etc.", - color=discord.Color.red() - ) - msg = await channel.send(embed=embed) - asyncio.create_task(delete_after_delay(msg)) - -async def handle_timeout_command(message: Message, bot): - parts = message.content.split() - if not has_staff_role(message.author.roles): - await send_access_denied(message.channel) - elif len(parts) < 3: - await send_timeout_usage(message.channel) - else: - target_user, timeout_seconds, reason = await parse_timeout_target_and_params(message, bot, parts) - if not target_user: - await send_user_not_found(message.channel) - elif not timeout_seconds: - await send_invalid_timeout_duration(message.channel) - else: - await _process_timeout_success(message, target_user, reason, timeout_seconds, bot) - -async def _process_timeout_success(message: Message, target_user, reason: str, timeout_seconds: int, bot): - member_obj = message.guild.get_member(target_user.id) - if not member_obj: - embed = discord.Embed( - title="❌ Erreur", - description="L'utilisateur n'est pas membre du serveur.", - color=discord.Color.red() - ) - msg = await message.channel.send(embed=embed) - asyncio.create_task(delete_after_delay(msg)) - return - - try: - until = discord.utils.utcnow() + timedelta(seconds=timeout_seconds) - await member_obj.timeout(until, reason=reason) - - timeout_event = ModerationEvent( - type='timeout', - username=target_user.name, - discord_id=str(target_user.id), - created_at=datetime.now(timezone.utc), - reason=reason, - staff_id=str(message.author.id), - staff_name=message.author.name, - duration=timeout_seconds - ) - db.session.add(timeout_event) - _commit_with_retry() - - await send_timeout_confirmation(message.channel, target_user, reason, timeout_seconds, message, bot) - except discord.Forbidden: - embed = discord.Embed( - title="❌ Erreur", - description="Je n'ai pas les permissions nécessaires pour exclure cet utilisateur.", - color=discord.Color.red() - ) - msg = await message.channel.send(embed=embed) - asyncio.create_task(delete_after_delay(msg)) - except Exception as e: - logging.error(f"Erreur lors du timeout de {target_user.name}: {e}") - embed = discord.Embed( - title="❌ Erreur", - description=f"Une erreur est survenue lors de l'exclusion : {str(e)}", - color=discord.Color.red() - ) - msg = await message.channel.send(embed=embed) - asyncio.create_task(delete_after_delay(msg)) - async def send_remove_warning_usage(channel): embed = discord.Embed( title="📋 Utilisation de la commande", @@ -605,54 +482,12 @@ async def handle_list_warnings_command(message: Message, bot): await handle_pagination_loop(msg, bot, message.author, events, per_page) await safe_delete_message(message) -async def handle_ban_command(message: Message, bot): - parts = message.content.split(maxsplit=2) - if not has_staff_role(message.author.roles): - await send_access_denied(message.channel) - elif len(parts) < 2: - await _send_ban_usage(message.channel) - else: - target_user, reason = await _parse_ban_target_and_reason(message, bot, parts) - if not target_user: - await _send_user_not_found_for_ban(message.channel) - else: - await _process_ban_success(message, target_user, reason, bot) - -async def _send_ban_usage(channel): - embed = discord.Embed( - title="📋 Utilisation de la commande", - description="**Syntaxe :** `!ban @utilisateur [raison]` ou `!ban [raison]`", - color=discord.Color.blue() - ) - embed.add_field(name="Exemples", value="• `!ban @User Spam répété`\n• `!ban 123456789012345678 Comportement toxique`", inline=False) - msg = await channel.send(embed=embed) - asyncio.create_task(delete_after_delay(msg)) - -async def _parse_ban_target_and_reason(message: Message, bot, parts: list): - if message.mentions: - return message.mentions[0], (parts[2] if len(parts) > 2 else "Sans raison") - try: - user_id = int(parts[1]) - user = await bot.fetch_user(user_id) - return user, (parts[2] if len(parts) > 2 else "Sans raison") - except (ValueError, discord.NotFound): - return None, None - -async def _send_user_not_found_for_ban(channel): - embed = discord.Embed( - title="❌ Erreur", - description="Utilisateur introuvable. Vérifiez la mention ou l'ID Discord.", - color=discord.Color.red() - ) - msg = await channel.send(embed=embed) - asyncio.create_task(delete_after_delay(msg)) - def _create_ban_event(target_user, reason: str, staff_member): event = ModerationEvent( type='ban', username=target_user.name, discord_id=str(target_user.id), - created_at=datetime.now(timezone.utc), + created_at=datetime.now(timezone.utc), reason=reason, staff_id=str(staff_member.id), staff_name=staff_member.name @@ -661,26 +496,33 @@ def _create_ban_event(target_user, reason: str, staff_member): _commit_with_retry() return event -async def _process_ban_success(message: Message, target_user, reason: str, bot): - member = message.guild.get_member(target_user.id) +def _normalized_slash_reason(raison: Optional[str]) -> str: + if raison is None: + return "Sans raison" + s = raison.strip() + return s if s else "Sans raison" + +async def moderation_perform_ban( + bot, + guild: discord.Guild, + staff: discord.Member, + target_user: discord.User, + reason: str, +) -> Optional[str]: + if target_user.id == bot.user.id: + return "Je ne peux pas me bannir moi-même." + if staff.id == target_user.id: + return "Vous ne pouvez pas vous bannir vous-même." + member = guild.get_member(target_user.id) joined_days = None if member and member.joined_at: delta = datetime.now(timezone.utc) - (member.joined_at if member.joined_at.tzinfo else member.joined_at.replace(tzinfo=timezone.utc)) joined_days = delta.days try: - await message.guild.ban(target_user, reason=reason) + await guild.ban(target_user, reason=reason) except discord.Forbidden: - embed = discord.Embed( - title="❌ Erreur", - description="Je n'ai pas les permissions nécessaires pour bannir cet utilisateur.", - color=discord.Color.red() - ) - msg = await message.channel.send(embed=embed) - asyncio.create_task(delete_after_delay(msg)) - return - - event = _create_ban_event(target_user, reason, message.author) - + return "Je n'ai pas les permissions nécessaires pour bannir cet utilisateur." + _create_ban_event(target_user, reason, staff) local_now = _to_local(datetime.now(timezone.utc)) embed = discord.Embed( title="🔨 Bannissement", @@ -689,16 +531,119 @@ async def _process_ban_success(message: Message, target_user, reason: str, bot): timestamp=datetime.now(timezone.utc) ) embed.add_field(name="👤 Utilisateur", value=f"**{target_user.name}**\n`{target_user.id}`", inline=True) - embed.add_field(name="🛡️ Modérateur", value=f"{message.author.name}", inline=True) + embed.add_field(name="🛡️ Modérateur", value=f"{staff.name}", inline=True) embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True) if joined_days is not None: embed.add_field(name="⏱️ Membre depuis", value=format_days_to_age(joined_days), inline=True) if reason != "Sans raison": embed.add_field(name="📝 Raison", value=reason, inline=False) - embed.set_footer(text=f"ID: {target_user.id} • Serveur: {message.guild.name}") - + embed.set_footer(text=f"ID: {target_user.id} • Serveur: {guild.name}") await send_to_moderation_log_channel(bot, embed) - await safe_delete_message(message) + return None + +async def moderation_perform_kick( + bot, + guild: discord.Guild, + staff: discord.Member, + target_user: discord.User, + reason: str, +) -> Optional[str]: + if target_user.id == bot.user.id: + return "Je ne peux pas m'expulser moi-même." + if staff.id == target_user.id: + return "Vous ne pouvez pas vous expulser vous-même." + member_obj = guild.get_member(target_user.id) + if not member_obj: + return "L'utilisateur n'est pas membre du serveur." + joined_days = None + if member_obj.joined_at: + delta = datetime.now(timezone.utc) - (member_obj.joined_at if member_obj.joined_at.tzinfo else member_obj.joined_at.replace(tzinfo=timezone.utc)) + joined_days = delta.days + try: + await guild.kick(member_obj, reason=reason) + except discord.Forbidden: + return "Je n'ai pas les permissions nécessaires pour expulser cet utilisateur." + create = ModerationEvent( + type='kick', + username=target_user.name, + discord_id=str(target_user.id), + created_at=datetime.now(timezone.utc), + reason=reason, + staff_id=str(staff.id), + staff_name=staff.name + ) + db.session.add(create) + _commit_with_retry() + local_now = _to_local(datetime.now(timezone.utc)) + embed = discord.Embed( + title="👢 Expulsion", + description=f"**{target_user.name}** (`{target_user.name}`) a été expulsé du serveur", + color=discord.Color.orange(), + timestamp=datetime.now(timezone.utc) + ) + embed.add_field(name="👤 Utilisateur", value=f"**{target_user.name}**\n`{target_user.id}`", inline=True) + embed.add_field(name="🛡️ Modérateur", value=f"**{staff.name}**", inline=True) + embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True) + if joined_days is not None: + embed.add_field(name="⏱️ Membre depuis", value=format_days_to_age(joined_days), inline=True) + if reason != "Sans raison": + embed.add_field(name="📝 Raison", value=reason, inline=False) + embed.set_footer(text=f"ID: {target_user.id} • Serveur: {guild.name}") + await send_to_moderation_log_channel(bot, embed) + return None + +async def moderation_perform_timeout( + bot, + guild: discord.Guild, + staff: discord.Member, + target_user: discord.User, + reason: str, + timeout_seconds: int, +) -> Optional[str]: + if target_user.id == bot.user.id: + return "Je ne peux pas m'exclure temporairement moi-même." + if staff.id == target_user.id: + return "Vous ne pouvez pas vous exclure vous-même temporairement." + member_obj = guild.get_member(target_user.id) + if not member_obj: + return "L'utilisateur n'est pas membre du serveur." + try: + until = discord.utils.utcnow() + timedelta(seconds=timeout_seconds) + await member_obj.timeout(until, reason=reason) + timeout_event = ModerationEvent( + type='timeout', + username=target_user.name, + discord_id=str(target_user.id), + created_at=datetime.now(timezone.utc), + reason=reason, + staff_id=str(staff.id), + staff_name=staff.name, + duration=timeout_seconds + ) + db.session.add(timeout_event) + _commit_with_retry() + local_now = _to_local(datetime.now(timezone.utc)) + embed = discord.Embed( + title="⏱️ Time out", + description=f"**{target_user.name}** (`{target_user.name}`) a été exclu temporairement ({format_timeout_duration(timeout_seconds)})", + color=discord.Color.orange(), + timestamp=datetime.now(timezone.utc) + ) + embed.add_field(name="👤 Utilisateur", value=f"**{target_user.name}**\n`{target_user.id}`", inline=True) + embed.add_field(name="🛡️ Modérateur", value=f"**{staff.name}**", inline=True) + embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True) + embed.add_field(name="⏱️ Durée", value=format_timeout_duration(timeout_seconds), inline=True) + if reason != "Sans raison": + embed.add_field(name="📝 Raison", value=reason, inline=False) + embed.set_footer(text=f"ID: {target_user.id} • Serveur: {guild.name}") + await send_to_moderation_log_channel(bot, embed) + return None + except discord.Forbidden: + return "Je n'ai pas les permissions nécessaires pour exclure cet utilisateur." + except Exception as e: + logging.error(f"Erreur lors du timeout de {target_user.name}: {e}") + return f"Une erreur est survenue lors de l'exclusion : {str(e)}" + async def handle_unban_command(message: Message, bot): parts = message.content.split(maxsplit=2) if not has_staff_role(message.author.roles): @@ -1006,10 +951,9 @@ async def handle_staff_help_command(message: Message, bot): " Donne un avertissement\n" "• `!warn @utilisateur raison --to durée`\n" " Avertissement + time out temporaire\n\n" - "**Time out uniquement:**\n" - "• `!to @utilisateur durée raison`\n" - " *Alias: !timeout*\n" - " Time out (sans avertissement)\n" + "**Time out (sans avertissement):**\n" + "• `/timeout` — commande slash (membre, durée, raison optionnelle)\n" + "• Clic droit sur un message → Applications → **Exclure temporairement**\n" " *Durées: 10s, 5m, 1h, 2j*\n\n" "**Gestion:**\n" "• `!delaverto id` - Supprime un événement\n" @@ -1017,7 +961,7 @@ async def handle_staff_help_command(message: Message, bot): "**Exemples:**\n" "`!warn @User Spam`\n" "`!warn @User Flood --to 10m` (averto + timeout)\n" - "`!to @User 5m Spam` (timeout seul)\n" + "`/timeout` : membre + durée + raison (timeout seul)\n" "`!warnings @User`" ) embed.add_field(name="⚠️ Avertissements & Time out", value=value, inline=False) @@ -1031,14 +975,14 @@ async def handle_staff_help_command(message: Message, bot): if ConfigurationHelper().getValue('moderation_ban_enable'): value = ( - "• `!ban @utilisateur raison`\n" - " Bannit définitivement un utilisateur\n" + "• `/ban` — bannit un membre (raison optionnelle)\n" + "• Clic droit sur un message → Applications → **Bannir l'auteur**\n" "• `!unban discord_id` ou `!unban #sanction_id raison`\n" " Révoque le ban et envoie une invitation\n" "• `!banlist`\n" " Affiche la liste des utilisateurs bannis\n" "Exemples:\n" - "`!ban @User Comportement toxique répété`\n" + "`/ban` sur un membre avec raison\n" "`!unban 123456789012345678 Erreur de modération`\n" "`!unban #5 Appel accepté`" ) @@ -1046,9 +990,8 @@ async def handle_staff_help_command(message: Message, bot): if ConfigurationHelper().getValue('moderation_kick_enable'): value = ( - "• `!kick @utilisateur raison` ou `!kick raison`\n" - " Expulse temporairement un utilisateur du serveur\n" - "Exemples: `!kick @User Spam de liens` ou `!kick 123456789012345678 Spam`" + "• `/kick` — expulse un membre (raison optionnelle)\n" + "• Clic droit sur un message → Applications → **Expulser l'auteur**" ) embed.add_field(name="👢 Expulsion", value=value, inline=False) @@ -1078,105 +1021,6 @@ async def handle_staff_help_command(message: Message, bot): pass await safe_delete_message(message) -async def handle_kick_command(message: Message, bot): - parts = message.content.split(maxsplit=2) - if not has_staff_role(message.author.roles): - await send_access_denied(message.channel) - elif len(parts) < 2: - await _send_kick_usage(message.channel) - else: - target_user, reason = await _parse_kick_target_and_reason(message, bot, parts) - if not target_user: - await _send_user_not_found_for_kick(message.channel) - else: - await _process_kick_success(message, target_user, reason, bot) - -async def _send_kick_usage(channel): - embed = discord.Embed( - title="📋 Utilisation de la commande", - description="**Syntaxe :** `!kick @utilisateur [raison]` ou `!kick [raison]`", - color=discord.Color.blue() - ) - embed.add_field(name="Exemples", value="• `!kick @User Spam dans le chat`\n• `!kick 123456789012345678 Comportement inapproprié`", inline=False) - msg = await channel.send(embed=embed) - asyncio.create_task(delete_after_delay(msg)) - -async def _parse_kick_target_and_reason(message: Message, bot, parts: list): - if message.mentions: - return message.mentions[0], (parts[2] if len(parts) > 2 else "Sans raison") - try: - user_id = int(parts[1]) - user = await bot.fetch_user(user_id) - return user, (parts[2] if len(parts) > 2 else "Sans raison") - except (ValueError, discord.NotFound): - return None, None - -async def _send_user_not_found_for_kick(channel): - embed = discord.Embed( - title="❌ Erreur", - description="Utilisateur introuvable. Vérifiez la mention ou l'ID Discord.", - color=discord.Color.red() - ) - msg = await channel.send(embed) - asyncio.create_task(delete_after_delay(msg)) - -async def _process_kick_success(message: Message, target_member, reason: str, bot): - member_obj = message.guild.get_member(target_member.id) - if not member_obj: - embed = discord.Embed( - title="❌ Erreur", - description="L'utilisateur n'est pas membre du serveur.", - color=discord.Color.red() - ) - msg = await message.channel.send(embed=embed) - asyncio.create_task(delete_after_delay(msg)) - return - joined_days = None - if member_obj.joined_at: - delta = datetime.now(timezone.utc) - (member_obj.joined_at if member_obj.joined_at.tzinfo else member_obj.joined_at.replace(tzinfo=timezone.utc)) - joined_days = delta.days - try: - await message.guild.kick(member_obj, reason=reason) - except discord.Forbidden: - embed = discord.Embed( - title="❌ Erreur", - description="Je n'ai pas les permissions nécessaires pour expulser cet utilisateur.", - color=discord.Color.red() - ) - msg = await message.channel.send(embed=embed) - asyncio.create_task(delete_after_delay(msg)) - return - create = ModerationEvent( - type='kick', - username=target_member.name, - discord_id=str(target_member.id), - created_at=datetime.now(timezone.utc), - reason=reason, - staff_id=str(message.author.id), - staff_name=message.author.name - ) - db.session.add(create) - _commit_with_retry() - - local_now = _to_local(datetime.now(timezone.utc)) - embed = discord.Embed( - title="👢 Expulsion", - description=f"**{target_member.name}** (`{target_member.name}`) a été expulsé du serveur", - color=discord.Color.orange(), - timestamp=datetime.now(timezone.utc) - ) - embed.add_field(name="👤 Utilisateur", value=f"**{target_member.name}**\n`{target_member.id}`", inline=True) - embed.add_field(name="🛡️ Modérateur", value=f"**{message.author.name}**", inline=True) - embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True) - if joined_days is not None: - embed.add_field(name="⏱️ Membre depuis", value=format_days_to_age(joined_days), inline=True) - if reason != "Sans raison": - embed.add_field(name="📝 Raison", value=reason, inline=False) - embed.set_footer(text=f"ID: {target_member.id} • Serveur: {message.guild.name}") - - await send_to_moderation_log_channel(bot, embed) - await safe_delete_message(message) - def format_days_to_age(days: int) -> str: if days >= 365: years = days // 365 @@ -1935,6 +1779,269 @@ class TransferView(View): super().__init__(timeout=180) self.add_item(TransferChannelSelect(message, bot)) +class BanAuthorReasonModal(Modal, title="Bannir l'auteur"): + reason = TextInput( + label="Raison", + placeholder="Optionnel", + required=False, + max_length=500, + style=discord.TextStyle.short, + ) + + def __init__(self, source_message: discord.Message): + super().__init__() + self.source_message = source_message + + async def on_submit(self, interaction: discord.Interaction): + await interaction.response.defer(ephemeral=True) + bot = interaction.client + guild = interaction.guild + if not guild or not isinstance(interaction.user, discord.Member): + await interaction.followup.send("❌ Action impossible dans ce contexte.", ephemeral=True) + return + reason = (self.reason.value or "").strip() or "Via menu contextuel" + target = self.source_message.author + err = await moderation_perform_ban(bot, guild, interaction.user, target, reason) + if err: + await interaction.followup.send(f"❌ {err}", ephemeral=True) + else: + await interaction.followup.send(f"✅ **{target.name}** a été banni.", ephemeral=True) + +class KickAuthorReasonModal(Modal, title="Expulser l'auteur"): + reason = TextInput( + label="Raison", + placeholder="Optionnel", + required=False, + max_length=500, + style=discord.TextStyle.short, + ) + + def __init__(self, source_message: discord.Message): + super().__init__() + self.source_message = source_message + + async def on_submit(self, interaction: discord.Interaction): + await interaction.response.defer(ephemeral=True) + bot = interaction.client + guild = interaction.guild + if not guild or not isinstance(interaction.user, discord.Member): + await interaction.followup.send("❌ Action impossible dans ce contexte.", ephemeral=True) + return + reason = (self.reason.value or "").strip() or "Via menu contextuel" + target = self.source_message.author + err = await moderation_perform_kick(bot, guild, interaction.user, target, reason) + if err: + await interaction.followup.send(f"❌ {err}", ephemeral=True) + else: + await interaction.followup.send(f"✅ **{target.name}** a été expulsé.", ephemeral=True) + +class TimeoutAuthorModal(Modal, title="Exclure temporairement"): + duration = TextInput( + label="Durée (ex. 10m, 1h, 2j)", + placeholder="10m", + required=True, + max_length=12, + style=discord.TextStyle.short, + ) + reason = TextInput( + label="Raison", + placeholder="Optionnel", + required=False, + max_length=500, + style=discord.TextStyle.short, + ) + + def __init__(self, source_message: discord.Message): + super().__init__() + self.source_message = source_message + + async def on_submit(self, interaction: discord.Interaction): + await interaction.response.defer(ephemeral=True) + bot = interaction.client + guild = interaction.guild + if not guild or not isinstance(interaction.user, discord.Member): + await interaction.followup.send("❌ Action impossible dans ce contexte.", ephemeral=True) + return + timeout_seconds = parse_timeout_from_args(self.duration.value.strip()) + if not timeout_seconds: + await interaction.followup.send( + "❌ Durée invalide. Utilisez un format comme `10m`, `1h`, `60s`, `2j`.", + ephemeral=True, + ) + return + reason = (self.reason.value or "").strip() or "Sans raison" + target = self.source_message.author + err = await moderation_perform_timeout(bot, guild, interaction.user, target, reason, timeout_seconds) + if err: + await interaction.followup.send(f"❌ {err}", ephemeral=True) + else: + await interaction.followup.send( + f"✅ **{target.name}** a été exclu temporairement ({format_timeout_duration(timeout_seconds)}).", + ephemeral=True, + ) + +def _interaction_must_be_staff_member(interaction: discord.Interaction) -> bool: + return isinstance(interaction.user, discord.Member) and has_staff_role(interaction.user.roles) + +@app_commands.command(name="ban", description="Bannit définitivement un membre du serveur.") +@app_commands.guild_only() +@app_commands.default_permissions(ban_members=True) +@app_commands.describe(utilisateur="Membre à bannir", raison="Raison enregistrée dans les logs") +async def moderation_slash_ban(interaction: discord.Interaction, utilisateur: discord.Member, raison: Optional[str] = None): + if not ConfigurationHelper().getValue('moderation_ban_enable'): + await interaction.response.send_message("❌ La modération (ban) n'est pas activée sur ce serveur.", ephemeral=True) + return + if not _interaction_must_be_staff_member(interaction): + await interaction.response.send_message( + "❌ Vous n'avez pas les permissions nécessaires pour utiliser cette commande.", + ephemeral=True, + ) + return + if utilisateur.bot: + await interaction.response.send_message("❌ Impossible de bannir un bot avec cette commande.", ephemeral=True) + return + reason = _normalized_slash_reason(raison) + await interaction.response.defer(ephemeral=True) + err = await moderation_perform_ban(interaction.client, interaction.guild, interaction.user, utilisateur, reason) + if err: + await interaction.followup.send(f"❌ {err}", ephemeral=True) + else: + await interaction.followup.send(f"✅ **{utilisateur.name}** a été banni.", ephemeral=True) + +@app_commands.command(name="kick", description="Expulse un membre du serveur.") +@app_commands.guild_only() +@app_commands.default_permissions(kick_members=True) +@app_commands.describe(utilisateur="Membre à expulser", raison="Raison enregistrée dans les logs") +async def moderation_slash_kick(interaction: discord.Interaction, utilisateur: discord.Member, raison: Optional[str] = None): + if not ConfigurationHelper().getValue('moderation_kick_enable'): + await interaction.response.send_message("❌ La modération (kick) n'est pas activée sur ce serveur.", ephemeral=True) + return + if not _interaction_must_be_staff_member(interaction): + await interaction.response.send_message( + "❌ Vous n'avez pas les permissions nécessaires pour utiliser cette commande.", + ephemeral=True, + ) + return + if utilisateur.bot: + await interaction.response.send_message("❌ Impossible d'expulser un bot avec cette commande.", ephemeral=True) + return + reason = _normalized_slash_reason(raison) + await interaction.response.defer(ephemeral=True) + err = await moderation_perform_kick(interaction.client, interaction.guild, interaction.user, utilisateur, reason) + if err: + await interaction.followup.send(f"❌ {err}", ephemeral=True) + else: + await interaction.followup.send(f"✅ **{utilisateur.name}** a été expulsé.", ephemeral=True) + +@app_commands.command(name="timeout", description="Exclut temporairement un membre (time out).") +@app_commands.guild_only() +@app_commands.default_permissions(moderate_members=True) +@app_commands.describe( + utilisateur="Membre à exclure temporairement", + duree="Durée : 10m, 1h, 60s, 2j, etc.", + raison="Raison enregistrée dans les logs", +) +async def moderation_slash_timeout( + interaction: discord.Interaction, + utilisateur: discord.Member, + duree: str, + raison: Optional[str] = None, +): + if not ConfigurationHelper().getValue('moderation_enable'): + await interaction.response.send_message("❌ La modération n'est pas activée sur ce serveur.", ephemeral=True) + return + if not _interaction_must_be_staff_member(interaction): + await interaction.response.send_message( + "❌ Vous n'avez pas les permissions nécessaires pour utiliser cette commande.", + ephemeral=True, + ) + return + if utilisateur.bot: + await interaction.response.send_message("❌ Impossible d'exclure temporairement un bot.", ephemeral=True) + return + timeout_seconds = parse_timeout_from_args(duree.strip()) + if not timeout_seconds: + await interaction.response.send_message( + "❌ Durée invalide. Exemples : `10m`, `1h`, `60s`, `2j`.", + ephemeral=True, + ) + return + reason = _normalized_slash_reason(raison) + await interaction.response.defer(ephemeral=True) + err = await moderation_perform_timeout( + interaction.client, interaction.guild, interaction.user, utilisateur, reason, timeout_seconds + ) + if err: + await interaction.followup.send(f"❌ {err}", ephemeral=True) + else: + await interaction.followup.send( + f"✅ **{utilisateur.name}** a été exclu temporairement ({format_timeout_duration(timeout_seconds)}).", + ephemeral=True, + ) + +@app_commands.context_menu(name="Bannir l'auteur") +@app_commands.guild_only() +@app_commands.default_permissions(ban_members=True) +async def moderation_ctx_ban_author(interaction: discord.Interaction, message: discord.Message): + if not ConfigurationHelper().getValue('moderation_ban_enable'): + await interaction.response.send_message("❌ La modération (ban) n'est pas activée sur ce serveur.", ephemeral=True) + return + if not _interaction_must_be_staff_member(interaction): + await interaction.response.send_message( + "❌ Vous n'avez pas les permissions nécessaires pour utiliser cette commande.", + ephemeral=True, + ) + return + if message.author.bot: + await interaction.response.send_message("❌ Impossible de bannir un bot.", ephemeral=True) + return + if message.author.id == interaction.client.user.id: + await interaction.response.send_message("❌ Impossible d'utiliser cette action sur ce message.", ephemeral=True) + return + await interaction.response.send_modal(BanAuthorReasonModal(message)) + +@app_commands.context_menu(name="Expulser l'auteur") +@app_commands.guild_only() +@app_commands.default_permissions(kick_members=True) +async def moderation_ctx_kick_author(interaction: discord.Interaction, message: discord.Message): + if not ConfigurationHelper().getValue('moderation_kick_enable'): + await interaction.response.send_message("❌ La modération (kick) n'est pas activée sur ce serveur.", ephemeral=True) + return + if not _interaction_must_be_staff_member(interaction): + await interaction.response.send_message( + "❌ Vous n'avez pas les permissions nécessaires pour utiliser cette commande.", + ephemeral=True, + ) + return + if message.author.bot: + await interaction.response.send_message("❌ Impossible d'expulser un bot.", ephemeral=True) + return + if message.author.id == interaction.client.user.id: + await interaction.response.send_message("❌ Impossible d'utiliser cette action sur ce message.", ephemeral=True) + return + await interaction.response.send_modal(KickAuthorReasonModal(message)) + +@app_commands.context_menu(name="Exclure temporairement") +@app_commands.guild_only() +@app_commands.default_permissions(moderate_members=True) +async def moderation_ctx_timeout_author(interaction: discord.Interaction, message: discord.Message): + if not ConfigurationHelper().getValue('moderation_enable'): + await interaction.response.send_message("❌ La modération n'est pas activée sur ce serveur.", ephemeral=True) + return + if not _interaction_must_be_staff_member(interaction): + await interaction.response.send_message( + "❌ Vous n'avez pas les permissions nécessaires pour utiliser cette commande.", + ephemeral=True, + ) + return + if message.author.bot: + await interaction.response.send_message("❌ Impossible d'exclure temporairement un bot.", ephemeral=True) + return + if message.author.id == interaction.client.user.id: + await interaction.response.send_message("❌ Impossible d'utiliser cette action sur ce message.", ephemeral=True) + return + await interaction.response.send_modal(TimeoutAuthorModal(message)) + @app_commands.context_menu(name="Déplacer le message") @app_commands.default_permissions(manage_messages=True) async def transfer_message_context_menu(interaction: discord.Interaction, message: discord.Message):