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 from utils.premium import check_premium_tier 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(embed=self.get_filter_embed(), view=self) 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(embed=self.get_filter_embed(), view=self) 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(embed=self.get_filter_embed(), view=self) 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(embed=self.get_filter_embed(), view=self) 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): 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.response.send_message("❌ Aucun rapport ne correspond à votre recherche.", ephemeral=True) return browser = ReportsBrowserView(self.cog, self.interaction, reports, filter_view=self) await inter.response.edit_message(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(embed=self.get_filter_embed(), view=self) reset_btn.callback = reset_callback self.add_item(reset_btn) 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}>' lines.append(f"👤 **Auteur** : {mention}") else: lines.append("👤 **Auteur** : Tous") if self.selected_role: mention = self.selected_role.mention if hasattr(self.selected_role, 'mention') else f'<@&{self.selected_role}>' lines.append(f"🛡️ **Rôle Auteur** : {mention}") else: lines.append("🛡️ **Rôle Auteur** : Tous") else: 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: 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( 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 ) 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=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=new_filter_view.get_filter_embed(), view=new_filter_view ) 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"), disnake.SelectOption(label="Traité", emoji="🟢", value="Traité"), disnake.SelectOption(label="Classé", emoji="🔴", value="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) 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 : **{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) 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)") @check_premium_tier() @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") @check_premium_tier() 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") @check_premium_tier() 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( 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))