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.
This commit is contained in:
2026-05-03 14:36:44 +02:00
parent 3e57accb11
commit 2b8b725bbb
2 changed files with 420 additions and 314 deletions
+18 -19
View File
@@ -14,16 +14,19 @@ from discordbot.moderation import (
handle_warning_command, handle_warning_command,
handle_remove_warning_command, handle_remove_warning_command,
handle_list_warnings_command, handle_list_warnings_command,
handle_ban_command,
handle_kick_command,
handle_unban_command, handle_unban_command,
handle_inspect_command, handle_inspect_command,
handle_ban_list_command, handle_ban_list_command,
handle_staff_help_command, handle_staff_help_command,
handle_timeout_command,
handle_say_command, handle_say_command,
handle_transfer_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.welcome import sendWelcomeMessage, sendLeaveMessage, updateInviteCache
from discordbot.patreon import checkPatreonPosts from discordbot.patreon import checkPatreonPosts
@@ -38,8 +41,17 @@ class DiscordBot(discord.Client):
self.synced = False self.synced = False
async def setup_hook(self): async def setup_hook(self):
self.tree.add_command(transfer_message_context_menu) for cmd in (
logging.info("Commande contextuelle 'Déplacer le message' ajoutée au CommandTree") 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): async def on_ready(self):
logging.info(f'Connecté en tant que {self.user} (ID: {self.user.id})') 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) await handle_warning_command(message, bot)
return return
if command_name in ['!to', '!timeout']:
await handle_timeout_command(message, bot)
return
if command_name in ['!delaverto', '!removewarn', '!unwarn']: if command_name in ['!delaverto', '!removewarn', '!unwarn']:
await handle_remove_warning_command(message, bot) await handle_remove_warning_command(message, bot)
return return
@@ -190,10 +198,6 @@ async def on_message(message: Message):
return return
if ConfigurationHelper().getValue('moderation_ban_enable'): if ConfigurationHelper().getValue('moderation_ban_enable'):
if command_name == '!ban':
await handle_ban_command(message, bot)
return
if command_name == '!unban': if command_name == '!unban':
await handle_unban_command(message, bot) await handle_unban_command(message, bot)
return return
@@ -201,11 +205,6 @@ async def on_message(message: Message):
await handle_ban_list_command(message, bot) await handle_ban_list_command(message, bot)
return return
if ConfigurationHelper().getValue('moderation_kick_enable'):
if command_name == '!kick':
await handle_kick_command(message, bot)
return
if ConfigurationHelper().getValue('moderation_enable'): if ConfigurationHelper().getValue('moderation_enable'):
if command_name == '!inspect': if command_name == '!inspect':
await handle_inspect_command(message, bot) await handle_inspect_command(message, bot)
+402 -295
View File
@@ -5,6 +5,7 @@ import os
import re import re
import discord import discord
import io import io
from typing import Optional
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from database import db 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) 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): def parse_timeout_from_args(duration_str: str):
match = re.match(r'^(\d+)([smhj])?$', duration_str.lower()) match = re.match(r'^(\d+)([smhj])?$', duration_str.lower())
if not match: if not match:
@@ -323,118 +312,6 @@ def parse_timeout_from_args(duration_str: str):
return value * 86400 return value * 86400
return None 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): async def send_remove_warning_usage(channel):
embed = discord.Embed( embed = discord.Embed(
title="📋 Utilisation de la commande", 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 handle_pagination_loop(msg, bot, message.author, events, per_page)
await safe_delete_message(message) 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 <id> [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): def _create_ban_event(target_user, reason: str, staff_member):
event = ModerationEvent( event = ModerationEvent(
type='ban', type='ban',
username=target_user.name, username=target_user.name,
discord_id=str(target_user.id), discord_id=str(target_user.id),
created_at=datetime.now(timezone.utc), created_at=datetime.now(timezone.utc),
reason=reason, reason=reason,
staff_id=str(staff_member.id), staff_id=str(staff_member.id),
staff_name=staff_member.name staff_name=staff_member.name
@@ -661,26 +496,33 @@ def _create_ban_event(target_user, reason: str, staff_member):
_commit_with_retry() _commit_with_retry()
return event return event
async def _process_ban_success(message: Message, target_user, reason: str, bot): def _normalized_slash_reason(raison: Optional[str]) -> str:
member = message.guild.get_member(target_user.id) 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 joined_days = None
if member and member.joined_at: 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)) 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 joined_days = delta.days
try: try:
await message.guild.ban(target_user, reason=reason) await guild.ban(target_user, reason=reason)
except discord.Forbidden: except discord.Forbidden:
embed = discord.Embed( return "Je n'ai pas les permissions nécessaires pour bannir cet utilisateur."
title="❌ Erreur", _create_ban_event(target_user, reason, staff)
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)
local_now = _to_local(datetime.now(timezone.utc)) local_now = _to_local(datetime.now(timezone.utc))
embed = discord.Embed( embed = discord.Embed(
title="🔨 Bannissement", title="🔨 Bannissement",
@@ -689,16 +531,119 @@ async def _process_ban_success(message: Message, target_user, reason: str, bot):
timestamp=datetime.now(timezone.utc) 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="👤 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) embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True)
if joined_days is not None: if joined_days is not None:
embed.add_field(name="⏱️ Membre depuis", value=format_days_to_age(joined_days), inline=True) embed.add_field(name="⏱️ Membre depuis", value=format_days_to_age(joined_days), inline=True)
if reason != "Sans raison": if reason != "Sans raison":
embed.add_field(name="📝 Raison", value=reason, inline=False) 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 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): async def handle_unban_command(message: Message, bot):
parts = message.content.split(maxsplit=2) parts = message.content.split(maxsplit=2)
if not has_staff_role(message.author.roles): 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" " Donne un avertissement\n"
"• `!warn @utilisateur raison --to durée`\n" "• `!warn @utilisateur raison --to durée`\n"
" Avertissement + time out temporaire\n\n" " Avertissement + time out temporaire\n\n"
"**Time out uniquement:**\n" "**Time out (sans avertissement):**\n"
"• `!to @utilisateur durée raison`\n" "• `/timeout` — commande slash (membre, durée, raison optionnelle)\n"
" *Alias: !timeout*\n" "• Clic droit sur un message → Applications → **Exclure temporairement**\n"
" Time out (sans avertissement)\n"
" *Durées: 10s, 5m, 1h, 2j*\n\n" " *Durées: 10s, 5m, 1h, 2j*\n\n"
"**Gestion:**\n" "**Gestion:**\n"
"• `!delaverto id` - Supprime un événement\n" "• `!delaverto id` - Supprime un événement\n"
@@ -1017,7 +961,7 @@ async def handle_staff_help_command(message: Message, bot):
"**Exemples:**\n" "**Exemples:**\n"
"`!warn @User Spam`\n" "`!warn @User Spam`\n"
"`!warn @User Flood --to 10m` (averto + timeout)\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`" "`!warnings @User`"
) )
embed.add_field(name="⚠️ Avertissements & Time out", value=value, inline=False) 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'): if ConfigurationHelper().getValue('moderation_ban_enable'):
value = ( value = (
"• `!ban @utilisateur raison`\n" "• `/ban` — bannit un membre (raison optionnelle)\n"
" Bannit définitivement un utilisateur\n" "• Clic droit sur un message → Applications → **Bannir l'auteur**\n"
"• `!unban discord_id` ou `!unban #sanction_id raison`\n" "• `!unban discord_id` ou `!unban #sanction_id raison`\n"
" Révoque le ban et envoie une invitation\n" " Révoque le ban et envoie une invitation\n"
"• `!banlist`\n" "• `!banlist`\n"
" Affiche la liste des utilisateurs bannis\n" " Affiche la liste des utilisateurs bannis\n"
"Exemples:\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 123456789012345678 Erreur de modération`\n"
"`!unban #5 Appel accepté`" "`!unban #5 Appel accepté`"
) )
@@ -1046,9 +990,8 @@ async def handle_staff_help_command(message: Message, bot):
if ConfigurationHelper().getValue('moderation_kick_enable'): if ConfigurationHelper().getValue('moderation_kick_enable'):
value = ( value = (
"• `!kick @utilisateur raison` ou `!kick <id> raison`\n" "• `/kick` — expulse un membre (raison optionnelle)\n"
" Expulse temporairement un utilisateur du serveur\n" "• Clic droit sur un message → Applications → **Expulser l'auteur**"
"Exemples: `!kick @User Spam de liens` ou `!kick 123456789012345678 Spam`"
) )
embed.add_field(name="👢 Expulsion", value=value, inline=False) embed.add_field(name="👢 Expulsion", value=value, inline=False)
@@ -1078,105 +1021,6 @@ async def handle_staff_help_command(message: Message, bot):
pass pass
await safe_delete_message(message) 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 <id> [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: def format_days_to_age(days: int) -> str:
if days >= 365: if days >= 365:
years = days // 365 years = days // 365
@@ -1935,6 +1779,269 @@ class TransferView(View):
super().__init__(timeout=180) super().__init__(timeout=180)
self.add_item(TransferChannelSelect(message, bot)) 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.context_menu(name="Déplacer le message")
@app_commands.default_permissions(manage_messages=True) @app_commands.default_permissions(manage_messages=True)
async def transfer_message_context_menu(interaction: discord.Interaction, message: discord.Message): async def transfer_message_context_menu(interaction: discord.Interaction, message: discord.Message):