diff --git a/commandes/bug_report.py b/commandes/bug_report.py index ad344c0..5e45ebd 100644 --- a/commandes/bug_report.py +++ b/commandes/bug_report.py @@ -12,6 +12,7 @@ kuby_logger = logging.getLogger("KubyBot") BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) REPORTS_FILE = os.path.join(BASE_DIR, "data", "gitlab_reports.json") + def save_report(issue_iid, user_id): os.makedirs(os.path.dirname(REPORTS_FILE), exist_ok=True) try: @@ -22,9 +23,9 @@ def save_report(issue_iid, user_id): data = json.load(f) except json.JSONDecodeError: kuby_logger.warning("REPORTS_FILE corrupted, re-initializing to avoid tracking loss.") - + data[str(issue_iid)] = user_id - + temp_file = f"{REPORTS_FILE}.tmp" with open(temp_file, "w", encoding="utf-8") as f: json.dump(data, f, indent=4) @@ -32,48 +33,75 @@ def save_report(issue_iid, user_id): except Exception as e: kuby_logger.error(f"Error saving report to JSON: {e}") + class BugReportModal(disnake.ui.Modal): def __init__(self, priority_choice): super().__init__(title="Signaler un Bug", components=[self.bug_title, self.description]) - self.priority_choice = priority_choice + + # 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 bug_title = disnake.ui.TextInput( label="Titre du bug", placeholder="Décrivez brièvement le problème...", required=True, - max_length=100 + max_length=100, ) - + description = disnake.ui.TextInput( label="Description détaillée", style=disnake.TextInputStyle.paragraph, placeholder="Que s'est-il passé ? Comment reproduire le bug ?", required=True, - max_length=1000 + max_length=1000, ) async def callback(self, interaction: disnake.Interaction): await interaction.response.send_message("Envoi de votre rapport de bug à GitLab...", ephemeral=True) - + + bug_title = (self.bug_title.value or "").strip() + if not bug_title: + bug_title = "Sans titre" + + priority_name = getattr(self.priority_choice, "name", None) or "Normal" + 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é:** {self.priority_choice.name}\n\n" - description_text += f"**Description:**\n{self.description.value}" - - # Mapping des priorités vers des labels GitLab stylisés (Scoped Labels) - labels = ["bug", "manual-report", f"Priority::{self.priority_choice.value}"] - + 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( - title=f"[MANUAL] {self.bug_title.value}", + title=f"[MANUAL] {bug_title}", description=description_text, - labels=labels + labels=labels, ) - + if result: issue_iid = result.get("iid") 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: - 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): def __init__(self): @@ -83,37 +111,49 @@ class FeatureSuggestionModal(disnake.ui.Modal): label="Titre de la suggestion", placeholder="Que voulez-vous ajouter ?", required=True, - max_length=100 + max_length=100, ) - + description = disnake.ui.TextInput( label="Détails de la fonctionnalité", style=disnake.TextInputStyle.paragraph, placeholder="Décrivez comment cela devrait fonctionner...", required=True, - max_length=1000 + max_length=1000, ) async def callback(self, interaction: disnake.Interaction): 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"**Description:**\n{self.description.value}" - + description_text += f"**Description:**\n{description_value}" + labels = ["enhancement", "manual-suggestion"] - + result = await gitlab_client.create_issue( - title=f"[SUGGESTION] {self.suggestion_title.value}", + title=f"[SUGGESTION] {suggestion_title}", description=description_text, - labels=labels + labels=labels, ) - + if result: issue_iid = result.get("iid") 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: - 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): def __init__(self, bot): @@ -124,20 +164,24 @@ class BugReport(commands.Cog): self.web_app.router.add_post('/bot_event', self.handle_bot_event) self.runner = web.AppRunner(self.web_app) await self.runner.setup() - + max_retries = 5 retry_delay = 2 - + for attempt in range(1, max_retries + 1): try: self.site = web.TCPSite(self.runner, '127.0.0.1', 5001) 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 except OSError as e: if e.errno == 98: # Address already in use 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) else: 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): if hasattr(self, 'runner'): - # On utilise create_task car cog_unload est synchrone async def cleanup(): try: if hasattr(self, 'site'): @@ -156,16 +199,14 @@ class BugReport(commands.Cog): kuby_logger.info("Internal Bot Webhook Server stopped and cleaned up.") except Exception as e: kuby_logger.error(f"Error during webhook server cleanup: {e}") - + self.bot.loop.create_task(cleanup()) 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") # On ignore la vérification du token pour faciliter le relais local try: - # On attend que le bot soit prêt si on vient de rebooter if not self.bot.is_ready(): kuby_logger.info("Webhook received but bot is not ready yet. Waiting up to 10s...") try: @@ -174,7 +215,7 @@ class BugReport(commands.Cog): kuby_logger.warning("Bot still not ready after 10s. Proceeding anyway (best effort).") payload = await request.json() - + event_type = payload.get("object_kind") if event_type not in ["issue", "note"]: return web.json_response({"status": "ignored"}, status=200) @@ -220,41 +261,58 @@ class BugReport(commands.Cog): if event_type == "issue": action = payload.get("object_attributes", {}).get("action") changes = payload.get("changes", {}) - - # Check for label changes + if "labels" in changes: 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: current_labels_str = ", ".join(display_labels) else: current_labels_str = "Mis à jour" try: - # 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) + 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}") + 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(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: - 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: - 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 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}.") + 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: @@ -262,35 +320,46 @@ class BugReport(commands.Cog): except Exception as 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") is_closing_now = action == "close" or (state == "closed" and "state_id" in changes) - + if is_closing_now: try: components = [ disnake.ui.Container( disnake.ui.Section( "🛠️ 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.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 !") + 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(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: - 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: - 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}).") 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: kuby_logger.error(f"Erreur HTTP lors de l'envoi du MP de clôture à {user}: {e}") except Exception as e: @@ -301,40 +370,42 @@ class BugReport(commands.Cog): elif event_type == "note": note_attr = payload.get("object_attributes", {}) is_system = note_attr.get("system", False) - + if not is_system: note_body = note_attr.get("note", "") author_name = payload.get("user", {}).get("name", "Développeur") - + try: components = [ disnake.ui.Container( disnake.ui.Section( "💬 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.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.Separator(divider=True), - disnake.ui.TextDisplay(f"✍️ **Par :** {author_name}") + disnake.ui.TextDisplay(f"✍️ **Par :** {author_name}"), ) ] - + try: 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: - await dev.send(f"✅ [SUIVI] L'utilisateur {user} ({user.id}) a bien reçu votre commentaire 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 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}") + await dev.send( + f"✅ [SUIVI] L'utilisateur {user} ({user.id}) a bien reçu votre commentaire pour l'issue #{issue_iid}." + ) + except (disnake.Forbidden, disnake.HTTPException): + 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 Exception as 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}") return web.json_response({"status": "error", "message": str(e)}, status=500) - @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"})): + @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] + mapping = {"Basse": "Low", "Normale": "Normal", "Haute": "High", "Urgente": "Urgent"} + 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) + 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): await interaction.response.send_modal(FeatureSuggestionModal()) + def setup(bot): bot.add_cog(BugReport(bot)) + diff --git a/commandes/moderation.py b/commandes/moderation.py index 3569c52..f6b5e87 100644 --- a/commandes/moderation.py +++ b/commandes/moderation.py @@ -246,12 +246,28 @@ class Moderation(commands.Cog): 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) + + # 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": 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_"): + 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_", "") if act in ["logs", "board"]: col = "log_channel_id" if act == "logs" else "board_channel_id" diff --git a/commandes/ticket/__init__.py b/commandes/ticket/__init__.py index b4dc43b..c0652b5 100644 --- a/commandes/ticket/__init__.py +++ b/commandes/ticket/__init__.py @@ -1200,9 +1200,9 @@ class CloseDelayView(disnake.ui.View): except Exception as e: print(f"Failed to send feedback DM: {e}") - # Send transcript to log channel - if self.config.log_channel_id: - try: + # Send transcript to log channel + if self.config.log_channel_id: + try: log_channel = interaction.guild.get_channel(self.config.log_channel_id) if log_channel: log_embed = disnake.Embed( @@ -1396,7 +1396,13 @@ class CloseTicketModal(disnake.ui.Modal): ) # 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) + 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}" log_embed.add_field(name="Créateur", value=creator_mention, inline=True)