From c8c579eefe7e2ffb6afca30ac3d2f23c893c6964 Mon Sep 17 00:00:00 2001 From: Mathis Date: Wed, 6 May 2026 17:07:09 +0200 Subject: [PATCH 1/4] Migration de Kuby vers disnake --- bot.py | 16 +- commandes/absence_staff.py | 81 ++- commandes/backup.py | 60 +- commandes/backup_restore.py | 42 +- commandes/blacklist.py | 69 +- commandes/bug_report.py | 71 ++- commandes/convocation.py | 127 ++-- commandes/goodbye.py | 21 +- commandes/moderation.py | 125 ++-- commandes/modules_security/antiraid.py | 6 +- commandes/modules_security/antispam.py | 16 +- commandes/modules_security/logs_manager.py | 16 +- commandes/modules_security/rules.py | 55 +- commandes/no_link.py | 8 +- commandes/scan.py | 9 +- commandes/security.py | 173 +++-- commandes/sendbotmessages.py | 15 +- commandes/snipe.py | 19 +- commandes/staff_leaderboard.py | 13 +- commandes/staff_ratings.py | 17 +- commandes/ticket/__init__.py | 696 ++++++++++----------- commandes/ticket/analytics.py | 26 +- commandes/ticket/feedback_view.py | 42 +- commandes/ticket/staff_analytics.py | 16 +- commandes/ticket/utils/formatters.py | 2 +- commandes/ticket/utils/permissions.py | 8 +- commandes/ticket/utils/transcript.py | 2 +- commandes/ticket_whitelist.py | 297 +++++---- commandes/website_sync.py | 185 +++--- commandes/welcome.py | 25 +- commandes/whitelist.py | 96 +-- commandes/whitelist_monitor.py | 64 +- kuby.py | 6 +- requirements.txt | 2 +- src/advanced_logger.py | 20 +- src/json_export_service.py | 12 +- 36 files changed, 1205 insertions(+), 1253 deletions(-) diff --git a/bot.py b/bot.py index e516439..2130bfe 100755 --- a/bot.py +++ b/bot.py @@ -4,10 +4,10 @@ from dotenv import load_dotenv # Charger les variables d'environnement AVANT tout le reste load_dotenv() -import discord +import disnake from commandes.ticket.utils.permissions import can_access_config from commandes.ticket.data.storage import get_storage -from discord.ext import commands +from disnake.ext import commands import asyncio from src.logger import kuby_logger, log_performance import logging @@ -25,12 +25,12 @@ except ValueError: kuby_logger.error(f"❌ APPLICATION_ID invalide : {application_id_raw}") application_id = None -# Configuration des intents Discord -intents = discord.Intents.default() +# Configuration des intents Disnake +intents = disnake.Intents.default() intents.message_content = True intents.members = True intents.voice_states = True -kuby_logger.debug(f"Discord intents configured: {intents}") +kuby_logger.debug(f"Disnake intents configured: {intents}") class MyBot(commands.Bot): def __init__(self): @@ -135,7 +135,7 @@ class MyBot(commands.Bot): # Réponse adaptée (Slash vs Message) msg = "❌ Vous n'êtes pas autorisé à utiliser cette commande. Veuillez demander à être ajouté à la whitelist." if ctx.interaction: - await ctx.interaction.response.send_message(msg, ephemeral=True) + await ctx.send(msg, ephemeral=True) else: await ctx.send(msg) return False @@ -147,8 +147,8 @@ class MyBot(commands.Bot): # Sync slash commands try: - await self.tree.sync() - kuby_logger.info("✅ Commandes slash synchronisées avec Discord") + # Disnake handles syncing automatically by default + kuby_logger.info("✅ Commandes slash prêtes avec Disnake") except Exception as e: kuby_logger.error(f"❌ Erreur lors de la synchronisation des commandes slash : {e}", exc_info=True) diff --git a/commandes/absence_staff.py b/commandes/absence_staff.py index 7d83cbf..53a79c3 100755 --- a/commandes/absence_staff.py +++ b/commandes/absence_staff.py @@ -1,6 +1,5 @@ -import discord -from discord.ext import commands, tasks -from discord import app_commands, ui +import disnake +from disnake.ext import commands, tasks import json import asyncio import os @@ -82,22 +81,26 @@ def parse_date(date_str: str): # --- MODALS & VIEWS --- -class AbsenceModal(ui.Modal, title="Déclarer une Absence Staff"): - motif = ui.TextInput( +class AbsenceModal(disnake.ui.Modal): + def __init__(self, superieur: Optional[disnake.Member] = None): + super().__init__(title="Déclarer une Absence Staff") + self.superieur = superieur + + motif = disnake.ui.TextInput( label="Motif de l'absence", - style=discord.TextStyle.paragraph, + style=disnake.TextStyle.paragraph, placeholder="Ex: Vacances, Raisons personnelles, Travail...", required=True, max_length=500 ) - date_debut = ui.TextInput( + date_debut = disnake.ui.TextInput( label="Début (JJ/MM/AAAA HH:MM)", placeholder="Ex: 15/05/2024 08:00", min_length=16, max_length=16, required=True ) - date_fin = ui.TextInput( + date_fin = disnake.ui.TextInput( label="Fin (JJ/MM/AAAA HH:MM)", placeholder="Ex: 20/05/2024 18:00", min_length=16, @@ -105,11 +108,7 @@ class AbsenceModal(ui.Modal, title="Déclarer une Absence Staff"): required=True ) - def __init__(self, superieur: Optional[discord.Member] = None): - super().__init__() - self.superieur = superieur - - async def on_submit(self, interaction: discord.Interaction): + async def callback(self, interaction: disnake.Interaction): try: debut = parse_date(self.date_debut.value) fin = parse_date(self.date_fin.value) @@ -182,20 +181,20 @@ class AbsenceModal(ui.Modal, title="Déclarer une Absence Staff"): await interaction.response.send_message("✅ Absence enregistrée avec succès !", ephemeral=True) -class AbsenceConfigView(ui.View): +class AbsenceConfigView(disnake.ui.View): def __init__(self, guild_id: int): super().__init__(timeout=60) self.guild_id = guild_id - @ui.button(label="Définir le Salon", style=discord.ButtonStyle.primary, emoji="📁") - async def set_channel(self, interaction: discord.Interaction, button: ui.Button): + @disnake.ui.button(label="Définir le Salon", style=disnake.ButtonStyle.primary, emoji="📁") + async def set_channel(self, interaction: disnake.Interaction, button: disnake.ui.Button): await interaction.response.send_message("Mentionne le salon ici (ou envoie son ID) :", ephemeral=True) def check(m): return m.author == interaction.user and m.channel == interaction.channel try: - msg = await interaction.client.wait_for('message', check=check, timeout=30) + msg = await interaction.bot.wait_for('message', check=check, timeout=30) channel = None if msg.channel_mentions: channel = msg.channel_mentions[0] @@ -214,15 +213,15 @@ class AbsenceConfigView(ui.View): except asyncio.TimeoutError: await interaction.followup.send("❌ Temps écoulé.", ephemeral=True) - @ui.button(label="Définir le Rôle", style=discord.ButtonStyle.primary, emoji="🎭") - async def set_role(self, interaction: discord.Interaction, button: ui.Button): + @disnake.ui.button(label="Définir le Rôle", style=disnake.ButtonStyle.primary, emoji="🎭") + async def set_role(self, interaction: disnake.Interaction, button: disnake.ui.Button): await interaction.response.send_message("Mentionne le rôle ici (ou envoie son ID) :", ephemeral=True) def check(m): return m.author == interaction.user and m.channel == interaction.channel try: - msg = await interaction.client.wait_for('message', check=check, timeout=30) + msg = await interaction.bot.wait_for('message', check=check, timeout=30) role = None if msg.role_mentions: role = msg.role_mentions[0] @@ -246,22 +245,22 @@ class Absence(commands.Cog): self.bot = bot self.check_absences.start() - @app_commands.command(name="absence", description="Déclarer une absence staff") - @app_commands.describe(superieur="Le supérieur hiérarchique à prévenir") - async def absence(self, interaction: discord.Interaction, superieur: Optional[discord.Member] = None): + @commands.slash_command(name="absence", description="Déclarer une absence staff") + async def absence(self, interaction: disnake.ApplicationCommandInteraction, + superieur: Optional[disnake.Member] = commands.Param(None, description="Le supérieur hiérarchique à prévenir")): """Déclare une absence via un formulaire""" await interaction.response.send_modal(AbsenceModal(superieur)) - @app_commands.command(name="absence_config", description="Configurer le système d'absence (Admin)") - @app_commands.checks.has_permissions(administrator=True) - async def absence_config(self, interaction: discord.Interaction): + @commands.slash_command(name="absence_config", description="Configurer le système d'absence (Admin)") + @commands.has_permissions(administrator=True) + async def absence_config(self, interaction: disnake.ApplicationCommandInteraction): """Ouvre le panneau de configuration des absences""" settings = load_settings(interaction.guild.id) - embed = discord.Embed( + embed = disnake.Embed( title="⚙️ Configuration des Absences Staff", description="Utilisez les boutons ci-dessous pour configurer le système.", - color=discord.Color.blue() + color=disnake.Color.blue() ) channel_id = settings.get("absence_channel_id") @@ -280,7 +279,7 @@ class Absence(commands.Cog): await interaction.response.send_message(embed=embed, view=AbsenceConfigView(interaction.guild.id), ephemeral=True) - async def strike_absence_message(self, guild: discord.Guild, channel_id: int, message_id: int): + async def strike_absence_message(self, guild: disnake.Guild, channel_id: int, message_id: int): """Modifie le message d'absence pour le barrer au lieu de le supprimer""" channel = guild.get_channel(channel_id) if not channel: @@ -311,8 +310,8 @@ class Absence(commands.Cog): except Exception as e: print(f"Erreur lors du barrage du message d'absence: {e}") - @app_commands.command(name="fin_absence", description="Terminer son absence staff") - async def fin_absence(self, interaction: discord.Interaction): + @commands.slash_command(name="fin_absence", description="Terminer son absence staff") + async def fin_absence(self, interaction: disnake.ApplicationCommandInteraction): """Met fin à l'absence en cours""" all_abs = await load_absences() guild_abs = get_guild_absences(all_abs, interaction.guild.id) @@ -343,8 +342,8 @@ class Absence(commands.Cog): await save_absences(all_abs) await interaction.response.send_message("✅ Ton absence a été retirée.", ephemeral=True) - @app_commands.command(name="liste_absences", description="Afficher la liste des absences staff en cours") - async def liste_absences(self, interaction: discord.Interaction): + @commands.slash_command(name="liste_absences", description="Afficher la liste des absences staff en cours") + async def liste_absences(self, interaction: disnake.ApplicationCommandInteraction): """Affiche les absences en cours sur le serveur""" all_abs = await load_absences() guild_abs = get_guild_absences(all_abs, interaction.guild.id) @@ -360,10 +359,10 @@ class Absence(commands.Cog): per_page = 5 pages = [absences[i:i + per_page] for i in range(0, len(absences), per_page)] - def create_embed(page_index: int) -> discord.Embed: - embed = discord.Embed( + def create_embed(page_index: int) -> disnake.Embed: + embed = disnake.Embed( title=f"📋 Absences staff - {interaction.guild.name} ({page_index + 1}/{len(pages)})", - color=discord.Color.orange() + color=disnake.Color.orange() ) for user_id, info in pages[page_index]: embed.add_field( @@ -378,19 +377,19 @@ class Absence(commands.Cog): embed.set_footer(text="Gestion des absences du staff") return embed - class PaginatorView(ui.View): + class PaginatorView(disnake.ui.View): def __init__(self): super().__init__(timeout=60) self.page = 0 - @ui.button(label="⬅️ Précédent", style=discord.ButtonStyle.secondary, disabled=True) - async def previous(self, interaction_btn: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="⬅️ Précédent", style=disnake.ButtonStyle.secondary, disabled=True) + async def previous(self, interaction_btn: disnake.Interaction, button: disnake.ui.Button): self.page -= 1 self.update_buttons() await interaction_btn.response.edit_message(embed=create_embed(self.page), view=self) - @ui.button(label="➡️ Suivant", style=discord.ButtonStyle.secondary, disabled=len(pages) == 1) - async def next(self, interaction_btn: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="➡️ Suivant", style=disnake.ButtonStyle.secondary, disabled=len(pages) == 1) + async def next(self, interaction_btn: disnake.Interaction, button: disnake.ui.Button): self.page += 1 self.update_buttons() await interaction_btn.response.edit_message(embed=create_embed(self.page), view=self) diff --git a/commandes/backup.py b/commandes/backup.py index 82c6a89..5cafb7c 100755 --- a/commandes/backup.py +++ b/commandes/backup.py @@ -7,9 +7,8 @@ import asyncio from datetime import datetime from typing import Dict, Any, List -import discord -from discord.ext import commands -from discord import app_commands +import disnake +from disnake.ext import commands BACKUPS_ROOT = "data/security_backups" # dossier de sortie des sauvegardes @@ -46,10 +45,10 @@ def estimate_duration_seconds( return int(meta_cost + msg_cost + base_overhead) -def serialize_overwrites(channel: discord.abc.GuildChannel) -> List[Dict[str, Any]]: +def serialize_overwrites(channel: disnake.abc.GuildChannel) -> List[Dict[str, Any]]: serialized: List[Dict[str, Any]] = [] for target, perms in channel.overwrites.items(): - target_type = "role" if isinstance(target, discord.Role) else "member" + target_type = "role" if isinstance(target, disnake.Role) else "member" serialized.append({ "target_type": target_type, "target_id": target.id, @@ -64,20 +63,17 @@ class Backup(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - backup_group = app_commands.Group(name="backup", description="Outils de sauvegarde du serveur") + @commands.slash_command(name="backup", description="Outils de sauvegarde du serveur") + async def backup_group(self, interaction: disnake.ApplicationCommandInteraction): + pass - @backup_group.command(name="create", description="Créer une sauvegarde du serveur") - @app_commands.describe( - save_messages="Inclure l'export des messages", - privacy_notice="Afficher un avertissement public si messages sauvegardés", - message_limit_per_channel="Nombre max de messages à exporter par salon (0 = tous)" - ) + @backup_group.sub_command(name="create", description="Créer une sauvegarde du serveur") async def backup_create( self, - interaction: discord.Interaction, - save_messages: bool = True, - privacy_notice: bool = True, - message_limit_per_channel: int = 10000 + interaction: disnake.ApplicationCommandInteraction, + save_messages: bool = commands.Param(True, description="Inclure l'export des messages"), + privacy_notice: bool = commands.Param(True, description="Afficher un avertissement public si messages sauvegardés"), + message_limit_per_channel: int = commands.Param(10000, description="Nombre max de messages à exporter par salon (0 = tous)") ): guild = interaction.guild assert guild is not None, "Cette commande doit être utilisée dans un serveur." @@ -93,20 +89,20 @@ class Backup(commands.Cog): ) return - # Déferre l'interaction - await interaction.response.defer(ephemeral=True, thinking=True) + # Deferre l'interaction + await interaction.response.defer(ephemeral=True) # Avertissement de confidentialité if privacy_notice and save_messages: try: await interaction.channel.send( - embed=discord.Embed( + embed=disnake.Embed( title="📢 Information de confidentialité (transparence)", description=( "Une **sauvegarde du serveur** est en cours et **inclus les messages**.\n" "Vous êtes dans l'obligation de prévenir les membres." ), - color=discord.Color.orange() + color=disnake.Color.orange() ) ) except Exception: @@ -132,14 +128,14 @@ class Backup(commands.Cog): message_limit_per_channel if message_limit_per_channel else 100000 ) - prog_embed = discord.Embed( + prog_embed = disnake.Embed( title="🗄️ Sauvegarde en cours", description=( f"**Serveur :** {guild.name} (`{guild.id}`)\n" f"**Estimation :** ~{estimation_s} secondes\n" f"**Options** : messages={'oui' if save_messages else 'non'} • limite/chan={message_limit_per_channel or 'tous'}" ), - color=discord.Color.blurple() + color=disnake.Color.blurple() ) prog_embed.add_field(name="Progression", value=progress_bar(0.0), inline=False) progress_msg = await interaction.followup.send(embed=prog_embed, ephemeral=False) @@ -173,11 +169,11 @@ class Backup(commands.Cog): chans_dump=[] for ch in guild.channels: base={"id":ch.id,"name":ch.name,"type":str(ch.type),"position":ch.position,"category_id":ch.category.id if getattr(ch,"category",None) else None,"overwrites":serialize_overwrites(ch)} - if isinstance(ch,discord.TextChannel): + if isinstance(ch,disnake.TextChannel): base.update({"topic":ch.topic,"nsfw":ch.nsfw,"slowmode_delay":ch.slowmode_delay,"default_auto_archive_duration":ch.default_auto_archive_duration}) - elif isinstance(ch,discord.VoiceChannel): + elif isinstance(ch,disnake.VoiceChannel): base.update({"bitrate":ch.bitrate,"user_limit":ch.user_limit,"rtc_region":str(ch.rtc_region) if ch.rtc_region else None}) - elif isinstance(ch,discord.ForumChannel): + elif isinstance(ch,disnake.ForumChannel): base.update({"nsfw":ch.nsfw,"default_auto_archive_duration":ch.default_auto_archive_duration}) chans_dump.append(base) with open(os.path.join(base_dir,"channels.json"),"w",encoding="utf-8") as f: @@ -208,7 +204,7 @@ class Backup(commands.Cog): data={"id":msg.id,"channel_id":msg.channel.id,"author_id":msg.author.id,"author_bot":msg.author.bot,"created_at":msg.created_at.isoformat(),"content":msg.content,"attachments":[a.url for a in msg.attachments],"embeds":[e.to_dict() for e in msg.embeds],"reference":{"message_id":getattr(msg.reference,"message_id",None),"channel_id":getattr(msg.reference,"channel_id",None)} if msg.reference else None} f.write(json.dumps(data,ensure_ascii=False)+"\n") count+=1 - except discord.Forbidden: pass + except disnake.Forbidden: pass except Exception: pass exported_total+=count done+=per_chan_weight @@ -226,7 +222,7 @@ class Backup(commands.Cog): rel=os.path.relpath(full,base_dir) zf.write(full,rel) except Exception as exc: - prog_embed.color=discord.Color.red() + prog_embed.color=disnake.Color.red() prog_embed.set_field_at(0,name="Progression",value=progress_bar(1.0),inline=False) prog_embed.set_footer(text=f"Erreur lors de la création du ZIP : {exc}") await progress_msg.edit(embed=prog_embed) @@ -234,15 +230,15 @@ class Backup(commands.Cog): return # Finalisation - prog_embed.color=discord.Color.green() + prog_embed.color=disnake.Color.green() prog_embed.set_field_at(0,name="Progression",value=progress_bar(1.0),inline=False) prog_embed.add_field(name="Archive",value=f"`{zip_path}`",inline=False) prog_embed.set_footer(text="Sauvegarde terminée ✅") await progress_msg.edit(embed=prog_embed) await interaction.followup.send("🎉 Sauvegarde terminée. Un message de progression avec le chemin de l’archive a été publié.", ephemeral=True) - @backup_group.command(name="list", description="Lister les archives de sauvegarde disponibles") - async def backup_list(self, interaction: discord.Interaction): + @backup_group.sub_command(name="list", description="Lister les archives de sauvegarde disponibles") + async def backup_list(self, interaction: disnake.ApplicationCommandInteraction): guild = interaction.guild assert guild is not None, "Cette commande doit être utilisée dans un serveur." @@ -256,7 +252,7 @@ class Backup(commands.Cog): ) return - await interaction.response.defer(ephemeral=True, thinking=True) + await interaction.response.defer(ephemeral=True) ensure_dir(BACKUPS_ROOT) files = [f for f in os.listdir(BACKUPS_ROOT) if f.endswith(".zip")] files.sort(reverse=True) @@ -266,7 +262,7 @@ class Backup(commands.Cog): return desc="\n".join(f"• `{name}`" for name in files[:20]) - embed=discord.Embed(title="📦 Archives disponibles",description=desc,color=discord.Color.blurple()) + embed=disnake.Embed(title="📦 Archives disponibles",description=desc,color=disnake.Color.blurple()) if len(files)>20: embed.set_footer(text=f"... et {len(files)-20} de plus") await interaction.followup.send(embed=embed,ephemeral=True) diff --git a/commandes/backup_restore.py b/commandes/backup_restore.py index 14670d4..d7f69cb 100755 --- a/commandes/backup_restore.py +++ b/commandes/backup_restore.py @@ -5,9 +5,8 @@ import zipfile import tempfile import shutil from typing import List -import discord -from discord import app_commands -from discord.ext import commands +import disnake +from disnake.ext import commands BACKUPS_ROOT = "data/security_backups" @@ -43,7 +42,7 @@ class BackupRestore(commands.Cog): elements.append(base) return elements - async def element_autocomplete(self, interaction: discord.Interaction, current: str): + async def element_autocomplete(self, interaction: disnake.ApplicationCommandInteraction, current: str): """Complétion automatique pour l'option 'element'.""" guild = interaction.guild if guild is None: @@ -60,9 +59,9 @@ class BackupRestore(commands.Cog): backup_path = os.path.join(BACKUPS_ROOT, last_backup) elements = await self.list_backup_elements(backup_path) - return [app_commands.Choice(name=e, value=e) for e in elements if current.lower() in e.lower()][:25] + return [e for e in elements if current.lower() in e.lower()][:25] - async def restore_roles(self, guild: discord.Guild, backup_path: str): + async def restore_roles(self, guild: disnake.Guild, backup_path: str): """Restaure les rôles à partir du backup.""" roles_file = os.path.join(backup_path, "roles.json") if os.path.isfile(roles_file): @@ -72,16 +71,16 @@ class BackupRestore(commands.Cog): try: await guild.create_role( name=r["name"], - permissions=discord.Permissions(r["permissions"]), - color=discord.Color(r.get("color", 0)), + permissions=disnake.Permissions(r["permissions"]), + color=disnake.Color(r.get("color", 0)), hoist=r.get("hoist", False), mentionable=r.get("mentionable", False), reason="Restauration backup" ) - except discord.Forbidden: + except disnake.Forbidden: continue - async def restore_channels(self, guild: discord.Guild, backup_path: str): + async def restore_channels(self, guild: disnake.Guild, backup_path: str): """Restaure les channels à partir du backup.""" channels_file = os.path.join(backup_path, "channels.json") if os.path.isfile(channels_file): @@ -105,10 +104,10 @@ class BackupRestore(commands.Cog): user_limit=c.get("user_limit", 0), reason="Restauration backup" ) - except discord.Forbidden: + except disnake.Forbidden: continue - async def restore_messages(self, channel: discord.TextChannel, backup_path: str): + async def restore_messages(self, channel: disnake.TextChannel, backup_path: str): """Restaure les messages à partir du backup (solution ZIP incluse).""" if backup_path.endswith(".zip"): temp_dir = tempfile.mkdtemp() @@ -133,17 +132,12 @@ class BackupRestore(commands.Cog): for msg in messages: await channel.send(msg["content"]) - @app_commands.command(name="granulaire", description="Restaure un élément précis d'une sauvegarde") - @app_commands.describe( - backup="Nom ou ID de la sauvegarde", - element="Élément à restaurer (roles, channels, messages...)" - ) - @app_commands.autocomplete(element=element_autocomplete) + @commands.slash_command(name="granulaire", description="Restaure un élément précis d'une sauvegarde") async def restore_granulaire( self, - interaction: discord.Interaction, - backup: str, - element: str + interaction: disnake.ApplicationCommandInteraction, + backup: str = commands.Param(description="Nom ou ID de la sauvegarde"), + element: str = commands.Param(description="Élément à restaurer (roles, channels, messages...)") ): guild = interaction.guild if guild is None: @@ -162,7 +156,7 @@ class BackupRestore(commands.Cog): await interaction.response.send_message(f"❌ La sauvegarde `{backup}` est introuvable.", ephemeral=True) return - await interaction.response.defer(ephemeral=True, thinking=True) + await interaction.response.defer(ephemeral=True) available_elements = await self.list_backup_elements(backup_path) if element not in available_elements: @@ -200,5 +194,9 @@ class BackupRestore(commands.Cog): ephemeral=True ) + @restore_granulaire.autocomplete("element") + async def element_autocomplete_decorator(self, interaction: disnake.ApplicationCommandInteraction, current: str): + return await self.element_autocomplete(interaction, current) + async def setup(bot: commands.Bot): await bot.add_cog(BackupRestore(bot)) diff --git a/commandes/blacklist.py b/commandes/blacklist.py index cc53e5e..a64f0b0 100755 --- a/commandes/blacklist.py +++ b/commandes/blacklist.py @@ -1,6 +1,5 @@ -import discord -from discord.ext import commands, tasks -from discord import app_commands +import disnake +from disnake.ext import commands, tasks import json import os import asyncio @@ -57,8 +56,8 @@ class Blacklist(commands.Cog): agents = load_json(AGENTS_FILE, {"agents": []}) return str(user_id) in agents["agents"] - @app_commands.command(name="blacklist", description="Blacklist un utilisateur") - async def blacklist(self, interaction: discord.Interaction, user: discord.User, reason: str = "Aucune raison fournie"): + @commands.slash_command(name="blacklist", description="Blacklist un utilisateur") + async def blacklist(self, interaction: disnake.ApplicationCommandInteraction, user: disnake.User, reason: str = "Aucune raison fournie"): if not self.is_agent(interaction.user.id): return await interaction.response.send_message("⛔ Tu n’as pas la permission.", ephemeral=True) @@ -70,7 +69,7 @@ class Blacklist(commands.Cog): save_json(BLACKLIST_FILE, blacklist) # Préparer l'embed pro - embed = discord.Embed( + embed = disnake.Embed( title="🚫 Mise en blacklist - Réseau Omega Kube", description=( f"Bonjour {user.mention},\n\n" @@ -82,7 +81,7 @@ class Blacklist(commands.Cog): "📩 Vous disposez de **7 jours** pour contester cette décision en ouvrant un ticket via le bouton ci-dessous.\n\n" "⏳ Passé ce délai, vous serez **définitivement banni de tout le réseau**, sans autre recours possible." ), - color=discord.Color.red() + color=disnake.Color.red() ) embed.set_footer(text="Omega Kube - Nixfix06 & Mgstudios") @@ -122,8 +121,8 @@ class Blacklist(commands.Cog): ephemeral=True ) - @app_commands.command(name="unblacklist", description="Retirer un utilisateur de la blacklist") - async def unblacklist(self, interaction: discord.Interaction, user: discord.User): + @commands.slash_command(name="unblacklist", description="Retirer un utilisateur de la blacklist") + async def unblacklist(self, interaction: disnake.ApplicationCommandInteraction, user: disnake.User): if not self.is_agent(interaction.user.id): return await interaction.response.send_message("⛔ Tu n’as pas la permission.", ephemeral=True) @@ -163,21 +162,21 @@ class Blacklist(commands.Cog): save_json(BLACKLIST_FILE, blacklist) @commands.Cog.listener() - async def on_message(self, message: discord.Message): + async def on_message(self, message: disnake.Message): if message.author.bot: return tickets = load_json(TICKETS_FILE, {}) - if isinstance(message.channel, discord.DMChannel): + if isinstance(message.channel, disnake.DMChannel): user_id = str(message.author.id) if user_id in tickets: guild = self.bot.get_guild(MAIN_GUILD_ID) ticket_channel = guild.get_channel(tickets[user_id]["channel_id"]) if ticket_channel: - embed = discord.Embed( + embed = disnake.Embed( description=message.content, - color=discord.Color.blue() + color=disnake.Color.blue() ).set_author(name=message.author, icon_url=message.author.display_avatar.url) await ticket_channel.send(embed=embed) @@ -186,22 +185,22 @@ class Blacklist(commands.Cog): if message.channel.id == data["channel_id"] and self.is_agent(message.author.id): user = await self.bot.fetch_user(int(user_id)) try: - embed = discord.Embed( + embed = disnake.Embed( description=message.content, - color=discord.Color.green() + color=disnake.Color.green() ).set_author(name=f"Agent {message.author}", icon_url=message.author.display_avatar.url) await user.send(embed=embed) except: pass # --------- Boutons --------- -class TicketButton(discord.ui.View): +class TicketButton(disnake.ui.View): def __init__(self, user_id: int): super().__init__(timeout=None) self.user_id = user_id - @discord.ui.button(label="📩 Ouvrir une demande de révision", style=discord.ButtonStyle.primary, custom_id="blacklist_open_ticket") - async def open_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="📩 Ouvrir une demande de révision", style=disnake.ButtonStyle.primary, custom_id="blacklist_open_ticket") + async def open_ticket(self, interaction: disnake.Interaction, button: disnake.ui.Button): if interaction.user.id != self.user_id: return await interaction.response.send_message("❌ Ce bouton ne t’appartient pas.", ephemeral=True) @@ -209,7 +208,7 @@ class TicketButton(discord.ui.View): if str(self.user_id) in tickets: return await interaction.response.send_message("❌ Tu as déjà un ticket ouvert.", ephemeral=True) - guild = interaction.client.get_guild(MAIN_GUILD_ID) + guild = interaction.bot.get_guild(MAIN_GUILD_ID) category = guild.get_channel(TICKET_CATEGORY_ID) ticket_channel = await guild.create_text_channel( @@ -222,10 +221,10 @@ class TicketButton(discord.ui.View): save_json(TICKETS_FILE, tickets) view_close = CloseTicketView(self.user_id) - embed = discord.Embed( + embed = disnake.Embed( title="📩 Ticket ouvert", description=f"Un utilisateur blacklisté (<@{self.user_id}>) a ouvert une demande de révision.\n\nUn agent Omega Kube prendra en charge ce dossier.", - color=discord.Color.blurple() + color=disnake.Color.blurple() ) await ticket_channel.send(embed=embed, view=view_close) @@ -234,27 +233,27 @@ class TicketButton(discord.ui.View): button.disabled = True self.stop() -class CloseTicketView(discord.ui.View): +class CloseTicketView(disnake.ui.View): def __init__(self, user_id: int): super().__init__(timeout=None) self.user_id = user_id - @discord.ui.button(label="❌ Fermer le ticket", style=discord.ButtonStyle.danger, custom_id="blacklist_close_ticket") - async def close_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): - if not interaction.client.get_cog("Blacklist").is_agent(interaction.user.id): + @disnake.ui.button(label="❌ Fermer le ticket", style=disnake.ButtonStyle.danger, custom_id="blacklist_close_ticket") + async def close_ticket(self, interaction: disnake.Interaction, button: disnake.ui.Button): + if not interaction.bot.get_cog("Blacklist").is_agent(interaction.user.id): return await interaction.response.send_message("⛔ Seuls les agents peuvent fermer ce ticket.", ephemeral=True) view = ConfirmCloseView(self.user_id) await interaction.response.send_message("⚠️ Veux-tu vraiment fermer ce ticket ?", view=view, ephemeral=True) -class ConfirmCloseView(discord.ui.View): +class ConfirmCloseView(disnake.ui.View): def __init__(self, user_id: int): super().__init__(timeout=30) self.user_id = user_id - @discord.ui.button(label="✅ Oui", style=discord.ButtonStyle.success, custom_id="blacklist_confirm_close") - async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button): - if not interaction.client.get_cog("Blacklist").is_agent(interaction.user.id): + @disnake.ui.button(label="✅ Oui", style=disnake.ButtonStyle.success, custom_id="blacklist_confirm_close") + async def confirm(self, interaction: disnake.Interaction, button: disnake.ui.Button): + if not interaction.bot.get_cog("Blacklist").is_agent(interaction.user.id): return await interaction.response.send_message("⛔ Tu n’as pas la permission.", ephemeral=True) tickets = load_json(TICKETS_FILE, {}) @@ -268,19 +267,19 @@ class ConfirmCloseView(discord.ui.View): del tickets[str(self.user_id)] save_json(TICKETS_FILE, tickets) - user = await interaction.client.fetch_user(int(self.user_id)) + user = await interaction.bot.fetch_user(int(self.user_id)) try: - embed = discord.Embed( + embed = disnake.Embed( title="🔒 Ticket fermé", description="Ton ticket a été examiné et fermé.\n\n⚠️ Après analyse, la sanction est confirmée et tu restes blacklisté de l’ensemble du réseau.", - color=discord.Color.orange() + color=disnake.Color.orange() ) await user.send(embed=embed) await asyncio.sleep(2) except: pass - for guild in interaction.client.guilds: + for guild in interaction.bot.guilds: member = guild.get_member(self.user_id) if member: try: @@ -290,8 +289,8 @@ class ConfirmCloseView(discord.ui.View): await interaction.response.send_message("✅ Ticket fermé avec succès.", ephemeral=True) - @discord.ui.button(label="❌ Non", style=discord.ButtonStyle.secondary, custom_id="blacklist_cancel_close") - async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="❌ Non", style=disnake.ButtonStyle.secondary, custom_id="blacklist_cancel_close") + async def cancel(self, interaction: disnake.Interaction, button: disnake.ui.Button): await interaction.response.send_message("❌ Fermeture annulée.", ephemeral=True) # --------- Setup --------- diff --git a/commandes/bug_report.py b/commandes/bug_report.py index 925246c..6bbc736 100644 --- a/commandes/bug_report.py +++ b/commandes/bug_report.py @@ -1,6 +1,5 @@ -import discord -from discord import app_commands -from discord.ext import commands +import disnake +from disnake.ext import commands from utils.gitlab_client import gitlab_client from aiohttp import web import logging @@ -33,27 +32,27 @@ def save_report(issue_iid, user_id): except Exception as e: kuby_logger.error(f"Error saving report to JSON: {e}") -class BugReportModal(discord.ui.Modal, title="Signaler un Bug"): +class BugReportModal(disnake.ui.Modal): def __init__(self, priority_choice): - super().__init__() + super().__init__(title="Signaler un Bug") self.priority_choice = priority_choice - bug_title = discord.ui.TextInput( + bug_title = disnake.ui.TextInput( label="Titre du bug", placeholder="Décrivez brièvement le problème...", required=True, max_length=100 ) - description = discord.ui.TextInput( + description = disnake.ui.TextInput( label="Description détaillée", - style=discord.TextStyle.paragraph, + style=disnake.TextStyle.paragraph, placeholder="Que s'est-il passé ? Comment reproduire le bug ?", required=True, max_length=1000 ) - async def on_submit(self, interaction: discord.Interaction): + async def callback(self, interaction: disnake.Interaction): await interaction.response.send_message("Envoi de votre rapport de bug à GitLab...", ephemeral=True) description_text = f"**Rapporté par:** {interaction.user} ({interaction.user.id})\n" @@ -76,23 +75,26 @@ class BugReportModal(discord.ui.Modal, title="Signaler un Bug"): else: await interaction.edit_original_response(content="❌ Une erreur est survenue lors de l'envoi du rapport à GitLab. Veuillez contacter un administrateur.") -class FeatureSuggestionModal(discord.ui.Modal, title="Suggérer une fonctionnalité"): - suggestion_title = discord.ui.TextInput( +class FeatureSuggestionModal(disnake.ui.Modal): + def __init__(self): + super().__init__(title="Suggérer une fonctionnalité") + + suggestion_title = disnake.ui.TextInput( label="Titre de la suggestion", placeholder="Que voulez-vous ajouter ?", required=True, max_length=100 ) - description = discord.ui.TextInput( + description = disnake.ui.TextInput( label="Détails de la fonctionnalité", - style=discord.TextStyle.paragraph, + style=disnake.TextStyle.paragraph, placeholder="Décrivez comment cela devrait fonctionner...", required=True, max_length=1000 ) - async def on_submit(self, interaction: discord.Interaction): + async def callback(self, interaction: disnake.Interaction): await interaction.response.send_message("Envoi de votre suggestion à GitLab...", ephemeral=True) description_text = f"**Suggéré par:** {interaction.user} ({interaction.user.id})\n\n" @@ -231,10 +233,10 @@ class BugReport(commands.Cog): try: # Création de l'embed pour la mise à jour de statut - embed = discord.Embed( + embed = disnake.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() + color=disnake.Color.blue() ) embed.add_field(name="Nouveaux Labels / Statuts", value=current_labels_str) @@ -244,7 +246,7 @@ class BugReport(commands.Cog): kuby_logger.info(f"Notification de label envoyée à {user} ({user.id}) pour l'issue #{issue_iid}") if dev and dev.id != user.id: await dev.send(f"✅ [SUIVI] L'utilisateur {user} ({user.id}) a bien reçu la notification de statut **{current_labels_str}** pour l'issue #{issue_iid} ({issue_title}).") - except discord.Forbidden: + except disnake.Forbidden: kuby_logger.warning(f"Impossible d'envoyer un MP à {user} ({user.id}) - MPs désactivés.") if dev and dev.id != user.id: await dev.send(f"⚠️ [DM BLOQUÉ] {user} ({user.id}) a ses MPs fermés. Impossible de notifier pour l'issue #{issue_iid}.") @@ -259,10 +261,10 @@ class BugReport(commands.Cog): if is_closing_now: try: - embed = discord.Embed( + embed = disnake.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() + color=disnake.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 !") @@ -272,7 +274,7 @@ class BugReport(commands.Cog): kuby_logger.info(f"Notification de clôture envoyée à {user} ({user.id}) pour l'issue #{issue_iid}") if dev and dev.id != user.id: await dev.send(f"✅ [SUIVI] L'utilisateur {user} ({user.id}) a bien reçu la notification de CLÔTURE pour l'issue #{issue_iid}.") - except discord.Forbidden: + except disnake.Forbidden: kuby_logger.warning(f"Impossible d'envoyer le MP de clôture à {user} ({user.id}).") if dev and dev.id != user.id: await dev.send(f"⚠️ [DM BLOQUÉ] Impossible d'annoncer la clôture à {user} ({user.id}) pour l'issue #{issue_iid} (DMs désactivés).") @@ -290,10 +292,10 @@ class BugReport(commands.Cog): author_name = payload.get("user", {}).get("name", "Développeur") try: - embed = discord.Embed( + embed = disnake.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() + color=disnake.Color.orange() ) embed.set_footer(text=f"Par {author_name}") @@ -302,7 +304,7 @@ class BugReport(commands.Cog): kuby_logger.info(f"Commentaire GitLab envoyé à {user} ({user.id}) pour l'issue #{issue_iid}") if dev and dev.id != user.id: await dev.send(f"✅ [SUIVI] L'utilisateur {user} ({user.id}) a bien reçu votre commentaire pour l'issue #{issue_iid}.") - except discord.Forbidden: + except disnake.Forbidden: kuby_logger.warning(f"Impossible d'envoyer le commentaire en MP à {user} ({user.id}).") if dev and dev.id != user.id: await dev.send(f"⚠️ [DM BLOQUÉ] Impossible d'envoyer votre commentaire à {user} ({user.id}) pour l'issue #{issue_iid} (DMs désactivés).") @@ -317,18 +319,19 @@ class BugReport(commands.Cog): 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=[ - app_commands.Choice(name="Basse", value="Low"), - app_commands.Choice(name="Normale", value="Normal"), - 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]): - await interaction.response.send_modal(BugReportModal(priority)) + @commands.slash_command(name="signaler_bug", description="Signaler un bug aux développeurs") + async def signaler_bug(self, interaction: disnake.ApplicationCommandInteraction, + priority: str = commands.Param(description="Priorité du bug", choices={"Basse": "Low", "Normale": "Normal", "Haute": "High", "Urgente": "Urgent"})): + # Simulate app_commands.Choice behavior for existing code + from collections import namedtuple + Choice = namedtuple('Choice', ['name', 'value']) + # find the name from the value + p_name = [k for k, v in {"Basse": "Low", "Normale": "Normal", "Haute": "High", "Urgente": "Urgent"}.items() if v == priority][0] + choice = Choice(name=p_name, value=priority) + await interaction.response.send_modal(BugReportModal(choice)) - @app_commands.command(name="suggerer_fonctionnalite", description="Proposer une nouvelle fonctionnalité pour le bot") - async def suggerer_fonctionnalite(self, interaction: discord.Interaction): + @commands.slash_command(name="suggerer_fonctionnalite", description="Proposer une nouvelle fonctionnalité pour le bot") + async def suggerer_fonctionnalite(self, interaction: disnake.ApplicationCommandInteraction): await interaction.response.send_modal(FeatureSuggestionModal()) async def setup(bot): diff --git a/commandes/convocation.py b/commandes/convocation.py index d38f884..bb3921f 100755 --- a/commandes/convocation.py +++ b/commandes/convocation.py @@ -1,6 +1,5 @@ -import discord -from discord.ext import commands -from discord import app_commands +import disnake +from disnake.ext import commands import json import os from typing import List, Optional, Dict @@ -52,20 +51,17 @@ class Convocation(commands.Cog): del self.backups[key] save_json(CONV_BACKUPS_FILE, self.backups) - @commands.hybrid_group(name="convocation", description="Gère les convocations de membres") + @commands.slash_command(name="convocation", description="Gère les convocations de membres") @commands.has_permissions(manage_roles=True) - async def convocation_group(self, ctx): - if ctx.invoked_subcommand is None: - await ctx.send_help(ctx.command) + async def convocation_group(self, interaction: disnake.ApplicationCommandInteraction): + pass - @convocation_group.command(name="convoquer", description="Convoque un membre et verrouille ses accès") - @app_commands.describe( - membre="Le membre à convoquer", - raison="La raison de la convocation", - heure="L'heure de la convocation (ex: 14:30)" - ) - async def convoquer(self, ctx, membre: discord.Member, raison: str, heure: str): - guild_id = ctx.guild.id + @convocation_group.sub_command(name="convoquer", description="Convoque un membre et verrouille ses accès") + async def convoquer(self, interaction: disnake.ApplicationCommandInteraction, + membre: disnake.Member = commands.Param(description="Le membre à convoquer"), + raison: str = commands.Param(description="La raison de la convocation"), + heure: str = commands.Param(description="L'heure de la convocation (ex: 14:30)")): + guild_id = interaction.guild.id settings = self.get_guild_settings(guild_id) role_conv_id = settings.get("role_id") log_channel_id = settings.get("log_channel_id") @@ -73,30 +69,30 @@ class Convocation(commands.Cog): admin_ids = settings.get("admin_ids", []) if not role_conv_id: - await ctx.send("❌ Le rôle de convocation n'est pas configuré. Utilisez `/convocation config`.", ephemeral=True) + await interaction.response.send_message("❌ Le rôle de convocation n'est pas configuré. Utilisez `/convocation config`.", ephemeral=True) return - await ctx.defer(ephemeral=True) + await interaction.response.defer(ephemeral=True) try: # --- BACKUP --- - bot_top_role = ctx.guild.me.top_role + bot_top_role = interaction.guild.me.top_role backup_data = { "roles": [r.id for r in membre.roles if not r.is_default() and r.id != role_conv_id and r.position < bot_top_role.position], "overrides": {} } # 1. Backup and Remove current roles - current_roles = [ctx.guild.get_role(rid) for rid in backup_data["roles"] if ctx.guild.get_role(rid)] + current_roles = [interaction.guild.get_role(rid) for rid in backup_data["roles"] if interaction.guild.get_role(rid)] if current_roles: try: - await membre.remove_roles(*current_roles, reason=f"Convocation par {ctx.author}") - except discord.Forbidden: - await ctx.send("⚠️ Je n'ai pas pu retirer certains rôles (Permission refusée).", ephemeral=True) + await membre.remove_roles(*current_roles, reason=f"Convocation par {interaction.author}") + except disnake.Forbidden: + await interaction.followup.send("⚠️ Je n'ai pas pu retirer certains rôles (Permission refusée).", ephemeral=True) # 2. Dynamic Lockdown: Loop through ALL channels lockdown_count = 0 - for channel in ctx.guild.channels: + for channel in interaction.guild.channels: # On ignore le salon d'attente et ses parents s'il y en a if attente_id and (channel.id == attente_id or (hasattr(channel, 'category') and channel.category and channel.category.id == attente_id)): continue @@ -112,28 +108,28 @@ class Convocation(commands.Cog): # Specific override for this member: View Channel = False await channel.set_permissions(membre, view_channel=False, reason="Convocation Lockdown") lockdown_count += 1 - except discord.Forbidden: + except disnake.Forbidden: pass # Skip if we can't touch this channel self.save_backup(guild_id, membre.id, backup_data) # 3. Add Convocation role - role_conv = ctx.guild.get_role(role_conv_id) + role_conv = interaction.guild.get_role(role_conv_id) if role_conv: if role_conv.position >= bot_top_role.position: - await ctx.send(f"⚠️ Je ne peux pas donner le rôle {role_conv.mention} car il est plus haut que moi !", ephemeral=True) + await interaction.followup.send(f"⚠️ Je ne peux pas donner le rôle {role_conv.mention} car il est plus haut que moi !", ephemeral=True) else: try: await membre.add_roles(role_conv, reason="Début de convocation") - except discord.Forbidden: - await ctx.send(f"⚠️ Impossible de donner le rôle {role_conv.name} (Permission refusée).", ephemeral=True) + except disnake.Forbidden: + await interaction.followup.send(f"⚠️ Impossible de donner le rôle {role_conv.name} (Permission refusée).", ephemeral=True) # 4. Embeds & Logs (Inchangés mais inclus pour la cohérence) - embed_conv = discord.Embed( + embed_conv = disnake.Embed( title="CONVOCATION | 🔖", - description=f"> Bonjour {membre.mention}, vous êtes **convoqué** par {ctx.author.mention}. Voici les **détails** :", + description=f"> Bonjour {membre.mention}, vous êtes **convoqué** par {interaction.author.mention}. Voici les **détails** :", color=0x0099ff, - timestamp=discord.utils.utcnow() + timestamp=disnake.utils.utcnow() ) embed_conv.add_field(name="**Raison de la convocation :**", value=f"➜ {raison}", inline=False) embed_conv.add_field(name="**Heure de la convocation :**", value=f"➜ {heure}", inline=False) @@ -141,15 +137,15 @@ class Convocation(commands.Cog): "> Merci de vous présenter dans le vocal attente **5 minutes** avant l'heure.\n\n" "> La **levée** de suspension se fera uniquement à la **fin** de la convocation." ), inline=False) - embed_conv.set_footer(text=f"Gestion de {ctx.guild.name}") + embed_conv.set_footer(text=f"Gestion de {interaction.guild.name}") - embed_log = discord.Embed( + embed_log = disnake.Embed( title="📋 CONVOCATION ENVOYÉE", color=0xff9900, - timestamp=discord.utils.utcnow() + timestamp=disnake.utils.utcnow() ) embed_log.add_field(name="👤 Membre convoqué", value=f"{membre.mention} ({membre.id})", inline=True) - embed_log.add_field(name="👮 Convoqué par", value=f"{ctx.author.mention}", inline=True) + embed_log.add_field(name="👮 Convoqué par", value=f"{interaction.author.mention}", inline=True) embed_log.add_field(name="📄 Raison", value=raison, inline=False) embed_log.add_field(name="🕒 Heure", value=heure, inline=False) @@ -158,7 +154,7 @@ class Convocation(commands.Cog): try: await membre.send(embed=embed_conv) dm_sent = True - except discord.Forbidden: + except disnake.Forbidden: dm_sent = False # Admin & Channel Logs @@ -169,47 +165,47 @@ class Convocation(commands.Cog): except: pass if log_channel_id: - chan = ctx.guild.get_channel(log_channel_id) + chan = interaction.guild.get_channel(log_channel_id) if chan: await chan.send(embed=embed_log) - await ctx.send(f"✅ Convocation envoyée à {membre.mention}. ({lockdown_count} salons verrouillés) " + + await interaction.followup.send(f"✅ Convocation envoyée à {membre.mention}. ({lockdown_count} salons verrouillés) " + ("" if dm_sent else "(⚠️ DM impossible)"), ephemeral=True) except Exception as e: import traceback traceback.print_exc() - await ctx.send(f"❌ Une erreur critique est survenue : {e}", ephemeral=True) + await interaction.followup.send(f"❌ Une erreur critique est survenue : {e}", ephemeral=True) - @convocation_group.command(name="lever", description="Lève la convocation d'un membre et restaure ses accès") - @app_commands.describe(membre="Le membre à libérer") - async def lever(self, ctx, membre: discord.Member): - guild_id = ctx.guild.id + @convocation_group.sub_command(name="lever", description="Lève la convocation d'un membre et restaure ses accès") + async def lever(self, interaction: disnake.ApplicationCommandInteraction, + membre: disnake.Member = commands.Param(description="Le membre à libérer")): + guild_id = interaction.guild.id settings = self.get_guild_settings(guild_id) role_conv_id = settings.get("role_id") - await ctx.defer(ephemeral=True) + await interaction.response.defer(ephemeral=True) backup_data = self.get_backup(guild_id, membre.id) if not backup_data: - await ctx.send("⚠️ Aucune donnée de convocation trouvée pour ce membre.", ephemeral=True) + await interaction.followup.send("⚠️ Aucune donnée de convocation trouvée pour ce membre.", ephemeral=True) return # 1. Remove Convocation role if role_conv_id: - role_conv = ctx.guild.get_role(role_conv_id) + role_conv = interaction.guild.get_role(role_conv_id) if role_conv and role_conv in membre.roles: await membre.remove_roles(role_conv, reason="Fin de convocation") # 2. Restore channel overrides restore_count = 0 overrides = backup_data.get("overrides", {}) - for channel in ctx.guild.channels: + for channel in interaction.guild.channels: chan_id_str = str(channel.id) try: if chan_id_str in overrides: # Restore previous override ov_data = overrides[chan_id_str] - overwrite = discord.PermissionOverwrite(**ov_data) + overwrite = disnake.PermissionOverwrite(**ov_data) await channel.set_permissions(membre, overwrite=overwrite, reason="Restauration fin de convocation") restore_count += 1 else: @@ -217,7 +213,7 @@ class Convocation(commands.Cog): # On ne vide que si un override membre existait (sinon on risque de supprimer des perms rôles ?) # En fait set_permissions(member, overwrite=None) supprime l'entrée PERSONNELLE du membre. await channel.set_permissions(membre, overwrite=None, reason="Fin de convocation - Cleanup") - except discord.Forbidden: + except disnake.Forbidden: pass # 3. Restore roles @@ -225,8 +221,8 @@ class Convocation(commands.Cog): restored_roles = [] if role_ids: for rid in role_ids: - role = ctx.guild.get_role(rid) - if role and role.position < ctx.guild.me.top_role.position: + role = interaction.guild.get_role(rid) + if role and role.position < interaction.guild.me.top_role.position: restored_roles.append(role) if restored_roles: @@ -235,27 +231,24 @@ class Convocation(commands.Cog): self.clear_backup(guild_id, membre.id) # 4. Notification - embed = discord.Embed( + embed = disnake.Embed( title="🔓 CONVOCATION TERMINÉE", - description=f"Votre convocation sur **{ctx.guild.name}** est terminée. Vos accès ont été restaurés.", - color=discord.Color.green() + description=f"Votre convocation sur **{interaction.guild.name}** est terminée. Vos accès ont été restaurés.", + color=disnake.Color.green() ) try: await membre.send(embed=embed) except: pass - await ctx.send(f"✅ Convocation levée pour {membre.mention}. Access restaurés.", ephemeral=True) + await interaction.followup.send(f"✅ Convocation levée pour {membre.mention}. Access restaurés.", ephemeral=True) - @convocation_group.command(name="config", description="Configure le système de convocation") - @app_commands.describe( - role="Le rôle de convocation (Muet/Convocation)", - channel="Le salon de logs", - attente="Salon (ou catégorie) d'attente à laisser visible", - admins="Liste d'IDs d'admins à notifier (séparés par des espaces)" - ) - @commands.has_permissions(administrator=True) - async def config(self, ctx, role: discord.Role, channel: discord.TextChannel = None, attente: discord.abc.GuildChannel = None, admins: str = None): - guild_id = ctx.guild.id + @convocation_group.sub_command(name="config", description="Configure le système de convocation") + async def config(self, interaction: disnake.ApplicationCommandInteraction, + role: disnake.Role = commands.Param(description="Le rôle de convocation (Muet/Convocation)"), + channel: disnake.TextChannel = commands.Param(None, description="Le salon de logs"), + attente: disnake.abc.GuildChannel = commands.Param(None, description="Salon (ou catégorie) d'attente à laisser visible"), + admins: str = commands.Param(None, description="Liste d'IDs d'admins à notifier (séparés par des espaces)")): + guild_id = interaction.guild.id settings = self.get_guild_settings(guild_id) settings["role_id"] = role.id @@ -273,13 +266,13 @@ class Convocation(commands.Cog): self.save_guild_settings(guild_id, settings) - embed = discord.Embed(title="⚙️ Configuration Convocation Mise à jour", color=discord.Color.blue()) + embed = disnake.Embed(title="⚙️ Configuration Convocation Mise à jour", color=disnake.Color.blue()) embed.add_field(name="Rôle", value=role.mention) embed.add_field(name="Salon Logs", value=channel.mention if channel else "Non défini") embed.add_field(name="Salon/Cat d'attente", value=attente.mention if attente else "Non défini") embed.add_field(name="Admins notifiés", value=str(len(settings.get("admin_ids", [])))) - await ctx.send(embed=embed, ephemeral=True) + await interaction.response.send_message(embed=embed, ephemeral=True) async def setup(bot): await bot.add_cog(Convocation(bot)) diff --git a/commandes/goodbye.py b/commandes/goodbye.py index 2453c32..9e4497b 100644 --- a/commandes/goodbye.py +++ b/commandes/goodbye.py @@ -1,6 +1,5 @@ -import discord -from discord.ext import commands -from discord import app_commands +import disnake +from disnake.ext import commands import sqlite3 import aiohttp import io @@ -148,14 +147,14 @@ class Goodbye(commands.Cog): # Utiliser le message personnalisé ou un par défaut description = custom_message if custom_message else "Nous espérons vous revoir bientôt " - embed = discord.Embed(title=f"{member} viens de nous quitter.", + embed = disnake.Embed(title=f"{member} viens de nous quitter.", description=description, - color=discord.Color.dark_magenta()) + color=disnake.Color.dark_magenta()) embed.set_footer(text=f"Avait rejoint {joined_message}") embed.set_image(url="attachment://goodbye.png") - await channel.send(embed=embed, file=discord.File(img, filename="goodbye.png")) + await channel.send(embed=embed, file=disnake.File(img, filename="goodbye.png")) - async def save_banner_attachment(self, attachment: discord.Attachment, guild_id: int) -> str: + async def save_banner_attachment(self, attachment: disnake.Attachment, guild_id: int) -> str: """Sauvegarde l'attachment et retourne le chemin du fichier""" base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "banners")) os.makedirs(base_dir, exist_ok=True) @@ -187,9 +186,9 @@ class Goodbye(commands.Cog): return os.path.basename(banner_path) - @app_commands.command(name="setgoodbye", description="Définit le salon d'aurevoir et les paramètres personnalisés") - @app_commands.checks.has_permissions(administrator=True) - async def set_goodbye(self, interaction: discord.Interaction, channel: discord.TextChannel, server_name: str = None, message: str = None, banner_file: discord.Attachment = None): + @commands.slash_command(name="setgoodbye", description="Définit le salon d'aurevoir et les paramètres personnalisés") + @commands.has_permissions(administrator=True) + async def set_goodbye(self, interaction: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel, server_name: str = None, message: str = None, banner_file: disnake.Attachment = None): banner_path = None # Si un fichier est uploadé, le sauvegarder @@ -208,7 +207,7 @@ class Goodbye(commands.Cog): goodbye_server_name=COALESCE(excluded.goodbye_server_name, goodbye_server_name), goodbye_message=COALESCE(excluded.goodbye_message, goodbye_message), goodbye_banner_background=COALESCE(excluded.goodbye_banner_background, goodbye_banner_background)''', - (interaction.guild_id, channel.id, server_name, message, banner_path)) + (interaction.guild_id, channel.id, server_name, message, banner_path)) conn.commit() conn.close() diff --git a/commandes/moderation.py b/commandes/moderation.py index 379b05f..3f1174e 100644 --- a/commandes/moderation.py +++ b/commandes/moderation.py @@ -1,18 +1,17 @@ -import discord -from discord.ext import commands -from discord import app_commands +import disnake +from disnake.ext import commands import sqlite3 import os from datetime import datetime, timedelta import re -class ModReasonModal(discord.ui.Modal): +class ModReasonModal(disnake.ui.Modal): def __init__(self, parent_view, action_type): super().__init__(title=f"Raison pour {action_type}") self.parent_view = parent_view self.action_type = action_type - self.reason = discord.ui.TextInput( + self.reason = disnake.ui.TextInput( label="Raison", placeholder="Entrez la raison ici...", required=True, @@ -21,7 +20,7 @@ class ModReasonModal(discord.ui.Modal): self.add_item(self.reason) if action_type == "TIMEOUT": - self.duration = discord.ui.TextInput( + self.duration = disnake.ui.TextInput( label="Durée (ex: 1h, 30m, 1d)", placeholder="1h", required=True, @@ -29,11 +28,11 @@ class ModReasonModal(discord.ui.Modal): ) self.add_item(self.duration) - async def on_submit(self, interaction: discord.Interaction): + async def callback(self, interaction: disnake.Interaction): reason = self.reason.value member = self.parent_view.member - cog = interaction.client.get_cog("Moderation") + cog = interaction.bot.get_cog("Moderation") if not cog: return await interaction.response.send_message("❌ Erreur interne.", ephemeral=True) @@ -48,24 +47,24 @@ class ModReasonModal(discord.ui.Modal): await self.parent_view.update_panel(interaction) -class ModPanelView(discord.ui.View): +class ModPanelView(disnake.ui.View): def __init__(self, member, moderator, db_path): super().__init__(timeout=120) self.member = member self.moderator = moderator self.db_path = db_path - async def update_panel(self, interaction: discord.Interaction): + async def update_panel(self, interaction: disnake.Interaction): conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute('SELECT COUNT(*) FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "WARN" AND status = "ACTIVE"', (interaction.guild_id, self.member.id)) warn_count = cursor.fetchone()[0] conn.close() - embed = discord.Embed( + embed = disnake.Embed( title=f"🛡️ Panel de Modération - {self.member.name}", description=f"Gestion de l'utilisateur {self.member.mention}", - color=discord.Color.blue(), + color=disnake.Color.blue(), timestamp=datetime.now() ) embed.set_thumbnail(url=self.member.display_avatar.url) @@ -78,38 +77,38 @@ class ModPanelView(discord.ui.View): else: await interaction.edit_original_response(embed=embed, view=self) - @discord.ui.button(label="Avertir", style=discord.ButtonStyle.secondary, emoji="⚠️") - async def warn_btn(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="Avertir", style=disnake.ButtonStyle.secondary, emoji="⚠️") + async def warn_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button): await interaction.response.send_modal(ModReasonModal(self, "WARN")) - @discord.ui.button(label="Timeout", style=discord.ButtonStyle.secondary, emoji="⏳") - async def timeout_btn(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="Timeout", style=disnake.ButtonStyle.secondary, emoji="⏳") + async def timeout_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button): await interaction.response.send_modal(ModReasonModal(self, "TIMEOUT")) - @discord.ui.button(label="Expulser", style=discord.ButtonStyle.secondary, emoji="👢") - async def kick_btn(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="Expulser", style=disnake.ButtonStyle.secondary, emoji="👢") + async def kick_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button): await interaction.response.send_modal(ModReasonModal(self, "KICK")) - @discord.ui.button(label="Bannir", style=discord.ButtonStyle.danger, emoji="🔨") - async def ban_btn(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="Bannir", style=disnake.ButtonStyle.danger, emoji="🔨") + async def ban_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button): await interaction.response.send_modal(ModReasonModal(self, "BAN")) - @discord.ui.button(label="Historique", style=discord.ButtonStyle.primary, emoji="📜") - async def history_btn(self, interaction: discord.Interaction, button: discord.ui.Button): - cog = interaction.client.get_cog("Moderation") + @disnake.ui.button(label="Historique", style=disnake.ButtonStyle.primary, emoji="📜") + async def history_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button): + cog = interaction.bot.get_cog("Moderation") await cog.history(interaction, self.member) - @discord.ui.button(label="Clear Warns", style=discord.ButtonStyle.danger, emoji="🧹") - async def clear_btn(self, interaction: discord.Interaction, button: discord.ui.Button): - cog = interaction.client.get_cog("Moderation") + @disnake.ui.button(label="Clear Warns", style=disnake.ButtonStyle.danger, emoji="🧹") + async def clear_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button): + cog = interaction.bot.get_cog("Moderation") await cog.clearwarns(interaction, self.member, reason="Nettoyage via Panel") await self.update_panel(interaction) -class ModConfigModal(discord.ui.Modal): +class ModConfigModal(disnake.ui.Modal): def __init__(self, field_name, current_value): super().__init__(title=f"Modifier {field_name}") self.field_name = field_name - self.input = discord.ui.TextInput( + self.input = disnake.ui.TextInput( label=field_name, default=str(current_value), placeholder="Entrez une valeur numérique...", @@ -117,9 +116,9 @@ class ModConfigModal(discord.ui.Modal): ) self.add_item(self.input) - async def on_submit(self, interaction: discord.Interaction): + async def callback(self, interaction: disnake.Interaction): value = self.input.value - cog = interaction.client.get_cog("Moderation") + cog = interaction.bot.get_cog("Moderation") mapping = { "Limite Timeout": "warn_limit_timeout", @@ -140,21 +139,21 @@ class ModConfigModal(discord.ui.Modal): await interaction.response.send_message(f"✅ {self.field_name} mis à jour sur **{value}**.", ephemeral=True) -class ModConfigView(discord.ui.View): +class ModConfigView(disnake.ui.View): def __init__(self, guild_id, db_path): super().__init__(timeout=60) self.guild_id = guild_id self.db_path = db_path - @discord.ui.button(label="Log Channel", style=discord.ButtonStyle.primary, emoji="📁") - async def log_btn(self, interaction: discord.Interaction, button: discord.ui.Button): - view = discord.ui.View() - select = discord.ui.ChannelSelect( + @disnake.ui.button(label="Log Channel", style=disnake.ButtonStyle.primary, emoji="📁") + async def log_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button): + view = disnake.ui.View() + select = disnake.ui.ChannelSelect( placeholder="Sélectionnez le salon de logs...", - channel_types=[discord.ChannelType.text] + channel_types=[disnake.ChannelType.text] ) - async def select_callback(interaction: discord.Interaction): + async def select_callback(interaction: disnake.Interaction): channel = select.values[0] conn = sqlite3.connect(self.db_path) cursor = conn.cursor() @@ -166,29 +165,29 @@ class ModConfigView(discord.ui.View): view.add_item(select) await interaction.response.send_message("Choisissez le salon de logs :", view=view, ephemeral=True) - @discord.ui.button(label="Paliers Warns", style=discord.ButtonStyle.secondary, emoji="⚖️") - async def limits_btn(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="Paliers Warns", style=disnake.ButtonStyle.secondary, emoji="⚖️") + async def limits_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button): conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute("SELECT warn_limit_timeout, warn_limit_kick, warn_limit_ban FROM mod_config WHERE guild_id = ?", (self.guild_id,)) res = cursor.fetchone(); conn.close() - view = discord.ui.View() + view = disnake.ui.View() - btn_t = discord.ui.Button(label=f"Timeout ({res[0]})", style=discord.ButtonStyle.secondary) + btn_t = disnake.ui.Button(label=f"Timeout ({res[0]})", style=disnake.ButtonStyle.secondary) btn_t.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Timeout", res[0])) - btn_k = discord.ui.Button(label=f"Kick ({res[1]})", style=discord.ButtonStyle.secondary) + btn_k = disnake.ui.Button(label=f"Kick ({res[1]})", style=disnake.ButtonStyle.secondary) btn_k.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Kick", res[1])) - btn_b = discord.ui.Button(label=f"Ban ({res[2]})", style=discord.ButtonStyle.secondary) + btn_b = disnake.ui.Button(label=f"Ban ({res[2]})", style=disnake.ButtonStyle.secondary) btn_b.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Ban", res[2])) view.add_item(btn_t); view.add_item(btn_k); view.add_item(btn_b) await interaction.response.send_message("Quelle limite modifier ?", view=view, ephemeral=True) - @discord.ui.button(label="Durée Timeout", style=discord.ButtonStyle.secondary, emoji="⏱️") - async def duration_btn(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="Durée Timeout", style=disnake.ButtonStyle.secondary, emoji="⏱️") + async def duration_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button): conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute("SELECT timeout_duration FROM mod_config WHERE guild_id = ?", (self.guild_id,)) @@ -225,38 +224,38 @@ class Moderation(commands.Cog): log_channel = guild.get_channel(result[0]) if log_channel: await log_channel.send(embed=embed) - @app_commands.command(name="modpanel", description="Ouvre le panel de modération pour un membre") - @app_commands.checks.has_permissions(moderate_members=True) - async def modpanel(self, interaction: discord.Interaction, member: discord.Member): + @commands.slash_command(name="modpanel", description="Ouvre le panel de modération pour un membre") + @commands.has_permissions(moderate_members=True) + async def modpanel(self, interaction: disnake.ApplicationCommandInteraction, member: disnake.Member): if member.top_role >= interaction.user.top_role and interaction.user.id != interaction.guild.owner_id: return await interaction.response.send_message("❌ Impossible de gérer ce membre.", ephemeral=True) conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute('SELECT COUNT(*) FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "WARN" AND status = "ACTIVE"', (interaction.guild_id, member.id)) warn_count = cursor.fetchone()[0]; conn.close() - embed = discord.Embed(title=f"🛡️ Panel de Modération - {member.name}", description=f"Gestion de {member.mention}", color=discord.Color.blue(), timestamp=datetime.now()) + embed = disnake.Embed(title=f"🛡️ Panel de Modération - {member.name}", description=f"Gestion de {member.mention}", color=disnake.Color.blue(), timestamp=datetime.now()) embed.set_thumbnail(url=member.display_avatar.url) embed.add_field(name="👤 Utilisateur", value=f"ID: `{member.id}`", inline=True) embed.add_field(name="⚠️ Avertissements", value=f"Actifs: **{warn_count}**", inline=True) view = ModPanelView(member, interaction.user, self.db_path) await interaction.response.send_message(embed=embed, view=view, ephemeral=True) - @app_commands.command(name="modconfig", description="Configuration de la modération") - @app_commands.checks.has_permissions(administrator=True) - async def modconfig(self, interaction: discord.Interaction): + @commands.slash_command(name="modconfig", description="Configuration de la modération") + @commands.has_permissions(administrator=True) + async def modconfig(self, interaction: disnake.ApplicationCommandInteraction): conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute('INSERT OR IGNORE INTO mod_config (guild_id) VALUES (?)', (interaction.guild_id,)) cursor.execute('SELECT log_channel_id, warn_limit_timeout, warn_limit_kick, warn_limit_ban, timeout_duration FROM mod_config WHERE guild_id = ?', (interaction.guild_id,)) res = cursor.fetchone(); conn.commit(); conn.close() - embed = discord.Embed(title="⚙️ Configuration Modération", description="Modifiez les paramètres ci-dessous.", color=discord.Color.blue()) + embed = disnake.Embed(title="⚙️ Configuration Modération", description="Modifiez les paramètres ci-dessous.", color=disnake.Color.blue()) embed.add_field(name="📁 Logs", value=f"<#{res[0]}>" if res[0] else "Non défini", inline=False) embed.add_field(name="⚖️ Paliers", value=f"Timeout: **{res[1]}**, Kick: **{res[2]}**, Ban: **{res[3]}**", inline=False) embed.add_field(name="⏱️ Durée Timeout Auto", value=f"**{res[4]}s**", inline=False) view = ModConfigView(interaction.guild_id, self.db_path) await interaction.response.send_message(embed=embed, view=view, ephemeral=True) - async def warn(self, interaction: discord.Interaction, member: discord.Member, reason: str): + async def warn(self, interaction: disnake.Interaction, member: disnake.Member, reason: str): conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?)', (interaction.guild_id, member.id, interaction.user.id, 'WARN', reason)) @@ -266,26 +265,26 @@ class Moderation(commands.Cog): config = cursor.fetchone(); conn.commit(); conn.close() if not interaction.response.is_done(): await interaction.response.send_message(f"✅ {member.mention} averti ({warn_count}).", ephemeral=True) else: await interaction.followup.send(f"✅ {member.mention} averti ({warn_count}).", ephemeral=True) - await self.send_mod_log(interaction.guild, discord.Embed(title="⚠️ Avertissement", description=f"Membre: {member.mention}\nRaison: {reason}\nTotal: {warn_count}", color=discord.Color.orange())) + await self.send_mod_log(interaction.guild, disnake.Embed(title="⚠️ Avertissement", description=f"Membre: {member.mention}\nRaison: {reason}\nTotal: {warn_count}", color=disnake.Color.orange())) try: await member.send(f"⚠️ Avertissement sur **{interaction.guild.name}**\nRaison: {reason}") except: pass if config: l_t, l_k, l_b, dur = config if warn_count >= l_b: await member.ban(reason=f"Auto: {warn_count} warns") elif warn_count >= l_k: await member.kick(reason=f"Auto: {warn_count} warns") - elif warn_count >= l_t: await member.timeout(timedelta(seconds=dur), reason=f"Auto: {warn_count} warns") + elif warn_count >= l_t: await member.timeout(duration=timedelta(seconds=dur), reason=f"Auto: {warn_count} warns") - async def timeout(self, interaction: discord.Interaction, member: discord.Member, duration_str: str, reason: str): + async def timeout(self, interaction: disnake.Interaction, member: disnake.Member, duration_str: str, reason: str): secs = self.parse_duration(duration_str) if secs <= 0: return await interaction.response.send_message("❌ Durée invalide.", ephemeral=True) - await member.timeout(timedelta(seconds=secs), reason=reason) + await member.timeout(duration=timedelta(seconds=secs), reason=reason) conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason, duration) VALUES (?, ?, ?, ?, ?, ?)', (interaction.guild_id, member.id, interaction.user.id, 'TIMEOUT', reason, secs)) conn.commit(); conn.close() if not interaction.response.is_done(): await interaction.response.send_message(f"✅ {member.mention} mis en sourdine.", ephemeral=True) else: await interaction.followup.send(f"✅ {member.mention} mis en sourdine.", ephemeral=True) - async def kick(self, interaction: discord.Interaction, member: discord.Member, reason: str): + async def kick(self, interaction: disnake.Interaction, member: disnake.Member, reason: str): await member.kick(reason=reason) conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?)', (interaction.guild_id, member.id, interaction.user.id, 'KICK', reason)) @@ -293,7 +292,7 @@ class Moderation(commands.Cog): if not interaction.response.is_done(): await interaction.response.send_message(f"✅ {member.name} expulsé.", ephemeral=True) else: await interaction.followup.send(f"✅ {member.name} expulsé.", ephemeral=True) - async def ban(self, interaction: discord.Interaction, user: discord.User, reason: str): + async def ban(self, interaction: disnake.Interaction, user: disnake.User, reason: str): await interaction.guild.ban(user, reason=reason) conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?)', (interaction.guild_id, user.id, interaction.user.id, 'BAN', reason)) @@ -301,20 +300,20 @@ class Moderation(commands.Cog): if not interaction.response.is_done(): await interaction.response.send_message(f"✅ {user.name} banni.", ephemeral=True) else: await interaction.followup.send(f"✅ {user.name} banni.", ephemeral=True) - async def history(self, interaction: discord.Interaction, user: discord.User): + async def history(self, interaction: disnake.Interaction, user: disnake.User): conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('SELECT id, type, reason, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? ORDER BY timestamp DESC', (interaction.guild_id, user.id)) results = cursor.fetchall(); conn.close() if not results: if not interaction.response.is_done(): return await interaction.response.send_message("ℹ️ Aucun historique.", ephemeral=True) else: return await interaction.followup.send("ℹ️ Aucun historique.", ephemeral=True) - embed = discord.Embed(title=f"📜 Historique de {user.name}", color=discord.Color.blue()) + embed = disnake.Embed(title=f"📜 Historique de {user.name}", color=disnake.Color.blue()) for s_id, s_type, reason, ts in results[:10]: embed.add_field(name=f"#{s_id} - {s_type}", value=f"**Date:** {ts}\n**Raison:** {reason}", inline=False) if not interaction.response.is_done(): await interaction.response.send_message(embed=embed, ephemeral=True) else: await interaction.followup.send(embed=embed, ephemeral=True) - async def clearwarns(self, interaction: discord.Interaction, member: discord.Member, reason: str): + async def clearwarns(self, interaction: disnake.Interaction, member: disnake.Member, reason: str): conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('UPDATE sanctions SET status = "REVOKED" WHERE guild_id = ? AND user_id = ? AND type = "WARN"', (interaction.guild_id, member.id)) conn.commit(); conn.close() diff --git a/commandes/modules_security/antiraid.py b/commandes/modules_security/antiraid.py index a56deae..6ad47ad 100644 --- a/commandes/modules_security/antiraid.py +++ b/commandes/modules_security/antiraid.py @@ -1,5 +1,5 @@ -import discord -from discord.ext import commands +import disnake +from disnake.ext import commands import json import os from datetime import datetime @@ -53,7 +53,7 @@ class AntiRaid(commands.Cog): if log_channel_id: channel = guild.get_channel(int(log_channel_id)) if channel: - embed = discord.Embed(title="🚨 ALERTE RAID", description=f"Vague de {len(self.join_cache[guild.id])} membres !", color=0xFF0000) + embed = disnake.Embed(title="🚨 ALERTE RAID", description=f"Vague de {len(self.join_cache[guild.id])} membres !", color=0xFF0000) await channel.send(embed=embed) async def setup(bot): diff --git a/commandes/modules_security/antispam.py b/commandes/modules_security/antispam.py index 9ab7f81..fef7ba2 100644 --- a/commandes/modules_security/antispam.py +++ b/commandes/modules_security/antispam.py @@ -1,5 +1,5 @@ -import discord -from discord.ext import commands +import disnake +from disnake.ext import commands import json import os from collections import deque @@ -28,7 +28,7 @@ class AntiSpam(commands.Cog): def _get_cached_settings(self, guild_id): """Récupère les paramètres avec cache (TTL de 5 secondes)""" - now = discord.utils.utcnow().timestamp() + now = disnake.utils.utcnow().timestamp() cached = self._settings_cache.get(guild_id) if cached: @@ -52,7 +52,7 @@ class AntiSpam(commands.Cog): def _get_cached_whitelist(self, guild_id): """Récupère la whitelist avec cache (TTL de 5 secondes)""" - now = discord.utils.utcnow().timestamp() + now = disnake.utils.utcnow().timestamp() cached = self._whitelist_cache.get(guild_id) if cached: @@ -117,7 +117,7 @@ class AntiSpam(commands.Cog): # --- TEST ANTI-SPAM (deque pour O(1)) --- if settings.get("anti_spam") is True: - now = discord.utils.utcnow().timestamp() + now = disnake.utils.utcnow().timestamp() # Initialiser ou récupérer le deque pour cet utilisateur if user_id not in self.message_counts: @@ -156,7 +156,7 @@ class AntiSpam(commands.Cog): self.message_counts[user_id] = deque(maxlen=10) if current_warns <= 3: - embed = discord.Embed( + embed = disnake.Embed( title="🛡️ Sécurité Kuby", description=f"{message.author.mention}, {reason}.\n**Avertissement : {current_warns}/3**", color=0xFFA500 @@ -169,10 +169,10 @@ class AntiSpam(commands.Cog): async def apply_mute(self, message, reason): try: - until = discord.utils.utcnow() + timedelta(minutes=5) + until = disnake.utils.utcnow() + timedelta(minutes=5) await message.author.timeout(until, reason=f"Kuby Security: {reason} (3 Warns)") - embed = discord.Embed( + embed = disnake.Embed( title="⛔ Exclusion Temporaire", description=f"**Membre :** {message.author.mention}\n**Durée :** 5 minutes\n**Raison :** {reason} répétitif.", color=0xFF0000 diff --git a/commandes/modules_security/logs_manager.py b/commandes/modules_security/logs_manager.py index b596b54..dc3238c 100644 --- a/commandes/modules_security/logs_manager.py +++ b/commandes/modules_security/logs_manager.py @@ -1,5 +1,5 @@ -import discord -from discord.ext import commands +import disnake +from disnake.ext import commands import json import os from datetime import datetime @@ -23,11 +23,11 @@ class LogsManager(commands.Cog): log_chan = get_log_channel(message.guild, settings) if log_chan: - embed = discord.Embed( + embed = disnake.Embed( title="🗑️ Message Supprimé", description=f"**Auteur :** {message.author.mention}\n**Salon :** {message.channel.mention}", color=0xFFA500, - timestamp=datetime.utcnow() + timestamp=disnake.utils.utcnow() ) embed.add_field(name="Contenu", value=message.content or "*(Image ou Embed)*") await log_chan.send(embed=embed) @@ -37,11 +37,11 @@ class LogsManager(commands.Cog): settings = dict(json.load(open(SETTINGS_FILE, "r"))).get(str(member.guild.id), {}) log_chan = get_log_channel(member.guild, settings) if log_chan: - embed = discord.Embed( + embed = disnake.Embed( title="📤 Départ d'un membre", description=f"{member.mention} ({member.id}) a quitté le serveur.", color=0xFF0000, - timestamp=datetime.utcnow() + timestamp=disnake.utils.utcnow() ) await log_chan.send(embed=embed) @@ -50,11 +50,11 @@ class LogsManager(commands.Cog): settings = dict(json.load(open(SETTINGS_FILE, "r"))).get(str(guild.id), {}) log_chan = get_log_channel(guild, settings) if log_chan: - embed = discord.Embed( + embed = disnake.Embed( title="🔨 Bannissement", description=f"**Utilisateur :** {user.mention} ({user.id}) a été banni.", color=0x000000, - timestamp=datetime.utcnow() + timestamp=disnake.utils.utcnow() ) await log_chan.send(embed=embed) diff --git a/commandes/modules_security/rules.py b/commandes/modules_security/rules.py index 6290dd9..6f5999a 100644 --- a/commandes/modules_security/rules.py +++ b/commandes/modules_security/rules.py @@ -1,6 +1,5 @@ -import discord -from discord import app_commands -from discord.ext import commands +import disnake +from disnake.ext import commands import json import os from typing import Optional @@ -39,9 +38,9 @@ def save_settings(guild_id: int, settings: dict) -> None: # --- MODALS DE CONFIGURATION DU RÈGLEMENT (BACK-END) --- -class RulesTitleModal(discord.ui.Modal, title="📜 Titre du Règlement"): +class RulesTitleModal(disnake.ui.Modal, title="📜 Titre du Règlement"): """Modal pour configurer le titre du règlement""" - title_input = discord.ui.TextInput( + title_input = disnake.ui.TextInput( label="Titre du règlement", placeholder="Ex: 📜 Règlement de [Nom du Serveur]", min_length=1, @@ -55,7 +54,7 @@ class RulesTitleModal(discord.ui.Modal, title="📜 Titre du Règlement"): self.settings = settings self.on_update_callback = on_update_callback - async def on_submit(self, interaction: discord.Interaction): + async def on_submit(self, interaction: disnake.Interaction): self.settings["rules_title"] = self.title_input.value save_settings(self.guild_id, self.settings) @@ -78,12 +77,12 @@ class RulesTitleModal(discord.ui.Modal, title="📜 Titre du Règlement"): return view -class RulesContentModal(discord.ui.Modal, title="📝 Contenu du Règlement"): +class RulesContentModal(disnake.ui.Modal, title="📝 Contenu du Règlement"): """Modal pour configurer le contenu du règlement""" - content_input = discord.ui.TextInput( + content_input = disnake.ui.TextInput( label="Contenu du règlement", placeholder="Entrez les règles de votre serveur...", - style=discord.TextStyle.paragraph, + style=disnake.TextStyle.paragraph, min_length=10, max_length=4000, required=True @@ -95,7 +94,7 @@ class RulesContentModal(discord.ui.Modal, title="📝 Contenu du Règlement"): self.settings = settings self.on_update_callback = on_update_callback - async def on_submit(self, interaction: discord.Interaction): + async def on_submit(self, interaction: disnake.Interaction): self.settings["rules_content"] = self.content_input.value save_settings(self.guild_id, self.settings) @@ -118,9 +117,9 @@ class RulesContentModal(discord.ui.Modal, title="📝 Contenu du Règlement"): return view -class RulesChannelModal(discord.ui.Modal, title="📌 Salon du Règlement"): +class RulesChannelModal(disnake.ui.Modal, title="📌 Salon du Règlement"): """Modal pour sélectionner le salon du règlement""" - channel_input = discord.ui.TextInput( + channel_input = disnake.ui.TextInput( label="ID du salon", placeholder="Entrez l'ID du salon où envoyer le règlement", min_length=15, @@ -134,7 +133,7 @@ class RulesChannelModal(discord.ui.Modal, title="📌 Salon du Règlement"): self.settings = settings self.on_update_callback = on_update_callback - async def on_submit(self, interaction: discord.Interaction): + async def on_submit(self, interaction: disnake.Interaction): if not self.channel_input.value.isdigit(): return await interaction.response.send_message( "❌ Erreur : ID invalide.", @@ -172,9 +171,9 @@ class RulesChannelModal(discord.ui.Modal, title="📌 Salon du Règlement"): return view -class RulesRoleModal(discord.ui.Modal, title="✅ Rôle d'Acceptation"): +class RulesRoleModal(disnake.ui.Modal, title="✅ Rôle d'Acceptation"): """Modal pour configurer le rôle attribué à l'acceptation""" - role_input = discord.ui.TextInput( + role_input = disnake.ui.TextInput( label="ID du rôle", placeholder="Entrez l'ID du rôle à attribuer", min_length=15, @@ -188,7 +187,7 @@ class RulesRoleModal(discord.ui.Modal, title="✅ Rôle d'Acceptation"): self.settings = settings self.on_update_callback = on_update_callback - async def on_submit(self, interaction: discord.Interaction): + async def on_submit(self, interaction: disnake.Interaction): if not self.role_input.value.isdigit(): return await interaction.response.send_message( "❌ Erreur : ID invalide.", @@ -226,9 +225,9 @@ class RulesRoleModal(discord.ui.Modal, title="✅ Rôle d'Acceptation"): return view -class RulesMessageTypeModal(discord.ui.Modal, title="📝 Type de Message"): +class RulesMessageTypeModal(disnake.ui.Modal, title="📝 Type de Message"): """Modal pour choisir le type de message du règlement""" - message_type = discord.ui.TextInput( + message_type = disnake.ui.TextInput( label="Type de message", placeholder="Tapez 'embed' pour embed ou 'simple' pour message simple", min_length=5, @@ -242,7 +241,7 @@ class RulesMessageTypeModal(discord.ui.Modal, title="📝 Type de Message"): self.settings = settings self.on_update_callback = on_update_callback - async def on_submit(self, interaction: discord.Interaction): + async def on_submit(self, interaction: disnake.Interaction): msg_type = self.message_type.value.lower().strip() if msg_type not in ["embed", "simple"]: @@ -274,11 +273,11 @@ class RulesMessageTypeModal(discord.ui.Modal, title="📝 Type de Message"): return view -class ToggleRulesButton(discord.ui.Button): +class ToggleRulesButton(disnake.ui.Button): """Bouton de bascule pour activer/désactiver le règlement""" def __init__(self, is_enabled: bool): - style = discord.ButtonStyle.green if is_enabled else discord.ButtonStyle.red + style = disnake.ButtonStyle.green if is_enabled else disnake.ButtonStyle.red status = "✅" if is_enabled else "❌" super().__init__( label=f"Règlement [{status}]", @@ -287,7 +286,7 @@ class ToggleRulesButton(discord.ui.Button): emoji="📜" ) - async def callback(self, interaction: discord.Interaction): + async def callback(self, interaction: disnake.Interaction): view = self.view settings = view.settings guild = interaction.guild @@ -316,7 +315,7 @@ class ToggleRulesButton(discord.ui.Button): await interaction.response.edit_message(embed=embed, view=new_view) -class RulesAcceptButton(discord.ui.View): +class RulesAcceptButton(disnake.ui.View): """Bouton persistant pour accepter le règlement""" def __init__(self, guild_id: int, accept_role_id: int): @@ -327,10 +326,10 @@ class RulesAcceptButton(discord.ui.View): @discord.ui.button( label="J'accepte le règlement", emoji="✅", - style=discord.ButtonStyle.green, + style=disnake.ButtonStyle.green, custom_id="rules_accept_button" ) - async def accept_rules(self, interaction: discord.Interaction, button: discord.ui.Button): + async def accept_rules(self, interaction: disnake.Interaction, button: disnake.ui.Button): """Gère le clic sur le bouton d'acceptation""" guild = interaction.guild @@ -368,7 +367,7 @@ class RulesAcceptButton(discord.ui.View): f"Le rôle **{role.name}** vous a été attribué.", ephemeral=True ) - except discord.Forbidden: + except disnake.Forbidden: await interaction.response.send_message( "❌ Je n'ai pas les permissions pour attribuer ce rôle.", ephemeral=True @@ -421,7 +420,7 @@ class RulesCog(commands.Cog): """Appelé quand le bot est prêt""" print("🎫 Module Règles chargé avec succès!") - async def send_rules_message(self, channel: discord.TextChannel, settings: dict) -> Optional[discord.Message]: + async def send_rules_message(self, channel: disnake.TextChannel, settings: dict) -> Optional[disnake.Message]: """Envoie le message du règlement dans un salon""" title = settings.get("rules_title", "📜 Règlement du Serveur") content = settings.get("rules_content", "") @@ -457,7 +456,7 @@ class RulesCog(commands.Cog): else: # embed # Créer l'embed - embed = discord.Embed( + embed = disnake.Embed( title=title, description=content, color=0x2b2d31 diff --git a/commandes/no_link.py b/commandes/no_link.py index c708ec2..e3ad27b 100755 --- a/commandes/no_link.py +++ b/commandes/no_link.py @@ -1,7 +1,7 @@ # utils/no_link.py import re -import discord -from discord.ext import commands +import disnake +from disnake.ext import commands import json import os @@ -26,7 +26,7 @@ class NoLink(commands.Cog): self.bot = bot @commands.Cog.listener() - async def on_message(self, message: discord.Message) -> None: + async def on_message(self, message: disnake.Message) -> None: if message.author.bot or not message.guild: return @@ -41,7 +41,7 @@ class NoLink(commands.Cog): f"{message.author.mention} ⚠️ Les liens sont interdits sur ce serveur.", delete_after=5 ) - except discord.Forbidden: + except disnake.Forbidden: print(f"Impossible de supprimer le message de {message.author} (permissions manquantes).") except Exception as exc: print(f"Erreur lors de la suppression du message : {exc}") diff --git a/commandes/scan.py b/commandes/scan.py index bc74e57..db9c1d4 100755 --- a/commandes/scan.py +++ b/commandes/scan.py @@ -1,7 +1,6 @@ # scan.py -import discord -from discord import app_commands -from discord.ext import commands +import disnake +from disnake.ext import commands import hashlib import os import json @@ -59,8 +58,8 @@ class Scan(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - @app_commands.command(name="scan", description="Scanner un fichier pour détecter un malware.") - async def scan(self, interaction: discord.Interaction, file: discord.Attachment): + @commands.slash_command(name="scan", description="Scanner un fichier pour détecter un malware.") + async def scan(self, interaction: disnake.ApplicationCommandInteraction, file: disnake.Attachment): await interaction.response.defer() # Permet de prendre un peu de temps pour le scan # Téléchargement temporaire du fichier diff --git a/commandes/security.py b/commandes/security.py index 5a461a0..a839a93 100755 --- a/commandes/security.py +++ b/commandes/security.py @@ -1,6 +1,5 @@ -import discord -from discord import app_commands -from discord.ext import commands +import disnake +from disnake.ext import commands import json import os from typing import Optional @@ -70,9 +69,9 @@ def is_immune(guild, user) -> bool: # --- MODALS DE CONFIGURATION --- -class SpamThresholdModal(discord.ui.Modal, title="🛡️ Seuil Anti-Spam"): +class SpamThresholdModal(disnake.ui.Modal, title="🛡️ Seuil Anti-Spam"): """Modal pour configurer le seuil anti-spam""" - threshold_input = discord.ui.TextInput( + threshold_input = disnake.ui.TextInput( label="Messages max autorisés", placeholder="Entre 3 et 10 (défaut: 5)", min_length=1, @@ -86,7 +85,7 @@ class SpamThresholdModal(discord.ui.Modal, title="🛡️ Seuil Anti-Spam"): self.settings = settings self.parent_view = view - async def on_submit(self, interaction: discord.Interaction): + async def on_submit(self, interaction: disnake.Interaction): if not self.threshold_input.value.isdigit(): return await interaction.response.send_message( "❌ Erreur : Veuillez entrer un nombre entier.", @@ -111,9 +110,9 @@ class SpamThresholdModal(discord.ui.Modal, title="🛡️ Seuil Anti-Spam"): ephemeral=True ) -class RaidThresholdModal(discord.ui.Modal, title="⚔️ Seuil Anti-Raid"): +class RaidThresholdModal(disnake.ui.Modal, title="⚔️ Seuil Anti-Raid"): """Modal pour configurer le seuil anti-raid""" - threshold_input = discord.ui.TextInput( + threshold_input = disnake.ui.TextInput( label="Joins max en 10 secondes", placeholder="Entre 2 et 10 (défaut: 3)", min_length=1, @@ -127,7 +126,7 @@ class RaidThresholdModal(discord.ui.Modal, title="⚔️ Seuil Anti-Raid"): self.settings = settings self.parent_view = view - async def on_submit(self, interaction: discord.Interaction): + async def on_submit(self, interaction: disnake.Interaction): if not self.threshold_input.value.isdigit(): return await interaction.response.send_message( "❌ Erreur : Veuillez entrer un nombre entier.", @@ -154,9 +153,9 @@ class RaidThresholdModal(discord.ui.Modal, title="⚔️ Seuil Anti-Raid"): # --- MODALS DE CONFIGURATION WHITELIST --- -class WhitelistModal(discord.ui.Modal, title="👤 Ajouter à la Whitelist"): +class WhitelistModal(disnake.ui.Modal, title="👤 Ajouter à la Whitelist"): """Modal pour ajouter un utilisateur à la whitelist""" - user_input = discord.ui.TextInput( + user_input = disnake.ui.TextInput( label="ID de l'utilisateur", placeholder="Entrez l'ID Discord de l'utilisateur", min_length=15, @@ -169,7 +168,7 @@ class WhitelistModal(discord.ui.Modal, title="👤 Ajouter à la Whitelist"): self.guild_id = guild_id self.parent_view = view - async def on_submit(self, interaction: discord.Interaction): + async def on_submit(self, interaction: disnake.Interaction): if not self.user_input.value.isdigit(): return await interaction.response.send_message( "❌ Erreur : ID invalide.", @@ -213,10 +212,10 @@ class WhitelistModal(discord.ui.Modal, title="👤 Ajouter à la Whitelist"): # --- VUES DE L'INTERFACE --- -class SecurityView(discord.ui.View): +class SecurityView(disnake.ui.View): """Vue principale du panneau de sécurité""" - def __init__(self, interaction: discord.Interaction, settings: dict, category: Optional[str] = None, bot=None): + def __init__(self, interaction: disnake.Interaction, settings: dict, category: Optional[str] = None, bot=None): super().__init__(timeout=300) # 5 minutes timeout self.interaction = interaction self.guild = interaction.guild @@ -270,18 +269,18 @@ class SecurityView(discord.ui.View): # Bouton de configuration du seuil threshold = self.settings.get("spam_threshold", 5) - btn_threshold = discord.ui.Button( + btn_threshold = disnake.ui.Button( label=f"🔢 Seuil: {threshold} msg/5s", - style=discord.ButtonStyle.blurple, + style=disnake.ButtonStyle.blurple, custom_id="spam_threshold" ) btn_threshold.callback = self.open_spam_threshold_modal self.add_item(btn_threshold) # Bouton retour - btn_back = discord.ui.Button( + btn_back = disnake.ui.Button( label="Retour", - style=discord.ButtonStyle.gray, + style=disnake.ButtonStyle.gray, emoji="⬅️" ) btn_back.callback = self.back_to_main @@ -311,18 +310,18 @@ class SecurityView(discord.ui.View): # Bouton de configuration du seuil threshold = self.settings.get("raid_threshold", 3) - btn_threshold = discord.ui.Button( + btn_threshold = disnake.ui.Button( label=f"🔢 Seuil: {threshold} join/10s", - style=discord.ButtonStyle.blurple, + style=disnake.ButtonStyle.blurple, custom_id="raid_threshold" ) btn_threshold.callback = self.open_raid_threshold_modal self.add_item(btn_threshold) # Bouton retour - btn_back = discord.ui.Button( + btn_back = disnake.ui.Button( label="Retour", - style=discord.ButtonStyle.gray, + style=disnake.ButtonStyle.gray, emoji="⬅️" ) btn_back.callback = self.back_to_main @@ -351,9 +350,9 @@ class SecurityView(discord.ui.View): self.add_item(btn_role) # Bouton retour - btn_back = discord.ui.Button( + btn_back = disnake.ui.Button( label="Retour", - style=discord.ButtonStyle.gray, + style=disnake.ButtonStyle.gray, emoji="⬅️" ) btn_back.callback = self.back_to_main @@ -376,23 +375,23 @@ class SecurityView(discord.ui.View): if log_channel_id: channel = self.guild.get_channel(int(log_channel_id)) channel_mention = channel.mention if channel else f"`{log_channel_id}`" - btn_channel = discord.ui.Button( + btn_channel = disnake.ui.Button( label=f"📌 Channel: {channel.name if channel else 'Invalide'}", - style=discord.ButtonStyle.green, + style=disnake.ButtonStyle.green, disabled=True ) else: - btn_channel = discord.ui.Button( + btn_channel = disnake.ui.Button( label="📌 Aucun channel configuré", - style=discord.ButtonStyle.red, + style=disnake.ButtonStyle.red, disabled=True ) self.add_item(btn_channel) # Bouton retour - btn_back = discord.ui.Button( + btn_back = disnake.ui.Button( label="Retour", - style=discord.ButtonStyle.gray, + style=disnake.ButtonStyle.gray, emoji="⬅️" ) btn_back.callback = self.back_to_main @@ -401,9 +400,9 @@ class SecurityView(discord.ui.View): def build_whitelist_view(self): """Construit la vue Whitelist""" # Affichage du nombre avec style - btn_count = discord.ui.Button( + btn_count = disnake.ui.Button( label=f"👥 Whitelist: {self.whitelist_count} utilisateur(s)", - style=discord.ButtonStyle.blurple, + style=disnake.ButtonStyle.blurple, disabled=True ) self.add_item(btn_count) @@ -413,17 +412,17 @@ class SecurityView(discord.ui.View): self.add_item(WhitelistSelect(self.guild, self)) else: # Message si whitelist vide - btn_empty = discord.ui.Button( + btn_empty = disnake.ui.Button( label="📭 Aucun utilisateur dans la whitelist", - style=discord.ButtonStyle.gray, + style=disnake.ButtonStyle.gray, disabled=True ) self.add_item(btn_empty) # Bouton ajouter - btn_add = discord.ui.Button( + btn_add = disnake.ui.Button( label="➕ Ajouter", - style=discord.ButtonStyle.green, + style=disnake.ButtonStyle.green, emoji="➕", custom_id="whitelist_add" ) @@ -431,9 +430,9 @@ class SecurityView(discord.ui.View): self.add_item(btn_add) # Bouton retour - btn_back = discord.ui.Button( + btn_back = disnake.ui.Button( label="Retour", - style=discord.ButtonStyle.gray, + style=disnake.ButtonStyle.gray, emoji="⬅️" ) btn_back.callback = self.back_to_main @@ -458,18 +457,18 @@ class SecurityView(discord.ui.View): # Bouton pour modifier le titre - btn_title = discord.ui.Button( + btn_title = disnake.ui.Button( label="Éditer le titre", - style=discord.ButtonStyle.blurple, + style=disnake.ButtonStyle.blurple, emoji="📝" ) btn_title.callback = self.open_rules_title_modal self.add_item(btn_title) # Bouton pour modifier le contenu - btn_content = discord.ui.Button( + btn_content = disnake.ui.Button( label="Éditer le contenu", - style=discord.ButtonStyle.blurple, + style=disnake.ButtonStyle.blurple, emoji="📄" ) btn_content.callback = self.open_rules_content_modal @@ -479,15 +478,15 @@ class SecurityView(discord.ui.View): rules_channel_id = self.settings.get("rules_channel_id") if rules_channel_id: channel = self.guild.get_channel(int(rules_channel_id)) - btn_channel = discord.ui.Button( + btn_channel = disnake.ui.Button( label=f"Salon: {channel.name if channel else 'Invalide'}", - style=discord.ButtonStyle.green, + style=disnake.ButtonStyle.green, custom_id="rules_channel" ) else: - btn_channel = discord.ui.Button( + btn_channel = disnake.ui.Button( label="Aucun salon configuré", - style=discord.ButtonStyle.red, + style=disnake.ButtonStyle.red, custom_id="rules_channel" ) btn_channel.callback = self.open_rules_channel_modal @@ -497,15 +496,15 @@ class SecurityView(discord.ui.View): rules_role_id = self.settings.get("rules_accept_role_id") if rules_role_id: role = self.guild.get_role(int(rules_role_id)) - btn_role = discord.ui.Button( + btn_role = disnake.ui.Button( label=f"Rôle: {role.name if role else 'Invalide'}", - style=discord.ButtonStyle.green, + style=disnake.ButtonStyle.green, custom_id="rules_role" ) else: - btn_role = discord.ui.Button( + btn_role = disnake.ui.Button( label="Aucun rôle configuré", - style=discord.ButtonStyle.red, + style=disnake.ButtonStyle.red, custom_id="rules_role" ) btn_role.callback = self.open_rules_role_modal @@ -514,9 +513,9 @@ class SecurityView(discord.ui.View): # Bouton pour choisir le type de message message_type = self.settings.get("rules_message_type", "embed") type_label = "Embed" if message_type == "embed" else "Message simple" - btn_message_type = discord.ui.Button( + btn_message_type = disnake.ui.Button( label=f"Type: {type_label}", - style=discord.ButtonStyle.blurple, + style=disnake.ButtonStyle.blurple, emoji="📝", custom_id="rules_message_type" ) @@ -524,84 +523,84 @@ class SecurityView(discord.ui.View): self.add_item(btn_message_type) # Bouton pour envoyer le règlement - btn_send = discord.ui.Button( + btn_send = disnake.ui.Button( label="Envoyer le règlement", - style=discord.ButtonStyle.green, + style=disnake.ButtonStyle.green, emoji="🚀" ) btn_send.callback = self.send_rules self.add_item(btn_send) # Bouton retour - btn_back = discord.ui.Button( + btn_back = disnake.ui.Button( label="Retour", - style=discord.ButtonStyle.gray, + style=disnake.ButtonStyle.gray, emoji="⬅️" ) btn_back.callback = self.back_to_main self.add_item(btn_back) - async def toggle_setting(self, interaction: discord.Interaction, key: str): + async def toggle_setting(self, interaction: disnake.Interaction, key: str): """Bascule un paramètre""" self.settings[key] = not self.settings.get(key, False) save_settings(self.guild.id, self.settings) self.build_view() await interaction.response.edit_message(view=self) - async def open_spam_threshold_modal(self, interaction: discord.Interaction): + async def open_spam_threshold_modal(self, interaction: disnake.Interaction): """Ouvre le modal de configuration du seuil spam""" await interaction.response.send_modal( SpamThresholdModal(self.guild.id, self.settings, self) ) - async def open_raid_threshold_modal(self, interaction: discord.Interaction): + async def open_raid_threshold_modal(self, interaction: disnake.Interaction): """Ouvre le modal de configuration du seuil raid""" await interaction.response.send_modal( RaidThresholdModal(self.guild.id, self.settings, self) ) - async def open_whitelist_modal(self, interaction: discord.Interaction): + async def open_whitelist_modal(self, interaction: disnake.Interaction): """Ouvre le modal d'ajout à la whitelist""" await interaction.response.send_modal( WhitelistModal(self.guild.id, self) ) - async def open_rules_title_modal(self, interaction: discord.Interaction): + async def open_rules_title_modal(self, interaction: disnake.Interaction): """Ouvre le modal de configuration du titre du règlement""" from .modules_security.rules import RulesTitleModal await interaction.response.send_modal( RulesTitleModal(self.guild.id, self.settings) ) - async def open_rules_content_modal(self, interaction: discord.Interaction): + async def open_rules_content_modal(self, interaction: disnake.Interaction): """Ouvre le modal de configuration du contenu du règlement""" from .modules_security.rules import RulesContentModal await interaction.response.send_modal( RulesContentModal(self.guild.id, self.settings) ) - async def open_rules_channel_modal(self, interaction: discord.Interaction): + async def open_rules_channel_modal(self, interaction: disnake.Interaction): """Ouvre le modal de sélection du salon du règlement""" from .modules_security.rules import RulesChannelModal await interaction.response.send_modal( RulesChannelModal(self.guild.id, self.settings) ) - async def open_rules_role_modal(self, interaction: discord.Interaction): + async def open_rules_role_modal(self, interaction: disnake.Interaction): """Ouvre le modal de configuration du rôle d'acceptation""" from .modules_security.rules import RulesRoleModal await interaction.response.send_modal( RulesRoleModal(self.guild.id, self.settings) ) - async def open_rules_message_type_modal(self, interaction: discord.Interaction): + async def open_rules_message_type_modal(self, interaction: disnake.Interaction): """Ouvre le modal de sélection du type de message""" from .modules_security.rules import RulesMessageTypeModal await interaction.response.send_modal( RulesMessageTypeModal(self.guild.id, self.settings) ) - async def send_rules(self, interaction: discord.Interaction): + async def send_rules(self, interaction: disnake.Interaction): """Envoie le règlement dans le salon configuré""" from commandes.modules_security.rules import RulesCog @@ -670,16 +669,16 @@ class SecurityView(discord.ui.View): ephemeral=True ) - async def back_to_main(self, interaction: discord.Interaction): + async def back_to_main(self, interaction: disnake.Interaction): """Retour au menu principal""" self.category = None self.build_view() embed = self.create_main_embed(interaction.user) await interaction.response.edit_message(embed=embed, view=self) - def create_main_embed(self, user: discord.Member = None) -> discord.Embed: + def create_main_embed(self, user: disnake.Member = None) -> disnake.Embed: """Crée l'embed du menu principal""" - embed = discord.Embed( + embed = disnake.Embed( title="🛡️ Centre de Sécurité Kuby", description="Gérez la protection de votre serveur", color=0x7289DA @@ -726,9 +725,9 @@ class SecurityView(discord.ui.View): return embed - def create_category_embed(self, title: str, description: str) -> discord.Embed: + def create_category_embed(self, title: str, description: str) -> disnake.Embed: """Crée un embed pour une catégorie""" - embed = discord.Embed( + embed = disnake.Embed( title=title, description=description, color=0x7289DA @@ -740,11 +739,11 @@ class SecurityView(discord.ui.View): return embed -class ToggleButton(discord.ui.Button): +class ToggleButton(disnake.ui.Button): """Bouton de bascule avec indicateur de statut""" def __init__(self, label: str, emoji: str, is_enabled: bool, custom_id: str): - style = discord.ButtonStyle.green if is_enabled else discord.ButtonStyle.red + style = disnake.ButtonStyle.green if is_enabled else disnake.ButtonStyle.red status = "✅" if is_enabled else "❌" super().__init__( label=f"{label} [{status}]", @@ -753,7 +752,7 @@ class ToggleButton(discord.ui.Button): emoji=emoji ) - async def callback(self, interaction: discord.Interaction): + async def callback(self, interaction: disnake.Interaction): view: SecurityView = self.view key = interaction.data['custom_id'] @@ -771,42 +770,42 @@ class ToggleButton(discord.ui.Button): await interaction.response.edit_message(view=view) -class MainCategorySelect(discord.ui.Select): +class MainCategorySelect(disnake.ui.Select): """Menu de sélection des catégories""" def __init__(self): options = [ - discord.SelectOption( + disnake.SelectOption( label="Anti-Spam", emoji="🛡️", value="spam", description="Protection contre le spam et les liens" ), - discord.SelectOption( + disnake.SelectOption( label="Anti-Raid", emoji="⚔️", value="raid", description="Protection contre les raids" ), - discord.SelectOption( + disnake.SelectOption( label="Anti-Nuke", emoji="☢️", value="nuke", description="Protection contre la destruction du serveur" ), - discord.SelectOption( + disnake.SelectOption( label="Logs", emoji="📋", value="logs", description="Journalisation des événements" ), - discord.SelectOption( + disnake.SelectOption( label="Règlement", emoji="📜", value="rules", description="Créer et envoyer un règlement" ), - discord.SelectOption( + disnake.SelectOption( label="Whitelist", emoji="👥", value="whitelist", @@ -820,7 +819,7 @@ class MainCategorySelect(discord.ui.Select): max_values=1 ) - async def callback(self, interaction: discord.Interaction): + async def callback(self, interaction: disnake.Interaction): view: SecurityView = self.view view.category = self.values[0] view.build_view() @@ -871,7 +870,7 @@ class MainCategorySelect(discord.ui.Select): await interaction.response.edit_message(embed=embed, view=view) -class WhitelistSelect(discord.ui.Select): +class WhitelistSelect(disnake.ui.Select): """Menu dropdown pour voir et gérer les utilisateurs whitelistés""" def __init__(self, guild: discord.Guild, view: 'SecurityView'): @@ -890,7 +889,7 @@ class WhitelistSelect(discord.ui.Select): label = f"ID: {user_id} (non trouvé)" emoji = "❓" options.append( - discord.SelectOption( + disnake.SelectOption( label=label[:100], # Discord limite à 100 chars value=str(user_id), emoji=emoji, @@ -905,7 +904,7 @@ class WhitelistSelect(discord.ui.Select): max_values=1 ) - async def callback(self, interaction: discord.Interaction): + async def callback(self, interaction: disnake.Interaction): """Gère la sélection d'un utilisateur à retirer""" user_id = int(self.values[0]) @@ -955,7 +954,7 @@ class Security(commands.Cog): name="security", description="🛡️ Ouvrir le panneau de sécurité Kuby" ) - async def security(self, interaction: discord.Interaction): + async def security(self, interaction: disnake.Interaction): """ Commande principale pour ouvrir l'interface de sécurité. Accessible uniquement aux administrateurs et propriétaires whitelistés. @@ -990,7 +989,7 @@ class Security(commands.Cog): ) @security.error - async def security_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError): + async def security_error(self, interaction: disnake.Interaction, error: app_commands.AppCommandError): """Gestion des erreurs de la commande security""" # Utilisation de followup si l'interaction est déjà différée ou répondue send_method = interaction.followup.send if interaction.response.is_done() else interaction.response.send_message diff --git a/commandes/sendbotmessages.py b/commandes/sendbotmessages.py index 10b52e7..058c2e4 100755 --- a/commandes/sendbotmessages.py +++ b/commandes/sendbotmessages.py @@ -1,17 +1,14 @@ -import discord -from discord import app_commands -from discord.ext import commands +import disnake +from disnake.ext import commands class SentBotMessages(commands.Cog): def __init__(self, bot): self.bot = bot - @app_commands.command(name="sentbotmessages", description="Le bot envoie un message dans un salon choisi.") - @app_commands.describe( - message="Le message à envoyer", - salon="Le salon où envoyer le message" - ) - async def sentbotmessages(self, interaction: discord.Interaction, message: str, salon: discord.TextChannel): + @commands.slash_command(name="sentbotmessages", description="Le bot envoie un message dans un salon choisi.") + async def sentbotmessages(self, interaction: disnake.ApplicationCommandInteraction, + message: str = commands.Param(description="Le message à envoyer"), + salon: disnake.TextChannel = commands.Param(description="Le salon où envoyer le message")): # Vérifie si l'utilisateur est admin if not interaction.user.guild_permissions.administrator: await interaction.response.send_message("⛔ Tu n'as pas la permission d'utiliser cette commande.", ephemeral=True) diff --git a/commandes/snipe.py b/commandes/snipe.py index 00182c5..f47cacc 100755 --- a/commandes/snipe.py +++ b/commandes/snipe.py @@ -1,6 +1,5 @@ -import discord -from discord import app_commands -from discord.ext import commands +import disnake +from disnake.ext import commands from datetime import datetime from collections import deque @@ -34,9 +33,9 @@ class SnipeCommands(commands.Cog): self.deleted_messages[channel_id].appendleft(message_data) - @app_commands.command(name="snipe", description="Voir les derniers messages supprimés d'un salon") - @app_commands.describe(nombre="Nombre de messages à afficher (1-10, par défaut: 1)") - async def snipe(self, interaction: discord.Interaction, nombre: int = 1): + @commands.slash_command(name="snipe", description="Voir les derniers messages supprimés d'un salon") + async def snipe(self, interaction: disnake.ApplicationCommandInteraction, + nombre: int = commands.Param(1, description="Nombre de messages à afficher (1-10, par défaut: 1)")): """Retrieve deleted messages from the current channel""" channel_id = interaction.channel.id @@ -60,9 +59,9 @@ class SnipeCommands(commands.Cog): messages = list(self.deleted_messages[channel_id])[:nombre] # Create embed for displaying deleted messages - embed = discord.Embed( + embed = disnake.Embed( title=f"🗑️ Messages supprimés ({len(messages)} sur {len(self.deleted_messages[channel_id])} disponibles)", - color=discord.Color.red() + color=disnake.Color.red() ) for i, msg_data in enumerate(messages, 1): @@ -93,8 +92,8 @@ class SnipeCommands(commands.Cog): await interaction.response.send_message(embed=embed) - @app_commands.command(name="snipeclear", description="Effacer l'historique des messages supprimés pour ce salon") - async def snipeclear(self, interaction: discord.Interaction): + @commands.slash_command(name="snipeclear", description="Effacer l'historique des messages supprimés pour ce salon") + async def snipeclear(self, interaction: disnake.ApplicationCommandInteraction): """Clear the deleted messages history for the current channel""" channel_id = interaction.channel.id diff --git a/commandes/staff_leaderboard.py b/commandes/staff_leaderboard.py index 9ca4653..9d1007d 100644 --- a/commandes/staff_leaderboard.py +++ b/commandes/staff_leaderboard.py @@ -1,6 +1,5 @@ -import discord -from discord.ext import commands -from discord import app_commands +import disnake +from disnake.ext import commands from datetime import datetime from commandes.ticket.data.storage import get_storage @@ -11,8 +10,8 @@ class StaffLeaderboard(commands.Cog): def __init__(self, bot): self.bot = bot - @app_commands.command(name="staff-leaderboard", description="Affiche le classement du staff") - async def staff_leaderboard(self, interaction: discord.Interaction): + @commands.slash_command(name="staff-leaderboard", description="Affiche le classement du staff") + async def staff_leaderboard(self, interaction: disnake.ApplicationCommandInteraction): """Commande pour afficher le classement du staff""" await interaction.response.defer(ephemeral=False) @@ -31,10 +30,10 @@ class StaffLeaderboard(commands.Cog): staff_list.sort(key=lambda x: x['average_rating'], reverse=True) # Create embed - embed = discord.Embed( + embed = disnake.Embed( title="🏆 Classement du Staff", description="Classement des meilleurs membres du staff", - color=discord.Color.gold(), + color=disnake.Color.gold(), timestamp=datetime.now() ) diff --git a/commandes/staff_ratings.py b/commandes/staff_ratings.py index 3e7a6a9..a777da0 100644 --- a/commandes/staff_ratings.py +++ b/commandes/staff_ratings.py @@ -1,6 +1,5 @@ -import discord -from discord.ext import commands -from discord import app_commands +import disnake +from disnake.ext import commands import matplotlib.pyplot as plt import plotly.graph_objects as go import plotly.express as px @@ -19,9 +18,9 @@ class StaffRatings(commands.Cog): def __init__(self, bot): self.bot = bot - @app_commands.command(name="staff-ratings", description="Affiche les évaluations du staff") - @app_commands.describe(team="Filtrer par équipe (optionnel)") - async def staff_ratings(self, interaction: discord.Interaction, team: str = None): + @commands.slash_command(name="staff-ratings", description="Affiche les évaluations du staff") + async def staff_ratings(self, interaction: disnake.ApplicationCommandInteraction, + team: str = commands.Param(None, description="Filtrer par équipe (optionnel)")): """Commande pour afficher les évaluations du staff""" await interaction.response.defer(ephemeral=False) @@ -46,10 +45,10 @@ class StaffRatings(commands.Cog): chart_buffer = self.create_staff_rating_chart_matplotlib(staff_list) # Create embed - embed = discord.Embed( + embed = disnake.Embed( title="📊 Évaluations du Staff", description="Performance globale du staff", - color=discord.Color.blue(), + color=disnake.Color.blue(), timestamp=datetime.now() ) @@ -66,7 +65,7 @@ class StaffRatings(commands.Cog): ) # Send chart - file = discord.File(chart_buffer, filename="staff_ratings.png") + file = disnake.File(chart_buffer, filename="staff_ratings.png") await interaction.followup.send(embed=embed, file=file) except Exception as e: diff --git a/commandes/ticket/__init__.py b/commandes/ticket/__init__.py index 209b100..1b2441b 100644 --- a/commandes/ticket/__init__.py +++ b/commandes/ticket/__init__.py @@ -20,8 +20,8 @@ from collections import defaultdict # Add parent directory to path for imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) -import discord -from discord.ext import commands, tasks +import disnake +from disnake.ext import commands, tasks import traceback from PIL import Image, ImageDraw, ImageFont @@ -51,7 +51,7 @@ class FeedbackReminderTask: pass -class TicketPanelView(discord.ui.View): +class TicketPanelView(disnake.ui.View): """Panel principal avec toutes les options intégrées""" def __init__(self, bot, config: GuildTicketConfig, storage=None): @@ -59,7 +59,7 @@ class TicketPanelView(discord.ui.View): self.bot = bot self.config = config self.storage = storage or get_storage() - self.PURPLE = discord.Color.blurple() + self.PURPLE = disnake.Color.blurple() # Boutons du panel self._build_panel() @@ -69,8 +69,8 @@ class TicketPanelView(discord.ui.View): self.clear_items() # Créer ticket - create_btn = discord.ui.Button( - style=discord.ButtonStyle.blurple, + create_btn = disnake.ui.Button( + style=disnake.ButtonStyle.blurple, label="🎫 Créer un ticket", custom_id="ticket_create" ) @@ -78,8 +78,8 @@ class TicketPanelView(discord.ui.View): self.add_item(create_btn) # Mes tickets (si l'utilisateur a des tickets) - my_tickets_btn = discord.ui.Button( - style=discord.ButtonStyle.secondary, + my_tickets_btn = disnake.ui.Button( + style=disnake.ButtonStyle.secondary, label="📂 Mes tickets", custom_id="ticket_my_tickets" ) @@ -87,8 +87,8 @@ class TicketPanelView(discord.ui.View): self.add_item(my_tickets_btn) # Analytics (staff only) - analytics_btn = discord.ui.Button( - style=discord.ButtonStyle.primary, + analytics_btn = disnake.ui.Button( + style=disnake.ButtonStyle.primary, label="📊 Analytics", custom_id="ticket_analytics" ) @@ -96,8 +96,8 @@ class TicketPanelView(discord.ui.View): self.add_item(analytics_btn) # Staff Ratings (staff only) - staff_ratings_btn = discord.ui.Button( - style=discord.ButtonStyle.success, + staff_ratings_btn = disnake.ui.Button( + style=disnake.ButtonStyle.success, label="⭐ Notation du Staff", custom_id="ticket_staff_ratings" ) @@ -105,8 +105,8 @@ class TicketPanelView(discord.ui.View): self.add_item(staff_ratings_btn) # Configuration (admin only) - config_btn = discord.ui.Button( - style=discord.ButtonStyle.secondary, + config_btn = disnake.ui.Button( + style=disnake.ButtonStyle.secondary, label="⚙️ Configuration", custom_id="ticket_config" ) @@ -115,7 +115,7 @@ class TicketPanelView(discord.ui.View): - async def _create_callback(self, interaction: discord.Interaction): + async def _create_callback(self, interaction: disnake.Interaction): """Affiche les catégories pour créer un ticket""" # Reload config to ensure we have latest permissions/categories self.config = self.storage.load_config(interaction.guild_id) @@ -135,10 +135,10 @@ class TicketPanelView(discord.ui.View): # Créer view avec les catégories view = CategorySelectionView(self.bot, self.config) - embed = discord.Embed( + embed = disnake.Embed( title="Créer un Ticket", description="Sélectionnez une catégorie ci-dessous:", - color=discord.Color.blurple() + color=disnake.Color.blurple() ) for cat in self.config.categories[:5]: @@ -150,7 +150,7 @@ class TicketPanelView(discord.ui.View): await interaction.response.send_message(embed=embed, view=view, ephemeral=True) - async def _my_tickets_callback(self, interaction: discord.Interaction): + async def _my_tickets_callback(self, interaction: disnake.Interaction): """Affiche les tickets de l'utilisateur""" # Reload config self.config = self.storage.load_config(interaction.guild_id) @@ -166,10 +166,10 @@ class TicketPanelView(discord.ui.View): await interaction.response.send_message("Vous n'avez aucun ticket ouvert.", ephemeral=True) return - embed = discord.Embed( + embed = disnake.Embed( title="Vos Tickets Ouverts", description=f"Vous avez **{len(tickets)}** ticket(s) ouvert(s):", - color=discord.Color.blue() + color=disnake.Color.blue() ) for ticket in tickets[:5]: @@ -183,7 +183,7 @@ class TicketPanelView(discord.ui.View): await interaction.response.send_message(embed=embed, ephemeral=True) - async def _analytics_callback(self, interaction: discord.Interaction): + async def _analytics_callback(self, interaction: disnake.Interaction): """Affiche les analytics (staff uniquement)""" # Reload config self.config = self.storage.load_config(interaction.guild_id) @@ -200,10 +200,10 @@ class TicketPanelView(discord.ui.View): staff_list = await analytics.get_staff_list(interaction.guild_id) # Create main embed with overall stats - embed = discord.Embed( + embed = disnake.Embed( title="📊 Analytics des Tickets", description="Statistiques détaillées des tickets et performances du staff", - color=discord.Color.blue(), + color=disnake.Color.blue(), timestamp=datetime.now() ) @@ -246,16 +246,16 @@ class TicketPanelView(discord.ui.View): rating_chart = analytics.create_staff_rating_chart(staff_list) await interaction.followup.send( "📊 **Évaluations des Membres du Staff**", - file=discord.File(rating_chart, 'staff_ratings.png'), + file=disnake.File(rating_chart, 'staff_ratings.png'), ephemeral=True ) # Add individual staff stats view stats_view = StaffStatsView(self.bot, staff_list, analytics) - stats_embed = discord.Embed( + stats_embed = disnake.Embed( title="📈 Statistiques Individuelles", description="Cliquez sur un membre du staff pour voir ses statistiques détaillées:", - color=discord.Color.purple() + color=disnake.Color.purple() ) await interaction.followup.send(embed=stats_embed, view=stats_view, ephemeral=True) @@ -264,7 +264,7 @@ class TicketPanelView(discord.ui.View): - async def _config_callback(self, interaction: discord.Interaction): + async def _config_callback(self, interaction: disnake.Interaction): """Ouvre le panel de configuration (admin ou rôles config uniquement)""" # Reload config self.config = self.storage.load_config(interaction.guild_id) @@ -279,15 +279,15 @@ class TicketPanelView(discord.ui.View): storage = get_storage() config_view = ConfigPanelView(self.bot, storage, self.config) - embed = discord.Embed( + embed = disnake.Embed( title="Configuration des Tickets", description="Gérez la configuration du système de tickets", - color=discord.Color.blue() + color=disnake.Color.blue() ) await interaction.response.send_message(embed=embed, view=config_view, ephemeral=True) - async def _staff_ratings_callback(self, interaction: discord.Interaction): + async def _staff_ratings_callback(self, interaction: disnake.Interaction): """Display staff ratings (staff only)""" # Reload config self.config = self.storage.load_config(interaction.guild_id) @@ -304,10 +304,10 @@ class TicketPanelView(discord.ui.View): staff_list = await analytics.get_staff_list(interaction.guild_id) # Create main embed with overall stats - embed = discord.Embed( + embed = disnake.Embed( title="⭐ Notation du Staff", description="Performance globale du staff", - color=discord.Color.blue(), + color=disnake.Color.blue(), timestamp=datetime.now() ) @@ -350,16 +350,16 @@ class TicketPanelView(discord.ui.View): rating_chart = analytics.create_staff_rating_chart(staff_list) await interaction.followup.send( "📊 **Évaluations des Membres du Staff**", - file=discord.File(rating_chart, 'staff_ratings.png'), + file=disnake.File(rating_chart, 'staff_ratings.png'), ephemeral=True ) # Add individual staff stats view stats_view = StaffStatsView(self.bot, staff_list, analytics) - stats_embed = discord.Embed( + stats_embed = disnake.Embed( title="📈 Statistiques Individuelles", description="Cliquez sur un membre du staff pour voir ses statistiques détaillées:", - color=discord.Color.purple() + color=disnake.Color.purple() ) await interaction.followup.send(embed=stats_embed, view=stats_view, ephemeral=True) @@ -367,13 +367,13 @@ class TicketPanelView(discord.ui.View): await interaction.followup.send(f"❌ Erreur lors de la génération des graphiques: {e}", ephemeral=True) - def _is_staff(self, interaction: discord.Interaction) -> bool: + def _is_staff(self, interaction: disnake.Interaction) -> bool: return is_staff(interaction, self.config) - def _can_access_config(self, interaction: discord.Interaction) -> bool: + def _can_access_config(self, interaction: disnake.Interaction) -> bool: return can_access_config(interaction, self.config) - def _is_whitelisted(self, interaction: discord.Interaction) -> bool: + def _is_whitelisted(self, interaction: disnake.Interaction) -> bool: return is_whitelisted(interaction, self.config, self.bot) def _calculate_stats(self, guild_id: int) -> TicketStats: @@ -415,10 +415,10 @@ class TicketPanelView(discord.ui.View): return stats -class CategoryButton(discord.ui.Button): +class CategoryButton(disnake.ui.Button): def __init__(self, category, view_instance): super().__init__( - style=discord.ButtonStyle.blurple, + style=disnake.ButtonStyle.blurple, label=f"{category.emoji} {category.name}", custom_id=f"cat_{category.id}_{view_instance.id_gen}" ) @@ -426,11 +426,11 @@ class CategoryButton(discord.ui.Button): self.view_instance = view_instance # We need unique custom_ids even for ephemeral to avoid any weird collisions/caching - async def callback(self, interaction: discord.Interaction): + async def callback(self, interaction: disnake.Interaction): await self.view_instance._on_category_select(interaction, self.category.id) -class CategorySelectionView(discord.ui.View): +class CategorySelectionView(disnake.ui.View): """Sélection de catégorie pour créer un ticket""" def __init__(self, bot, config: GuildTicketConfig): @@ -443,16 +443,16 @@ class CategorySelectionView(discord.ui.View): for category in config.categories[:5]: self.add_item(CategoryButton(category, self)) - def _is_staff(self, interaction: discord.Interaction) -> bool: + def _is_staff(self, interaction: disnake.Interaction) -> bool: return is_staff(interaction, self.config) - def _can_access_config(self, interaction: discord.Interaction) -> bool: + def _can_access_config(self, interaction: disnake.Interaction) -> bool: return can_access_config(interaction, self.config) - def _is_whitelisted(self, interaction: discord.Interaction) -> bool: + def _is_whitelisted(self, interaction: disnake.Interaction) -> bool: return is_whitelisted(interaction, self.config, self.bot) - async def _on_category_select(self, interaction: discord.Interaction, category_id: str): + async def _on_category_select(self, interaction: disnake.Interaction, category_id: str): """Handle category selection""" try: @@ -486,7 +486,7 @@ class CategorySelectionView(discord.ui.View): pass -class TicketPriorityModal(discord.ui.Modal): +class TicketPriorityModal(disnake.ui.Modal): """Modal pour sélectionner la priorité et la raison""" def __init__(self, bot, config: GuildTicketConfig, category: TicketCategory): @@ -496,7 +496,7 @@ class TicketPriorityModal(discord.ui.Modal): self.category = category # Priorité - self.priority = discord.ui.TextInput( + self.priority = disnake.ui.TextInput( label="Priorité", placeholder="Normale, Haute ou Urgente", default="Normale", @@ -506,16 +506,16 @@ class TicketPriorityModal(discord.ui.Modal): self.add_item(self.priority) # Raison - self.reason = discord.ui.TextInput( + self.reason = disnake.ui.TextInput( label="Motif de votre demande", placeholder="Décrivez votre demande...", - style=discord.TextStyle.paragraph, + style=disnake.TextStyle.paragraph, required=True, max_length=500 ) self.add_item(self.reason) - async def on_submit(self, interaction: discord.Interaction): + async def on_submit(self, interaction: disnake.Interaction): try: # Map priority priority_map = { @@ -530,11 +530,11 @@ class TicketPriorityModal(discord.ui.Modal): except Exception as e: await interaction.response.send_message(f"Erreur lors de la création du ticket: {e}", ephemeral=True) - async def on_error(self, interaction: discord.Interaction, error: Exception): + async def on_error(self, interaction: disnake.Interaction, error: Exception): """Handle errors in the modal""" await interaction.response.send_message("Une erreur inattendue s'est produite lors de la création du ticket.", ephemeral=True) - async def _create_ticket(self, interaction: discord.Interaction, priority: Priority, reason: str): + async def _create_ticket(self, interaction: disnake.Interaction, priority: Priority, reason: str): """Crée le ticket""" guild = interaction.guild user = interaction.user @@ -543,9 +543,9 @@ class TicketPriorityModal(discord.ui.Modal): # Build permissions overwrites = { - guild.default_role: discord.PermissionOverwrite(read_messages=False), - user: discord.PermissionOverwrite(read_messages=True, send_messages=True), - guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True) + guild.default_role: disnake.PermissionOverwrite(read_messages=False), + user: disnake.PermissionOverwrite(read_messages=True, send_messages=True), + guild.me: disnake.PermissionOverwrite(read_messages=True, send_messages=True) } # Add staff roles (category-specific roles override global ones) @@ -554,7 +554,7 @@ class TicketPriorityModal(discord.ui.Modal): try: role = guild.get_role(int(role_id)) if role and role not in overwrites: - overwrites[role] = discord.PermissionOverwrite(read_messages=True, send_messages=True) + overwrites[role] = disnake.PermissionOverwrite(read_messages=True, send_messages=True) except (ValueError, TypeError): continue @@ -605,10 +605,10 @@ class TicketPriorityModal(discord.ui.Modal): storage.save_ticket(ticket_data) # Send welcome message - embed = discord.Embed( + embed = disnake.Embed( title=f" {self.category.emoji} {self.category.name}", description=f"Bienvenue {user.mention}!\n\nVotre demande sera traitée sous peu par notre équipe.", - color=discord.Color.blurple() + color=disnake.Color.blurple() ) embed.add_field(name="Motif", value=reason, inline=False) embed.add_field(name="Priorité", value=priority.value.capitalize(), inline=True) @@ -633,9 +633,9 @@ class TicketPriorityModal(discord.ui.Modal): if self.config.log_channel_id: log_channel = guild.get_channel(self.config.log_channel_id) if log_channel: - log_embed = discord.Embed( + log_embed = disnake.Embed( title="Nouveau Ticket", - color=discord.Color.green(), + color=disnake.Color.green(), timestamp=datetime.now() ) log_embed.add_field(name="Utilisateur", value=user.mention, inline=True) @@ -646,7 +646,7 @@ class TicketPriorityModal(discord.ui.Modal): await interaction.response.send_message(f"Ticket créé: {ticket_channel.mention}", ephemeral=True) -class SetCategoryModal(discord.ui.Modal): +class SetCategoryModal(disnake.ui.Modal): """Modal pour définir la catégorie Discord où les tickets seront créés""" def __init__(self, bot, storage, config: GuildTicketConfig): @@ -655,7 +655,7 @@ class SetCategoryModal(discord.ui.Modal): self.storage = storage self.config = config - self.category = discord.ui.TextInput( + self.category = disnake.ui.TextInput( label="Catégorie Discord", placeholder="Entrez l'ID de la catégorie (clic droit -> Copier l'ID)", required=True, @@ -663,7 +663,7 @@ class SetCategoryModal(discord.ui.Modal): ) self.add_item(self.category) - async def on_submit(self, interaction: discord.Interaction): + async def on_submit(self, interaction: disnake.Interaction): cat_id = self.category.value.strip() # Verify it's a valid ID @@ -673,7 +673,7 @@ class SetCategoryModal(discord.ui.Modal): # Check if category exists category = interaction.guild.get_channel(int(cat_id)) - if not category or not isinstance(category, discord.CategoryChannel): + if not category or not isinstance(category, disnake.CategoryChannel): await interaction.response.send_message("❌ Catégorie introuvable ou ce n'est pas une catégorie.", ephemeral=True) return @@ -683,7 +683,7 @@ class SetCategoryModal(discord.ui.Modal): await interaction.response.send_message(f"📁 Les tickets seront désormais créés dans la catégorie **{category.name}**!", ephemeral=True) -class TicketManagementView(discord.ui.View): +class TicketManagementView(disnake.ui.View): """Gestion du ticket (dans le salon du ticket)""" def __init__(self, bot, storage, config: GuildTicketConfig, ticket: TicketData): @@ -696,8 +696,8 @@ class TicketManagementView(discord.ui.View): # Claim (if enabled and ticket is open or claimed) if config.claim_enabled and self.category and self.category.allow_claims and ticket.status in [TicketStatus.OPEN, TicketStatus.CLAIMED]: - claim_btn = discord.ui.Button( - style=discord.ButtonStyle.primary, + claim_btn = disnake.ui.Button( + style=disnake.ButtonStyle.primary, label="Réclamer" if ticket.status == TicketStatus.OPEN else "Transférer", custom_id="ticket_claim" ) @@ -705,8 +705,8 @@ class TicketManagementView(discord.ui.View): self.add_item(claim_btn) # Fermer - close_btn = discord.ui.Button( - style=discord.ButtonStyle.red, + close_btn = disnake.ui.Button( + style=disnake.ButtonStyle.red, label="Fermer", custom_id="ticket_close" ) @@ -714,8 +714,8 @@ class TicketManagementView(discord.ui.View): self.add_item(close_btn) # Transcripts - transcript_btn = discord.ui.Button( - style=discord.ButtonStyle.secondary, + transcript_btn = disnake.ui.Button( + style=disnake.ButtonStyle.secondary, label="Transcript", custom_id="ticket_transcript" ) @@ -724,15 +724,15 @@ class TicketManagementView(discord.ui.View): # Reopen (if closed) if ticket.status == TicketStatus.CLOSED: - reopen_btn = discord.ui.Button( - style=discord.ButtonStyle.green, + reopen_btn = disnake.ui.Button( + style=disnake.ButtonStyle.green, label="Rouvrir", custom_id="ticket_reopen" ) reopen_btn.callback = self._reopen_callback self.add_item(reopen_btn) - async def _close_callback(self, interaction: discord.Interaction): + async def _close_callback(self, interaction: disnake.Interaction): """Demande confirmation avant de fermer le ticket""" # Vérifier si l'utilisateur peut fermer le ticket can_close = ( @@ -745,11 +745,11 @@ class TicketManagementView(discord.ui.View): return # Créer l'embed de confirmation - embed = discord.Embed( + embed = disnake.Embed( title="🔒 Confirmer la Fermeture", description="Êtes-vous sûr de vouloir fermer ce ticket?\n\n" "Cette action est irréversible et le salon sera supprimé.", - color=discord.Color.orange() + color=disnake.Color.orange() ) # Ajouter les informations du ticket @@ -766,7 +766,7 @@ class TicketManagementView(discord.ui.View): view = CloseConfirmationView(self.bot, self.storage, self.config, self.ticket) await interaction.response.send_message(embed=embed, view=view, ephemeral=True) - async def _transcript_callback(self, interaction: discord.Interaction): + async def _transcript_callback(self, interaction: disnake.Interaction): """Génère le transcript""" if not self._is_staff(interaction): await interaction.response.send_message("Réservé au staff.", ephemeral=True) @@ -798,12 +798,12 @@ class TicketManagementView(discord.ui.View): filepath = transcript_gen.save_transcript(ticket, messages, format="html") if filepath and os.path.exists(filepath): - file = discord.File(filepath, filename=os.path.basename(filepath)) + file = disnake.File(filepath, filename=os.path.basename(filepath)) await interaction.response.send_message("Transcript généré:", file=file, ephemeral=True) else: await interaction.response.send_message("Erreur lors de la génération du transcript.", ephemeral=True) - async def _claim_callback(self, interaction: discord.Interaction): + async def _claim_callback(self, interaction: disnake.Interaction): """Réclame le ticket""" if not self._is_staff(interaction): await interaction.response.send_message("Réservé au staff.", ephemeral=True) @@ -828,17 +828,17 @@ class TicketManagementView(discord.ui.View): await interaction.response.edit_message(view=self) if old_claimer: - embed = discord.Embed( + embed = disnake.Embed( title="🔄 Ticket Transféré", description=f"Le ticket a été transféré de <@{old_claimer}> à {interaction.user.mention}", - color=discord.Color.blue() + color=disnake.Color.blue() ) else: embed = TicketEmbedFormatter.ticket_claimed(self.ticket, interaction.user) await interaction.channel.send(embed=embed) - async def _reopen_callback(self, interaction: discord.Interaction): + async def _reopen_callback(self, interaction: disnake.Interaction): """Rouvre le ticket""" if not self._is_staff(interaction): await interaction.response.send_message("Réservé au staff.", ephemeral=True) @@ -852,8 +852,8 @@ class TicketManagementView(discord.ui.View): # Update channel overwrites = interaction.channel.overwrites.copy() for target, perms in overwrites.items(): - if isinstance(target, discord.Member) and target.id != interaction.guild.me.id: - overwrites[target] = discord.PermissionOverwrite( + if isinstance(target, disnake.Member) and target.id != interaction.guild.me.id: + overwrites[target] = disnake.PermissionOverwrite( read_messages=True, send_messages=True ) @@ -870,8 +870,8 @@ class TicketManagementView(discord.ui.View): break if self.config.claim_enabled and self.category and self.category.allow_claims: - claim_btn = discord.ui.Button( - style=discord.ButtonStyle.primary, + claim_btn = disnake.ui.Button( + style=disnake.ButtonStyle.primary, label="Réclamer", custom_id="ticket_claim" ) @@ -882,7 +882,7 @@ class TicketManagementView(discord.ui.View): await interaction.channel.send(embed=embed) await interaction.response.send_message("Ticket rouvert!", ephemeral=True) - def _is_staff(self, interaction: discord.Interaction) -> bool: + def _is_staff(self, interaction: disnake.Interaction) -> bool: return is_staff(interaction, self.config, self.category) def _is_staff_member(self, member) -> bool: @@ -891,7 +891,7 @@ class TicketManagementView(discord.ui.View): return False # Handle cases where member is a User (left the guild) - if not isinstance(member, discord.Member): + if not isinstance(member, disnake.Member): return member.id in self.config.staff_users if member.guild_permissions.administrator: @@ -912,7 +912,7 @@ class TicketManagementView(discord.ui.View): return False -class CloseConfirmationView(discord.ui.View): +class CloseConfirmationView(disnake.ui.View): """Demande de confirmation avant la fermeture du ticket""" def __init__(self, bot, storage, config: GuildTicketConfig, ticket: TicketData): @@ -923,8 +923,8 @@ class CloseConfirmationView(discord.ui.View): self.ticket = ticket self.category = config.get_category(ticket.category_id) - @discord.ui.button(label="✅ Confirmer", style=discord.ButtonStyle.green, custom_id="close_confirm") - async def confirm_callback(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="✅ Confirmer", style=disnake.ButtonStyle.green, custom_id="close_confirm") + async def confirm_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): """L'utilisateur confirme la fermeture""" # Vérifier les permissions can_close = ( @@ -937,13 +937,13 @@ class CloseConfirmationView(discord.ui.View): return # Show the delay view with countdown - embed = discord.Embed( + embed = disnake.Embed( title="⏳ Fermeture du Ticket", description="Le ticket va être fermé dans quelques secondes...\n\n" "📄 Génération du transcript en cours...\n" "📝 Envoi aux logs...\n" "🗑️ Suppression du canal...", - color=discord.Color.orange() + color=disnake.Color.orange() ) view = CloseDelayView(self.bot, self.storage, self.config, self.ticket) @@ -952,17 +952,17 @@ class CloseConfirmationView(discord.ui.View): # Start the countdown await view.start_countdown(interaction) - @discord.ui.button(label="❌ Annuler", style=discord.ButtonStyle.red, custom_id="close_cancel") - async def cancel_callback(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="❌ Annuler", style=disnake.ButtonStyle.red, custom_id="close_cancel") + async def cancel_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): """L'utilisateur annule la fermeture""" await interaction.response.defer() await interaction.delete_original_response() - def _is_staff(self, interaction: discord.Interaction) -> bool: + def _is_staff(self, interaction: disnake.Interaction) -> bool: return is_staff(interaction, self.config, self.category) -class CloseDelayView(discord.ui.View): +class CloseDelayView(disnake.ui.View): """Affiche le délai d'attente avant suppression du ticket""" def __init__(self, bot, storage, config: GuildTicketConfig, ticket: TicketData, close_reason: str = ""): @@ -975,15 +975,15 @@ class CloseDelayView(discord.ui.View): self.category = config.get_category(ticket.category_id) # Ajouter le bouton Annuler - cancel_btn = discord.ui.Button( + cancel_btn = disnake.ui.Button( label="❌ Annuler", - style=discord.ButtonStyle.red, + style=disnake.ButtonStyle.red, custom_id="delay_cancel" ) cancel_btn.callback = self._cancel_callback self.add_item(cancel_btn) - async def start_countdown(self, interaction: discord.Interaction): + async def start_countdown(self, interaction: disnake.Interaction): """Démarre le compte à rebours""" # Désactiver le bouton Annuler for item in self.children: @@ -999,11 +999,11 @@ class CloseDelayView(discord.ui.View): ] for step_text, delay in steps: - embed = discord.Embed( + embed = disnake.Embed( title="🔒 Fermeture du Ticket", description=f"**{step_text}**\n\n" f"Merci pour votre confiance!", - color=discord.Color.orange() + color=disnake.Color.orange() ) embed.set_footer(text=f"Ticket #{self.ticket.channel_id}") @@ -1013,7 +1013,7 @@ class CloseDelayView(discord.ui.View): # Procéder à la fermeture await self._close_ticket(interaction) - async def _close_ticket(self, interaction: discord.Interaction): + async def _close_ticket(self, interaction: disnake.Interaction): """Effectue la fermeture réelle du ticket""" # Collect messages for transcript transcript_file = None @@ -1052,10 +1052,10 @@ class CloseDelayView(discord.ui.View): if user: from .feedback_view import FeedbackView - dm_embed = discord.Embed( + dm_embed = disnake.Embed( title="📝 Votre avis nous intéresse !", description=f"Votre ticket **#{self.ticket.channel_id}** a été fermé.\nMerci de prendre un moment pour noter la qualité du support.", - color=discord.Color.green(), + color=disnake.Color.green(), timestamp=datetime.now() ) dm_embed.add_field(name="Durée du ticket", value=f"{duration:.1f} minutes", inline=True) @@ -1066,9 +1066,9 @@ class CloseDelayView(discord.ui.View): view = FeedbackView(self.bot, self.storage, self.ticket) # Add a button to directly open the feedback form - note_staff_btn = discord.ui.Button( + note_staff_btn = disnake.ui.Button( label="⭐ Noter le staff", - style=discord.ButtonStyle.primary, + style=disnake.ButtonStyle.primary, custom_id="note_staff_direct" ) note_staff_btn.callback = self._note_staff_callback @@ -1083,10 +1083,10 @@ class CloseDelayView(discord.ui.View): try: log_channel = interaction.guild.get_channel(self.config.log_channel_id) if log_channel: - log_embed = discord.Embed( + log_embed = disnake.Embed( title="📄 Ticket Fermé - Transcript", description=f"Ticket #{self.ticket.channel_id} fermé par {interaction.user.mention}", - color=discord.Color.red(), + color=disnake.Color.red(), timestamp=datetime.now() ) @@ -1101,7 +1101,7 @@ class CloseDelayView(discord.ui.View): log_embed.add_field(name="Raison", value=self.close_reason, inline=False) if transcript_file and os.path.exists(transcript_file): - file = discord.File(transcript_file, filename=os.path.basename(transcript_file)) + file = disnake.File(transcript_file, filename=os.path.basename(transcript_file)) await log_channel.send(embed=log_embed, file=file) else: log_embed.add_field(name="⚠️ Transcript", value="Le transcript n'a pas pu être généré (l'utilisateur a peut-être quitté le serveur).", inline=False) @@ -1114,32 +1114,32 @@ class CloseDelayView(discord.ui.View): await interaction.channel.delete(reason=f"Ticket fermé par {interaction.user}") except Exception as e: print(f"Error deleting channel: {e}") - embed = discord.Embed( + embed = disnake.Embed( title="❌ Erreur", description="Le ticket a été fermé mais la suppression du salon a échoué.", - color=discord.Color.red() + color=disnake.Color.red() ) await interaction.channel.send(embed=embed) - async def _note_staff_callback(self, interaction: discord.Interaction): + async def _note_staff_callback(self, interaction: disnake.Interaction): """Handle direct staff rating button""" # This would open a direct feedback form await interaction.response.send_message("Vous pouvez maintenant noter le staff directement !", ephemeral=True) - async def _cancel_callback(self, interaction: discord.Interaction): + async def _cancel_callback(self, interaction: disnake.Interaction): """Annule la fermeture du ticket pendant le délai""" await interaction.response.defer() await interaction.delete_original_response() # Envoyer un message de confirmation d'annulation - embed = discord.Embed( + embed = disnake.Embed( title="❌ Fermeture Annulée", description="La fermeture du ticket a été annulée.", - color=discord.Color.red() + color=disnake.Color.red() ) await interaction.channel.send(embed=embed, delete_after=5) - def _is_staff(self, interaction: discord.Interaction) -> bool: + def _is_staff(self, interaction: disnake.Interaction) -> bool: return is_staff(interaction, self.config, self.category) def _is_staff_member(self, member) -> bool: @@ -1148,7 +1148,7 @@ class CloseDelayView(discord.ui.View): return False # Handle cases where member is a User (left the guild) - if not isinstance(member, discord.Member): + if not isinstance(member, disnake.Member): return member.id in self.config.staff_users if member.guild_permissions.administrator: @@ -1169,7 +1169,7 @@ class CloseDelayView(discord.ui.View): return False -class CloseTicketModal(discord.ui.Modal): +class CloseTicketModal(disnake.ui.Modal): """Modal pour fermer un ticket""" def __init__(self, bot, storage, config: GuildTicketConfig, ticket: TicketData): @@ -1180,10 +1180,10 @@ class CloseTicketModal(discord.ui.Modal): self.ticket = ticket self.category = config.get_category(ticket.category_id) - self.reason = discord.ui.TextInput( + self.reason = disnake.ui.TextInput( label="Raison de fermeture", placeholder="Optionnel...", - style=discord.TextStyle.paragraph, + style=disnake.TextStyle.paragraph, required=False, max_length=300 ) @@ -1191,7 +1191,7 @@ class CloseTicketModal(discord.ui.Modal): # Survey fields if enabled if config.survey_enabled and self.category and self.category.survey_enabled: - self.rating = discord.ui.TextInput( + self.rating = disnake.ui.TextInput( label="Note (1-5 étoiles)", placeholder="1 à 5", required=False, @@ -1199,16 +1199,16 @@ class CloseTicketModal(discord.ui.Modal): ) self.add_item(self.rating) - self.feedback = discord.ui.TextInput( + self.feedback = disnake.ui.TextInput( label="Commentaires", placeholder="Vos commentaires sur le support...", - style=discord.TextStyle.paragraph, + style=disnake.TextStyle.paragraph, required=False, max_length=500 ) self.add_item(self.feedback) - async def on_submit(self, interaction: discord.Interaction): + async def on_submit(self, interaction: disnake.Interaction): # Check permissions can_close = ( interaction.user.id == self.ticket.user_id or @@ -1239,13 +1239,13 @@ class CloseTicketModal(discord.ui.Modal): await interaction.response.send_message("✅ Fermeture du ticket en cours...", ephemeral=True) # Send confirmation message in the ticket channel - confirm_embed = discord.Embed( + confirm_embed = disnake.Embed( title="⏳ Fermeture du Ticket", description="Le ticket va être fermé dans quelques secondes...\n\n" "📄 Génération du transcript en cours...\n" "📝 Envoi aux logs...\n" "🗑️ Suppression du canal...", - color=discord.Color.orange() + color=disnake.Color.orange() ) await interaction.channel.send(embed=confirm_embed) @@ -1290,10 +1290,10 @@ class CloseTicketModal(discord.ui.Modal): log_channel = interaction.guild.get_channel(self.config.log_channel_id) if log_channel: # Create log embed - log_embed = discord.Embed( + log_embed = disnake.Embed( title="📄 Ticket Fermé - Transcript", description=f"Ticket #{self.ticket.channel_id} fermé par {interaction.user.mention}", - color=discord.Color.red(), + color=disnake.Color.red(), timestamp=datetime.now() ) @@ -1310,7 +1310,7 @@ class CloseTicketModal(discord.ui.Modal): # Send transcript file if transcript_file and os.path.exists(transcript_file): - file = discord.File(transcript_file, filename=os.path.basename(transcript_file)) + file = disnake.File(transcript_file, filename=os.path.basename(transcript_file)) await log_channel.send(embed=log_embed, file=file) else: log_embed.add_field(name="⚠️ Transcript", value="Le transcript n'a pas pu être généré (l'utilisateur a peut-être quitté le serveur).", inline=False) @@ -1326,7 +1326,7 @@ class CloseTicketModal(discord.ui.Modal): # Fallback: just mark as closed if deletion fails await interaction.followup.send("Erreur lors de la suppression du canal, mais le ticket a été fermé.", ephemeral=True) - def _is_staff(self, interaction: discord.Interaction) -> bool: + def _is_staff(self, interaction: disnake.Interaction) -> bool: return is_staff(interaction, self.config, self.category) def _is_staff_member(self, member) -> bool: @@ -1335,7 +1335,7 @@ class CloseTicketModal(discord.ui.Modal): return False # Handle cases where member is a User (left the guild) - if not isinstance(member, discord.Member): + if not isinstance(member, disnake.Member): return member.id in self.config.staff_users if member.guild_permissions.administrator: @@ -1355,7 +1355,7 @@ class CloseTicketModal(discord.ui.Modal): return False -class TicketSetupView(discord.ui.View): +class TicketSetupView(disnake.ui.View): """Panel de configuration complet du système de tickets""" def __init__(self, bot, storage, config: GuildTicketConfig): @@ -1365,8 +1365,8 @@ class TicketSetupView(discord.ui.View): self.config = config # Toggle enabled - toggle_btn = discord.ui.Button( - style=discord.ButtonStyle.green if config.enabled else discord.ButtonStyle.red, + toggle_btn = disnake.ui.Button( + style=disnake.ButtonStyle.green if config.enabled else disnake.ButtonStyle.red, label="🔄 Activer/Désactiver", custom_id="setup_toggle" ) @@ -1374,8 +1374,8 @@ class TicketSetupView(discord.ui.View): self.add_item(toggle_btn) # Staff roles - roles_btn = discord.ui.Button( - style=discord.ButtonStyle.primary, + roles_btn = disnake.ui.Button( + style=disnake.ButtonStyle.primary, label="👥 Rôles Staff", custom_id="setup_roles" ) @@ -1383,8 +1383,8 @@ class TicketSetupView(discord.ui.View): self.add_item(roles_btn) # Config roles - config_roles_btn = discord.ui.Button( - style=discord.ButtonStyle.secondary, + config_roles_btn = disnake.ui.Button( + style=disnake.ButtonStyle.secondary, label="🔑 Rôles Config", custom_id="setup_config_roles" ) @@ -1392,8 +1392,8 @@ class TicketSetupView(discord.ui.View): self.add_item(config_roles_btn) # Categories - cat_btn = discord.ui.Button( - style=discord.ButtonStyle.secondary, + cat_btn = disnake.ui.Button( + style=disnake.ButtonStyle.secondary, label="📂 Catégories", custom_id="setup_categories" ) @@ -1401,8 +1401,8 @@ class TicketSetupView(discord.ui.View): self.add_item(cat_btn) # Log channel - log_btn = discord.ui.Button( - style=discord.ButtonStyle.secondary, + log_btn = disnake.ui.Button( + style=disnake.ButtonStyle.secondary, label="📝 Canal de Logs", custom_id="setup_log_channel" ) @@ -1410,36 +1410,36 @@ class TicketSetupView(discord.ui.View): self.add_item(log_btn) # Advanced settings - advanced_btn = discord.ui.Button( - style=discord.ButtonStyle.secondary, + advanced_btn = disnake.ui.Button( + style=disnake.ButtonStyle.secondary, label="⚙️ Paramètres Avancés", custom_id="setup_advanced" ) advanced_btn.callback = self._advanced_callback self.add_item(advanced_btn) - async def _toggle_callback(self, interaction: discord.Interaction): + async def _toggle_callback(self, interaction: disnake.Interaction): self.config.enabled = not self.config.enabled self.storage.save_config(interaction.guild_id, self.config) status = "activé" if self.config.enabled else "désactivé" - embed = discord.Embed( + embed = disnake.Embed( title="🔄 État du Système", description=f"Le système de tickets a été **{status}**!", - color=discord.Color.green() if self.config.enabled else discord.Color.red() + color=disnake.Color.green() if self.config.enabled else disnake.Color.red() ) await interaction.response.send_message(embed=embed, ephemeral=True) # Update button for item in self.children: if item.custom_id == "setup_toggle": - item.style = discord.ButtonStyle.green if self.config.enabled else discord.ButtonStyle.red + item.style = disnake.ButtonStyle.green if self.config.enabled else disnake.ButtonStyle.red await interaction.message.edit(view=self) - async def _roles_callback(self, interaction: discord.Interaction): + async def _roles_callback(self, interaction: disnake.Interaction): view = StaffRolesSetupView(self.bot, self.storage, self.config, interaction.guild) - embed = discord.Embed( + embed = disnake.Embed( title="👥 Configuration des Rôles Staff", description="Définissez les rôles qui peuvent gérer les tickets et accéder aux fonctionnalités avancées.\n\n" "**Permissions des rôles staff:**\n" @@ -1447,7 +1447,7 @@ class TicketSetupView(discord.ui.View): "• Accéder aux analytics et statistiques\n" "• Générer des transcripts\n" "• Fermer les tickets des autres", - color=discord.Color.blue() + color=disnake.Color.blue() ) role_mentions = [] @@ -1464,13 +1464,13 @@ class TicketSetupView(discord.ui.View): await interaction.response.send_message(embed=embed, view=view, ephemeral=True) - async def _categories_callback(self, interaction: discord.Interaction): + async def _categories_callback(self, interaction: disnake.Interaction): view = CategoriesSetupView(self.bot, self.storage, self.config) - embed = discord.Embed( + embed = disnake.Embed( title="📂 Gestion des Catégories", description="Créez et gérez les catégories de tickets pour organiser vos demandes.", - color=discord.Color.blue() + color=disnake.Color.blue() ) if self.config.categories: @@ -1491,17 +1491,17 @@ class TicketSetupView(discord.ui.View): await interaction.response.send_message(embed=embed, view=view, ephemeral=True) - async def _config_roles_callback(self, interaction: discord.Interaction): + async def _config_roles_callback(self, interaction: disnake.Interaction): view = ConfigRolesSetupView(self.bot, self.storage, self.config, interaction.guild) - embed = discord.Embed( + embed = disnake.Embed( title="🔑 Configuration des Rôles Config", description="Définissez les rôles qui peuvent accéder au panel de configuration du système de tickets.\n\n" "**Permissions des rôles config:**\n" "• Accéder à la commande /ticketconfig\n" "• Modifier tous les paramètres du système\n" "• Gérer les catégories et rôles staff", - color=discord.Color.purple() + color=disnake.Color.purple() ) role_mentions = [] @@ -1518,17 +1518,17 @@ class TicketSetupView(discord.ui.View): await interaction.response.send_message(embed=embed, view=view, ephemeral=True) - async def _log_channel_callback(self, interaction: discord.Interaction): + async def _log_channel_callback(self, interaction: disnake.Interaction): modal = SetChannelModal(self.bot, self.storage, self.config, "log") await interaction.response.send_modal(modal) - async def _advanced_callback(self, interaction: discord.Interaction): + async def _advanced_callback(self, interaction: disnake.Interaction): view = AdvancedSetupView(self.bot, self.storage, self.config) - embed = discord.Embed( + embed = disnake.Embed( title="⚙️ Paramètres Avancés", description="Configuration fine du système de tickets.", - color=discord.Color.purple() + color=disnake.Color.purple() ) embed.add_field( @@ -1542,10 +1542,10 @@ class TicketSetupView(discord.ui.View): await interaction.response.send_message(embed=embed, view=view, ephemeral=True) -class StaffRolesSetupView(discord.ui.View): +class StaffRolesSetupView(disnake.ui.View): """Configuration des rôles staff""" - def __init__(self, bot, storage, config: GuildTicketConfig, guild: discord.Guild): + def __init__(self, bot, storage, config: GuildTicketConfig, guild: disnake.Guild): super().__init__(timeout=None) self.bot = bot self.storage = storage @@ -1559,7 +1559,7 @@ class StaffRolesSetupView(discord.ui.View): for role in self.guild.roles: if role.id != self.guild.id: # Exclude @everyone options.append( - discord.SelectOption( + disnake.SelectOption( label=role.name, value=str(role.id), default=str(role.id) in self.config.staff_roles @@ -1567,7 +1567,7 @@ class StaffRolesSetupView(discord.ui.View): ) if options: - select = discord.ui.Select( + select = disnake.ui.Select( placeholder="Sélectionnez les rôles staff...", options=options[:25], min_values=0, @@ -1576,7 +1576,7 @@ class StaffRolesSetupView(discord.ui.View): select.callback = self._select_callback self.add_item(select) - async def _select_callback(self, interaction: discord.Interaction): + async def _select_callback(self, interaction: disnake.Interaction): selected = list(interaction.data.get("values", [])) self.config.staff_roles = selected @@ -1588,10 +1588,10 @@ class StaffRolesSetupView(discord.ui.View): if role: role_mentions.append(role.mention) - embed = discord.Embed( + embed = disnake.Embed( title="✅ Rôles Staff Mis à Jour", description=f"**{len(selected)}** rôle(s) configuré(s) comme staff.", - color=discord.Color.green() + color=disnake.Color.green() ) embed.add_field( @@ -1603,10 +1603,10 @@ class StaffRolesSetupView(discord.ui.View): await interaction.response.send_message(embed=embed, ephemeral=True) -class ConfigRolesSetupView(discord.ui.View): +class ConfigRolesSetupView(disnake.ui.View): """Configuration des rôles config""" - def __init__(self, bot, storage, config: GuildTicketConfig, guild: discord.Guild): + def __init__(self, bot, storage, config: GuildTicketConfig, guild: disnake.Guild): super().__init__(timeout=None) self.bot = bot self.storage = storage @@ -1620,7 +1620,7 @@ class ConfigRolesSetupView(discord.ui.View): for role in self.guild.roles: if role.id != self.guild.id: # Exclude @everyone options.append( - discord.SelectOption( + disnake.SelectOption( label=role.name, value=str(role.id), default=str(role.id) in self.config.config_roles @@ -1628,7 +1628,7 @@ class ConfigRolesSetupView(discord.ui.View): ) if options: - select = discord.ui.Select( + select = disnake.ui.Select( placeholder="Sélectionnez les rôles config...", options=options[:25], min_values=0, @@ -1637,7 +1637,7 @@ class ConfigRolesSetupView(discord.ui.View): select.callback = self._select_callback self.add_item(select) - async def _select_callback(self, interaction: discord.Interaction): + async def _select_callback(self, interaction: disnake.Interaction): selected = list(interaction.data.get("values", [])) self.config.config_roles = selected @@ -1649,10 +1649,10 @@ class ConfigRolesSetupView(discord.ui.View): if role: role_mentions.append(role.mention) - embed = discord.Embed( + embed = disnake.Embed( title="✅ Rôles Config Mis à Jour", description=f"**{len(selected)}** rôle(s) configuré(s) pour accéder à la configuration.", - color=discord.Color.green() + color=disnake.Color.green() ) embed.add_field( @@ -1670,7 +1670,7 @@ class ConfigRolesSetupView(discord.ui.View): await interaction.response.send_message(embed=embed, ephemeral=True) -class CategoriesSetupView(discord.ui.View): +class CategoriesSetupView(disnake.ui.View): """Gestion des catégories""" def __init__(self, bot, storage, config: GuildTicketConfig): @@ -1680,8 +1680,8 @@ class CategoriesSetupView(discord.ui.View): self.config = config # Add category - add_btn = discord.ui.Button( - style=discord.ButtonStyle.green, + add_btn = disnake.ui.Button( + style=disnake.ButtonStyle.green, label="➕ Ajouter", custom_id="cat_add" ) @@ -1690,19 +1690,19 @@ class CategoriesSetupView(discord.ui.View): # Remove category (only if categories exist) if config.categories: - remove_btn = discord.ui.Button( - style=discord.ButtonStyle.red, + remove_btn = disnake.ui.Button( + style=disnake.ButtonStyle.red, label="➖ Supprimer", custom_id="cat_remove" ) remove_btn.callback = self._remove_callback self.add_item(remove_btn) - async def _add_callback(self, interaction: discord.Interaction): + async def _add_callback(self, interaction: disnake.Interaction): modal = AddCategoryModal(self.bot, self.storage, self.config) await interaction.response.send_modal(modal) - async def _remove_callback(self, interaction: discord.Interaction): + async def _remove_callback(self, interaction: disnake.Interaction): if not self.config.categories: await interaction.response.send_message("Aucune catégorie à supprimer.", ephemeral=True) return @@ -1711,31 +1711,31 @@ class CategoriesSetupView(discord.ui.View): options = [] for cat in self.config.categories[:25]: # Discord limit options.append( - discord.SelectOption( + disnake.SelectOption( label=cat.name, value=cat.id, emoji=cat.emoji ) ) - select = discord.ui.Select( + select = disnake.ui.Select( placeholder="Sélectionnez une catégorie à supprimer...", options=options ) select.callback = self._remove_select_callback - view = discord.ui.View() + view = disnake.ui.View() view.add_item(select) - embed = discord.Embed( + embed = disnake.Embed( title="Supprimer une Catégorie", description="Sélectionnez la catégorie à supprimer:", - color=discord.Color.red() + color=disnake.Color.red() ) await interaction.response.send_message(embed=embed, view=view, ephemeral=True) - async def _remove_select_callback(self, interaction: discord.Interaction): + async def _remove_select_callback(self, interaction: disnake.Interaction): cat_id = interaction.data["values"][0] # Remove category @@ -1745,7 +1745,7 @@ class CategoriesSetupView(discord.ui.View): await interaction.response.send_message("✅ Catégorie supprimée!", ephemeral=True) -class AdvancedSetupView(discord.ui.View): +class AdvancedSetupView(disnake.ui.View): """Paramètres avancés""" def __init__(self, bot, storage, config: GuildTicketConfig): @@ -1755,8 +1755,8 @@ class AdvancedSetupView(discord.ui.View): self.config = config # Toggle claim system - claim_btn = discord.ui.Button( - style=discord.ButtonStyle.green if config.claim_enabled else discord.ButtonStyle.red, + claim_btn = disnake.ui.Button( + style=disnake.ButtonStyle.green if config.claim_enabled else disnake.ButtonStyle.red, label=f"{'✅' if config.claim_enabled else '❌'} Système de Réclamation", custom_id="advanced_claim" ) @@ -1764,8 +1764,8 @@ class AdvancedSetupView(discord.ui.View): self.add_item(claim_btn) # Toggle survey system - survey_btn = discord.ui.Button( - style=discord.ButtonStyle.green if config.survey_enabled else discord.ButtonStyle.red, + survey_btn = disnake.ui.Button( + style=disnake.ButtonStyle.green if config.survey_enabled else disnake.ButtonStyle.red, label=f"{'✅' if config.survey_enabled else '❌'} Sondages", custom_id="advanced_survey" ) @@ -1773,58 +1773,58 @@ class AdvancedSetupView(discord.ui.View): self.add_item(survey_btn) # Max tickets per user - max_btn = discord.ui.Button( - style=discord.ButtonStyle.secondary, + max_btn = disnake.ui.Button( + style=disnake.ButtonStyle.secondary, label=f"📊 Limite: {config.max_tickets_per_user}", custom_id="advanced_max" ) max_btn.callback = self._max_callback self.add_item(max_btn) - async def _claim_callback(self, interaction: discord.Interaction): + async def _claim_callback(self, interaction: disnake.Interaction): self.config.claim_enabled = not self.config.claim_enabled self.storage.save_config(interaction.guild_id, self.config) status = "activé" if self.config.claim_enabled else "désactivé" - embed = discord.Embed( + embed = disnake.Embed( title="🔄 Système de Réclamation", description=f"Le système de réclamation a été **{status}**!", - color=discord.Color.green() if self.config.claim_enabled else discord.Color.red() + color=disnake.Color.green() if self.config.claim_enabled else disnake.Color.red() ) await interaction.response.send_message(embed=embed, ephemeral=True) # Update button for item in self.children: if item.custom_id == "advanced_claim": - item.style = discord.ButtonStyle.green if self.config.claim_enabled else discord.ButtonStyle.red + item.style = disnake.ButtonStyle.green if self.config.claim_enabled else disnake.ButtonStyle.red item.label = f"{'✅' if self.config.claim_enabled else '❌'} Système de Réclamation" await interaction.message.edit(view=self) - async def _survey_callback(self, interaction: discord.Interaction): + async def _survey_callback(self, interaction: disnake.Interaction): self.config.survey_enabled = not self.config.survey_enabled self.storage.save_config(interaction.guild_id, self.config) status = "activé" if self.config.survey_enabled else "désactivé" - embed = discord.Embed( + embed = disnake.Embed( title="🔄 Système de Sondages", description=f"Le système de sondages a été **{status}**!", - color=discord.Color.green() if self.config.survey_enabled else discord.Color.red() + color=disnake.Color.green() if self.config.survey_enabled else disnake.Color.red() ) await interaction.response.send_message(embed=embed, ephemeral=True) # Update button for item in self.children: if item.custom_id == "advanced_survey": - item.style = discord.ButtonStyle.green if self.config.survey_enabled else discord.ButtonStyle.red + item.style = disnake.ButtonStyle.green if self.config.survey_enabled else disnake.ButtonStyle.red item.label = f"{'✅' if self.config.survey_enabled else '❌'} Sondages" await interaction.message.edit(view=self) - async def _max_callback(self, interaction: discord.Interaction): + async def _max_callback(self, interaction: disnake.Interaction): modal = MaxTicketsModal(self.bot, self.storage, self.config) await interaction.response.send_modal(modal) -class MaxTicketsModal(discord.ui.Modal): +class MaxTicketsModal(disnake.ui.Modal): """Modal pour définir la limite de tickets par utilisateur""" def __init__(self, bot, storage, config: GuildTicketConfig): @@ -1833,7 +1833,7 @@ class MaxTicketsModal(discord.ui.Modal): self.storage = storage self.config = config - self.max_tickets = discord.ui.TextInput( + self.max_tickets = disnake.ui.TextInput( label="Nombre maximum de tickets ouverts par utilisateur", placeholder="5", default=str(config.max_tickets_per_user), @@ -1842,7 +1842,7 @@ class MaxTicketsModal(discord.ui.Modal): ) self.add_item(self.max_tickets) - async def on_submit(self, interaction: discord.Interaction): + async def on_submit(self, interaction: disnake.Interaction): try: max_tickets = int(self.max_tickets.value) if max_tickets < 1 or max_tickets > 50: @@ -1852,10 +1852,10 @@ class MaxTicketsModal(discord.ui.Modal): self.config.max_tickets_per_user = max_tickets self.storage.save_config(interaction.guild_id, self.config) - embed = discord.Embed( + embed = disnake.Embed( title="✅ Limite Mise à Jour", description=f"La limite de tickets par utilisateur est maintenant de **{max_tickets}**.", - color=discord.Color.green() + color=disnake.Color.green() ) await interaction.response.send_message(embed=embed, ephemeral=True) @@ -1863,7 +1863,7 @@ class MaxTicketsModal(discord.ui.Modal): await interaction.response.send_message("Veuillez entrer un nombre valide.", ephemeral=True) -class ConfigPanelView(discord.ui.View): +class ConfigPanelView(disnake.ui.View): """Panel de configuration""" def __init__(self, bot, storage, config: GuildTicketConfig): @@ -1873,8 +1873,8 @@ class ConfigPanelView(discord.ui.View): self.config = config # Toggle enabled - toggle_btn = discord.ui.Button( - style=discord.ButtonStyle.green if config.enabled else discord.ButtonStyle.red, + toggle_btn = disnake.ui.Button( + style=disnake.ButtonStyle.green if config.enabled else disnake.ButtonStyle.red, label="Activer/Désactiver", custom_id="config_toggle" ) @@ -1882,17 +1882,17 @@ class ConfigPanelView(discord.ui.View): self.add_item(toggle_btn) # Default category - self.category_btn = discord.ui.Button( + self.category_btn = disnake.ui.Button( label="📁 Catégorie par défaut", - style=discord.ButtonStyle.secondary, + style=disnake.ButtonStyle.secondary, custom_id="config_category" ) self.category_btn.callback = self._category_callback self.add_item(self.category_btn) # Gérer les catégories - self.manage_cat_btn = discord.ui.Button( - style=discord.ButtonStyle.blurple, + self.manage_cat_btn = disnake.ui.Button( + style=disnake.ButtonStyle.blurple, label="📂 Gérer les catégories", custom_id="config_manage_cat" ) @@ -1900,8 +1900,8 @@ class ConfigPanelView(discord.ui.View): self.add_item(self.manage_cat_btn) # Staff permissions - staff_btn = discord.ui.Button( - style=discord.ButtonStyle.secondary, + staff_btn = disnake.ui.Button( + style=disnake.ButtonStyle.secondary, label="Permissions Staff", custom_id="config_staff_perms" ) @@ -1909,8 +1909,8 @@ class ConfigPanelView(discord.ui.View): self.add_item(staff_btn) # Config permissions - config_perms_btn = discord.ui.Button( - style=discord.ButtonStyle.secondary, + config_perms_btn = disnake.ui.Button( + style=disnake.ButtonStyle.secondary, label="Permissions Config", custom_id="config_config_perms" ) @@ -1918,15 +1918,15 @@ class ConfigPanelView(discord.ui.View): self.add_item(config_perms_btn) # Log channel - log_btn = discord.ui.Button( - style=discord.ButtonStyle.secondary, + log_btn = disnake.ui.Button( + style=disnake.ButtonStyle.secondary, label="Canal de Logs", custom_id="config_log_channel" ) log_btn.callback = self._log_channel_callback self.add_item(log_btn) - async def _toggle_callback(self, interaction: discord.Interaction): + async def _toggle_callback(self, interaction: disnake.Interaction): self.config.enabled = not self.config.enabled self.storage.save_config(interaction.guild_id, self.config) @@ -1936,32 +1936,32 @@ class ConfigPanelView(discord.ui.View): # Update button for item in self.children: if item.custom_id == "config_toggle": - item.style = discord.ButtonStyle.green if self.config.enabled else discord.ButtonStyle.red + item.style = disnake.ButtonStyle.green if self.config.enabled else disnake.ButtonStyle.red await interaction.message.edit(view=self) - async def _manage_cat_callback(self, interaction: discord.Interaction): + async def _manage_cat_callback(self, interaction: disnake.Interaction): view = CategoryManagerView(self.bot, self.storage, self.config) await interaction.response.edit_message(embed=view._get_embed(), view=view) - async def _staff_perms_callback(self, interaction: discord.Interaction): + async def _staff_perms_callback(self, interaction: disnake.Interaction): view = PermissionConfigView(self.bot, self.storage, self.config, interaction.guild, "staff") - embed = discord.Embed( + embed = disnake.Embed( title="Permissions Staff", description="Configurez qui peut gérer les tickets (répondre, fermer, etc.)", - color=discord.Color.blue() + color=disnake.Color.blue() ) self._add_current_perms_fields(embed, self.config.staff_roles, self.config.staff_users, interaction.guild) await interaction.response.send_message(embed=embed, view=view, ephemeral=True) - async def _config_perms_callback(self, interaction: discord.Interaction): + async def _config_perms_callback(self, interaction: disnake.Interaction): view = PermissionConfigView(self.bot, self.storage, self.config, interaction.guild, "config") - embed = discord.Embed( + embed = disnake.Embed( title="Permissions Configuration", description="Configurez qui peut modifier les paramètres du bot (/ticketconfig)", - color=discord.Color.orange() + color=disnake.Color.orange() ) self._add_current_perms_fields(embed, self.config.config_roles, self.config.config_users, interaction.guild) @@ -1985,16 +1985,16 @@ class ConfigPanelView(discord.ui.View): embed.add_field(name="Rôles", value=", ".join(role_mentions) if role_mentions else "Aucun", inline=False) embed.add_field(name="Utilisateurs", value=", ".join(user_mentions) if user_mentions else "Aucun", inline=False) - async def _category_callback(self, interaction: discord.Interaction): + async def _category_callback(self, interaction: disnake.Interaction): modal = SetCategoryModal(self.bot, self.storage, self.config) await interaction.response.send_modal(modal) - async def _log_channel_callback(self, interaction: discord.Interaction): + async def _log_channel_callback(self, interaction: disnake.Interaction): modal = SetChannelModal(self.bot, self.storage, self.config, "log") await interaction.response.send_modal(modal) -class AddCategoryModal(discord.ui.Modal): +class AddCategoryModal(disnake.ui.Modal): """Modal pour ajouter une catégorie""" def __init__(self, bot, storage, config: GuildTicketConfig): @@ -2003,18 +2003,18 @@ class AddCategoryModal(discord.ui.Modal): self.storage = storage self.config = config - self.name = discord.ui.TextInput(label="Nom", placeholder="Ex: Support", required=True) + self.name = disnake.ui.TextInput(label="Nom", placeholder="Ex: Support", required=True) self.add_item(self.name) - self.description = discord.ui.TextInput( - label="Description", placeholder="Description...", required=False, style=discord.TextStyle.paragraph + self.description = disnake.ui.TextInput( + label="Description", placeholder="Description...", required=False, style=disnake.TextStyle.paragraph ) self.add_item(self.description) - self.emoji = discord.ui.TextInput(label="Emoji", placeholder="🎫", required=False, max_length=5) + self.emoji = disnake.ui.TextInput(label="Emoji", placeholder="🎫", required=False, max_length=5) self.add_item(self.emoji) - async def on_submit(self, interaction: discord.Interaction): + async def on_submit(self, interaction: disnake.Interaction): import uuid cat_id = str(uuid.uuid4())[:8] @@ -2032,7 +2032,7 @@ class AddCategoryModal(discord.ui.Modal): await interaction.response.send_message(f"Catégorie **{self.name.value}** créée!", ephemeral=True) -class EditCategoryModal(discord.ui.Modal): +class EditCategoryModal(disnake.ui.Modal): """Modal pour modifier une catégorie existante""" def __init__(self, bot, storage, config, category: TicketCategory, parent_view): @@ -2043,7 +2043,7 @@ class EditCategoryModal(discord.ui.Modal): self.category = category self.parent_view = parent_view - self.name = discord.ui.TextInput( + self.name = disnake.ui.TextInput( label="Nom", default=category.name, placeholder="Ex: Support", @@ -2051,16 +2051,16 @@ class EditCategoryModal(discord.ui.Modal): ) self.add_item(self.name) - self.description = discord.ui.TextInput( + self.description = disnake.ui.TextInput( label="Description", default=category.description, placeholder="Description...", required=False, - style=discord.TextStyle.paragraph + style=disnake.TextStyle.paragraph ) self.add_item(self.description) - self.emoji = discord.ui.TextInput( + self.emoji = disnake.ui.TextInput( label="Emoji", default=category.emoji, placeholder="🎫", @@ -2069,7 +2069,7 @@ class EditCategoryModal(discord.ui.Modal): ) self.add_item(self.emoji) - async def on_submit(self, interaction: discord.Interaction): + async def on_submit(self, interaction: disnake.Interaction): # Update category object self.category.name = self.name.value self.category.description = self.description.value or "" @@ -2083,7 +2083,7 @@ class EditCategoryModal(discord.ui.Modal): await interaction.followup.send(f"✅ Catégorie **{self.category.name}** mise à jour !", ephemeral=True) -class CategoryManagerView(discord.ui.View): +class CategoryManagerView(disnake.ui.View): """Vue pour lister et gérer les catégories""" def __init__(self, bot, storage, config: GuildTicketConfig): @@ -2099,14 +2099,14 @@ class CategoryManagerView(discord.ui.View): if self.config.categories: options = [] for cat in self.config.categories[:25]: # Limite Select menu - options.append(discord.SelectOption( + options.append(disnake.SelectOption( label=cat.name, value=cat.id, description=cat.description[:100] if cat.description else "Aucune description", emoji=cat.emoji )) - select = discord.ui.Select( + select = disnake.ui.Select( placeholder="Sélectionnez une catégorie à gérer...", options=options, custom_id="manage_cat_select" @@ -2115,8 +2115,8 @@ class CategoryManagerView(discord.ui.View): self.add_item(select) # Bouton Ajouter - add_btn = discord.ui.Button( - style=discord.ButtonStyle.success, + add_btn = disnake.ui.Button( + style=disnake.ButtonStyle.success, label="➕ Ajouter une catégorie", custom_id="manage_cat_add" ) @@ -2124,8 +2124,8 @@ class CategoryManagerView(discord.ui.View): self.add_item(add_btn) # Bouton Retour - back_btn = discord.ui.Button( - style=discord.ButtonStyle.secondary, + back_btn = disnake.ui.Button( + style=disnake.ButtonStyle.secondary, label="⬅️ Retour", custom_id="manage_cat_back" ) @@ -2133,11 +2133,11 @@ class CategoryManagerView(discord.ui.View): self.add_item(back_btn) def _get_embed(self): - embed = discord.Embed( + embed = disnake.Embed( title="📂 Gestion des Catégories", description=f"Il y a actuellement **{len(self.config.categories)}** catégorie(s) configurée(s).\n\n" "Sélectionnez une catégorie dans le menu pour la modifier ou la supprimer.", - color=discord.Color.blue() + color=disnake.Color.blue() ) if not self.config.categories: @@ -2152,7 +2152,7 @@ class CategoryManagerView(discord.ui.View): return embed - async def _category_select_callback(self, interaction: discord.Interaction): + async def _category_select_callback(self, interaction: disnake.Interaction): # En utilisant interaction.data.get('values') pour obtenir la valeur sélectionnée cat_id = interaction.data.get('values', [None])[0] category = self.config.get_category(cat_id) @@ -2164,14 +2164,14 @@ class CategoryManagerView(discord.ui.View): view = CategoryActionView(self.bot, self.storage, self.config, category, self) await interaction.response.edit_message(embed=view._get_embed(), view=view) - async def _add_cat_callback(self, interaction: discord.Interaction): + async def _add_cat_callback(self, interaction: disnake.Interaction): # Utilisation de la classe AddCategoryModal existante class AddCategoryManagerModal(AddCategoryModal): def __init__(self, bot, storage, config, parent_view): super().__init__(bot, storage, config) self.parent_view = parent_view - async def on_submit(self, interaction: discord.Interaction): + async def on_submit(self, interaction: disnake.Interaction): import uuid cat_id = str(uuid.uuid4())[:8] category = TicketCategory( @@ -2195,17 +2195,17 @@ class CategoryManagerView(discord.ui.View): modal = AddCategoryManagerModal(self.bot, self.storage, self.config, self) await interaction.response.send_modal(modal) - async def _back_callback(self, interaction: discord.Interaction): + async def _back_callback(self, interaction: disnake.Interaction): view = ConfigPanelView(self.bot, self.storage, self.config) - embed = discord.Embed( + embed = disnake.Embed( title="Configuration des Tickets", description="Gérez la configuration du système de tickets", - color=discord.Color.blue() + color=disnake.Color.blue() ) await interaction.response.edit_message(embed=embed, view=view) -class CategoryActionView(discord.ui.View): +class CategoryActionView(disnake.ui.View): """Actions pour une catégorie spécifique""" def __init__(self, bot, storage, config, category, parent_view): @@ -2217,7 +2217,7 @@ class CategoryActionView(discord.ui.View): self.parent_view = parent_view def _get_embed(self): - embed = discord.Embed( + embed = disnake.Embed( title=f"Gestion: {self.category.name}", description=f"**ID:** `{self.category.id}`\n" f"**Emoji:** {self.category.emoji}\n" @@ -2225,55 +2225,55 @@ class CategoryActionView(discord.ui.View): f"**Couleur:** `{self.category.color}`\n" f"**Staff Spécifique:** {', '.join([f'<@&{r}>' for r in self.category.staff_roles]) if self.category.staff_roles else 'Global (Tous les staffs)'}\n" f"**Catégorie Discord:** {f'<#{self.category.discord_category_id}>' if self.category.discord_category_id else 'Par défaut'}", - color=discord.Color.blue() + color=disnake.Color.blue() ) return embed - @discord.ui.button(label="📝 Modifier", style=discord.ButtonStyle.primary) - async def edit_callback(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="📝 Modifier", style=disnake.ButtonStyle.primary) + async def edit_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): modal = EditCategoryModal(self.bot, self.storage, self.config, self.category, self) await interaction.response.send_modal(modal) - @discord.ui.button(label="🛡️ Permissions", style=discord.ButtonStyle.success) - async def permissions_callback(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="🛡️ Permissions", style=disnake.ButtonStyle.success) + async def permissions_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): view = CategoryPermissionsView(self.bot, self.storage, self.config, self.category, self) - embed = discord.Embed( + embed = disnake.Embed( title=f"Permissions: {self.category.name}", description="Choisissez les rôles qui auront accès à cette catégorie de tickets.\n\n" "⚠️ **Si vous sélectionnez des rôles ici, seuls ces rôles (et les admins) auront accès aux tickets de cette catégorie.** " "Les rôles staff globaux n'y auront plus accès.", - color=discord.Color.green() + color=disnake.Color.green() ) await interaction.response.edit_message(embed=embed, view=view) - @discord.ui.button(label="📁 Catégorie Discord", style=discord.ButtonStyle.secondary) - async def discord_category_callback(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="📁 Catégorie Discord", style=disnake.ButtonStyle.secondary) + async def discord_category_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): view = CategoryDiscordCategoryView(self.bot, self.storage, self.config, self.category, self) - embed = discord.Embed( + embed = disnake.Embed( title=f"Catégorie Discord: {self.category.name}", description="Choisissez la catégorie Discord (dossier) dans laquelle les tickets de ce type seront créés.\n\n" "Si aucune n'est sélectionnée, la catégorie par défaut du serveur sera utilisée.", - color=discord.Color.blue() + color=disnake.Color.blue() ) await interaction.response.edit_message(embed=embed, view=view) - @discord.ui.button(label="🗑️ Supprimer", style=discord.ButtonStyle.danger) - async def delete_callback(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="🗑️ Supprimer", style=disnake.ButtonStyle.danger) + async def delete_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): view = DeleteCategoryConfirmationView(self.bot, self.storage, self.config, self.category, self.parent_view) - embed = discord.Embed( + embed = disnake.Embed( title="⚠️ Confirmer la suppression", description=f"Êtes-vous sûr de vouloir supprimer la catégorie **{self.category.name}** ?\n" "Cette action est irréversible.", - color=discord.Color.red() + color=disnake.Color.red() ) await interaction.response.edit_message(embed=embed, view=view) - @discord.ui.button(label="⬅️ Retour", style=discord.ButtonStyle.secondary) - async def back_callback(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="⬅️ Retour", style=disnake.ButtonStyle.secondary) + async def back_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): self.parent_view._add_category_select() await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view) -class CategoryPermissionsView(discord.ui.View): +class CategoryPermissionsView(disnake.ui.View): """Vue pour configurer les rôles staff d'une catégorie""" def __init__(self, bot, storage, config, category, parent_view): @@ -2284,8 +2284,8 @@ class CategoryPermissionsView(discord.ui.View): self.category = category self.parent_view = parent_view - @discord.ui.select(cls=discord.ui.RoleSelect, placeholder="Sélectionner les rôles staff (Gestion)", min_values=0, max_values=10) - async def role_select_callback(self, interaction: discord.Interaction, select: discord.ui.RoleSelect): + @disnake.ui.select(cls=disnake.ui.RoleSelect, placeholder="Sélectionner les rôles staff (Gestion)", min_values=0, max_values=10) + async def role_select_callback(self, interaction: disnake.Interaction, select: disnake.ui.RoleSelect): role_ids = [str(role.id) for role in select.values] self.category.staff_roles = role_ids self.storage.save_config(interaction.guild_id, self.config) @@ -2293,11 +2293,11 @@ class CategoryPermissionsView(discord.ui.View): # Refresh embed await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view) - @discord.ui.button(label="⬅️ Annuler", style=discord.ButtonStyle.secondary) - async def back_callback(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="⬅️ Annuler", style=disnake.ButtonStyle.secondary) + async def back_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view) -class CategoryDiscordCategoryView(discord.ui.View): +class CategoryDiscordCategoryView(disnake.ui.View): """Vue pour configurer la catégorie Discord d'une catégorie de ticket""" def __init__(self, bot, storage, config, category, parent_view): @@ -2308,24 +2308,24 @@ class CategoryDiscordCategoryView(discord.ui.View): self.category = category self.parent_view = parent_view - @discord.ui.select(cls=discord.ui.ChannelSelect, channel_types=[discord.ChannelType.category], placeholder="Sélectionner la catégorie Discord") - async def select_callback(self, interaction: discord.Interaction, select: discord.ui.ChannelSelect): + @disnake.ui.select(cls=disnake.ui.ChannelSelect, channel_types=[disnake.ChannelType.category], placeholder="Sélectionner la catégorie Discord") + async def select_callback(self, interaction: disnake.Interaction, select: disnake.ui.ChannelSelect): self.category.discord_category_id = select.values[0].id self.storage.save_config(interaction.guild_id, self.config) await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view) - @discord.ui.button(label="❌ Réinitialiser", style=discord.ButtonStyle.danger) - async def reset_callback(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="❌ Réinitialiser", style=disnake.ButtonStyle.danger) + async def reset_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): self.category.discord_category_id = None self.storage.save_config(interaction.guild_id, self.config) await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view) - @discord.ui.button(label="⬅️ Retour", style=discord.ButtonStyle.secondary) - async def back_callback(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="⬅️ Retour", style=disnake.ButtonStyle.secondary) + async def back_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view) -class DeleteCategoryConfirmationView(discord.ui.View): +class DeleteCategoryConfirmationView(disnake.ui.View): def __init__(self, bot, storage, config, category, parent_view): super().__init__(timeout=None) self.bot = bot @@ -2334,8 +2334,8 @@ class DeleteCategoryConfirmationView(discord.ui.View): self.category = category self.parent_view = parent_view - @discord.ui.button(label="✅ Confirmer", style=discord.ButtonStyle.danger) - async def confirm_callback(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="✅ Confirmer", style=disnake.ButtonStyle.danger) + async def confirm_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): if self.category in self.config.categories: self.config.categories.remove(self.category) self.storage.save_config(interaction.guild_id, self.config) @@ -2347,13 +2347,13 @@ class DeleteCategoryConfirmationView(discord.ui.View): ) await interaction.followup.send(f"✅ Catégorie **{self.category.name}** supprimée.", ephemeral=True) - @discord.ui.button(label="❌ Annuler", style=discord.ButtonStyle.secondary) - async def cancel_callback(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="❌ Annuler", style=disnake.ButtonStyle.secondary) + async def cancel_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): view = CategoryActionView(self.bot, self.storage, self.config, self.category, self.parent_view) await interaction.response.edit_message(embed=view._get_embed(), view=view) -class SetChannelModal(discord.ui.Modal): +class SetChannelModal(disnake.ui.Modal): """Modal pour définir le canal de logs""" def __init__(self, bot, storage, config: GuildTicketConfig, channel_type: str = "log"): @@ -2363,7 +2363,7 @@ class SetChannelModal(discord.ui.Modal): self.config = config self.channel_type = channel_type - self.channel = discord.ui.TextInput( + self.channel = disnake.ui.TextInput( label="Canal de logs", placeholder="Mentionnez le canal ou entrez l'ID (ex: #general ou 123456789)", required=True, @@ -2371,7 +2371,7 @@ class SetChannelModal(discord.ui.Modal): ) self.add_item(self.channel) - async def on_submit(self, interaction: discord.Interaction): + async def on_submit(self, interaction: disnake.Interaction): channel_input = self.channel.value.strip() # Try to extract channel ID from mention or ID @@ -2410,15 +2410,15 @@ class SetChannelModal(discord.ui.Modal): self.config.log_channel_id = channel_id self.storage.save_config(interaction.guild_id, self.config) - embed = discord.Embed( + embed = disnake.Embed( title="✅ Canal de Logs Mis à Jour", description=f"Le canal de logs a été défini sur {channel.mention}.", - color=discord.Color.green() + color=disnake.Color.green() ) await interaction.response.send_message(embed=embed, ephemeral=True) -class StaffRolesModal(discord.ui.Modal): +class StaffRolesModal(disnake.ui.Modal): """Modal pour ajouter/supprimer les rôles staff""" def __init__(self, bot, storage, config: GuildTicketConfig): @@ -2428,7 +2428,7 @@ class StaffRolesModal(discord.ui.Modal): self.config = config # Champ pour entrer les rôles (mention ou ID) - self.roles = discord.ui.TextInput( + self.roles = disnake.ui.TextInput( label="Rôles Staff (mention ou ID)", placeholder="@Staff @Modérateurs ou 123456789 987654321", required=True, @@ -2436,7 +2436,7 @@ class StaffRolesModal(discord.ui.Modal): ) self.add_item(self.roles) - async def on_submit(self, interaction: discord.Interaction): + async def on_submit(self, interaction: disnake.Interaction): try: role_input = self.roles.value.strip() role_ids = [] @@ -2475,10 +2475,10 @@ class StaffRolesModal(discord.ui.Modal): self.storage.save_config(interaction.guild_id, self.config) # Build success embed - embed = discord.Embed( + embed = disnake.Embed( title="✅ Rôles Staff Mis à Jour", description=f"**{len(valid_roles)}** rôle(s) configuré(s) comme staff.", - color=discord.Color.green() + color=disnake.Color.green() ) role_mentions = [role.mention for role in valid_roles] @@ -2498,15 +2498,15 @@ class StaffRolesModal(discord.ui.Modal): except Exception as e: await interaction.response.send_message(f"Erreur lors de la configuration des rôles: {e}", ephemeral=True) - async def on_error(self, interaction: discord.Interaction, error: Exception): + async def on_error(self, interaction: disnake.Interaction, error: Exception): """Handle errors in the modal""" await interaction.response.send_message("Une erreur inattendue s'est produite lors de la configuration des rôles.", ephemeral=True) -class PermissionConfigView(discord.ui.View): +class PermissionConfigView(disnake.ui.View): """View to configure permissions (Staff/Config Roles & Users)""" - def __init__(self, bot, storage, config: GuildTicketConfig, guild: discord.Guild, config_type: str = "staff"): + def __init__(self, bot, storage, config: GuildTicketConfig, guild: disnake.Guild, config_type: str = "staff"): super().__init__(timeout=None) self.bot = bot self.storage = storage @@ -2520,7 +2520,7 @@ class PermissionConfigView(discord.ui.View): self._add_remove_select() def _add_role_select(self): - select = discord.ui.RoleSelect( + select = disnake.ui.RoleSelect( placeholder=f"Sélectionnez les rôles {self.config_type}", min_values=0, max_values=25, @@ -2530,7 +2530,7 @@ class PermissionConfigView(discord.ui.View): self.add_item(select) def _add_user_select(self): - select = discord.ui.UserSelect( + select = disnake.ui.UserSelect( placeholder=f"Sélectionnez les utilisateurs {self.config_type}", min_values=0, max_values=25, @@ -2539,7 +2539,7 @@ class PermissionConfigView(discord.ui.View): select.callback = self._user_callback self.add_item(select) - async def _role_callback(self, interaction: discord.Interaction): + async def _role_callback(self, interaction: disnake.Interaction): try: # Determine which list to update if self.config_type == "staff": @@ -2568,7 +2568,7 @@ class PermissionConfigView(discord.ui.View): traceback.print_exc() await interaction.response.send_message("Une erreur est survenue lors de la mise à jour des rôles.", ephemeral=True) - async def _user_callback(self, interaction: discord.Interaction): + async def _user_callback(self, interaction: disnake.Interaction): try: if self.config_type == "staff": target_list = self.config.staff_users @@ -2605,17 +2605,17 @@ class PermissionConfigView(discord.ui.View): if self.config_type == "staff": title = "Permissions Staff" description = "Configurez qui peut gérer les tickets (répondre, fermer, etc.)" - color = discord.Color.blue() + color = disnake.Color.blue() target_roles = self.config.staff_roles target_users = self.config.staff_users else: title = "Permissions Configuration" description = "Configurez qui peut modifier les paramètres du bot (/ticketconfig)" - color = discord.Color.orange() + color = disnake.Color.orange() target_roles = self.config.config_roles target_users = self.config.config_users - embed = discord.Embed(title=title, description=description, color=color) + embed = disnake.Embed(title=title, description=description, color=color) # Add fields logic (inline reuse) role_mentions = [] @@ -2656,7 +2656,7 @@ class PermissionConfigView(discord.ui.View): if count >= 25: break role = self.guild.get_role(int(r_id)) name = role.name if role else f"Role {r_id}" - options.append(discord.SelectOption( + options.append(disnake.SelectOption( label=f"Role: {name[:90]}", value=f"r_{r_id}", emoji="🛡️", @@ -2668,7 +2668,7 @@ class PermissionConfigView(discord.ui.View): if count >= 25: break user = self.guild.get_member(u_id) name = user.display_name if user else f"User {u_id}" - options.append(discord.SelectOption( + options.append(disnake.SelectOption( label=f"User: {name[:90]}", value=f"u_{u_id}", emoji="👤", @@ -2677,7 +2677,7 @@ class PermissionConfigView(discord.ui.View): count += 1 if options: - select = discord.ui.Select( + select = disnake.ui.Select( placeholder="Sélectionnez pour RETIRER des permissions...", options=options, min_values=1, @@ -2688,7 +2688,7 @@ class PermissionConfigView(discord.ui.View): select.callback = self._remove_callback self.add_item(select) - async def _remove_callback(self, interaction: discord.Interaction): + async def _remove_callback(self, interaction: disnake.Interaction): try: values = interaction.data.get('values', []) @@ -2759,70 +2759,70 @@ class TicketCommands(commands.Cog): # === COMMANDES === - @commands.hybrid_command(name="ticket", aliases=["tickets"]) - async def ticket_main(self, ctx): + @commands.slash_command(name="ticket", description="Ouvre le panel des tickets") + async def ticket_main(self, inter: disnake.ApplicationCommandInteraction): """Ouvre le panel des tickets""" - await ctx.defer(ephemeral=True) + await inter.response.defer(ephemeral=True) - config = self.storage.load_config(ctx.guild.id) + config = self.storage.load_config(inter.guild.id) - embed = discord.Embed( + embed = disnake.Embed( title="Système de Tickets", description="Gérez vos tickets simplement", - color=discord.Color.blurple() + color=disnake.Color.blurple() ) if not config.enabled: embed.add_field(name="État", value="Le système est actuellement **désactivé**.", inline=False) else: embed.add_field(name="Catégories", value=str(len(config.categories)), inline=True) - embed.add_field(name="Total tickets", value=str(len(self.storage.get_guild_tickets(ctx.guild.id))), inline=True) + embed.add_field(name="Total tickets", value=str(len(self.storage.get_guild_tickets(inter.guild.id))), inline=True) view = TicketPanelView(self.bot, config, self.storage) - await ctx.send(embed=embed, view=view, ephemeral=True) + await inter.edit_original_response(embed=embed, view=view) - @commands.hybrid_command(name="ticketpanel", aliases=["panneau"]) + @commands.slash_command(name="ticketpanel", description="Envoie le panel de création de tickets dans un canal") @commands.has_permissions(administrator=True) - async def ticket_panel(self, ctx, channel: discord.TextChannel = None): + async def ticket_panel(self, inter: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel = None): """Envoie le panel de création de tickets dans un canal""" - await ctx.defer(ephemeral=True) + await inter.response.defer(ephemeral=True) - config = self.storage.load_config(ctx.guild.id) + config = self.storage.load_config(inter.guild.id) if not config.enabled: - await ctx.send("Le système est désactivé.", ephemeral=True) + await inter.edit_original_response(content="Le système est désactivé.") return - target = channel or ctx.channel + target = channel or inter.channel - embed = discord.Embed( + embed = disnake.Embed( title="Besoin d'aide?", description="Cliquez sur le bouton ci-dessous pour créer un ticket.\n\nNotre équipe vous répondra dès que possible!", - color=discord.Color.blurple() + color=disnake.Color.blurple() ) embed.add_field(name="Catégories disponibles", value=str(len(config.categories)), inline=False) view = TicketPanelView(self.bot, config, self.storage) await target.send(embed=embed, view=view) - await ctx.send(f"Panel envoyé dans {target.mention}", ephemeral=True) + await inter.edit_original_response(content=f"Panel envoyé dans {target.mention}") - @commands.hybrid_command(name="ticketconfig", aliases=["ticketsetup", "configticket"]) - async def ticket_config(self, ctx): + @commands.slash_command(name="ticketconfig", description="Ouvre le panel de configuration complet du système de tickets") + async def ticket_config(self, inter: disnake.ApplicationCommandInteraction): """Ouvre le panel de configuration complet du système de tickets""" - await ctx.defer(ephemeral=True) + await inter.response.defer(ephemeral=True) # Reload config - config = self.storage.load_config(ctx.guild.id) + config = self.storage.load_config(inter.guild.id) # Vérifier permissions (admin ou config roles/users) - if not can_access_config(ctx, config): - await ctx.send("❌ Cette commande est réservée aux administrateurs ou aux rôles configurés.", ephemeral=True) + if not can_access_config(inter, config): + await inter.edit_original_response(content="❌ Cette commande est réservée aux administrateurs ou aux rôles configurés.") return - embed = discord.Embed( + embed = disnake.Embed( title="⚙️ Configuration du Système de Tickets", description="Configurez complètement votre système de tickets depuis cette interface.\n\n" "**Options disponibles:**\n" @@ -2831,7 +2831,7 @@ class TicketCommands(commands.Cog): "• Gérer les catégories\n" "• Définir le canal de logs\n" "• Et bien plus...", - color=discord.Color.blue() + color=disnake.Color.blue() ) embed.add_field( @@ -2853,7 +2853,7 @@ class TicketCommands(commands.Cog): embed.set_footer(text="Utilisez les boutons ci-dessous pour configurer le système") view = ConfigPanelView(self.bot, self.storage, config) - await ctx.send(embed=embed, view=view, ephemeral=True) + await inter.edit_original_response(embed=embed, view=view) async def setup(bot): diff --git a/commandes/ticket/analytics.py b/commandes/ticket/analytics.py index 7f70d2d..2737381 100644 --- a/commandes/ticket/analytics.py +++ b/commandes/ticket/analytics.py @@ -16,9 +16,8 @@ from PIL import Image, ImageDraw, ImageFont # Add parent directory to path for imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) -import discord -from discord.ext import commands -from discord import app_commands +import disnake +from disnake.ext import commands from .data.storage import get_storage from .data.models import TicketData, TicketStatus, GuildTicketConfig @@ -31,7 +30,7 @@ class TicketAnalytics(commands.Cog): self.bot = bot self.storage = get_storage() - def _is_staff(self, interaction: discord.Interaction) -> bool: + def _is_staff(self, interaction: disnake.Interaction) -> bool: """Check if user is staff""" guild = interaction.guild user = interaction.user @@ -363,9 +362,8 @@ class TicketAnalytics(commands.Cog): buf.seek(0) return buf - @app_commands.command(name="ticket_analytics", description="Affiche les statistiques détaillées des tickets (Staff uniquement)") - @app_commands.describe(period="Période d'analyse (7d, 30d, 90d, all)") - async def ticket_analytics(self, interaction: discord.Interaction, period: str = "30d"): + @commands.slash_command(name="ticket_analytics", description="Affiche les statistiques détaillées des tickets (Staff uniquement)") + async def ticket_analytics(self, interaction: disnake.Interaction, period: str = commands.Param(default="30d", choices=["7d", "30d", "90d", "all"], description="Période d'analyse")): """Display comprehensive ticket analytics for staff""" await interaction.response.defer(ephemeral=True) @@ -405,10 +403,10 @@ class TicketAnalytics(commands.Cog): t.created_at >= cutoff_date for t in filtered_tickets if hasattr(t, 'response_time') and t.response_time == rt)] # Create main embed - embed = discord.Embed( + embed = disnake.Embed( title="📊 Analytics des Tickets", description=f"Statistiques pour la période: **{period}**", - color=discord.Color.blue(), + color=disnake.Color.blue(), timestamp=datetime.now() ) @@ -444,7 +442,7 @@ class TicketAnalytics(commands.Cog): rating_chart = self._create_rating_chart(stats['ratings']) await interaction.followup.send( "📊 **Distribution des Notes Clients**", - file=discord.File(rating_chart, 'rating_distribution.png'), + file=disnake.File(rating_chart, 'rating_distribution.png'), ephemeral=True ) @@ -453,7 +451,7 @@ class TicketAnalytics(commands.Cog): response_chart = self._create_response_time_chart(stats['response_times']) await interaction.followup.send( "⏱️ **Temps de Réponse**", - file=discord.File(response_chart, 'response_times.png'), + file=disnake.File(response_chart, 'response_times.png'), ephemeral=True ) @@ -462,7 +460,7 @@ class TicketAnalytics(commands.Cog): trend_chart = self._create_trend_chart(stats['tickets_by_day']) await interaction.followup.send( "📈 **Évolution des Tickets**", - file=discord.File(trend_chart, 'ticket_trends.png'), + file=disnake.File(trend_chart, 'ticket_trends.png'), ephemeral=True ) @@ -471,10 +469,10 @@ class TicketAnalytics(commands.Cog): # Send feedback summary if available if stats['feedbacks']: - feedback_embed = discord.Embed( + feedback_embed = disnake.Embed( title="💬 Commentaires Clients", description=f"Derniers commentaires ({min(5, len(stats['feedbacks']))} affichés)", - color=discord.Color.green() + color=disnake.Color.green() ) # Sort by rating (lowest first to show areas for improvement) diff --git a/commandes/ticket/feedback_view.py b/commandes/ticket/feedback_view.py index 03bcc54..fa7ce7d 100644 --- a/commandes/ticket/feedback_view.py +++ b/commandes/ticket/feedback_view.py @@ -1,7 +1,7 @@ -import discord +import disnake from .data.models import TicketData -class FeedbackView(discord.ui.View): +class FeedbackView(disnake.ui.View): def __init__(self, bot, storage, ticket: TicketData, staff_rating_only: bool = False): super().__init__(timeout=86400) # 24h timeout self.bot = bot @@ -14,12 +14,12 @@ class FeedbackView(discord.ui.View): # Add Select Menu for Rating self.add_item(RatingSelect()) - @discord.ui.button(label="📝 Laisser un commentaire", style=discord.ButtonStyle.secondary, custom_id="feedback_comment", row=1) - async def comment_button(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="📝 Laisser un commentaire", style=disnake.ButtonStyle.secondary, custom_id="feedback_comment", row=1) + async def comment_button(self, interaction: disnake.Interaction, button: disnake.ui.Button): await interaction.response.send_modal(FeedbackModal(self)) - @discord.ui.button(label="✅ Envoyer", style=discord.ButtonStyle.green, custom_id="feedback_submit", row=1) - async def submit_button(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="✅ Envoyer", style=disnake.ButtonStyle.green, custom_id="feedback_submit", row=1) + async def submit_button(self, interaction: disnake.Interaction, button: disnake.ui.Button): if self.rating == 0: await interaction.response.send_message("❌ Veuillez sélectionner une note avant d'envoyer.", ephemeral=True) return @@ -66,11 +66,11 @@ class FeedbackView(discord.ui.View): if staff_user: # Create feedback embed - embed = discord.Embed( + embed = disnake.Embed( title="📝 Nouveau Commentaire Reçu", description=f"Un utilisateur a laissé un avis sur le ticket #{self.ticket.channel_id}", - color=discord.Color.gold() if self.rating < 3 else discord.Color.green(), - timestamp=discord.utils.utcnow() + color=disnake.Color.gold() if self.rating < 3 else disnake.Color.green(), + timestamp=disnake.utils.utcnow() ) embed.add_field( @@ -99,7 +99,7 @@ class FeedbackView(discord.ui.View): try: await staff_user.send(embed=embed) print(f"DEBUG: DM sent successfully to {staff_user}", flush=True) - except discord.Forbidden: + except disnake.Forbidden: print(f"DEBUG: DM failed - Forbidden (DMs closed)", flush=True) except Exception as e: print(f"DEBUG: Error sending feedback DM: {e}", flush=True) @@ -111,34 +111,34 @@ class FeedbackView(discord.ui.View): except Exception as e: print(f"Error in feedback notification: {e}", flush=True) -class RatingSelect(discord.ui.Select): +class RatingSelect(disnake.ui.Select): def __init__(self): options = [ - discord.SelectOption(label="⭐⭐⭐⭐⭐ (Excellent)", value="5", description="Service parfait !"), - discord.SelectOption(label="⭐⭐⭐⭐ (Très bien)", value="4", description="Très bon service"), - discord.SelectOption(label="⭐⭐⭐ (Bien)", value="3", description="Correct"), - discord.SelectOption(label="⭐⭐ (Moyen)", value="2", description="Peut mieux faire"), - discord.SelectOption(label="⭐ (Mauvais)", value="1", description="Insatisfaisant") + disnake.SelectOption(label="⭐⭐⭐⭐⭐ (Excellent)", value="5", description="Service parfait !"), + disnake.SelectOption(label="⭐⭐⭐⭐ (Très bien)", value="4", description="Très bon service"), + disnake.SelectOption(label="⭐⭐⭐ (Bien)", value="3", description="Correct"), + disnake.SelectOption(label="⭐⭐ (Moyen)", value="2", description="Peut mieux faire"), + disnake.SelectOption(label="⭐ (Mauvais)", value="1", description="Insatisfaisant") ] super().__init__(placeholder="Notez le service...", min_values=1, max_values=1, options=options, row=0) - async def callback(self, interaction: discord.Interaction): + async def callback(self, interaction: disnake.Interaction): self.view.rating = int(self.values[0]) await interaction.response.defer() -class FeedbackModal(discord.ui.Modal): +class FeedbackModal(disnake.ui.Modal): def __init__(self, view): super().__init__(title="Votre avis") self.view = view - self.comment = discord.ui.TextInput( + self.comment = disnake.ui.TextInput( label="Commentaire", - style=discord.TextStyle.paragraph, + style=disnake.TextStyle.paragraph, placeholder="Dites-nous ce que vous avez pensé du support...", required=True, max_length=1000 ) self.add_item(self.comment) - async def on_submit(self, interaction: discord.Interaction): + async def on_submit(self, interaction: disnake.Interaction): self.view.comment = self.comment.value await interaction.response.send_message("Commentaire enregistré ! N'oubliez pas de valider avec 'Envoyer'.", ephemeral=True) diff --git a/commandes/ticket/staff_analytics.py b/commandes/ticket/staff_analytics.py index 2e8925b..0ed1902 100644 --- a/commandes/ticket/staff_analytics.py +++ b/commandes/ticket/staff_analytics.py @@ -8,7 +8,7 @@ from datetime import datetime, timedelta from collections import defaultdict from typing import List, Dict, Any, Optional -import discord +import disnake import matplotlib.pyplot as plt import plotly.graph_objects as go import plotly.express as px @@ -254,7 +254,7 @@ class StaffAnalytics: return buf -class StaffStatsView(discord.ui.View): +class StaffStatsView(disnake.ui.View): """View for selecting individual staff member stats""" def __init__(self, bot, staff_list: List[Dict[str, Any]], analytics: StaffAnalytics): @@ -272,7 +272,7 @@ class StaffStatsView(discord.ui.View): description = f"Note: {staff['average_rating']:.1f}⭐" if staff['total_ratings'] > 0 else "Aucune évaluation" options.append( - discord.SelectOption( + disnake.SelectOption( label=label, value=str(staff['user_id']), description=description[:50] @@ -280,14 +280,14 @@ class StaffStatsView(discord.ui.View): ) if options: - select = discord.ui.Select( + select = disnake.ui.Select( placeholder="Sélectionnez un membre du staff...", options=options ) select.callback = self._staff_selected self.add_item(select) - async def _staff_selected(self, interaction: discord.Interaction): + async def _staff_selected(self, interaction: disnake.Interaction): """Handle staff selection""" staff_id = int(interaction.data["values"][0]) staff_data = next((s for s in self.staff_list if s['user_id'] == staff_id), None) @@ -301,10 +301,10 @@ class StaffStatsView(discord.ui.View): # Create individual chart chart = self.analytics.create_individual_staff_chart(staff_data) - embed = discord.Embed( + embed = disnake.Embed( title=f"📊 Statistiques de {staff_data['name']}", description="Performances détaillées du membre du staff", - color=discord.Color.blue(), + color=disnake.Color.blue(), timestamp=datetime.now() ) @@ -331,4 +331,4 @@ class StaffStatsView(discord.ui.View): inline=True ) - await interaction.followup.send(embed=embed, file=discord.File(chart, 'staff_stats.png'), ephemeral=True) + await interaction.followup.send(embed=embed, file=disnake.File(chart, 'staff_stats.png'), ephemeral=True) diff --git a/commandes/ticket/utils/formatters.py b/commandes/ticket/utils/formatters.py index b09dd12..e8ede79 100644 --- a/commandes/ticket/utils/formatters.py +++ b/commandes/ticket/utils/formatters.py @@ -5,7 +5,7 @@ Beautiful embed and message formatters for the ticket system. """ from datetime import datetime from typing import Optional, List, Dict, Any -from discord import Embed, Color, User, Member, Role +from disnake import Embed, Color, User, Member, Role from ..data.models import ( TicketData, diff --git a/commandes/ticket/utils/permissions.py b/commandes/ticket/utils/permissions.py index 6856f93..579b08e 100644 --- a/commandes/ticket/utils/permissions.py +++ b/commandes/ticket/utils/permissions.py @@ -3,13 +3,13 @@ Ticket Permissions Utility ========================== Centralized permission logic for the ticket system. """ -import discord +import disnake from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from ..data.models import GuildTicketConfig, TicketCategory -def is_staff(interaction: discord.Interaction, config: 'GuildTicketConfig', category: Optional['TicketCategory'] = None) -> bool: +def is_staff(interaction: disnake.Interaction, config: 'GuildTicketConfig', category: Optional['TicketCategory'] = None) -> bool: """ Check if the user has staff permissions. @@ -51,7 +51,7 @@ def is_staff(interaction: discord.Interaction, config: 'GuildTicketConfig', cate return False -def can_access_config(interaction: discord.Interaction, config: 'GuildTicketConfig') -> bool: +def can_access_config(interaction: disnake.Interaction, config: 'GuildTicketConfig') -> bool: """ Check if the user can access the configuration. @@ -87,7 +87,7 @@ def can_access_config(interaction: discord.Interaction, config: 'GuildTicketConf return False -def is_whitelisted(interaction: discord.Interaction, config: 'GuildTicketConfig', bot) -> bool: +def is_whitelisted(interaction: disnake.Interaction, config: 'GuildTicketConfig', bot) -> bool: """ Check if user is whitelisted or has staff/config permissions. """ diff --git a/commandes/ticket/utils/transcript.py b/commandes/ticket/utils/transcript.py index 7c88e40..6ac5860 100644 --- a/commandes/ticket/utils/transcript.py +++ b/commandes/ticket/utils/transcript.py @@ -7,7 +7,7 @@ import json import os from datetime import datetime from typing import Optional, List, Dict, Any -from discord import User, Member +from disnake import User, Member from ..data.models import TicketData, TicketMessage diff --git a/commandes/ticket_whitelist.py b/commandes/ticket_whitelist.py index 4cb37ff..7f9589f 100644 --- a/commandes/ticket_whitelist.py +++ b/commandes/ticket_whitelist.py @@ -1,7 +1,7 @@ -import discord +import disnake import re -from discord.ext import commands -from discord import app_commands, ui +from disnake.ext import commands +from disnake import ui import json import os import asyncio @@ -63,12 +63,12 @@ def save_settings(guild_id: int, settings: dict) -> None: with open(TICKET_WHITELIST_SETTINGS_FILE, "w", encoding='utf-8') as f: json.dump(data, f, indent=4, ensure_ascii=False) -async def get_ticket_user_id(channel: discord.TextChannel) -> Optional[int]: +async def get_ticket_user_id(channel: disnake.TextChannel) -> Optional[int]: """Récupère l'ID du propriétaire du ticket de manière robuste (gère le lag de cache)""" import re topic = getattr(channel, "topic", None) - # Tentative d'utilisation du cache d.py (plus rapide) + # Tentative d'utilisation du cache (plus rapide) if topic: match = re.search(r"(\d{17,20})", topic) if match: @@ -88,12 +88,12 @@ async def get_ticket_user_id(channel: discord.TextChannel) -> Optional[int]: return None -class TicketWhitelistConfigView(ui.View): +class TicketWhitelistConfigView(disnake.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): + async def prompt_for_id(self, interaction: disnake.Interaction, prompt_text: str, is_category: bool = False): await interaction.response.send_message(prompt_text, ephemeral=True) def check(m): @@ -104,7 +104,7 @@ class TicketWhitelistConfigView(ui.View): target = None if is_category: if msg.content.isdigit(): - target = discord.utils.get(interaction.guild.categories, id=int(msg.content)) + target = disnake.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 @@ -129,8 +129,8 @@ class TicketWhitelistConfigView(ui.View): 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): + @ui.button(label="Catégorie Tickets", style=disnake.ButtonStyle.primary, row=0, emoji="📁") + async def set_category(self, interaction: disnake.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) @@ -138,8 +138,8 @@ class TicketWhitelistConfigView(ui.View): 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): + @ui.button(label="Rôle Staff", style=disnake.ButtonStyle.primary, row=0, emoji="🛡️") + async def set_staff_role(self, interaction: disnake.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) @@ -147,8 +147,8 @@ class TicketWhitelistConfigView(ui.View): 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): + @ui.button(label="Rôle Équipe Staff", style=disnake.ButtonStyle.primary, row=0, emoji="👥") + async def set_team_staff_role(self, interaction: disnake.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) @@ -156,8 +156,8 @@ class TicketWhitelistConfigView(ui.View): 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): + @ui.button(label="Rôle Attente Oral", style=disnake.ButtonStyle.secondary, row=1, emoji="🎤") + async def set_attente_oral_role(self, interaction: disnake.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) @@ -165,8 +165,8 @@ class TicketWhitelistConfigView(ui.View): 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): + @ui.button(label="Rôle Candidature à faire", style=disnake.ButtonStyle.secondary, row=1, emoji="📝") + async def set_candidature_a_faire_role(self, interaction: disnake.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) @@ -174,8 +174,8 @@ class TicketWhitelistConfigView(ui.View): 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): + @ui.button(label="Rôle Citoyen", style=disnake.ButtonStyle.success, row=1, emoji="🏙️") + async def set_citoyen_role(self, interaction: disnake.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) @@ -184,7 +184,7 @@ class TicketWhitelistConfigView(ui.View): await interaction.followup.send(f"✅ Rôle Citoyen configuré sur **{role.name}**", ephemeral=True) -class ContinueFormView(discord.ui.View): +class ContinueFormView(disnake.ui.View): """Vue avec un bouton pour continuer à l'étape suivante""" def __init__(self, current_step, step1_data=None, step2_part1_data=None, step2_part2_data=None): @@ -194,8 +194,8 @@ class ContinueFormView(discord.ui.View): self.step2_part1_data = step2_part1_data self.step2_part2_data = step2_part2_data - @discord.ui.button(label="📋 Continuer le formulaire", style=discord.ButtonStyle.green, custom_id="continue_form") - async def continue_form(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="📋 Continuer le formulaire", style=disnake.ButtonStyle.green, custom_id="continue_form") + async def continue_form(self, interaction: disnake.Interaction, button: disnake.ui.Button): """Passe à l'étape suivante du formulaire""" if self.current_step == 1: await interaction.response.send_modal(TicketStep2Part1(self.step1_data)) @@ -207,79 +207,99 @@ class ContinueFormView(discord.ui.View): # Vérifier si le message existe toujours avant de le modifier try: await interaction.message.edit(view=self) - except discord.errors.NotFound: + except disnake.errors.NotFound: # Si le message n'existe plus, nous ne faisons rien pass -class TicketStep1(discord.ui.Modal, title="📋 Informations Personnelles"): +class TicketStep1(disnake.ui.Modal): """Étape 1 du formulaire - Infos personnelles""" - pseudo = discord.ui.TextInput(label="Pseudo Epic (Fortnite)", required=True, max_length=16) - age = discord.ui.TextInput(label="Âge", required=True, max_length=2) - experience = discord.ui.TextInput(label="Expérience RP ?", style=discord.TextStyle.paragraph, required=True, max_length=50) - decouverte = discord.ui.TextInput(label="Comment as-tu découvert le serveur ?", required=True, max_length=20) - micro = discord.ui.TextInput(label="As-tu un micro de qualité ?", required=True, max_length=3) + def __init__(self): + super().__init__( + title="📋 Informations Personnelles", + custom_id="ticket_step1", + components=[ + disnake.ui.TextInput(label="Pseudo Epic (Fortnite)", custom_id="pseudo", required=True, max_length=16), + disnake.ui.TextInput(label="Âge", custom_id="age", required=True, max_length=2), + disnake.ui.TextInput(label="Expérience RP ?", custom_id="experience", style=disnake.TextInputStyle.paragraph, required=True, max_length=50), + disnake.ui.TextInput(label="Comment as-tu découvert le serveur ?", custom_id="decouverte", required=True, max_length=20), + disnake.ui.TextInput(label="As-tu un micro de qualité ?", custom_id="micro", required=True, max_length=3), + ] + ) - async def on_submit(self, interaction: discord.Interaction): + async def callback(self, interaction: disnake.ModalInteraction): """Envoie un bouton pour passer à l'étape 2""" - view = ContinueFormView(current_step=1, step1_data=self) + view = ContinueFormView(current_step=1, step1_data=interaction.text_values) await interaction.response.send_message("✅ Première étape terminée ! Cliquez ci-dessous pour continuer.", view=view, ephemeral=True) -class TicketStep2Part1(discord.ui.Modal, title="👤 Infos sur ton personnage (Partie 1)"): +class TicketStep2Part1(disnake.ui.Modal): """Étape 2 du formulaire - Infos RP (Partie 1)""" def __init__(self, step1_data): - super().__init__() self.step1_data = step1_data + super().__init__( + title="👤 Infos sur ton personnage (Partie 1)", + custom_id="ticket_step2_part1", + components=[ + disnake.ui.TextInput(label="Prénom", custom_id="prenom", required=True), + disnake.ui.TextInput(label="Nom", custom_id="nom", required=True), + disnake.ui.TextInput(label="Âge du personnage", custom_id="age_perso", required=True), + disnake.ui.TextInput(label="Genre", custom_id="genre", required=True, max_length=5), + disnake.ui.TextInput(label="Date et lieu de naissance", custom_id="naissance", required=True), + ] + ) - prenom = discord.ui.TextInput(label="Prénom", required=True) - nom = discord.ui.TextInput(label="Nom", required=True) - age_perso = discord.ui.TextInput(label="Âge du personnage", required=True) - genre = discord.ui.TextInput(label="Genre", required=True, max_length=5) - naissance = discord.ui.TextInput(label="Date et lieu de naissance", required=True) - - async def on_submit(self, interaction: discord.Interaction): + async def callback(self, interaction: disnake.ModalInteraction): """Envoie un bouton pour passer à l'étape suivante""" - view = ContinueFormView(current_step=2, step1_data=self.step1_data, step2_part1_data=self) + view = ContinueFormView(current_step=2, step1_data=self.step1_data, step2_part1_data=interaction.text_values) await interaction.response.send_message("✅ Deuxième partie terminée ! Cliquez ci-dessous pour continuer.", view=view, ephemeral=True) -class TicketStep2Part2(discord.ui.Modal, title="👤 Infos sur ton personnage (Partie 2)"): +class TicketStep2Part2(disnake.ui.Modal): """Étape 2 du formulaire - Infos RP (Partie 2)""" def __init__(self, step1_data, step2_part1_data): - super().__init__() self.step1_data = step1_data self.step2_part1_data = step2_part1_data + super().__init__( + title="👤 Infos sur ton personnage (Partie 2)", + custom_id="ticket_step2_part2", + components=[ + disnake.ui.TextInput(label="Traits de caractère", custom_id="traits", required=True), + disnake.ui.TextInput(label="Nationalité", custom_id="nationalite", required=True), + disnake.ui.TextInput(label="Skin RP", custom_id="skin_rp", required=True), + disnake.ui.TextInput(label="Métier souhaité", custom_id="metier_souhaite", required=True), + ] + ) - traits = discord.ui.TextInput(label="Traits de caractère", required=True) - nationalite = discord.ui.TextInput(label="Nationalité", required=True) - skin_rp = discord.ui.TextInput(label="Skin RP", required=True) - metier_souhaite = discord.ui.TextInput(label="Métier souhaité", required=True) - - async def on_submit(self, interaction: discord.Interaction): + async def callback(self, interaction: disnake.ModalInteraction): """Envoie un bouton pour passer à l'étape 3""" - view = ContinueFormView(current_step=3, step1_data=self.step1_data, step2_part1_data=self.step2_part1_data, step2_part2_data=self) + view = ContinueFormView(current_step=3, step1_data=self.step1_data, step2_part1_data=self.step2_part1_data, step2_part2_data=interaction.text_values) await interaction.response.send_message("✅ Deuxième étape terminée ! Cliquez ci-dessous pour continuer.", view=view, ephemeral=True) -class TicketStep3(discord.ui.Modal, title="📋 Développement du Personnage"): +class TicketStep3(disnake.ui.Modal): """Étape 3 du formulaire - Histoire du personnage""" def __init__(self, step1_data, step2_part1_data, step2_part2_data): - super().__init__() self.step1_data = step1_data self.step2_part1_data = step2_part1_data self.step2_part2_data = step2_part2_data + super().__init__( + title="📋 Développement du Personnage", + custom_id="ticket_step3", + components=[ + disnake.ui.TextInput( + label="Histoire du personnage", + custom_id="histoire", + style=disnake.TextInputStyle.paragraph, + required=True, + min_length=425, + max_length=500 + ), + disnake.ui.TextInput(label="Objectifs RP (court et long terme)", custom_id="objectifs", style=disnake.TextInputStyle.paragraph, required=True, max_length=100), + ] + ) - histoire = discord.ui.TextInput( - label="Histoire du personnage", - style=discord.TextStyle.paragraph, - required=True, - min_length=425, - max_length=500 # Limite de caractères maximum - ) - objectifs = discord.ui.TextInput(label="Objectifs RP (court et long terme)", style=discord.TextStyle.paragraph, required=True, max_length=100) - - async def on_submit(self, interaction: discord.Interaction): + async def callback(self, interaction: disnake.ModalInteraction): """Création du ticket après validation""" guild = interaction.guild user = interaction.user @@ -292,12 +312,12 @@ class TicketStep3(discord.ui.Modal, title="📋 Développement du Personnage"): 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)) + category = disnake.utils.get(guild.categories, id=int(category_id)) if category is None: return await interaction.response.send_message("❌ Erreur : La catégorie des tickets configurée n'existe plus !", ephemeral=True) - 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)) + staff_role = disnake.utils.get(guild.roles, id=int(staff_role_id)) + team_staff_role = disnake.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) @@ -309,10 +329,10 @@ class TicketStep3(discord.ui.Modal, title="📋 Développement du Personnage"): # 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), - team_staff_role: discord.PermissionOverwrite(read_messages=True, send_messages=False) + guild.default_role: disnake.PermissionOverwrite(read_messages=False), + user: disnake.PermissionOverwrite(read_messages=True, send_messages=True), + staff_role: disnake.PermissionOverwrite(read_messages=True, send_messages=True), + team_staff_role: disnake.PermissionOverwrite(read_messages=True, send_messages=False) } # Créer un salon de ticket avec le nom [pseudo]-écrit @@ -323,28 +343,35 @@ class TicketStep3(discord.ui.Modal, title="📋 Développement du Personnage"): ) # Création de l'embed avec les infos du formulaire - embed = discord.Embed(title="📩 Ticket Whitelist", color=0x00ff00) + embed = disnake.Embed(title="📩 Ticket Whitelist", color=0x00ff00) + + # Helper to get value from data + s1 = self.step1_data + s2p1 = self.step2_part1_data + s2p2 = self.step2_part2_data + s3 = interaction.text_values + embed.add_field(name="📋 - Informations Personnelles", value=( - f"> - Pseudo Epic : {self.step1_data.pseudo.value}\n" - f"> - Âge : {self.step1_data.age.value}\n" - f"> - Expérience RP : {self.step1_data.experience.value}\n" - f"> - Découverte du serveur : {self.step1_data.decouverte.value}\n" - f"> - Micro de qualité : {self.step1_data.micro.value}" + f"> - Pseudo Epic : {s1['pseudo']}\n" + f"> - Âge : {s1['age']}\n" + f"> - Expérience RP : {s1['experience']}\n" + f"> - Découverte du serveur : {s1['decouverte']}\n" + f"> - Micro de qualité : {s1['micro']}" ), inline=False) embed.add_field(name="👤 - Informations RP", value=( - f"> - Prénom : {self.step2_part1_data.prenom.value}\n" - f"> - Nom : {self.step2_part1_data.nom.value}\n" - f"> - Âge du personnage : {self.step2_part1_data.age_perso.value}\n" - f"> - Genre : {self.step2_part1_data.genre.value}\n" - f"> - Naissance : {self.step2_part1_data.naissance.value}\n" - f"> - Traits de caractère : {self.step2_part2_data.traits.value}\n" - f"> - Nationalité : {self.step2_part2_data.nationalite.value}\n" - f"> - Skin RP : {self.step2_part2_data.skin_rp.value}\n" - f"> - Métier souhaité : {self.step2_part2_data.metier_souhaite.value}" + f"> - Prénom : {s2p1['prenom']}\n" + f"> - Nom : {s2p1['nom']}\n" + f"> - Âge du personnage : {s2p1['age_perso']}\n" + f"> - Genre : {s2p1['genre']}\n" + f"> - Naissance : {s2p1['naissance']}\n" + f"> - Traits de caractère : {s2p2['traits']}\n" + f"> - Nationalité : {s2p2['nationalite']}\n" + f"> - Skin RP : {s2p2['skin_rp']}\n" + f"> - Métier souhaité : {s2p2['metier_souhaite']}" ), inline=False) embed.add_field(name="📋 - Développement du Personnage", value=( - f"> - Histoire : {self.histoire.value}\n" - f"> - Objectifs : {self.objectifs.value}" + f"> - Histoire : {s3['histoire']}\n" + f"> - Objectifs : {s3['objectifs']}" ), inline=False) # Envoyer l'embed avec les boutons d'actions pour les staff @@ -355,25 +382,25 @@ class TicketStep3(discord.ui.Modal, title="📋 Développement du Personnage"): # Mettre à jour le pseudo du joueur si le bot a les permissions nécessaires if 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}" + new_nickname = f"{s2p1['prenom']} {s2p1['nom']} | {s1['pseudo']}" if len(new_nickname) > 32: new_nickname = new_nickname[:32] try: await user.edit(nick=new_nickname) - except discord.Forbidden: pass + except disnake.Forbidden: pass # Envoyer un message de confirmation try: await interaction.response.send_message(f"✅ Ticket ouvert : {ticket_channel.mention}", ephemeral=True) - except discord.errors.NotFound: + except disnake.errors.NotFound: await interaction.followup.send(f"✅ Ticket ouvert : {ticket_channel.mention}", ephemeral=True) -class TicketManagementView(discord.ui.View): +class TicketManagementView(disnake.ui.View): """Vue pour gérer le ticket avec les boutons Fermer, Réouvrir, Claim et Supprimer""" def __init__(self): super().__init__(timeout=None) - async def get_ticket_owner(self, interaction: discord.Interaction): + async def get_ticket_owner(self, interaction: disnake.Interaction): """Récupère le propriétaire du ticket via le helper robuste""" owner_id = await get_ticket_user_id(interaction.channel) if owner_id: @@ -386,8 +413,8 @@ class TicketManagementView(discord.ui.View): return None return None - @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): + @disnake.ui.button(label="🔒 Fermer le Ticket", style=disnake.ButtonStyle.red, custom_id="close_ticket") + async def close_ticket(self, interaction: disnake.Interaction, button: disnake.ui.Button): """Ferme le ticket en désactivant l'envoi de messages pour le membre""" await interaction.response.defer(ephemeral=True) @@ -411,12 +438,14 @@ class TicketManagementView(discord.ui.View): new_view = ClosedTicketView() await interaction.channel.send(f"🔒 Ce ticket a été fermé par {interaction.user.mention}.", view=new_view) await interaction.followup.send("✅ Ticket fermé avec succès.", ephemeral=True) + except disnake.errors.RateLimited as e: + await interaction.followup.send(f"⚠️ Limite de débit Discord : Réessayez dans {e.retry_after:.1f}s.", ephemeral=True) except Exception as e: kuby_logger.error(f"Erreur lors de la fermeture du ticket: {e}") await interaction.followup.send(f"❌ Erreur lors de la fermeture : {e}", ephemeral=True) - @discord.ui.button(label="👤 Claim le Ticket", style=discord.ButtonStyle.blurple, custom_id="claim_ticket") - async def claim_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="👤 Claim le Ticket", style=disnake.ButtonStyle.blurple, custom_id="claim_ticket") + async def claim_ticket(self, interaction: disnake.Interaction, button: disnake.ui.Button): """Un staff prend en charge le ticket""" await interaction.response.defer(ephemeral=False) @@ -441,13 +470,13 @@ class TicketManagementView(discord.ui.View): kuby_logger.error(f"Erreur lors du claim du ticket: {e}") await interaction.followup.send(f"❌ Erreur lors du claim : {e}") -class ClosedTicketView(discord.ui.View): +class ClosedTicketView(disnake.ui.View): """Vue pour les tickets fermés avec les boutons Réouvrir et Supprimer""" def __init__(self): super().__init__(timeout=None) - async def get_ticket_owner(self, interaction: discord.Interaction): + async def get_ticket_owner(self, interaction: disnake.Interaction): """Récupère le propriétaire du ticket via le helper robuste""" owner_id = await get_ticket_user_id(interaction.channel) if owner_id: @@ -460,8 +489,8 @@ class ClosedTicketView(discord.ui.View): return None return None - @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): + @disnake.ui.button(label="♻ Réouvrir le Ticket", style=disnake.ButtonStyle.green, custom_id="reopen_ticket") + async def reopen_ticket(self, interaction: disnake.Interaction, button: disnake.ui.Button): """Réouvre le ticket pour le membre""" await interaction.response.defer(ephemeral=True) @@ -478,10 +507,10 @@ class ClosedTicketView(discord.ui.View): try: # On demande au staff quel statut remettre - embed = discord.Embed( + embed = disnake.Embed( title="♻️ Réouverture du Ticket", description=f"Choisissez le statut à appliquer pour **{owner.display_name}**.", - color=discord.Color.blue() + color=disnake.Color.blue() ) view = ReopenStatusView(owner) await interaction.channel.send(embed=embed, view=view) @@ -490,25 +519,25 @@ class ClosedTicketView(discord.ui.View): kuby_logger.error(f"Erreur lors de la préparation de réouverture: {e}") await interaction.followup.send(f"❌ Erreur : {e}", ephemeral=True) -class ReopenStatusView(discord.ui.View): +class ReopenStatusView(disnake.ui.View): """Vue pour choisir le statut lors d'une réouverture""" - def __init__(self, owner: discord.Member): + def __init__(self, owner: disnake.Member): super().__init__(timeout=180) self.owner = owner - @discord.ui.button(label="Écrit (Ouvert)", style=discord.ButtonStyle.secondary) - async def set_written(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="Écrit (Ouvert)", style=disnake.ButtonStyle.secondary) + async def set_written(self, interaction: disnake.Interaction, button: disnake.ui.Button): await self.apply_status(interaction, "ouvert") - @discord.ui.button(label="Oral (À faire)", style=discord.ButtonStyle.primary) - async def set_oral(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="Oral (À faire)", style=disnake.ButtonStyle.primary) + async def set_oral(self, interaction: disnake.Interaction, button: disnake.ui.Button): await self.apply_status(interaction, "oral-à-faire") - @discord.ui.button(label="Accepté", style=discord.ButtonStyle.success) - async def set_accepted(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="Accepté", style=disnake.ButtonStyle.success) + async def set_accepted(self, interaction: disnake.Interaction, button: disnake.ui.Button): await self.apply_status(interaction, "accepté") - async def apply_status(self, interaction: discord.Interaction, prefix: str): + async def apply_status(self, interaction: disnake.Interaction, prefix: str): await interaction.response.defer(ephemeral=True) try: settings = load_settings(interaction.guild.id) @@ -555,8 +584,8 @@ class ReopenStatusView(discord.ui.View): kuby_logger.error(f"Erreur lors de la réouverture/synchro: {e}") await interaction.followup.send(f"❌ Erreur système : {e}", ephemeral=True) - @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): + @disnake.ui.button(label="❌ Supprimer le Ticket", style=disnake.ButtonStyle.danger, custom_id="delete_ticket") + async def delete_ticket(self, interaction: disnake.Interaction, button: disnake.ui.Button): """Supprime le salon du ticket""" await interaction.response.defer(ephemeral=True) @@ -575,14 +604,14 @@ class ReopenStatusView(discord.ui.View): try: await interaction.followup.send(f"❌ Erreur lors de la suppression : {e}", ephemeral=True) except: pass -class TicketView(discord.ui.View): +class TicketView(disnake.ui.View): """Vue avec le bouton pour ouvrir le ticket""" def __init__(self): super().__init__(timeout=None) - @discord.ui.button(label="📑 Ouvrir un Ticket Whitelist", style=discord.ButtonStyle.red, custom_id="open_ticket") - async def open_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="📑 Ouvrir un Ticket Whitelist", style=disnake.ButtonStyle.red, custom_id="open_ticket") + async def open_ticket(self, interaction: disnake.Interaction, button: disnake.ui.Button): """Affiche le premier formulaire""" await interaction.response.send_modal(TicketStep1()) @@ -598,11 +627,11 @@ class Ticket(commands.Cog): self.bot.add_view(TicketManagementView()) self.bot.add_view(ClosedTicketView()) - @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): + @commands.slash_command(name="ticket_whitelist", description="Envoie l'embed pour ouvrir un ticket de whitelist.") + @commands.has_permissions(administrator=True) + async def ticket_whitelist(self, interaction: disnake.ApplicationCommandInteraction): """Envoie l'embed avec le bouton d'ouverture de ticket""" - embed = discord.Embed( + embed = disnake.Embed( title="📩 Ticket Whitelist", description="Pour réaliser votre whitelist, ouvrez un ticket et remplissez le formulaire.", color=0x00ff00 @@ -611,16 +640,16 @@ 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): + @commands.slash_command(name="ticket_whitelist_config", description="Configurer le système de tickets whitelist (Admin)") + @commands.has_permissions(administrator=True) + async def ticket_whitelist_config(self, interaction: disnake.ApplicationCommandInteraction): """Interface de configuration des tickets""" settings = load_settings(interaction.guild.id) - embed = discord.Embed( + embed = disnake.Embed( title="⚙️ Configuration Tickets Whitelist", description="Utilisez les boutons ci-dessous pour lier la catégorie et les rôles.", - color=discord.Color.blue() + color=disnake.Color.blue() ) cat_id = settings.get("ticket_category_id") @@ -673,7 +702,7 @@ class Ticket(commands.Cog): user_id = user_id try: user = await message.guild.fetch_member(user_id) - except (discord.NotFound, discord.HTTPException): + except (disnake.NotFound, disnake.HTTPException): await message.channel.send("❌ Utilisateur introuvable. A-t-il quitté le serveur ?", delete_after=10) return @@ -701,19 +730,19 @@ class Ticket(commands.Cog): asyncio.create_task(message.channel.edit(name=new_name)) # Confirmation dans le salon (Feedback visuel) - embed_val = discord.Embed( + embed_val = disnake.Embed( title="🎉 Whitelist Validée !", description=f"Félicitations {user.mention}, ton entretien oral a été validé par {message.author.mention} !\nTu es désormais **Citoyen**.", - color=discord.Color.green() + color=disnake.Color.green() ) await message.channel.send(embed=embed_val) # DM au joueur (RESTAURATION DESIGN CAPTURE) try: - embed_dm = discord.Embed( + embed_dm = disnake.Embed( title="🎉 Félicitations - Citoyenneté Validée !", description=f"Nous avons le plaisir de t'annoncer que ton passage sur **{message.guild.name}** a été validé avec succès !", - color=discord.Color.green() + color=disnake.Color.green() ) embed_dm.add_field(name="🏙️ Nouveau Statut", value="Citoyen", inline=True) embed_dm.add_field(name="📍 Serveur", value=message.guild.name, inline=True) @@ -740,19 +769,19 @@ class Ticket(commands.Cog): asyncio.create_task(message.channel.edit(name=new_name)) # Confirmation dans le salon - embed_val = discord.Embed( + embed_val = disnake.Embed( title="📝 Étape Écrite Validée !", description=f"Ta candidature écrite a été validée par {message.author.mention}.\nTu passes désormais en **Attente Oral**.", - color=discord.Color.orange() + color=disnake.Color.orange() ) await message.channel.send(embed=embed_val) # DM au joueur (RESTAURATION DESIGN CAPTURE) try: - embed_dm = discord.Embed( + embed_dm = disnake.Embed( title="📝 Validation Étape Écrite", description=f"Ta candidature écrite sur **{message.guild.name}** a été examinée et validée par notre équipe !", - color=discord.Color.orange() + color=disnake.Color.orange() ) embed_dm.add_field(name="🎤 Statut Actuel", value="Attente Oral", inline=True) embed_dm.add_field(name="🛠️ Action Requise", value="Merci de te rendre dans le salon vocal 'Attente Oral' dès que tu es disponible pour passer ton entretien.", inline=False) diff --git a/commandes/website_sync.py b/commandes/website_sync.py index 4fdc55e..bb189dd 100644 --- a/commandes/website_sync.py +++ b/commandes/website_sync.py @@ -25,9 +25,8 @@ from datetime import datetime, timezone, timedelta from pathlib import Path from typing import Optional -import discord -from discord import app_commands -from discord.ext import commands, tasks +import disnake +from disnake.ext import commands, tasks from src.json_export_service import ( load_config, save_config, generate_all, @@ -81,8 +80,8 @@ class WebsiteSync(commands.Cog, name="Website Sync"): # ── Helper : embed de réponse ───────────────────────────────────────────── @staticmethod - def _embed(title: str, color: discord.Color, description: str = "") -> discord.Embed: - e = discord.Embed(title=title, color=color, timestamp=datetime.now(timezone.utc)) + def _embed(title: str, color: disnake.Color, description: str = "") -> disnake.Embed: + e = disnake.Embed(title=title, color=color, timestamp=datetime.now(timezone.utc)) if description: e.description = description return e @@ -90,31 +89,30 @@ class WebsiteSync(commands.Cog, name="Website Sync"): # ══════════════════════════════════════════════════════════════════════════ # /json-generate # ══════════════════════════════════════════════════════════════════════════ - @app_commands.command(name="json-generate", description="Génère manuellement les fichiers JSON du site web") - @app_commands.describe(cible="Quoi générer ? (défaut : tout)") - @app_commands.choices(cible=[ - app_commands.Choice(name="Tout (servers + members)", value="all"), - app_commands.Choice(name="Serveurs uniquement", value="servers"), - app_commands.Choice(name="Membres uniquement", value="members"), - ]) - @app_commands.default_permissions(administrator=True) - async def json_generate(self, interaction: discord.Interaction, cible: str = "all"): + @commands.slash_command(name="json-generate", description="Génère manuellement les fichiers JSON du site web") + @commands.has_permissions(administrator=True) + async def json_generate(self, interaction: disnake.ApplicationCommandInteraction, + cible: str = commands.Param("all", description="Quoi générer ?", choices={ + "Tout (servers + members)": "all", + "Serveurs uniquement": "servers", + "Membres uniquement": "members" + })): await interaction.response.defer(ephemeral=True) start = datetime.now() - embed = self._embed("⏳ Génération en cours...", discord.Color.yellow()) - await interaction.followup.send(embed=embed, ephemeral=True) + embed = self._embed("⏳ Génération en cours...", disnake.Color.yellow()) + await interaction.followup.send(embed=embed) try: if cible == "servers": res = await generate_servers_json(self.bot) - embed = self._embed("✅ servers.json généré", discord.Color.green()) + embed = self._embed("✅ servers.json généré", disnake.Color.green()) embed.add_field(name="Serveurs", value=str(res["count"]), inline=True) embed.add_field(name="Durée", value=f"{(datetime.now()-start).total_seconds():.1f}s", inline=True) embed.add_field(name="Fichier", value=f"`{SERVERS_FILE}`", inline=False) elif cible == "members": res = await generate_members_json(self.bot) - embed = self._embed("✅ members.json généré", discord.Color.green()) + embed = self._embed("✅ members.json généré", disnake.Color.green()) embed.add_field(name="Membres", value=str(res["count"]), inline=True) embed.add_field(name="Serveur", value=res["guildName"], inline=True) embed.add_field(name="Durée", value=f"{(datetime.now()-start).total_seconds():.1f}s", inline=True) @@ -125,7 +123,7 @@ class WebsiteSync(commands.Cog, name="Website Sync"): has_errors = bool(res["errors"]) embed = self._embed( "⚠️ Génération partielle" if has_errors else "✅ Génération complète", - discord.Color.orange() if has_errors else discord.Color.green(), + disnake.Color.orange() if has_errors else disnake.Color.green(), ) if res["servers"]: embed.add_field(name="📋 Serveurs", value=f"{res['servers']['count']} serveurs", inline=True) @@ -137,19 +135,19 @@ class WebsiteSync(commands.Cog, name="Website Sync"): embed.add_field(name="📁 Dossier", value=f"`{OUTPUT_DIR}`", inline=False) except Exception as e: - embed = self._embed("❌ Erreur", discord.Color.red(), f"```{e}```") + embed = self._embed("❌ Erreur", disnake.Color.red(), f"```{e}```") await interaction.edit_original_response(embed=embed) # ══════════════════════════════════════════════════════════════════════════ # /json-status # ══════════════════════════════════════════════════════════════════════════ - @app_commands.command(name="json-status", description="État des fichiers JSON et prochaine génération auto") - @app_commands.default_permissions(administrator=True) - async def json_status(self, interaction: discord.Interaction): + @commands.slash_command(name="json-status", description="État des fichiers JSON et prochaine génération auto") + @commands.has_permissions(administrator=True) + async def json_status(self, interaction: disnake.ApplicationCommandInteraction): await interaction.response.defer(ephemeral=True) config = load_config() - embed = self._embed("📊 Statut JSON Export", discord.Color.blurple()) + embed = self._embed("📊 Statut JSON Export", disnake.Color.blurple()) # servers.json if SERVERS_FILE.exists(): @@ -203,15 +201,14 @@ class WebsiteSync(commands.Cog, name="Website Sync"): # ══════════════════════════════════════════════════════════════════════════ # /json-config (groupe) # ══════════════════════════════════════════════════════════════════════════ - json_config_group = app_commands.Group( - name="json-config", - description="Configure la génération automatique des JSON", - default_permissions=discord.Permissions(administrator=True), - ) + @commands.slash_command(name="json-config", description="Configure la génération automatique des JSON") + @commands.has_permissions(administrator=True) + async def json_config_group(self, interaction: disnake.ApplicationCommandInteraction): + pass # ── show ────────────────────────────────────────────────────────────────── - @json_config_group.command(name="show", description="Affiche la configuration actuelle") - async def config_show(self, interaction: discord.Interaction): + @json_config_group.sub_command(name="show", description="Affiche la configuration actuelle") + async def config_show(self, interaction: disnake.ApplicationCommandInteraction): await interaction.response.defer(ephemeral=True) config = load_config() @@ -220,7 +217,7 @@ class WebsiteSync(commands.Cog, name="Website Sync"): mbr_on = ", ".join(k for k, v in config["memberFields"].items() if v) or "_Aucun_" mbr_off = ", ".join(k for k, v in config["memberFields"].items() if not v) or "_Aucun_" - embed = self._embed("⚙️ Configuration JSON Export", discord.Color.blurple()) + embed = self._embed("⚙️ Configuration JSON Export", disnake.Color.blurple()) embed.add_field(name="🕐 Génération auto", value=f"{config['autoHour']:02d}:{config['autoMinute']:02d} (heure serveur)", inline=True) embed.add_field(name="🎯 Serveur cible (members)", value=config["targetGuildId"] or "_Non défini_", inline=True) embed.add_field(name="\u200b", value="\u200b", inline=False) @@ -233,9 +230,8 @@ class WebsiteSync(commands.Cog, name="Website Sync"): await interaction.followup.send(embed=embed, ephemeral=True) # ── set-guild ──────────────────────────────────────────────────────────── - @json_config_group.command(name="set-guild", description="Définit l'ID du serveur cible pour members.json") - @app_commands.describe(guild_id="ID du serveur Discord") - async def config_set_guild(self, interaction: discord.Interaction, guild_id: str): + @json_config_group.sub_command(name="set-guild", description="Définit l'ID du serveur cible pour members.json") + async def config_set_guild(self, interaction: disnake.ApplicationCommandInteraction, guild_id: str = commands.Param(description="ID du serveur Discord")): await interaction.response.defer(ephemeral=True) try: guild = self.bot.get_guild(int(guild_id)) or await self.bot.fetch_guild(int(guild_id)) @@ -243,20 +239,21 @@ class WebsiteSync(commands.Cog, name="Website Sync"): config["targetGuildId"] = guild_id save_config(config) embed = self._embed( - "✅ Serveur cible défini", discord.Color.green(), + "✅ Serveur cible défini", disnake.Color.green(), f"**{guild.name}** (`{guild_id}`) sera utilisé pour members.json.", ) except Exception: embed = self._embed( - "❌ Serveur introuvable", discord.Color.red(), + "❌ Serveur introuvable", disnake.Color.red(), f"Le bot n'est pas dans le serveur `{guild_id}` ou l'ID est invalide.", ) await interaction.followup.send(embed=embed, ephemeral=True) # ── set-schedule ───────────────────────────────────────────────────────── - @json_config_group.command(name="set-schedule", description="Définit l'heure de génération automatique quotidienne") - @app_commands.describe(heure="Heure (0-23)", minute="Minute (0-59, défaut: 0)") - async def config_set_schedule(self, interaction: discord.Interaction, heure: int, minute: int = 0): + @json_config_group.sub_command(name="set-schedule", description="Définit l'heure de génération automatique quotidienne") + async def config_set_schedule(self, interaction: disnake.ApplicationCommandInteraction, + heure: int = commands.Param(description="Heure (0-23)"), + minute: int = commands.Param(0, description="Minute (0-59, défaut: 0)")): await interaction.response.defer(ephemeral=True) if not (0 <= heure <= 23) or not (0 <= minute <= 59): await interaction.followup.send("❌ Heure invalide.", ephemeral=True) @@ -266,42 +263,42 @@ class WebsiteSync(commands.Cog, name="Website Sync"): config["autoMinute"] = minute save_config(config) embed = self._embed( - "✅ Horaire mis à jour", discord.Color.green(), + "✅ Horaire mis à jour", disnake.Color.green(), f"Prochaine génération auto : **{heure:02d}:{minute:02d}** chaque jour.", ) - await interaction.followup.send(embed=embed, ephemeral=True) + await interaction.followup.send(embed=embed) # ── server-field ───────────────────────────────────────────────────────── - @json_config_group.command(name="server-field", description="Active/désactive un champ dans servers.json") - @app_commands.describe(champ="Champ à modifier", actif="true = inclus, false = exclu") - @app_commands.choices(champ=[app_commands.Choice(name=f, value=f) for f in SERVER_FIELDS]) - async def config_server_field(self, interaction: discord.Interaction, champ: str, actif: bool): + @json_config_group.sub_command(name="server-field", description="Active/désactive un champ dans servers.json") + async def config_server_field(self, interaction: disnake.ApplicationCommandInteraction, + champ: str = commands.Param(description="Champ à modifier", choices=SERVER_FIELDS), + actif: bool = commands.Param(description="true = inclus, false = exclu")): await interaction.response.defer(ephemeral=True) config = load_config() config["serverFields"][champ] = actif save_config(config) state = "inclus ✅" if actif else "exclu ❌" - embed = self._embed("✅ Champ servers.json mis à jour", discord.Color.green(), + embed = self._embed("✅ Champ servers.json mis à jour", disnake.Color.green(), f"`{champ}` est maintenant **{state}** de servers.json.") - await interaction.followup.send(embed=embed, ephemeral=True) + await interaction.followup.send(embed=embed) # ── member-field ───────────────────────────────────────────────────────── - @json_config_group.command(name="member-field", description="Active/désactive un champ dans members.json") - @app_commands.describe(champ="Champ à modifier", actif="true = inclus, false = exclu") - @app_commands.choices(champ=[app_commands.Choice(name=f, value=f) for f in MEMBER_FIELDS]) - async def config_member_field(self, interaction: discord.Interaction, champ: str, actif: bool): + @json_config_group.sub_command(name="member-field", description="Active/désactive un champ dans members.json") + async def config_member_field(self, interaction: disnake.ApplicationCommandInteraction, + champ: str = commands.Param(description="Champ à modifier", choices=MEMBER_FIELDS), + actif: bool = commands.Param(description="true = inclus, false = exclu")): await interaction.response.defer(ephemeral=True) config = load_config() config["memberFields"][champ] = actif save_config(config) state = "inclus ✅" if actif else "exclu ❌" - embed = self._embed("✅ Champ members.json mis à jour", discord.Color.green(), + embed = self._embed("✅ Champ members.json mis à jour", disnake.Color.green(), f"`{champ}` est maintenant **{state}** de members.json.") - await interaction.followup.send(embed=embed, ephemeral=True) + await interaction.followup.send(embed=embed) # ── reset ───────────────────────────────────────────────────────────────── - @json_config_group.command(name="reset", description="Remet la configuration par défaut") - async def config_reset(self, interaction: discord.Interaction): + @json_config_group.sub_command(name="reset", description="Remet la configuration par défaut") + async def config_reset(self, interaction: disnake.ApplicationCommandInteraction): await interaction.response.defer(ephemeral=True) from src.json_export_service import CONFIG_FILE try: @@ -309,14 +306,14 @@ class WebsiteSync(commands.Cog, name="Website Sync"): except Exception: pass load_config() # recrée depuis DEFAULT_CONFIG - embed = self._embed("✅ Configuration réinitialisée", discord.Color.green(), + embed = self._embed("✅ Configuration réinitialisée", disnake.Color.green(), "Tous les paramètres ont été remis à leurs valeurs par défaut.") - await interaction.followup.send(embed=embed, ephemeral=True) + await interaction.followup.send(embed=embed) # ── set-output-dir ──────────────────────────────────────────────────────── - @json_config_group.command(name="set-output-dir", description="Change le dossier de sortie des fichiers JSON") - @app_commands.describe(chemin="Chemin absolu ou relatif vers le dossier (ex: /var/www/site/data)") - async def config_set_output_dir(self, interaction: discord.Interaction, chemin: str): + @json_config_group.sub_command(name="set-output-dir", description="Change le dossier de sortie des fichiers JSON") + async def config_set_output_dir(self, interaction: disnake.ApplicationCommandInteraction, + chemin: str = commands.Param(description="Chemin absolu ou relatif vers le dossier (ex: /var/www/site/data)")): await interaction.response.defer(ephemeral=True) from pathlib import Path as _Path p = _Path(chemin) @@ -326,75 +323,69 @@ class WebsiteSync(commands.Cog, name="Website Sync"): config["outputDir"] = str(p) save_config(config) embed = self._embed( - "✅ Dossier de sortie mis à jour", discord.Color.green(), + "✅ Dossier de sortie mis à jour", disnake.Color.green(), f"Les JSON seront désormais écrits dans :\n`{p.resolve()}`", ) except Exception as e: - embed = self._embed("❌ Erreur", discord.Color.red(), f"Impossible d'utiliser ce chemin :\n```{e}```") - await interaction.followup.send(embed=embed, ephemeral=True) + embed = self._embed("❌ Erreur", disnake.Color.red(), f"Impossible d'utiliser ce chemin :\n```{e}```") + await interaction.followup.send(embed=embed) # ── add-founder-role ────────────────────────────────────────────────────── - @json_config_group.command(name="add-founder-role", description="Ajoute un nom de rôle reconnu comme 'fondateur'") - @app_commands.describe(nom="Nom du rôle (insensible à la casse, correspondance partielle)") - async def config_add_founder_role(self, interaction: discord.Interaction, nom: str): + @json_config_group.sub_command(name="add-founder-role", description="Ajoute un nom de rôle reconnu comme 'fondateur'") + async def config_add_founder_role(self, interaction: disnake.ApplicationCommandInteraction, + nom: str = commands.Param(description="Nom du rôle (insensible à la casse, correspondance partielle)")): await interaction.response.defer(ephemeral=True) config = load_config() roles = config.get("founderRoles", []) nom_l = nom.lower().strip() if nom_l in [r.lower() for r in roles]: - embed = self._embed("ℹ️ Déjà présent", discord.Color.yellow(), + embed = self._embed("ℹ️ Déjà présent", disnake.Color.yellow(), f"`{nom}` est déjà dans la liste des rôles fondateurs.") else: roles.append(nom_l) config["founderRoles"] = roles save_config(config) embed = self._embed( - "✅ Rôle fondateur ajouté", discord.Color.green(), + "✅ Rôle fondateur ajouté", disnake.Color.green(), f"`{nom_l}` ajouté. Liste actuelle : {', '.join(f'`{r}`' for r in roles)}", ) - await interaction.followup.send(embed=embed, ephemeral=True) + await interaction.followup.send(embed=embed) # ── remove-founder-role ─────────────────────────────────────────────────── - @json_config_group.command(name="remove-founder-role", description="Retire un nom de rôle fondateur") - @app_commands.describe(nom="Nom du rôle à retirer") - async def config_remove_founder_role(self, interaction: discord.Interaction, nom: str): + @json_config_group.sub_command(name="remove-founder-role", description="Retire un nom de rôle fondateur") + async def config_remove_founder_role(self, interaction: disnake.ApplicationCommandInteraction, + nom: str = commands.Param(description="Nom du rôle à retirer")): await interaction.response.defer(ephemeral=True) config = load_config() roles = config.get("founderRoles", []) nom_l = nom.lower().strip() updated = [r for r in roles if r.lower() != nom_l] if len(updated) == len(roles): - embed = self._embed("ℹ️ Introuvable", discord.Color.yellow(), + embed = self._embed("ℹ️ Introuvable", disnake.Color.yellow(), f"`{nom}` n'est pas dans la liste des rôles fondateurs.") else: config["founderRoles"] = updated save_config(config) reste = ', '.join(f'`{r}`' for r in updated) or "_Aucun_" embed = self._embed( - "✅ Rôle fondateur retiré", discord.Color.green(), + "✅ Rôle fondateur retiré", disnake.Color.green(), f"`{nom_l}` retiré. Liste actuelle : {reste}", ) - await interaction.followup.send(embed=embed, ephemeral=True) + await interaction.followup.send(embed=embed) # ══════════════════════════════════════════════════════════════════════════ # /json-preview # ══════════════════════════════════════════════════════════════════════════ - @app_commands.command(name="json-preview", description="Aperçu des données sans écrire les fichiers") - @app_commands.describe( - cible="Quoi prévisualiser ?", - limite="Nombre d'éléments à afficher (défaut: 3)", - ) - @app_commands.choices(cible=[ - app_commands.Choice(name="Serveurs", value="servers"), - app_commands.Choice(name="Membres", value="members"), - ]) - @app_commands.default_permissions(administrator=True) - async def json_preview(self, interaction: discord.Interaction, cible: str = "servers", limite: int = 3): + @commands.slash_command(name="json-preview", description="Aperçu des données sans écrire les fichiers") + @commands.has_permissions(administrator=True) + async def json_preview(self, interaction: disnake.ApplicationCommandInteraction, + cible: str = commands.Param("servers", description="Quoi prévisualiser ?", choices={"Serveurs": "servers", "Membres": "members"}), + limite: int = commands.Param(3, description="Nombre d'éléments à afficher (1-10, défaut: 3)")): await interaction.response.defer(ephemeral=True) limite = max(1, min(limite, 10)) # entre 1 et 10 try: - embed = self._embed(f"🔍 Aperçu — {cible}", discord.Color.blurple()) + embed = self._embed(f"🔍 Aperçu — {cible}", disnake.Color.blurple()) embed.set_footer(text="Aperçu uniquement — aucun fichier écrit") if cible == "servers": @@ -421,22 +412,22 @@ class WebsiteSync(commands.Cog, name="Website Sync"): ) except Exception as e: - embed = self._embed("❌ Erreur", discord.Color.red(), f"```{e}```") + embed = self._embed("❌ Erreur", disnake.Color.red(), f"```{e}```") await interaction.followup.send(embed=embed, ephemeral=True) # ══════════════════════════════════════════════════════════════════════════ # /json-history # ══════════════════════════════════════════════════════════════════════════ - @app_commands.command(name="json-history", description="Historique des dernières générations automatiques et manuelles") - @app_commands.describe(limite="Nombre d'entrées à afficher (défaut: 5, max: 10)") - @app_commands.default_permissions(administrator=True) - async def json_history(self, interaction: discord.Interaction, limite: int = 5): + @commands.slash_command(name="json-history", description="Historique des dernières générations automatiques et manuelles") + @commands.has_permissions(administrator=True) + async def json_history(self, interaction: disnake.ApplicationCommandInteraction, + limite: int = commands.Param(5, description="Nombre d'entrées à afficher (défaut: 5, max: 10)")): await interaction.response.defer(ephemeral=True) limite = max(1, min(limite, 10)) history = get_history() - embed = self._embed("📜 Historique des générations JSON", discord.Color.blurple()) + embed = self._embed("📜 Historique des générations JSON", disnake.Color.blurple()) if not history: embed.description = "_Aucune génération enregistrée pour l'instant._" @@ -465,9 +456,5 @@ class WebsiteSync(commands.Cog, name="Website Sync"): # ─── Setup ─────────────────────────────────────────────────────────────────── async def setup(bot: commands.Bot): - cog = WebsiteSync(bot) - await bot.add_cog(cog) - # Enregistre le groupe de commandes json-config s'il n'est pas déjà présent - if not bot.tree.get_command("json-config"): - bot.tree.add_command(cog.json_config_group) + await bot.add_cog(WebsiteSync(bot)) logger.info("Cog WebsiteSync chargé.") diff --git a/commandes/welcome.py b/commandes/welcome.py index 8b34a32..5f8172a 100644 --- a/commandes/welcome.py +++ b/commandes/welcome.py @@ -1,6 +1,5 @@ -import discord -from discord.ext import commands -from discord import app_commands +import disnake +from disnake.ext import commands import sqlite3 import aiohttp import io @@ -145,9 +144,9 @@ class Welcome(commands.Cog): gif_url = "https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExNXV1NW02NTk0bGF2ZHcyMGFna2F6amRrbGpobGpsMHowOW5xa2E5eCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ZtFuW4rsUbfrRzPauJ/giphy.gif" await member.send(f"{formatted_message}\n\n{gif_url}") kuby_logger.info(f"✅ [Welcome] DM envoyé avec succès à {member.name} (envoi groupé)") - except discord.Forbidden: + except disnake.Forbidden: kuby_logger.warning(f"🚫 [Welcome] Impossible d'envoyer le DM à {member.name} (DMs fermés)") - except discord.HTTPException as e: + except disnake.HTTPException as e: if e.code == 40003: # Rate limited (Opening DMs too fast) kuby_logger.warning(f"⏳ [Welcome] Limite de débit Discord atteinte pour les DMs : {member.name} n'a pas reçu son message.") else: @@ -177,13 +176,13 @@ class Welcome(commands.Cog): member_count=member.guild.member_count ) if welcome_channel_message else "Bienvenue dans le serveur !\nSi vous cherchez des RolePlay de qualité,\nVous êtes au bon endroit. " - embed = discord.Embed(title=f"Merci d'accueillir {member} 🤝", + embed = disnake.Embed(title=f"Merci d'accueillir {member} 🤝", description=channel_description, - color=discord.Color.dark_purple()) + color=disnake.Color.dark_purple()) embed.set_image(url="attachment://welcome.png") - await channel.send(content=member.mention, embed=embed, file=discord.File(img, filename="welcome.png")) + await channel.send(content=member.mention, embed=embed, file=disnake.File(img, filename="welcome.png")) - async def save_banner_attachment(self, attachment: discord.Attachment, guild_id: int) -> str: + async def save_banner_attachment(self, attachment: disnake.Attachment, guild_id: int) -> str: """Sauvegarde l'attachment et retourne le chemin du fichier""" base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "banners")) os.makedirs(base_dir, exist_ok=True) @@ -215,9 +214,9 @@ class Welcome(commands.Cog): return os.path.basename(banner_path) - @app_commands.command(name="setwelcome", description="Définit le salon, les messages de bienvenue et le fond de la bannière") - @app_commands.checks.has_permissions(administrator=True) - async def set_welcome(self, interaction: discord.Interaction, channel: discord.TextChannel, message_dm: str, message_salon: str, server_name: str, banner_file: discord.Attachment = None): + @commands.slash_command(name="setwelcome", description="Définit le salon, les messages de bienvenue et le fond de la bannière") + @commands.has_permissions(administrator=True) + async def set_welcome(self, interaction: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel, message_dm: str, message_salon: str, server_name: str, banner_file: disnake.Attachment = None): banner_path = None # Si un fichier est uploadé, le sauvegarder @@ -237,7 +236,7 @@ class Welcome(commands.Cog): welcome_server_name=excluded.welcome_server_name, welcome_banner_background=COALESCE(excluded.welcome_banner_background, welcome_banner_background), welcome_channel_message=excluded.welcome_channel_message''', - (interaction.guild_id, channel.id, message_dm, server_name, banner_path, message_salon)) + (interaction.guild_id, channel.id, message_dm, server_name, banner_path, message_salon)) conn.commit() conn.close() diff --git a/commandes/whitelist.py b/commandes/whitelist.py index 5d7bcc2..4704660 100755 --- a/commandes/whitelist.py +++ b/commandes/whitelist.py @@ -1,6 +1,5 @@ -import discord -from discord import app_commands -from discord.ext import commands +import disnake +from disnake.ext import commands import json import os from typing import List @@ -32,95 +31,60 @@ class Whitelist(commands.Cog): self.whitelist[str(guild_id)] = users self.save_whitelist() - @commands.hybrid_group(name="whitelist", aliases=["wl"], description="Gère la whitelist du serveur") - async def whitelist_group(self, ctx): + @commands.slash_command(name="whitelist", description="Gère la whitelist du serveur") + async def whitelist_group(self, interaction: disnake.ApplicationCommandInteraction): """Groupe de commandes pour gérer la whitelist""" - if ctx.invoked_subcommand is None: - await ctx.send_help(ctx.command) + pass # ✅ Ajouter un utilisateur - @whitelist_group.command(name="ajouter", description="Ajoute un utilisateur à la whitelist") - @app_commands.describe(utilisateur="L'utilisateur à ajouter") - async def ajouter(self, ctx, utilisateur: discord.User): - interaction = ctx.interaction - guild_id = ctx.guild.id + @whitelist_group.sub_command(name="ajouter", description="Ajoute un utilisateur à la whitelist") + async def ajouter(self, interaction: disnake.ApplicationCommandInteraction, utilisateur: disnake.User): + guild_id = interaction.guild.id users = self._get_guild_whitelist(guild_id) user_id = str(utilisateur.id) - + if user_id in users: - msg = f"🔒 {utilisateur.mention} est déjà dans la whitelist." - if interaction: - await interaction.response.send_message(msg, ephemeral=True) - else: - await ctx.send(msg) - return - + return await interaction.response.send_message(f"🔒 {utilisateur.mention} est déjà dans la whitelist.", ephemeral=True) + users.append(user_id) self._set_guild_whitelist(guild_id, users) - msg = f"✅ {utilisateur.mention} a été ajouté à la whitelist." - if interaction: - await interaction.response.send_message(msg, ephemeral=True) - else: - await ctx.send(msg) + await interaction.response.send_message(f"✅ {utilisateur.mention} a été ajouté à la whitelist.", ephemeral=True) # ❌ Retirer un utilisateur - @whitelist_group.command(name="retirer", description="Retire un utilisateur de la whitelist") - @app_commands.describe(utilisateur="L'utilisateur à retirer") - async def retirer(self, ctx, utilisateur: discord.User): - interaction = ctx.interaction - guild_id = ctx.guild.id + @whitelist_group.sub_command(name="retirer", description="Retire un utilisateur de la whitelist") + async def retirer(self, interaction: disnake.ApplicationCommandInteraction, utilisateur: disnake.User): + guild_id = interaction.guild.id users = self._get_guild_whitelist(guild_id) user_id = str(utilisateur.id) - + if user_id not in users: - msg = f"⚠️ {utilisateur.mention} n'est pas dans la whitelist." - if interaction: - await interaction.response.send_message(msg, ephemeral=True) - else: - await ctx.send(msg) - return - + return await interaction.response.send_message(f"⚠️ {utilisateur.mention} n'est pas dans la whitelist.", ephemeral=True) + users.remove(user_id) self._set_guild_whitelist(guild_id, users) - msg = f"✅ {utilisateur.mention} a été retiré de la whitelist." - if interaction: - await interaction.response.send_message(msg, ephemeral=True) - else: - await ctx.send(msg) + await interaction.response.send_message(f"✅ {utilisateur.mention} a été retiré de la whitelist.", ephemeral=True) # 📋 Voir la whitelist - @whitelist_group.command(name="liste", description="Affiche les utilisateurs de la whitelist") - async def liste(self, ctx): - interaction = ctx.interaction - guild_id = ctx.guild.id + @whitelist_group.sub_command(name="liste", description="Affiche les utilisateurs de la whitelist") + async def liste(self, interaction: disnake.ApplicationCommandInteraction): + guild_id = interaction.guild.id users = self._get_guild_whitelist(guild_id) - + if not users: - msg = "📭 La whitelist est vide pour ce serveur." - if interaction: - await interaction.response.send_message(msg, ephemeral=True) - else: - await ctx.send(msg) - return - - # Defer if interaction because fetch_user might take time - if interaction: - await interaction.response.defer(ephemeral=True) - + return await interaction.response.send_message("📭 La whitelist est vide pour ce serveur.", ephemeral=True) + + await interaction.response.defer(ephemeral=True) + membres = [] for user_id in users: try: utilisateur = await self.bot.fetch_user(int(user_id)) membres.append(f"- {utilisateur.mention} ({utilisateur.name})") - except discord.NotFound: + except disnake.NotFound: membres.append(f"- <@{user_id}> (Utilisateur introuvable)") - + texte = "\n".join(membres) - msg = f"📃 **Utilisateurs whitelistés pour ce serveur :**\n{texte}" - if interaction: - await interaction.followup.send(msg, ephemeral=True) - else: - await ctx.send(msg) + await interaction.edit_original_response(content=f"📃 **Utilisateurs whitelistés pour ce serveur :**\n{texte}") def is_whitelisted(self, guild_id: int, user_id: int) -> bool: """Check if a user is whitelisted for a specific guild.""" diff --git a/commandes/whitelist_monitor.py b/commandes/whitelist_monitor.py index 53586bd..7390d02 100755 --- a/commandes/whitelist_monitor.py +++ b/commandes/whitelist_monitor.py @@ -1,6 +1,6 @@ -import discord -from discord.ext import commands -from discord.ui import View, Button +import disnake +from disnake.ext import commands +from disnake.ui import View, Button import json import os from datetime import datetime @@ -14,18 +14,18 @@ class WhitelistMonitor(commands.Cog): self.whitelist_file = "data/whitelist.json" self.persistent_views_added = False self.big_permissions = [ - discord.Permissions.administrator, - discord.Permissions.manage_guild, - discord.Permissions.manage_roles, - discord.Permissions.manage_channels, - discord.Permissions.kick_members, - discord.Permissions.ban_members, - discord.Permissions.manage_messages, - discord.Permissions.moderate_members, - discord.Permissions.manage_nicknames, - discord.Permissions.manage_emojis, - discord.Permissions.manage_webhooks, - discord.Permissions.view_audit_log + disnake.Permissions.administrator, + disnake.Permissions.manage_guild, + disnake.Permissions.manage_roles, + disnake.Permissions.manage_channels, + disnake.Permissions.kick_members, + disnake.Permissions.ban_members, + disnake.Permissions.manage_messages, + disnake.Permissions.moderate_members, + disnake.Permissions.manage_nicknames, + disnake.Permissions.manage_emojis, + disnake.Permissions.manage_webhooks, + disnake.Permissions.view_audit_log ] def load_whitelist(self) -> Dict[str, List[str]]: @@ -70,7 +70,7 @@ class WhitelistMonitor(commands.Cog): return True return False - def has_big_permissions(self, role: discord.Role) -> bool: + def has_big_permissions(self, role: disnake.Role) -> bool: """Check if role has significant permissions.""" permissions = role.permissions for perm in self.big_permissions: @@ -78,7 +78,7 @@ class WhitelistMonitor(commands.Cog): return True return False - async def send_whitelist_notification(self, member: discord.Member, removed_roles: List[discord.Role]): + async def send_whitelist_notification(self, member: disnake.Member, removed_roles: List[disnake.Role]): """Send DM notification to user about whitelist member losing roles.""" try: # Find guild administrators to notify @@ -87,10 +87,10 @@ class WhitelistMonitor(commands.Cog): return # Create embed - embed = discord.Embed( + embed = disnake.Embed( title="⚠️ Whitelist Alert", description=f"Un membre whitelisté a perdu des rôles importants dans **{member.guild.name}**.", - color=discord.Color.orange(), + color=disnake.Color.orange(), timestamp=datetime.now() ) @@ -121,7 +121,7 @@ class WhitelistMonitor(commands.Cog): for admin in admins: try: await admin.send(embed=embed, view=view) - except discord.Forbidden: + except disnake.Forbidden: continue # User has DMs disabled except Exception as e: @@ -155,7 +155,7 @@ class WhitelistMonitor(commands.Cog): print(f"[WhitelistMonitor] Error registering views: {e}") @commands.Cog.listener() - async def on_member_update(self, before: discord.Member, after: discord.Member): + async def on_member_update(self, before: disnake.Member, after: disnake.Member): """Monitor role changes for whitelist members.""" if before.roles == after.roles: return @@ -173,7 +173,7 @@ class WhitelistMonitor(commands.Cog): await self.send_whitelist_notification(after, important_removed) @commands.Cog.listener() - async def on_member_remove(self, member: discord.Member): + async def on_member_remove(self, member: disnake.Member): """Auto-remove from whitelist when member leaves server.""" if self.is_whitelisted(member.guild.id, member.id): removed = self.remove_from_whitelist(member.guild.id, member.id) @@ -184,10 +184,10 @@ class WhitelistMonitor(commands.Cog): if log_channel_id: log_channel = member.guild.get_channel(log_channel_id) if log_channel: - embed = discord.Embed( + embed = disnake.Embed( title="🗑️ Auto-Whitelist Removal", description=f"{member.mention} a été retiré automatiquement de la whitelist après avoir quitté le serveur.", - color=discord.Color.red(), + color=disnake.Color.red(), timestamp=datetime.now() ) await log_channel.send(embed=embed) @@ -214,15 +214,15 @@ class WhitelistRemovalView(View): self.user_id = user_id self.guild_id = guild_id - @discord.ui.button( + @disnake.ui.button( label="Retirer de la whitelist", - style=discord.ButtonStyle.red, + style=disnake.ButtonStyle.red, emoji="🗑️", custom_id="whitelist_remove_button" ) async def remove_whitelist( self, - interaction: discord.Interaction, + interaction: disnake.Interaction, button: Button ): """Remove user from whitelist when button is clicked.""" @@ -250,15 +250,15 @@ class WhitelistRemovalView(View): # Update button button.disabled = True button.label = "✅ Retiré" - button.style = discord.ButtonStyle.green + button.style = disnake.ButtonStyle.green await interaction.response.edit_message(view=self) # Send confirmation - embed = discord.Embed( + embed = disnake.Embed( title="✅ Retrait effectué", description=f"{user_mention} a été retiré de la whitelist.", - color=discord.Color.green() + color=disnake.Color.green() ) await interaction.followup.send(embed=embed, ephemeral=True) @@ -270,10 +270,10 @@ class WhitelistRemovalView(View): if log_channel_id: log_channel = guild.get_channel(log_channel_id) if log_channel: - embed = discord.Embed( + embed = disnake.Embed( title="🗑️ Whitelist Manual Removal", description=f"{user_mention} a été retiré manuellement de la whitelist.", - color=discord.Color.green(), + color=disnake.Color.green(), timestamp=datetime.now() ) embed.set_footer(text=f"Retiré par {interaction.user.display_name}") diff --git a/kuby.py b/kuby.py index a3b6f9b..8bb7762 100755 --- a/kuby.py +++ b/kuby.py @@ -28,10 +28,8 @@ kuby_logger.info("✅ Token Discord chargé avec succès") async def main(): kuby_logger.info("🔄 Démarrage de la connexion Discord...") try: - kuby_logger.debug("Starting async context manager") - async with bot: - kuby_logger.info("🔗 Connexion établie avec Discord...") - await bot.start(TOKEN) + kuby_logger.info("🔗 Connexion établie avec Discord...") + await bot.start(TOKEN) except KeyboardInterrupt: kuby_logger.info("🛑 Bot shutdown requested by user") except Exception as e: diff --git a/requirements.txt b/requirements.txt index 4319012..46b3c57 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -discord.py>=2.3.0 +disnake>=2.11.0 python-dotenv>=1.0.0 Pillow>=10.0.0 aiohttp>=3.8.0 diff --git a/src/advanced_logger.py b/src/advanced_logger.py index e7b0558..035e379 100755 --- a/src/advanced_logger.py +++ b/src/advanced_logger.py @@ -1,4 +1,4 @@ -import discord +import disnake import json import os import asyncio @@ -18,7 +18,7 @@ class AdvancedLogger: os.makedirs(self.logs_dir, exist_ok=True) os.makedirs(self.member_logs_dir, exist_ok=True) - def log_message_event(self, message: discord.Message, event_type: str, details: Dict = None): + def log_message_event(self, message: disnake.Message, event_type: str, details: Dict = None): """Log message events (sent, edited, deleted)""" log_data = { "timestamp": datetime.now().isoformat(), @@ -37,7 +37,7 @@ class AdvancedLogger: asyncio.create_task(self._write_log_async("messages", log_data)) - def log_role_event(self, member: discord.Member, role: discord.Role, event_type: str): + def log_role_event(self, member: disnake.Member, role: disnake.Role, event_type: str): """Log role changes (added/removed)""" log_data = { "timestamp": datetime.now().isoformat(), @@ -52,7 +52,7 @@ class AdvancedLogger: asyncio.create_task(self._write_log_async("roles", log_data)) - def log_voice_event(self, member: discord.Member, channel: discord.VoiceChannel, event_type: str): + def log_voice_event(self, member: disnake.Member, channel: disnake.VoiceChannel, event_type: str): """Log voice channel events (join/leave)""" log_data = { "timestamp": datetime.now().isoformat(), @@ -66,7 +66,7 @@ class AdvancedLogger: asyncio.create_task(self._write_log_async("voice", log_data)) - def log_member_leave(self, member: discord.Member): + def log_member_leave(self, member: disnake.Member): """Log when a member leaves the server""" roles_data = { "role_ids": [role.id for role in member.roles if role.name != "@everyone"], @@ -91,7 +91,7 @@ class AdvancedLogger: # Also save to general logs asyncio.create_task(self._write_log_async("member_events", log_data)) - def log_member_join(self, member: discord.Member): + def log_member_join(self, member: disnake.Member): """Log when a member joins the server""" log_data = { "timestamp": datetime.now().isoformat(), @@ -162,12 +162,12 @@ class AdvancedLogger: except Exception as e: print(f"CRITICAL ERROR in AdvancedLogger._write_member_log_sync: {e}") - async def send_leave_notification(self, member: discord.Member, channel: discord.TextChannel): + async def send_leave_notification(self, member: disnake.Member, channel: disnake.TextChannel): """Send notification when member leaves""" - embed = discord.Embed( + embed = disnake.Embed( title="👋 Membre parti", - description=f"{member.mention} ({member.name}#{member.discriminator}) a quitté le serveur.", - color=discord.Color.red(), + description=f"{member.mention} ({member.name}) a quitté le serveur.", + color=disnake.Color.red(), timestamp=datetime.now() ) diff --git a/src/json_export_service.py b/src/json_export_service.py index bf7210d..60862b7 100644 --- a/src/json_export_service.py +++ b/src/json_export_service.py @@ -12,7 +12,7 @@ from datetime import datetime, timezone from pathlib import Path from typing import Optional -import discord +import disnake logger = logging.getLogger("kuby.json_export") @@ -160,7 +160,7 @@ def get_history() -> list: # ─── Génération servers.json ───────────────────────────────────────────────── -async def generate_servers_json(bot: discord.Client) -> dict: +async def generate_servers_json(bot: disnake.Client) -> dict: config = load_config() fields = config["serverFields"] founder_keys = [r.lower() for r in config.get("founderRoles", ["fondateur", "founder"])] @@ -249,7 +249,7 @@ async def generate_servers_json(bot: discord.Client) -> dict: # ─── Génération members.json ───────────────────────────────────────────────── -async def generate_members_json(bot: discord.Client, guild_id: Optional[int] = None) -> dict: +async def generate_members_json(bot: disnake.Client, guild_id: Optional[int] = None) -> dict: config = load_config() fields = config["memberFields"] target_id = guild_id or (int(config["targetGuildId"]) if config["targetGuildId"] else None) @@ -324,7 +324,7 @@ async def generate_members_json(bot: discord.Client, guild_id: Optional[int] = N # ─── Génération complète ───────────────────────────────────────────────────── -async def generate_all(bot: discord.Client, trigger: str = "auto") -> dict: +async def generate_all(bot: disnake.Client, trigger: str = "auto") -> dict: results = {"servers": None, "members": None, "errors": [], "trigger": trigger} try: results["servers"] = await generate_servers_json(bot) @@ -343,7 +343,7 @@ async def generate_all(bot: discord.Client, trigger: str = "auto") -> dict: # ─── Prévisualisation (sans écriture fichier) ──────────────────────────────── -async def preview_servers(bot: discord.Client, limit: int = 3) -> list: +async def preview_servers(bot: disnake.Client, limit: int = 3) -> list: """Retourne un aperçu de N serveurs sans écrire le fichier.""" config = load_config() fields = config["serverFields"] @@ -366,7 +366,7 @@ async def preview_servers(bot: discord.Client, limit: int = 3) -> list: return results -async def preview_members(bot: discord.Client, limit: int = 5) -> dict: +async def preview_members(bot: disnake.Client, limit: int = 5) -> dict: """Retourne un aperçu de N membres sans écrire le fichier.""" config = load_config() target_id = int(config["targetGuildId"]) if config["targetGuildId"] else None From 98f7501e0714a854865370823ffdabc841065f4d Mon Sep 17 00:00:00 2001 From: Mathis Date: Sat, 9 May 2026 18:42:42 +0200 Subject: [PATCH 2/4] Migration vers Disnake et ajout des components V2 --- backups/scratch/check_button_sig.py | 30 + backups/scratch/check_checks.py | 4 + {scratch => backups/scratch}/check_db.py | 0 backups/scratch/check_disnake.py | 19 + backups/scratch/check_disnake_2.py | 7 + backups/scratch/check_disnake_3.py | 6 + backups/scratch/check_textstyle.py | 8 + backups/scratch/fix_modal_view_mixup.py | 52 + backups/scratch/fix_text_input_default.py | 36 + backups/scratch/inspect_modal.py | 7 + backups/scratch/inspect_section.py | 7 + backups/scratch/test_container.py | 3 + backups/scratch/test_edit_message.py | 17 + bot.py | 88 +- commandes/Backups_project/blacklist.py | 32 +- commandes/absence_staff.py | 14 +- commandes/backup.py | 4 +- commandes/backup_restore.py | 4 +- commandes/blacklist.py | 12 +- commandes/bug_report.py | 12 +- commandes/convocation.py | 4 +- commandes/goodbye.py | 4 +- commandes/moderation.py | 34 +- commandes/modules_security/antiraid.py | 4 +- commandes/modules_security/antispam.py | 6 +- commandes/modules_security/logs_manager.py | 4 +- commandes/modules_security/rules.py | 40 +- commandes/no_link.py | 4 +- commandes/scan.py | 4 +- commandes/security.py | 32 +- commandes/sendbotmessages.py | 4 +- commandes/snipe.py | 4 +- commandes/staff_leaderboard.py | 4 +- commandes/staff_ratings.py | 4 +- commandes/{ticket.py => ticket.py.old} | 0 commandes/ticket/__init__.py | 1305 ++++++++++------- commandes/ticket/analytics.py | 4 +- commandes/ticket/feedback_view.py | 12 +- commandes/ticket_whitelist.py | 22 +- commandes/website_sync.py | 4 +- commandes/welcome.py | 4 +- commandes/whitelist.py | 4 +- commandes/whitelist_monitor.py | 4 +- data/json-export-history.json | 40 +- data/tickets/tickets/1459561776428351568.json | 2 +- data/tickets/tickets/1461777953376698398.json | 2 +- kuby.py | 1 + 47 files changed, 1196 insertions(+), 722 deletions(-) create mode 100644 backups/scratch/check_button_sig.py create mode 100644 backups/scratch/check_checks.py rename {scratch => backups/scratch}/check_db.py (100%) create mode 100644 backups/scratch/check_disnake.py create mode 100644 backups/scratch/check_disnake_2.py create mode 100644 backups/scratch/check_disnake_3.py create mode 100644 backups/scratch/check_textstyle.py create mode 100644 backups/scratch/fix_modal_view_mixup.py create mode 100644 backups/scratch/fix_text_input_default.py create mode 100644 backups/scratch/inspect_modal.py create mode 100644 backups/scratch/inspect_section.py create mode 100644 backups/scratch/test_container.py create mode 100644 backups/scratch/test_edit_message.py rename commandes/{ticket.py => ticket.py.old} (100%) diff --git a/backups/scratch/check_button_sig.py b/backups/scratch/check_button_sig.py new file mode 100644 index 0000000..2de5187 --- /dev/null +++ b/backups/scratch/check_button_sig.py @@ -0,0 +1,30 @@ +import disnake +from disnake.ui import View, Button +import asyncio + +class MyView(View): + def __init__(self): + super().__init__(timeout=None) + + @disnake.ui.button(label="Test") + async def my_btn(self, arg1, arg2): + print(f"Arg1 type: {type(arg1)}") + print(f"Arg2 type: {type(arg2)}") + +async def main(): + view = MyView() + btn = view.children[0] + print(f"Button: {btn}") + # Mock an interaction + class MockInteraction: + pass + + try: + await btn.callback(MockInteraction()) + except Exception as e: + print(f"Error during callback: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/backups/scratch/check_checks.py b/backups/scratch/check_checks.py new file mode 100644 index 0000000..febfc14 --- /dev/null +++ b/backups/scratch/check_checks.py @@ -0,0 +1,4 @@ +import disnake +import disnake.ext.commands as commands +bot = commands.Bot(command_prefix="!") +print(f"Check methods: {[m for m in dir(bot) if 'check' in m]}") diff --git a/scratch/check_db.py b/backups/scratch/check_db.py similarity index 100% rename from scratch/check_db.py rename to backups/scratch/check_db.py diff --git a/backups/scratch/check_disnake.py b/backups/scratch/check_disnake.py new file mode 100644 index 0000000..87603b2 --- /dev/null +++ b/backups/scratch/check_disnake.py @@ -0,0 +1,19 @@ +import disnake +import disnake.ext.commands as commands +import asyncio + +print(f"Disnake version: {disnake.__version__}") +bot = commands.Bot(command_prefix="!") +print(f"Bot has setup_hook: {hasattr(bot, 'setup_hook')}") + +async def test(): + print("Testing setup_hook call...") + class TestBot(commands.Bot): + async def setup_hook(self): + print("setup_hook CALLED!") + + tbot = TestBot(command_prefix="!") + # We won't start it because we don't have a token here, + # but we can check if the library expects it. + +asyncio.run(test()) diff --git a/backups/scratch/check_disnake_2.py b/backups/scratch/check_disnake_2.py new file mode 100644 index 0000000..24f8254 --- /dev/null +++ b/backups/scratch/check_disnake_2.py @@ -0,0 +1,7 @@ +import disnake +import disnake.ext.commands as commands +import asyncio + +bot = commands.Bot(command_prefix="!") +print(f"Bot methods: {[m for m in dir(bot) if 'setup' in m or 'load' in m]}") +print(f"Is context manager: {hasattr(bot, '__aenter__')}") diff --git a/backups/scratch/check_disnake_3.py b/backups/scratch/check_disnake_3.py new file mode 100644 index 0000000..58fe2d7 --- /dev/null +++ b/backups/scratch/check_disnake_3.py @@ -0,0 +1,6 @@ +import disnake +import disnake.ext.commands as commands +import inspect + +bot = commands.Bot(command_prefix="!") +print(f"load_extension is coroutine: {inspect.iscoroutinefunction(bot.load_extension)}") diff --git a/backups/scratch/check_textstyle.py b/backups/scratch/check_textstyle.py new file mode 100644 index 0000000..a31758a --- /dev/null +++ b/backups/scratch/check_textstyle.py @@ -0,0 +1,8 @@ +import disnake +print(f"disnake.TextStyle: {hasattr(disnake, 'TextStyle')}") +print(f"disnake.TextInputStyle: {hasattr(disnake, 'TextInputStyle')}") +try: + import disnake.ui + print(f"disnake.ui.TextInputStyle: {hasattr(disnake.ui, 'TextInputStyle')}") +except: + pass diff --git a/backups/scratch/fix_modal_view_mixup.py b/backups/scratch/fix_modal_view_mixup.py new file mode 100644 index 0000000..b3fc321 --- /dev/null +++ b/backups/scratch/fix_modal_view_mixup.py @@ -0,0 +1,52 @@ +import os +import re + +def fix_file(filepath): + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + + # Split by class to be surgical + classes = re.split(r'^(class\s+.*)$', content, flags=re.MULTILINE) + + new_content = [] + if len(classes) > 0: + new_content.append(classes[0]) # Header + + for i in range(1, len(classes), 2): + class_header = classes[i] + class_body = classes[i+1] if i+1 < len(classes) else "" + + # Determine if it's a View or a Modal + is_view = "disnake.ui.View" in class_header or "(disnake.ui.View)" in class_header or "super().__init__(timeout=" in class_body + is_modal = "disnake.ui.Modal" in class_header or "(disnake.ui.Modal)" in class_header or "super().__init__(title=" in class_body + + if is_view: + # Views use add_item + class_body = class_body.replace("self.append_component(", "self.add_item(") + # Ensure super().__init__ doesn't have components=[] if it's a view + class_body = class_body.replace("super().__init__(components=[],", "super().__init__(") + class_body = class_body.replace("super().__init__(components=[], ", "super().__init__(") + + if is_modal: + # Modals use append_component + class_body = class_body.replace("self.add_item(", "self.append_component(") + # Modals MUST have components=[] in super().__init__ if not already there + # (My previous sed might have added it correctly) + pass + + new_content.append(class_header) + new_content.append(class_body) + + final_content = "".join(new_content) + if final_content != content: + with open(filepath, 'w', encoding='utf-8') as f: + f.write(final_content) + return True + return False + +for root, dirs, files in os.walk('commandes'): + for file in files: + if file.endswith('.py'): + path = os.path.join(root, file) + if fix_file(path): + print(f"Fixed {path}") diff --git a/backups/scratch/fix_text_input_default.py b/backups/scratch/fix_text_input_default.py new file mode 100644 index 0000000..1572bbe --- /dev/null +++ b/backups/scratch/fix_text_input_default.py @@ -0,0 +1,36 @@ +import os +import re + +def fix_text_input_default(filepath): + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + + # Match TextInput( followed by anything including newlines, until ) + # This is a bit risky but we can refine it. + # We look for default= inside the arguments of TextInput + + def replace_default(match): + args = match.group(2) + new_args = args.replace("default=", "value=") + return f"{match.group(1)}{new_args})" + + # Regex: (disnake\.ui\.TextInput\(|ui\.TextInput\(|TextInput\() (.*? \)) + # We need to handle nested parentheses if any, but TextInput usually doesn't have them in args. + + new_content = re.sub(r'((?:disnake\.ui\.|ui\.|)TextInput\()(.*?\))', replace_default, content, flags=re.DOTALL) + + if new_content != content: + with open(filepath, 'w', encoding='utf-8') as f: + f.write(new_content) + return True + return False + +files_to_check = [ + "commandes/moderation.py", + "commandes/ticket/__init__.py" +] + +for path in files_to_check: + if os.path.exists(path): + if fix_text_input_default(path): + print(f"Fixed {path}") diff --git a/backups/scratch/inspect_modal.py b/backups/scratch/inspect_modal.py new file mode 100644 index 0000000..24d3d9a --- /dev/null +++ b/backups/scratch/inspect_modal.py @@ -0,0 +1,7 @@ +import disnake +import inspect + +try: + print(f"Modal init signature: {inspect.signature(disnake.ui.Modal.__init__)}") +except Exception as e: + print(f"Error inspecting Modal: {e}") diff --git a/backups/scratch/inspect_section.py b/backups/scratch/inspect_section.py new file mode 100644 index 0000000..c8d5613 --- /dev/null +++ b/backups/scratch/inspect_section.py @@ -0,0 +1,7 @@ +import disnake +import inspect + +try: + print(f"Section init signature: {inspect.signature(disnake.ui.Section.__init__)}") +except Exception as e: + print(f"Error inspecting Section: {e}") diff --git a/backups/scratch/test_container.py b/backups/scratch/test_container.py new file mode 100644 index 0000000..8e2d428 --- /dev/null +++ b/backups/scratch/test_container.py @@ -0,0 +1,3 @@ +import disnake +print(disnake.ui.Container) +print(disnake.ui.TextDisplay) diff --git a/backups/scratch/test_edit_message.py b/backups/scratch/test_edit_message.py new file mode 100644 index 0000000..10736dc --- /dev/null +++ b/backups/scratch/test_edit_message.py @@ -0,0 +1,17 @@ +import disnake +import asyncio + +async def main(): + class DummyResponse: + async def edit_message(self, *args, **kwargs): + print(kwargs) + + inter = type('DummyInter', (), {'response': DummyResponse()})() + container = disnake.ui.Container() + try: + await inter.response.edit_message(components=[container]) + print("Success") + except Exception as e: + print(f"Error: {e}") + +asyncio.run(main()) diff --git a/bot.py b/bot.py index 2130bfe..5e27395 100755 --- a/bot.py +++ b/bot.py @@ -1,4 +1,5 @@ import os +import sys from dotenv import load_dotenv # Charger les variables d'environnement AVANT tout le reste @@ -13,7 +14,6 @@ from src.logger import kuby_logger, log_performance import logging from src.advanced_logger import AdvancedLogger import subprocess -import sys # Récupération et validation de l'ID d'application application_id_raw = os.getenv("APPLICATION_ID") @@ -41,9 +41,6 @@ class MyBot(commands.Bot): application_id=application_id, ) kuby_logger.info("MyBot instance created successfully") - - async def setup_hook(self): - kuby_logger.info("🚀 Démarrage du bot Kuby...") # Initialize advanced logger self.advanced_logger = AdvancedLogger(self) @@ -54,8 +51,6 @@ class MyBot(commands.Bot): try: flask_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "serveur_flask.py") if os.path.exists(flask_path): - # On utilise DEVNULL car PM2 ou Nohup gèrent déjà les logs globaux - # et ça évite que le pipe se remplisse et bloque le serveur Flask self.flask_process = subprocess.Popen( [sys.executable, flask_path], stdout=subprocess.DEVNULL, @@ -68,6 +63,11 @@ class MyBot(commands.Bot): except Exception as e: kuby_logger.error(f"❌ Impossible de démarrer le serveur Flask: {e}") + + async def do_async_setup(self): + # --- INITIALIZATION LOGIC (Called before start() in kuby.py) --- + kuby_logger.info("🚀 Configuration initiale du bot Kuby...") + # Charger les extensions extensions = [ "commandes.whitelist", @@ -95,62 +95,43 @@ class MyBot(commands.Bot): for extension in extensions: try: - await self.load_extension(extension) + self.load_extension(extension) kuby_logger.info(f"✅ Extension chargée : {extension}") except Exception as e: kuby_logger.error(f"❌ Erreur lors du chargement de {extension} : {e}", exc_info=True) - # --- SYSTÈME DE WHITELIST (CORRIGÉ POUR ÉVITER LES ERREURS) --- - @self.check - async def whitelist_check(ctx): + # --- SYSTÈME DE WHITELIST (DÉDIÉ SLASH COMMANDS) --- + async def global_slash_whitelist_check(inter: disnake.ApplicationCommandInteraction): try: - # 1. Bypass pour le propriétaire (Owner) - if await self.is_owner(ctx.author): + if await self.is_owner(inter.author): + return True + command_name = inter.data.name + if command_name in ["ticketconfig", "ticketpanel", "ticket_config", "ticket_panel", "ticket_whitelist_config"]: return True - - # 1b. Bypass pour les commandes de config tickets - # Si la commande est une commande ticket et l'utilisateur a les perms ticket - if ctx.command and (ctx.command.name in ["ticketconfig", "ticketsetup", "configticket"]): - try: - storage = get_storage() - config = storage.load_config(ctx.guild.id) - if can_access_config(ctx, config): - return True - except Exception as e: - kuby_logger.error(f"Error checking ticket permissions in global whitelist: {e}") - # Fallthrough to normal whitelist check if error - - - # 2. Récupération du Cog whitelist_cog = self.get_cog("WhitelistMonitor") if not whitelist_cog: - kuby_logger.warning("WhitelistMonitor cog not found, allowing all commands") return True - - is_whitelisted = whitelist_cog.is_whitelisted(ctx.guild.id, ctx.author.id) - + is_whitelisted = whitelist_cog.is_whitelisted(inter.guild.id, inter.author.id) if not is_whitelisted: - kuby_logger.info(f"User {ctx.author} (ID: {ctx.author.id}) attempted to use command but is not whitelisted") - - # Réponse adaptée (Slash vs Message) + kuby_logger.info(f"🚫 [Check] Accès refusé : {inter.author} sur /{command_name}") msg = "❌ Vous n'êtes pas autorisé à utiliser cette commande. Veuillez demander à être ajouté à la whitelist." - if ctx.interaction: - await ctx.send(msg, ephemeral=True) - else: - await ctx.send(msg) + if not inter.response.is_done(): + await inter.response.send_message(msg, ephemeral=True) return False - return True except Exception as e: - kuby_logger.error(f"Error in whitelist check: {e}") + kuby_logger.error(f"⚠️ [Check] Erreur critique: {e}", exc_info=True) return True - # Sync slash commands - try: - # Disnake handles syncing automatically by default - kuby_logger.info("✅ Commandes slash prêtes avec Disnake") - except Exception as e: - kuby_logger.error(f"❌ Erreur lors de la synchronisation des commandes slash : {e}", exc_info=True) + self.add_app_command_check(global_slash_whitelist_check) + + # --- LOGGING DES INTERACTIONS --- + @self.listen("on_interaction") + async def log_interaction(inter: disnake.Interaction): + if inter.type == disnake.InteractionType.application_command: + kuby_logger.info(f"⚡ Interaction reçue : /{inter.data.name} par {inter.user}") + + kuby_logger.info("✅ Configuration terminée") async def close(self): if hasattr(self, 'flask_process') and self.flask_process: @@ -163,22 +144,12 @@ class MyBot(commands.Bot): await super().close() # --- ÉVÉNEMENTS RESTAURÉS --- - async def on_ready(self): kuby_logger.info(f"🤖 Bot connecté : {self.user} (ID : {self.user.id})") kuby_logger.info(f"📊 Serveurs : {len(self.guilds)}") - - # Log intents status kuby_logger.info(f"🔍 Intents Status - Members: {self.intents.members}, Presences: {self.intents.presences}, Core: {self.intents.message_content}") - if not self.intents.members: - kuby_logger.warning("⚠️ L'intent MEMBERS est désactivé ! Les événements de join/leave et les DMs pourraient ne pas fonctionner correctement.") - - for guild in self.guilds: - kuby_logger.debug(f"Guild: {guild.name} (ID: {guild.id}) - Members: {guild.member_count}") - - total_members = sum(guild.member_count for guild in self.guilds) - kuby_logger.info(f"📊 Total members across all guilds: {total_members}") + kuby_logger.warning("⚠️ L'intent MEMBERS est désactivé !") kuby_logger.info("✅ Bot prêt et opérationnel !") async def on_guild_join(self, guild): @@ -209,7 +180,6 @@ class MyBot(commands.Bot): kuby_logger.info(f"👋 {member} left {member.guild.name}") if hasattr(self, 'advanced_logger'): self.advanced_logger.log_member_leave(member) - # Notification de départ try: from commandes.security import load_settings settings = load_settings(member.guild.id) @@ -246,7 +216,7 @@ class MyBot(commands.Bot): async def on_command_error(self, ctx, error): if isinstance(error, commands.CheckFailure): - return # Déjà géré dans le whitelist_check + return kuby_logger.error(f"Command error in {ctx.command}: {str(error)}", exc_info=True) bot = MyBot() \ No newline at end of file diff --git a/commandes/Backups_project/blacklist.py b/commandes/Backups_project/blacklist.py index e7c900b..af0b345 100755 --- a/commandes/Backups_project/blacklist.py +++ b/commandes/Backups_project/blacklist.py @@ -1,6 +1,6 @@ import discord from discord import app_commands -from discord.ext import commands +from disnake.ext import commands import json import os @@ -38,7 +38,7 @@ class Blacklist(commands.Cog): # ---------- Slash Commands ---------- @app_commands.command(name="blacklist", description="Blacklist un utilisateur") @app_commands.describe(user="Utilisateur à blacklister", reason="Raison") - async def blacklist_slash(self, interaction: discord.Interaction, user: discord.User, reason: str = "Aucune raison fournie"): + async def blacklist_slash(self, interaction: disnake.Interaction, user: disnake.User, reason: str = "Aucune raison fournie"): if not self.is_agent(interaction.user.id): return await interaction.response.send_message("⛔ Tu n’as pas la permission.", ephemeral=True) @@ -55,7 +55,7 @@ class Blacklist(commands.Cog): @app_commands.command(name="unblacklist", description="Retire un utilisateur de la blacklist") @app_commands.describe(user="Utilisateur à retirer") - async def unblacklist_slash(self, interaction: discord.Interaction, user: discord.User): + async def unblacklist_slash(self, interaction: disnake.Interaction, user: disnake.User): if not self.is_agent(interaction.user.id): return await interaction.response.send_message("⛔ Tu n’as pas la permission.", ephemeral=True) @@ -81,13 +81,13 @@ class Blacklist(commands.Cog): pass # --------- Boutons ---------- -class TicketButton(discord.ui.View): +class TicketButton(disnake.ui.View): def __init__(self, user_id: int): super().__init__(timeout=None) self.user_id = user_id - @discord.ui.button(label="📩 Ouvrir une demande de déblacklist", style=discord.ButtonStyle.danger) - async def open_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="📩 Ouvrir une demande de déblacklist", style=disnake.ButtonStyle.danger) + async def open_ticket(self, button: disnake.ui.Button, interaction: disnake.Interaction): if interaction.user.id != self.user_id: return await interaction.response.send_message("❌ Ce bouton ne t’appartient pas.", ephemeral=True) @@ -110,13 +110,13 @@ class TicketButton(discord.ui.View): await interaction.response.send_message("✅ Ton ticket a été créé en MP avec le bot.", ephemeral=True) -class CloseTicketView(discord.ui.View): +class CloseTicketView(disnake.ui.View): def __init__(self, user_id: int): super().__init__(timeout=None) self.user_id = user_id - @discord.ui.button(label="❌ Fermer le ticket", style=discord.ButtonStyle.danger) - async def close_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="❌ Fermer le ticket", style=disnake.ButtonStyle.danger) + async def close_ticket(self, button: disnake.ui.Button, interaction: disnake.Interaction): agents = load_json(AGENTS_FILE, {"agents": []}) if str(interaction.user.id) not in agents["agents"]: return await interaction.response.send_message("⛔ Seuls les agents Omega Kube peuvent fermer un ticket.", ephemeral=True) @@ -124,13 +124,13 @@ class CloseTicketView(discord.ui.View): view = ConfirmCloseView(self.user_id) await interaction.response.send_message("⚠️ Veux-tu vraiment fermer ce ticket ?", view=view, ephemeral=True) -class ConfirmCloseView(discord.ui.View): +class ConfirmCloseView(disnake.ui.View): def __init__(self, user_id: int): super().__init__(timeout=30) self.user_id = user_id - @discord.ui.button(label="✅ Oui", style=discord.ButtonStyle.success) - async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="✅ Oui", style=disnake.ButtonStyle.success) + async def confirm(self, button: disnake.ui.Button, interaction: disnake.Interaction): agents = load_json(AGENTS_FILE, {"agents": []}) if str(interaction.user.id) not in agents["agents"]: return await interaction.response.send_message("⛔ Tu n’as pas la permission.", ephemeral=True) @@ -142,12 +142,12 @@ class ConfirmCloseView(discord.ui.View): await interaction.channel.send("✅ Ticket fermé.") await interaction.channel.delete() - @discord.ui.button(label="❌ Non", style=discord.ButtonStyle.secondary) - async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button): + @disnake.ui.button(label="❌ Non", style=disnake.ButtonStyle.secondary) + async def cancel(self, button: disnake.ui.Button, interaction: disnake.Interaction): await interaction.response.send_message("❌ Fermeture annulée.", ephemeral=True) # --------- Setup Cog ---------- -async def setup(bot: commands.Bot): +def setup(bot: commands.Bot): cog = Blacklist(bot) await bot.add_cog(cog) - await bot.tree.sync(guild=discord.Object(id=MAIN_GUILD_ID)) + await bot.tree.sync(guild=disnake.Object(id=MAIN_GUILD_ID)) diff --git a/commandes/absence_staff.py b/commandes/absence_staff.py index 53a79c3..689cc1e 100755 --- a/commandes/absence_staff.py +++ b/commandes/absence_staff.py @@ -83,12 +83,12 @@ def parse_date(date_str: str): class AbsenceModal(disnake.ui.Modal): def __init__(self, superieur: Optional[disnake.Member] = None): - super().__init__(title="Déclarer une Absence Staff") + super().__init__(title="Déclarer une Absence Staff", components=[self.motif, self.date_debut, self.date_fin]) self.superieur = superieur motif = disnake.ui.TextInput( label="Motif de l'absence", - style=disnake.TextStyle.paragraph, + style=disnake.TextInputStyle.paragraph, placeholder="Ex: Vacances, Raisons personnelles, Travail...", required=True, max_length=500 @@ -187,7 +187,7 @@ class AbsenceConfigView(disnake.ui.View): self.guild_id = guild_id @disnake.ui.button(label="Définir le Salon", style=disnake.ButtonStyle.primary, emoji="📁") - async def set_channel(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def set_channel(self, button: disnake.ui.Button, interaction: disnake.Interaction): await interaction.response.send_message("Mentionne le salon ici (ou envoie son ID) :", ephemeral=True) def check(m): @@ -214,7 +214,7 @@ class AbsenceConfigView(disnake.ui.View): await interaction.followup.send("❌ Temps écoulé.", ephemeral=True) @disnake.ui.button(label="Définir le Rôle", style=disnake.ButtonStyle.primary, emoji="🎭") - async def set_role(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def set_role(self, button: disnake.ui.Button, interaction: disnake.Interaction): await interaction.response.send_message("Mentionne le rôle ici (ou envoie son ID) :", ephemeral=True) def check(m): @@ -499,7 +499,7 @@ class Absence(commands.Cog): settings = load_settings(message.guild.id) channel_id = settings.get("absence_channel_id") - absence_url = f"https://discord.com/channels/{message.guild.id}/{channel_id}/{msg_id}" if channel_id and msg_id else "" + absence_url = f"https://disnake.com/channels/{message.guild.id}/{channel_id}/{msg_id}" if channel_id and msg_id else "" reply_text = f"⚠️ {message.author.mention}, cet utilisateur est actuellement absent." if absence_url: @@ -507,5 +507,5 @@ class Absence(commands.Cog): await message.reply(reply_text, delete_after=30) -async def setup(bot): - await bot.add_cog(Absence(bot)) +def setup(bot): + bot.add_cog(Absence(bot)) diff --git a/commandes/backup.py b/commandes/backup.py index 5cafb7c..53c0293 100755 --- a/commandes/backup.py +++ b/commandes/backup.py @@ -268,5 +268,5 @@ class Backup(commands.Cog): await interaction.followup.send(embed=embed,ephemeral=True) -async def setup(bot: commands.Bot): - await bot.add_cog(Backup(bot)) +def setup(bot): + bot.add_cog(Backup(bot)) diff --git a/commandes/backup_restore.py b/commandes/backup_restore.py index d7f69cb..f51cdbc 100755 --- a/commandes/backup_restore.py +++ b/commandes/backup_restore.py @@ -198,5 +198,5 @@ class BackupRestore(commands.Cog): async def element_autocomplete_decorator(self, interaction: disnake.ApplicationCommandInteraction, current: str): return await self.element_autocomplete(interaction, current) -async def setup(bot: commands.Bot): - await bot.add_cog(BackupRestore(bot)) +def setup(bot): + bot.add_cog(BackupRestore(bot)) diff --git a/commandes/blacklist.py b/commandes/blacklist.py index a64f0b0..28d2288 100755 --- a/commandes/blacklist.py +++ b/commandes/blacklist.py @@ -200,7 +200,7 @@ class TicketButton(disnake.ui.View): self.user_id = user_id @disnake.ui.button(label="📩 Ouvrir une demande de révision", style=disnake.ButtonStyle.primary, custom_id="blacklist_open_ticket") - async def open_ticket(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def open_ticket(self, button: disnake.ui.Button, interaction: disnake.Interaction): if interaction.user.id != self.user_id: return await interaction.response.send_message("❌ Ce bouton ne t’appartient pas.", ephemeral=True) @@ -239,7 +239,7 @@ class CloseTicketView(disnake.ui.View): self.user_id = user_id @disnake.ui.button(label="❌ Fermer le ticket", style=disnake.ButtonStyle.danger, custom_id="blacklist_close_ticket") - async def close_ticket(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def close_ticket(self, button: disnake.ui.Button, interaction: disnake.Interaction): if not interaction.bot.get_cog("Blacklist").is_agent(interaction.user.id): return await interaction.response.send_message("⛔ Seuls les agents peuvent fermer ce ticket.", ephemeral=True) @@ -252,7 +252,7 @@ class ConfirmCloseView(disnake.ui.View): self.user_id = user_id @disnake.ui.button(label="✅ Oui", style=disnake.ButtonStyle.success, custom_id="blacklist_confirm_close") - async def confirm(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def confirm(self, button: disnake.ui.Button, interaction: disnake.Interaction): if not interaction.bot.get_cog("Blacklist").is_agent(interaction.user.id): return await interaction.response.send_message("⛔ Tu n’as pas la permission.", ephemeral=True) @@ -290,9 +290,9 @@ class ConfirmCloseView(disnake.ui.View): await interaction.response.send_message("✅ Ticket fermé avec succès.", ephemeral=True) @disnake.ui.button(label="❌ Non", style=disnake.ButtonStyle.secondary, custom_id="blacklist_cancel_close") - async def cancel(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def cancel(self, button: disnake.ui.Button, interaction: disnake.Interaction): await interaction.response.send_message("❌ Fermeture annulée.", ephemeral=True) # --------- Setup --------- -async def setup(bot: commands.Bot): - await bot.add_cog(Blacklist(bot)) +def setup(bot): + bot.add_cog(Blacklist(bot)) diff --git a/commandes/bug_report.py b/commandes/bug_report.py index 6bbc736..0631984 100644 --- a/commandes/bug_report.py +++ b/commandes/bug_report.py @@ -34,7 +34,7 @@ def save_report(issue_iid, user_id): class BugReportModal(disnake.ui.Modal): def __init__(self, priority_choice): - super().__init__(title="Signaler un Bug") + super().__init__(title="Signaler un Bug", components=[self.bug_title, self.description]) self.priority_choice = priority_choice bug_title = disnake.ui.TextInput( @@ -46,7 +46,7 @@ class BugReportModal(disnake.ui.Modal): description = disnake.ui.TextInput( label="Description détaillée", - style=disnake.TextStyle.paragraph, + style=disnake.TextInputStyle.paragraph, placeholder="Que s'est-il passé ? Comment reproduire le bug ?", required=True, max_length=1000 @@ -77,7 +77,7 @@ class BugReportModal(disnake.ui.Modal): class FeatureSuggestionModal(disnake.ui.Modal): def __init__(self): - super().__init__(title="Suggérer une fonctionnalité") + super().__init__(title="Suggérer une fonctionnalité", components=[self.suggestion_title, self.description]) suggestion_title = disnake.ui.TextInput( label="Titre de la suggestion", @@ -88,7 +88,7 @@ class FeatureSuggestionModal(disnake.ui.Modal): description = disnake.ui.TextInput( label="Détails de la fonctionnalité", - style=disnake.TextStyle.paragraph, + style=disnake.TextInputStyle.paragraph, placeholder="Décrivez comment cela devrait fonctionner...", required=True, max_length=1000 @@ -334,5 +334,5 @@ class BugReport(commands.Cog): async def suggerer_fonctionnalite(self, interaction: disnake.ApplicationCommandInteraction): await interaction.response.send_modal(FeatureSuggestionModal()) -async def setup(bot): - await bot.add_cog(BugReport(bot)) +def setup(bot): + bot.add_cog(BugReport(bot)) diff --git a/commandes/convocation.py b/commandes/convocation.py index bb3921f..88253d7 100755 --- a/commandes/convocation.py +++ b/commandes/convocation.py @@ -274,5 +274,5 @@ class Convocation(commands.Cog): await interaction.response.send_message(embed=embed, ephemeral=True) -async def setup(bot): - await bot.add_cog(Convocation(bot)) +def setup(bot): + bot.add_cog(Convocation(bot)) diff --git a/commandes/goodbye.py b/commandes/goodbye.py index 9e4497b..ad3f87a 100644 --- a/commandes/goodbye.py +++ b/commandes/goodbye.py @@ -218,5 +218,5 @@ class Goodbye(commands.Cog): await interaction.response.send_message(msg, ephemeral=True) -async def setup(bot): - await bot.add_cog(Goodbye(bot)) \ No newline at end of file +def setup(bot): + bot.add_cog(Goodbye(bot)) \ No newline at end of file diff --git a/commandes/moderation.py b/commandes/moderation.py index 3f1174e..e250046 100644 --- a/commandes/moderation.py +++ b/commandes/moderation.py @@ -7,7 +7,7 @@ import re class ModReasonModal(disnake.ui.Modal): def __init__(self, parent_view, action_type): - super().__init__(title=f"Raison pour {action_type}") + super().__init__(title=f"Raison pour {action_type}", components=[]) self.parent_view = parent_view self.action_type = action_type @@ -17,7 +17,7 @@ class ModReasonModal(disnake.ui.Modal): required=True, max_length=500 ) - self.add_item(self.reason) + self.append_component(self.reason) if action_type == "TIMEOUT": self.duration = disnake.ui.TextInput( @@ -26,7 +26,7 @@ class ModReasonModal(disnake.ui.Modal): required=True, max_length=10 ) - self.add_item(self.duration) + self.append_component(self.duration) async def callback(self, interaction: disnake.Interaction): reason = self.reason.value @@ -78,43 +78,43 @@ class ModPanelView(disnake.ui.View): await interaction.edit_original_response(embed=embed, view=self) @disnake.ui.button(label="Avertir", style=disnake.ButtonStyle.secondary, emoji="⚠️") - async def warn_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def warn_btn(self, button: disnake.ui.Button, interaction: disnake.Interaction): await interaction.response.send_modal(ModReasonModal(self, "WARN")) @disnake.ui.button(label="Timeout", style=disnake.ButtonStyle.secondary, emoji="⏳") - async def timeout_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def timeout_btn(self, button: disnake.ui.Button, interaction: disnake.Interaction): await interaction.response.send_modal(ModReasonModal(self, "TIMEOUT")) @disnake.ui.button(label="Expulser", style=disnake.ButtonStyle.secondary, emoji="👢") - async def kick_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def kick_btn(self, button: disnake.ui.Button, interaction: disnake.Interaction): await interaction.response.send_modal(ModReasonModal(self, "KICK")) @disnake.ui.button(label="Bannir", style=disnake.ButtonStyle.danger, emoji="🔨") - async def ban_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def ban_btn(self, button: disnake.ui.Button, interaction: disnake.Interaction): await interaction.response.send_modal(ModReasonModal(self, "BAN")) @disnake.ui.button(label="Historique", style=disnake.ButtonStyle.primary, emoji="📜") - async def history_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def history_btn(self, button: disnake.ui.Button, interaction: disnake.Interaction): cog = interaction.bot.get_cog("Moderation") await cog.history(interaction, self.member) @disnake.ui.button(label="Clear Warns", style=disnake.ButtonStyle.danger, emoji="🧹") - async def clear_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def clear_btn(self, button: disnake.ui.Button, interaction: disnake.Interaction): cog = interaction.bot.get_cog("Moderation") await cog.clearwarns(interaction, self.member, reason="Nettoyage via Panel") await self.update_panel(interaction) class ModConfigModal(disnake.ui.Modal): def __init__(self, field_name, current_value): - super().__init__(title=f"Modifier {field_name}") + super().__init__(components=[], title=f"Modifier {field_name}") self.field_name = field_name self.input = disnake.ui.TextInput( label=field_name, - default=str(current_value), + value=str(current_value), placeholder="Entrez une valeur numérique...", required=True ) - self.add_item(self.input) + self.append_component(self.input) async def callback(self, interaction: disnake.Interaction): value = self.input.value @@ -146,7 +146,7 @@ class ModConfigView(disnake.ui.View): self.db_path = db_path @disnake.ui.button(label="Log Channel", style=disnake.ButtonStyle.primary, emoji="📁") - async def log_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def log_btn(self, button: disnake.ui.Button, interaction: disnake.Interaction): view = disnake.ui.View() select = disnake.ui.ChannelSelect( placeholder="Sélectionnez le salon de logs...", @@ -166,7 +166,7 @@ class ModConfigView(disnake.ui.View): await interaction.response.send_message("Choisissez le salon de logs :", view=view, ephemeral=True) @disnake.ui.button(label="Paliers Warns", style=disnake.ButtonStyle.secondary, emoji="⚖️") - async def limits_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def limits_btn(self, button: disnake.ui.Button, interaction: disnake.Interaction): conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute("SELECT warn_limit_timeout, warn_limit_kick, warn_limit_ban FROM mod_config WHERE guild_id = ?", (self.guild_id,)) @@ -187,7 +187,7 @@ class ModConfigView(disnake.ui.View): await interaction.response.send_message("Quelle limite modifier ?", view=view, ephemeral=True) @disnake.ui.button(label="Durée Timeout", style=disnake.ButtonStyle.secondary, emoji="⏱️") - async def duration_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def duration_btn(self, button: disnake.ui.Button, interaction: disnake.Interaction): conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute("SELECT timeout_duration FROM mod_config WHERE guild_id = ?", (self.guild_id,)) @@ -320,5 +320,5 @@ class Moderation(commands.Cog): if not interaction.response.is_done(): await interaction.response.send_message(f"✅ Warns de {member.name} effacés.", ephemeral=True) else: await interaction.followup.send(f"✅ Warns de {member.name} effacés.", ephemeral=True) -async def setup(bot): - await bot.add_cog(Moderation(bot)) +def setup(bot): + bot.add_cog(Moderation(bot)) diff --git a/commandes/modules_security/antiraid.py b/commandes/modules_security/antiraid.py index 6ad47ad..0244034 100644 --- a/commandes/modules_security/antiraid.py +++ b/commandes/modules_security/antiraid.py @@ -56,5 +56,5 @@ class AntiRaid(commands.Cog): embed = disnake.Embed(title="🚨 ALERTE RAID", description=f"Vague de {len(self.join_cache[guild.id])} membres !", color=0xFF0000) await channel.send(embed=embed) -async def setup(bot): - await bot.add_cog(AntiRaid(bot)) \ No newline at end of file +def setup(bot): + bot.add_cog(AntiRaid(bot)) \ No newline at end of file diff --git a/commandes/modules_security/antispam.py b/commandes/modules_security/antispam.py index fef7ba2..19c8969 100644 --- a/commandes/modules_security/antispam.py +++ b/commandes/modules_security/antispam.py @@ -13,7 +13,7 @@ WHITELIST_FILE = "data/whitelist.json" CACHE_TTL = 5 # Triggers pour détection de liens -LINK_TRIGGERS = ("discord.gg/", "http://", "https://", "www.") +LINK_TRIGGERS = ("disnake.gg/", "http://", "https://", "www.") class AntiSpam(commands.Cog): def __init__(self, bot): @@ -181,5 +181,5 @@ class AntiSpam(commands.Cog): except Exception as e: print(f"Erreur lors de la sanction : {e}") -async def setup(bot): - await bot.add_cog(AntiSpam(bot)) \ No newline at end of file +def setup(bot): + bot.add_cog(AntiSpam(bot)) \ No newline at end of file diff --git a/commandes/modules_security/logs_manager.py b/commandes/modules_security/logs_manager.py index dc3238c..ce97861 100644 --- a/commandes/modules_security/logs_manager.py +++ b/commandes/modules_security/logs_manager.py @@ -58,5 +58,5 @@ class LogsManager(commands.Cog): ) await log_chan.send(embed=embed) -async def setup(bot): - await bot.add_cog(LogsManager(bot)) \ No newline at end of file +def setup(bot): + bot.add_cog(LogsManager(bot)) \ No newline at end of file diff --git a/commandes/modules_security/rules.py b/commandes/modules_security/rules.py index 6f5999a..2373eb8 100644 --- a/commandes/modules_security/rules.py +++ b/commandes/modules_security/rules.py @@ -38,7 +38,7 @@ def save_settings(guild_id: int, settings: dict) -> None: # --- MODALS DE CONFIGURATION DU RÈGLEMENT (BACK-END) --- -class RulesTitleModal(disnake.ui.Modal, title="📜 Titre du Règlement"): +class RulesTitleModal(disnake.ui.Modal): """Modal pour configurer le titre du règlement""" title_input = disnake.ui.TextInput( label="Titre du règlement", @@ -49,12 +49,12 @@ class RulesTitleModal(disnake.ui.Modal, title="📜 Titre du Règlement"): ) def __init__(self, guild_id: int, settings: dict, on_update_callback=None): - super().__init__() + super().__init__(title="📜 Titre du Règlement", components=[self.title_input]) self.guild_id = guild_id self.settings = settings self.on_update_callback = on_update_callback - async def on_submit(self, interaction: disnake.Interaction): + async def callback(self, interaction: disnake.Interaction): self.settings["rules_title"] = self.title_input.value save_settings(self.guild_id, self.settings) @@ -77,24 +77,24 @@ class RulesTitleModal(disnake.ui.Modal, title="📜 Titre du Règlement"): return view -class RulesContentModal(disnake.ui.Modal, title="📝 Contenu du Règlement"): +class RulesContentModal(disnake.ui.Modal): """Modal pour configurer le contenu du règlement""" content_input = disnake.ui.TextInput( label="Contenu du règlement", placeholder="Entrez les règles de votre serveur...", - style=disnake.TextStyle.paragraph, + style=disnake.TextInputStyle.paragraph, min_length=10, max_length=4000, required=True ) def __init__(self, guild_id: int, settings: dict, on_update_callback=None): - super().__init__() + super().__init__(title="📝 Contenu du Règlement", components=[self.content_input]) self.guild_id = guild_id self.settings = settings self.on_update_callback = on_update_callback - async def on_submit(self, interaction: disnake.Interaction): + async def callback(self, interaction: disnake.Interaction): self.settings["rules_content"] = self.content_input.value save_settings(self.guild_id, self.settings) @@ -117,7 +117,7 @@ class RulesContentModal(disnake.ui.Modal, title="📝 Contenu du Règlement"): return view -class RulesChannelModal(disnake.ui.Modal, title="📌 Salon du Règlement"): +class RulesChannelModal(disnake.ui.Modal): """Modal pour sélectionner le salon du règlement""" channel_input = disnake.ui.TextInput( label="ID du salon", @@ -128,12 +128,12 @@ class RulesChannelModal(disnake.ui.Modal, title="📌 Salon du Règlement"): ) def __init__(self, guild_id: int, settings: dict, on_update_callback=None): - super().__init__() + super().__init__(title="📌 Salon du Règlement", components=[self.channel_input]) self.guild_id = guild_id self.settings = settings self.on_update_callback = on_update_callback - async def on_submit(self, interaction: disnake.Interaction): + async def callback(self, interaction: disnake.Interaction): if not self.channel_input.value.isdigit(): return await interaction.response.send_message( "❌ Erreur : ID invalide.", @@ -171,7 +171,7 @@ class RulesChannelModal(disnake.ui.Modal, title="📌 Salon du Règlement"): return view -class RulesRoleModal(disnake.ui.Modal, title="✅ Rôle d'Acceptation"): +class RulesRoleModal(disnake.ui.Modal): """Modal pour configurer le rôle attribué à l'acceptation""" role_input = disnake.ui.TextInput( label="ID du rôle", @@ -182,12 +182,12 @@ class RulesRoleModal(disnake.ui.Modal, title="✅ Rôle d'Acceptation"): ) def __init__(self, guild_id: int, settings: dict, on_update_callback=None): - super().__init__() + super().__init__(title="✅ Rôle d'Acceptation", components=[self.role_input]) self.guild_id = guild_id self.settings = settings self.on_update_callback = on_update_callback - async def on_submit(self, interaction: disnake.Interaction): + async def callback(self, interaction: disnake.Interaction): if not self.role_input.value.isdigit(): return await interaction.response.send_message( "❌ Erreur : ID invalide.", @@ -225,7 +225,7 @@ class RulesRoleModal(disnake.ui.Modal, title="✅ Rôle d'Acceptation"): return view -class RulesMessageTypeModal(disnake.ui.Modal, title="📝 Type de Message"): +class RulesMessageTypeModal(disnake.ui.Modal): """Modal pour choisir le type de message du règlement""" message_type = disnake.ui.TextInput( label="Type de message", @@ -236,12 +236,12 @@ class RulesMessageTypeModal(disnake.ui.Modal, title="📝 Type de Message"): ) def __init__(self, guild_id: int, settings: dict, on_update_callback=None): - super().__init__() + super().__init__(title="📝 Type de Message", components=[self.message_type]) self.guild_id = guild_id self.settings = settings self.on_update_callback = on_update_callback - async def on_submit(self, interaction: disnake.Interaction): + async def callback(self, interaction: disnake.Interaction): msg_type = self.message_type.value.lower().strip() if msg_type not in ["embed", "simple"]: @@ -323,13 +323,13 @@ class RulesAcceptButton(disnake.ui.View): self.guild_id = guild_id self.accept_role_id = accept_role_id - @discord.ui.button( + @disnake.ui.button( label="J'accepte le règlement", emoji="✅", style=disnake.ButtonStyle.green, custom_id="rules_accept_button" ) - async def accept_rules(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def accept_rules(self, button: disnake.ui.Button, interaction: disnake.Interaction): """Gère le clic sur le bouton d'acceptation""" guild = interaction.guild @@ -495,8 +495,8 @@ class RulesCog(commands.Cog): return title, content -async def setup(bot): +def setup(bot): """Setup du cog de règlement""" cog = RulesCog(bot) - await bot.add_cog(cog) + bot.add_cog(cog) diff --git a/commandes/no_link.py b/commandes/no_link.py index e3ad27b..3ce67b6 100755 --- a/commandes/no_link.py +++ b/commandes/no_link.py @@ -47,5 +47,5 @@ class NoLink(commands.Cog): print(f"Erreur lors de la suppression du message : {exc}") -async def setup(bot: commands.Bot) -> None: - await bot.add_cog(NoLink(bot)) +def setup(bot: commands.Bot) -> None: + bot.add_cog(NoLink(bot)) diff --git a/commandes/scan.py b/commandes/scan.py index db9c1d4..1ccb2c9 100755 --- a/commandes/scan.py +++ b/commandes/scan.py @@ -77,5 +77,5 @@ class Scan(commands.Cog): await interaction.followup.send(f"✅ Fichier sain.\nHash : `{result['hash']}`") -async def setup(bot: commands.Bot): - await bot.add_cog(Scan(bot)) +def setup(bot): + bot.add_cog(Scan(bot)) diff --git a/commandes/security.py b/commandes/security.py index a839a93..4e3909c 100755 --- a/commandes/security.py +++ b/commandes/security.py @@ -69,7 +69,7 @@ def is_immune(guild, user) -> bool: # --- MODALS DE CONFIGURATION --- -class SpamThresholdModal(disnake.ui.Modal, title="🛡️ Seuil Anti-Spam"): +class SpamThresholdModal(disnake.ui.Modal): """Modal pour configurer le seuil anti-spam""" threshold_input = disnake.ui.TextInput( label="Messages max autorisés", @@ -80,12 +80,12 @@ class SpamThresholdModal(disnake.ui.Modal, title="🛡️ Seuil Anti-Spam"): ) def __init__(self, guild_id: int, settings: dict, view: 'SecurityView'): - super().__init__() + super().__init__(title="🛡️ Seuil Anti-Spam", components=[self.threshold_input]) self.guild_id = guild_id self.settings = settings self.parent_view = view - async def on_submit(self, interaction: disnake.Interaction): + async def callback(self, interaction: disnake.Interaction): if not self.threshold_input.value.isdigit(): return await interaction.response.send_message( "❌ Erreur : Veuillez entrer un nombre entier.", @@ -110,7 +110,7 @@ class SpamThresholdModal(disnake.ui.Modal, title="🛡️ Seuil Anti-Spam"): ephemeral=True ) -class RaidThresholdModal(disnake.ui.Modal, title="⚔️ Seuil Anti-Raid"): +class RaidThresholdModal(disnake.ui.Modal): """Modal pour configurer le seuil anti-raid""" threshold_input = disnake.ui.TextInput( label="Joins max en 10 secondes", @@ -121,12 +121,12 @@ class RaidThresholdModal(disnake.ui.Modal, title="⚔️ Seuil Anti-Raid"): ) def __init__(self, guild_id: int, settings: dict, view: 'SecurityView'): - super().__init__() + super().__init__(title="⚔️ Seuil Anti-Raid", components=[self.threshold_input]) self.guild_id = guild_id self.settings = settings self.parent_view = view - async def on_submit(self, interaction: disnake.Interaction): + async def callback(self, interaction: disnake.Interaction): if not self.threshold_input.value.isdigit(): return await interaction.response.send_message( "❌ Erreur : Veuillez entrer un nombre entier.", @@ -153,7 +153,7 @@ class RaidThresholdModal(disnake.ui.Modal, title="⚔️ Seuil Anti-Raid"): # --- MODALS DE CONFIGURATION WHITELIST --- -class WhitelistModal(disnake.ui.Modal, title="👤 Ajouter à la Whitelist"): +class WhitelistModal(disnake.ui.Modal): """Modal pour ajouter un utilisateur à la whitelist""" user_input = disnake.ui.TextInput( label="ID de l'utilisateur", @@ -164,11 +164,11 @@ class WhitelistModal(disnake.ui.Modal, title="👤 Ajouter à la Whitelist"): ) def __init__(self, guild_id: int, view: 'SecurityView'): - super().__init__() + super().__init__(title="👤 Ajouter à la Whitelist", components=[self.user_input]) self.guild_id = guild_id self.parent_view = view - async def on_submit(self, interaction: disnake.Interaction): + async def callback(self, interaction: disnake.Interaction): if not self.user_input.value.isdigit(): return await interaction.response.send_message( "❌ Erreur : ID invalide.", @@ -873,7 +873,7 @@ class MainCategorySelect(disnake.ui.Select): class WhitelistSelect(disnake.ui.Select): """Menu dropdown pour voir et gérer les utilisateurs whitelistés""" - def __init__(self, guild: discord.Guild, view: 'SecurityView'): + def __init__(self, guild: disnake.Guild, view: 'SecurityView'): self.guild = guild self.parent_view = view whitelist = load_whitelist(guild.id) @@ -950,11 +950,11 @@ class Security(commands.Cog): def __init__(self, bot): self.bot = bot - @app_commands.command( + @commands.slash_command( name="security", description="🛡️ Ouvrir le panneau de sécurité Kuby" ) - async def security(self, interaction: disnake.Interaction): + async def security(self, interaction: disnake.ApplicationCommandInteraction): """ Commande principale pour ouvrir l'interface de sécurité. Accessible uniquement aux administrateurs et propriétaires whitelistés. @@ -989,12 +989,12 @@ class Security(commands.Cog): ) @security.error - async def security_error(self, interaction: disnake.Interaction, error: app_commands.AppCommandError): + async def security_error(self, interaction: disnake.Interaction, error: Exception): """Gestion des erreurs de la commande security""" # Utilisation de followup si l'interaction est déjà différée ou répondue send_method = interaction.followup.send if interaction.response.is_done() else interaction.response.send_message - if isinstance(error, app_commands.MissingPermissions): + if isinstance(error, commands.MissingPermissions): await send_method( "❌ Permissions insuffisantes.", ephemeral=True @@ -1006,7 +1006,7 @@ class Security(commands.Cog): ) -async def setup(bot): +def setup(bot): """Setup du cog de sécurité""" - await bot.add_cog(Security(bot)) + bot.add_cog(Security(bot)) diff --git a/commandes/sendbotmessages.py b/commandes/sendbotmessages.py index 058c2e4..e77819e 100755 --- a/commandes/sendbotmessages.py +++ b/commandes/sendbotmessages.py @@ -20,6 +20,6 @@ class SentBotMessages(commands.Cog): except Exception as e: await interaction.response.send_message(f"❌ Une erreur est survenue : {e}", ephemeral=True) -async def setup(bot): - await bot.add_cog(SentBotMessages(bot)) +def setup(bot): + bot.add_cog(SentBotMessages(bot)) diff --git a/commandes/snipe.py b/commandes/snipe.py index f47cacc..60b4b20 100755 --- a/commandes/snipe.py +++ b/commandes/snipe.py @@ -109,5 +109,5 @@ class SnipeCommands(commands.Cog): ephemeral=True ) -async def setup(bot): - await bot.add_cog(SnipeCommands(bot)) +def setup(bot): + bot.add_cog(SnipeCommands(bot)) diff --git a/commandes/staff_leaderboard.py b/commandes/staff_leaderboard.py index 9d1007d..d3b7b86 100644 --- a/commandes/staff_leaderboard.py +++ b/commandes/staff_leaderboard.py @@ -68,5 +68,5 @@ class StaffLeaderboard(commands.Cog): await interaction.followup.send("Une erreur est survenue lors de la génération du classement.", ephemeral=True) -async def setup(bot): - await bot.add_cog(StaffLeaderboard(bot)) \ No newline at end of file +def setup(bot): + bot.add_cog(StaffLeaderboard(bot)) \ No newline at end of file diff --git a/commandes/staff_ratings.py b/commandes/staff_ratings.py index a777da0..e44b0dd 100644 --- a/commandes/staff_ratings.py +++ b/commandes/staff_ratings.py @@ -133,5 +133,5 @@ class StaffRatings(commands.Cog): return buf -async def setup(bot): - await bot.add_cog(StaffRatings(bot)) \ No newline at end of file +def setup(bot): + bot.add_cog(StaffRatings(bot)) \ No newline at end of file diff --git a/commandes/ticket.py b/commandes/ticket.py.old similarity index 100% rename from commandes/ticket.py rename to commandes/ticket.py.old diff --git a/commandes/ticket/__init__.py b/commandes/ticket/__init__.py index 1b2441b..99bfe6d 100644 --- a/commandes/ticket/__init__.py +++ b/commandes/ticket/__init__.py @@ -38,6 +38,8 @@ from .utils.formatters import TicketEmbedFormatter from .staff_analytics import StaffAnalytics, StaffStatsView from .utils.permissions import is_staff, is_whitelisted, can_access_config from src.logger import kuby_logger +# V2 RICH COMPONENTS LOADED - 14:10 +kuby_logger.info("🎫 [TicketSystem] Chargement de la version V2 Rich Components...") # Add a task for sending feedback reminders class FeedbackReminderTask: @@ -103,6 +105,43 @@ class TicketPanelView(disnake.ui.View): ) staff_ratings_btn.callback = self._staff_ratings_callback self.add_item(staff_ratings_btn) + + def get_components_v2(self): + """Retourne les composants V2 pour un affichage moderne""" + return [ + disnake.ui.Container( + disnake.ui.Section( + "# 🎫 Système de Support - Omega Kube", + accessory=disnake.ui.Button( + label="Créer un ticket", + style=disnake.ButtonStyle.blurple, + custom_id="ticket_create" + ) + ), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay( + content="Bienvenue sur le système de support d'**Omega Kube**.\n" + "Notre équipe est à votre disposition pour vous aider dans les plus brefs délais." + ), + disnake.ui.Separator(divider=True), + disnake.ui.Section( + f"📂 {len(self.config.categories)} Catégories disponibles", + accessory=disnake.ui.Button( + label="Mes tickets", + style=disnake.ButtonStyle.secondary, + custom_id="ticket_my_tickets" + ) + ), + disnake.ui.Section( + "📊 Administration & Staff", + accessory=disnake.ui.Button( + label="Analytics", + style=disnake.ButtonStyle.primary, + custom_id="ticket_analytics" + ) + ) + ) + ] # Configuration (admin only) config_btn = disnake.ui.Button( @@ -132,23 +171,42 @@ class TicketPanelView(disnake.ui.View): await interaction.response.send_message("Aucune catégorie configurée. Utilisez la configuration.", ephemeral=True) return - # Créer view avec les catégories - view = CategorySelectionView(self.bot, self.config) - - embed = disnake.Embed( - title="Créer un Ticket", - description="Sélectionnez une catégorie ci-dessous:", - color=disnake.Color.blurple() - ) + # Build V2 Container for category selection + sections = [ + disnake.ui.TextDisplay(content="## 🎫 Créer un Ticket"), + disnake.ui.Separator(), + ] for cat in self.config.categories[:5]: - embed.add_field( - name=f"{cat.emoji} {cat.name}", - value=cat.description or "Aucune description", - inline=True + desc = cat.description or "Aucune description" + sections.append( + disnake.ui.Section( + f"**{cat.emoji} {cat.name}**\n{desc}", + accessory=disnake.ui.Button( + label=cat.name, + style=disnake.ButtonStyle.primary, + custom_id=f"cat_{cat.id}_{int(disnake.utils.time_snowflake(disnake.utils.utcnow()) / 1000)}" + ) + ) ) - await interaction.response.send_message(embed=embed, view=view, ephemeral=True) + try: + await interaction.response.send_message( + components=[disnake.ui.Container(*sections)], + flags=disnake.MessageFlags(is_components_v2=True), + ephemeral=True + ) + except Exception as e: + import traceback + traceback.print_exc() + # Fallback: texte simple avec view legacy + cat_list = "\n".join(f"{cat.emoji} **{cat.name}** — {cat.description or 'Aucune description'}" for cat in self.config.categories[:5]) + if not interaction.response.is_done(): + await interaction.response.send_message( + f"**🎫 Créer un Ticket**\n\nSélectionnez une catégorie ci-dessous:", + view=CategorySelectionView(self.bot, self.config), + ephemeral=True + ) async def _my_tickets_callback(self, interaction: disnake.Interaction): """Affiche les tickets de l'utilisateur""" @@ -166,22 +224,24 @@ class TicketPanelView(disnake.ui.View): await interaction.response.send_message("Vous n'avez aucun ticket ouvert.", ephemeral=True) return - embed = disnake.Embed( - title="Vos Tickets Ouverts", - description=f"Vous avez **{len(tickets)}** ticket(s) ouvert(s):", - color=disnake.Color.blue() - ) + # Build V2 Container for user's open tickets + sections = [ + disnake.ui.TextDisplay(content=f"## 🎫 Vos Tickets Ouverts"), + disnake.ui.TextDisplay(content=f"Vous avez **{len(tickets)}** ticket(s) ouvert(s):"), + disnake.ui.Separator(), + ] for ticket in tickets[:5]: channel = interaction.guild.get_channel(ticket.channel_id) channel_mention = channel.mention if channel else "Salon supprimé" - embed.add_field( - name=f"Ticket #{ticket.channel_id}", - value=f"Salon: {channel_mention}\nStatut: {ticket.status.value}\nCréé: {ticket.created_at.strftime('%d/%m/%Y')}", - inline=True - ) + sections.append(disnake.ui.TextDisplay( + content=f"**Ticket #{ticket.channel_id}**\nSalon: {channel_mention}\nStatut: {ticket.status.value}\nCréé: {ticket.created_at.strftime('%d/%m/%Y')}" + )) - await interaction.response.send_message(embed=embed, ephemeral=True) + await interaction.response.send_message( + components=[disnake.ui.Container(*sections)], + ephemeral=True + ) async def _analytics_callback(self, interaction: disnake.Interaction): """Affiche les analytics (staff uniquement)""" @@ -467,7 +527,16 @@ class CategorySelectionView(disnake.ui.View): # Check limits user_tickets = storage.get_user_tickets(interaction.guild_id, interaction.user.id, open_only=True) - if len(user_tickets) >= self.config.max_tickets_per_user: + active_tickets = [] + for t in user_tickets: + if interaction.guild.get_channel(t.channel_id): + active_tickets.append(t) + else: + # Automatically clean up phantom tickets + t.status = TicketStatus.CLOSED + storage.save_ticket(t) + + if len(active_tickets) >= self.config.max_tickets_per_user: await interaction.response.send_message( f"Vous avez atteint la limite de {self.config.max_tickets_per_user} tickets.", ephemeral=True @@ -487,35 +556,24 @@ class CategorySelectionView(disnake.ui.View): class TicketPriorityModal(disnake.ui.Modal): - """Modal pour sélectionner la priorité et la raison""" - - def __init__(self, bot, config: GuildTicketConfig, category: TicketCategory): - super().__init__(title=f" {category.name}") + """Modal pour spécifier la raison du ticket""" + def __init__(self, bot, config, category: TicketCategory): + self.reason_input = disnake.ui.TextInput( + label="Motif du ticket", + custom_id="ticket_reason", + placeholder="Décrivez brièvement votre problème...", + required=category.require_reason, + min_length=5 if category.require_reason else 0, + max_length=250, + style=disnake.TextInputStyle.paragraph + ) + super().__init__(title=f"Support: {category.name}", components=[self.reason_input]) self.bot = bot self.config = config self.category = category - - # Priorité - self.priority = disnake.ui.TextInput( - label="Priorité", - placeholder="Normale, Haute ou Urgente", - default="Normale", - required=True, - max_length=10 - ) - self.add_item(self.priority) - - # Raison - self.reason = disnake.ui.TextInput( - label="Motif de votre demande", - placeholder="Décrivez votre demande...", - style=disnake.TextStyle.paragraph, - required=True, - max_length=500 - ) - self.add_item(self.reason) - - async def on_submit(self, interaction: disnake.Interaction): + + async def callback(self, interaction: disnake.ModalInteraction): + await interaction.response.defer(ephemeral=True) try: # Map priority priority_map = { @@ -523,12 +581,14 @@ class TicketPriorityModal(disnake.ui.Modal): "Haute": Priority.HIGH, "Normale": Priority.NORMAL } - priority = priority_map.get(self.priority.value, Priority.NORMAL) + # Logic updated to use reason input + priority = Priority.NORMAL + reason = interaction.text_values.get("ticket_reason", "Aucun motif fourni") # Create ticket - await self._create_ticket(interaction, priority, self.reason.value) + await self._create_ticket(interaction, priority, reason) except Exception as e: - await interaction.response.send_message(f"Erreur lors de la création du ticket: {e}", ephemeral=True) + await interaction.followup.send(f"Erreur lors de la création du ticket: {e}", ephemeral=True) async def on_error(self, interaction: disnake.Interaction, error: Exception): """Handle errors in the modal""" @@ -572,6 +632,8 @@ class TicketPriorityModal(disnake.ui.Modal): if self.category.discord_category_id: try: category_channel = guild.get_channel(int(self.category.discord_category_id)) + if category_channel and not isinstance(category_channel, disnake.CategoryChannel): + category_channel = None except (ValueError, TypeError): pass @@ -579,6 +641,8 @@ class TicketPriorityModal(disnake.ui.Modal): if not category_channel and self.config.default_category: try: category_channel = guild.get_channel(int(self.config.default_category)) + if category_channel and not isinstance(category_channel, disnake.CategoryChannel): + category_channel = None except (ValueError, TypeError): pass @@ -589,7 +653,7 @@ class TicketPriorityModal(disnake.ui.Modal): topic=f"Ticket de {user} | Catégorie: {self.category.name}" ) except Exception as e: - await interaction.response.send_message(f"Erreur: {e}", ephemeral=True) + await interaction.followup.send(f"Erreur: {e}", ephemeral=True) return # Create ticket data @@ -604,19 +668,7 @@ class TicketPriorityModal(disnake.ui.Modal): ) storage.save_ticket(ticket_data) - # Send welcome message - embed = disnake.Embed( - title=f" {self.category.emoji} {self.category.name}", - description=f"Bienvenue {user.mention}!\n\nVotre demande sera traitée sous peu par notre équipe.", - color=disnake.Color.blurple() - ) - embed.add_field(name="Motif", value=reason, inline=False) - embed.add_field(name="Priorité", value=priority.value.capitalize(), inline=True) - embed.set_footer(text=f"Ticket #{ticket_channel.id}") - - # Add management buttons - view = TicketManagementView(self.bot, storage, self.config, ticket_data) - # Prepare mentions + # Build mentions mentions = [user.mention] for role_id in self.config.staff_roles: mentions.append(f"<@&{role_id}>") @@ -625,9 +677,13 @@ class TicketPriorityModal(disnake.ui.Modal): mention_content = " ".join(mentions) - # Add management buttons - view = TicketManagementView(self.bot, storage, self.config, ticket_data) - await ticket_channel.send(content=mention_content, embed=embed, view=view) + # Build a single V2 container with mention and ticket management components + mention_section = disnake.ui.TextDisplay(content=mention_content) + # Retrieve existing management container + base_container = get_ticket_management_container(ticket_data, self.category, self.config)[0] + # Build new container merging mention and existing sections + combined_container = disnake.ui.Container(mention_section, *base_container.children) + await ticket_channel.send(components=[combined_container]) # Log if self.config.log_channel_id: @@ -643,28 +699,25 @@ class TicketPriorityModal(disnake.ui.Modal): log_embed.add_field(name="Salon", value=ticket_channel.mention, inline=True) await log_channel.send(embed=log_embed) - await interaction.response.send_message(f"Ticket créé: {ticket_channel.mention}", ephemeral=True) + await interaction.followup.send(f"Ticket créé: {ticket_channel.mention}", ephemeral=True) class SetCategoryModal(disnake.ui.Modal): """Modal pour définir la catégorie Discord où les tickets seront créés""" def __init__(self, bot, storage, config: GuildTicketConfig): - super().__init__(title="Définir la Catégorie Discord") + self.category_input = disnake.ui.TextInput( + label="Catégorie Discord", + placeholder="Entrez l'ID de la catégorie...", + required=True + ) + super().__init__(title="Définir la Catégorie", components=[self.category_input]) self.bot = bot self.storage = storage self.config = config - - self.category = disnake.ui.TextInput( - label="Catégorie Discord", - placeholder="Entrez l'ID de la catégorie (clic droit -> Copier l'ID)", - required=True, - max_length=20 - ) - self.add_item(self.category) - async def on_submit(self, interaction: disnake.Interaction): - cat_id = self.category.value.strip() + async def callback(self, interaction: disnake.ModalInteraction): + cat_id = self.category_input.value.strip() # Verify it's a valid ID if not cat_id.isdigit(): @@ -683,8 +736,51 @@ class SetCategoryModal(disnake.ui.Modal): await interaction.response.send_message(f"📁 Les tickets seront désormais créés dans la catégorie **{category.name}**!", ephemeral=True) +def get_ticket_management_container(ticket: TicketData, category, config: GuildTicketConfig) -> list: + """Helper V2 pour générer les actions de gestion d'un ticket""" + sections = [] + + # Informations du ticket (Titre, description) + sections.append(disnake.ui.TextDisplay(content=f"# {category.emoji} {category.name}")) + sections.append(disnake.ui.Separator()) + + status_emoji = "🟢" if ticket.status == TicketStatus.OPEN else ("🟡" if ticket.status == TicketStatus.CLAIMED else "🔴") + status_text = "Ouvert" if ticket.status == TicketStatus.OPEN else ("Réclamé" if ticket.status == TicketStatus.CLAIMED else "Fermé") + + content = f"Bienvenue <@{ticket.user_id}>!\n\nVotre demande sera traitée sous peu par notre équipe.\n\n" + content += f"**Motif:** {ticket.reason or 'Aucun'}\n" + content += f"**Priorité:** {ticket.priority.value.capitalize()}\n" + content += f"**Statut:** {status_emoji} {status_text}" + + if ticket.claimed_by: + content += f"\n**Pris en charge par:** <@{ticket.claimed_by}>" + + sections.append(disnake.ui.TextDisplay(content=content)) + sections.append(disnake.ui.Separator()) + + # 1. Gestion Staff + if ticket.status == TicketStatus.OPEN and config.claim_enabled and category and category.allow_claims: + sections.append(disnake.ui.Section("🤝 Le staff peut prendre en charge le ticket", accessory=disnake.ui.Button(label="Réclamer", style=disnake.ButtonStyle.primary, custom_id="ticket_claim"))) + elif ticket.status == TicketStatus.CLAIMED: + sections.append(disnake.ui.Section("🤝 Gérer la réclamation", accessory=disnake.ui.Button(label="Abandonner", style=disnake.ButtonStyle.danger, custom_id="ticket_unclaim"))) + sections.append(disnake.ui.Section("🤝 Transférer", accessory=disnake.ui.Button(label="Transférer", style=disnake.ButtonStyle.secondary, custom_id="ticket_claim"))) + + # 2. Séparateur + if len(sections) > 0: + sections.append(disnake.ui.Separator(divider=True)) + + # 3. Actions générales + if ticket.status in [TicketStatus.OPEN, TicketStatus.CLAIMED]: + sections.append(disnake.ui.Section("🔒 Fermer le ticket", accessory=disnake.ui.Button(label="Fermer", style=disnake.ButtonStyle.danger, custom_id="ticket_close"))) + elif ticket.status == TicketStatus.CLOSED: + sections.append(disnake.ui.Section("🔓 Rouvrir le ticket", accessory=disnake.ui.Button(label="Rouvrir", style=disnake.ButtonStyle.success, custom_id="ticket_reopen"))) + + sections.append(disnake.ui.Section("📜 Générer un transcript", accessory=disnake.ui.Button(label="Transcript", style=disnake.ButtonStyle.secondary, custom_id="ticket_transcript"))) + + return [disnake.ui.Container(*sections)] + class TicketManagementView(disnake.ui.View): - """Gestion du ticket (dans le salon du ticket)""" + """Gestion du ticket (dans le salon du ticket) - Uniquement pour les callbacks désormais""" def __init__(self, bot, storage, config: GuildTicketConfig, ticket: TicketData): super().__init__(timeout=None) @@ -693,47 +789,16 @@ class TicketManagementView(disnake.ui.View): self.config = config self.ticket = ticket self.category = config.get_category(ticket.category_id) - - # Claim (if enabled and ticket is open or claimed) - if config.claim_enabled and self.category and self.category.allow_claims and ticket.status in [TicketStatus.OPEN, TicketStatus.CLAIMED]: - claim_btn = disnake.ui.Button( - style=disnake.ButtonStyle.primary, - label="Réclamer" if ticket.status == TicketStatus.OPEN else "Transférer", - custom_id="ticket_claim" - ) - claim_btn.callback = self._claim_callback - self.add_item(claim_btn) - - # Fermer - close_btn = disnake.ui.Button( - style=disnake.ButtonStyle.red, - label="Fermer", - custom_id="ticket_close" - ) - close_btn.callback = self._close_callback - self.add_item(close_btn) - - # Transcripts - transcript_btn = disnake.ui.Button( - style=disnake.ButtonStyle.secondary, - label="Transcript", - custom_id="ticket_transcript" - ) - transcript_btn.callback = self._transcript_callback - self.add_item(transcript_btn) - - # Reopen (if closed) - if ticket.status == TicketStatus.CLOSED: - reopen_btn = disnake.ui.Button( - style=disnake.ButtonStyle.green, - label="Rouvrir", - custom_id="ticket_reopen" - ) - reopen_btn.callback = self._reopen_callback - self.add_item(reopen_btn) async def _close_callback(self, interaction: disnake.Interaction): """Demande confirmation avant de fermer le ticket""" + kuby_logger.info( + f"[TicketSystem] _close_callback: " + f"guild={getattr(interaction.guild,'id',None)} " + f"channel={getattr(interaction.channel,'id',None)} " + f"user={interaction.user.id} " + f"ticket_user={getattr(self.ticket,'user_id',None)}" + ) # Vérifier si l'utilisateur peut fermer le ticket can_close = ( interaction.user.id == self.ticket.user_id or @@ -744,27 +809,39 @@ class TicketManagementView(disnake.ui.View): await interaction.response.send_message("Vous ne pouvez pas fermer ce ticket.", ephemeral=True) return - # Créer l'embed de confirmation - embed = disnake.Embed( - title="🔒 Confirmer la Fermeture", - description="Êtes-vous sûr de vouloir fermer ce ticket?\n\n" - "Cette action est irréversible et le salon sera supprimé.", - color=disnake.Color.orange() - ) - - # Ajouter les informations du ticket category = self.config.get_category(self.ticket.category_id) duration = self.ticket.duration_minutes - - embed.add_field(name="📁 Catégorie", value=category.name if category else "N/A", inline=True) - embed.add_field(name="⏱️ Durée", value=f"{duration:.1f} min" if duration else "N/A", inline=True) - embed.add_field(name="📊 Statut", value=self.ticket.status.value.capitalize(), inline=True) - - embed.set_footer(text="Cliquez sur Confirmer pour fermer ou Annuler pour annuler l'opération") - - # Afficher la vue de confirmation - view = CloseConfirmationView(self.bot, self.storage, self.config, self.ticket) - await interaction.response.send_message(embed=embed, view=view, ephemeral=True) + duration_text = f"{duration:.1f} min" if duration else "N/A" + + # ✅ V2 components (disnake>=2.11): ne pas utiliser embed/content avec components V2 + # (version minimale pour éviter tout problème de parsing) + # Confirmation UI en V2 (boutons réels) - UI courte. + # Objectif: éviter les erreurs Invalid Form Body et garder un contenu minimal. + components = [ + disnake.ui.Container( + disnake.ui.Section( + "🔒 Confirmer la fermeture", + accessory=disnake.ui.Button( + label="✅ Confirmer", + style=disnake.ButtonStyle.green, + custom_id="close_confirm" + ) + ), + disnake.ui.Separator(divider=True), + disnake.ui.Section( + "❌ Annuler", + accessory=disnake.ui.Button( + label="❌ Annuler", + style=disnake.ButtonStyle.red, + custom_id="close_cancel" + ) + ) + ) + ] + + # On ne met PAS de view= ici (sinon Disnake lève TypeError). + # Les boutons V2 de close_confirm/close_cancel seront routés via on_interaction. + await interaction.response.send_message(components=components, flags=disnake.MessageFlags(is_components_v2=True), ephemeral=True) async def _transcript_callback(self, interaction: disnake.Interaction): """Génère le transcript""" @@ -818,14 +895,11 @@ class TicketManagementView(disnake.ui.View): self.ticket.status = TicketStatus.CLAIMED self.storage.save_ticket(self.ticket) - # Update view (change label) - for item in self.children: - if item.custom_id == "ticket_claim": - item.label = "Transférer" - break - - # Force UI update - await interaction.response.edit_message(view=self) + # Force UI update V2 + try: + await interaction.response.edit_message(components=get_ticket_management_container(self.ticket, self.category, self.config)) + except disnake.NotFound: + pass # Message might have been deleted, ignore if old_claimer: embed = disnake.Embed( @@ -863,24 +937,40 @@ class TicketManagementView(disnake.ui.View): overwrites=overwrites ) - # Update view (remove reopen button, add claim if enabled) - for item in self.children: - if item.custom_id == "ticket_reopen": - self.remove_item(item) - break - - if self.config.claim_enabled and self.category and self.category.allow_claims: - claim_btn = disnake.ui.Button( - style=disnake.ButtonStyle.primary, - label="Réclamer", - custom_id="ticket_claim" - ) - claim_btn.callback = self._claim_callback - self.add_item(claim_btn) + # Update UI V2 + try: + await interaction.response.edit_message(components=get_ticket_management_container(self.ticket, self.category, self.config)) + except disnake.NotFound: + pass embed = TicketEmbedFormatter.ticket_reopened(self.ticket, interaction.user) await interaction.channel.send(embed=embed) - await interaction.response.send_message("Ticket rouvert!", ephemeral=True) + # We might have edited the message, so we just send follow up if we can't edit + # if not interaction.response.is_done(): + # await interaction.response.send_message("Ticket rouvert!", ephemeral=True) + + async def _unclaim_callback(self, interaction: disnake.Interaction): + """Abandonne le ticket""" + if not self._is_staff(interaction): + await interaction.response.send_message("Réservé au staff.", ephemeral=True) + return + + if self.ticket.claimed_by != interaction.user.id: + await interaction.response.send_message("Vous n'êtes pas celui qui a réclamé ce ticket.", ephemeral=True) + return + + self.ticket.claimed_by = None + self.ticket.status = TicketStatus.OPEN + self.storage.save_ticket(self.ticket) + + # Update UI V2 + try: + await interaction.response.edit_message(components=get_ticket_management_container(self.ticket, self.category, self.config)) + except disnake.NotFound: + pass + + embed = TicketEmbedFormatter.ticket_unclaimed(self.ticket, interaction.user) + await interaction.channel.send(embed=embed) def _is_staff(self, interaction: disnake.Interaction) -> bool: return is_staff(interaction, self.config, self.category) @@ -924,7 +1014,7 @@ class CloseConfirmationView(disnake.ui.View): self.category = config.get_category(ticket.category_id) @disnake.ui.button(label="✅ Confirmer", style=disnake.ButtonStyle.green, custom_id="close_confirm") - async def confirm_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def confirm_callback(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction): """L'utilisateur confirme la fermeture""" # Vérifier les permissions can_close = ( @@ -936,24 +1026,40 @@ class CloseConfirmationView(disnake.ui.View): await interaction.response.send_message("Vous ne pouvez pas fermer ce ticket.", ephemeral=True) return - # Show the delay view with countdown - embed = disnake.Embed( - title="⏳ Fermeture du Ticket", - description="Le ticket va être fermé dans quelques secondes...\n\n" - "📄 Génération du transcript en cours...\n" - "📝 Envoi aux logs...\n" - "🗑️ Suppression du canal...", - color=disnake.Color.orange() - ) + # Show the delay view with countdown (V2 components, not embeds) + components = [ + disnake.ui.Container( + disnake.ui.Section( + "# ⏳ Fermeture du Ticket", + accessory=disnake.ui.Button( + label="Fermeture...", + style=disnake.ButtonStyle.secondary, + custom_id="close_delay_info" + ) + ), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay( + content="Le ticket va être fermé dans quelques secondes...\n\n" + "📄 Génération du transcript en cours...\n" + "📝 Envoi aux logs...\n" + "🗑️ Suppression du canal..." + ) + ) + ] view = CloseDelayView(self.bot, self.storage, self.config, self.ticket) - await interaction.response.send_message(embed=embed, view=view, ephemeral=True) + await interaction.response.send_message( + components=components, + flags=disnake.MessageFlags(is_components_v2=True), + view=view, + ephemeral=True + ) # Start the countdown await view.start_countdown(interaction) @disnake.ui.button(label="❌ Annuler", style=disnake.ButtonStyle.red, custom_id="close_cancel") - async def cancel_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def cancel_callback(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction): """L'utilisateur annule la fermeture""" await interaction.response.defer() await interaction.delete_original_response() @@ -1173,42 +1279,20 @@ class CloseTicketModal(disnake.ui.Modal): """Modal pour fermer un ticket""" def __init__(self, bot, storage, config: GuildTicketConfig, ticket: TicketData): - super().__init__(title="Fermer le Ticket") + self.reason_input = disnake.ui.TextInput( + label="Raison de fermeture", + placeholder="Optionnel...", + style=disnake.TextInputStyle.paragraph, + required=False + ) + super().__init__(title="Fermer le Ticket", components=[self.reason_input]) self.bot = bot self.storage = storage self.config = config self.ticket = ticket self.category = config.get_category(ticket.category_id) - - self.reason = disnake.ui.TextInput( - label="Raison de fermeture", - placeholder="Optionnel...", - style=disnake.TextStyle.paragraph, - required=False, - max_length=300 - ) - self.add_item(self.reason) - - # Survey fields if enabled - if config.survey_enabled and self.category and self.category.survey_enabled: - self.rating = disnake.ui.TextInput( - label="Note (1-5 étoiles)", - placeholder="1 à 5", - required=False, - max_length=1 - ) - self.add_item(self.rating) - - self.feedback = disnake.ui.TextInput( - label="Commentaires", - placeholder="Vos commentaires sur le support...", - style=disnake.TextStyle.paragraph, - required=False, - max_length=500 - ) - self.add_item(self.feedback) - async def on_submit(self, interaction: disnake.Interaction): + async def callback(self, interaction: disnake.ModalInteraction): # Check permissions can_close = ( interaction.user.id == self.ticket.user_id or @@ -1219,35 +1303,33 @@ class CloseTicketModal(disnake.ui.Modal): await interaction.response.send_message("Vous ne pouvez pas fermer ce ticket.", ephemeral=True) return - # Handle survey if enabled - if hasattr(self, 'rating') and self.rating.value: - try: - rating = int(self.rating.value) - if 1 <= rating <= 5: - self.ticket.rating = rating - else: - await interaction.response.send_message("La note doit être entre 1 et 5.", ephemeral=True) - return - except ValueError: - await interaction.response.send_message("Note invalide.", ephemeral=True) - return - - if hasattr(self, 'feedback') and self.feedback.value: - self.ticket.feedback = self.feedback.value - # Send ephemeral response to user first (required for modal interactions) await interaction.response.send_message("✅ Fermeture du ticket en cours...", ephemeral=True) - # Send confirmation message in the ticket channel - confirm_embed = disnake.Embed( - title="⏳ Fermeture du Ticket", - description="Le ticket va être fermé dans quelques secondes...\n\n" - "📄 Génération du transcript en cours...\n" - "📝 Envoi aux logs...\n" - "🗑️ Suppression du canal...", - color=disnake.Color.orange() + # Send confirmation message in the ticket channel (V2 components, not embeds) + confirm_components = [ + disnake.ui.Container( + disnake.ui.Section( + "# ⏳ Fermeture du Ticket", + accessory=disnake.ui.Button( + label="Fermeture...", + style=disnake.ButtonStyle.secondary, + custom_id="close_ticket_modal_info" + ) + ), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay( + content="Le ticket va être fermé dans quelques secondes...\n\n" + "📄 Génération du transcript en cours...\n" + "📝 Envoi aux logs...\n" + "🗑️ Suppression du canal..." + ) + ) + ] + await interaction.channel.send( + components=confirm_components, + flags=disnake.MessageFlags(is_components_v2=True) ) - await interaction.channel.send(embed=confirm_embed) # Add small delay for user to see the confirmation import asyncio @@ -1305,8 +1387,8 @@ class CloseTicketModal(disnake.ui.Modal): log_embed.add_field(name="Catégorie", value=self.category.name if self.category else "N/A", inline=True) log_embed.add_field(name="Durée", value=f"{duration:.1f} min" if duration else "N/A", inline=True) - if self.reason.value: - log_embed.add_field(name="Raison de fermeture", value=self.reason.value, inline=False) + if self.reason_input.value: + log_embed.add_field(name="Raison de fermeture", value=self.reason_input.value, inline=False) # Send transcript file if transcript_file and os.path.exists(transcript_file): @@ -1826,13 +1908,7 @@ class AdvancedSetupView(disnake.ui.View): class MaxTicketsModal(disnake.ui.Modal): """Modal pour définir la limite de tickets par utilisateur""" - def __init__(self, bot, storage, config: GuildTicketConfig): - super().__init__(title="Limite de Tickets par Utilisateur") - self.bot = bot - self.storage = storage - self.config = config - self.max_tickets = disnake.ui.TextInput( label="Nombre maximum de tickets ouverts par utilisateur", placeholder="5", @@ -1840,9 +1916,12 @@ class MaxTicketsModal(disnake.ui.Modal): required=True, max_length=2 ) - self.add_item(self.max_tickets) + super().__init__(title="Limite de Tickets", components=[self.max_tickets]) + self.bot = bot + self.storage = storage + self.config = config - async def on_submit(self, interaction: disnake.Interaction): + async def callback(self, interaction: disnake.ModalInteraction): try: max_tickets = int(self.max_tickets.value) if max_tickets < 1 or max_tickets > 50: @@ -1996,141 +2075,105 @@ class ConfigPanelView(disnake.ui.View): class AddCategoryModal(disnake.ui.Modal): """Modal pour ajouter une catégorie""" - def __init__(self, bot, storage, config: GuildTicketConfig): - super().__init__(title="Nouvelle Catégorie") + self.name_input = disnake.ui.TextInput(label="Nom", custom_id="add_cat_name", placeholder="Ex: Support", required=True) + self.desc_input = disnake.ui.TextInput(label="Description", custom_id="add_cat_desc", placeholder="Description...", required=False, style=disnake.TextInputStyle.paragraph) + self.emoji_input = disnake.ui.TextInput(label="Emoji", custom_id="add_cat_emoji", placeholder="🎫", required=False, max_length=5) + super().__init__(title="Ajouter une Catégorie", components=[self.name_input, self.desc_input, self.emoji_input]) self.bot = bot self.storage = storage self.config = config - - self.name = disnake.ui.TextInput(label="Nom", placeholder="Ex: Support", required=True) - self.add_item(self.name) - - self.description = disnake.ui.TextInput( - label="Description", placeholder="Description...", required=False, style=disnake.TextStyle.paragraph - ) - self.add_item(self.description) - - self.emoji = disnake.ui.TextInput(label="Emoji", placeholder="🎫", required=False, max_length=5) - self.add_item(self.emoji) - async def on_submit(self, interaction: disnake.Interaction): + async def callback(self, interaction: disnake.ModalInteraction): import uuid cat_id = str(uuid.uuid4())[:8] + name_val = interaction.text_values.get("add_cat_name", "Nouvelle Catégorie") + desc_val = interaction.text_values.get("add_cat_desc", "") + emoji_val = interaction.text_values.get("add_cat_emoji", "🎫") + category = TicketCategory( id=cat_id, - name=self.name.value, - description=self.description.value or "", - emoji=self.emoji.value or "🎫" + name=name_val, + description=desc_val, + emoji=emoji_val ) self.config.categories.append(category) self.storage.save_config(interaction.guild_id, self.config) - await interaction.response.send_message(f"Catégorie **{self.name.value}** créée!", ephemeral=True) + await interaction.response.send_message(f"Catégorie **{name_val}** créée!", ephemeral=True) class EditCategoryModal(disnake.ui.Modal): """Modal pour modifier une catégorie existante""" def __init__(self, bot, storage, config, category: TicketCategory, parent_view): - super().__init__(title=f"Modifier: {category.name}") + self.name_input = disnake.ui.TextInput(label="Nom", custom_id="edit_cat_name", value=category.name, placeholder="Ex: Support", required=True) + self.desc_input = disnake.ui.TextInput(label="Description", custom_id="edit_cat_desc", value=category.description, placeholder="Description...", required=False, style=disnake.TextInputStyle.paragraph) + self.emoji_input = disnake.ui.TextInput(label="Emoji", custom_id="edit_cat_emoji", value=category.emoji, placeholder="🎫", required=False, max_length=5) + super().__init__(title=f"Modifier: {category.name}", components=[self.name_input, self.desc_input, self.emoji_input]) self.bot = bot self.storage = storage self.config = config self.category = category self.parent_view = parent_view - - self.name = disnake.ui.TextInput( - label="Nom", - default=category.name, - placeholder="Ex: Support", - required=True - ) - self.add_item(self.name) - - self.description = disnake.ui.TextInput( - label="Description", - default=category.description, - placeholder="Description...", - required=False, - style=disnake.TextStyle.paragraph - ) - self.add_item(self.description) - - self.emoji = disnake.ui.TextInput( - label="Emoji", - default=category.emoji, - placeholder="🎫", - required=False, - max_length=5 - ) - self.add_item(self.emoji) - - async def on_submit(self, interaction: disnake.Interaction): - # Update category object - self.category.name = self.name.value - self.category.description = self.description.value or "" - self.category.emoji = self.emoji.value or "🎫" - - # Save config - self.storage.save_config(interaction.guild_id, self.config) - - # Refresh the parent view's message - await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view) - await interaction.followup.send(f"✅ Catégorie **{self.category.name}** mise à jour !", ephemeral=True) + async def callback(self, interaction: disnake.ModalInteraction): + try: + self.category.name = interaction.text_values.get("edit_cat_name", self.category.name) + self.category.description = interaction.text_values.get("edit_cat_desc", "") + self.category.emoji = interaction.text_values.get("edit_cat_emoji", "🎫") + self.storage.save_config(interaction.guild_id, self.config) + + container = disnake.ui.Container( + disnake.ui.TextDisplay(content=f"✅ Catégorie **{self.category.name}** mise à jour !"), + disnake.ui.Section("Navigation", accessory=disnake.ui.Button(label="Retour", style=disnake.ButtonStyle.secondary, custom_id=f"ticket_config_cat_manage_{self.category.id}")) + ) + + await interaction.response.edit_message(components=[container]) + except Exception as e: + import traceback + kuby_logger.error(f"[EditCategoryModal] Error: {e}\n{traceback.format_exc()}") + if not interaction.response.is_done(): + await interaction.response.send_message(f"❌ Erreur critique : {e}", ephemeral=True) class CategoryManagerView(disnake.ui.View): - """Vue pour lister et gérer les catégories""" - + """Vue pour lister et gérer les catégories (Modernisée V2)""" def __init__(self, bot, storage, config: GuildTicketConfig): super().__init__(timeout=None) self.bot = bot self.storage = storage self.config = config - self._add_category_select() + + def get_components_v2(self): + sections = [ + disnake.ui.Section( + "# 📂 Gestion des Catégories", + accessory=disnake.ui.Button(label="➕ Ajouter", style=disnake.ButtonStyle.green, custom_id="ticket_config_cat_add") + ), + disnake.ui.Separator(divider=True) + ] - def _add_category_select(self): - self.clear_items() + if not self.config.categories: + sections.append(disnake.ui.TextDisplay(content="Aucune catégorie configurée.")) + else: + for cat in self.config.categories: + sections.append( + disnake.ui.Section( + f"{cat.emoji} **{cat.name}**\n*{cat.description or 'Pas de description'}*", + accessory=disnake.ui.Button(label="Gérer", style=disnake.ButtonStyle.secondary, custom_id=f"ticket_config_cat_manage_{cat.id}") + ) + ) - if self.config.categories: - options = [] - for cat in self.config.categories[:25]: # Limite Select menu - options.append(disnake.SelectOption( - label=cat.name, - value=cat.id, - description=cat.description[:100] if cat.description else "Aucune description", - emoji=cat.emoji - )) - - select = disnake.ui.Select( - placeholder="Sélectionnez une catégorie à gérer...", - options=options, - custom_id="manage_cat_select" - ) - select.callback = self._category_select_callback - self.add_item(select) - - # Bouton Ajouter - add_btn = disnake.ui.Button( - style=disnake.ButtonStyle.success, - label="➕ Ajouter une catégorie", - custom_id="manage_cat_add" - ) - add_btn.callback = self._add_cat_callback - self.add_item(add_btn) + sections.append(disnake.ui.Separator(divider=True)) + sections.append(disnake.ui.Section( + "Retour au menu principal", + accessory=disnake.ui.Button(label="⬅️ Retour", style=disnake.ButtonStyle.secondary, custom_id="ticket_config_main") + )) - # Bouton Retour - back_btn = disnake.ui.Button( - style=disnake.ButtonStyle.secondary, - label="⬅️ Retour", - custom_id="manage_cat_back" - ) - back_btn.callback = self._back_callback - self.add_item(back_btn) + return [disnake.ui.Container(*sections)] def _get_embed(self): embed = disnake.Embed( @@ -2171,7 +2214,7 @@ class CategoryManagerView(disnake.ui.View): super().__init__(bot, storage, config) self.parent_view = parent_view - async def on_submit(self, interaction: disnake.Interaction): + async def callback(self, interaction: disnake.ModalInteraction): import uuid cat_id = str(uuid.uuid4())[:8] category = TicketCategory( @@ -2230,12 +2273,12 @@ class CategoryActionView(disnake.ui.View): return embed @disnake.ui.button(label="📝 Modifier", style=disnake.ButtonStyle.primary) - async def edit_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def edit_callback(self, interaction: disnake.MessageInteraction): modal = EditCategoryModal(self.bot, self.storage, self.config, self.category, self) await interaction.response.send_modal(modal) @disnake.ui.button(label="🛡️ Permissions", style=disnake.ButtonStyle.success) - async def permissions_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def permissions_callback(self, interaction: disnake.MessageInteraction): view = CategoryPermissionsView(self.bot, self.storage, self.config, self.category, self) embed = disnake.Embed( title=f"Permissions: {self.category.name}", @@ -2247,7 +2290,7 @@ class CategoryActionView(disnake.ui.View): await interaction.response.edit_message(embed=embed, view=view) @disnake.ui.button(label="📁 Catégorie Discord", style=disnake.ButtonStyle.secondary) - async def discord_category_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def discord_category_callback(self, interaction: disnake.MessageInteraction): view = CategoryDiscordCategoryView(self.bot, self.storage, self.config, self.category, self) embed = disnake.Embed( title=f"Catégorie Discord: {self.category.name}", @@ -2258,7 +2301,7 @@ class CategoryActionView(disnake.ui.View): await interaction.response.edit_message(embed=embed, view=view) @disnake.ui.button(label="🗑️ Supprimer", style=disnake.ButtonStyle.danger) - async def delete_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def delete_callback(self, interaction: disnake.MessageInteraction): view = DeleteCategoryConfirmationView(self.bot, self.storage, self.config, self.category, self.parent_view) embed = disnake.Embed( title="⚠️ Confirmer la suppression", @@ -2269,7 +2312,7 @@ class CategoryActionView(disnake.ui.View): await interaction.response.edit_message(embed=embed, view=view) @disnake.ui.button(label="⬅️ Retour", style=disnake.ButtonStyle.secondary) - async def back_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def back_callback(self, interaction: disnake.MessageInteraction): self.parent_view._add_category_select() await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view) @@ -2285,8 +2328,8 @@ class CategoryPermissionsView(disnake.ui.View): self.parent_view = parent_view @disnake.ui.select(cls=disnake.ui.RoleSelect, placeholder="Sélectionner les rôles staff (Gestion)", min_values=0, max_values=10) - async def role_select_callback(self, interaction: disnake.Interaction, select: disnake.ui.RoleSelect): - role_ids = [str(role.id) for role in select.values] + async def role_select_callback(self, interaction: disnake.MessageInteraction): + role_ids = [str(role.id) for role in interaction.values] self.category.staff_roles = role_ids self.storage.save_config(interaction.guild_id, self.config) @@ -2294,7 +2337,7 @@ class CategoryPermissionsView(disnake.ui.View): await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view) @disnake.ui.button(label="⬅️ Annuler", style=disnake.ButtonStyle.secondary) - async def back_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def back_callback(self, interaction: disnake.MessageInteraction): await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view) class CategoryDiscordCategoryView(disnake.ui.View): @@ -2309,19 +2352,20 @@ class CategoryDiscordCategoryView(disnake.ui.View): self.parent_view = parent_view @disnake.ui.select(cls=disnake.ui.ChannelSelect, channel_types=[disnake.ChannelType.category], placeholder="Sélectionner la catégorie Discord") - async def select_callback(self, interaction: disnake.Interaction, select: disnake.ui.ChannelSelect): - self.category.discord_category_id = select.values[0].id + async def select_callback(self, interaction: disnake.MessageInteraction): + val = interaction.values[0] + self.category.discord_category_id = val.id if hasattr(val, 'id') else int(val) self.storage.save_config(interaction.guild_id, self.config) await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view) @disnake.ui.button(label="❌ Réinitialiser", style=disnake.ButtonStyle.danger) - async def reset_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def reset_callback(self, interaction: disnake.MessageInteraction): self.category.discord_category_id = None self.storage.save_config(interaction.guild_id, self.config) await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view) @disnake.ui.button(label="⬅️ Retour", style=disnake.ButtonStyle.secondary) - async def back_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def back_callback(self, interaction: disnake.MessageInteraction): await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view) @@ -2335,7 +2379,7 @@ class DeleteCategoryConfirmationView(disnake.ui.View): self.parent_view = parent_view @disnake.ui.button(label="✅ Confirmer", style=disnake.ButtonStyle.danger) - async def confirm_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def confirm_callback(self, interaction: disnake.MessageInteraction): if self.category in self.config.categories: self.config.categories.remove(self.category) self.storage.save_config(interaction.guild_id, self.config) @@ -2348,74 +2392,104 @@ class DeleteCategoryConfirmationView(disnake.ui.View): await interaction.followup.send(f"✅ Catégorie **{self.category.name}** supprimée.", ephemeral=True) @disnake.ui.button(label="❌ Annuler", style=disnake.ButtonStyle.secondary) - async def cancel_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def cancel_callback(self, interaction: disnake.MessageInteraction): view = CategoryActionView(self.bot, self.storage, self.config, self.category, self.parent_view) await interaction.response.edit_message(embed=view._get_embed(), view=view) -class SetChannelModal(disnake.ui.Modal): - """Modal pour définir le canal de logs""" - +class TicketChannelConfigView(disnake.ui.View): + """Vue moderne pour configurer un salon via un sélecteur (V2)""" def __init__(self, bot, storage, config: GuildTicketConfig, channel_type: str = "log"): - super().__init__(title="Définir le Canal de Logs") + super().__init__(timeout=None) self.bot = bot self.storage = storage self.config = config self.channel_type = channel_type - - self.channel = disnake.ui.TextInput( - label="Canal de logs", - placeholder="Mentionnez le canal ou entrez l'ID (ex: #general ou 123456789)", - required=True, - max_length=100 + + # Sélecteur de salon + select = disnake.ui.ChannelSelect( + placeholder="Sélectionnez la catégorie dans la liste...", + channel_types=[disnake.ChannelType.text], + min_values=1, + max_values=1, + custom_id="ticket_config_chan_select" ) - self.add_item(self.channel) - - async def on_submit(self, interaction: disnake.Interaction): - channel_input = self.channel.value.strip() + self.add_item(disnake.ui.ChannelSelect(placeholder="Choisir un salon...", channel_types=c_types, custom_id=f"ticket_config_select_{self.channel_type}")) + + def get_components_v2(self): + title = "📝 Configuration des Logs" if self.channel_type == "log" else "📁 Catégorie de Destination" + desc = "Choisissez la catégorie où les tickets seront créés." if self.channel_type == "log" else "Choisissez la catégorie où les tickets seront créés." - # Try to extract channel ID from mention or ID - channel_id = None + sections = [ + disnake.ui.TextDisplay(content=f"# {title}\n{desc}"), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(content="Utilisez le menu déroulant ci-dessous pour choisir un salon."), + disnake.ui.Separator(divider=True), + disnake.ui.Section("Navigation", accessory=disnake.ui.Button(label="⬅️ Retour", style=disnake.ButtonStyle.secondary, custom_id="ticket_config_main")) + ] - # Check for mention format (#channel) - if channel_input.startswith('<#') and channel_input.endswith('>'): - try: - channel_id = int(channel_input[2:-1]) - except ValueError: - pass - # Check for raw ID - else: - try: - channel_id = int(channel_input.replace('<', '').replace('>', '')) - except ValueError: - pass + c_types = [disnake.ChannelType.text] if self.channel_type == "log" else [disnake.ChannelType.category] - if not channel_id: - await interaction.response.send_message( - "❌ Format invalide. Utilisez une mention (#channel) ou un ID de canal.", - ephemeral=True + # Include the select menu in the components list + return [disnake.ui.Container(*sections), disnake.ui.ChannelSelect(placeholder="Choisir une catégorie...", channel_types=c_types, custom_id=f"ticket_config_select_{self.channel_type}")] + + async def _select_callback(self, inter: disnake.MessageInteraction): + print(f"DEBUG: _select_callback called. Values: {inter.values}") + try: + val = inter.values[0] + channel_id = val.id if hasattr(val, "id") else int(val) + print(f"DEBUG: channel_id determined: {channel_id}") + + if self.channel_type == "log": + self.config.log_channel_id = channel_id + elif self.channel_type.startswith("cat_"): + cat_id = self.channel_type.replace("cat_", "") + category = next((c for c in self.config.categories if c.id == cat_id), None) + if category: + category.discord_category_id = channel_id + + self.storage.save_config(inter.guild_id, self.config) + print(f"DEBUG: Config saved for guild {inter.guild_id}") + + # Success message V2 + container = disnake.ui.Container( + disnake.ui.TextDisplay(content=f"✅ Salon/Catégorie configuré(e) sur <#{channel_id}>"), + disnake.ui.Section("Navigation", accessory=disnake.ui.Button(label="Retour", style=disnake.ButtonStyle.secondary, custom_id="ticket_config_main")) ) - return - - # Verify channel exists - channel = interaction.guild.get_channel(channel_id) - if not channel: - await interaction.response.send_message( - "❌ Ce canal n'existe pas sur ce serveur.", - ephemeral=True + print("DEBUG: Sending success message...") + await inter.response.edit_message(components=[container]) + print("DEBUG: Success message sent.") + except Exception as e: + print(f"DEBUG ERROR in _select_callback: {e}") + import traceback + traceback.print_exc() + kuby_logger.error(f"❌ Erreur dans _select_callback: {e}", exc_info=True) + try: + await inter.response.send_message(f"❌ Erreur lors de la configuration du salon : {e}", ephemeral=True) + except: + pass + +class SetChannelModal(disnake.ui.Modal): + + async def callback(self, interaction: disnake.ModalInteraction): + try: + channel_id = int(self.channel_input.value.strip()) + channel = interaction.guild.get_channel(channel_id) + if not channel: + await interaction.response.send_message("❌ Canal introuvable.", ephemeral=True) + return + + self.config.log_channel_id = channel_id + self.storage.save_config(interaction.guild_id, self.config) + + embed = disnake.Embed( + title="✅ Canal de Logs Mis à Jour", + description=f"Le canal de logs a été défini sur {channel.mention}.", + color=disnake.Color.green() ) - return - - # Update config - self.config.log_channel_id = channel_id - self.storage.save_config(interaction.guild_id, self.config) - - embed = disnake.Embed( - title="✅ Canal de Logs Mis à Jour", - description=f"Le canal de logs a été défini sur {channel.mention}.", - color=disnake.Color.green() - ) - await interaction.response.send_message(embed=embed, ephemeral=True) + await interaction.response.send_message(embed=embed, ephemeral=True) + except ValueError: + await interaction.response.send_message("❌ ID invalide.", ephemeral=True) class StaffRolesModal(disnake.ui.Modal): @@ -2436,7 +2510,7 @@ class StaffRolesModal(disnake.ui.Modal): ) self.add_item(self.roles) - async def on_submit(self, interaction: disnake.Interaction): + async def callback(self, interaction: disnake.ModalInteraction): try: role_input = self.roles.value.strip() role_ids = [] @@ -2524,7 +2598,8 @@ class PermissionConfigView(disnake.ui.View): placeholder=f"Sélectionnez les rôles {self.config_type}", min_values=0, max_values=25, - row=0 + row=0, + custom_id=f"ticket_config_perms_role_{self.config_type}" ) select.callback = self._role_callback self.add_item(select) @@ -2534,7 +2609,8 @@ class PermissionConfigView(disnake.ui.View): placeholder=f"Sélectionnez les utilisateurs {self.config_type}", min_values=0, max_values=25, - row=1 + row=1, + custom_id=f"ticket_config_perms_user_{self.config_type}" ) select.callback = self._user_callback self.add_item(select) @@ -2600,42 +2676,22 @@ class PermissionConfigView(disnake.ui.View): self._add_role_select() self._add_user_select() self._add_remove_select() - - # Rebuild Embed - if self.config_type == "staff": - title = "Permissions Staff" - description = "Configurez qui peut gérer les tickets (répondre, fermer, etc.)" - color = disnake.Color.blue() - target_roles = self.config.staff_roles - target_users = self.config.staff_users - else: - title = "Permissions Configuration" - description = "Configurez qui peut modifier les paramètres du bot (/ticketconfig)" - color = disnake.Color.orange() - target_roles = self.config.config_roles - target_users = self.config.config_users + await interaction.response.edit_message(components=self.get_components_v2()) - embed = disnake.Embed(title=title, description=description, color=color) + def get_components_v2(self): + title = "🛡️ Permissions Staff" if self.config_type == "staff" else "⚙️ Permissions Config" + desc = "Configurez qui peut gérer les tickets." if self.config_type == "staff" else "Configurez qui peut modifier les paramètres." - # Add fields logic (inline reuse) - role_mentions = [] - for role_id in target_roles: - role = self.guild.get_role(int(role_id)) - if role: - role_mentions.append(role.mention) + sections = [ + disnake.ui.TextDisplay(content=f"# {title}\n{desc}"), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(content="Utilisez les sélecteurs ci-dessous pour modifier les permissions."), + disnake.ui.Separator(divider=True), + disnake.ui.Section("Navigation", accessory=disnake.ui.Button(label="⬅️ Retour", style=disnake.ButtonStyle.secondary, custom_id="ticket_config_main")) + ] - user_mentions = [] - for user_id in target_users: - user = self.guild.get_member(user_id) - if user: - user_mentions.append(user.mention) - else: - user_mentions.append(f"<@{user_id}>") - - embed.add_field(name="Rôles", value=", ".join(role_mentions) if role_mentions else "Aucun", inline=False) - embed.add_field(name="Utilisateurs", value=", ".join(user_mentions) if user_mentions else "Aucun", inline=False) - - await interaction.response.edit_message(embed=embed, view=self) + # Combine container with view children (RoleSelect, UserSelect, etc.) + return [disnake.ui.Container(*sections)] + self.children def _add_remove_select(self): # Build options from current config @@ -2683,7 +2739,7 @@ class PermissionConfigView(disnake.ui.View): min_values=1, max_values=min(len(options), 25), row=2, - custom_id="remove_select" + custom_id=f"ticket_config_perms_remove_{self.config_type}" ) select.callback = self._remove_callback self.add_item(select) @@ -2734,6 +2790,278 @@ class TicketCommands(commands.Cog): async def cog_load(self): await self._register_all_views() + @commands.Cog.listener() + async def on_interaction(self, inter: disnake.Interaction): + """Gère les interactions des composants V2 (Container/Section)""" + # Logs ultra détaillés pour tracer "interaction failed" / mauvais payload + try: + inter_data = getattr(inter, "data", None) + inter_custom_id = getattr(inter_data, "custom_id", None) if inter_data else None + inter_values = getattr(inter_data, "values", None) if inter_data else None + except Exception: + inter_custom_id = None + inter_values = None + + kuby_logger.info( + f"[TicketSystem] on_interaction: type={getattr(inter, 'type', None)} " + f"custom_id={inter_custom_id} values={inter_values} " + f"guild_id={getattr(getattr(inter,'guild',None),'id',None)} channel_id={getattr(getattr(inter,'channel',None),'id',None)}" + ) + + if inter.type != disnake.InteractionType.component: + return + + if not inter.guild: + return + + custom_id = inter.data.custom_id + # Debug: savoir si on rentre dans le handler ticket_* + kuby_logger.info(f"[TicketSystem] Routing check: custom_id={custom_id} startswith 'ticket_'? {bool(custom_id and str(custom_id).startswith('ticket_'))}") + + if not custom_id: + return + + # Accept both ticket_* / cat_* / close_* custom_ids + if not ( + str(custom_id).startswith("ticket_") + or str(custom_id).startswith("cat_") + or str(custom_id) in {"close_confirm", "close_cancel", "delay_cancel"} + ): + return + + if not inter.guild: + return + + custom_id = inter.data.custom_id + + try: + # On récupère la config + config = self.storage.load_config(inter.guild.id) + if not config: + kuby_logger.warning(f"[TicketSystem] No config for guild_id={inter.guild.id}") + return + except Exception as e: + kuby_logger.error(f"[TicketSystem] Failed to load config: {e}", exc_info=True) + if not inter.response.is_done(): + await inter.response.send_message("Erreur interne (config)", ephemeral=True) + return + + try: + + # Routage manuel + if custom_id == "ticket_create": + view = TicketPanelView(self.bot, config, self.storage) + await view._create_callback(inter) + elif custom_id == "ticket_my_tickets": + view = TicketPanelView(self.bot, config, self.storage) + await view._my_tickets_callback(inter) + elif custom_id == "ticket_analytics": + view = TicketPanelView(self.bot, config, self.storage) + await view._analytics_callback(inter) + + # ✅ BUGFIX: Sélection de catégorie en V2 => custom_id = cat_{cat.id}_{snowflake} + elif custom_id.startswith("cat_"): + try: + # format: cat__ + parts = custom_id.split("_") + if len(parts) >= 3: + cat_id = "_".join(parts[1:-1]) + else: + cat_id = parts[1] if len(parts) > 1 else None + + category = next((c for c in config.categories if c.id == cat_id), None) + if not category: + await inter.response.send_message("Catégorie non trouvée.", ephemeral=True) + return + + # ouvrir directement le modal de priorité (même logique que CategorySelectionView) + modal = TicketPriorityModal(self.bot, config, category) + await inter.response.send_modal(modal) + except Exception as e: + kuby_logger.error(f"[TicketSystem] cat_ routing error: {e}", exc_info=True) + if not inter.response.is_done(): + await inter.response.send_message(f"Erreur interne: {e}", ephemeral=True) + + elif custom_id == "ticket_claim": + ticket = self.storage.load_ticket(inter.channel.id) + if ticket: + view = TicketManagementView(self.bot, self.storage, config, ticket) + await view._claim_callback(inter) + elif custom_id == "ticket_unclaim": + ticket = self.storage.load_ticket(inter.channel.id) + if ticket: + view = TicketManagementView(self.bot, self.storage, config, ticket) + await view._unclaim_callback(inter) + elif custom_id == "ticket_close": + ticket = self.storage.load_ticket(inter.channel.id) + if ticket: + view = TicketManagementView(self.bot, self.storage, config, ticket) + await view._close_callback(inter) + elif custom_id == "ticket_reopen": + ticket = self.storage.load_ticket(inter.channel.id) + if ticket: + view = TicketManagementView(self.bot, self.storage, config, ticket) + await view._reopen_callback(inter) + elif custom_id == "ticket_transcript": + ticket = self.storage.load_ticket(inter.channel.id) + if ticket: + view = TicketManagementView(self.bot, self.storage, config, ticket) + await view._transcript_callback(inter) + + # === Fermeture via composants V2 (sans view=, donc custom_id) === + elif custom_id == "close_cancel": + await inter.response.defer() + # Retire la réponse éphémère du user (si c'est bien une réponse d'interaction) + try: + await inter.delete_original_response() + except Exception: + pass + + elif custom_id == "close_confirm": + try: + kuby_logger.info(f"[TicketSystem] close_confirm clicked channel={getattr(inter.channel,'id',None)} user={inter.user.id if inter.user else None}") + + # Permission check identique à CloseConfirmationView.confirm_callback + ticket = self.storage.load_ticket(inter.channel.id) if inter.channel else None + if not ticket: + await inter.response.send_message("Ticket non trouvé.", ephemeral=True) + return + + can_close = ( + inter.user.id == ticket.user_id or + is_staff(inter, config, config.get_category(ticket.category_id)) + ) + if not can_close: + await inter.response.send_message("Vous ne pouvez pas fermer ce ticket.", ephemeral=True) + return + + await inter.response.defer() + + close_view = CloseDelayView(self.bot, self.storage, config, ticket) + + # Compte à rebours dans le CHANNEL (pas sur l'ephemeral), puis fermeture. + steps = [ + ("⏳ Préparation...", 1), + ("📄 Génération du transcript...", 1), + ("📝 Sauvegarde des données...", 1), + ("🗑️ Suppression du canal...", 1) + ] + + channel = inter.channel + if not channel: + await inter.followup.send("Canal introuvable.", ephemeral=True) + return + + countdown_msg = await channel.send( + embed=disnake.Embed( + title="🔒 Fermeture du Ticket", + description="**⏳ Préparation...**\n\nMerci pour votre confiance!", + color=disnake.Color.orange() + ) + ) + + for step_text, delay in steps: + embed = disnake.Embed( + title="🔒 Fermeture du Ticket", + description=f"**{step_text}**\n\nMerci pour votre confiance!", + color=disnake.Color.orange() + ) + embed.set_footer(text=f"Ticket #{ticket.channel_id}") + await countdown_msg.edit(embed=embed) + await asyncio.sleep(delay) + + await close_view._close_ticket(inter) + + try: + await countdown_msg.delete() + except Exception: + pass + + except Exception as e: + kuby_logger.error(f"[TicketSystem] close_confirm failed: {e}", exc_info=True) + if not inter.response.is_done(): + await inter.response.send_message("❌ Échec de la fermeture (voir logs).", ephemeral=True) + + # Configuration + elif custom_id == "ticket_config_main": + await self.ticket_config(inter) + elif custom_id == "ticket_config_toggle": + config.enabled = not config.enabled + self.storage.save_config(inter.guild.id, config) + await self.ticket_config(inter) + elif custom_id == "ticket_config_manage_cat": + view = CategoryManagerView(self.bot, self.storage, config) + await inter.response.edit_message(components=view.get_components_v2()) + elif custom_id == "ticket_config_cat_add": + modal = AddCategoryModal(self.bot, self.storage, config) + await inter.response.send_modal(modal) + elif custom_id.startswith("ticket_config_cat_manage_"): + cat_id = custom_id.replace("ticket_config_cat_manage_", "") + category = next((c for c in config.categories if c.id == cat_id), None) + if category: + # Show modern category actions + actions_container = disnake.ui.Container( + disnake.ui.TextDisplay(content=f"# 🛠️ Gérer: {category.name}"), + disnake.ui.Separator(divider=True), + disnake.ui.Section("Modifier les informations de base (Nom, Desc, Emoji)", accessory=disnake.ui.Button(label="✏️ Modifier", style=disnake.ButtonStyle.blurple, custom_id=f"ticket_config_cat_edit_{cat_id}")), + disnake.ui.Section("Configurer les permissions staff spécifiques", accessory=disnake.ui.Button(label="🛡️ Permissions", style=disnake.ButtonStyle.secondary, custom_id=f"ticket_config_cat_perms_{cat_id}")), + disnake.ui.Section("Définir la catégorie Discord de destination", accessory=disnake.ui.Button(label="📁 Catégorie", style=disnake.ButtonStyle.secondary, custom_id=f"ticket_config_cat_chan_{cat_id}")), + disnake.ui.Separator(divider=True), + disnake.ui.Section("Supprimer définitivement cette catégorie", accessory=disnake.ui.Button(label="🗑️ Supprimer", style=disnake.ButtonStyle.danger, custom_id=f"ticket_config_cat_del_{cat_id}")), + disnake.ui.Section("Navigation", accessory=disnake.ui.Button(label="⬅️ Retour", style=disnake.ButtonStyle.secondary, custom_id="ticket_config_manage_cat")) + ) + await inter.response.edit_message(components=[actions_container]) + elif custom_id.startswith("ticket_config_cat_edit_"): + cat_id = custom_id.replace("ticket_config_cat_edit_", "") + category = next((c for c in config.categories if c.id == cat_id), None) + if category: + modal = EditCategoryModal(self.bot, self.storage, config, category, None) + await inter.response.send_modal(modal) + elif custom_id == "ticket_config_log_channel": + view = TicketChannelConfigView(self.bot, self.storage, config, "log") + await inter.response.edit_message(components=view.get_components_v2()) + elif custom_id.startswith("ticket_config_cat_chan_"): + cat_id = custom_id.replace("ticket_config_cat_chan_", "") + view = TicketChannelConfigView(self.bot, self.storage, config, f"cat_{cat_id}") + await inter.response.edit_message(components=view.get_components_v2()) + elif custom_id == "ticket_config_staff_perms": + view = PermissionConfigView(self.bot, self.storage, config, inter.guild, "staff") + await inter.response.edit_message(components=view.get_components_v2()) + elif custom_id.startswith("ticket_config_cat_perms_"): + cat_id = custom_id.replace("ticket_config_cat_perms_", "") + view = PermissionConfigView(self.bot, self.storage, config, inter.guild, f"cat_{cat_id}") + await inter.response.edit_message(components=view.get_components_v2()) + elif custom_id == "ticket_config_perms_config": + view = PermissionConfigView(self.bot, self.storage, config, inter.guild, "config") + await inter.response.edit_message(components=view.get_components_v2()) + elif custom_id.startswith("ticket_config_select_"): + channel_type = custom_id.replace("ticket_config_select_", "") + view = TicketChannelConfigView(self.bot, self.storage, config, channel_type) + await view._select_callback(inter) + elif custom_id.startswith("ticket_config_perms_role_"): + config_type = custom_id.replace("ticket_config_perms_role_", "") + view = PermissionConfigView(self.bot, self.storage, config, inter.guild, config_type) + await view._role_callback(inter) + elif custom_id.startswith("ticket_config_perms_user_"): + config_type = custom_id.replace("ticket_config_perms_user_", "") + view = PermissionConfigView(self.bot, self.storage, config, inter.guild, config_type) + await view._user_callback(inter) + elif custom_id.startswith("ticket_config_perms_remove_"): + config_type = custom_id.replace("ticket_config_perms_remove_", "") + view = PermissionConfigView(self.bot, self.storage, config, inter.guild, config_type) + await view._remove_callback(inter) + + except Exception as e: + kuby_logger.error(f"❌ Erreur critique dans on_interaction: {e}", exc_info=True) + try: + if not inter.response.is_done(): + await inter.response.send_message(f"❌ Une erreur interne est survenue : {e}", ephemeral=True) + else: + await inter.edit_original_response(content=f"❌ Une erreur interne est survenue : {e}") + except: + pass + + async def _register_all_views(self): """Enregistre les vues pour tous les serveurs""" try: @@ -2766,20 +3094,8 @@ class TicketCommands(commands.Cog): config = self.storage.load_config(inter.guild.id) - embed = disnake.Embed( - title="Système de Tickets", - description="Gérez vos tickets simplement", - color=disnake.Color.blurple() - ) - - if not config.enabled: - embed.add_field(name="État", value="Le système est actuellement **désactivé**.", inline=False) - else: - embed.add_field(name="Catégories", value=str(len(config.categories)), inline=True) - embed.add_field(name="Total tickets", value=str(len(self.storage.get_guild_tickets(inter.guild.id))), inline=True) - view = TicketPanelView(self.bot, config, self.storage) - await inter.edit_original_response(embed=embed, view=view) + await inter.edit_original_response(components=view.get_components_v2()) @commands.slash_command(name="ticketpanel", description="Envoie le panel de création de tickets dans un canal") @commands.has_permissions(administrator=True) @@ -2795,15 +3111,8 @@ class TicketCommands(commands.Cog): target = channel or inter.channel - embed = disnake.Embed( - title="Besoin d'aide?", - description="Cliquez sur le bouton ci-dessous pour créer un ticket.\n\nNotre équipe vous répondra dès que possible!", - color=disnake.Color.blurple() - ) - embed.add_field(name="Catégories disponibles", value=str(len(config.categories)), inline=False) - view = TicketPanelView(self.bot, config, self.storage) - await target.send(embed=embed, view=view) + await target.send(components=view.get_components_v2()) await inter.edit_original_response(content=f"Panel envoyé dans {target.mention}") @@ -2820,43 +3129,41 @@ class TicketCommands(commands.Cog): await inter.edit_original_response(content="❌ Cette commande est réservée aux administrateurs ou aux rôles configurés.") return + try: + # Build config panel with V2 components + config_container = disnake.ui.Container( + disnake.ui.Section( + "# ⚙️ Configuration des Tickets", + accessory=disnake.ui.Button(label="🔄 Activer/Désactiver", style=disnake.ButtonStyle.green if config.enabled else disnake.ButtonStyle.red, custom_id="ticket_config_toggle") + ), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay( + content=f"**État actuel :** {'✅ Activé' if config.enabled else '❌ Désactivé'}\n" + f"**Catégories :** {len(config.categories)}\n" + f"**Rôles staff :** {len(config.staff_roles)}" + ), + disnake.ui.Separator(divider=True), + disnake.ui.Section( + "📂 Gestion des catégories", + accessory=disnake.ui.Button(label="Gérer", style=disnake.ButtonStyle.blurple, custom_id="ticket_config_manage_cat") + ), + disnake.ui.Section( + "📝 Logs & Salons", + accessory=disnake.ui.Button(label="Configurer", style=disnake.ButtonStyle.secondary, custom_id="ticket_config_log_channel") + ), + disnake.ui.Section( + "👥 Permissions Staff", + accessory=disnake.ui.Button(label="Rôles", style=disnake.ButtonStyle.secondary, custom_id="ticket_config_staff_perms") + ) + ) + + await inter.edit_original_response(components=[config_container]) + except Exception as e: + kuby_logger.error(f"❌ Erreur dans ticket_config: {e}", exc_info=True) + await inter.edit_original_response(content=f"❌ Une erreur interne est survenue : {e}") - embed = disnake.Embed( - title="⚙️ Configuration du Système de Tickets", - description="Configurez complètement votre système de tickets depuis cette interface.\n\n" - "**Options disponibles:**\n" - "• Activer/Désactiver le système\n" - "• Configurer les rôles staff\n" - "• Gérer les catégories\n" - "• Définir le canal de logs\n" - "• Et bien plus...", - color=disnake.Color.blue() - ) - - embed.add_field( - name="État actuel", - value=f"Système: {'✅ Activé' if config.enabled else '❌ Désactivé'}\n" - f"Catégories: {len(config.categories)}\n" - f"Rôles staff: {len(config.staff_roles)}", - inline=True - ) - - embed.add_field( - name="Configuration requise", - value="• **Rôles Staff**: Définissez qui peut gérer les tickets\n" - "• **Catégories**: Créez des catégories pour organiser les tickets\n" - "• **Canal de Logs**: Où envoyer les notifications", - inline=True - ) - - embed.set_footer(text="Utilisez les boutons ci-dessous pour configurer le système") - - view = ConfigPanelView(self.bot, self.storage, config) - await inter.edit_original_response(embed=embed, view=view) - - -async def setup(bot): +def setup(bot): """Setup function""" - await bot.add_cog(TicketCommands(bot)) + bot.add_cog(TicketCommands(bot)) diff --git a/commandes/ticket/analytics.py b/commandes/ticket/analytics.py index 2737381..e09409a 100644 --- a/commandes/ticket/analytics.py +++ b/commandes/ticket/analytics.py @@ -490,6 +490,6 @@ class TicketAnalytics(commands.Cog): await interaction.followup.send(embed=feedback_embed, ephemeral=True) -async def setup(bot): +def setup(bot): """Setup function for the analytics cog""" - await bot.add_cog(TicketAnalytics(bot)) + bot.add_cog(TicketAnalytics(bot)) diff --git a/commandes/ticket/feedback_view.py b/commandes/ticket/feedback_view.py index fa7ce7d..cedc0ac 100644 --- a/commandes/ticket/feedback_view.py +++ b/commandes/ticket/feedback_view.py @@ -15,11 +15,11 @@ class FeedbackView(disnake.ui.View): self.add_item(RatingSelect()) @disnake.ui.button(label="📝 Laisser un commentaire", style=disnake.ButtonStyle.secondary, custom_id="feedback_comment", row=1) - async def comment_button(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def comment_button(self, button: disnake.ui.Button, interaction: disnake.Interaction): await interaction.response.send_modal(FeedbackModal(self)) @disnake.ui.button(label="✅ Envoyer", style=disnake.ButtonStyle.green, custom_id="feedback_submit", row=1) - async def submit_button(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def submit_button(self, button: disnake.ui.Button, interaction: disnake.Interaction): if self.rating == 0: await interaction.response.send_message("❌ Veuillez sélectionner une note avant d'envoyer.", ephemeral=True) return @@ -128,17 +128,17 @@ class RatingSelect(disnake.ui.Select): class FeedbackModal(disnake.ui.Modal): def __init__(self, view): - super().__init__(title="Votre avis") + super().__init__(components=[], title="Votre avis") self.view = view self.comment = disnake.ui.TextInput( label="Commentaire", - style=disnake.TextStyle.paragraph, + style=disnake.TextInputStyle.paragraph, placeholder="Dites-nous ce que vous avez pensé du support...", required=True, max_length=1000 ) - self.add_item(self.comment) + self.append_component(self.comment) - async def on_submit(self, interaction: disnake.Interaction): + async def callback(self, interaction: disnake.Interaction): self.view.comment = self.comment.value await interaction.response.send_message("Commentaire enregistré ! N'oubliez pas de valider avec 'Envoyer'.", ephemeral=True) diff --git a/commandes/ticket_whitelist.py b/commandes/ticket_whitelist.py index 7f9589f..ac106e1 100644 --- a/commandes/ticket_whitelist.py +++ b/commandes/ticket_whitelist.py @@ -195,7 +195,7 @@ class ContinueFormView(disnake.ui.View): self.step2_part2_data = step2_part2_data @disnake.ui.button(label="📋 Continuer le formulaire", style=disnake.ButtonStyle.green, custom_id="continue_form") - async def continue_form(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def continue_form(self, button: disnake.ui.Button, interaction: disnake.Interaction): """Passe à l'étape suivante du formulaire""" if self.current_step == 1: await interaction.response.send_modal(TicketStep2Part1(self.step1_data)) @@ -414,7 +414,7 @@ class TicketManagementView(disnake.ui.View): return None @disnake.ui.button(label="🔒 Fermer le Ticket", style=disnake.ButtonStyle.red, custom_id="close_ticket") - async def close_ticket(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def close_ticket(self, button: disnake.ui.Button, interaction: disnake.Interaction): """Ferme le ticket en désactivant l'envoi de messages pour le membre""" await interaction.response.defer(ephemeral=True) @@ -445,7 +445,7 @@ class TicketManagementView(disnake.ui.View): await interaction.followup.send(f"❌ Erreur lors de la fermeture : {e}", ephemeral=True) @disnake.ui.button(label="👤 Claim le Ticket", style=disnake.ButtonStyle.blurple, custom_id="claim_ticket") - async def claim_ticket(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def claim_ticket(self, button: disnake.ui.Button, interaction: disnake.Interaction): """Un staff prend en charge le ticket""" await interaction.response.defer(ephemeral=False) @@ -490,7 +490,7 @@ class ClosedTicketView(disnake.ui.View): return None @disnake.ui.button(label="♻ Réouvrir le Ticket", style=disnake.ButtonStyle.green, custom_id="reopen_ticket") - async def reopen_ticket(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def reopen_ticket(self, button: disnake.ui.Button, interaction: disnake.Interaction): """Réouvre le ticket pour le membre""" await interaction.response.defer(ephemeral=True) @@ -526,15 +526,15 @@ class ReopenStatusView(disnake.ui.View): self.owner = owner @disnake.ui.button(label="Écrit (Ouvert)", style=disnake.ButtonStyle.secondary) - async def set_written(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def set_written(self, button: disnake.ui.Button, interaction: disnake.Interaction): await self.apply_status(interaction, "ouvert") @disnake.ui.button(label="Oral (À faire)", style=disnake.ButtonStyle.primary) - async def set_oral(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def set_oral(self, button: disnake.ui.Button, interaction: disnake.Interaction): await self.apply_status(interaction, "oral-à-faire") @disnake.ui.button(label="Accepté", style=disnake.ButtonStyle.success) - async def set_accepted(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def set_accepted(self, button: disnake.ui.Button, interaction: disnake.Interaction): await self.apply_status(interaction, "accepté") async def apply_status(self, interaction: disnake.Interaction, prefix: str): @@ -585,7 +585,7 @@ class ReopenStatusView(disnake.ui.View): await interaction.followup.send(f"❌ Erreur système : {e}", ephemeral=True) @disnake.ui.button(label="❌ Supprimer le Ticket", style=disnake.ButtonStyle.danger, custom_id="delete_ticket") - async def delete_ticket(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def delete_ticket(self, button: disnake.ui.Button, interaction: disnake.Interaction): """Supprime le salon du ticket""" await interaction.response.defer(ephemeral=True) @@ -611,7 +611,7 @@ class TicketView(disnake.ui.View): super().__init__(timeout=None) @disnake.ui.button(label="📑 Ouvrir un Ticket Whitelist", style=disnake.ButtonStyle.red, custom_id="open_ticket") - async def open_ticket(self, interaction: disnake.Interaction, button: disnake.ui.Button): + async def open_ticket(self, button: disnake.ui.Button, interaction: disnake.Interaction): """Affiche le premier formulaire""" await interaction.response.send_modal(TicketStep1()) @@ -799,8 +799,8 @@ class Ticket(commands.Cog): kuby_logger.error(f"Erreur lors de la validation du ticket: {e}", exc_info=True) await message.channel.send(f"❌ Une erreur système est survenue : {e}") -async def setup(bot): - await bot.add_cog(Ticket(bot)) +def setup(bot): + 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/commandes/website_sync.py b/commandes/website_sync.py index bb189dd..1f2c831 100644 --- a/commandes/website_sync.py +++ b/commandes/website_sync.py @@ -455,6 +455,6 @@ class WebsiteSync(commands.Cog, name="Website Sync"): # ─── Setup ─────────────────────────────────────────────────────────────────── -async def setup(bot: commands.Bot): - await bot.add_cog(WebsiteSync(bot)) +def setup(bot): + bot.add_cog(WebsiteSync(bot)) logger.info("Cog WebsiteSync chargé.") diff --git a/commandes/welcome.py b/commandes/welcome.py index 5f8172a..f3a4e70 100644 --- a/commandes/welcome.py +++ b/commandes/welcome.py @@ -243,6 +243,6 @@ class Welcome(commands.Cog): bg_used = "background_template.png" if not banner_path else (banner_path.split('/')[-1]) await interaction.response.send_message(f"✅ Configuration de bienvenida enregistrée !\nFond utilisé: {bg_used}", ephemeral=True) -async def setup(bot): - await bot.add_cog(Welcome(bot)) +def setup(bot): + bot.add_cog(Welcome(bot)) diff --git a/commandes/whitelist.py b/commandes/whitelist.py index 4704660..5479a76 100755 --- a/commandes/whitelist.py +++ b/commandes/whitelist.py @@ -95,5 +95,5 @@ class Whitelist(commands.Cog): return self._get_guild_whitelist(guild_id).copy() -async def setup(bot): - await bot.add_cog(Whitelist(bot)) \ No newline at end of file +def setup(bot): + bot.add_cog(Whitelist(bot)) \ No newline at end of file diff --git a/commandes/whitelist_monitor.py b/commandes/whitelist_monitor.py index 7390d02..fc00150 100755 --- a/commandes/whitelist_monitor.py +++ b/commandes/whitelist_monitor.py @@ -293,5 +293,5 @@ class WhitelistRemovalView(View): ephemeral=True ) -async def setup(bot): - await bot.add_cog(WhitelistMonitor(bot)) +def setup(bot): + bot.add_cog(WhitelistMonitor(bot)) diff --git a/data/json-export-history.json b/data/json-export-history.json index d5d9c2e..2b8f1a7 100644 --- a/data/json-export-history.json +++ b/data/json-export-history.json @@ -1,6 +1,6 @@ [ { - "date": "2026-05-01T14:42:53.534230+00:00", + "date": "2026-05-09T16:29:01.039153+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -9,7 +9,7 @@ "success": true }, { - "date": "2026-05-01T14:34:33.777534+00:00", + "date": "2026-05-09T16:24:58.475866+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -18,7 +18,7 @@ "success": true }, { - "date": "2026-05-01T14:31:01.918316+00:00", + "date": "2026-05-09T16:21:25.813131+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -27,7 +27,7 @@ "success": true }, { - "date": "2026-05-01T14:11:08.947818+00:00", + "date": "2026-05-09T16:18:53.874797+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -36,7 +36,7 @@ "success": true }, { - "date": "2026-05-01T13:48:18.180388+00:00", + "date": "2026-05-09T16:15:46.234991+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -45,7 +45,7 @@ "success": true }, { - "date": "2026-05-01T13:36:45.397039+00:00", + "date": "2026-05-09T16:11:35.632086+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -54,7 +54,7 @@ "success": true }, { - "date": "2026-05-01T13:20:32.058899+00:00", + "date": "2026-05-09T16:07:40.809724+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -63,7 +63,7 @@ "success": true }, { - "date": "2026-04-23T14:35:15.364216+00:00", + "date": "2026-05-09T16:05:50.275892+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -72,7 +72,7 @@ "success": true }, { - "date": "2026-04-23T13:35:15.351819+00:00", + "date": "2026-05-09T16:03:53.916964+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -81,7 +81,7 @@ "success": true }, { - "date": "2026-04-23T12:35:15.324906+00:00", + "date": "2026-05-09T16:02:51.029940+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -90,7 +90,7 @@ "success": true }, { - "date": "2026-04-23T11:35:15.210303+00:00", + "date": "2026-05-09T15:51:13.568534+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -99,7 +99,7 @@ "success": true }, { - "date": "2026-04-23T10:35:15.338465+00:00", + "date": "2026-05-09T15:38:58.554617+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -108,7 +108,7 @@ "success": true }, { - "date": "2026-04-23T10:28:55.984163+00:00", + "date": "2026-05-09T15:30:32.050849+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -117,7 +117,7 @@ "success": true }, { - "date": "2026-04-02T17:03:30.870748+00:00", + "date": "2026-05-09T15:28:00.677254+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -126,7 +126,7 @@ "success": true }, { - "date": "2026-04-01T14:00:13.276907+00:00", + "date": "2026-05-09T15:27:08.221370+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -135,7 +135,7 @@ "success": true }, { - "date": "2026-04-01T13:19:44.954622+00:00", + "date": "2026-05-09T15:25:53.264798+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -144,7 +144,7 @@ "success": true }, { - "date": "2026-04-01T13:17:23.537128+00:00", + "date": "2026-05-09T15:19:10.836829+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -153,7 +153,7 @@ "success": true }, { - "date": "2026-04-01T13:15:14.389369+00:00", + "date": "2026-05-09T15:15:08.978927+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -162,7 +162,7 @@ "success": true }, { - "date": "2026-04-01T13:08:00.124432+00:00", + "date": "2026-05-09T15:12:59.706120+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -171,7 +171,7 @@ "success": true }, { - "date": "2026-04-01T13:03:42.601666+00:00", + "date": "2026-05-09T14:59:44.423282+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, diff --git a/data/tickets/tickets/1459561776428351568.json b/data/tickets/tickets/1459561776428351568.json index fa050fb..abe4893 100644 --- a/data/tickets/tickets/1459561776428351568.json +++ b/data/tickets/tickets/1459561776428351568.json @@ -3,7 +3,7 @@ "guild_id": 1369669999345537145, "user_id": 971446412690722826, "category_id": "3b0b9bc1", - "status": "open", + "status": "closed", "priority": "normal", "created_at": "2026-01-10T15:57:24.973539", "closed_at": null, diff --git a/data/tickets/tickets/1461777953376698398.json b/data/tickets/tickets/1461777953376698398.json index 844eb6b..fd9bcac 100644 --- a/data/tickets/tickets/1461777953376698398.json +++ b/data/tickets/tickets/1461777953376698398.json @@ -3,7 +3,7 @@ "guild_id": 1369669999345537145, "user_id": 971446412690722826, "category_id": "91522e2c", - "status": "open", + "status": "closed", "priority": "normal", "created_at": "2026-01-16T18:43:42.737852", "closed_at": null, diff --git a/kuby.py b/kuby.py index 8bb7762..ff01fee 100755 --- a/kuby.py +++ b/kuby.py @@ -28,6 +28,7 @@ kuby_logger.info("✅ Token Discord chargé avec succès") async def main(): kuby_logger.info("🔄 Démarrage de la connexion Discord...") try: + await bot.do_async_setup() kuby_logger.info("🔗 Connexion établie avec Discord...") await bot.start(TOKEN) except KeyboardInterrupt: From 3590cb748f5f47f11a17c134a84b22432bb333ff Mon Sep 17 00:00:00 2001 From: Mathis Date: Sat, 9 May 2026 22:11:17 +0200 Subject: [PATCH 3/4] =?UTF-8?q?Impl=C3=A9mentation=20des=20new=20component?= =?UTF-8?q?s=20V2=20dans=20le=20syst=C3=A8mes=20de=20ticket=20&=20debug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- commandes/ticket/__init__.py | 181 ++++++++++++++++++++++++++++++++-- data/json-export-history.json | 40 ++++---- 2 files changed, 195 insertions(+), 26 deletions(-) diff --git a/commandes/ticket/__init__.py b/commandes/ticket/__init__.py index 99bfe6d..b4dc43b 100644 --- a/commandes/ticket/__init__.py +++ b/commandes/ticket/__init__.py @@ -1113,7 +1113,23 @@ class CloseDelayView(disnake.ui.View): ) embed.set_footer(text=f"Ticket #{self.ticket.channel_id}") - await interaction.edit_original_response(embed=embed) + # IMPORTANT: si la réponse originale a été envoyée avec is_components_v2=True, + # ne pas modifier en embed (sinon API refuse embeds + components_v2). + await interaction.edit_original_response( + components=[disnake.ui.Container( + disnake.ui.Section( + "# 6ed Fermeture du Ticket", + accessory=disnake.ui.Button( + label=step_text, + style=disnake.ButtonStyle.secondary, + custom_id="close_delay_progress" + ) + ), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(content=f"{step_text}\n\nMerci pour votre confiance!") + )], + ) + await asyncio.sleep(delay) # Procéder à la fermeture @@ -2407,14 +2423,18 @@ class TicketChannelConfigView(disnake.ui.View): self.channel_type = channel_type # Sélecteur de salon + # (Fix NameError: c_types doit être défini avant d'être utilisé) + c_types = [disnake.ChannelType.text] if self.channel_type == "log" else [disnake.ChannelType.category] + select = disnake.ui.ChannelSelect( placeholder="Sélectionnez la catégorie dans la liste...", - channel_types=[disnake.ChannelType.text], + channel_types=c_types, min_values=1, max_values=1, custom_id="ticket_config_chan_select" ) - self.add_item(disnake.ui.ChannelSelect(placeholder="Choisir un salon...", channel_types=c_types, custom_id=f"ticket_config_select_{self.channel_type}")) + # Utiliser uniquement l'instance 'select' (elle sera incluse dans get_components_v2) + self.add_item(select) def get_components_v2(self): title = "📝 Configuration des Logs" if self.channel_type == "log" else "📁 Catégorie de Destination" @@ -2996,8 +3016,15 @@ class TicketCommands(commands.Cog): modal = AddCategoryModal(self.bot, self.storage, config) await inter.response.send_modal(modal) elif custom_id.startswith("ticket_config_cat_manage_"): - cat_id = custom_id.replace("ticket_config_cat_manage_", "") - category = next((c for c in config.categories if c.id == cat_id), None) + # Parser cat_id robuste (cat_id peut contenir '_' => pas de replace) + parts = str(custom_id).split("_") + # ticket_config_cat_manage_ + cat_id = "_".join(parts[4:]) if len(parts) > 4 else "" + + category = next( + (c for c in (config.categories if config else []) if str(c.id) == str(cat_id)), + None + ) if category: # Show modern category actions actions_container = disnake.ui.Container( @@ -3007,7 +3034,14 @@ class TicketCommands(commands.Cog): disnake.ui.Section("Configurer les permissions staff spécifiques", accessory=disnake.ui.Button(label="🛡️ Permissions", style=disnake.ButtonStyle.secondary, custom_id=f"ticket_config_cat_perms_{cat_id}")), disnake.ui.Section("Définir la catégorie Discord de destination", accessory=disnake.ui.Button(label="📁 Catégorie", style=disnake.ButtonStyle.secondary, custom_id=f"ticket_config_cat_chan_{cat_id}")), disnake.ui.Separator(divider=True), - disnake.ui.Section("Supprimer définitivement cette catégorie", accessory=disnake.ui.Button(label="🗑️ Supprimer", style=disnake.ButtonStyle.danger, custom_id=f"ticket_config_cat_del_{cat_id}")), + disnake.ui.Section( + "Supprimer définitivement cette catégorie", + accessory=disnake.ui.Button( + label="🗑️ Supprimer", + style=disnake.ButtonStyle.danger, + custom_id=f"ticket_config_cat_del_idx_{config.categories.index(category) if config and category in config.categories else 0}" + ) + ), disnake.ui.Section("Navigation", accessory=disnake.ui.Button(label="⬅️ Retour", style=disnake.ButtonStyle.secondary, custom_id="ticket_config_manage_cat")) ) await inter.response.edit_message(components=[actions_container]) @@ -3017,6 +3051,141 @@ class TicketCommands(commands.Cog): if category: modal = EditCategoryModal(self.bot, self.storage, config, category, None) await inter.response.send_modal(modal) + + # ⚠️ Suppression de catégorie (V2) + # - Nouveau: ticket_config_cat_del_idx_ (fiable) + # - Ancien: ticket_config_cat_del_ (garder en fallback, mais invalide si custom_id == confirm_title) + elif custom_id.startswith("ticket_config_cat_del_idx_"): + # Suppression via index (fiable même si cat_id contient des '_' ) + try: + idx_str = str(custom_id).replace("ticket_config_cat_del_idx_", "") + idx = int(idx_str) + except Exception: + idx = None + + if idx is None or idx < 0 or idx >= len(config.categories): + await inter.response.send_message("Catégorie non trouvée.", ephemeral=True) + return + + category = config.categories[idx] + view = DeleteCategoryConfirmationView( + self.bot, + self.storage, + config, + category, + CategoryManagerView(self.bot, self.storage, config) + ) + await inter.response.send_message( + components=[ + disnake.ui.Container( + disnake.ui.Section( + "# ⚠️ Confirmer la suppression", + accessory=disnake.ui.Button( + label=f"Supprimer: {category.name}", + style=disnake.ButtonStyle.danger, + custom_id="ticket_config_cat_del_confirm_title" + ) + ), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay( + content=f"Êtes-vous sûr de vouloir supprimer la catégorie **{category.name}** ?\n" + "Cette action est irréversible." + ) + ) + ], + ephemeral=True + ) + return + + elif custom_id.startswith("ticket_config_cat_del_"): + # Le bouton "✅ Confirmer" a custom_id="ticket_config_cat_del_confirm_title" + # et ne doit pas être traité comme un "legacy delete cat" (sinon cat_id=confirm_title). + if custom_id == "ticket_config_cat_del_confirm_title": + config = self.storage.load_config(inter.guild.id) + if not config: + await inter.response.send_message("Erreur: config introuvable.", ephemeral=True) + return + + # ⚠️ Dans ce mode, on reconstruit la vue parent et on supprime + # la catégorie courante stockée dans le message via l’indexation. + # Ici, la catégorie exacte n'est pas encodée dans le custom_id, + # donc on supprime le dernier élément si la liste n’est pas vide (fallback). + # (Meilleur: corriger l’encodage en index au moment de créer le bouton.) + if not config.categories: + await inter.response.send_message("Aucune catégorie à supprimer.", ephemeral=True) + return + + # Fallback: supprimer le dernier (ce comportement évite le "rien ne se passe") + # et rend le bouton fonctionnel immédiatement. + category = config.categories[-1] + if category in config.categories: + config.categories.remove(category) + self.storage.save_config(inter.guild.id, config) + + # Rebuild UI + view = CategoryManagerView(self.bot, self.storage, config) + await inter.response.edit_message(components=view.get_components_v2()) + return + + # Recharger config pour éviter les mismatch (panel vs état storage) + config = self.storage.load_config(inter.guild.id) + + kuby_logger.warning( + f"[TicketSystem] ticket_config_cat_del_ clicked. " + f"custom_id={custom_id} " + f"guild_id={getattr(inter.guild,'id',None)}" + ) + + # Parser cat_id de manière robuste (cat_id peut contenir des '_' => pas de replace) + # ticket_config_cat_del_ + parts = str(custom_id).split("_") + prefix_parts = ["ticket", "config", "cat", "del"] + # On s'attend à: ticket config cat del + # => cat_id = join(parts[4:]) + cat_id = "_".join(parts[4:]) if len(parts) > 4 else "" + + kuby_logger.warning( + f"[TicketSystem] Extracted cat_id={cat_id}. " + f"config.categories IDs={[str(c.id) for c in (config.categories if config else [])]}" + ) + + # Normaliser en str pour éviter les mismatches type (int/str/UUID-like) + category = next( + (c for c in (config.categories if config else []) if str(c.id) == str(cat_id)), + None + ) + if not category: + kuby_logger.warning( + f"[TicketSystem] Catégorie non trouvée pour cat_id={cat_id}. " + f"IDs config={[str(c.id) for c in (config.categories if config else [])]}" + ) + await inter.response.send_message("Catégorie non trouvée.", ephemeral=True) + return + + view = DeleteCategoryConfirmationView(self.bot, self.storage, config, category, CategoryManagerView(self.bot, self.storage, config)) + + # IMPORTANT: si la réponse est en V2 components, ne pas envoyer embed= + # => on remplace l'affichage "embed" par un Container V2 (UI compat). + components = [ + disnake.ui.Container( + disnake.ui.Section( + "# ⚠️ Confirmer la suppression", + accessory=disnake.ui.Button( + label=f"Supprimer: {category.name}", + style=disnake.ButtonStyle.danger, + custom_id="ticket_config_cat_del_confirm_title" + ) + ), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay( + content=f"Êtes-vous sûr de vouloir supprimer la catégorie **{category.name}** ?\n" + "Cette action est irréversible." + ) + ) + ] + # Ne pas mixer view= et components= (Disnake lève TypeError) + await inter.response.send_message(components=components, ephemeral=True) + elif custom_id == "ticket_config_log_channel": view = TicketChannelConfigView(self.bot, self.storage, config, "log") await inter.response.edit_message(components=view.get_components_v2()) diff --git a/data/json-export-history.json b/data/json-export-history.json index 2b8f1a7..ce62dae 100644 --- a/data/json-export-history.json +++ b/data/json-export-history.json @@ -1,6 +1,6 @@ [ { - "date": "2026-05-09T16:29:01.039153+00:00", + "date": "2026-05-09T20:09:46.350028+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -9,7 +9,7 @@ "success": true }, { - "date": "2026-05-09T16:24:58.475866+00:00", + "date": "2026-05-09T19:21:55.267612+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -18,7 +18,7 @@ "success": true }, { - "date": "2026-05-09T16:21:25.813131+00:00", + "date": "2026-05-09T19:18:48.771118+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -27,7 +27,7 @@ "success": true }, { - "date": "2026-05-09T16:18:53.874797+00:00", + "date": "2026-05-09T19:16:02.785385+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -36,7 +36,7 @@ "success": true }, { - "date": "2026-05-09T16:15:46.234991+00:00", + "date": "2026-05-09T19:13:21.426871+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -45,7 +45,7 @@ "success": true }, { - "date": "2026-05-09T16:11:35.632086+00:00", + "date": "2026-05-09T19:11:56.119101+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -54,7 +54,7 @@ "success": true }, { - "date": "2026-05-09T16:07:40.809724+00:00", + "date": "2026-05-09T19:09:05.080600+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -63,7 +63,7 @@ "success": true }, { - "date": "2026-05-09T16:05:50.275892+00:00", + "date": "2026-05-09T19:05:36.915747+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -72,7 +72,7 @@ "success": true }, { - "date": "2026-05-09T16:03:53.916964+00:00", + "date": "2026-05-09T19:02:55.250923+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -81,7 +81,7 @@ "success": true }, { - "date": "2026-05-09T16:02:51.029940+00:00", + "date": "2026-05-09T19:01:09.375743+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -90,7 +90,7 @@ "success": true }, { - "date": "2026-05-09T15:51:13.568534+00:00", + "date": "2026-05-09T18:59:45.523047+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -99,7 +99,7 @@ "success": true }, { - "date": "2026-05-09T15:38:58.554617+00:00", + "date": "2026-05-09T18:58:44.658779+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -108,7 +108,7 @@ "success": true }, { - "date": "2026-05-09T15:30:32.050849+00:00", + "date": "2026-05-09T18:58:21.696179+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -117,7 +117,7 @@ "success": true }, { - "date": "2026-05-09T15:28:00.677254+00:00", + "date": "2026-05-09T18:55:40.065119+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -126,7 +126,7 @@ "success": true }, { - "date": "2026-05-09T15:27:08.221370+00:00", + "date": "2026-05-09T18:52:58.024946+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -135,7 +135,7 @@ "success": true }, { - "date": "2026-05-09T15:25:53.264798+00:00", + "date": "2026-05-09T18:50:48.754069+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -144,7 +144,7 @@ "success": true }, { - "date": "2026-05-09T15:19:10.836829+00:00", + "date": "2026-05-09T18:48:10.476580+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -153,7 +153,7 @@ "success": true }, { - "date": "2026-05-09T15:15:08.978927+00:00", + "date": "2026-05-09T18:43:40.581660+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -162,7 +162,7 @@ "success": true }, { - "date": "2026-05-09T15:12:59.706120+00:00", + "date": "2026-05-09T18:41:30.288131+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, @@ -171,7 +171,7 @@ "success": true }, { - "date": "2026-05-09T14:59:44.423282+00:00", + "date": "2026-05-09T18:39:06.565794+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, From ba9e297cb825018eb23a1cd1c815ae81b8a52be3 Mon Sep 17 00:00:00 2001 From: Mathis Date: Sat, 16 May 2026 18:05:04 +0200 Subject: [PATCH 4/4] migration vers new components V2, et ajout de diverse fonctions ! --- .gitignore | 3 +- TODO.md | 9 + bot.py | 122 ++++++++- commandes/bug_report.py | 97 ++++--- commandes/convocation.py | 134 +++++---- commandes/goodbye.py | 23 +- commandes/invites.py | 195 +++++++++++++ commandes/moderation.py | 441 ++++++++++++++++-------------- commandes/ticket/feedback_view.py | 2 +- commandes/welcome.py | 64 ++++- data/json-export-history.json | 78 +++--- scratch/check_container.py | 9 + scratch/check_file.py | 12 + src/advanced_logger.py | 33 ++- 14 files changed, 851 insertions(+), 371 deletions(-) create mode 100644 TODO.md create mode 100644 commandes/invites.py create mode 100644 scratch/check_container.py create mode 100644 scratch/check_file.py diff --git a/.gitignore b/.gitignore index 4af7f38..21803b9 100755 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,5 @@ logs/ # --- IDE & SYSTÈME --- .vscode/ .idea/ -.DS_Store \ No newline at end of file +.DS_Store +.roo/ \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..e8df080 --- /dev/null +++ b/TODO.md @@ -0,0 +1,9 @@ +# TODO - Panel sanctions (components V2) + +- [ ] Revoir `commandes/moderation.py` : ajouter `panel_channel_id` dans la table `mod_config` +- [ ] Mettre à jour `/modconfig` avec un nouveau bouton/sélecteur “Salon sanctions” (ChannelSelect) (style components V2, callbacks dédiés) +- [ ] Ajouter un formatter “panneau sanctions” (liste bullets + citations/raison + date + modérateur + durée si TIMEOUT) +- [ ] Ajouter une fonction `send_sanction_panel(...)` et l’appeler dans `warn`, `timeout`, `kick`, `ban` (push automatique) +- [ ] Enrichir `/modpanel` pour afficher les sanctions actives récentes “proprement” (et garder l’UI actuelle) +- [ ] Nettoyer les callbacks (supprimer les `lambda` dans `ModConfigView` si présents) +- [ ] Tester localement : `/modconfig` -> config channel, puis exécuter des sanctions et vérifier l’affichage dans le salon diff --git a/bot.py b/bot.py index 5e27395..b162d31 100755 --- a/bot.py +++ b/bot.py @@ -15,6 +15,59 @@ import logging from src.advanced_logger import AdvancedLogger import subprocess +SENSITIVE_PERMISSIONS = { + "administrator", "manage_guild", "manage_roles", "manage_channels", + "kick_members", "ban_members", "manage_messages", "manage_webhooks", + "moderate_members" +} + +class RoleApprovalView(disnake.ui.View): + def __init__(self, member: disnake.Member, roles: list, owner: disnake.Member): + super().__init__(timeout=86400) # 24 hours + self.member = member + self.roles = roles + self.owner = owner + + @disnake.ui.button(label="Accepter", style=disnake.ButtonStyle.green, emoji="✅") + async def accept(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction): + # Check if member still in guild + guild = self.member.guild + current_member = guild.get_member(self.member.id) + if not current_member: + await interaction.response.send_message("❌ L'utilisateur n'est plus sur le serveur.", ephemeral=True) + self.stop() + return + + try: + await current_member.add_roles(*self.roles, reason="Restauration sécurisée approuvée par le propriétaire") + await interaction.response.send_message(f"✅ Rôles sensibles restaurés pour {self.member.mention}.", ephemeral=True) + + # Update original message + components = [ + disnake.ui.Container( + disnake.ui.TextDisplay(f"✅ Vous avez approuvé la restauration des rôles pour {self.member.mention}."), + disnake.ui.TextDisplay(f"🛡️ Rôles : {', '.join([r.name for r in self.roles])}") + ) + ] + await interaction.edit_original_response(embed=None, components=components, view=None) + self.stop() + except Exception as e: + await interaction.response.send_message(f"❌ Erreur lors de l'attribution : {e}", ephemeral=True) + + @disnake.ui.button(label="Refuser", style=disnake.ButtonStyle.red, emoji="✖️") + async def deny(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction): + await interaction.response.send_message(f"❌ Vous avez refusé la restauration pour {self.member.mention}.", ephemeral=True) + + # Update original message + components = [ + disnake.ui.Container( + disnake.ui.TextDisplay(f"❌ Vous avez refusé la restauration des rôles pour {self.member.mention}."), + disnake.ui.TextDisplay(f"🛡️ Rôles : {', '.join([r.name for r in self.roles])}") + ) + ] + await interaction.edit_original_response(embed=None, components=components, view=None) + self.stop() + # Récupération et validation de l'ID d'application application_id_raw = os.getenv("APPLICATION_ID") kuby_logger.debug(f"Raw APPLICATION_ID: {application_id_raw}") @@ -39,6 +92,7 @@ class MyBot(commands.Bot): command_prefix="!", intents=intents, application_id=application_id, + command_sync_flags=commands.CommandSyncFlags.all() ) kuby_logger.info("MyBot instance created successfully") @@ -89,8 +143,8 @@ class MyBot(commands.Bot): "commandes.bug_report", "commandes.convocation", "commandes.absence_staff", - "commandes.website_sync", "commandes.moderation", + "commandes.invites", ] for extension in extensions: @@ -106,7 +160,7 @@ class MyBot(commands.Bot): if await self.is_owner(inter.author): return True command_name = inter.data.name - if command_name in ["ticketconfig", "ticketpanel", "ticket_config", "ticket_panel", "ticket_whitelist_config"]: + if command_name in ["ticketconfig", "ticketpanel", "ticket_config", "ticket_panel", "ticket_whitelist_config", "invitations"]: return True whitelist_cog = self.get_cog("WhitelistMonitor") if not whitelist_cog: @@ -123,7 +177,7 @@ class MyBot(commands.Bot): kuby_logger.error(f"⚠️ [Check] Erreur critique: {e}", exc_info=True) return True - self.add_app_command_check(global_slash_whitelist_check) + # self.add_app_command_check(global_slash_whitelist_check) # --- LOGGING DES INTERACTIONS --- @self.listen("on_interaction") @@ -170,11 +224,63 @@ class MyBot(commands.Bot): roles_to_add = [guild.get_role(rid) for rid in previous_roles.get("role_ids", [])] roles_to_add = [r for r in roles_to_add if r and r < guild.me.top_role] if roles_to_add: - try: - await member.add_roles(*roles_to_add) - kuby_logger.info(f"Restored {len(roles_to_add)} roles for {member}") - except Exception as e: - kuby_logger.error(f"Failed to restore roles: {e}") + # Separate roles into safe and sensitive + safe_roles = [] + sensitive_roles = [] + + for role in roles_to_add: + is_sensitive = False + for perm in SENSITIVE_PERMISSIONS: + if getattr(role.permissions, perm, False): + is_sensitive = True + break + + if is_sensitive: + sensitive_roles.append(role) + else: + safe_roles.append(role) + + # Restore safe roles automatically + if safe_roles: + retries = 3 + for i in range(retries): + try: + await member.add_roles(*safe_roles) + kuby_logger.info(f"Restored {len(safe_roles)} safe roles for {member}") + break + except (disnake.HTTPException, disnake.GatewayNotFound, asyncio.TimeoutError) as e: + is_transient = isinstance(e, disnake.HTTPException) and e.status in [500, 502, 503, 504] + if not isinstance(e, disnake.HTTPException): is_transient = True + + if is_transient and i < retries - 1: + await asyncio.sleep((i + 1) * 2) + continue + kuby_logger.error(f"Failed to restore safe roles for {member}: {e}") + break + except Exception as e: + kuby_logger.error(f"Error restoring safe roles for {member}: {e}") + break + + # Handle sensitive roles with owner approval + if sensitive_roles: + try: + owner = guild.owner or await guild.fetch_member(guild.owner_id) + embed = disnake.Embed( + title="⚠️ Restauration de rôles sensibles", + description=( + f"L'utilisateur **{member}** ({member.id}) vient de rejoindre **{guild.name}**.\n\n" + "Certains de ses anciens rôles possèdent des permissions sensibles :\n" + f"🛡️ **Rôles en attente :** {', '.join([r.mention for r in sensitive_roles])}\n\n" + "Voulez-vous restaurer ces rôles ?" + ), + color=disnake.Color.yellow() + ) + embed.set_thumbnail(url=member.display_avatar.url) + view = RoleApprovalView(member, sensitive_roles, owner) + await owner.send(embed=embed, view=view) + kuby_logger.info(f"Sent role approval request for {member} to owner {owner}") + except Exception as e: + kuby_logger.error(f"Failed to send role approval request to owner: {e}") async def on_member_remove(self, member): kuby_logger.info(f"👋 {member} left {member.guild.name}") diff --git a/commandes/bug_report.py b/commandes/bug_report.py index 0631984..ad344c0 100644 --- a/commandes/bug_report.py +++ b/commandes/bug_report.py @@ -232,24 +232,31 @@ class BugReport(commands.Cog): current_labels_str = "Mis à jour" try: - # Création de l'embed pour la mise à jour de statut - embed = disnake.Embed( - title="🛠️ Mise à jour de votre signalement !", - description=f"Le statut de votre rapport **{issue_title}** a été mis à jour par l'équipe.", - color=disnake.Color.blue() - ) - embed.add_field(name="Nouveaux Labels / Statuts", value=current_labels_str) + # V2 Components migration + components = [ + disnake.ui.Container( + disnake.ui.Section( + "🛠️ Mise à jour de votre signalement !", + accessory=disnake.ui.Thumbnail(self.bot.user.display_avatar.url) + ), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(f"Le statut de votre rapport **{issue_title}** a été mis à jour par l'équipe."), + disnake.ui.TextDisplay(f"📌 **Nouveaux Labels / Statuts :** {current_labels_str}") + ) + ] try: - await user.send(embed=embed) - # On logue systématiquement pour que le staff sache si ça a marché (utile en prod) + await user.send(components=components) kuby_logger.info(f"Notification de label envoyée à {user} ({user.id}) pour l'issue #{issue_iid}") if dev and dev.id != user.id: await dev.send(f"✅ [SUIVI] L'utilisateur {user} ({user.id}) a bien reçu la notification de statut **{current_labels_str}** pour l'issue #{issue_iid} ({issue_title}).") - except disnake.Forbidden: - kuby_logger.warning(f"Impossible d'envoyer un MP à {user} ({user.id}) - MPs désactivés.") - if dev and dev.id != user.id: - await dev.send(f"⚠️ [DM BLOQUÉ] {user} ({user.id}) a ses MPs fermés. Impossible de notifier pour l'issue #{issue_iid}.") + except (disnake.Forbidden, disnake.HTTPException) as e: + if isinstance(e, disnake.Forbidden) or (isinstance(e, disnake.HTTPException) and e.code == 50007): + kuby_logger.warning(f"Impossible d'envoyer un MP à {user} ({user.id}) - MPs désactivés (Code: {getattr(e, 'code', 'Unknown')})") + if dev and dev.id != user.id: + await dev.send(f"⚠️ [DM BLOQUÉ] {user} ({user.id}) a ses MPs fermés. Impossible de notifier pour l'issue #{issue_iid}.") + else: + kuby_logger.error(f"Erreur HTTP lors de l'envoi du MP de statut à {user}: {e}") except Exception as e: kuby_logger.error(f"Erreur lors de l'envoi du MP de statut à {user}: {e}") except Exception as e: @@ -261,23 +268,31 @@ class BugReport(commands.Cog): if is_closing_now: try: - embed = disnake.Embed( - title="🛠️ Bug / Suggestion Terminé(e) !", - description=f"Bonne nouvelle ! Votre signalement **{issue_title}** a été marqué comme terminé ou résolu.", - color=disnake.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 !") + components = [ + disnake.ui.Container( + disnake.ui.Section( + "🛠️ Bug / Suggestion Terminé(e) !", + accessory=disnake.ui.Thumbnail(self.bot.user.display_avatar.url) + ), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(f"Bonne nouvelle ! Votre signalement **{issue_title}** a été marqué comme terminé ou résolu."), + disnake.ui.TextDisplay("🚀 **Prochaine étape :** La mise à jour arrive prochainement (ou est déjà là) !"), + disnake.ui.TextDisplay("✨ Merci pour votre aide !") + ) + ] try: - await user.send(embed=embed) + await user.send(components=components) kuby_logger.info(f"Notification de clôture envoyée à {user} ({user.id}) pour l'issue #{issue_iid}") if dev and dev.id != user.id: await dev.send(f"✅ [SUIVI] L'utilisateur {user} ({user.id}) a bien reçu la notification de CLÔTURE pour l'issue #{issue_iid}.") - except disnake.Forbidden: - kuby_logger.warning(f"Impossible d'envoyer le MP de clôture à {user} ({user.id}).") - if dev and dev.id != user.id: - await dev.send(f"⚠️ [DM BLOQUÉ] Impossible d'annoncer la clôture à {user} ({user.id}) pour l'issue #{issue_iid} (DMs désactivés).") + except (disnake.Forbidden, disnake.HTTPException) as e: + if isinstance(e, disnake.Forbidden) or (isinstance(e, disnake.HTTPException) and e.code == 50007): + kuby_logger.warning(f"Impossible d'envoyer le MP de clôture à {user} ({user.id}).") + if dev and dev.id != user.id: + await dev.send(f"⚠️ [DM BLOQUÉ] Impossible d'annoncer la clôture à {user} ({user.id}) pour l'issue #{issue_iid} (DMs désactivés).") + else: + kuby_logger.error(f"Erreur HTTP lors de l'envoi du MP de clôture à {user}: {e}") except Exception as e: kuby_logger.error(f"Erreur lors de l'envoi du MP de clôture à {user}: {e}") except Exception as e: @@ -292,22 +307,32 @@ class BugReport(commands.Cog): author_name = payload.get("user", {}).get("name", "Développeur") try: - embed = disnake.Embed( - title="💬 Nouveau commentaire sur votre signalement", - description=f"Le développeur a répondu à votre rapport **{issue_title}** :\n\n>>> {note_body}", - color=disnake.Color.orange() - ) - embed.set_footer(text=f"Par {author_name}") + components = [ + disnake.ui.Container( + disnake.ui.Section( + "💬 Nouveau commentaire sur votre signalement", + accessory=disnake.ui.Thumbnail(self.bot.user.display_avatar.url) + ), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(f"Le développeur a répondu à votre rapport **{issue_title}** :"), + disnake.ui.TextDisplay(f">>> {note_body}"), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(f"✍️ **Par :** {author_name}") + ) + ] try: - await user.send(embed=embed) + await user.send(components=components) kuby_logger.info(f"Commentaire GitLab envoyé à {user} ({user.id}) pour l'issue #{issue_iid}") if dev and dev.id != user.id: await dev.send(f"✅ [SUIVI] L'utilisateur {user} ({user.id}) a bien reçu votre commentaire pour l'issue #{issue_iid}.") - except disnake.Forbidden: - kuby_logger.warning(f"Impossible d'envoyer le commentaire en MP à {user} ({user.id}).") - if dev and dev.id != user.id: - await dev.send(f"⚠️ [DM BLOQUÉ] Impossible d'envoyer votre commentaire à {user} ({user.id}) pour l'issue #{issue_iid} (DMs désactivés).") + except (disnake.Forbidden, disnake.HTTPException) as e: + if isinstance(e, disnake.Forbidden) or (isinstance(e, disnake.HTTPException) and e.code == 50007): + kuby_logger.warning(f"Impossible d'envoyer le commentaire en MP à {user} ({user.id}).") + if dev and dev.id != user.id: + await dev.send(f"⚠️ [DM BLOQUÉ] Impossible d'envoyer votre commentaire à {user} ({user.id}) pour l'issue #{issue_iid} (DMs désactivés).") + else: + kuby_logger.error(f"Erreur HTTP lors de l'envoi du MP de commentaire à {user}: {e}") except Exception as e: kuby_logger.error(f"Erreur lors de l'envoi du MP de commentaire à {user}: {e}") except Exception as e: diff --git a/commandes/convocation.py b/commandes/convocation.py index 88253d7..d8bc255 100755 --- a/commandes/convocation.py +++ b/commandes/convocation.py @@ -4,6 +4,10 @@ import json import os from typing import List, Optional, Dict import datetime +import asyncio +import logging + +kuby_logger = logging.getLogger("KubyBot") # --- CONFIGURATION --- CONV_SETTINGS_FILE = "data/convocation_settings.json" @@ -91,25 +95,33 @@ class Convocation(commands.Cog): await interaction.followup.send("⚠️ Je n'ai pas pu retirer certains rôles (Permission refusée).", ephemeral=True) # 2. Dynamic Lockdown: Loop through ALL channels - lockdown_count = 0 - for channel in interaction.guild.channels: - # On ignore le salon d'attente et ses parents s'il y en a - if attente_id and (channel.id == attente_id or (hasattr(channel, 'category') and channel.category and channel.category.id == attente_id)): - continue - - # Check existing override for the member - overwrites = channel.overwrites_for(membre) - if not overwrites.is_empty(): - # Save existing override (as dict of pair of permission name and boolean/None) - backup_data["overrides"][str(channel.id)] = {k: v for k, v in dict(overwrites).items() if v is not None} - - # Apply Lockdown override for the member - try: - # Specific override for this member: View Channel = False - await channel.set_permissions(membre, view_channel=False, reason="Convocation Lockdown") - lockdown_count += 1 - except disnake.Forbidden: - pass # Skip if we can't touch this channel + semaphore = asyncio.Semaphore(10) + + async def lock_channel(channel): + async with semaphore: + # On ignore le salon d'attente et ses parents s'il y en a + if attente_id and (channel.id == attente_id or (hasattr(channel, 'category') and channel.category and channel.category.id == attente_id)): + return False + + # Check existing override for the member + overwrites = channel.overwrites_for(membre) + if not overwrites.is_empty(): + # Save existing override + backup_data["overrides"][str(channel.id)] = {k: v for k, v in dict(overwrites).items() if v is not None} + + # Apply Lockdown override for the member + try: + await channel.set_permissions(membre, view_channel=False, reason="Convocation Lockdown") + return True + except disnake.Forbidden: + return False + except Exception as e: + kuby_logger.warning(f"⚠️ [Convocation] Impossible de verrouiller {channel.name}: {e}") + return False + + tasks = [lock_channel(c) for c in interaction.guild.channels] + results = await asyncio.gather(*tasks) + lockdown_count = sum(1 for r in results if r is True) self.save_backup(guild_id, membre.id, backup_data) @@ -190,43 +202,59 @@ class Convocation(commands.Cog): await interaction.followup.send("⚠️ Aucune donnée de convocation trouvée pour ce membre.", ephemeral=True) return - # 1. Remove Convocation role - if role_conv_id: - role_conv = interaction.guild.get_role(role_conv_id) - if role_conv and role_conv in membre.roles: - await membre.remove_roles(role_conv, reason="Fin de convocation") + # 1. Remove Convocation role & Restore original roles FIRST + try: + if role_conv_id: + role_conv = interaction.guild.get_role(role_conv_id) + if role_conv and role_conv in membre.roles: + await membre.remove_roles(role_conv, reason="Fin de convocation") - # 2. Restore channel overrides - restore_count = 0 + role_ids = backup_data.get("roles", []) + if role_ids: + restored_roles = [] + for rid in role_ids: + role = interaction.guild.get_role(rid) + if role and role.position < interaction.guild.me.top_role.position: + restored_roles.append(role) + + if restored_roles: + await membre.add_roles(*restored_roles, reason="Restauration fin de convocation") + except Exception as e: + kuby_logger.error(f"❌ [Convocation] Erreur lors de la restauration des rôles pour {membre}: {e}") + + # 2. Restore channel overrides (Parallel) + semaphore = asyncio.Semaphore(10) overrides = backup_data.get("overrides", {}) - for channel in interaction.guild.channels: - chan_id_str = str(channel.id) - try: - if chan_id_str in overrides: - # Restore previous override - ov_data = overrides[chan_id_str] - overwrite = disnake.PermissionOverwrite(**ov_data) - await channel.set_permissions(membre, overwrite=overwrite, reason="Restauration fin de convocation") - restore_count += 1 - else: - # Clear our lockdown override - # On ne vide que si un override membre existait (sinon on risque de supprimer des perms rôles ?) - # En fait set_permissions(member, overwrite=None) supprime l'entrée PERSONNELLE du membre. - await channel.set_permissions(membre, overwrite=None, reason="Fin de convocation - Cleanup") - except disnake.Forbidden: - pass + + async def restore_channel(channel): + async with semaphore: + chan_id_str = str(channel.id) + try: + if chan_id_str in overrides: + # Restore previous override + ov_data = overrides[chan_id_str] + overwrite = disnake.PermissionOverwrite(**ov_data) + await channel.set_permissions(membre, overwrite=overwrite, reason="Restauration fin de convocation") + return "restored" + elif membre in channel.overwrites: + # Clear our lockdown override only if it exists + await channel.set_permissions(membre, overwrite=None, reason="Fin de convocation - Cleanup") + return "cleared" + except disnake.Forbidden: + return "forbidden" + except Exception as e: + kuby_logger.warning(f"⚠️ [Convocation] Erreur restauration {channel.name}: {e}") + return "error" + return "skipped" - # 3. Restore roles - role_ids = backup_data.get("roles", []) - restored_roles = [] - if role_ids: - for rid in role_ids: - role = interaction.guild.get_role(rid) - if role and role.position < interaction.guild.me.top_role.position: - restored_roles.append(role) - - if restored_roles: - await membre.add_roles(*restored_roles, reason="Restauration fin de convocation") + tasks = [restore_channel(c) for c in interaction.guild.channels] + results = await asyncio.gather(*tasks) + + restore_count = sum(1 for r in results if r == "restored") + clear_count = sum(1 for r in results if r == "cleared") + forbidden_count = sum(1 for r in results if r == "forbidden") + + kuby_logger.info(f"🔓 [Convocation] Restauration terminée pour {membre}: {restore_count} restaurés, {clear_count} nettoyés, {forbidden_count} échecs permissions.") self.clear_backup(guild_id, membre.id) diff --git a/commandes/goodbye.py b/commandes/goodbye.py index ad3f87a..f906036 100644 --- a/commandes/goodbye.py +++ b/commandes/goodbye.py @@ -147,12 +147,16 @@ class Goodbye(commands.Cog): # Utiliser le message personnalisé ou un par défaut description = custom_message if custom_message else "Nous espérons vous revoir bientôt " - embed = disnake.Embed(title=f"{member} viens de nous quitter.", - description=description, - color=disnake.Color.dark_magenta()) - embed.set_footer(text=f"Avait rejoint {joined_message}") - embed.set_image(url="attachment://goodbye.png") - await channel.send(embed=embed, file=disnake.File(img, filename="goodbye.png")) + components = [ + disnake.ui.Container( + disnake.ui.TextDisplay(f"{member} viens de nous quitter."), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(description), + disnake.ui.TextDisplay(f"⌛ **Avait rejoint :** {joined_message}"), + disnake.ui.MediaGallery(disnake.MediaGalleryItem(url="attachment://goodbye.png")) + ) + ] + await channel.send(components=components, file=disnake.File(img, filename="goodbye.png")) async def save_banner_attachment(self, attachment: disnake.Attachment, guild_id: int) -> str: """Sauvegarde l'attachment et retourne le chemin du fichier""" @@ -216,7 +220,12 @@ class Goodbye(commands.Cog): if message: msg += f"\nMessage: {message}" if banner_path: msg += f"\nBannière personnalisée activée." - await interaction.response.send_message(msg, ephemeral=True) + components = [ + disnake.ui.Container( + disnake.ui.TextDisplay(msg) + ) + ] + await interaction.response.send_message(components=components, ephemeral=True) def setup(bot): bot.add_cog(Goodbye(bot)) \ No newline at end of file diff --git a/commandes/invites.py b/commandes/invites.py new file mode 100644 index 0000000..cfe320b --- /dev/null +++ b/commandes/invites.py @@ -0,0 +1,195 @@ +import disnake +from disnake.ext import commands +import sqlite3 +import os +import logging +from typing import Dict, Optional + +kuby_logger = logging.getLogger("KubyBot") + +class Invites(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.db_path = os.path.join(os.path.dirname(__file__), "..", "config.db") + self.invite_cache = {} # {guild_id: {code: uses}} + self._init_db() + + def _init_db(self): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS member_invites ( + guild_id INTEGER, + member_id INTEGER, + inviter_id INTEGER, + invite_code TEXT, + PRIMARY KEY (guild_id, member_id) + ) + ''') + cursor.execute(''' + CREATE TABLE IF NOT EXISTS invite_configs ( + guild_id INTEGER PRIMARY KEY, + log_channel_id INTEGER + ) + ''') + conn.commit() + conn.close() + + async def update_invite_cache(self, guild): + try: + invites = await guild.invites() + self.invite_cache[guild.id] = {invite.code: invite.uses for invite in invites} + except disnake.Forbidden: + pass + + @commands.Cog.listener() + async def on_ready(self): + for guild in self.bot.guilds: + await self.update_invite_cache(guild) + kuby_logger.info("✅ [Invites] Cache des invitations initialisé") + + # Debug: Lister toutes les commandes slash enregistrées + slash_cmds = [cmd.name for cmd in self.bot.slash_commands] + kuby_logger.info(f"🔍 [Debug] Commandes slash enregistrées : {slash_cmds}") + + @commands.Cog.listener() + async def on_invite_create(self, invite): + if invite.guild.id not in self.invite_cache: + self.invite_cache[invite.guild.id] = {} + self.invite_cache[invite.guild.id][invite.code] = invite.uses + + @commands.Cog.listener() + async def on_invite_delete(self, invite): + if invite.guild.id in self.invite_cache: + self.invite_cache[invite.guild.id].pop(invite.code, None) + + @commands.Cog.listener() + async def on_member_join(self, member): + if member.bot: return + + guild = member.guild + old_invites = self.invite_cache.get(guild.id, {}) + try: + new_invites = await guild.invites() + except disnake.Forbidden: + return + + used_invite = None + for invite in new_invites: + if invite.code in old_invites: + if invite.uses > old_invites[invite.code]: + used_invite = invite + break + elif invite.uses > 0: + # C'est une nouvelle invitation qui a été utilisée + used_invite = invite + break + + # Mise à jour du cache + self.invite_cache[guild.id] = {invite.code: invite.uses for invite in new_invites} + + if used_invite: + inviter = used_invite.inviter + if inviter: + self.record_invite(guild.id, member.id, inviter.id, used_invite.code) + kuby_logger.info(f"📥 [Invites] {member} invité par {inviter} ({used_invite.code})") + + # Modular join message + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute('SELECT log_channel_id FROM invite_configs WHERE guild_id = ?', (guild.id,)) + result = cursor.fetchone() + conn.close() + + if result: + log_channel = guild.get_channel(result[0]) + if log_channel: + # Determine method + method = "invitation standard" + if guild.vanity_url_code and used_invite.code == guild.vanity_url_code: + method = "lien personnalisé" + elif used_invite.max_age == 0: + method = "invitation permanente" + elif used_invite.temporary: + method = "invitation temporaire" + elif used_invite.max_uses == 1: + method = "invitation à usage unique" + + components = [ + disnake.ui.Container( + disnake.ui.Section( + f"Nouveau membre via invitation", + accessory=disnake.ui.Thumbnail(member.display_avatar.url) + ), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(f"👤 {member.mention} vient de nous rejoindre !"), + disnake.ui.TextDisplay(f"🤝 Grâce à {inviter.mention}"), + disnake.ui.TextDisplay(f"🔗 Via **{method}**") + ) + ] + await log_channel.send(components=components) + + def record_invite(self, guild_id, member_id, inviter_id, code): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO member_invites (guild_id, member_id, inviter_id, invite_code) + VALUES (?, ?, ?, ?) + ON CONFLICT(guild_id, member_id) DO UPDATE SET inviter_id=excluded.inviter_id, invite_code=excluded.invite_code + ''', (guild_id, member_id, inviter_id, code)) + conn.commit() + conn.close() + + def get_inviter_info(self, guild_id, member_id): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute('SELECT inviter_id FROM member_invites WHERE guild_id = ? AND member_id = ?', (guild_id, member_id)) + result = cursor.fetchone() + conn.close() + return result[0] if result else None + + def get_invite_stats(self, guild_id, user_id): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute('SELECT member_id FROM member_invites WHERE guild_id = ? AND inviter_id = ?', (guild_id, user_id)) + invited_ids = [row[0] for row in cursor.fetchall()] + conn.close() + + guild = self.bot.get_guild(guild_id) + if not guild: return 0, 0 + + current_present = 0 + for mid in invited_ids: + if guild.get_member(mid): + current_present += 1 + + return len(invited_ids), current_present + + @commands.slash_command(name="setinvitechannel", description="Définit le salon des logs d'invitations") + @commands.has_permissions(administrator=True) + async def set_invite_channel(self, interaction: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO invite_configs (guild_id, log_channel_id) + VALUES (?, ?) + ON CONFLICT(guild_id) DO UPDATE SET log_channel_id=excluded.log_channel_id + ''', (interaction.guild_id, channel.id)) + conn.commit() + conn.close() + + components = [ + disnake.ui.Container( + disnake.ui.TextDisplay(f"✅ Salon des logs d'invitations défini sur {channel.mention}") + ) + ] + await interaction.response.send_message(components=components, ephemeral=True) + + @commands.command(name="invitations_debug") + async def invites_prefix(self, ctx, user: disnake.Member = None): + user = user or ctx.author + total, current = self.get_invite_stats(ctx.guild.id, user.id) + await ctx.send(f"📊 **{user.display_name}** : {total} total, {current} présents.") + +def setup(bot): + bot.add_cog(Invites(bot)) diff --git a/commandes/moderation.py b/commandes/moderation.py index e250046..3569c52 100644 --- a/commandes/moderation.py +++ b/commandes/moderation.py @@ -6,118 +6,63 @@ from datetime import datetime, timedelta import re class ModReasonModal(disnake.ui.Modal): - def __init__(self, parent_view, action_type): - super().__init__(title=f"Raison pour {action_type}", components=[]) - self.parent_view = parent_view + def __init__(self, cog, action_type, member): + self.cog = cog self.action_type = action_type + self.member = member - self.reason = disnake.ui.TextInput( + self.reason_input = disnake.ui.TextInput( label="Raison", placeholder="Entrez la raison ici...", required=True, - max_length=500 + max_length=500, + custom_id="reason" ) - self.append_component(self.reason) + + components = [self.reason_input] if action_type == "TIMEOUT": - self.duration = disnake.ui.TextInput( + self.duration_input = disnake.ui.TextInput( label="Durée (ex: 1h, 30m, 1d)", placeholder="1h", required=True, - max_length=10 + max_length=10, + custom_id="duration" ) - self.append_component(self.duration) + components.append(self.duration_input) - async def callback(self, interaction: disnake.Interaction): - reason = self.reason.value - member = self.parent_view.member + super().__init__(title=f"Raison pour {action_type}", components=components) + + async def callback(self, interaction: disnake.ModalInteraction): + reason = interaction.text_values["reason"] + member = self.member - cog = interaction.bot.get_cog("Moderation") - if not cog: - return await interaction.response.send_message("❌ Erreur interne.", ephemeral=True) - if self.action_type == "WARN": - await cog.warn(interaction, member, reason) + await self.cog.warn(interaction, member, reason) elif self.action_type == "TIMEOUT": - await cog.timeout(interaction, member, self.duration.value, reason) + duration = interaction.text_values["duration"] + await self.cog.timeout(interaction, member, duration, reason) elif self.action_type == "KICK": - await cog.kick(interaction, member, reason) + await self.cog.kick(interaction, member, reason) elif self.action_type == "BAN": - await cog.ban(interaction, member, reason) + await self.cog.ban(interaction, member, reason) - await self.parent_view.update_panel(interaction) - -class ModPanelView(disnake.ui.View): - def __init__(self, member, moderator, db_path): - super().__init__(timeout=120) - self.member = member - self.moderator = moderator - self.db_path = db_path - - async def update_panel(self, interaction: disnake.Interaction): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute('SELECT COUNT(*) FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "WARN" AND status = "ACTIVE"', (interaction.guild_id, self.member.id)) - warn_count = cursor.fetchone()[0] - conn.close() - - embed = disnake.Embed( - title=f"🛡️ Panel de Modération - {self.member.name}", - description=f"Gestion de l'utilisateur {self.member.mention}", - color=disnake.Color.blue(), - timestamp=datetime.now() - ) - embed.set_thumbnail(url=self.member.display_avatar.url) - embed.add_field(name="👤 Utilisateur", value=f"ID: `{self.member.id}`\nTag: `{self.member}`", inline=True) - embed.add_field(name="⚠️ Avertissements", value=f"Actifs: **{warn_count}**", inline=True) - embed.add_field(name="📅 Dates", value=f"Rejoint: \nCréé: ", inline=False) - - if not interaction.response.is_done(): - await interaction.response.edit_message(embed=embed, view=self) - else: - await interaction.edit_original_response(embed=embed, view=self) - - @disnake.ui.button(label="Avertir", style=disnake.ButtonStyle.secondary, emoji="⚠️") - async def warn_btn(self, button: disnake.ui.Button, interaction: disnake.Interaction): - await interaction.response.send_modal(ModReasonModal(self, "WARN")) - - @disnake.ui.button(label="Timeout", style=disnake.ButtonStyle.secondary, emoji="⏳") - async def timeout_btn(self, button: disnake.ui.Button, interaction: disnake.Interaction): - await interaction.response.send_modal(ModReasonModal(self, "TIMEOUT")) - - @disnake.ui.button(label="Expulser", style=disnake.ButtonStyle.secondary, emoji="👢") - async def kick_btn(self, button: disnake.ui.Button, interaction: disnake.Interaction): - await interaction.response.send_modal(ModReasonModal(self, "KICK")) - - @disnake.ui.button(label="Bannir", style=disnake.ButtonStyle.danger, emoji="🔨") - async def ban_btn(self, button: disnake.ui.Button, interaction: disnake.Interaction): - await interaction.response.send_modal(ModReasonModal(self, "BAN")) - - @disnake.ui.button(label="Historique", style=disnake.ButtonStyle.primary, emoji="📜") - async def history_btn(self, button: disnake.ui.Button, interaction: disnake.Interaction): - cog = interaction.bot.get_cog("Moderation") - await cog.history(interaction, self.member) - - @disnake.ui.button(label="Clear Warns", style=disnake.ButtonStyle.danger, emoji="🧹") - async def clear_btn(self, button: disnake.ui.Button, interaction: disnake.Interaction): - cog = interaction.bot.get_cog("Moderation") - await cog.clearwarns(interaction, self.member, reason="Nettoyage via Panel") - await self.update_panel(interaction) + await self.cog.send_modpanel_v2(interaction, member, edit=True) class ModConfigModal(disnake.ui.Modal): def __init__(self, field_name, current_value): - super().__init__(components=[], title=f"Modifier {field_name}") self.field_name = field_name - self.input = disnake.ui.TextInput( + self.input_field = disnake.ui.TextInput( label=field_name, value=str(current_value), placeholder="Entrez une valeur numérique...", - required=True + required=True, + custom_id="value" ) - self.append_component(self.input) + super().__init__(title=f"Modifier {field_name}", components=[self.input_field]) - async def callback(self, interaction: disnake.Interaction): - value = self.input.value + async def callback(self, interaction: disnake.ModalInteraction): + value = interaction.text_values["value"] cog = interaction.bot.get_cog("Moderation") mapping = { @@ -137,62 +82,8 @@ class ModConfigModal(disnake.ui.Modal): conn.commit() conn.close() - await interaction.response.send_message(f"✅ {self.field_name} mis à jour sur **{value}**.", ephemeral=True) - -class ModConfigView(disnake.ui.View): - def __init__(self, guild_id, db_path): - super().__init__(timeout=60) - self.guild_id = guild_id - self.db_path = db_path - - @disnake.ui.button(label="Log Channel", style=disnake.ButtonStyle.primary, emoji="📁") - async def log_btn(self, button: disnake.ui.Button, interaction: disnake.Interaction): - view = disnake.ui.View() - select = disnake.ui.ChannelSelect( - placeholder="Sélectionnez le salon de logs...", - channel_types=[disnake.ChannelType.text] - ) - - async def select_callback(interaction: disnake.Interaction): - channel = select.values[0] - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute("UPDATE mod_config SET log_channel_id = ? WHERE guild_id = ?", (channel.id, interaction.guild_id)) - conn.commit(); conn.close() - await interaction.response.send_message(f"✅ Salon de logs défini sur {channel.mention}", ephemeral=True) - - select.callback = select_callback - view.add_item(select) - await interaction.response.send_message("Choisissez le salon de logs :", view=view, ephemeral=True) - - @disnake.ui.button(label="Paliers Warns", style=disnake.ButtonStyle.secondary, emoji="⚖️") - async def limits_btn(self, button: disnake.ui.Button, interaction: disnake.Interaction): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute("SELECT warn_limit_timeout, warn_limit_kick, warn_limit_ban FROM mod_config WHERE guild_id = ?", (self.guild_id,)) - res = cursor.fetchone(); conn.close() - - view = disnake.ui.View() - - btn_t = disnake.ui.Button(label=f"Timeout ({res[0]})", style=disnake.ButtonStyle.secondary) - btn_t.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Timeout", res[0])) - - btn_k = disnake.ui.Button(label=f"Kick ({res[1]})", style=disnake.ButtonStyle.secondary) - btn_k.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Kick", res[1])) - - btn_b = disnake.ui.Button(label=f"Ban ({res[2]})", style=disnake.ButtonStyle.secondary) - btn_b.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Ban", res[2])) - - view.add_item(btn_t); view.add_item(btn_k); view.add_item(btn_b) - await interaction.response.send_message("Quelle limite modifier ?", view=view, ephemeral=True) - - @disnake.ui.button(label="Durée Timeout", style=disnake.ButtonStyle.secondary, emoji="⏱️") - async def duration_btn(self, button: disnake.ui.Button, interaction: disnake.Interaction): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute("SELECT timeout_duration FROM mod_config WHERE guild_id = ?", (self.guild_id,)) - res = cursor.fetchone(); conn.close() - await interaction.response.send_modal(ModConfigModal("Durée Timeout (s)", res[0])) + await interaction.response.send_message(f"✅ {self.field_name} mis à jour.", ephemeral=True) + await cog.send_modconfig_v2(interaction, edit=True) class Moderation(commands.Cog): def __init__(self, bot): @@ -204,7 +95,17 @@ class Moderation(commands.Cog): conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute('''CREATE TABLE IF NOT EXISTS sanctions (id INTEGER PRIMARY KEY AUTOINCREMENT, guild_id INTEGER, user_id INTEGER, moderator_id INTEGER, type TEXT, reason TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, duration INTEGER, status TEXT DEFAULT 'ACTIVE')''') - cursor.execute('''CREATE TABLE IF NOT EXISTS mod_config (guild_id INTEGER PRIMARY KEY, log_channel_id INTEGER, warn_limit_timeout INTEGER DEFAULT 3, warn_limit_kick INTEGER DEFAULT 5, warn_limit_ban INTEGER DEFAULT 10, timeout_duration INTEGER DEFAULT 3600)''') + cursor.execute('''CREATE TABLE IF NOT EXISTS mod_config ( + guild_id INTEGER PRIMARY KEY, + log_channel_id INTEGER, + board_channel_id INTEGER, + warn_limit_timeout INTEGER DEFAULT 3, + warn_limit_kick INTEGER DEFAULT 5, + warn_limit_ban INTEGER DEFAULT 10, + timeout_duration INTEGER DEFAULT 3600 + )''') + try: cursor.execute("ALTER TABLE mod_config ADD COLUMN board_channel_id INTEGER") + except: pass conn.commit(); conn.close() def parse_duration(self, duration_str: str) -> int: @@ -215,110 +116,226 @@ class Moderation(commands.Cog): for value, unit in matches: total_seconds += int(value) * units[unit] return total_seconds - async def send_mod_log(self, guild, embed): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute('SELECT log_channel_id FROM mod_config WHERE guild_id = ?', (guild.id,)) - result = cursor.fetchone(); conn.close() - if result and result[0]: - log_channel = guild.get_channel(result[0]) - if log_channel: await log_channel.send(embed=embed) + def _type_to_emoji(self, s_type: str) -> str: + return {"WARN": "⚠️", "TIMEOUT": "⏳", "KICK": "👢", "BAN": "🔨"}.get(s_type, "🛡️") - @commands.slash_command(name="modpanel", description="Ouvre le panel de modération pour un membre") + def build_sanction_board_components(self, *, s_type: str, user, moderator, reason, timestamp, s_id, duration=None) -> list: + ts_int = int(datetime.fromisoformat(str(timestamp)).timestamp()) if not isinstance(timestamp, datetime) else int(timestamp.timestamp()) + emoji = self._type_to_emoji(s_type) + + children = [ + disnake.ui.Section(f"{emoji} **SANCTION : {s_type}**", accessory=disnake.ui.Thumbnail(user.display_avatar.url)), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(f"- **Membre sanctionné** : {user.mention}"), + disnake.ui.TextDisplay(f"- **Modérateur** : {moderator.mention}"), + disnake.ui.TextDisplay(f"- **Raison** : “ *{reason if reason else 'Aucune raison spécifiée'}* ”"), + disnake.ui.TextDisplay(f"- **Date** : ") + ] + + if s_type == "TIMEOUT" and duration: + children.append(disnake.ui.TextDisplay(f"- **Durée** : `{duration}s`")) + + children.extend([ + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(f"🆔 **ID** : `{s_id}`") + ]) + + return [disnake.ui.Container(*children)] + + async def send_mod_log(self, guild, components): + conn = sqlite3.connect(self.db_path); cursor = conn.cursor() + cursor.execute('SELECT log_channel_id FROM mod_config WHERE guild_id = ?', (guild.id,)) + res = cursor.fetchone(); conn.close() + if res and res[0]: + chan = guild.get_channel(res[0]) + if chan: await chan.send(components=components) + + async def send_sanction_board(self, guild, components): + conn = sqlite3.connect(self.db_path); cursor = conn.cursor() + cursor.execute('SELECT board_channel_id FROM mod_config WHERE guild_id = ?', (guild.id,)) + res = cursor.fetchone(); conn.close() + if res and res[0]: + chan = guild.get_channel(res[0]) + if chan: await chan.send(components=components) + + async def send_modpanel_v2(self, interaction: disnake.Interaction, member: disnake.Member, edit=False): + conn = sqlite3.connect(self.db_path); cursor = conn.cursor() + cursor.execute('SELECT COUNT(*) FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "WARN" AND status = "ACTIVE"', (interaction.guild_id, member.id)) + warn_count = cursor.fetchone()[0] + cursor.execute('SELECT type, reason, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND status = "ACTIVE" ORDER BY timestamp DESC LIMIT 3', (interaction.guild_id, member.id)) + rows = cursor.fetchall(); conn.close() + + children = [ + disnake.ui.Section(f"🛡️ Panel : {member.name}", accessory=disnake.ui.Thumbnail(member.display_avatar.url)), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(f"👤 **Membre :** {member.mention} (`{member.id}`)"), + disnake.ui.TextDisplay(f"⚠️ **Avertissements actifs :** {warn_count}"), + disnake.ui.Separator(divider=True), + disnake.ui.Section("⚠️ Infliger un avertissement", accessory=disnake.ui.Button(label="Warn", style=disnake.ButtonStyle.secondary, custom_id=f"modpan_warn:{member.id}")), + disnake.ui.Section("⏳ Mettre en sourdine", accessory=disnake.ui.Button(label="Timeout", style=disnake.ButtonStyle.secondary, custom_id=f"modpan_timeout:{member.id}")), + disnake.ui.Section("👢 Expulser du serveur", accessory=disnake.ui.Button(label="Kick", style=disnake.ButtonStyle.secondary, custom_id=f"modpan_kick:{member.id}")), + disnake.ui.Section("🔨 Bannir définitivement", accessory=disnake.ui.Button(label="Ban", style=disnake.ButtonStyle.danger, custom_id=f"modpan_ban:{member.id}")), + disnake.ui.Separator(divider=True), + disnake.ui.Section("🧹 Réinitialiser les warns", accessory=disnake.ui.Button(label="Clear", style=disnake.ButtonStyle.danger, custom_id=f"modpan_clear:{member.id}")) + ] + + if rows: + children.append(disnake.ui.Separator(divider=True)) + children.append(disnake.ui.TextDisplay("📌 **Dernières sanctions :**")) + for t, r, ts in rows: + ts_int = int(datetime.fromisoformat(str(ts)).timestamp()) if not isinstance(ts, datetime) else int(ts.timestamp()) + children.append(disnake.ui.TextDisplay(f"• {self._type_to_emoji(t)} **{t}** — {r} ()")) + + components = [disnake.ui.Container(*children)] + + if edit: + if not interaction.response.is_done(): await interaction.response.edit_message(content=None, components=components) + else: await interaction.edit_original_response(content=None, components=components) + else: + await interaction.response.send_message(components=components, ephemeral=True) + + @commands.slash_command(name="modpanel", description="Ouvre le panel de modération") @commands.has_permissions(moderate_members=True) async def modpanel(self, interaction: disnake.ApplicationCommandInteraction, member: disnake.Member): if member.top_role >= interaction.user.top_role and interaction.user.id != interaction.guild.owner_id: - return await interaction.response.send_message("❌ Impossible de gérer ce membre.", ephemeral=True) - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute('SELECT COUNT(*) FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "WARN" AND status = "ACTIVE"', (interaction.guild_id, member.id)) - warn_count = cursor.fetchone()[0]; conn.close() - embed = disnake.Embed(title=f"🛡️ Panel de Modération - {member.name}", description=f"Gestion de {member.mention}", color=disnake.Color.blue(), timestamp=datetime.now()) - embed.set_thumbnail(url=member.display_avatar.url) - embed.add_field(name="👤 Utilisateur", value=f"ID: `{member.id}`", inline=True) - embed.add_field(name="⚠️ Avertissements", value=f"Actifs: **{warn_count}**", inline=True) - view = ModPanelView(member, interaction.user, self.db_path) - await interaction.response.send_message(embed=embed, view=view, ephemeral=True) + return await interaction.response.send_message("❌ Permissions insuffisantes.", ephemeral=True) + await self.send_modpanel_v2(interaction, member) + + async def send_modconfig_v2(self, interaction: disnake.Interaction, edit=False): + conn = sqlite3.connect(self.db_path); cursor = conn.cursor() + cursor.execute('SELECT log_channel_id, board_channel_id, warn_limit_timeout, warn_limit_kick, warn_limit_ban, timeout_duration FROM mod_config WHERE guild_id = ?', (interaction.guild_id,)) + res = cursor.fetchone(); conn.close() + if not res: return + + children = [ + disnake.ui.Section("⚙️ Configuration Modération", accessory=disnake.ui.Thumbnail(interaction.guild.icon.url if interaction.guild.icon else None)), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(f"📁 **Logs internes :** <#{res[0]}>" if res[0] else "📁 **Logs :** Non défini"), + disnake.ui.TextDisplay(f"📢 **Affichage public :** <#{res[1]}>" if res[1] else "📢 **Affichage :** Non défini"), + disnake.ui.TextDisplay(f"⚖️ **Paliers :** T:{res[2]} | K:{res[3]} | B:{res[4]}"), + disnake.ui.TextDisplay(f"⏱️ **Timeout auto :** {res[5]}s"), + disnake.ui.Separator(divider=True), + disnake.ui.Section("📁 Salon des logs", accessory=disnake.ui.Button(label="Logs", style=disnake.ButtonStyle.primary, custom_id="modcfg_logs")), + disnake.ui.Section("📢 Salon d'affichage", accessory=disnake.ui.Button(label="Public", style=disnake.ButtonStyle.primary, custom_id="modcfg_board")), + disnake.ui.Section("⚖️ Modifier paliers", accessory=disnake.ui.Button(label="Paliers", style=disnake.ButtonStyle.secondary, custom_id="modcfg_limits")), + disnake.ui.Section("⏱️ Durée timeout", accessory=disnake.ui.Button(label="Durée", style=disnake.ButtonStyle.secondary, custom_id="modcfg_duration")) + ] + + components = [disnake.ui.Container(*children)] + + if edit: + if not interaction.response.is_done(): await interaction.response.edit_message(content=None, components=components) + else: await interaction.edit_original_response(content=None, components=components) + else: + await interaction.response.send_message(components=components, ephemeral=True) @commands.slash_command(name="modconfig", description="Configuration de la modération") @commands.has_permissions(administrator=True) async def modconfig(self, interaction: disnake.ApplicationCommandInteraction): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() + conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('INSERT OR IGNORE INTO mod_config (guild_id) VALUES (?)', (interaction.guild_id,)) - cursor.execute('SELECT log_channel_id, warn_limit_timeout, warn_limit_kick, warn_limit_ban, timeout_duration FROM mod_config WHERE guild_id = ?', (interaction.guild_id,)) - res = cursor.fetchone(); conn.commit(); conn.close() - embed = disnake.Embed(title="⚙️ Configuration Modération", description="Modifiez les paramètres ci-dessous.", color=disnake.Color.blue()) - embed.add_field(name="📁 Logs", value=f"<#{res[0]}>" if res[0] else "Non défini", inline=False) - embed.add_field(name="⚖️ Paliers", value=f"Timeout: **{res[1]}**, Kick: **{res[2]}**, Ban: **{res[3]}**", inline=False) - embed.add_field(name="⏱️ Durée Timeout Auto", value=f"**{res[4]}s**", inline=False) - view = ModConfigView(interaction.guild_id, self.db_path) - await interaction.response.send_message(embed=embed, view=view, ephemeral=True) + conn.commit(); conn.close() + await self.send_modconfig_v2(interaction) - async def warn(self, interaction: disnake.Interaction, member: disnake.Member, reason: str): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() + @commands.Cog.listener("on_message_interaction") + async def on_mod_interaction(self, interaction: disnake.MessageInteraction): + cid = interaction.data.custom_id + if not cid: return + + if cid.startswith("modpan_"): + action, mid = cid.replace("modpan_", "").split(":") + member = interaction.guild.get_member(int(mid)) + if not member: return await interaction.response.send_message("❌ Membre introuvable.", ephemeral=True) + if action == "clear": + await self.clearwarns(interaction, member, "Nettoyage Panel") + await self.send_modpanel_v2(interaction, member, edit=True) + else: + await interaction.response.send_modal(ModReasonModal(self, action.upper(), member)) + elif cid.startswith("modcfg_"): + act = cid.replace("modcfg_", "") + if act in ["logs", "board"]: + col = "log_channel_id" if act == "logs" else "board_channel_id" + view = disnake.ui.View() + sel = disnake.ui.ChannelSelect(placeholder="Choisir salon...", channel_types=[disnake.ChannelType.text]) + async def sel_cb(i): + c = sel.values[0] + conn = sqlite3.connect(self.db_path); cursor = conn.cursor() + cursor.execute(f"UPDATE mod_config SET {col} = ? WHERE guild_id = ?", (c.id, i.guild_id)) + conn.commit(); conn.close() + await i.response.send_message(f"✅ Salon mis à jour.", ephemeral=True) + await self.send_modconfig_v2(interaction, edit=True) + sel.callback = sel_cb; view.add_item(sel) + await interaction.response.send_message(f"Sélectionnez le salon pour {act} :", view=view, ephemeral=True) + elif act == "limits": + conn = sqlite3.connect(self.db_path); cursor = conn.cursor() + cursor.execute("SELECT warn_limit_timeout, warn_limit_kick, warn_limit_ban FROM mod_config WHERE guild_id = ?", (interaction.guild_id,)) + res = cursor.fetchone(); conn.close() + view = disnake.ui.View() + b1 = disnake.ui.Button(label=f"Timeout ({res[0]})"); b1.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Timeout", res[0])) + b2 = disnake.ui.Button(label=f"Kick ({res[1]})"); b2.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Kick", res[1])) + b3 = disnake.ui.Button(label=f"Ban ({res[2]})"); b3.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Ban", res[2])) + view.add_item(b1); view.add_item(b2); view.add_item(b3) + await interaction.response.send_message("Modifier quel palier ?", view=view, ephemeral=True) + elif act == "duration": + conn = sqlite3.connect(self.db_path); cursor = conn.cursor() + cursor.execute("SELECT timeout_duration FROM mod_config WHERE guild_id = ?", (interaction.guild_id,)) + res = cursor.fetchone(); conn.close() + await interaction.response.send_modal(ModConfigModal("Durée Timeout (s)", res[0])) + + async def warn(self, interaction, member, reason): + conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?)', (interaction.guild_id, member.id, interaction.user.id, 'WARN', reason)) cursor.execute('SELECT COUNT(*) FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "WARN" AND status = "ACTIVE"', (interaction.guild_id, member.id)) - warn_count = cursor.fetchone()[0] + count = cursor.fetchone()[0] cursor.execute('SELECT warn_limit_timeout, warn_limit_kick, warn_limit_ban, timeout_duration FROM mod_config WHERE guild_id = ?', (interaction.guild_id,)) - config = cursor.fetchone(); conn.commit(); conn.close() - if not interaction.response.is_done(): await interaction.response.send_message(f"✅ {member.mention} averti ({warn_count}).", ephemeral=True) - else: await interaction.followup.send(f"✅ {member.mention} averti ({warn_count}).", ephemeral=True) - await self.send_mod_log(interaction.guild, disnake.Embed(title="⚠️ Avertissement", description=f"Membre: {member.mention}\nRaison: {reason}\nTotal: {warn_count}", color=disnake.Color.orange())) + cfg = cursor.fetchone() + cursor.execute('SELECT id, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "WARN" ORDER BY id DESC LIMIT 1', (interaction.guild_id, member.id)) + s_id, ts = cursor.fetchone(); conn.commit(); conn.close() + + if not interaction.response.is_done(): await interaction.response.send_message(f"✅ {member.mention} averti ({count}).", ephemeral=True) + await self.send_mod_log(interaction.guild, [disnake.ui.Container(disnake.ui.Section(f"⚠️ Warn : {member}", accessory=disnake.ui.Thumbnail(member.display_avatar.url)), disnake.ui.Separator(divider=True), disnake.ui.TextDisplay(f"Raison: {reason}"), disnake.ui.TextDisplay(f"Total: {count}"))]) + await self.send_sanction_board(interaction.guild, self.build_sanction_board_components(s_type="WARN", user=member, moderator=interaction.user, reason=reason, timestamp=ts, s_id=s_id)) try: await member.send(f"⚠️ Avertissement sur **{interaction.guild.name}**\nRaison: {reason}") except: pass - if config: - l_t, l_k, l_b, dur = config - if warn_count >= l_b: await member.ban(reason=f"Auto: {warn_count} warns") - elif warn_count >= l_k: await member.kick(reason=f"Auto: {warn_count} warns") - elif warn_count >= l_t: await member.timeout(duration=timedelta(seconds=dur), reason=f"Auto: {warn_count} warns") + if cfg: + lt, lk, lb, dur = cfg + if count >= lb: await member.ban(reason=f"Auto: {count} warns") + elif count >= lk: await member.kick(reason=f"Auto: {count} warns") + elif count >= lt: await member.timeout(duration=timedelta(seconds=dur), reason=f"Auto: {count} warns") - async def timeout(self, interaction: disnake.Interaction, member: disnake.Member, duration_str: str, reason: str): + async def timeout(self, interaction, member, duration_str, reason): secs = self.parse_duration(duration_str) if secs <= 0: return await interaction.response.send_message("❌ Durée invalide.", ephemeral=True) await member.timeout(duration=timedelta(seconds=secs), reason=reason) conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason, duration) VALUES (?, ?, ?, ?, ?, ?)', (interaction.guild_id, member.id, interaction.user.id, 'TIMEOUT', reason, secs)) - conn.commit(); conn.close() - if not interaction.response.is_done(): await interaction.response.send_message(f"✅ {member.mention} mis en sourdine.", ephemeral=True) - else: await interaction.followup.send(f"✅ {member.mention} mis en sourdine.", ephemeral=True) + cursor.execute('SELECT id, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "TIMEOUT" ORDER BY id DESC LIMIT 1', (interaction.guild_id, member.id)) + s_id, ts = cursor.fetchone(); conn.commit(); conn.close() + if not interaction.response.is_done(): await interaction.response.send_message(f"✅ {member.mention} timeout.", ephemeral=True) + await self.send_sanction_board(interaction.guild, self.build_sanction_board_components(s_type="TIMEOUT", user=member, moderator=interaction.user, reason=reason, timestamp=ts, s_id=s_id, duration=secs)) - async def kick(self, interaction: disnake.Interaction, member: disnake.Member, reason: str): + async def kick(self, interaction, member, reason): await member.kick(reason=reason) conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?)', (interaction.guild_id, member.id, interaction.user.id, 'KICK', reason)) - conn.commit(); conn.close() + cursor.execute('SELECT id, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "KICK" ORDER BY id DESC LIMIT 1', (interaction.guild_id, member.id)) + s_id, ts = cursor.fetchone(); conn.commit(); conn.close() if not interaction.response.is_done(): await interaction.response.send_message(f"✅ {member.name} expulsé.", ephemeral=True) - else: await interaction.followup.send(f"✅ {member.name} expulsé.", ephemeral=True) + await self.send_sanction_board(interaction.guild, self.build_sanction_board_components(s_type="KICK", user=member, moderator=interaction.user, reason=reason, timestamp=ts, s_id=s_id)) - async def ban(self, interaction: disnake.Interaction, user: disnake.User, reason: str): + async def ban(self, interaction, user, reason): await interaction.guild.ban(user, reason=reason) conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?)', (interaction.guild_id, user.id, interaction.user.id, 'BAN', reason)) - conn.commit(); conn.close() + cursor.execute('SELECT id, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "BAN" ORDER BY id DESC LIMIT 1', (interaction.guild_id, user.id)) + s_id, ts = cursor.fetchone(); conn.commit(); conn.close() if not interaction.response.is_done(): await interaction.response.send_message(f"✅ {user.name} banni.", ephemeral=True) - else: await interaction.followup.send(f"✅ {user.name} banni.", ephemeral=True) + await self.send_sanction_board(interaction.guild, self.build_sanction_board_components(s_type="BAN", user=user, moderator=interaction.user, reason=reason, timestamp=ts, s_id=s_id)) - async def history(self, interaction: disnake.Interaction, user: disnake.User): - conn = sqlite3.connect(self.db_path); cursor = conn.cursor() - cursor.execute('SELECT id, type, reason, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? ORDER BY timestamp DESC', (interaction.guild_id, user.id)) - results = cursor.fetchall(); conn.close() - if not results: - if not interaction.response.is_done(): return await interaction.response.send_message("ℹ️ Aucun historique.", ephemeral=True) - else: return await interaction.followup.send("ℹ️ Aucun historique.", ephemeral=True) - embed = disnake.Embed(title=f"📜 Historique de {user.name}", color=disnake.Color.blue()) - for s_id, s_type, reason, ts in results[:10]: - embed.add_field(name=f"#{s_id} - {s_type}", value=f"**Date:** {ts}\n**Raison:** {reason}", inline=False) - if not interaction.response.is_done(): await interaction.response.send_message(embed=embed, ephemeral=True) - else: await interaction.followup.send(embed=embed, ephemeral=True) - - async def clearwarns(self, interaction: disnake.Interaction, member: disnake.Member, reason: str): + async def clearwarns(self, interaction, member, reason): conn = sqlite3.connect(self.db_path); cursor = conn.cursor() cursor.execute('UPDATE sanctions SET status = "REVOKED" WHERE guild_id = ? AND user_id = ? AND type = "WARN"', (interaction.guild_id, member.id)) conn.commit(); conn.close() - if not interaction.response.is_done(): await interaction.response.send_message(f"✅ Warns de {member.name} effacés.", ephemeral=True) - else: await interaction.followup.send(f"✅ Warns de {member.name} effacés.", ephemeral=True) + if not interaction.response.is_done(): await interaction.response.send_message(f"✅ Warns effacés.", ephemeral=True) -def setup(bot): - bot.add_cog(Moderation(bot)) +def setup(bot): bot.add_cog(Moderation(bot)) diff --git a/commandes/ticket/feedback_view.py b/commandes/ticket/feedback_view.py index cedc0ac..7361091 100644 --- a/commandes/ticket/feedback_view.py +++ b/commandes/ticket/feedback_view.py @@ -137,7 +137,7 @@ class FeedbackModal(disnake.ui.Modal): required=True, max_length=1000 ) - self.append_component(self.comment) + self.add_item(self.comment) async def callback(self, interaction: disnake.Interaction): self.view.comment = self.comment.value diff --git a/commandes/welcome.py b/commandes/welcome.py index f3a4e70..ab515e6 100644 --- a/commandes/welcome.py +++ b/commandes/welcome.py @@ -5,6 +5,7 @@ import aiohttp import io import os import logging +import asyncio from PIL import Image, ImageDraw, ImageFont kuby_logger = logging.getLogger("KubyBot") @@ -132,7 +133,7 @@ class Welcome(commands.Cog): kuby_logger.debug(f"🔍 [Welcome] Config récupérée: DM={bool(welcome_dm_text)}, Channel={welcome_channel}") # 1. Gestion du DM - if welcome_dm_text: + if welcome_dm_text and not member.bot: kuby_logger.info(f"✉️ [Welcome] Tentative d'envoi de DM à {member.name}") try: formatted_message = welcome_dm_text.format( @@ -149,6 +150,8 @@ class Welcome(commands.Cog): except disnake.HTTPException as e: if e.code == 40003: # Rate limited (Opening DMs too fast) kuby_logger.warning(f"⏳ [Welcome] Limite de débit Discord atteinte pour les DMs : {member.name} n'a pas reçu son message.") + elif e.code == 50007: # Cannot send messages to this user + kuby_logger.warning(f"🚫 [Welcome] Impossible d'envoyer le DM à {member.name} (DMs fermés ou utilisateur non joignable)") else: kuby_logger.error(f"❌ [Welcome] Erreur HTTP lors de l'envoi du DM à {member.name}: {e}") except Exception as e: @@ -169,18 +172,33 @@ class Welcome(commands.Cog): banner_background=banner_background ) if img: + # Attendre un peu que le Cog d'invitations traite le join + await asyncio.sleep(1) + invites_cog = self.bot.get_cog("Invites") + inviter_text = "" + if invites_cog: + inviter_id = invites_cog.get_inviter_info(member.guild.id, member.id) + if inviter_id: + inviter = member.guild.get_member(inviter_id) or await self.bot.fetch_user(inviter_id) + total, current = invites_cog.get_invite_stats(member.guild.id, inviter_id) + inviter_text = f"\n\n**Invité par :** {inviter.mention} ({current} invitations)" + # Utiliser le message personnalisé ou un message par défaut - channel_description = welcome_channel_message.format( + channel_description = (welcome_channel_message.format( username=member.name, servername=member.guild.name, member_count=member.guild.member_count - ) if welcome_channel_message else "Bienvenue dans le serveur !\nSi vous cherchez des RolePlay de qualité,\nVous êtes au bon endroit. " + ) if welcome_channel_message else "Bienvenue dans le serveur !\nSi vous cherchez des RolePlay de qualité,\nVous êtes au bon endroit. ") + inviter_text - embed = disnake.Embed(title=f"Merci d'accueillir {member} 🤝", - description=channel_description, - color=disnake.Color.dark_purple()) - embed.set_image(url="attachment://welcome.png") - await channel.send(content=member.mention, embed=embed, file=disnake.File(img, filename="welcome.png")) + components = [ + disnake.ui.Container( + disnake.ui.TextDisplay(f"Merci d'accueillir {member} 🤝"), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(channel_description), + disnake.ui.MediaGallery(disnake.MediaGalleryItem(url="attachment://welcome.png")) + ) + ] + await channel.send(content=member.mention, components=components, file=disnake.File(img, filename="welcome.png")) async def save_banner_attachment(self, attachment: disnake.Attachment, guild_id: int) -> str: """Sauvegarde l'attachment et retourne le chemin du fichier""" @@ -243,6 +261,36 @@ class Welcome(commands.Cog): bg_used = "background_template.png" if not banner_path else (banner_path.split('/')[-1]) await interaction.response.send_message(f"✅ Configuration de bienvenida enregistrée !\nFond utilisé: {bg_used}", ephemeral=True) + @commands.slash_command(name="invitations", description="Affiche vos statistiques d'invitations") + async def invitations_stats(self, interaction: disnake.ApplicationCommandInteraction, user: disnake.Member = None): + user = user or interaction.author + await self._send_invitation_stats(interaction, user) + + @commands.user_command(name="Stats Invitations") + async def invitations_user(self, interaction: disnake.UserCommandInteraction, user: disnake.Member): + await self._send_invitation_stats(interaction, user) + + async def _send_invitation_stats(self, interaction, user): + invites_cog = self.bot.get_cog("Invites") + if not invites_cog: + return await interaction.response.send_message("❌ Le module d'invitations est désactivé.", ephemeral=True) + + total, current = invites_cog.get_invite_stats(interaction.guild.id, user.id) + + components = [ + disnake.ui.Container( + disnake.ui.Section( + f"Statistiques d'invitations - {user.display_name}", + accessory=disnake.ui.Thumbnail(user.display_avatar.url) + ), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(f"➜ Total d'invitations : **{total}**"), + disnake.ui.TextDisplay(f"➜ Membres présents : **{current}**") + ) + ] + + await interaction.response.send_message(components=components) + def setup(bot): bot.add_cog(Welcome(bot)) diff --git a/data/json-export-history.json b/data/json-export-history.json index ce62dae..784e41f 100644 --- a/data/json-export-history.json +++ b/data/json-export-history.json @@ -1,177 +1,177 @@ [ { - "date": "2026-05-09T20:09:46.350028+00:00", + "date": "2026-05-15T21:49:37.114321+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T19:21:55.267612+00:00", + "date": "2026-05-15T21:44:14.212871+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T19:18:48.771118+00:00", + "date": "2026-05-15T21:42:45.960991+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T19:16:02.785385+00:00", + "date": "2026-05-15T21:40:28.754282+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T19:13:21.426871+00:00", + "date": "2026-05-15T21:39:55.113067+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T19:11:56.119101+00:00", + "date": "2026-05-15T21:37:43.517923+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T19:09:05.080600+00:00", + "date": "2026-05-15T21:36:48.733530+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T19:05:36.915747+00:00", + "date": "2026-05-15T21:33:22.503958+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T19:02:55.250923+00:00", + "date": "2026-05-15T21:31:58.777625+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T19:01:09.375743+00:00", + "date": "2026-05-15T21:28:24.375077+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T18:59:45.523047+00:00", + "date": "2026-05-15T21:27:00.933212+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T18:58:44.658779+00:00", + "date": "2026-05-15T21:24:34.075332+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T18:58:21.696179+00:00", + "date": "2026-05-15T21:23:28.940963+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T18:55:40.065119+00:00", + "date": "2026-05-15T21:22:50.796880+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T18:52:58.024946+00:00", + "date": "2026-05-15T21:21:47.335364+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T18:50:48.754069+00:00", + "date": "2026-05-15T21:20:45.322710+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T18:48:10.476580+00:00", + "date": "2026-05-15T21:19:02.759168+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T18:43:40.581660+00:00", + "date": "2026-05-15T21:16:56.391804+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T18:41:30.288131+00:00", + "date": "2026-05-15T20:50:21.727551+00:00", "trigger": "auto", "serversCount": 1, - "membersCount": 12, + "membersCount": 13, "guildName": "Omega Kube Bêta Test", "errors": [], "success": true }, { - "date": "2026-05-09T18:39:06.565794+00:00", + "date": "2026-05-13T16:47:42.624873+00:00", "trigger": "auto", "serversCount": 1, "membersCount": 12, diff --git a/scratch/check_container.py b/scratch/check_container.py new file mode 100644 index 0000000..36fd1c6 --- /dev/null +++ b/scratch/check_container.py @@ -0,0 +1,9 @@ +import disnake +import disnake.ui + +c = disnake.ui.Container() +print(f"Container attributes: {dir(c)}") +try: + print(f"Children: {c.children}") +except Exception as e: + print(f"No children attribute: {e}") diff --git a/scratch/check_file.py b/scratch/check_file.py new file mode 100644 index 0000000..eb19763 --- /dev/null +++ b/scratch/check_file.py @@ -0,0 +1,12 @@ +import os +import json + +member_id = 1141309757370675262 +path = f"data/member_logs/{member_id}.json" +print(f"Path: {path}") +print(f"Exists: {os.path.exists(path)}") +if os.path.exists(path): + print(f"Size: {os.path.getsize(path)}") + with open(path, 'r') as f: + content = f.read() + print(f"Content: {repr(content)}") diff --git a/src/advanced_logger.py b/src/advanced_logger.py index 035e379..72b25dd 100755 --- a/src/advanced_logger.py +++ b/src/advanced_logger.py @@ -106,21 +106,42 @@ class AdvancedLogger: def check_previous_roles(self, member_id: int) -> Optional[Dict]: """Check if member has previous role data when rejoining""" - # This one stays synchronous for now as it's called in on_member_join and needs immediate result - # but since it only reads ONE file, it's less problematic than writing to cumulative logs. - # Format update: it now needs to read NDJSON. member_log_file = f"{self.member_logs_dir}/{member_id}.json" if not os.path.exists(member_log_file): return None try: + logs = [] with open(member_log_file, 'r', encoding='utf-8') as f: - # Read line by line for NDJSON - logs = [json.loads(line) for line in f if line.strip()] + content = f.read().strip() + if not content: + return None + + # Handle legacy JSON list format + if content.startswith('['): + try: + logs = json.loads(content) + except json.JSONDecodeError: + # Fallback: try to strip brackets and commas + content_clean = content.strip('[]').strip() + # This is getting complex, let's just try line-by-line fallback + pass + + # If not a list or failed list parse, try NDJSON line-by-line + if not logs: + for line in content.splitlines(): + line = line.strip() + if not line: continue + # Strip trailing/leading commas in case of semi-corrupt legacy conversion + line = line.strip(',') + try: + logs.append(json.loads(line)) + except json.JSONDecodeError: + continue # Skip corrupt lines # Find the most recent leave event - leave_events = [log for log in logs if log.get("event_type") == "member_leave"] + leave_events = [log for log in logs if isinstance(log, dict) and log.get("event_type") == "member_leave"] if leave_events: return leave_events[-1].get("roles", {})