From fd172e2ea056b396e13630196f410f1a0a3eece2 Mon Sep 17 00:00:00 2001 From: Mow Date: Wed, 15 Oct 2025 22:29:41 +0200 Subject: [PATCH] Refonte du systeme d'avertissement et devient un moderation_event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajout de la commande !ban !kick !unban !listevent Ajout du role ID dans le panel , et possibilité d'activé les commandes via le panel --- database/models.py | 4 +- database/schema.sql | 6 +- discordbot/__init__.py | 30 +- discordbot/command.py | 517 ++++++++++++++++++++++++++- webapp/configurations.py | 6 + webapp/moderation.py | 23 +- webapp/templates/configurations.html | 28 ++ webapp/templates/moderation.html | 28 +- 8 files changed, 610 insertions(+), 32 deletions(-) diff --git a/database/models.py b/database/models.py index a9c464a..cf1bee1 100644 --- a/database/models.py +++ b/database/models.py @@ -40,12 +40,14 @@ class Commande(db.Model): trigger = db.Column(db.String(32), unique=True) response = db.Column(db.String(2000)) -class Warning(db.Model): +class ModerationEvent(db.Model): id = db.Column(db.Integer, primary_key=True) + type = db.Column(db.String(32)) username = db.Column(db.String(256)) discord_id = db.Column(db.String(64)) created_at = db.Column(db.DateTime) reason = db.Column(db.String(1024)) staff_id = db.Column(db.String(64)) staff_name = db.Column(db.String(256)) + duration = db.Column(db.Integer) diff --git a/database/schema.sql b/database/schema.sql index e9b4dad..7051d17 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -46,12 +46,14 @@ CREATE TABLE IF NOT EXISTS `commande` ( `response` VARCHAR(2000) NOT NULL ); -CREATE TABLE IF NOT EXISTS `warning` ( +CREATE TABLE IF NOT EXISTS `moderation_event` ( id INTEGER PRIMARY KEY AUTOINCREMENT, + `type` VARCHAR(32) NOT NULL, `username` VARCHAR(256) NOT NULL, `discord_id` VARCHAR(64) NOT NULL, `created_at` DATETIME NOT NULL, `reason` VARCHAR(1024) NOT NULL, `staff_id` VARCHAR(64) NOT NULL, - `staff_name` VARCHAR(256) NOT NULL + `staff_name` VARCHAR(256) NOT NULL, + `duration` INTEGER NULL ); diff --git a/discordbot/__init__.py b/discordbot/__init__.py index f7a7da0..c01b2f5 100644 --- a/discordbot/__init__.py +++ b/discordbot/__init__.py @@ -8,7 +8,7 @@ from database.helpers import ConfigurationHelper from database.models import Configuration, Humeur, Commande from discord import Message, TextChannel from discordbot.humblebundle import checkHumbleBundleAndNotify -from discordbot.command import handle_warning_command +from discordbot.command import handle_warning_command, handle_remove_warning_command, handle_list_warnings_command, handle_ban_command, handle_kick_command, handle_unban_command from protondb import searhProtonDb class DiscordBot(discord.Client): @@ -65,10 +65,32 @@ async def on_message(message: Message): return command_name = message.content.split()[0] - if command_name in ['!averto', '!av', '!avertissement', '!warn']: - await handle_warning_command(message, bot) - return + if ConfigurationHelper().getValue('moderation_enable'): + if command_name in ['!averto', '!av', '!avertissement', '!warn']: + await handle_warning_command(message, bot) + return + if command_name in ['!delaverto', '!removewarn', '!unwarn']: + await handle_remove_warning_command(message, bot) + return + + if command_name in ['!listevent', '!listwarn', '!warnings']: + await handle_list_warnings_command(message, bot) + 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 + + if ConfigurationHelper().getValue('moderation_kick_enable'): + if command_name == '!kick': + await handle_kick_command(message, bot) + return commande = Commande.query.filter_by(discord_enable=True, trigger=command_name).first() if commande: diff --git a/discordbot/command.py b/discordbot/command.py index 2ca851d..ee533d7 100644 --- a/discordbot/command.py +++ b/discordbot/command.py @@ -1,24 +1,80 @@ import discord +import asyncio from datetime import datetime from database import db -from database.models import Warning +from database.helpers import ConfigurationHelper +from database.models import ModerationEvent from discord import Message -STAFF_ROLE_ID = 581990740431732738 +def get_staff_role_id(): + staff_role = ConfigurationHelper().getValue('moderation_staff_role_id') + return int(staff_role) if staff_role else 581990740431732738 + +def get_embed_delete_delay(): + delay = ConfigurationHelper().getValue('moderation_embed_delete_delay') + return int(delay) if delay else 0 + +async def delete_after_delay(message): + delay = get_embed_delete_delay() + if delay > 0: + await asyncio.sleep(delay) + try: + await message.delete() + except: + pass async def handle_warning_command(message: Message, bot): - if not any(role.id == STAFF_ROLE_ID for role in message.author.roles): + if not any(role.id == get_staff_role_id() for role in message.author.roles): + embed = discord.Embed( + title="❌ Accès refusé", + description="Vous n'avez pas les permissions nécessaires pour utiliser cette commande.", + color=discord.Color.red() + ) + await message.channel.send(embed=embed) return parts = message.content.split(maxsplit=2) - if len(parts) < 2 or not message.mentions: + if len(parts) < 2: + embed = discord.Embed( + title="📋 Utilisation de la commande", + description="**Syntaxe :** `!averto @utilisateur [raison]` ou `!averto [raison]`", + color=discord.Color.blue() + ) + embed.add_field(name="Exemples", value="• `!averto @User Spam dans le chat`\n• `!warn 123456789012345678 Comportement inapproprié`\n• `!av @User`", inline=False) + embed.add_field(name="Aliases", value="`!averto`, `!av`, `!avertissement`, `!warn`", inline=False) + await message.channel.send(embed=embed) return - target_user = message.mentions[0] - reason = parts[2] if len(parts) > 2 else "Sans raison" + target_user = None + if message.mentions: + target_user = message.mentions[0] + reason = parts[2] if len(parts) > 2 else "Sans raison" + else: + try: + user_id = int(parts[1]) + target_user = await bot.fetch_user(user_id) + reason = parts[2] if len(parts) > 2 else "Sans raison" + except (ValueError, discord.NotFound): + embed = discord.Embed( + title="❌ Erreur", + description="Utilisateur introuvable. Vérifiez la mention ou l'ID Discord.", + color=discord.Color.red() + ) + await message.channel.send(embed=embed) + return - warning = Warning( + if not target_user: + embed = discord.Embed( + title="❌ Erreur", + description="Impossible de trouver l'utilisateur.", + color=discord.Color.red() + ) + await message.channel.send(embed=embed) + return + + event = ModerationEvent( + type='warning', username=target_user.name, discord_id=str(target_user.id), created_at=datetime.utcnow(), @@ -26,19 +82,452 @@ async def handle_warning_command(message: Message, bot): staff_id=str(message.author.id), staff_name=message.author.name ) - db.session.add(warning) + db.session.add(event) db.session.commit() embed = discord.Embed( - title="⚠️ Avertissement", - description=f"{target_user.mention} a reçu un avertissement de la part de l'équipe de modération", - color=discord.Color.red(), + title="⚠️ Sanction", + description=f"L'utilisateur **{target_user.name}** (`@{target_user.name}`) a été **averti**.", + color=discord.Color.orange() + ) + if reason != "Sans raison": + embed.add_field(name="Raison", value=reason, inline=False) + + sent_message = await message.channel.send(embed=embed) + await message.delete() + asyncio.create_task(delete_after_delay(sent_message)) + +async def handle_remove_warning_command(message: Message, bot): + if not any(role.id == get_staff_role_id() for role in message.author.roles): + embed = discord.Embed( + title="❌ Accès refusé", + description="Vous n'avez pas les permissions nécessaires pour utiliser cette commande.", + color=discord.Color.red() + ) + await message.channel.send(embed=embed) + return + + parts = message.content.split(maxsplit=1) + + if len(parts) < 2: + embed = discord.Embed( + title="📋 Utilisation de la commande", + description="**Syntaxe :** `!delaverto `", + color=discord.Color.blue() + ) + embed.add_field(name="Exemples", value="• `!delaverto 5`\n• `!removewarn 12`", inline=False) + embed.add_field(name="Aliases", value="`!delaverto`, `!removewarn`, `!delwarn`", inline=False) + await message.channel.send(embed=embed) + return + + try: + event_id = int(parts[1]) + except ValueError: + embed = discord.Embed( + title="❌ Erreur", + description="L'ID doit être un nombre entier.", + color=discord.Color.red() + ) + await message.channel.send(embed=embed) + return + + event = ModerationEvent.query.filter_by(id=event_id).first() + + if not event: + embed = discord.Embed( + title="❌ Erreur", + description=f"Aucun événement de modération trouvé avec l'ID `{event_id}`.", + color=discord.Color.red() + ) + await message.channel.send(embed=embed) + return + + username = event.username + event_type = event.type + + db.session.delete(event) + db.session.commit() + + embed = discord.Embed( + title="✅ Événement supprimé", + description=f"L'événement de type **{event_type}** pour **{username}** (ID: {event_id}) a été supprimé.", + color=discord.Color.green(), timestamp=datetime.utcnow() ) - 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}\n`{message.author.id}`", inline=True) - embed.add_field(name="📝 Raison", value=reason, inline=False) + embed.add_field(name="🛡️ Modérateur", value=f"{message.author.name}\n`{message.author.id}`", inline=True) embed.set_footer(text="Mamie Henriette") await message.channel.send(embed=embed) await message.delete() + +async def handle_list_warnings_command(message: Message, bot): + if not any(role.id == get_staff_role_id() for role in message.author.roles): + embed = discord.Embed( + title="❌ Accès refusé", + description="Vous n'avez pas les permissions nécessaires pour utiliser cette commande.", + color=discord.Color.red() + ) + await message.channel.send(embed=embed) + return + + parts = message.content.split(maxsplit=1) + user_filter = None + + if len(parts) > 1 and message.mentions: + user_filter = str(message.mentions[0].id) + + if user_filter: + events = ModerationEvent.query.filter_by(discord_id=user_filter).order_by(ModerationEvent.created_at.desc()).all() + else: + events = ModerationEvent.query.order_by(ModerationEvent.created_at.desc()).all() + + if not events: + embed = discord.Embed( + title="📋 Liste des événements", + description="Aucun événement de modération trouvé.", + color=discord.Color.blue() + ) + await message.channel.send(embed=embed) + return + + page = 0 + per_page = 5 + max_page = (len(events) - 1) // per_page + + def create_embed(page_num): + start = page_num * per_page + end = start + per_page + page_events = events[start:end] + + embed = discord.Embed( + title="📋 Liste des événements de modération", + description=f"Total : {len(events)} événement(s)", + color=discord.Color.blue(), + timestamp=datetime.utcnow() + ) + + for event in page_events: + date_str = event.created_at.strftime('%d/%m/%Y %H:%M') if event.created_at else 'N/A' + embed.add_field( + name=f"ID {event.id} - {event.type.upper()} - {event.username}", + value=f"**Discord ID:** `{event.discord_id}`\n**Date:** {date_str}\n**Raison:** {event.reason}\n**Staff:** {event.staff_name}", + inline=False + ) + + embed.set_footer(text=f"Page {page_num + 1}/{max_page + 1}") + return embed + + msg = await message.channel.send(embed=create_embed(page)) + + if max_page > 0: + await msg.add_reaction('⬅️') + await msg.add_reaction('➡️') + await msg.add_reaction('❌') + + def check(reaction, user): + return user == message.author and str(reaction.emoji) in ['⬅️', '➡️', '❌'] and reaction.message.id == msg.id + + while True: + try: + reaction, user = await bot.wait_for('reaction_add', timeout=60.0, check=check) + + if str(reaction.emoji) == '❌': + await msg.delete() + break + elif str(reaction.emoji) == '➡️' and page < max_page: + page += 1 + await msg.edit(embed=create_embed(page)) + elif str(reaction.emoji) == '⬅️' and page > 0: + page -= 1 + await msg.edit(embed=create_embed(page)) + + await msg.remove_reaction(reaction, user) + except: + break + + if msg: + try: + await msg.clear_reactions() + except: + pass + + await message.delete() + +async def handle_ban_command(message: Message, bot): + if not any(role.id == get_staff_role_id() for role in message.author.roles): + embed = discord.Embed( + title="❌ Accès refusé", + description="Vous n'avez pas les permissions nécessaires pour utiliser cette commande.", + color=discord.Color.red() + ) + await message.channel.send(embed=embed) + return + + parts = message.content.split(maxsplit=2) + + if len(parts) < 2: + 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) + await message.channel.send(embed=embed) + return + + target_user = None + if message.mentions: + target_user = message.mentions[0] + reason = parts[2] if len(parts) > 2 else "Sans raison" + else: + try: + user_id = int(parts[1]) + target_user = await bot.fetch_user(user_id) + reason = parts[2] if len(parts) > 2 else "Sans raison" + except (ValueError, discord.NotFound): + embed = discord.Embed( + title="❌ Erreur", + description="Utilisateur introuvable. Vérifiez la mention ou l'ID Discord.", + color=discord.Color.red() + ) + await message.channel.send(embed=embed) + return + + if not target_user: + embed = discord.Embed( + title="❌ Erreur", + description="Impossible de trouver l'utilisateur.", + color=discord.Color.red() + ) + await message.channel.send(embed=embed) + return + + member = message.guild.get_member(target_user.id) + joined_days = None + if member and member.joined_at: + delta = datetime.utcnow() - member.joined_at.replace(tzinfo=None) + joined_days = delta.days + + try: + await message.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() + ) + await message.channel.send(embed=embed) + return + + event = ModerationEvent( + type='ban', + username=target_user.name, + discord_id=str(target_user.id), + created_at=datetime.utcnow(), + reason=reason, + staff_id=str(message.author.id), + staff_name=message.author.name + ) + db.session.add(event) + db.session.commit() + + embed = discord.Embed( + title="⚠️ Sanction", + description=f"L'utilisateur **{target_user.name}** (`@{target_user.name}`) a été **banni**.", + color=discord.Color.red() + ) + embed.add_field(name="ID Discord", value=f"`{target_user.id}`", inline=False) + if joined_days is not None: + embed.add_field(name="Membre depuis", value=f"{joined_days} jour{'s' if joined_days > 1 else ''}", inline=False) + if reason != "Sans raison": + embed.add_field(name="Raison", value=reason, inline=False) + + sent_message = await message.channel.send(embed=embed) + await message.delete() + asyncio.create_task(delete_after_delay(sent_message)) + +async def handle_kick_command(message: Message, bot): + if not any(role.id == get_staff_role_id() for role in message.author.roles): + embed = discord.Embed( + title="❌ Accès refusé", + description="Vous n'avez pas les permissions nécessaires pour utiliser cette commande.", + color=discord.Color.red() + ) + await message.channel.send(embed=embed) + return + + parts = message.content.split(maxsplit=2) + + if len(parts) < 2 or not message.mentions: + embed = discord.Embed( + title="📋 Utilisation de la commande", + description="**Syntaxe :** `!kick @utilisateur [raison]`", + color=discord.Color.blue() + ) + embed.add_field(name="Exemples", value="• `!kick @User Spam dans le chat`\n• `!kick @User Comportement inapproprié`", inline=False) + await message.channel.send(embed=embed) + return + + target_member = message.mentions[0] + reason = parts[2] if len(parts) > 2 else "Sans raison" + + 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() + ) + await message.channel.send(embed=embed) + return + + joined_days = None + if member_obj.joined_at: + delta = datetime.utcnow() - member_obj.joined_at.replace(tzinfo=None) + 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() + ) + await message.channel.send(embed=embed) + return + + event = ModerationEvent( + type='kick', + username=target_member.name, + discord_id=str(target_member.id), + created_at=datetime.utcnow(), + reason=reason, + staff_id=str(message.author.id), + staff_name=message.author.name + ) + db.session.add(event) + db.session.commit() + + embed = discord.Embed( + title="⚠️ Sanction", + description=f"L'utilisateur **{target_member.name}** (`@{target_member.name}`) a été **expulsé**.", + color=discord.Color.orange() + ) + if joined_days is not None: + embed.add_field(name="Membre depuis", value=f"{joined_days} jour{'s' if joined_days > 1 else ''}", inline=False) + if reason != "Sans raison": + embed.add_field(name="Raison", value=reason, inline=False) + + sent_message = await message.channel.send(embed=embed) + await message.delete() + asyncio.create_task(delete_after_delay(sent_message)) + +async def handle_unban_command(message: Message, bot): + if not any(role.id == get_staff_role_id() for role in message.author.roles): + embed = discord.Embed( + title="❌ Accès refusé", + description="Vous n'avez pas les permissions nécessaires pour utiliser cette commande.", + color=discord.Color.red() + ) + await message.channel.send(embed=embed) + return + + parts = message.content.split(maxsplit=2) + + if len(parts) < 2: + embed = discord.Embed( + title="📋 Utilisation de la commande", + description="**Syntaxe :** `!unban ` ou `!unban # [raison]`", + color=discord.Color.blue() + ) + embed.add_field(name="Exemples", value="• `!unban 123456789012345678`\n• `!unban #5 Appel accepté`", inline=False) + await message.channel.send(embed=embed) + return + + reason = parts[2] if len(parts) > 2 else "Sans raison" + target_user = None + discord_id = None + + if parts[1].startswith('#'): + try: + sanction_id = int(parts[1][1:]) + event = ModerationEvent.query.filter_by(id=sanction_id, type='ban').first() + if not event: + embed = discord.Embed( + title="❌ Erreur", + description=f"Aucune sanction de ban trouvée avec l'ID #{sanction_id}.", + color=discord.Color.red() + ) + await message.channel.send(embed=embed) + return + discord_id = event.discord_id + try: + target_user = await bot.fetch_user(int(discord_id)) + except discord.NotFound: + pass + except ValueError: + embed = discord.Embed( + title="❌ Erreur", + description="ID de sanction invalide.", + color=discord.Color.red() + ) + await message.channel.send(embed=embed) + return + else: + try: + discord_id = parts[1] + target_user = await bot.fetch_user(int(discord_id)) + except (ValueError, discord.NotFound): + embed = discord.Embed( + title="❌ Erreur", + description="ID Discord invalide ou utilisateur introuvable.", + color=discord.Color.red() + ) + await message.channel.send(embed=embed) + return + + try: + await message.guild.unban(discord.Object(id=int(discord_id)), reason=reason) + except discord.NotFound: + embed = discord.Embed( + title="❌ Erreur", + description="Cet utilisateur n'est pas banni.", + color=discord.Color.red() + ) + await message.channel.send(embed=embed) + return + except discord.Forbidden: + embed = discord.Embed( + title="❌ Erreur", + description="Je n'ai pas les permissions nécessaires pour débannir cet utilisateur.", + color=discord.Color.red() + ) + await message.channel.send(embed=embed) + return + + username = target_user.name if target_user else f"ID: {discord_id}" + + event = ModerationEvent( + type='unban', + username=username, + discord_id=discord_id, + created_at=datetime.utcnow(), + reason=reason, + staff_id=str(message.author.id), + staff_name=message.author.name + ) + db.session.add(event) + db.session.commit() + + embed = discord.Embed( + title="⚠️ Sanction", + description=f"L'utilisateur **{username}** (`@{username}`) a été **débanni**.", + color=discord.Color.green() + ) + if reason != "Sans raison": + embed.add_field(name="Raison", value=reason, inline=False) + + sent_message = await message.channel.send(embed=embed) + await message.delete() + asyncio.create_task(delete_after_delay(sent_message)) diff --git a/webapp/configurations.py b/webapp/configurations.py index 93d5d0b..fbe53d1 100644 --- a/webapp/configurations.py +++ b/webapp/configurations.py @@ -17,6 +17,12 @@ def updateConfiguration(): ConfigurationHelper().createOrUpdate('humble_bundle_enable', False) if (request.form.get("proton_db_api_id") != None and request.form.get("proton_db_enable_enable") == None) : ConfigurationHelper().createOrUpdate('proton_db_enable_enable', False) + if (request.form.get("moderation_staff_role_id") != None and request.form.get("moderation_enable") == None) : + ConfigurationHelper().createOrUpdate('moderation_enable', False) + if (request.form.get("moderation_staff_role_id") != None and request.form.get("moderation_ban_enable") == None) : + ConfigurationHelper().createOrUpdate('moderation_ban_enable', False) + if (request.form.get("moderation_staff_role_id") != None and request.form.get("moderation_kick_enable") == None) : + ConfigurationHelper().createOrUpdate('moderation_kick_enable', False) db.session.commit() return redirect(request.referrer) diff --git a/webapp/moderation.py b/webapp/moderation.py index ae1df0f..b26cf47 100644 --- a/webapp/moderation.py +++ b/webapp/moderation.py @@ -1,9 +1,24 @@ -from flask import render_template +from flask import render_template, request, redirect, url_for from webapp import webapp -from database.models import Warning +from database import db +from database.models import ModerationEvent @webapp.route("/moderation") def moderation(): - warnings = Warning.query.order_by(Warning.created_at.desc()).all() - return render_template("moderation.html", warnings=warnings) + events = ModerationEvent.query.order_by(ModerationEvent.created_at.desc()).all() + return render_template("moderation.html", events=events) + +@webapp.route("/moderation/update/", methods=['POST']) +def update_moderation_event(event_id): + event = ModerationEvent.query.get_or_404(event_id) + event.reason = request.form.get('reason') + db.session.commit() + return redirect(url_for('moderation')) + +@webapp.route("/moderation/delete/") +def delete_moderation_event(event_id): + event = ModerationEvent.query.get_or_404(event_id) + db.session.delete(event) + db.session.commit() + return redirect(url_for('moderation')) diff --git a/webapp/templates/configurations.html b/webapp/templates/configurations.html index c8c43f3..47a465c 100644 --- a/webapp/templates/configurations.html +++ b/webapp/templates/configurations.html @@ -55,4 +55,32 @@ + +

Modération Discord

+
+ + + + + + + + + + + + + + + + + + + +
{% endblock %} \ No newline at end of file diff --git a/webapp/templates/moderation.html b/webapp/templates/moderation.html index 77de26d..58f35cd 100644 --- a/webapp/templates/moderation.html +++ b/webapp/templates/moderation.html @@ -2,25 +2,39 @@ {% block content %}

Modération Discord

-

Liste des avertissements émis sur le serveur Discord.

+

Historique des actions de modération sur le serveur Discord.

+ + + - {% for warning in warnings %} + {% for event in events %} - - - - - + + + + + + + + {% endfor %}
IDType Utilisateur Discord ID Date & Heure Raison StaffActions
{{ warning.username }}{{ warning.discord_id }}{{ warning.created_at.strftime('%d/%m/%Y %H:%M') if warning.created_at else 'N/A' }}{{ warning.reason }}{{ warning.staff_name }}{{ event.id }}{{ event.type }}{{ event.username }}{{ event.discord_id }}{{ event.created_at.strftime('%d/%m/%Y %H:%M') if event.created_at else 'N/A' }} +
+ +
+
{{ event.staff_name }} + Modifier + | + 🗑️ Supprimer +