Kuby/commandes/convocation.py

279 lines
13 KiB
Python
Raw Normal View History

2026-05-06 17:07:09 +02:00
import disnake
from disnake.ext import commands
2026-02-27 14:18:01 +01:00
import json
import os
from typing import List, Optional, Dict
import datetime
# --- CONFIGURATION ---
CONV_SETTINGS_FILE = "data/convocation_settings.json"
CONV_BACKUPS_FILE = "data/convocation_backups.json"
def load_json(filepath: str) -> Dict:
if not os.path.exists(filepath):
return {}
try:
with open(filepath, "r", encoding='utf-8') as f:
return json.load(f)
except (json.JSONDecodeError, FileNotFoundError):
return {}
def save_json(filepath: str, data: Dict):
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, "w", encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False)
2026-02-27 12:26:52 +01:00
class Convocation(commands.Cog):
def __init__(self, bot):
self.bot = bot
2026-02-27 14:18:01 +01:00
self.settings = load_json(CONV_SETTINGS_FILE)
self.backups = load_json(CONV_BACKUPS_FILE)
2026-02-27 12:26:52 +01:00
2026-02-27 14:18:01 +01:00
def get_guild_settings(self, guild_id: int) -> Dict:
return self.settings.get(str(guild_id), {})
def save_guild_settings(self, guild_id: int, guild_settings: Dict):
self.settings[str(guild_id)] = guild_settings
save_json(CONV_SETTINGS_FILE, self.settings)
def get_backup(self, guild_id: int, user_id: int) -> Optional[Dict]:
key = f"{guild_id}_{user_id}"
return self.backups.get(key)
def save_backup(self, guild_id: int, user_id: int, backup_data: Dict):
key = f"{guild_id}_{user_id}"
self.backups[key] = backup_data
save_json(CONV_BACKUPS_FILE, self.backups)
def clear_backup(self, guild_id: int, user_id: int):
key = f"{guild_id}_{user_id}"
if key in self.backups:
del self.backups[key]
save_json(CONV_BACKUPS_FILE, self.backups)
2026-05-06 17:07:09 +02:00
@commands.slash_command(name="convocation", description="Gère les convocations de membres")
2026-02-27 14:18:01 +01:00
@commands.has_permissions(manage_roles=True)
2026-05-06 17:07:09 +02:00
async def convocation_group(self, interaction: disnake.ApplicationCommandInteraction):
pass
@convocation_group.sub_command(name="convoquer", description="Convoque un membre et verrouille ses accès")
async def convoquer(self, interaction: disnake.ApplicationCommandInteraction,
membre: disnake.Member = commands.Param(description="Le membre à convoquer"),
raison: str = commands.Param(description="La raison de la convocation"),
heure: str = commands.Param(description="L'heure de la convocation (ex: 14:30)")):
guild_id = interaction.guild.id
2026-02-27 14:18:01 +01:00
settings = self.get_guild_settings(guild_id)
role_conv_id = settings.get("role_id")
log_channel_id = settings.get("log_channel_id")
attente_id = settings.get("attente_id") # Salon d'attente à ignorer
admin_ids = settings.get("admin_ids", [])
if not role_conv_id:
2026-05-06 17:07:09 +02:00
await interaction.response.send_message("❌ Le rôle de convocation n'est pas configuré. Utilisez `/convocation config`.", ephemeral=True)
2026-02-27 14:18:01 +01:00
return
2026-05-06 17:07:09 +02:00
await interaction.response.defer(ephemeral=True)
2026-02-27 12:26:52 +01:00
try:
2026-02-27 14:18:01 +01:00
# --- BACKUP ---
2026-05-06 17:07:09 +02:00
bot_top_role = interaction.guild.me.top_role
2026-02-27 14:18:01 +01:00
backup_data = {
"roles": [r.id for r in membre.roles if not r.is_default() and r.id != role_conv_id and r.position < bot_top_role.position],
"overrides": {}
}
# 1. Backup and Remove current roles
2026-05-06 17:07:09 +02:00
current_roles = [interaction.guild.get_role(rid) for rid in backup_data["roles"] if interaction.guild.get_role(rid)]
2026-02-27 14:18:01 +01:00
if current_roles:
try:
2026-05-06 17:07:09 +02:00
await membre.remove_roles(*current_roles, reason=f"Convocation par {interaction.author}")
except disnake.Forbidden:
await interaction.followup.send("⚠️ Je n'ai pas pu retirer certains rôles (Permission refusée).", ephemeral=True)
2026-02-27 14:18:01 +01:00
# 2. Dynamic Lockdown: Loop through ALL channels
lockdown_count = 0
2026-05-06 17:07:09 +02:00
for channel in interaction.guild.channels:
2026-02-27 14:18:01 +01:00
# On ignore le salon d'attente et ses parents s'il y en a
if attente_id and (channel.id == attente_id or (hasattr(channel, 'category') and channel.category and channel.category.id == attente_id)):
continue
# Check existing override for the member
overwrites = channel.overwrites_for(membre)
if not overwrites.is_empty():
# Save existing override (as dict of pair of permission name and boolean/None)
backup_data["overrides"][str(channel.id)] = {k: v for k, v in dict(overwrites).items() if v is not None}
# Apply Lockdown override for the member
try:
# Specific override for this member: View Channel = False
await channel.set_permissions(membre, view_channel=False, reason="Convocation Lockdown")
lockdown_count += 1
2026-05-06 17:07:09 +02:00
except disnake.Forbidden:
2026-02-27 14:18:01 +01:00
pass # Skip if we can't touch this channel
self.save_backup(guild_id, membre.id, backup_data)
# 3. Add Convocation role
2026-05-06 17:07:09 +02:00
role_conv = interaction.guild.get_role(role_conv_id)
2026-02-27 14:18:01 +01:00
if role_conv:
if role_conv.position >= bot_top_role.position:
2026-05-06 17:07:09 +02:00
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)
2026-02-27 14:18:01 +01:00
else:
try:
await membre.add_roles(role_conv, reason="Début de convocation")
2026-05-06 17:07:09 +02:00
except disnake.Forbidden:
await interaction.followup.send(f"⚠️ Impossible de donner le rôle {role_conv.name} (Permission refusée).", ephemeral=True)
2026-02-27 14:18:01 +01:00
# 4. Embeds & Logs (Inchangés mais inclus pour la cohérence)
2026-05-06 17:07:09 +02:00
embed_conv = disnake.Embed(
2026-02-27 12:26:52 +01:00
title="CONVOCATION | 🔖",
2026-05-06 17:07:09 +02:00
description=f"> Bonjour {membre.mention}, vous êtes **convoqué** par {interaction.author.mention}. Voici les **détails** :",
2026-02-27 14:18:01 +01:00
color=0x0099ff,
2026-05-06 17:07:09 +02:00
timestamp=disnake.utils.utcnow()
2026-02-27 12:26:52 +01:00
)
2026-02-27 14:18:01 +01:00
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="\u200B", value=(
"> Merci de vous présenter dans le vocal attente **5 minutes** avant l'heure.\n\n"
2026-02-27 12:26:52 +01:00
"> La **levée** de suspension se fera uniquement à la **fin** de la convocation."
), inline=False)
2026-05-06 17:07:09 +02:00
embed_conv.set_footer(text=f"Gestion de {interaction.guild.name}")
2026-02-27 12:26:52 +01:00
2026-05-06 17:07:09 +02:00
embed_log = disnake.Embed(
2026-02-27 12:26:52 +01:00
title="📋 CONVOCATION ENVOYÉE",
2026-02-27 14:18:01 +01:00
color=0xff9900,
2026-05-06 17:07:09 +02:00
timestamp=disnake.utils.utcnow()
2026-02-27 12:26:52 +01:00
)
2026-02-27 14:18:01 +01:00
embed_log.add_field(name="👤 Membre convoqué", value=f"{membre.mention} ({membre.id})", inline=True)
2026-05-06 17:07:09 +02:00
embed_log.add_field(name="👮 Convoqué par", value=f"{interaction.author.mention}", inline=True)
2026-02-27 12:26:52 +01:00
embed_log.add_field(name="📄 Raison", value=raison, inline=False)
embed_log.add_field(name="🕒 Heure", value=heure, inline=False)
2026-02-27 14:18:01 +01:00
# DM
2026-02-27 12:26:52 +01:00
dm_sent = False
try:
2026-02-27 14:18:01 +01:00
await membre.send(embed=embed_conv)
2026-02-27 12:26:52 +01:00
dm_sent = True
2026-05-06 17:07:09 +02:00
except disnake.Forbidden:
2026-02-27 12:26:52 +01:00
dm_sent = False
2026-02-27 14:18:01 +01:00
# Admin & Channel Logs
for admin_id in admin_ids:
2026-02-27 12:26:52 +01:00
try:
2026-02-27 14:18:01 +01:00
admin = await self.bot.fetch_user(admin_id)
await admin.send(embed=embed_log)
except: pass
2026-02-27 12:26:52 +01:00
2026-02-27 14:18:01 +01:00
if log_channel_id:
2026-05-06 17:07:09 +02:00
chan = interaction.guild.get_channel(log_channel_id)
2026-02-27 14:18:01 +01:00
if chan: await chan.send(embed=embed_log)
2026-05-06 17:07:09 +02:00
await interaction.followup.send(f"✅ Convocation envoyée à {membre.mention}. ({lockdown_count} salons verrouillés) " +
2026-02-27 14:18:01 +01:00
("" if dm_sent else "(⚠️ DM impossible)"), ephemeral=True)
2026-02-27 12:26:52 +01:00
except Exception as e:
2026-02-27 14:18:01 +01:00
import traceback
traceback.print_exc()
2026-05-06 17:07:09 +02:00
await interaction.followup.send(f"❌ Une erreur critique est survenue : {e}", ephemeral=True)
2026-02-27 14:18:01 +01:00
2026-05-06 17:07:09 +02:00
@convocation_group.sub_command(name="lever", description="Lève la convocation d'un membre et restaure ses accès")
async def lever(self, interaction: disnake.ApplicationCommandInteraction,
membre: disnake.Member = commands.Param(description="Le membre à libérer")):
guild_id = interaction.guild.id
2026-02-27 14:18:01 +01:00
settings = self.get_guild_settings(guild_id)
role_conv_id = settings.get("role_id")
2026-05-06 17:07:09 +02:00
await interaction.response.defer(ephemeral=True)
2026-02-27 14:18:01 +01:00
backup_data = self.get_backup(guild_id, membre.id)
if not backup_data:
2026-05-06 17:07:09 +02:00
await interaction.followup.send("⚠️ Aucune donnée de convocation trouvée pour ce membre.", ephemeral=True)
2026-02-27 14:18:01 +01:00
return
# 1. Remove Convocation role
if role_conv_id:
2026-05-06 17:07:09 +02:00
role_conv = interaction.guild.get_role(role_conv_id)
2026-02-27 14:18:01 +01:00
if role_conv and role_conv in membre.roles:
await membre.remove_roles(role_conv, reason="Fin de convocation")
# 2. Restore channel overrides
restore_count = 0
overrides = backup_data.get("overrides", {})
2026-05-06 17:07:09 +02:00
for channel in interaction.guild.channels:
2026-02-27 14:18:01 +01:00
chan_id_str = str(channel.id)
try:
if chan_id_str in overrides:
# Restore previous override
ov_data = overrides[chan_id_str]
2026-05-06 17:07:09 +02:00
overwrite = disnake.PermissionOverwrite(**ov_data)
2026-02-27 14:18:01 +01:00
await channel.set_permissions(membre, overwrite=overwrite, reason="Restauration fin de convocation")
restore_count += 1
else:
# Clear our lockdown override
# On ne vide que si un override membre existait (sinon on risque de supprimer des perms rôles ?)
# En fait set_permissions(member, overwrite=None) supprime l'entrée PERSONNELLE du membre.
await channel.set_permissions(membre, overwrite=None, reason="Fin de convocation - Cleanup")
2026-05-06 17:07:09 +02:00
except disnake.Forbidden:
2026-02-27 14:18:01 +01:00
pass
# 3. Restore roles
role_ids = backup_data.get("roles", [])
restored_roles = []
if role_ids:
for rid in role_ids:
2026-05-06 17:07:09 +02:00
role = interaction.guild.get_role(rid)
if role and role.position < interaction.guild.me.top_role.position:
2026-02-27 14:18:01 +01:00
restored_roles.append(role)
if restored_roles:
await membre.add_roles(*restored_roles, reason="Restauration fin de convocation")
self.clear_backup(guild_id, membre.id)
# 4. Notification
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
2026-02-27 14:18:01 +01:00
title="🔓 CONVOCATION TERMINÉE",
2026-05-06 17:07:09 +02:00
description=f"Votre convocation sur **{interaction.guild.name}** est terminée. Vos accès ont été restaurés.",
color=disnake.Color.green()
2026-02-27 14:18:01 +01:00
)
try:
await membre.send(embed=embed)
except: pass
2026-05-06 17:07:09 +02:00
await interaction.followup.send(f"✅ Convocation levée pour {membre.mention}. Access restaurés.", ephemeral=True)
@convocation_group.sub_command(name="config", description="Configure le système de convocation")
async def config(self, interaction: disnake.ApplicationCommandInteraction,
role: disnake.Role = commands.Param(description="Le rôle de convocation (Muet/Convocation)"),
channel: disnake.TextChannel = commands.Param(None, description="Le salon de logs"),
attente: disnake.abc.GuildChannel = commands.Param(None, description="Salon (ou catégorie) d'attente à laisser visible"),
admins: str = commands.Param(None, description="Liste d'IDs d'admins à notifier (séparés par des espaces)")):
guild_id = interaction.guild.id
2026-02-27 14:18:01 +01:00
settings = self.get_guild_settings(guild_id)
settings["role_id"] = role.id
if channel:
settings["log_channel_id"] = channel.id
if attente:
settings["attente_id"] = attente.id
if admins:
admin_list = []
for uid in admins.split():
if uid.isdigit():
admin_list.append(int(uid))
settings["admin_ids"] = admin_list
self.save_guild_settings(guild_id, settings)
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(title="⚙️ Configuration Convocation Mise à jour", color=disnake.Color.blue())
2026-02-27 14:18:01 +01:00
embed.add_field(name="Rôle", value=role.mention)
embed.add_field(name="Salon Logs", value=channel.mention if channel else "Non défini")
embed.add_field(name="Salon/Cat d'attente", value=attente.mention if attente else "Non défini")
embed.add_field(name="Admins notifiés", value=str(len(settings.get("admin_ids", []))))
2026-05-06 17:07:09 +02:00
await interaction.response.send_message(embed=embed, ephemeral=True)
2026-02-27 12:26:52 +01:00
def setup(bot):
bot.add_cog(Convocation(bot))