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
# Utilisation de chemins absolus pour la persistance après reboot
BASE_DIR = os . path . dirname ( os . path . dirname ( os . path . abspath ( __file__ ) ) )
REPORTS_FILE = os . path . join ( BASE_DIR , " data " , " gitlab_reports.json " )
2026-02-07 16:03:16 +01:00
def save_report ( issue_iid , user_id ) :
2026-03-29 19:29:17 +02:00
os . makedirs ( os . path . dirname ( REPORTS_FILE ) , exist_ok = True )
2026-02-07 16:03:16 +01:00
try :
2026-05-01 16:16:33 +02:00
data = { }
2026-02-07 16:03:16 +01:00
if os . path . exists ( REPORTS_FILE ) :
2026-05-01 16:16:33 +02:00
try :
with open ( REPORTS_FILE , " r " , encoding = " utf-8 " ) as f :
data = json . load ( f )
except json . JSONDecodeError :
kuby_logger . warning ( " REPORTS_FILE corrupted, re-initializing to avoid tracking loss. " )
2026-02-07 16:03:16 +01:00
data [ str ( issue_iid ) ] = user_id
2026-05-01 16:16:33 +02:00
temp_file = f " { REPORTS_FILE } .tmp "
with open ( temp_file , " w " , encoding = " utf-8 " ) as f :
2026-02-07 16:03:16 +01:00
json . dump ( data , f , indent = 4 )
2026-05-01 16:16:33 +02:00
os . replace ( temp_file , REPORTS_FILE )
2026-02-07 16:03:16 +01:00
except Exception as e :
kuby_logger . error ( f " Error saving report to JSON: { e } " )
2026-05-06 17:07:09 +02:00
class BugReportModal ( disnake . ui . Modal ) :
2026-02-07 16:03:16 +01:00
def __init__ ( self , priority_choice ) :
2026-05-09 18:42:42 +02:00
super ( ) . __init__ ( title = " Signaler un Bug " , components = [ self . bug_title , self . description ] )
2026-02-07 16:03:16 +01:00
self . priority_choice = priority_choice
2026-05-06 17:07:09 +02:00
bug_title = disnake . ui . TextInput (
2026-02-07 16:03:16 +01:00
label = " Titre du bug " ,
placeholder = " Décrivez brièvement le problème... " ,
required = True ,
max_length = 100
)
2026-05-06 17:07:09 +02:00
description = disnake . ui . TextInput (
2026-02-07 16:03:16 +01:00
label = " Description détaillée " ,
2026-05-09 18:42:42 +02:00
style = disnake . TextInputStyle . paragraph ,
2026-02-07 16:03:16 +01:00
placeholder = " Que s ' est-il passé ? Comment reproduire le bug ? " ,
required = True ,
max_length = 1000
)
2026-05-06 17:07:09 +02:00
async def callback ( self , interaction : disnake . Interaction ) :
2026-02-07 16:03:16 +01:00
await interaction . response . send_message ( " Envoi de votre rapport de bug à GitLab... " , ephemeral = True )
description_text = f " **Rapporté par:** { interaction . user } ( { interaction . user . id } ) \n "
description_text + = f " **Priorité:** { self . priority_choice . name } \n \n "
description_text + = f " **Description:** \n { self . description . value } "
# Mapping des priorités vers des labels GitLab stylisés (Scoped Labels)
labels = [ " bug " , " manual-report " , f " Priority:: { self . priority_choice . value } " ]
result = await gitlab_client . create_issue (
title = f " [MANUAL] { self . bug_title . value } " ,
description = description_text ,
labels = labels
)
if result :
issue_iid = result . get ( " iid " )
save_report ( issue_iid , interaction . user . id )
await interaction . edit_original_response ( content = f " ✅ Votre bug a été signalé avec succès ! Gameur a bien été averti et le bug sera réglé dans la prochaine mise à jour. [Voir l ' issue]( { result . get ( ' web_url ' ) } ) " )
else :
await interaction . edit_original_response ( content = " ❌ Une erreur est survenue lors de l ' envoi du rapport à GitLab. Veuillez contacter un administrateur. " )
2026-05-06 17:07:09 +02:00
class FeatureSuggestionModal ( disnake . ui . Modal ) :
def __init__ ( self ) :
2026-05-09 18:42:42 +02:00
super ( ) . __init__ ( title = " Suggérer une fonctionnalité " , components = [ self . suggestion_title , self . description ] )
2026-05-06 17:07:09 +02:00
suggestion_title = disnake . ui . TextInput (
2026-02-08 14:42:40 +01:00
label = " Titre de la suggestion " ,
placeholder = " Que voulez-vous ajouter ? " ,
required = True ,
max_length = 100
)
2026-05-06 17:07:09 +02:00
description = disnake . ui . TextInput (
2026-02-08 14:42:40 +01:00
label = " Détails de la fonctionnalité " ,
2026-05-09 18:42:42 +02:00
style = disnake . TextInputStyle . paragraph ,
2026-02-08 14:42:40 +01:00
placeholder = " Décrivez comment cela devrait fonctionner... " ,
required = True ,
max_length = 1000
)
2026-05-06 17:07:09 +02:00
async def callback ( self , interaction : disnake . Interaction ) :
2026-02-08 14:42:40 +01:00
await interaction . response . send_message ( " Envoi de votre suggestion à GitLab... " , ephemeral = True )
description_text = f " **Suggéré par:** { interaction . user } ( { interaction . user . id } ) \n \n "
description_text + = f " **Description:** \n { self . description . value } "
labels = [ " enhancement " , " manual-suggestion " ]
result = await gitlab_client . create_issue (
title = f " [SUGGESTION] { self . suggestion_title . value } " ,
description = description_text ,
labels = labels
)
if result :
issue_iid = result . get ( " iid " )
save_report ( issue_iid , interaction . user . id )
await interaction . edit_original_response ( content = f " ✅ Votre suggestion a été envoyée avec succès ! Merci de contribuer à l ' amélioration du bot. [Voir l ' issue]( { result . get ( ' web_url ' ) } ) " )
else :
await interaction . edit_original_response ( content = " ❌ Une erreur est survenue lors de l ' envoi de la suggestion à GitLab. " )
2026-02-07 16:03:16 +01:00
class BugReport ( commands . Cog ) :
def __init__ ( self , bot ) :
self . bot = bot
2026-03-14 17:05:20 +01:00
async def cog_load ( self ) :
self . web_app = web . Application ( )
self . web_app . router . add_post ( ' /bot_event ' , self . handle_bot_event )
self . runner = web . AppRunner ( self . web_app )
await self . runner . setup ( )
2026-03-28 21:58:44 +01:00
max_retries = 5
retry_delay = 2
for attempt in range ( 1 , max_retries + 1 ) :
try :
self . site = web . TCPSite ( self . runner , ' 127.0.0.1 ' , 5001 )
await self . site . start ( )
kuby_logger . info ( f " Internal Bot Webhook Server started on 127.0.0.1:5001 (Attempt { attempt } ) " )
return
except OSError as e :
if e . errno == 98 : # Address already in use
if attempt < max_retries :
kuby_logger . warning ( f " Port 5001 already in use, retrying in { retry_delay } s... ( { attempt } / { max_retries } ) " )
await asyncio . sleep ( retry_delay )
else :
kuby_logger . error ( f " Failed to bind to port 5001 after { max_retries } attempts. " )
raise e
else :
raise 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
# On utilise create_task car cog_unload est synchrone
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-05-01 16:16:33 +02:00
# Sécurité interne désactivée comme demandé (le port 5001 n'est accessible que via localhost)
provided_token = request . headers . get ( " X-Gitlab-Token " )
# On ignore la vérification du token pour faciliter le relais local
2026-03-29 19:29:17 +02:00
2026-02-07 16:03:16 +01:00
try :
2026-03-29 19:29:17 +02:00
# On attend que le bot soit prêt si on vient de rebooter
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 " )
if event_type not in [ " issue " , " note " ] :
return web . json_response ( { " status " : " ignored " } , status = 200 )
if event_type == " issue " :
issue_iid = payload . get ( " object_attributes " , { } ) . get ( " iid " )
else :
issue_iid = payload . get ( " issue " , { } ) . get ( " iid " )
if not issue_iid :
return web . json_response ( { " status " : " no issue id " } , status = 200 )
if not os . path . exists ( REPORTS_FILE ) :
return web . json_response ( { " status " : " no reports file " } , status = 200 )
2026-05-01 16:16:33 +02:00
try :
with open ( REPORTS_FILE , " r " , encoding = " utf-8 " ) as f :
reports = json . load ( f )
except json . JSONDecodeError :
kuby_logger . error ( " REPORTS_FILE corrupted during read! " )
return web . json_response ( { " status " : " corrupted file " } , status = 500 )
2026-03-14 17:05:20 +01:00
user_id = reports . get ( str ( issue_iid ) )
if not user_id :
return web . json_response ( { " status " : " untracked issue " } , status = 200 )
user = self . bot . get_user ( user_id )
if not user :
try :
user = await self . bot . fetch_user ( user_id )
except :
pass
if not user :
return web . json_response ( { " status " : " user not found " } , status = 200 )
app_info = await self . bot . application_info ( )
dev = app_info . owner
issue_title = payload . get ( " object_attributes " , { } ) . get ( " title " , f " # { issue_iid } " )
if event_type == " note " :
issue_title = payload . get ( " issue " , { } ) . get ( " title " , f " # { issue_iid } " )
if event_type == " issue " :
action = payload . get ( " object_attributes " , { } ) . get ( " action " )
changes = payload . get ( " changes " , { } )
# Check for label changes
if " labels " in changes :
curr_labels = [ l . get ( " title " ) for l in changes [ " labels " ] . get ( " current " , [ ] ) if l . get ( " title " ) ]
display_labels = [ l for l in curr_labels if not l . startswith ( " Priority:: " ) and l not in [ " bug " , " manual-report " , " manual-suggestion " ] ]
if display_labels :
current_labels_str = " , " . join ( display_labels )
2026-02-07 16:03:16 +01:00
else :
2026-03-14 17:05:20 +01:00
current_labels_str = " Mis à jour "
try :
2026-05-01 16:16:33 +02:00
# Création de l'embed pour la mise à jour de statut
2026-05-06 17:07:09 +02:00
embed = disnake . Embed (
2026-03-14 17:05:20 +01:00
title = " 🛠️ Mise à jour de votre signalement ! " ,
description = f " Le statut de votre rapport ** { issue_title } ** a été mis à jour par l ' équipe. " ,
2026-05-06 17:07:09 +02:00
color = disnake . Color . blue ( )
2026-03-14 17:05:20 +01:00
)
embed . add_field ( name = " Nouveaux Labels / Statuts " , value = current_labels_str )
2026-03-15 15:48:28 +01:00
try :
await user . send ( embed = embed )
2026-05-01 16:16:33 +02:00
# On logue systématiquement pour que le staff sache si ça a marché (utile en prod)
kuby_logger . info ( f " Notification de label envoyée à { user } ( { user . id } ) pour l ' issue # { issue_iid } " )
2026-03-15 15:48:28 +01:00
if dev and dev . id != user . id :
2026-05-01 16:16:33 +02:00
await dev . send ( f " ✅ [SUIVI] L ' utilisateur { user } ( { user . id } ) a bien reçu la notification de statut ** { current_labels_str } ** pour l ' issue # { issue_iid } ( { issue_title } ). " )
2026-05-06 17:07:09 +02:00
except disnake . Forbidden :
2026-05-01 16:16:33 +02:00
kuby_logger . warning ( f " Impossible d ' envoyer un MP à { user } ( { user . id } ) - MPs désactivés. " )
2026-03-15 15:48:28 +01:00
if dev and dev . id != user . id :
2026-05-01 16:16:33 +02:00
await dev . send ( f " ⚠️ [DM BLOQUÉ] { user } ( { user . id } ) a ses MPs fermés. Impossible de notifier pour l ' issue # { issue_iid } . " )
except Exception as e :
kuby_logger . error ( f " Erreur lors de l ' envoi du MP de statut à { user } : { e } " )
2026-03-14 17:05:20 +01:00
except Exception as e :
2026-05-01 16:16:33 +02:00
kuby_logger . error ( f " Erreur inattendue dans la notification de label : { e } " )
2026-03-14 17:05:20 +01:00
# Check if it was closed
2026-05-01 16:16:33 +02:00
state = payload . get ( " object_attributes " , { } ) . get ( " state " )
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-06 17:07:09 +02:00
embed = disnake . Embed (
2026-03-14 17:05:20 +01:00
title = " 🛠️ Bug / Suggestion Terminé(e) ! " ,
description = f " Bonne nouvelle ! Votre signalement ** { issue_title } ** a été marqué comme terminé ou résolu. " ,
2026-05-06 17:07:09 +02:00
color = disnake . Color . green ( )
2026-03-14 17:05:20 +01:00
)
embed . add_field ( name = " Prochaine étape " , value = " La mise à jour arrive prochainement (ou est déjà là) ! " )
embed . set_footer ( text = " Merci pour votre aide ! " )
2026-03-15 15:48:28 +01:00
try :
await user . send ( embed = embed )
2026-05-01 16:16:33 +02:00
kuby_logger . info ( f " Notification de clôture envoyée à { user } ( { user . id } ) pour l ' issue # { issue_iid } " )
2026-03-15 15:48:28 +01:00
if dev and dev . id != user . id :
2026-05-01 16:16:33 +02:00
await dev . send ( f " ✅ [SUIVI] L ' utilisateur { user } ( { user . id } ) a bien reçu la notification de CLÔTURE pour l ' issue # { issue_iid } . " )
2026-05-06 17:07:09 +02:00
except disnake . Forbidden :
2026-05-01 16:16:33 +02:00
kuby_logger . warning ( f " Impossible d ' envoyer le MP de clôture à { user } ( { user . id } ). " )
2026-03-15 15:48:28 +01:00
if dev and dev . id != user . id :
2026-05-01 16:16:33 +02:00
await dev . send ( f " ⚠️ [DM BLOQUÉ] Impossible d ' annoncer la clôture à { user } ( { user . id } ) pour l ' issue # { issue_iid } (DMs désactivés). " )
except Exception as e :
kuby_logger . error ( f " Erreur lors de l ' envoi du MP de clôture à { user } : { e } " )
2026-03-14 17:05:20 +01:00
except Exception as e :
2026-05-01 16:16:33 +02:00
kuby_logger . error ( f " Erreur inattendue dans la notification de clôture : { e } " )
2026-03-14 17:05:20 +01:00
elif event_type == " note " :
note_attr = payload . get ( " object_attributes " , { } )
is_system = note_attr . get ( " system " , False )
if not is_system :
note_body = note_attr . get ( " note " , " " )
author_name = payload . get ( " user " , { } ) . get ( " name " , " Développeur " )
2026-02-07 16:03:16 +01:00
2026-03-14 17:05:20 +01:00
try :
2026-05-06 17:07:09 +02:00
embed = disnake . Embed (
2026-03-14 17:05:20 +01:00
title = " 💬 Nouveau commentaire sur votre signalement " ,
description = f " Le développeur a répondu à votre rapport ** { issue_title } ** : \n \n >>> { note_body } " ,
2026-05-06 17:07:09 +02:00
color = disnake . Color . orange ( )
2026-03-14 17:05:20 +01:00
)
embed . set_footer ( text = f " Par { author_name } " )
2026-03-15 15:48:28 +01:00
try :
await user . send ( embed = embed )
2026-05-01 16:16:33 +02:00
kuby_logger . info ( f " Commentaire GitLab envoyé à { user } ( { user . id } ) pour l ' issue # { issue_iid } " )
2026-03-15 15:48:28 +01:00
if dev and dev . id != user . id :
2026-05-01 16:16:33 +02:00
await dev . send ( f " ✅ [SUIVI] L ' utilisateur { user } ( { user . id } ) a bien reçu votre commentaire pour l ' issue # { issue_iid } . " )
2026-05-06 17:07:09 +02:00
except disnake . Forbidden :
2026-05-01 16:16:33 +02:00
kuby_logger . warning ( f " Impossible d ' envoyer le commentaire en MP à { user } ( { user . id } ). " )
2026-03-15 15:48:28 +01:00
if dev and dev . id != user . id :
2026-05-01 16:16:33 +02:00
await dev . send ( f " ⚠️ [DM BLOQUÉ] Impossible d ' envoyer votre commentaire à { user } ( { user . id } ) pour l ' issue # { issue_iid } (DMs désactivés). " )
except Exception as e :
kuby_logger . error ( f " Erreur lors de l ' envoi du MP de commentaire à { user } : { e } " )
2026-03-14 17:05:20 +01:00
except Exception as e :
2026-05-01 16:16:33 +02:00
kuby_logger . error ( f " Erreur inattendue dans la notification de commentaire : { 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-05-06 17:07:09 +02:00
@commands.slash_command ( name = " signaler_bug " , description = " Signaler un bug aux développeurs " )
async def signaler_bug ( self , interaction : disnake . ApplicationCommandInteraction ,
priority : str = commands . Param ( description = " Priorité du bug " , choices = { " Basse " : " Low " , " Normale " : " Normal " , " Haute " : " High " , " Urgente " : " Urgent " } ) ) :
# Simulate app_commands.Choice behavior for existing code
from collections import namedtuple
Choice = namedtuple ( ' Choice ' , [ ' name ' , ' value ' ] )
# find the name from the value
p_name = [ k for k , v in { " Basse " : " Low " , " Normale " : " Normal " , " Haute " : " High " , " Urgente " : " Urgent " } . items ( ) if v == priority ] [ 0 ]
choice = Choice ( name = p_name , value = priority )
await interaction . response . send_modal ( BugReportModal ( choice ) )
2026-02-07 16:03:16 +01:00
2026-05-06 17:07:09 +02:00
@commands.slash_command ( name = " suggerer_fonctionnalite " , description = " Proposer une nouvelle fonctionnalité pour le bot " )
async def suggerer_fonctionnalite ( self , interaction : disnake . ApplicationCommandInteraction ) :
2026-02-08 14:42:40 +01:00
await interaction . response . send_modal ( FeatureSuggestionModal ( ) )
2026-05-09 18:42:42 +02:00
def setup ( bot ) :
bot . add_cog ( BugReport ( bot ) )