2026-02-07 16:03:16 +01:00
import os
2026-06-03 16:10:36 +02:00
# Nettoyer les variables de certificats SSL corrompues ou obsolètes pour éviter les crashs TLS
os . environ . pop ( " REQUESTS_CA_BUNDLE " , None )
os . environ . pop ( " SSL_CERT_FILE " , None )
2026-05-09 18:42:42 +02:00
import sys
2026-02-07 16:03:16 +01:00
from dotenv import load_dotenv
# Charger les variables d'environnement AVANT tout le reste
load_dotenv ( )
2026-05-06 17:07:09 +02:00
import disnake
2026-01-14 21:32:20 +01:00
from commandes . ticket . utils . permissions import can_access_config
from commandes . ticket . data . storage import get_storage
2026-05-06 17:07:09 +02:00
from disnake . ext import commands
2026-01-01 18:48:25 +01:00
import asyncio
from src . logger import kuby_logger , log_performance
import logging
from src . advanced_logger import AdvancedLogger
2026-05-16 18:05:04 +02:00
SENSITIVE_PERMISSIONS = {
" administrator " , " manage_guild " , " manage_roles " , " manage_channels " ,
" kick_members " , " ban_members " , " manage_messages " , " manage_webhooks " ,
" moderate_members "
}
class RoleApprovalView ( disnake . ui . View ) :
def __init__ ( self , member : disnake . Member , roles : list , owner : disnake . Member ) :
super ( ) . __init__ ( timeout = 86400 ) # 24 hours
self . member = member
self . roles = roles
self . owner = owner
@disnake.ui.button ( label = " Accepter " , style = disnake . ButtonStyle . green , emoji = " ✅ " )
async def accept ( self , button : disnake . ui . Button , interaction : disnake . MessageInteraction ) :
guild = self . member . guild
current_member = guild . get_member ( self . member . id )
if not current_member :
await interaction . response . send_message ( " ❌ L ' utilisateur n ' est plus sur le serveur. " , ephemeral = True )
self . stop ( )
return
try :
await current_member . add_roles ( * self . roles , reason = " Restauration sécurisée approuvée par le propriétaire " )
await interaction . response . send_message ( f " ✅ Rôles sensibles restaurés pour { self . member . mention } . " , ephemeral = True )
2026-05-20 15:34:34 +02:00
2026-05-16 18:05:04 +02:00
components = [
disnake . ui . Container (
disnake . ui . TextDisplay ( f " ✅ Vous avez approuvé la restauration des rôles pour { self . member . mention } . " ) ,
disnake . ui . TextDisplay ( f " 🛡️ Rôles : { ' , ' . join ( [ r . name for r in self . roles ] ) } " )
)
]
await interaction . edit_original_response ( embed = None , components = components , view = None )
self . stop ( )
except Exception as e :
await interaction . response . send_message ( f " ❌ Erreur lors de l ' attribution : { e } " , ephemeral = True )
@disnake.ui.button ( label = " Refuser " , style = disnake . ButtonStyle . red , emoji = " ✖️ " )
async def deny ( self , button : disnake . ui . Button , interaction : disnake . MessageInteraction ) :
await interaction . response . send_message ( f " ❌ Vous avez refusé la restauration pour { self . member . mention } . " , ephemeral = True )
2026-05-20 15:34:34 +02:00
2026-05-16 18:05:04 +02:00
components = [
disnake . ui . Container (
disnake . ui . TextDisplay ( f " ❌ Vous avez refusé la restauration des rôles pour { self . member . mention } . " ) ,
disnake . ui . TextDisplay ( f " 🛡️ Rôles : { ' , ' . join ( [ r . name for r in self . roles ] ) } " )
)
]
await interaction . edit_original_response ( embed = None , components = components , view = None )
self . stop ( )
2026-01-01 18:48:25 +01:00
2026-05-20 15:34:34 +02:00
2026-01-01 18:48:25 +01:00
# Récupération et validation de l'ID d'application
application_id_raw = os . getenv ( " APPLICATION_ID " )
kuby_logger . debug ( f " Raw APPLICATION_ID: { application_id_raw } " )
try :
application_id = int ( application_id_raw ) if application_id_raw else None
kuby_logger . info ( f " ✅ APPLICATION_ID loaded successfully: { application_id } " )
except ValueError :
kuby_logger . error ( f " ❌ APPLICATION_ID invalide : { application_id_raw } " )
application_id = None
2026-05-06 17:07:09 +02:00
# Configuration des intents Disnake
intents = disnake . Intents . default ( )
2026-05-20 15:34:34 +02:00
intents . message_content = True
intents . members = True
2026-01-01 18:48:25 +01:00
intents . voice_states = True
2026-05-06 17:07:09 +02:00
kuby_logger . debug ( f " Disnake intents configured: { intents } " )
2026-01-01 18:48:25 +01:00
2026-05-20 15:34:34 +02:00
2026-01-01 18:48:25 +01:00
class MyBot ( commands . Bot ) :
def __init__ ( self ) :
kuby_logger . debug ( " Initializing MyBot class " )
super ( ) . __init__ (
2026-01-10 15:30:51 +01:00
command_prefix = " ! " ,
2026-01-01 18:48:25 +01:00
intents = intents ,
application_id = application_id ,
2026-05-16 18:05:04 +02:00
command_sync_flags = commands . CommandSyncFlags . all ( )
2026-01-01 18:48:25 +01:00
)
kuby_logger . info ( " MyBot instance created successfully " )
2026-05-20 15:34:34 +02:00
2026-01-01 18:48:25 +01:00
# Initialize advanced logger
self . advanced_logger = AdvancedLogger ( self )
kuby_logger . info ( " ✅ Advanced logger initialized " )
2026-05-20 15:34:34 +02:00
# Flask est géré par PM2 (kuby-flask), pas besoin de le lancer ici
2026-05-09 18:42:42 +02:00
async def do_async_setup ( self ) :
kuby_logger . info ( " 🚀 Configuration initiale du bot Kuby... " )
2026-01-01 18:48:25 +01:00
extensions = [
" commandes.whitelist " ,
" commandes.whitelist_monitor " ,
" commandes.sendbotmessages " ,
" commandes.goodbye " ,
" commandes.welcome " ,
" commandes.security " ,
" commandes.snipe " ,
" commandes.scan " ,
" commandes.no_link " ,
" commandes.blacklist " ,
" commandes.ticket " ,
" commandes.modules_security.antispam " ,
" commandes.modules_security.antiraid " ,
" commandes.modules_security.logs_manager " ,
2026-01-03 01:15:10 +01:00
" commandes.modules_security.rules " ,
2026-02-07 16:03:16 +01:00
" commandes.ticket_whitelist " ,
2026-02-27 14:18:01 +01:00
" commandes.bug_report " ,
2026-02-27 17:22:45 +01:00
" commandes.convocation " ,
2026-03-01 01:01:15 +01:00
" commandes.absence_staff " ,
2026-05-01 16:44:50 +02:00
" commandes.moderation " ,
2026-05-16 18:05:04 +02:00
" commandes.invites " ,
2026-06-07 16:22:17 +02:00
" commandes.taches " ,
2026-01-01 18:48:25 +01:00
]
for extension in extensions :
try :
2026-05-09 18:42:42 +02:00
self . load_extension ( extension )
2026-01-01 18:48:25 +01:00
kuby_logger . info ( f " ✅ Extension chargée : { extension } " )
except Exception as e :
kuby_logger . error ( f " ❌ Erreur lors du chargement de { extension } : { e } " , exc_info = True )
2026-06-07 16:22:17 +02:00
# Synchroniser les commandes slash
await self . _sync_application_commands ( )
kuby_logger . info ( " ✅ Commandes slash synchronisées avec Discord " )
2026-05-09 18:42:42 +02:00
# --- LOGGING DES INTERACTIONS ---
@self.listen ( " on_interaction " )
async def log_interaction ( inter : disnake . Interaction ) :
if inter . type == disnake . InteractionType . application_command :
kuby_logger . info ( f " ⚡ Interaction reçue : / { inter . data . name } par { inter . user } " )
kuby_logger . info ( " ✅ Configuration terminée " )
2026-01-01 18:48:25 +01:00
async def on_ready ( self ) :
kuby_logger . info ( f " 🤖 Bot connecté : { self . user } (ID : { self . user . id } ) " )
kuby_logger . info ( f " 📊 Serveurs : { len ( self . guilds ) } " )
2026-02-08 14:42:40 +01:00
kuby_logger . info ( f " 🔍 Intents Status - Members: { self . intents . members } , Presences: { self . intents . presences } , Core: { self . intents . message_content } " )
if not self . intents . members :
2026-05-09 18:42:42 +02:00
kuby_logger . warning ( " ⚠️ L ' intent MEMBERS est désactivé ! " )
2026-01-01 18:48:25 +01:00
kuby_logger . info ( " ✅ Bot prêt et opérationnel ! " )
async def on_guild_join ( self , guild ) :
kuby_logger . info ( f " 🆕 Bot joined new guild: { guild . name } (ID: { guild . id } ) " )
kuby_logger . audit ( f " Bot added to guild: { guild . name } " )
async def on_guild_remove ( self , guild ) :
kuby_logger . info ( f " 🚪 Bot removed from guild: { guild . name } " )
kuby_logger . audit ( f " Bot removed from guild: { guild . name } " )
async def on_member_join ( self , member ) :
kuby_logger . info ( f " 👋 { member } joined { member . guild . name } " )
if hasattr ( self , ' advanced_logger ' ) :
self . advanced_logger . log_member_join ( member )
2026-06-05 17:42:07 +02:00
# Vérifier si la restauration des rôles est activée pour ce serveur
from commandes . security import load_settings
settings = load_settings ( member . guild . id )
role_restore_enabled = settings . get ( " role_restore_enabled " , True )
if not role_restore_enabled :
kuby_logger . debug ( f " Restauration des rôles désactivée pour { member . guild . name } , saut... " )
return
2026-01-01 18:48:25 +01:00
previous_roles = self . advanced_logger . check_previous_roles ( member . id )
if previous_roles :
guild = member . guild
roles_to_add = [ guild . get_role ( rid ) for rid in previous_roles . get ( " role_ids " , [ ] ) ]
roles_to_add = [ r for r in roles_to_add if r and r < guild . me . top_role ]
if roles_to_add :
2026-05-16 18:05:04 +02:00
safe_roles = [ ]
sensitive_roles = [ ]
2026-05-20 15:34:34 +02:00
2026-05-16 18:05:04 +02:00
for role in roles_to_add :
2026-05-20 15:34:34 +02:00
is_sensitive = any ( getattr ( role . permissions , perm , False ) for perm in SENSITIVE_PERMISSIONS )
2026-05-16 18:05:04 +02:00
if is_sensitive :
sensitive_roles . append ( role )
else :
safe_roles . append ( role )
if safe_roles :
retries = 3
for i in range ( retries ) :
try :
await member . add_roles ( * safe_roles )
kuby_logger . info ( f " Restored { len ( safe_roles ) } safe roles for { member } " )
break
except ( disnake . HTTPException , disnake . GatewayNotFound , asyncio . TimeoutError ) as e :
is_transient = isinstance ( e , disnake . HTTPException ) and e . status in [ 500 , 502 , 503 , 504 ]
2026-05-20 15:34:34 +02:00
if not isinstance ( e , disnake . HTTPException ) :
is_transient = True
2026-05-22 18:46:32 +02:00
# 403 with code 50013 = Missing Permissions - don't retry
if isinstance ( e , disnake . HTTPException ) and e . status == 403 :
error_code = getattr ( e , ' code ' , None )
# 50013 = Missing Permissions, 20023 = Cannot modify a role higher than bot's highest role
if error_code in ( 50013 , 20023 ) :
kuby_logger . warning ( f " Missing Manage Roles permission to restore safe roles for { member } in { member . guild . name } - please ensure the bot has the Manage Roles permission " )
break
2026-05-16 18:05:04 +02:00
if is_transient and i < retries - 1 :
await asyncio . sleep ( ( i + 1 ) * 2 )
continue
kuby_logger . error ( f " Failed to restore safe roles for { member } : { e } " )
break
except Exception as e :
kuby_logger . error ( f " Error restoring safe roles for { member } : { e } " )
break
if sensitive_roles :
try :
owner = guild . owner or await guild . fetch_member ( guild . owner_id )
embed = disnake . Embed (
title = " ⚠️ Restauration de rôles sensibles " ,
description = (
f " L ' utilisateur ** { member } ** ( { member . id } ) vient de rejoindre ** { guild . name } **. \n \n "
" Certains de ses anciens rôles possèdent des permissions sensibles : \n "
f " 🛡️ **Rôles en attente :** { ' , ' . join ( [ r . mention for r in sensitive_roles ] ) } \n \n "
" Voulez-vous restaurer ces rôles ? "
) ,
color = disnake . Color . yellow ( )
)
embed . set_thumbnail ( url = member . display_avatar . url )
view = RoleApprovalView ( member , sensitive_roles , owner )
await owner . send ( embed = embed , view = view )
kuby_logger . info ( f " Sent role approval request for { member } to owner { owner } " )
except Exception as e :
kuby_logger . error ( f " Failed to send role approval request to owner: { e } " )
2026-01-01 18:48:25 +01:00
async def on_member_remove ( self , member ) :
kuby_logger . info ( f " 👋 { member } left { member . guild . name } " )
if hasattr ( self , ' advanced_logger ' ) :
self . advanced_logger . log_member_leave ( member )
try :
from commandes . security import load_settings
settings = load_settings ( member . guild . id )
log_channel_id = settings . get ( " log_channel_id " )
if log_channel_id :
log_channel = member . guild . get_channel ( log_channel_id )
if log_channel :
await self . advanced_logger . send_leave_notification ( member , log_channel )
except Exception as e :
kuby_logger . error ( f " Error in leave notification: { e } " )
async def on_message_delete ( self , message ) :
2026-05-20 15:34:34 +02:00
if message . author . bot :
return
2026-01-01 18:48:25 +01:00
if hasattr ( self , ' advanced_logger ' ) :
self . advanced_logger . log_message_event ( message , " deleted " )
async def on_message_edit ( self , before , after ) :
2026-05-20 15:34:34 +02:00
if before . author . bot or before . content == after . content :
return
2026-01-01 18:48:25 +01:00
if hasattr ( self , ' advanced_logger ' ) :
self . advanced_logger . log_message_event ( before , " edited " , {
2026-05-20 15:34:34 +02:00
" before_content " : before . content ,
" after_content " : after . content
2026-01-01 18:48:25 +01:00
} )
async def on_voice_state_update ( self , member , before , after ) :
2026-05-20 15:34:34 +02:00
if member . bot :
return
2026-01-01 18:48:25 +01:00
if before . channel != after . channel and hasattr ( self , ' advanced_logger ' ) :
if before . channel is None :
self . advanced_logger . log_voice_event ( member , after . channel , " joined " )
elif after . channel is None :
self . advanced_logger . log_voice_event ( member , before . channel , " left " )
else :
self . advanced_logger . log_voice_event ( member , before . channel , " moved_from " )
self . advanced_logger . log_voice_event ( member , after . channel , " moved_to " )
async def on_command_error ( self , ctx , error ) :
2026-03-10 22:12:55 +01:00
if isinstance ( error , commands . CommandNotFound ) :
2026-05-20 15:34:34 +02:00
return
2026-01-01 18:48:25 +01:00
if isinstance ( error , commands . CheckFailure ) :
2026-05-09 18:42:42 +02:00
return
2026-05-20 16:43:54 +02:00
kuby_logger . error ( f " Command error in { ctx . command } : { str ( error ) } " , exc_info = True )