Merge branch 'dev' into 'main'
New components V2, correctifs multiple et ajout de plusieurs nouvelles fonctionnalités ! See merge request Omega_Kube/kuby!13
This commit is contained in:
commit
69e5583e74
59 changed files with 3177 additions and 2102 deletions
216
bot.py
216
bot.py
|
|
@ -1,19 +1,72 @@
|
|||
import os
|
||||
import sys
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Charger les variables d'environnement AVANT tout le reste
|
||||
load_dotenv()
|
||||
|
||||
import discord
|
||||
import disnake
|
||||
from commandes.ticket.utils.permissions import can_access_config
|
||||
from commandes.ticket.data.storage import get_storage
|
||||
from discord.ext import commands
|
||||
from disnake.ext import commands
|
||||
import asyncio
|
||||
from src.logger import kuby_logger, log_performance
|
||||
import logging
|
||||
from src.advanced_logger import AdvancedLogger
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
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):
|
||||
# Check if member still in guild
|
||||
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)
|
||||
|
||||
# Update original message
|
||||
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)
|
||||
|
||||
# Update original message
|
||||
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()
|
||||
|
||||
# Récupération et validation de l'ID d'application
|
||||
application_id_raw = os.getenv("APPLICATION_ID")
|
||||
|
|
@ -25,12 +78,12 @@ except ValueError:
|
|||
kuby_logger.error(f"❌ APPLICATION_ID invalide : {application_id_raw}")
|
||||
application_id = None
|
||||
|
||||
# Configuration des intents Discord
|
||||
intents = discord.Intents.default()
|
||||
# Configuration des intents Disnake
|
||||
intents = disnake.Intents.default()
|
||||
intents.message_content = True
|
||||
intents.members = True
|
||||
intents.voice_states = True
|
||||
kuby_logger.debug(f"Discord intents configured: {intents}")
|
||||
kuby_logger.debug(f"Disnake intents configured: {intents}")
|
||||
|
||||
class MyBot(commands.Bot):
|
||||
def __init__(self):
|
||||
|
|
@ -39,11 +92,9 @@ class MyBot(commands.Bot):
|
|||
command_prefix="!",
|
||||
intents=intents,
|
||||
application_id=application_id,
|
||||
command_sync_flags=commands.CommandSyncFlags.all()
|
||||
)
|
||||
kuby_logger.info("MyBot instance created successfully")
|
||||
|
||||
async def setup_hook(self):
|
||||
kuby_logger.info("🚀 Démarrage du bot Kuby...")
|
||||
|
||||
# Initialize advanced logger
|
||||
self.advanced_logger = AdvancedLogger(self)
|
||||
|
|
@ -54,8 +105,6 @@ class MyBot(commands.Bot):
|
|||
try:
|
||||
flask_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "serveur_flask.py")
|
||||
if os.path.exists(flask_path):
|
||||
# On utilise DEVNULL car PM2 ou Nohup gèrent déjà les logs globaux
|
||||
# et ça évite que le pipe se remplisse et bloque le serveur Flask
|
||||
self.flask_process = subprocess.Popen(
|
||||
[sys.executable, flask_path],
|
||||
stdout=subprocess.DEVNULL,
|
||||
|
|
@ -68,6 +117,11 @@ class MyBot(commands.Bot):
|
|||
except Exception as e:
|
||||
kuby_logger.error(f"❌ Impossible de démarrer le serveur Flask: {e}")
|
||||
|
||||
|
||||
async def do_async_setup(self):
|
||||
# --- INITIALIZATION LOGIC (Called before start() in kuby.py) ---
|
||||
kuby_logger.info("🚀 Configuration initiale du bot Kuby...")
|
||||
|
||||
# Charger les extensions
|
||||
extensions = [
|
||||
"commandes.whitelist",
|
||||
|
|
@ -89,68 +143,49 @@ class MyBot(commands.Bot):
|
|||
"commandes.bug_report",
|
||||
"commandes.convocation",
|
||||
"commandes.absence_staff",
|
||||
"commandes.website_sync",
|
||||
"commandes.moderation",
|
||||
"commandes.invites",
|
||||
]
|
||||
|
||||
for extension in extensions:
|
||||
try:
|
||||
await self.load_extension(extension)
|
||||
self.load_extension(extension)
|
||||
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)
|
||||
|
||||
# --- SYSTÈME DE WHITELIST (CORRIGÉ POUR ÉVITER LES ERREURS) ---
|
||||
@self.check
|
||||
async def whitelist_check(ctx):
|
||||
# --- SYSTÈME DE WHITELIST (DÉDIÉ SLASH COMMANDS) ---
|
||||
async def global_slash_whitelist_check(inter: disnake.ApplicationCommandInteraction):
|
||||
try:
|
||||
# 1. Bypass pour le propriétaire (Owner)
|
||||
if await self.is_owner(ctx.author):
|
||||
if await self.is_owner(inter.author):
|
||||
return True
|
||||
command_name = inter.data.name
|
||||
if command_name in ["ticketconfig", "ticketpanel", "ticket_config", "ticket_panel", "ticket_whitelist_config", "invitations"]:
|
||||
return True
|
||||
|
||||
# 1b. Bypass pour les commandes de config tickets
|
||||
# Si la commande est une commande ticket et l'utilisateur a les perms ticket
|
||||
if ctx.command and (ctx.command.name in ["ticketconfig", "ticketsetup", "configticket"]):
|
||||
try:
|
||||
storage = get_storage()
|
||||
config = storage.load_config(ctx.guild.id)
|
||||
if can_access_config(ctx, config):
|
||||
return True
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"Error checking ticket permissions in global whitelist: {e}")
|
||||
# Fallthrough to normal whitelist check if error
|
||||
|
||||
|
||||
# 2. Récupération du Cog
|
||||
whitelist_cog = self.get_cog("WhitelistMonitor")
|
||||
if not whitelist_cog:
|
||||
kuby_logger.warning("WhitelistMonitor cog not found, allowing all commands")
|
||||
return True
|
||||
|
||||
is_whitelisted = whitelist_cog.is_whitelisted(ctx.guild.id, ctx.author.id)
|
||||
|
||||
is_whitelisted = whitelist_cog.is_whitelisted(inter.guild.id, inter.author.id)
|
||||
if not is_whitelisted:
|
||||
kuby_logger.info(f"User {ctx.author} (ID: {ctx.author.id}) attempted to use command but is not whitelisted")
|
||||
|
||||
# Réponse adaptée (Slash vs Message)
|
||||
kuby_logger.info(f"🚫 [Check] Accès refusé : {inter.author} sur /{command_name}")
|
||||
msg = "❌ Vous n'êtes pas autorisé à utiliser cette commande. Veuillez demander à être ajouté à la whitelist."
|
||||
if ctx.interaction:
|
||||
await ctx.interaction.response.send_message(msg, ephemeral=True)
|
||||
else:
|
||||
await ctx.send(msg)
|
||||
if not inter.response.is_done():
|
||||
await inter.response.send_message(msg, ephemeral=True)
|
||||
return False
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"Error in whitelist check: {e}")
|
||||
kuby_logger.error(f"⚠️ [Check] Erreur critique: {e}", exc_info=True)
|
||||
return True
|
||||
|
||||
# Sync slash commands
|
||||
try:
|
||||
await self.tree.sync()
|
||||
kuby_logger.info("✅ Commandes slash synchronisées avec Discord")
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"❌ Erreur lors de la synchronisation des commandes slash : {e}", exc_info=True)
|
||||
# self.add_app_command_check(global_slash_whitelist_check)
|
||||
|
||||
# --- 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")
|
||||
|
||||
async def close(self):
|
||||
if hasattr(self, 'flask_process') and self.flask_process:
|
||||
|
|
@ -163,22 +198,12 @@ class MyBot(commands.Bot):
|
|||
await super().close()
|
||||
|
||||
# --- ÉVÉNEMENTS RESTAURÉS ---
|
||||
|
||||
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)}")
|
||||
|
||||
# Log intents status
|
||||
kuby_logger.info(f"🔍 Intents Status - Members: {self.intents.members}, Presences: {self.intents.presences}, Core: {self.intents.message_content}")
|
||||
|
||||
if not self.intents.members:
|
||||
kuby_logger.warning("⚠️ L'intent MEMBERS est désactivé ! Les événements de join/leave et les DMs pourraient ne pas fonctionner correctement.")
|
||||
|
||||
for guild in self.guilds:
|
||||
kuby_logger.debug(f"Guild: {guild.name} (ID: {guild.id}) - Members: {guild.member_count}")
|
||||
|
||||
total_members = sum(guild.member_count for guild in self.guilds)
|
||||
kuby_logger.info(f"📊 Total members across all guilds: {total_members}")
|
||||
kuby_logger.warning("⚠️ L'intent MEMBERS est désactivé !")
|
||||
kuby_logger.info("✅ Bot prêt et opérationnel !")
|
||||
|
||||
async def on_guild_join(self, guild):
|
||||
|
|
@ -199,17 +224,68 @@ class MyBot(commands.Bot):
|
|||
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:
|
||||
try:
|
||||
await member.add_roles(*roles_to_add)
|
||||
kuby_logger.info(f"Restored {len(roles_to_add)} roles for {member}")
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"Failed to restore roles: {e}")
|
||||
# Separate roles into safe and sensitive
|
||||
safe_roles = []
|
||||
sensitive_roles = []
|
||||
|
||||
for role in roles_to_add:
|
||||
is_sensitive = False
|
||||
for perm in SENSITIVE_PERMISSIONS:
|
||||
if getattr(role.permissions, perm, False):
|
||||
is_sensitive = True
|
||||
break
|
||||
|
||||
if is_sensitive:
|
||||
sensitive_roles.append(role)
|
||||
else:
|
||||
safe_roles.append(role)
|
||||
|
||||
# Restore safe roles automatically
|
||||
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]
|
||||
if not isinstance(e, disnake.HTTPException): is_transient = True
|
||||
|
||||
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
|
||||
|
||||
# Handle sensitive roles with owner approval
|
||||
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}")
|
||||
|
||||
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)
|
||||
# Notification de départ
|
||||
try:
|
||||
from commandes.security import load_settings
|
||||
settings = load_settings(member.guild.id)
|
||||
|
|
@ -248,7 +324,7 @@ class MyBot(commands.Bot):
|
|||
if isinstance(error, commands.CommandNotFound):
|
||||
return # Ignorer les erreurs "Command not found" pour éviter de polluer les logs (ex: !!)
|
||||
if isinstance(error, commands.CheckFailure):
|
||||
return # Déjà géré dans le whitelist_check
|
||||
return
|
||||
kuby_logger.error(f"Command error in {ctx.command}: {str(error)}", exc_info=True)
|
||||
|
||||
bot = MyBot()
|
||||
Loading…
Add table
Add a link
Reference in a new issue