Mise à jour du système de whitelist, sécurité et logging
This commit is contained in:
parent
d82f704c75
commit
1e384032be
7 changed files with 199 additions and 145 deletions
15
commandes/PENDING_TASKS.md
Normal file
15
commandes/PENDING_TASKS.md
Normal 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.
|
||||
|
|
@ -9,10 +9,12 @@ import os
|
|||
import asyncio
|
||||
|
||||
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):
|
||||
os.makedirs("data", exist_ok=True)
|
||||
os.makedirs(os.path.dirname(REPORTS_FILE), exist_ok=True)
|
||||
try:
|
||||
if os.path.exists(REPORTS_FILE):
|
||||
with open(REPORTS_FILE, "r") as f:
|
||||
|
|
@ -152,7 +154,23 @@ class BugReport(commands.Cog):
|
|||
self.bot.loop.create_task(cleanup())
|
||||
|
||||
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:
|
||||
# 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()
|
||||
|
||||
event_type = payload.get("object_kind")
|
||||
|
|
|
|||
|
|
@ -960,53 +960,49 @@ class Security(commands.Cog):
|
|||
Commande principale pour ouvrir l'interface de sécurité.
|
||||
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
|
||||
if not is_immune(interaction.guild, interaction.user):
|
||||
if interaction.response.is_done():
|
||||
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(
|
||||
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
|
||||
)
|
||||
|
||||
# Chargement des paramètres
|
||||
settings = load_settings(interaction.guild.id)
|
||||
try:
|
||||
# Chargement des paramètres
|
||||
settings = load_settings(interaction.guild.id)
|
||||
|
||||
# Création de la vue
|
||||
view = SecurityView(interaction, settings, bot=self.bot)
|
||||
embed = view.create_main_embed(interaction.user)
|
||||
# Création de la vue
|
||||
view = SecurityView(interaction, settings, bot=self.bot)
|
||||
embed = view.create_main_embed(interaction.user)
|
||||
|
||||
if interaction.response.is_done():
|
||||
await interaction.followup.send(
|
||||
embed=embed,
|
||||
view=view,
|
||||
ephemeral=True
|
||||
)
|
||||
else:
|
||||
await interaction.response.send_message(
|
||||
embed=embed,
|
||||
view=view,
|
||||
except Exception as e:
|
||||
await interaction.followup.send(
|
||||
f"❌ Une erreur est survenue lors de l'ouverture du panneau : {e}",
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
@security.error
|
||||
async def security_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError):
|
||||
"""Gestion des erreurs de la commande security"""
|
||||
# Ne pas envoyer de message si l'interaction a déjà été traitée
|
||||
if interaction.response.is_done():
|
||||
return
|
||||
# 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
|
||||
|
||||
if isinstance(error, app_commands.MissingPermissions):
|
||||
await interaction.response.send_message(
|
||||
await send_method(
|
||||
"❌ Permissions insuffisantes.",
|
||||
ephemeral=True
|
||||
)
|
||||
else:
|
||||
await interaction.response.send_message(
|
||||
f"❌ Une erreur est survenue.",
|
||||
await send_method(
|
||||
f"❌ Une erreur est survenue lors de l'exécution de la commande : {error}",
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import discord
|
||||
import re
|
||||
from discord.ext import commands
|
||||
from discord import app_commands, ui
|
||||
import json
|
||||
|
|
@ -313,26 +314,28 @@ class TicketStep3(discord.ui.Modal, title="📋 Développement du Personnage"):
|
|||
except discord.errors.NotFound:
|
||||
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"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(timeout=None)
|
||||
|
||||
async def get_ticket_owner(self, interaction: discord.Interaction):
|
||||
"""Récupère le propriétaire du ticket via le topic du salon (robuste)"""
|
||||
if interaction.channel.topic and "Ticket de" in interaction.channel.topic:
|
||||
"""Récupère le propriétaire du ticket via le topic du salon (robuste via regex)"""
|
||||
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:
|
||||
user_id_str = interaction.channel.topic.replace("Ticket de ", "").strip()
|
||||
owner_id = int(user_id_str)
|
||||
member = interaction.guild.get_member(owner_id)
|
||||
if not member:
|
||||
try:
|
||||
member = await interaction.guild.fetch_member(owner_id)
|
||||
except (discord.NotFound, discord.HTTPException):
|
||||
return None
|
||||
member = await interaction.guild.fetch_member(owner_id)
|
||||
return member
|
||||
except (ValueError, IndexError):
|
||||
except:
|
||||
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}")
|
||||
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"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(timeout=None)
|
||||
|
||||
async def get_ticket_owner(self, interaction: discord.Interaction):
|
||||
"""Récupère le propriétaire du ticket via le topic du salon (robuste)"""
|
||||
if interaction.channel.topic and "Ticket de" in interaction.channel.topic:
|
||||
"""Récupère le propriétaire du ticket via le topic du salon (robuste via regex)"""
|
||||
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:
|
||||
user_id_str = interaction.channel.topic.replace("Ticket de ", "").strip()
|
||||
owner_id = int(user_id_str)
|
||||
member = interaction.guild.get_member(owner_id)
|
||||
if not member:
|
||||
try:
|
||||
member = await interaction.guild.fetch_member(owner_id)
|
||||
except (discord.NotFound, discord.HTTPException):
|
||||
return None
|
||||
member = await interaction.guild.fetch_member(owner_id)
|
||||
return member
|
||||
except (ValueError, IndexError):
|
||||
except:
|
||||
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}")
|
||||
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"""
|
||||
def __init__(self, owner: discord.Member):
|
||||
super().__init__(timeout=180)
|
||||
|
|
@ -464,16 +469,48 @@ class ReopenStatusView(discord.ui.view.View):
|
|||
async def apply_status(self, interaction: discord.Interaction, prefix: str):
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
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)
|
||||
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()
|
||||
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.channel.send(f"✅ Ticket réouvert par {interaction.user.mention} (Statut: **{prefix}**).", view=new_view)
|
||||
await interaction.followup.send(f"✅ Statut **{prefix}** appliqué et rôles synchronisés.", ephemeral=True)
|
||||
self.stop()
|
||||
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")
|
||||
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:
|
||||
return
|
||||
|
||||
# Détection du format du topic du ticket
|
||||
if not message.channel.topic or not message.channel.topic.startswith("Ticket de"):
|
||||
# Détection du format du topic du ticket via regex (robuste)
|
||||
if not message.channel.topic:
|
||||
return
|
||||
|
||||
match = re.search(r"(\d{17,20})", message.channel.topic)
|
||||
if not match:
|
||||
return
|
||||
|
||||
# On ne traite que si le message contient "validé"
|
||||
|
|
@ -585,12 +626,12 @@ class Ticket(commands.Cog):
|
|||
return
|
||||
|
||||
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:
|
||||
user_id_str = message.channel.topic.replace("Ticket de ", "").strip()
|
||||
user = await message.guild.fetch_member(int(user_id_str))
|
||||
except (ValueError, IndexError, discord.NotFound, discord.HTTPException):
|
||||
kuby_logger.warning(f"Impossible de trouver l'utilisateur du ticket via le topic: {message.channel.topic}")
|
||||
user = await message.guild.fetch_member(user_id)
|
||||
except (discord.NotFound, discord.HTTPException):
|
||||
await message.channel.send("❌ Utilisateur introuvable. A-t-il quitté le serveur ?", delete_after=10)
|
||||
return
|
||||
|
||||
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))
|
||||
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
|
||||
if attente_oral_role: await user.remove_roles(attente_oral_role)
|
||||
if citoyen_role: await user.add_roles(citoyen_role)
|
||||
|
|
|
|||
|
|
@ -64,11 +64,11 @@ class WebsiteSync(commands.Cog, name="Website Sync"):
|
|||
def cog_unload(self):
|
||||
self.daily_export.cancel()
|
||||
|
||||
# ── Tâche de fond : génération (30s pour test) ────────────────────────────
|
||||
@tasks.loop(seconds=30)
|
||||
# ── Tâche de fond : génération (1 heure) ────────────────────────────
|
||||
@tasks.loop(hours=1)
|
||||
async def daily_export(self):
|
||||
"""Génère les JSON toutes les 30 secondes (Mode Test)."""
|
||||
logger.info("[JSON Export] Génération automatique (toutes les 30s) déclenchée.")
|
||||
"""Génère les JSON toutes les heures."""
|
||||
logger.info("[JSON Export] Génération automatique déclenchée.")
|
||||
await generate_all(self.bot)
|
||||
|
||||
@daily_export.before_loop
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class AdvancedLogger:
|
|||
"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):
|
||||
"""Log role changes (added/removed)"""
|
||||
|
|
@ -49,7 +49,7 @@ class AdvancedLogger:
|
|||
"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):
|
||||
"""Log voice channel events (join/leave)"""
|
||||
|
|
@ -63,7 +63,7 @@ class AdvancedLogger:
|
|||
"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):
|
||||
"""Log when a member leaves the server"""
|
||||
|
|
@ -85,10 +85,10 @@ class AdvancedLogger:
|
|||
}
|
||||
|
||||
# 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
|
||||
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):
|
||||
"""Log when a member joins the server"""
|
||||
|
|
@ -101,10 +101,13 @@ class AdvancedLogger:
|
|||
"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]:
|
||||
"""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"
|
||||
|
||||
if not os.path.exists(member_log_file):
|
||||
|
|
@ -112,7 +115,8 @@ class AdvancedLogger:
|
|||
|
||||
try:
|
||||
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
|
||||
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}")
|
||||
|
||||
return None
|
||||
|
||||
def _write_log(self, log_type: str, data: Dict):
|
||||
"""Write log data to appropriate file safely with atomic write"""
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
log_file = f"{self.logs_dir}/{log_type}_{date_str}.json"
|
||||
temp_file = f"{log_file}.tmp"
|
||||
|
||||
existing_logs = []
|
||||
|
||||
async def _write_log_async(self, log_type: str, data: Dict):
|
||||
"""Asynchronous wrapper for log writing"""
|
||||
return await asyncio.to_thread(self._write_log_sync, log_type, data)
|
||||
|
||||
async def _write_member_log_async(self, member_id: int, data: Dict):
|
||||
"""Asynchronous wrapper for member log writing"""
|
||||
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:
|
||||
if os.path.exists(log_file):
|
||||
with open(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"{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 = []
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
log_file = f"{self.logs_dir}/{log_type}_{date_str}.json"
|
||||
|
||||
existing_logs.append(data)
|
||||
|
||||
# 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, log_file)
|
||||
# Simple append in NDJSON format
|
||||
with open(log_file, 'a', encoding='utf-8') as f:
|
||||
f.write(json.dumps(data, ensure_ascii=False) + "\n")
|
||||
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"Error writing log for {log_type}: {e}")
|
||||
if os.path.exists(temp_file):
|
||||
try: os.remove(temp_file)
|
||||
except: pass
|
||||
# We don't use kuby_logger here to avoid infinite recursion if nested
|
||||
print(f"CRITICAL ERROR in AdvancedLogger._write_log_sync: {e}")
|
||||
|
||||
def _write_member_log(self, member_id: int, data: Dict):
|
||||
"""Write member-specific log safely with atomic write"""
|
||||
member_log_file = f"{self.member_logs_dir}/{member_id}.json"
|
||||
temp_file = f"{member_log_file}.tmp"
|
||||
|
||||
existing_logs = []
|
||||
def _write_member_log_sync(self, member_id: int, data: Dict):
|
||||
"""Write member-specific log using NDJSON (Append-only)"""
|
||||
try:
|
||||
if os.path.exists(member_log_file):
|
||||
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 = []
|
||||
member_log_file = f"{self.member_logs_dir}/{member_id}.json"
|
||||
|
||||
existing_logs.append(data)
|
||||
|
||||
# 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)
|
||||
with open(member_log_file, 'a', encoding='utf-8') as f:
|
||||
f.write(json.dumps(data, ensure_ascii=False) + "\n")
|
||||
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"Error writing member log for {member_id}: {e}")
|
||||
if os.path.exists(temp_file):
|
||||
try: os.remove(temp_file)
|
||||
except: pass
|
||||
print(f"CRITICAL ERROR in AdvancedLogger._write_member_log_sync: {e}")
|
||||
|
||||
async def send_leave_notification(self, member: discord.Member, channel: discord.TextChannel):
|
||||
"""Send notification when member leaves"""
|
||||
|
|
|
|||
|
|
@ -188,10 +188,10 @@ async def generate_servers_json(bot: discord.Client) -> dict:
|
|||
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:
|
||||
# Use cache if available (intents.members is True), otherwise fallback to count
|
||||
if guild.members:
|
||||
entry["memberCount"] = sum(1 for m in guild.members if not m.bot)
|
||||
else:
|
||||
entry["memberCount"] = guild.member_count
|
||||
|
||||
if fields.get("owner"):
|
||||
|
|
@ -209,22 +209,27 @@ async def generate_servers_json(bot: discord.Client) -> dict:
|
|||
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
|
||||
# Use cache if possible
|
||||
members_to_check = guild.members if guild.members else []
|
||||
if not members_to_check:
|
||||
try:
|
||||
members_to_check = [m async for m in guild.fetch_members(limit=None)]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for member in members_to_check:
|
||||
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
|
||||
|
||||
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}")
|
||||
|
||||
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:
|
||||
continue
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue