2026-02-07 16:03:16 +01:00
import discord
from discord import app_commands
2026-03-14 17:05:20 +01:00
from discord . 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 :
if os . path . exists ( REPORTS_FILE ) :
with open ( REPORTS_FILE , " r " ) as f :
data = json . load ( f )
else :
data = { }
data [ str ( issue_iid ) ] = user_id
with open ( REPORTS_FILE , " w " ) as f :
json . dump ( data , f , indent = 4 )
except Exception as e :
kuby_logger . error ( f " Error saving report to JSON: { e } " )
class BugReportModal ( discord . ui . Modal , title = " Signaler un Bug " ) :
def __init__ ( self , priority_choice ) :
super ( ) . __init__ ( )
self . priority_choice = priority_choice
bug_title = discord . ui . TextInput (
label = " Titre du bug " ,
placeholder = " Décrivez brièvement le problème... " ,
required = True ,
max_length = 100
)
description = discord . ui . TextInput (
label = " Description détaillée " ,
style = discord . TextStyle . paragraph ,
placeholder = " Que s ' est-il passé ? Comment reproduire le bug ? " ,
required = True ,
max_length = 1000
)
async def on_submit ( self , interaction : discord . Interaction ) :
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-02-08 14:42:40 +01:00
class FeatureSuggestionModal ( discord . ui . Modal , title = " Suggérer une fonctionnalité " ) :
suggestion_title = discord . ui . TextInput (
label = " Titre de la suggestion " ,
placeholder = " Que voulez-vous ajouter ? " ,
required = True ,
max_length = 100
)
description = discord . ui . TextInput (
label = " Détails de la fonctionnalité " ,
style = discord . TextStyle . paragraph ,
placeholder = " Décrivez comment cela devrait fonctionner... " ,
required = True ,
max_length = 1000
)
async def on_submit ( self , interaction : discord . Interaction ) :
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-03-29 19:29:17 +02:00
# Vérification de la sécurité (Secret Token GitLab)
webhook_secret = os . getenv ( " GITLAB_WEBHOOK_SECRET " )
if webhook_secret :
provided_token = request . headers . get ( " X-Gitlab-Token " )
if provided_token != webhook_secret :
kuby_logger . warning ( " Unauthorised webhook attempt detected (invalid X-Gitlab-Token). " )
return web . json_response ( { " status " : " unauthorised " } , status = 401 )
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-02-07 16:03:16 +01:00
with open ( REPORTS_FILE , " r " ) as f :
reports = json . load ( f )
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 :
embed = discord . Embed (
title = " 🛠️ Mise à jour de votre signalement ! " ,
description = f " Le statut de votre rapport ** { issue_title } ** a été mis à jour par l ' équipe. " ,
color = discord . Color . blue ( )
)
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 )
if dev and dev . id != user . id :
await dev . send ( f " ✅ [FIABLE] L ' utilisateur { user } ( { user . id } ) a bien reçu la notification de statut ** { current_labels_str } ** pour l ' issue # { issue_iid } ( { issue_title } ). " )
except discord . Forbidden :
kuby_logger . warning ( f " Unable to DM user { user } ( { user . id } ) - DMs are disabled or bot is blocked. " )
if dev and dev . id != user . id :
try :
await dev . send ( f " ⚠️ [DM BLOQUÉ] Impossible de notifier l ' utilisateur { user } ( { user . id } ) pour l ' issue # { issue_iid } (DMs désactivés). " )
except discord . Forbidden :
pass
except discord . HTTPException as e :
kuby_logger . error ( f " HTTP error sending DM to { user } : { e } " )
2026-03-14 17:05:20 +01:00
except Exception as e :
2026-03-15 15:48:28 +01:00
kuby_logger . error ( f " Unexpected error in label update notification: { e } " )
2026-03-14 17:05:20 +01:00
# Check if it was closed
if action == " close " or payload . get ( " object_attributes " , { } ) . get ( " state " ) == " closed " :
try :
embed = discord . Embed (
title = " 🛠️ Bug / Suggestion Terminé(e) ! " ,
description = f " Bonne nouvelle ! Votre signalement ** { issue_title } ** a été marqué comme terminé ou résolu. " ,
color = discord . Color . green ( )
)
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 )
if dev and dev . id != user . id :
await dev . send ( f " ✅ [FIABLE] L ' utilisateur { user } ( { user . id } ) a bien reçu la notification de CLÔTURE pour l ' issue # { issue_iid } . " )
except discord . Forbidden :
kuby_logger . warning ( f " Unable to DM user { user } ( { user . id } ) - DMs are disabled or bot is blocked during closure. " )
if dev and dev . id != user . id :
try :
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 discord . Forbidden :
pass
except discord . HTTPException as e :
kuby_logger . error ( f " HTTP error sending closure DM to { user } : { e } " )
2026-03-14 17:05:20 +01:00
except Exception as e :
2026-03-15 15:48:28 +01:00
kuby_logger . error ( f " Unexpected error in closure notification: { 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 :
embed = discord . Embed (
title = " 💬 Nouveau commentaire sur votre signalement " ,
description = f " Le développeur a répondu à votre rapport ** { issue_title } ** : \n \n >>> { note_body } " ,
color = discord . Color . orange ( )
)
embed . set_footer ( text = f " Par { author_name } " )
2026-03-15 15:48:28 +01:00
try :
await user . send ( embed = embed )
if dev and dev . id != user . id :
await dev . send ( f " ✅ [FIABLE] L ' utilisateur { user } ( { user . id } ) a bien reçu votre commentaire pour l ' issue # { issue_iid } . " )
except discord . Forbidden :
kuby_logger . warning ( f " Unable to DM user { user } ( { user . id } ) - DMs are disabled or bot is blocked during comment notification. " )
if dev and dev . id != user . id :
try :
await dev . send ( f " ⚠️ [DM BLOQUÉ] Impossible d ' envoyer votre commentaire à { user } ( { user . id } ) pour l ' issue # { issue_iid } (DMs désactivés). " )
except discord . Forbidden :
pass
except discord . HTTPException as e :
kuby_logger . error ( f " HTTP error sending comment DM to { user } : { e } " )
2026-03-14 17:05:20 +01:00
except Exception as e :
2026-03-15 15:48:28 +01:00
kuby_logger . error ( f " Unexpected error in comment notification: { 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
@app_commands.command ( name = " signaler_bug " , description = " Signaler un bug aux développeurs " )
@app_commands.choices ( priority = [
app_commands . Choice ( name = " Basse " , value = " Low " ) ,
app_commands . Choice ( name = " Normale " , value = " Normal " ) ,
app_commands . Choice ( name = " Haute " , value = " High " ) ,
app_commands . Choice ( name = " Urgente " , value = " Urgent " ) ,
] )
2026-03-14 22:49:38 +01:00
async def signaler_bug ( self , interaction : discord . Interaction , priority : app_commands . Choice [ str ] ) :
2026-02-07 16:03:16 +01:00
await interaction . response . send_modal ( BugReportModal ( priority ) )
2026-02-08 14:42:40 +01:00
@app_commands.command ( name = " suggerer_fonctionnalite " , description = " Proposer une nouvelle fonctionnalité pour le bot " )
2026-03-14 22:49:38 +01:00
async def suggerer_fonctionnalite ( self , interaction : discord . Interaction ) :
2026-02-08 14:42:40 +01:00
await interaction . response . send_modal ( FeatureSuggestionModal ( ) )
2026-02-07 16:03:16 +01:00
async def setup ( bot ) :
await bot . add_cog ( BugReport ( bot ) )