import disnake from disnake.ext import commands import json import os from typing import Optional # Chemins des fichiers SETTINGS_FILE = "data/security_settings.json" def escape_mentions(text: str) -> str: """Échappe les mentions @everyone et @here pour éviter les notifications involontaires""" if not text: return text # Utilise un zero-width space pour briser la mention Discord return text.replace("@everyone", "@\u200beveryone").replace("@here", "@\u200bhere") def load_settings(guild_id: int) -> dict: """Charge les paramètres de sécurité pour un serveur""" if not os.path.exists(SETTINGS_FILE): return {} try: with open(SETTINGS_FILE, "r", encoding='utf-8') as f: return json.load(f).get(str(guild_id), {}) except (json.JSONDecodeError, FileNotFoundError): return {} def save_settings(guild_id: int, settings: dict) -> None: """Sauvegarde les paramètres de sécurité""" data = {} if os.path.exists(SETTINGS_FILE): try: with open(SETTINGS_FILE, "r", encoding='utf-8') as f: data = json.load(f) except (json.JSONDecodeError, FileNotFoundError): pass data[str(guild_id)] = settings os.makedirs(os.path.dirname(SETTINGS_FILE), exist_ok=True) with open(SETTINGS_FILE, "w", encoding='utf-8') as f: json.dump(data, f, indent=4, ensure_ascii=False) # --- MODALS DE CONFIGURATION DU RÈGLEMENT (BACK-END) --- class RulesTitleModal(disnake.ui.Modal): """Modal pour configurer le titre du règlement""" title_input = disnake.ui.TextInput( label="Titre du règlement", placeholder="Ex: 📜 Règlement de [Nom du Serveur]", min_length=1, max_length=100, required=True ) def __init__(self, guild_id: int, settings: dict, on_update_callback=None): 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 callback(self, interaction: disnake.Interaction): self.settings["rules_title"] = self.title_input.value save_settings(self.guild_id, self.settings) if self.on_update_callback: await self.on_update_callback(interaction, self.settings) else: await interaction.response.edit_message(view=self._create_view(interaction)) await interaction.followup.send( f"✅ Titre du règlement mis à jour.", ephemeral=True ) def _create_view(self, interaction): """Crée la vue mise à jour""" from commandes.security import SecurityView # Essayer d'obtenir le bot depuis l'interaction bot = getattr(interaction, 'client', None) view = SecurityView(interaction, self.settings, category="rules", bot=bot) return view 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.TextInputStyle.paragraph, min_length=10, max_length=4000, required=True ) def __init__(self, guild_id: int, settings: dict, on_update_callback=None): 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 callback(self, interaction: disnake.Interaction): self.settings["rules_content"] = escape_mentions(self.content_input.value) save_settings(self.guild_id, self.settings) if self.on_update_callback: await self.on_update_callback(interaction, self.settings) else: await interaction.response.edit_message(view=self._create_view(interaction)) await interaction.followup.send( f"✅ Contenu du règlement mis à jour.", ephemeral=True ) def _create_view(self, interaction): """Crée la vue mise à jour""" from commandes.security import SecurityView # Essayer d'obtenir le bot depuis l'interaction bot = getattr(interaction, 'client', None) view = SecurityView(interaction, self.settings, category="rules", bot=bot) return view class RulesChannelModal(disnake.ui.Modal): """Modal pour sélectionner le salon du règlement""" channel_input = disnake.ui.TextInput( label="ID du salon", placeholder="Entrez l'ID du salon où envoyer le règlement", min_length=15, max_length=20, required=True ) def __init__(self, guild_id: int, settings: dict, on_update_callback=None): 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 callback(self, interaction: disnake.Interaction): if not self.channel_input.value.isdigit(): return await interaction.response.send_message( "❌ Erreur : ID invalide.", ephemeral=True ) channel_id = int(self.channel_input.value) channel = interaction.guild.get_channel(channel_id) if not channel: return await interaction.response.send_message( "❌ Salon non trouvé. Vérifiez l'ID.", ephemeral=True ) self.settings["rules_channel_id"] = channel_id save_settings(self.guild_id, self.settings) if self.on_update_callback: await self.on_update_callback(interaction, self.settings) else: await interaction.response.edit_message(view=self._create_view(interaction)) await interaction.followup.send( f"✅ Salon du règlement défini sur {channel.mention}.", ephemeral=True ) def _create_view(self, interaction): """Crée la vue mise à jour""" from commandes.security import SecurityView # Essayer d'obtenir le bot depuis l'interaction bot = getattr(interaction, 'client', None) view = SecurityView(interaction, self.settings, category="rules", bot=bot) return view class RulesRoleModal(disnake.ui.Modal): """Modal pour configurer le rôle attribué à l'acceptation""" role_input = disnake.ui.TextInput( label="ID du rôle", placeholder="Entrez l'ID du rôle à attribuer", min_length=15, max_length=20, required=True ) def __init__(self, guild_id: int, settings: dict, on_update_callback=None): 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 callback(self, interaction: disnake.Interaction): if not self.role_input.value.isdigit(): return await interaction.response.send_message( "❌ Erreur : ID invalide.", ephemeral=True ) role_id = int(self.role_input.value) role = interaction.guild.get_role(role_id) if not role: return await interaction.response.send_message( "❌ Rôle non trouvé. Vérifiez l'ID.", ephemeral=True ) self.settings["rules_accept_role_id"] = role_id save_settings(self.guild_id, self.settings) if self.on_update_callback: await self.on_update_callback(interaction, self.settings) else: await interaction.response.edit_message(view=self._create_view(interaction)) await interaction.followup.send( f"✅ Rôle d'acceptation défini sur **{role.name}**.", ephemeral=True ) def _create_view(self, interaction): """Crée la vue mise à jour""" from commandes.security import SecurityView # Essayer d'obtenir le bot depuis l'interaction bot = getattr(interaction, 'client', None) view = SecurityView(interaction, self.settings, category="rules", bot=bot) return view class RulesMessageTypeModal(disnake.ui.Modal): """Modal pour choisir le type de message du règlement""" message_type = disnake.ui.TextInput( label="Type de message", placeholder="Tapez 'embed' pour embed ou 'simple' pour message simple", min_length=5, max_length=6, required=True ) def __init__(self, guild_id: int, settings: dict, on_update_callback=None): 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 callback(self, interaction: disnake.Interaction): msg_type = self.message_type.value.lower().strip() if msg_type not in ["embed", "simple"]: return await interaction.response.send_message( "❌ Erreur : Tapez 'embed' ou 'simple'.", ephemeral=True ) self.settings["rules_message_type"] = msg_type save_settings(self.guild_id, self.settings) if self.on_update_callback: await self.on_update_callback(interaction, self.settings) else: await interaction.response.edit_message(view=self._create_view(interaction)) type_name = "Embed" if msg_type == "embed" else "Message simple" await interaction.followup.send( f"✅ Type de message défini sur **{type_name}**.", ephemeral=True ) def _create_view(self, interaction): """Crée la vue mise à jour""" from commandes.security import SecurityView # Essayer d'obtenir le bot depuis l'interaction bot = getattr(interaction, 'client', None) view = SecurityView(interaction, self.settings, category="rules", bot=bot) return view class ToggleRulesButton(disnake.ui.Button): """Bouton de bascule pour activer/désactiver le règlement""" def __init__(self, is_enabled: bool): style = disnake.ButtonStyle.green if is_enabled else disnake.ButtonStyle.red status = "✅" if is_enabled else "❌" super().__init__( label=f"Règlement [{status}]", style=style, custom_id="rules_enabled", emoji="📜" ) async def callback(self, interaction: disnake.Interaction): view = self.view settings = view.settings guild = interaction.guild # Basculer le paramètre settings["rules_enabled"] = not settings.get("rules_enabled", False) save_settings(guild.id, settings) # Reconstruire la vue from commandes.security import SecurityView # Essayer d'obtenir le bot depuis l'interaction bot = getattr(interaction, 'client', None) new_view = SecurityView(interaction, settings, category="rules", bot=bot) # Recréer l'embed embed = new_view.create_category_embed( "Configuration Règlement", "Créez et envoyez un règlement pour votre serveur.\n\n" "• **Titre** : Personnalisez le titre du règlement\n" "• **Contenu** : Rédigez vos règles\n" "• **Salon** : Choisissez où l'envoyer\n" "• **Rôle** : Rôle attribué à l'acceptation\n\n" "Les membres devront accepter le règlement pour accéder au serveur." ) await interaction.response.edit_message(embed=embed, view=new_view) class RulesAcceptButton(disnake.ui.View): """Bouton persistant pour accepter le règlement""" def __init__(self, guild_id: int, accept_role_id: int): super().__init__(timeout=None) # Pas de timeout - bouton persistant self.guild_id = guild_id self.accept_role_id = accept_role_id @disnake.ui.button( label="J'accepte le règlement", emoji="✅", style=disnake.ButtonStyle.green, custom_id="rules_accept_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 # Vérifier si le bouton est toujours activé (activé dans les paramètres) settings = load_settings(guild.id) if not settings.get("rules_enabled", False): return await interaction.response.send_message( "❌ Cette fonctionnalité a été désactivée.", ephemeral=True ) # Vérifier si l'utilisateur a déjà le rôle role = guild.get_role(self.accept_role_id) if not role: return await interaction.response.send_message( "❌ Le rôle d'acceptation n'est plus configuré sur ce serveur.", ephemeral=True ) member = interaction.user if role in member.roles: return await interaction.response.send_message( "✅ Vous avez déjà accepté le règlement !", ephemeral=True ) # Attribuer le rôle à l'utilisateur try: await member.add_roles(role) # Envoyer une confirmation éphémère await interaction.response.send_message( f"✅ {member.mention}, vous avez accepté le règlement !\n" f"Le rôle **{role.name}** vous a été attribué.", ephemeral=True ) except disnake.Forbidden: await interaction.response.send_message( "❌ Je n'ai pas les permissions pour attribuer ce rôle.", ephemeral=True ) except Exception as e: await interaction.response.send_message( f"❌ Une erreur est survenue: {e}", ephemeral=True ) class RulesCog(commands.Cog): """Cog pour la gestion du règlement""" def __init__(self, bot): self.bot = bot self.persistent_views_added = False async def cog_load(self): """Called when cog is loaded - setup persistent views""" await self.setup_persistent_views() async def setup_persistent_views(self): """Configure les views persistantes pour les règlements existants""" if self.persistent_views_added: return # Charger les paramètres de tous les serveurs if not os.path.exists(SETTINGS_FILE): return try: with open(SETTINGS_FILE, "r", encoding='utf-8') as f: all_settings = json.load(f) # Pour chaque serveur avec le règlement activé for guild_id_str, settings in all_settings.items(): if settings.get("rules_enabled", False) and settings.get("rules_accept_role_id"): # Ajouter la view persistente view = RulesAcceptButton(int(guild_id_str), settings["rules_accept_role_id"]) self.bot.add_view(view) print(f"[Rules] View registered for guild {guild_id_str}") self.persistent_views_added = True except (json.JSONDecodeError, FileNotFoundError): pass @commands.Cog.listener() async def on_ready(self): """Appelé quand le bot est prêt""" print("🎫 Module Règles chargé avec succès!") 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", "") accept_role_id = settings.get("rules_accept_role_id") message_type = settings.get("rules_message_type", "embed") # Par défaut embed if not accept_role_id: return None # Créer la view avec le bouton persistant view = RulesAcceptButton(channel.guild.id, accept_role_id) if message_type == "simple": # Message simple - échappe les mentions @everyone/@here pour éviter les notifications involontaires safe_content = content.replace("@everyone", "@\u200beveryone").replace("@here", "@\u200bhere") text_message = ( f"📜 **{title}**\n\n" f"{safe_content}\n\n" "Bienvenue ! Pour accéder à l'ensemble des canaux du serveur, " "vous devez accepter le règlement en cliquant sur le bouton ci-dessous.\n\n" "En acceptant, vous confirmez avoir lu et compris les règles du serveur." ) # Envoyer le message simple try: message = await channel.send( content=text_message, view=view ) return message except Exception as e: print(f"Erreur lors de l'envoi du règlement simple: {e}") return None else: # embed # Échappe les mentions @everyone/@here pour éviter les notifications involontaires safe_content = content.replace("@everyone", "@\u200beveryone").replace("@here", "@\u200bhere") # Utilise un Container avec TextDisplay pour un meilleur rendu du texte (markdown supporté) components = [ disnake.ui.Container( disnake.ui.TextDisplay(safe_content) ) ] # Créer l'embed embed = disnake.Embed( title=title, color=0x2b2d31 ) embed.set_footer(text="Cliquez sur le bouton ci-dessous pour accepter le règlement") # Envoyer l'embed avec le container et la view try: message = await channel.send( embed=embed, components=components, view=view ) return message except Exception as e: print(f"Erreur lors de l'envoi du règlement embed: {e}") return None def format_default_rules(self, guild_name: str) -> tuple: """Retourne le titre et contenu par défaut du règlement""" title = f"📜 Règlement de {guild_name}" content = ( "Bienvenue sur notre serveur ! Avant de commencer à interagir avec la communauté, " "veuille lire et accepter notre règlement.\n\n" "**📌 Règles à respecter :**\n\n" "1. **Respectez les autres membres** - Pas d'insultes, de harcèlement ou de discrimination.\n" "2. **Pas de contenu inapproprié** - Aucun contenu NSFW, violent ou choquant.\n" "3. **Pas de spam ou de flood** - Évitez les messages répétitifs ou les emojis excessifs.\n" "4. **Pas de publicité** - Ne partagez pas de liens vers d'autres serveurs sans autorisation.\n" "5. **Restez dans les salons appropriés** - Respectez la thématique de chaque canal.\n\n" "**⚠️ Sanctions :**\n" "Le non-respect de ces règles peut entraîner des avertissements, des mute temporaires, " "ou un banissement définitif selon la gravité des infractions.\n\n" "*En cliquant sur \"J'accepte\", vous confirmez avoir lu et accepter ce règlement.*" ) return title, content def setup(bot): """Setup du cog de règlement""" cog = RulesCog(bot) bot.add_cog(cog)