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:
Gameur 2026-05-16 18:06:58 +02:00
commit 69e5583e74
59 changed files with 3177 additions and 2102 deletions

3
.gitignore vendored
View file

@ -35,4 +35,5 @@ logs/
# --- IDE & SYSTÈME ---
.vscode/
.idea/
.DS_Store
.DS_Store
.roo/

9
TODO.md Normal file
View file

@ -0,0 +1,9 @@
# TODO - Panel sanctions (components V2)
- [ ] Revoir `commandes/moderation.py` : ajouter `panel_channel_id` dans la table `mod_config`
- [ ] Mettre à jour `/modconfig` avec un nouveau bouton/sélecteur “Salon sanctions” (ChannelSelect) (style components V2, callbacks dédiés)
- [ ] Ajouter un formatter “panneau sanctions” (liste bullets + citations/raison + date + modérateur + durée si TIMEOUT)
- [ ] Ajouter une fonction `send_sanction_panel(...)` et lappeler dans `warn`, `timeout`, `kick`, `ban` (push automatique)
- [ ] Enrichir `/modpanel` pour afficher les sanctions actives récentes “proprement” (et garder lUI actuelle)
- [ ] Nettoyer les callbacks (supprimer les `lambda` dans `ModConfigView` si présents)
- [ ] Tester localement : `/modconfig` -> config channel, puis exécuter des sanctions et vérifier laffichage dans le salon

View file

@ -0,0 +1,30 @@
import disnake
from disnake.ui import View, Button
import asyncio
class MyView(View):
def __init__(self):
super().__init__(timeout=None)
@disnake.ui.button(label="Test")
async def my_btn(self, arg1, arg2):
print(f"Arg1 type: {type(arg1)}")
print(f"Arg2 type: {type(arg2)}")
async def main():
view = MyView()
btn = view.children[0]
print(f"Button: {btn}")
# Mock an interaction
class MockInteraction:
pass
try:
await btn.callback(MockInteraction())
except Exception as e:
print(f"Error during callback: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(main())

View file

@ -0,0 +1,4 @@
import disnake
import disnake.ext.commands as commands
bot = commands.Bot(command_prefix="!")
print(f"Check methods: {[m for m in dir(bot) if 'check' in m]}")

View file

@ -0,0 +1,19 @@
import disnake
import disnake.ext.commands as commands
import asyncio
print(f"Disnake version: {disnake.__version__}")
bot = commands.Bot(command_prefix="!")
print(f"Bot has setup_hook: {hasattr(bot, 'setup_hook')}")
async def test():
print("Testing setup_hook call...")
class TestBot(commands.Bot):
async def setup_hook(self):
print("setup_hook CALLED!")
tbot = TestBot(command_prefix="!")
# We won't start it because we don't have a token here,
# but we can check if the library expects it.
asyncio.run(test())

View file

@ -0,0 +1,7 @@
import disnake
import disnake.ext.commands as commands
import asyncio
bot = commands.Bot(command_prefix="!")
print(f"Bot methods: {[m for m in dir(bot) if 'setup' in m or 'load' in m]}")
print(f"Is context manager: {hasattr(bot, '__aenter__')}")

View file

@ -0,0 +1,6 @@
import disnake
import disnake.ext.commands as commands
import inspect
bot = commands.Bot(command_prefix="!")
print(f"load_extension is coroutine: {inspect.iscoroutinefunction(bot.load_extension)}")

View file

@ -0,0 +1,8 @@
import disnake
print(f"disnake.TextStyle: {hasattr(disnake, 'TextStyle')}")
print(f"disnake.TextInputStyle: {hasattr(disnake, 'TextInputStyle')}")
try:
import disnake.ui
print(f"disnake.ui.TextInputStyle: {hasattr(disnake.ui, 'TextInputStyle')}")
except:
pass

View file

@ -0,0 +1,52 @@
import os
import re
def fix_file(filepath):
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# Split by class to be surgical
classes = re.split(r'^(class\s+.*)$', content, flags=re.MULTILINE)
new_content = []
if len(classes) > 0:
new_content.append(classes[0]) # Header
for i in range(1, len(classes), 2):
class_header = classes[i]
class_body = classes[i+1] if i+1 < len(classes) else ""
# Determine if it's a View or a Modal
is_view = "disnake.ui.View" in class_header or "(disnake.ui.View)" in class_header or "super().__init__(timeout=" in class_body
is_modal = "disnake.ui.Modal" in class_header or "(disnake.ui.Modal)" in class_header or "super().__init__(title=" in class_body
if is_view:
# Views use add_item
class_body = class_body.replace("self.append_component(", "self.add_item(")
# Ensure super().__init__ doesn't have components=[] if it's a view
class_body = class_body.replace("super().__init__(components=[],", "super().__init__(")
class_body = class_body.replace("super().__init__(components=[], ", "super().__init__(")
if is_modal:
# Modals use append_component
class_body = class_body.replace("self.add_item(", "self.append_component(")
# Modals MUST have components=[] in super().__init__ if not already there
# (My previous sed might have added it correctly)
pass
new_content.append(class_header)
new_content.append(class_body)
final_content = "".join(new_content)
if final_content != content:
with open(filepath, 'w', encoding='utf-8') as f:
f.write(final_content)
return True
return False
for root, dirs, files in os.walk('commandes'):
for file in files:
if file.endswith('.py'):
path = os.path.join(root, file)
if fix_file(path):
print(f"Fixed {path}")

View file

@ -0,0 +1,36 @@
import os
import re
def fix_text_input_default(filepath):
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# Match TextInput( followed by anything including newlines, until )
# This is a bit risky but we can refine it.
# We look for default= inside the arguments of TextInput
def replace_default(match):
args = match.group(2)
new_args = args.replace("default=", "value=")
return f"{match.group(1)}{new_args})"
# Regex: (disnake\.ui\.TextInput\(|ui\.TextInput\(|TextInput\() (.*? \))
# We need to handle nested parentheses if any, but TextInput usually doesn't have them in args.
new_content = re.sub(r'((?:disnake\.ui\.|ui\.|)TextInput\()(.*?\))', replace_default, content, flags=re.DOTALL)
if new_content != content:
with open(filepath, 'w', encoding='utf-8') as f:
f.write(new_content)
return True
return False
files_to_check = [
"commandes/moderation.py",
"commandes/ticket/__init__.py"
]
for path in files_to_check:
if os.path.exists(path):
if fix_text_input_default(path):
print(f"Fixed {path}")

View file

@ -0,0 +1,7 @@
import disnake
import inspect
try:
print(f"Modal init signature: {inspect.signature(disnake.ui.Modal.__init__)}")
except Exception as e:
print(f"Error inspecting Modal: {e}")

View file

@ -0,0 +1,7 @@
import disnake
import inspect
try:
print(f"Section init signature: {inspect.signature(disnake.ui.Section.__init__)}")
except Exception as e:
print(f"Error inspecting Section: {e}")

View file

@ -0,0 +1,3 @@
import disnake
print(disnake.ui.Container)
print(disnake.ui.TextDisplay)

View file

@ -0,0 +1,17 @@
import disnake
import asyncio
async def main():
class DummyResponse:
async def edit_message(self, *args, **kwargs):
print(kwargs)
inter = type('DummyInter', (), {'response': DummyResponse()})()
container = disnake.ui.Container()
try:
await inter.response.edit_message(components=[container])
print("Success")
except Exception as e:
print(f"Error: {e}")
asyncio.run(main())

216
bot.py
View file

@ -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()

View file

@ -1,6 +1,6 @@
import discord
from discord import app_commands
from discord.ext import commands
from disnake.ext import commands
import json
import os
@ -38,7 +38,7 @@ class Blacklist(commands.Cog):
# ---------- Slash Commands ----------
@app_commands.command(name="blacklist", description="Blacklist un utilisateur")
@app_commands.describe(user="Utilisateur à blacklister", reason="Raison")
async def blacklist_slash(self, interaction: discord.Interaction, user: discord.User, reason: str = "Aucune raison fournie"):
async def blacklist_slash(self, interaction: disnake.Interaction, user: disnake.User, reason: str = "Aucune raison fournie"):
if not self.is_agent(interaction.user.id):
return await interaction.response.send_message("⛔ Tu nas pas la permission.", ephemeral=True)
@ -55,7 +55,7 @@ class Blacklist(commands.Cog):
@app_commands.command(name="unblacklist", description="Retire un utilisateur de la blacklist")
@app_commands.describe(user="Utilisateur à retirer")
async def unblacklist_slash(self, interaction: discord.Interaction, user: discord.User):
async def unblacklist_slash(self, interaction: disnake.Interaction, user: disnake.User):
if not self.is_agent(interaction.user.id):
return await interaction.response.send_message("⛔ Tu nas pas la permission.", ephemeral=True)
@ -81,13 +81,13 @@ class Blacklist(commands.Cog):
pass
# --------- Boutons ----------
class TicketButton(discord.ui.View):
class TicketButton(disnake.ui.View):
def __init__(self, user_id: int):
super().__init__(timeout=None)
self.user_id = user_id
@discord.ui.button(label="📩 Ouvrir une demande de déblacklist", style=discord.ButtonStyle.danger)
async def open_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="📩 Ouvrir une demande de déblacklist", style=disnake.ButtonStyle.danger)
async def open_ticket(self, button: disnake.ui.Button, interaction: disnake.Interaction):
if interaction.user.id != self.user_id:
return await interaction.response.send_message("❌ Ce bouton ne tappartient pas.", ephemeral=True)
@ -110,13 +110,13 @@ class TicketButton(discord.ui.View):
await interaction.response.send_message("✅ Ton ticket a été créé en MP avec le bot.", ephemeral=True)
class CloseTicketView(discord.ui.View):
class CloseTicketView(disnake.ui.View):
def __init__(self, user_id: int):
super().__init__(timeout=None)
self.user_id = user_id
@discord.ui.button(label="❌ Fermer le ticket", style=discord.ButtonStyle.danger)
async def close_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="❌ Fermer le ticket", style=disnake.ButtonStyle.danger)
async def close_ticket(self, button: disnake.ui.Button, interaction: disnake.Interaction):
agents = load_json(AGENTS_FILE, {"agents": []})
if str(interaction.user.id) not in agents["agents"]:
return await interaction.response.send_message("⛔ Seuls les agents Omega Kube peuvent fermer un ticket.", ephemeral=True)
@ -124,13 +124,13 @@ class CloseTicketView(discord.ui.View):
view = ConfirmCloseView(self.user_id)
await interaction.response.send_message("⚠️ Veux-tu vraiment fermer ce ticket ?", view=view, ephemeral=True)
class ConfirmCloseView(discord.ui.View):
class ConfirmCloseView(disnake.ui.View):
def __init__(self, user_id: int):
super().__init__(timeout=30)
self.user_id = user_id
@discord.ui.button(label="✅ Oui", style=discord.ButtonStyle.success)
async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="✅ Oui", style=disnake.ButtonStyle.success)
async def confirm(self, button: disnake.ui.Button, interaction: disnake.Interaction):
agents = load_json(AGENTS_FILE, {"agents": []})
if str(interaction.user.id) not in agents["agents"]:
return await interaction.response.send_message("⛔ Tu nas pas la permission.", ephemeral=True)
@ -142,12 +142,12 @@ class ConfirmCloseView(discord.ui.View):
await interaction.channel.send("✅ Ticket fermé.")
await interaction.channel.delete()
@discord.ui.button(label="❌ Non", style=discord.ButtonStyle.secondary)
async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="❌ Non", style=disnake.ButtonStyle.secondary)
async def cancel(self, button: disnake.ui.Button, interaction: disnake.Interaction):
await interaction.response.send_message("❌ Fermeture annulée.", ephemeral=True)
# --------- Setup Cog ----------
async def setup(bot: commands.Bot):
def setup(bot: commands.Bot):
cog = Blacklist(bot)
await bot.add_cog(cog)
await bot.tree.sync(guild=discord.Object(id=MAIN_GUILD_ID))
await bot.tree.sync(guild=disnake.Object(id=MAIN_GUILD_ID))

View file

@ -1,6 +1,5 @@
import discord
from discord.ext import commands, tasks
from discord import app_commands, ui
import disnake
from disnake.ext import commands, tasks
import json
import asyncio
import os
@ -82,22 +81,26 @@ def parse_date(date_str: str):
# --- MODALS & VIEWS ---
class AbsenceModal(ui.Modal, title="Déclarer une Absence Staff"):
motif = ui.TextInput(
class AbsenceModal(disnake.ui.Modal):
def __init__(self, superieur: Optional[disnake.Member] = None):
super().__init__(title="Déclarer une Absence Staff", components=[self.motif, self.date_debut, self.date_fin])
self.superieur = superieur
motif = disnake.ui.TextInput(
label="Motif de l'absence",
style=discord.TextStyle.paragraph,
style=disnake.TextInputStyle.paragraph,
placeholder="Ex: Vacances, Raisons personnelles, Travail...",
required=True,
max_length=500
)
date_debut = ui.TextInput(
date_debut = disnake.ui.TextInput(
label="Début (JJ/MM/AAAA HH:MM)",
placeholder="Ex: 15/05/2024 08:00",
min_length=16,
max_length=16,
required=True
)
date_fin = ui.TextInput(
date_fin = disnake.ui.TextInput(
label="Fin (JJ/MM/AAAA HH:MM)",
placeholder="Ex: 20/05/2024 18:00",
min_length=16,
@ -105,11 +108,7 @@ class AbsenceModal(ui.Modal, title="Déclarer une Absence Staff"):
required=True
)
def __init__(self, superieur: Optional[discord.Member] = None):
super().__init__()
self.superieur = superieur
async def on_submit(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.Interaction):
try:
debut = parse_date(self.date_debut.value)
fin = parse_date(self.date_fin.value)
@ -182,20 +181,20 @@ class AbsenceModal(ui.Modal, title="Déclarer une Absence Staff"):
await interaction.response.send_message("✅ Absence enregistrée avec succès !", ephemeral=True)
class AbsenceConfigView(ui.View):
class AbsenceConfigView(disnake.ui.View):
def __init__(self, guild_id: int):
super().__init__(timeout=60)
self.guild_id = guild_id
@ui.button(label="Définir le Salon", style=discord.ButtonStyle.primary, emoji="📁")
async def set_channel(self, interaction: discord.Interaction, button: ui.Button):
@disnake.ui.button(label="Définir le Salon", style=disnake.ButtonStyle.primary, emoji="📁")
async def set_channel(self, button: disnake.ui.Button, interaction: disnake.Interaction):
await interaction.response.send_message("Mentionne le salon ici (ou envoie son ID) :", ephemeral=True)
def check(m):
return m.author == interaction.user and m.channel == interaction.channel
try:
msg = await interaction.client.wait_for('message', check=check, timeout=30)
msg = await interaction.bot.wait_for('message', check=check, timeout=30)
channel = None
if msg.channel_mentions:
channel = msg.channel_mentions[0]
@ -214,15 +213,15 @@ class AbsenceConfigView(ui.View):
except asyncio.TimeoutError:
await interaction.followup.send("❌ Temps écoulé.", ephemeral=True)
@ui.button(label="Définir le Rôle", style=discord.ButtonStyle.primary, emoji="🎭")
async def set_role(self, interaction: discord.Interaction, button: ui.Button):
@disnake.ui.button(label="Définir le Rôle", style=disnake.ButtonStyle.primary, emoji="🎭")
async def set_role(self, button: disnake.ui.Button, interaction: disnake.Interaction):
await interaction.response.send_message("Mentionne le rôle ici (ou envoie son ID) :", ephemeral=True)
def check(m):
return m.author == interaction.user and m.channel == interaction.channel
try:
msg = await interaction.client.wait_for('message', check=check, timeout=30)
msg = await interaction.bot.wait_for('message', check=check, timeout=30)
role = None
if msg.role_mentions:
role = msg.role_mentions[0]
@ -246,22 +245,22 @@ class Absence(commands.Cog):
self.bot = bot
self.check_absences.start()
@app_commands.command(name="absence", description="Déclarer une absence staff")
@app_commands.describe(superieur="Le supérieur hiérarchique à prévenir")
async def absence(self, interaction: discord.Interaction, superieur: Optional[discord.Member] = None):
@commands.slash_command(name="absence", description="Déclarer une absence staff")
async def absence(self, interaction: disnake.ApplicationCommandInteraction,
superieur: Optional[disnake.Member] = commands.Param(None, description="Le supérieur hiérarchique à prévenir")):
"""Déclare une absence via un formulaire"""
await interaction.response.send_modal(AbsenceModal(superieur))
@app_commands.command(name="absence_config", description="Configurer le système d'absence (Admin)")
@app_commands.checks.has_permissions(administrator=True)
async def absence_config(self, interaction: discord.Interaction):
@commands.slash_command(name="absence_config", description="Configurer le système d'absence (Admin)")
@commands.has_permissions(administrator=True)
async def absence_config(self, interaction: disnake.ApplicationCommandInteraction):
"""Ouvre le panneau de configuration des absences"""
settings = load_settings(interaction.guild.id)
embed = discord.Embed(
embed = disnake.Embed(
title="⚙️ Configuration des Absences Staff",
description="Utilisez les boutons ci-dessous pour configurer le système.",
color=discord.Color.blue()
color=disnake.Color.blue()
)
channel_id = settings.get("absence_channel_id")
@ -280,7 +279,7 @@ class Absence(commands.Cog):
await interaction.response.send_message(embed=embed, view=AbsenceConfigView(interaction.guild.id), ephemeral=True)
async def strike_absence_message(self, guild: discord.Guild, channel_id: int, message_id: int):
async def strike_absence_message(self, guild: disnake.Guild, channel_id: int, message_id: int):
"""Modifie le message d'absence pour le barrer au lieu de le supprimer"""
channel = guild.get_channel(channel_id)
if not channel:
@ -311,8 +310,8 @@ class Absence(commands.Cog):
except Exception as e:
print(f"Erreur lors du barrage du message d'absence: {e}")
@app_commands.command(name="fin_absence", description="Terminer son absence staff")
async def fin_absence(self, interaction: discord.Interaction):
@commands.slash_command(name="fin_absence", description="Terminer son absence staff")
async def fin_absence(self, interaction: disnake.ApplicationCommandInteraction):
"""Met fin à l'absence en cours"""
all_abs = await load_absences()
guild_abs = get_guild_absences(all_abs, interaction.guild.id)
@ -343,8 +342,8 @@ class Absence(commands.Cog):
await save_absences(all_abs)
await interaction.response.send_message("✅ Ton absence a été retirée.", ephemeral=True)
@app_commands.command(name="liste_absences", description="Afficher la liste des absences staff en cours")
async def liste_absences(self, interaction: discord.Interaction):
@commands.slash_command(name="liste_absences", description="Afficher la liste des absences staff en cours")
async def liste_absences(self, interaction: disnake.ApplicationCommandInteraction):
"""Affiche les absences en cours sur le serveur"""
all_abs = await load_absences()
guild_abs = get_guild_absences(all_abs, interaction.guild.id)
@ -360,10 +359,10 @@ class Absence(commands.Cog):
per_page = 5
pages = [absences[i:i + per_page] for i in range(0, len(absences), per_page)]
def create_embed(page_index: int) -> discord.Embed:
embed = discord.Embed(
def create_embed(page_index: int) -> disnake.Embed:
embed = disnake.Embed(
title=f"📋 Absences staff - {interaction.guild.name} ({page_index + 1}/{len(pages)})",
color=discord.Color.orange()
color=disnake.Color.orange()
)
for user_id, info in pages[page_index]:
embed.add_field(
@ -378,19 +377,19 @@ class Absence(commands.Cog):
embed.set_footer(text="Gestion des absences du staff")
return embed
class PaginatorView(ui.View):
class PaginatorView(disnake.ui.View):
def __init__(self):
super().__init__(timeout=60)
self.page = 0
@ui.button(label="⬅️ Précédent", style=discord.ButtonStyle.secondary, disabled=True)
async def previous(self, interaction_btn: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="⬅️ Précédent", style=disnake.ButtonStyle.secondary, disabled=True)
async def previous(self, interaction_btn: disnake.Interaction, button: disnake.ui.Button):
self.page -= 1
self.update_buttons()
await interaction_btn.response.edit_message(embed=create_embed(self.page), view=self)
@ui.button(label="➡️ Suivant", style=discord.ButtonStyle.secondary, disabled=len(pages) == 1)
async def next(self, interaction_btn: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="➡️ Suivant", style=disnake.ButtonStyle.secondary, disabled=len(pages) == 1)
async def next(self, interaction_btn: disnake.Interaction, button: disnake.ui.Button):
self.page += 1
self.update_buttons()
await interaction_btn.response.edit_message(embed=create_embed(self.page), view=self)
@ -500,7 +499,7 @@ class Absence(commands.Cog):
settings = load_settings(message.guild.id)
channel_id = settings.get("absence_channel_id")
absence_url = f"https://discord.com/channels/{message.guild.id}/{channel_id}/{msg_id}" if channel_id and msg_id else ""
absence_url = f"https://disnake.com/channels/{message.guild.id}/{channel_id}/{msg_id}" if channel_id and msg_id else ""
reply_text = f"⚠️ {message.author.mention}, cet utilisateur est actuellement absent."
if absence_url:
@ -508,5 +507,5 @@ class Absence(commands.Cog):
await message.reply(reply_text, delete_after=30)
async def setup(bot):
await bot.add_cog(Absence(bot))
def setup(bot):
bot.add_cog(Absence(bot))

View file

@ -7,9 +7,8 @@ import asyncio
from datetime import datetime
from typing import Dict, Any, List
import discord
from discord.ext import commands
from discord import app_commands
import disnake
from disnake.ext import commands
BACKUPS_ROOT = "data/security_backups" # dossier de sortie des sauvegardes
@ -46,10 +45,10 @@ def estimate_duration_seconds(
return int(meta_cost + msg_cost + base_overhead)
def serialize_overwrites(channel: discord.abc.GuildChannel) -> List[Dict[str, Any]]:
def serialize_overwrites(channel: disnake.abc.GuildChannel) -> List[Dict[str, Any]]:
serialized: List[Dict[str, Any]] = []
for target, perms in channel.overwrites.items():
target_type = "role" if isinstance(target, discord.Role) else "member"
target_type = "role" if isinstance(target, disnake.Role) else "member"
serialized.append({
"target_type": target_type,
"target_id": target.id,
@ -64,20 +63,17 @@ class Backup(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
backup_group = app_commands.Group(name="backup", description="Outils de sauvegarde du serveur")
@commands.slash_command(name="backup", description="Outils de sauvegarde du serveur")
async def backup_group(self, interaction: disnake.ApplicationCommandInteraction):
pass
@backup_group.command(name="create", description="Créer une sauvegarde du serveur")
@app_commands.describe(
save_messages="Inclure l'export des messages",
privacy_notice="Afficher un avertissement public si messages sauvegardés",
message_limit_per_channel="Nombre max de messages à exporter par salon (0 = tous)"
)
@backup_group.sub_command(name="create", description="Créer une sauvegarde du serveur")
async def backup_create(
self,
interaction: discord.Interaction,
save_messages: bool = True,
privacy_notice: bool = True,
message_limit_per_channel: int = 10000
interaction: disnake.ApplicationCommandInteraction,
save_messages: bool = commands.Param(True, description="Inclure l'export des messages"),
privacy_notice: bool = commands.Param(True, description="Afficher un avertissement public si messages sauvegardés"),
message_limit_per_channel: int = commands.Param(10000, description="Nombre max de messages à exporter par salon (0 = tous)")
):
guild = interaction.guild
assert guild is not None, "Cette commande doit être utilisée dans un serveur."
@ -93,20 +89,20 @@ class Backup(commands.Cog):
)
return
# Déferre l'interaction
await interaction.response.defer(ephemeral=True, thinking=True)
# Deferre l'interaction
await interaction.response.defer(ephemeral=True)
# Avertissement de confidentialité
if privacy_notice and save_messages:
try:
await interaction.channel.send(
embed=discord.Embed(
embed=disnake.Embed(
title="📢 Information de confidentialité (transparence)",
description=(
"Une **sauvegarde du serveur** est en cours et **inclus les messages**.\n"
"Vous êtes dans l'obligation de prévenir les membres."
),
color=discord.Color.orange()
color=disnake.Color.orange()
)
)
except Exception:
@ -132,14 +128,14 @@ class Backup(commands.Cog):
message_limit_per_channel if message_limit_per_channel else 100000
)
prog_embed = discord.Embed(
prog_embed = disnake.Embed(
title="🗄️ Sauvegarde en cours",
description=(
f"**Serveur :** {guild.name} (`{guild.id}`)\n"
f"**Estimation :** ~{estimation_s} secondes\n"
f"**Options** : messages={'oui' if save_messages else 'non'} • limite/chan={message_limit_per_channel or 'tous'}"
),
color=discord.Color.blurple()
color=disnake.Color.blurple()
)
prog_embed.add_field(name="Progression", value=progress_bar(0.0), inline=False)
progress_msg = await interaction.followup.send(embed=prog_embed, ephemeral=False)
@ -173,11 +169,11 @@ class Backup(commands.Cog):
chans_dump=[]
for ch in guild.channels:
base={"id":ch.id,"name":ch.name,"type":str(ch.type),"position":ch.position,"category_id":ch.category.id if getattr(ch,"category",None) else None,"overwrites":serialize_overwrites(ch)}
if isinstance(ch,discord.TextChannel):
if isinstance(ch,disnake.TextChannel):
base.update({"topic":ch.topic,"nsfw":ch.nsfw,"slowmode_delay":ch.slowmode_delay,"default_auto_archive_duration":ch.default_auto_archive_duration})
elif isinstance(ch,discord.VoiceChannel):
elif isinstance(ch,disnake.VoiceChannel):
base.update({"bitrate":ch.bitrate,"user_limit":ch.user_limit,"rtc_region":str(ch.rtc_region) if ch.rtc_region else None})
elif isinstance(ch,discord.ForumChannel):
elif isinstance(ch,disnake.ForumChannel):
base.update({"nsfw":ch.nsfw,"default_auto_archive_duration":ch.default_auto_archive_duration})
chans_dump.append(base)
with open(os.path.join(base_dir,"channels.json"),"w",encoding="utf-8") as f:
@ -208,7 +204,7 @@ class Backup(commands.Cog):
data={"id":msg.id,"channel_id":msg.channel.id,"author_id":msg.author.id,"author_bot":msg.author.bot,"created_at":msg.created_at.isoformat(),"content":msg.content,"attachments":[a.url for a in msg.attachments],"embeds":[e.to_dict() for e in msg.embeds],"reference":{"message_id":getattr(msg.reference,"message_id",None),"channel_id":getattr(msg.reference,"channel_id",None)} if msg.reference else None}
f.write(json.dumps(data,ensure_ascii=False)+"\n")
count+=1
except discord.Forbidden: pass
except disnake.Forbidden: pass
except Exception: pass
exported_total+=count
done+=per_chan_weight
@ -226,7 +222,7 @@ class Backup(commands.Cog):
rel=os.path.relpath(full,base_dir)
zf.write(full,rel)
except Exception as exc:
prog_embed.color=discord.Color.red()
prog_embed.color=disnake.Color.red()
prog_embed.set_field_at(0,name="Progression",value=progress_bar(1.0),inline=False)
prog_embed.set_footer(text=f"Erreur lors de la création du ZIP : {exc}")
await progress_msg.edit(embed=prog_embed)
@ -234,15 +230,15 @@ class Backup(commands.Cog):
return
# Finalisation
prog_embed.color=discord.Color.green()
prog_embed.color=disnake.Color.green()
prog_embed.set_field_at(0,name="Progression",value=progress_bar(1.0),inline=False)
prog_embed.add_field(name="Archive",value=f"`{zip_path}`",inline=False)
prog_embed.set_footer(text="Sauvegarde terminée ✅")
await progress_msg.edit(embed=prog_embed)
await interaction.followup.send("🎉 Sauvegarde terminée. Un message de progression avec le chemin de larchive a été publié.", ephemeral=True)
@backup_group.command(name="list", description="Lister les archives de sauvegarde disponibles")
async def backup_list(self, interaction: discord.Interaction):
@backup_group.sub_command(name="list", description="Lister les archives de sauvegarde disponibles")
async def backup_list(self, interaction: disnake.ApplicationCommandInteraction):
guild = interaction.guild
assert guild is not None, "Cette commande doit être utilisée dans un serveur."
@ -256,7 +252,7 @@ class Backup(commands.Cog):
)
return
await interaction.response.defer(ephemeral=True, thinking=True)
await interaction.response.defer(ephemeral=True)
ensure_dir(BACKUPS_ROOT)
files = [f for f in os.listdir(BACKUPS_ROOT) if f.endswith(".zip")]
files.sort(reverse=True)
@ -266,11 +262,11 @@ class Backup(commands.Cog):
return
desc="\n".join(f"• `{name}`" for name in files[:20])
embed=discord.Embed(title="📦 Archives disponibles",description=desc,color=discord.Color.blurple())
embed=disnake.Embed(title="📦 Archives disponibles",description=desc,color=disnake.Color.blurple())
if len(files)>20:
embed.set_footer(text=f"... et {len(files)-20} de plus")
await interaction.followup.send(embed=embed,ephemeral=True)
async def setup(bot: commands.Bot):
await bot.add_cog(Backup(bot))
def setup(bot):
bot.add_cog(Backup(bot))

View file

@ -5,9 +5,8 @@ import zipfile
import tempfile
import shutil
from typing import List
import discord
from discord import app_commands
from discord.ext import commands
import disnake
from disnake.ext import commands
BACKUPS_ROOT = "data/security_backups"
@ -43,7 +42,7 @@ class BackupRestore(commands.Cog):
elements.append(base)
return elements
async def element_autocomplete(self, interaction: discord.Interaction, current: str):
async def element_autocomplete(self, interaction: disnake.ApplicationCommandInteraction, current: str):
"""Complétion automatique pour l'option 'element'."""
guild = interaction.guild
if guild is None:
@ -60,9 +59,9 @@ class BackupRestore(commands.Cog):
backup_path = os.path.join(BACKUPS_ROOT, last_backup)
elements = await self.list_backup_elements(backup_path)
return [app_commands.Choice(name=e, value=e) for e in elements if current.lower() in e.lower()][:25]
return [e for e in elements if current.lower() in e.lower()][:25]
async def restore_roles(self, guild: discord.Guild, backup_path: str):
async def restore_roles(self, guild: disnake.Guild, backup_path: str):
"""Restaure les rôles à partir du backup."""
roles_file = os.path.join(backup_path, "roles.json")
if os.path.isfile(roles_file):
@ -72,16 +71,16 @@ class BackupRestore(commands.Cog):
try:
await guild.create_role(
name=r["name"],
permissions=discord.Permissions(r["permissions"]),
color=discord.Color(r.get("color", 0)),
permissions=disnake.Permissions(r["permissions"]),
color=disnake.Color(r.get("color", 0)),
hoist=r.get("hoist", False),
mentionable=r.get("mentionable", False),
reason="Restauration backup"
)
except discord.Forbidden:
except disnake.Forbidden:
continue
async def restore_channels(self, guild: discord.Guild, backup_path: str):
async def restore_channels(self, guild: disnake.Guild, backup_path: str):
"""Restaure les channels à partir du backup."""
channels_file = os.path.join(backup_path, "channels.json")
if os.path.isfile(channels_file):
@ -105,10 +104,10 @@ class BackupRestore(commands.Cog):
user_limit=c.get("user_limit", 0),
reason="Restauration backup"
)
except discord.Forbidden:
except disnake.Forbidden:
continue
async def restore_messages(self, channel: discord.TextChannel, backup_path: str):
async def restore_messages(self, channel: disnake.TextChannel, backup_path: str):
"""Restaure les messages à partir du backup (solution ZIP incluse)."""
if backup_path.endswith(".zip"):
temp_dir = tempfile.mkdtemp()
@ -133,17 +132,12 @@ class BackupRestore(commands.Cog):
for msg in messages:
await channel.send(msg["content"])
@app_commands.command(name="granulaire", description="Restaure un élément précis d'une sauvegarde")
@app_commands.describe(
backup="Nom ou ID de la sauvegarde",
element="Élément à restaurer (roles, channels, messages...)"
)
@app_commands.autocomplete(element=element_autocomplete)
@commands.slash_command(name="granulaire", description="Restaure un élément précis d'une sauvegarde")
async def restore_granulaire(
self,
interaction: discord.Interaction,
backup: str,
element: str
interaction: disnake.ApplicationCommandInteraction,
backup: str = commands.Param(description="Nom ou ID de la sauvegarde"),
element: str = commands.Param(description="Élément à restaurer (roles, channels, messages...)")
):
guild = interaction.guild
if guild is None:
@ -162,7 +156,7 @@ class BackupRestore(commands.Cog):
await interaction.response.send_message(f"❌ La sauvegarde `{backup}` est introuvable.", ephemeral=True)
return
await interaction.response.defer(ephemeral=True, thinking=True)
await interaction.response.defer(ephemeral=True)
available_elements = await self.list_backup_elements(backup_path)
if element not in available_elements:
@ -200,5 +194,9 @@ class BackupRestore(commands.Cog):
ephemeral=True
)
async def setup(bot: commands.Bot):
await bot.add_cog(BackupRestore(bot))
@restore_granulaire.autocomplete("element")
async def element_autocomplete_decorator(self, interaction: disnake.ApplicationCommandInteraction, current: str):
return await self.element_autocomplete(interaction, current)
def setup(bot):
bot.add_cog(BackupRestore(bot))

View file

@ -1,6 +1,5 @@
import discord
from discord.ext import commands, tasks
from discord import app_commands
import disnake
from disnake.ext import commands, tasks
import json
import os
import asyncio
@ -57,8 +56,8 @@ class Blacklist(commands.Cog):
agents = load_json(AGENTS_FILE, {"agents": []})
return str(user_id) in agents["agents"]
@app_commands.command(name="blacklist", description="Blacklist un utilisateur")
async def blacklist(self, interaction: discord.Interaction, user: discord.User, reason: str = "Aucune raison fournie"):
@commands.slash_command(name="blacklist", description="Blacklist un utilisateur")
async def blacklist(self, interaction: disnake.ApplicationCommandInteraction, user: disnake.User, reason: str = "Aucune raison fournie"):
if not self.is_agent(interaction.user.id):
return await interaction.response.send_message("⛔ Tu nas pas la permission.", ephemeral=True)
@ -70,7 +69,7 @@ class Blacklist(commands.Cog):
save_json(BLACKLIST_FILE, blacklist)
# Préparer l'embed pro
embed = discord.Embed(
embed = disnake.Embed(
title="🚫 Mise en blacklist - Réseau Omega Kube",
description=(
f"Bonjour {user.mention},\n\n"
@ -82,7 +81,7 @@ class Blacklist(commands.Cog):
"📩 Vous disposez de **7 jours** pour contester cette décision en ouvrant un ticket via le bouton ci-dessous.\n\n"
"⏳ Passé ce délai, vous serez **définitivement banni de tout le réseau**, sans autre recours possible."
),
color=discord.Color.red()
color=disnake.Color.red()
)
embed.set_footer(text="Omega Kube - Nixfix06 & Mgstudios")
@ -122,8 +121,8 @@ class Blacklist(commands.Cog):
ephemeral=True
)
@app_commands.command(name="unblacklist", description="Retirer un utilisateur de la blacklist")
async def unblacklist(self, interaction: discord.Interaction, user: discord.User):
@commands.slash_command(name="unblacklist", description="Retirer un utilisateur de la blacklist")
async def unblacklist(self, interaction: disnake.ApplicationCommandInteraction, user: disnake.User):
if not self.is_agent(interaction.user.id):
return await interaction.response.send_message("⛔ Tu nas pas la permission.", ephemeral=True)
@ -163,21 +162,21 @@ class Blacklist(commands.Cog):
save_json(BLACKLIST_FILE, blacklist)
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
async def on_message(self, message: disnake.Message):
if message.author.bot:
return
tickets = load_json(TICKETS_FILE, {})
if isinstance(message.channel, discord.DMChannel):
if isinstance(message.channel, disnake.DMChannel):
user_id = str(message.author.id)
if user_id in tickets:
guild = self.bot.get_guild(MAIN_GUILD_ID)
ticket_channel = guild.get_channel(tickets[user_id]["channel_id"])
if ticket_channel:
embed = discord.Embed(
embed = disnake.Embed(
description=message.content,
color=discord.Color.blue()
color=disnake.Color.blue()
).set_author(name=message.author, icon_url=message.author.display_avatar.url)
await ticket_channel.send(embed=embed)
@ -186,22 +185,22 @@ class Blacklist(commands.Cog):
if message.channel.id == data["channel_id"] and self.is_agent(message.author.id):
user = await self.bot.fetch_user(int(user_id))
try:
embed = discord.Embed(
embed = disnake.Embed(
description=message.content,
color=discord.Color.green()
color=disnake.Color.green()
).set_author(name=f"Agent {message.author}", icon_url=message.author.display_avatar.url)
await user.send(embed=embed)
except:
pass
# --------- Boutons ---------
class TicketButton(discord.ui.View):
class TicketButton(disnake.ui.View):
def __init__(self, user_id: int):
super().__init__(timeout=None)
self.user_id = user_id
@discord.ui.button(label="📩 Ouvrir une demande de révision", style=discord.ButtonStyle.primary, custom_id="blacklist_open_ticket")
async def open_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="📩 Ouvrir une demande de révision", style=disnake.ButtonStyle.primary, custom_id="blacklist_open_ticket")
async def open_ticket(self, button: disnake.ui.Button, interaction: disnake.Interaction):
if interaction.user.id != self.user_id:
return await interaction.response.send_message("❌ Ce bouton ne tappartient pas.", ephemeral=True)
@ -209,7 +208,7 @@ class TicketButton(discord.ui.View):
if str(self.user_id) in tickets:
return await interaction.response.send_message("❌ Tu as déjà un ticket ouvert.", ephemeral=True)
guild = interaction.client.get_guild(MAIN_GUILD_ID)
guild = interaction.bot.get_guild(MAIN_GUILD_ID)
category = guild.get_channel(TICKET_CATEGORY_ID)
ticket_channel = await guild.create_text_channel(
@ -222,10 +221,10 @@ class TicketButton(discord.ui.View):
save_json(TICKETS_FILE, tickets)
view_close = CloseTicketView(self.user_id)
embed = discord.Embed(
embed = disnake.Embed(
title="📩 Ticket ouvert",
description=f"Un utilisateur blacklisté (<@{self.user_id}>) a ouvert une demande de révision.\n\nUn agent Omega Kube prendra en charge ce dossier.",
color=discord.Color.blurple()
color=disnake.Color.blurple()
)
await ticket_channel.send(embed=embed, view=view_close)
@ -234,27 +233,27 @@ class TicketButton(discord.ui.View):
button.disabled = True
self.stop()
class CloseTicketView(discord.ui.View):
class CloseTicketView(disnake.ui.View):
def __init__(self, user_id: int):
super().__init__(timeout=None)
self.user_id = user_id
@discord.ui.button(label="❌ Fermer le ticket", style=discord.ButtonStyle.danger, custom_id="blacklist_close_ticket")
async def close_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
if not interaction.client.get_cog("Blacklist").is_agent(interaction.user.id):
@disnake.ui.button(label="❌ Fermer le ticket", style=disnake.ButtonStyle.danger, custom_id="blacklist_close_ticket")
async def close_ticket(self, button: disnake.ui.Button, interaction: disnake.Interaction):
if not interaction.bot.get_cog("Blacklist").is_agent(interaction.user.id):
return await interaction.response.send_message("⛔ Seuls les agents peuvent fermer ce ticket.", ephemeral=True)
view = ConfirmCloseView(self.user_id)
await interaction.response.send_message("⚠️ Veux-tu vraiment fermer ce ticket ?", view=view, ephemeral=True)
class ConfirmCloseView(discord.ui.View):
class ConfirmCloseView(disnake.ui.View):
def __init__(self, user_id: int):
super().__init__(timeout=30)
self.user_id = user_id
@discord.ui.button(label="✅ Oui", style=discord.ButtonStyle.success, custom_id="blacklist_confirm_close")
async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button):
if not interaction.client.get_cog("Blacklist").is_agent(interaction.user.id):
@disnake.ui.button(label="✅ Oui", style=disnake.ButtonStyle.success, custom_id="blacklist_confirm_close")
async def confirm(self, button: disnake.ui.Button, interaction: disnake.Interaction):
if not interaction.bot.get_cog("Blacklist").is_agent(interaction.user.id):
return await interaction.response.send_message("⛔ Tu nas pas la permission.", ephemeral=True)
tickets = load_json(TICKETS_FILE, {})
@ -268,19 +267,19 @@ class ConfirmCloseView(discord.ui.View):
del tickets[str(self.user_id)]
save_json(TICKETS_FILE, tickets)
user = await interaction.client.fetch_user(int(self.user_id))
user = await interaction.bot.fetch_user(int(self.user_id))
try:
embed = discord.Embed(
embed = disnake.Embed(
title="🔒 Ticket fermé",
description="Ton ticket a été examiné et fermé.\n\n⚠️ Après analyse, la sanction est confirmée et tu restes blacklisté de lensemble du réseau.",
color=discord.Color.orange()
color=disnake.Color.orange()
)
await user.send(embed=embed)
await asyncio.sleep(2)
except:
pass
for guild in interaction.client.guilds:
for guild in interaction.bot.guilds:
member = guild.get_member(self.user_id)
if member:
try:
@ -290,10 +289,10 @@ class ConfirmCloseView(discord.ui.View):
await interaction.response.send_message("✅ Ticket fermé avec succès.", ephemeral=True)
@discord.ui.button(label="❌ Non", style=discord.ButtonStyle.secondary, custom_id="blacklist_cancel_close")
async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="❌ Non", style=disnake.ButtonStyle.secondary, custom_id="blacklist_cancel_close")
async def cancel(self, button: disnake.ui.Button, interaction: disnake.Interaction):
await interaction.response.send_message("❌ Fermeture annulée.", ephemeral=True)
# --------- Setup ---------
async def setup(bot: commands.Bot):
await bot.add_cog(Blacklist(bot))
def setup(bot):
bot.add_cog(Blacklist(bot))

View file

@ -1,6 +1,5 @@
import discord
from discord import app_commands
from discord.ext import commands
import disnake
from disnake.ext import commands
from utils.gitlab_client import gitlab_client
from aiohttp import web
import logging
@ -33,27 +32,27 @@ def save_report(issue_iid, user_id):
except Exception as e:
kuby_logger.error(f"Error saving report to JSON: {e}")
class BugReportModal(discord.ui.Modal, title="Signaler un Bug"):
class BugReportModal(disnake.ui.Modal):
def __init__(self, priority_choice):
super().__init__()
super().__init__(title="Signaler un Bug", components=[self.bug_title, self.description])
self.priority_choice = priority_choice
bug_title = discord.ui.TextInput(
bug_title = disnake.ui.TextInput(
label="Titre du bug",
placeholder="Décrivez brièvement le problème...",
required=True,
max_length=100
)
description = discord.ui.TextInput(
description = disnake.ui.TextInput(
label="Description détaillée",
style=discord.TextStyle.paragraph,
style=disnake.TextInputStyle.paragraph,
placeholder="Que s'est-il passé ? Comment reproduire le bug ?",
required=True,
max_length=1000
)
async def on_submit(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.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"
@ -76,23 +75,26 @@ class BugReportModal(discord.ui.Modal, title="Signaler un Bug"):
else:
await interaction.edit_original_response(content="❌ Une erreur est survenue lors de l'envoi du rapport à GitLab. Veuillez contacter un administrateur.")
class FeatureSuggestionModal(discord.ui.Modal, title="Suggérer une fonctionnalité"):
suggestion_title = discord.ui.TextInput(
class FeatureSuggestionModal(disnake.ui.Modal):
def __init__(self):
super().__init__(title="Suggérer une fonctionnalité", components=[self.suggestion_title, self.description])
suggestion_title = disnake.ui.TextInput(
label="Titre de la suggestion",
placeholder="Que voulez-vous ajouter ?",
required=True,
max_length=100
)
description = discord.ui.TextInput(
description = disnake.ui.TextInput(
label="Détails de la fonctionnalité",
style=discord.TextStyle.paragraph,
style=disnake.TextInputStyle.paragraph,
placeholder="Décrivez comment cela devrait fonctionner...",
required=True,
max_length=1000
)
async def on_submit(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.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"
@ -230,24 +232,31 @@ class BugReport(commands.Cog):
current_labels_str = "Mis à jour"
try:
# Création de l'embed pour la mise à jour de statut
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)
# V2 Components migration
components = [
disnake.ui.Container(
disnake.ui.Section(
"🛠️ Mise à jour de votre signalement !",
accessory=disnake.ui.Thumbnail(self.bot.user.display_avatar.url)
),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(f"Le statut de votre rapport **{issue_title}** a été mis à jour par l'équipe."),
disnake.ui.TextDisplay(f"📌 **Nouveaux Labels / Statuts :** {current_labels_str}")
)
]
try:
await user.send(embed=embed)
# On logue systématiquement pour que le staff sache si ça a marché (utile en prod)
await user.send(components=components)
kuby_logger.info(f"Notification de label envoyée à {user} ({user.id}) pour l'issue #{issue_iid}")
if dev and dev.id != user.id:
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}).")
except discord.Forbidden:
kuby_logger.warning(f"Impossible d'envoyer un MP à {user} ({user.id}) - MPs désactivés.")
if dev and dev.id != user.id:
await dev.send(f"⚠️ [DM BLOQUÉ] {user} ({user.id}) a ses MPs fermés. Impossible de notifier pour l'issue #{issue_iid}.")
except (disnake.Forbidden, disnake.HTTPException) as e:
if isinstance(e, disnake.Forbidden) or (isinstance(e, disnake.HTTPException) and e.code == 50007):
kuby_logger.warning(f"Impossible d'envoyer un MP à {user} ({user.id}) - MPs désactivés (Code: {getattr(e, 'code', 'Unknown')})")
if dev and dev.id != user.id:
await dev.send(f"⚠️ [DM BLOQUÉ] {user} ({user.id}) a ses MPs fermés. Impossible de notifier pour l'issue #{issue_iid}.")
else:
kuby_logger.error(f"Erreur HTTP lors de l'envoi du MP de statut à {user}: {e}")
except Exception as e:
kuby_logger.error(f"Erreur lors de l'envoi du MP de statut à {user}: {e}")
except Exception as e:
@ -259,23 +268,31 @@ class BugReport(commands.Cog):
if is_closing_now:
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 !")
components = [
disnake.ui.Container(
disnake.ui.Section(
"🛠️ Bug / Suggestion Terminé(e) !",
accessory=disnake.ui.Thumbnail(self.bot.user.display_avatar.url)
),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(f"Bonne nouvelle ! Votre signalement **{issue_title}** a été marqué comme terminé ou résolu."),
disnake.ui.TextDisplay("🚀 **Prochaine étape :** La mise à jour arrive prochainement (ou est déjà là) !"),
disnake.ui.TextDisplay("✨ Merci pour votre aide !")
)
]
try:
await user.send(embed=embed)
await user.send(components=components)
kuby_logger.info(f"Notification de clôture envoyée à {user} ({user.id}) pour l'issue #{issue_iid}")
if dev and dev.id != user.id:
await dev.send(f"✅ [SUIVI] 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"Impossible d'envoyer le MP de clôture à {user} ({user.id}).")
if dev and dev.id != user.id:
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 (disnake.Forbidden, disnake.HTTPException) as e:
if isinstance(e, disnake.Forbidden) or (isinstance(e, disnake.HTTPException) and e.code == 50007):
kuby_logger.warning(f"Impossible d'envoyer le MP de clôture à {user} ({user.id}).")
if dev and dev.id != user.id:
await dev.send(f"⚠️ [DM BLOQUÉ] Impossible d'annoncer la clôture à {user} ({user.id}) pour l'issue #{issue_iid} (DMs désactivés).")
else:
kuby_logger.error(f"Erreur HTTP lors de l'envoi du MP de clôture à {user}: {e}")
except Exception as e:
kuby_logger.error(f"Erreur lors de l'envoi du MP de clôture à {user}: {e}")
except Exception as e:
@ -290,22 +307,32 @@ class BugReport(commands.Cog):
author_name = payload.get("user", {}).get("name", "Développeur")
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}")
components = [
disnake.ui.Container(
disnake.ui.Section(
"💬 Nouveau commentaire sur votre signalement",
accessory=disnake.ui.Thumbnail(self.bot.user.display_avatar.url)
),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(f"Le développeur a répondu à votre rapport **{issue_title}** :"),
disnake.ui.TextDisplay(f">>> {note_body}"),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(f"✍️ **Par :** {author_name}")
)
]
try:
await user.send(embed=embed)
await user.send(components=components)
kuby_logger.info(f"Commentaire GitLab envoyé à {user} ({user.id}) pour l'issue #{issue_iid}")
if dev and dev.id != user.id:
await dev.send(f"✅ [SUIVI] L'utilisateur {user} ({user.id}) a bien reçu votre commentaire pour l'issue #{issue_iid}.")
except discord.Forbidden:
kuby_logger.warning(f"Impossible d'envoyer le commentaire en MP à {user} ({user.id}).")
if dev and dev.id != user.id:
await dev.send(f"⚠️ [DM BLOQUÉ] Impossible d'envoyer votre commentaire à {user} ({user.id}) pour l'issue #{issue_iid} (DMs désactivés).")
except (disnake.Forbidden, disnake.HTTPException) as e:
if isinstance(e, disnake.Forbidden) or (isinstance(e, disnake.HTTPException) and e.code == 50007):
kuby_logger.warning(f"Impossible d'envoyer le commentaire en MP à {user} ({user.id}).")
if dev and dev.id != user.id:
await dev.send(f"⚠️ [DM BLOQUÉ] Impossible d'envoyer votre commentaire à {user} ({user.id}) pour l'issue #{issue_iid} (DMs désactivés).")
else:
kuby_logger.error(f"Erreur HTTP lors de l'envoi du MP de commentaire à {user}: {e}")
except Exception as e:
kuby_logger.error(f"Erreur lors de l'envoi du MP de commentaire à {user}: {e}")
except Exception as e:
@ -317,19 +344,20 @@ class BugReport(commands.Cog):
kuby_logger.error(f"Error handling bot event: {e}")
return web.json_response({"status": "error", "message": str(e)}, status=500)
@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"),
])
async def signaler_bug(self, interaction: discord.Interaction, priority: app_commands.Choice[str]):
await interaction.response.send_modal(BugReportModal(priority))
@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))
@app_commands.command(name="suggerer_fonctionnalite", description="Proposer une nouvelle fonctionnalité pour le bot")
async def suggerer_fonctionnalite(self, interaction: discord.Interaction):
@commands.slash_command(name="suggerer_fonctionnalite", description="Proposer une nouvelle fonctionnalité pour le bot")
async def suggerer_fonctionnalite(self, interaction: disnake.ApplicationCommandInteraction):
await interaction.response.send_modal(FeatureSuggestionModal())
async def setup(bot):
await bot.add_cog(BugReport(bot))
def setup(bot):
bot.add_cog(BugReport(bot))

View file

@ -1,10 +1,13 @@
import discord
from discord.ext import commands
from discord import app_commands
import disnake
from disnake.ext import commands
import json
import os
from typing import List, Optional, Dict
import datetime
import asyncio
import logging
kuby_logger = logging.getLogger("KubyBot")
# --- CONFIGURATION ---
CONV_SETTINGS_FILE = "data/convocation_settings.json"
@ -52,20 +55,17 @@ class Convocation(commands.Cog):
del self.backups[key]
save_json(CONV_BACKUPS_FILE, self.backups)
@commands.hybrid_group(name="convocation", description="Gère les convocations de membres")
@commands.slash_command(name="convocation", description="Gère les convocations de membres")
@commands.has_permissions(manage_roles=True)
async def convocation_group(self, ctx):
if ctx.invoked_subcommand is None:
await ctx.send_help(ctx.command)
async def convocation_group(self, interaction: disnake.ApplicationCommandInteraction):
pass
@convocation_group.command(name="convoquer", description="Convoque un membre et verrouille ses accès")
@app_commands.describe(
membre="Le membre à convoquer",
raison="La raison de la convocation",
heure="L'heure de la convocation (ex: 14:30)"
)
async def convoquer(self, ctx, membre: discord.Member, raison: str, heure: str):
guild_id = ctx.guild.id
@convocation_group.sub_command(name="convoquer", description="Convoque un membre et verrouille ses accès")
async def convoquer(self, interaction: disnake.ApplicationCommandInteraction,
membre: disnake.Member = commands.Param(description="Le membre à convoquer"),
raison: str = commands.Param(description="La raison de la convocation"),
heure: str = commands.Param(description="L'heure de la convocation (ex: 14:30)")):
guild_id = interaction.guild.id
settings = self.get_guild_settings(guild_id)
role_conv_id = settings.get("role_id")
log_channel_id = settings.get("log_channel_id")
@ -73,67 +73,75 @@ class Convocation(commands.Cog):
admin_ids = settings.get("admin_ids", [])
if not role_conv_id:
await ctx.send("❌ Le rôle de convocation n'est pas configuré. Utilisez `/convocation config`.", ephemeral=True)
await interaction.response.send_message("❌ Le rôle de convocation n'est pas configuré. Utilisez `/convocation config`.", ephemeral=True)
return
await ctx.defer(ephemeral=True)
await interaction.response.defer(ephemeral=True)
try:
# --- BACKUP ---
bot_top_role = ctx.guild.me.top_role
bot_top_role = interaction.guild.me.top_role
backup_data = {
"roles": [r.id for r in membre.roles if not r.is_default() and r.id != role_conv_id and r.position < bot_top_role.position],
"overrides": {}
}
# 1. Backup and Remove current roles
current_roles = [ctx.guild.get_role(rid) for rid in backup_data["roles"] if ctx.guild.get_role(rid)]
current_roles = [interaction.guild.get_role(rid) for rid in backup_data["roles"] if interaction.guild.get_role(rid)]
if current_roles:
try:
await membre.remove_roles(*current_roles, reason=f"Convocation par {ctx.author}")
except discord.Forbidden:
await ctx.send("⚠️ Je n'ai pas pu retirer certains rôles (Permission refusée).", ephemeral=True)
await membre.remove_roles(*current_roles, reason=f"Convocation par {interaction.author}")
except disnake.Forbidden:
await interaction.followup.send("⚠️ Je n'ai pas pu retirer certains rôles (Permission refusée).", ephemeral=True)
# 2. Dynamic Lockdown: Loop through ALL channels
lockdown_count = 0
for channel in ctx.guild.channels:
# On ignore le salon d'attente et ses parents s'il y en a
if attente_id and (channel.id == attente_id or (hasattr(channel, 'category') and channel.category and channel.category.id == attente_id)):
continue
# Check existing override for the member
overwrites = channel.overwrites_for(membre)
if not overwrites.is_empty():
# Save existing override (as dict of pair of permission name and boolean/None)
backup_data["overrides"][str(channel.id)] = {k: v for k, v in dict(overwrites).items() if v is not None}
# Apply Lockdown override for the member
try:
# Specific override for this member: View Channel = False
await channel.set_permissions(membre, view_channel=False, reason="Convocation Lockdown")
lockdown_count += 1
except discord.Forbidden:
pass # Skip if we can't touch this channel
semaphore = asyncio.Semaphore(10)
async def lock_channel(channel):
async with semaphore:
# On ignore le salon d'attente et ses parents s'il y en a
if attente_id and (channel.id == attente_id or (hasattr(channel, 'category') and channel.category and channel.category.id == attente_id)):
return False
# Check existing override for the member
overwrites = channel.overwrites_for(membre)
if not overwrites.is_empty():
# Save existing override
backup_data["overrides"][str(channel.id)] = {k: v for k, v in dict(overwrites).items() if v is not None}
# Apply Lockdown override for the member
try:
await channel.set_permissions(membre, view_channel=False, reason="Convocation Lockdown")
return True
except disnake.Forbidden:
return False
except Exception as e:
kuby_logger.warning(f"⚠️ [Convocation] Impossible de verrouiller {channel.name}: {e}")
return False
tasks = [lock_channel(c) for c in interaction.guild.channels]
results = await asyncio.gather(*tasks)
lockdown_count = sum(1 for r in results if r is True)
self.save_backup(guild_id, membre.id, backup_data)
# 3. Add Convocation role
role_conv = ctx.guild.get_role(role_conv_id)
role_conv = interaction.guild.get_role(role_conv_id)
if role_conv:
if role_conv.position >= bot_top_role.position:
await ctx.send(f"⚠️ Je ne peux pas donner le rôle {role_conv.mention} car il est plus haut que moi !", ephemeral=True)
await interaction.followup.send(f"⚠️ Je ne peux pas donner le rôle {role_conv.mention} car il est plus haut que moi !", ephemeral=True)
else:
try:
await membre.add_roles(role_conv, reason="Début de convocation")
except discord.Forbidden:
await ctx.send(f"⚠️ Impossible de donner le rôle {role_conv.name} (Permission refusée).", ephemeral=True)
except disnake.Forbidden:
await interaction.followup.send(f"⚠️ Impossible de donner le rôle {role_conv.name} (Permission refusée).", ephemeral=True)
# 4. Embeds & Logs (Inchangés mais inclus pour la cohérence)
embed_conv = discord.Embed(
embed_conv = disnake.Embed(
title="CONVOCATION | 🔖",
description=f"> Bonjour {membre.mention}, vous êtes **convoqué** par {ctx.author.mention}. Voici les **détails** :",
description=f"> Bonjour {membre.mention}, vous êtes **convoqué** par {interaction.author.mention}. Voici les **détails** :",
color=0x0099ff,
timestamp=discord.utils.utcnow()
timestamp=disnake.utils.utcnow()
)
embed_conv.add_field(name="**Raison de la convocation :**", value=f"{raison}", inline=False)
embed_conv.add_field(name="**Heure de la convocation :**", value=f"{heure}", inline=False)
@ -141,15 +149,15 @@ class Convocation(commands.Cog):
"> Merci de vous présenter dans le vocal attente **5 minutes** avant l'heure.\n\n"
"> La **levée** de suspension se fera uniquement à la **fin** de la convocation."
), inline=False)
embed_conv.set_footer(text=f"Gestion de {ctx.guild.name}")
embed_conv.set_footer(text=f"Gestion de {interaction.guild.name}")
embed_log = discord.Embed(
embed_log = disnake.Embed(
title="📋 CONVOCATION ENVOYÉE",
color=0xff9900,
timestamp=discord.utils.utcnow()
timestamp=disnake.utils.utcnow()
)
embed_log.add_field(name="👤 Membre convoqué", value=f"{membre.mention} ({membre.id})", inline=True)
embed_log.add_field(name="👮 Convoqué par", value=f"{ctx.author.mention}", inline=True)
embed_log.add_field(name="👮 Convoqué par", value=f"{interaction.author.mention}", inline=True)
embed_log.add_field(name="📄 Raison", value=raison, inline=False)
embed_log.add_field(name="🕒 Heure", value=heure, inline=False)
@ -158,7 +166,7 @@ class Convocation(commands.Cog):
try:
await membre.send(embed=embed_conv)
dm_sent = True
except discord.Forbidden:
except disnake.Forbidden:
dm_sent = False
# Admin & Channel Logs
@ -169,93 +177,106 @@ class Convocation(commands.Cog):
except: pass
if log_channel_id:
chan = ctx.guild.get_channel(log_channel_id)
chan = interaction.guild.get_channel(log_channel_id)
if chan: await chan.send(embed=embed_log)
await ctx.send(f"✅ Convocation envoyée à {membre.mention}. ({lockdown_count} salons verrouillés) " +
await interaction.followup.send(f"✅ Convocation envoyée à {membre.mention}. ({lockdown_count} salons verrouillés) " +
("" if dm_sent else "(⚠️ DM impossible)"), ephemeral=True)
except Exception as e:
import traceback
traceback.print_exc()
await ctx.send(f"❌ Une erreur critique est survenue : {e}", ephemeral=True)
await interaction.followup.send(f"❌ Une erreur critique est survenue : {e}", ephemeral=True)
@convocation_group.command(name="lever", description="Lève la convocation d'un membre et restaure ses accès")
@app_commands.describe(membre="Le membre à libérer")
async def lever(self, ctx, membre: discord.Member):
guild_id = ctx.guild.id
@convocation_group.sub_command(name="lever", description="Lève la convocation d'un membre et restaure ses accès")
async def lever(self, interaction: disnake.ApplicationCommandInteraction,
membre: disnake.Member = commands.Param(description="Le membre à libérer")):
guild_id = interaction.guild.id
settings = self.get_guild_settings(guild_id)
role_conv_id = settings.get("role_id")
await ctx.defer(ephemeral=True)
await interaction.response.defer(ephemeral=True)
backup_data = self.get_backup(guild_id, membre.id)
if not backup_data:
await ctx.send("⚠️ Aucune donnée de convocation trouvée pour ce membre.", ephemeral=True)
await interaction.followup.send("⚠️ Aucune donnée de convocation trouvée pour ce membre.", ephemeral=True)
return
# 1. Remove Convocation role
if role_conv_id:
role_conv = ctx.guild.get_role(role_conv_id)
if role_conv and role_conv in membre.roles:
await membre.remove_roles(role_conv, reason="Fin de convocation")
# 1. Remove Convocation role & Restore original roles FIRST
try:
if role_conv_id:
role_conv = interaction.guild.get_role(role_conv_id)
if role_conv and role_conv in membre.roles:
await membre.remove_roles(role_conv, reason="Fin de convocation")
# 2. Restore channel overrides
restore_count = 0
role_ids = backup_data.get("roles", [])
if role_ids:
restored_roles = []
for rid in role_ids:
role = interaction.guild.get_role(rid)
if role and role.position < interaction.guild.me.top_role.position:
restored_roles.append(role)
if restored_roles:
await membre.add_roles(*restored_roles, reason="Restauration fin de convocation")
except Exception as e:
kuby_logger.error(f"❌ [Convocation] Erreur lors de la restauration des rôles pour {membre}: {e}")
# 2. Restore channel overrides (Parallel)
semaphore = asyncio.Semaphore(10)
overrides = backup_data.get("overrides", {})
for channel in ctx.guild.channels:
chan_id_str = str(channel.id)
try:
if chan_id_str in overrides:
# Restore previous override
ov_data = overrides[chan_id_str]
overwrite = discord.PermissionOverwrite(**ov_data)
await channel.set_permissions(membre, overwrite=overwrite, reason="Restauration fin de convocation")
restore_count += 1
else:
# Clear our lockdown override
# On ne vide que si un override membre existait (sinon on risque de supprimer des perms rôles ?)
# En fait set_permissions(member, overwrite=None) supprime l'entrée PERSONNELLE du membre.
await channel.set_permissions(membre, overwrite=None, reason="Fin de convocation - Cleanup")
except discord.Forbidden:
pass
async def restore_channel(channel):
async with semaphore:
chan_id_str = str(channel.id)
try:
if chan_id_str in overrides:
# Restore previous override
ov_data = overrides[chan_id_str]
overwrite = disnake.PermissionOverwrite(**ov_data)
await channel.set_permissions(membre, overwrite=overwrite, reason="Restauration fin de convocation")
return "restored"
elif membre in channel.overwrites:
# Clear our lockdown override only if it exists
await channel.set_permissions(membre, overwrite=None, reason="Fin de convocation - Cleanup")
return "cleared"
except disnake.Forbidden:
return "forbidden"
except Exception as e:
kuby_logger.warning(f"⚠️ [Convocation] Erreur restauration {channel.name}: {e}")
return "error"
return "skipped"
# 3. Restore roles
role_ids = backup_data.get("roles", [])
restored_roles = []
if role_ids:
for rid in role_ids:
role = ctx.guild.get_role(rid)
if role and role.position < ctx.guild.me.top_role.position:
restored_roles.append(role)
if restored_roles:
await membre.add_roles(*restored_roles, reason="Restauration fin de convocation")
tasks = [restore_channel(c) for c in interaction.guild.channels]
results = await asyncio.gather(*tasks)
restore_count = sum(1 for r in results if r == "restored")
clear_count = sum(1 for r in results if r == "cleared")
forbidden_count = sum(1 for r in results if r == "forbidden")
kuby_logger.info(f"🔓 [Convocation] Restauration terminée pour {membre}: {restore_count} restaurés, {clear_count} nettoyés, {forbidden_count} échecs permissions.")
self.clear_backup(guild_id, membre.id)
# 4. Notification
embed = discord.Embed(
embed = disnake.Embed(
title="🔓 CONVOCATION TERMINÉE",
description=f"Votre convocation sur **{ctx.guild.name}** est terminée. Vos accès ont été restaurés.",
color=discord.Color.green()
description=f"Votre convocation sur **{interaction.guild.name}** est terminée. Vos accès ont été restaurés.",
color=disnake.Color.green()
)
try:
await membre.send(embed=embed)
except: pass
await ctx.send(f"✅ Convocation levée pour {membre.mention}. Access restaurés.", ephemeral=True)
await interaction.followup.send(f"✅ Convocation levée pour {membre.mention}. Access restaurés.", ephemeral=True)
@convocation_group.command(name="config", description="Configure le système de convocation")
@app_commands.describe(
role="Le rôle de convocation (Muet/Convocation)",
channel="Le salon de logs",
attente="Salon (ou catégorie) d'attente à laisser visible",
admins="Liste d'IDs d'admins à notifier (séparés par des espaces)"
)
@commands.has_permissions(administrator=True)
async def config(self, ctx, role: discord.Role, channel: discord.TextChannel = None, attente: discord.abc.GuildChannel = None, admins: str = None):
guild_id = ctx.guild.id
@convocation_group.sub_command(name="config", description="Configure le système de convocation")
async def config(self, interaction: disnake.ApplicationCommandInteraction,
role: disnake.Role = commands.Param(description="Le rôle de convocation (Muet/Convocation)"),
channel: disnake.TextChannel = commands.Param(None, description="Le salon de logs"),
attente: disnake.abc.GuildChannel = commands.Param(None, description="Salon (ou catégorie) d'attente à laisser visible"),
admins: str = commands.Param(None, description="Liste d'IDs d'admins à notifier (séparés par des espaces)")):
guild_id = interaction.guild.id
settings = self.get_guild_settings(guild_id)
settings["role_id"] = role.id
@ -273,13 +294,13 @@ class Convocation(commands.Cog):
self.save_guild_settings(guild_id, settings)
embed = discord.Embed(title="⚙️ Configuration Convocation Mise à jour", color=discord.Color.blue())
embed = disnake.Embed(title="⚙️ Configuration Convocation Mise à jour", color=disnake.Color.blue())
embed.add_field(name="Rôle", value=role.mention)
embed.add_field(name="Salon Logs", value=channel.mention if channel else "Non défini")
embed.add_field(name="Salon/Cat d'attente", value=attente.mention if attente else "Non défini")
embed.add_field(name="Admins notifiés", value=str(len(settings.get("admin_ids", []))))
await ctx.send(embed=embed, ephemeral=True)
await interaction.response.send_message(embed=embed, ephemeral=True)
async def setup(bot):
await bot.add_cog(Convocation(bot))
def setup(bot):
bot.add_cog(Convocation(bot))

View file

@ -1,6 +1,5 @@
import discord
from discord.ext import commands
from discord import app_commands
import disnake
from disnake.ext import commands
import sqlite3
import aiohttp
import io
@ -148,14 +147,18 @@ class Goodbye(commands.Cog):
# Utiliser le message personnalisé ou un par défaut
description = custom_message if custom_message else "Nous espérons vous revoir bientôt <a:rain:1317973615714504806>"
embed = discord.Embed(title=f"{member} viens de nous quitter.",
description=description,
color=discord.Color.dark_magenta())
embed.set_footer(text=f"Avait rejoint {joined_message}")
embed.set_image(url="attachment://goodbye.png")
await channel.send(embed=embed, file=discord.File(img, filename="goodbye.png"))
components = [
disnake.ui.Container(
disnake.ui.TextDisplay(f"{member} viens de nous quitter."),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(description),
disnake.ui.TextDisplay(f"⌛ **Avait rejoint :** {joined_message}"),
disnake.ui.MediaGallery(disnake.MediaGalleryItem(url="attachment://goodbye.png"))
)
]
await channel.send(components=components, file=disnake.File(img, filename="goodbye.png"))
async def save_banner_attachment(self, attachment: discord.Attachment, guild_id: int) -> str:
async def save_banner_attachment(self, attachment: disnake.Attachment, guild_id: int) -> str:
"""Sauvegarde l'attachment et retourne le chemin du fichier"""
base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "banners"))
os.makedirs(base_dir, exist_ok=True)
@ -187,9 +190,9 @@ class Goodbye(commands.Cog):
return os.path.basename(banner_path)
@app_commands.command(name="setgoodbye", description="Définit le salon d'aurevoir et les paramètres personnalisés")
@app_commands.checks.has_permissions(administrator=True)
async def set_goodbye(self, interaction: discord.Interaction, channel: discord.TextChannel, server_name: str = None, message: str = None, banner_file: discord.Attachment = None):
@commands.slash_command(name="setgoodbye", description="Définit le salon d'aurevoir et les paramètres personnalisés")
@commands.has_permissions(administrator=True)
async def set_goodbye(self, interaction: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel, server_name: str = None, message: str = None, banner_file: disnake.Attachment = None):
banner_path = None
# Si un fichier est uploadé, le sauvegarder
@ -208,7 +211,7 @@ class Goodbye(commands.Cog):
goodbye_server_name=COALESCE(excluded.goodbye_server_name, goodbye_server_name),
goodbye_message=COALESCE(excluded.goodbye_message, goodbye_message),
goodbye_banner_background=COALESCE(excluded.goodbye_banner_background, goodbye_banner_background)''',
(interaction.guild_id, channel.id, server_name, message, banner_path))
(interaction.guild_id, channel.id, server_name, message, banner_path))
conn.commit()
conn.close()
@ -217,7 +220,12 @@ class Goodbye(commands.Cog):
if message: msg += f"\nMessage: {message}"
if banner_path: msg += f"\nBannière personnalisée activée."
await interaction.response.send_message(msg, ephemeral=True)
components = [
disnake.ui.Container(
disnake.ui.TextDisplay(msg)
)
]
await interaction.response.send_message(components=components, ephemeral=True)
async def setup(bot):
await bot.add_cog(Goodbye(bot))
def setup(bot):
bot.add_cog(Goodbye(bot))

195
commandes/invites.py Normal file
View file

@ -0,0 +1,195 @@
import disnake
from disnake.ext import commands
import sqlite3
import os
import logging
from typing import Dict, Optional
kuby_logger = logging.getLogger("KubyBot")
class Invites(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.db_path = os.path.join(os.path.dirname(__file__), "..", "config.db")
self.invite_cache = {} # {guild_id: {code: uses}}
self._init_db()
def _init_db(self):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS member_invites (
guild_id INTEGER,
member_id INTEGER,
inviter_id INTEGER,
invite_code TEXT,
PRIMARY KEY (guild_id, member_id)
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS invite_configs (
guild_id INTEGER PRIMARY KEY,
log_channel_id INTEGER
)
''')
conn.commit()
conn.close()
async def update_invite_cache(self, guild):
try:
invites = await guild.invites()
self.invite_cache[guild.id] = {invite.code: invite.uses for invite in invites}
except disnake.Forbidden:
pass
@commands.Cog.listener()
async def on_ready(self):
for guild in self.bot.guilds:
await self.update_invite_cache(guild)
kuby_logger.info("✅ [Invites] Cache des invitations initialisé")
# Debug: Lister toutes les commandes slash enregistrées
slash_cmds = [cmd.name for cmd in self.bot.slash_commands]
kuby_logger.info(f"🔍 [Debug] Commandes slash enregistrées : {slash_cmds}")
@commands.Cog.listener()
async def on_invite_create(self, invite):
if invite.guild.id not in self.invite_cache:
self.invite_cache[invite.guild.id] = {}
self.invite_cache[invite.guild.id][invite.code] = invite.uses
@commands.Cog.listener()
async def on_invite_delete(self, invite):
if invite.guild.id in self.invite_cache:
self.invite_cache[invite.guild.id].pop(invite.code, None)
@commands.Cog.listener()
async def on_member_join(self, member):
if member.bot: return
guild = member.guild
old_invites = self.invite_cache.get(guild.id, {})
try:
new_invites = await guild.invites()
except disnake.Forbidden:
return
used_invite = None
for invite in new_invites:
if invite.code in old_invites:
if invite.uses > old_invites[invite.code]:
used_invite = invite
break
elif invite.uses > 0:
# C'est une nouvelle invitation qui a été utilisée
used_invite = invite
break
# Mise à jour du cache
self.invite_cache[guild.id] = {invite.code: invite.uses for invite in new_invites}
if used_invite:
inviter = used_invite.inviter
if inviter:
self.record_invite(guild.id, member.id, inviter.id, used_invite.code)
kuby_logger.info(f"📥 [Invites] {member} invité par {inviter} ({used_invite.code})")
# Modular join message
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('SELECT log_channel_id FROM invite_configs WHERE guild_id = ?', (guild.id,))
result = cursor.fetchone()
conn.close()
if result:
log_channel = guild.get_channel(result[0])
if log_channel:
# Determine method
method = "invitation standard"
if guild.vanity_url_code and used_invite.code == guild.vanity_url_code:
method = "lien personnalisé"
elif used_invite.max_age == 0:
method = "invitation permanente"
elif used_invite.temporary:
method = "invitation temporaire"
elif used_invite.max_uses == 1:
method = "invitation à usage unique"
components = [
disnake.ui.Container(
disnake.ui.Section(
f"Nouveau membre via invitation",
accessory=disnake.ui.Thumbnail(member.display_avatar.url)
),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(f"👤 {member.mention} vient de nous rejoindre !"),
disnake.ui.TextDisplay(f"🤝 Grâce à {inviter.mention}"),
disnake.ui.TextDisplay(f"🔗 Via **{method}**")
)
]
await log_channel.send(components=components)
def record_invite(self, guild_id, member_id, inviter_id, code):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO member_invites (guild_id, member_id, inviter_id, invite_code)
VALUES (?, ?, ?, ?)
ON CONFLICT(guild_id, member_id) DO UPDATE SET inviter_id=excluded.inviter_id, invite_code=excluded.invite_code
''', (guild_id, member_id, inviter_id, code))
conn.commit()
conn.close()
def get_inviter_info(self, guild_id, member_id):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('SELECT inviter_id FROM member_invites WHERE guild_id = ? AND member_id = ?', (guild_id, member_id))
result = cursor.fetchone()
conn.close()
return result[0] if result else None
def get_invite_stats(self, guild_id, user_id):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('SELECT member_id FROM member_invites WHERE guild_id = ? AND inviter_id = ?', (guild_id, user_id))
invited_ids = [row[0] for row in cursor.fetchall()]
conn.close()
guild = self.bot.get_guild(guild_id)
if not guild: return 0, 0
current_present = 0
for mid in invited_ids:
if guild.get_member(mid):
current_present += 1
return len(invited_ids), current_present
@commands.slash_command(name="setinvitechannel", description="Définit le salon des logs d'invitations")
@commands.has_permissions(administrator=True)
async def set_invite_channel(self, interaction: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO invite_configs (guild_id, log_channel_id)
VALUES (?, ?)
ON CONFLICT(guild_id) DO UPDATE SET log_channel_id=excluded.log_channel_id
''', (interaction.guild_id, channel.id))
conn.commit()
conn.close()
components = [
disnake.ui.Container(
disnake.ui.TextDisplay(f"✅ Salon des logs d'invitations défini sur {channel.mention}")
)
]
await interaction.response.send_message(components=components, ephemeral=True)
@commands.command(name="invitations_debug")
async def invites_prefix(self, ctx, user: disnake.Member = None):
user = user or ctx.author
total, current = self.get_invite_stats(ctx.guild.id, user.id)
await ctx.send(f"📊 **{user.display_name}** : {total} total, {current} présents.")
def setup(bot):
bot.add_cog(Invites(bot))

View file

@ -1,125 +1,69 @@
import discord
from discord.ext import commands
from discord import app_commands
import disnake
from disnake.ext import commands
import sqlite3
import os
from datetime import datetime, timedelta
import re
class ModReasonModal(discord.ui.Modal):
def __init__(self, parent_view, action_type):
super().__init__(title=f"Raison pour {action_type}")
self.parent_view = parent_view
class ModReasonModal(disnake.ui.Modal):
def __init__(self, cog, action_type, member):
self.cog = cog
self.action_type = action_type
self.member = member
self.reason = discord.ui.TextInput(
self.reason_input = disnake.ui.TextInput(
label="Raison",
placeholder="Entrez la raison ici...",
required=True,
max_length=500
max_length=500,
custom_id="reason"
)
self.add_item(self.reason)
components = [self.reason_input]
if action_type == "TIMEOUT":
self.duration = discord.ui.TextInput(
self.duration_input = disnake.ui.TextInput(
label="Durée (ex: 1h, 30m, 1d)",
placeholder="1h",
required=True,
max_length=10
max_length=10,
custom_id="duration"
)
self.add_item(self.duration)
components.append(self.duration_input)
async def on_submit(self, interaction: discord.Interaction):
reason = self.reason.value
member = self.parent_view.member
super().__init__(title=f"Raison pour {action_type}", components=components)
async def callback(self, interaction: disnake.ModalInteraction):
reason = interaction.text_values["reason"]
member = self.member
cog = interaction.client.get_cog("Moderation")
if not cog:
return await interaction.response.send_message("❌ Erreur interne.", ephemeral=True)
if self.action_type == "WARN":
await cog.warn(interaction, member, reason)
await self.cog.warn(interaction, member, reason)
elif self.action_type == "TIMEOUT":
await cog.timeout(interaction, member, self.duration.value, reason)
duration = interaction.text_values["duration"]
await self.cog.timeout(interaction, member, duration, reason)
elif self.action_type == "KICK":
await cog.kick(interaction, member, reason)
await self.cog.kick(interaction, member, reason)
elif self.action_type == "BAN":
await cog.ban(interaction, member, reason)
await self.cog.ban(interaction, member, reason)
await self.parent_view.update_panel(interaction)
await self.cog.send_modpanel_v2(interaction, member, edit=True)
class ModPanelView(discord.ui.View):
def __init__(self, member, moderator, db_path):
super().__init__(timeout=120)
self.member = member
self.moderator = moderator
self.db_path = db_path
async def update_panel(self, interaction: discord.Interaction):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('SELECT COUNT(*) FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "WARN" AND status = "ACTIVE"', (interaction.guild_id, self.member.id))
warn_count = cursor.fetchone()[0]
conn.close()
embed = discord.Embed(
title=f"🛡️ Panel de Modération - {self.member.name}",
description=f"Gestion de l'utilisateur {self.member.mention}",
color=discord.Color.blue(),
timestamp=datetime.now()
)
embed.set_thumbnail(url=self.member.display_avatar.url)
embed.add_field(name="👤 Utilisateur", value=f"ID: `{self.member.id}`\nTag: `{self.member}`", inline=True)
embed.add_field(name="⚠️ Avertissements", value=f"Actifs: **{warn_count}**", inline=True)
embed.add_field(name="📅 Dates", value=f"Rejoint: <t:{int(self.member.joined_at.timestamp())}:R>\nCréé: <t:{int(self.member.created_at.timestamp())}:R>", inline=False)
if not interaction.response.is_done():
await interaction.response.edit_message(embed=embed, view=self)
else:
await interaction.edit_original_response(embed=embed, view=self)
@discord.ui.button(label="Avertir", style=discord.ButtonStyle.secondary, emoji="⚠️")
async def warn_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.send_modal(ModReasonModal(self, "WARN"))
@discord.ui.button(label="Timeout", style=discord.ButtonStyle.secondary, emoji="")
async def timeout_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.send_modal(ModReasonModal(self, "TIMEOUT"))
@discord.ui.button(label="Expulser", style=discord.ButtonStyle.secondary, emoji="👢")
async def kick_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.send_modal(ModReasonModal(self, "KICK"))
@discord.ui.button(label="Bannir", style=discord.ButtonStyle.danger, emoji="🔨")
async def ban_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.send_modal(ModReasonModal(self, "BAN"))
@discord.ui.button(label="Historique", style=discord.ButtonStyle.primary, emoji="📜")
async def history_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
cog = interaction.client.get_cog("Moderation")
await cog.history(interaction, self.member)
@discord.ui.button(label="Clear Warns", style=discord.ButtonStyle.danger, emoji="🧹")
async def clear_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
cog = interaction.client.get_cog("Moderation")
await cog.clearwarns(interaction, self.member, reason="Nettoyage via Panel")
await self.update_panel(interaction)
class ModConfigModal(discord.ui.Modal):
class ModConfigModal(disnake.ui.Modal):
def __init__(self, field_name, current_value):
super().__init__(title=f"Modifier {field_name}")
self.field_name = field_name
self.input = discord.ui.TextInput(
self.input_field = disnake.ui.TextInput(
label=field_name,
default=str(current_value),
value=str(current_value),
placeholder="Entrez une valeur numérique...",
required=True
required=True,
custom_id="value"
)
self.add_item(self.input)
super().__init__(title=f"Modifier {field_name}", components=[self.input_field])
async def on_submit(self, interaction: discord.Interaction):
value = self.input.value
cog = interaction.client.get_cog("Moderation")
async def callback(self, interaction: disnake.ModalInteraction):
value = interaction.text_values["value"]
cog = interaction.bot.get_cog("Moderation")
mapping = {
"Limite Timeout": "warn_limit_timeout",
@ -138,62 +82,8 @@ class ModConfigModal(discord.ui.Modal):
conn.commit()
conn.close()
await interaction.response.send_message(f"{self.field_name} mis à jour sur **{value}**.", ephemeral=True)
class ModConfigView(discord.ui.View):
def __init__(self, guild_id, db_path):
super().__init__(timeout=60)
self.guild_id = guild_id
self.db_path = db_path
@discord.ui.button(label="Log Channel", style=discord.ButtonStyle.primary, emoji="📁")
async def log_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
view = discord.ui.View()
select = discord.ui.ChannelSelect(
placeholder="Sélectionnez le salon de logs...",
channel_types=[discord.ChannelType.text]
)
async def select_callback(interaction: discord.Interaction):
channel = select.values[0]
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("UPDATE mod_config SET log_channel_id = ? WHERE guild_id = ?", (channel.id, interaction.guild_id))
conn.commit(); conn.close()
await interaction.response.send_message(f"✅ Salon de logs défini sur {channel.mention}", ephemeral=True)
select.callback = select_callback
view.add_item(select)
await interaction.response.send_message("Choisissez le salon de logs :", view=view, ephemeral=True)
@discord.ui.button(label="Paliers Warns", style=discord.ButtonStyle.secondary, emoji="⚖️")
async def limits_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("SELECT warn_limit_timeout, warn_limit_kick, warn_limit_ban FROM mod_config WHERE guild_id = ?", (self.guild_id,))
res = cursor.fetchone(); conn.close()
view = discord.ui.View()
btn_t = discord.ui.Button(label=f"Timeout ({res[0]})", style=discord.ButtonStyle.secondary)
btn_t.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Timeout", res[0]))
btn_k = discord.ui.Button(label=f"Kick ({res[1]})", style=discord.ButtonStyle.secondary)
btn_k.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Kick", res[1]))
btn_b = discord.ui.Button(label=f"Ban ({res[2]})", style=discord.ButtonStyle.secondary)
btn_b.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Ban", res[2]))
view.add_item(btn_t); view.add_item(btn_k); view.add_item(btn_b)
await interaction.response.send_message("Quelle limite modifier ?", view=view, ephemeral=True)
@discord.ui.button(label="Durée Timeout", style=discord.ButtonStyle.secondary, emoji="⏱️")
async def duration_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("SELECT timeout_duration FROM mod_config WHERE guild_id = ?", (self.guild_id,))
res = cursor.fetchone(); conn.close()
await interaction.response.send_modal(ModConfigModal("Durée Timeout (s)", res[0]))
await interaction.response.send_message(f"{self.field_name} mis à jour.", ephemeral=True)
await cog.send_modconfig_v2(interaction, edit=True)
class Moderation(commands.Cog):
def __init__(self, bot):
@ -205,7 +95,17 @@ class Moderation(commands.Cog):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS sanctions (id INTEGER PRIMARY KEY AUTOINCREMENT, guild_id INTEGER, user_id INTEGER, moderator_id INTEGER, type TEXT, reason TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, duration INTEGER, status TEXT DEFAULT 'ACTIVE')''')
cursor.execute('''CREATE TABLE IF NOT EXISTS mod_config (guild_id INTEGER PRIMARY KEY, log_channel_id INTEGER, warn_limit_timeout INTEGER DEFAULT 3, warn_limit_kick INTEGER DEFAULT 5, warn_limit_ban INTEGER DEFAULT 10, timeout_duration INTEGER DEFAULT 3600)''')
cursor.execute('''CREATE TABLE IF NOT EXISTS mod_config (
guild_id INTEGER PRIMARY KEY,
log_channel_id INTEGER,
board_channel_id INTEGER,
warn_limit_timeout INTEGER DEFAULT 3,
warn_limit_kick INTEGER DEFAULT 5,
warn_limit_ban INTEGER DEFAULT 10,
timeout_duration INTEGER DEFAULT 3600
)''')
try: cursor.execute("ALTER TABLE mod_config ADD COLUMN board_channel_id INTEGER")
except: pass
conn.commit(); conn.close()
def parse_duration(self, duration_str: str) -> int:
@ -216,110 +116,226 @@ class Moderation(commands.Cog):
for value, unit in matches: total_seconds += int(value) * units[unit]
return total_seconds
async def send_mod_log(self, guild, embed):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
def _type_to_emoji(self, s_type: str) -> str:
return {"WARN": "⚠️", "TIMEOUT": "", "KICK": "👢", "BAN": "🔨"}.get(s_type, "🛡️")
def build_sanction_board_components(self, *, s_type: str, user, moderator, reason, timestamp, s_id, duration=None) -> list:
ts_int = int(datetime.fromisoformat(str(timestamp)).timestamp()) if not isinstance(timestamp, datetime) else int(timestamp.timestamp())
emoji = self._type_to_emoji(s_type)
children = [
disnake.ui.Section(f"{emoji} **SANCTION : {s_type}**", accessory=disnake.ui.Thumbnail(user.display_avatar.url)),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(f"- **Membre sanctionné** : {user.mention}"),
disnake.ui.TextDisplay(f"- **Modérateur** : {moderator.mention}"),
disnake.ui.TextDisplay(f"- **Raison** : “ *{reason if reason else 'Aucune raison spécifiée'}* ”"),
disnake.ui.TextDisplay(f"- **Date** : <t:{ts_int}:F>")
]
if s_type == "TIMEOUT" and duration:
children.append(disnake.ui.TextDisplay(f"- **Durée** : `{duration}s`"))
children.extend([
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(f"🆔 **ID** : `{s_id}`")
])
return [disnake.ui.Container(*children)]
async def send_mod_log(self, guild, components):
conn = sqlite3.connect(self.db_path); cursor = conn.cursor()
cursor.execute('SELECT log_channel_id FROM mod_config WHERE guild_id = ?', (guild.id,))
result = cursor.fetchone(); conn.close()
if result and result[0]:
log_channel = guild.get_channel(result[0])
if log_channel: await log_channel.send(embed=embed)
res = cursor.fetchone(); conn.close()
if res and res[0]:
chan = guild.get_channel(res[0])
if chan: await chan.send(components=components)
@app_commands.command(name="modpanel", description="Ouvre le panel de modération pour un membre")
@app_commands.checks.has_permissions(moderate_members=True)
async def modpanel(self, interaction: discord.Interaction, member: discord.Member):
if member.top_role >= interaction.user.top_role and interaction.user.id != interaction.guild.owner_id:
return await interaction.response.send_message("❌ Impossible de gérer ce membre.", ephemeral=True)
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('SELECT COUNT(*) FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "WARN" AND status = "ACTIVE"', (interaction.guild_id, member.id))
warn_count = cursor.fetchone()[0]; conn.close()
embed = discord.Embed(title=f"🛡️ Panel de Modération - {member.name}", description=f"Gestion de {member.mention}", color=discord.Color.blue(), timestamp=datetime.now())
embed.set_thumbnail(url=member.display_avatar.url)
embed.add_field(name="👤 Utilisateur", value=f"ID: `{member.id}`", inline=True)
embed.add_field(name="⚠️ Avertissements", value=f"Actifs: **{warn_count}**", inline=True)
view = ModPanelView(member, interaction.user, self.db_path)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
async def send_sanction_board(self, guild, components):
conn = sqlite3.connect(self.db_path); cursor = conn.cursor()
cursor.execute('SELECT board_channel_id FROM mod_config WHERE guild_id = ?', (guild.id,))
res = cursor.fetchone(); conn.close()
if res and res[0]:
chan = guild.get_channel(res[0])
if chan: await chan.send(components=components)
@app_commands.command(name="modconfig", description="Configuration de la modération")
@app_commands.checks.has_permissions(administrator=True)
async def modconfig(self, interaction: discord.Interaction):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('INSERT OR IGNORE INTO mod_config (guild_id) VALUES (?)', (interaction.guild_id,))
cursor.execute('SELECT log_channel_id, warn_limit_timeout, warn_limit_kick, warn_limit_ban, timeout_duration FROM mod_config WHERE guild_id = ?', (interaction.guild_id,))
res = cursor.fetchone(); conn.commit(); conn.close()
embed = discord.Embed(title="⚙️ Configuration Modération", description="Modifiez les paramètres ci-dessous.", color=discord.Color.blue())
embed.add_field(name="📁 Logs", value=f"<#{res[0]}>" if res[0] else "Non défini", inline=False)
embed.add_field(name="⚖️ Paliers", value=f"Timeout: **{res[1]}**, Kick: **{res[2]}**, Ban: **{res[3]}**", inline=False)
embed.add_field(name="⏱️ Durée Timeout Auto", value=f"**{res[4]}s**", inline=False)
view = ModConfigView(interaction.guild_id, self.db_path)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
async def warn(self, interaction: discord.Interaction, member: discord.Member, reason: str):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?)', (interaction.guild_id, member.id, interaction.user.id, 'WARN', reason))
async def send_modpanel_v2(self, interaction: disnake.Interaction, member: disnake.Member, edit=False):
conn = sqlite3.connect(self.db_path); cursor = conn.cursor()
cursor.execute('SELECT COUNT(*) FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "WARN" AND status = "ACTIVE"', (interaction.guild_id, member.id))
warn_count = cursor.fetchone()[0]
cursor.execute('SELECT type, reason, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND status = "ACTIVE" ORDER BY timestamp DESC LIMIT 3', (interaction.guild_id, member.id))
rows = cursor.fetchall(); conn.close()
children = [
disnake.ui.Section(f"🛡️ Panel : {member.name}", accessory=disnake.ui.Thumbnail(member.display_avatar.url)),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(f"👤 **Membre :** {member.mention} (`{member.id}`)"),
disnake.ui.TextDisplay(f"⚠️ **Avertissements actifs :** {warn_count}"),
disnake.ui.Separator(divider=True),
disnake.ui.Section("⚠️ Infliger un avertissement", accessory=disnake.ui.Button(label="Warn", style=disnake.ButtonStyle.secondary, custom_id=f"modpan_warn:{member.id}")),
disnake.ui.Section("⏳ Mettre en sourdine", accessory=disnake.ui.Button(label="Timeout", style=disnake.ButtonStyle.secondary, custom_id=f"modpan_timeout:{member.id}")),
disnake.ui.Section("👢 Expulser du serveur", accessory=disnake.ui.Button(label="Kick", style=disnake.ButtonStyle.secondary, custom_id=f"modpan_kick:{member.id}")),
disnake.ui.Section("🔨 Bannir définitivement", accessory=disnake.ui.Button(label="Ban", style=disnake.ButtonStyle.danger, custom_id=f"modpan_ban:{member.id}")),
disnake.ui.Separator(divider=True),
disnake.ui.Section("🧹 Réinitialiser les warns", accessory=disnake.ui.Button(label="Clear", style=disnake.ButtonStyle.danger, custom_id=f"modpan_clear:{member.id}"))
]
if rows:
children.append(disnake.ui.Separator(divider=True))
children.append(disnake.ui.TextDisplay("📌 **Dernières sanctions :**"))
for t, r, ts in rows:
ts_int = int(datetime.fromisoformat(str(ts)).timestamp()) if not isinstance(ts, datetime) else int(ts.timestamp())
children.append(disnake.ui.TextDisplay(f"{self._type_to_emoji(t)} **{t}** — {r} (<t:{ts_int}:R>)"))
components = [disnake.ui.Container(*children)]
if edit:
if not interaction.response.is_done(): await interaction.response.edit_message(content=None, components=components)
else: await interaction.edit_original_response(content=None, components=components)
else:
await interaction.response.send_message(components=components, ephemeral=True)
@commands.slash_command(name="modpanel", description="Ouvre le panel de modération")
@commands.has_permissions(moderate_members=True)
async def modpanel(self, interaction: disnake.ApplicationCommandInteraction, member: disnake.Member):
if member.top_role >= interaction.user.top_role and interaction.user.id != interaction.guild.owner_id:
return await interaction.response.send_message("❌ Permissions insuffisantes.", ephemeral=True)
await self.send_modpanel_v2(interaction, member)
async def send_modconfig_v2(self, interaction: disnake.Interaction, edit=False):
conn = sqlite3.connect(self.db_path); cursor = conn.cursor()
cursor.execute('SELECT log_channel_id, board_channel_id, warn_limit_timeout, warn_limit_kick, warn_limit_ban, timeout_duration FROM mod_config WHERE guild_id = ?', (interaction.guild_id,))
res = cursor.fetchone(); conn.close()
if not res: return
children = [
disnake.ui.Section("⚙️ Configuration Modération", accessory=disnake.ui.Thumbnail(interaction.guild.icon.url if interaction.guild.icon else None)),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(f"📁 **Logs internes :** <#{res[0]}>" if res[0] else "📁 **Logs :** Non défini"),
disnake.ui.TextDisplay(f"📢 **Affichage public :** <#{res[1]}>" if res[1] else "📢 **Affichage :** Non défini"),
disnake.ui.TextDisplay(f"⚖️ **Paliers :** T:{res[2]} | K:{res[3]} | B:{res[4]}"),
disnake.ui.TextDisplay(f"⏱️ **Timeout auto :** {res[5]}s"),
disnake.ui.Separator(divider=True),
disnake.ui.Section("📁 Salon des logs", accessory=disnake.ui.Button(label="Logs", style=disnake.ButtonStyle.primary, custom_id="modcfg_logs")),
disnake.ui.Section("📢 Salon d'affichage", accessory=disnake.ui.Button(label="Public", style=disnake.ButtonStyle.primary, custom_id="modcfg_board")),
disnake.ui.Section("⚖️ Modifier paliers", accessory=disnake.ui.Button(label="Paliers", style=disnake.ButtonStyle.secondary, custom_id="modcfg_limits")),
disnake.ui.Section("⏱️ Durée timeout", accessory=disnake.ui.Button(label="Durée", style=disnake.ButtonStyle.secondary, custom_id="modcfg_duration"))
]
components = [disnake.ui.Container(*children)]
if edit:
if not interaction.response.is_done(): await interaction.response.edit_message(content=None, components=components)
else: await interaction.edit_original_response(content=None, components=components)
else:
await interaction.response.send_message(components=components, ephemeral=True)
@commands.slash_command(name="modconfig", description="Configuration de la modération")
@commands.has_permissions(administrator=True)
async def modconfig(self, interaction: disnake.ApplicationCommandInteraction):
conn = sqlite3.connect(self.db_path); cursor = conn.cursor()
cursor.execute('INSERT OR IGNORE INTO mod_config (guild_id) VALUES (?)', (interaction.guild_id,))
conn.commit(); conn.close()
await self.send_modconfig_v2(interaction)
@commands.Cog.listener("on_message_interaction")
async def on_mod_interaction(self, interaction: disnake.MessageInteraction):
cid = interaction.data.custom_id
if not cid: return
if cid.startswith("modpan_"):
action, mid = cid.replace("modpan_", "").split(":")
member = interaction.guild.get_member(int(mid))
if not member: return await interaction.response.send_message("❌ Membre introuvable.", ephemeral=True)
if action == "clear":
await self.clearwarns(interaction, member, "Nettoyage Panel")
await self.send_modpanel_v2(interaction, member, edit=True)
else:
await interaction.response.send_modal(ModReasonModal(self, action.upper(), member))
elif cid.startswith("modcfg_"):
act = cid.replace("modcfg_", "")
if act in ["logs", "board"]:
col = "log_channel_id" if act == "logs" else "board_channel_id"
view = disnake.ui.View()
sel = disnake.ui.ChannelSelect(placeholder="Choisir salon...", channel_types=[disnake.ChannelType.text])
async def sel_cb(i):
c = sel.values[0]
conn = sqlite3.connect(self.db_path); cursor = conn.cursor()
cursor.execute(f"UPDATE mod_config SET {col} = ? WHERE guild_id = ?", (c.id, i.guild_id))
conn.commit(); conn.close()
await i.response.send_message(f"✅ Salon mis à jour.", ephemeral=True)
await self.send_modconfig_v2(interaction, edit=True)
sel.callback = sel_cb; view.add_item(sel)
await interaction.response.send_message(f"Sélectionnez le salon pour {act} :", view=view, ephemeral=True)
elif act == "limits":
conn = sqlite3.connect(self.db_path); cursor = conn.cursor()
cursor.execute("SELECT warn_limit_timeout, warn_limit_kick, warn_limit_ban FROM mod_config WHERE guild_id = ?", (interaction.guild_id,))
res = cursor.fetchone(); conn.close()
view = disnake.ui.View()
b1 = disnake.ui.Button(label=f"Timeout ({res[0]})"); b1.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Timeout", res[0]))
b2 = disnake.ui.Button(label=f"Kick ({res[1]})"); b2.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Kick", res[1]))
b3 = disnake.ui.Button(label=f"Ban ({res[2]})"); b3.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Ban", res[2]))
view.add_item(b1); view.add_item(b2); view.add_item(b3)
await interaction.response.send_message("Modifier quel palier ?", view=view, ephemeral=True)
elif act == "duration":
conn = sqlite3.connect(self.db_path); cursor = conn.cursor()
cursor.execute("SELECT timeout_duration FROM mod_config WHERE guild_id = ?", (interaction.guild_id,))
res = cursor.fetchone(); conn.close()
await interaction.response.send_modal(ModConfigModal("Durée Timeout (s)", res[0]))
async def warn(self, interaction, member, reason):
conn = sqlite3.connect(self.db_path); cursor = conn.cursor()
cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?)', (interaction.guild_id, member.id, interaction.user.id, 'WARN', reason))
cursor.execute('SELECT COUNT(*) FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "WARN" AND status = "ACTIVE"', (interaction.guild_id, member.id))
count = cursor.fetchone()[0]
cursor.execute('SELECT warn_limit_timeout, warn_limit_kick, warn_limit_ban, timeout_duration FROM mod_config WHERE guild_id = ?', (interaction.guild_id,))
config = cursor.fetchone(); conn.commit(); conn.close()
if not interaction.response.is_done(): await interaction.response.send_message(f"{member.mention} averti ({warn_count}).", ephemeral=True)
else: await interaction.followup.send(f"{member.mention} averti ({warn_count}).", ephemeral=True)
await self.send_mod_log(interaction.guild, discord.Embed(title="⚠️ Avertissement", description=f"Membre: {member.mention}\nRaison: {reason}\nTotal: {warn_count}", color=discord.Color.orange()))
cfg = cursor.fetchone()
cursor.execute('SELECT id, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "WARN" ORDER BY id DESC LIMIT 1', (interaction.guild_id, member.id))
s_id, ts = cursor.fetchone(); conn.commit(); conn.close()
if not interaction.response.is_done(): await interaction.response.send_message(f"{member.mention} averti ({count}).", ephemeral=True)
await self.send_mod_log(interaction.guild, [disnake.ui.Container(disnake.ui.Section(f"⚠️ Warn : {member}", accessory=disnake.ui.Thumbnail(member.display_avatar.url)), disnake.ui.Separator(divider=True), disnake.ui.TextDisplay(f"Raison: {reason}"), disnake.ui.TextDisplay(f"Total: {count}"))])
await self.send_sanction_board(interaction.guild, self.build_sanction_board_components(s_type="WARN", user=member, moderator=interaction.user, reason=reason, timestamp=ts, s_id=s_id))
try: await member.send(f"⚠️ Avertissement sur **{interaction.guild.name}**\nRaison: {reason}")
except: pass
if config:
l_t, l_k, l_b, dur = config
if warn_count >= l_b: await member.ban(reason=f"Auto: {warn_count} warns")
elif warn_count >= l_k: await member.kick(reason=f"Auto: {warn_count} warns")
elif warn_count >= l_t: await member.timeout(timedelta(seconds=dur), reason=f"Auto: {warn_count} warns")
if cfg:
lt, lk, lb, dur = cfg
if count >= lb: await member.ban(reason=f"Auto: {count} warns")
elif count >= lk: await member.kick(reason=f"Auto: {count} warns")
elif count >= lt: await member.timeout(duration=timedelta(seconds=dur), reason=f"Auto: {count} warns")
async def timeout(self, interaction: discord.Interaction, member: discord.Member, duration_str: str, reason: str):
async def timeout(self, interaction, member, duration_str, reason):
secs = self.parse_duration(duration_str)
if secs <= 0: return await interaction.response.send_message("❌ Durée invalide.", ephemeral=True)
await member.timeout(timedelta(seconds=secs), reason=reason)
await member.timeout(duration=timedelta(seconds=secs), reason=reason)
conn = sqlite3.connect(self.db_path); cursor = conn.cursor()
cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason, duration) VALUES (?, ?, ?, ?, ?, ?)', (interaction.guild_id, member.id, interaction.user.id, 'TIMEOUT', reason, secs))
conn.commit(); conn.close()
if not interaction.response.is_done(): await interaction.response.send_message(f"{member.mention} mis en sourdine.", ephemeral=True)
else: await interaction.followup.send(f"{member.mention} mis en sourdine.", ephemeral=True)
cursor.execute('SELECT id, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "TIMEOUT" ORDER BY id DESC LIMIT 1', (interaction.guild_id, member.id))
s_id, ts = cursor.fetchone(); conn.commit(); conn.close()
if not interaction.response.is_done(): await interaction.response.send_message(f"{member.mention} timeout.", ephemeral=True)
await self.send_sanction_board(interaction.guild, self.build_sanction_board_components(s_type="TIMEOUT", user=member, moderator=interaction.user, reason=reason, timestamp=ts, s_id=s_id, duration=secs))
async def kick(self, interaction: discord.Interaction, member: discord.Member, reason: str):
async def kick(self, interaction, member, reason):
await member.kick(reason=reason)
conn = sqlite3.connect(self.db_path); cursor = conn.cursor()
cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?)', (interaction.guild_id, member.id, interaction.user.id, 'KICK', reason))
conn.commit(); conn.close()
cursor.execute('SELECT id, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "KICK" ORDER BY id DESC LIMIT 1', (interaction.guild_id, member.id))
s_id, ts = cursor.fetchone(); conn.commit(); conn.close()
if not interaction.response.is_done(): await interaction.response.send_message(f"{member.name} expulsé.", ephemeral=True)
else: await interaction.followup.send(f"{member.name} expulsé.", ephemeral=True)
await self.send_sanction_board(interaction.guild, self.build_sanction_board_components(s_type="KICK", user=member, moderator=interaction.user, reason=reason, timestamp=ts, s_id=s_id))
async def ban(self, interaction: discord.Interaction, user: discord.User, reason: str):
async def ban(self, interaction, user, reason):
await interaction.guild.ban(user, reason=reason)
conn = sqlite3.connect(self.db_path); cursor = conn.cursor()
cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?)', (interaction.guild_id, user.id, interaction.user.id, 'BAN', reason))
conn.commit(); conn.close()
cursor.execute('SELECT id, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = "BAN" ORDER BY id DESC LIMIT 1', (interaction.guild_id, user.id))
s_id, ts = cursor.fetchone(); conn.commit(); conn.close()
if not interaction.response.is_done(): await interaction.response.send_message(f"{user.name} banni.", ephemeral=True)
else: await interaction.followup.send(f"{user.name} banni.", ephemeral=True)
await self.send_sanction_board(interaction.guild, self.build_sanction_board_components(s_type="BAN", user=user, moderator=interaction.user, reason=reason, timestamp=ts, s_id=s_id))
async def history(self, interaction: discord.Interaction, user: discord.User):
conn = sqlite3.connect(self.db_path); cursor = conn.cursor()
cursor.execute('SELECT id, type, reason, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? ORDER BY timestamp DESC', (interaction.guild_id, user.id))
results = cursor.fetchall(); conn.close()
if not results:
if not interaction.response.is_done(): return await interaction.response.send_message(" Aucun historique.", ephemeral=True)
else: return await interaction.followup.send(" Aucun historique.", ephemeral=True)
embed = discord.Embed(title=f"📜 Historique de {user.name}", color=discord.Color.blue())
for s_id, s_type, reason, ts in results[:10]:
embed.add_field(name=f"#{s_id} - {s_type}", value=f"**Date:** {ts}\n**Raison:** {reason}", inline=False)
if not interaction.response.is_done(): await interaction.response.send_message(embed=embed, ephemeral=True)
else: await interaction.followup.send(embed=embed, ephemeral=True)
async def clearwarns(self, interaction: discord.Interaction, member: discord.Member, reason: str):
async def clearwarns(self, interaction, member, reason):
conn = sqlite3.connect(self.db_path); cursor = conn.cursor()
cursor.execute('UPDATE sanctions SET status = "REVOKED" WHERE guild_id = ? AND user_id = ? AND type = "WARN"', (interaction.guild_id, member.id))
conn.commit(); conn.close()
if not interaction.response.is_done(): await interaction.response.send_message(f"✅ Warns de {member.name} effacés.", ephemeral=True)
else: await interaction.followup.send(f"✅ Warns de {member.name} effacés.", ephemeral=True)
if not interaction.response.is_done(): await interaction.response.send_message(f"✅ Warns effacés.", ephemeral=True)
async def setup(bot):
await bot.add_cog(Moderation(bot))
def setup(bot): bot.add_cog(Moderation(bot))

View file

@ -1,5 +1,5 @@
import discord
from discord.ext import commands
import disnake
from disnake.ext import commands
import json
import os
from datetime import datetime
@ -53,8 +53,8 @@ class AntiRaid(commands.Cog):
if log_channel_id:
channel = guild.get_channel(int(log_channel_id))
if channel:
embed = discord.Embed(title="🚨 ALERTE RAID", description=f"Vague de {len(self.join_cache[guild.id])} membres !", color=0xFF0000)
embed = disnake.Embed(title="🚨 ALERTE RAID", description=f"Vague de {len(self.join_cache[guild.id])} membres !", color=0xFF0000)
await channel.send(embed=embed)
async def setup(bot):
await bot.add_cog(AntiRaid(bot))
def setup(bot):
bot.add_cog(AntiRaid(bot))

View file

@ -1,5 +1,5 @@
import discord
from discord.ext import commands
import disnake
from disnake.ext import commands
import json
import os
from collections import deque
@ -13,7 +13,7 @@ WHITELIST_FILE = "data/whitelist.json"
CACHE_TTL = 5
# Triggers pour détection de liens
LINK_TRIGGERS = ("discord.gg/", "http://", "https://", "www.")
LINK_TRIGGERS = ("disnake.gg/", "http://", "https://", "www.")
class AntiSpam(commands.Cog):
def __init__(self, bot):
@ -28,7 +28,7 @@ class AntiSpam(commands.Cog):
def _get_cached_settings(self, guild_id):
"""Récupère les paramètres avec cache (TTL de 5 secondes)"""
now = discord.utils.utcnow().timestamp()
now = disnake.utils.utcnow().timestamp()
cached = self._settings_cache.get(guild_id)
if cached:
@ -52,7 +52,7 @@ class AntiSpam(commands.Cog):
def _get_cached_whitelist(self, guild_id):
"""Récupère la whitelist avec cache (TTL de 5 secondes)"""
now = discord.utils.utcnow().timestamp()
now = disnake.utils.utcnow().timestamp()
cached = self._whitelist_cache.get(guild_id)
if cached:
@ -117,7 +117,7 @@ class AntiSpam(commands.Cog):
# --- TEST ANTI-SPAM (deque pour O(1)) ---
if settings.get("anti_spam") is True:
now = discord.utils.utcnow().timestamp()
now = disnake.utils.utcnow().timestamp()
# Initialiser ou récupérer le deque pour cet utilisateur
if user_id not in self.message_counts:
@ -156,7 +156,7 @@ class AntiSpam(commands.Cog):
self.message_counts[user_id] = deque(maxlen=10)
if current_warns <= 3:
embed = discord.Embed(
embed = disnake.Embed(
title="🛡️ Sécurité Kuby",
description=f"{message.author.mention}, {reason}.\n**Avertissement : {current_warns}/3**",
color=0xFFA500
@ -169,10 +169,10 @@ class AntiSpam(commands.Cog):
async def apply_mute(self, message, reason):
try:
until = discord.utils.utcnow() + timedelta(minutes=5)
until = disnake.utils.utcnow() + timedelta(minutes=5)
await message.author.timeout(until, reason=f"Kuby Security: {reason} (3 Warns)")
embed = discord.Embed(
embed = disnake.Embed(
title="⛔ Exclusion Temporaire",
description=f"**Membre :** {message.author.mention}\n**Durée :** 5 minutes\n**Raison :** {reason} répétitif.",
color=0xFF0000
@ -181,5 +181,5 @@ class AntiSpam(commands.Cog):
except Exception as e:
print(f"Erreur lors de la sanction : {e}")
async def setup(bot):
await bot.add_cog(AntiSpam(bot))
def setup(bot):
bot.add_cog(AntiSpam(bot))

View file

@ -1,5 +1,5 @@
import discord
from discord.ext import commands
import disnake
from disnake.ext import commands
import json
import os
from datetime import datetime
@ -23,11 +23,11 @@ class LogsManager(commands.Cog):
log_chan = get_log_channel(message.guild, settings)
if log_chan:
embed = discord.Embed(
embed = disnake.Embed(
title="🗑️ Message Supprimé",
description=f"**Auteur :** {message.author.mention}\n**Salon :** {message.channel.mention}",
color=0xFFA500,
timestamp=datetime.utcnow()
timestamp=disnake.utils.utcnow()
)
embed.add_field(name="Contenu", value=message.content or "*(Image ou Embed)*")
await log_chan.send(embed=embed)
@ -37,11 +37,11 @@ class LogsManager(commands.Cog):
settings = dict(json.load(open(SETTINGS_FILE, "r"))).get(str(member.guild.id), {})
log_chan = get_log_channel(member.guild, settings)
if log_chan:
embed = discord.Embed(
embed = disnake.Embed(
title="📤 Départ d'un membre",
description=f"{member.mention} ({member.id}) a quitté le serveur.",
color=0xFF0000,
timestamp=datetime.utcnow()
timestamp=disnake.utils.utcnow()
)
await log_chan.send(embed=embed)
@ -50,13 +50,13 @@ class LogsManager(commands.Cog):
settings = dict(json.load(open(SETTINGS_FILE, "r"))).get(str(guild.id), {})
log_chan = get_log_channel(guild, settings)
if log_chan:
embed = discord.Embed(
embed = disnake.Embed(
title="🔨 Bannissement",
description=f"**Utilisateur :** {user.mention} ({user.id}) a été banni.",
color=0x000000,
timestamp=datetime.utcnow()
timestamp=disnake.utils.utcnow()
)
await log_chan.send(embed=embed)
async def setup(bot):
await bot.add_cog(LogsManager(bot))
def setup(bot):
bot.add_cog(LogsManager(bot))

View file

@ -1,6 +1,5 @@
import discord
from discord import app_commands
from discord.ext import commands
import disnake
from disnake.ext import commands
import json
import os
from typing import Optional
@ -39,9 +38,9 @@ def save_settings(guild_id: int, settings: dict) -> None:
# --- MODALS DE CONFIGURATION DU RÈGLEMENT (BACK-END) ---
class RulesTitleModal(discord.ui.Modal, title="📜 Titre du Règlement"):
class RulesTitleModal(disnake.ui.Modal):
"""Modal pour configurer le titre du règlement"""
title_input = discord.ui.TextInput(
title_input = disnake.ui.TextInput(
label="Titre du règlement",
placeholder="Ex: 📜 Règlement de [Nom du Serveur]",
min_length=1,
@ -50,12 +49,12 @@ class RulesTitleModal(discord.ui.Modal, title="📜 Titre du Règlement"):
)
def __init__(self, guild_id: int, settings: dict, on_update_callback=None):
super().__init__()
super().__init__(title="📜 Titre du Règlement", components=[self.title_input])
self.guild_id = guild_id
self.settings = settings
self.on_update_callback = on_update_callback
async def on_submit(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.Interaction):
self.settings["rules_title"] = self.title_input.value
save_settings(self.guild_id, self.settings)
@ -78,24 +77,24 @@ class RulesTitleModal(discord.ui.Modal, title="📜 Titre du Règlement"):
return view
class RulesContentModal(discord.ui.Modal, title="📝 Contenu du Règlement"):
class RulesContentModal(disnake.ui.Modal):
"""Modal pour configurer le contenu du règlement"""
content_input = discord.ui.TextInput(
content_input = disnake.ui.TextInput(
label="Contenu du règlement",
placeholder="Entrez les règles de votre serveur...",
style=discord.TextStyle.paragraph,
style=disnake.TextInputStyle.paragraph,
min_length=10,
max_length=4000,
required=True
)
def __init__(self, guild_id: int, settings: dict, on_update_callback=None):
super().__init__()
super().__init__(title="📝 Contenu du Règlement", components=[self.content_input])
self.guild_id = guild_id
self.settings = settings
self.on_update_callback = on_update_callback
async def on_submit(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.Interaction):
self.settings["rules_content"] = self.content_input.value
save_settings(self.guild_id, self.settings)
@ -118,9 +117,9 @@ class RulesContentModal(discord.ui.Modal, title="📝 Contenu du Règlement"):
return view
class RulesChannelModal(discord.ui.Modal, title="📌 Salon du Règlement"):
class RulesChannelModal(disnake.ui.Modal):
"""Modal pour sélectionner le salon du règlement"""
channel_input = discord.ui.TextInput(
channel_input = disnake.ui.TextInput(
label="ID du salon",
placeholder="Entrez l'ID du salon où envoyer le règlement",
min_length=15,
@ -129,12 +128,12 @@ class RulesChannelModal(discord.ui.Modal, title="📌 Salon du Règlement"):
)
def __init__(self, guild_id: int, settings: dict, on_update_callback=None):
super().__init__()
super().__init__(title="📌 Salon du Règlement", components=[self.channel_input])
self.guild_id = guild_id
self.settings = settings
self.on_update_callback = on_update_callback
async def on_submit(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.Interaction):
if not self.channel_input.value.isdigit():
return await interaction.response.send_message(
"❌ Erreur : ID invalide.",
@ -172,9 +171,9 @@ class RulesChannelModal(discord.ui.Modal, title="📌 Salon du Règlement"):
return view
class RulesRoleModal(discord.ui.Modal, title="✅ Rôle d'Acceptation"):
class RulesRoleModal(disnake.ui.Modal):
"""Modal pour configurer le rôle attribué à l'acceptation"""
role_input = discord.ui.TextInput(
role_input = disnake.ui.TextInput(
label="ID du rôle",
placeholder="Entrez l'ID du rôle à attribuer",
min_length=15,
@ -183,12 +182,12 @@ class RulesRoleModal(discord.ui.Modal, title="✅ Rôle d'Acceptation"):
)
def __init__(self, guild_id: int, settings: dict, on_update_callback=None):
super().__init__()
super().__init__(title="✅ Rôle d'Acceptation", components=[self.role_input])
self.guild_id = guild_id
self.settings = settings
self.on_update_callback = on_update_callback
async def on_submit(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.Interaction):
if not self.role_input.value.isdigit():
return await interaction.response.send_message(
"❌ Erreur : ID invalide.",
@ -226,9 +225,9 @@ class RulesRoleModal(discord.ui.Modal, title="✅ Rôle d'Acceptation"):
return view
class RulesMessageTypeModal(discord.ui.Modal, title="📝 Type de Message"):
class RulesMessageTypeModal(disnake.ui.Modal):
"""Modal pour choisir le type de message du règlement"""
message_type = discord.ui.TextInput(
message_type = disnake.ui.TextInput(
label="Type de message",
placeholder="Tapez 'embed' pour embed ou 'simple' pour message simple",
min_length=5,
@ -237,12 +236,12 @@ class RulesMessageTypeModal(discord.ui.Modal, title="📝 Type de Message"):
)
def __init__(self, guild_id: int, settings: dict, on_update_callback=None):
super().__init__()
super().__init__(title="📝 Type de Message", components=[self.message_type])
self.guild_id = guild_id
self.settings = settings
self.on_update_callback = on_update_callback
async def on_submit(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.Interaction):
msg_type = self.message_type.value.lower().strip()
if msg_type not in ["embed", "simple"]:
@ -274,11 +273,11 @@ class RulesMessageTypeModal(discord.ui.Modal, title="📝 Type de Message"):
return view
class ToggleRulesButton(discord.ui.Button):
class ToggleRulesButton(disnake.ui.Button):
"""Bouton de bascule pour activer/désactiver le règlement"""
def __init__(self, is_enabled: bool):
style = discord.ButtonStyle.green if is_enabled else discord.ButtonStyle.red
style = disnake.ButtonStyle.green if is_enabled else disnake.ButtonStyle.red
status = "" if is_enabled else ""
super().__init__(
label=f"Règlement [{status}]",
@ -287,7 +286,7 @@ class ToggleRulesButton(discord.ui.Button):
emoji="📜"
)
async def callback(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.Interaction):
view = self.view
settings = view.settings
guild = interaction.guild
@ -316,7 +315,7 @@ class ToggleRulesButton(discord.ui.Button):
await interaction.response.edit_message(embed=embed, view=new_view)
class RulesAcceptButton(discord.ui.View):
class RulesAcceptButton(disnake.ui.View):
"""Bouton persistant pour accepter le règlement"""
def __init__(self, guild_id: int, accept_role_id: int):
@ -324,13 +323,13 @@ class RulesAcceptButton(discord.ui.View):
self.guild_id = guild_id
self.accept_role_id = accept_role_id
@discord.ui.button(
@disnake.ui.button(
label="J'accepte le règlement",
emoji="",
style=discord.ButtonStyle.green,
style=disnake.ButtonStyle.green,
custom_id="rules_accept_button"
)
async def accept_rules(self, interaction: discord.Interaction, button: discord.ui.Button):
async def accept_rules(self, button: disnake.ui.Button, interaction: disnake.Interaction):
"""Gère le clic sur le bouton d'acceptation"""
guild = interaction.guild
@ -368,7 +367,7 @@ class RulesAcceptButton(discord.ui.View):
f"Le rôle **{role.name}** vous a été attribué.",
ephemeral=True
)
except discord.Forbidden:
except disnake.Forbidden:
await interaction.response.send_message(
"❌ Je n'ai pas les permissions pour attribuer ce rôle.",
ephemeral=True
@ -421,7 +420,7 @@ class RulesCog(commands.Cog):
"""Appelé quand le bot est prêt"""
print("🎫 Module Règles chargé avec succès!")
async def send_rules_message(self, channel: discord.TextChannel, settings: dict) -> Optional[discord.Message]:
async def send_rules_message(self, channel: disnake.TextChannel, settings: dict) -> Optional[disnake.Message]:
"""Envoie le message du règlement dans un salon"""
title = settings.get("rules_title", "📜 Règlement du Serveur")
content = settings.get("rules_content", "")
@ -457,7 +456,7 @@ class RulesCog(commands.Cog):
else: # embed
# Créer l'embed
embed = discord.Embed(
embed = disnake.Embed(
title=title,
description=content,
color=0x2b2d31
@ -496,8 +495,8 @@ class RulesCog(commands.Cog):
return title, content
async def setup(bot):
def setup(bot):
"""Setup du cog de règlement"""
cog = RulesCog(bot)
await bot.add_cog(cog)
bot.add_cog(cog)

View file

@ -1,7 +1,7 @@
# utils/no_link.py
import re
import discord
from discord.ext import commands
import disnake
from disnake.ext import commands
import json
import os
@ -26,7 +26,7 @@ class NoLink(commands.Cog):
self.bot = bot
@commands.Cog.listener()
async def on_message(self, message: discord.Message) -> None:
async def on_message(self, message: disnake.Message) -> None:
if message.author.bot or not message.guild:
return
@ -41,11 +41,11 @@ class NoLink(commands.Cog):
f"{message.author.mention} ⚠️ Les liens sont interdits sur ce serveur.",
delete_after=5
)
except discord.Forbidden:
except disnake.Forbidden:
print(f"Impossible de supprimer le message de {message.author} (permissions manquantes).")
except Exception as exc:
print(f"Erreur lors de la suppression du message : {exc}")
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(NoLink(bot))
def setup(bot: commands.Bot) -> None:
bot.add_cog(NoLink(bot))

View file

@ -1,7 +1,6 @@
# scan.py
import discord
from discord import app_commands
from discord.ext import commands
import disnake
from disnake.ext import commands
import hashlib
import os
import json
@ -59,8 +58,8 @@ class Scan(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
@app_commands.command(name="scan", description="Scanner un fichier pour détecter un malware.")
async def scan(self, interaction: discord.Interaction, file: discord.Attachment):
@commands.slash_command(name="scan", description="Scanner un fichier pour détecter un malware.")
async def scan(self, interaction: disnake.ApplicationCommandInteraction, file: disnake.Attachment):
await interaction.response.defer() # Permet de prendre un peu de temps pour le scan
# Téléchargement temporaire du fichier
@ -78,5 +77,5 @@ class Scan(commands.Cog):
await interaction.followup.send(f"✅ Fichier sain.\nHash : `{result['hash']}`")
async def setup(bot: commands.Bot):
await bot.add_cog(Scan(bot))
def setup(bot):
bot.add_cog(Scan(bot))

View file

@ -1,6 +1,5 @@
import discord
from discord import app_commands
from discord.ext import commands
import disnake
from disnake.ext import commands
import json
import os
from typing import Optional
@ -70,9 +69,9 @@ def is_immune(guild, user) -> bool:
# --- MODALS DE CONFIGURATION ---
class SpamThresholdModal(discord.ui.Modal, title="🛡️ Seuil Anti-Spam"):
class SpamThresholdModal(disnake.ui.Modal):
"""Modal pour configurer le seuil anti-spam"""
threshold_input = discord.ui.TextInput(
threshold_input = disnake.ui.TextInput(
label="Messages max autorisés",
placeholder="Entre 3 et 10 (défaut: 5)",
min_length=1,
@ -81,12 +80,12 @@ class SpamThresholdModal(discord.ui.Modal, title="🛡️ Seuil Anti-Spam"):
)
def __init__(self, guild_id: int, settings: dict, view: 'SecurityView'):
super().__init__()
super().__init__(title="🛡️ Seuil Anti-Spam", components=[self.threshold_input])
self.guild_id = guild_id
self.settings = settings
self.parent_view = view
async def on_submit(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.Interaction):
if not self.threshold_input.value.isdigit():
return await interaction.response.send_message(
"❌ Erreur : Veuillez entrer un nombre entier.",
@ -111,9 +110,9 @@ class SpamThresholdModal(discord.ui.Modal, title="🛡️ Seuil Anti-Spam"):
ephemeral=True
)
class RaidThresholdModal(discord.ui.Modal, title="⚔️ Seuil Anti-Raid"):
class RaidThresholdModal(disnake.ui.Modal):
"""Modal pour configurer le seuil anti-raid"""
threshold_input = discord.ui.TextInput(
threshold_input = disnake.ui.TextInput(
label="Joins max en 10 secondes",
placeholder="Entre 2 et 10 (défaut: 3)",
min_length=1,
@ -122,12 +121,12 @@ class RaidThresholdModal(discord.ui.Modal, title="⚔️ Seuil Anti-Raid"):
)
def __init__(self, guild_id: int, settings: dict, view: 'SecurityView'):
super().__init__()
super().__init__(title="⚔️ Seuil Anti-Raid", components=[self.threshold_input])
self.guild_id = guild_id
self.settings = settings
self.parent_view = view
async def on_submit(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.Interaction):
if not self.threshold_input.value.isdigit():
return await interaction.response.send_message(
"❌ Erreur : Veuillez entrer un nombre entier.",
@ -154,9 +153,9 @@ class RaidThresholdModal(discord.ui.Modal, title="⚔️ Seuil Anti-Raid"):
# --- MODALS DE CONFIGURATION WHITELIST ---
class WhitelistModal(discord.ui.Modal, title="👤 Ajouter à la Whitelist"):
class WhitelistModal(disnake.ui.Modal):
"""Modal pour ajouter un utilisateur à la whitelist"""
user_input = discord.ui.TextInput(
user_input = disnake.ui.TextInput(
label="ID de l'utilisateur",
placeholder="Entrez l'ID Discord de l'utilisateur",
min_length=15,
@ -165,11 +164,11 @@ class WhitelistModal(discord.ui.Modal, title="👤 Ajouter à la Whitelist"):
)
def __init__(self, guild_id: int, view: 'SecurityView'):
super().__init__()
super().__init__(title="👤 Ajouter à la Whitelist", components=[self.user_input])
self.guild_id = guild_id
self.parent_view = view
async def on_submit(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.Interaction):
if not self.user_input.value.isdigit():
return await interaction.response.send_message(
"❌ Erreur : ID invalide.",
@ -213,10 +212,10 @@ class WhitelistModal(discord.ui.Modal, title="👤 Ajouter à la Whitelist"):
# --- VUES DE L'INTERFACE ---
class SecurityView(discord.ui.View):
class SecurityView(disnake.ui.View):
"""Vue principale du panneau de sécurité"""
def __init__(self, interaction: discord.Interaction, settings: dict, category: Optional[str] = None, bot=None):
def __init__(self, interaction: disnake.Interaction, settings: dict, category: Optional[str] = None, bot=None):
super().__init__(timeout=300) # 5 minutes timeout
self.interaction = interaction
self.guild = interaction.guild
@ -270,18 +269,18 @@ class SecurityView(discord.ui.View):
# Bouton de configuration du seuil
threshold = self.settings.get("spam_threshold", 5)
btn_threshold = discord.ui.Button(
btn_threshold = disnake.ui.Button(
label=f"🔢 Seuil: {threshold} msg/5s",
style=discord.ButtonStyle.blurple,
style=disnake.ButtonStyle.blurple,
custom_id="spam_threshold"
)
btn_threshold.callback = self.open_spam_threshold_modal
self.add_item(btn_threshold)
# Bouton retour
btn_back = discord.ui.Button(
btn_back = disnake.ui.Button(
label="Retour",
style=discord.ButtonStyle.gray,
style=disnake.ButtonStyle.gray,
emoji="⬅️"
)
btn_back.callback = self.back_to_main
@ -311,18 +310,18 @@ class SecurityView(discord.ui.View):
# Bouton de configuration du seuil
threshold = self.settings.get("raid_threshold", 3)
btn_threshold = discord.ui.Button(
btn_threshold = disnake.ui.Button(
label=f"🔢 Seuil: {threshold} join/10s",
style=discord.ButtonStyle.blurple,
style=disnake.ButtonStyle.blurple,
custom_id="raid_threshold"
)
btn_threshold.callback = self.open_raid_threshold_modal
self.add_item(btn_threshold)
# Bouton retour
btn_back = discord.ui.Button(
btn_back = disnake.ui.Button(
label="Retour",
style=discord.ButtonStyle.gray,
style=disnake.ButtonStyle.gray,
emoji="⬅️"
)
btn_back.callback = self.back_to_main
@ -351,9 +350,9 @@ class SecurityView(discord.ui.View):
self.add_item(btn_role)
# Bouton retour
btn_back = discord.ui.Button(
btn_back = disnake.ui.Button(
label="Retour",
style=discord.ButtonStyle.gray,
style=disnake.ButtonStyle.gray,
emoji="⬅️"
)
btn_back.callback = self.back_to_main
@ -376,23 +375,23 @@ class SecurityView(discord.ui.View):
if log_channel_id:
channel = self.guild.get_channel(int(log_channel_id))
channel_mention = channel.mention if channel else f"`{log_channel_id}`"
btn_channel = discord.ui.Button(
btn_channel = disnake.ui.Button(
label=f"📌 Channel: {channel.name if channel else 'Invalide'}",
style=discord.ButtonStyle.green,
style=disnake.ButtonStyle.green,
disabled=True
)
else:
btn_channel = discord.ui.Button(
btn_channel = disnake.ui.Button(
label="📌 Aucun channel configuré",
style=discord.ButtonStyle.red,
style=disnake.ButtonStyle.red,
disabled=True
)
self.add_item(btn_channel)
# Bouton retour
btn_back = discord.ui.Button(
btn_back = disnake.ui.Button(
label="Retour",
style=discord.ButtonStyle.gray,
style=disnake.ButtonStyle.gray,
emoji="⬅️"
)
btn_back.callback = self.back_to_main
@ -401,9 +400,9 @@ class SecurityView(discord.ui.View):
def build_whitelist_view(self):
"""Construit la vue Whitelist"""
# Affichage du nombre avec style
btn_count = discord.ui.Button(
btn_count = disnake.ui.Button(
label=f"👥 Whitelist: {self.whitelist_count} utilisateur(s)",
style=discord.ButtonStyle.blurple,
style=disnake.ButtonStyle.blurple,
disabled=True
)
self.add_item(btn_count)
@ -413,17 +412,17 @@ class SecurityView(discord.ui.View):
self.add_item(WhitelistSelect(self.guild, self))
else:
# Message si whitelist vide
btn_empty = discord.ui.Button(
btn_empty = disnake.ui.Button(
label="📭 Aucun utilisateur dans la whitelist",
style=discord.ButtonStyle.gray,
style=disnake.ButtonStyle.gray,
disabled=True
)
self.add_item(btn_empty)
# Bouton ajouter
btn_add = discord.ui.Button(
btn_add = disnake.ui.Button(
label=" Ajouter",
style=discord.ButtonStyle.green,
style=disnake.ButtonStyle.green,
emoji="",
custom_id="whitelist_add"
)
@ -431,9 +430,9 @@ class SecurityView(discord.ui.View):
self.add_item(btn_add)
# Bouton retour
btn_back = discord.ui.Button(
btn_back = disnake.ui.Button(
label="Retour",
style=discord.ButtonStyle.gray,
style=disnake.ButtonStyle.gray,
emoji="⬅️"
)
btn_back.callback = self.back_to_main
@ -458,18 +457,18 @@ class SecurityView(discord.ui.View):
# Bouton pour modifier le titre
btn_title = discord.ui.Button(
btn_title = disnake.ui.Button(
label="Éditer le titre",
style=discord.ButtonStyle.blurple,
style=disnake.ButtonStyle.blurple,
emoji="📝"
)
btn_title.callback = self.open_rules_title_modal
self.add_item(btn_title)
# Bouton pour modifier le contenu
btn_content = discord.ui.Button(
btn_content = disnake.ui.Button(
label="Éditer le contenu",
style=discord.ButtonStyle.blurple,
style=disnake.ButtonStyle.blurple,
emoji="📄"
)
btn_content.callback = self.open_rules_content_modal
@ -479,15 +478,15 @@ class SecurityView(discord.ui.View):
rules_channel_id = self.settings.get("rules_channel_id")
if rules_channel_id:
channel = self.guild.get_channel(int(rules_channel_id))
btn_channel = discord.ui.Button(
btn_channel = disnake.ui.Button(
label=f"Salon: {channel.name if channel else 'Invalide'}",
style=discord.ButtonStyle.green,
style=disnake.ButtonStyle.green,
custom_id="rules_channel"
)
else:
btn_channel = discord.ui.Button(
btn_channel = disnake.ui.Button(
label="Aucun salon configuré",
style=discord.ButtonStyle.red,
style=disnake.ButtonStyle.red,
custom_id="rules_channel"
)
btn_channel.callback = self.open_rules_channel_modal
@ -497,15 +496,15 @@ class SecurityView(discord.ui.View):
rules_role_id = self.settings.get("rules_accept_role_id")
if rules_role_id:
role = self.guild.get_role(int(rules_role_id))
btn_role = discord.ui.Button(
btn_role = disnake.ui.Button(
label=f"Rôle: {role.name if role else 'Invalide'}",
style=discord.ButtonStyle.green,
style=disnake.ButtonStyle.green,
custom_id="rules_role"
)
else:
btn_role = discord.ui.Button(
btn_role = disnake.ui.Button(
label="Aucun rôle configuré",
style=discord.ButtonStyle.red,
style=disnake.ButtonStyle.red,
custom_id="rules_role"
)
btn_role.callback = self.open_rules_role_modal
@ -514,9 +513,9 @@ class SecurityView(discord.ui.View):
# Bouton pour choisir le type de message
message_type = self.settings.get("rules_message_type", "embed")
type_label = "Embed" if message_type == "embed" else "Message simple"
btn_message_type = discord.ui.Button(
btn_message_type = disnake.ui.Button(
label=f"Type: {type_label}",
style=discord.ButtonStyle.blurple,
style=disnake.ButtonStyle.blurple,
emoji="📝",
custom_id="rules_message_type"
)
@ -524,84 +523,84 @@ class SecurityView(discord.ui.View):
self.add_item(btn_message_type)
# Bouton pour envoyer le règlement
btn_send = discord.ui.Button(
btn_send = disnake.ui.Button(
label="Envoyer le règlement",
style=discord.ButtonStyle.green,
style=disnake.ButtonStyle.green,
emoji="🚀"
)
btn_send.callback = self.send_rules
self.add_item(btn_send)
# Bouton retour
btn_back = discord.ui.Button(
btn_back = disnake.ui.Button(
label="Retour",
style=discord.ButtonStyle.gray,
style=disnake.ButtonStyle.gray,
emoji="⬅️"
)
btn_back.callback = self.back_to_main
self.add_item(btn_back)
async def toggle_setting(self, interaction: discord.Interaction, key: str):
async def toggle_setting(self, interaction: disnake.Interaction, key: str):
"""Bascule un paramètre"""
self.settings[key] = not self.settings.get(key, False)
save_settings(self.guild.id, self.settings)
self.build_view()
await interaction.response.edit_message(view=self)
async def open_spam_threshold_modal(self, interaction: discord.Interaction):
async def open_spam_threshold_modal(self, interaction: disnake.Interaction):
"""Ouvre le modal de configuration du seuil spam"""
await interaction.response.send_modal(
SpamThresholdModal(self.guild.id, self.settings, self)
)
async def open_raid_threshold_modal(self, interaction: discord.Interaction):
async def open_raid_threshold_modal(self, interaction: disnake.Interaction):
"""Ouvre le modal de configuration du seuil raid"""
await interaction.response.send_modal(
RaidThresholdModal(self.guild.id, self.settings, self)
)
async def open_whitelist_modal(self, interaction: discord.Interaction):
async def open_whitelist_modal(self, interaction: disnake.Interaction):
"""Ouvre le modal d'ajout à la whitelist"""
await interaction.response.send_modal(
WhitelistModal(self.guild.id, self)
)
async def open_rules_title_modal(self, interaction: discord.Interaction):
async def open_rules_title_modal(self, interaction: disnake.Interaction):
"""Ouvre le modal de configuration du titre du règlement"""
from .modules_security.rules import RulesTitleModal
await interaction.response.send_modal(
RulesTitleModal(self.guild.id, self.settings)
)
async def open_rules_content_modal(self, interaction: discord.Interaction):
async def open_rules_content_modal(self, interaction: disnake.Interaction):
"""Ouvre le modal de configuration du contenu du règlement"""
from .modules_security.rules import RulesContentModal
await interaction.response.send_modal(
RulesContentModal(self.guild.id, self.settings)
)
async def open_rules_channel_modal(self, interaction: discord.Interaction):
async def open_rules_channel_modal(self, interaction: disnake.Interaction):
"""Ouvre le modal de sélection du salon du règlement"""
from .modules_security.rules import RulesChannelModal
await interaction.response.send_modal(
RulesChannelModal(self.guild.id, self.settings)
)
async def open_rules_role_modal(self, interaction: discord.Interaction):
async def open_rules_role_modal(self, interaction: disnake.Interaction):
"""Ouvre le modal de configuration du rôle d'acceptation"""
from .modules_security.rules import RulesRoleModal
await interaction.response.send_modal(
RulesRoleModal(self.guild.id, self.settings)
)
async def open_rules_message_type_modal(self, interaction: discord.Interaction):
async def open_rules_message_type_modal(self, interaction: disnake.Interaction):
"""Ouvre le modal de sélection du type de message"""
from .modules_security.rules import RulesMessageTypeModal
await interaction.response.send_modal(
RulesMessageTypeModal(self.guild.id, self.settings)
)
async def send_rules(self, interaction: discord.Interaction):
async def send_rules(self, interaction: disnake.Interaction):
"""Envoie le règlement dans le salon configuré"""
from commandes.modules_security.rules import RulesCog
@ -670,16 +669,16 @@ class SecurityView(discord.ui.View):
ephemeral=True
)
async def back_to_main(self, interaction: discord.Interaction):
async def back_to_main(self, interaction: disnake.Interaction):
"""Retour au menu principal"""
self.category = None
self.build_view()
embed = self.create_main_embed(interaction.user)
await interaction.response.edit_message(embed=embed, view=self)
def create_main_embed(self, user: discord.Member = None) -> discord.Embed:
def create_main_embed(self, user: disnake.Member = None) -> disnake.Embed:
"""Crée l'embed du menu principal"""
embed = discord.Embed(
embed = disnake.Embed(
title="🛡️ Centre de Sécurité Kuby",
description="Gérez la protection de votre serveur",
color=0x7289DA
@ -726,9 +725,9 @@ class SecurityView(discord.ui.View):
return embed
def create_category_embed(self, title: str, description: str) -> discord.Embed:
def create_category_embed(self, title: str, description: str) -> disnake.Embed:
"""Crée un embed pour une catégorie"""
embed = discord.Embed(
embed = disnake.Embed(
title=title,
description=description,
color=0x7289DA
@ -740,11 +739,11 @@ class SecurityView(discord.ui.View):
return embed
class ToggleButton(discord.ui.Button):
class ToggleButton(disnake.ui.Button):
"""Bouton de bascule avec indicateur de statut"""
def __init__(self, label: str, emoji: str, is_enabled: bool, custom_id: str):
style = discord.ButtonStyle.green if is_enabled else discord.ButtonStyle.red
style = disnake.ButtonStyle.green if is_enabled else disnake.ButtonStyle.red
status = "" if is_enabled else ""
super().__init__(
label=f"{label} [{status}]",
@ -753,7 +752,7 @@ class ToggleButton(discord.ui.Button):
emoji=emoji
)
async def callback(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.Interaction):
view: SecurityView = self.view
key = interaction.data['custom_id']
@ -771,42 +770,42 @@ class ToggleButton(discord.ui.Button):
await interaction.response.edit_message(view=view)
class MainCategorySelect(discord.ui.Select):
class MainCategorySelect(disnake.ui.Select):
"""Menu de sélection des catégories"""
def __init__(self):
options = [
discord.SelectOption(
disnake.SelectOption(
label="Anti-Spam",
emoji="🛡️",
value="spam",
description="Protection contre le spam et les liens"
),
discord.SelectOption(
disnake.SelectOption(
label="Anti-Raid",
emoji="⚔️",
value="raid",
description="Protection contre les raids"
),
discord.SelectOption(
disnake.SelectOption(
label="Anti-Nuke",
emoji="☢️",
value="nuke",
description="Protection contre la destruction du serveur"
),
discord.SelectOption(
disnake.SelectOption(
label="Logs",
emoji="📋",
value="logs",
description="Journalisation des événements"
),
discord.SelectOption(
disnake.SelectOption(
label="Règlement",
emoji="📜",
value="rules",
description="Créer et envoyer un règlement"
),
discord.SelectOption(
disnake.SelectOption(
label="Whitelist",
emoji="👥",
value="whitelist",
@ -820,7 +819,7 @@ class MainCategorySelect(discord.ui.Select):
max_values=1
)
async def callback(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.Interaction):
view: SecurityView = self.view
view.category = self.values[0]
view.build_view()
@ -871,10 +870,10 @@ class MainCategorySelect(discord.ui.Select):
await interaction.response.edit_message(embed=embed, view=view)
class WhitelistSelect(discord.ui.Select):
class WhitelistSelect(disnake.ui.Select):
"""Menu dropdown pour voir et gérer les utilisateurs whitelistés"""
def __init__(self, guild: discord.Guild, view: 'SecurityView'):
def __init__(self, guild: disnake.Guild, view: 'SecurityView'):
self.guild = guild
self.parent_view = view
whitelist = load_whitelist(guild.id)
@ -890,7 +889,7 @@ class WhitelistSelect(discord.ui.Select):
label = f"ID: {user_id} (non trouvé)"
emoji = ""
options.append(
discord.SelectOption(
disnake.SelectOption(
label=label[:100], # Discord limite à 100 chars
value=str(user_id),
emoji=emoji,
@ -905,7 +904,7 @@ class WhitelistSelect(discord.ui.Select):
max_values=1
)
async def callback(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.Interaction):
"""Gère la sélection d'un utilisateur à retirer"""
user_id = int(self.values[0])
@ -951,11 +950,11 @@ class Security(commands.Cog):
def __init__(self, bot):
self.bot = bot
@app_commands.command(
@commands.slash_command(
name="security",
description="🛡️ Ouvrir le panneau de sécurité Kuby"
)
async def security(self, interaction: discord.Interaction):
async def security(self, interaction: disnake.ApplicationCommandInteraction):
"""
Commande principale pour ouvrir l'interface de sécurité.
Accessible uniquement aux administrateurs et propriétaires whitelistés.
@ -990,12 +989,12 @@ class Security(commands.Cog):
)
@security.error
async def security_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError):
async def security_error(self, interaction: disnake.Interaction, error: Exception):
"""Gestion des erreurs de la commande security"""
# Utilisation de followup si l'interaction est déjà différée ou répondue
send_method = interaction.followup.send if interaction.response.is_done() else interaction.response.send_message
if isinstance(error, app_commands.MissingPermissions):
if isinstance(error, commands.MissingPermissions):
await send_method(
"❌ Permissions insuffisantes.",
ephemeral=True
@ -1007,7 +1006,7 @@ class Security(commands.Cog):
)
async def setup(bot):
def setup(bot):
"""Setup du cog de sécurité"""
await bot.add_cog(Security(bot))
bot.add_cog(Security(bot))

View file

@ -1,17 +1,14 @@
import discord
from discord import app_commands
from discord.ext import commands
import disnake
from disnake.ext import commands
class SentBotMessages(commands.Cog):
def __init__(self, bot):
self.bot = bot
@app_commands.command(name="sentbotmessages", description="Le bot envoie un message dans un salon choisi.")
@app_commands.describe(
message="Le message à envoyer",
salon="Le salon où envoyer le message"
)
async def sentbotmessages(self, interaction: discord.Interaction, message: str, salon: discord.TextChannel):
@commands.slash_command(name="sentbotmessages", description="Le bot envoie un message dans un salon choisi.")
async def sentbotmessages(self, interaction: disnake.ApplicationCommandInteraction,
message: str = commands.Param(description="Le message à envoyer"),
salon: disnake.TextChannel = commands.Param(description="Le salon où envoyer le message")):
# Vérifie si l'utilisateur est admin
if not interaction.user.guild_permissions.administrator:
await interaction.response.send_message("⛔ Tu n'as pas la permission d'utiliser cette commande.", ephemeral=True)
@ -23,6 +20,6 @@ class SentBotMessages(commands.Cog):
except Exception as e:
await interaction.response.send_message(f"❌ Une erreur est survenue : {e}", ephemeral=True)
async def setup(bot):
await bot.add_cog(SentBotMessages(bot))
def setup(bot):
bot.add_cog(SentBotMessages(bot))

View file

@ -1,6 +1,5 @@
import discord
from discord import app_commands
from discord.ext import commands
import disnake
from disnake.ext import commands
from datetime import datetime
from collections import deque
@ -34,9 +33,9 @@ class SnipeCommands(commands.Cog):
self.deleted_messages[channel_id].appendleft(message_data)
@app_commands.command(name="snipe", description="Voir les derniers messages supprimés d'un salon")
@app_commands.describe(nombre="Nombre de messages à afficher (1-10, par défaut: 1)")
async def snipe(self, interaction: discord.Interaction, nombre: int = 1):
@commands.slash_command(name="snipe", description="Voir les derniers messages supprimés d'un salon")
async def snipe(self, interaction: disnake.ApplicationCommandInteraction,
nombre: int = commands.Param(1, description="Nombre de messages à afficher (1-10, par défaut: 1)")):
"""Retrieve deleted messages from the current channel"""
channel_id = interaction.channel.id
@ -60,9 +59,9 @@ class SnipeCommands(commands.Cog):
messages = list(self.deleted_messages[channel_id])[:nombre]
# Create embed for displaying deleted messages
embed = discord.Embed(
embed = disnake.Embed(
title=f"🗑️ Messages supprimés ({len(messages)} sur {len(self.deleted_messages[channel_id])} disponibles)",
color=discord.Color.red()
color=disnake.Color.red()
)
for i, msg_data in enumerate(messages, 1):
@ -93,8 +92,8 @@ class SnipeCommands(commands.Cog):
await interaction.response.send_message(embed=embed)
@app_commands.command(name="snipeclear", description="Effacer l'historique des messages supprimés pour ce salon")
async def snipeclear(self, interaction: discord.Interaction):
@commands.slash_command(name="snipeclear", description="Effacer l'historique des messages supprimés pour ce salon")
async def snipeclear(self, interaction: disnake.ApplicationCommandInteraction):
"""Clear the deleted messages history for the current channel"""
channel_id = interaction.channel.id
@ -110,5 +109,5 @@ class SnipeCommands(commands.Cog):
ephemeral=True
)
async def setup(bot):
await bot.add_cog(SnipeCommands(bot))
def setup(bot):
bot.add_cog(SnipeCommands(bot))

View file

@ -1,6 +1,5 @@
import discord
from discord.ext import commands
from discord import app_commands
import disnake
from disnake.ext import commands
from datetime import datetime
from commandes.ticket.data.storage import get_storage
@ -11,8 +10,8 @@ class StaffLeaderboard(commands.Cog):
def __init__(self, bot):
self.bot = bot
@app_commands.command(name="staff-leaderboard", description="Affiche le classement du staff")
async def staff_leaderboard(self, interaction: discord.Interaction):
@commands.slash_command(name="staff-leaderboard", description="Affiche le classement du staff")
async def staff_leaderboard(self, interaction: disnake.ApplicationCommandInteraction):
"""Commande pour afficher le classement du staff"""
await interaction.response.defer(ephemeral=False)
@ -31,10 +30,10 @@ class StaffLeaderboard(commands.Cog):
staff_list.sort(key=lambda x: x['average_rating'], reverse=True)
# Create embed
embed = discord.Embed(
embed = disnake.Embed(
title="🏆 Classement du Staff",
description="Classement des meilleurs membres du staff",
color=discord.Color.gold(),
color=disnake.Color.gold(),
timestamp=datetime.now()
)
@ -69,5 +68,5 @@ class StaffLeaderboard(commands.Cog):
await interaction.followup.send("Une erreur est survenue lors de la génération du classement.", ephemeral=True)
async def setup(bot):
await bot.add_cog(StaffLeaderboard(bot))
def setup(bot):
bot.add_cog(StaffLeaderboard(bot))

View file

@ -1,6 +1,5 @@
import discord
from discord.ext import commands
from discord import app_commands
import disnake
from disnake.ext import commands
import matplotlib.pyplot as plt
import plotly.graph_objects as go
import plotly.express as px
@ -19,9 +18,9 @@ class StaffRatings(commands.Cog):
def __init__(self, bot):
self.bot = bot
@app_commands.command(name="staff-ratings", description="Affiche les évaluations du staff")
@app_commands.describe(team="Filtrer par équipe (optionnel)")
async def staff_ratings(self, interaction: discord.Interaction, team: str = None):
@commands.slash_command(name="staff-ratings", description="Affiche les évaluations du staff")
async def staff_ratings(self, interaction: disnake.ApplicationCommandInteraction,
team: str = commands.Param(None, description="Filtrer par équipe (optionnel)")):
"""Commande pour afficher les évaluations du staff"""
await interaction.response.defer(ephemeral=False)
@ -46,10 +45,10 @@ class StaffRatings(commands.Cog):
chart_buffer = self.create_staff_rating_chart_matplotlib(staff_list)
# Create embed
embed = discord.Embed(
embed = disnake.Embed(
title="📊 Évaluations du Staff",
description="Performance globale du staff",
color=discord.Color.blue(),
color=disnake.Color.blue(),
timestamp=datetime.now()
)
@ -66,7 +65,7 @@ class StaffRatings(commands.Cog):
)
# Send chart
file = discord.File(chart_buffer, filename="staff_ratings.png")
file = disnake.File(chart_buffer, filename="staff_ratings.png")
await interaction.followup.send(embed=embed, file=file)
except Exception as e:
@ -134,5 +133,5 @@ class StaffRatings(commands.Cog):
return buf
async def setup(bot):
await bot.add_cog(StaffRatings(bot))
def setup(bot):
bot.add_cog(StaffRatings(bot))

File diff suppressed because it is too large Load diff

View file

@ -16,9 +16,8 @@ from PIL import Image, ImageDraw, ImageFont
# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
import discord
from discord.ext import commands
from discord import app_commands
import disnake
from disnake.ext import commands
from .data.storage import get_storage
from .data.models import TicketData, TicketStatus, GuildTicketConfig
@ -31,7 +30,7 @@ class TicketAnalytics(commands.Cog):
self.bot = bot
self.storage = get_storage()
def _is_staff(self, interaction: discord.Interaction) -> bool:
def _is_staff(self, interaction: disnake.Interaction) -> bool:
"""Check if user is staff"""
guild = interaction.guild
user = interaction.user
@ -363,9 +362,8 @@ class TicketAnalytics(commands.Cog):
buf.seek(0)
return buf
@app_commands.command(name="ticket_analytics", description="Affiche les statistiques détaillées des tickets (Staff uniquement)")
@app_commands.describe(period="Période d'analyse (7d, 30d, 90d, all)")
async def ticket_analytics(self, interaction: discord.Interaction, period: str = "30d"):
@commands.slash_command(name="ticket_analytics", description="Affiche les statistiques détaillées des tickets (Staff uniquement)")
async def ticket_analytics(self, interaction: disnake.Interaction, period: str = commands.Param(default="30d", choices=["7d", "30d", "90d", "all"], description="Période d'analyse")):
"""Display comprehensive ticket analytics for staff"""
await interaction.response.defer(ephemeral=True)
@ -405,10 +403,10 @@ class TicketAnalytics(commands.Cog):
t.created_at >= cutoff_date for t in filtered_tickets if hasattr(t, 'response_time') and t.response_time == rt)]
# Create main embed
embed = discord.Embed(
embed = disnake.Embed(
title="📊 Analytics des Tickets",
description=f"Statistiques pour la période: **{period}**",
color=discord.Color.blue(),
color=disnake.Color.blue(),
timestamp=datetime.now()
)
@ -444,7 +442,7 @@ class TicketAnalytics(commands.Cog):
rating_chart = self._create_rating_chart(stats['ratings'])
await interaction.followup.send(
"📊 **Distribution des Notes Clients**",
file=discord.File(rating_chart, 'rating_distribution.png'),
file=disnake.File(rating_chart, 'rating_distribution.png'),
ephemeral=True
)
@ -453,7 +451,7 @@ class TicketAnalytics(commands.Cog):
response_chart = self._create_response_time_chart(stats['response_times'])
await interaction.followup.send(
"⏱️ **Temps de Réponse**",
file=discord.File(response_chart, 'response_times.png'),
file=disnake.File(response_chart, 'response_times.png'),
ephemeral=True
)
@ -462,7 +460,7 @@ class TicketAnalytics(commands.Cog):
trend_chart = self._create_trend_chart(stats['tickets_by_day'])
await interaction.followup.send(
"📈 **Évolution des Tickets**",
file=discord.File(trend_chart, 'ticket_trends.png'),
file=disnake.File(trend_chart, 'ticket_trends.png'),
ephemeral=True
)
@ -471,10 +469,10 @@ class TicketAnalytics(commands.Cog):
# Send feedback summary if available
if stats['feedbacks']:
feedback_embed = discord.Embed(
feedback_embed = disnake.Embed(
title="💬 Commentaires Clients",
description=f"Derniers commentaires ({min(5, len(stats['feedbacks']))} affichés)",
color=discord.Color.green()
color=disnake.Color.green()
)
# Sort by rating (lowest first to show areas for improvement)
@ -492,6 +490,6 @@ class TicketAnalytics(commands.Cog):
await interaction.followup.send(embed=feedback_embed, ephemeral=True)
async def setup(bot):
def setup(bot):
"""Setup function for the analytics cog"""
await bot.add_cog(TicketAnalytics(bot))
bot.add_cog(TicketAnalytics(bot))

View file

@ -1,7 +1,7 @@
import discord
import disnake
from .data.models import TicketData
class FeedbackView(discord.ui.View):
class FeedbackView(disnake.ui.View):
def __init__(self, bot, storage, ticket: TicketData, staff_rating_only: bool = False):
super().__init__(timeout=86400) # 24h timeout
self.bot = bot
@ -14,12 +14,12 @@ class FeedbackView(discord.ui.View):
# Add Select Menu for Rating
self.add_item(RatingSelect())
@discord.ui.button(label="📝 Laisser un commentaire", style=discord.ButtonStyle.secondary, custom_id="feedback_comment", row=1)
async def comment_button(self, interaction: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="📝 Laisser un commentaire", style=disnake.ButtonStyle.secondary, custom_id="feedback_comment", row=1)
async def comment_button(self, button: disnake.ui.Button, interaction: disnake.Interaction):
await interaction.response.send_modal(FeedbackModal(self))
@discord.ui.button(label="✅ Envoyer", style=discord.ButtonStyle.green, custom_id="feedback_submit", row=1)
async def submit_button(self, interaction: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="✅ Envoyer", style=disnake.ButtonStyle.green, custom_id="feedback_submit", row=1)
async def submit_button(self, button: disnake.ui.Button, interaction: disnake.Interaction):
if self.rating == 0:
await interaction.response.send_message("❌ Veuillez sélectionner une note avant d'envoyer.", ephemeral=True)
return
@ -66,11 +66,11 @@ class FeedbackView(discord.ui.View):
if staff_user:
# Create feedback embed
embed = discord.Embed(
embed = disnake.Embed(
title="📝 Nouveau Commentaire Reçu",
description=f"Un utilisateur a laissé un avis sur le ticket #{self.ticket.channel_id}",
color=discord.Color.gold() if self.rating < 3 else discord.Color.green(),
timestamp=discord.utils.utcnow()
color=disnake.Color.gold() if self.rating < 3 else disnake.Color.green(),
timestamp=disnake.utils.utcnow()
)
embed.add_field(
@ -99,7 +99,7 @@ class FeedbackView(discord.ui.View):
try:
await staff_user.send(embed=embed)
print(f"DEBUG: DM sent successfully to {staff_user}", flush=True)
except discord.Forbidden:
except disnake.Forbidden:
print(f"DEBUG: DM failed - Forbidden (DMs closed)", flush=True)
except Exception as e:
print(f"DEBUG: Error sending feedback DM: {e}", flush=True)
@ -111,34 +111,34 @@ class FeedbackView(discord.ui.View):
except Exception as e:
print(f"Error in feedback notification: {e}", flush=True)
class RatingSelect(discord.ui.Select):
class RatingSelect(disnake.ui.Select):
def __init__(self):
options = [
discord.SelectOption(label="⭐⭐⭐⭐⭐ (Excellent)", value="5", description="Service parfait !"),
discord.SelectOption(label="⭐⭐⭐⭐ (Très bien)", value="4", description="Très bon service"),
discord.SelectOption(label="⭐⭐⭐ (Bien)", value="3", description="Correct"),
discord.SelectOption(label="⭐⭐ (Moyen)", value="2", description="Peut mieux faire"),
discord.SelectOption(label="⭐ (Mauvais)", value="1", description="Insatisfaisant")
disnake.SelectOption(label="⭐⭐⭐⭐⭐ (Excellent)", value="5", description="Service parfait !"),
disnake.SelectOption(label="⭐⭐⭐⭐ (Très bien)", value="4", description="Très bon service"),
disnake.SelectOption(label="⭐⭐⭐ (Bien)", value="3", description="Correct"),
disnake.SelectOption(label="⭐⭐ (Moyen)", value="2", description="Peut mieux faire"),
disnake.SelectOption(label="⭐ (Mauvais)", value="1", description="Insatisfaisant")
]
super().__init__(placeholder="Notez le service...", min_values=1, max_values=1, options=options, row=0)
async def callback(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.Interaction):
self.view.rating = int(self.values[0])
await interaction.response.defer()
class FeedbackModal(discord.ui.Modal):
class FeedbackModal(disnake.ui.Modal):
def __init__(self, view):
super().__init__(title="Votre avis")
super().__init__(components=[], title="Votre avis")
self.view = view
self.comment = discord.ui.TextInput(
self.comment = disnake.ui.TextInput(
label="Commentaire",
style=discord.TextStyle.paragraph,
style=disnake.TextInputStyle.paragraph,
placeholder="Dites-nous ce que vous avez pensé du support...",
required=True,
max_length=1000
)
self.add_item(self.comment)
async def on_submit(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.Interaction):
self.view.comment = self.comment.value
await interaction.response.send_message("Commentaire enregistré ! N'oubliez pas de valider avec 'Envoyer'.", ephemeral=True)

View file

@ -8,7 +8,7 @@ from datetime import datetime, timedelta
from collections import defaultdict
from typing import List, Dict, Any, Optional
import discord
import disnake
import matplotlib.pyplot as plt
import plotly.graph_objects as go
import plotly.express as px
@ -254,7 +254,7 @@ class StaffAnalytics:
return buf
class StaffStatsView(discord.ui.View):
class StaffStatsView(disnake.ui.View):
"""View for selecting individual staff member stats"""
def __init__(self, bot, staff_list: List[Dict[str, Any]], analytics: StaffAnalytics):
@ -272,7 +272,7 @@ class StaffStatsView(discord.ui.View):
description = f"Note: {staff['average_rating']:.1f}" if staff['total_ratings'] > 0 else "Aucune évaluation"
options.append(
discord.SelectOption(
disnake.SelectOption(
label=label,
value=str(staff['user_id']),
description=description[:50]
@ -280,14 +280,14 @@ class StaffStatsView(discord.ui.View):
)
if options:
select = discord.ui.Select(
select = disnake.ui.Select(
placeholder="Sélectionnez un membre du staff...",
options=options
)
select.callback = self._staff_selected
self.add_item(select)
async def _staff_selected(self, interaction: discord.Interaction):
async def _staff_selected(self, interaction: disnake.Interaction):
"""Handle staff selection"""
staff_id = int(interaction.data["values"][0])
staff_data = next((s for s in self.staff_list if s['user_id'] == staff_id), None)
@ -301,10 +301,10 @@ class StaffStatsView(discord.ui.View):
# Create individual chart
chart = self.analytics.create_individual_staff_chart(staff_data)
embed = discord.Embed(
embed = disnake.Embed(
title=f"📊 Statistiques de {staff_data['name']}",
description="Performances détaillées du membre du staff",
color=discord.Color.blue(),
color=disnake.Color.blue(),
timestamp=datetime.now()
)
@ -331,4 +331,4 @@ class StaffStatsView(discord.ui.View):
inline=True
)
await interaction.followup.send(embed=embed, file=discord.File(chart, 'staff_stats.png'), ephemeral=True)
await interaction.followup.send(embed=embed, file=disnake.File(chart, 'staff_stats.png'), ephemeral=True)

View file

@ -5,7 +5,7 @@ Beautiful embed and message formatters for the ticket system.
"""
from datetime import datetime
from typing import Optional, List, Dict, Any
from discord import Embed, Color, User, Member, Role
from disnake import Embed, Color, User, Member, Role
from ..data.models import (
TicketData,

View file

@ -3,13 +3,13 @@ Ticket Permissions Utility
==========================
Centralized permission logic for the ticket system.
"""
import discord
import disnake
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from ..data.models import GuildTicketConfig, TicketCategory
def is_staff(interaction: discord.Interaction, config: 'GuildTicketConfig', category: Optional['TicketCategory'] = None) -> bool:
def is_staff(interaction: disnake.Interaction, config: 'GuildTicketConfig', category: Optional['TicketCategory'] = None) -> bool:
"""
Check if the user has staff permissions.
@ -51,7 +51,7 @@ def is_staff(interaction: discord.Interaction, config: 'GuildTicketConfig', cate
return False
def can_access_config(interaction: discord.Interaction, config: 'GuildTicketConfig') -> bool:
def can_access_config(interaction: disnake.Interaction, config: 'GuildTicketConfig') -> bool:
"""
Check if the user can access the configuration.
@ -87,7 +87,7 @@ def can_access_config(interaction: discord.Interaction, config: 'GuildTicketConf
return False
def is_whitelisted(interaction: discord.Interaction, config: 'GuildTicketConfig', bot) -> bool:
def is_whitelisted(interaction: disnake.Interaction, config: 'GuildTicketConfig', bot) -> bool:
"""
Check if user is whitelisted or has staff/config permissions.
"""

View file

@ -7,7 +7,7 @@ import json
import os
from datetime import datetime
from typing import Optional, List, Dict, Any
from discord import User, Member
from disnake import User, Member
from ..data.models import TicketData, TicketMessage

View file

@ -1,7 +1,7 @@
import discord
import disnake
import re
from discord.ext import commands
from discord import app_commands, ui
from disnake.ext import commands
from disnake import ui
import json
import os
import asyncio
@ -63,12 +63,12 @@ def save_settings(guild_id: int, settings: dict) -> None:
with open(TICKET_WHITELIST_SETTINGS_FILE, "w", encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False)
async def get_ticket_user_id(channel: discord.TextChannel) -> Optional[int]:
async def get_ticket_user_id(channel: disnake.TextChannel) -> Optional[int]:
"""Récupère l'ID du propriétaire du ticket de manière robuste (gère le lag de cache)"""
import re
topic = getattr(channel, "topic", None)
# Tentative d'utilisation du cache d.py (plus rapide)
# Tentative d'utilisation du cache (plus rapide)
if topic:
match = re.search(r"(\d{17,20})", topic)
if match:
@ -88,12 +88,12 @@ async def get_ticket_user_id(channel: discord.TextChannel) -> Optional[int]:
return None
class TicketWhitelistConfigView(ui.View):
class TicketWhitelistConfigView(disnake.ui.View):
def __init__(self, guild_id: int):
super().__init__(timeout=60)
self.guild_id = guild_id
async def prompt_for_id(self, interaction: discord.Interaction, prompt_text: str, is_category: bool = False):
async def prompt_for_id(self, interaction: disnake.Interaction, prompt_text: str, is_category: bool = False):
await interaction.response.send_message(prompt_text, ephemeral=True)
def check(m):
@ -104,7 +104,7 @@ class TicketWhitelistConfigView(ui.View):
target = None
if is_category:
if msg.content.isdigit():
target = discord.utils.get(interaction.guild.categories, id=int(msg.content))
target = disnake.utils.get(interaction.guild.categories, id=int(msg.content))
if not target:
await interaction.followup.send("❌ Catégorie introuvable avec cet ID.", ephemeral=True)
return None
@ -129,8 +129,8 @@ class TicketWhitelistConfigView(ui.View):
await interaction.followup.send("❌ Temps écoulé.", ephemeral=True)
return None
@ui.button(label="Catégorie Tickets", style=discord.ButtonStyle.primary, row=0, emoji="📁")
async def set_category(self, interaction: discord.Interaction, button: ui.Button):
@ui.button(label="Catégorie Tickets", style=disnake.ButtonStyle.primary, row=0, emoji="📁")
async def set_category(self, interaction: disnake.Interaction, button: ui.Button):
category = await self.prompt_for_id(interaction, "Envoie l'ID de la **catégorie** où créer les tickets :", is_category=True)
if category:
settings = load_settings(self.guild_id)
@ -138,8 +138,8 @@ class TicketWhitelistConfigView(ui.View):
save_settings(self.guild_id, settings)
await interaction.followup.send(f"✅ Catégorie configurée sur **{category.name}**", ephemeral=True)
@ui.button(label="Rôle Staff", style=discord.ButtonStyle.primary, row=0, emoji="🛡️")
async def set_staff_role(self, interaction: discord.Interaction, button: ui.Button):
@ui.button(label="Rôle Staff", style=disnake.ButtonStyle.primary, row=0, emoji="🛡️")
async def set_staff_role(self, interaction: disnake.Interaction, button: ui.Button):
role = await self.prompt_for_id(interaction, "Mentionne le **rôle Staff** ou envoie son ID :")
if role:
settings = load_settings(self.guild_id)
@ -147,8 +147,8 @@ class TicketWhitelistConfigView(ui.View):
save_settings(self.guild_id, settings)
await interaction.followup.send(f"✅ Rôle Staff configuré sur **{role.name}**", ephemeral=True)
@ui.button(label="Rôle Équipe Staff", style=discord.ButtonStyle.primary, row=0, emoji="👥")
async def set_team_staff_role(self, interaction: discord.Interaction, button: ui.Button):
@ui.button(label="Rôle Équipe Staff", style=disnake.ButtonStyle.primary, row=0, emoji="👥")
async def set_team_staff_role(self, interaction: disnake.Interaction, button: ui.Button):
role = await self.prompt_for_id(interaction, "Mentionne le **rôle Équipe Staff** (à ping pour les nouveaux tickets) ou envoie son ID :")
if role:
settings = load_settings(self.guild_id)
@ -156,8 +156,8 @@ class TicketWhitelistConfigView(ui.View):
save_settings(self.guild_id, settings)
await interaction.followup.send(f"✅ Rôle Équipe Staff configuré sur **{role.name}**", ephemeral=True)
@ui.button(label="Rôle Attente Oral", style=discord.ButtonStyle.secondary, row=1, emoji="🎤")
async def set_attente_oral_role(self, interaction: discord.Interaction, button: ui.Button):
@ui.button(label="Rôle Attente Oral", style=disnake.ButtonStyle.secondary, row=1, emoji="🎤")
async def set_attente_oral_role(self, interaction: disnake.Interaction, button: ui.Button):
role = await self.prompt_for_id(interaction, "Mentionne le rôle **Attente Oral** ou envoie son ID :")
if role:
settings = load_settings(self.guild_id)
@ -165,8 +165,8 @@ class TicketWhitelistConfigView(ui.View):
save_settings(self.guild_id, settings)
await interaction.followup.send(f"✅ Rôle Attente Oral configuré sur **{role.name}**", ephemeral=True)
@ui.button(label="Rôle Candidature à faire", style=discord.ButtonStyle.secondary, row=1, emoji="📝")
async def set_candidature_a_faire_role(self, interaction: discord.Interaction, button: ui.Button):
@ui.button(label="Rôle Candidature à faire", style=disnake.ButtonStyle.secondary, row=1, emoji="📝")
async def set_candidature_a_faire_role(self, interaction: disnake.Interaction, button: ui.Button):
role = await self.prompt_for_id(interaction, "Mentionne le rôle **Candidature à faire** ou envoie son ID :")
if role:
settings = load_settings(self.guild_id)
@ -174,8 +174,8 @@ class TicketWhitelistConfigView(ui.View):
save_settings(self.guild_id, settings)
await interaction.followup.send(f"✅ Rôle Candidature à faire configuré sur **{role.name}**", ephemeral=True)
@ui.button(label="Rôle Citoyen", style=discord.ButtonStyle.success, row=1, emoji="🏙️")
async def set_citoyen_role(self, interaction: discord.Interaction, button: ui.Button):
@ui.button(label="Rôle Citoyen", style=disnake.ButtonStyle.success, row=1, emoji="🏙️")
async def set_citoyen_role(self, interaction: disnake.Interaction, button: ui.Button):
role = await self.prompt_for_id(interaction, "Mentionne le rôle **Citoyen** ou envoie son ID :")
if role:
settings = load_settings(self.guild_id)
@ -184,7 +184,7 @@ class TicketWhitelistConfigView(ui.View):
await interaction.followup.send(f"✅ Rôle Citoyen configuré sur **{role.name}**", ephemeral=True)
class ContinueFormView(discord.ui.View):
class ContinueFormView(disnake.ui.View):
"""Vue avec un bouton pour continuer à l'étape suivante"""
def __init__(self, current_step, step1_data=None, step2_part1_data=None, step2_part2_data=None):
@ -194,8 +194,8 @@ class ContinueFormView(discord.ui.View):
self.step2_part1_data = step2_part1_data
self.step2_part2_data = step2_part2_data
@discord.ui.button(label="📋 Continuer le formulaire", style=discord.ButtonStyle.green, custom_id="continue_form")
async def continue_form(self, interaction: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="📋 Continuer le formulaire", style=disnake.ButtonStyle.green, custom_id="continue_form")
async def continue_form(self, button: disnake.ui.Button, interaction: disnake.Interaction):
"""Passe à l'étape suivante du formulaire"""
if self.current_step == 1:
await interaction.response.send_modal(TicketStep2Part1(self.step1_data))
@ -207,79 +207,99 @@ class ContinueFormView(discord.ui.View):
# Vérifier si le message existe toujours avant de le modifier
try:
await interaction.message.edit(view=self)
except discord.errors.NotFound:
except disnake.errors.NotFound:
# Si le message n'existe plus, nous ne faisons rien
pass
class TicketStep1(discord.ui.Modal, title="📋 Informations Personnelles"):
class TicketStep1(disnake.ui.Modal):
"""Étape 1 du formulaire - Infos personnelles"""
pseudo = discord.ui.TextInput(label="Pseudo Epic (Fortnite)", required=True, max_length=16)
age = discord.ui.TextInput(label="Âge", required=True, max_length=2)
experience = discord.ui.TextInput(label="Expérience RP ?", style=discord.TextStyle.paragraph, required=True, max_length=50)
decouverte = discord.ui.TextInput(label="Comment as-tu découvert le serveur ?", required=True, max_length=20)
micro = discord.ui.TextInput(label="As-tu un micro de qualité ?", required=True, max_length=3)
def __init__(self):
super().__init__(
title="📋 Informations Personnelles",
custom_id="ticket_step1",
components=[
disnake.ui.TextInput(label="Pseudo Epic (Fortnite)", custom_id="pseudo", required=True, max_length=16),
disnake.ui.TextInput(label="Âge", custom_id="age", required=True, max_length=2),
disnake.ui.TextInput(label="Expérience RP ?", custom_id="experience", style=disnake.TextInputStyle.paragraph, required=True, max_length=50),
disnake.ui.TextInput(label="Comment as-tu découvert le serveur ?", custom_id="decouverte", required=True, max_length=20),
disnake.ui.TextInput(label="As-tu un micro de qualité ?", custom_id="micro", required=True, max_length=3),
]
)
async def on_submit(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.ModalInteraction):
"""Envoie un bouton pour passer à l'étape 2"""
view = ContinueFormView(current_step=1, step1_data=self)
view = ContinueFormView(current_step=1, step1_data=interaction.text_values)
await interaction.response.send_message("✅ Première étape terminée ! Cliquez ci-dessous pour continuer.", view=view, ephemeral=True)
class TicketStep2Part1(discord.ui.Modal, title="👤 Infos sur ton personnage (Partie 1)"):
class TicketStep2Part1(disnake.ui.Modal):
"""Étape 2 du formulaire - Infos RP (Partie 1)"""
def __init__(self, step1_data):
super().__init__()
self.step1_data = step1_data
super().__init__(
title="👤 Infos sur ton personnage (Partie 1)",
custom_id="ticket_step2_part1",
components=[
disnake.ui.TextInput(label="Prénom", custom_id="prenom", required=True),
disnake.ui.TextInput(label="Nom", custom_id="nom", required=True),
disnake.ui.TextInput(label="Âge du personnage", custom_id="age_perso", required=True),
disnake.ui.TextInput(label="Genre", custom_id="genre", required=True, max_length=5),
disnake.ui.TextInput(label="Date et lieu de naissance", custom_id="naissance", required=True),
]
)
prenom = discord.ui.TextInput(label="Prénom", required=True)
nom = discord.ui.TextInput(label="Nom", required=True)
age_perso = discord.ui.TextInput(label="Âge du personnage", required=True)
genre = discord.ui.TextInput(label="Genre", required=True, max_length=5)
naissance = discord.ui.TextInput(label="Date et lieu de naissance", required=True)
async def on_submit(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.ModalInteraction):
"""Envoie un bouton pour passer à l'étape suivante"""
view = ContinueFormView(current_step=2, step1_data=self.step1_data, step2_part1_data=self)
view = ContinueFormView(current_step=2, step1_data=self.step1_data, step2_part1_data=interaction.text_values)
await interaction.response.send_message("✅ Deuxième partie terminée ! Cliquez ci-dessous pour continuer.", view=view, ephemeral=True)
class TicketStep2Part2(discord.ui.Modal, title="👤 Infos sur ton personnage (Partie 2)"):
class TicketStep2Part2(disnake.ui.Modal):
"""Étape 2 du formulaire - Infos RP (Partie 2)"""
def __init__(self, step1_data, step2_part1_data):
super().__init__()
self.step1_data = step1_data
self.step2_part1_data = step2_part1_data
super().__init__(
title="👤 Infos sur ton personnage (Partie 2)",
custom_id="ticket_step2_part2",
components=[
disnake.ui.TextInput(label="Traits de caractère", custom_id="traits", required=True),
disnake.ui.TextInput(label="Nationalité", custom_id="nationalite", required=True),
disnake.ui.TextInput(label="Skin RP", custom_id="skin_rp", required=True),
disnake.ui.TextInput(label="Métier souhaité", custom_id="metier_souhaite", required=True),
]
)
traits = discord.ui.TextInput(label="Traits de caractère", required=True)
nationalite = discord.ui.TextInput(label="Nationalité", required=True)
skin_rp = discord.ui.TextInput(label="Skin RP", required=True)
metier_souhaite = discord.ui.TextInput(label="Métier souhaité", required=True)
async def on_submit(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.ModalInteraction):
"""Envoie un bouton pour passer à l'étape 3"""
view = ContinueFormView(current_step=3, step1_data=self.step1_data, step2_part1_data=self.step2_part1_data, step2_part2_data=self)
view = ContinueFormView(current_step=3, step1_data=self.step1_data, step2_part1_data=self.step2_part1_data, step2_part2_data=interaction.text_values)
await interaction.response.send_message("✅ Deuxième étape terminée ! Cliquez ci-dessous pour continuer.", view=view, ephemeral=True)
class TicketStep3(discord.ui.Modal, title="📋 Développement du Personnage"):
class TicketStep3(disnake.ui.Modal):
"""Étape 3 du formulaire - Histoire du personnage"""
def __init__(self, step1_data, step2_part1_data, step2_part2_data):
super().__init__()
self.step1_data = step1_data
self.step2_part1_data = step2_part1_data
self.step2_part2_data = step2_part2_data
super().__init__(
title="📋 Développement du Personnage",
custom_id="ticket_step3",
components=[
disnake.ui.TextInput(
label="Histoire du personnage",
custom_id="histoire",
style=disnake.TextInputStyle.paragraph,
required=True,
min_length=425,
max_length=500
),
disnake.ui.TextInput(label="Objectifs RP (court et long terme)", custom_id="objectifs", style=disnake.TextInputStyle.paragraph, required=True, max_length=100),
]
)
histoire = discord.ui.TextInput(
label="Histoire du personnage",
style=discord.TextStyle.paragraph,
required=True,
min_length=425,
max_length=500 # Limite de caractères maximum
)
objectifs = discord.ui.TextInput(label="Objectifs RP (court et long terme)", style=discord.TextStyle.paragraph, required=True, max_length=100)
async def on_submit(self, interaction: discord.Interaction):
async def callback(self, interaction: disnake.ModalInteraction):
"""Création du ticket après validation"""
guild = interaction.guild
user = interaction.user
@ -292,12 +312,12 @@ class TicketStep3(discord.ui.Modal, title="📋 Développement du Personnage"):
if not category_id or not staff_role_id or not team_staff_role_id:
return await interaction.response.send_message("❌ Erreur : Le système de tickets n'est pas entièrement configuré sur ce serveur (Catégorie, Rôle Staff ou Rôle Équipe Staff manquant).", ephemeral=True)
category = discord.utils.get(guild.categories, id=int(category_id))
category = disnake.utils.get(guild.categories, id=int(category_id))
if category is None:
return await interaction.response.send_message("❌ Erreur : La catégorie des tickets configurée n'existe plus !", ephemeral=True)
staff_role = discord.utils.get(guild.roles, id=int(staff_role_id))
team_staff_role = discord.utils.get(guild.roles, id=int(team_staff_role_id))
staff_role = disnake.utils.get(guild.roles, id=int(staff_role_id))
team_staff_role = disnake.utils.get(guild.roles, id=int(team_staff_role_id))
if staff_role is None or team_staff_role is None:
return await interaction.response.send_message("❌ Erreur : L'un des rôles staff configurés n'existe plus !", ephemeral=True)
@ -309,10 +329,10 @@ class TicketStep3(discord.ui.Modal, title="📋 Développement du Personnage"):
# Définir les permissions du salon du ticket
overwrites = {
guild.default_role: discord.PermissionOverwrite(read_messages=False),
user: discord.PermissionOverwrite(read_messages=True, send_messages=True),
staff_role: discord.PermissionOverwrite(read_messages=True, send_messages=True),
team_staff_role: discord.PermissionOverwrite(read_messages=True, send_messages=False)
guild.default_role: disnake.PermissionOverwrite(read_messages=False),
user: disnake.PermissionOverwrite(read_messages=True, send_messages=True),
staff_role: disnake.PermissionOverwrite(read_messages=True, send_messages=True),
team_staff_role: disnake.PermissionOverwrite(read_messages=True, send_messages=False)
}
# Créer un salon de ticket avec le nom [pseudo]-écrit
@ -323,28 +343,35 @@ class TicketStep3(discord.ui.Modal, title="📋 Développement du Personnage"):
)
# Création de l'embed avec les infos du formulaire
embed = discord.Embed(title="📩 Ticket Whitelist", color=0x00ff00)
embed = disnake.Embed(title="📩 Ticket Whitelist", color=0x00ff00)
# Helper to get value from data
s1 = self.step1_data
s2p1 = self.step2_part1_data
s2p2 = self.step2_part2_data
s3 = interaction.text_values
embed.add_field(name="📋 - Informations Personnelles", value=(
f"> - Pseudo Epic : {self.step1_data.pseudo.value}\n"
f"> - Âge : {self.step1_data.age.value}\n"
f"> - Expérience RP : {self.step1_data.experience.value}\n"
f"> - Découverte du serveur : {self.step1_data.decouverte.value}\n"
f"> - Micro de qualité : {self.step1_data.micro.value}"
f"> - Pseudo Epic : {s1['pseudo']}\n"
f"> - Âge : {s1['age']}\n"
f"> - Expérience RP : {s1['experience']}\n"
f"> - Découverte du serveur : {s1['decouverte']}\n"
f"> - Micro de qualité : {s1['micro']}"
), inline=False)
embed.add_field(name="👤 - Informations RP", value=(
f"> - Prénom : {self.step2_part1_data.prenom.value}\n"
f"> - Nom : {self.step2_part1_data.nom.value}\n"
f"> - Âge du personnage : {self.step2_part1_data.age_perso.value}\n"
f"> - Genre : {self.step2_part1_data.genre.value}\n"
f"> - Naissance : {self.step2_part1_data.naissance.value}\n"
f"> - Traits de caractère : {self.step2_part2_data.traits.value}\n"
f"> - Nationalité : {self.step2_part2_data.nationalite.value}\n"
f"> - Skin RP : {self.step2_part2_data.skin_rp.value}\n"
f"> - Métier souhaité : {self.step2_part2_data.metier_souhaite.value}"
f"> - Prénom : {s2p1['prenom']}\n"
f"> - Nom : {s2p1['nom']}\n"
f"> - Âge du personnage : {s2p1['age_perso']}\n"
f"> - Genre : {s2p1['genre']}\n"
f"> - Naissance : {s2p1['naissance']}\n"
f"> - Traits de caractère : {s2p2['traits']}\n"
f"> - Nationalité : {s2p2['nationalite']}\n"
f"> - Skin RP : {s2p2['skin_rp']}\n"
f"> - Métier souhaité : {s2p2['metier_souhaite']}"
), inline=False)
embed.add_field(name="📋 - Développement du Personnage", value=(
f"> - Histoire : {self.histoire.value}\n"
f"> - Objectifs : {self.objectifs.value}"
f"> - Histoire : {s3['histoire']}\n"
f"> - Objectifs : {s3['objectifs']}"
), inline=False)
# Envoyer l'embed avec les boutons d'actions pour les staff
@ -355,25 +382,25 @@ class TicketStep3(discord.ui.Modal, title="📋 Développement du Personnage"):
# Mettre à jour le pseudo du joueur si le bot a les permissions nécessaires
if guild.me.guild_permissions.manage_nicknames:
new_nickname = f"{self.step2_part1_data.prenom.value} {self.step2_part1_data.nom.value} | {self.step1_data.pseudo.value}"
new_nickname = f"{s2p1['prenom']} {s2p1['nom']} | {s1['pseudo']}"
if len(new_nickname) > 32:
new_nickname = new_nickname[:32]
try: await user.edit(nick=new_nickname)
except discord.Forbidden: pass
except disnake.Forbidden: pass
# Envoyer un message de confirmation
try:
await interaction.response.send_message(f"✅ Ticket ouvert : {ticket_channel.mention}", ephemeral=True)
except discord.errors.NotFound:
except disnake.errors.NotFound:
await interaction.followup.send(f"✅ Ticket ouvert : {ticket_channel.mention}", ephemeral=True)
class TicketManagementView(discord.ui.View):
class TicketManagementView(disnake.ui.View):
"""Vue pour gérer le ticket avec les boutons Fermer, Réouvrir, Claim et Supprimer"""
def __init__(self):
super().__init__(timeout=None)
async def get_ticket_owner(self, interaction: discord.Interaction):
async def get_ticket_owner(self, interaction: disnake.Interaction):
"""Récupère le propriétaire du ticket via le helper robuste"""
owner_id = await get_ticket_user_id(interaction.channel)
if owner_id:
@ -386,8 +413,8 @@ class TicketManagementView(discord.ui.View):
return None
return None
@discord.ui.button(label="🔒 Fermer le Ticket", style=discord.ButtonStyle.red, custom_id="close_ticket")
async def close_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="🔒 Fermer le Ticket", style=disnake.ButtonStyle.red, custom_id="close_ticket")
async def close_ticket(self, button: disnake.ui.Button, interaction: disnake.Interaction):
"""Ferme le ticket en désactivant l'envoi de messages pour le membre"""
await interaction.response.defer(ephemeral=True)
@ -411,12 +438,14 @@ class TicketManagementView(discord.ui.View):
new_view = ClosedTicketView()
await interaction.channel.send(f"🔒 Ce ticket a été fermé par {interaction.user.mention}.", view=new_view)
await interaction.followup.send("✅ Ticket fermé avec succès.", ephemeral=True)
except disnake.errors.RateLimited as e:
await interaction.followup.send(f"⚠️ Limite de débit Discord : Réessayez dans {e.retry_after:.1f}s.", ephemeral=True)
except Exception as e:
kuby_logger.error(f"Erreur lors de la fermeture du ticket: {e}")
await interaction.followup.send(f"❌ Erreur lors de la fermeture : {e}", ephemeral=True)
@discord.ui.button(label="👤 Claim le Ticket", style=discord.ButtonStyle.blurple, custom_id="claim_ticket")
async def claim_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="👤 Claim le Ticket", style=disnake.ButtonStyle.blurple, custom_id="claim_ticket")
async def claim_ticket(self, button: disnake.ui.Button, interaction: disnake.Interaction):
"""Un staff prend en charge le ticket"""
await interaction.response.defer(ephemeral=False)
@ -441,13 +470,13 @@ class TicketManagementView(discord.ui.View):
kuby_logger.error(f"Erreur lors du claim du ticket: {e}")
await interaction.followup.send(f"❌ Erreur lors du claim : {e}")
class ClosedTicketView(discord.ui.View):
class ClosedTicketView(disnake.ui.View):
"""Vue pour les tickets fermés avec les boutons Réouvrir et Supprimer"""
def __init__(self):
super().__init__(timeout=None)
async def get_ticket_owner(self, interaction: discord.Interaction):
async def get_ticket_owner(self, interaction: disnake.Interaction):
"""Récupère le propriétaire du ticket via le helper robuste"""
owner_id = await get_ticket_user_id(interaction.channel)
if owner_id:
@ -460,8 +489,8 @@ class ClosedTicketView(discord.ui.View):
return None
return None
@discord.ui.button(label="♻ Réouvrir le Ticket", style=discord.ButtonStyle.green, custom_id="reopen_ticket")
async def reopen_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="♻ Réouvrir le Ticket", style=disnake.ButtonStyle.green, custom_id="reopen_ticket")
async def reopen_ticket(self, button: disnake.ui.Button, interaction: disnake.Interaction):
"""Réouvre le ticket pour le membre"""
await interaction.response.defer(ephemeral=True)
@ -478,10 +507,10 @@ class ClosedTicketView(discord.ui.View):
try:
# On demande au staff quel statut remettre
embed = discord.Embed(
embed = disnake.Embed(
title="♻️ Réouverture du Ticket",
description=f"Choisissez le statut à appliquer pour **{owner.display_name}**.",
color=discord.Color.blue()
color=disnake.Color.blue()
)
view = ReopenStatusView(owner)
await interaction.channel.send(embed=embed, view=view)
@ -490,25 +519,25 @@ class ClosedTicketView(discord.ui.View):
kuby_logger.error(f"Erreur lors de la préparation de réouverture: {e}")
await interaction.followup.send(f"❌ Erreur : {e}", ephemeral=True)
class ReopenStatusView(discord.ui.View):
class ReopenStatusView(disnake.ui.View):
"""Vue pour choisir le statut lors d'une réouverture"""
def __init__(self, owner: discord.Member):
def __init__(self, owner: disnake.Member):
super().__init__(timeout=180)
self.owner = owner
@discord.ui.button(label="Écrit (Ouvert)", style=discord.ButtonStyle.secondary)
async def set_written(self, interaction: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="Écrit (Ouvert)", style=disnake.ButtonStyle.secondary)
async def set_written(self, button: disnake.ui.Button, interaction: disnake.Interaction):
await self.apply_status(interaction, "ouvert")
@discord.ui.button(label="Oral (À faire)", style=discord.ButtonStyle.primary)
async def set_oral(self, interaction: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="Oral (À faire)", style=disnake.ButtonStyle.primary)
async def set_oral(self, button: disnake.ui.Button, interaction: disnake.Interaction):
await self.apply_status(interaction, "oral-à-faire")
@discord.ui.button(label="Accepté", style=discord.ButtonStyle.success)
async def set_accepted(self, interaction: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="Accepté", style=disnake.ButtonStyle.success)
async def set_accepted(self, button: disnake.ui.Button, interaction: disnake.Interaction):
await self.apply_status(interaction, "accepté")
async def apply_status(self, interaction: discord.Interaction, prefix: str):
async def apply_status(self, interaction: disnake.Interaction, prefix: str):
await interaction.response.defer(ephemeral=True)
try:
settings = load_settings(interaction.guild.id)
@ -555,8 +584,8 @@ class ReopenStatusView(discord.ui.View):
kuby_logger.error(f"Erreur lors de la réouverture/synchro: {e}")
await interaction.followup.send(f"❌ Erreur système : {e}", ephemeral=True)
@discord.ui.button(label="❌ Supprimer le Ticket", style=discord.ButtonStyle.danger, custom_id="delete_ticket")
async def delete_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="❌ Supprimer le Ticket", style=disnake.ButtonStyle.danger, custom_id="delete_ticket")
async def delete_ticket(self, button: disnake.ui.Button, interaction: disnake.Interaction):
"""Supprime le salon du ticket"""
await interaction.response.defer(ephemeral=True)
@ -575,14 +604,14 @@ class ReopenStatusView(discord.ui.View):
try: await interaction.followup.send(f"❌ Erreur lors de la suppression : {e}", ephemeral=True)
except: pass
class TicketView(discord.ui.View):
class TicketView(disnake.ui.View):
"""Vue avec le bouton pour ouvrir le ticket"""
def __init__(self):
super().__init__(timeout=None)
@discord.ui.button(label="📑 Ouvrir un Ticket Whitelist", style=discord.ButtonStyle.red, custom_id="open_ticket")
async def open_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
@disnake.ui.button(label="📑 Ouvrir un Ticket Whitelist", style=disnake.ButtonStyle.red, custom_id="open_ticket")
async def open_ticket(self, button: disnake.ui.Button, interaction: disnake.Interaction):
"""Affiche le premier formulaire"""
await interaction.response.send_modal(TicketStep1())
@ -598,11 +627,11 @@ class Ticket(commands.Cog):
self.bot.add_view(TicketManagementView())
self.bot.add_view(ClosedTicketView())
@app_commands.command(name="ticket_whitelist", description="Envoie l'embed pour ouvrir un ticket de whitelist.")
@app_commands.checks.has_permissions(administrator=True)
async def ticket_whitelist(self, interaction: discord.Interaction):
@commands.slash_command(name="ticket_whitelist", description="Envoie l'embed pour ouvrir un ticket de whitelist.")
@commands.has_permissions(administrator=True)
async def ticket_whitelist(self, interaction: disnake.ApplicationCommandInteraction):
"""Envoie l'embed avec le bouton d'ouverture de ticket"""
embed = discord.Embed(
embed = disnake.Embed(
title="📩 Ticket Whitelist",
description="Pour réaliser votre whitelist, ouvrez un ticket et remplissez le formulaire.",
color=0x00ff00
@ -611,16 +640,16 @@ class Ticket(commands.Cog):
await interaction.channel.send(embed=embed, view=view)
await interaction.response.send_message("✅ Embed des tickets envoyé.", ephemeral=True)
@app_commands.command(name="ticket_whitelist_config", description="Configurer le système de tickets whitelist (Admin)")
@app_commands.checks.has_permissions(administrator=True)
async def ticket_whitelist_config(self, interaction: discord.Interaction):
@commands.slash_command(name="ticket_whitelist_config", description="Configurer le système de tickets whitelist (Admin)")
@commands.has_permissions(administrator=True)
async def ticket_whitelist_config(self, interaction: disnake.ApplicationCommandInteraction):
"""Interface de configuration des tickets"""
settings = load_settings(interaction.guild.id)
embed = discord.Embed(
embed = disnake.Embed(
title="⚙️ Configuration Tickets Whitelist",
description="Utilisez les boutons ci-dessous pour lier la catégorie et les rôles.",
color=discord.Color.blue()
color=disnake.Color.blue()
)
cat_id = settings.get("ticket_category_id")
@ -673,7 +702,7 @@ class Ticket(commands.Cog):
user_id = user_id
try:
user = await message.guild.fetch_member(user_id)
except (discord.NotFound, discord.HTTPException):
except (disnake.NotFound, disnake.HTTPException):
await message.channel.send("❌ Utilisateur introuvable. A-t-il quitté le serveur ?", delete_after=10)
return
@ -701,19 +730,19 @@ class Ticket(commands.Cog):
asyncio.create_task(message.channel.edit(name=new_name))
# Confirmation dans le salon (Feedback visuel)
embed_val = discord.Embed(
embed_val = disnake.Embed(
title="🎉 Whitelist Validée !",
description=f"Félicitations {user.mention}, ton entretien oral a été validé par {message.author.mention} !\nTu es désormais **Citoyen**.",
color=discord.Color.green()
color=disnake.Color.green()
)
await message.channel.send(embed=embed_val)
# DM au joueur (RESTAURATION DESIGN CAPTURE)
try:
embed_dm = discord.Embed(
embed_dm = disnake.Embed(
title="🎉 Félicitations - Citoyenneté Validée !",
description=f"Nous avons le plaisir de t'annoncer que ton passage sur **{message.guild.name}** a été validé avec succès !",
color=discord.Color.green()
color=disnake.Color.green()
)
embed_dm.add_field(name="🏙️ Nouveau Statut", value="Citoyen", inline=True)
embed_dm.add_field(name="📍 Serveur", value=message.guild.name, inline=True)
@ -740,19 +769,19 @@ class Ticket(commands.Cog):
asyncio.create_task(message.channel.edit(name=new_name))
# Confirmation dans le salon
embed_val = discord.Embed(
embed_val = disnake.Embed(
title="📝 Étape Écrite Validée !",
description=f"Ta candidature écrite a été validée par {message.author.mention}.\nTu passes désormais en **Attente Oral**.",
color=discord.Color.orange()
color=disnake.Color.orange()
)
await message.channel.send(embed=embed_val)
# DM au joueur (RESTAURATION DESIGN CAPTURE)
try:
embed_dm = discord.Embed(
embed_dm = disnake.Embed(
title="📝 Validation Étape Écrite",
description=f"Ta candidature écrite sur **{message.guild.name}** a été examinée et validée par notre équipe !",
color=discord.Color.orange()
color=disnake.Color.orange()
)
embed_dm.add_field(name="🎤 Statut Actuel", value="Attente Oral", inline=True)
embed_dm.add_field(name="🛠️ Action Requise", value="Merci de te rendre dans le salon vocal 'Attente Oral' dès que tu es disponible pour passer ton entretien.", inline=False)
@ -770,8 +799,8 @@ class Ticket(commands.Cog):
kuby_logger.error(f"Erreur lors de la validation du ticket: {e}", exc_info=True)
await message.channel.send(f"❌ Une erreur système est survenue : {e}")
async def setup(bot):
await bot.add_cog(Ticket(bot))
def setup(bot):
bot.add_cog(Ticket(bot))
bot.add_view(TicketView())
bot.add_view(TicketManagementView())
bot.add_view(ClosedTicketView())

View file

@ -25,9 +25,8 @@ from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Optional
import discord
from discord import app_commands
from discord.ext import commands, tasks
import disnake
from disnake.ext import commands, tasks
from src.json_export_service import (
load_config, save_config, generate_all,
@ -81,8 +80,8 @@ class WebsiteSync(commands.Cog, name="Website Sync"):
# ── Helper : embed de réponse ─────────────────────────────────────────────
@staticmethod
def _embed(title: str, color: discord.Color, description: str = "") -> discord.Embed:
e = discord.Embed(title=title, color=color, timestamp=datetime.now(timezone.utc))
def _embed(title: str, color: disnake.Color, description: str = "") -> disnake.Embed:
e = disnake.Embed(title=title, color=color, timestamp=datetime.now(timezone.utc))
if description:
e.description = description
return e
@ -90,31 +89,30 @@ class WebsiteSync(commands.Cog, name="Website Sync"):
# ══════════════════════════════════════════════════════════════════════════
# /json-generate
# ══════════════════════════════════════════════════════════════════════════
@app_commands.command(name="json-generate", description="Génère manuellement les fichiers JSON du site web")
@app_commands.describe(cible="Quoi générer ? (défaut : tout)")
@app_commands.choices(cible=[
app_commands.Choice(name="Tout (servers + members)", value="all"),
app_commands.Choice(name="Serveurs uniquement", value="servers"),
app_commands.Choice(name="Membres uniquement", value="members"),
])
@app_commands.default_permissions(administrator=True)
async def json_generate(self, interaction: discord.Interaction, cible: str = "all"):
@commands.slash_command(name="json-generate", description="Génère manuellement les fichiers JSON du site web")
@commands.has_permissions(administrator=True)
async def json_generate(self, interaction: disnake.ApplicationCommandInteraction,
cible: str = commands.Param("all", description="Quoi générer ?", choices={
"Tout (servers + members)": "all",
"Serveurs uniquement": "servers",
"Membres uniquement": "members"
})):
await interaction.response.defer(ephemeral=True)
start = datetime.now()
embed = self._embed("⏳ Génération en cours...", discord.Color.yellow())
await interaction.followup.send(embed=embed, ephemeral=True)
embed = self._embed("⏳ Génération en cours...", disnake.Color.yellow())
await interaction.followup.send(embed=embed)
try:
if cible == "servers":
res = await generate_servers_json(self.bot)
embed = self._embed("✅ servers.json généré", discord.Color.green())
embed = self._embed("✅ servers.json généré", disnake.Color.green())
embed.add_field(name="Serveurs", value=str(res["count"]), inline=True)
embed.add_field(name="Durée", value=f"{(datetime.now()-start).total_seconds():.1f}s", inline=True)
embed.add_field(name="Fichier", value=f"`{SERVERS_FILE}`", inline=False)
elif cible == "members":
res = await generate_members_json(self.bot)
embed = self._embed("✅ members.json généré", discord.Color.green())
embed = self._embed("✅ members.json généré", disnake.Color.green())
embed.add_field(name="Membres", value=str(res["count"]), inline=True)
embed.add_field(name="Serveur", value=res["guildName"], inline=True)
embed.add_field(name="Durée", value=f"{(datetime.now()-start).total_seconds():.1f}s", inline=True)
@ -125,7 +123,7 @@ class WebsiteSync(commands.Cog, name="Website Sync"):
has_errors = bool(res["errors"])
embed = self._embed(
"⚠️ Génération partielle" if has_errors else "✅ Génération complète",
discord.Color.orange() if has_errors else discord.Color.green(),
disnake.Color.orange() if has_errors else disnake.Color.green(),
)
if res["servers"]:
embed.add_field(name="📋 Serveurs", value=f"{res['servers']['count']} serveurs", inline=True)
@ -137,19 +135,19 @@ class WebsiteSync(commands.Cog, name="Website Sync"):
embed.add_field(name="📁 Dossier", value=f"`{OUTPUT_DIR}`", inline=False)
except Exception as e:
embed = self._embed("❌ Erreur", discord.Color.red(), f"```{e}```")
embed = self._embed("❌ Erreur", disnake.Color.red(), f"```{e}```")
await interaction.edit_original_response(embed=embed)
# ══════════════════════════════════════════════════════════════════════════
# /json-status
# ══════════════════════════════════════════════════════════════════════════
@app_commands.command(name="json-status", description="État des fichiers JSON et prochaine génération auto")
@app_commands.default_permissions(administrator=True)
async def json_status(self, interaction: discord.Interaction):
@commands.slash_command(name="json-status", description="État des fichiers JSON et prochaine génération auto")
@commands.has_permissions(administrator=True)
async def json_status(self, interaction: disnake.ApplicationCommandInteraction):
await interaction.response.defer(ephemeral=True)
config = load_config()
embed = self._embed("📊 Statut JSON Export", discord.Color.blurple())
embed = self._embed("📊 Statut JSON Export", disnake.Color.blurple())
# servers.json
if SERVERS_FILE.exists():
@ -203,15 +201,14 @@ class WebsiteSync(commands.Cog, name="Website Sync"):
# ══════════════════════════════════════════════════════════════════════════
# /json-config (groupe)
# ══════════════════════════════════════════════════════════════════════════
json_config_group = app_commands.Group(
name="json-config",
description="Configure la génération automatique des JSON",
default_permissions=discord.Permissions(administrator=True),
)
@commands.slash_command(name="json-config", description="Configure la génération automatique des JSON")
@commands.has_permissions(administrator=True)
async def json_config_group(self, interaction: disnake.ApplicationCommandInteraction):
pass
# ── show ──────────────────────────────────────────────────────────────────
@json_config_group.command(name="show", description="Affiche la configuration actuelle")
async def config_show(self, interaction: discord.Interaction):
@json_config_group.sub_command(name="show", description="Affiche la configuration actuelle")
async def config_show(self, interaction: disnake.ApplicationCommandInteraction):
await interaction.response.defer(ephemeral=True)
config = load_config()
@ -220,7 +217,7 @@ class WebsiteSync(commands.Cog, name="Website Sync"):
mbr_on = ", ".join(k for k, v in config["memberFields"].items() if v) or "_Aucun_"
mbr_off = ", ".join(k for k, v in config["memberFields"].items() if not v) or "_Aucun_"
embed = self._embed("⚙️ Configuration JSON Export", discord.Color.blurple())
embed = self._embed("⚙️ Configuration JSON Export", disnake.Color.blurple())
embed.add_field(name="🕐 Génération auto", value=f"{config['autoHour']:02d}:{config['autoMinute']:02d} (heure serveur)", inline=True)
embed.add_field(name="🎯 Serveur cible (members)", value=config["targetGuildId"] or "_Non défini_", inline=True)
embed.add_field(name="\u200b", value="\u200b", inline=False)
@ -233,9 +230,8 @@ class WebsiteSync(commands.Cog, name="Website Sync"):
await interaction.followup.send(embed=embed, ephemeral=True)
# ── set-guild ────────────────────────────────────────────────────────────
@json_config_group.command(name="set-guild", description="Définit l'ID du serveur cible pour members.json")
@app_commands.describe(guild_id="ID du serveur Discord")
async def config_set_guild(self, interaction: discord.Interaction, guild_id: str):
@json_config_group.sub_command(name="set-guild", description="Définit l'ID du serveur cible pour members.json")
async def config_set_guild(self, interaction: disnake.ApplicationCommandInteraction, guild_id: str = commands.Param(description="ID du serveur Discord")):
await interaction.response.defer(ephemeral=True)
try:
guild = self.bot.get_guild(int(guild_id)) or await self.bot.fetch_guild(int(guild_id))
@ -243,20 +239,21 @@ class WebsiteSync(commands.Cog, name="Website Sync"):
config["targetGuildId"] = guild_id
save_config(config)
embed = self._embed(
"✅ Serveur cible défini", discord.Color.green(),
"✅ Serveur cible défini", disnake.Color.green(),
f"**{guild.name}** (`{guild_id}`) sera utilisé pour members.json.",
)
except Exception:
embed = self._embed(
"❌ Serveur introuvable", discord.Color.red(),
"❌ Serveur introuvable", disnake.Color.red(),
f"Le bot n'est pas dans le serveur `{guild_id}` ou l'ID est invalide.",
)
await interaction.followup.send(embed=embed, ephemeral=True)
# ── set-schedule ─────────────────────────────────────────────────────────
@json_config_group.command(name="set-schedule", description="Définit l'heure de génération automatique quotidienne")
@app_commands.describe(heure="Heure (0-23)", minute="Minute (0-59, défaut: 0)")
async def config_set_schedule(self, interaction: discord.Interaction, heure: int, minute: int = 0):
@json_config_group.sub_command(name="set-schedule", description="Définit l'heure de génération automatique quotidienne")
async def config_set_schedule(self, interaction: disnake.ApplicationCommandInteraction,
heure: int = commands.Param(description="Heure (0-23)"),
minute: int = commands.Param(0, description="Minute (0-59, défaut: 0)")):
await interaction.response.defer(ephemeral=True)
if not (0 <= heure <= 23) or not (0 <= minute <= 59):
await interaction.followup.send("❌ Heure invalide.", ephemeral=True)
@ -266,42 +263,42 @@ class WebsiteSync(commands.Cog, name="Website Sync"):
config["autoMinute"] = minute
save_config(config)
embed = self._embed(
"✅ Horaire mis à jour", discord.Color.green(),
"✅ Horaire mis à jour", disnake.Color.green(),
f"Prochaine génération auto : **{heure:02d}:{minute:02d}** chaque jour.",
)
await interaction.followup.send(embed=embed, ephemeral=True)
await interaction.followup.send(embed=embed)
# ── server-field ─────────────────────────────────────────────────────────
@json_config_group.command(name="server-field", description="Active/désactive un champ dans servers.json")
@app_commands.describe(champ="Champ à modifier", actif="true = inclus, false = exclu")
@app_commands.choices(champ=[app_commands.Choice(name=f, value=f) for f in SERVER_FIELDS])
async def config_server_field(self, interaction: discord.Interaction, champ: str, actif: bool):
@json_config_group.sub_command(name="server-field", description="Active/désactive un champ dans servers.json")
async def config_server_field(self, interaction: disnake.ApplicationCommandInteraction,
champ: str = commands.Param(description="Champ à modifier", choices=SERVER_FIELDS),
actif: bool = commands.Param(description="true = inclus, false = exclu")):
await interaction.response.defer(ephemeral=True)
config = load_config()
config["serverFields"][champ] = actif
save_config(config)
state = "inclus ✅" if actif else "exclu ❌"
embed = self._embed("✅ Champ servers.json mis à jour", discord.Color.green(),
embed = self._embed("✅ Champ servers.json mis à jour", disnake.Color.green(),
f"`{champ}` est maintenant **{state}** de servers.json.")
await interaction.followup.send(embed=embed, ephemeral=True)
await interaction.followup.send(embed=embed)
# ── member-field ─────────────────────────────────────────────────────────
@json_config_group.command(name="member-field", description="Active/désactive un champ dans members.json")
@app_commands.describe(champ="Champ à modifier", actif="true = inclus, false = exclu")
@app_commands.choices(champ=[app_commands.Choice(name=f, value=f) for f in MEMBER_FIELDS])
async def config_member_field(self, interaction: discord.Interaction, champ: str, actif: bool):
@json_config_group.sub_command(name="member-field", description="Active/désactive un champ dans members.json")
async def config_member_field(self, interaction: disnake.ApplicationCommandInteraction,
champ: str = commands.Param(description="Champ à modifier", choices=MEMBER_FIELDS),
actif: bool = commands.Param(description="true = inclus, false = exclu")):
await interaction.response.defer(ephemeral=True)
config = load_config()
config["memberFields"][champ] = actif
save_config(config)
state = "inclus ✅" if actif else "exclu ❌"
embed = self._embed("✅ Champ members.json mis à jour", discord.Color.green(),
embed = self._embed("✅ Champ members.json mis à jour", disnake.Color.green(),
f"`{champ}` est maintenant **{state}** de members.json.")
await interaction.followup.send(embed=embed, ephemeral=True)
await interaction.followup.send(embed=embed)
# ── reset ─────────────────────────────────────────────────────────────────
@json_config_group.command(name="reset", description="Remet la configuration par défaut")
async def config_reset(self, interaction: discord.Interaction):
@json_config_group.sub_command(name="reset", description="Remet la configuration par défaut")
async def config_reset(self, interaction: disnake.ApplicationCommandInteraction):
await interaction.response.defer(ephemeral=True)
from src.json_export_service import CONFIG_FILE
try:
@ -309,14 +306,14 @@ class WebsiteSync(commands.Cog, name="Website Sync"):
except Exception:
pass
load_config() # recrée depuis DEFAULT_CONFIG
embed = self._embed("✅ Configuration réinitialisée", discord.Color.green(),
embed = self._embed("✅ Configuration réinitialisée", disnake.Color.green(),
"Tous les paramètres ont été remis à leurs valeurs par défaut.")
await interaction.followup.send(embed=embed, ephemeral=True)
await interaction.followup.send(embed=embed)
# ── set-output-dir ────────────────────────────────────────────────────────
@json_config_group.command(name="set-output-dir", description="Change le dossier de sortie des fichiers JSON")
@app_commands.describe(chemin="Chemin absolu ou relatif vers le dossier (ex: /var/www/site/data)")
async def config_set_output_dir(self, interaction: discord.Interaction, chemin: str):
@json_config_group.sub_command(name="set-output-dir", description="Change le dossier de sortie des fichiers JSON")
async def config_set_output_dir(self, interaction: disnake.ApplicationCommandInteraction,
chemin: str = commands.Param(description="Chemin absolu ou relatif vers le dossier (ex: /var/www/site/data)")):
await interaction.response.defer(ephemeral=True)
from pathlib import Path as _Path
p = _Path(chemin)
@ -326,75 +323,69 @@ class WebsiteSync(commands.Cog, name="Website Sync"):
config["outputDir"] = str(p)
save_config(config)
embed = self._embed(
"✅ Dossier de sortie mis à jour", discord.Color.green(),
"✅ Dossier de sortie mis à jour", disnake.Color.green(),
f"Les JSON seront désormais écrits dans :\n`{p.resolve()}`",
)
except Exception as e:
embed = self._embed("❌ Erreur", discord.Color.red(), f"Impossible d'utiliser ce chemin :\n```{e}```")
await interaction.followup.send(embed=embed, ephemeral=True)
embed = self._embed("❌ Erreur", disnake.Color.red(), f"Impossible d'utiliser ce chemin :\n```{e}```")
await interaction.followup.send(embed=embed)
# ── add-founder-role ──────────────────────────────────────────────────────
@json_config_group.command(name="add-founder-role", description="Ajoute un nom de rôle reconnu comme 'fondateur'")
@app_commands.describe(nom="Nom du rôle (insensible à la casse, correspondance partielle)")
async def config_add_founder_role(self, interaction: discord.Interaction, nom: str):
@json_config_group.sub_command(name="add-founder-role", description="Ajoute un nom de rôle reconnu comme 'fondateur'")
async def config_add_founder_role(self, interaction: disnake.ApplicationCommandInteraction,
nom: str = commands.Param(description="Nom du rôle (insensible à la casse, correspondance partielle)")):
await interaction.response.defer(ephemeral=True)
config = load_config()
roles = config.get("founderRoles", [])
nom_l = nom.lower().strip()
if nom_l in [r.lower() for r in roles]:
embed = self._embed(" Déjà présent", discord.Color.yellow(),
embed = self._embed(" Déjà présent", disnake.Color.yellow(),
f"`{nom}` est déjà dans la liste des rôles fondateurs.")
else:
roles.append(nom_l)
config["founderRoles"] = roles
save_config(config)
embed = self._embed(
"✅ Rôle fondateur ajouté", discord.Color.green(),
"✅ Rôle fondateur ajouté", disnake.Color.green(),
f"`{nom_l}` ajouté. Liste actuelle : {', '.join(f'`{r}`' for r in roles)}",
)
await interaction.followup.send(embed=embed, ephemeral=True)
await interaction.followup.send(embed=embed)
# ── remove-founder-role ───────────────────────────────────────────────────
@json_config_group.command(name="remove-founder-role", description="Retire un nom de rôle fondateur")
@app_commands.describe(nom="Nom du rôle à retirer")
async def config_remove_founder_role(self, interaction: discord.Interaction, nom: str):
@json_config_group.sub_command(name="remove-founder-role", description="Retire un nom de rôle fondateur")
async def config_remove_founder_role(self, interaction: disnake.ApplicationCommandInteraction,
nom: str = commands.Param(description="Nom du rôle à retirer")):
await interaction.response.defer(ephemeral=True)
config = load_config()
roles = config.get("founderRoles", [])
nom_l = nom.lower().strip()
updated = [r for r in roles if r.lower() != nom_l]
if len(updated) == len(roles):
embed = self._embed(" Introuvable", discord.Color.yellow(),
embed = self._embed(" Introuvable", disnake.Color.yellow(),
f"`{nom}` n'est pas dans la liste des rôles fondateurs.")
else:
config["founderRoles"] = updated
save_config(config)
reste = ', '.join(f'`{r}`' for r in updated) or "_Aucun_"
embed = self._embed(
"✅ Rôle fondateur retiré", discord.Color.green(),
"✅ Rôle fondateur retiré", disnake.Color.green(),
f"`{nom_l}` retiré. Liste actuelle : {reste}",
)
await interaction.followup.send(embed=embed, ephemeral=True)
await interaction.followup.send(embed=embed)
# ══════════════════════════════════════════════════════════════════════════
# /json-preview
# ══════════════════════════════════════════════════════════════════════════
@app_commands.command(name="json-preview", description="Aperçu des données sans écrire les fichiers")
@app_commands.describe(
cible="Quoi prévisualiser ?",
limite="Nombre d'éléments à afficher (défaut: 3)",
)
@app_commands.choices(cible=[
app_commands.Choice(name="Serveurs", value="servers"),
app_commands.Choice(name="Membres", value="members"),
])
@app_commands.default_permissions(administrator=True)
async def json_preview(self, interaction: discord.Interaction, cible: str = "servers", limite: int = 3):
@commands.slash_command(name="json-preview", description="Aperçu des données sans écrire les fichiers")
@commands.has_permissions(administrator=True)
async def json_preview(self, interaction: disnake.ApplicationCommandInteraction,
cible: str = commands.Param("servers", description="Quoi prévisualiser ?", choices={"Serveurs": "servers", "Membres": "members"}),
limite: int = commands.Param(3, description="Nombre d'éléments à afficher (1-10, défaut: 3)")):
await interaction.response.defer(ephemeral=True)
limite = max(1, min(limite, 10)) # entre 1 et 10
try:
embed = self._embed(f"🔍 Aperçu — {cible}", discord.Color.blurple())
embed = self._embed(f"🔍 Aperçu — {cible}", disnake.Color.blurple())
embed.set_footer(text="Aperçu uniquement — aucun fichier écrit")
if cible == "servers":
@ -421,22 +412,22 @@ class WebsiteSync(commands.Cog, name="Website Sync"):
)
except Exception as e:
embed = self._embed("❌ Erreur", discord.Color.red(), f"```{e}```")
embed = self._embed("❌ Erreur", disnake.Color.red(), f"```{e}```")
await interaction.followup.send(embed=embed, ephemeral=True)
# ══════════════════════════════════════════════════════════════════════════
# /json-history
# ══════════════════════════════════════════════════════════════════════════
@app_commands.command(name="json-history", description="Historique des dernières générations automatiques et manuelles")
@app_commands.describe(limite="Nombre d'entrées à afficher (défaut: 5, max: 10)")
@app_commands.default_permissions(administrator=True)
async def json_history(self, interaction: discord.Interaction, limite: int = 5):
@commands.slash_command(name="json-history", description="Historique des dernières générations automatiques et manuelles")
@commands.has_permissions(administrator=True)
async def json_history(self, interaction: disnake.ApplicationCommandInteraction,
limite: int = commands.Param(5, description="Nombre d'entrées à afficher (défaut: 5, max: 10)")):
await interaction.response.defer(ephemeral=True)
limite = max(1, min(limite, 10))
history = get_history()
embed = self._embed("📜 Historique des générations JSON", discord.Color.blurple())
embed = self._embed("📜 Historique des générations JSON", disnake.Color.blurple())
if not history:
embed.description = "_Aucune génération enregistrée pour l'instant._"
@ -464,10 +455,6 @@ class WebsiteSync(commands.Cog, name="Website Sync"):
# ─── Setup ───────────────────────────────────────────────────────────────────
async def setup(bot: commands.Bot):
cog = WebsiteSync(bot)
await bot.add_cog(cog)
# Enregistre le groupe de commandes json-config s'il n'est pas déjà présent
if not bot.tree.get_command("json-config"):
bot.tree.add_command(cog.json_config_group)
def setup(bot):
bot.add_cog(WebsiteSync(bot))
logger.info("Cog WebsiteSync chargé.")

View file

@ -1,11 +1,11 @@
import discord
from discord.ext import commands
from discord import app_commands
import disnake
from disnake.ext import commands
import sqlite3
import aiohttp
import io
import os
import logging
import asyncio
from PIL import Image, ImageDraw, ImageFont
kuby_logger = logging.getLogger("KubyBot")
@ -133,7 +133,7 @@ class Welcome(commands.Cog):
kuby_logger.debug(f"🔍 [Welcome] Config récupérée: DM={bool(welcome_dm_text)}, Channel={welcome_channel}")
# 1. Gestion du DM
if welcome_dm_text:
if welcome_dm_text and not member.bot:
kuby_logger.info(f"✉️ [Welcome] Tentative d'envoi de DM à {member.name}")
try:
formatted_message = welcome_dm_text.format(
@ -145,11 +145,13 @@ class Welcome(commands.Cog):
gif_url = "https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExNXV1NW02NTk0bGF2ZHcyMGFna2F6amRrbGpobGpsMHowOW5xa2E5eCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ZtFuW4rsUbfrRzPauJ/giphy.gif"
await member.send(f"{formatted_message}\n\n{gif_url}")
kuby_logger.info(f"✅ [Welcome] DM envoyé avec succès à {member.name} (envoi groupé)")
except discord.Forbidden:
except disnake.Forbidden:
kuby_logger.warning(f"🚫 [Welcome] Impossible d'envoyer le DM à {member.name} (DMs fermés)")
except discord.HTTPException as e:
except disnake.HTTPException as e:
if e.code == 40003: # Rate limited (Opening DMs too fast)
kuby_logger.warning(f"⏳ [Welcome] Limite de débit Discord atteinte pour les DMs : {member.name} n'a pas reçu son message.")
elif e.code == 50007: # Cannot send messages to this user
kuby_logger.warning(f"🚫 [Welcome] Impossible d'envoyer le DM à {member.name} (DMs fermés ou utilisateur non joignable)")
else:
kuby_logger.error(f"❌ [Welcome] Erreur HTTP lors de l'envoi du DM à {member.name}: {e}")
except Exception as e:
@ -170,20 +172,35 @@ class Welcome(commands.Cog):
banner_background=banner_background
)
if img:
# Attendre un peu que le Cog d'invitations traite le join
await asyncio.sleep(1)
invites_cog = self.bot.get_cog("Invites")
inviter_text = ""
if invites_cog:
inviter_id = invites_cog.get_inviter_info(member.guild.id, member.id)
if inviter_id:
inviter = member.guild.get_member(inviter_id) or await self.bot.fetch_user(inviter_id)
total, current = invites_cog.get_invite_stats(member.guild.id, inviter_id)
inviter_text = f"\n\n**Invité par :** {inviter.mention} ({current} invitations)"
# Utiliser le message personnalisé ou un message par défaut
channel_description = welcome_channel_message.format(
channel_description = (welcome_channel_message.format(
username=member.name,
servername=member.guild.name,
member_count=member.guild.member_count
) if welcome_channel_message else "Bienvenue dans le serveur !\nSi vous cherchez des RolePlay de qualité,\nVous êtes au bon endroit. <a:sip:1316891821858619452>"
) if welcome_channel_message else "Bienvenue dans le serveur !\nSi vous cherchez des RolePlay de qualité,\nVous êtes au bon endroit. <a:sip:1316891821858619452>") + inviter_text
embed = discord.Embed(title=f"Merci d'accueillir {member} 🤝",
description=channel_description,
color=discord.Color.dark_purple())
embed.set_image(url="attachment://welcome.png")
await channel.send(content=member.mention, embed=embed, file=discord.File(img, filename="welcome.png"))
components = [
disnake.ui.Container(
disnake.ui.TextDisplay(f"Merci d'accueillir {member} 🤝"),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(channel_description),
disnake.ui.MediaGallery(disnake.MediaGalleryItem(url="attachment://welcome.png"))
)
]
await channel.send(content=member.mention, components=components, file=disnake.File(img, filename="welcome.png"))
async def save_banner_attachment(self, attachment: discord.Attachment, guild_id: int) -> str:
async def save_banner_attachment(self, attachment: disnake.Attachment, guild_id: int) -> str:
"""Sauvegarde l'attachment et retourne le chemin du fichier"""
base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "banners"))
os.makedirs(base_dir, exist_ok=True)
@ -215,9 +232,9 @@ class Welcome(commands.Cog):
return os.path.basename(banner_path)
@app_commands.command(name="setwelcome", description="Définit le salon, les messages de bienvenue et le fond de la bannière")
@app_commands.checks.has_permissions(administrator=True)
async def set_welcome(self, interaction: discord.Interaction, channel: discord.TextChannel, message_dm: str, message_salon: str, server_name: str, banner_file: discord.Attachment = None):
@commands.slash_command(name="setwelcome", description="Définit le salon, les messages de bienvenue et le fond de la bannière")
@commands.has_permissions(administrator=True)
async def set_welcome(self, interaction: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel, message_dm: str, message_salon: str, server_name: str, banner_file: disnake.Attachment = None):
banner_path = None
# Si un fichier est uploadé, le sauvegarder
@ -237,13 +254,43 @@ class Welcome(commands.Cog):
welcome_server_name=excluded.welcome_server_name,
welcome_banner_background=COALESCE(excluded.welcome_banner_background, welcome_banner_background),
welcome_channel_message=excluded.welcome_channel_message''',
(interaction.guild_id, channel.id, message_dm, server_name, banner_path, message_salon))
(interaction.guild_id, channel.id, message_dm, server_name, banner_path, message_salon))
conn.commit()
conn.close()
bg_used = "background_template.png" if not banner_path else (banner_path.split('/')[-1])
await interaction.response.send_message(f"✅ Configuration de bienvenida enregistrée !\nFond utilisé: {bg_used}", ephemeral=True)
async def setup(bot):
await bot.add_cog(Welcome(bot))
@commands.slash_command(name="invitations", description="Affiche vos statistiques d'invitations")
async def invitations_stats(self, interaction: disnake.ApplicationCommandInteraction, user: disnake.Member = None):
user = user or interaction.author
await self._send_invitation_stats(interaction, user)
@commands.user_command(name="Stats Invitations")
async def invitations_user(self, interaction: disnake.UserCommandInteraction, user: disnake.Member):
await self._send_invitation_stats(interaction, user)
async def _send_invitation_stats(self, interaction, user):
invites_cog = self.bot.get_cog("Invites")
if not invites_cog:
return await interaction.response.send_message("❌ Le module d'invitations est désactivé.", ephemeral=True)
total, current = invites_cog.get_invite_stats(interaction.guild.id, user.id)
components = [
disnake.ui.Container(
disnake.ui.Section(
f"Statistiques d'invitations - {user.display_name}",
accessory=disnake.ui.Thumbnail(user.display_avatar.url)
),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(f"➜ Total d'invitations : **{total}**"),
disnake.ui.TextDisplay(f"➜ Membres présents : **{current}**")
)
]
await interaction.response.send_message(components=components)
def setup(bot):
bot.add_cog(Welcome(bot))

View file

@ -1,6 +1,5 @@
import discord
from discord import app_commands
from discord.ext import commands
import disnake
from disnake.ext import commands
import json
import os
from typing import List
@ -32,95 +31,60 @@ class Whitelist(commands.Cog):
self.whitelist[str(guild_id)] = users
self.save_whitelist()
@commands.hybrid_group(name="whitelist", aliases=["wl"], description="Gère la whitelist du serveur")
async def whitelist_group(self, ctx):
@commands.slash_command(name="whitelist", description="Gère la whitelist du serveur")
async def whitelist_group(self, interaction: disnake.ApplicationCommandInteraction):
"""Groupe de commandes pour gérer la whitelist"""
if ctx.invoked_subcommand is None:
await ctx.send_help(ctx.command)
pass
# ✅ Ajouter un utilisateur
@whitelist_group.command(name="ajouter", description="Ajoute un utilisateur à la whitelist")
@app_commands.describe(utilisateur="L'utilisateur à ajouter")
async def ajouter(self, ctx, utilisateur: discord.User):
interaction = ctx.interaction
guild_id = ctx.guild.id
@whitelist_group.sub_command(name="ajouter", description="Ajoute un utilisateur à la whitelist")
async def ajouter(self, interaction: disnake.ApplicationCommandInteraction, utilisateur: disnake.User):
guild_id = interaction.guild.id
users = self._get_guild_whitelist(guild_id)
user_id = str(utilisateur.id)
if user_id in users:
msg = f"🔒 {utilisateur.mention} est déjà dans la whitelist."
if interaction:
await interaction.response.send_message(msg, ephemeral=True)
else:
await ctx.send(msg)
return
return await interaction.response.send_message(f"🔒 {utilisateur.mention} est déjà dans la whitelist.", ephemeral=True)
users.append(user_id)
self._set_guild_whitelist(guild_id, users)
msg = f"{utilisateur.mention} a été ajouté à la whitelist."
if interaction:
await interaction.response.send_message(msg, ephemeral=True)
else:
await ctx.send(msg)
await interaction.response.send_message(f"{utilisateur.mention} a été ajouté à la whitelist.", ephemeral=True)
# ❌ Retirer un utilisateur
@whitelist_group.command(name="retirer", description="Retire un utilisateur de la whitelist")
@app_commands.describe(utilisateur="L'utilisateur à retirer")
async def retirer(self, ctx, utilisateur: discord.User):
interaction = ctx.interaction
guild_id = ctx.guild.id
@whitelist_group.sub_command(name="retirer", description="Retire un utilisateur de la whitelist")
async def retirer(self, interaction: disnake.ApplicationCommandInteraction, utilisateur: disnake.User):
guild_id = interaction.guild.id
users = self._get_guild_whitelist(guild_id)
user_id = str(utilisateur.id)
if user_id not in users:
msg = f"⚠️ {utilisateur.mention} n'est pas dans la whitelist."
if interaction:
await interaction.response.send_message(msg, ephemeral=True)
else:
await ctx.send(msg)
return
return await interaction.response.send_message(f"⚠️ {utilisateur.mention} n'est pas dans la whitelist.", ephemeral=True)
users.remove(user_id)
self._set_guild_whitelist(guild_id, users)
msg = f"{utilisateur.mention} a été retiré de la whitelist."
if interaction:
await interaction.response.send_message(msg, ephemeral=True)
else:
await ctx.send(msg)
await interaction.response.send_message(f"{utilisateur.mention} a été retiré de la whitelist.", ephemeral=True)
# 📋 Voir la whitelist
@whitelist_group.command(name="liste", description="Affiche les utilisateurs de la whitelist")
async def liste(self, ctx):
interaction = ctx.interaction
guild_id = ctx.guild.id
@whitelist_group.sub_command(name="liste", description="Affiche les utilisateurs de la whitelist")
async def liste(self, interaction: disnake.ApplicationCommandInteraction):
guild_id = interaction.guild.id
users = self._get_guild_whitelist(guild_id)
if not users:
msg = "📭 La whitelist est vide pour ce serveur."
if interaction:
await interaction.response.send_message(msg, ephemeral=True)
else:
await ctx.send(msg)
return
# Defer if interaction because fetch_user might take time
if interaction:
await interaction.response.defer(ephemeral=True)
return await interaction.response.send_message("📭 La whitelist est vide pour ce serveur.", ephemeral=True)
await interaction.response.defer(ephemeral=True)
membres = []
for user_id in users:
try:
utilisateur = await self.bot.fetch_user(int(user_id))
membres.append(f"- {utilisateur.mention} ({utilisateur.name})")
except discord.NotFound:
except disnake.NotFound:
membres.append(f"- <@{user_id}> (Utilisateur introuvable)")
texte = "\n".join(membres)
msg = f"📃 **Utilisateurs whitelistés pour ce serveur :**\n{texte}"
if interaction:
await interaction.followup.send(msg, ephemeral=True)
else:
await ctx.send(msg)
await interaction.edit_original_response(content=f"📃 **Utilisateurs whitelistés pour ce serveur :**\n{texte}")
def is_whitelisted(self, guild_id: int, user_id: int) -> bool:
"""Check if a user is whitelisted for a specific guild."""
@ -131,5 +95,5 @@ class Whitelist(commands.Cog):
return self._get_guild_whitelist(guild_id).copy()
async def setup(bot):
await bot.add_cog(Whitelist(bot))
def setup(bot):
bot.add_cog(Whitelist(bot))

View file

@ -1,6 +1,6 @@
import discord
from discord.ext import commands
from discord.ui import View, Button
import disnake
from disnake.ext import commands
from disnake.ui import View, Button
import json
import os
from datetime import datetime
@ -14,18 +14,18 @@ class WhitelistMonitor(commands.Cog):
self.whitelist_file = "data/whitelist.json"
self.persistent_views_added = False
self.big_permissions = [
discord.Permissions.administrator,
discord.Permissions.manage_guild,
discord.Permissions.manage_roles,
discord.Permissions.manage_channels,
discord.Permissions.kick_members,
discord.Permissions.ban_members,
discord.Permissions.manage_messages,
discord.Permissions.moderate_members,
discord.Permissions.manage_nicknames,
discord.Permissions.manage_emojis,
discord.Permissions.manage_webhooks,
discord.Permissions.view_audit_log
disnake.Permissions.administrator,
disnake.Permissions.manage_guild,
disnake.Permissions.manage_roles,
disnake.Permissions.manage_channels,
disnake.Permissions.kick_members,
disnake.Permissions.ban_members,
disnake.Permissions.manage_messages,
disnake.Permissions.moderate_members,
disnake.Permissions.manage_nicknames,
disnake.Permissions.manage_emojis,
disnake.Permissions.manage_webhooks,
disnake.Permissions.view_audit_log
]
def load_whitelist(self) -> Dict[str, List[str]]:
@ -70,7 +70,7 @@ class WhitelistMonitor(commands.Cog):
return True
return False
def has_big_permissions(self, role: discord.Role) -> bool:
def has_big_permissions(self, role: disnake.Role) -> bool:
"""Check if role has significant permissions."""
permissions = role.permissions
for perm in self.big_permissions:
@ -78,7 +78,7 @@ class WhitelistMonitor(commands.Cog):
return True
return False
async def send_whitelist_notification(self, member: discord.Member, removed_roles: List[discord.Role]):
async def send_whitelist_notification(self, member: disnake.Member, removed_roles: List[disnake.Role]):
"""Send DM notification to user about whitelist member losing roles."""
try:
# Find guild administrators to notify
@ -87,10 +87,10 @@ class WhitelistMonitor(commands.Cog):
return
# Create embed
embed = discord.Embed(
embed = disnake.Embed(
title="⚠️ Whitelist Alert",
description=f"Un membre whitelisté a perdu des rôles importants dans **{member.guild.name}**.",
color=discord.Color.orange(),
color=disnake.Color.orange(),
timestamp=datetime.now()
)
@ -121,7 +121,7 @@ class WhitelistMonitor(commands.Cog):
for admin in admins:
try:
await admin.send(embed=embed, view=view)
except discord.Forbidden:
except disnake.Forbidden:
continue # User has DMs disabled
except Exception as e:
@ -155,7 +155,7 @@ class WhitelistMonitor(commands.Cog):
print(f"[WhitelistMonitor] Error registering views: {e}")
@commands.Cog.listener()
async def on_member_update(self, before: discord.Member, after: discord.Member):
async def on_member_update(self, before: disnake.Member, after: disnake.Member):
"""Monitor role changes for whitelist members."""
if before.roles == after.roles:
return
@ -173,7 +173,7 @@ class WhitelistMonitor(commands.Cog):
await self.send_whitelist_notification(after, important_removed)
@commands.Cog.listener()
async def on_member_remove(self, member: discord.Member):
async def on_member_remove(self, member: disnake.Member):
"""Auto-remove from whitelist when member leaves server."""
if self.is_whitelisted(member.guild.id, member.id):
removed = self.remove_from_whitelist(member.guild.id, member.id)
@ -184,10 +184,10 @@ class WhitelistMonitor(commands.Cog):
if log_channel_id:
log_channel = member.guild.get_channel(log_channel_id)
if log_channel:
embed = discord.Embed(
embed = disnake.Embed(
title="🗑️ Auto-Whitelist Removal",
description=f"{member.mention} a été retiré automatiquement de la whitelist après avoir quitté le serveur.",
color=discord.Color.red(),
color=disnake.Color.red(),
timestamp=datetime.now()
)
await log_channel.send(embed=embed)
@ -214,15 +214,15 @@ class WhitelistRemovalView(View):
self.user_id = user_id
self.guild_id = guild_id
@discord.ui.button(
@disnake.ui.button(
label="Retirer de la whitelist",
style=discord.ButtonStyle.red,
style=disnake.ButtonStyle.red,
emoji="🗑️",
custom_id="whitelist_remove_button"
)
async def remove_whitelist(
self,
interaction: discord.Interaction,
interaction: disnake.Interaction,
button: Button
):
"""Remove user from whitelist when button is clicked."""
@ -250,15 +250,15 @@ class WhitelistRemovalView(View):
# Update button
button.disabled = True
button.label = "✅ Retiré"
button.style = discord.ButtonStyle.green
button.style = disnake.ButtonStyle.green
await interaction.response.edit_message(view=self)
# Send confirmation
embed = discord.Embed(
embed = disnake.Embed(
title="✅ Retrait effectué",
description=f"{user_mention} a été retiré de la whitelist.",
color=discord.Color.green()
color=disnake.Color.green()
)
await interaction.followup.send(embed=embed, ephemeral=True)
@ -270,10 +270,10 @@ class WhitelistRemovalView(View):
if log_channel_id:
log_channel = guild.get_channel(log_channel_id)
if log_channel:
embed = discord.Embed(
embed = disnake.Embed(
title="🗑️ Whitelist Manual Removal",
description=f"{user_mention} a été retiré manuellement de la whitelist.",
color=discord.Color.green(),
color=disnake.Color.green(),
timestamp=datetime.now()
)
embed.set_footer(text=f"Retiré par {interaction.user.display_name}")
@ -293,5 +293,5 @@ class WhitelistRemovalView(View):
ephemeral=True
)
async def setup(bot):
await bot.add_cog(WhitelistMonitor(bot))
def setup(bot):
bot.add_cog(WhitelistMonitor(bot))

View file

@ -1,177 +1,177 @@
[
{
"date": "2026-05-01T14:42:53.534230+00:00",
"date": "2026-05-15T21:49:37.114321+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-05-01T14:34:33.777534+00:00",
"date": "2026-05-15T21:44:14.212871+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-05-01T14:31:01.918316+00:00",
"date": "2026-05-15T21:42:45.960991+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-05-01T14:11:08.947818+00:00",
"date": "2026-05-15T21:40:28.754282+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-05-01T13:48:18.180388+00:00",
"date": "2026-05-15T21:39:55.113067+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-05-01T13:36:45.397039+00:00",
"date": "2026-05-15T21:37:43.517923+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-05-01T13:20:32.058899+00:00",
"date": "2026-05-15T21:36:48.733530+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-04-23T14:35:15.364216+00:00",
"date": "2026-05-15T21:33:22.503958+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-04-23T13:35:15.351819+00:00",
"date": "2026-05-15T21:31:58.777625+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-04-23T12:35:15.324906+00:00",
"date": "2026-05-15T21:28:24.375077+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-04-23T11:35:15.210303+00:00",
"date": "2026-05-15T21:27:00.933212+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-04-23T10:35:15.338465+00:00",
"date": "2026-05-15T21:24:34.075332+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-04-23T10:28:55.984163+00:00",
"date": "2026-05-15T21:23:28.940963+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-04-02T17:03:30.870748+00:00",
"date": "2026-05-15T21:22:50.796880+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-04-01T14:00:13.276907+00:00",
"date": "2026-05-15T21:21:47.335364+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-04-01T13:19:44.954622+00:00",
"date": "2026-05-15T21:20:45.322710+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-04-01T13:17:23.537128+00:00",
"date": "2026-05-15T21:19:02.759168+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-04-01T13:15:14.389369+00:00",
"date": "2026-05-15T21:16:56.391804+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-04-01T13:08:00.124432+00:00",
"date": "2026-05-15T20:50:21.727551+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,
"membersCount": 13,
"guildName": "Omega Kube Bêta Test",
"errors": [],
"success": true
},
{
"date": "2026-04-01T13:03:42.601666+00:00",
"date": "2026-05-13T16:47:42.624873+00:00",
"trigger": "auto",
"serversCount": 1,
"membersCount": 12,

View file

@ -3,7 +3,7 @@
"guild_id": 1369669999345537145,
"user_id": 971446412690722826,
"category_id": "3b0b9bc1",
"status": "open",
"status": "closed",
"priority": "normal",
"created_at": "2026-01-10T15:57:24.973539",
"closed_at": null,

View file

@ -3,7 +3,7 @@
"guild_id": 1369669999345537145,
"user_id": 971446412690722826,
"category_id": "91522e2c",
"status": "open",
"status": "closed",
"priority": "normal",
"created_at": "2026-01-16T18:43:42.737852",
"closed_at": null,

View file

@ -28,10 +28,9 @@ kuby_logger.info("✅ Token Discord chargé avec succès")
async def main():
kuby_logger.info("🔄 Démarrage de la connexion Discord...")
try:
kuby_logger.debug("Starting async context manager")
async with bot:
kuby_logger.info("🔗 Connexion établie avec Discord...")
await bot.start(TOKEN)
await bot.do_async_setup()
kuby_logger.info("🔗 Connexion établie avec Discord...")
await bot.start(TOKEN)
except KeyboardInterrupt:
kuby_logger.info("🛑 Bot shutdown requested by user")
except Exception as e:

View file

@ -1,4 +1,4 @@
discord.py>=2.3.0
disnake>=2.11.0
python-dotenv>=1.0.0
Pillow>=10.0.0
aiohttp>=3.8.0

View file

@ -0,0 +1,9 @@
import disnake
import disnake.ui
c = disnake.ui.Container()
print(f"Container attributes: {dir(c)}")
try:
print(f"Children: {c.children}")
except Exception as e:
print(f"No children attribute: {e}")

12
scratch/check_file.py Normal file
View file

@ -0,0 +1,12 @@
import os
import json
member_id = 1141309757370675262
path = f"data/member_logs/{member_id}.json"
print(f"Path: {path}")
print(f"Exists: {os.path.exists(path)}")
if os.path.exists(path):
print(f"Size: {os.path.getsize(path)}")
with open(path, 'r') as f:
content = f.read()
print(f"Content: {repr(content)}")

View file

@ -1,4 +1,4 @@
import discord
import disnake
import json
import os
import asyncio
@ -18,7 +18,7 @@ class AdvancedLogger:
os.makedirs(self.logs_dir, exist_ok=True)
os.makedirs(self.member_logs_dir, exist_ok=True)
def log_message_event(self, message: discord.Message, event_type: str, details: Dict = None):
def log_message_event(self, message: disnake.Message, event_type: str, details: Dict = None):
"""Log message events (sent, edited, deleted)"""
log_data = {
"timestamp": datetime.now().isoformat(),
@ -37,7 +37,7 @@ class AdvancedLogger:
asyncio.create_task(self._write_log_async("messages", log_data))
def log_role_event(self, member: discord.Member, role: discord.Role, event_type: str):
def log_role_event(self, member: disnake.Member, role: disnake.Role, event_type: str):
"""Log role changes (added/removed)"""
log_data = {
"timestamp": datetime.now().isoformat(),
@ -52,7 +52,7 @@ class AdvancedLogger:
asyncio.create_task(self._write_log_async("roles", log_data))
def log_voice_event(self, member: discord.Member, channel: discord.VoiceChannel, event_type: str):
def log_voice_event(self, member: disnake.Member, channel: disnake.VoiceChannel, event_type: str):
"""Log voice channel events (join/leave)"""
log_data = {
"timestamp": datetime.now().isoformat(),
@ -66,7 +66,7 @@ class AdvancedLogger:
asyncio.create_task(self._write_log_async("voice", log_data))
def log_member_leave(self, member: discord.Member):
def log_member_leave(self, member: disnake.Member):
"""Log when a member leaves the server"""
roles_data = {
"role_ids": [role.id for role in member.roles if role.name != "@everyone"],
@ -91,7 +91,7 @@ class AdvancedLogger:
# Also save to general logs
asyncio.create_task(self._write_log_async("member_events", log_data))
def log_member_join(self, member: discord.Member):
def log_member_join(self, member: disnake.Member):
"""Log when a member joins the server"""
log_data = {
"timestamp": datetime.now().isoformat(),
@ -106,21 +106,42 @@ class AdvancedLogger:
def check_previous_roles(self, member_id: int) -> Optional[Dict]:
"""Check if member has previous role data when rejoining"""
# This one stays synchronous for now as it's called in on_member_join and needs immediate result
# but since it only reads ONE file, it's less problematic than writing to cumulative logs.
# Format update: it now needs to read NDJSON.
member_log_file = f"{self.member_logs_dir}/{member_id}.json"
if not os.path.exists(member_log_file):
return None
try:
logs = []
with open(member_log_file, 'r', encoding='utf-8') as f:
# Read line by line for NDJSON
logs = [json.loads(line) for line in f if line.strip()]
content = f.read().strip()
if not content:
return None
# Handle legacy JSON list format
if content.startswith('['):
try:
logs = json.loads(content)
except json.JSONDecodeError:
# Fallback: try to strip brackets and commas
content_clean = content.strip('[]').strip()
# This is getting complex, let's just try line-by-line fallback
pass
# If not a list or failed list parse, try NDJSON line-by-line
if not logs:
for line in content.splitlines():
line = line.strip()
if not line: continue
# Strip trailing/leading commas in case of semi-corrupt legacy conversion
line = line.strip(',')
try:
logs.append(json.loads(line))
except json.JSONDecodeError:
continue # Skip corrupt lines
# Find the most recent leave event
leave_events = [log for log in logs if log.get("event_type") == "member_leave"]
leave_events = [log for log in logs if isinstance(log, dict) and log.get("event_type") == "member_leave"]
if leave_events:
return leave_events[-1].get("roles", {})
@ -162,12 +183,12 @@ class AdvancedLogger:
except Exception as e:
print(f"CRITICAL ERROR in AdvancedLogger._write_member_log_sync: {e}")
async def send_leave_notification(self, member: discord.Member, channel: discord.TextChannel):
async def send_leave_notification(self, member: disnake.Member, channel: disnake.TextChannel):
"""Send notification when member leaves"""
embed = discord.Embed(
embed = disnake.Embed(
title="👋 Membre parti",
description=f"{member.mention} ({member.name}#{member.discriminator}) a quitté le serveur.",
color=discord.Color.red(),
description=f"{member.mention} ({member.name}) a quitté le serveur.",
color=disnake.Color.red(),
timestamp=datetime.now()
)

View file

@ -12,7 +12,7 @@ from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
import discord
import disnake
logger = logging.getLogger("kuby.json_export")
@ -160,7 +160,7 @@ def get_history() -> list:
# ─── Génération servers.json ─────────────────────────────────────────────────
async def generate_servers_json(bot: discord.Client) -> dict:
async def generate_servers_json(bot: disnake.Client) -> dict:
config = load_config()
fields = config["serverFields"]
founder_keys = [r.lower() for r in config.get("founderRoles", ["fondateur", "founder"])]
@ -249,7 +249,7 @@ async def generate_servers_json(bot: discord.Client) -> dict:
# ─── Génération members.json ─────────────────────────────────────────────────
async def generate_members_json(bot: discord.Client, guild_id: Optional[int] = None) -> dict:
async def generate_members_json(bot: disnake.Client, guild_id: Optional[int] = None) -> dict:
config = load_config()
fields = config["memberFields"]
target_id = guild_id or (int(config["targetGuildId"]) if config["targetGuildId"] else None)
@ -324,7 +324,7 @@ async def generate_members_json(bot: discord.Client, guild_id: Optional[int] = N
# ─── Génération complète ─────────────────────────────────────────────────────
async def generate_all(bot: discord.Client, trigger: str = "auto") -> dict:
async def generate_all(bot: disnake.Client, trigger: str = "auto") -> dict:
results = {"servers": None, "members": None, "errors": [], "trigger": trigger}
try:
results["servers"] = await generate_servers_json(bot)
@ -343,7 +343,7 @@ async def generate_all(bot: discord.Client, trigger: str = "auto") -> dict:
# ─── Prévisualisation (sans écriture fichier) ────────────────────────────────
async def preview_servers(bot: discord.Client, limit: int = 3) -> list:
async def preview_servers(bot: disnake.Client, limit: int = 3) -> list:
"""Retourne un aperçu de N serveurs sans écrire le fichier."""
config = load_config()
fields = config["serverFields"]
@ -366,7 +366,7 @@ async def preview_servers(bot: discord.Client, limit: int = 3) -> list:
return results
async def preview_members(bot: discord.Client, limit: int = 5) -> dict:
async def preview_members(bot: disnake.Client, limit: int = 5) -> dict:
"""Retourne un aperçu de N membres sans écrire le fichier."""
config = load_config()
target_id = int(config["targetGuildId"]) if config["targetGuildId"] else None