2026-05-06 17:07:09 +02:00
import disnake
from disnake . ext import commands
2026-02-07 16:03:16 +01:00
from utils . gitlab_client import gitlab_client
2026-03-14 17:05:20 +01:00
from aiohttp import web
2026-02-07 16:03:16 +01:00
import logging
import json
import os
import asyncio
kuby_logger = logging . getLogger ( " KubyBot " )
2026-03-29 19:29:17 +02:00
BASE_DIR = os . path . dirname ( os . path . dirname ( os . path . abspath ( __file__ ) ) )
2026-02-07 16:03:16 +01:00
2026-06-04 08:40:14 +02:00
# ⚡ NOUVEAU: Utilise le même fichier que gitlab_integration.py
TRACKING_FILE = os . path . join ( BASE_DIR , " commandes " , " gitlab_issues.json " )
LEGACY_REPORTS_FILE = os . path . join ( BASE_DIR , " data " , " gitlab_reports.json " )
2026-05-19 18:50:16 +02:00
2026-06-04 08:40:14 +02:00
def load_tracking ( ) :
""" Charge le tracking depuis gitlab_issues.json (avec migration automatique) """
tracking = { }
if os . path . exists ( TRACKING_FILE ) :
try :
with open ( TRACKING_FILE , " r " , encoding = " utf-8 " ) as f :
tracking = json . load ( f )
except Exception as e :
kuby_logger . error ( f " Erreur chargement gitlab_issues.json: { e } " )
# Migration automatique depuis l'ancien fichier
if os . path . exists ( LEGACY_REPORTS_FILE ) :
try :
with open ( LEGACY_REPORTS_FILE , " r " , encoding = " utf-8 " ) as f :
legacy_data = json . load ( f )
for issue_iid , user_id in legacy_data . items ( ) :
if issue_iid not in tracking :
tracking [ issue_iid ] = {
" requester_id " : user_id ,
" last_note_id " : 0 ,
" type " : " legacy_report "
}
kuby_logger . info ( f " Migration automatique: { len ( legacy_data ) } issues fusionnées " )
except Exception as e :
kuby_logger . error ( f " Erreur migration gitlab_reports.json: { e } " )
return tracking
2026-05-19 18:50:16 +02:00
2026-06-04 08:40:14 +02:00
def save_tracking ( data : Dict [ str , Any ] ) - > None :
""" Sauvegarde dans gitlab_issues.json """
os . makedirs ( os . path . dirname ( TRACKING_FILE ) , exist_ok = True )
temp_file = f " { TRACKING_FILE } .tmp "
with open ( temp_file , " w " , encoding = " utf-8 " ) as f :
json . dump ( data , f , indent = 2 )
os . replace ( temp_file , TRACKING_FILE )
2026-02-07 16:03:16 +01:00
2026-05-19 18:50:16 +02:00
2026-05-06 17:07:09 +02:00
class BugReportModal ( disnake . ui . Modal ) :
2026-06-04 08:40:14 +02:00
def __init__ ( self , cog ) :
self . cog = cog # ⚡ NOUVEAU: Reçoit le cog pour tracking
components = [
disnake . ui . TextInput (
label = " Titre du bug " ,
placeholder = " Décrivez brièvement le problème... " ,
custom_id = " bug_title " ,
required = True ,
max_length = 100 ,
) ,
disnake . ui . TextInput (
label = " Description détaillée " ,
style = disnake . TextInputStyle . paragraph ,
placeholder = " Que s ' est-il passé ? " ,
custom_id = " bug_description " ,
required = True ,
max_length = 1000 ,
) ,
]
2026-05-19 20:05:23 +02:00
super ( ) . __init__ (
title = " Signaler un Bug " ,
2026-06-04 08:40:14 +02:00
custom_id = " bug_modal " ,
components = components ,
2026-05-19 20:05:23 +02:00
)
async def callback ( self , interaction : disnake . ModalInteraction ) :
2026-02-07 16:03:16 +01:00
await interaction . response . send_message ( " Envoi de votre rapport de bug à GitLab... " , ephemeral = True )
2026-05-19 18:50:16 +02:00
2026-05-19 20:05:23 +02:00
bug_title = ( interaction . text_values . get ( " bug_title " ) or " " ) . strip ( )
2026-05-19 18:50:16 +02:00
if not bug_title :
bug_title = " Sans titre "
2026-05-23 15:11:47 +02:00
priority_name = " Normale "
priority_value = " Normal "
2026-05-19 20:05:23 +02:00
description_value = interaction . text_values . get ( " bug_description " ) or " "
2026-05-19 18:50:16 +02:00
2026-02-07 16:03:16 +01:00
description_text = f " **Rapporté par:** { interaction . user } ( { interaction . user . id } ) \n "
2026-05-19 18:50:16 +02:00
description_text + = f " **Priorité:** { priority_name } \n \n "
description_text + = f " **Description:** \n { description_value } "
labels = [ " bug " , " manual-report " , f " Priority:: { priority_value } " ]
2026-02-07 16:03:16 +01:00
result = await gitlab_client . create_issue (
2026-05-19 18:50:16 +02:00
title = f " [MANUAL] { bug_title } " ,
2026-02-07 16:03:16 +01:00
description = description_text ,
2026-05-19 18:50:16 +02:00
labels = labels ,
2026-02-07 16:03:16 +01:00
)
2026-05-19 18:50:16 +02:00
2026-02-07 16:03:16 +01:00
if result :
2026-06-04 08:40:14 +02:00
issue_iid = str ( result . get ( " iid " ) )
# ⚡ NOUVEAU: Utilise le tracking unifié
self . cog . issue_data [ issue_iid ] = {
" requester_id " : interaction . user . id ,
" last_note_id " : 0 ,
" type " : " manual-report "
}
save_tracking ( self . cog . issue_data )
2026-05-19 18:50:16 +02:00
await interaction . edit_original_response (
content = (
" ✅ Votre bug a été signalé avec succès ! "
2026-05-19 21:19:43 +02:00
" Nous avons bien été averti et le bug sera réglé dans la prochaine mise à jour. "
2026-05-19 18:50:16 +02:00
f " [Voir l ' issue]( { result . get ( ' web_url ' ) } ) "
)
)
2026-02-07 16:03:16 +01:00
else :
2026-05-19 18:50:16 +02:00
await interaction . edit_original_response (
content = " ❌ Une erreur est survenue lors de l ' envoi du rapport à GitLab. Veuillez contacter un administrateur. "
)
2026-02-07 16:03:16 +01:00
2026-05-06 17:07:09 +02:00
class FeatureSuggestionModal ( disnake . ui . Modal ) :
2026-06-04 08:40:14 +02:00
def __init__ ( self , cog ) :
self . cog = cog # ⚡ NOUVEAU: Reçoit le cog pour tracking
components = [
disnake . ui . TextInput (
label = " Titre de la suggestion " ,
placeholder = " Que voulez-vous ajouter ? " ,
custom_id = " suggestion_title " ,
required = True ,
max_length = 100 ,
) ,
disnake . ui . TextInput (
label = " Détails de la fonctionnalité " ,
style = disnake . TextInputStyle . paragraph ,
placeholder = " Décrivez comment cela devrait fonctionner... " ,
custom_id = " suggestion_description " ,
required = True ,
max_length = 1000 ,
) ,
]
2026-05-19 20:05:23 +02:00
super ( ) . __init__ (
title = " Suggérer une fonctionnalité " ,
2026-06-04 08:40:14 +02:00
custom_id = " suggestion_modal " ,
components = components ,
2026-05-19 20:05:23 +02:00
)
async def callback ( self , interaction : disnake . ModalInteraction ) :
2026-02-08 14:42:40 +01:00
await interaction . response . send_message ( " Envoi de votre suggestion à GitLab... " , ephemeral = True )
2026-05-19 18:50:16 +02:00
2026-05-19 20:05:23 +02:00
suggestion_title = ( interaction . text_values . get ( " suggestion_title " ) or " " ) . strip ( ) or " Sans titre "
description_value = interaction . text_values . get ( " suggestion_description " ) or " "
2026-05-19 18:50:16 +02:00
2026-05-23 15:13:56 +02:00
description_text = f " **Sugéré par:** { interaction . user } ( { interaction . user . id } ) \n \n "
2026-05-19 18:50:16 +02:00
description_text + = f " **Description:** \n { description_value } "
2026-02-08 14:42:40 +01:00
labels = [ " enhancement " , " manual-suggestion " ]
2026-05-19 18:50:16 +02:00
2026-02-08 14:42:40 +01:00
result = await gitlab_client . create_issue (
2026-05-19 18:50:16 +02:00
title = f " [SUGGESTION] { suggestion_title } " ,
2026-02-08 14:42:40 +01:00
description = description_text ,
2026-05-19 18:50:16 +02:00
labels = labels ,
2026-02-08 14:42:40 +01:00
)
2026-05-19 18:50:16 +02:00
2026-02-08 14:42:40 +01:00
if result :
2026-06-04 08:40:14 +02:00
issue_iid = str ( result . get ( " iid " ) )
# ⚡ NOUVEAU: Utilise le tracking unifié
self . cog . issue_data [ issue_iid ] = {
" requester_id " : interaction . user . id ,
" last_note_id " : 0 ,
" type " : " manual-suggestion "
}
save_tracking ( self . cog . issue_data )
2026-05-19 18:50:16 +02:00
await interaction . edit_original_response (
content = (
" ✅ Votre suggestion a été envoyée avec succès ! "
" Merci de contribuer à l ' amélioration du bot. "
f " [Voir l ' issue]( { result . get ( ' web_url ' ) } ) "
)
)
2026-02-08 14:42:40 +01:00
else :
2026-05-19 18:50:16 +02:00
await interaction . edit_original_response (
content = " ❌ Une erreur est survenue lors de l ' envoi de la suggestion à GitLab. "
)
2026-02-08 14:42:40 +01:00
2026-06-04 08:40:14 +02:00
class BugReportView ( disnake . ui . View ) :
def __init__ ( self , cog ) :
self . cog = cog # ⚡ NOUVEAU: Reçoit le cog
super ( ) . __init__ ( timeout = None )
@disnake.ui.button ( label = " 🎫 Remplir le formulaire " , style = disnake . ButtonStyle . primary , custom_id = " open_bug_form " )
async def open_bug_form ( self , button : disnake . ui . Button , interaction : disnake . MessageInteraction ) :
modal = BugReportModal ( self . cog )
await interaction . response . send_modal ( modal )
2026-05-23 15:13:56 +02:00
class FeatureSuggestionView ( disnake . ui . View ) :
2026-06-04 08:40:14 +02:00
def __init__ ( self , cog ) :
self . cog = cog # ⚡ NOUVEAU: Reçoit le cog
2026-05-23 15:13:56 +02:00
super ( ) . __init__ ( timeout = None )
@disnake.ui.button ( label = " 🎫 Remplir le formulaire " , style = disnake . ButtonStyle . green , custom_id = " open_suggestion_form " )
async def open_suggestion_form ( self , button : disnake . ui . Button , interaction : disnake . MessageInteraction ) :
2026-06-04 08:40:14 +02:00
modal = FeatureSuggestionModal ( self . cog )
2026-05-23 15:13:56 +02:00
await interaction . response . send_modal ( modal )
2026-02-07 16:03:16 +01:00
class BugReport ( commands . Cog ) :
def __init__ ( self , bot ) :
self . bot = bot
2026-06-04 08:40:14 +02:00
self . issue_data = load_tracking ( ) # ⚡ NOUVEAU: Charge le tracking unifié
2026-05-20 15:49:40 +02:00
async def cog_load ( self ) :
2026-06-03 16:10:36 +02:00
kuby_logger . info ( " BugReport cog chargé. Démarrage du serveur de relais interne (port 5001)... " )
2026-06-04 08:40:14 +02:00
REPORTS_DIR = os . path . dirname ( LEGACY_REPORTS_FILE )
2026-05-25 19:08:45 +02:00
os . makedirs ( REPORTS_DIR , exist_ok = True )
2026-06-03 16:10:36 +02:00
await self . _start_webhook_server ( )
2026-05-20 15:23:09 +02:00
async def _start_webhook_server ( self ) :
2026-06-03 16:10:36 +02:00
try :
app = web . Application ( )
app . router . add_post ( ' /bot_event ' , self . handle_bot_event )
self . runner = web . AppRunner ( app )
await self . runner . setup ( )
self . site = web . TCPSite ( self . runner , ' 127.0.0.1 ' , 5001 )
await self . site . start ( )
kuby_logger . info ( " Internal Bot Webhook Server started on 127.0.0.1:5001 " )
except Exception as e :
kuby_logger . error ( f " Failed to start Internal Bot Webhook Server: { e } " )
2026-02-07 16:03:16 +01:00
2026-03-14 17:05:20 +01:00
def cog_unload ( self ) :
if hasattr ( self , ' runner ' ) :
2026-03-28 21:58:44 +01:00
async def cleanup ( ) :
try :
if hasattr ( self , ' site ' ) :
await self . site . stop ( )
await self . runner . cleanup ( )
kuby_logger . info ( " Internal Bot Webhook Server stopped and cleaned up. " )
except Exception as e :
kuby_logger . error ( f " Error during webhook server cleanup: { e } " )
self . bot . loop . create_task ( cleanup ( ) )
2026-02-07 16:03:16 +01:00
2026-03-14 17:05:20 +01:00
async def handle_bot_event ( self , request ) :
2026-06-04 08:40:14 +02:00
""" Gère les webhooks GitLab (notifications instantanées) """
2026-02-07 16:03:16 +01:00
try :
2026-03-29 19:29:17 +02:00
if not self . bot . is_ready ( ) :
kuby_logger . info ( " Webhook received but bot is not ready yet. Waiting up to 10s... " )
try :
await asyncio . wait_for ( self . bot . wait_until_ready ( ) , timeout = 10.0 )
except asyncio . TimeoutError :
kuby_logger . warning ( " Bot still not ready after 10s. Proceeding anyway (best effort). " )
2026-03-14 17:05:20 +01:00
payload = await request . json ( )
event_type = payload . get ( " object_kind " )
2026-06-04 08:40:14 +02:00
2026-03-14 17:05:20 +01:00
if event_type not in [ " issue " , " note " ] :
return web . json_response ( { " status " : " ignored " } , status = 200 )
2026-06-04 08:40:14 +02:00
# Récupère l'IID de l'issue
issue_iid = str (
payload . get ( " object_attributes " , { } ) . get ( " iid " ) or
payload . get ( " issue " , { } ) . get ( " iid " )
)
2026-03-14 17:05:20 +01:00
if not issue_iid :
return web . json_response ( { " status " : " no issue id " } , status = 200 )
2026-06-04 08:40:14 +02:00
# Vérifie si l'issue est trackée
issue_data = self . issue_data . get ( issue_iid )
if not issue_data :
2026-03-14 17:05:20 +01:00
return web . json_response ( { " status " : " untracked issue " } , status = 200 )
2026-06-04 08:40:14 +02:00
user_id = issue_data . get ( " requester_id " )
if not user_id :
return web . json_response ( { " status " : " no requester " } , status = 200 )
2026-03-14 17:05:20 +01:00
user = self . bot . get_user ( user_id )
if not user :
try :
user = await self . bot . fetch_user ( user_id )
except :
2026-06-04 08:40:14 +02:00
return web . json_response ( { " status " : " user not found " } , status = 200 )
2026-05-19 21:19:43 +02:00
2026-03-14 17:05:20 +01:00
app_info = await self . bot . application_info ( )
dev = app_info . owner
issue_title = payload . get ( " object_attributes " , { } ) . get ( " title " , f " # { issue_iid } " )
2026-06-04 08:40:14 +02:00
2026-03-14 17:05:20 +01:00
if event_type == " note " :
2026-06-04 08:40:14 +02:00
# ⚡ NOUVEAU: Notifie les commentaires via le webhook (instantané)
note_attr = payload . get ( " object_attributes " , { } )
is_system = note_attr . get ( " system " , False )
if not is_system :
note_body = (
note_attr . get ( " note " ) or
note_attr . get ( " body " ) or
payload . get ( " note " , " " ) or
" "
)
author_name = payload . get ( " user " , { } ) . get ( " name " , " Développeur " )
if note_body :
try :
embed = disnake . Embed (
title = " 💬 Nouveau commentaire sur votre signalement " ,
description = f " Le développeur a répondu à votre rapport ** { issue_title } ** : \n \n >>> { note_body } \n \n ✍️ **Par :** { author_name } " ,
color = disnake . Color . orange ( )
)
embed . set_thumbnail ( url = self . bot . user . display_avatar . url )
await user . send ( embed = embed )
kuby_logger . info ( f " Commentaire GitLab envoyé à { user } pour l ' issue # { issue_iid } " )
except ( disnake . Forbidden , disnake . HTTPException ) as e :
kuby_logger . warning ( f " Impossible d ' envoyer le DM à { user } : { e } " )
if dev and dev . id != user . id :
try :
await dev . send ( f " ⚠️ [DM BLOQUÉ] Impossible d ' envoyer le commentaire à { user } pour l ' issue # { issue_iid } " )
except :
pass
elif event_type == " issue " :
# Notifie les changements d'état/label
2026-03-14 17:05:20 +01:00
action = payload . get ( " object_attributes " , { } ) . get ( " action " )
changes = payload . get ( " changes " , { } )
2026-06-04 08:40:14 +02:00
state = payload . get ( " object_attributes " , { } ) . get ( " state " )
2026-03-14 17:05:20 +01:00
if " labels " in changes :
curr_labels = [ l . get ( " title " ) for l in changes [ " labels " ] . get ( " current " , [ ] ) if l . get ( " title " ) ]
2026-06-04 08:40:14 +02:00
display_labels = [ l for l in curr_labels if not l . startswith ( " Priority:: " ) and l not in [ " bug " , " manual-report " , " manual-suggestion " ] ]
current_labels_str = " , " . join ( display_labels ) if display_labels else " Mis à jour "
2026-03-14 17:05:20 +01:00
try :
2026-05-20 15:49:40 +02:00
embed = disnake . Embed (
title = " 🛠️ Mise à jour de votre signalement ! " ,
2026-06-04 08:40:14 +02:00
description = f " Le statut de votre rapport ** { issue_title } ** a été mis à jour par l ' équipe. \n \n 📌 **Nouveaux Labels / Statuts :** { current_labels_str } " ,
2026-05-20 15:49:40 +02:00
color = disnake . Color . blue ( )
)
embed . set_thumbnail ( url = self . bot . user . display_avatar . url )
2026-06-04 08:40:14 +02:00
await user . send ( embed = embed )
2026-03-14 17:05:20 +01:00
except Exception as e :
2026-06-04 08:40:14 +02:00
kuby_logger . error ( f " Erreur notification label: { e } " )
2026-05-01 16:16:33 +02:00
is_closing_now = action == " close " or ( state == " closed " and " state_id " in changes )
if is_closing_now :
2026-03-14 17:05:20 +01:00
try :
2026-05-20 15:49:40 +02:00
embed = disnake . Embed (
title = " 🛠️ Bug / Suggestion Terminé(e) ! " ,
2026-06-04 08:40:14 +02:00
description = f " Bonne nouvelle ! Votre signalement ** { issue_title } ** a été marqué comme terminé ou résolu. \n \n 🚀 **Prochaine étape :** La mise à jour arrive prochainement ! \n ✨ Merci pour votre aide ! " ,
2026-05-20 15:49:40 +02:00
color = disnake . Color . green ( )
)
embed . set_thumbnail ( url = self . bot . user . display_avatar . url )
2026-06-04 08:40:14 +02:00
await user . send ( embed = embed )
2026-03-14 17:05:20 +01:00
except Exception as e :
2026-06-04 08:40:14 +02:00
kuby_logger . error ( f " Erreur notification clôture: { e } " )
2026-03-14 17:05:20 +01:00
return web . json_response ( { " status " : " success " } , status = 200 )
2026-02-07 16:03:16 +01:00
except Exception as e :
2026-03-14 17:05:20 +01:00
kuby_logger . error ( f " Error handling bot event: { e } " )
return web . json_response ( { " status " : " error " , " message " : str ( e ) } , status = 500 )
2026-02-07 16:03:16 +01:00
2026-06-04 08:40:14 +02:00
@commands.slash_command ( name = " signaler_bug " , description = " Signaler un bug aux développeurs " )
2026-05-23 15:11:47 +02:00
async def signifier_bug ( self , interaction : disnake . ApplicationCommandInteraction ) :
2026-06-04 08:40:14 +02:00
modal = BugReportModal ( self )
2026-05-23 15:17:35 +02:00
await interaction . response . send_modal ( modal )
2026-02-07 16:03:16 +01:00
2026-06-04 08:40:14 +02:00
@commands.slash_command ( name = " suggerer_fonctionnalite " , description = " Proposer une nouvelle fonctionnalité " )
2026-05-06 17:07:09 +02:00
async def suggerer_fonctionnalite ( self , interaction : disnake . ApplicationCommandInteraction ) :
2026-06-04 08:40:14 +02:00
modal = FeatureSuggestionModal ( self )
2026-05-23 15:17:35 +02:00
await interaction . response . send_modal ( modal )
2026-02-08 14:42:40 +01:00
2026-05-19 18:50:16 +02:00
2026-05-09 18:42:42 +02:00
def setup ( bot ) :
2026-06-04 08:40:14 +02:00
bot . add_cog ( BugReport ( bot ) )