Mise à jour du système de whitelist, sécurité et logging

This commit is contained in:
Mathis 2026-03-29 19:29:17 +02:00
parent d82f704c75
commit 1e384032be
7 changed files with 199 additions and 145 deletions

View file

@ -0,0 +1,15 @@
# À RÉGLER - Session Suivante (Ticket Whitelist)
### Bug Identifié
La commande **"validé"** par message (on_message) n'est pas détectée ou ne réagit pas si le ticket a été refermé puis réouvert avec le statut "Ouvert".
### État Actuel
- Le bouton "Fermer" fonctionne et détecte l'ID via Regex.
- Le bouton "Réouvrir" synchronise maintenant les rôles (Écrit/Oral/Accepté).
- Le design des MPs (Orange/Vert) est restauré conforme aux captures.
- **Reste** : Vérifier pourquoi `on_message` ignore parfois le message "validé" (probablement un problème de sujet (topic) ou de permissions au moment de la lecture).
### Prochaine Étape
1. Lancer le bot.
2. Ouvrir/Réouvrir un ticket.
3. Taper "validé" et observer les messages d'erreur explicites (❌) que j'ai ajoutés.

View file

@ -9,10 +9,12 @@ import os
import asyncio import asyncio
kuby_logger = logging.getLogger("KubyBot") kuby_logger = logging.getLogger("KubyBot")
REPORTS_FILE = "data/gitlab_reports.json" # Utilisation de chemins absolus pour la persistance après reboot
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
REPORTS_FILE = os.path.join(BASE_DIR, "data", "gitlab_reports.json")
def save_report(issue_iid, user_id): def save_report(issue_iid, user_id):
os.makedirs("data", exist_ok=True) os.makedirs(os.path.dirname(REPORTS_FILE), exist_ok=True)
try: try:
if os.path.exists(REPORTS_FILE): if os.path.exists(REPORTS_FILE):
with open(REPORTS_FILE, "r") as f: with open(REPORTS_FILE, "r") as f:
@ -152,7 +154,23 @@ class BugReport(commands.Cog):
self.bot.loop.create_task(cleanup()) self.bot.loop.create_task(cleanup())
async def handle_bot_event(self, request): async def handle_bot_event(self, request):
# Vérification de la sécurité (Secret Token GitLab)
webhook_secret = os.getenv("GITLAB_WEBHOOK_SECRET")
if webhook_secret:
provided_token = request.headers.get("X-Gitlab-Token")
if provided_token != webhook_secret:
kuby_logger.warning("Unauthorised webhook attempt detected (invalid X-Gitlab-Token).")
return web.json_response({"status": "unauthorised"}, status=401)
try: try:
# On attend que le bot soit prêt si on vient de rebooter
if not self.bot.is_ready():
kuby_logger.info("Webhook received but bot is not ready yet. Waiting up to 10s...")
try:
await asyncio.wait_for(self.bot.wait_until_ready(), timeout=10.0)
except asyncio.TimeoutError:
kuby_logger.warning("Bot still not ready after 10s. Proceeding anyway (best effort).")
payload = await request.json() payload = await request.json()
event_type = payload.get("object_kind") event_type = payload.get("object_kind")

View file

@ -960,53 +960,49 @@ class Security(commands.Cog):
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.
""" """
# On diffère la réponse immédiatement car le chargement des réglages peut prendre du temps
await interaction.response.defer(ephemeral=True)
# Vérification des permissions # Vérification des permissions
if not is_immune(interaction.guild, interaction.user): if not is_immune(interaction.guild, interaction.user):
if interaction.response.is_done(): return await interaction.followup.send(
return await interaction.followup.send(
"❌ **Accès refusé**\n\nVous devez être administrateur, propriétaire du serveur, ou être dans la whitelist pour accéder à ce panneau.",
ephemeral=True
)
return await interaction.response.send_message(
"❌ **Accès refusé**\n\nVous devez être administrateur, propriétaire du serveur, ou être dans la whitelist pour accéder à ce panneau.", "❌ **Accès refusé**\n\nVous devez être administrateur, propriétaire du serveur, ou être dans la whitelist pour accéder à ce panneau.",
ephemeral=True ephemeral=True
) )
# Chargement des paramètres try:
settings = load_settings(interaction.guild.id) # Chargement des paramètres
settings = load_settings(interaction.guild.id)
# Création de la vue # Création de la vue
view = SecurityView(interaction, settings, bot=self.bot) view = SecurityView(interaction, settings, bot=self.bot)
embed = view.create_main_embed(interaction.user) embed = view.create_main_embed(interaction.user)
if interaction.response.is_done():
await interaction.followup.send( await interaction.followup.send(
embed=embed, embed=embed,
view=view, view=view,
ephemeral=True ephemeral=True
) )
else: except Exception as e:
await interaction.response.send_message( await interaction.followup.send(
embed=embed, f"❌ Une erreur est survenue lors de l'ouverture du panneau : {e}",
view=view,
ephemeral=True ephemeral=True
) )
@security.error @security.error
async def security_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError): async def security_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError):
"""Gestion des erreurs de la commande security""" """Gestion des erreurs de la commande security"""
# Ne pas envoyer de message si l'interaction a déjà été traitée # Utilisation de followup si l'interaction est déjà différée ou répondue
if interaction.response.is_done(): send_method = interaction.followup.send if interaction.response.is_done() else interaction.response.send_message
return
if isinstance(error, app_commands.MissingPermissions): if isinstance(error, app_commands.MissingPermissions):
await interaction.response.send_message( await send_method(
"❌ Permissions insuffisantes.", "❌ Permissions insuffisantes.",
ephemeral=True ephemeral=True
) )
else: else:
await interaction.response.send_message( await send_method(
f"❌ Une erreur est survenue.", f"❌ Une erreur est survenue lors de l'exécution de la commande : {error}",
ephemeral=True ephemeral=True
) )

View file

@ -1,4 +1,5 @@
import discord import discord
import re
from discord.ext import commands from discord.ext import commands
from discord import app_commands, ui from discord import app_commands, ui
import json import json
@ -313,26 +314,28 @@ class TicketStep3(discord.ui.Modal, title="📋 Développement du Personnage"):
except discord.errors.NotFound: except discord.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.View): class TicketManagementView(discord.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: discord.Interaction):
"""Récupère le propriétaire du ticket via le topic du salon (robuste)""" """Récupère le propriétaire du ticket via le topic du salon (robuste via regex)"""
if interaction.channel.topic and "Ticket de" in interaction.channel.topic: import re
topic = interaction.channel.topic
if not topic:
return None
match = re.search(r"(\d{17,20})", topic)
if match:
owner_id = int(match.group(1))
try: try:
user_id_str = interaction.channel.topic.replace("Ticket de ", "").strip()
owner_id = int(user_id_str)
member = interaction.guild.get_member(owner_id) member = interaction.guild.get_member(owner_id)
if not member: if not member:
try: member = await interaction.guild.fetch_member(owner_id)
member = await interaction.guild.fetch_member(owner_id)
except (discord.NotFound, discord.HTTPException):
return None
return member return member
except (ValueError, IndexError): except:
return None return None
return None return None
@ -390,26 +393,28 @@ class TicketManagementView(discord.ui.view.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.View): class ClosedTicketView(discord.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: discord.Interaction):
"""Récupère le propriétaire du ticket via le topic du salon (robuste)""" """Récupère le propriétaire du ticket via le topic du salon (robuste via regex)"""
if interaction.channel.topic and "Ticket de" in interaction.channel.topic: import re
topic = interaction.channel.topic
if not topic:
return None
match = re.search(r"(\d{17,20})", topic)
if match:
owner_id = int(match.group(1))
try: try:
user_id_str = interaction.channel.topic.replace("Ticket de ", "").strip()
owner_id = int(user_id_str)
member = interaction.guild.get_member(owner_id) member = interaction.guild.get_member(owner_id)
if not member: if not member:
try: member = await interaction.guild.fetch_member(owner_id)
member = await interaction.guild.fetch_member(owner_id)
except (discord.NotFound, discord.HTTPException):
return None
return member return member
except (ValueError, IndexError): except:
return None return None
return None return None
@ -443,7 +448,7 @@ class ClosedTicketView(discord.ui.view.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.View): class ReopenStatusView(discord.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: discord.Member):
super().__init__(timeout=180) super().__init__(timeout=180)
@ -464,16 +469,48 @@ class ReopenStatusView(discord.ui.view.View):
async def apply_status(self, interaction: discord.Interaction, prefix: str): async def apply_status(self, interaction: discord.Interaction, prefix: str):
await interaction.response.defer(ephemeral=True) await interaction.response.defer(ephemeral=True)
try: try:
settings = load_settings(interaction.guild.id)
candidature_a_faire_role_id = settings.get("candidature_a_faire_role_id")
attente_oral_role_id = settings.get("attente_oral_role_id")
citoyen_role_id = settings.get("citoyen_role_id")
# On récupère les objets rôles
role_écrit = interaction.guild.get_role(int(candidature_a_faire_role_id)) if candidature_a_faire_role_id else None
role_oral = interaction.guild.get_role(int(attente_oral_role_id)) if attente_oral_role_id else None
role_citoyen = interaction.guild.get_role(int(citoyen_role_id)) if citoyen_role_id else None
# Synchronisation des RÔLES en fonction du statut choisi
roles_to_remove = []
role_to_add = None
if prefix == "ouvert":
role_to_add = role_écrit
roles_to_remove = [r for r in [role_oral, role_citoyen] if r]
elif prefix == "oral-à-faire":
role_to_add = role_oral
roles_to_remove = [r for r in [role_écrit, role_citoyen] if r]
elif prefix == "accepté":
role_to_add = role_citoyen
roles_to_remove = [r for r in [role_écrit, role_oral] if r]
# Mise à jour des rôles sur le membre
if roles_to_remove:
await self.owner.remove_roles(*roles_to_remove, reason="Réouverture du ticket - Maj Statut")
if role_to_add:
await self.owner.add_roles(role_to_add, reason="Réouverture du ticket - Maj Statut")
# Mise à jour du SALON
await interaction.channel.set_permissions(self.owner, read_messages=True, send_messages=True) await interaction.channel.set_permissions(self.owner, read_messages=True, send_messages=True)
new_name = f"{prefix}-{self.owner.display_name}".replace(" ", "-").lower()[:100] new_name = f"{prefix}-{self.owner.display_name}".replace(" ", "-").lower()[:100]
await interaction.channel.edit(name=new_name) await interaction.channel.edit(name=new_name, topic=f"Ticket de {self.owner.id}")
new_view = TicketManagementView() new_view = TicketManagementView()
await interaction.channel.send(f"✅ Ticket réouvert par {interaction.user.mention} (Statut: {prefix}).", view=new_view) await interaction.channel.send(f"✅ Ticket réouvert par {interaction.user.mention} (Statut: **{prefix}**).", view=new_view)
await interaction.followup.send(f"✅ Statut {prefix} appliqué.", ephemeral=True) await interaction.followup.send(f"✅ Statut **{prefix}** appliqué et rôles synchronisés.", ephemeral=True)
self.stop() self.stop()
except Exception as e: except Exception as e:
await interaction.followup.send(f"❌ Erreur : {e}", ephemeral=True) kuby_logger.error(f"Erreur lors de la réouverture/synchro: {e}")
await interaction.followup.send(f"❌ Erreur système : {e}", ephemeral=True)
@discord.ui.button(label="❌ Supprimer le Ticket", style=discord.ButtonStyle.danger, custom_id="delete_ticket") @discord.ui.button(label="❌ Supprimer le Ticket", style=discord.ButtonStyle.danger, custom_id="delete_ticket")
async def delete_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): async def delete_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
@ -565,8 +602,12 @@ class Ticket(commands.Cog):
if message.guild is None or message.author.bot: if message.guild is None or message.author.bot:
return return
# Détection du format du topic du ticket # Détection du format du topic du ticket via regex (robuste)
if not message.channel.topic or not message.channel.topic.startswith("Ticket de"): if not message.channel.topic:
return
match = re.search(r"(\d{17,20})", message.channel.topic)
if not match:
return return
# On ne traite que si le message contient "validé" # On ne traite que si le message contient "validé"
@ -585,12 +626,12 @@ class Ticket(commands.Cog):
return return
try: try:
# Extraction propre de l'ID utilisateur depuis le topic # L'ID est déjà trouvé par le regex ci-dessus
user_id = int(match.group(1))
try: try:
user_id_str = message.channel.topic.replace("Ticket de ", "").strip() user = await message.guild.fetch_member(user_id)
user = await message.guild.fetch_member(int(user_id_str)) except (discord.NotFound, discord.HTTPException):
except (ValueError, IndexError, discord.NotFound, discord.HTTPException): await message.channel.send("❌ Utilisateur introuvable. A-t-il quitté le serveur ?", delete_after=10)
kuby_logger.warning(f"Impossible de trouver l'utilisateur du ticket via le topic: {message.channel.topic}")
return return
attente_oral_role_id = settings.get("attente_oral_role_id") attente_oral_role_id = settings.get("attente_oral_role_id")
@ -605,7 +646,7 @@ class Ticket(commands.Cog):
candidature_a_faire_role = message.guild.get_role(int(candidature_a_faire_role_id)) candidature_a_faire_role = message.guild.get_role(int(candidature_a_faire_role_id))
citoyen_role = message.guild.get_role(int(citoyen_role_id)) citoyen_role = message.guild.get_role(int(citoyen_role_id))
if attente_oral_role in user.roles: if (attente_oral_role and attente_oral_role in user.roles) or "oral-à-faire" in message.channel.name.lower():
# DEUXIÈME VALIDATION -> ACCEPTÉ / CITOYEN # DEUXIÈME VALIDATION -> ACCEPTÉ / CITOYEN
if attente_oral_role: await user.remove_roles(attente_oral_role) if attente_oral_role: await user.remove_roles(attente_oral_role)
if citoyen_role: await user.add_roles(citoyen_role) if citoyen_role: await user.add_roles(citoyen_role)

View file

@ -64,11 +64,11 @@ class WebsiteSync(commands.Cog, name="Website Sync"):
def cog_unload(self): def cog_unload(self):
self.daily_export.cancel() self.daily_export.cancel()
# ── Tâche de fond : génération (30s pour test) ──────────────────────────── # ── Tâche de fond : génération (1 heure) ────────────────────────────
@tasks.loop(seconds=30) @tasks.loop(hours=1)
async def daily_export(self): async def daily_export(self):
"""Génère les JSON toutes les 30 secondes (Mode Test).""" """Génère les JSON toutes les heures."""
logger.info("[JSON Export] Génération automatique (toutes les 30s) déclenchée.") logger.info("[JSON Export] Génération automatique déclenchée.")
await generate_all(self.bot) await generate_all(self.bot)
@daily_export.before_loop @daily_export.before_loop

View file

@ -34,7 +34,7 @@ class AdvancedLogger:
"details": details or {} "details": details or {}
} }
self._write_log("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: discord.Member, role: discord.Role, event_type: str):
"""Log role changes (added/removed)""" """Log role changes (added/removed)"""
@ -49,7 +49,7 @@ class AdvancedLogger:
"role_permissions": role.permissions.value "role_permissions": role.permissions.value
} }
self._write_log("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: discord.Member, channel: discord.VoiceChannel, event_type: str):
"""Log voice channel events (join/leave)""" """Log voice channel events (join/leave)"""
@ -63,7 +63,7 @@ class AdvancedLogger:
"channel_type": str(channel.type) "channel_type": str(channel.type)
} }
self._write_log("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: discord.Member):
"""Log when a member leaves the server""" """Log when a member leaves the server"""
@ -85,10 +85,10 @@ class AdvancedLogger:
} }
# Save to member-specific log # Save to member-specific log
self._write_member_log(member.id, log_data) asyncio.create_task(self._write_member_log_async(member.id, log_data))
# Also save to general logs # Also save to general logs
self._write_log("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: discord.Member):
"""Log when a member joins the server""" """Log when a member joins the server"""
@ -101,10 +101,13 @@ class AdvancedLogger:
"member_avatar": str(member.avatar.url) if member.avatar else None "member_avatar": str(member.avatar.url) if member.avatar else None
} }
self._write_log("member_events", log_data) asyncio.create_task(self._write_log_async("member_events", log_data))
def check_previous_roles(self, member_id: int) -> Optional[Dict]: def check_previous_roles(self, member_id: int) -> Optional[Dict]:
"""Check if member has previous role data when rejoining""" """Check if member has previous role data when rejoining"""
# This one stays synchronous for now as it's called in on_member_join and needs immediate result
# but since it only reads ONE file, it's less problematic than writing to cumulative logs.
# Format update: it now needs to read NDJSON.
member_log_file = f"{self.member_logs_dir}/{member_id}.json" member_log_file = f"{self.member_logs_dir}/{member_id}.json"
if not os.path.exists(member_log_file): if not os.path.exists(member_log_file):
@ -112,7 +115,8 @@ class AdvancedLogger:
try: try:
with open(member_log_file, 'r', encoding='utf-8') as f: with open(member_log_file, 'r', encoding='utf-8') as f:
logs = json.load(f) # Read line by line for NDJSON
logs = [json.loads(line) for line in f if line.strip()]
# Find the most recent leave event # Find the most recent leave event
leave_events = [log for log in logs if log.get("event_type") == "member_leave"] leave_events = [log for log in logs if log.get("event_type") == "member_leave"]
@ -123,71 +127,39 @@ class AdvancedLogger:
kuby_logger.error(f"Error checking previous roles for {member_id}: {e}") kuby_logger.error(f"Error checking previous roles for {member_id}: {e}")
return None return None
def _write_log(self, log_type: str, data: Dict): async def _write_log_async(self, log_type: str, data: Dict):
"""Write log data to appropriate file safely with atomic write""" """Asynchronous wrapper for log writing"""
date_str = datetime.now().strftime("%Y-%m-%d") return await asyncio.to_thread(self._write_log_sync, log_type, data)
log_file = f"{self.logs_dir}/{log_type}_{date_str}.json"
temp_file = f"{log_file}.tmp" async def _write_member_log_async(self, member_id: int, data: Dict):
"""Asynchronous wrapper for member log writing"""
existing_logs = [] return await asyncio.to_thread(self._write_member_log_sync, member_id, data)
def _write_log_sync(self, log_type: str, data: Dict):
"""Write log data to appropriate file using NDJSON (Append-only)"""
try: try:
if os.path.exists(log_file): date_str = datetime.now().strftime("%Y-%m-%d")
with open(log_file, 'r', encoding='utf-8') as f: log_file = f"{self.logs_dir}/{log_type}_{date_str}.json"
try:
existing_logs = json.load(f)
except json.JSONDecodeError as e:
# Handle corrupted file by backing it up and starting fresh
corrupted_backup = f"{log_file}.corrupted"
os.rename(log_file, corrupted_backup)
kuby_logger.warning(f"Corrupted log file detected and backed up to {corrupted_backup}: {e}. Starting new log.")
existing_logs = []
existing_logs.append(data) # Simple append in NDJSON format
with open(log_file, 'a', encoding='utf-8') as f:
# Atomic write using a temporary file f.write(json.dumps(data, ensure_ascii=False) + "\n")
with open(temp_file, 'w', encoding='utf-8') as f:
json.dump(existing_logs, f, indent=2, ensure_ascii=False)
os.replace(temp_file, log_file)
except Exception as e: except Exception as e:
kuby_logger.error(f"Error writing log for {log_type}: {e}") # We don't use kuby_logger here to avoid infinite recursion if nested
if os.path.exists(temp_file): print(f"CRITICAL ERROR in AdvancedLogger._write_log_sync: {e}")
try: os.remove(temp_file)
except: pass
def _write_member_log(self, member_id: int, data: Dict): def _write_member_log_sync(self, member_id: int, data: Dict):
"""Write member-specific log safely with atomic write""" """Write member-specific log using NDJSON (Append-only)"""
member_log_file = f"{self.member_logs_dir}/{member_id}.json"
temp_file = f"{member_log_file}.tmp"
existing_logs = []
try: try:
if os.path.exists(member_log_file): member_log_file = f"{self.member_logs_dir}/{member_id}.json"
with open(member_log_file, 'r', encoding='utf-8') as f:
try:
existing_logs = json.load(f)
except json.JSONDecodeError as e:
# Handle corrupted file by backing it up and starting fresh
corrupted_backup = f"{member_log_file}.corrupted"
os.rename(member_log_file, corrupted_backup)
kuby_logger.warning(f"Corrupted member log detected and backed up to {corrupted_backup}: {e}. Starting new log.")
existing_logs = []
existing_logs.append(data) with open(member_log_file, 'a', encoding='utf-8') as f:
f.write(json.dumps(data, ensure_ascii=False) + "\n")
# Atomic write using a temporary file
with open(temp_file, 'w', encoding='utf-8') as f:
json.dump(existing_logs, f, indent=2, ensure_ascii=False)
os.replace(temp_file, member_log_file)
except Exception as e: except Exception as e:
kuby_logger.error(f"Error writing member log for {member_id}: {e}") print(f"CRITICAL ERROR in AdvancedLogger._write_member_log_sync: {e}")
if os.path.exists(temp_file):
try: os.remove(temp_file)
except: pass
async def send_leave_notification(self, member: discord.Member, channel: discord.TextChannel): async def send_leave_notification(self, member: discord.Member, channel: discord.TextChannel):
"""Send notification when member leaves""" """Send notification when member leaves"""

View file

@ -188,10 +188,10 @@ async def generate_servers_json(bot: discord.Client) -> dict:
entry["verificationLevel"] = str(guild.verification_level) entry["verificationLevel"] = str(guild.verification_level)
if fields.get("memberCount"): if fields.get("memberCount"):
try: # Use cache if available (intents.members is True), otherwise fallback to count
members = [m async for m in guild.fetch_members(limit=None)] if guild.members:
entry["memberCount"] = sum(1 for m in members if not m.bot) entry["memberCount"] = sum(1 for m in guild.members if not m.bot)
except Exception: else:
entry["memberCount"] = guild.member_count entry["memberCount"] = guild.member_count
if fields.get("owner"): if fields.get("owner"):
@ -209,22 +209,27 @@ async def generate_servers_json(bot: discord.Client) -> dict:
pass pass
# Membres avec un rôle dont le nom correspond à founderRoles # Membres avec un rôle dont le nom correspond à founderRoles
try: # Use cache if possible
async for member in guild.fetch_members(limit=None): members_to_check = guild.members if guild.members else []
if member.bot or member.id == guild.owner_id: if not members_to_check:
continue try:
for role in member.roles: members_to_check = [m async for m in guild.fetch_members(limit=None)]
if any(key in role.name.lower() for key in founder_keys): except Exception:
founders.append({ pass
"id": str(member.id),
"username": member.name, for member in members_to_check:
"displayName": member.display_name, if member.bot or member.id == guild.owner_id:
"avatar": str(member.display_avatar.url), continue
"isOwner": False, for role in member.roles:
}) if any(key in role.name.lower() for key in founder_keys):
break founders.append({
except Exception: "id": str(member.id),
pass "username": member.name,
"displayName": member.display_name,
"avatar": str(member.display_avatar.url),
"isOwner": False,
})
break
entry["founders"] = founders entry["founders"] = founders
@ -259,7 +264,14 @@ async def generate_members_json(bot: discord.Client, guild_id: Optional[int] = N
raise ValueError(f"Serveur introuvable : {target_id}") raise ValueError(f"Serveur introuvable : {target_id}")
members_list = [] members_list = []
async for member in guild.fetch_members(limit=None): # Use cached members if available, or fetch if not
source = guild.members if guild.members else []
if not source:
async for m in guild.fetch_members(limit=None):
if not m.bot:
source.append(m)
for member in source:
if member.bot: if member.bot:
continue continue