# Règlement Discord : embed + bouton persistant, rôles arrivée / validé, promo sur canal présentation. import asyncio import logging import discord from discord import TextChannel from discord.ui import Button, View from webapp import webapp from database import db from database.helpers import ConfigurationHelper RULES_BUTTON_CUSTOM_ID = "mamie_rules_accept" DEFAULT_BUTTON_LABEL = "J'ai lu le règlement" class AcceptRulesButton(Button): def __init__(self, label: str): super().__init__( style=discord.ButtonStyle.success, label=(label or DEFAULT_BUTTON_LABEL)[:80], custom_id=RULES_BUTTON_CUSTOM_ID, ) async def callback(self, interaction: discord.Interaction): await handle_rules_accept(interaction) class RulesAcceptView(View): def __init__(self, button_label: str): super().__init__(timeout=None) self.add_item(AcceptRulesButton(button_label)) def register_persistent_rules_view(client: discord.Client) -> None: with webapp.app_context(): label = (ConfigurationHelper().getValue("rules_button_label") or "").strip() or DEFAULT_BUTTON_LABEL client.add_view(RulesAcceptView(label)) def _rules_ack_success_text( role: discord.Role, presentation_ch: TextChannel | None, validated_role: discord.Role | None, ) -> str: base = f"c'est bon 😌 tu as maintenant le rôle **{role.name}**." if presentation_ch and validated_role: return ( f"{base} va te présenter dans {presentation_ch.mention} " f"pour recevoir **{validated_role.name}**." ) if presentation_ch: return f"{base} va te présenter dans {presentation_ch.mention}." return base async def handle_rules_accept(interaction: discord.Interaction) -> None: if not interaction.guild: await interaction.response.send_message("Action impossible dans ce contexte.", ephemeral=True) return try: member = await interaction.guild.fetch_member(interaction.user.id) except (discord.NotFound, discord.HTTPException): member = interaction.user if isinstance(interaction.user, discord.Member) else None if member is None: await interaction.response.send_message("Action impossible dans ce contexte.", ephemeral=True) return with webapp.app_context(): config = ConfigurationHelper() enabled = config.getValue("rules_ack_enable") arrival_id = config.getIntValue("rules_arrival_role_id") presentation_id = config.getIntValue("rules_presentation_channel_id") validated_id = config.getIntValue("rules_validated_role_id") if not enabled: await interaction.response.send_message("Cette fonctionnalité est désactivée.", ephemeral=True) return if not arrival_id: await interaction.response.send_message("Rôle d'arrivée non configuré.", ephemeral=True) return role = interaction.guild.get_role(arrival_id) if not role: await interaction.response.send_message("Rôle d'arrivée introuvable sur ce serveur.", ephemeral=True) return presentation_ch = interaction.guild.get_channel(presentation_id) presentation_ch = presentation_ch if isinstance(presentation_ch, TextChannel) else None validated_role = interaction.guild.get_role(validated_id) if validated_id else None success_text = _rules_ack_success_text(role, presentation_ch, validated_role) if role in member.roles: # Idempotent : double clic, rôle auto-attribué à l'entrée, ou cache corrigé par fetch_member. await interaction.response.send_message(success_text, ephemeral=True) return try: await member.add_roles(role, reason="Acceptation du règlement (bouton)") except discord.Forbidden: await interaction.response.send_message( "Je n'ai pas la permission de t'attribuer ce rôle (rôle du bot trop bas ou « Gérer les rôles » manquant).", ephemeral=True, ) return except discord.HTTPException as e: await interaction.response.send_message(f"Erreur Discord : {e}", ephemeral=True) return await interaction.response.send_message(success_text, ephemeral=True) async def publish_rules_embed(bot: discord.Client) -> tuple[bool, str]: with webapp.app_context(): config = ConfigurationHelper() if not config.getValue("rules_ack_enable"): return False, "Activez d'abord « Règlement avec bouton » et enregistrez la configuration." channel_id = config.getIntValue("rules_channel_id") body = (config.getValue("rules_embed_body") or "").strip() title = (config.getValue("rules_embed_title") or "").strip() or "Bienvenue" button_label = (config.getValue("rules_button_label") or "").strip() or DEFAULT_BUTTON_LABEL old_mid = config.getIntValue("rules_message_id") old_ch_id = config.getIntValue("rules_message_channel_id") if not channel_id: return False, "Choisissez un canal du règlement." if not body: return False, "Le texte du règlement est vide." channel = bot.get_channel(channel_id) if not channel or not isinstance(channel, TextChannel): return False, "Canal du règlement introuvable." if len(body) > 4096: body = body[:4093] + "..." embed = discord.Embed(title=title, description=body, color=discord.Color.blurple()) view = RulesAcceptView(button_label) try: if old_mid and old_ch_id: old_ch = bot.get_channel(old_ch_id) if old_ch and isinstance(old_ch, TextChannel): try: old_msg = await old_ch.fetch_message(old_mid) await old_msg.delete() except (discord.NotFound, discord.Forbidden, discord.HTTPException): pass msg = await channel.send(embed=embed, view=view) with webapp.app_context(): ConfigurationHelper().createOrUpdate("rules_message_id", str(msg.id)) ConfigurationHelper().createOrUpdate("rules_message_channel_id", str(channel.id)) db.session.commit() return True, "Message du règlement publié sur Discord." except discord.Forbidden: return False, "Permission refusée pour envoyer ou supprimer un message dans ce canal." except Exception as e: logging.exception("publish_rules_embed") return False, str(e) def publish_rules_embed_sync(bot: discord.Client) -> tuple[bool, str]: try: future = asyncio.run_coroutine_threadsafe(publish_rules_embed(bot), bot.loop) return future.result(timeout=30) except Exception as e: logging.exception("publish_rules_embed_sync") return False, str(e) async def assign_rules_arrival_on_join(bot: discord.Client, member: discord.Member) -> None: """Si activé dans la config, attribue le rôle d'arrivée dès le join (sans attendre le bouton).""" with webapp.app_context(): config = ConfigurationHelper() if not config.getValue("rules_ack_enable"): return if not config.getValue("rules_arrival_on_join_enable"): return arrival_id = config.getIntValue("rules_arrival_role_id") validated_id = config.getIntValue("rules_validated_role_id") if not arrival_id: return guild = member.guild arrival_role = guild.get_role(arrival_id) if not arrival_role: logging.warning("assign_rules_arrival_on_join: rôle d'arrivée %s introuvable sur %s", arrival_id, guild.id) return if validated_id: validated_role = guild.get_role(validated_id) if validated_role and validated_role in member.roles: return if arrival_role in member.roles: return try: await member.add_roles(arrival_role, reason="Règlement : rôle d'arrivée à la connexion") except discord.Forbidden: logging.warning( "assign_rules_arrival_on_join: permission refusée pour %s sur %s (hiérarchie des rôles ?)", member.id, guild.id, ) except discord.HTTPException as e: logging.warning("assign_rules_arrival_on_join: %s", e) async def on_presentation_message(bot: discord.Client, message: discord.Message) -> None: if message.author.bot: return with webapp.app_context(): config = ConfigurationHelper() if not config.getValue("rules_ack_enable"): return presentation_id = config.getIntValue("rules_presentation_channel_id") if not presentation_id or message.channel.id != presentation_id: return arrival_id = config.getIntValue("rules_arrival_role_id") validated_id = config.getIntValue("rules_validated_role_id") if not validated_id or not arrival_id: return member = message.author if not isinstance(member, discord.Member): return arrival_role = message.guild.get_role(arrival_id) validated_role = message.guild.get_role(validated_id) if not validated_role or not arrival_role: return if arrival_role not in member.roles: return if validated_role in member.roles: return try: await member.add_roles(validated_role, reason="Présentation dans le canal configuré") if arrival_role: await member.remove_roles(arrival_role, reason="Membre validé après présentation") except (discord.Forbidden, discord.HTTPException) as e: logging.warning("on_presentation_message: %s", e)