V1.0 de l'intégration de la feature de récupération automatique des informations des agents O.K sur le siteweb
This commit is contained in:
parent
d116bb4141
commit
fe05572549
3 changed files with 858 additions and 1 deletions
3
bot.py
3
bot.py
|
|
@ -67,7 +67,8 @@ class MyBot(commands.Bot):
|
||||||
"commandes.ticket_whitelist",
|
"commandes.ticket_whitelist",
|
||||||
"commandes.bug_report",
|
"commandes.bug_report",
|
||||||
"commandes.convocation",
|
"commandes.convocation",
|
||||||
"commandes.absence_staff"
|
"commandes.absence_staff",
|
||||||
|
"commandes.website_sync",
|
||||||
]
|
]
|
||||||
|
|
||||||
for extension in extensions:
|
for extension in extensions:
|
||||||
|
|
|
||||||
476
commandes/website_sync.py
Normal file
476
commandes/website_sync.py
Normal file
|
|
@ -0,0 +1,476 @@
|
||||||
|
"""
|
||||||
|
Cog : website_sync
|
||||||
|
Commandes pour gérer la synchronisation JSON du site web.
|
||||||
|
|
||||||
|
Slash commands disponibles :
|
||||||
|
/json-generate [cible] — Génère les JSON manuellement
|
||||||
|
/json-status — État des fichiers + prochaine génération
|
||||||
|
/json-preview [cible] [limite] — Aperçu sans écrire les fichiers
|
||||||
|
/json-history [limite] — Historique des N dernières générations
|
||||||
|
/json-config show — Affiche la config actuelle
|
||||||
|
/json-config set-guild <id> — Définit le serveur cible (members.json)
|
||||||
|
/json-config set-schedule <h> [m] — Heure de génération auto
|
||||||
|
/json-config set-output-dir <chemin> — Changer le dossier de sortie
|
||||||
|
/json-config add-founder-role <nom> — Ajouter un nom de rôle fondateur
|
||||||
|
/json-config remove-founder-role <nom> — Retirer un nom de rôle fondateur
|
||||||
|
/json-config server-field <champ> <actif> — Active/désactive un champ servers.json
|
||||||
|
/json-config member-field <champ> <actif> — Active/désactive un champ members.json
|
||||||
|
/json-config reset — Remet la config par défaut
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
from discord.ext import commands, tasks
|
||||||
|
|
||||||
|
from src.json_export_service import (
|
||||||
|
load_config, save_config, generate_all,
|
||||||
|
generate_servers_json, generate_members_json,
|
||||||
|
preview_servers, preview_members,
|
||||||
|
get_history, log_history,
|
||||||
|
SERVERS_FILE, MEMBERS_FILE, OUTPUT_DIR,
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger("kuby.website_sync")
|
||||||
|
|
||||||
|
# Listes des champs disponibles (pour les choix des commandes)
|
||||||
|
SERVER_FIELDS = [
|
||||||
|
"id", "name", "icon", "banner", "owner",
|
||||||
|
"memberCount", "description", "createdAt",
|
||||||
|
"boostLevel", "boostCount", "verificationLevel",
|
||||||
|
]
|
||||||
|
MEMBER_FIELDS = [
|
||||||
|
"id", "username", "displayName", "globalName",
|
||||||
|
"avatar", "bio", "roles", "joinedAt", "createdAt",
|
||||||
|
"badges", "accentColor", "isBot",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class WebsiteSync(commands.Cog, name="Website Sync"):
|
||||||
|
"""Synchronisation JSON pour le site web."""
|
||||||
|
|
||||||
|
def __init__(self, bot: commands.Bot):
|
||||||
|
self.bot = bot
|
||||||
|
self._scheduler_task: Optional[asyncio.Task] = None
|
||||||
|
self.daily_export.start()
|
||||||
|
|
||||||
|
def cog_unload(self):
|
||||||
|
self.daily_export.cancel()
|
||||||
|
|
||||||
|
# ── Tâche de fond : génération quotidienne ────────────────────────────────
|
||||||
|
@tasks.loop(minutes=1)
|
||||||
|
async def daily_export(self):
|
||||||
|
"""Vérifie chaque minute si c'est l'heure de générer les JSON."""
|
||||||
|
config = load_config()
|
||||||
|
now = datetime.now()
|
||||||
|
if now.hour == config["autoHour"] and now.minute == config["autoMinute"]:
|
||||||
|
logger.info("[JSON Export] Génération automatique quotidienne déclenchée.")
|
||||||
|
await generate_all(self.bot)
|
||||||
|
|
||||||
|
@daily_export.before_loop
|
||||||
|
async def before_daily_export(self):
|
||||||
|
await self.bot.wait_until_ready()
|
||||||
|
config = load_config()
|
||||||
|
logger.info(
|
||||||
|
f"[JSON Export] Scheduler actif → génération auto à "
|
||||||
|
f"{config['autoHour']:02d}:{config['autoMinute']:02d} chaque jour."
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Helper : embed de réponse ─────────────────────────────────────────────
|
||||||
|
@staticmethod
|
||||||
|
def _embed(title: str, color: discord.Color, description: str = "") -> discord.Embed:
|
||||||
|
e = discord.Embed(title=title, color=color, timestamp=datetime.now(timezone.utc))
|
||||||
|
if description:
|
||||||
|
e.description = description
|
||||||
|
return e
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════
|
||||||
|
# /json-generate
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════
|
||||||
|
@app_commands.command(name="json-generate", description="Génère manuellement les fichiers JSON du site web")
|
||||||
|
@app_commands.describe(cible="Quoi générer ? (défaut : tout)")
|
||||||
|
@app_commands.choices(cible=[
|
||||||
|
app_commands.Choice(name="Tout (servers + members)", value="all"),
|
||||||
|
app_commands.Choice(name="Serveurs uniquement", value="servers"),
|
||||||
|
app_commands.Choice(name="Membres uniquement", value="members"),
|
||||||
|
])
|
||||||
|
@app_commands.default_permissions(administrator=True)
|
||||||
|
async def json_generate(self, interaction: discord.Interaction, cible: str = "all"):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
start = datetime.now()
|
||||||
|
embed = self._embed("⏳ Génération en cours...", discord.Color.yellow())
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if cible == "servers":
|
||||||
|
res = await generate_servers_json(self.bot)
|
||||||
|
embed = self._embed("✅ servers.json généré", discord.Color.green())
|
||||||
|
embed.add_field(name="Serveurs", value=str(res["count"]), inline=True)
|
||||||
|
embed.add_field(name="Durée", value=f"{(datetime.now()-start).total_seconds():.1f}s", inline=True)
|
||||||
|
embed.add_field(name="Fichier", value=f"`{SERVERS_FILE}`", inline=False)
|
||||||
|
|
||||||
|
elif cible == "members":
|
||||||
|
res = await generate_members_json(self.bot)
|
||||||
|
embed = self._embed("✅ members.json généré", discord.Color.green())
|
||||||
|
embed.add_field(name="Membres", value=str(res["count"]), inline=True)
|
||||||
|
embed.add_field(name="Serveur", value=res["guildName"], inline=True)
|
||||||
|
embed.add_field(name="Durée", value=f"{(datetime.now()-start).total_seconds():.1f}s", inline=True)
|
||||||
|
embed.add_field(name="Fichier", value=f"`{MEMBERS_FILE}`", inline=False)
|
||||||
|
|
||||||
|
else: # all
|
||||||
|
res = await generate_all(self.bot, trigger="manual")
|
||||||
|
has_errors = bool(res["errors"])
|
||||||
|
embed = self._embed(
|
||||||
|
"⚠️ Génération partielle" if has_errors else "✅ Génération complète",
|
||||||
|
discord.Color.orange() if has_errors else discord.Color.green(),
|
||||||
|
)
|
||||||
|
if res["servers"]:
|
||||||
|
embed.add_field(name="📋 Serveurs", value=f"{res['servers']['count']} serveurs", inline=True)
|
||||||
|
if res["members"]:
|
||||||
|
embed.add_field(name="👥 Membres", value=f"{res['members']['count']} membres", inline=True)
|
||||||
|
embed.add_field(name="Durée totale", value=f"{(datetime.now()-start).total_seconds():.1f}s", inline=True)
|
||||||
|
if has_errors:
|
||||||
|
embed.add_field(name="❌ Erreurs", value="\n".join(res["errors"]), inline=False)
|
||||||
|
embed.add_field(name="📁 Dossier", value=f"`{OUTPUT_DIR}`", inline=False)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
embed = self._embed("❌ Erreur", discord.Color.red(), f"```{e}```")
|
||||||
|
|
||||||
|
await interaction.edit_original_response(embed=embed)
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════
|
||||||
|
# /json-status
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════
|
||||||
|
@app_commands.command(name="json-status", description="État des fichiers JSON et prochaine génération auto")
|
||||||
|
@app_commands.default_permissions(administrator=True)
|
||||||
|
async def json_status(self, interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
config = load_config()
|
||||||
|
embed = self._embed("📊 Statut JSON Export", discord.Color.blurple())
|
||||||
|
|
||||||
|
# servers.json
|
||||||
|
if SERVERS_FILE.exists():
|
||||||
|
try:
|
||||||
|
data = json.loads(SERVERS_FILE.read_text())
|
||||||
|
size = SERVERS_FILE.stat().st_size / 1024
|
||||||
|
gen_at = datetime.fromisoformat(data["generatedAt"]).strftime("%d/%m/%Y %H:%M")
|
||||||
|
embed.add_field(
|
||||||
|
name="🌐 servers.json",
|
||||||
|
value=f"✅ {data['count']} serveurs — {size:.1f} KB\n📅 {gen_at}",
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
embed.add_field(name="🌐 servers.json", value="⚠️ Fichier corrompu", inline=False)
|
||||||
|
else:
|
||||||
|
embed.add_field(name="🌐 servers.json", value="❌ Non généré", inline=False)
|
||||||
|
|
||||||
|
# members.json
|
||||||
|
if MEMBERS_FILE.exists():
|
||||||
|
try:
|
||||||
|
data = json.loads(MEMBERS_FILE.read_text())
|
||||||
|
size = MEMBERS_FILE.stat().st_size / 1024
|
||||||
|
gen_at = datetime.fromisoformat(data["generatedAt"]).strftime("%d/%m/%Y %H:%M")
|
||||||
|
embed.add_field(
|
||||||
|
name="👥 members.json",
|
||||||
|
value=f"✅ {data['count']} membres ({data['guildName']}) — {size:.1f} KB\n📅 {gen_at}",
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
embed.add_field(name="👥 members.json", value="⚠️ Fichier corrompu", inline=False)
|
||||||
|
else:
|
||||||
|
embed.add_field(name="👥 members.json", value="❌ Non généré", inline=False)
|
||||||
|
|
||||||
|
# Prochain run
|
||||||
|
now = datetime.now()
|
||||||
|
next_run = now.replace(hour=config["autoHour"], minute=config["autoMinute"], second=0, microsecond=0)
|
||||||
|
if next_run <= now:
|
||||||
|
next_run += timedelta(days=1)
|
||||||
|
diff = next_run - now
|
||||||
|
h, m = divmod(int(diff.total_seconds() // 60), 60)
|
||||||
|
in_text = f"dans {h}h {m}min" if h else f"dans {m}min"
|
||||||
|
embed.add_field(
|
||||||
|
name="⏰ Prochaine génération auto",
|
||||||
|
value=f"**{config['autoHour']:02d}:{config['autoMinute']:02d}** — {in_text}\n({next_run.strftime('%d/%m/%Y %H:%M')})",
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
embed.add_field(name="📁 Dossier", value=f"`{OUTPUT_DIR}`", inline=False)
|
||||||
|
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════
|
||||||
|
# /json-config (groupe)
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════
|
||||||
|
json_config_group = app_commands.Group(
|
||||||
|
name="json-config",
|
||||||
|
description="Configure la génération automatique des JSON",
|
||||||
|
default_permissions=discord.Permissions(administrator=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── show ──────────────────────────────────────────────────────────────────
|
||||||
|
@json_config_group.command(name="show", description="Affiche la configuration actuelle")
|
||||||
|
async def config_show(self, interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
config = load_config()
|
||||||
|
|
||||||
|
srv_on = ", ".join(k for k, v in config["serverFields"].items() if v) or "_Aucun_"
|
||||||
|
srv_off = ", ".join(k for k, v in config["serverFields"].items() if not 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_"
|
||||||
|
|
||||||
|
embed = self._embed("⚙️ Configuration JSON Export", discord.Color.blurple())
|
||||||
|
embed.add_field(name="🕐 Génération auto", value=f"{config['autoHour']:02d}:{config['autoMinute']:02d} (heure serveur)", inline=True)
|
||||||
|
embed.add_field(name="🎯 Serveur cible (members)", value=config["targetGuildId"] or "_Non défini_", inline=True)
|
||||||
|
embed.add_field(name="\u200b", value="\u200b", inline=False)
|
||||||
|
embed.add_field(name="✅ servers.json actifs", value=srv_on, inline=False)
|
||||||
|
embed.add_field(name="❌ servers.json inactifs", value=srv_off, inline=False)
|
||||||
|
embed.add_field(name="✅ members.json actifs", value=mbr_on, inline=False)
|
||||||
|
embed.add_field(name="❌ members.json inactifs", value=mbr_off, inline=False)
|
||||||
|
embed.add_field(name="📁 Dossier de sortie", value=f"`{OUTPUT_DIR}`", inline=False)
|
||||||
|
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
# ── set-guild ────────────────────────────────────────────────────────────
|
||||||
|
@json_config_group.command(name="set-guild", description="Définit l'ID du serveur cible pour members.json")
|
||||||
|
@app_commands.describe(guild_id="ID du serveur Discord")
|
||||||
|
async def config_set_guild(self, interaction: discord.Interaction, guild_id: str):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
try:
|
||||||
|
guild = self.bot.get_guild(int(guild_id)) or await self.bot.fetch_guild(int(guild_id))
|
||||||
|
config = load_config()
|
||||||
|
config["targetGuildId"] = guild_id
|
||||||
|
save_config(config)
|
||||||
|
embed = self._embed(
|
||||||
|
"✅ Serveur cible défini", discord.Color.green(),
|
||||||
|
f"**{guild.name}** (`{guild_id}`) sera utilisé pour members.json.",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
embed = self._embed(
|
||||||
|
"❌ Serveur introuvable", discord.Color.red(),
|
||||||
|
f"Le bot n'est pas dans le serveur `{guild_id}` ou l'ID est invalide.",
|
||||||
|
)
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
# ── set-schedule ─────────────────────────────────────────────────────────
|
||||||
|
@json_config_group.command(name="set-schedule", description="Définit l'heure de génération automatique quotidienne")
|
||||||
|
@app_commands.describe(heure="Heure (0-23)", minute="Minute (0-59, défaut: 0)")
|
||||||
|
async def config_set_schedule(self, interaction: discord.Interaction, heure: int, minute: int = 0):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
if not (0 <= heure <= 23) or not (0 <= minute <= 59):
|
||||||
|
await interaction.followup.send("❌ Heure invalide.", ephemeral=True)
|
||||||
|
return
|
||||||
|
config = load_config()
|
||||||
|
config["autoHour"] = heure
|
||||||
|
config["autoMinute"] = minute
|
||||||
|
save_config(config)
|
||||||
|
embed = self._embed(
|
||||||
|
"✅ Horaire mis à jour", discord.Color.green(),
|
||||||
|
f"Prochaine génération auto : **{heure:02d}:{minute:02d}** chaque jour.",
|
||||||
|
)
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
# ── server-field ─────────────────────────────────────────────────────────
|
||||||
|
@json_config_group.command(name="server-field", description="Active/désactive un champ dans servers.json")
|
||||||
|
@app_commands.describe(champ="Champ à modifier", actif="true = inclus, false = exclu")
|
||||||
|
@app_commands.choices(champ=[app_commands.Choice(name=f, value=f) for f in SERVER_FIELDS])
|
||||||
|
async def config_server_field(self, interaction: discord.Interaction, champ: str, actif: bool):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
config = load_config()
|
||||||
|
config["serverFields"][champ] = actif
|
||||||
|
save_config(config)
|
||||||
|
state = "inclus ✅" if actif else "exclu ❌"
|
||||||
|
embed = self._embed("✅ Champ servers.json mis à jour", discord.Color.green(),
|
||||||
|
f"`{champ}` est maintenant **{state}** de servers.json.")
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
# ── member-field ─────────────────────────────────────────────────────────
|
||||||
|
@json_config_group.command(name="member-field", description="Active/désactive un champ dans members.json")
|
||||||
|
@app_commands.describe(champ="Champ à modifier", actif="true = inclus, false = exclu")
|
||||||
|
@app_commands.choices(champ=[app_commands.Choice(name=f, value=f) for f in MEMBER_FIELDS])
|
||||||
|
async def config_member_field(self, interaction: discord.Interaction, champ: str, actif: bool):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
config = load_config()
|
||||||
|
config["memberFields"][champ] = actif
|
||||||
|
save_config(config)
|
||||||
|
state = "inclus ✅" if actif else "exclu ❌"
|
||||||
|
embed = self._embed("✅ Champ members.json mis à jour", discord.Color.green(),
|
||||||
|
f"`{champ}` est maintenant **{state}** de members.json.")
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
# ── reset ─────────────────────────────────────────────────────────────────
|
||||||
|
@json_config_group.command(name="reset", description="Remet la configuration par défaut")
|
||||||
|
async def config_reset(self, interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
from src.json_export_service import CONFIG_FILE
|
||||||
|
try:
|
||||||
|
CONFIG_FILE.unlink(missing_ok=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
load_config() # recrée depuis DEFAULT_CONFIG
|
||||||
|
embed = self._embed("✅ Configuration réinitialisée", discord.Color.green(),
|
||||||
|
"Tous les paramètres ont été remis à leurs valeurs par défaut.")
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
# ── set-output-dir ────────────────────────────────────────────────────────
|
||||||
|
@json_config_group.command(name="set-output-dir", description="Change le dossier de sortie des fichiers JSON")
|
||||||
|
@app_commands.describe(chemin="Chemin absolu ou relatif vers le dossier (ex: /var/www/site/data)")
|
||||||
|
async def config_set_output_dir(self, interaction: discord.Interaction, chemin: str):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
from pathlib import Path as _Path
|
||||||
|
p = _Path(chemin)
|
||||||
|
try:
|
||||||
|
p.mkdir(parents=True, exist_ok=True)
|
||||||
|
config = load_config()
|
||||||
|
config["outputDir"] = str(p)
|
||||||
|
save_config(config)
|
||||||
|
embed = self._embed(
|
||||||
|
"✅ Dossier de sortie mis à jour", discord.Color.green(),
|
||||||
|
f"Les JSON seront désormais écrits dans :\n`{p.resolve()}`",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
embed = self._embed("❌ Erreur", discord.Color.red(), f"Impossible d'utiliser ce chemin :\n```{e}```")
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
# ── add-founder-role ──────────────────────────────────────────────────────
|
||||||
|
@json_config_group.command(name="add-founder-role", description="Ajoute un nom de rôle reconnu comme 'fondateur'")
|
||||||
|
@app_commands.describe(nom="Nom du rôle (insensible à la casse, correspondance partielle)")
|
||||||
|
async def config_add_founder_role(self, interaction: discord.Interaction, nom: str):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
config = load_config()
|
||||||
|
roles = config.get("founderRoles", [])
|
||||||
|
nom_l = nom.lower().strip()
|
||||||
|
if nom_l in [r.lower() for r in roles]:
|
||||||
|
embed = self._embed("ℹ️ Déjà présent", discord.Color.yellow(),
|
||||||
|
f"`{nom}` est déjà dans la liste des rôles fondateurs.")
|
||||||
|
else:
|
||||||
|
roles.append(nom_l)
|
||||||
|
config["founderRoles"] = roles
|
||||||
|
save_config(config)
|
||||||
|
embed = self._embed(
|
||||||
|
"✅ Rôle fondateur ajouté", discord.Color.green(),
|
||||||
|
f"`{nom_l}` ajouté. Liste actuelle : {', '.join(f'`{r}`' for r in roles)}",
|
||||||
|
)
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
# ── remove-founder-role ───────────────────────────────────────────────────
|
||||||
|
@json_config_group.command(name="remove-founder-role", description="Retire un nom de rôle fondateur")
|
||||||
|
@app_commands.describe(nom="Nom du rôle à retirer")
|
||||||
|
async def config_remove_founder_role(self, interaction: discord.Interaction, nom: str):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
config = load_config()
|
||||||
|
roles = config.get("founderRoles", [])
|
||||||
|
nom_l = nom.lower().strip()
|
||||||
|
updated = [r for r in roles if r.lower() != nom_l]
|
||||||
|
if len(updated) == len(roles):
|
||||||
|
embed = self._embed("ℹ️ Introuvable", discord.Color.yellow(),
|
||||||
|
f"`{nom}` n'est pas dans la liste des rôles fondateurs.")
|
||||||
|
else:
|
||||||
|
config["founderRoles"] = updated
|
||||||
|
save_config(config)
|
||||||
|
reste = ', '.join(f'`{r}`' for r in updated) or "_Aucun_"
|
||||||
|
embed = self._embed(
|
||||||
|
"✅ Rôle fondateur retiré", discord.Color.green(),
|
||||||
|
f"`{nom_l}` retiré. Liste actuelle : {reste}",
|
||||||
|
)
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════
|
||||||
|
# /json-preview
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════
|
||||||
|
@app_commands.command(name="json-preview", description="Aperçu des données sans écrire les fichiers")
|
||||||
|
@app_commands.describe(
|
||||||
|
cible="Quoi prévisualiser ?",
|
||||||
|
limite="Nombre d'éléments à afficher (défaut: 3)",
|
||||||
|
)
|
||||||
|
@app_commands.choices(cible=[
|
||||||
|
app_commands.Choice(name="Serveurs", value="servers"),
|
||||||
|
app_commands.Choice(name="Membres", value="members"),
|
||||||
|
])
|
||||||
|
@app_commands.default_permissions(administrator=True)
|
||||||
|
async def json_preview(self, interaction: discord.Interaction, cible: str = "servers", limite: int = 3):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
limite = max(1, min(limite, 10)) # entre 1 et 10
|
||||||
|
|
||||||
|
try:
|
||||||
|
embed = self._embed(f"🔍 Aperçu — {cible}", discord.Color.blurple())
|
||||||
|
embed.set_footer(text="Aperçu uniquement — aucun fichier écrit")
|
||||||
|
|
||||||
|
if cible == "servers":
|
||||||
|
data = await preview_servers(self.bot, limit=limite)
|
||||||
|
for s in data:
|
||||||
|
val = f"ID: `{s['id']}`"
|
||||||
|
if "memberCount" in s:
|
||||||
|
val += f"\nMembres: {s['memberCount']}"
|
||||||
|
if s.get("icon"):
|
||||||
|
val += f"\nIcon: {s['icon']}"
|
||||||
|
embed.add_field(name=f"🌐 {s['name']}", value=val, inline=False)
|
||||||
|
|
||||||
|
else: # members
|
||||||
|
data = await preview_members(self.bot, limit=limite)
|
||||||
|
embed.description = f"Serveur : **{data['guildName']}**"
|
||||||
|
for m in data["members"]:
|
||||||
|
roles_str = ", ".join(m["roles"][:5]) or "_Aucun_"
|
||||||
|
if len(m["roles"]) > 5:
|
||||||
|
roles_str += f" +{len(m['roles'])-5}"
|
||||||
|
embed.add_field(
|
||||||
|
name=f"👤 {m['displayName']} ({m['username']})",
|
||||||
|
value=f"ID: `{m['id']}`\nRôles: {roles_str}",
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
embed = self._embed("❌ Erreur", discord.Color.red(), f"```{e}```")
|
||||||
|
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════
|
||||||
|
# /json-history
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════
|
||||||
|
@app_commands.command(name="json-history", description="Historique des dernières générations automatiques et manuelles")
|
||||||
|
@app_commands.describe(limite="Nombre d'entrées à afficher (défaut: 5, max: 10)")
|
||||||
|
@app_commands.default_permissions(administrator=True)
|
||||||
|
async def json_history(self, interaction: discord.Interaction, limite: int = 5):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
limite = max(1, min(limite, 10))
|
||||||
|
history = get_history()
|
||||||
|
|
||||||
|
embed = self._embed("📜 Historique des générations JSON", discord.Color.blurple())
|
||||||
|
|
||||||
|
if not history:
|
||||||
|
embed.description = "_Aucune génération enregistrée pour l'instant._"
|
||||||
|
else:
|
||||||
|
for entry in history[:limite]:
|
||||||
|
date_str = datetime.fromisoformat(entry["date"]).strftime("%d/%m/%Y %H:%M")
|
||||||
|
trigger = "🤖 Auto" if entry.get("trigger") == "auto" else "👤 Manuel"
|
||||||
|
status = "✅" if entry.get("success") else "⚠️"
|
||||||
|
|
||||||
|
lines = [f"{trigger} — {date_str}"]
|
||||||
|
if entry.get("serversCount") is not None:
|
||||||
|
lines.append(f"📋 {entry['serversCount']} serveurs")
|
||||||
|
if entry.get("membersCount") is not None:
|
||||||
|
lines.append(f"👥 {entry['membersCount']} membres ({entry.get('guildName', '?')})")
|
||||||
|
if entry.get("errors"):
|
||||||
|
lines.append(f"❌ Erreurs: {'; '.join(entry['errors'])}")
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name=f"{status} Génération du {date_str}",
|
||||||
|
value="\n".join(lines),
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Setup ───────────────────────────────────────────────────────────────────
|
||||||
|
async def setup(bot: commands.Bot):
|
||||||
|
cog = WebsiteSync(bot)
|
||||||
|
await bot.add_cog(cog)
|
||||||
|
# Enregistre le groupe de commandes json-config
|
||||||
|
bot.tree.add_command(cog.json_config_group)
|
||||||
|
logger.info("Cog WebsiteSync chargé.")
|
||||||
380
src/json_export_service.py
Normal file
380
src/json_export_service.py
Normal file
|
|
@ -0,0 +1,380 @@
|
||||||
|
"""
|
||||||
|
Service d'export JSON pour le site web.
|
||||||
|
Génère deux fichiers :
|
||||||
|
- servers.json : tous les serveurs où le bot est présent
|
||||||
|
- members.json : tous les membres (sans bots) d'un serveur cible
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import discord
|
||||||
|
|
||||||
|
logger = logging.getLogger("kuby.json_export")
|
||||||
|
|
||||||
|
# ─── Chemins ────────────────────────────────────────────────────────────────
|
||||||
|
CONFIG_FILE = Path("data/json-export-config.json")
|
||||||
|
HISTORY_FILE = Path("data/json-export-history.json")
|
||||||
|
|
||||||
|
def _get_output_dir() -> Path:
|
||||||
|
"""Lit le dossier de sortie depuis la config (ou .env en fallback)."""
|
||||||
|
if CONFIG_FILE.exists():
|
||||||
|
try:
|
||||||
|
saved = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
||||||
|
if saved.get("outputDir"):
|
||||||
|
return Path(saved["outputDir"])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return Path(os.getenv("JSON_OUTPUT_DIR", "data/website-json"))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def OUTPUT_DIR() -> Path:
|
||||||
|
return _get_output_dir()
|
||||||
|
|
||||||
|
# Propriétés dynamiques — recalculées à chaque accès
|
||||||
|
def get_output_dir() -> Path:
|
||||||
|
return _get_output_dir()
|
||||||
|
|
||||||
|
def get_servers_file() -> Path:
|
||||||
|
return _get_output_dir() / "servers.json"
|
||||||
|
|
||||||
|
def get_members_file() -> Path:
|
||||||
|
return _get_output_dir() / "members.json"
|
||||||
|
|
||||||
|
# Aliases statiques utilisés dans les imports existants (valeur initiale)
|
||||||
|
OUTPUT_DIR = _get_output_dir()
|
||||||
|
SERVERS_FILE = OUTPUT_DIR / "servers.json"
|
||||||
|
MEMBERS_FILE = OUTPUT_DIR / "members.json"
|
||||||
|
|
||||||
|
# ─── Config par défaut ───────────────────────────────────────────────────────
|
||||||
|
DEFAULT_CONFIG: dict = {
|
||||||
|
# ID du serveur ciblé pour members.json (ne change jamais)
|
||||||
|
"targetGuildId": os.getenv("TARGET_GUILD_ID", ""),
|
||||||
|
|
||||||
|
# Heure de génération automatique quotidienne (format 24h)
|
||||||
|
"autoHour": 3,
|
||||||
|
"autoMinute": 0,
|
||||||
|
|
||||||
|
# Dossier de sortie (vide = utilise JSON_OUTPUT_DIR du .env)
|
||||||
|
"outputDir": os.getenv("JSON_OUTPUT_DIR", "data/website-json"),
|
||||||
|
|
||||||
|
# Noms de rôles reconnus comme "fondateur" (insensible à la casse)
|
||||||
|
"founderRoles": ["fondateur", "founder"],
|
||||||
|
|
||||||
|
# Champs inclus dans servers.json
|
||||||
|
"serverFields": {
|
||||||
|
"id": True,
|
||||||
|
"name": True,
|
||||||
|
"icon": True, # URL pp du serveur
|
||||||
|
"banner": True, # URL bannière
|
||||||
|
"owner": True, # Propriétaire + rôle fondateur
|
||||||
|
"memberCount": True, # Membres humains uniquement
|
||||||
|
"description": True,
|
||||||
|
"createdAt": True,
|
||||||
|
"boostLevel": False,
|
||||||
|
"boostCount": False,
|
||||||
|
"verificationLevel": False,
|
||||||
|
},
|
||||||
|
|
||||||
|
# Champs inclus dans members.json
|
||||||
|
"memberFields": {
|
||||||
|
"id": True,
|
||||||
|
"username": True,
|
||||||
|
"displayName": True, # Pseudo sur le serveur
|
||||||
|
"globalName": True, # Nom d'affichage global
|
||||||
|
"avatar": True, # URL avatar (serveur + global)
|
||||||
|
"bio": True, # Description (toujours null, limite API Discord)
|
||||||
|
"roles": True, # Tous les rôles avec couleur
|
||||||
|
"joinedAt": True, # Date d'arrivée sur le serveur
|
||||||
|
"createdAt": True, # Date de création du compte
|
||||||
|
"badges": False, # Badges du compte
|
||||||
|
"accentColor": False, # Couleur de profil
|
||||||
|
"isBot": False, # Flag bot (les bots sont toujours exclus de la liste)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Helpers config ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def load_config() -> dict:
|
||||||
|
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if not CONFIG_FILE.exists():
|
||||||
|
save_config(DEFAULT_CONFIG)
|
||||||
|
return dict(DEFAULT_CONFIG)
|
||||||
|
try:
|
||||||
|
saved = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
||||||
|
# Merge avec les valeurs par défaut pour les nouvelles clés
|
||||||
|
merged = {**DEFAULT_CONFIG, **saved}
|
||||||
|
merged["serverFields"] = {**DEFAULT_CONFIG["serverFields"], **saved.get("serverFields", {})}
|
||||||
|
merged["memberFields"] = {**DEFAULT_CONFIG["memberFields"], **saved.get("memberFields", {})}
|
||||||
|
merged["founderRoles"] = saved.get("founderRoles", DEFAULT_CONFIG["founderRoles"])
|
||||||
|
merged["outputDir"] = saved.get("outputDir", DEFAULT_CONFIG["outputDir"])
|
||||||
|
return merged
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur lecture config : {e}")
|
||||||
|
return dict(DEFAULT_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
|
def save_config(config: dict) -> None:
|
||||||
|
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
CONFIG_FILE.write_text(json.dumps(config, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Historique des générations ──────────────────────────────────────────────
|
||||||
|
MAX_HISTORY = 20 # nombre max d'entrées conservées
|
||||||
|
|
||||||
|
def _load_history() -> list:
|
||||||
|
if not HISTORY_FILE.exists():
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
return json.loads(HISTORY_FILE.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _save_history(history: list) -> None:
|
||||||
|
HISTORY_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
HISTORY_FILE.write_text(json.dumps(history, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||||
|
|
||||||
|
def log_history(result: dict) -> None:
|
||||||
|
"""Ajoute une entrée dans l'historique après une génération."""
|
||||||
|
history = _load_history()
|
||||||
|
entry = {
|
||||||
|
"date": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"trigger": result.get("trigger", "auto"), # "auto" ou "manual"
|
||||||
|
"serversCount": result["servers"]["count"] if result.get("servers") else None,
|
||||||
|
"membersCount": result["members"]["count"] if result.get("members") else None,
|
||||||
|
"guildName": result["members"]["guildName"] if result.get("members") else None,
|
||||||
|
"errors": result.get("errors", []),
|
||||||
|
"success": len(result.get("errors", [])) == 0,
|
||||||
|
}
|
||||||
|
history.insert(0, entry)
|
||||||
|
_save_history(history[:MAX_HISTORY])
|
||||||
|
|
||||||
|
def get_history() -> list:
|
||||||
|
return _load_history()
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Génération servers.json ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def generate_servers_json(bot: discord.Client) -> dict:
|
||||||
|
config = load_config()
|
||||||
|
fields = config["serverFields"]
|
||||||
|
founder_keys = [r.lower() for r in config.get("founderRoles", ["fondateur", "founder"])]
|
||||||
|
out_dir = Path(config["outputDir"])
|
||||||
|
servers_file = out_dir / "servers.json"
|
||||||
|
servers = []
|
||||||
|
|
||||||
|
for guild in bot.guilds:
|
||||||
|
try:
|
||||||
|
guild = await bot.fetch_guild(guild.id, with_counts=False)
|
||||||
|
except Exception:
|
||||||
|
pass # on travaille avec la version cache
|
||||||
|
|
||||||
|
entry = {}
|
||||||
|
|
||||||
|
if fields.get("id"): entry["id"] = str(guild.id)
|
||||||
|
if fields.get("name"): entry["name"] = guild.name
|
||||||
|
if fields.get("icon"): entry["icon"] = str(guild.icon.url) if guild.icon else None
|
||||||
|
if fields.get("banner"): entry["banner"] = str(guild.banner.url) if guild.banner else None
|
||||||
|
if fields.get("description"): entry["description"] = guild.description
|
||||||
|
if fields.get("createdAt"): entry["createdAt"] = guild.created_at.isoformat()
|
||||||
|
if fields.get("boostLevel"): entry["boostLevel"] = guild.premium_tier
|
||||||
|
if fields.get("boostCount"): entry["boostCount"] = guild.premium_subscription_count
|
||||||
|
if fields.get("verificationLevel"):
|
||||||
|
entry["verificationLevel"] = str(guild.verification_level)
|
||||||
|
|
||||||
|
if fields.get("memberCount"):
|
||||||
|
try:
|
||||||
|
members = [m async for m in guild.fetch_members(limit=None)]
|
||||||
|
entry["memberCount"] = sum(1 for m in members if not m.bot)
|
||||||
|
except Exception:
|
||||||
|
entry["memberCount"] = guild.member_count
|
||||||
|
|
||||||
|
if fields.get("owner"):
|
||||||
|
founders = []
|
||||||
|
try:
|
||||||
|
owner = await guild.fetch_member(guild.owner_id)
|
||||||
|
founders.append({
|
||||||
|
"id": str(owner.id),
|
||||||
|
"username": owner.name,
|
||||||
|
"displayName": owner.display_name,
|
||||||
|
"avatar": str(owner.display_avatar.url),
|
||||||
|
"isOwner": True,
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Membres avec un rôle dont le nom correspond à founderRoles
|
||||||
|
try:
|
||||||
|
async for member in guild.fetch_members(limit=None):
|
||||||
|
if member.bot or member.id == guild.owner_id:
|
||||||
|
continue
|
||||||
|
for role in member.roles:
|
||||||
|
if any(key in role.name.lower() for key in founder_keys):
|
||||||
|
founders.append({
|
||||||
|
"id": str(member.id),
|
||||||
|
"username": member.name,
|
||||||
|
"displayName": member.display_name,
|
||||||
|
"avatar": str(member.display_avatar.url),
|
||||||
|
"isOwner": False,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
entry["founders"] = founders
|
||||||
|
|
||||||
|
servers.append(entry)
|
||||||
|
|
||||||
|
output = {
|
||||||
|
"generatedAt": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"count": len(servers),
|
||||||
|
"servers": servers,
|
||||||
|
}
|
||||||
|
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
servers_file.write_text(json.dumps(output, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||||
|
logger.info(f"servers.json généré → {servers_file} ({len(servers)} serveurs)")
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Génération members.json ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def generate_members_json(bot: discord.Client, guild_id: Optional[int] = None) -> dict:
|
||||||
|
config = load_config()
|
||||||
|
fields = config["memberFields"]
|
||||||
|
target_id = guild_id or (int(config["targetGuildId"]) if config["targetGuildId"] else None)
|
||||||
|
out_dir = Path(config["outputDir"])
|
||||||
|
members_file = out_dir / "members.json"
|
||||||
|
|
||||||
|
if not target_id:
|
||||||
|
raise ValueError("Aucun targetGuildId configuré. Utilise /json-config set-guild <id>")
|
||||||
|
|
||||||
|
guild = bot.get_guild(target_id) or await bot.fetch_guild(target_id)
|
||||||
|
if not guild:
|
||||||
|
raise ValueError(f"Serveur introuvable : {target_id}")
|
||||||
|
|
||||||
|
members_list = []
|
||||||
|
async for member in guild.fetch_members(limit=None):
|
||||||
|
if member.bot:
|
||||||
|
continue
|
||||||
|
|
||||||
|
entry = {}
|
||||||
|
|
||||||
|
if fields.get("id"): entry["id"] = str(member.id)
|
||||||
|
if fields.get("username"): entry["username"] = member.name
|
||||||
|
if fields.get("displayName"): entry["displayName"] = member.display_name
|
||||||
|
if fields.get("globalName"): entry["globalName"] = member.global_name or member.name
|
||||||
|
if fields.get("avatar"):
|
||||||
|
entry["avatar"] = str(member.display_avatar.url)
|
||||||
|
entry["globalAvatar"] = str(member.avatar.url) if member.avatar else str(member.default_avatar.url)
|
||||||
|
if fields.get("joinedAt"): entry["joinedAt"] = member.joined_at.isoformat() if member.joined_at else None
|
||||||
|
if fields.get("createdAt"): entry["createdAt"] = member.created_at.isoformat()
|
||||||
|
if fields.get("isBot"): entry["isBot"] = member.bot
|
||||||
|
if fields.get("bio"): entry["bio"] = None # Non accessible via l'API Bot officielle
|
||||||
|
if fields.get("accentColor"): entry["accentColor"] = None # Nécessite fetch user OAuth2
|
||||||
|
|
||||||
|
if fields.get("roles"):
|
||||||
|
entry["roles"] = [
|
||||||
|
{
|
||||||
|
"id": str(r.id),
|
||||||
|
"name": r.name,
|
||||||
|
"color": str(r.color),
|
||||||
|
"position": r.position,
|
||||||
|
"hoist": r.hoist,
|
||||||
|
}
|
||||||
|
for r in sorted(member.roles, key=lambda r: -r.position)
|
||||||
|
if r.id != guild.id # exclut @everyone
|
||||||
|
]
|
||||||
|
|
||||||
|
if fields.get("badges"):
|
||||||
|
entry["badges"] = [flag.name for flag in member.public_flags.all()]
|
||||||
|
|
||||||
|
members_list.append(entry)
|
||||||
|
|
||||||
|
output = {
|
||||||
|
"generatedAt": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"guildId": str(guild.id),
|
||||||
|
"guildName": guild.name,
|
||||||
|
"count": len(members_list),
|
||||||
|
"members": members_list,
|
||||||
|
}
|
||||||
|
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
members_file.write_text(json.dumps(output, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||||
|
logger.info(f"members.json généré → {members_file} ({len(members_list)} membres)")
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Génération complète ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def generate_all(bot: discord.Client, trigger: str = "auto") -> dict:
|
||||||
|
results = {"servers": None, "members": None, "errors": [], "trigger": trigger}
|
||||||
|
try:
|
||||||
|
results["servers"] = await generate_servers_json(bot)
|
||||||
|
except Exception as e:
|
||||||
|
results["errors"].append(f"servers: {e}")
|
||||||
|
logger.error(f"Erreur génération servers.json : {e}")
|
||||||
|
try:
|
||||||
|
results["members"] = await generate_members_json(bot)
|
||||||
|
except Exception as e:
|
||||||
|
results["errors"].append(f"members: {e}")
|
||||||
|
logger.error(f"Erreur génération members.json : {e}")
|
||||||
|
|
||||||
|
log_history(results)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Prévisualisation (sans écriture fichier) ────────────────────────────────
|
||||||
|
|
||||||
|
async def preview_servers(bot: discord.Client, limit: int = 3) -> list:
|
||||||
|
"""Retourne un aperçu de N serveurs sans écrire le fichier."""
|
||||||
|
config = load_config()
|
||||||
|
fields = config["serverFields"]
|
||||||
|
founder_keys = [r.lower() for r in config.get("founderRoles", ["fondateur"])]
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for guild in list(bot.guilds)[:limit]:
|
||||||
|
entry = {
|
||||||
|
"id": str(guild.id),
|
||||||
|
"name": guild.name,
|
||||||
|
"icon": str(guild.icon.url) if guild.icon else None,
|
||||||
|
}
|
||||||
|
if fields.get("memberCount"):
|
||||||
|
try:
|
||||||
|
members = [m async for m in guild.fetch_members(limit=None)]
|
||||||
|
entry["memberCount"] = sum(1 for m in members if not m.bot)
|
||||||
|
except Exception:
|
||||||
|
entry["memberCount"] = guild.member_count
|
||||||
|
results.append(entry)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
async def preview_members(bot: discord.Client, limit: int = 5) -> dict:
|
||||||
|
"""Retourne un aperçu de N membres sans écrire le fichier."""
|
||||||
|
config = load_config()
|
||||||
|
target_id = int(config["targetGuildId"]) if config["targetGuildId"] else None
|
||||||
|
if not target_id:
|
||||||
|
raise ValueError("Aucun targetGuildId configuré.")
|
||||||
|
|
||||||
|
guild = bot.get_guild(target_id) or await bot.fetch_guild(target_id)
|
||||||
|
preview = []
|
||||||
|
count = 0
|
||||||
|
async for member in guild.fetch_members(limit=None):
|
||||||
|
if member.bot:
|
||||||
|
continue
|
||||||
|
preview.append({
|
||||||
|
"id": str(member.id),
|
||||||
|
"username": member.name,
|
||||||
|
"displayName": member.display_name,
|
||||||
|
"avatar": str(member.display_avatar.url),
|
||||||
|
"roles": [r.name for r in member.roles if r.id != guild.id],
|
||||||
|
})
|
||||||
|
count += 1
|
||||||
|
if count >= limit:
|
||||||
|
break
|
||||||
|
return {"guildName": guild.name, "totalFetched": limit, "members": preview}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue