2026-06-14 22:36:21 +02:00
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
2026-07-06 16:08:14 +02:00
from utils . premium import check_premium_tier
2026-06-14 22:36:21 +02:00
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 ( )
2026-06-18 17:30:35 +02:00
await inter . response . edit_message ( embed = self . get_filter_embed ( ) , view = self )
2026-06-14 22:36:21 +02:00
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 ( )
2026-06-18 17:30:35 +02:00
await inter . response . edit_message ( embed = self . get_filter_embed ( ) , view = self )
2026-06-14 22:36:21 +02:00
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 ( )
2026-06-18 17:30:35 +02:00
await inter . response . edit_message ( embed = self . get_filter_embed ( ) , view = self )
2026-06-14 22:36:21 +02:00
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 ( )
2026-06-18 17:30:35 +02:00
await inter . response . edit_message ( embed = self . get_filter_embed ( ) , view = self )
2026-06-14 22:36:21 +02:00
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 :
2026-06-18 17:30:35 +02:00
await inter . response . send_message ( " ❌ Aucun rapport ne correspond à votre recherche. " , ephemeral = True )
2026-06-14 22:36:21 +02:00
return
browser = ReportsBrowserView ( self . cog , self . interaction , reports , filter_view = self )
2026-06-18 17:30:35 +02:00
await inter . response . edit_message ( embed = browser . get_embed ( ) , view = browser )
2026-06-14 22:36:21 +02:00
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 ( )
2026-06-18 17:30:35 +02:00
await inter . response . edit_message ( embed = self . get_filter_embed ( ) , view = self )
2026-06-14 22:36:21 +02:00
reset_btn . callback = reset_callback
self . add_item ( reset_btn )
2026-06-18 17:30:35 +02:00
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 ' } " )
2026-06-14 22:36:21 +02:00
if self . has_global_access :
if self . selected_user :
2026-06-18 17:30:35 +02:00
mention = self . selected_user . mention if hasattr ( self . selected_user , ' mention ' ) else f ' <@ { self . selected_user } > '
lines . append ( f " 👤 **Auteur** : { mention } " )
2026-06-14 22:36:21 +02:00
else :
2026-06-18 17:30:35 +02:00
lines . append ( " 👤 **Auteur** : Tous " )
2026-06-14 22:36:21 +02:00
if self . selected_role :
2026-06-18 17:30:35 +02:00
mention = self . selected_role . mention if hasattr ( self . selected_role , ' mention ' ) else f ' <@& { self . selected_role } > '
lines . append ( f " 🛡️ **Rôle Auteur** : { mention } " )
2026-06-14 22:36:21 +02:00
else :
2026-06-18 17:30:35 +02:00
lines . append ( " 🛡️ **Rôle Auteur** : Tous " )
2026-06-14 22:36:21 +02:00
else :
2026-06-18 17:30:35 +02:00
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
2026-06-14 22:36:21 +02:00
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 ( )
2026-06-18 17:30:35 +02:00
await inter . response . edit_message (
embed = self . filter_view . get_filter_embed ( ) , view = self . filter_view
)
2026-06-14 22:36:21 +02:00
else :
2026-06-18 17:30:35 +02:00
await inter . response . edit_message (
content = " ✅ Le rapport a été supprimé. Plus aucun rapport dans cette recherche. " ,
embed = None , view = None
)
2026-06-14 22:36:21 +02:00
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 ( )
2026-06-18 17:30:35 +02:00
await inter . response . edit_message (
embed = self . filter_view . get_filter_embed ( ) , view = self . filter_view
)
2026-06-14 22:36:21 +02:00
else :
new_filter_view = ReportsFilterView ( self . cog , self . interaction )
2026-06-18 17:30:35 +02:00
await inter . response . edit_message (
embed = new_filter_view . get_filter_embed ( ) , view = new_filter_view
)
2026-06-14 22:36:21 +02:00
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 = [
2026-06-18 17:30:35 +02:00
disnake . SelectOption ( label = " En attente " , emoji = " 🟡 " , value = " En attente " ) ,
disnake . SelectOption ( label = " Traité " , emoji = " 🟢 " , value = " Traité " ) ,
disnake . SelectOption ( label = " Classé " , emoji = " 🔴 " , value = " Classé " )
2026-06-14 22:36:21 +02:00
]
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 )
2026-06-18 17:30:35 +02:00
old_status = report [ ' status ' ]
2026-06-14 22:36:21 +02:00
new_status = inter . values [ 0 ]
2026-06-18 17:30:35 +02:00
if old_status == new_status :
return await inter . response . send_message ( f " ℹ ️ Le rapport est déjà en statut **{ new_status } **. " , ephemeral = True )
2026-06-14 22:36:21 +02:00
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
2026-06-18 17:30:35 +02:00
2026-06-14 22:36:21 +02:00
self . update_components ( )
await inter . response . edit_message ( embed = self . get_embed ( ) , view = self )
2026-06-18 17:30:35 +02:00
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
)
2026-06-14 22:36:21 +02:00
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) " )
2026-07-06 16:08:14 +02:00
@check_premium_tier ( )
2026-06-14 22:36:21 +02:00
@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 \n Vous 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 " )
2026-07-06 16:08:14 +02:00
@check_premium_tier ( )
2026-06-14 22:36:21 +02:00
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 " )
2026-07-06 16:08:14 +02:00
@check_premium_tier ( )
2026-06-14 22:36:21 +02:00
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 ( )
2026-06-18 17:30:35 +02:00
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
)
2026-06-14 22:36:21 +02:00
def setup ( bot ) :
bot . add_cog ( Rapports ( bot ) )