Kuby/commandes/rapports.py

984 lines
42 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"<t:{created_ts}:F> (<t:{created_ts}:R>)", 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))