From 8bcc40f217c57796a460487996a3e4a6478eb0b2 Mon Sep 17 00:00:00 2001 From: Mathis Date: Sun, 14 Jun 2026 22:36:21 +0200 Subject: [PATCH 1/3] =?UTF-8?q?V0.5=20du=20syst=C3=A8me=20de=20rapport=20p?= =?UTF-8?q?lusieurs=20choses=20=C3=A0=20revoir=20avant=20MEP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 1 + commandes/rapports.py | 904 ++++++++++++++++++++++++++++++++++ commandes/ticket/__init__.py | 11 +- data/convocation_backups.json | 10 +- 4 files changed, 921 insertions(+), 5 deletions(-) create mode 100644 commandes/rapports.py diff --git a/bot.py b/bot.py index 9303c6f..67a3a0a 100755 --- a/bot.py +++ b/bot.py @@ -130,6 +130,7 @@ class MyBot(commands.Bot): "commandes.moderation", "commandes.invites", "commandes.taches", + "commandes.rapports", ] for extension in extensions: diff --git a/commandes/rapports.py b/commandes/rapports.py new file mode 100644 index 0000000..8f1bf12 --- /dev/null +++ b/commandes/rapports.py @@ -0,0 +1,904 @@ +import os +import sqlite3 +import logging +import asyncio +from datetime import datetime, timedelta +try: + from zoneinfo import ZoneInfo +except ImportError: + from backports.zoneinfo import ZoneInfo + +import disnake +from disnake.ext import commands, tasks + +kuby_logger = logging.getLogger("KubyBot") +PARIS_TZ = ZoneInfo("Europe/Paris") + +class ReportsDB: + def __init__(self, db_path): + self.db_path = db_path + self._init_db() + + def _init_db(self): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # Table pour les rapports + cursor.execute(''' + CREATE TABLE IF NOT EXISTS reports ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id INTEGER NOT NULL, + author_id INTEGER NOT NULL, + title TEXT NOT NULL, + subject TEXT NOT NULL, + content TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'En attente', + created_at TEXT NOT NULL, + last_edited_at TEXT, + channel_id INTEGER NOT NULL, + role_id_auteur INTEGER + ) + ''') + + # Table pour la configuration du module + cursor.execute(''' + CREATE TABLE IF NOT EXISTS reports_config ( + guild_id INTEGER PRIMARY KEY, + viewer_role_id INTEGER + ) + ''') + conn.commit() + conn.close() + + def get_config(self, guild_id): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute("SELECT viewer_role_id FROM reports_config WHERE guild_id = ?", (guild_id,)) + row = cursor.fetchone() + conn.close() + return row[0] if row else None + + def set_config(self, guild_id, viewer_role_id): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO reports_config (guild_id, viewer_role_id) + VALUES (?, ?) + ON CONFLICT(guild_id) DO UPDATE SET viewer_role_id = excluded.viewer_role_id + ''', (guild_id, viewer_role_id)) + conn.commit() + conn.close() + + def add_report(self, guild_id, author_id, title, subject, content, channel_id, role_id_auteur): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + created_at = datetime.now(PARIS_TZ).isoformat() + cursor.execute(''' + INSERT INTO reports (guild_id, author_id, title, subject, content, status, created_at, channel_id, role_id_auteur) + VALUES (?, ?, ?, ?, ?, 'En attente', ?, ?, ?) + ''', (guild_id, author_id, title, subject, content, created_at, channel_id, role_id_auteur)) + report_id = cursor.lastrowid + conn.commit() + conn.close() + return report_id + + def get_report(self, report_id): + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("SELECT * FROM reports WHERE id = ?", (report_id,)) + row = cursor.fetchone() + conn.close() + return dict(row) if row else None + + def update_report_status(self, report_id, status): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + last_edited_at = datetime.now(PARIS_TZ).isoformat() + cursor.execute(''' + UPDATE reports + SET status = ?, last_edited_at = ? + WHERE id = ? + ''', (status, last_edited_at, report_id)) + conn.commit() + conn.close() + + def update_report_content(self, report_id, title, subject, content): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + last_edited_at = datetime.now(PARIS_TZ).isoformat() + cursor.execute(''' + UPDATE reports + SET title = ?, subject = ?, content = ?, last_edited_at = ? + WHERE id = ? + ''', (title, subject, content, last_edited_at, report_id)) + conn.commit() + conn.close() + + def delete_report(self, report_id): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute("DELETE FROM reports WHERE id = ?", (report_id,)) + conn.commit() + conn.close() + + def get_reports(self, guild_id, author_id=None, subject=None, status=None, role_id=None): + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + query = "SELECT * FROM reports WHERE guild_id = ?" + params = [guild_id] + + if author_id is not None: + query += " AND author_id = ?" + params.append(author_id) + if subject is not None: + query += " AND subject LIKE ?" + params.append(f"%{subject}%") + if status is not None: + query += " AND status = ?" + params.append(status) + if role_id is not None: + query += " AND role_id_auteur = ?" + params.append(role_id) + + query += " ORDER BY id ASC" + + cursor.execute(query, params) + rows = cursor.fetchall() + conn.close() + return [dict(r) for r in rows] + + def get_distinct_subjects(self, guild_id): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute("SELECT DISTINCT subject FROM reports WHERE guild_id = ? ORDER BY subject ASC", (guild_id,)) + rows = cursor.fetchall() + conn.close() + return [row[0] for row in rows if row[0]] + + def purge_manual(self, guild_id): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cutoff = (datetime.now(PARIS_TZ) - timedelta(days=7)).isoformat() + cursor.execute(''' + DELETE FROM reports + WHERE guild_id = ? AND status != 'Classé' AND created_at < ? + ''', (guild_id, cutoff)) + deleted_count = cursor.rowcount + conn.commit() + conn.close() + return deleted_count + + def purge_automatic(self): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cutoff = (datetime.now(PARIS_TZ) - timedelta(days=90)).isoformat() + cursor.execute(''' + DELETE FROM reports + WHERE status != 'Classé' AND created_at < ? + ''', (cutoff,)) + deleted_count = cursor.rowcount + conn.commit() + conn.close() + return deleted_count + +class ReportsConfigView(disnake.ui.View): + def __init__(self, cog, guild_id: int): + super().__init__(timeout=300) + self.cog = cog + self.guild_id = guild_id + + @disnake.ui.role_select(placeholder="Modifier le rôle Lecteur (Viewer)", min_values=1, max_values=1, custom_id="reports_select_viewer_role") + async def select_viewer_role(self, select: disnake.ui.RoleSelect, interaction: disnake.MessageInteraction): + if not interaction.author.guild_permissions.administrator: + return await interaction.response.send_message("❌ Vous devez être administrateur pour configurer ce module.", ephemeral=True) + + role = select.values[0] + self.cog.db.set_config(self.guild_id, role.id) + + embed = interaction.message.embeds[0] + embed.set_field_at( + 0, + name="👁️ Rôle Lecteur (Viewer)", + value=f"{role.mention}\n*(Les membres ayant ce rôle peuvent consulter tous les rapports du serveur)*", + inline=False + ) + await interaction.response.edit_message(embed=embed, view=self) + await interaction.followup.send(f"✅ Rôle Lecteur configuré sur {role.mention}", ephemeral=True) + + @disnake.ui.button(label="Purge Manuelle", style=disnake.ButtonStyle.red, emoji="🗑️", custom_id="reports_manual_purge_btn") + async def manual_purge(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction): + if not interaction.author.guild_permissions.administrator: + return await interaction.response.send_message("❌ Vous devez être administrateur pour lancer la purge.", ephemeral=True) + + await interaction.response.defer(ephemeral=True) + deleted_count = self.cog.db.purge_manual(self.guild_id) + await interaction.followup.send(f"✅ Purge terminée ! **{deleted_count}** rapports (créés il y a > 7 jours et non-classés) ont été définitivement supprimés.", ephemeral=True) + +class ReportModal(disnake.ui.Modal): + def __init__(self, cog): + self.cog = cog + components = [ + disnake.ui.TextInput( + label="Titre du rapport", + placeholder="Entrez le titre du rapport...", + custom_id="report_title", + required=True, + max_length=100 + ), + disnake.ui.TextInput( + label="Objet du rapport", + placeholder="Ex: Bug, Plainte, Suggestion...", + custom_id="report_subject", + required=True, + max_length=100 + ), + disnake.ui.TextInput( + label="Contenu", + style=disnake.TextInputStyle.paragraph, + placeholder="Décrivez en détail...", + custom_id="report_content", + required=True, + max_length=2000 + ) + ] + super().__init__(title="Créer un Rapport", custom_id="report_modal", components=components) + + async def callback(self, interaction: disnake.ModalInteraction): + title = interaction.text_values["report_title"].strip() + subject = interaction.text_values["report_subject"].strip() + content = interaction.text_values["report_content"].strip() + + role_id_auteur = interaction.author.top_role.id if interaction.author.roles else None + + report_id = self.cog.db.add_report( + guild_id=interaction.guild.id, + author_id=interaction.author.id, + title=title, + subject=subject, + content=content, + channel_id=interaction.channel.id, + role_id_auteur=role_id_auteur + ) + + await interaction.response.send_message( + f"✅ Votre rapport a été enregistré avec succès sous l'ID **#{report_id}**.", + ephemeral=True + ) + +class EditReportModal(disnake.ui.Modal): + def __init__(self, cog, report_id, browser_view): + self.cog = cog + self.report_id = report_id + self.browser_view = browser_view + + report = self.cog.db.get_report(report_id) + + components = [ + disnake.ui.TextInput( + label="Modifier le titre", + value=report['title'], + custom_id="edit_title", + required=True, + max_length=100 + ), + disnake.ui.TextInput( + label="Modifier l'objet", + value=report['subject'], + custom_id="edit_subject", + required=True, + max_length=100 + ), + disnake.ui.TextInput( + label="Modifier le contenu", + style=disnake.TextInputStyle.paragraph, + value=report['content'], + custom_id="edit_content", + required=True, + max_length=2000 + ) + ] + super().__init__(title=f"Modifier le Rapport #{report_id}", custom_id=f"edit_modal_{report_id}", components=components) + + async def callback(self, interaction: disnake.ModalInteraction): + report = self.cog.db.get_report(self.report_id) + if not report: + return await interaction.response.send_message("❌ Ce rapport n'existe plus.", ephemeral=True) + if report['status'] in ['Traité', 'Classé']: + return await interaction.response.send_message("❌ Ce rapport a été traité ou classé et ne peut plus être modifié.", ephemeral=True) + + title = interaction.text_values["edit_title"].strip() + subject = interaction.text_values["edit_subject"].strip() + content = interaction.text_values["edit_content"].strip() + + self.cog.db.update_report_content(self.report_id, title, subject, content) + + updated_report = self.cog.db.get_report(self.report_id) + + for idx, r in enumerate(self.browser_view.reports): + if r['id'] == self.report_id: + self.browser_view.reports[idx] = updated_report + break + + self.browser_view.update_components() + + await interaction.response.defer(ephemeral=True) + await interaction.message.edit(embed=self.browser_view.get_embed(), view=self.browser_view) + await interaction.followup.send("✅ Votre rapport a été modifié avec succès.", ephemeral=True) + +class ReportsFilterView(disnake.ui.View): + def __init__(self, cog, interaction): + super().__init__(timeout=300) + self.cog = cog + self.interaction = interaction + self.viewer_id = interaction.author.id + self.guild_id = interaction.guild.id + self.is_admin = interaction.author.guild_permissions.administrator + + viewer_role_id = self.cog.db.get_config(self.guild_id) + self.is_viewer = viewer_role_id in [r.id for r in interaction.author.roles] if viewer_role_id else False + self.has_global_access = self.is_admin or self.is_viewer + + self.selected_user = None + self.selected_role = None + self.selected_status = None + self.selected_subject = None + + self.update_components() + + def update_components(self): + self.clear_items() + + # Row 0: Menu Status + status_options = [ + disnake.SelectOption(label="Tous les statuts", emoji="⚪", value="all", default=(self.selected_status is None)), + disnake.SelectOption(label="En attente", emoji="🟡", value="En attente", default=(self.selected_status == "En attente")), + disnake.SelectOption(label="Traité", emoji="🟢", value="Traité", default=(self.selected_status == "Traité")), + disnake.SelectOption(label="Classé", emoji="🔴", value="Classé", default=(self.selected_status == "Classé")) + ] + status_select = disnake.ui.StringSelect( + placeholder="Filtrer par statut...", + options=status_options, + custom_id="reports_filter_status", + row=0 + ) + async def status_callback(inter: disnake.MessageInteraction): + val = inter.values[0] + self.selected_status = None if val == "all" else val + self.update_components() + await inter.response.edit_message(components=self.get_components_v2()) + status_select.callback = status_callback + self.add_item(status_select) + + # Row 1: Menu Catégorie + subjects = self.cog.db.get_distinct_subjects(self.guild_id) + if subjects: + subject_options = [ + disnake.SelectOption(label="Toutes les catégories", emoji="📁", value="all", default=(self.selected_subject is None)) + ] + for subj in subjects[:24]: + subject_options.append( + disnake.SelectOption(label=subj, value=subj, default=(self.selected_subject == subj)) + ) + subject_select = disnake.ui.StringSelect( + placeholder="Filtrer par catégorie...", + options=subject_options, + custom_id="reports_filter_subject", + row=1 + ) + async def subject_callback(inter: disnake.MessageInteraction): + val = inter.values[0] + self.selected_subject = None if val == "all" else val + self.update_components() + await inter.response.edit_message(components=self.get_components_v2()) + subject_select.callback = subject_callback + self.add_item(subject_select) + + current_row = 2 + + # Row 2 & 3: Filtres utilisateur / rôle (globaux uniquement) + if self.has_global_access: + user_select = disnake.ui.UserSelect( + placeholder="Filtrer par auteur...", + min_values=0, + max_values=1, + custom_id="reports_filter_user", + row=current_row + ) + async def user_callback(inter: disnake.MessageInteraction): + if inter.values: + val = inter.values[0] + if isinstance(val, str): + self.selected_user = inter.guild.get_member(int(val)) or val + else: + self.selected_user = val + else: + self.selected_user = None + self.update_components() + await inter.response.edit_message(components=self.get_components_v2()) + user_select.callback = user_callback + self.add_item(user_select) + current_row += 1 + + role_select = disnake.ui.RoleSelect( + placeholder="Filtrer par rôle auteur...", + min_values=0, + max_values=1, + custom_id="reports_filter_role", + row=current_row + ) + async def role_callback(inter: disnake.MessageInteraction): + if inter.values: + val = inter.values[0] + if isinstance(val, str): + self.selected_role = inter.guild.get_role(int(val)) or val + else: + self.selected_role = val + else: + self.selected_role = None + self.update_components() + await inter.response.edit_message(components=self.get_components_v2()) + role_select.callback = role_callback + self.add_item(role_select) + current_row += 1 + + # Row 4: Boutons Recherche / Reset + search_btn = disnake.ui.Button( + label="Rechercher", + style=disnake.ButtonStyle.green, + emoji="🔍", + custom_id="reports_filter_search", + row=current_row + ) + async def search_callback(inter: disnake.MessageInteraction): + await inter.response.defer(ephemeral=True) + + author_filter = None + if not self.has_global_access: + author_filter = self.viewer_id + elif self.selected_user: + author_filter = self.selected_user.id if hasattr(self.selected_user, "id") else int(self.selected_user) + + role_filter_id = None + if self.selected_role: + role_filter_id = self.selected_role.id if hasattr(self.selected_role, "id") else int(self.selected_role) + + reports = self.cog.db.get_reports( + guild_id=self.guild_id, + author_id=author_filter, + subject=self.selected_subject, + status=self.selected_status, + role_id=role_filter_id + ) + + if not reports: + await inter.followup.send("❌ Aucun rapport ne correspond à votre recherche.", ephemeral=True) + return + + browser = ReportsBrowserView(self.cog, self.interaction, reports, filter_view=self) + await inter.edit_original_message(content=None, embed=browser.get_embed(), view=browser) + + search_btn.callback = search_callback + self.add_item(search_btn) + + reset_btn = disnake.ui.Button( + label="Réinitialiser", + style=disnake.ButtonStyle.red, + emoji="❌", + custom_id="reports_filter_reset", + row=current_row + ) + async def reset_callback(inter: disnake.MessageInteraction): + self.selected_user = None + self.selected_role = None + self.selected_status = None + self.selected_subject = None + self.update_components() + await inter.response.edit_message(components=self.get_components_v2()) + reset_btn.callback = reset_callback + self.add_item(reset_btn) + + def get_components_v2(self): + sections = [ + disnake.ui.Section( + "# 🔍 Recherche & Filtres de Rapports", + accessory=disnake.ui.Thumbnail(self.interaction.author.display_avatar.url) + ), + disnake.ui.Separator(divider=True) + ] + + filters_desc = [] + if self.selected_status: + filters_desc.append(f"🚦 **Statut** : {self.selected_status}") + else: + filters_desc.append("🚦 **Statut** : Tous") + + if self.has_global_access: + if self.selected_user: + mention = self.selected_user.mention if hasattr(self.selected_user, "mention") else f"<@{self.selected_user}>" + filters_desc.append(f"👤 **Auteur** : {mention}") + else: + filters_desc.append("👤 **Auteur** : Tous") + + if self.selected_role: + mention = self.selected_role.mention if hasattr(self.selected_role, "mention") else f"<@&{self.selected_role}>" + filters_desc.append(f"🛡️ **Rôle Auteur** : {mention}") + else: + filters_desc.append("🛡️ **Rôle Auteur** : Tous") + else: + filters_desc.append(f"👤 **Auteur** : {self.interaction.author.mention} *(Accès restreint à vos propres rapports)*") + + if self.selected_subject: + filters_desc.append(f"📁 **Objet / Catégorie** : `{self.selected_subject}`") + else: + filters_desc.append("📁 **Objet / Catégorie** : Tous") + + sections.append(disnake.ui.TextDisplay(content="\n".join(filters_desc))) + sections.append(disnake.ui.Separator(divider=True)) + + return [disnake.ui.Container(*sections)] + self.children + + async def interaction_check(self, interaction: disnake.MessageInteraction) -> bool: + if interaction.author.id != self.viewer_id: + await interaction.response.send_message("❌ Vous ne pouvez pas interagir avec ce menu.", ephemeral=True) + return False + return True + +class ReportsBrowserView(disnake.ui.View): + def __init__(self, cog, interaction, reports, filter_view=None, current_index=0): + super().__init__(timeout=600) + self.cog = cog + self.interaction = interaction + self.guild_id = interaction.guild.id + self.viewer_id = interaction.author.id + self.is_admin = interaction.author.guild_permissions.administrator + self.reports = reports + self.filter_view = filter_view + self.current_index = current_index + self.page_index = current_index // 25 + self.update_components() + + def update_components(self): + self.clear_items() + + if not self.reports: + return + + report = self.reports[self.current_index] + + # Row 0: StringSelectMenu de selection du rapport + total_pages = (len(self.reports) + 24) // 25 + start = self.page_index * 25 + end = min(start + 25, len(self.reports)) + + options = [] + for i in range(start, end): + rep = self.reports[i] + title = rep['title'] + if len(title) > 60: + title = title[:57] + "..." + label = f"#{rep['id']} - {title}" + description = f"Statut: {rep['status']} | Objet: {rep['subject']}" + if len(description) > 100: + description = description[:97] + "..." + options.append( + disnake.SelectOption( + label=label, + description=description, + value=str(i), + default=(i == self.current_index) + ) + ) + + select_menu = disnake.ui.StringSelect( + placeholder="Sélectionnez un rapport à lire...", + options=options, + custom_id="reports_browser_select", + row=0 + ) + + async def select_callback(inter: disnake.MessageInteraction): + self.current_index = int(inter.values[0]) + self.page_index = self.current_index // 25 + self.update_components() + await inter.response.edit_message(embed=self.get_embed(), view=self) + + select_menu.callback = select_callback + self.add_item(select_menu) + + # Row 1: Boutons de pagination de pages (si > 25 rapports) + if total_pages > 1: + prev_btn = disnake.ui.Button( + label="⬅️ Page précédente", + style=disnake.ButtonStyle.secondary, + disabled=(self.page_index == 0), + custom_id="reports_browser_prev_page", + row=1 + ) + next_btn = disnake.ui.Button( + label="➡️ Page suivante", + style=disnake.ButtonStyle.secondary, + disabled=(self.page_index >= total_pages - 1), + custom_id="reports_browser_next_page", + row=1 + ) + + async def prev_callback(inter: disnake.MessageInteraction): + self.page_index -= 1 + self.current_index = self.page_index * 25 + self.update_components() + await inter.response.edit_message(embed=self.get_embed(), view=self) + + async def next_callback(inter: disnake.MessageInteraction): + self.page_index += 1 + self.current_index = self.page_index * 25 + self.update_components() + await inter.response.edit_message(embed=self.get_embed(), view=self) + + prev_btn.callback = prev_callback + next_btn.callback = next_callback + self.add_item(prev_btn) + self.add_item(next_btn) + + # Row 2: Boutons d'Action (Modifier, Supprimer, Retour) + is_author = (self.viewer_id == report['author_id']) + is_blocked = report['status'] in ['Traité', 'Classé'] + + edit_disabled = not is_author or is_blocked + delete_disabled = not (is_author or self.is_admin) or is_blocked + + edit_btn = disnake.ui.Button( + label="Modifier", + style=disnake.ButtonStyle.primary, + emoji="✏️", + disabled=edit_disabled, + custom_id="reports_browser_edit", + row=2 + ) + delete_btn = disnake.ui.Button( + label="Supprimer", + style=disnake.ButtonStyle.danger, + emoji="🗑️", + disabled=delete_disabled, + custom_id="reports_browser_delete", + row=2 + ) + back_btn = disnake.ui.Button( + label="Retour aux filtres", + style=disnake.ButtonStyle.secondary, + emoji="⬅️", + custom_id="reports_browser_back", + row=2 + ) + + async def edit_callback(inter: disnake.MessageInteraction): + report_fresh = self.cog.db.get_report(report['id']) + if not report_fresh: + return await inter.response.send_message("❌ Ce rapport n'existe plus.", ephemeral=True) + if report_fresh['status'] in ['Traité', 'Classé']: + return await inter.response.send_message("❌ Ce rapport a été traité ou classé et ne peut plus être modifié.", ephemeral=True) + if inter.author.id != report_fresh['author_id']: + return await inter.response.send_message("❌ Seul l'auteur du rapport peut le modifier.", ephemeral=True) + + modal = EditReportModal(self.cog, report_fresh['id'], self) + await inter.response.send_modal(modal) + + async def delete_callback(inter: disnake.MessageInteraction): + report_fresh = self.cog.db.get_report(report['id']) + if not report_fresh: + return await inter.response.send_message("❌ Ce rapport n'existe plus.", ephemeral=True) + if report_fresh['status'] in ['Traité', 'Classé']: + return await inter.response.send_message("❌ Ce rapport a été traité ou classé et ne peut pas être supprimé.", ephemeral=True) + is_auth = (inter.author.id == report_fresh['author_id']) + if not (is_auth or inter.author.guild_permissions.administrator): + return await inter.response.send_message("❌ Vous n'avez pas la permission de supprimer ce rapport.", ephemeral=True) + + self.cog.db.delete_report(report['id']) + del self.reports[self.current_index] + + if not self.reports: + if self.filter_view: + self.filter_view.update_components() + await inter.response.edit_message(content=None, embed=None, components=self.filter_view.get_components_v2()) + else: + await inter.response.edit_message(content="✅ Le rapport a été supprimé. Plus aucun rapport dans cette recherche.", embed=None, view=None) + return + + if self.current_index >= len(self.reports): + self.current_index = len(self.reports) - 1 + self.page_index = self.current_index // 25 + + self.update_components() + await inter.response.edit_message(embed=self.get_embed(), view=self) + await inter.followup.send("✅ Rapport supprimé avec succès.", ephemeral=True) + + async def back_callback(inter: disnake.MessageInteraction): + if self.filter_view: + self.filter_view.update_components() + await inter.response.edit_message(embed=None, components=self.filter_view.get_components_v2()) + else: + new_filter_view = ReportsFilterView(self.cog, self.interaction) + await inter.response.edit_message(embed=None, components=new_filter_view.get_components_v2()) + + edit_btn.callback = edit_callback + delete_btn.callback = delete_callback + back_btn.callback = back_callback + self.add_item(edit_btn) + self.add_item(delete_btn) + self.add_item(back_btn) + + # Row 3: Menu Déroulant "Changer l'état" (Admin uniquement) + if self.is_admin: + status_options = [ + disnake.SelectOption(label="En attente", emoji="🟡", value="En attente", default=(report['status'] == "En attente")), + disnake.SelectOption(label="Traité", emoji="🟢", value="Traité", default=(report['status'] == "Traité")), + disnake.SelectOption(label="Classé", emoji="🔴", value="Classé", default=(report['status'] == "Classé")) + ] + status_menu = disnake.ui.StringSelect( + placeholder="Changer le statut...", + options=status_options, + custom_id="reports_browser_status", + row=3 + ) + + async def status_callback(inter: disnake.MessageInteraction): + if not inter.author.guild_permissions.administrator: + return await inter.response.send_message("❌ Seuls les administrateurs peuvent modifier le statut.", ephemeral=True) + + new_status = inter.values[0] + self.cog.db.update_report_status(report['id'], new_status) + + updated_rep = self.cog.db.get_report(report['id']) + self.reports[self.current_index] = updated_rep + + self.update_components() + await inter.response.edit_message(embed=self.get_embed(), view=self) + await inter.followup.send(f"✅ Statut du rapport #{report['id']} mis à jour vers : **{new_status}**.", ephemeral=True) + + status_menu.callback = status_callback + self.add_item(status_menu) + + def get_embed(self): + if not self.reports: + return disnake.Embed( + title="Aucun rapport disponible", + description="Il n'y a aucun rapport correspondant aux critères.", + color=disnake.Color.red() + ) + + report = self.reports[self.current_index] + + status_colors = { + "En attente": 0xF1C40F, + "Traité": 0x2ECC71, + "Classé": 0x95A5A6 + } + color = status_colors.get(report['status'], 0x3498DB) + + status_emojis = { + "En attente": "🟡 En attente", + "Traité": "🟢 Traité", + "Classé": "🔴 Classé" + } + status_display = status_emojis.get(report['status'], report['status']) + + embed = disnake.Embed( + title=f"Rapport #{report['id']} : {report['title']}", + description=report['content'], + color=color + ) + embed.add_field(name="📁 Objet / Catégorie", value=report['subject'], inline=True) + embed.add_field(name="🚦 Statut", value=status_display, inline=True) + embed.add_field(name="👤 Auteur", value=f"<@{report['author_id']}>", inline=True) + + try: + created_dt = datetime.fromisoformat(report['created_at']) + created_ts = int(created_dt.timestamp()) + embed.add_field(name="📅 Date de création", value=f" ()", inline=False) + except Exception: + embed.add_field(name="📅 Date de création", value=report['created_at'], inline=False) + + if report['last_edited_at']: + try: + edited_dt = datetime.fromisoformat(report['last_edited_at']) + edited_str = edited_dt.strftime("%d/%m/%Y à %H:%M") + embed.set_footer(text=f"Dernière action le {edited_str}") + except Exception: + embed.set_footer(text=f"Dernière action le {report['last_edited_at']}") + else: + embed.set_footer(text="Rapports Hybrides") + + return embed + + async def interaction_check(self, interaction: disnake.MessageInteraction) -> bool: + if interaction.author.id != self.viewer_id: + await interaction.response.send_message("❌ Vous ne pouvez pas interagir avec ce menu.", ephemeral=True) + return False + return True + +class Rapports(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + self.db_path = os.path.join(self.base_dir, "config.db") + self.db = ReportsDB(self.db_path) + self.retention_loop.start() + + def cog_unload(self): + self.retention_loop.cancel() + + @tasks.loop(hours=24) + async def retention_loop(self): + try: + deleted_count = self.db.purge_automatic() + if deleted_count > 0: + kuby_logger.info(f"🧹 [Rétention] Nettoyage automatique effectué : {deleted_count} rapports de plus de 90 jours (non classés) supprimés.") + except Exception as e: + kuby_logger.error(f"❌ [Rétention] Erreur lors du nettoyage automatique: {e}", exc_info=True) + + @retention_loop.before_loop + async def before_retention_loop(self): + await self.bot.wait_until_ready() + + @commands.slash_command(name="config-system", description="Configuration du module de rapports (Admin uniquement)") + @commands.has_permissions(administrator=True) + async def config_system(self, interaction: disnake.ApplicationCommandInteraction, role_viewer: disnake.Role = None): + guild_id = interaction.guild.id + + if role_viewer: + self.db.set_config(guild_id, role_viewer.id) + viewer_text = f"{role_viewer.mention}" + else: + viewer_role_id = self.db.get_config(guild_id) + viewer_text = f"<@&{viewer_role_id}>" if viewer_role_id else "Aucun rôle configuré" + + embed = disnake.Embed( + title="⚙️ Configuration du Système de Rapports", + description="Gérez les autorisations de consultation globale et effectuez la maintenance.", + color=disnake.Color.blue() + ) + embed.add_field( + name="👁️ Rôle Lecteur (Viewer)", + value=f"{viewer_text}\n*(Les membres ayant ce rôle peuvent consulter tous les rapports du serveur)*", + inline=False + ) + embed.add_field( + name="🧹 Maintenance & Rétention", + value="Les rapports vieux de plus de **90 jours** sont automatiquement nettoyés chaque jour, sauf s'ils sont **Classés**.\n\nVous pouvez également lancer une purge manuelle pour supprimer les rapports de plus de **7 jours** non classés.", + inline=False + ) + embed.set_footer(text="Système de Rapports Hybride") + + view = ReportsConfigView(self, guild_id) + await interaction.response.send_message(embed=embed, view=view, ephemeral=True) + + @commands.slash_command(name="rapport", description="Ouvrir le formulaire de saisie de rapport") + async def rapport(self, interaction: disnake.ApplicationCommandInteraction): + await interaction.response.send_modal(ReportModal(self)) + + @commands.slash_command(name="consulter_rapport", description="Consulter les rapports du serveur via une interface interactive") + async def consulter_rapport( + self, + interaction: disnake.ApplicationCommandInteraction, + utilisateur: disnake.Member = None, + categorie: str = None, + statut: str = commands.Param(None, choices=["En attente", "Traité", "Classé"]), + role: disnake.Role = None + ): + filter_view = ReportsFilterView(self, interaction) + if utilisateur: + filter_view.selected_user = utilisateur + if categorie: + filter_view.selected_subject = categorie + if statut: + filter_view.selected_status = statut + if role: + filter_view.selected_role = role + + filter_view.update_components() + await interaction.response.send_message(components=filter_view.get_components_v2(), ephemeral=True) + +def setup(bot): + bot.add_cog(Rapports(bot)) diff --git a/commandes/ticket/__init__.py b/commandes/ticket/__init__.py index def4775..0f632c3 100644 --- a/commandes/ticket/__init__.py +++ b/commandes/ticket/__init__.py @@ -3159,14 +3159,17 @@ class TicketCommands(commands.Cog): return # Accept both ticket_* / catv2_* / close_* custom_ids - if not ( - str(custom_id).startswith("ticket_") + + + if not (str(custom_id).startswith("ticket_") or str(custom_id).startswith("catv2_") or str(custom_id).startswith("cat_") - or str(custom_id) in {"close_confirm", "close_cancel", "delay_cancel"} - ): + or str(custom_id).startswith("reports_filter_") + or str(custom_id) in {"close_confirm", "close_cancel", "delay_cancel"}): return + + if not inter.guild: return diff --git a/data/convocation_backups.json b/data/convocation_backups.json index b1f3572..f2e450d 100644 --- a/data/convocation_backups.json +++ b/data/convocation_backups.json @@ -1,5 +1,13 @@ { "1369669999345537145_1337733859814543410": [ 1370452086193655840 - ] + ], + "1369669999345537145_1508391404831903836": { + "roles": [], + "overrides": { + "1510558364420149310": { + "view_channel": false + } + } + } } \ No newline at end of file From b0535af75caf4893b3692038f9aa6b2977a4ac3d Mon Sep 17 00:00:00 2001 From: Mathis Date: Thu, 18 Jun 2026 17:30:35 +0200 Subject: [PATCH 2/3] =?UTF-8?q?Ajout=20de=20la=20possibilit=C3=A9s=20de=20?= =?UTF-8?q?communiquer=20une=20information=20aux=20partenaires=20Omega=20K?= =?UTF-8?q?ube?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- commandes/rapports.py | 184 +++++++++++++++++++++++++---------- commandes/sendbotmessages.py | 156 +++++++++++++++++++++++++++-- commandes/ticket/__init__.py | 19 ++-- 3 files changed, 287 insertions(+), 72 deletions(-) diff --git a/commandes/rapports.py b/commandes/rapports.py index 8f1bf12..dd549d5 100644 --- a/commandes/rapports.py +++ b/commandes/rapports.py @@ -368,7 +368,7 @@ class ReportsFilterView(disnake.ui.View): val = inter.values[0] self.selected_status = None if val == "all" else val self.update_components() - await inter.response.edit_message(components=self.get_components_v2()) + await inter.response.edit_message(embed=self.get_filter_embed(), view=self) status_select.callback = status_callback self.add_item(status_select) @@ -392,7 +392,7 @@ class ReportsFilterView(disnake.ui.View): val = inter.values[0] self.selected_subject = None if val == "all" else val self.update_components() - await inter.response.edit_message(components=self.get_components_v2()) + await inter.response.edit_message(embed=self.get_filter_embed(), view=self) subject_select.callback = subject_callback self.add_item(subject_select) @@ -417,7 +417,7 @@ class ReportsFilterView(disnake.ui.View): else: self.selected_user = None self.update_components() - await inter.response.edit_message(components=self.get_components_v2()) + await inter.response.edit_message(embed=self.get_filter_embed(), view=self) user_select.callback = user_callback self.add_item(user_select) current_row += 1 @@ -439,7 +439,7 @@ class ReportsFilterView(disnake.ui.View): else: self.selected_role = None self.update_components() - await inter.response.edit_message(components=self.get_components_v2()) + await inter.response.edit_message(embed=self.get_filter_embed(), view=self) role_select.callback = role_callback self.add_item(role_select) current_row += 1 @@ -453,8 +453,6 @@ class ReportsFilterView(disnake.ui.View): row=current_row ) async def search_callback(inter: disnake.MessageInteraction): - await inter.response.defer(ephemeral=True) - author_filter = None if not self.has_global_access: author_filter = self.viewer_id @@ -474,11 +472,11 @@ class ReportsFilterView(disnake.ui.View): ) if not reports: - await inter.followup.send("❌ Aucun rapport ne correspond à votre recherche.", ephemeral=True) + await inter.response.send_message("❌ Aucun rapport ne correspond à votre recherche.", ephemeral=True) return browser = ReportsBrowserView(self.cog, self.interaction, reports, filter_view=self) - await inter.edit_original_message(content=None, embed=browser.get_embed(), view=browser) + await inter.response.edit_message(embed=browser.get_embed(), view=browser) search_btn.callback = search_callback self.add_item(search_btn) @@ -496,49 +494,40 @@ class ReportsFilterView(disnake.ui.View): self.selected_status = None self.selected_subject = None self.update_components() - await inter.response.edit_message(components=self.get_components_v2()) + await inter.response.edit_message(embed=self.get_filter_embed(), view=self) reset_btn.callback = reset_callback self.add_item(reset_btn) - def get_components_v2(self): - sections = [ - disnake.ui.Section( - "# 🔍 Recherche & Filtres de Rapports", - accessory=disnake.ui.Thumbnail(self.interaction.author.display_avatar.url) - ), - disnake.ui.Separator(divider=True) - ] - - filters_desc = [] - if self.selected_status: - filters_desc.append(f"🚦 **Statut** : {self.selected_status}") - else: - filters_desc.append("🚦 **Statut** : Tous") - + def get_filter_embed(self) -> disnake.Embed: + """Génère l'embed résumant les filtres actifs.""" + author = self.interaction.author + lines = [] + lines.append(f"🚦 **Statut** : {self.selected_status or 'Tous'}") if self.has_global_access: if self.selected_user: - mention = self.selected_user.mention if hasattr(self.selected_user, "mention") else f"<@{self.selected_user}>" - filters_desc.append(f"👤 **Auteur** : {mention}") + mention = self.selected_user.mention if hasattr(self.selected_user, 'mention') else f'<@{self.selected_user}>' + lines.append(f"👤 **Auteur** : {mention}") else: - filters_desc.append("👤 **Auteur** : Tous") - + lines.append("👤 **Auteur** : Tous") if self.selected_role: - mention = self.selected_role.mention if hasattr(self.selected_role, "mention") else f"<@&{self.selected_role}>" - filters_desc.append(f"🛡️ **Rôle Auteur** : {mention}") + mention = self.selected_role.mention if hasattr(self.selected_role, 'mention') else f'<@&{self.selected_role}>' + lines.append(f"🛡️ **Rôle Auteur** : {mention}") else: - filters_desc.append("🛡️ **Rôle Auteur** : Tous") + lines.append("🛡️ **Rôle Auteur** : Tous") else: - filters_desc.append(f"👤 **Auteur** : {self.interaction.author.mention} *(Accès restreint à vos propres rapports)*") - - if self.selected_subject: - filters_desc.append(f"📁 **Objet / Catégorie** : `{self.selected_subject}`") - else: - filters_desc.append("📁 **Objet / Catégorie** : Tous") - - sections.append(disnake.ui.TextDisplay(content="\n".join(filters_desc))) - sections.append(disnake.ui.Separator(divider=True)) - - return [disnake.ui.Container(*sections)] + self.children + lines.append(f"👤 **Auteur** : {author.mention} *(accès limité à vos rapports)*") + lines.append(f"📁 **Objet / Catégorie** : {self.selected_subject or 'Tous'}") + embed = disnake.Embed( + title="🔍 Recherche & Filtres de Rapports", + description="\n".join(lines), + color=disnake.Color.blurple() + ) + embed.set_thumbnail(url=author.display_avatar.url) + return embed + + def get_components_v2(self): + """Kept for backward compatibility — returns view children only.""" + return self.children async def interaction_check(self, interaction: disnake.MessageInteraction) -> bool: if interaction.author.id != self.viewer_id: @@ -701,9 +690,14 @@ class ReportsBrowserView(disnake.ui.View): if not self.reports: if self.filter_view: self.filter_view.update_components() - await inter.response.edit_message(content=None, embed=None, components=self.filter_view.get_components_v2()) + await inter.response.edit_message( + embed=self.filter_view.get_filter_embed(), view=self.filter_view + ) else: - await inter.response.edit_message(content="✅ Le rapport a été supprimé. Plus aucun rapport dans cette recherche.", embed=None, view=None) + await inter.response.edit_message( + content="✅ Le rapport a été supprimé. Plus aucun rapport dans cette recherche.", + embed=None, view=None + ) return if self.current_index >= len(self.reports): @@ -717,10 +711,14 @@ class ReportsBrowserView(disnake.ui.View): async def back_callback(inter: disnake.MessageInteraction): if self.filter_view: self.filter_view.update_components() - await inter.response.edit_message(embed=None, components=self.filter_view.get_components_v2()) + await inter.response.edit_message( + embed=self.filter_view.get_filter_embed(), view=self.filter_view + ) else: new_filter_view = ReportsFilterView(self.cog, self.interaction) - await inter.response.edit_message(embed=None, components=new_filter_view.get_components_v2()) + await inter.response.edit_message( + embed=new_filter_view.get_filter_embed(), view=new_filter_view + ) edit_btn.callback = edit_callback delete_btn.callback = delete_callback @@ -732,9 +730,9 @@ class ReportsBrowserView(disnake.ui.View): # Row 3: Menu Déroulant "Changer l'état" (Admin uniquement) if self.is_admin: status_options = [ - disnake.SelectOption(label="En attente", emoji="🟡", value="En attente", default=(report['status'] == "En attente")), - disnake.SelectOption(label="Traité", emoji="🟢", value="Traité", default=(report['status'] == "Traité")), - disnake.SelectOption(label="Classé", emoji="🔴", value="Classé", default=(report['status'] == "Classé")) + disnake.SelectOption(label="En attente", emoji="🟡", value="En attente"), + disnake.SelectOption(label="Traité", emoji="🟢", value="Traité"), + disnake.SelectOption(label="Classé", emoji="🔴", value="Classé") ] status_menu = disnake.ui.StringSelect( placeholder="Changer le statut...", @@ -746,16 +744,81 @@ class ReportsBrowserView(disnake.ui.View): async def status_callback(inter: disnake.MessageInteraction): if not inter.author.guild_permissions.administrator: return await inter.response.send_message("❌ Seuls les administrateurs peuvent modifier le statut.", ephemeral=True) - + + old_status = report['status'] new_status = inter.values[0] + + if old_status == new_status: + return await inter.response.send_message(f"ℹ️ Le rapport est déjà en statut **{new_status}**.", ephemeral=True) + self.cog.db.update_report_status(report['id'], new_status) - updated_rep = self.cog.db.get_report(report['id']) self.reports[self.current_index] = updated_rep - + self.update_components() await inter.response.edit_message(embed=self.get_embed(), view=self) - await inter.followup.send(f"✅ Statut du rapport #{report['id']} mis à jour vers : **{new_status}**.", ephemeral=True) + await inter.followup.send( + f"✅ Statut du rapport **#{report['id']}** mis à jour : **{old_status}** → **{new_status}**.", + ephemeral=True + ) + + # === Notification DM à l'auteur === + status_colors = { + "En attente": disnake.Color.yellow(), + "Traité": disnake.Color.green(), + "Classé": disnake.Color.greyple(), + } + status_emojis = {"En attente": "🟡", "Traité": "🟢", "Classé": "🔴"} + dm_sent = False + try: + author_member = inter.guild.get_member(report['author_id']) + if not author_member: + author_member = await inter.guild.fetch_member(report['author_id']) + + if author_member: + dm_embed = disnake.Embed( + title="📋 Mise à jour de votre rapport", + description=( + f"Le statut de votre rapport **#{report['id']} — {report['title']}** " + f"vient d'être modifié." + ), + color=status_colors.get(new_status, disnake.Color.blurple()), + timestamp=datetime.now(PARIS_TZ) + ) + dm_embed.add_field( + name="Ancien statut", + value=f"{status_emojis.get(old_status, '⚪')} {old_status}", + inline=True + ) + dm_embed.add_field( + name="Nouveau statut", + value=f"{status_emojis.get(new_status, '⚪')} {new_status}", + inline=True + ) + dm_embed.add_field(name="Modifié par", value=inter.author.mention, inline=False) + dm_embed.add_field(name="Serveur", value=inter.guild.name, inline=False) + dm_embed.set_footer(text=f"Rapport #{report['id']} • {inter.guild.name}") + if inter.guild.icon: + dm_embed.set_thumbnail(url=inter.guild.icon.url) + try: + await author_member.send(embed=dm_embed) + dm_sent = True + except disnake.Forbidden: + kuby_logger.info( + f"[Rapports] DM impossible pour {author_member} (DMs fermés) — rapport #{report['id']}" + ) + except Exception as e: + kuby_logger.error(f"[Rapports] Erreur DM notification: {e}", exc_info=True) + except disnake.NotFound: + pass + except Exception as e: + kuby_logger.error(f"[Rapports] Erreur récupération membre pour DM: {e}", exc_info=True) + + dm_note = " • 📬 Auteur notifié par DM." if dm_sent else " • ⚠️ DM non envoyé (DMs fermés ou membre introuvable)." + await inter.followup.send( + f"ℹ️{dm_note}", + ephemeral=True + ) status_menu.callback = status_callback self.add_item(status_menu) @@ -898,7 +961,20 @@ class Rapports(commands.Cog): filter_view.selected_role = role filter_view.update_components() - await interaction.response.send_message(components=filter_view.get_components_v2(), ephemeral=True) + await interaction.response.send_message( + embed=disnake.Embed( + title="🔍 Recherche & Filtres de Rapports", + description=( + f"🚦 **Statut** : {filter_view.selected_status or 'Tous'}\n" + f"📁 **Objet / Catégorie** : {filter_view.selected_subject or 'Tous'}\n" + + (f"👤 **Auteur** : {filter_view.selected_user.mention if hasattr(filter_view.selected_user, 'mention') else 'Tous'}\n" if filter_view.has_global_access else f"👤 **Auteur** : {interaction.author.mention} *(accès limité à vos rapports)*\n") + + (f"🛡️ **Rôle Auteur** : {filter_view.selected_role.mention if hasattr(filter_view.selected_role, 'mention') else 'Tous'}" if filter_view.has_global_access else "") + ), + color=disnake.Color.blurple() + ), + view=filter_view, + ephemeral=True + ) def setup(bot): bot.add_cog(Rapports(bot)) diff --git a/commandes/sendbotmessages.py b/commandes/sendbotmessages.py index e77819e..b2c9f56 100755 --- a/commandes/sendbotmessages.py +++ b/commandes/sendbotmessages.py @@ -1,24 +1,166 @@ import disnake from disnake.ext import commands + +OWNER_ID = 971446412690722826 + + +def is_authorized(interaction: disnake.ApplicationCommandInteraction) -> bool: + return interaction.user.id == OWNER_ID + + +class AnnouncementModal(disnake.ui.Modal): + def __init__(self, cog: "SentBotMessages"): + self.cog = cog + components = [ + disnake.ui.TextInput( + label="Titre principal de l'annonce", + placeholder="Ex: Mise à jour important / Maintenance / Nouveau système", + custom_id="announcement_title", + required=True, + max_length=100, + ), + disnake.ui.TextInput( + label="Message de l'annonce (contenu réel)", + style=disnake.TextInputStyle.paragraph, + placeholder="Écris le vrai contenu de l'annonce ici.", + custom_id="announcement_message", + required=True, + max_length=4000, + ), + disnake.ui.TextInput( + label="Message de prévention / contexte MP", + style=disnake.TextInputStyle.paragraph, + placeholder="Ex: Ce MP vous informe d'une annonce importante.", + custom_id="announcement_prevention", + required=True, + max_length=1000, + ), + ] + super().__init__( + title="Annonce aux propriétaires", + custom_id="announcement_modal", + components=components, + ) + + async def callback(self, interaction: disnake.ModalInteraction): + title = (interaction.text_values.get("announcement_title") or "").strip() + message = (interaction.text_values.get("announcement_message") or "").strip() + prevention = (interaction.text_values.get("announcement_prevention") or "").strip() + + await interaction.response.defer(ephemeral=True) + + preview_container = disnake.ui.Container( + disnake.ui.TextDisplay(content=f"# {title}"), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(content=prevention), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(content=message), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay( + content=( + "📍 **Prévisualisation**\n" + "Le même format sera envoyé aux propriétaires." + ) + ) + ) + + successful = 0 + failed_servers = [] + + for guild in self.cog.bot.guilds: + try: + owner = guild.owner + if owner is None: + owner = await guild.fetch_member(guild.owner_id) + + container = disnake.ui.Container( + disnake.ui.TextDisplay(content=f"# {title}"), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(content=prevention), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(content=message), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay( + content=( + f"📍 **Serveur** : {guild.name}\n" + f"🆔 **ID** : `{guild.id}`" + ) + ) + ) + + await owner.send(components=[container]) + successful += 1 + except Exception as e: + failed_servers.append(f"{guild.name} ({guild.id}) : {e}") + + result_text = ( + f"✅ Annonce envoyée à {successful} propriétaire(s)." + if not failed_servers + else ( + f"✅ Annonce envoyée à {successful} propriétaire(s).\n" + f"❌ Échecs ({len(failed_servers)}):\n" + f"- {'\n- '.join(failed_servers[:10])}" + f"{'\n... et ' + str(len(failed_servers) - 10) + ' autres' if len(failed_servers) > 10 else ''}" + ) + ) + + result_container = disnake.ui.Container( + disnake.ui.TextDisplay(content=result_text), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(content="🧩 Aperçu du format envoyé aux propriétaires :"), + disnake.ui.Separator(divider=True), + *preview_container.children + ) + + await interaction.edit_original_response(components=[result_container]) + + class SentBotMessages(commands.Cog): def __init__(self, bot): self.bot = bot - @commands.slash_command(name="sentbotmessages", description="Le bot envoie un message dans un salon choisi.") - async def sentbotmessages(self, interaction: disnake.ApplicationCommandInteraction, - message: str = commands.Param(description="Le message à envoyer"), - salon: disnake.TextChannel = commands.Param(description="Le salon où envoyer le message")): + @commands.slash_command( + name="sentbotmessages", + description="Le bot envoie un message dans un salon choisi." + ) + async def sentbotmessages( + self, + interaction: disnake.ApplicationCommandInteraction, + message: str = commands.Param(description="Le message à envoyer"), + salon: disnake.TextChannel = commands.Param(description="Le salon où envoyer le message") + ): # Vérifie si l'utilisateur est admin if not interaction.user.guild_permissions.administrator: - await interaction.response.send_message("⛔ Tu n'as pas la permission d'utiliser cette commande.", ephemeral=True) + await interaction.response.send_message( + "⛔ Tu n'as pas la permission d'utiliser cette commande.", + ephemeral=True + ) return try: await salon.send(message) - await interaction.response.send_message(f"✅ Message envoyé dans {salon.mention}.", ephemeral=True) + await interaction.response.send_message( + f"✅ Message envoyé dans {salon.mention}.", + ephemeral=True + ) except Exception as e: - await interaction.response.send_message(f"❌ Une erreur est survenue : {e}", ephemeral=True) + await interaction.response.send_message( + f"❌ Une erreur est survenue : {e}", + ephemeral=True + ) + + @commands.slash_command( + name="annonce_proprietaires", + description="Ouvrir le formulaire d'annonce pour les propriétaires." + ) + @commands.check(is_authorized) + async def annonce_proprietaires( + self, + interaction: disnake.ApplicationCommandInteraction, + ): + await interaction.response.send_modal(AnnouncementModal(self)) + def setup(bot): bot.add_cog(SentBotMessages(bot)) diff --git a/commandes/ticket/__init__.py b/commandes/ticket/__init__.py index 0f632c3..4516f05 100644 --- a/commandes/ticket/__init__.py +++ b/commandes/ticket/__init__.py @@ -3158,13 +3158,11 @@ class TicketCommands(commands.Cog): if not custom_id: return - # Accept both ticket_* / catv2_* / close_* custom_ids - - + # Accept only ticket_* / catv2_* / cat_* / close_* custom_ids + # reports_filter_* are intentionally excluded — handled by ReportsFilterView callbacks if not (str(custom_id).startswith("ticket_") or str(custom_id).startswith("catv2_") or str(custom_id).startswith("cat_") - or str(custom_id).startswith("reports_filter_") or str(custom_id) in {"close_confirm", "close_cancel", "delay_cancel"}): return @@ -3222,6 +3220,7 @@ class TicketCommands(commands.Cog): await inter.response.send_message(f"Erreur interne: {e}", ephemeral=True) # backward compatibility (legacy cat__) + elif custom_id.startswith("cat_"): try: parts = str(custom_id).split("_") @@ -3402,13 +3401,11 @@ class TicketCommands(commands.Cog): await self._send_recruitment_decision(inter, ticket, category, accepted=False) # === Fermeture via composants V2 (sans view=, donc custom_id) === - elif custom_id == "close_cancel": - await inter.response.defer() - # Retire la réponse éphémère du user (si c'est bien une réponse d'interaction) - try: - await inter.delete_original_response() - except Exception: - pass + + elif custom_id.startswith("reports_filter_"): + # No direct response; let the view's callback handle the interaction + return + elif custom_id == "close_confirm": try: From 886bfa4b1e7142f135ebeafb18716840b5d7ecd8 Mon Sep 17 00:00:00 2001 From: Mathis Date: Thu, 18 Jun 2026 17:40:55 +0200 Subject: [PATCH 3/3] Fix multiple bug et optimisation --- commandes/goodbye.py | 55 ++++++++++++++++++++++++++++++++++++-------- commandes/welcome.py | 26 +++++++++++++++++---- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/commandes/goodbye.py b/commandes/goodbye.py index 07406f3..9b23c9b 100644 --- a/commandes/goodbye.py +++ b/commandes/goodbye.py @@ -121,12 +121,45 @@ class Goodbye(commands.Cog): kuby_logger.error(f"[Goodbye] Police introuvable: {font_path}") raise FileNotFoundError(f"Police introuvable: {font_path}") - font_large = ImageFont.truetype(font_large_path, 70) - font_small = ImageFont.truetype(font_small_path, 50) - - # Dessiner uniquement le nom du serveur (comme pour welcome) - display_name = server_name if server_name else "OMEGA KUBE" - draw.text((325, 135), display_name, font=font_large, fill=(255,255,255)) + # Adapter le texte pour éviter les débordements et afficher correctement le nom + line1_text = "AU REVOIR" + line2_text = username.upper() + line3_text = (server_name or "OMEGA KUBE").upper() + + text_area_left = 300 + text_area_right = background.width - 20 + text_area_width = text_area_right - text_area_left + text_area_center_x = text_area_left + text_area_width // 2 + + def fit_font(font_path, text, max_size, max_width): + size = max_size + while size > 10: + font = ImageFont.truetype(font_path, size) + bbox = draw.textbbox((0, 0), text, font=font) + w = bbox[2] - bbox[0] + if w <= max_width: + h = bbox[3] - bbox[1] + return font, w, h + size -= 2 + font = ImageFont.truetype(font_path, 10) + bbox = draw.textbbox((0, 0), text, font=font) + return font, bbox[2] - bbox[0], bbox[3] - bbox[1] + + font1, w1, h1 = fit_font(font_large_path, line1_text, 65, text_area_width) + font2, w2, h2 = fit_font(font_small_path, line2_text, 38, text_area_width) + font3, w3, h3 = fit_font(font_large_path, line3_text, 65, text_area_width) + + spacing_1_2 = 12 + spacing_2_3 = 12 + total_height = h1 + spacing_1_2 + h2 + spacing_2_3 + h3 + start_y = (background.height - total_height) // 2 + + y = start_y + draw.text((text_area_center_x - w1 // 2, y), line1_text, font=font1, fill=(255, 255, 255)) + y += h1 + spacing_1_2 + draw.text((text_area_center_x - w2 // 2, y), line2_text, font=font2, fill=(255, 255, 255)) + y += h2 + spacing_2_3 + draw.text((text_area_center_x - w3 // 2, y), line3_text, font=font3, fill=(255, 255, 255)) # Ajouter l'avatar avatar = Image.open(io.BytesIO(avatar_bytes)).convert("RGBA").resize((224, 224), Image.LANCZOS) @@ -188,9 +221,10 @@ class Goodbye(commands.Cog): if img: embed = disnake.Embed( - description=f"{member.mention} vient de nous quitter.\n\n{message_text}\n\n⌛ **Avait rejoint :** {joined_message}", - color=0x7289DA + description=f"{member.mention} vient de nous quitter.\n\n{message_text}", + color=0x9B59B6 ) + embed.set_footer(text=f"avait rejoint · {joined_message}") embed.set_image(url="attachment://goodbye.png") await channel.send(embed=embed, file=disnake.File(img, filename="goodbye.png")) kuby_logger.info(f"[Goodbye] Message de départ avec image envoyé pour {member.name}") @@ -198,9 +232,10 @@ class Goodbye(commands.Cog): # Fallback : image non générée kuby_logger.warning(f"[Goodbye] Image non générée pour {member.name}, envoi d'un message texte") embed = disnake.Embed( - description=f"{member.mention} vient de nous quitter.\n\n{message_text}\n\n⌛ **Avait rejoint :** {joined_message}", - color=0x7289DA + description=f"{member.mention} vient de nous quitter.\n\n{message_text}", + color=0x9B59B6 ) + embed.set_footer(text=f"avait rejoint · {joined_message}") await channel.send(embed=embed) except Exception as e: diff --git a/commandes/welcome.py b/commandes/welcome.py index 81c9d53..b03db89 100644 --- a/commandes/welcome.py +++ b/commandes/welcome.py @@ -191,11 +191,27 @@ class Welcome(commands.Cog): if welcome_dm_text and not member.bot: kuby_logger.info(f"✉️ [Welcome] Tentative d'envoi de DM à {member.name}") try: - formatted_message = welcome_dm_text.format( - username=member.name, - servername=member.guild.name, - member_count=member.guild.member_count - ) + template_values = { + # Alias populaires + "user": member.mention, + "username": member.name, + "member": member.mention, + "member_name": member.name, + "server": member.guild.name, + "servername": member.guild.name, + "guild": member.guild.name, + "guild_name": member.guild.name, + "member_count": member.guild.member_count, + "count": member.guild.member_count, + "members": member.guild.member_count, + } + + class SafeDict(dict): + def __missing__(self, key): + return "{" + key + "}" + + formatted_message = welcome_dm_text.format_map(SafeDict(template_values)) + # On combine le texte et le GIF dans un Embed élégant pour masquer le lien brut gif_url = "https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExNXV1NW02NTk0bGF2ZHcyMGFna2F6amRrbGpobGpsMHowOW5xa2E5eCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ZtFuW4rsUbfrRzPauJ/giphy.gif" embed = disnake.Embed(