905 lines
38 KiB
Python
905 lines
38 KiB
Python
|
|
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"<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)")
|
||
|
|
@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))
|