Merge branch 'dev' into 'main'

Fix(Ticket Whitelist): Ajout système async et tracking pour le deblocage des validation rate limitees (10 min) + infos MP

See merge request Omega_Kube/kuby!10
This commit is contained in:
Gameur 2026-04-02 19:16:19 +02:00
commit c9b098d87b
46 changed files with 930 additions and 3283 deletions

View file

@ -0,0 +1,19 @@
# À 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 (À faire lors de la reprise)
1. **Ticket Ouvert/Réouvert** : Le bot est actuellement en ligne avec des messages de debug (❌).
2. **Test "validé"** : Taper "validé" dans un ticket (neuf ou réouvert).
3. **Analyser les retours** :
- `❌ Debug: message.channel.topic est vide ou None.` (Cache d.py obsolète ?)
- `❌ Debug: Topic trouvé mais pas d'ID matché.` (Format de topic invalide ?)
- `❌ Debug: Permission refusée.` (Rôle Staff ou Permissions Admin non reconnus ?)
4. **Action** : Une fois la cause identifiée, implémenter le "fetch_channel" ou une méthode alternative de récupération de l'owner.

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:
@ -116,16 +118,59 @@ class BugReport(commands.Cog):
self.web_app.router.add_post('/bot_event', self.handle_bot_event) self.web_app.router.add_post('/bot_event', self.handle_bot_event)
self.runner = web.AppRunner(self.web_app) self.runner = web.AppRunner(self.web_app)
await self.runner.setup() await self.runner.setup()
self.site = web.TCPSite(self.runner, '127.0.0.1', 5001)
await self.site.start() max_retries = 5
kuby_logger.info("Internal Bot Webhook Server started on 127.0.0.1:5001") retry_delay = 2
for attempt in range(1, max_retries + 1):
try:
self.site = web.TCPSite(self.runner, '127.0.0.1', 5001)
await self.site.start()
kuby_logger.info(f"Internal Bot Webhook Server started on 127.0.0.1:5001 (Attempt {attempt})")
return
except OSError as e:
if e.errno == 98: # Address already in use
if attempt < max_retries:
kuby_logger.warning(f"Port 5001 already in use, retrying in {retry_delay}s... ({attempt}/{max_retries})")
await asyncio.sleep(retry_delay)
else:
kuby_logger.error(f"Failed to bind to port 5001 after {max_retries} attempts.")
raise e
else:
raise e
def cog_unload(self): def cog_unload(self):
if hasattr(self, 'runner'): if hasattr(self, 'runner'):
self.bot.loop.create_task(self.runner.cleanup()) # On utilise create_task car cog_unload est synchrone
async def cleanup():
try:
if hasattr(self, 'site'):
await self.site.stop()
await self.runner.cleanup()
kuby_logger.info("Internal Bot Webhook Server stopped and cleaned up.")
except Exception as e:
kuby_logger.error(f"Error during webhook server cleanup: {e}")
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")
@ -187,13 +232,22 @@ class BugReport(commands.Cog):
color=discord.Color.blue() color=discord.Color.blue()
) )
embed.add_field(name="Nouveaux Labels / Statuts", value=current_labels_str) embed.add_field(name="Nouveaux Labels / Statuts", value=current_labels_str)
await user.send(embed=embed)
if dev: try:
await dev.send(f"✅ [FIABLE] L'utilisateur {user} ({user.id}) a bien reçu la notification de statut **{current_labels_str}** pour l'issue #{issue_iid} ({issue_title}).") await user.send(embed=embed)
if dev and dev.id != user.id:
await dev.send(f"✅ [FIABLE] L'utilisateur {user} ({user.id}) a bien reçu la notification de statut **{current_labels_str}** pour l'issue #{issue_iid} ({issue_title}).")
except discord.Forbidden:
kuby_logger.warning(f"Unable to DM user {user} ({user.id}) - DMs are disabled or bot is blocked.")
if dev and dev.id != user.id:
try:
await dev.send(f"⚠️ [DM BLOQUÉ] Impossible de notifier l'utilisateur {user} ({user.id}) pour l'issue #{issue_iid} (DMs désactivés).")
except discord.Forbidden:
pass
except discord.HTTPException as e:
kuby_logger.error(f"HTTP error sending DM to {user}: {e}")
except Exception as e: except Exception as e:
if dev: kuby_logger.error(f"Unexpected error in label update notification: {e}")
await dev.send(f"❌ [ERREUR] Impossible de notifier l'utilisateur {user} pour l'issue #{issue_iid} : {e}")
# Check if it was closed # Check if it was closed
if action == "close" or payload.get("object_attributes", {}).get("state") == "closed": if action == "close" or payload.get("object_attributes", {}).get("state") == "closed":
@ -205,14 +259,22 @@ class BugReport(commands.Cog):
) )
embed.add_field(name="Prochaine étape", value="La mise à jour arrive prochainement (ou est déjà là) !") embed.add_field(name="Prochaine étape", value="La mise à jour arrive prochainement (ou est déjà là) !")
embed.set_footer(text="Merci pour votre aide !") embed.set_footer(text="Merci pour votre aide !")
await user.send(embed=embed)
if dev:
await dev.send(f"✅ [FIABLE] L'utilisateur {user} ({user.id}) a bien reçu la notification de CLÔTURE pour l'issue #{issue_iid}.")
try:
await user.send(embed=embed)
if dev and dev.id != user.id:
await dev.send(f"✅ [FIABLE] L'utilisateur {user} ({user.id}) a bien reçu la notification de CLÔTURE pour l'issue #{issue_iid}.")
except discord.Forbidden:
kuby_logger.warning(f"Unable to DM user {user} ({user.id}) - DMs are disabled or bot is blocked during closure.")
if dev and dev.id != user.id:
try:
await dev.send(f"⚠️ [DM BLOQUÉ] Impossible d'annoncer la clôture à {user} ({user.id}) pour l'issue #{issue_iid} (DMs désactivés).")
except discord.Forbidden:
pass
except discord.HTTPException as e:
kuby_logger.error(f"HTTP error sending closure DM to {user}: {e}")
except Exception as e: except Exception as e:
if dev: kuby_logger.error(f"Unexpected error in closure notification: {e}")
await dev.send(f"❌ [ERREUR] Impossible d'annoncer la clôture à {user} pour l'issue #{issue_iid} : {e}")
elif event_type == "note": elif event_type == "note":
note_attr = payload.get("object_attributes", {}) note_attr = payload.get("object_attributes", {})
@ -229,14 +291,22 @@ class BugReport(commands.Cog):
color=discord.Color.orange() color=discord.Color.orange()
) )
embed.set_footer(text=f"Par {author_name}") embed.set_footer(text=f"Par {author_name}")
await user.send(embed=embed)
try:
if dev: await user.send(embed=embed)
await dev.send(f"✅ [FIABLE] L'utilisateur {user} ({user.id}) a bien reçu votre commentaire pour l'issue #{issue_iid}.") if dev and dev.id != user.id:
await dev.send(f"✅ [FIABLE] L'utilisateur {user} ({user.id}) a bien reçu votre commentaire pour l'issue #{issue_iid}.")
except discord.Forbidden:
kuby_logger.warning(f"Unable to DM user {user} ({user.id}) - DMs are disabled or bot is blocked during comment notification.")
if dev and dev.id != user.id:
try:
await dev.send(f"⚠️ [DM BLOQUÉ] Impossible d'envoyer votre commentaire à {user} ({user.id}) pour l'issue #{issue_iid} (DMs désactivés).")
except discord.Forbidden:
pass
except discord.HTTPException as e:
kuby_logger.error(f"HTTP error sending comment DM to {user}: {e}")
except Exception as e: except Exception as e:
if dev: kuby_logger.error(f"Unexpected error in comment notification: {e}")
await dev.send(f"❌ [ERREUR] Impossible d'envoyer votre commentaire à {user} pour l'issue #{issue_iid} : {e}")
return web.json_response({"status": "success"}, status=200) return web.json_response({"status": "success"}, status=200)

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

@ -1707,14 +1707,14 @@ class ConfigPanelView(discord.ui.View):
self.category_btn.callback = self._category_callback self.category_btn.callback = self._category_callback
self.add_item(self.category_btn) self.add_item(self.category_btn)
# Ajouter catégorie # Gérer les catégories
self.add_cat_btn = discord.ui.Button( self.manage_cat_btn = discord.ui.Button(
style=discord.ButtonStyle.blurple, style=discord.ButtonStyle.blurple,
label="Ajouter Catégorie", label="📂 Gérer les catégories",
custom_id="config_add_cat" custom_id="config_manage_cat"
) )
self.add_cat_btn.callback = self._add_cat_callback self.manage_cat_btn.callback = self._manage_cat_callback
self.add_item(self.add_cat_btn) self.add_item(self.manage_cat_btn)
# Staff permissions # Staff permissions
staff_btn = discord.ui.Button( staff_btn = discord.ui.Button(
@ -1756,9 +1756,9 @@ class ConfigPanelView(discord.ui.View):
item.style = discord.ButtonStyle.green if self.config.enabled else discord.ButtonStyle.red item.style = discord.ButtonStyle.green if self.config.enabled else discord.ButtonStyle.red
await interaction.message.edit(view=self) await interaction.message.edit(view=self)
async def _add_cat_callback(self, interaction: discord.Interaction): async def _manage_cat_callback(self, interaction: discord.Interaction):
modal = AddCategoryModal(self.bot, self.storage, self.config) view = CategoryManagerView(self.bot, self.storage, self.config)
await interaction.response.send_modal(modal) await interaction.response.edit_message(embed=view._get_embed(), view=view)
async def _staff_perms_callback(self, interaction: discord.Interaction): async def _staff_perms_callback(self, interaction: discord.Interaction):
view = PermissionConfigView(self.bot, self.storage, self.config, interaction.guild, "staff") view = PermissionConfigView(self.bot, self.storage, self.config, interaction.guild, "staff")
@ -1849,6 +1849,251 @@ class AddCategoryModal(discord.ui.Modal):
await interaction.response.send_message(f"Catégorie **{self.name.value}** créée!", ephemeral=True) await interaction.response.send_message(f"Catégorie **{self.name.value}** créée!", ephemeral=True)
class EditCategoryModal(discord.ui.Modal):
"""Modal pour modifier une catégorie existante"""
def __init__(self, bot, storage, config, category: TicketCategory, parent_view):
super().__init__(title=f"Modifier: {category.name}")
self.bot = bot
self.storage = storage
self.config = config
self.category = category
self.parent_view = parent_view
self.name = discord.ui.TextInput(
label="Nom",
default=category.name,
placeholder="Ex: Support",
required=True
)
self.add_item(self.name)
self.description = discord.ui.TextInput(
label="Description",
default=category.description,
placeholder="Description...",
required=False,
style=discord.TextStyle.paragraph
)
self.add_item(self.description)
self.emoji = discord.ui.TextInput(
label="Emoji",
default=category.emoji,
placeholder="🎫",
required=False,
max_length=5
)
self.add_item(self.emoji)
async def on_submit(self, interaction: discord.Interaction):
# Update category object
self.category.name = self.name.value
self.category.description = self.description.value or ""
self.category.emoji = self.emoji.value or "🎫"
# Save config
self.storage.save_config(interaction.guild_id, self.config)
# Refresh the parent view's message
await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view)
await interaction.followup.send(f"✅ Catégorie **{self.category.name}** mise à jour !", ephemeral=True)
class CategoryManagerView(discord.ui.View):
"""Vue pour lister et gérer les catégories"""
def __init__(self, bot, storage, config: GuildTicketConfig):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
self._add_category_select()
def _add_category_select(self):
self.clear_items()
if self.config.categories:
options = []
for cat in self.config.categories[:25]: # Limite Select menu
options.append(discord.SelectOption(
label=cat.name,
value=cat.id,
description=cat.description[:100] if cat.description else "Aucune description",
emoji=cat.emoji
))
select = discord.ui.Select(
placeholder="Sélectionnez une catégorie à gérer...",
options=options,
custom_id="manage_cat_select"
)
select.callback = self._category_select_callback
self.add_item(select)
# Bouton Ajouter
add_btn = discord.ui.Button(
style=discord.ButtonStyle.success,
label=" Ajouter une catégorie",
custom_id="manage_cat_add"
)
add_btn.callback = self._add_cat_callback
self.add_item(add_btn)
# Bouton Retour
back_btn = discord.ui.Button(
style=discord.ButtonStyle.secondary,
label="⬅️ Retour",
custom_id="manage_cat_back"
)
back_btn.callback = self._back_callback
self.add_item(back_btn)
def _get_embed(self):
embed = discord.Embed(
title="📂 Gestion des Catégories",
description=f"Il y a actuellement **{len(self.config.categories)}** catégorie(s) configurée(s).\n\n"
"Sélectionnez une catégorie dans le menu pour la modifier ou la supprimer.",
color=discord.Color.blue()
)
if not self.config.categories:
embed.description += "\n\n⚠️ **Aucune catégorie configurée.** Commencez par en ajouter une !"
for cat in self.config.categories[:10]:
embed.add_field(
name=f"{cat.emoji} {cat.name}",
value=cat.description or "Pas de description",
inline=False
)
return embed
async def _category_select_callback(self, interaction: discord.Interaction):
# En utilisant interaction.data.get('values') pour obtenir la valeur sélectionnée
cat_id = interaction.data.get('values', [None])[0]
category = self.config.get_category(cat_id)
if not category:
await interaction.response.send_message("Catégorie introuvable.", ephemeral=True)
return
view = CategoryActionView(self.bot, self.storage, self.config, category, self)
await interaction.response.edit_message(embed=view._get_embed(), view=view)
async def _add_cat_callback(self, interaction: discord.Interaction):
# Utilisation de la classe AddCategoryModal existante
class AddCategoryManagerModal(AddCategoryModal):
def __init__(self, bot, storage, config, parent_view):
super().__init__(bot, storage, config)
self.parent_view = parent_view
async def on_submit(self, interaction: discord.Interaction):
import uuid
cat_id = str(uuid.uuid4())[:8]
category = TicketCategory(
id=cat_id,
name=self.name.value,
description=self.description.value or "",
emoji=self.emoji.value or "🎫"
)
self.config.categories.append(category)
self.storage.save_config(interaction.guild_id, self.config)
# Refresh parent view
self.parent_view._add_category_select()
# On doit utiliser edit_message sur l'interaction d'origine ou renvoyer un embed
await interaction.response.edit_message(
embed=self.parent_view._get_embed(),
view=self.parent_view
)
await interaction.followup.send(f"Catégorie **{self.name.value}** créée!", ephemeral=True)
modal = AddCategoryManagerModal(self.bot, self.storage, self.config, self)
await interaction.response.send_modal(modal)
async def _back_callback(self, interaction: discord.Interaction):
view = ConfigPanelView(self.bot, self.storage, self.config)
embed = discord.Embed(
title="Configuration des Tickets",
description="Gérez la configuration du système de tickets",
color=discord.Color.blue()
)
await interaction.response.edit_message(embed=embed, view=view)
class CategoryActionView(discord.ui.View):
"""Actions pour une catégorie spécifique"""
def __init__(self, bot, storage, config, category, parent_view):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
self.category = category
self.parent_view = parent_view
def _get_embed(self):
embed = discord.Embed(
title=f"Gestion: {self.category.name}",
description=f"**ID:** `{self.category.id}`\n"
f"**Emoji:** {self.category.emoji}\n"
f"**Description:** {self.category.description or 'Aucune'}\n"
f"**Couleur:** `{self.category.color}`",
color=discord.Color.blue()
)
return embed
@discord.ui.button(label="📝 Modifier", style=discord.ButtonStyle.primary)
async def edit_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
modal = EditCategoryModal(self.bot, self.storage, self.config, self.category, self)
await interaction.response.send_modal(modal)
@discord.ui.button(label="🗑️ Supprimer", style=discord.ButtonStyle.danger)
async def delete_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
view = DeleteCategoryConfirmationView(self.bot, self.storage, self.config, self.category, self.parent_view)
embed = discord.Embed(
title="⚠️ Confirmer la suppression",
description=f"Êtes-vous sûr de vouloir supprimer la catégorie **{self.category.name}** ?\n"
"Cette action est irréversible.",
color=discord.Color.red()
)
await interaction.response.edit_message(embed=embed, view=view)
@discord.ui.button(label="⬅️ Retour", style=discord.ButtonStyle.secondary)
async def back_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
self.parent_view._add_category_select()
await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view)
class DeleteCategoryConfirmationView(discord.ui.View):
def __init__(self, bot, storage, config, category, parent_view):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
self.category = category
self.parent_view = parent_view
@discord.ui.button(label="✅ Confirmer", style=discord.ButtonStyle.danger)
async def confirm_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
if self.category in self.config.categories:
self.config.categories.remove(self.category)
self.storage.save_config(interaction.guild_id, self.config)
self.parent_view._add_category_select()
await interaction.response.edit_message(
embed=self.parent_view._get_embed(),
view=self.parent_view
)
await interaction.followup.send(f"✅ Catégorie **{self.category.name}** supprimée.", ephemeral=True)
@discord.ui.button(label="❌ Annuler", style=discord.ButtonStyle.secondary)
async def cancel_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
view = CategoryActionView(self.bot, self.storage, self.config, self.category, self.parent_view)
await interaction.response.edit_message(embed=view._get_embed(), view=view)
class SetChannelModal(discord.ui.Modal): class SetChannelModal(discord.ui.Modal):
"""Modal pour définir le canal de logs""" """Modal pour définir le canal de logs"""

View file

@ -1,9 +1,40 @@
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
import os import os
import asyncio
from typing import Optional from typing import Optional
from src.logger import kuby_logger
import time
# Dictionnaire global pour suivre les renommages de salons
# Format: {channel_id: [timestamp1, timestamp2, ...]}
channel_rename_timestamps = {}
def check_channel_rename_rate_limit(channel_id: int) -> int:
"""Vérifie si le renommage va atteindre la limite de Discord (2 edits / 10 min)."""
now = time.time()
if channel_id not in channel_rename_timestamps:
channel_rename_timestamps[channel_id] = []
timestamps = [ts for ts in channel_rename_timestamps[channel_id] if now - ts < 600]
channel_rename_timestamps[channel_id] = timestamps
if len(timestamps) >= 2:
return max(0, int(600 - (now - timestamps[0])))
return 0
def log_channel_rename(channel_id: int):
"""Enregistre un renommage de salon."""
now = time.time()
if channel_id not in channel_rename_timestamps:
channel_rename_timestamps[channel_id] = []
timestamps = [ts for ts in channel_rename_timestamps[channel_id] if now - ts < 600]
timestamps.append(now)
channel_rename_timestamps[channel_id] = timestamps
TICKET_WHITELIST_SETTINGS_FILE = "data/ticket_whitelist_settings.json" TICKET_WHITELIST_SETTINGS_FILE = "data/ticket_whitelist_settings.json"
@ -32,6 +63,31 @@ def save_settings(guild_id: int, settings: dict) -> None:
with open(TICKET_WHITELIST_SETTINGS_FILE, "w", encoding='utf-8') as f: with open(TICKET_WHITELIST_SETTINGS_FILE, "w", encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False) json.dump(data, f, indent=4, ensure_ascii=False)
async def get_ticket_user_id(channel: discord.TextChannel) -> Optional[int]:
"""Récupère l'ID du propriétaire du ticket de manière robuste (gère le lag de cache)"""
import re
topic = getattr(channel, "topic", None)
# Tentative d'utilisation du cache d.py (plus rapide)
if topic:
match = re.search(r"(\d{17,20})", topic)
if match:
return int(match.group(1))
# Si le topic est vide ou ID introuvable, on tente de forcer un refresh via l'API
try:
# On utilise fetch_channel pour avoir un objet tout neuf avec le topic à jour
refreshed_channel = await channel.guild.fetch_channel(channel.id)
topic = getattr(refreshed_channel, "topic", None)
if topic:
match = re.search(r"(\d{17,20})", topic)
if match:
return int(match.group(1))
except:
pass
return None
class TicketWhitelistConfigView(ui.View): class TicketWhitelistConfigView(ui.View):
def __init__(self, guild_id: int): def __init__(self, guild_id: int):
super().__init__(timeout=60) super().__init__(timeout=60)
@ -43,12 +99,6 @@ class TicketWhitelistConfigView(ui.View):
def check(m): def check(m):
return m.author == interaction.user and m.channel == interaction.channel return m.author == interaction.user and m.channel == interaction.channel
try:
# We use an explicit import here to avoid clashing if __import__ syntax is weird, but using standard asyncio module is fine if we import it at top. Let's add import asyncio at the top instead!
pass
except __import__("asyncio").TimeoutError:
pass
try: try:
msg = await interaction.client.wait_for('message', check=check, timeout=30) msg = await interaction.client.wait_for('message', check=check, timeout=30)
target = None target = None
@ -323,52 +373,73 @@ class TicketManagementView(discord.ui.View):
def __init__(self): def __init__(self):
super().__init__(timeout=None) super().__init__(timeout=None)
async def get_ticket_owner(self, interaction: discord.Interaction):
"""Récupère le propriétaire du ticket via le helper robuste"""
owner_id = await get_ticket_user_id(interaction.channel)
if owner_id:
try:
member = interaction.guild.get_member(owner_id)
if not member:
member = await interaction.guild.fetch_member(owner_id)
return member
except:
return None
return None
@discord.ui.button(label="🔒 Fermer le Ticket", style=discord.ButtonStyle.red, custom_id="close_ticket") @discord.ui.button(label="🔒 Fermer le Ticket", style=discord.ButtonStyle.red, custom_id="close_ticket")
async def close_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): async def close_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
"""Ferme le ticket en désactivant l'envoi de messages pour le membre""" """Ferme le ticket en désactivant l'envoi de messages pour le membre"""
await interaction.response.defer(ephemeral=True)
settings = load_settings(interaction.guild.id) settings = load_settings(interaction.guild.id)
staff_role_id = settings.get("staff_role_id") staff_role_id = settings.get("staff_role_id")
if not discord.utils.get(interaction.user.roles, id=int(staff_role_id) if staff_role_id else 0) and not interaction.user.guild_permissions.administrator: is_staff = any(r.id == int(staff_role_id) for r in interaction.user.roles) if staff_role_id else False
return await interaction.response.send_message("❌ Seuls les staffs peuvent fermer un ticket.", ephemeral=True) if not is_staff and not interaction.user.guild_permissions.administrator:
return await interaction.followup.send("❌ Seuls les staffs peuvent fermer un ticket.", ephemeral=True)
owner = await self.get_ticket_owner(interaction)
if not owner:
return await interaction.followup.send("❌ Impossible de trouver le propriétaire du ticket à fermer.", ephemeral=True)
topic = interaction.channel.topic or ""
if not topic.startswith("Ticket de "):
return await interaction.response.send_message("❌ Impossible de trouver le propriétaire du ticket à fermer.", ephemeral=True)
try: try:
owner_id = int(topic.split("Ticket de ")[1]) await interaction.channel.set_permissions(owner, read_messages=False, send_messages=False)
ticket_owner = interaction.guild.get_member(owner_id) log_channel_rename(interaction.channel.id)
if not ticket_owner: asyncio.create_task(interaction.channel.edit(name=f"fermé-{owner.display_name}".replace(" ", "-").lower()[:100]))
ticket_owner = await interaction.guild.fetch_member(owner_id)
except (IndexError, ValueError, discord.NotFound): # Envoi du message de fermeture dans le salon
return await interaction.response.send_message("❌ Membre introuvable.", ephemeral=True) new_view = ClosedTicketView()
await interaction.channel.send(f"🔒 Ce ticket a été fermé par {interaction.user.mention}.", view=new_view)
await interaction.followup.send("✅ Ticket fermé avec succès.", ephemeral=True)
except Exception as e:
kuby_logger.error(f"Erreur lors de la fermeture du ticket: {e}")
await interaction.followup.send(f"❌ Erreur lors de la fermeture : {e}", ephemeral=True)
await interaction.channel.set_permissions(ticket_owner, read_messages=False, send_messages=False) @discord.ui.button(label="👤 Claim le Ticket", style=discord.ButtonStyle.blurple, custom_id="claim_ticket")
await interaction.channel.edit(name="ticket-fermé")
await interaction.response.send_message("🔒 Le ticket a été fermé. Vous ne pouvez plus le voir.", ephemeral=True)
# Mettre à jour la vue pour afficher uniquement les boutons Réouvrir et Supprimer
new_view = ClosedTicketView()
await interaction.channel.send("Le ticket a été fermé.", view=new_view)
@discord.ui.button(label="👤 Prendre en charge le Ticket", style=discord.ButtonStyle.blurple, custom_id="claim_ticket")
async def claim_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): async def claim_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
"""Un staff prend en charge le ticket""" """Un staff prend en charge le ticket"""
await interaction.response.defer(ephemeral=False)
settings = load_settings(interaction.guild.id) settings = load_settings(interaction.guild.id)
staff_role_id = settings.get("staff_role_id") staff_role_id = settings.get("staff_role_id")
if not discord.utils.get(interaction.user.roles, id=int(staff_role_id) if staff_role_id else 0) and not interaction.user.guild_permissions.administrator: is_staff = any(r.id == int(staff_role_id) for r in interaction.user.roles) if staff_role_id else False
return await interaction.response.send_message("❌ Seuls les staffs peuvent claim un ticket.", ephemeral=True) if not is_staff and not interaction.user.guild_permissions.administrator:
return await interaction.followup.send("❌ Seuls les staffs peuvent claim un ticket.")
staff_role = discord.utils.get(interaction.guild.roles, id=int(staff_role_id)) if staff_role_id else None
try:
claimed_by = interaction.user staff_role = interaction.guild.get_role(int(staff_role_id)) if staff_role_id else None
if staff_role:
await interaction.channel.set_permissions(staff_role, send_messages=False, read_messages=True) # Retrait des permissions du rôle staff pour forcer le claimer à parler seul
if staff_role:
await interaction.channel.set_permissions(claimed_by, read_messages=True, send_messages=True) await interaction.channel.set_permissions(staff_role, send_messages=False, read_messages=True)
await interaction.response.send_message(f"👤 {claimed_by.mention} a claim ce ticket.", ephemeral=False)
# Donner les permissions au claimer
await interaction.channel.set_permissions(interaction.user, read_messages=True, send_messages=True)
await interaction.followup.send(f"👥 {interaction.user.mention} a pris en charge ce ticket.")
except Exception as e:
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): 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"""
@ -376,45 +447,133 @@ class ClosedTicketView(discord.ui.View):
def __init__(self): def __init__(self):
super().__init__(timeout=None) super().__init__(timeout=None)
async def get_ticket_owner(self, interaction: discord.Interaction):
"""Récupère le propriétaire du ticket via le helper robuste"""
owner_id = await get_ticket_user_id(interaction.channel)
if owner_id:
try:
member = interaction.guild.get_member(owner_id)
if not member:
member = await interaction.guild.fetch_member(owner_id)
return member
except:
return None
return None
@discord.ui.button(label="♻ Réouvrir le Ticket", style=discord.ButtonStyle.green, custom_id="reopen_ticket") @discord.ui.button(label="♻ Réouvrir le Ticket", style=discord.ButtonStyle.green, custom_id="reopen_ticket")
async def reopen_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): async def reopen_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
"""Réouvre le ticket pour le membre""" """Réouvre le ticket pour le membre"""
await interaction.response.defer(ephemeral=True)
settings = load_settings(interaction.guild.id) settings = load_settings(interaction.guild.id)
staff_role_id = settings.get("staff_role_id") staff_role_id = settings.get("staff_role_id")
if not discord.utils.get(interaction.user.roles, id=int(staff_role_id) if staff_role_id else 0) and not interaction.user.guild_permissions.administrator: is_staff = any(r.id == int(staff_role_id) for r in interaction.user.roles) if staff_role_id else False
return await interaction.response.send_message("❌ Seuls les staffs peuvent réouvrir un ticket.", ephemeral=True) if not is_staff and not interaction.user.guild_permissions.administrator:
return await interaction.followup.send("❌ Seuls les staffs peuvent réouvrir un ticket.", ephemeral=True)
topic = interaction.channel.topic or "" owner = await self.get_ticket_owner(interaction)
if not topic.startswith("Ticket de "): if not owner:
return await interaction.response.send_message("❌ Impossible de trouver le propriétaire du ticket à réouvrir.", ephemeral=True) return await interaction.followup.send("❌ Impossible de trouver le propriétaire du ticket à réouvrir.", ephemeral=True)
try: try:
owner_id = int(topic.split("Ticket de ")[1]) # On demande au staff quel statut remettre
ticket_owner = interaction.guild.get_member(owner_id) embed = discord.Embed(
if not ticket_owner: title="♻️ Réouverture du Ticket",
ticket_owner = await interaction.guild.fetch_member(owner_id) description=f"Choisissez le statut à appliquer pour **{owner.display_name}**.",
except (IndexError, ValueError, discord.NotFound): color=discord.Color.blue()
return await interaction.response.send_message("❌ Membre introuvable.", ephemeral=True) )
view = ReopenStatusView(owner)
await interaction.channel.set_permissions(ticket_owner, read_messages=True, send_messages=True) await interaction.channel.send(embed=embed, view=view)
await interaction.response.send_message("✅ Le ticket a été réouvert.", ephemeral=True) await interaction.followup.send("✅ Choisissez le statut ci-dessous.", ephemeral=True)
except Exception as e:
kuby_logger.error(f"Erreur lors de la préparation de réouverture: {e}")
await interaction.followup.send(f"❌ Erreur : {e}", ephemeral=True)
# Mettre à jour la vue pour afficher tous les boutons class ReopenStatusView(discord.ui.View):
new_view = TicketManagementView() """Vue pour choisir le statut lors d'une réouverture"""
await interaction.channel.send("Le ticket a été réouvert.", view=new_view) def __init__(self, owner: discord.Member):
super().__init__(timeout=180)
self.owner = owner
@discord.ui.button(label="Écrit (Ouvert)", style=discord.ButtonStyle.secondary)
async def set_written(self, interaction: discord.Interaction, button: discord.ui.Button):
await self.apply_status(interaction, "ouvert")
@discord.ui.button(label="Oral (À faire)", style=discord.ButtonStyle.primary)
async def set_oral(self, interaction: discord.Interaction, button: discord.ui.Button):
await self.apply_status(interaction, "oral-à-faire")
@discord.ui.button(label="Accepté", style=discord.ButtonStyle.success)
async def set_accepted(self, interaction: discord.Interaction, button: discord.ui.Button):
await self.apply_status(interaction, "accepté")
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]
log_channel_rename(interaction.channel.id)
asyncio.create_task(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é et rôles synchronisés.", ephemeral=True)
self.stop()
except Exception as e:
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):
"""Supprime le salon du ticket""" """Supprime le salon du ticket"""
await interaction.response.defer(ephemeral=True)
settings = load_settings(interaction.guild.id) settings = load_settings(interaction.guild.id)
staff_role_id = settings.get("staff_role_id") staff_role_id = settings.get("staff_role_id")
if not discord.utils.get(interaction.user.roles, id=int(staff_role_id) if staff_role_id else 0) and not interaction.user.guild_permissions.administrator: is_staff = any(r.id == int(staff_role_id) for r in interaction.user.roles) if staff_role_id else False
return await interaction.response.send_message("❌ Seuls les staffs peuvent supprimer un ticket.", ephemeral=True) if not is_staff and not interaction.user.guild_permissions.administrator:
return await interaction.followup.send("❌ Seuls les staffs peuvent supprimer un ticket.", ephemeral=True)
await interaction.response.send_message("❌ Suppression du ticket...", ephemeral=True) try:
await interaction.channel.delete() await interaction.followup.send("❌ Suppression du ticket...", ephemeral=True)
await interaction.channel.delete()
except Exception as e:
kuby_logger.error(f"Erreur lors de la suppression du ticket: {e}")
try: await interaction.followup.send(f"❌ Erreur lors de la suppression : {e}", ephemeral=True)
except: pass
class TicketView(discord.ui.View): class TicketView(discord.ui.View):
"""Vue avec le bouton pour ouvrir le ticket""" """Vue avec le bouton pour ouvrir le ticket"""
@ -433,6 +592,12 @@ class Ticket(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
async def cog_load(self):
"""Enregistre les vues persistantes"""
self.bot.add_view(TicketView())
self.bot.add_view(TicketManagementView())
self.bot.add_view(ClosedTicketView())
@app_commands.command(name="ticket_whitelist", description="Envoie l'embed pour ouvrir un ticket de whitelist.") @app_commands.command(name="ticket_whitelist", description="Envoie l'embed pour ouvrir un ticket de whitelist.")
@app_commands.checks.has_permissions(administrator=True) @app_commands.checks.has_permissions(administrator=True)
async def ticket_whitelist(self, interaction: discord.Interaction): async def ticket_whitelist(self, interaction: discord.Interaction):
@ -476,50 +641,134 @@ class Ticket(commands.Cog):
@commands.Cog.listener() @commands.Cog.listener()
async def on_message(self, message): async def on_message(self, message):
"""Surveille les messages des staffs dans les tickets""" """Surveille les messages des staffs dans les tickets pour validation automatique"""
if message.guild is None or message.author.bot: if message.guild is None or message.author.bot:
return return
if not message.channel.topic or not message.channel.topic.startswith("Ticket de"):
# On ne traite que si le message contient "validé"
if "validé" not in message.content.lower():
return
# Détection du format du topic du ticket via helper (robuste)
user_id = await get_ticket_user_id(message.channel)
if not user_id:
await message.channel.send("❌ Debug: Impossible de récupérer l'ID utilisateur via le topic (même après refresh API).")
return return
settings = load_settings(message.guild.id) settings = load_settings(message.guild.id)
staff_role_id = settings.get("staff_role_id") staff_role_id = settings.get("staff_role_id")
attente_oral_role_id = settings.get("attente_oral_role_id")
candidature_a_faire_role_id = settings.get("candidature_a_faire_role_id")
citoyen_role_id = settings.get("citoyen_role_id")
if not staff_role_id: # Vérification si l'auteur est staff ou admin
is_staff = False
if staff_role_id:
is_staff = any(r.id == int(staff_role_id) for r in message.author.roles)
if not is_staff and not message.author.guild_permissions.administrator:
if "validé" in message.content.lower():
await message.channel.send("❌ Debug: Permission refusée. Vous n'êtes ni Staff ni Admin.")
return return
staff_role = discord.utils.get(message.guild.roles, id=int(staff_role_id)) try:
# L'ID est déjà trouvé par le helper ci-dessus
if staff_role in message.author.roles and "validé" in message.content.lower(): user_id = user_id
try: try:
user_id = message.channel.topic.split("Ticket de ")[1] user = await message.guild.fetch_member(user_id)
user = await message.guild.fetch_member(int(user_id)) except (discord.NotFound, discord.HTTPException):
await message.channel.send("❌ Utilisateur introuvable. A-t-il quitté le serveur ?", delete_after=10)
return
if not attente_oral_role_id or not candidature_a_faire_role_id or not citoyen_role_id: attente_oral_role_id = settings.get("attente_oral_role_id")
await message.channel.send("❌ Erreur de configuration : Impossible d'attribuer les rôles, ils ne sont pas tous définis dans `/ticket_whitelist_config`.", delete_after=10) candidature_a_faire_role_id = settings.get("candidature_a_faire_role_id")
return citoyen_role_id = settings.get("citoyen_role_id")
# Vérifier si le rôle "attente oral" est déjà attribué if not attente_oral_role_id or not candidature_a_faire_role_id or not citoyen_role_id:
attente_oral_role = discord.utils.get(message.guild.roles, id=int(attente_oral_role_id)) await message.channel.send("❌ Erreur de configuration : Impossible d'attribuer les rôles, ils ne sont pas tous définis dans `/ticket_whitelist_config`.", delete_after=10)
candidature_a_faire_role = discord.utils.get(message.guild.roles, id=int(candidature_a_faire_role_id)) return
citoyen_role = discord.utils.get(message.guild.roles, id=int(citoyen_role_id))
if attente_oral_role in user.roles: attente_oral_role = message.guild.get_role(int(attente_oral_role_id))
# Deuxième "validé" détecté candidature_a_faire_role = message.guild.get_role(int(candidature_a_faire_role_id))
if attente_oral_role: await user.remove_roles(attente_oral_role) citoyen_role = message.guild.get_role(int(citoyen_role_id))
if citoyen_role: await user.add_roles(citoyen_role)
await message.channel.edit(name=f"{user.name}-citoyen")
else:
# Premier "validé" détecté
if candidature_a_faire_role: await user.remove_roles(candidature_a_faire_role)
if attente_oral_role: await user.add_roles(attente_oral_role)
await message.channel.edit(name=f"{user.name}-oral")
except Exception as e: if (attente_oral_role and attente_oral_role in user.roles) or "oral-à-faire" in message.channel.name.lower():
print(f"Erreur lors du traitement du ticket : {e}") # 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)
# Mise à jour du nom du salon
new_name = f"accepté-{user.display_name}".replace(" ", "-").lower()[:100]
wait_time = check_channel_rename_rate_limit(message.channel.id)
log_channel_rename(message.channel.id)
asyncio.create_task(message.channel.edit(name=new_name))
# Confirmation dans le salon (Feedback visuel)
embed_val = discord.Embed(
title="🎉 Whitelist Validée !",
description=f"Félicitations {user.mention}, ton entretien oral a été validé par {message.author.mention} !\nTu es désormais **Citoyen**.",
color=discord.Color.green()
)
await message.channel.send(embed=embed_val)
# DM au joueur (RESTAURATION DESIGN CAPTURE)
try:
embed_dm = discord.Embed(
title="🎉 Félicitations - Citoyenneté Validée !",
description=f"Nous avons le plaisir de t'annoncer que ton passage sur **{message.guild.name}** a été validé avec succès !",
color=discord.Color.green()
)
embed_dm.add_field(name="🏙️ Nouveau Statut", value="Citoyen", inline=True)
embed_dm.add_field(name="📍 Serveur", value=message.guild.name, inline=True)
embed_dm.add_field(name="📋 Prochaine étape", value="Tu peux désormais te connecter au serveur et commencer ton aventure RP. N'oublie pas de consulter les salons d'informations !", inline=False)
if wait_time > 0:
m, s = divmod(wait_time, 60)
embed_dm.add_field(name="⏳ Nom du ticket", value=f"Le nom de ton ticket sera actualisé d'ici {m}m et {s}s environ (limite de sécurité Discord).", inline=False)
embed_dm.add_field(name="🔗 Lien vers ton ticket", value=f"[Clique ici pour voir ton ticket]({message.channel.jump_url})", inline=False)
embed_dm.set_footer(text=f"L'équipe {message.guild.name}")
if message.guild.icon: embed_dm.set_thumbnail(url=message.guild.icon.url)
await user.send(embed=embed_dm)
except: pass
else:
# PREMIÈRE VALIDATION -> ORAL À FAIRE
if candidature_a_faire_role: await user.remove_roles(candidature_a_faire_role)
if attente_oral_role: await user.add_roles(attente_oral_role)
# Mise à jour du nom du salon
new_name = f"oral-à-faire-{user.display_name}".replace(" ", "-").lower()[:100]
wait_time = check_channel_rename_rate_limit(message.channel.id)
log_channel_rename(message.channel.id)
asyncio.create_task(message.channel.edit(name=new_name))
# Confirmation dans le salon
embed_val = discord.Embed(
title="📝 Étape Écrite Validée !",
description=f"Ta candidature écrite a été validée par {message.author.mention}.\nTu passes désormais en **Attente Oral**.",
color=discord.Color.orange()
)
await message.channel.send(embed=embed_val)
# DM au joueur (RESTAURATION DESIGN CAPTURE)
try:
embed_dm = discord.Embed(
title="📝 Validation Étape Écrite",
description=f"Ta candidature écrite sur **{message.guild.name}** a été examinée et validée par notre équipe !",
color=discord.Color.orange()
)
embed_dm.add_field(name="🎤 Statut Actuel", value="Attente Oral", inline=True)
embed_dm.add_field(name="🛠️ Action Requise", value="Merci de te rendre dans le salon vocal 'Attente Oral' dès que tu es disponible pour passer ton entretien.", inline=False)
if wait_time > 0:
m, s = divmod(wait_time, 60)
embed_dm.add_field(name="⏳ Nom du ticket", value=f"Le nom de ton ticket sera actualisé d'ici {m}m et {s}s environ (limite de sécurité Discord).", inline=False)
embed_dm.add_field(name="🔗 Lien vers ton ticket", value=f"[Clique ici pour voir ton ticket]({message.channel.jump_url})", inline=False)
embed_dm.set_footer(text=f"L'équipe {message.guild.name}")
if message.guild.icon: embed_dm.set_thumbnail(url=message.guild.icon.url)
await user.send(embed=embed_dm)
except: pass
except Exception as e:
kuby_logger.error(f"Erreur lors de la validation du ticket: {e}", exc_info=True)
await message.channel.send(f"❌ Une erreur système est survenue : {e}")
async def setup(bot): async def setup(bot):
await bot.add_cog(Ticket(bot)) await bot.add_cog(Ticket(bot))

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

@ -141,11 +141,17 @@ class Welcome(commands.Cog):
servername=member.guild.name, servername=member.guild.name,
member_count=member.guild.member_count member_count=member.guild.member_count
) )
await member.send(formatted_message) # On combine le texte et le GIF dans un SEUL envoi pour réduire les requêtes API (limite de débit)
await member.send("https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExNXV1NW02NTk0bGF2ZHcyMGFna2F6amRrbGpobGpsMHowOW5xa2E5eCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ZtFuW4rsUbfrRzPauJ/giphy.gif") gif_url = "https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExNXV1NW02NTk0bGF2ZHcyMGFna2F6amRrbGpobGpsMHowOW5xa2E5eCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ZtFuW4rsUbfrRzPauJ/giphy.gif"
kuby_logger.info(f"✅ [Welcome] DM envoyé avec succès à {member.name}") await member.send(f"{formatted_message}\n\n{gif_url}")
kuby_logger.info(f"✅ [Welcome] DM envoyé avec succès à {member.name} (envoi groupé)")
except discord.Forbidden: except discord.Forbidden:
kuby_logger.warning(f"🚫 [Welcome] Impossible d'envoyer le DM à {member.name} (DMs fermés)") kuby_logger.warning(f"🚫 [Welcome] Impossible d'envoyer le DM à {member.name} (DMs fermés)")
except discord.HTTPException as e:
if e.code == 40003: # Rate limited (Opening DMs too fast)
kuby_logger.warning(f"⏳ [Welcome] Limite de débit Discord atteinte pour les DMs : {member.name} n'a pas reçu son message.")
else:
kuby_logger.error(f"❌ [Welcome] Erreur HTTP lors de l'envoi du DM à {member.name}: {e}")
except Exception as e: except Exception as e:
kuby_logger.error(f"❌ [Welcome] Erreur lors de l'envoi du DM à {member.name}: {e}", exc_info=True) kuby_logger.error(f"❌ [Welcome] Erreur lors de l'envoi du DM à {member.name}: {e}", exc_info=True)
else: else:

View file

@ -1,180 +1,180 @@
[ [
{ {
"date": "2026-03-14T14:26:12.694595+00:00", "date": "2026-03-31T20:07:49.786458+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:25:42.467679+00:00", "date": "2026-03-31T20:06:47.718241+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:25:11.992208+00:00", "date": "2026-03-31T20:06:07.381254+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:24:42.190744+00:00", "date": "2026-03-31T20:00:43.918120+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:24:12.614886+00:00", "date": "2026-03-29T20:16:08.626593+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:23:42.012764+00:00", "date": "2026-03-29T19:16:08.300074+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:23:12.416066+00:00", "date": "2026-03-29T18:16:08.376405+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:22:42.200744+00:00", "date": "2026-03-29T17:16:08.514497+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:22:12.128982+00:00", "date": "2026-03-29T17:15:42.635599+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:21:41.879828+00:00", "date": "2026-03-29T17:15:41.574258+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:21:25.894956+00:00", "date": "2026-03-29T14:18:49.697634+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:20:56.179577+00:00", "date": "2026-03-29T14:16:08.163385+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:20:25.382325+00:00", "date": "2026-03-29T14:09:18.074641+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:19:55.416432+00:00", "date": "2026-03-29T13:56:24.990061+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:19:25.281242+00:00", "date": "2026-03-29T13:54:43.766970+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:18:55.315861+00:00", "date": "2026-03-29T13:48:39.075696+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:18:25.672058+00:00", "date": "2026-03-29T13:48:08.960324+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:17:55.409042+00:00", "date": "2026-03-29T13:47:39.191350+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:17:25.447376+00:00", "date": "2026-03-29T13:47:09.059722+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true
}, },
{ {
"date": "2026-03-14T14:16:55.368379+00:00", "date": "2026-03-29T13:46:39.263054+00:00",
"trigger": "auto", "trigger": "auto",
"serversCount": 1, "serversCount": 1,
"membersCount": 13, "membersCount": 12,
"guildName": "Omega Kube Bêta Test", "guildName": "Omega Kube Bêta Test",
"errors": [], "errors": [],
"success": true "success": true

View file

@ -1,100 +0,0 @@
[
{
"timestamp": "2025-08-19T14:50:31.191194",
"event_type": "member_join",
"member_id": 318312854816161792,
"member_name": "DraftBot#0535",
"member_discriminator": "0535",
"member_avatar": "https://cdn.discordapp.com/avatars/318312854816161792/f1d4dc93441273f3f8985a07e0bad164.png?size=1024"
},
{
"timestamp": "2025-08-19T14:51:48.780957",
"event_type": "member_leave",
"member_id": 828239898975666216,
"member_name": "docteur_w.d_axolotl",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/828239898975666216/1835a7eaede10e9dbb78649beeed070c.png?size=1024",
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2025-05-09T18:36:32.808000+00:00"
},
{
"timestamp": "2025-08-19T14:58:16.673562",
"event_type": "member_leave",
"member_id": 318312854816161792,
"member_name": "DraftBot#0535",
"member_discriminator": "0535",
"member_avatar": "https://cdn.discordapp.com/avatars/318312854816161792/f1d4dc93441273f3f8985a07e0bad164.png?size=1024",
"roles": {
"role_ids": [
1403041729330024508
],
"role_names": [
"🗝️"
],
"permissions": [
8
]
},
"joined_at": "2025-08-19T12:50:31.007000+00:00"
},
{
"timestamp": "2025-08-19T14:59:06.458094",
"event_type": "member_leave",
"member_id": 353942721997832195,
"member_name": "mg_studios_",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/353942721997832195/99c6b7514ce56e19cf87b2c8f0d41316.png?size=1024",
"roles": {
"role_ids": [
1396130252144508939,
1369669999366770724,
1369669999366770726
],
"role_names": [
"mg_studios_",
"👔 | Co-Directeur",
"🔑"
],
"permissions": [
0,
0,
8
]
},
"joined_at": "2025-05-07T13:41:48.145000+00:00"
},
{
"timestamp": "2025-08-19T14:59:19.218122",
"event_type": "member_join",
"member_id": 353942721997832195,
"member_name": "mg_studios_",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/353942721997832195/99c6b7514ce56e19cf87b2c8f0d41316.png?size=1024"
},
{
"timestamp": "2025-08-19T21:41:25.723957",
"event_type": "member_join",
"member_id": 1407448905817391215,
"member_name": "test_33878",
"member_discriminator": "0",
"member_avatar": null
},
{
"timestamp": "2025-08-19T23:48:12.347482",
"event_type": "member_leave",
"member_id": 1407448905817391215,
"member_name": "test_33878",
"member_discriminator": "0",
"member_avatar": null,
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2025-08-19T19:41:25.391607+00:00"
}
]

View file

@ -1,68 +0,0 @@
[
{
"timestamp": "2025-08-20T00:40:57.820048",
"event_type": "member_join",
"member_id": 1407448905817391215,
"member_name": "test_33878",
"member_discriminator": "0",
"member_avatar": null
},
{
"timestamp": "2025-08-20T00:42:00.543535",
"event_type": "member_leave",
"member_id": 1407448905817391215,
"member_name": "test_33878",
"member_discriminator": "0",
"member_avatar": null,
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2025-08-19T22:40:57.711576+00:00"
},
{
"timestamp": "2025-08-20T00:47:10.265057",
"event_type": "member_join",
"member_id": 1407448905817391215,
"member_name": "test_33878",
"member_discriminator": "0",
"member_avatar": null
},
{
"timestamp": "2025-08-20T00:47:23.923297",
"event_type": "member_leave",
"member_id": 1407448905817391215,
"member_name": "test_33878",
"member_discriminator": "0",
"member_avatar": null,
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2025-08-19T22:47:10.020304+00:00"
},
{
"timestamp": "2025-08-20T12:24:08.056140",
"event_type": "member_leave",
"member_id": 1407670411164127242,
"member_name": "test_109375",
"member_discriminator": "0",
"member_avatar": null,
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2025-08-20T10:21:05.730620+00:00"
},
{
"timestamp": "2025-08-20T12:27:27.999521",
"event_type": "member_join",
"member_id": 1407670411164127242,
"member_name": "test_109375",
"member_discriminator": "0",
"member_avatar": null
}
]

View file

@ -1,22 +0,0 @@
[
{
"timestamp": "2025-08-22T19:06:00.143653",
"event_type": "member_leave",
"member_id": 872192738920108074,
"member_name": "titis_zfr_7007",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/872192738920108074/986a18e279578123ec855f63defcc23f.png?size=1024",
"roles": {
"role_ids": [
1369769836028231730
],
"role_names": [
"⚒️ | Bêta Testeurs"
],
"permissions": [
1032
]
},
"joined_at": "2025-07-18T21:59:11.510000+00:00"
}
]

View file

@ -1,106 +0,0 @@
[
{
"timestamp": "2025-12-30T01:03:48.950150",
"event_type": "member_join",
"member_id": 1003956683040624672,
"member_name": "eva56_",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/1003956683040624672/c364667c914a821cd24212d32f7762b8.png?size=1024"
},
{
"timestamp": "2025-12-30T01:04:52.974501",
"event_type": "member_join",
"member_id": 1013376581458210986,
"member_name": "kayrixtv",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/1013376581458210986/42d93e13065339ae40c50bcbf7a7b667.png?size=1024"
},
{
"timestamp": "2025-12-30T01:05:11.496294",
"event_type": "member_leave",
"member_id": 1003956683040624672,
"member_name": "eva56_",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/1003956683040624672/c364667c914a821cd24212d32f7762b8.png?size=1024",
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2025-12-30T00:03:48.810667+00:00"
},
{
"timestamp": "2025-12-30T01:05:38.027616",
"event_type": "member_join",
"member_id": 1003956683040624672,
"member_name": "eva56_",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/1003956683040624672/c364667c914a821cd24212d32f7762b8.png?size=1024"
},
{
"timestamp": "2025-12-30T01:06:16.615009",
"event_type": "member_leave",
"member_id": 1003956683040624672,
"member_name": "eva56_",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/1003956683040624672/c364667c914a821cd24212d32f7762b8.png?size=1024",
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2025-12-30T00:05:37.900874+00:00"
},
{
"timestamp": "2025-12-30T01:06:36.640980",
"event_type": "member_join",
"member_id": 1003956683040624672,
"member_name": "eva56_",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/1003956683040624672/c364667c914a821cd24212d32f7762b8.png?size=1024"
},
{
"timestamp": "2025-12-30T01:09:36.950883",
"event_type": "member_leave",
"member_id": 1003956683040624672,
"member_name": "eva56_",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/1003956683040624672/c364667c914a821cd24212d32f7762b8.png?size=1024",
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2025-12-30T00:06:36.510813+00:00"
},
{
"timestamp": "2025-12-30T01:13:30.386228",
"event_type": "member_join",
"member_id": 1003956683040624672,
"member_name": "eva56_",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/1003956683040624672/c364667c914a821cd24212d32f7762b8.png?size=1024"
},
{
"timestamp": "2025-12-30T01:13:47.629282",
"event_type": "member_leave",
"member_id": 1003956683040624672,
"member_name": "eva56_",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/1003956683040624672/c364667c914a821cd24212d32f7762b8.png?size=1024",
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2025-12-30T00:13:30.252167+00:00"
},
{
"timestamp": "2025-12-30T01:15:07.364595",
"event_type": "member_join",
"member_id": 1003956683040624672,
"member_name": "eva56_",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/1003956683040624672/c364667c914a821cd24212d32f7762b8.png?size=1024"
}
]

View file

@ -1,10 +0,0 @@
[
{
"timestamp": "2026-01-01T22:00:26.255907",
"event_type": "member_join",
"member_id": 1456390746167967932,
"member_name": "juibu_66388",
"member_discriminator": "0",
"member_avatar": null
}
]

View file

@ -1,58 +0,0 @@
[
{
"timestamp": "2026-01-03T21:25:17.730927",
"event_type": "member_leave",
"member_id": 1456390746167967932,
"member_name": "juibu_66388",
"member_discriminator": "0",
"member_avatar": null,
"roles": {
"role_ids": [
1369669999366770721
],
"role_names": [
"Faux banquier"
],
"permissions": [
8192
]
},
"joined_at": "2026-01-01T21:00:26.074000+00:00"
},
{
"timestamp": "2026-01-03T21:25:28.894683",
"event_type": "member_join",
"member_id": 1456390746167967932,
"member_name": "juibu_66388",
"member_discriminator": "0",
"member_avatar": null
},
{
"timestamp": "2026-01-03T21:49:33.975462",
"event_type": "member_leave",
"member_id": 1456390746167967932,
"member_name": "juibu_66388",
"member_discriminator": "0",
"member_avatar": null,
"roles": {
"role_ids": [
1369669999366770721
],
"role_names": [
"Faux banquier"
],
"permissions": [
8192
]
},
"joined_at": "2026-01-03T20:25:28.737000+00:00"
},
{
"timestamp": "2026-01-03T21:49:55.375389",
"event_type": "member_join",
"member_id": 1456390746167967932,
"member_name": "juibu_66388",
"member_discriminator": "0",
"member_avatar": null
}
]

View file

@ -1,10 +0,0 @@
[
{
"timestamp": "2026-01-10T16:19:17.701722",
"event_type": "member_join",
"member_id": 1459567012031234198,
"member_name": "fequjifenq_85720",
"member_discriminator": "0",
"member_avatar": null
}
]

View file

@ -1,10 +0,0 @@
[
{
"timestamp": "2026-01-24T15:13:01.488356",
"event_type": "member_join",
"member_id": 1038035891450556507,
"member_name": "celness",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/1038035891450556507/e57596b7be5db3a5f3baf75f5fd4720b.png?size=1024"
}
]

View file

@ -1,24 +0,0 @@
[
{
"timestamp": "2026-02-08T11:49:12.200722",
"event_type": "member_join",
"member_id": 1470008249322438678,
"member_name": "truc_19275",
"member_discriminator": "0",
"member_avatar": null
},
{
"timestamp": "2026-02-08T12:38:12.823655",
"event_type": "member_leave",
"member_id": 1470008249322438678,
"member_name": "truc_19275",
"member_discriminator": "0",
"member_avatar": null,
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2026-02-08T10:49:12.106083+00:00"
}
]

View file

@ -1,241 +0,0 @@
[
{
"timestamp": "2025-08-15T17:03:39.778010",
"event_type": "deleted",
"message_id": 1405929928641351842,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 1324439906046574693,
"author_name": "_radio_demon77_",
"author_roles": [
1369669999345537145,
1403041729330024508
],
"content": "test",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-08-15T17:04:15.541259",
"event_type": "deleted",
"message_id": 1405930092764336172,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 1324439906046574693,
"author_name": "_radio_demon77_",
"author_roles": [
1369669999345537145,
1403041729330024508
],
"content": "bl",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-08-15T17:04:26.128394",
"event_type": "deleted",
"message_id": 1405930138096635996,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 353942721997832195,
"author_name": "mg_studios_",
"author_roles": [
1369669999345537145,
1396130252144508939,
1369669999366770724,
1369669999366770726
],
"content": "fe",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-08-15T17:04:26.581875",
"event_type": "deleted",
"message_id": 1405930139358859386,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 353942721997832195,
"author_name": "mg_studios_",
"author_roles": [
1369669999345537145,
1396130252144508939,
1369669999366770724,
1369669999366770726
],
"content": "fe",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-08-15T17:04:27.110643",
"event_type": "deleted",
"message_id": 1405930141888282686,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 353942721997832195,
"author_name": "mg_studios_",
"author_roles": [
1369669999345537145,
1396130252144508939,
1369669999366770724,
1369669999366770726
],
"content": "fe",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-08-15T17:04:27.420456",
"event_type": "deleted",
"message_id": 1405930143549100153,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 353942721997832195,
"author_name": "mg_studios_",
"author_roles": [
1369669999345537145,
1396130252144508939,
1369669999366770724,
1369669999366770726
],
"content": "fef",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-08-15T17:04:27.895168",
"event_type": "deleted",
"message_id": 1405930145155514388,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 353942721997832195,
"author_name": "mg_studios_",
"author_roles": [
1369669999345537145,
1396130252144508939,
1369669999366770724,
1369669999366770726
],
"content": "ef",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-08-15T17:04:55.151271",
"event_type": "deleted",
"message_id": 1405930245793517729,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 1324439906046574693,
"author_name": "_radio_demon77_",
"author_roles": [
1369669999345537145,
1403041729330024508
],
"content": "carré pour ça",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-08-15T17:07:29.586201",
"event_type": "edited",
"message_id": 1405930871201988708,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 353942721997832195,
"author_name": "mg_studios_",
"author_roles": [
1369669999345537145,
1396130252144508939,
1369669999366770724,
1369669999366770726
],
"content": "il est pas kick du serv juste il peux plus écrire",
"attachments": [],
"embeds": [],
"details": {
"before_content": "il est pas kick du serv juste il peux plus écrire",
"after_content": "il est pas kick du serv juste il peut plus écrire"
}
},
{
"timestamp": "2025-08-15T17:08:24.314935",
"event_type": "edited",
"message_id": 1405931135254528171,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 1324439906046574693,
"author_name": "_radio_demon77_",
"author_roles": [
1369669999345537145,
1403041729330024508
],
"content": "[fae](https://cdn.discordapp.com/emojis/1377581019649151077.webp?size=48&name=fae)",
"attachments": [],
"embeds": [],
"details": {
"before_content": "[fae](https://cdn.discordapp.com/emojis/1377581019649151077.webp?size=48&name=fae)",
"after_content": "[fae](https://cdn.discordapp.com/emojis/1377581019649151077.webp?size=48&name=fae)"
}
},
{
"timestamp": "2025-08-15T17:51:19.548967",
"event_type": "deleted",
"message_id": 1405941938007314614,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 1324439906046574693,
"author_name": "_radio_demon77_",
"author_roles": [
1369669999345537145,
1403041729330024508
],
"content": "k",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-08-15T17:51:20.415679",
"event_type": "deleted",
"message_id": 1405941942227046552,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 1324439906046574693,
"author_name": "_radio_demon77_",
"author_roles": [
1369669999345537145,
1403041729330024508
],
"content": "k",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-08-15T17:52:26.716628",
"event_type": "deleted",
"message_id": 1405942220049219737,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 1324439906046574693,
"author_name": "_radio_demon77_",
"author_roles": [
1369669999345537145,
1403041729330024508
],
"content": "<@353942721997832195> il me faut qqn de pas op pour tester le spam",
"attachments": [],
"embeds": [],
"details": {}
}
]

View file

@ -1,39 +0,0 @@
[
{
"timestamp": "2025-08-16T17:11:20.495657",
"event_type": "deleted",
"message_id": 1406294262861140100,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 1324439906046574693,
"author_name": "_radio_demon77_",
"author_roles": [
1369669999345537145,
1403041729330024508
],
"content": "ou @here",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-08-16T17:22:10.841132",
"event_type": "edited",
"message_id": 1406296949967290461,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 1324439906046574693,
"author_name": "_radio_demon77_",
"author_roles": [
1369669999345537145,
1403041729330024508
],
"content": "je dois yalelr en plus",
"attachments": [],
"embeds": [],
"details": {
"before_content": "je dois yalelr en plus",
"after_content": "je dois y aller en plus"
}
}
]

View file

@ -1,87 +0,0 @@
[
{
"timestamp": "2025-08-17T13:14:10.888477",
"event_type": "deleted",
"message_id": 1406596967433244725,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 1324439906046574693,
"author_name": "_radio_demon77_",
"author_roles": [
1369669999345537145,
1403041729330024508
],
"content": "h",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-08-17T13:14:11.627298",
"event_type": "deleted",
"message_id": 1406596971757568060,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 1324439906046574693,
"author_name": "_radio_demon77_",
"author_roles": [
1369669999345537145,
1403041729330024508
],
"content": "h",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-08-17T13:14:11.996001",
"event_type": "deleted",
"message_id": 1406596972982308935,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 1324439906046574693,
"author_name": "_radio_demon77_",
"author_roles": [
1369669999345537145,
1403041729330024508
],
"content": "h",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-08-17T13:14:12.397997",
"event_type": "deleted",
"message_id": 1406596974714818569,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 1324439906046574693,
"author_name": "_radio_demon77_",
"author_roles": [
1369669999345537145,
1403041729330024508
],
"content": "h",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-08-17T13:15:46.771221",
"event_type": "deleted",
"message_id": 1406597370661310564,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 1324439906046574693,
"author_name": "_radio_demon77_",
"author_roles": [
1369669999345537145,
1403041729330024508
],
"content": "h",
"attachments": [],
"embeds": [],
"details": {}
}
]

View file

@ -1,303 +0,0 @@
[
{
"timestamp": "2025-08-19T14:31:07.894788",
"event_type": "edited",
"message_id": 1407341109302267986,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1396112611292090368,
1369669999366770726
],
"content": "https://music.youtube.com/watch?v=ZjHjIvlokkg&list=RDAMVMZjHjIvlokkg",
"attachments": [],
"embeds": [],
"details": {
"before_content": "https://music.youtube.com/watch?v=ZjHjIvlokkg&list=RDAMVMZjHjIvlokkg",
"after_content": "https://music.youtube.com/watch?v=ZjHjIvlokkg&list=RDAMVMZjHjIvlokkg"
}
},
{
"timestamp": "2025-08-19T14:31:07.908824",
"event_type": "deleted",
"message_id": 1407341109302267986,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1396112611292090368,
1369669999366770726
],
"content": "https://music.youtube.com/watch?v=ZjHjIvlokkg&list=RDAMVMZjHjIvlokkg",
"attachments": [],
"embeds": [
{
"thumbnail": {
"width": 1280,
"url": "https://i.ytimg.com/vi/ZjHjIvlokkg/maxresdefault.jpg",
"proxy_url": "https://images-ext-1.discordapp.net/external/rqEye0Kbh537345_yiwGFQmGsNDfxFg10JHdnuiizrI/https/i.ytimg.com/vi/ZjHjIvlokkg/maxresdefault.jpg",
"placeholder_version": 1,
"placeholder": "BQgGDIIIplqHd3h4iIUIfHXzJw==",
"height": 720,
"flags": 0,
"content_type": "image/jpeg"
},
"video": {
"width": 1280,
"url": "https://www.youtube.com/embed/ZjHjIvlokkg",
"placeholder_version": 1,
"placeholder": "BQgGDIIIplqHd3h4iIUIfHXzJw==",
"height": 720,
"flags": 0
},
"provider": {
"url": "https://www.youtube.com",
"name": "YouTube"
},
"author": {
"url": "https://www.youtube.com/channel/UCQgovvTTLPxAazVQE6X2ivQ",
"name": "Fredz"
},
"color": 16711680,
"type": "video",
"description": "Les belles filles - Fredz ft. Leslie Medina\nlive au Badaboum à PARIS le 16/12/22\nJ'espère que ça vous plaira ! Merci pour tout\n\nRéalisation: Noémie Fray (@nomzoa)\nÉtalonnage: Stéphane Martins (@smartins_dop)\n\nRetrouvez Fredz sur :\nInstagram : https://www.instagram.com/fredz_musicc/\nTik Tok : https://vm.tiktok.com/ZSv9b5US/\nFacebook : http...",
"url": "https://www.youtube.com/watch?v=ZjHjIvlokkg",
"title": "FREDZ feat. Leslie Medina - Les belles filles (Live à Paris)"
}
],
"details": {}
},
{
"timestamp": "2025-08-19T14:31:25.362973",
"event_type": "deleted",
"message_id": 1407341171679826030,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1396112611292090368,
1369669999366770726
],
"content": "https://music.youtube.com/watch?v=ZjHjIvlokkg&list=RDAMVMZjHjIvlokkg",
"attachments": [],
"embeds": [
{
"thumbnail": {
"width": 1280,
"url": "https://i.ytimg.com/vi/ZjHjIvlokkg/maxresdefault.jpg",
"proxy_url": "https://images-ext-1.discordapp.net/external/rqEye0Kbh537345_yiwGFQmGsNDfxFg10JHdnuiizrI/https/i.ytimg.com/vi/ZjHjIvlokkg/maxresdefault.jpg",
"placeholder_version": 1,
"placeholder": "BQgGDIIIplqHd3h4iIUIfHXzJw==",
"height": 720,
"flags": 0,
"content_type": "image/jpeg"
},
"video": {
"width": 1280,
"url": "https://www.youtube.com/embed/ZjHjIvlokkg",
"placeholder_version": 1,
"placeholder": "BQgGDIIIplqHd3h4iIUIfHXzJw==",
"height": 720,
"flags": 0
},
"provider": {
"url": "https://www.youtube.com",
"name": "YouTube"
},
"author": {
"url": "https://www.youtube.com/channel/UCQgovvTTLPxAazVQE6X2ivQ",
"name": "Fredz"
},
"color": 16711680,
"type": "video",
"description": "Les belles filles - Fredz ft. Leslie Medina\nlive au Badaboum à PARIS le 16/12/22\nJ'espère que ça vous plaira ! Merci pour tout\n\nRéalisation: Noémie Fray (@nomzoa)\nÉtalonnage: Stéphane Martins (@smartins_dop)\n\nRetrouvez Fredz sur :\nInstagram : https://www.instagram.com/fredz_musicc/\nTik Tok : https://vm.tiktok.com/ZSv9b5US/\nFacebook : http...",
"url": "https://www.youtube.com/watch?v=ZjHjIvlokkg",
"title": "FREDZ feat. Leslie Medina - Les belles filles (Live à Paris)"
}
],
"details": {}
},
{
"timestamp": "2025-08-19T14:31:30.378149",
"event_type": "deleted",
"message_id": 1407341203208278036,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1396112611292090368,
1369669999366770726
],
"content": "https://music.youtube.com/watch?v=ZjHjIvlokkg&list=RDAMVMZjHjIvlokkg",
"attachments": [],
"embeds": [
{
"thumbnail": {
"width": 1280,
"url": "https://i.ytimg.com/vi/ZjHjIvlokkg/maxresdefault.jpg",
"proxy_url": "https://images-ext-1.discordapp.net/external/rqEye0Kbh537345_yiwGFQmGsNDfxFg10JHdnuiizrI/https/i.ytimg.com/vi/ZjHjIvlokkg/maxresdefault.jpg",
"placeholder_version": 1,
"placeholder": "BQgGDIIIplqHd3h4iIUIfHXzJw==",
"height": 720,
"flags": 0,
"content_type": "image/jpeg"
},
"video": {
"width": 1280,
"url": "https://www.youtube.com/embed/ZjHjIvlokkg",
"placeholder_version": 1,
"placeholder": "BQgGDIIIplqHd3h4iIUIfHXzJw==",
"height": 720,
"flags": 0
},
"provider": {
"url": "https://www.youtube.com",
"name": "YouTube"
},
"author": {
"url": "https://www.youtube.com/channel/UCQgovvTTLPxAazVQE6X2ivQ",
"name": "Fredz"
},
"color": 16711680,
"type": "video",
"description": "Les belles filles - Fredz ft. Leslie Medina\nlive au Badaboum à PARIS le 16/12/22\nJ'espère que ça vous plaira ! Merci pour tout\n\nRéalisation: Noémie Fray (@nomzoa)\nÉtalonnage: Stéphane Martins (@smartins_dop)\n\nRetrouvez Fredz sur :\nInstagram : https://www.instagram.com/fredz_musicc/\nTik Tok : https://vm.tiktok.com/ZSv9b5US/\nFacebook : http...",
"url": "https://www.youtube.com/watch?v=ZjHjIvlokkg",
"title": "FREDZ feat. Leslie Medina - Les belles filles (Live à Paris)"
}
],
"details": {}
},
{
"timestamp": "2025-08-19T20:00:33.051055",
"event_type": "edited",
"message_id": 1407423989206356008,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1396112611292090368,
1369669999366770723,
1369669999366770726
],
"content": "Viens ici mo cobay préférer",
"attachments": [],
"embeds": [],
"details": {
"before_content": "Viens ici mo cobay préférer",
"after_content": "Viens ici mon cobay préférer"
}
},
{
"timestamp": "2025-08-19T20:09:09.967269",
"event_type": "deleted",
"message_id": 1407426178486374564,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1396112611292090368,
1369669999366770723,
1369669999366770726
],
"content": "@everyone qui veut faire le cobay avec moi",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-08-19T22:19:00.115805",
"event_type": "edited",
"message_id": 1407458853196529705,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 353942721997832195,
"author_name": "mg_studios_",
"author_roles": [
1369669999345537145,
1396130252144508939,
1369669999366770724
],
"content": "https://tenor.com/view/14th-doctor-doctor-who-david-tennant-wild-blue-yonder-rage-gif-12474509552290072827",
"attachments": [],
"embeds": [],
"details": {
"before_content": "https://tenor.com/view/14th-doctor-doctor-who-david-tennant-wild-blue-yonder-rage-gif-12474509552290072827",
"after_content": "https://tenor.com/view/14th-doctor-doctor-who-david-tennant-wild-blue-yonder-rage-gif-12474509552290072827"
}
},
{
"timestamp": "2025-08-19T22:19:14.095772",
"event_type": "edited",
"message_id": 1407458911929503875,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 353942721997832195,
"author_name": "mg_studios_",
"author_roles": [
1369669999345537145,
1396130252144508939,
1369669999366770724
],
"content": "https://tenor.com/view/dr-who-doctor-who-david-tennant-sad-annoyed-gif-4879151",
"attachments": [],
"embeds": [],
"details": {
"before_content": "https://tenor.com/view/dr-who-doctor-who-david-tennant-sad-annoyed-gif-4879151",
"after_content": "https://tenor.com/view/dr-who-doctor-who-david-tennant-sad-annoyed-gif-4879151"
}
},
{
"timestamp": "2025-08-19T22:24:31.079905",
"event_type": "edited",
"message_id": 1407460213409452235,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1396112611292090368,
1369669999366770723,
1369669999366770726
],
"content": "OUi",
"attachments": [],
"embeds": [],
"details": {
"before_content": "OUi",
"after_content": "Oui"
}
},
{
"timestamp": "2025-08-19T22:50:46.776609",
"event_type": "deleted",
"message_id": 1407466849981038592,
"channel_id": 1369669999677145101,
"channel_name": "┇🔧・salon-de-tests",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1396112611292090368,
1369669999366770723,
1369669999366770726
],
"content": "https://www.perplexity.ai",
"attachments": [],
"embeds": [],
"details": {}
}
]

View file

@ -1,34 +0,0 @@
[
{
"timestamp": "2025-08-20T00:42:00.965568",
"event_type": "deleted",
"message_id": 1407494580709883994,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 1407448905817391215,
"author_name": "test_33878",
"author_roles": [
1369669999345537145
],
"content": "",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-08-20T00:47:24.265760",
"event_type": "deleted",
"message_id": 1407496141343559791,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 1407448905817391215,
"author_name": "test_33878",
"author_roles": [
1369669999345537145
],
"content": "",
"attachments": [],
"embeds": [],
"details": {}
}
]

View file

@ -1,40 +0,0 @@
[
{
"timestamp": "2025-08-23T20:54:00.125408",
"event_type": "deleted",
"message_id": 1408887012731129877,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1396112611292090368,
1369669999366770723,
1369669999366770726
],
"content": "G",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-08-23T20:54:00.775382",
"event_type": "deleted",
"message_id": 1408887015570804787,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1396112611292090368,
1369669999366770723,
1369669999366770726
],
"content": "G",
"attachments": [],
"embeds": [],
"details": {}
}
]

View file

@ -1,705 +0,0 @@
[
{
"timestamp": "2025-12-30T14:52:01.432517",
"event_type": "deleted",
"message_id": 1455559052359631046,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T14:52:01.860802",
"event_type": "deleted",
"message_id": 1455559053878235177,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T14:52:02.287326",
"event_type": "deleted",
"message_id": 1455559055304167619,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T14:52:02.656882",
"event_type": "deleted",
"message_id": 1455559057019633797,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T14:52:13.025829",
"event_type": "deleted",
"message_id": 1455559100954837196,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T14:52:13.582926",
"event_type": "deleted",
"message_id": 1455559103081353248,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T14:52:13.780030",
"event_type": "deleted",
"message_id": 1455559104306217063,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T14:52:14.165698",
"event_type": "deleted",
"message_id": 1455559105845395639,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T14:52:14.612894",
"event_type": "deleted",
"message_id": 1455559107791556762,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T14:52:14.798228",
"event_type": "deleted",
"message_id": 1455559108798185593,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T14:52:15.528887",
"event_type": "deleted",
"message_id": 1455559111071629509,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T14:52:16.499969",
"event_type": "deleted",
"message_id": 1455559109825790114,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g\ng",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T14:52:17.142340",
"event_type": "deleted",
"message_id": 1455559112875049105,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T14:52:18.196550",
"event_type": "deleted",
"message_id": 1455559117165826120,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T14:52:19.152467",
"event_type": "deleted",
"message_id": 1455559114519482421,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "gg",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T14:52:21.275434",
"event_type": "deleted",
"message_id": 1455559116280828005,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T14:52:21.407804",
"event_type": "deleted",
"message_id": 1455559118264729792,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:00:35.602064",
"event_type": "deleted",
"message_id": 1455561207493955786,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:00:35.638746",
"event_type": "deleted",
"message_id": 1455561208953442438,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "gg",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:00:36.363545",
"event_type": "deleted",
"message_id": 1455561211679604820,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "gg",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:00:36.826764",
"event_type": "deleted",
"message_id": 1455561214045454437,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:00:37.001960",
"event_type": "deleted",
"message_id": 1455561215060480071,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:00:37.594651",
"event_type": "deleted",
"message_id": 1455561215970644102,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g\ng",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:00:37.727681",
"event_type": "deleted",
"message_id": 1455561217610612777,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:00:40.753645",
"event_type": "deleted",
"message_id": 1455561219493728380,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:04:21.499784",
"event_type": "deleted",
"message_id": 1455562138142642443,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:04:23.080095",
"event_type": "deleted",
"message_id": 1455562136209199266,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:04:24.026167",
"event_type": "deleted",
"message_id": 1455562134749581559,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:04:25.951553",
"event_type": "deleted",
"message_id": 1455562133088637020,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:04:27.294471",
"event_type": "deleted",
"message_id": 1455562131473961092,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:04:28.245179",
"event_type": "deleted",
"message_id": 1455562129750102081,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:04:29.191484",
"event_type": "deleted",
"message_id": 1455562128076312689,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "gg",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:04:30.278917",
"event_type": "deleted",
"message_id": 1455562127220801537,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:36:52.387291",
"event_type": "deleted",
"message_id": 1455570313935847598,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:37:05.116440",
"event_type": "deleted",
"message_id": 1455570329702367242,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "gg",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:42:16.699177",
"event_type": "deleted",
"message_id": 1455571624299462862,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "https://www.ecoledirecte.com/login?cameFrom=%2FE%2F5835%2FEmploiDuTemps",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2025-12-30T15:42:24.379846",
"event_type": "deleted",
"message_id": 1455571718037962854,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "https://www.ecoledirecte.com/login?cameFrom=%2FE%2F5835%2FEmploiDuTemps",
"attachments": [],
"embeds": [],
"details": {}
}
]

View file

@ -1,284 +0,0 @@
[
{
"timestamp": "2026-01-01T21:05:28.276062",
"event_type": "deleted",
"message_id": 1456377795595730994,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:12:06.156108",
"event_type": "deleted",
"message_id": 1456379476211531970,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:12:06.462625",
"event_type": "deleted",
"message_id": 1456379478606348421,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "gg",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:12:12.104726",
"event_type": "deleted",
"message_id": 1456379484264595700,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:35:44.759055",
"event_type": "deleted",
"message_id": 1456385391564296297,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:35:51.064634",
"event_type": "deleted",
"message_id": 1456385392482844682,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:40:18.016792",
"event_type": "deleted",
"message_id": 1456386513947463855,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:40:21.147093",
"event_type": "deleted",
"message_id": 1456386515096703242,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:40:24.322926",
"event_type": "deleted",
"message_id": 1456386516439007288,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:43:53.352930",
"event_type": "deleted",
"message_id": 1456387431472435231,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:44:15.896248",
"event_type": "deleted",
"message_id": 1456387423477956670,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "g",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:44:32.743792",
"event_type": "deleted",
"message_id": 1456387525080776788,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "https://github.com/Omega-Kube/Kuby",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:44:43.533530",
"event_type": "deleted",
"message_id": 1456387651883237437,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "https://github.com/Omega-Kube/Kuby",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T21:51:23.069501",
"event_type": "deleted",
"message_id": 1456389356301582459,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "[DEBUG] anti_link_enabled: True, settings: {'anti_spam': True, 'anti_perm': True, 'anti_salon': True, 'anti_raid': True, 'anti_nuke': True, 'anti_link': True, 'anti_mention': True, 'verification_enabled': True, 'spam_threshold': 5, 'raid_threshold': 3, 'nuke_threshold': 3, 'log_channel_id': 1396088860412346428, 'lockdown_enabled': False, 'auto_ban': True, 'webhook_protection': True, 'mass_delete_protection': True, 'invite_filter': True, 'file_scanning': True, 'ai_moderation': True, 'channel_block_enabled': False, 'server_logs': True, 'anti_channel_delete': True, 'anti_role_create': True}",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-01T22:01:31.237385",
"event_type": "deleted",
"message_id": 1456391913694433361,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 1456390746167967932,
"author_name": "juibu_66388",
"author_roles": [
1369669999345537145
],
"content": "https://github.com/Omega-Kube/Kuby",
"attachments": [],
"embeds": [],
"details": {}
}
]

View file

@ -1,59 +0,0 @@
[
{
"timestamp": "2026-01-02T13:30:27.725564",
"event_type": "deleted",
"message_id": 1456625676537823243,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "gg",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-02T13:30:27.754495",
"event_type": "deleted",
"message_id": 1456625676537823243,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "gg",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-01-02T13:30:37.215876",
"event_type": "deleted",
"message_id": 1456625680153444448,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "gg",
"attachments": [],
"embeds": [],
"details": {}
}
]

View file

@ -1,25 +0,0 @@
[
{
"timestamp": "2026-01-10T00:57:48.563188",
"event_type": "edited",
"message_id": 1459335326660300935,
"channel_id": 1369669999677145098,
"channel_name": "╭💬・général",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369769836028231730,
1369669999366770723,
1403041729330024508,
1369669999366770726
],
"content": "Non c'est gentil t'inqiutes",
"attachments": [],
"embeds": [],
"details": {
"before_content": "Non c'est gentil t'inqiutes",
"after_content": "Non c'est gentil tinquiètes"
}
}
]

View file

@ -1,77 +0,0 @@
[
{
"timestamp": "2026-01-24T15:05:23.725825",
"event_type": "edited",
"message_id": 1464622068598575155,
"channel_id": 1464621977376522291,
"channel_name": "gameurpro12-écrit",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1366461904976871565,
1453708383235473459,
1454035592391299234,
1454034686979346495,
1453708344526246071,
1454034988080038040,
1366876636259024946
],
"content": "Les tickets prendront cette forme",
"attachments": [],
"embeds": [],
"details": {
"before_content": "Les tickets prendront cette forme",
"after_content": "Les tickets whitelist prendront cette forme"
}
},
{
"timestamp": "2026-01-24T15:05:52.316927",
"event_type": "edited",
"message_id": 1464622206439919658,
"channel_id": 1460048825665454344,
"channel_name": "🔢・nombres-infinis",
"author_id": 1355564506754973767,
"author_name": "alto1500",
"author_roles": [
1366461904976871565,
1453708383235473459,
1454042474141192316,
1453708358417912075
],
"content": "24",
"attachments": [],
"embeds": [],
"details": {
"before_content": "24",
"after_content": "240"
}
},
{
"timestamp": "2026-01-24T15:23:49.868209",
"event_type": "edited",
"message_id": 1464626575415705761,
"channel_id": 1453386193311105188,
"channel_name": "📡・annonces",
"author_id": 983747660265693258,
"author_name": "mr_reiko_",
"author_roles": [
1366461904976871565,
1453708383235473459,
1454035592391299234,
1454033803998920769,
1454034686979346495,
1464424459393040667,
1454034988080038040,
1453395626128707656,
1369003041927200858,
1366876636259024946
],
"content": "> # **__Annonce Ouverture officielle des Whitelist__** <:emoji_1:1458776441041850430> \n> \n> **Nous vous informons que les Whitelist sont officiellement ouvertes.** <a:56125loading:1413178655038902482> \n> \n> - Pour effectuer votre WL,** il vous suffit de créer un ticket dans le salon** <#1450002116537090149> / <#1463081283679817822> prévu à cet effet.\n> Une fois le ticket ouvert, **un membre du staff prendra votre demande en charge,** vous expliquera lensemble de la procédure et **vous accompagnera tout au long du processus.** \n> \n> - __**Rôles disponibles en semi FA**__\n> **Les rôles suivants sont actuellement disponibles en semi FA pour les joueurs WL ecrite effectuée :**\n> \n> <@&1453708467700502561> \n> \n> <@&1453708401933553849> \n> \n> <@&1453708560511926273> \n> \n> <@&1453708517302337695> \n> \n> <@&1453708475132674070> \n> \n> <@&1453708562474860617> \n> \n> - **Merci de respecter les consignes et de faire preuve de sérieux lors de votre démarche.**\n> **Nous restons à votre disposition via les tickets pour toute question.** <:50773approvedids:1464433520301572096> £\n> \n> **Cordialement <@&1458084692803981354> / <@&1453708378336399392> /** <t:1769264525:S> \n> \n> ||@everyone ||",
"attachments": [],
"embeds": [],
"details": {
"before_content": "> # **__Annonce Ouverture officielle des Whitelist__** <:emoji_1:1458776441041850430> \n> \n> **Nous vous informons que les Whitelist sont officiellement ouvertes.** <a:56125loading:1413178655038902482> \n> \n> - Pour effectuer votre WL,** il vous suffit de créer un ticket dans le salon** <#1450002116537090149> / <#1463081283679817822> prévu à cet effet.\n> Une fois le ticket ouvert, **un membre du staff prendra votre demande en charge,** vous expliquera lensemble de la procédure et **vous accompagnera tout au long du processus.** \n> \n> - __**Rôles disponibles en semi FA**__\n> **Les rôles suivants sont actuellement disponibles en semi FA pour les joueurs WL ecrite effectuée :**\n> \n> <@&1453708467700502561> \n> \n> <@&1453708401933553849> \n> \n> <@&1453708560511926273> \n> \n> <@&1453708517302337695> \n> \n> <@&1453708475132674070> \n> \n> <@&1453708562474860617> \n> \n> - **Merci de respecter les consignes et de faire preuve de sérieux lors de votre démarche.**\n> **Nous restons à votre disposition via les tickets pour toute question.** <:50773approvedids:1464433520301572096> £\n> \n> **Cordialement <@&1458084692803981354> / <@&1453708378336399392> /** <t:1769264525:S> \n> \n> ||@everyone ||",
"after_content": "> # **__Annonce Ouverture officielle des Whitelist__** <:emoji_1:1458776441041850430> \n> \n> **Nous vous informons que les Whitelist sont officiellement ouvertes.** <a:56125loading:1413178655038902482> \n> \n> - Pour effectuer votre WL,** il vous suffit de créer un ticket dans le salon** <#1450002116537090149> / <#1463081283679817822> prévu à cet effet.\n> Une fois le ticket ouvert, **un membre du staff prendra votre demande en charge,** vous expliquera lensemble de la procédure et **vous accompagnera tout au long du processus.** \n> \n> - __**Rôles disponibles en semi FA**__\n> **Les rôles suivants sont actuellement disponibles en semi FA pour les joueurs WL ecrite effectuée :**\n> \n> <@&1453708467700502561> \n> \n> <@&1453708401933553849> \n> \n> <@&1453708560511926273> \n> \n> <@&1453708517302337695> \n> \n> <@&1453708475132674070> \n> \n> <@&1453708562474860617> \n> \n> - **Merci de respecter les consignes et de faire preuve de sérieux lors de votre démarche.**\n> **Nous restons à votre disposition via les tickets pour toute question.** <:50773approvedids:1464433520301572096> \n> \n> **Cordialement <@&1458084692803981354> / <@&1453708378336399392> /** <t:1769264525:S> \n> \n> ||@everyone ||"
}
}
]

View file

@ -1,38 +0,0 @@
[
{
"timestamp": "2026-02-27T16:51:53.636507",
"event_type": "deleted",
"message_id": 1476970101038125148,
"channel_id": 1369669999677145103,
"channel_name": "╰📂・rapports",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369769836028231730,
1369669999366770726
],
"content": "1369669999677145103",
"attachments": [],
"embeds": [],
"details": {}
},
{
"timestamp": "2026-02-27T16:52:08.494696",
"event_type": "deleted",
"message_id": 1476970163726188639,
"channel_id": 1369669999677145103,
"channel_name": "╰📂・rapports",
"author_id": 971446412690722826,
"author_name": "gameurpro12",
"author_roles": [
1369669999345537145,
1369769836028231730,
1369669999366770726
],
"content": "<@&1369669999345537154>",
"attachments": [],
"embeds": [],
"details": {}
}
]

View file

@ -1,146 +0,0 @@
[
{
"timestamp": "2025-08-19T20:20:09.529504",
"event_type": "joined",
"member_id": 353942721997832195,
"member_name": "mg_studios_",
"channel_id": 1369669999677145105,
"channel_name": "╭🎤・Général",
"channel_type": "voice"
},
{
"timestamp": "2025-08-19T20:20:10.360577",
"event_type": "joined",
"member_id": 971446412690722826,
"member_name": "gameurpro12",
"channel_id": 1369669999677145105,
"channel_name": "╭🎤・Général",
"channel_type": "voice"
},
{
"timestamp": "2025-08-19T20:20:16.614354",
"event_type": "joined",
"member_id": 872192738920108074,
"member_name": "titis_zfr_7007",
"channel_id": 1369669999677145105,
"channel_name": "╭🎤・Général",
"channel_type": "voice"
},
{
"timestamp": "2025-08-19T20:53:27.763722",
"event_type": "left",
"member_id": 872192738920108074,
"member_name": "titis_zfr_7007",
"channel_id": 1369669999677145105,
"channel_name": "╭🎤・Général",
"channel_type": "voice"
},
{
"timestamp": "2025-08-19T21:22:53.512388",
"event_type": "joined",
"member_id": 768915179995004988,
"member_name": "fantom_arn",
"channel_id": 1369669999677145105,
"channel_name": "╭🎤・Général",
"channel_type": "voice"
},
{
"timestamp": "2025-08-19T21:23:56.602997",
"event_type": "left",
"member_id": 768915179995004988,
"member_name": "fantom_arn",
"channel_id": 1369669999677145105,
"channel_name": "╭🎤・Général",
"channel_type": "voice"
},
{
"timestamp": "2025-08-19T21:39:13.888020",
"event_type": "joined",
"member_id": 768915179995004988,
"member_name": "fantom_arn",
"channel_id": 1369669999677145105,
"channel_name": "╭🎤・Général",
"channel_type": "voice"
},
{
"timestamp": "2025-08-19T21:40:19.180475",
"event_type": "left",
"member_id": 768915179995004988,
"member_name": "fantom_arn",
"channel_id": 1369669999677145105,
"channel_name": "╭🎤・Général",
"channel_type": "voice"
},
{
"timestamp": "2025-08-19T21:44:18.819339",
"event_type": "joined",
"member_id": 1108087282692522105,
"member_name": "tricks_zz",
"channel_id": 1369669999677145105,
"channel_name": "╭🎤・Général",
"channel_type": "voice"
},
{
"timestamp": "2025-08-19T21:54:19.095370",
"event_type": "joined",
"member_id": 1069386396764221450,
"member_name": "matlo13",
"channel_id": 1369669999677145105,
"channel_name": "╭🎤・Général",
"channel_type": "voice"
},
{
"timestamp": "2025-08-19T22:12:16.991281",
"event_type": "left",
"member_id": 1108087282692522105,
"member_name": "tricks_zz",
"channel_id": 1369669999677145105,
"channel_name": "╭🎤・Général",
"channel_type": "voice"
},
{
"timestamp": "2025-08-19T22:14:16.428151",
"event_type": "joined",
"member_id": 768915179995004988,
"member_name": "fantom_arn",
"channel_id": 1369669999677145105,
"channel_name": "╭🎤・Général",
"channel_type": "voice"
},
{
"timestamp": "2025-08-19T22:21:48.361355",
"event_type": "left",
"member_id": 971446412690722826,
"member_name": "gameurpro12",
"channel_id": 1369669999677145105,
"channel_name": "╭🎤・Général",
"channel_type": "voice"
},
{
"timestamp": "2025-08-19T22:21:50.542994",
"event_type": "left",
"member_id": 353942721997832195,
"member_name": "mg_studios_",
"channel_id": 1369669999677145105,
"channel_name": "╭🎤・Général",
"channel_type": "voice"
},
{
"timestamp": "2025-08-19T22:23:14.826669",
"event_type": "left",
"member_id": 768915179995004988,
"member_name": "fantom_arn",
"channel_id": 1369669999677145105,
"channel_name": "╭🎤・Général",
"channel_type": "voice"
},
{
"timestamp": "2025-08-19T22:23:15.993478",
"event_type": "left",
"member_id": 1069386396764221450,
"member_name": "matlo13",
"channel_id": 1369669999677145105,
"channel_name": "╭🎤・Général",
"channel_type": "voice"
}
]

View file

@ -1,20 +0,0 @@
[
{
"timestamp": "2025-08-20T12:28:09.674559",
"event_type": "joined",
"member_id": 1407670411164127242,
"member_name": "test_109375",
"channel_id": 1369669999677145105,
"channel_name": "╭🎤・Général",
"channel_type": "voice"
},
{
"timestamp": "2025-08-20T12:28:17.126525",
"event_type": "left",
"member_id": 1407670411164127242,
"member_name": "test_109375",
"channel_id": 1369669999677145105,
"channel_name": "╭🎤・Général",
"channel_type": "voice"
}
]

View file

@ -1,245 +0,0 @@
[
{
"timestamp": "2026-01-24T15:04:34.353302",
"event_type": "left",
"member_id": 1453734314088075306,
"member_name": "patalhuile",
"channel_id": 1407722372588376084,
"channel_name": "〚🌟〛𝐊𝐀𝐘𝐑𝐈𝐗",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:06:06.060691",
"event_type": "joined",
"member_id": 971446412690722826,
"member_name": "gameurpro12",
"channel_id": 1457073413616046080,
"channel_name": "👑・Bureau Fondation",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:06:12.905500",
"event_type": "left",
"member_id": 971446412690722826,
"member_name": "gameurpro12",
"channel_id": 1457073413616046080,
"channel_name": "👑・Bureau Fondation",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:12:23.224137",
"event_type": "joined",
"member_id": 983747660265693258,
"member_name": "mr_reiko_",
"channel_id": 1453397135234957442,
"channel_name": "📓・Bureaux 1",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:12:25.430175",
"event_type": "left",
"member_id": 983747660265693258,
"member_name": "mr_reiko_",
"channel_id": 1453397135234957442,
"channel_name": "📓・Bureaux 1",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:12:30.909163",
"event_type": "left",
"member_id": 1263573205268828231,
"member_name": "arthena_931",
"channel_id": 1457073413616046080,
"channel_name": "👑・Bureau Fondation",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:13:02.024297",
"event_type": "left",
"member_id": 353942721997832195,
"member_name": "mg_studios_",
"channel_id": 1457073413616046080,
"channel_name": "👑・Bureau Fondation",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:13:17.531634",
"event_type": "joined",
"member_id": 1263573205268828231,
"member_name": "arthena_931",
"channel_id": 1447649079952802055,
"channel_name": "🔊・Publique 1",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:13:26.906602",
"event_type": "left",
"member_id": 1263573205268828231,
"member_name": "arthena_931",
"channel_id": 1447649079952802055,
"channel_name": "🔊・Publique 1",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:15:50.817844",
"event_type": "joined",
"member_id": 1453734314088075306,
"member_name": "patalhuile",
"channel_id": 1407722372588376084,
"channel_name": "〚🌟〛𝐊𝐀𝐘𝐑𝐈𝐗",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:18:40.775427",
"event_type": "left",
"member_id": 945580436732641320,
"member_name": "melhoan",
"channel_id": 1407722372588376084,
"channel_name": "〚🌟〛𝐊𝐀𝐘𝐑𝐈𝐗",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:18:44.516024",
"event_type": "joined",
"member_id": 945580436732641320,
"member_name": "melhoan",
"channel_id": 1407722372588376084,
"channel_name": "〚🌟〛𝐊𝐀𝐘𝐑𝐈𝐗",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:20:36.058676",
"event_type": "left",
"member_id": 1453734314088075306,
"member_name": "patalhuile",
"channel_id": 1407722372588376084,
"channel_name": "〚🌟〛𝐊𝐀𝐘𝐑𝐈𝐗",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:27:14.017625",
"event_type": "left",
"member_id": 945580436732641320,
"member_name": "melhoan",
"channel_id": 1407722372588376084,
"channel_name": "〚🌟〛𝐊𝐀𝐘𝐑𝐈𝐗",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:29:52.855600",
"event_type": "joined",
"member_id": 1263573205268828231,
"member_name": "arthena_931",
"channel_id": 1407780201462497522,
"channel_name": "〚🔈〛𝐀𝐓𝐓𝐄𝐍𝐓𝐄 𝐌𝐎𝐎𝐕",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:31:33.895832",
"event_type": "joined",
"member_id": 1296914730669183066,
"member_name": "iikileur_02378",
"channel_id": 1447649079952802055,
"channel_name": "🔊・Publique 1",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:31:44.051002",
"event_type": "moved_from",
"member_id": 534003230435442690,
"member_name": "nixfix06",
"channel_id": 1457073413616046080,
"channel_name": "👑・Bureau Fondation",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:31:44.051366",
"event_type": "moved_to",
"member_id": 534003230435442690,
"member_name": "nixfix06",
"channel_id": 1447649079952802055,
"channel_name": "🔊・Publique 1",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:33:48.580429",
"event_type": "joined",
"member_id": 1320118755090759711,
"member_name": "jl_xixo",
"channel_id": 1447649079952802055,
"channel_name": "🔊・Publique 1",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:33:51.104140",
"event_type": "joined",
"member_id": 1389671060902777056,
"member_name": "elite_v.2_70323",
"channel_id": 1407780201462497522,
"channel_name": "〚🔈〛𝐀𝐓𝐓𝐄𝐍𝐓𝐄 𝐌𝐎𝐎𝐕",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:35:01.627777",
"event_type": "left",
"member_id": 1389671060902777056,
"member_name": "elite_v.2_70323",
"channel_id": 1407780201462497522,
"channel_name": "〚🔈〛𝐀𝐓𝐓𝐄𝐍𝐓𝐄 𝐌𝐎𝐎𝐕",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:35:02.783293",
"event_type": "left",
"member_id": 1296914730669183066,
"member_name": "iikileur_02378",
"channel_id": 1447649079952802055,
"channel_name": "🔊・Publique 1",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:35:09.067327",
"event_type": "left",
"member_id": 1263573205268828231,
"member_name": "arthena_931",
"channel_id": 1407780201462497522,
"channel_name": "〚🔈〛𝐀𝐓𝐓𝐄𝐍𝐓𝐄 𝐌𝐎𝐎𝐕",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:35:15.317831",
"event_type": "moved_from",
"member_id": 534003230435442690,
"member_name": "nixfix06",
"channel_id": 1447649079952802055,
"channel_name": "🔊・Publique 1",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:35:15.318377",
"event_type": "moved_to",
"member_id": 534003230435442690,
"member_name": "nixfix06",
"channel_id": 1457073756764639355,
"channel_name": "👔・Bureau Direction",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:35:20.522454",
"event_type": "left",
"member_id": 1320118755090759711,
"member_name": "jl_xixo",
"channel_id": 1447649079952802055,
"channel_name": "🔊・Publique 1",
"channel_type": "voice"
},
{
"timestamp": "2026-01-24T15:37:06.015710",
"event_type": "joined",
"member_id": 1433834565042180149,
"member_name": "alexandre07_08_13804",
"channel_id": 1447649079952802055,
"channel_name": "🔊・Publique 1",
"channel_type": "voice"
}
]

View file

@ -1,58 +0,0 @@
[
{
"timestamp": "2025-12-30T01:05:11.496294",
"event_type": "member_leave",
"member_id": 1003956683040624672,
"member_name": "eva56_",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/1003956683040624672/c364667c914a821cd24212d32f7762b8.png?size=1024",
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2025-12-30T00:03:48.810667+00:00"
},
{
"timestamp": "2025-12-30T01:06:16.615009",
"event_type": "member_leave",
"member_id": 1003956683040624672,
"member_name": "eva56_",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/1003956683040624672/c364667c914a821cd24212d32f7762b8.png?size=1024",
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2025-12-30T00:05:37.900874+00:00"
},
{
"timestamp": "2025-12-30T01:09:36.950883",
"event_type": "member_leave",
"member_id": 1003956683040624672,
"member_name": "eva56_",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/1003956683040624672/c364667c914a821cd24212d32f7762b8.png?size=1024",
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2025-12-30T00:06:36.510813+00:00"
},
{
"timestamp": "2025-12-30T01:13:47.629282",
"event_type": "member_leave",
"member_id": 1003956683040624672,
"member_name": "eva56_",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/1003956683040624672/c364667c914a821cd24212d32f7762b8.png?size=1024",
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2025-12-30T00:13:30.252167+00:00"
}
]

View file

@ -1,44 +0,0 @@
[
{
"timestamp": "2025-08-19T23:48:12.347482",
"event_type": "member_leave",
"member_id": 1407448905817391215,
"member_name": "test_33878",
"member_discriminator": "0",
"member_avatar": null,
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2025-08-19T19:41:25.391607+00:00"
},
{
"timestamp": "2025-08-20T00:42:00.543535",
"event_type": "member_leave",
"member_id": 1407448905817391215,
"member_name": "test_33878",
"member_discriminator": "0",
"member_avatar": null,
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2025-08-19T22:40:57.711576+00:00"
},
{
"timestamp": "2025-08-20T00:47:23.923297",
"event_type": "member_leave",
"member_id": 1407448905817391215,
"member_name": "test_33878",
"member_discriminator": "0",
"member_avatar": null,
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2025-08-19T22:47:10.020304+00:00"
}
]

View file

@ -1,16 +0,0 @@
[
{
"timestamp": "2025-08-20T12:24:08.056140",
"event_type": "member_leave",
"member_id": 1407670411164127242,
"member_name": "test_109375",
"member_discriminator": "0",
"member_avatar": null,
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2025-08-20T10:21:05.730620+00:00"
}
]

View file

@ -1,42 +0,0 @@
[
{
"timestamp": "2026-01-03T21:25:17.730927",
"event_type": "member_leave",
"member_id": 1456390746167967932,
"member_name": "juibu_66388",
"member_discriminator": "0",
"member_avatar": null,
"roles": {
"role_ids": [
1369669999366770721
],
"role_names": [
"Faux banquier"
],
"permissions": [
8192
]
},
"joined_at": "2026-01-01T21:00:26.074000+00:00"
},
{
"timestamp": "2026-01-03T21:49:33.975462",
"event_type": "member_leave",
"member_id": 1456390746167967932,
"member_name": "juibu_66388",
"member_discriminator": "0",
"member_avatar": null,
"roles": {
"role_ids": [
1369669999366770721
],
"role_names": [
"Faux banquier"
],
"permissions": [
8192
]
},
"joined_at": "2026-01-03T20:25:28.737000+00:00"
}
]

View file

@ -1,16 +0,0 @@
[
{
"timestamp": "2026-02-08T12:38:12.823655",
"event_type": "member_leave",
"member_id": 1470008249322438678,
"member_name": "truc_19275",
"member_discriminator": "0",
"member_avatar": null,
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2026-02-08T10:49:12.106083+00:00"
}
]

View file

@ -1,22 +0,0 @@
[
{
"timestamp": "2025-08-19T14:58:16.673562",
"event_type": "member_leave",
"member_id": 318312854816161792,
"member_name": "DraftBot#0535",
"member_discriminator": "0535",
"member_avatar": "https://cdn.discordapp.com/avatars/318312854816161792/f1d4dc93441273f3f8985a07e0bad164.png?size=1024",
"roles": {
"role_ids": [
1403041729330024508
],
"role_names": [
"🗝️"
],
"permissions": [
8
]
},
"joined_at": "2025-08-19T12:50:31.007000+00:00"
}
]

View file

@ -1,28 +0,0 @@
[
{
"timestamp": "2025-08-19T14:59:06.458094",
"event_type": "member_leave",
"member_id": 353942721997832195,
"member_name": "mg_studios_",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/353942721997832195/99c6b7514ce56e19cf87b2c8f0d41316.png?size=1024",
"roles": {
"role_ids": [
1396130252144508939,
1369669999366770724,
1369669999366770726
],
"role_names": [
"mg_studios_",
"👔 | Co-Directeur",
"🔑"
],
"permissions": [
0,
0,
8
]
},
"joined_at": "2025-05-07T13:41:48.145000+00:00"
}
]

View file

@ -1,16 +0,0 @@
[
{
"timestamp": "2025-08-19T14:51:48.780957",
"event_type": "member_leave",
"member_id": 828239898975666216,
"member_name": "docteur_w.d_axolotl",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/828239898975666216/1835a7eaede10e9dbb78649beeed070c.png?size=1024",
"roles": {
"role_ids": [],
"role_names": [],
"permissions": []
},
"joined_at": "2025-05-09T18:36:32.808000+00:00"
}
]

View file

@ -1,22 +0,0 @@
[
{
"timestamp": "2025-08-22T19:06:00.143653",
"event_type": "member_leave",
"member_id": 872192738920108074,
"member_name": "titis_zfr_7007",
"member_discriminator": "0",
"member_avatar": "https://cdn.discordapp.com/avatars/872192738920108074/986a18e279578123ec855f63defcc23f.png?size=1024",
"roles": {
"role_ids": [
1369769836028231730
],
"role_names": [
"⚒️ | Bêta Testeurs"
],
"permissions": [
1032
]
},
"joined_at": "2025-07-18T21:59:11.510000+00:00"
}
]

View file

@ -33,16 +33,16 @@ def send_discord_log(status, message, color, author=None, commit_msg=None):
except Exception as e: except Exception as e:
print(f"[!] Erreur envoi Discord : {e}") print(f"[!] Erreur envoi Discord : {e}")
def run_mep_logic(author, commit_msg): def run_mep_logic(author, commit_msg, branch="main"):
start_time = datetime.now() start_time = datetime.now()
send_discord_log("EN COURS", "Déploiement lancé sur la machine de guerre...", 3447003, author, commit_msg) send_discord_log("EN COURS", f"Déploiement branch **{branch}** lancé sur la machine de guerre...", 3447003, author, commit_msg)
try: try:
os.chdir(PROJECT_PATH) os.chdir(PROJECT_PATH)
# 1. GIT # 1. GIT
subprocess.run(["git", "fetch", "origin"], check=True) subprocess.run(["git", "fetch", "origin"], check=True)
subprocess.run(["git", "reset", "--hard", "origin/main"], check=True) subprocess.run(["git", "reset", "--hard", f"origin/{branch}"], check=True)
duration = (datetime.now() - start_time).total_seconds() duration = (datetime.now() - start_time).total_seconds()
msg = f"✅ **Déploiement réussi en {duration:.2f}s**\nLe bot est à jour et redémarré." msg = f"✅ **Déploiement réussi en {duration:.2f}s**\nLe bot est à jour et redémarré."
@ -70,8 +70,12 @@ def deploy():
author = data['commits'][0].get('author', {}).get('name', 'Inconnu') author = data['commits'][0].get('author', {}).get('name', 'Inconnu')
commit_msg = data['commits'][0].get('message', 'Pas de message') commit_msg = data['commits'][0].get('message', 'Pas de message')
# Extraction de la branche
ref = data.get('ref', 'refs/heads/main')
branch = ref.split('/')[-1] if ref else 'main'
# Threading pour répondre vite à GitLab # Threading pour répondre vite à GitLab
threading.Thread(target=run_mep_logic, args=(author, commit_msg)).start() threading.Thread(target=run_mep_logic, args=(author, commit_msg, branch)).start()
return {"status": "accepted"}, 202 return {"status": "accepted"}, 202

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,47 +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""" """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"
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: try:
# Append to existing file or create new date_str = datetime.now().strftime("%Y-%m-%d")
if os.path.exists(log_file): log_file = f"{self.logs_dir}/{log_type}_{date_str}.json"
with open(log_file, 'r', encoding='utf-8') as f:
existing_logs = json.load(f)
else:
existing_logs = []
existing_logs.append(data)
with open(log_file, 'w', encoding='utf-8') as f: # Simple append in NDJSON format
json.dump(existing_logs, f, indent=2, ensure_ascii=False) with open(log_file, 'a', encoding='utf-8') as f:
f.write(json.dumps(data, ensure_ascii=False) + "\n")
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
print(f"CRITICAL ERROR in AdvancedLogger._write_log_sync: {e}")
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""" """Write member-specific log using NDJSON (Append-only)"""
member_log_file = f"{self.member_logs_dir}/{member_id}.json"
try: try:
# Append to existing file or create new member_log_file = f"{self.member_logs_dir}/{member_id}.json"
if os.path.exists(member_log_file):
with open(member_log_file, 'r', encoding='utf-8') as f:
existing_logs = json.load(f)
else:
existing_logs = []
existing_logs.append(data)
with open(member_log_file, 'w', encoding='utf-8') as f: with open(member_log_file, 'a', encoding='utf-8') as f:
json.dump(existing_logs, f, indent=2, ensure_ascii=False) f.write(json.dumps(data, ensure_ascii=False) + "\n")
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}")
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

View file

@ -3,10 +3,14 @@ import sys
import time import time
import functools import functools
import asyncio import asyncio
import json
import os
from datetime import datetime from datetime import datetime
from typing import Any, Callable from typing import Any, Callable
import traceback import traceback
REPORTED_ERRORS_FILE = "data/automated_reports.json"
class KubyLogger: class KubyLogger:
def __init__(self, name="KubyBot"): def __init__(self, name="KubyBot"):
self.logger = logging.getLogger(name) self.logger = logging.getLogger(name)
@ -53,37 +57,69 @@ class KubyLogger:
def __init__(self, client): def __init__(self, client):
super().__init__() super().__init__()
self.client = client self.client = client
self.last_errors = {}
self.cooldown = 3600 # 1 hour cooldown per unique error self.cooldown = 3600 # 1 hour cooldown per unique error
self.last_errors = self._load_reported_errors()
def _load_reported_errors(self):
if os.path.exists(REPORTED_ERRORS_FILE):
try:
with open(REPORTED_ERRORS_FILE, "r") as f:
return json.load(f)
except Exception as e:
print(f"Error loading reported errors: {e}")
return {}
def _save_reported_errors(self):
try:
os.makedirs(os.path.dirname(REPORTED_ERRORS_FILE), exist_ok=True)
with open(REPORTED_ERRORS_FILE, "w") as f:
json.dump(self.last_errors, f, indent=4)
except Exception as e:
print(f"Error saving reported errors: {e}")
def emit(self, record): def emit(self, record):
if record.levelno >= logging.ERROR: if record.levelno >= logging.ERROR:
# Extract message and first line of traceback if available
msg = record.getMessage() msg = record.getMessage()
error_key = msg # Use message + location + simplified traceback for unique key
if record.exc_info: # This key is used for the local cooldown
error_key += str(record.exc_info[1]) error_key = f"{record.name}.{record.funcName}:{record.lineno} - {msg}"
current_time = time.time() current_time = time.time()
if error_key in self.last_errors and current_time - self.last_errors[error_key] < self.cooldown: last_time = self.last_errors.get(error_key, 0)
return # Skip if same error within cooldown
if current_time - last_time < self.cooldown:
return
self.last_errors[error_key] = current_time self.last_errors[error_key] = current_time
self._save_reported_errors()
# Create issue (Fire and forget, we don't want to block logging) # Prepare issue data
description = f"**Type:** {record.levelname}\n" description = f"**Type:** {record.levelname}\n"
description += f"**Message:** {msg}\n" description += f"**Message:** {msg}\n"
description += f"**Source:** {record.name}.{record.funcName}:{record.lineno}\n\n" description += f"**Source:** {record.name}.{record.funcName}:{record.lineno}\n\n"
if record.exc_info: if record.exc_info:
description += f"**Traceback:**\n```python\n{traceback.format_exc()}\n```" description += f"**Traceback:**\n```python\n{traceback.format_exc()}\n```"
title = f"[AUTO] Error: {msg[:50]}..." title = f"[AUTO] Error: {msg[:100]}..."
labels = ["bug", "auto-report"]
# Async reporting flow
async def report_flow(client, t, d, l):
try:
# Check if an open issue with the SAME title already exists on GitLab
existing = await client.find_issue_by_title(t, state="opened")
if existing:
# If it exists, we don't create a new one to avoid clutter
return
# Otherwise, create it
await client.create_issue(t, d, l)
except Exception as e:
print(f"Error in automated reporting flow: {e}")
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
if loop.is_running(): if loop.is_running():
loop.create_task(self.client.create_issue(title, description, labels=["bug", "auto-report"])) loop.create_task(report_flow(self.client, title, description, labels))
else:
# Fallback if loop is not running (rare for this bot)
pass
gitlab_handler = GitLabHandler(gitlab_client) gitlab_handler = GitLabHandler(gitlab_client)
gitlab_handler.setLevel(logging.ERROR) gitlab_handler.setLevel(logging.ERROR)

View file

@ -63,6 +63,35 @@ class GitLabClient:
gitlab_internal_logger.error(f"❌ Error connecting to GitLab API: {e}") gitlab_internal_logger.error(f"❌ Error connecting to GitLab API: {e}")
return None return None
async def find_issue_by_title(self, title: str, state: str = "opened") -> Optional[dict]:
"""
Searches for an issue by its exact title and state.
"""
if not self.token or not self.project_id:
return None
headers = {"PRIVATE-TOKEN": self.token}
params = {
"search": title,
"state": state
}
async with aiohttp.ClientSession() as session:
try:
# search param is fuzzy on GitLab, so we must double-check for exact match
async with session.get(self.api_url, headers=headers, params=params) as response:
if response.status == 200:
issues = await response.json()
for issue in issues:
if issue.get("title") == title:
return issue
return None
else:
return None
except Exception as e:
gitlab_internal_logger.error(f"❌ Error searching GitLab issues: {e}")
return None
async def get_closed_issues(self) -> List[dict]: async def get_closed_issues(self) -> List[dict]:
""" """
Fetches recently closed issues. Fetches recently closed issues.