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
|
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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue