# -*- coding: utf-8 -*- """ Team JSON Generator =================== Ce module parcourt les membres d'un serveur Discord, filtre ceux qui possèdent un ou plusieurs rôles spécifiés, télécharge leurs avatars et génère un fichier JSON compatible avec le site de production. Le JSON attendu ressemble à : [ { "id": "123456789012345678", "name": "Nom affiché", "role": "Nom du rôle filtré", "avatar": "123456789012345678.png", "bio": "", "stats": [], "skills": [], "social": {"discord":"","twitter":"",...} }, ... ] Le champ ``avatar`` correspond au nom de fichier stocké dans ``website/avatars/``. """ import os import json import aiohttp from pathlib import Path from typing import List import disnake from disnake.ext import commands, tasks # ---------------------------------------------------------------------- # Configuration – variables d'environnement (dans le .env du projet) # ---------------------------------------------------------------------- TARGET_GUILD_ID = os.getenv("TARGET_GUILD_ID") # ID du serveur contenant l'équipe # Liste d'IDs de rôles séparés par des virgules (ex. "123,456,789") TEAM_ROLE_IDS = os.getenv("TEAM_ROLE_IDS", "").split(",") if os.getenv("TEAM_ROLE_IDS") else [] # Dossiers où seront stockés les avatars et le JSON final AVATAR_ROOT = Path(__file__).parents[2] / "website" / "avatars" AVATAR_ROOT.mkdir(parents=True, exist_ok=True) JSON_OUTPUT = Path(__file__).parents[2] / "website" / "team.json" # ---------------------------------------------------------------------- class TeamJSON(commands.Cog): """Cog responsable de la génération du JSON d'équipe et du téléchargement des avatars.""" def __init__(self, bot: commands.Bot): self.bot = bot # Démarre la tâche quotidienne (synchronisation 24h/24) self.sync_daily.start() # ------------------------------------------------------------------ @tasks.loop(hours=24) async def sync_daily(self): """Tâche planifiée exécutée chaque jour (cron).""" await self._generate_team(trigger="cron") @sync_daily.before_loop async def before_sync(self): await self.bot.wait_until_ready() # ------------------------------------------------------------------ async def _download_avatar(self, session: aiohttp.ClientSession, member: disnake.Member) -> str: """Télécharge l'avatar du *member* et le sauvegarde dans ``AVATAR_ROOT``. Retourne le nom de fichier (ex. ``"123456789.png"``).""" avatar_url = str(member.display_avatar.url) filename = f"{member.id}.png" dest_path = AVATAR_ROOT / filename try: async with session.get(avatar_url) as resp: if resp.status == 200: dest_path.write_bytes(await resp.read()) else: self.bot.logger.warning( f"[TeamJSON] Avatar download failed for {member.id} – HTTP {resp.status}" ) except Exception as exc: self.bot.logger.error(f"[TeamJSON] Exception while downloading avatar {member.id}: {exc}") return filename # ------------------------------------------------------------------ async def _generate_team(self, *, trigger: str = "manual") -> None: """Construit le JSON de l'équipe et télécharge les avatars. Le JSON produit suit exactement le format attendu par le site de production. Les champs ``stats``, ``skills`` et ``social`` sont laissés vides car ils ne sont pas disponibles via l'API bot. """ if not TARGET_GUILD_ID: self.bot.logger.error("[TeamJSON] TARGET_GUILD_ID non défini – arrêt du générateur.") return if not TEAM_ROLE_IDS: self.bot.logger.error("[TeamJSON] TEAM_ROLE_IDS non défini – arrêt du générateur.") return # Récupération de la guilde guild = self.bot.get_guild(int(TARGET_GUILD_ID)) if not guild: try: guild = await self.bot.fetch_guild(int(TARGET_GUILD_ID)) except Exception as exc: self.bot.logger.error(f"[TeamJSON] Impossible de récupérer la guilde {TARGET_GUILD_ID}: {exc}") return # Convertir les IDs de rôle en entiers pour la comparaison role_ids = {int(r) for r in TEAM_ROLE_IDS if r.strip()} members_data: List[dict] = [] async with aiohttp.ClientSession() as session: async for member in guild.fetch_members(limit=None): if member.bot: continue # Intersection entre les rôles du membre et les rôles ciblés member_role_ids = {r.id for r in member.roles} intersect = member_role_ids.intersection(role_ids) if not intersect: continue # le membre ne possède aucun des rôles recherchés # On prend le premier rôle correspondant comme "role" dans le JSON role_name = next((r.name for r in member.roles if r.id in intersect), "") avatar_file = await self._download_avatar(session, member) entry = { "id": str(member.id), "name": member.display_name, "role": role_name, "avatar": avatar_file, "bio": "", # non récupérable via l'API bot "stats": [], "skills": [], "social": { "discord": "", "twitter": "", "instagram": "", "youtube": "", "tiktok": "", "github": "", }, } members_data.append(entry) # Écriture du JSON final try: JSON_OUTPUT.parent.mkdir(parents=True, exist_ok=True) JSON_OUTPUT.write_text( json.dumps(members_data, indent=2, ensure_ascii=False), encoding="utf-8", ) self.bot.logger.info( f"[TeamJSON] JSON généré ({len(members_data)} membres) → {JSON_OUTPUT}" ) except Exception as exc: self.bot.logger.error(f"[TeamJSON] Échec de l'écriture du JSON : {exc}") # ------------------------------------------------------------------ @commands.slash_command( name="generate_team_json", description="Génère le JSON de l'équipe (admin only)" ) @commands.has_permissions(administrator=True) async def generate_team_json(self, inter: disnake.ApplicationCommandInteraction): """Commande manuelle pour (re)générer le JSON et les avatars.""" await inter.response.defer(ephemeral=True) await self._generate_team(trigger="manual") await inter.edit_original_response( content="✅ JSON de l'équipe généré et avatars synchronisés." ) # ---------------------------------------------------------------------- def setup(bot: commands.Bot): bot.add_cog(TeamJSON(bot))