From be8bf3fa5c139bd4aabf73042dea253e0d222767 Mon Sep 17 00:00:00 2001 From: Mathis Date: Fri, 13 Mar 2026 22:42:08 +0100 Subject: [PATCH 1/3] =?UTF-8?q?Mise=20=C3=A0=20jour=20du=20gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 48283f0..4af7f38 100755 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,38 @@ -# Fichiers d'environnement (contiennent des tokens/clés sensibles) +# --- SÉCURITÉ --- +# Identifiants et tokens (LE PLUS IMPORTANT) .env -# Fichiers de configuration générés + +# --- DONNÉES LOCALES (Ignorées pour la prod) --- +# Fichiers JSON de config et BDD (pour ne pas les écraser lors du git reset) config.json whitelist.json bridges.json -config.db malware.json +config.db +*.db +*.db-journal +*.db-wal +*.db-shm -# Cache Python +# Dossiers de stockage +data/ +.data/ +backups/ # <--- IMPORTANT : pour ne pas versionner tes sauvegardes ZIP + +# --- PYTHON --- __pycache__/ -*.pyc -*.pyo -*.pyd - -# Environnements virtuels +*.py[cod] +*$py.class venv/ -env/ .venv/ +env/ -# IDE +# --- LOGS --- +*.log +*.Logs +logs/ + +# --- IDE & SYSTÈME --- .vscode/ .idea/ - -# Logs -*.Logs -*.log -.data +.DS_Store \ No newline at end of file From fd9251fc6ec38d34bbf2b1452ba51d8f1e055625 Mon Sep 17 00:00:00 2001 From: Mathis Date: Sat, 14 Mar 2026 15:40:19 +0100 Subject: [PATCH 2/3] =?UTF-8?q?Modularisation=20du=20syst=C3=A8me=20de=20t?= =?UTF-8?q?icket=20&=20ajout=20des=20boutons=20persistant=20syst=C3=A8me?= =?UTF-8?q?=20WL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- commandes/ticket_whitelist.py | 345 +++++++++++++++++++++++++++------- data/json-export-history.json | 82 ++++++-- 2 files changed, 340 insertions(+), 87 deletions(-) diff --git a/commandes/ticket_whitelist.py b/commandes/ticket_whitelist.py index 7e3f535..c730e3d 100644 --- a/commandes/ticket_whitelist.py +++ b/commandes/ticket_whitelist.py @@ -1,13 +1,138 @@ import discord from discord.ext import commands -from discord import app_commands +from discord import app_commands, ui +import json +import os +from typing import Optional + +TICKET_WHITELIST_SETTINGS_FILE = "data/ticket_whitelist_settings.json" + +def load_settings(guild_id: int) -> dict: + """Charge les paramètres de ticket pour un serveur""" + if not os.path.exists(TICKET_WHITELIST_SETTINGS_FILE): + return {} + try: + with open(TICKET_WHITELIST_SETTINGS_FILE, "r", encoding='utf-8') as f: + return json.load(f).get(str(guild_id), {}) + except (json.JSONDecodeError, FileNotFoundError): + return {} + +def save_settings(guild_id: int, settings: dict) -> None: + """Sauvegarde les paramètres de ticket""" + data = {} + if os.path.exists(TICKET_WHITELIST_SETTINGS_FILE): + try: + with open(TICKET_WHITELIST_SETTINGS_FILE, "r", encoding='utf-8') as f: + data = json.load(f) + except (json.JSONDecodeError, FileNotFoundError): + pass + + data[str(guild_id)] = settings + os.makedirs(os.path.dirname(TICKET_WHITELIST_SETTINGS_FILE), exist_ok=True) + with open(TICKET_WHITELIST_SETTINGS_FILE, "w", encoding='utf-8') as f: + json.dump(data, f, indent=4, ensure_ascii=False) + +class TicketWhitelistConfigView(ui.View): + def __init__(self, guild_id: int): + super().__init__(timeout=60) + self.guild_id = guild_id + + async def prompt_for_id(self, interaction: discord.Interaction, prompt_text: str, is_category: bool = False): + await interaction.response.send_message(prompt_text, ephemeral=True) + + def check(m): + return m.author == interaction.user and m.channel == interaction.channel + + try: + # We use an explicit import here to avoid clashing if __import__ syntax is weird, but using standard asyncio module is fine if we import it at top. Let's add import asyncio at the top instead! + pass + except __import__("asyncio").TimeoutError: + pass + + try: + msg = await interaction.client.wait_for('message', check=check, timeout=30) + target = None + if is_category: + if msg.content.isdigit(): + target = discord.utils.get(interaction.guild.categories, id=int(msg.content)) + if not target: + await interaction.followup.send("❌ Catégorie introuvable avec cet ID.", ephemeral=True) + return None + else: + await interaction.followup.send("❌ Veuillez envoyer l'ID (les chiffres) de la catégorie.", ephemeral=True) + return None + else: + if msg.role_mentions: + target = msg.role_mentions[0] + elif msg.content.isdigit(): + target = interaction.guild.get_role(int(msg.content)) + + if not target: + await interaction.followup.send("❌ ID ou mention invalide.", ephemeral=True) + return None + + try: await msg.delete() + except: pass + return target + + except __import__("asyncio").TimeoutError: + await interaction.followup.send("❌ Temps écoulé.", ephemeral=True) + return None + + @ui.button(label="Catégorie Tickets", style=discord.ButtonStyle.primary, row=0, emoji="📁") + async def set_category(self, interaction: discord.Interaction, button: ui.Button): + category = await self.prompt_for_id(interaction, "Envoie l'ID de la **catégorie** où créer les tickets :", is_category=True) + if category: + settings = load_settings(self.guild_id) + settings["ticket_category_id"] = category.id + save_settings(self.guild_id, settings) + await interaction.followup.send(f"✅ Catégorie configurée sur **{category.name}**", ephemeral=True) + + @ui.button(label="Rôle Staff", style=discord.ButtonStyle.primary, row=0, emoji="🛡️") + async def set_staff_role(self, interaction: discord.Interaction, button: ui.Button): + role = await self.prompt_for_id(interaction, "Mentionne le **rôle Staff** ou envoie son ID :") + if role: + settings = load_settings(self.guild_id) + settings["staff_role_id"] = role.id + save_settings(self.guild_id, settings) + await interaction.followup.send(f"✅ Rôle Staff configuré sur **{role.name}**", ephemeral=True) + + @ui.button(label="Rôle Équipe Staff", style=discord.ButtonStyle.primary, row=0, emoji="👥") + async def set_team_staff_role(self, interaction: discord.Interaction, button: ui.Button): + role = await self.prompt_for_id(interaction, "Mentionne le **rôle Équipe Staff** (à ping pour les nouveaux tickets) ou envoie son ID :") + if role: + settings = load_settings(self.guild_id) + settings["team_staff_role_id"] = role.id + save_settings(self.guild_id, settings) + await interaction.followup.send(f"✅ Rôle Équipe Staff configuré sur **{role.name}**", ephemeral=True) + + @ui.button(label="Rôle Attente Oral", style=discord.ButtonStyle.secondary, row=1, emoji="🎤") + async def set_attente_oral_role(self, interaction: discord.Interaction, button: ui.Button): + role = await self.prompt_for_id(interaction, "Mentionne le rôle **Attente Oral** ou envoie son ID :") + if role: + settings = load_settings(self.guild_id) + settings["attente_oral_role_id"] = role.id + save_settings(self.guild_id, settings) + await interaction.followup.send(f"✅ Rôle Attente Oral configuré sur **{role.name}**", ephemeral=True) + + @ui.button(label="Rôle Candidature à faire", style=discord.ButtonStyle.secondary, row=1, emoji="📝") + async def set_candidature_a_faire_role(self, interaction: discord.Interaction, button: ui.Button): + role = await self.prompt_for_id(interaction, "Mentionne le rôle **Candidature à faire** ou envoie son ID :") + if role: + settings = load_settings(self.guild_id) + settings["candidature_a_faire_role_id"] = role.id + save_settings(self.guild_id, settings) + await interaction.followup.send(f"✅ Rôle Candidature à faire configuré sur **{role.name}**", ephemeral=True) + + @ui.button(label="Rôle Citoyen", style=discord.ButtonStyle.success, row=1, emoji="🏙️") + async def set_citoyen_role(self, interaction: discord.Interaction, button: ui.Button): + role = await self.prompt_for_id(interaction, "Mentionne le rôle **Citoyen** ou envoie son ID :") + if role: + settings = load_settings(self.guild_id) + settings["citoyen_role_id"] = role.id + save_settings(self.guild_id, settings) + await interaction.followup.send(f"✅ Rôle Citoyen configuré sur **{role.name}**", ephemeral=True) -TICKET_CATEGORY_ID = 1450001640127205388 # ID de la catégorie des tickets -STAFF_ROLE_ID = 1454042474141192316 # ID du rôle staff -TEAM_STAFF_ROLE_ID = 1454042474141192316 # ID du rôle équipe staff -ATTENTE_ORAL_ROLE_ID = 11464424211442307279 # ID du rôle "attente oral" -CANDIDATURE_A_FAIRE_ROLE_ID = 1464428034906919038 # ID du rôle "candidature à faire" -CITOYEN_ROLE_ID = 1453708386674806835 # ID du rôle "citoyen" class ContinueFormView(discord.ui.View): """Vue avec un bouton pour continuer à l'étape suivante""" @@ -108,43 +233,45 @@ class TicketStep3(discord.ui.Modal, title="📋 Développement du Personnage"): """Création du ticket après validation""" guild = interaction.guild user = interaction.user - category = discord.utils.get(guild.categories, id=TICKET_CATEGORY_ID) + + settings = load_settings(guild.id) + category_id = settings.get("ticket_category_id") + staff_role_id = settings.get("staff_role_id") + team_staff_role_id = settings.get("team_staff_role_id") - # Vérifier si la catégorie existe + if not category_id or not staff_role_id or not team_staff_role_id: + return await interaction.response.send_message("❌ Erreur : Le système de tickets n'est pas entièrement configuré sur ce serveur (Catégorie, Rôle Staff ou Rôle Équipe Staff manquant).", ephemeral=True) + + category = discord.utils.get(guild.categories, id=int(category_id)) if category is None: - await interaction.response.send_message("❌ Erreur : La catégorie des tickets n'existe pas !", ephemeral=True) - return + return await interaction.response.send_message("❌ Erreur : La catégorie des tickets configurée n'existe plus !", ephemeral=True) - # Vérifier si le rôle staff existe - staff_role = discord.utils.get(guild.roles, id=STAFF_ROLE_ID) - if staff_role is None: - await interaction.response.send_message("❌ Erreur : Le rôle staff n'existe pas !", ephemeral=True) - return + staff_role = discord.utils.get(guild.roles, id=int(staff_role_id)) + team_staff_role = discord.utils.get(guild.roles, id=int(team_staff_role_id)) + + if staff_role is None or team_staff_role is None: + return await interaction.response.send_message("❌ Erreur : L'un des rôles staff configurés n'existe plus !", ephemeral=True) # Vérifier si l'utilisateur a déjà un ticket ouvert for channel in category.text_channels: if channel.topic == f"Ticket de {user.id}": - await interaction.response.send_message(f"❌ Vous avez déjà un ticket ouvert : {channel.mention}", ephemeral=True) - return + return await interaction.response.send_message(f"❌ Vous avez déjà un ticket ouvert : {channel.mention}", ephemeral=True) # Définir les permissions du salon du ticket overwrites = { guild.default_role: discord.PermissionOverwrite(read_messages=False), user: discord.PermissionOverwrite(read_messages=True, send_messages=True), staff_role: discord.PermissionOverwrite(read_messages=True, send_messages=True), - discord.utils.get(guild.roles, id=TEAM_STAFF_ROLE_ID): discord.PermissionOverwrite(read_messages=True, send_messages=False) + team_staff_role: discord.PermissionOverwrite(read_messages=True, send_messages=False) } - # Créer un salon de ticket avec le nom [pseudo_du_membre]-écrit + # Créer un salon de ticket avec le nom [pseudo]-écrit ticket_channel = await category.create_text_channel( name=f"{user.name}-écrit", topic=f"Ticket de {user.id}", overwrites=overwrites ) - # Récupérer le rôle "équipe staff" - team_staff_role = discord.utils.get(guild.roles, id=TEAM_STAFF_ROLE_ID) - # Création de l'embed avec les infos du formulaire embed = discord.Embed(title="📩 Ticket Whitelist", color=0x00ff00) embed.add_field(name="📋 - Informations Personnelles", value=( @@ -171,7 +298,7 @@ class TicketStep3(discord.ui.Modal, title="📋 Développement du Personnage"): ), inline=False) # Envoyer l'embed avec les boutons d'actions pour les staff - await ticket_channel.send(embed=embed, view=TicketManagementView(user)) + await ticket_channel.send(embed=embed, view=TicketManagementView()) # Envoyer un message pour mentionner l'équipe staff await ticket_channel.send(f"{team_staff_role.mention} - Nouveau ticket créé !") @@ -181,10 +308,9 @@ class TicketStep3(discord.ui.Modal, title="📋 Développement du Personnage"): new_nickname = f"{self.step2_part1_data.prenom.value} {self.step2_part1_data.nom.value} | {self.step1_data.pseudo.value}" if len(new_nickname) > 32: new_nickname = new_nickname[:32] - await user.edit(nick=new_nickname) - else: - await interaction.followup.send("❌ Le bot n'a pas les permissions nécessaires pour modifier le pseudo.", ephemeral=True) - + try: await user.edit(nick=new_nickname) + except discord.Forbidden: pass + # Envoyer un message de confirmation try: await interaction.response.send_message(f"✅ Ticket ouvert : {ticket_channel.mention}", ephemeral=True) @@ -194,63 +320,99 @@ class TicketStep3(discord.ui.Modal, title="📋 Développement du Personnage"): class TicketManagementView(discord.ui.View): """Vue pour gérer le ticket avec les boutons Fermer, Réouvrir, Claim et Supprimer""" - def __init__(self, ticket_owner): + def __init__(self): super().__init__(timeout=None) - self.ticket_owner = ticket_owner - self.claimed_by = None # Stocke le staff qui claim le ticket @discord.ui.button(label="🔒 Fermer le Ticket", style=discord.ButtonStyle.red, custom_id="close_ticket") async def close_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): """Ferme le ticket en désactivant l'envoi de messages pour le membre""" - if not discord.utils.get(interaction.user.roles, id=STAFF_ROLE_ID): - await interaction.response.send_message("❌ Seuls les staffs peuvent fermer un ticket.", ephemeral=True) - return + settings = load_settings(interaction.guild.id) + staff_role_id = settings.get("staff_role_id") + + if not discord.utils.get(interaction.user.roles, id=int(staff_role_id) if staff_role_id else 0) and not interaction.user.guild_permissions.administrator: + return await interaction.response.send_message("❌ Seuls les staffs peuvent fermer un ticket.", ephemeral=True) - await interaction.channel.set_permissions(self.ticket_owner, read_messages=False, send_messages=False) + topic = interaction.channel.topic or "" + if not topic.startswith("Ticket de "): + return await interaction.response.send_message("❌ Impossible de trouver le propriétaire du ticket à fermer.", ephemeral=True) + + try: + owner_id = int(topic.split("Ticket de ")[1]) + ticket_owner = interaction.guild.get_member(owner_id) + if not ticket_owner: + ticket_owner = await interaction.guild.fetch_member(owner_id) + except (IndexError, ValueError, discord.NotFound): + return await interaction.response.send_message("❌ Membre introuvable.", ephemeral=True) + + await interaction.channel.set_permissions(ticket_owner, read_messages=False, send_messages=False) await interaction.channel.edit(name="ticket-fermé") await interaction.response.send_message("🔒 Le ticket a été fermé. Vous ne pouvez plus le voir.", ephemeral=True) # Mettre à jour la vue pour afficher uniquement les boutons Réouvrir et Supprimer - new_view = ClosedTicketView(self.ticket_owner) + new_view = ClosedTicketView() await interaction.channel.send("Le ticket a été fermé.", view=new_view) @discord.ui.button(label="👤 Prendre en charge le Ticket", style=discord.ButtonStyle.blurple, custom_id="claim_ticket") async def claim_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): """Un staff prend en charge le ticket""" - if not discord.utils.get(interaction.user.roles, id=STAFF_ROLE_ID): - await interaction.response.send_message("❌ Seuls les staffs peuvent claim un ticket.", ephemeral=True) - return - self.claimed_by = interaction.user - await interaction.channel.set_permissions(discord.utils.get(interaction.guild.roles, id=STAFF_ROLE_ID), send_messages=False, read_messages=True) - await interaction.channel.set_permissions(self.claimed_by, read_messages=True, send_messages=True) - await interaction.response.send_message(f"👤 {self.claimed_by.mention} a claim ce ticket.", ephemeral=False) + settings = load_settings(interaction.guild.id) + staff_role_id = settings.get("staff_role_id") + + if not discord.utils.get(interaction.user.roles, id=int(staff_role_id) if staff_role_id else 0) and not interaction.user.guild_permissions.administrator: + return await interaction.response.send_message("❌ Seuls les staffs peuvent claim un ticket.", ephemeral=True) + + staff_role = discord.utils.get(interaction.guild.roles, id=int(staff_role_id)) if staff_role_id else None + + claimed_by = interaction.user + if staff_role: + await interaction.channel.set_permissions(staff_role, send_messages=False, read_messages=True) + + await interaction.channel.set_permissions(claimed_by, read_messages=True, send_messages=True) + await interaction.response.send_message(f"👤 {claimed_by.mention} a claim ce ticket.", ephemeral=False) class ClosedTicketView(discord.ui.View): """Vue pour les tickets fermés avec les boutons Réouvrir et Supprimer""" - def __init__(self, ticket_owner): + def __init__(self): super().__init__(timeout=None) - self.ticket_owner = ticket_owner @discord.ui.button(label="♻ Réouvrir le Ticket", style=discord.ButtonStyle.green, custom_id="reopen_ticket") async def reopen_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): """Réouvre le ticket pour le membre""" - if not discord.utils.get(interaction.user.roles, id=STAFF_ROLE_ID): - await interaction.response.send_message("❌ Seuls les staffs peuvent réouvrir un ticket.", ephemeral=True) - return - await interaction.channel.set_permissions(self.ticket_owner, read_messages=True, send_messages=True) + settings = load_settings(interaction.guild.id) + staff_role_id = settings.get("staff_role_id") + + if not discord.utils.get(interaction.user.roles, id=int(staff_role_id) if staff_role_id else 0) and not interaction.user.guild_permissions.administrator: + return await interaction.response.send_message("❌ Seuls les staffs peuvent réouvrir un ticket.", ephemeral=True) + + topic = interaction.channel.topic or "" + if not topic.startswith("Ticket de "): + return await interaction.response.send_message("❌ Impossible de trouver le propriétaire du ticket à réouvrir.", ephemeral=True) + + try: + owner_id = int(topic.split("Ticket de ")[1]) + ticket_owner = interaction.guild.get_member(owner_id) + if not ticket_owner: + ticket_owner = await interaction.guild.fetch_member(owner_id) + except (IndexError, ValueError, discord.NotFound): + return await interaction.response.send_message("❌ Membre introuvable.", ephemeral=True) + + await interaction.channel.set_permissions(ticket_owner, read_messages=True, send_messages=True) await interaction.response.send_message("✅ Le ticket a été réouvert.", ephemeral=True) # Mettre à jour la vue pour afficher tous les boutons - new_view = TicketManagementView(self.ticket_owner) + new_view = TicketManagementView() await interaction.channel.send("Le ticket a été réouvert.", view=new_view) @discord.ui.button(label="❌ Supprimer le Ticket", style=discord.ButtonStyle.danger, custom_id="delete_ticket") async def delete_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): """Supprime le salon du ticket""" - if not discord.utils.get(interaction.user.roles, id=STAFF_ROLE_ID): - await interaction.response.send_message("❌ Seuls les staffs peuvent supprimer un ticket.", ephemeral=True) - return + settings = load_settings(interaction.guild.id) + staff_role_id = settings.get("staff_role_id") + + if not discord.utils.get(interaction.user.roles, id=int(staff_role_id) if staff_role_id else 0) and not interaction.user.guild_permissions.administrator: + return await interaction.response.send_message("❌ Seuls les staffs peuvent supprimer un ticket.", ephemeral=True) + await interaction.response.send_message("❌ Suppression du ticket...", ephemeral=True) await interaction.channel.delete() @@ -272,6 +434,7 @@ class Ticket(commands.Cog): self.bot = bot @app_commands.command(name="ticket_whitelist", description="Envoie l'embed pour ouvrir un ticket de whitelist.") + @app_commands.checks.has_permissions(administrator=True) async def ticket_whitelist(self, interaction: discord.Interaction): """Envoie l'embed avec le bouton d'ouverture de ticket""" embed = discord.Embed( @@ -283,6 +446,34 @@ class Ticket(commands.Cog): await interaction.channel.send(embed=embed, view=view) await interaction.response.send_message("✅ Embed des tickets envoyé.", ephemeral=True) + @app_commands.command(name="ticket_whitelist_config", description="Configurer le système de tickets whitelist (Admin)") + @app_commands.checks.has_permissions(administrator=True) + async def ticket_whitelist_config(self, interaction: discord.Interaction): + """Interface de configuration des tickets""" + settings = load_settings(interaction.guild.id) + + embed = discord.Embed( + title="⚙️ Configuration Tickets Whitelist", + description="Utilisez les boutons ci-dessous pour lier la catégorie et les rôles.", + color=discord.Color.blue() + ) + + cat_id = settings.get("ticket_category_id") + s_role_id = settings.get("staff_role_id") + ts_role_id = settings.get("team_staff_role_id") + ao_role_id = settings.get("attente_oral_role_id") + caf_role_id = settings.get("candidature_a_faire_role_id") + c_role_id = settings.get("citoyen_role_id") + + embed.add_field(name="📁 Catégorie Tickets", value=f"Id: {cat_id}" if cat_id else "❌ Non configuré", inline=True) + embed.add_field(name="🛡️ Staff", value=f"<@&{s_role_id}>" if s_role_id else "❌ Non configuré", inline=True) + embed.add_field(name="👥 Équipe Staff", value=f"<@&{ts_role_id}>" if ts_role_id else "❌ Non configuré", inline=True) + embed.add_field(name="🎤 Attente Oral", value=f"<@&{ao_role_id}>" if ao_role_id else "❌ Non configuré", inline=True) + embed.add_field(name="📝 Candidature à faire", value=f"<@&{caf_role_id}>" if caf_role_id else "❌ Non configuré", inline=True) + embed.add_field(name="🏙️ Citoyen", value=f"<@&{c_role_id}>" if c_role_id else "❌ Non configuré", inline=True) + + await interaction.response.send_message(embed=embed, view=TicketWhitelistConfigView(interaction.guild.id), ephemeral=True) + @commands.Cog.listener() async def on_message(self, message): """Surveille les messages des staffs dans les tickets""" @@ -291,39 +482,47 @@ class Ticket(commands.Cog): if not message.channel.topic or not message.channel.topic.startswith("Ticket de"): return - staff_role = discord.utils.get(message.guild.roles, id=STAFF_ROLE_ID) + settings = load_settings(message.guild.id) + staff_role_id = settings.get("staff_role_id") + attente_oral_role_id = settings.get("attente_oral_role_id") + candidature_a_faire_role_id = settings.get("candidature_a_faire_role_id") + citoyen_role_id = settings.get("citoyen_role_id") + + if not staff_role_id: + return + + staff_role = discord.utils.get(message.guild.roles, id=int(staff_role_id)) + if staff_role in message.author.roles and "validé" in message.content.lower(): try: user_id = message.channel.topic.split("Ticket de ")[1] - user = await message.guild.fetch_member(user_id) + user = await message.guild.fetch_member(int(user_id)) + + if not attente_oral_role_id or not candidature_a_faire_role_id or not citoyen_role_id: + await message.channel.send("❌ Erreur de configuration : Impossible d'attribuer les rôles, ils ne sont pas tous définis dans `/ticket_whitelist_config`.", delete_after=10) + return # Vérifier si le rôle "attente oral" est déjà attribué - attente_oral_role = discord.utils.get(message.guild.roles, id=ATTENTE_ORAL_ROLE_ID) - candidature_a_faire_role = discord.utils.get(message.guild.roles, id=CANDIDATURE_A_FAIRE_ROLE_ID) - citoyen_role = discord.utils.get(message.guild.roles, id=CITOYEN_ROLE_ID) + attente_oral_role = discord.utils.get(message.guild.roles, id=int(attente_oral_role_id)) + candidature_a_faire_role = discord.utils.get(message.guild.roles, id=int(candidature_a_faire_role_id)) + citoyen_role = discord.utils.get(message.guild.roles, id=int(citoyen_role_id)) if attente_oral_role in user.roles: # Deuxième "validé" détecté - await user.remove_roles(attente_oral_role) - await user.add_roles(citoyen_role) + if attente_oral_role: await user.remove_roles(attente_oral_role) + if citoyen_role: await user.add_roles(citoyen_role) await message.channel.edit(name=f"{user.name}-citoyen") else: # Premier "validé" détecté - await user.remove_roles(candidature_a_faire_role) - await user.add_roles(attente_oral_role) + if candidature_a_faire_role: await user.remove_roles(candidature_a_faire_role) + if attente_oral_role: await user.add_roles(attente_oral_role) await message.channel.edit(name=f"{user.name}-oral") - # Mettre à jour le pseudo du joueur si le bot a les permissions nécessaires - if message.guild.me.guild_permissions.manage_nicknames: - new_nickname = f"{self.step2_part1_data.prenom.value} {self.step2_part1_data.nom.value} | {self.step1_data.pseudo.value}" - if len(new_nickname) > 32: - new_nickname = new_nickname[:32] - await user.edit(nick=new_nickname) - else: - await message.channel.send("❌ Le bot n'a pas les permissions nécessaires pour modifier le pseudo.", delete_after=10) - except Exception as e: - print(f"Erreur lors du renommage du ticket: {e}") + print(f"Erreur lors du traitement du ticket : {e}") async def setup(bot): - await bot.add_cog(Ticket(bot)) \ No newline at end of file + await bot.add_cog(Ticket(bot)) + bot.add_view(TicketView()) + bot.add_view(TicketManagementView()) + bot.add_view(ClosedTicketView()) \ No newline at end of file diff --git a/data/json-export-history.json b/data/json-export-history.json index 84e4600..faef25a 100644 --- a/data/json-export-history.json +++ b/data/json-export-history.json @@ -1,6 +1,6 @@ [ { - "date": "2026-03-01T00:26:14.746054+00:00", + "date": "2026-03-14T14:26:12.694595+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 13, @@ -9,7 +9,7 @@ "success": true }, { - "date": "2026-03-01T00:25:45.072770+00:00", + "date": "2026-03-14T14:25:42.467679+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 13, @@ -18,7 +18,7 @@ "success": true }, { - "date": "2026-03-01T00:25:15.071380+00:00", + "date": "2026-03-14T14:25:11.992208+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 13, @@ -27,7 +27,7 @@ "success": true }, { - "date": "2026-03-01T00:24:45.388984+00:00", + "date": "2026-03-14T14:24:42.190744+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 13, @@ -36,7 +36,7 @@ "success": true }, { - "date": "2026-03-01T00:24:14.975026+00:00", + "date": "2026-03-14T14:24:12.614886+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 13, @@ -45,7 +45,7 @@ "success": true }, { - "date": "2026-03-01T00:23:44.713904+00:00", + "date": "2026-03-14T14:23:42.012764+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 13, @@ -54,7 +54,7 @@ "success": true }, { - "date": "2026-03-01T00:23:16.510199+00:00", + "date": "2026-03-14T14:23:12.416066+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 13, @@ -63,7 +63,7 @@ "success": true }, { - "date": "2026-03-01T00:22:44.966638+00:00", + "date": "2026-03-14T14:22:42.200744+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 13, @@ -72,7 +72,7 @@ "success": true }, { - "date": "2026-03-01T00:22:14.857332+00:00", + "date": "2026-03-14T14:22:12.128982+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 13, @@ -81,7 +81,7 @@ "success": true }, { - "date": "2026-03-01T00:21:44.958489+00:00", + "date": "2026-03-14T14:21:41.879828+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 13, @@ -90,7 +90,7 @@ "success": true }, { - "date": "2026-03-01T00:21:14.844468+00:00", + "date": "2026-03-14T14:21:25.894956+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 13, @@ -99,7 +99,7 @@ "success": true }, { - "date": "2026-03-01T00:20:44.959093+00:00", + "date": "2026-03-14T14:20:56.179577+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 13, @@ -108,7 +108,7 @@ "success": true }, { - "date": "2026-03-01T00:20:14.948095+00:00", + "date": "2026-03-14T14:20:25.382325+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 13, @@ -117,7 +117,61 @@ "success": true }, { - "date": "2026-03-01T00:19:44.835750+00:00", + "date": "2026-03-14T14:19:55.416432+00:00", + "trigger": "auto", + "serversCount": 1, + "membersCount": 13, + "guildName": "Omega Kube Bêta Test", + "errors": [], + "success": true + }, + { + "date": "2026-03-14T14:19:25.281242+00:00", + "trigger": "auto", + "serversCount": 1, + "membersCount": 13, + "guildName": "Omega Kube Bêta Test", + "errors": [], + "success": true + }, + { + "date": "2026-03-14T14:18:55.315861+00:00", + "trigger": "auto", + "serversCount": 1, + "membersCount": 13, + "guildName": "Omega Kube Bêta Test", + "errors": [], + "success": true + }, + { + "date": "2026-03-14T14:18:25.672058+00:00", + "trigger": "auto", + "serversCount": 1, + "membersCount": 13, + "guildName": "Omega Kube Bêta Test", + "errors": [], + "success": true + }, + { + "date": "2026-03-14T14:17:55.409042+00:00", + "trigger": "auto", + "serversCount": 1, + "membersCount": 13, + "guildName": "Omega Kube Bêta Test", + "errors": [], + "success": true + }, + { + "date": "2026-03-14T14:17:25.447376+00:00", + "trigger": "auto", + "serversCount": 1, + "membersCount": 13, + "guildName": "Omega Kube Bêta Test", + "errors": [], + "success": true + }, + { + "date": "2026-03-14T14:16:55.368379+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 13, From d155ebfcb0c19d78d5917b1ac41bfe4b20e2de94 Mon Sep 17 00:00:00 2001 From: Mathis Date: Sat, 14 Mar 2026 17:05:20 +0100 Subject: [PATCH 3/3] =?UTF-8?q?Premi=C3=A8re=20version=20de=20la=20mise=20?= =?UTF-8?q?=C3=A0=20jour=20du=20suivie=20des=20bugs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- commandes/bug_report.py | 183 +++++++++++++++++++++++++++++----------- serveur_flask.py | 101 ++++++++++++++++++++++ 2 files changed, 235 insertions(+), 49 deletions(-) create mode 100644 serveur_flask.py diff --git a/commandes/bug_report.py b/commandes/bug_report.py index 7a97449..3504473 100644 --- a/commandes/bug_report.py +++ b/commandes/bug_report.py @@ -1,7 +1,8 @@ import discord from discord import app_commands -from discord.ext import commands, tasks +from discord.ext import commands from utils.gitlab_client import gitlab_client +from aiohttp import web import logging import json import os @@ -109,61 +110,139 @@ class FeatureSuggestionModal(discord.ui.Modal, title="Suggérer une fonctionnali class BugReport(commands.Cog): def __init__(self, bot): self.bot = bot - self.check_gitlab_issues.start() + + async def cog_load(self): + self.web_app = web.Application() + self.web_app.router.add_post('/bot_event', self.handle_bot_event) + self.runner = web.AppRunner(self.web_app) + await self.runner.setup() + self.site = web.TCPSite(self.runner, '127.0.0.1', 5001) + await self.site.start() + kuby_logger.info("Internal Bot Webhook Server started on 127.0.0.1:5001") def cog_unload(self): - self.check_gitlab_issues.cancel() - - @tasks.loop(minutes=30) - async def check_gitlab_issues(self): - """Vérifie périodiquement si des issues suivies ont été fermées""" - if not os.path.exists(REPORTS_FILE): - return + if hasattr(self, 'runner'): + self.bot.loop.create_task(self.runner.cleanup()) + async def handle_bot_event(self, request): try: + payload = await request.json() + + event_type = payload.get("object_kind") + if event_type not in ["issue", "note"]: + return web.json_response({"status": "ignored"}, status=200) + + if event_type == "issue": + issue_iid = payload.get("object_attributes", {}).get("iid") + else: + issue_iid = payload.get("issue", {}).get("iid") + + if not issue_iid: + return web.json_response({"status": "no issue id"}, status=200) + + if not os.path.exists(REPORTS_FILE): + return web.json_response({"status": "no reports file"}, status=200) + with open(REPORTS_FILE, "r") as f: reports = json.load(f) - - if not reports: - return - issues_to_remove = [] - for issue_iid, user_id in reports.items(): - issue = await gitlab_client.get_issue(int(issue_iid)) - if issue and issue.get("state") == "closed": - # L'issue est fermée ! On prévient l'utilisateur - user = self.bot.get_user(user_id) - if user: - try: - is_suggestion = "[SUGGESTION]" in issue.get('title', '') - title_type = "Suggestion Implémentée !" if is_suggestion else "🛠️ Bug Résolu !" - msg_type = "suggestion" if is_suggestion else "bug" - status_val = "Implémenté / Terminé" if is_suggestion else "Terminé / Résolu" - - embed = discord.Embed( - title=title_type, - description=f"Bonne nouvelle ! Votre {msg_type} (**{issue.get('title')}**) a été marqué comme terminé sur GitLab.", - color=discord.Color.green() - ) - embed.add_field(name="Statut", value=status_val) - embed.add_field(name="Prochaine étape", value="La mise à jour arrive prochainement !") - embed.set_footer(text="Merci de votre aide pour améliorer le bot.") - await user.send(embed=embed) - issues_to_remove.append(issue_iid) - except Exception as e: - kuby_logger.error(f"Impossible d'envoyer un MP à {user_id}: {e}") - else: - # On ne peut pas trouver l'user, on retire quand même pour ne pas boucler - issues_to_remove.append(issue_iid) - - if issues_to_remove: - for iid in issues_to_remove: - del reports[iid] - with open(REPORTS_FILE, "w") as f: - json.dump(reports, f, indent=4) + user_id = reports.get(str(issue_iid)) + if not user_id: + return web.json_response({"status": "untracked issue"}, status=200) + + user = self.bot.get_user(user_id) + if not user: + try: + user = await self.bot.fetch_user(user_id) + except: + pass + + if not user: + return web.json_response({"status": "user not found"}, status=200) + + app_info = await self.bot.application_info() + dev = app_info.owner + issue_title = payload.get("object_attributes", {}).get("title", f"#{issue_iid}") + if event_type == "note": + issue_title = payload.get("issue", {}).get("title", f"#{issue_iid}") + + if event_type == "issue": + action = payload.get("object_attributes", {}).get("action") + changes = payload.get("changes", {}) + + # Check for label changes + if "labels" in changes: + curr_labels = [l.get("title") for l in changes["labels"].get("current", []) if l.get("title")] + display_labels = [l for l in curr_labels if not l.startswith("Priority::") and l not in ["bug", "manual-report", "manual-suggestion"]] + if display_labels: + current_labels_str = ", ".join(display_labels) + else: + current_labels_str = "Mis à jour" + + try: + embed = discord.Embed( + title="🛠️ Mise à jour de votre signalement !", + description=f"Le statut de votre rapport **{issue_title}** a été mis à jour par l'équipe.", + color=discord.Color.blue() + ) + embed.add_field(name="Nouveaux Labels / Statuts", value=current_labels_str) + await user.send(embed=embed) + + if dev: + await dev.send(f"✅ [FIABLE] L'utilisateur {user} ({user.id}) a bien reçu la notification de statut **{current_labels_str}** pour l'issue #{issue_iid} ({issue_title}).") + except Exception as e: + if dev: + await dev.send(f"❌ [ERREUR] Impossible de notifier l'utilisateur {user} pour l'issue #{issue_iid} : {e}") + + # Check if it was closed + if action == "close" or payload.get("object_attributes", {}).get("state") == "closed": + try: + embed = discord.Embed( + title="🛠️ Bug / Suggestion Terminé(e) !", + description=f"Bonne nouvelle ! Votre signalement **{issue_title}** a été marqué comme terminé ou résolu.", + color=discord.Color.green() + ) + embed.add_field(name="Prochaine étape", value="La mise à jour arrive prochainement (ou est déjà là) !") + embed.set_footer(text="Merci pour votre aide !") + await user.send(embed=embed) + + if dev: + await dev.send(f"✅ [FIABLE] L'utilisateur {user} ({user.id}) a bien reçu la notification de CLÔTURE pour l'issue #{issue_iid}.") + + except Exception as e: + if dev: + await dev.send(f"❌ [ERREUR] Impossible d'annoncer la clôture à {user} pour l'issue #{issue_iid} : {e}") + + elif event_type == "note": + note_attr = payload.get("object_attributes", {}) + is_system = note_attr.get("system", False) + + if not is_system: + note_body = note_attr.get("note", "") + author_name = payload.get("user", {}).get("name", "Développeur") + + try: + embed = discord.Embed( + title="💬 Nouveau commentaire sur votre signalement", + description=f"Le développeur a répondu à votre rapport **{issue_title}** :\n\n>>> {note_body}", + color=discord.Color.orange() + ) + embed.set_footer(text=f"Par {author_name}") + await user.send(embed=embed) + + if dev: + await dev.send(f"✅ [FIABLE] L'utilisateur {user} ({user.id}) a bien reçu votre commentaire pour l'issue #{issue_iid}.") + + except Exception as e: + if dev: + await dev.send(f"❌ [ERREUR] Impossible d'envoyer votre commentaire à {user} pour l'issue #{issue_iid} : {e}") + + return web.json_response({"status": "success"}, status=200) + except Exception as e: - kuby_logger.error(f"Erreur lors du cycle de vérification GitLab: {e}") + kuby_logger.error(f"Error handling bot event: {e}") + return web.json_response({"status": "error", "message": str(e)}, status=500) @app_commands.command(name="signaler_bug", description="Signaler un bug aux développeurs") @app_commands.choices(priority=[ @@ -172,11 +251,17 @@ class BugReport(commands.Cog): app_commands.Choice(name="Haute", value="High"), app_commands.Choice(name="Urgente", value="Urgent"), ]) - async def signaler_bug(self, interaction: discord.Interaction, priority: app_commands.Choice[str]): + async def signaler_bug(self, interaction: discord.Interaction, priority: app_commands.Choice[str], mot_de_passe: str): + if mot_de_passe != "01thisma": + await interaction.response.send_message("❌ Mot de passe incorrect. Vous n'êtes pas autorisé à signaler un bug.", ephemeral=True) + return await interaction.response.send_modal(BugReportModal(priority)) @app_commands.command(name="suggerer_fonctionnalite", description="Proposer une nouvelle fonctionnalité pour le bot") - async def suggerer_fonctionnalite(self, interaction: discord.Interaction): + async def suggerer_fonctionnalite(self, interaction: discord.Interaction, mot_de_passe: str): + if mot_de_passe != "01thisma": + await interaction.response.send_message("❌ Mot de passe incorrect.", ephemeral=True) + return await interaction.response.send_modal(FeatureSuggestionModal()) async def setup(bot): diff --git a/serveur_flask.py b/serveur_flask.py new file mode 100644 index 0000000..9f57112 --- /dev/null +++ b/serveur_flask.py @@ -0,0 +1,101 @@ +import os, subprocess, threading, requests +from flask import Flask, request, abort +from datetime import datetime, timezone + +app = Flask(__name__) + +# --- CONFIGURATION --- +PROJECT_PATH = "/home/discord/Bot/Kuby_V2" +WEBHOOK_SECRET = "Nois1" +PROCESS_NAME = "kuby-bot" +DISCORD_LOG_URL = "https://discord.com/api/webhooks/1482148910888652910/IA9CcOWtjGswbuxMaOu_6uaclv5zojZo4ttxtV6RYyZzJ9gW7BF7xp_Zv3oPIjYEuh8o" + +# Bot Local Proxy +BOT_LOCAL_URL = "http://127.0.0.1:5001/bot_event" + +def send_discord_log(status, message, color, author=None, commit_msg=None): + embed = { + "title": f"🚀 Kuby V2 : {status}", + "description": message, + "color": color, + "timestamp": datetime.now(timezone.utc).isoformat(), + "fields": [] + } + + if author: + embed["fields"].append({"name": "👤 Auteur", "value": author, "inline": True}) + if commit_msg: + embed["fields"].append({"name": "📝 Commit", "value": commit_msg, "inline": True}) + + payload = {"embeds": [embed]} + try: + requests.post(DISCORD_LOG_URL, json=payload, timeout=5) + except Exception as e: + print(f"[!] Erreur envoi Discord : {e}") + +def run_mep_logic(author, commit_msg): + start_time = datetime.now() + send_discord_log("EN COURS", "Déploiement lancé sur la machine de guerre...", 3447003, author, commit_msg) + + try: + os.chdir(PROJECT_PATH) + + # 1. GIT + subprocess.run(["git", "fetch", "origin"], check=True) + subprocess.run(["git", "reset", "--hard", "origin/main"], check=True) + + # 2. PM2 + subprocess.run(f"pm2 restart {PROCESS_NAME}", shell=True, check=True, capture_output=True) + + duration = (datetime.now() - start_time).total_seconds() + msg = f"✅ **Déploiement réussi en {duration:.2f}s**\nLe bot est à jour et redémarré." + send_discord_log("TERMINÉ", msg, 3066993, author, commit_msg) + + except Exception as e: + error_msg = f"❌ **ERREUR de déploiement**\n```python\n{str(e)}\n```" + send_discord_log("ÉCHEC", error_msg, 15158332, author, commit_msg) + +@app.route('/deploy', methods=['POST']) +def deploy(): + # Sécurité + if request.headers.get('X-Gitlab-Token') != WEBHOOK_SECRET: + abort(403) + + # Extraction des infos GitLab + data = request.json + author = "Inconnu" + commit_msg = "Pas de message" + + if data and 'commits' in data and len(data['commits']) > 0: + author = data['commits'][0].get('author', {}).get('name', 'Inconnu') + commit_msg = data['commits'][0].get('message', 'Pas de message') + + # Threading pour répondre vite à GitLab + threading.Thread(target=run_mep_logic, args=(author, commit_msg)).start() + + return {"status": "accepted"}, 202 + +@app.route('/gitlab_webhook', methods=['POST']) +def gitlab_webhook(): + # Protection stricte Anti-Crawler + user_agent = request.headers.get('User-Agent', '') + if 'GitLab' not in user_agent: + abort(404) # Retourne une 404 pour simuler que la page n'existe pas + + if request.headers.get('X-Gitlab-Token') != WEBHOOK_SECRET: + abort(404) + + # Transférer la payload au bot Discord localement sur le port 5001 + payload = request.json + try: + # Timeout très court car c'est en local + requests.post(BOT_LOCAL_URL, json=payload, timeout=2) + except requests.exceptions.RequestException as e: + print(f"[!] Erreur de relais vers le bot Discord : {e}") + # On renvoie 200 à GitLab quand même car GitLab n'a pas à savoir l'état interne + + return {"status": "received"}, 200 + +if __name__ == '__main__': + print(f"[*] Webhook opérationnel. En attente de push ou d'events...") + app.run(host='0.0.0.0', port=5000)