Merge branch 'dev' into 'main'

Derniers ajouts en date

See merge request Omega_Kube/kuby!18
This commit is contained in:
Gameur 2026-05-19 18:50:34 +02:00
commit e87ac6f86e
3 changed files with 202 additions and 90 deletions

View file

@ -12,6 +12,7 @@ kuby_logger = logging.getLogger("KubyBot")
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
REPORTS_FILE = os.path.join(BASE_DIR, "data", "gitlab_reports.json") REPORTS_FILE = os.path.join(BASE_DIR, "data", "gitlab_reports.json")
def save_report(issue_iid, user_id): def save_report(issue_iid, user_id):
os.makedirs(os.path.dirname(REPORTS_FILE), exist_ok=True) os.makedirs(os.path.dirname(REPORTS_FILE), exist_ok=True)
try: try:
@ -32,16 +33,26 @@ def save_report(issue_iid, user_id):
except Exception as e: except Exception as e:
kuby_logger.error(f"Error saving report to JSON: {e}") kuby_logger.error(f"Error saving report to JSON: {e}")
class BugReportModal(disnake.ui.Modal): class BugReportModal(disnake.ui.Modal):
def __init__(self, priority_choice): def __init__(self, priority_choice):
super().__init__(title="Signaler un Bug", components=[self.bug_title, self.description]) super().__init__(title="Signaler un Bug", components=[self.bug_title, self.description])
# Sécurisation: éviter les valeurs None qui finissent dans GitLab
if priority_choice is None:
self.priority_choice = type(
"PriorityChoice",
(),
{"name": "Normal", "value": "Normal"},
)()
else:
self.priority_choice = priority_choice self.priority_choice = priority_choice
bug_title = disnake.ui.TextInput( bug_title = disnake.ui.TextInput(
label="Titre du bug", label="Titre du bug",
placeholder="Décrivez brièvement le problème...", placeholder="Décrivez brièvement le problème...",
required=True, required=True,
max_length=100 max_length=100,
) )
description = disnake.ui.TextInput( description = disnake.ui.TextInput(
@ -49,31 +60,48 @@ class BugReportModal(disnake.ui.Modal):
style=disnake.TextInputStyle.paragraph, style=disnake.TextInputStyle.paragraph,
placeholder="Que s'est-il passé ? Comment reproduire le bug ?", placeholder="Que s'est-il passé ? Comment reproduire le bug ?",
required=True, required=True,
max_length=1000 max_length=1000,
) )
async def callback(self, interaction: disnake.Interaction): async def callback(self, interaction: disnake.Interaction):
await interaction.response.send_message("Envoi de votre rapport de bug à GitLab...", ephemeral=True) 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" bug_title = (self.bug_title.value or "").strip()
description_text += f"**Priorité:** {self.priority_choice.name}\n\n" if not bug_title:
description_text += f"**Description:**\n{self.description.value}" bug_title = "Sans titre"
# Mapping des priorités vers des labels GitLab stylisés (Scoped Labels) priority_name = getattr(self.priority_choice, "name", None) or "Normal"
labels = ["bug", "manual-report", f"Priority::{self.priority_choice.value}"] priority_value = getattr(self.priority_choice, "value", None) or "Normal"
description_value = self.description.value or ""
description_text = f"**Rapporté par:** {interaction.user} ({interaction.user.id})\n"
description_text += f"**Priorité:** {priority_name}\n\n"
description_text += f"**Description:**\n{description_value}"
labels = ["bug", "manual-report", f"Priority::{priority_value}"]
result = await gitlab_client.create_issue( result = await gitlab_client.create_issue(
title=f"[MANUAL] {self.bug_title.value}", title=f"[MANUAL] {bug_title}",
description=description_text, description=description_text,
labels=labels labels=labels,
) )
if result: if result:
issue_iid = result.get("iid") issue_iid = result.get("iid")
save_report(issue_iid, interaction.user.id) save_report(issue_iid, interaction.user.id)
await interaction.edit_original_response(content=f"✅ Votre bug a été signalé avec succès ! Gameur a bien été averti et le bug sera réglé dans la prochaine mise à jour. [Voir l'issue]({result.get('web_url')})") await interaction.edit_original_response(
content=(
"✅ Votre bug a été signalé avec succès ! "
"Gameur a bien été averti et le bug sera réglé dans la prochaine mise à jour. "
f"[Voir l'issue]({result.get('web_url')})"
)
)
else: else:
await interaction.edit_original_response(content="❌ Une erreur est survenue lors de l'envoi du rapport à GitLab. Veuillez contacter un administrateur.") await interaction.edit_original_response(
content="❌ Une erreur est survenue lors de l'envoi du rapport à GitLab. Veuillez contacter un administrateur."
)
class FeatureSuggestionModal(disnake.ui.Modal): class FeatureSuggestionModal(disnake.ui.Modal):
def __init__(self): def __init__(self):
@ -83,7 +111,7 @@ class FeatureSuggestionModal(disnake.ui.Modal):
label="Titre de la suggestion", label="Titre de la suggestion",
placeholder="Que voulez-vous ajouter ?", placeholder="Que voulez-vous ajouter ?",
required=True, required=True,
max_length=100 max_length=100,
) )
description = disnake.ui.TextInput( description = disnake.ui.TextInput(
@ -91,29 +119,41 @@ class FeatureSuggestionModal(disnake.ui.Modal):
style=disnake.TextInputStyle.paragraph, style=disnake.TextInputStyle.paragraph,
placeholder="Décrivez comment cela devrait fonctionner...", placeholder="Décrivez comment cela devrait fonctionner...",
required=True, required=True,
max_length=1000 max_length=1000,
) )
async def callback(self, interaction: disnake.Interaction): async def callback(self, interaction: disnake.Interaction):
await interaction.response.send_message("Envoi de votre suggestion à GitLab...", ephemeral=True) await interaction.response.send_message("Envoi de votre suggestion à GitLab...", ephemeral=True)
suggestion_title = (self.suggestion_title.value or "").strip() or "Sans titre"
description_value = self.description.value or ""
description_text = f"**Suggéré par:** {interaction.user} ({interaction.user.id})\n\n" description_text = f"**Suggéré par:** {interaction.user} ({interaction.user.id})\n\n"
description_text += f"**Description:**\n{self.description.value}" description_text += f"**Description:**\n{description_value}"
labels = ["enhancement", "manual-suggestion"] labels = ["enhancement", "manual-suggestion"]
result = await gitlab_client.create_issue( result = await gitlab_client.create_issue(
title=f"[SUGGESTION] {self.suggestion_title.value}", title=f"[SUGGESTION] {suggestion_title}",
description=description_text, description=description_text,
labels=labels labels=labels,
) )
if result: if result:
issue_iid = result.get("iid") issue_iid = result.get("iid")
save_report(issue_iid, interaction.user.id) save_report(issue_iid, interaction.user.id)
await interaction.edit_original_response(content=f"✅ Votre suggestion a été envoyée avec succès ! Merci de contribuer à l'amélioration du bot. [Voir l'issue]({result.get('web_url')})") await interaction.edit_original_response(
content=(
"✅ Votre suggestion a été envoyée avec succès ! "
"Merci de contribuer à l'amélioration du bot. "
f"[Voir l'issue]({result.get('web_url')})"
)
)
else: else:
await interaction.edit_original_response(content="❌ Une erreur est survenue lors de l'envoi de la suggestion à GitLab.") await interaction.edit_original_response(
content="❌ Une erreur est survenue lors de l'envoi de la suggestion à GitLab."
)
class BugReport(commands.Cog): class BugReport(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
@ -132,12 +172,16 @@ class BugReport(commands.Cog):
try: try:
self.site = web.TCPSite(self.runner, '127.0.0.1', 5001) self.site = web.TCPSite(self.runner, '127.0.0.1', 5001)
await self.site.start() await self.site.start()
kuby_logger.info(f"Internal Bot Webhook Server started on 127.0.0.1:5001 (Attempt {attempt})") kuby_logger.info(
f"Internal Bot Webhook Server started on 127.0.0.1:5001 (Attempt {attempt})"
)
return return
except OSError as e: except OSError as e:
if e.errno == 98: # Address already in use if e.errno == 98: # Address already in use
if attempt < max_retries: if attempt < max_retries:
kuby_logger.warning(f"Port 5001 already in use, retrying in {retry_delay}s... ({attempt}/{max_retries})") kuby_logger.warning(
f"Port 5001 already in use, retrying in {retry_delay}s... ({attempt}/{max_retries})"
)
await asyncio.sleep(retry_delay) await asyncio.sleep(retry_delay)
else: else:
kuby_logger.error(f"Failed to bind to port 5001 after {max_retries} attempts.") kuby_logger.error(f"Failed to bind to port 5001 after {max_retries} attempts.")
@ -147,7 +191,6 @@ class BugReport(commands.Cog):
def cog_unload(self): def cog_unload(self):
if hasattr(self, 'runner'): if hasattr(self, 'runner'):
# On utilise create_task car cog_unload est synchrone
async def cleanup(): async def cleanup():
try: try:
if hasattr(self, 'site'): if hasattr(self, 'site'):
@ -160,12 +203,10 @@ class BugReport(commands.Cog):
self.bot.loop.create_task(cleanup()) self.bot.loop.create_task(cleanup())
async def handle_bot_event(self, request): async def handle_bot_event(self, request):
# Sécurité interne désactivée comme demandé (le port 5001 n'est accessible que via localhost)
provided_token = request.headers.get("X-Gitlab-Token") provided_token = request.headers.get("X-Gitlab-Token")
# On ignore la vérification du token pour faciliter le relais local # On ignore la vérification du token pour faciliter le relais local
try: try:
# On attend que le bot soit prêt si on vient de rebooter
if not self.bot.is_ready(): if not self.bot.is_ready():
kuby_logger.info("Webhook received but bot is not ready yet. Waiting up to 10s...") kuby_logger.info("Webhook received but bot is not ready yet. Waiting up to 10s...")
try: try:
@ -221,10 +262,13 @@ class BugReport(commands.Cog):
action = payload.get("object_attributes", {}).get("action") action = payload.get("object_attributes", {}).get("action")
changes = payload.get("changes", {}) changes = payload.get("changes", {})
# Check for label changes
if "labels" in changes: if "labels" in changes:
curr_labels = [l.get("title") for l in changes["labels"].get("current", []) if l.get("title")] curr_labels = [l.get("title") for l in changes["labels"].get("current", []) if l.get("title")]
display_labels = [l for l in curr_labels if not l.startswith("Priority::") and l not in ["bug", "manual-report", "manual-suggestion"]] display_labels = [
l
for l in curr_labels
if not l.startswith("Priority::") and l not in ["bug", "manual-report", "manual-suggestion"]
]
if display_labels: if display_labels:
current_labels_str = ", ".join(display_labels) current_labels_str = ", ".join(display_labels)
@ -232,29 +276,43 @@ class BugReport(commands.Cog):
current_labels_str = "Mis à jour" current_labels_str = "Mis à jour"
try: try:
# V2 Components migration
components = [ components = [
disnake.ui.Container( disnake.ui.Container(
disnake.ui.Section( disnake.ui.Section(
"🛠️ Mise à jour de votre signalement !", "🛠️ Mise à jour de votre signalement !",
accessory=disnake.ui.Thumbnail(self.bot.user.display_avatar.url) accessory=disnake.ui.Thumbnail(self.bot.user.display_avatar.url),
), ),
disnake.ui.Separator(divider=True), 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(
disnake.ui.TextDisplay(f"📌 **Nouveaux Labels / Statuts :** {current_labels_str}") 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: try:
await user.send(components=components) await user.send(components=components)
kuby_logger.info(f"Notification de label envoyée à {user} ({user.id}) pour l'issue #{issue_iid}") kuby_logger.info(
f"Notification de label envoyée à {user} ({user.id}) pour l'issue #{issue_iid}"
)
if dev and dev.id != user.id: 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}).") await dev.send(
f"✅ [SUIVI] L'utilisateur {user} ({user.id}) a bien reçu la notification de statut "
f"**{current_labels_str}** pour l'issue #{issue_iid} ({issue_title})."
)
except (disnake.Forbidden, disnake.HTTPException) as e: except (disnake.Forbidden, disnake.HTTPException) as e:
if isinstance(e, disnake.Forbidden) or (isinstance(e, disnake.HTTPException) and e.code == 50007): if isinstance(e, disnake.Forbidden) or (
kuby_logger.warning(f"Impossible d'envoyer un MP à {user} ({user.id}) - MPs désactivés (Code: {getattr(e, 'code', 'Unknown')})") 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: 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}.") await dev.send(
f"⚠️ [DM BLOQUÉ] {user} ({user.id}) a ses MPs fermés. Impossible de notifier pour l'issue #{issue_iid}."
)
else: else:
kuby_logger.error(f"Erreur HTTP lors de l'envoi du MP de statut à {user}: {e}") kuby_logger.error(f"Erreur HTTP lors de l'envoi du MP de statut à {user}: {e}")
except Exception as e: except Exception as e:
@ -262,7 +320,6 @@ class BugReport(commands.Cog):
except Exception as e: except Exception as e:
kuby_logger.error(f"Erreur inattendue dans la notification de label : {e}") kuby_logger.error(f"Erreur inattendue dans la notification de label : {e}")
# Check if it was closed
state = payload.get("object_attributes", {}).get("state") state = payload.get("object_attributes", {}).get("state")
is_closing_now = action == "close" or (state == "closed" and "state_id" in changes) is_closing_now = action == "close" or (state == "closed" and "state_id" in changes)
@ -272,25 +329,37 @@ class BugReport(commands.Cog):
disnake.ui.Container( disnake.ui.Container(
disnake.ui.Section( disnake.ui.Section(
"🛠️ Bug / Suggestion Terminé(e) !", "🛠️ Bug / Suggestion Terminé(e) !",
accessory=disnake.ui.Thumbnail(self.bot.user.display_avatar.url) accessory=disnake.ui.Thumbnail(self.bot.user.display_avatar.url),
), ),
disnake.ui.Separator(divider=True), 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(
disnake.ui.TextDisplay("🚀 **Prochaine étape :** La mise à jour arrive prochainement (ou est déjà là) !"), f"Bonne nouvelle ! Votre signalement **{issue_title}** a été marqué comme terminé ou résolu."
disnake.ui.TextDisplay("✨ Merci pour votre aide !") ),
disnake.ui.TextDisplay(
"🚀 **Prochaine étape :** La mise à jour arrive prochainement (ou est déjà là) !"
),
disnake.ui.TextDisplay("✨ Merci pour votre aide !"),
) )
] ]
try: try:
await user.send(components=components) await user.send(components=components)
kuby_logger.info(f"Notification de clôture envoyée à {user} ({user.id}) pour l'issue #{issue_iid}") 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: 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}.") 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, disnake.HTTPException) as e: except (disnake.Forbidden, disnake.HTTPException) as e:
if isinstance(e, disnake.Forbidden) or (isinstance(e, disnake.HTTPException) and e.code == 50007): 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}).") kuby_logger.warning(f"Impossible d'envoyer le MP de clôture à {user} ({user.id}).")
if dev and dev.id != 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).") 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: else:
kuby_logger.error(f"Erreur HTTP lors de l'envoi du MP de clôture à {user}: {e}") kuby_logger.error(f"Erreur HTTP lors de l'envoi du MP de clôture à {user}: {e}")
except Exception as e: except Exception as e:
@ -311,30 +380,32 @@ class BugReport(commands.Cog):
disnake.ui.Container( disnake.ui.Container(
disnake.ui.Section( disnake.ui.Section(
"💬 Nouveau commentaire sur votre signalement", "💬 Nouveau commentaire sur votre signalement",
accessory=disnake.ui.Thumbnail(self.bot.user.display_avatar.url) accessory=disnake.ui.Thumbnail(self.bot.user.display_avatar.url),
), ),
disnake.ui.Separator(divider=True), disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(f"Le développeur a répondu à votre rapport **{issue_title}** :"), disnake.ui.TextDisplay(
f"Le développeur a répondu à votre rapport **{issue_title}** :"
),
disnake.ui.TextDisplay(f">>> {note_body}"), disnake.ui.TextDisplay(f">>> {note_body}"),
disnake.ui.Separator(divider=True), disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(f"✍️ **Par :** {author_name}") disnake.ui.TextDisplay(f"✍️ **Par :** {author_name}"),
) )
] ]
try: try:
await user.send(components=components) await user.send(components=components)
kuby_logger.info(f"Commentaire GitLab envoyé à {user} ({user.id}) pour l'issue #{issue_iid}") kuby_logger.info(
f"Commentaire GitLab envoyé à {user} ({user.id}) pour l'issue #{issue_iid}"
)
if dev and dev.id != user.id: 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}.") await dev.send(
except (disnake.Forbidden, disnake.HTTPException) as e: f"✅ [SUIVI] L'utilisateur {user} ({user.id}) a bien reçu votre commentaire pour l'issue #{issue_iid}."
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}).") except (disnake.Forbidden, disnake.HTTPException):
if dev and dev.id != 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).") await dev.send(
else: f"⚠️ [DM BLOQUÉ] Impossible d'envoyer votre commentaire à {user} ({user.id}) pour l'issue #{issue_iid} (DMs désactivés)."
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: except Exception as e:
kuby_logger.error(f"Erreur inattendue dans la notification de commentaire : {e}") kuby_logger.error(f"Erreur inattendue dans la notification de commentaire : {e}")
@ -344,20 +415,39 @@ class BugReport(commands.Cog):
kuby_logger.error(f"Error handling bot event: {e}") kuby_logger.error(f"Error handling bot event: {e}")
return web.json_response({"status": "error", "message": str(e)}, status=500) return web.json_response({"status": "error", "message": str(e)}, status=500)
@commands.slash_command(name="signaler_bug", description="Signaler un bug aux développeurs") @commands.slash_command(
async def signaler_bug(self, interaction: disnake.ApplicationCommandInteraction, name="signaler_bug",
priority: str = commands.Param(description="Priorité du bug", choices={"Basse": "Low", "Normale": "Normal", "Haute": "High", "Urgente": "Urgent"})): 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 # Simulate app_commands.Choice behavior for existing code
from collections import namedtuple from collections import namedtuple
Choice = namedtuple('Choice', ['name', 'value']) Choice = namedtuple('Choice', ['name', 'value'])
# find the name from the value mapping = {"Basse": "Low", "Normale": "Normal", "Haute": "High", "Urgente": "Urgent"}
p_name = [k for k, v in {"Basse": "Low", "Normale": "Normal", "Haute": "High", "Urgente": "Urgent"}.items() if v == priority][0] reverse = {v: k for k, v in mapping.items()}
# priority vient du value GitLab (Low/Normal/High/Urgent)
p_name = reverse.get(priority, "Normal")
choice = Choice(name=p_name, value=priority) choice = Choice(name=p_name, value=priority)
await interaction.response.send_modal(BugReportModal(choice)) await interaction.response.send_modal(BugReportModal(choice))
@commands.slash_command(name="suggerer_fonctionnalite", description="Proposer une nouvelle fonctionnalité pour le bot") @commands.slash_command(
name="suggerer_fonctionnalite",
description="Proposer une nouvelle fonctionnalité pour le bot",
)
async def suggerer_fonctionnalite(self, interaction: disnake.ApplicationCommandInteraction): async def suggerer_fonctionnalite(self, interaction: disnake.ApplicationCommandInteraction):
await interaction.response.send_modal(FeatureSuggestionModal()) await interaction.response.send_modal(FeatureSuggestionModal())
def setup(bot): def setup(bot):
bot.add_cog(BugReport(bot)) bot.add_cog(BugReport(bot))

View file

@ -246,12 +246,28 @@ class Moderation(commands.Cog):
action, mid = cid.replace("modpan_", "").split(":") action, mid = cid.replace("modpan_", "").split(":")
member = interaction.guild.get_member(int(mid)) member = interaction.guild.get_member(int(mid))
if not member: return await interaction.response.send_message("❌ Membre introuvable.", ephemeral=True) if not member: return await interaction.response.send_message("❌ Membre introuvable.", ephemeral=True)
# Vérifications des permissions et de la hiérarchie
if member.top_role >= interaction.user.top_role and interaction.user.id != interaction.guild.owner_id:
return await interaction.response.send_message("❌ Permissions insuffisantes : ce membre a un rôle supérieur ou égal au vôtre.", ephemeral=True)
perms = interaction.user.guild_permissions
if action == "ban" and not perms.ban_members:
return await interaction.response.send_message("❌ Vous n'avez pas la permission de bannir des membres.", ephemeral=True)
elif action == "kick" and not perms.kick_members:
return await interaction.response.send_message("❌ Vous n'avez pas la permission d'expulser des membres.", ephemeral=True)
elif action in ["timeout", "warn", "clear"] and not perms.moderate_members:
return await interaction.response.send_message("❌ Vous n'avez pas la permission de gérer les sanctions (moderate_members).", ephemeral=True)
if action == "clear": if action == "clear":
await self.clearwarns(interaction, member, "Nettoyage Panel") await self.clearwarns(interaction, member, "Nettoyage Panel")
await self.send_modpanel_v2(interaction, member, edit=True) await self.send_modpanel_v2(interaction, member, edit=True)
else: else:
await interaction.response.send_modal(ModReasonModal(self, action.upper(), member)) await interaction.response.send_modal(ModReasonModal(self, action.upper(), member))
elif cid.startswith("modcfg_"): elif cid.startswith("modcfg_"):
if not interaction.user.guild_permissions.administrator:
return await interaction.response.send_message("❌ Vous devez être administrateur pour modifier cette configuration.", ephemeral=True)
act = cid.replace("modcfg_", "") act = cid.replace("modcfg_", "")
if act in ["logs", "board"]: if act in ["logs", "board"]:
col = "log_channel_id" if act == "logs" else "board_channel_id" col = "log_channel_id" if act == "logs" else "board_channel_id"

View file

@ -1396,7 +1396,13 @@ class CloseTicketModal(disnake.ui.Modal):
) )
# Get ticket creator # Get ticket creator
# Fetch le créateur pour affichage fiable même si absent du cache
ticket_creator = interaction.guild.get_member(self.ticket.user_id) ticket_creator = interaction.guild.get_member(self.ticket.user_id)
if not ticket_creator:
try:
ticket_creator = await interaction.guild.fetch_member(self.ticket.user_id)
except Exception:
ticket_creator = None
creator_mention = ticket_creator.mention if ticket_creator else f"Utilisateur {self.ticket.user_id}" creator_mention = ticket_creator.mention if ticket_creator else f"Utilisateur {self.ticket.user_id}"
log_embed.add_field(name="Créateur", value=creator_mention, inline=True) log_embed.add_field(name="Créateur", value=creator_mention, inline=True)