Migration de Kuby vers disnake

This commit is contained in:
Mathis 2026-05-06 17:07:09 +02:00
parent dc6e235f27
commit c8c579eefe
36 changed files with 1205 additions and 1253 deletions

16
bot.py
View file

@ -4,10 +4,10 @@ from dotenv import load_dotenv
# Charger les variables d'environnement AVANT tout le reste # Charger les variables d'environnement AVANT tout le reste
load_dotenv() load_dotenv()
import discord import disnake
from commandes.ticket.utils.permissions import can_access_config from commandes.ticket.utils.permissions import can_access_config
from commandes.ticket.data.storage import get_storage from commandes.ticket.data.storage import get_storage
from discord.ext import commands from disnake.ext import commands
import asyncio import asyncio
from src.logger import kuby_logger, log_performance from src.logger import kuby_logger, log_performance
import logging import logging
@ -25,12 +25,12 @@ except ValueError:
kuby_logger.error(f"❌ APPLICATION_ID invalide : {application_id_raw}") kuby_logger.error(f"❌ APPLICATION_ID invalide : {application_id_raw}")
application_id = None application_id = None
# Configuration des intents Discord # Configuration des intents Disnake
intents = discord.Intents.default() intents = disnake.Intents.default()
intents.message_content = True intents.message_content = True
intents.members = True intents.members = True
intents.voice_states = 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): class MyBot(commands.Bot):
def __init__(self): def __init__(self):
@ -135,7 +135,7 @@ class MyBot(commands.Bot):
# Réponse adaptée (Slash vs Message) # Réponse adaptée (Slash vs Message)
msg = "❌ Vous n'êtes pas autorisé à utiliser cette commande. Veuillez demander à être ajouté à la whitelist." msg = "❌ Vous n'êtes pas autorisé à utiliser cette commande. Veuillez demander à être ajouté à la whitelist."
if ctx.interaction: if ctx.interaction:
await ctx.interaction.response.send_message(msg, ephemeral=True) await ctx.send(msg, ephemeral=True)
else: else:
await ctx.send(msg) await ctx.send(msg)
return False return False
@ -147,8 +147,8 @@ class MyBot(commands.Bot):
# Sync slash commands # Sync slash commands
try: try:
await self.tree.sync() # Disnake handles syncing automatically by default
kuby_logger.info("✅ Commandes slash synchronisées avec Discord") kuby_logger.info("✅ Commandes slash prêtes avec Disnake")
except Exception as e: except Exception as e:
kuby_logger.error(f"❌ Erreur lors de la synchronisation des commandes slash : {e}", exc_info=True) kuby_logger.error(f"❌ Erreur lors de la synchronisation des commandes slash : {e}", exc_info=True)

View file

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

View file

@ -7,9 +7,8 @@ import asyncio
from datetime import datetime from datetime import datetime
from typing import Dict, Any, List from typing import Dict, Any, List
import discord import disnake
from discord.ext import commands from disnake.ext import commands
from discord import app_commands
BACKUPS_ROOT = "data/security_backups" # dossier de sortie des sauvegardes 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) 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]] = [] serialized: List[Dict[str, Any]] = []
for target, perms in channel.overwrites.items(): 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({ serialized.append({
"target_type": target_type, "target_type": target_type,
"target_id": target.id, "target_id": target.id,
@ -64,20 +63,17 @@ class Backup(commands.Cog):
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
self.bot = 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") @backup_group.sub_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)"
)
async def backup_create( async def backup_create(
self, self,
interaction: discord.Interaction, interaction: disnake.ApplicationCommandInteraction,
save_messages: bool = True, save_messages: bool = commands.Param(True, description="Inclure l'export des messages"),
privacy_notice: bool = True, privacy_notice: bool = commands.Param(True, description="Afficher un avertissement public si messages sauvegardés"),
message_limit_per_channel: int = 10000 message_limit_per_channel: int = commands.Param(10000, description="Nombre max de messages à exporter par salon (0 = tous)")
): ):
guild = interaction.guild guild = interaction.guild
assert guild is not None, "Cette commande doit être utilisée dans un serveur." assert guild is not None, "Cette commande doit être utilisée dans un serveur."
@ -93,20 +89,20 @@ class Backup(commands.Cog):
) )
return return
# Déferre l'interaction # Deferre l'interaction
await interaction.response.defer(ephemeral=True, thinking=True) await interaction.response.defer(ephemeral=True)
# Avertissement de confidentialité # Avertissement de confidentialité
if privacy_notice and save_messages: if privacy_notice and save_messages:
try: try:
await interaction.channel.send( await interaction.channel.send(
embed=discord.Embed( embed=disnake.Embed(
title="📢 Information de confidentialité (transparence)", title="📢 Information de confidentialité (transparence)",
description=( description=(
"Une **sauvegarde du serveur** est en cours et **inclus les messages**.\n" "Une **sauvegarde du serveur** est en cours et **inclus les messages**.\n"
"Vous êtes dans l'obligation de prévenir les membres." "Vous êtes dans l'obligation de prévenir les membres."
), ),
color=discord.Color.orange() color=disnake.Color.orange()
) )
) )
except Exception: except Exception:
@ -132,14 +128,14 @@ class Backup(commands.Cog):
message_limit_per_channel if message_limit_per_channel else 100000 message_limit_per_channel if message_limit_per_channel else 100000
) )
prog_embed = discord.Embed( prog_embed = disnake.Embed(
title="🗄️ Sauvegarde en cours", title="🗄️ Sauvegarde en cours",
description=( description=(
f"**Serveur :** {guild.name} (`{guild.id}`)\n" f"**Serveur :** {guild.name} (`{guild.id}`)\n"
f"**Estimation :** ~{estimation_s} secondes\n" f"**Estimation :** ~{estimation_s} secondes\n"
f"**Options** : messages={'oui' if save_messages else 'non'} • limite/chan={message_limit_per_channel or 'tous'}" 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) prog_embed.add_field(name="Progression", value=progress_bar(0.0), inline=False)
progress_msg = await interaction.followup.send(embed=prog_embed, ephemeral=False) progress_msg = await interaction.followup.send(embed=prog_embed, ephemeral=False)
@ -173,11 +169,11 @@ class Backup(commands.Cog):
chans_dump=[] chans_dump=[]
for ch in guild.channels: 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)} 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}) 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}) 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}) base.update({"nsfw":ch.nsfw,"default_auto_archive_duration":ch.default_auto_archive_duration})
chans_dump.append(base) chans_dump.append(base)
with open(os.path.join(base_dir,"channels.json"),"w",encoding="utf-8") as f: 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} 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") f.write(json.dumps(data,ensure_ascii=False)+"\n")
count+=1 count+=1
except discord.Forbidden: pass except disnake.Forbidden: pass
except Exception: pass except Exception: pass
exported_total+=count exported_total+=count
done+=per_chan_weight done+=per_chan_weight
@ -226,7 +222,7 @@ class Backup(commands.Cog):
rel=os.path.relpath(full,base_dir) rel=os.path.relpath(full,base_dir)
zf.write(full,rel) zf.write(full,rel)
except Exception as exc: 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_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}") prog_embed.set_footer(text=f"Erreur lors de la création du ZIP : {exc}")
await progress_msg.edit(embed=prog_embed) await progress_msg.edit(embed=prog_embed)
@ -234,15 +230,15 @@ class Backup(commands.Cog):
return return
# Finalisation # 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.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.add_field(name="Archive",value=f"`{zip_path}`",inline=False)
prog_embed.set_footer(text="Sauvegarde terminée ✅") prog_embed.set_footer(text="Sauvegarde terminée ✅")
await progress_msg.edit(embed=prog_embed) 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) 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") @backup_group.sub_command(name="list", description="Lister les archives de sauvegarde disponibles")
async def backup_list(self, interaction: discord.Interaction): async def backup_list(self, interaction: disnake.ApplicationCommandInteraction):
guild = interaction.guild guild = interaction.guild
assert guild is not None, "Cette commande doit être utilisée dans un serveur." assert guild is not None, "Cette commande doit être utilisée dans un serveur."
@ -256,7 +252,7 @@ class Backup(commands.Cog):
) )
return return
await interaction.response.defer(ephemeral=True, thinking=True) await interaction.response.defer(ephemeral=True)
ensure_dir(BACKUPS_ROOT) ensure_dir(BACKUPS_ROOT)
files = [f for f in os.listdir(BACKUPS_ROOT) if f.endswith(".zip")] files = [f for f in os.listdir(BACKUPS_ROOT) if f.endswith(".zip")]
files.sort(reverse=True) files.sort(reverse=True)
@ -266,7 +262,7 @@ class Backup(commands.Cog):
return return
desc="\n".join(f"• `{name}`" for name in files[:20]) 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: if len(files)>20:
embed.set_footer(text=f"... et {len(files)-20} de plus") embed.set_footer(text=f"... et {len(files)-20} de plus")
await interaction.followup.send(embed=embed,ephemeral=True) await interaction.followup.send(embed=embed,ephemeral=True)

View file

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

View file

@ -1,6 +1,5 @@
import discord import disnake
from discord.ext import commands, tasks from disnake.ext import commands, tasks
from discord import app_commands
import json import json
import os import os
import asyncio import asyncio
@ -57,8 +56,8 @@ class Blacklist(commands.Cog):
agents = load_json(AGENTS_FILE, {"agents": []}) agents = load_json(AGENTS_FILE, {"agents": []})
return str(user_id) in agents["agents"] return str(user_id) in agents["agents"]
@app_commands.command(name="blacklist", description="Blacklist un utilisateur") @commands.slash_command(name="blacklist", description="Blacklist un utilisateur")
async def blacklist(self, interaction: discord.Interaction, user: discord.User, reason: str = "Aucune raison fournie"): async def blacklist(self, interaction: disnake.ApplicationCommandInteraction, user: disnake.User, reason: str = "Aucune raison fournie"):
if not self.is_agent(interaction.user.id): if not self.is_agent(interaction.user.id):
return await interaction.response.send_message("⛔ Tu nas pas la permission.", ephemeral=True) 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) save_json(BLACKLIST_FILE, blacklist)
# Préparer l'embed pro # Préparer l'embed pro
embed = discord.Embed( embed = disnake.Embed(
title="🚫 Mise en blacklist - Réseau Omega Kube", title="🚫 Mise en blacklist - Réseau Omega Kube",
description=( description=(
f"Bonjour {user.mention},\n\n" 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" "📩 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." "⏳ 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") embed.set_footer(text="Omega Kube - Nixfix06 & Mgstudios")
@ -122,8 +121,8 @@ class Blacklist(commands.Cog):
ephemeral=True ephemeral=True
) )
@app_commands.command(name="unblacklist", description="Retirer un utilisateur de la blacklist") @commands.slash_command(name="unblacklist", description="Retirer un utilisateur de la blacklist")
async def unblacklist(self, interaction: discord.Interaction, user: discord.User): async def unblacklist(self, interaction: disnake.ApplicationCommandInteraction, user: disnake.User):
if not self.is_agent(interaction.user.id): if not self.is_agent(interaction.user.id):
return await interaction.response.send_message("⛔ Tu nas pas la permission.", ephemeral=True) 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) save_json(BLACKLIST_FILE, blacklist)
@commands.Cog.listener() @commands.Cog.listener()
async def on_message(self, message: discord.Message): async def on_message(self, message: disnake.Message):
if message.author.bot: if message.author.bot:
return return
tickets = load_json(TICKETS_FILE, {}) tickets = load_json(TICKETS_FILE, {})
if isinstance(message.channel, discord.DMChannel): if isinstance(message.channel, disnake.DMChannel):
user_id = str(message.author.id) user_id = str(message.author.id)
if user_id in tickets: if user_id in tickets:
guild = self.bot.get_guild(MAIN_GUILD_ID) guild = self.bot.get_guild(MAIN_GUILD_ID)
ticket_channel = guild.get_channel(tickets[user_id]["channel_id"]) ticket_channel = guild.get_channel(tickets[user_id]["channel_id"])
if ticket_channel: if ticket_channel:
embed = discord.Embed( embed = disnake.Embed(
description=message.content, description=message.content,
color=discord.Color.blue() color=disnake.Color.blue()
).set_author(name=message.author, icon_url=message.author.display_avatar.url) ).set_author(name=message.author, icon_url=message.author.display_avatar.url)
await ticket_channel.send(embed=embed) 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): if message.channel.id == data["channel_id"] and self.is_agent(message.author.id):
user = await self.bot.fetch_user(int(user_id)) user = await self.bot.fetch_user(int(user_id))
try: try:
embed = discord.Embed( embed = disnake.Embed(
description=message.content, 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) ).set_author(name=f"Agent {message.author}", icon_url=message.author.display_avatar.url)
await user.send(embed=embed) await user.send(embed=embed)
except: except:
pass pass
# --------- Boutons --------- # --------- Boutons ---------
class TicketButton(discord.ui.View): class TicketButton(disnake.ui.View):
def __init__(self, user_id: int): def __init__(self, user_id: int):
super().__init__(timeout=None) super().__init__(timeout=None)
self.user_id = user_id self.user_id = user_id
@discord.ui.button(label="📩 Ouvrir une demande de révision", style=discord.ButtonStyle.primary, custom_id="blacklist_open_ticket") @disnake.ui.button(label="📩 Ouvrir une demande de révision", style=disnake.ButtonStyle.primary, custom_id="blacklist_open_ticket")
async def open_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): async def open_ticket(self, interaction: disnake.Interaction, button: disnake.ui.Button):
if interaction.user.id != self.user_id: if interaction.user.id != self.user_id:
return await interaction.response.send_message("❌ Ce bouton ne tappartient pas.", ephemeral=True) 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: if str(self.user_id) in tickets:
return await interaction.response.send_message("❌ Tu as déjà un ticket ouvert.", ephemeral=True) 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) category = guild.get_channel(TICKET_CATEGORY_ID)
ticket_channel = await guild.create_text_channel( ticket_channel = await guild.create_text_channel(
@ -222,10 +221,10 @@ class TicketButton(discord.ui.View):
save_json(TICKETS_FILE, tickets) save_json(TICKETS_FILE, tickets)
view_close = CloseTicketView(self.user_id) view_close = CloseTicketView(self.user_id)
embed = discord.Embed( embed = disnake.Embed(
title="📩 Ticket ouvert", 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.", 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) await ticket_channel.send(embed=embed, view=view_close)
@ -234,27 +233,27 @@ class TicketButton(discord.ui.View):
button.disabled = True button.disabled = True
self.stop() self.stop()
class CloseTicketView(discord.ui.View): class CloseTicketView(disnake.ui.View):
def __init__(self, user_id: int): def __init__(self, user_id: int):
super().__init__(timeout=None) super().__init__(timeout=None)
self.user_id = user_id self.user_id = user_id
@discord.ui.button(label="❌ Fermer le ticket", style=discord.ButtonStyle.danger, custom_id="blacklist_close_ticket") @disnake.ui.button(label="❌ Fermer le ticket", style=disnake.ButtonStyle.danger, custom_id="blacklist_close_ticket")
async def close_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): async def close_ticket(self, interaction: disnake.Interaction, button: disnake.ui.Button):
if not interaction.client.get_cog("Blacklist").is_agent(interaction.user.id): 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) return await interaction.response.send_message("⛔ Seuls les agents peuvent fermer ce ticket.", ephemeral=True)
view = ConfirmCloseView(self.user_id) view = ConfirmCloseView(self.user_id)
await interaction.response.send_message("⚠️ Veux-tu vraiment fermer ce ticket ?", view=view, ephemeral=True) 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): def __init__(self, user_id: int):
super().__init__(timeout=30) super().__init__(timeout=30)
self.user_id = user_id self.user_id = user_id
@discord.ui.button(label="✅ Oui", style=discord.ButtonStyle.success, custom_id="blacklist_confirm_close") @disnake.ui.button(label="✅ Oui", style=disnake.ButtonStyle.success, custom_id="blacklist_confirm_close")
async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button): async def confirm(self, interaction: disnake.Interaction, button: disnake.ui.Button):
if not interaction.client.get_cog("Blacklist").is_agent(interaction.user.id): 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) return await interaction.response.send_message("⛔ Tu nas pas la permission.", ephemeral=True)
tickets = load_json(TICKETS_FILE, {}) tickets = load_json(TICKETS_FILE, {})
@ -268,19 +267,19 @@ class ConfirmCloseView(discord.ui.View):
del tickets[str(self.user_id)] del tickets[str(self.user_id)]
save_json(TICKETS_FILE, tickets) 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: try:
embed = discord.Embed( embed = disnake.Embed(
title="🔒 Ticket fermé", 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.", 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 user.send(embed=embed)
await asyncio.sleep(2) await asyncio.sleep(2)
except: except:
pass pass
for guild in interaction.client.guilds: for guild in interaction.bot.guilds:
member = guild.get_member(self.user_id) member = guild.get_member(self.user_id)
if member: if member:
try: try:
@ -290,8 +289,8 @@ class ConfirmCloseView(discord.ui.View):
await interaction.response.send_message("✅ Ticket fermé avec succès.", ephemeral=True) 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") @disnake.ui.button(label="❌ Non", style=disnake.ButtonStyle.secondary, custom_id="blacklist_cancel_close")
async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button): async def cancel(self, interaction: disnake.Interaction, button: disnake.ui.Button):
await interaction.response.send_message("❌ Fermeture annulée.", ephemeral=True) await interaction.response.send_message("❌ Fermeture annulée.", ephemeral=True)
# --------- Setup --------- # --------- Setup ---------

View file

@ -1,6 +1,5 @@
import discord import disnake
from discord import app_commands from disnake.ext import commands
from discord.ext import commands
from utils.gitlab_client import gitlab_client from utils.gitlab_client import gitlab_client
from aiohttp import web from aiohttp import web
import logging import logging
@ -33,27 +32,27 @@ def save_report(issue_iid, user_id):
except Exception as e: except Exception as e:
kuby_logger.error(f"Error saving report to JSON: {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): def __init__(self, priority_choice):
super().__init__() super().__init__(title="Signaler un Bug")
self.priority_choice = priority_choice self.priority_choice = priority_choice
bug_title = discord.ui.TextInput( bug_title = disnake.ui.TextInput(
label="Titre du bug", label="Titre du bug",
placeholder="Décrivez brièvement le problème...", placeholder="Décrivez brièvement le problème...",
required=True, required=True,
max_length=100 max_length=100
) )
description = discord.ui.TextInput( description = disnake.ui.TextInput(
label="Description détaillée", label="Description détaillée",
style=discord.TextStyle.paragraph, style=disnake.TextStyle.paragraph,
placeholder="Que s'est-il passé ? Comment reproduire le bug ?", placeholder="Que s'est-il passé ? Comment reproduire le bug ?",
required=True, required=True,
max_length=1000 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) await interaction.response.send_message("Envoi de votre rapport de bug à GitLab...", ephemeral=True)
description_text = f"**Rapporté par:** {interaction.user} ({interaction.user.id})\n" description_text = f"**Rapporté par:** {interaction.user} ({interaction.user.id})\n"
@ -76,23 +75,26 @@ class BugReportModal(discord.ui.Modal, title="Signaler un Bug"):
else: else:
await interaction.edit_original_response(content="❌ Une erreur est survenue lors de l'envoi du rapport à GitLab. Veuillez contacter un administrateur.") 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é"): class FeatureSuggestionModal(disnake.ui.Modal):
suggestion_title = discord.ui.TextInput( def __init__(self):
super().__init__(title="Suggérer une fonctionnalité")
suggestion_title = disnake.ui.TextInput(
label="Titre de la suggestion", label="Titre de la suggestion",
placeholder="Que voulez-vous ajouter ?", placeholder="Que voulez-vous ajouter ?",
required=True, required=True,
max_length=100 max_length=100
) )
description = discord.ui.TextInput( description = disnake.ui.TextInput(
label="Détails de la fonctionnalité", label="Détails de la fonctionnalité",
style=discord.TextStyle.paragraph, style=disnake.TextStyle.paragraph,
placeholder="Décrivez comment cela devrait fonctionner...", placeholder="Décrivez comment cela devrait fonctionner...",
required=True, required=True,
max_length=1000 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) await interaction.response.send_message("Envoi de votre suggestion à GitLab...", ephemeral=True)
description_text = f"**Suggéré par:** {interaction.user} ({interaction.user.id})\n\n" description_text = f"**Suggéré par:** {interaction.user} ({interaction.user.id})\n\n"
@ -231,10 +233,10 @@ class BugReport(commands.Cog):
try: try:
# Création de l'embed pour la mise à jour de statut # Création de l'embed pour la mise à jour de statut
embed = discord.Embed( embed = disnake.Embed(
title="🛠️ Mise à jour de votre signalement !", title="🛠️ Mise à jour de votre signalement !",
description=f"Le statut de votre rapport **{issue_title}** a été mis à jour par l'équipe.", description=f"Le statut de votre rapport **{issue_title}** a été mis à jour par l'équipe.",
color=discord.Color.blue() color=disnake.Color.blue()
) )
embed.add_field(name="Nouveaux Labels / Statuts", value=current_labels_str) embed.add_field(name="Nouveaux Labels / Statuts", value=current_labels_str)
@ -244,7 +246,7 @@ class BugReport(commands.Cog):
kuby_logger.info(f"Notification de label envoyée à {user} ({user.id}) pour l'issue #{issue_iid}") kuby_logger.info(f"Notification de label envoyée à {user} ({user.id}) pour l'issue #{issue_iid}")
if dev and dev.id != user.id: 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}).") 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: except disnake.Forbidden:
kuby_logger.warning(f"Impossible d'envoyer un MP à {user} ({user.id}) - MPs désactivés.") kuby_logger.warning(f"Impossible d'envoyer un MP à {user} ({user.id}) - MPs désactivés.")
if dev and dev.id != user.id: 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}.") await dev.send(f"⚠️ [DM BLOQUÉ] {user} ({user.id}) a ses MPs fermés. Impossible de notifier pour l'issue #{issue_iid}.")
@ -259,10 +261,10 @@ class BugReport(commands.Cog):
if is_closing_now: if is_closing_now:
try: try:
embed = discord.Embed( embed = disnake.Embed(
title="🛠️ Bug / Suggestion Terminé(e) !", title="🛠️ Bug / Suggestion Terminé(e) !",
description=f"Bonne nouvelle ! Votre signalement **{issue_title}** a été marqué comme terminé ou résolu.", description=f"Bonne nouvelle ! Votre signalement **{issue_title}** a été marqué comme terminé ou résolu.",
color=discord.Color.green() color=disnake.Color.green()
) )
embed.add_field(name="Prochaine étape", value="La mise à jour arrive prochainement (ou est déjà là) !") 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 !") embed.set_footer(text="Merci pour votre aide !")
@ -272,7 +274,7 @@ class BugReport(commands.Cog):
kuby_logger.info(f"Notification de clôture envoyée à {user} ({user.id}) pour l'issue #{issue_iid}") 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: 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}.") 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: except disnake.Forbidden:
kuby_logger.warning(f"Impossible d'envoyer le MP de clôture à {user} ({user.id}).") kuby_logger.warning(f"Impossible d'envoyer le MP de clôture à {user} ({user.id}).")
if dev and dev.id != 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).") await dev.send(f"⚠️ [DM BLOQUÉ] Impossible d'annoncer la clôture à {user} ({user.id}) pour l'issue #{issue_iid} (DMs désactivés).")
@ -290,10 +292,10 @@ class BugReport(commands.Cog):
author_name = payload.get("user", {}).get("name", "Développeur") author_name = payload.get("user", {}).get("name", "Développeur")
try: try:
embed = discord.Embed( embed = disnake.Embed(
title="💬 Nouveau commentaire sur votre signalement", title="💬 Nouveau commentaire sur votre signalement",
description=f"Le développeur a répondu à votre rapport **{issue_title}** :\n\n>>> {note_body}", description=f"Le développeur a répondu à votre rapport **{issue_title}** :\n\n>>> {note_body}",
color=discord.Color.orange() color=disnake.Color.orange()
) )
embed.set_footer(text=f"Par {author_name}") embed.set_footer(text=f"Par {author_name}")
@ -302,7 +304,7 @@ class BugReport(commands.Cog):
kuby_logger.info(f"Commentaire GitLab envoyé à {user} ({user.id}) pour l'issue #{issue_iid}") kuby_logger.info(f"Commentaire GitLab envoyé à {user} ({user.id}) pour l'issue #{issue_iid}")
if dev and dev.id != user.id: 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}.") await dev.send(f"✅ [SUIVI] L'utilisateur {user} ({user.id}) a bien reçu votre commentaire pour l'issue #{issue_iid}.")
except discord.Forbidden: except disnake.Forbidden:
kuby_logger.warning(f"Impossible d'envoyer le commentaire en MP à {user} ({user.id}).") kuby_logger.warning(f"Impossible d'envoyer le commentaire en MP à {user} ({user.id}).")
if dev and dev.id != 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).") await dev.send(f"⚠️ [DM BLOQUÉ] Impossible d'envoyer votre commentaire à {user} ({user.id}) pour l'issue #{issue_iid} (DMs désactivés).")
@ -317,18 +319,19 @@ class BugReport(commands.Cog):
kuby_logger.error(f"Error handling bot event: {e}") kuby_logger.error(f"Error handling bot event: {e}")
return web.json_response({"status": "error", "message": str(e)}, status=500) return web.json_response({"status": "error", "message": str(e)}, status=500)
@app_commands.command(name="signaler_bug", description="Signaler un bug aux développeurs") @commands.slash_command(name="signaler_bug", description="Signaler un bug aux développeurs")
@app_commands.choices(priority=[ async def signaler_bug(self, interaction: disnake.ApplicationCommandInteraction,
app_commands.Choice(name="Basse", value="Low"), priority: str = commands.Param(description="Priorité du bug", choices={"Basse": "Low", "Normale": "Normal", "Haute": "High", "Urgente": "Urgent"})):
app_commands.Choice(name="Normale", value="Normal"), # Simulate app_commands.Choice behavior for existing code
app_commands.Choice(name="Haute", value="High"), from collections import namedtuple
app_commands.Choice(name="Urgente", value="Urgent"), Choice = namedtuple('Choice', ['name', 'value'])
]) # find the name from the value
async def signaler_bug(self, interaction: discord.Interaction, priority: app_commands.Choice[str]): p_name = [k for k, v in {"Basse": "Low", "Normale": "Normal", "Haute": "High", "Urgente": "Urgent"}.items() if v == priority][0]
await interaction.response.send_modal(BugReportModal(priority)) 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") @commands.slash_command(name="suggerer_fonctionnalite", description="Proposer une nouvelle fonctionnalité pour le bot")
async def suggerer_fonctionnalite(self, interaction: discord.Interaction): async def suggerer_fonctionnalite(self, interaction: disnake.ApplicationCommandInteraction):
await interaction.response.send_modal(FeatureSuggestionModal()) await interaction.response.send_modal(FeatureSuggestionModal())
async def setup(bot): async def setup(bot):

View file

@ -1,6 +1,5 @@
import discord import disnake
from discord.ext import commands from disnake.ext import commands
from discord import app_commands
import json import json
import os import os
from typing import List, Optional, Dict from typing import List, Optional, Dict
@ -52,20 +51,17 @@ class Convocation(commands.Cog):
del self.backups[key] del self.backups[key]
save_json(CONV_BACKUPS_FILE, self.backups) 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) @commands.has_permissions(manage_roles=True)
async def convocation_group(self, ctx): async def convocation_group(self, interaction: disnake.ApplicationCommandInteraction):
if ctx.invoked_subcommand is None: pass
await ctx.send_help(ctx.command)
@convocation_group.command(name="convoquer", description="Convoque un membre et verrouille ses accès") @convocation_group.sub_command(name="convoquer", description="Convoque un membre et verrouille ses accès")
@app_commands.describe( async def convoquer(self, interaction: disnake.ApplicationCommandInteraction,
membre="Le membre à convoquer", membre: disnake.Member = commands.Param(description="Le membre à convoquer"),
raison="La raison de la convocation", raison: str = commands.Param(description="La raison de la convocation"),
heure="L'heure de la convocation (ex: 14:30)" heure: str = commands.Param(description="L'heure de la convocation (ex: 14:30)")):
) guild_id = interaction.guild.id
async def convoquer(self, ctx, membre: discord.Member, raison: str, heure: str):
guild_id = ctx.guild.id
settings = self.get_guild_settings(guild_id) settings = self.get_guild_settings(guild_id)
role_conv_id = settings.get("role_id") role_conv_id = settings.get("role_id")
log_channel_id = settings.get("log_channel_id") log_channel_id = settings.get("log_channel_id")
@ -73,30 +69,30 @@ class Convocation(commands.Cog):
admin_ids = settings.get("admin_ids", []) admin_ids = settings.get("admin_ids", [])
if not role_conv_id: 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 return
await ctx.defer(ephemeral=True) await interaction.response.defer(ephemeral=True)
try: try:
# --- BACKUP --- # --- BACKUP ---
bot_top_role = ctx.guild.me.top_role bot_top_role = interaction.guild.me.top_role
backup_data = { 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], "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": {} "overrides": {}
} }
# 1. Backup and Remove current roles # 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: if current_roles:
try: try:
await membre.remove_roles(*current_roles, reason=f"Convocation par {ctx.author}") await membre.remove_roles(*current_roles, reason=f"Convocation par {interaction.author}")
except discord.Forbidden: except disnake.Forbidden:
await ctx.send("⚠️ Je n'ai pas pu retirer certains rôles (Permission refusée).", ephemeral=True) 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 # 2. Dynamic Lockdown: Loop through ALL channels
lockdown_count = 0 lockdown_count = 0
for channel in ctx.guild.channels: for channel in interaction.guild.channels:
# On ignore le salon d'attente et ses parents s'il y en a # 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)): if attente_id and (channel.id == attente_id or (hasattr(channel, 'category') and channel.category and channel.category.id == attente_id)):
continue continue
@ -112,28 +108,28 @@ class Convocation(commands.Cog):
# Specific override for this member: View Channel = False # Specific override for this member: View Channel = False
await channel.set_permissions(membre, view_channel=False, reason="Convocation Lockdown") await channel.set_permissions(membre, view_channel=False, reason="Convocation Lockdown")
lockdown_count += 1 lockdown_count += 1
except discord.Forbidden: except disnake.Forbidden:
pass # Skip if we can't touch this channel pass # Skip if we can't touch this channel
self.save_backup(guild_id, membre.id, backup_data) self.save_backup(guild_id, membre.id, backup_data)
# 3. Add Convocation role # 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:
if role_conv.position >= bot_top_role.position: 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: else:
try: try:
await membre.add_roles(role_conv, reason="Début de convocation") await membre.add_roles(role_conv, reason="Début de convocation")
except discord.Forbidden: except disnake.Forbidden:
await ctx.send(f"⚠️ Impossible de donner le rôle {role_conv.name} (Permission refusée).", ephemeral=True) 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) # 4. Embeds & Logs (Inchangés mais inclus pour la cohérence)
embed_conv = discord.Embed( embed_conv = disnake.Embed(
title="CONVOCATION | 🔖", 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, 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="**Raison de la convocation :**", value=f"{raison}", inline=False)
embed_conv.add_field(name="**Heure de la convocation :**", value=f"{heure}", inline=False) embed_conv.add_field(name="**Heure de la convocation :**", value=f"{heure}", inline=False)
@ -141,15 +137,15 @@ class Convocation(commands.Cog):
"> Merci de vous présenter dans le vocal attente **5 minutes** avant l'heure.\n\n" "> 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." "> La **levée** de suspension se fera uniquement à la **fin** de la convocation."
), inline=False) ), 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", title="📋 CONVOCATION ENVOYÉE",
color=0xff9900, 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="👤 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="📄 Raison", value=raison, inline=False)
embed_log.add_field(name="🕒 Heure", value=heure, inline=False) embed_log.add_field(name="🕒 Heure", value=heure, inline=False)
@ -158,7 +154,7 @@ class Convocation(commands.Cog):
try: try:
await membre.send(embed=embed_conv) await membre.send(embed=embed_conv)
dm_sent = True dm_sent = True
except discord.Forbidden: except disnake.Forbidden:
dm_sent = False dm_sent = False
# Admin & Channel Logs # Admin & Channel Logs
@ -169,47 +165,47 @@ class Convocation(commands.Cog):
except: pass except: pass
if log_channel_id: 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) 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) ("" if dm_sent else "(⚠️ DM impossible)"), ephemeral=True)
except Exception as e: except Exception as e:
import traceback import traceback
traceback.print_exc() 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") @convocation_group.sub_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, interaction: disnake.ApplicationCommandInteraction,
async def lever(self, ctx, membre: discord.Member): membre: disnake.Member = commands.Param(description="Le membre à libérer")):
guild_id = ctx.guild.id guild_id = interaction.guild.id
settings = self.get_guild_settings(guild_id) settings = self.get_guild_settings(guild_id)
role_conv_id = settings.get("role_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) backup_data = self.get_backup(guild_id, membre.id)
if not backup_data: 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 return
# 1. Remove Convocation role # 1. Remove Convocation role
if role_conv_id: if role_conv_id:
role_conv = ctx.guild.get_role(role_conv_id) role_conv = interaction.guild.get_role(role_conv_id)
if role_conv and role_conv in membre.roles: if role_conv and role_conv in membre.roles:
await membre.remove_roles(role_conv, reason="Fin de convocation") await membre.remove_roles(role_conv, reason="Fin de convocation")
# 2. Restore channel overrides # 2. Restore channel overrides
restore_count = 0 restore_count = 0
overrides = backup_data.get("overrides", {}) overrides = backup_data.get("overrides", {})
for channel in ctx.guild.channels: for channel in interaction.guild.channels:
chan_id_str = str(channel.id) chan_id_str = str(channel.id)
try: try:
if chan_id_str in overrides: if chan_id_str in overrides:
# Restore previous override # Restore previous override
ov_data = overrides[chan_id_str] ov_data = overrides[chan_id_str]
overwrite = discord.PermissionOverwrite(**ov_data) overwrite = disnake.PermissionOverwrite(**ov_data)
await channel.set_permissions(membre, overwrite=overwrite, reason="Restauration fin de convocation") await channel.set_permissions(membre, overwrite=overwrite, reason="Restauration fin de convocation")
restore_count += 1 restore_count += 1
else: else:
@ -217,7 +213,7 @@ class Convocation(commands.Cog):
# On ne vide que si un override membre existait (sinon on risque de supprimer des perms rôles ?) # 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. # 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") await channel.set_permissions(membre, overwrite=None, reason="Fin de convocation - Cleanup")
except discord.Forbidden: except disnake.Forbidden:
pass pass
# 3. Restore roles # 3. Restore roles
@ -225,8 +221,8 @@ class Convocation(commands.Cog):
restored_roles = [] restored_roles = []
if role_ids: if role_ids:
for rid in role_ids: for rid in role_ids:
role = ctx.guild.get_role(rid) role = interaction.guild.get_role(rid)
if role and role.position < ctx.guild.me.top_role.position: if role and role.position < interaction.guild.me.top_role.position:
restored_roles.append(role) restored_roles.append(role)
if restored_roles: if restored_roles:
@ -235,27 +231,24 @@ class Convocation(commands.Cog):
self.clear_backup(guild_id, membre.id) self.clear_backup(guild_id, membre.id)
# 4. Notification # 4. Notification
embed = discord.Embed( embed = disnake.Embed(
title="🔓 CONVOCATION TERMINÉE", title="🔓 CONVOCATION TERMINÉE",
description=f"Votre convocation sur **{ctx.guild.name}** est terminée. Vos accès ont été restaurés.", description=f"Votre convocation sur **{interaction.guild.name}** est terminée. Vos accès ont été restaurés.",
color=discord.Color.green() color=disnake.Color.green()
) )
try: try:
await membre.send(embed=embed) await membre.send(embed=embed)
except: pass 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") @convocation_group.sub_command(name="config", description="Configure le système de convocation")
@app_commands.describe( async def config(self, interaction: disnake.ApplicationCommandInteraction,
role="Le rôle de convocation (Muet/Convocation)", role: disnake.Role = commands.Param(description="Le rôle de convocation (Muet/Convocation)"),
channel="Le salon de logs", channel: disnake.TextChannel = commands.Param(None, description="Le salon de logs"),
attente="Salon (ou catégorie) d'attente à laisser visible", attente: disnake.abc.GuildChannel = commands.Param(None, description="Salon (ou catégorie) d'attente à laisser visible"),
admins="Liste d'IDs d'admins à notifier (séparés par des espaces)" admins: str = commands.Param(None, description="Liste d'IDs d'admins à notifier (séparés par des espaces)")):
) guild_id = interaction.guild.id
@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
settings = self.get_guild_settings(guild_id) settings = self.get_guild_settings(guild_id)
settings["role_id"] = role.id settings["role_id"] = role.id
@ -273,13 +266,13 @@ class Convocation(commands.Cog):
self.save_guild_settings(guild_id, settings) 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="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 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="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", [])))) 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): async def setup(bot):
await bot.add_cog(Convocation(bot)) await bot.add_cog(Convocation(bot))

View file

@ -1,6 +1,5 @@
import discord import disnake
from discord.ext import commands from disnake.ext import commands
from discord import app_commands
import sqlite3 import sqlite3
import aiohttp import aiohttp
import io import io
@ -148,14 +147,14 @@ class Goodbye(commands.Cog):
# Utiliser le message personnalisé ou un par défaut # 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>" 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.", embed = disnake.Embed(title=f"{member} viens de nous quitter.",
description=description, description=description,
color=discord.Color.dark_magenta()) color=disnake.Color.dark_magenta())
embed.set_footer(text=f"Avait rejoint {joined_message}") embed.set_footer(text=f"Avait rejoint {joined_message}")
embed.set_image(url="attachment://goodbye.png") embed.set_image(url="attachment://goodbye.png")
await channel.send(embed=embed, file=discord.File(img, filename="goodbye.png")) await channel.send(embed=embed, 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""" """Sauvegarde l'attachment et retourne le chemin du fichier"""
base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "banners")) base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "banners"))
os.makedirs(base_dir, exist_ok=True) os.makedirs(base_dir, exist_ok=True)
@ -187,9 +186,9 @@ class Goodbye(commands.Cog):
return os.path.basename(banner_path) return os.path.basename(banner_path)
@app_commands.command(name="setgoodbye", description="Définit le salon d'aurevoir et les paramètres personnalisés") @commands.slash_command(name="setgoodbye", description="Définit le salon d'aurevoir et les paramètres personnalisés")
@app_commands.checks.has_permissions(administrator=True) @commands.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): 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 banner_path = None
# Si un fichier est uploadé, le sauvegarder # Si un fichier est uploadé, le sauvegarder
@ -208,7 +207,7 @@ class Goodbye(commands.Cog):
goodbye_server_name=COALESCE(excluded.goodbye_server_name, goodbye_server_name), goodbye_server_name=COALESCE(excluded.goodbye_server_name, goodbye_server_name),
goodbye_message=COALESCE(excluded.goodbye_message, goodbye_message), goodbye_message=COALESCE(excluded.goodbye_message, goodbye_message),
goodbye_banner_background=COALESCE(excluded.goodbye_banner_background, goodbye_banner_background)''', 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.commit()
conn.close() conn.close()

View file

@ -1,18 +1,17 @@
import discord import disnake
from discord.ext import commands from disnake.ext import commands
from discord import app_commands
import sqlite3 import sqlite3
import os import os
from datetime import datetime, timedelta from datetime import datetime, timedelta
import re import re
class ModReasonModal(discord.ui.Modal): class ModReasonModal(disnake.ui.Modal):
def __init__(self, parent_view, action_type): def __init__(self, parent_view, action_type):
super().__init__(title=f"Raison pour {action_type}") super().__init__(title=f"Raison pour {action_type}")
self.parent_view = parent_view self.parent_view = parent_view
self.action_type = action_type self.action_type = action_type
self.reason = discord.ui.TextInput( self.reason = disnake.ui.TextInput(
label="Raison", label="Raison",
placeholder="Entrez la raison ici...", placeholder="Entrez la raison ici...",
required=True, required=True,
@ -21,7 +20,7 @@ class ModReasonModal(discord.ui.Modal):
self.add_item(self.reason) self.add_item(self.reason)
if action_type == "TIMEOUT": if action_type == "TIMEOUT":
self.duration = discord.ui.TextInput( self.duration = disnake.ui.TextInput(
label="Durée (ex: 1h, 30m, 1d)", label="Durée (ex: 1h, 30m, 1d)",
placeholder="1h", placeholder="1h",
required=True, required=True,
@ -29,11 +28,11 @@ class ModReasonModal(discord.ui.Modal):
) )
self.add_item(self.duration) self.add_item(self.duration)
async def on_submit(self, interaction: discord.Interaction): async def callback(self, interaction: disnake.Interaction):
reason = self.reason.value reason = self.reason.value
member = self.parent_view.member member = self.parent_view.member
cog = interaction.client.get_cog("Moderation") cog = interaction.bot.get_cog("Moderation")
if not cog: if not cog:
return await interaction.response.send_message("❌ Erreur interne.", ephemeral=True) return await interaction.response.send_message("❌ Erreur interne.", ephemeral=True)
@ -48,24 +47,24 @@ class ModReasonModal(discord.ui.Modal):
await self.parent_view.update_panel(interaction) await self.parent_view.update_panel(interaction)
class ModPanelView(discord.ui.View): class ModPanelView(disnake.ui.View):
def __init__(self, member, moderator, db_path): def __init__(self, member, moderator, db_path):
super().__init__(timeout=120) super().__init__(timeout=120)
self.member = member self.member = member
self.moderator = moderator self.moderator = moderator
self.db_path = db_path self.db_path = db_path
async def update_panel(self, interaction: discord.Interaction): async def update_panel(self, interaction: disnake.Interaction):
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() 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)) 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] warn_count = cursor.fetchone()[0]
conn.close() conn.close()
embed = discord.Embed( embed = disnake.Embed(
title=f"🛡️ Panel de Modération - {self.member.name}", title=f"🛡️ Panel de Modération - {self.member.name}",
description=f"Gestion de l'utilisateur {self.member.mention}", description=f"Gestion de l'utilisateur {self.member.mention}",
color=discord.Color.blue(), color=disnake.Color.blue(),
timestamp=datetime.now() timestamp=datetime.now()
) )
embed.set_thumbnail(url=self.member.display_avatar.url) embed.set_thumbnail(url=self.member.display_avatar.url)
@ -78,38 +77,38 @@ class ModPanelView(discord.ui.View):
else: else:
await interaction.edit_original_response(embed=embed, view=self) await interaction.edit_original_response(embed=embed, view=self)
@discord.ui.button(label="Avertir", style=discord.ButtonStyle.secondary, emoji="⚠️") @disnake.ui.button(label="Avertir", style=disnake.ButtonStyle.secondary, emoji="⚠️")
async def warn_btn(self, interaction: discord.Interaction, button: discord.ui.Button): async def warn_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button):
await interaction.response.send_modal(ModReasonModal(self, "WARN")) await interaction.response.send_modal(ModReasonModal(self, "WARN"))
@discord.ui.button(label="Timeout", style=discord.ButtonStyle.secondary, emoji="") @disnake.ui.button(label="Timeout", style=disnake.ButtonStyle.secondary, emoji="")
async def timeout_btn(self, interaction: discord.Interaction, button: discord.ui.Button): async def timeout_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button):
await interaction.response.send_modal(ModReasonModal(self, "TIMEOUT")) await interaction.response.send_modal(ModReasonModal(self, "TIMEOUT"))
@discord.ui.button(label="Expulser", style=discord.ButtonStyle.secondary, emoji="👢") @disnake.ui.button(label="Expulser", style=disnake.ButtonStyle.secondary, emoji="👢")
async def kick_btn(self, interaction: discord.Interaction, button: discord.ui.Button): async def kick_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button):
await interaction.response.send_modal(ModReasonModal(self, "KICK")) await interaction.response.send_modal(ModReasonModal(self, "KICK"))
@discord.ui.button(label="Bannir", style=discord.ButtonStyle.danger, emoji="🔨") @disnake.ui.button(label="Bannir", style=disnake.ButtonStyle.danger, emoji="🔨")
async def ban_btn(self, interaction: discord.Interaction, button: discord.ui.Button): async def ban_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button):
await interaction.response.send_modal(ModReasonModal(self, "BAN")) await interaction.response.send_modal(ModReasonModal(self, "BAN"))
@discord.ui.button(label="Historique", style=discord.ButtonStyle.primary, emoji="📜") @disnake.ui.button(label="Historique", style=disnake.ButtonStyle.primary, emoji="📜")
async def history_btn(self, interaction: discord.Interaction, button: discord.ui.Button): async def history_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button):
cog = interaction.client.get_cog("Moderation") cog = interaction.bot.get_cog("Moderation")
await cog.history(interaction, self.member) await cog.history(interaction, self.member)
@discord.ui.button(label="Clear Warns", style=discord.ButtonStyle.danger, emoji="🧹") @disnake.ui.button(label="Clear Warns", style=disnake.ButtonStyle.danger, emoji="🧹")
async def clear_btn(self, interaction: discord.Interaction, button: discord.ui.Button): async def clear_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button):
cog = interaction.client.get_cog("Moderation") cog = interaction.bot.get_cog("Moderation")
await cog.clearwarns(interaction, self.member, reason="Nettoyage via Panel") await cog.clearwarns(interaction, self.member, reason="Nettoyage via Panel")
await self.update_panel(interaction) await self.update_panel(interaction)
class ModConfigModal(discord.ui.Modal): class ModConfigModal(disnake.ui.Modal):
def __init__(self, field_name, current_value): def __init__(self, field_name, current_value):
super().__init__(title=f"Modifier {field_name}") super().__init__(title=f"Modifier {field_name}")
self.field_name = field_name self.field_name = field_name
self.input = discord.ui.TextInput( self.input = disnake.ui.TextInput(
label=field_name, label=field_name,
default=str(current_value), default=str(current_value),
placeholder="Entrez une valeur numérique...", placeholder="Entrez une valeur numérique...",
@ -117,9 +116,9 @@ class ModConfigModal(discord.ui.Modal):
) )
self.add_item(self.input) self.add_item(self.input)
async def on_submit(self, interaction: discord.Interaction): async def callback(self, interaction: disnake.Interaction):
value = self.input.value value = self.input.value
cog = interaction.client.get_cog("Moderation") cog = interaction.bot.get_cog("Moderation")
mapping = { mapping = {
"Limite Timeout": "warn_limit_timeout", "Limite Timeout": "warn_limit_timeout",
@ -140,21 +139,21 @@ class ModConfigModal(discord.ui.Modal):
await interaction.response.send_message(f"{self.field_name} mis à jour sur **{value}**.", ephemeral=True) await interaction.response.send_message(f"{self.field_name} mis à jour sur **{value}**.", ephemeral=True)
class ModConfigView(discord.ui.View): class ModConfigView(disnake.ui.View):
def __init__(self, guild_id, db_path): def __init__(self, guild_id, db_path):
super().__init__(timeout=60) super().__init__(timeout=60)
self.guild_id = guild_id self.guild_id = guild_id
self.db_path = db_path self.db_path = db_path
@discord.ui.button(label="Log Channel", style=discord.ButtonStyle.primary, emoji="📁") @disnake.ui.button(label="Log Channel", style=disnake.ButtonStyle.primary, emoji="📁")
async def log_btn(self, interaction: discord.Interaction, button: discord.ui.Button): async def log_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button):
view = discord.ui.View() view = disnake.ui.View()
select = discord.ui.ChannelSelect( select = disnake.ui.ChannelSelect(
placeholder="Sélectionnez le salon de logs...", placeholder="Sélectionnez le salon de logs...",
channel_types=[discord.ChannelType.text] channel_types=[disnake.ChannelType.text]
) )
async def select_callback(interaction: discord.Interaction): async def select_callback(interaction: disnake.Interaction):
channel = select.values[0] channel = select.values[0]
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
@ -166,29 +165,29 @@ class ModConfigView(discord.ui.View):
view.add_item(select) view.add_item(select)
await interaction.response.send_message("Choisissez le salon de logs :", view=view, ephemeral=True) 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="⚖️") @disnake.ui.button(label="Paliers Warns", style=disnake.ButtonStyle.secondary, emoji="⚖️")
async def limits_btn(self, interaction: discord.Interaction, button: discord.ui.Button): async def limits_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button):
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT warn_limit_timeout, warn_limit_kick, warn_limit_ban FROM mod_config WHERE guild_id = ?", (self.guild_id,)) 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() res = cursor.fetchone(); conn.close()
view = discord.ui.View() view = disnake.ui.View()
btn_t = discord.ui.Button(label=f"Timeout ({res[0]})", style=discord.ButtonStyle.secondary) btn_t = disnake.ui.Button(label=f"Timeout ({res[0]})", style=disnake.ButtonStyle.secondary)
btn_t.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Timeout", res[0])) 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 = disnake.ui.Button(label=f"Kick ({res[1]})", style=disnake.ButtonStyle.secondary)
btn_k.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Kick", res[1])) 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 = disnake.ui.Button(label=f"Ban ({res[2]})", style=disnake.ButtonStyle.secondary)
btn_b.callback = lambda i: i.response.send_modal(ModConfigModal("Limite Ban", res[2])) 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) 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) await interaction.response.send_message("Quelle limite modifier ?", view=view, ephemeral=True)
@discord.ui.button(label="Durée Timeout", style=discord.ButtonStyle.secondary, emoji="⏱️") @disnake.ui.button(label="Durée Timeout", style=disnake.ButtonStyle.secondary, emoji="⏱️")
async def duration_btn(self, interaction: discord.Interaction, button: discord.ui.Button): async def duration_btn(self, interaction: disnake.Interaction, button: disnake.ui.Button):
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT timeout_duration FROM mod_config WHERE guild_id = ?", (self.guild_id,)) cursor.execute("SELECT timeout_duration FROM mod_config WHERE guild_id = ?", (self.guild_id,))
@ -225,38 +224,38 @@ class Moderation(commands.Cog):
log_channel = guild.get_channel(result[0]) log_channel = guild.get_channel(result[0])
if log_channel: await log_channel.send(embed=embed) if log_channel: await log_channel.send(embed=embed)
@app_commands.command(name="modpanel", description="Ouvre le panel de modération pour un membre") @commands.slash_command(name="modpanel", description="Ouvre le panel de modération pour un membre")
@app_commands.checks.has_permissions(moderate_members=True) @commands.has_permissions(moderate_members=True)
async def modpanel(self, interaction: discord.Interaction, member: discord.Member): 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: 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) return await interaction.response.send_message("❌ Impossible de gérer ce membre.", ephemeral=True)
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() 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)) 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() 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 = disnake.Embed(title=f"🛡️ Panel de Modération - {member.name}", description=f"Gestion de {member.mention}", color=disnake.Color.blue(), timestamp=datetime.now())
embed.set_thumbnail(url=member.display_avatar.url) embed.set_thumbnail(url=member.display_avatar.url)
embed.add_field(name="👤 Utilisateur", value=f"ID: `{member.id}`", inline=True) embed.add_field(name="👤 Utilisateur", value=f"ID: `{member.id}`", inline=True)
embed.add_field(name="⚠️ Avertissements", value=f"Actifs: **{warn_count}**", inline=True) embed.add_field(name="⚠️ Avertissements", value=f"Actifs: **{warn_count}**", inline=True)
view = ModPanelView(member, interaction.user, self.db_path) view = ModPanelView(member, interaction.user, self.db_path)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True) await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
@app_commands.command(name="modconfig", description="Configuration de la modération") @commands.slash_command(name="modconfig", description="Configuration de la modération")
@app_commands.checks.has_permissions(administrator=True) @commands.has_permissions(administrator=True)
async def modconfig(self, interaction: discord.Interaction): async def modconfig(self, interaction: disnake.ApplicationCommandInteraction):
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('INSERT OR IGNORE INTO mod_config (guild_id) VALUES (?)', (interaction.guild_id,)) 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,)) 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() 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 = disnake.Embed(title="⚙️ Configuration Modération", description="Modifiez les paramètres ci-dessous.", color=disnake.Color.blue())
embed.add_field(name="📁 Logs", value=f"<#{res[0]}>" if res[0] else "Non défini", inline=False) 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="⚖️ 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) embed.add_field(name="⏱️ Durée Timeout Auto", value=f"**{res[4]}s**", inline=False)
view = ModConfigView(interaction.guild_id, self.db_path) view = ModConfigView(interaction.guild_id, self.db_path)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True) await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
async def warn(self, interaction: discord.Interaction, member: discord.Member, reason: str): async def warn(self, interaction: disnake.Interaction, member: disnake.Member, reason: str):
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() 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('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?)', (interaction.guild_id, member.id, interaction.user.id, 'WARN', reason))
@ -266,26 +265,26 @@ class Moderation(commands.Cog):
config = cursor.fetchone(); conn.commit(); conn.close() 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) 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) 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())) await self.send_mod_log(interaction.guild, disnake.Embed(title="⚠️ Avertissement", description=f"Membre: {member.mention}\nRaison: {reason}\nTotal: {warn_count}", color=disnake.Color.orange()))
try: await member.send(f"⚠️ Avertissement sur **{interaction.guild.name}**\nRaison: {reason}") try: await member.send(f"⚠️ Avertissement sur **{interaction.guild.name}**\nRaison: {reason}")
except: pass except: pass
if config: if config:
l_t, l_k, l_b, dur = config l_t, l_k, l_b, dur = config
if warn_count >= l_b: await member.ban(reason=f"Auto: {warn_count} warns") 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_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") elif warn_count >= l_t: await member.timeout(duration=timedelta(seconds=dur), reason=f"Auto: {warn_count} warns")
async def timeout(self, interaction: discord.Interaction, member: discord.Member, duration_str: str, reason: str): async def timeout(self, interaction: disnake.Interaction, member: disnake.Member, duration_str: str, reason: str):
secs = self.parse_duration(duration_str) secs = self.parse_duration(duration_str)
if secs <= 0: return await interaction.response.send_message("❌ Durée invalide.", ephemeral=True) 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() 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)) 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() conn.commit(); conn.close()
if not interaction.response.is_done(): await interaction.response.send_message(f"{member.mention} mis en sourdine.", ephemeral=True) 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) else: await interaction.followup.send(f"{member.mention} mis en sourdine.", ephemeral=True)
async def kick(self, interaction: discord.Interaction, member: discord.Member, reason: str): async def kick(self, interaction: disnake.Interaction, member: disnake.Member, reason: str):
await member.kick(reason=reason) await member.kick(reason=reason)
conn = sqlite3.connect(self.db_path); cursor = conn.cursor() 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)) cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?)', (interaction.guild_id, member.id, interaction.user.id, 'KICK', reason))
@ -293,7 +292,7 @@ class Moderation(commands.Cog):
if not interaction.response.is_done(): await interaction.response.send_message(f"{member.name} expulsé.", ephemeral=True) 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) else: await interaction.followup.send(f"{member.name} expulsé.", ephemeral=True)
async def ban(self, interaction: discord.Interaction, user: discord.User, reason: str): async def ban(self, interaction: disnake.Interaction, user: disnake.User, reason: str):
await interaction.guild.ban(user, reason=reason) await interaction.guild.ban(user, reason=reason)
conn = sqlite3.connect(self.db_path); cursor = conn.cursor() 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)) cursor.execute('INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?)', (interaction.guild_id, user.id, interaction.user.id, 'BAN', reason))
@ -301,20 +300,20 @@ class Moderation(commands.Cog):
if not interaction.response.is_done(): await interaction.response.send_message(f"{user.name} banni.", ephemeral=True) 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) else: await interaction.followup.send(f"{user.name} banni.", ephemeral=True)
async def history(self, interaction: discord.Interaction, user: discord.User): async def history(self, interaction: disnake.Interaction, user: disnake.User):
conn = sqlite3.connect(self.db_path); cursor = conn.cursor() 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)) 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() results = cursor.fetchall(); conn.close()
if not results: if not results:
if not interaction.response.is_done(): return await interaction.response.send_message(" Aucun historique.", ephemeral=True) 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) else: return await interaction.followup.send(" Aucun historique.", ephemeral=True)
embed = discord.Embed(title=f"📜 Historique de {user.name}", color=discord.Color.blue()) embed = disnake.Embed(title=f"📜 Historique de {user.name}", color=disnake.Color.blue())
for s_id, s_type, reason, ts in results[:10]: 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) 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) if not interaction.response.is_done(): await interaction.response.send_message(embed=embed, ephemeral=True)
else: await interaction.followup.send(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: disnake.Interaction, member: disnake.Member, reason: str):
conn = sqlite3.connect(self.db_path); cursor = conn.cursor() 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)) 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() conn.commit(); conn.close()

View file

@ -1,5 +1,5 @@
import discord import disnake
from discord.ext import commands from disnake.ext import commands
import json import json
import os import os
from datetime import datetime from datetime import datetime
@ -53,7 +53,7 @@ class AntiRaid(commands.Cog):
if log_channel_id: if log_channel_id:
channel = guild.get_channel(int(log_channel_id)) channel = guild.get_channel(int(log_channel_id))
if channel: 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) await channel.send(embed=embed)
async def setup(bot): async def setup(bot):

View file

@ -1,5 +1,5 @@
import discord import disnake
from discord.ext import commands from disnake.ext import commands
import json import json
import os import os
from collections import deque from collections import deque
@ -28,7 +28,7 @@ class AntiSpam(commands.Cog):
def _get_cached_settings(self, guild_id): def _get_cached_settings(self, guild_id):
"""Récupère les paramètres avec cache (TTL de 5 secondes)""" """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) cached = self._settings_cache.get(guild_id)
if cached: if cached:
@ -52,7 +52,7 @@ class AntiSpam(commands.Cog):
def _get_cached_whitelist(self, guild_id): def _get_cached_whitelist(self, guild_id):
"""Récupère la whitelist avec cache (TTL de 5 secondes)""" """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) cached = self._whitelist_cache.get(guild_id)
if cached: if cached:
@ -117,7 +117,7 @@ class AntiSpam(commands.Cog):
# --- TEST ANTI-SPAM (deque pour O(1)) --- # --- TEST ANTI-SPAM (deque pour O(1)) ---
if settings.get("anti_spam") is True: 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 # Initialiser ou récupérer le deque pour cet utilisateur
if user_id not in self.message_counts: if user_id not in self.message_counts:
@ -156,7 +156,7 @@ class AntiSpam(commands.Cog):
self.message_counts[user_id] = deque(maxlen=10) self.message_counts[user_id] = deque(maxlen=10)
if current_warns <= 3: if current_warns <= 3:
embed = discord.Embed( embed = disnake.Embed(
title="🛡️ Sécurité Kuby", title="🛡️ Sécurité Kuby",
description=f"{message.author.mention}, {reason}.\n**Avertissement : {current_warns}/3**", description=f"{message.author.mention}, {reason}.\n**Avertissement : {current_warns}/3**",
color=0xFFA500 color=0xFFA500
@ -169,10 +169,10 @@ class AntiSpam(commands.Cog):
async def apply_mute(self, message, reason): async def apply_mute(self, message, reason):
try: 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)") await message.author.timeout(until, reason=f"Kuby Security: {reason} (3 Warns)")
embed = discord.Embed( embed = disnake.Embed(
title="⛔ Exclusion Temporaire", title="⛔ Exclusion Temporaire",
description=f"**Membre :** {message.author.mention}\n**Durée :** 5 minutes\n**Raison :** {reason} répétitif.", description=f"**Membre :** {message.author.mention}\n**Durée :** 5 minutes\n**Raison :** {reason} répétitif.",
color=0xFF0000 color=0xFF0000

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
# scan.py # scan.py
import discord import disnake
from discord import app_commands from disnake.ext import commands
from discord.ext import commands
import hashlib import hashlib
import os import os
import json import json
@ -59,8 +58,8 @@ class Scan(commands.Cog):
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
self.bot = bot self.bot = bot
@app_commands.command(name="scan", description="Scanner un fichier pour détecter un malware.") @commands.slash_command(name="scan", description="Scanner un fichier pour détecter un malware.")
async def scan(self, interaction: discord.Interaction, file: discord.Attachment): async def scan(self, interaction: disnake.ApplicationCommandInteraction, file: disnake.Attachment):
await interaction.response.defer() # Permet de prendre un peu de temps pour le scan await interaction.response.defer() # Permet de prendre un peu de temps pour le scan
# Téléchargement temporaire du fichier # Téléchargement temporaire du fichier

View file

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

View file

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

View file

@ -1,6 +1,5 @@
import discord import disnake
from discord import app_commands from disnake.ext import commands
from discord.ext import commands
from datetime import datetime from datetime import datetime
from collections import deque from collections import deque
@ -34,9 +33,9 @@ class SnipeCommands(commands.Cog):
self.deleted_messages[channel_id].appendleft(message_data) self.deleted_messages[channel_id].appendleft(message_data)
@app_commands.command(name="snipe", description="Voir les derniers messages supprimés d'un salon") @commands.slash_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: disnake.ApplicationCommandInteraction,
async def snipe(self, interaction: discord.Interaction, nombre: int = 1): nombre: int = commands.Param(1, description="Nombre de messages à afficher (1-10, par défaut: 1)")):
"""Retrieve deleted messages from the current channel""" """Retrieve deleted messages from the current channel"""
channel_id = interaction.channel.id channel_id = interaction.channel.id
@ -60,9 +59,9 @@ class SnipeCommands(commands.Cog):
messages = list(self.deleted_messages[channel_id])[:nombre] messages = list(self.deleted_messages[channel_id])[:nombre]
# Create embed for displaying deleted messages # 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)", 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): for i, msg_data in enumerate(messages, 1):
@ -93,8 +92,8 @@ class SnipeCommands(commands.Cog):
await interaction.response.send_message(embed=embed) await interaction.response.send_message(embed=embed)
@app_commands.command(name="snipeclear", description="Effacer l'historique des messages supprimés pour ce salon") @commands.slash_command(name="snipeclear", description="Effacer l'historique des messages supprimés pour ce salon")
async def snipeclear(self, interaction: discord.Interaction): async def snipeclear(self, interaction: disnake.ApplicationCommandInteraction):
"""Clear the deleted messages history for the current channel""" """Clear the deleted messages history for the current channel"""
channel_id = interaction.channel.id channel_id = interaction.channel.id

View file

@ -1,6 +1,5 @@
import discord import disnake
from discord.ext import commands from disnake.ext import commands
from discord import app_commands
from datetime import datetime from datetime import datetime
from commandes.ticket.data.storage import get_storage from commandes.ticket.data.storage import get_storage
@ -11,8 +10,8 @@ class StaffLeaderboard(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
@app_commands.command(name="staff-leaderboard", description="Affiche le classement du staff") @commands.slash_command(name="staff-leaderboard", description="Affiche le classement du staff")
async def staff_leaderboard(self, interaction: discord.Interaction): async def staff_leaderboard(self, interaction: disnake.ApplicationCommandInteraction):
"""Commande pour afficher le classement du staff""" """Commande pour afficher le classement du staff"""
await interaction.response.defer(ephemeral=False) 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) staff_list.sort(key=lambda x: x['average_rating'], reverse=True)
# Create embed # Create embed
embed = discord.Embed( embed = disnake.Embed(
title="🏆 Classement du Staff", title="🏆 Classement du Staff",
description="Classement des meilleurs membres du staff", description="Classement des meilleurs membres du staff",
color=discord.Color.gold(), color=disnake.Color.gold(),
timestamp=datetime.now() timestamp=datetime.now()
) )

View file

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

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

View file

@ -1,7 +1,7 @@
import discord import disnake
from .data.models import TicketData 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): def __init__(self, bot, storage, ticket: TicketData, staff_rating_only: bool = False):
super().__init__(timeout=86400) # 24h timeout super().__init__(timeout=86400) # 24h timeout
self.bot = bot self.bot = bot
@ -14,12 +14,12 @@ class FeedbackView(discord.ui.View):
# Add Select Menu for Rating # Add Select Menu for Rating
self.add_item(RatingSelect()) self.add_item(RatingSelect())
@discord.ui.button(label="📝 Laisser un commentaire", style=discord.ButtonStyle.secondary, custom_id="feedback_comment", row=1) @disnake.ui.button(label="📝 Laisser un commentaire", style=disnake.ButtonStyle.secondary, custom_id="feedback_comment", row=1)
async def comment_button(self, interaction: discord.Interaction, button: discord.ui.Button): async def comment_button(self, interaction: disnake.Interaction, button: disnake.ui.Button):
await interaction.response.send_modal(FeedbackModal(self)) await interaction.response.send_modal(FeedbackModal(self))
@discord.ui.button(label="✅ Envoyer", style=discord.ButtonStyle.green, custom_id="feedback_submit", row=1) @disnake.ui.button(label="✅ Envoyer", style=disnake.ButtonStyle.green, custom_id="feedback_submit", row=1)
async def submit_button(self, interaction: discord.Interaction, button: discord.ui.Button): async def submit_button(self, interaction: disnake.Interaction, button: disnake.ui.Button):
if self.rating == 0: if self.rating == 0:
await interaction.response.send_message("❌ Veuillez sélectionner une note avant d'envoyer.", ephemeral=True) await interaction.response.send_message("❌ Veuillez sélectionner une note avant d'envoyer.", ephemeral=True)
return return
@ -66,11 +66,11 @@ class FeedbackView(discord.ui.View):
if staff_user: if staff_user:
# Create feedback embed # Create feedback embed
embed = discord.Embed( embed = disnake.Embed(
title="📝 Nouveau Commentaire Reçu", title="📝 Nouveau Commentaire Reçu",
description=f"Un utilisateur a laissé un avis sur le ticket #{self.ticket.channel_id}", 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(), color=disnake.Color.gold() if self.rating < 3 else disnake.Color.green(),
timestamp=discord.utils.utcnow() timestamp=disnake.utils.utcnow()
) )
embed.add_field( embed.add_field(
@ -99,7 +99,7 @@ class FeedbackView(discord.ui.View):
try: try:
await staff_user.send(embed=embed) await staff_user.send(embed=embed)
print(f"DEBUG: DM sent successfully to {staff_user}", flush=True) 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) print(f"DEBUG: DM failed - Forbidden (DMs closed)", flush=True)
except Exception as e: except Exception as e:
print(f"DEBUG: Error sending feedback DM: {e}", flush=True) print(f"DEBUG: Error sending feedback DM: {e}", flush=True)
@ -111,34 +111,34 @@ class FeedbackView(discord.ui.View):
except Exception as e: except Exception as e:
print(f"Error in feedback notification: {e}", flush=True) print(f"Error in feedback notification: {e}", flush=True)
class RatingSelect(discord.ui.Select): class RatingSelect(disnake.ui.Select):
def __init__(self): def __init__(self):
options = [ options = [
discord.SelectOption(label="⭐⭐⭐⭐⭐ (Excellent)", value="5", description="Service parfait !"), disnake.SelectOption(label="⭐⭐⭐⭐⭐ (Excellent)", value="5", description="Service parfait !"),
discord.SelectOption(label="⭐⭐⭐⭐ (Très bien)", value="4", description="Très bon service"), disnake.SelectOption(label="⭐⭐⭐⭐ (Très bien)", value="4", description="Très bon service"),
discord.SelectOption(label="⭐⭐⭐ (Bien)", value="3", description="Correct"), disnake.SelectOption(label="⭐⭐⭐ (Bien)", value="3", description="Correct"),
discord.SelectOption(label="⭐⭐ (Moyen)", value="2", description="Peut mieux faire"), disnake.SelectOption(label="⭐⭐ (Moyen)", value="2", description="Peut mieux faire"),
discord.SelectOption(label="⭐ (Mauvais)", value="1", description="Insatisfaisant") disnake.SelectOption(label="⭐ (Mauvais)", value="1", description="Insatisfaisant")
] ]
super().__init__(placeholder="Notez le service...", min_values=1, max_values=1, options=options, row=0) 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]) self.view.rating = int(self.values[0])
await interaction.response.defer() await interaction.response.defer()
class FeedbackModal(discord.ui.Modal): class FeedbackModal(disnake.ui.Modal):
def __init__(self, view): def __init__(self, view):
super().__init__(title="Votre avis") super().__init__(title="Votre avis")
self.view = view self.view = view
self.comment = discord.ui.TextInput( self.comment = disnake.ui.TextInput(
label="Commentaire", label="Commentaire",
style=discord.TextStyle.paragraph, style=disnake.TextStyle.paragraph,
placeholder="Dites-nous ce que vous avez pensé du support...", placeholder="Dites-nous ce que vous avez pensé du support...",
required=True, required=True,
max_length=1000 max_length=1000
) )
self.add_item(self.comment) self.add_item(self.comment)
async def on_submit(self, interaction: discord.Interaction): async def on_submit(self, interaction: disnake.Interaction):
self.view.comment = self.comment.value self.view.comment = self.comment.value
await interaction.response.send_message("Commentaire enregistré ! N'oubliez pas de valider avec 'Envoyer'.", ephemeral=True) 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 collections import defaultdict
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
import discord import disnake
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import plotly.graph_objects as go import plotly.graph_objects as go
import plotly.express as px import plotly.express as px
@ -254,7 +254,7 @@ class StaffAnalytics:
return buf return buf
class StaffStatsView(discord.ui.View): class StaffStatsView(disnake.ui.View):
"""View for selecting individual staff member stats""" """View for selecting individual staff member stats"""
def __init__(self, bot, staff_list: List[Dict[str, Any]], analytics: StaffAnalytics): 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" description = f"Note: {staff['average_rating']:.1f}" if staff['total_ratings'] > 0 else "Aucune évaluation"
options.append( options.append(
discord.SelectOption( disnake.SelectOption(
label=label, label=label,
value=str(staff['user_id']), value=str(staff['user_id']),
description=description[:50] description=description[:50]
@ -280,14 +280,14 @@ class StaffStatsView(discord.ui.View):
) )
if options: if options:
select = discord.ui.Select( select = disnake.ui.Select(
placeholder="Sélectionnez un membre du staff...", placeholder="Sélectionnez un membre du staff...",
options=options options=options
) )
select.callback = self._staff_selected select.callback = self._staff_selected
self.add_item(select) self.add_item(select)
async def _staff_selected(self, interaction: discord.Interaction): async def _staff_selected(self, interaction: disnake.Interaction):
"""Handle staff selection""" """Handle staff selection"""
staff_id = int(interaction.data["values"][0]) staff_id = int(interaction.data["values"][0])
staff_data = next((s for s in self.staff_list if s['user_id'] == staff_id), None) 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 # Create individual chart
chart = self.analytics.create_individual_staff_chart(staff_data) chart = self.analytics.create_individual_staff_chart(staff_data)
embed = discord.Embed( embed = disnake.Embed(
title=f"📊 Statistiques de {staff_data['name']}", title=f"📊 Statistiques de {staff_data['name']}",
description="Performances détaillées du membre du staff", description="Performances détaillées du membre du staff",
color=discord.Color.blue(), color=disnake.Color.blue(),
timestamp=datetime.now() timestamp=datetime.now()
) )
@ -331,4 +331,4 @@ class StaffStatsView(discord.ui.View):
inline=True 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 datetime import datetime
from typing import Optional, List, Dict, Any 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 ( from ..data.models import (
TicketData, TicketData,

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
import discord import disnake
from discord.ext import commands from disnake.ext import commands
from discord import app_commands
import sqlite3 import sqlite3
import aiohttp import aiohttp
import io import io
@ -145,9 +144,9 @@ class Welcome(commands.Cog):
gif_url = "https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExNXV1NW02NTk0bGF2ZHcyMGFna2F6amRrbGpobGpsMHowOW5xa2E5eCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ZtFuW4rsUbfrRzPauJ/giphy.gif" gif_url = "https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExNXV1NW02NTk0bGF2ZHcyMGFna2F6amRrbGpobGpsMHowOW5xa2E5eCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ZtFuW4rsUbfrRzPauJ/giphy.gif"
await member.send(f"{formatted_message}\n\n{gif_url}") await member.send(f"{formatted_message}\n\n{gif_url}")
kuby_logger.info(f"✅ [Welcome] DM envoyé avec succès à {member.name} (envoi groupé)") 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)") 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) 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.") kuby_logger.warning(f"⏳ [Welcome] Limite de débit Discord atteinte pour les DMs : {member.name} n'a pas reçu son message.")
else: else:
@ -177,13 +176,13 @@ class Welcome(commands.Cog):
member_count=member.guild.member_count 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>"
embed = discord.Embed(title=f"Merci d'accueillir {member} 🤝", embed = disnake.Embed(title=f"Merci d'accueillir {member} 🤝",
description=channel_description, description=channel_description,
color=discord.Color.dark_purple()) color=disnake.Color.dark_purple())
embed.set_image(url="attachment://welcome.png") embed.set_image(url="attachment://welcome.png")
await channel.send(content=member.mention, embed=embed, file=discord.File(img, filename="welcome.png")) await channel.send(content=member.mention, embed=embed, 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""" """Sauvegarde l'attachment et retourne le chemin du fichier"""
base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "banners")) base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "banners"))
os.makedirs(base_dir, exist_ok=True) os.makedirs(base_dir, exist_ok=True)
@ -215,9 +214,9 @@ class Welcome(commands.Cog):
return os.path.basename(banner_path) 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") @commands.slash_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) @commands.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): 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 banner_path = None
# Si un fichier est uploadé, le sauvegarder # Si un fichier est uploadé, le sauvegarder
@ -237,7 +236,7 @@ class Welcome(commands.Cog):
welcome_server_name=excluded.welcome_server_name, welcome_server_name=excluded.welcome_server_name,
welcome_banner_background=COALESCE(excluded.welcome_banner_background, welcome_banner_background), welcome_banner_background=COALESCE(excluded.welcome_banner_background, welcome_banner_background),
welcome_channel_message=excluded.welcome_channel_message''', 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.commit()
conn.close() conn.close()

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import discord import disnake
import json import json
import os import os
import asyncio import asyncio
@ -18,7 +18,7 @@ class AdvancedLogger:
os.makedirs(self.logs_dir, exist_ok=True) os.makedirs(self.logs_dir, exist_ok=True)
os.makedirs(self.member_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 message events (sent, edited, deleted)"""
log_data = { log_data = {
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
@ -37,7 +37,7 @@ class AdvancedLogger:
asyncio.create_task(self._write_log_async("messages", log_data)) 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 role changes (added/removed)"""
log_data = { log_data = {
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
@ -52,7 +52,7 @@ class AdvancedLogger:
asyncio.create_task(self._write_log_async("roles", log_data)) 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 voice channel events (join/leave)"""
log_data = { log_data = {
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
@ -66,7 +66,7 @@ class AdvancedLogger:
asyncio.create_task(self._write_log_async("voice", log_data)) 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""" """Log when a member leaves the server"""
roles_data = { roles_data = {
"role_ids": [role.id for role in member.roles if role.name != "@everyone"], "role_ids": [role.id for role in member.roles if role.name != "@everyone"],
@ -91,7 +91,7 @@ class AdvancedLogger:
# Also save to general logs # Also save to general logs
asyncio.create_task(self._write_log_async("member_events", log_data)) 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 when a member joins the server"""
log_data = { log_data = {
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
@ -162,12 +162,12 @@ class AdvancedLogger:
except Exception as e: except Exception as e:
print(f"CRITICAL ERROR in AdvancedLogger._write_member_log_sync: {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""" """Send notification when member leaves"""
embed = discord.Embed( embed = disnake.Embed(
title="👋 Membre parti", title="👋 Membre parti",
description=f"{member.mention} ({member.name}#{member.discriminator}) a quitté le serveur.", description=f"{member.mention} ({member.name}) a quitté le serveur.",
color=discord.Color.red(), color=disnake.Color.red(),
timestamp=datetime.now() timestamp=datetime.now()
) )

View file

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