219 lines
7.6 KiB
Python
219 lines
7.6 KiB
Python
# Règlement Discord : rôle d'arrivée à la connexion, rôle validé au clic sur le bouton.
|
|
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_button_success_text(
|
|
validated_role: discord.Role,
|
|
presentation_ch: TextChannel | None,
|
|
) -> str:
|
|
base = f"c'est bon 😌 tu as maintenant le rôle **{validated_role.name}**."
|
|
if presentation_ch:
|
|
return f"{base} Tu peux aller 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 validated_id:
|
|
await interaction.response.send_message("Rôle membre validé non configuré.", ephemeral=True)
|
|
return
|
|
|
|
validated_role = interaction.guild.get_role(validated_id)
|
|
if not validated_role:
|
|
await interaction.response.send_message("Rôle membre validé introuvable sur ce serveur.", ephemeral=True)
|
|
return
|
|
|
|
arrival_role = interaction.guild.get_role(arrival_id) if arrival_id else None
|
|
|
|
presentation_ch = interaction.guild.get_channel(presentation_id)
|
|
presentation_ch = presentation_ch if isinstance(presentation_ch, TextChannel) else None
|
|
success_text = _rules_ack_button_success_text(validated_role, presentation_ch)
|
|
|
|
# Le retrait du rôle d'arrivée est la marque persistante de l'acceptation.
|
|
# Le rôle validé peut ensuite être remplacé par le système de présentation ;
|
|
# dans ce cas, un nouveau clic ne doit surtout pas rejouer l'attribution.
|
|
if arrival_role and arrival_role not in member.roles:
|
|
await interaction.response.send_message(
|
|
"Tu as déjà accepté le règlement. Tes rôles ne seront pas modifiés.",
|
|
ephemeral=True,
|
|
)
|
|
return
|
|
|
|
if validated_role in member.roles:
|
|
await interaction.response.send_message(success_text, ephemeral=True)
|
|
return
|
|
|
|
try:
|
|
await member.add_roles(validated_role, reason="Acceptation du règlement (bouton)")
|
|
if arrival_role and arrival_role in member.roles:
|
|
await member.remove_roles(arrival_role, reason="Passage membre validé après charte")
|
|
except discord.Forbidden:
|
|
await interaction.response.send_message(
|
|
"Je n'ai pas la permission de modifier tes rôles (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:
|
|
"""Attribue uniquement le rôle d'arrivée à la connexion (le rôle validé vient du bouton)."""
|
|
with webapp.app_context():
|
|
config = ConfigurationHelper()
|
|
if not config.getValue("rules_ack_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)
|