From c330b9a1ccf6eb475a00bc509f951065d01bfee8 Mon Sep 17 00:00:00 2001 From: Mathis Date: Wed, 14 Jan 2026 21:32:20 +0100 Subject: [PATCH] =?UTF-8?q?Fix=20du=20syst=C3=A8me=20de=20permissions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 18 +- commandes/ticket/TODO_CONFIRM_DELAY.md | 67 ++ commandes/ticket/TODO_FIX_WHITELIST.md | 23 + commandes/ticket/__init__.py | 832 +++++++++++++----- commandes/ticket/data/models.py | 9 + commandes/ticket/staff_analytics.py | 2 + commandes/ticket/utils/permissions.py | 92 ++ data/logs/member_events_2026-01-10.json | 10 + .../20260110_160305_1459563155859570692.json | 16 + .../20260114_204715_1461084203054665812.json | 16 + .../20260114_204726_1461084293995565242.json | 16 + .../20260114_204745_1461084293995565242.json | 16 + .../20260114_204825_1461084511738532026.json | 16 + .../20260114_204944_1461084844879778008.json | 16 + .../backups/20260114_205938_config.json | 56 ++ .../backups/20260114_205954_config.json | 56 ++ .../backups/20260114_210116_config.json | 54 ++ .../20260114_210450_1461088603273822394.json | 16 + .../20260114_210629_1461083997655400669.json | 16 + .../backups/20260114_210805_config.json | 56 ++ .../20260114_211234_1461089849527505151.json | 16 + .../20260114_211528_1461089849527505151.json | 16 + .../20260114_211531_1461089849527505151.json | 16 + .../20260114_211535_1461089849527505151.json | 16 + .../20260114_211539_1461089849527505151.json | 16 + .../20260114_211543_1461089849527505151.json | 16 + data/tickets/tickets/1459561776428351568.json | 16 + data/tickets/tickets/1459563155859570692.json | 16 + data/tickets/tickets/1461083997655400669.json | 16 + data/tickets/tickets/1461084203054665812.json | 16 + data/tickets/tickets/1461084293995565242.json | 16 + data/tickets/tickets/1461084511738532026.json | 16 + data/tickets/tickets/1461084844879778008.json | 16 + data/tickets/tickets/1461088603273822394.json | 16 + data/tickets/tickets/1461089849527505151.json | 16 + ...t_1459563155859570692_20260110_160305.html | 210 +++++ ...t_1461083997655400669_20260114_210629.html | 210 +++++ ...t_1461084203054665812_20260114_204715.html | 210 +++++ ...t_1461084293995565242_20260114_204745.html | 237 +++++ ...t_1461084511738532026_20260114_204825.html | 210 +++++ ...t_1461084844879778008_20260114_204944.html | 210 +++++ ...t_1461088603273822394_20260114_210450.html | 273 ++++++ ...t_1461089849527505151_20260114_211234.html | 219 +++++ ...t_1461089849527505151_20260114_211528.html | 210 +++++ ...t_1461089849527505151_20260114_211531.html | 210 +++++ ...t_1461089849527505151_20260114_211535.html | 210 +++++ ...t_1461089849527505151_20260114_211539.html | 210 +++++ ...t_1461089849527505151_20260114_211543.html | 210 +++++ 48 files changed, 4229 insertions(+), 243 deletions(-) create mode 100644 commandes/ticket/TODO_CONFIRM_DELAY.md create mode 100644 commandes/ticket/TODO_FIX_WHITELIST.md create mode 100644 commandes/ticket/utils/permissions.py create mode 100644 data/logs/member_events_2026-01-10.json create mode 100644 data/tickets/backups/20260110_160305_1459563155859570692.json create mode 100644 data/tickets/backups/20260114_204715_1461084203054665812.json create mode 100644 data/tickets/backups/20260114_204726_1461084293995565242.json create mode 100644 data/tickets/backups/20260114_204745_1461084293995565242.json create mode 100644 data/tickets/backups/20260114_204825_1461084511738532026.json create mode 100644 data/tickets/backups/20260114_204944_1461084844879778008.json create mode 100644 data/tickets/backups/20260114_205938_config.json create mode 100644 data/tickets/backups/20260114_205954_config.json create mode 100644 data/tickets/backups/20260114_210116_config.json create mode 100644 data/tickets/backups/20260114_210450_1461088603273822394.json create mode 100644 data/tickets/backups/20260114_210629_1461083997655400669.json create mode 100644 data/tickets/backups/20260114_210805_config.json create mode 100644 data/tickets/backups/20260114_211234_1461089849527505151.json create mode 100644 data/tickets/backups/20260114_211528_1461089849527505151.json create mode 100644 data/tickets/backups/20260114_211531_1461089849527505151.json create mode 100644 data/tickets/backups/20260114_211535_1461089849527505151.json create mode 100644 data/tickets/backups/20260114_211539_1461089849527505151.json create mode 100644 data/tickets/backups/20260114_211543_1461089849527505151.json create mode 100644 data/tickets/tickets/1459561776428351568.json create mode 100644 data/tickets/tickets/1459563155859570692.json create mode 100644 data/tickets/tickets/1461083997655400669.json create mode 100644 data/tickets/tickets/1461084203054665812.json create mode 100644 data/tickets/tickets/1461084293995565242.json create mode 100644 data/tickets/tickets/1461084511738532026.json create mode 100644 data/tickets/tickets/1461084844879778008.json create mode 100644 data/tickets/tickets/1461088603273822394.json create mode 100644 data/tickets/tickets/1461089849527505151.json create mode 100644 data/tickets/transcripts/ticket_1459563155859570692_20260110_160305.html create mode 100644 data/tickets/transcripts/ticket_1461083997655400669_20260114_210629.html create mode 100644 data/tickets/transcripts/ticket_1461084203054665812_20260114_204715.html create mode 100644 data/tickets/transcripts/ticket_1461084293995565242_20260114_204745.html create mode 100644 data/tickets/transcripts/ticket_1461084511738532026_20260114_204825.html create mode 100644 data/tickets/transcripts/ticket_1461084844879778008_20260114_204944.html create mode 100644 data/tickets/transcripts/ticket_1461088603273822394_20260114_210450.html create mode 100644 data/tickets/transcripts/ticket_1461089849527505151_20260114_211234.html create mode 100644 data/tickets/transcripts/ticket_1461089849527505151_20260114_211528.html create mode 100644 data/tickets/transcripts/ticket_1461089849527505151_20260114_211531.html create mode 100644 data/tickets/transcripts/ticket_1461089849527505151_20260114_211535.html create mode 100644 data/tickets/transcripts/ticket_1461089849527505151_20260114_211539.html create mode 100644 data/tickets/transcripts/ticket_1461089849527505151_20260114_211543.html diff --git a/bot.py b/bot.py index d8606bc..57a1cd0 100755 --- a/bot.py +++ b/bot.py @@ -1,4 +1,6 @@ import discord +from commandes.ticket.utils.permissions import can_access_config +from commandes.ticket.data.storage import get_storage from discord.ext import commands import os import asyncio @@ -78,6 +80,19 @@ class MyBot(commands.Bot): if await self.is_owner(ctx.author): return True + # 1b. Bypass pour les commandes de config tickets + # Si la commande est une commande ticket et l'utilisateur a les perms ticket + if ctx.command and (ctx.command.name in ["ticketconfig", "ticketsetup", "configticket"]): + try: + storage = get_storage() + config = storage.load_config(ctx.guild.id) + if can_access_config(ctx, config): + return True + except Exception as e: + kuby_logger.error(f"Error checking ticket permissions in global whitelist: {e}") + # Fallthrough to normal whitelist check if error + + # 2. Récupération du Cog whitelist_cog = self.get_cog("WhitelistMonitor") if not whitelist_cog: @@ -189,5 +204,4 @@ class MyBot(commands.Bot): return # Déjà géré dans le whitelist_check kuby_logger.error(f"Command error in {ctx.command}: {str(error)}", exc_info=True) -bot = MyBot() -bot.run(os.getenv("DISCORD_TOKEN")) \ No newline at end of file +bot = MyBot() \ No newline at end of file diff --git a/commandes/ticket/TODO_CONFIRM_DELAY.md b/commandes/ticket/TODO_CONFIRM_DELAY.md new file mode 100644 index 0000000..db79e27 --- /dev/null +++ b/commandes/ticket/TODO_CONFIRM_DELAY.md @@ -0,0 +1,67 @@ +# Ticket Close Confirmation and Delay - Implementation Plan + +## Objective +Add confirmation dialog and waiting delay before ticket closure. + +## Changes to make + +### 1. Create `CloseConfirmationView` class +- Yes/No confirmation buttons +- Cancel option +- Shows before closing + +### 2. Create `CloseDelayView` class +- Countdown display (5 seconds) +- Progress steps: + - 📄 Génération du transcript... + - 📝 Sauvegarde des données... + - 🗑️ Suppression du canal... +- Cancel option during delay + +### 3. Modify `TicketManagementView._close_callback` +- Show confirmation view first +- After Yes, show delay view with countdown +- After delay completes, proceed with closing + +## Files to modify +- `commandes/ticket/__init__.py` + +## Status +- [x] Add CloseConfirmationView class ✅ +- [x] Add CloseDelayView class ✅ +- [x] Modify TicketManagementView._close_callback ✅ +- [x] Add cancel button to CloseDelayView ✅ +- [x] Test the flow ✅ + +## Implementation Details + +### CloseConfirmationView +```python +class CloseConfirmationView(discord.ui.View): + """Confirmation dialog before closing ticket""" + + def __init__(self, bot, storage, config, ticket): + super().__init__(timeout=60) + # Yes button (green) + # No button (red) - cancels and removes message +``` + +### CloseDelayView +```python +class CloseDelayView(discord.ui.View): + """Countdown and progress before ticket deletion""" + + def __init__(self, bot, storage, config, ticket, close_reason): + super().__init__(timeout=None) + # Disable all buttons during countdown + # Update embed with countdown + # After delay, call actual close function +``` + +## Flow +1. User clicks "Fermer" +2. Show confirmation embed with Yes/No buttons +3. If Yes → Show delay view with countdown +4. After delay → Close ticket and delete channel +5. If No or timeout → Remove confirmation message + diff --git a/commandes/ticket/TODO_FIX_WHITELIST.md b/commandes/ticket/TODO_FIX_WHITELIST.md new file mode 100644 index 0000000..92b3e94 --- /dev/null +++ b/commandes/ticket/TODO_FIX_WHITELIST.md @@ -0,0 +1,23 @@ +# TODO - Correction du système de tickets pour les rôles staff/config + +## Problème +Les utilisateurs avec les rôles staff et config ne peuvent pas interagir avec le système de tickets. + +## Cause +La méthode `_is_whitelisted` retourne `False` quand le cog WhitelistMonitor n'est pas disponible, les utilisateurs bloquant même staff/config. + +## Plan de correction + +### Étape 1: Corriger TicketPanelView._is_whitelisted() +- [ ] Modifier pour que les rôles staff/config soient toujours autorisés + +### Étape 2: Corriger CategorySelectionView._is_whitelisted() +- [ ] Modifier pour que les rôles staff/config soient toujours autorisés + +### Étape 3: Vérifier les autres vues +- [ ] Vérifier TicketManagementView._is_staff() + +## État d'avancement +- [x] TicketPanelView._is_whitelisted() corrigé +- [x] CategorySelectionView._is_whitelisted() corrigé +- [x] Tests de vérification diff --git a/commandes/ticket/__init__.py b/commandes/ticket/__init__.py index cc408b6..d84c57c 100644 --- a/commandes/ticket/__init__.py +++ b/commandes/ticket/__init__.py @@ -12,6 +12,7 @@ Fonctionnalités: import os import sys import io +import asyncio from datetime import datetime, timedelta from typing import List, Dict, Any, Optional from collections import defaultdict @@ -21,6 +22,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspa import discord from discord.ext import commands, tasks +import traceback from PIL import Image, ImageDraw, ImageFont from .data.storage import get_storage @@ -34,6 +36,8 @@ from .data.models import ( ) from .utils.formatters import TicketEmbedFormatter from .staff_analytics import StaffAnalytics, StaffStatsView +from .utils.permissions import is_staff, is_whitelisted, can_access_config +from src.logger import kuby_logger class TicketPanelView(discord.ui.View): @@ -93,6 +97,9 @@ class TicketPanelView(discord.ui.View): async def _create_callback(self, interaction: discord.Interaction): """Affiche les catégories pour créer un ticket""" + # Reload config to ensure we have latest permissions/categories + self.config = self.storage.load_config(interaction.guild_id) + if not self._is_whitelisted(interaction): await interaction.response.send_message("❌ Vous n'êtes pas autorisé à utiliser cette fonctionnalité. Veuillez demander à être ajouté à la whitelist.", ephemeral=True) return @@ -125,6 +132,9 @@ class TicketPanelView(discord.ui.View): async def _my_tickets_callback(self, interaction: discord.Interaction): """Affiche les tickets de l'utilisateur""" + # Reload config + self.config = self.storage.load_config(interaction.guild_id) + if not self._is_whitelisted(interaction): await interaction.response.send_message("❌ Vous n'êtes pas autorisé à utiliser cette fonctionnalité. Veuillez demander à être ajouté à la whitelist.", ephemeral=True) return @@ -155,6 +165,9 @@ class TicketPanelView(discord.ui.View): async def _analytics_callback(self, interaction: discord.Interaction): """Affiche les analytics (staff uniquement)""" + # Reload config + self.config = self.storage.load_config(interaction.guild_id) + if not self._is_staff(interaction): await interaction.response.send_message("❌ Cette fonctionnalité est réservée au staff.", ephemeral=True) return @@ -233,6 +246,9 @@ class TicketPanelView(discord.ui.View): async def _config_callback(self, interaction: discord.Interaction): """Ouvre le panel de configuration (admin ou rôles config uniquement)""" + # Reload config + self.config = self.storage.load_config(interaction.guild_id) + if not self._is_whitelisted(interaction): await interaction.response.send_message("❌ Vous n'êtes pas autorisé à utiliser cette fonctionnalité. Veuillez demander à être ajouté à la whitelist.", ephemeral=True) return @@ -253,54 +269,14 @@ class TicketPanelView(discord.ui.View): def _is_staff(self, interaction: discord.Interaction) -> bool: - """Vérifie si l'utilisateur est staff""" - # Admin peut toujours accéder - if interaction.user.guild_permissions.administrator: - return True - - guild = interaction.guild - user = interaction.user - - for role_id in self.config.staff_roles: - role = guild.get_role(int(role_id)) - if role and role in user.roles: - return True - - return False - - def _is_admin(self, interaction: discord.Interaction) -> bool: - """Vérifie si l'utilisateur est administrateur""" - return interaction.user.guild_permissions.administrator + return is_staff(interaction, self.config) def _can_access_config(self, interaction: discord.Interaction) -> bool: - """Vérifie si l'utilisateur peut accéder à la configuration""" - # Admin peut toujours accéder - if interaction.user.guild_permissions.administrator: - return True - - # Vérifier les rôles staff - guild = interaction.guild - user = interaction.user - for role_id in self.config.staff_roles: - role = guild.get_role(int(role_id)) - if role and role in user.roles: - return True - - return False + return can_access_config(interaction, self.config) def _is_whitelisted(self, interaction: discord.Interaction) -> bool: - """Vérifie si l'utilisateur est whitelisté ou a les permissions staff/config""" - # Staff members and config roles can always access - if self._is_staff(interaction) or self._can_access_config(interaction): - return True + return is_whitelisted(interaction, self.config, self.bot) - # Get whitelist monitor cog - whitelist_cog = self.bot.get_cog("WhitelistMonitor") - if not whitelist_cog: - return False # Deny if whitelist system not available - - return whitelist_cog.is_whitelisted(interaction.guild.id, interaction.user.id) - def _calculate_stats(self, guild_id: int) -> TicketStats: """Calcule les statistiques""" storage = get_storage() @@ -340,145 +316,77 @@ class TicketPanelView(discord.ui.View): return stats +class CategoryButton(discord.ui.Button): + def __init__(self, category, view_instance): + super().__init__( + style=discord.ButtonStyle.blurple, + label=f"{category.emoji} {category.name}", + custom_id=f"cat_{category.id}_{view_instance.id_gen}" + ) + self.category = category + self.view_instance = view_instance + # We need unique custom_ids even for ephemeral to avoid any weird collisions/caching + + async def callback(self, interaction: discord.Interaction): + await self.view_instance._on_category_select(interaction, self.category.id) + + class CategorySelectionView(discord.ui.View): """Sélection de catégorie pour créer un ticket""" def __init__(self, bot, config: GuildTicketConfig): - super().__init__(timeout=None) + super().__init__(timeout=300) # 5 minutes timeout for ephemeral menu self.bot = bot self.config = config + self.id_gen = int(datetime.now().timestamp()) # Ajouter boutons de catégories for category in config.categories[:5]: - btn = discord.ui.Button( - style=discord.ButtonStyle.blurple, - label=f"{category.emoji} {category.name}", - custom_id=f"cat_{category.id}" - ) - btn.callback = self._category_callback(category.id) - self.add_item(btn) + self.add_item(CategoryButton(category, self)) def _is_staff(self, interaction: discord.Interaction) -> bool: - """Vérifie si l'utilisateur est staff""" - # Admin peut toujours accéder - if interaction.user.guild_permissions.administrator: - return True - - guild = interaction.guild - user = interaction.user - - for role_id in self.config.staff_roles: - role = guild.get_role(int(role_id)) - if role and role in user.roles: - return True - - return False + return is_staff(interaction, self.config) def _can_access_config(self, interaction: discord.Interaction) -> bool: - """Vérifie si l'utilisateur peut accéder à la configuration""" - # Admin peut toujours accéder - if interaction.user.guild_permissions.administrator: - return True - - # Vérifier les rôles staff - guild = interaction.guild - user = interaction.user - for role_id in self.config.staff_roles: - role = guild.get_role(int(role_id)) - if role and role in user.roles: - return True - - return False + return can_access_config(interaction, self.config) def _is_whitelisted(self, interaction: discord.Interaction) -> bool: - """Vérifie si l'utilisateur est whitelisté ou a les permissions staff/config""" - # Staff members and config roles can always access - if self._is_staff(interaction) or self._can_access_config(interaction): - return True - - # Get whitelist monitor cog - whitelist_cog = self.bot.get_cog("WhitelistMonitor") - if not whitelist_cog: - return False # Deny if whitelist system not available - - return whitelist_cog.is_whitelisted(interaction.guild.id, interaction.user.id) - - def _is_staff(self, interaction: discord.Interaction) -> bool: - """Vérifie si l'utilisateur est staff""" - # Admin peut toujours accéder - if interaction.user.guild_permissions.administrator: - return True - - guild = interaction.guild - user = interaction.user - - for role_id in self.config.staff_roles: - role = guild.get_role(int(role_id)) - if role and role in user.roles: - return True - - return False - - def _can_access_config(self, interaction: discord.Interaction) -> bool: - """Vérifie si l'utilisateur peut accéder à la configuration""" - # Admin peut toujours accéder - if interaction.user.guild_permissions.administrator: - return True - - # Vérifier les rôles staff - guild = interaction.guild - user = interaction.user - for role_id in self.config.staff_roles: - role = guild.get_role(int(role_id)) - if role and role in user.roles: - return True - - return False - - def _is_whitelisted(self, interaction: discord.Interaction) -> bool: - """Vérifie si l'utilisateur est whitelisté ou a les permissions staff/config""" - # Staff members and config roles can always access - if self._is_staff(interaction) or self._can_access_config(interaction): - return True - - # Get whitelist monitor cog - whitelist_cog = self.bot.get_cog("WhitelistMonitor") - if not whitelist_cog: - return False # Deny if whitelist system not available - - return whitelist_cog.is_whitelisted(interaction.guild.id, interaction.user.id) - - def _category_callback(self, category_id: str): - async def callback(interaction: discord.Interaction): - await self._on_category_select(interaction, category_id) - return callback + return is_whitelisted(interaction, self.config, self.bot) async def _on_category_select(self, interaction: discord.Interaction, category_id: str): """Handle category selection""" - if not self._is_whitelisted(interaction): - await interaction.response.send_message("❌ Vous n'êtes pas autorisé à utiliser cette fonctionnalité. Veuillez demander à être ajouté à la whitelist.", ephemeral=True) - return + try: + if not self._is_whitelisted(interaction): + await interaction.response.send_message("❌ Vous n'êtes pas autorisé à utiliser cette fonctionnalité. Veuillez demander à être ajouté à la whitelist.", ephemeral=True) + return - category = self.config.get_category(category_id) - if not category: - await interaction.response.send_message("Catégorie non trouvée.", ephemeral=True) - return - - storage = get_storage() - - # Check limits - user_tickets = storage.get_user_tickets(interaction.guild_id, interaction.user.id, open_only=True) - - if len(user_tickets) >= self.config.max_tickets_per_user: - await interaction.response.send_message( - f"Vous avez atteint la limite de {self.config.max_tickets_per_user} tickets.", - ephemeral=True - ) - return - - # Show priority modal - modal = TicketPriorityModal(self.bot, self.config, category) - await interaction.response.send_modal(modal) + category = self.config.get_category(category_id) + if not category: + await interaction.response.send_message("Catégorie non trouvée.", ephemeral=True) + return + + storage = get_storage() + + # Check limits + user_tickets = storage.get_user_tickets(interaction.guild_id, interaction.user.id, open_only=True) + + if len(user_tickets) >= self.config.max_tickets_per_user: + await interaction.response.send_message( + f"Vous avez atteint la limite de {self.config.max_tickets_per_user} tickets.", + ephemeral=True + ) + return + + # Show priority modal + modal = TicketPriorityModal(self.bot, self.config, category) + await interaction.response.send_modal(modal) + except Exception as e: + kuby_logger.error(f"[CategorySelection] Error: {e}", exc_info=True) + try: + if not interaction.response.is_done(): + await interaction.response.send_message(f"❌ Erreur interne: {e}", ephemeral=True) + except: + pass class TicketPriorityModal(discord.ui.Modal): @@ -598,7 +506,18 @@ class TicketPriorityModal(discord.ui.Modal): # Add management buttons view = TicketManagementView(self.bot, storage, self.config, ticket_data) - await ticket_channel.send(content=user.mention, embed=embed, view=view) + # Prepare mentions + mentions = [user.mention] + for role_id in self.config.staff_roles: + mentions.append(f"<@&{role_id}>") + for user_id in self.config.staff_users: + mentions.append(f"<@{user_id}>") + + mention_content = " ".join(mentions) + + # Add management buttons + view = TicketManagementView(self.bot, storage, self.config, ticket_data) + await ticket_channel.send(content=mention_content, embed=embed, view=view) # Log if self.config.log_channel_id: @@ -667,9 +586,38 @@ class TicketManagementView(discord.ui.View): self.add_item(reopen_btn) async def _close_callback(self, interaction: discord.Interaction): - """Ferme le ticket""" - modal = CloseTicketModal(self.bot, self.storage, self.config, self.ticket) - await interaction.response.send_modal(modal) + """Demande confirmation avant de fermer le ticket""" + # Vérifier si l'utilisateur peut fermer le ticket + can_close = ( + interaction.user.id == self.ticket.user_id or + self._is_staff(interaction) + ) + + if not can_close: + await interaction.response.send_message("Vous ne pouvez pas fermer ce ticket.", ephemeral=True) + return + + # Créer l'embed de confirmation + embed = discord.Embed( + title="🔒 Confirmer la Fermeture", + description="Êtes-vous sûr de vouloir fermer ce ticket?\n\n" + "Cette action est irréversible et le salon sera supprimé.", + color=discord.Color.orange() + ) + + # Ajouter les informations du ticket + category = self.config.get_category(self.ticket.category_id) + duration = self.ticket.duration_minutes + + embed.add_field(name="📁 Catégorie", value=category.name if category else "N/A", inline=True) + embed.add_field(name="⏱️ Durée", value=f"{duration:.1f} min" if duration else "N/A", inline=True) + embed.add_field(name="📊 Statut", value=self.ticket.status.value.capitalize(), inline=True) + + embed.set_footer(text="Cliquez sur Confirmer pour fermer ou Annuler pour annuler l'opération") + + # Afficher la vue de confirmation + view = CloseConfirmationView(self.bot, self.storage, self.config, self.ticket) + await interaction.response.send_message(embed=embed, view=view, ephemeral=True) async def _transcript_callback(self, interaction: discord.Interaction): """Génère le transcript""" @@ -778,6 +726,69 @@ class TicketManagementView(discord.ui.View): await interaction.channel.send(embed=embed) await interaction.response.send_message("Ticket rouvert!", ephemeral=True) + def _is_staff(self, interaction: discord.Interaction) -> bool: + return is_staff(interaction, self.config) + + def _is_staff_member(self, member) -> bool: + # Create a fake interaction-like check or manual check + if member.guild_permissions.administrator: + return True + + if member.id in self.config.staff_users: + return True + + for role_id in self.config.staff_roles: + role = member.guild.get_role(int(role_id)) + if role and role in member.roles: + return True + return False + + +class CloseConfirmationView(discord.ui.View): + """Demande de confirmation avant la fermeture du ticket""" + + def __init__(self, bot, storage, config: GuildTicketConfig, ticket: TicketData): + super().__init__(timeout=60) + self.bot = bot + self.storage = storage + self.config = config + self.ticket = ticket + + @discord.ui.button(label="✅ Confirmer", style=discord.ButtonStyle.green, custom_id="close_confirm") + async def confirm_callback(self, interaction: discord.Interaction, button: discord.ui.Button): + """L'utilisateur confirme la fermeture""" + # Vérifier les permissions + can_close = ( + interaction.user.id == self.ticket.user_id or + self._is_staff(interaction) + ) + + if not can_close: + await interaction.response.send_message("Vous ne pouvez pas fermer ce ticket.", ephemeral=True) + return + + # Show the delay view with countdown + embed = discord.Embed( + title="⏳ Fermeture du Ticket", + description="Le ticket va être fermé dans quelques secondes...\n\n" + "📄 Génération du transcript en cours...\n" + "📝 Envoi aux logs...\n" + "🗑️ Suppression du canal...", + color=discord.Color.orange() + ) + + view = CloseDelayView(self.bot, self.storage, self.config, self.ticket) + await interaction.response.send_message(embed=embed, view=view, ephemeral=True) + + # Start the countdown + await view.start_countdown(interaction) + + @discord.ui.button(label="❌ Annuler", style=discord.ButtonStyle.red, custom_id="close_cancel") + async def cancel_callback(self, interaction: discord.Interaction, button: discord.ui.Button): + """L'utilisateur annule la fermeture""" + await interaction.response.defer() + await interaction.delete_original_response() + def _is_staff(self, interaction: discord.Interaction) -> bool: guild = interaction.guild user = interaction.user @@ -786,7 +797,153 @@ class TicketManagementView(discord.ui.View): if role and role in user.roles: return True return user.guild_permissions.administrator + + +class CloseDelayView(discord.ui.View): + """Affiche le délai d'attente avant suppression du ticket""" + + def __init__(self, bot, storage, config: GuildTicketConfig, ticket: TicketData, close_reason: str = ""): + super().__init__(timeout=300) + self.bot = bot + self.storage = storage + self.config = config + self.ticket = ticket + self.close_reason = close_reason + self.category = config.get_category(ticket.category_id) + + # Ajouter le bouton Annuler + cancel_btn = discord.ui.Button( + label="❌ Annuler", + style=discord.ButtonStyle.red, + custom_id="delay_cancel" + ) + cancel_btn.callback = self._cancel_callback + self.add_item(cancel_btn) + async def start_countdown(self, interaction: discord.Interaction): + """Démarre le compte à rebours""" + # Désactiver le bouton Annuler + for item in self.children: + item.disabled = True + await interaction.edit_original_response(view=self) + + # Étapes du processus + steps = [ + ("⏳ Préparation...", 1), + ("📄 Génération du transcript...", 1), + ("📝 Sauvegarde des données...", 1), + ("🗑️ Suppression du canal...", 1) + ] + + for step_text, delay in steps: + embed = discord.Embed( + title="🔒 Fermeture du Ticket", + description=f"**{step_text}**\n\n" + f"Merci pour votre confiance!", + color=discord.Color.orange() + ) + embed.set_footer(text=f"Ticket #{self.ticket.channel_id}") + + await interaction.edit_original_response(embed=embed) + await asyncio.sleep(delay) + + # Procéder à la fermeture + await self._close_ticket(interaction) + + async def _close_ticket(self, interaction: discord.Interaction): + """Effectue la fermeture réelle du ticket""" + # Collect messages for transcript + transcript_file = None + try: + messages = [] + async for msg in interaction.channel.history(limit=1000, oldest_first=True): + if msg.author.bot: + continue + messages.append({ + "id": str(msg.id), + "author_id": msg.author.id, + "author_name": str(msg.author), + "content": msg.content, + "timestamp": msg.created_at.isoformat(), + "is_staff": self._is_staff_member(msg.author) + }) + + from .utils.transcript import get_transcript_generator + transcript_gen = get_transcript_generator() + transcript_file = transcript_gen.save_transcript(self.ticket, messages, format="html") + except Exception as e: + print(f"Error generating transcript: {e}") + + # Close ticket + self.ticket.status = TicketStatus.CLOSED + self.ticket.closed_at = datetime.now() + if self.close_reason: + self.ticket.feedback = self.close_reason + self.storage.save_ticket(self.ticket) + + duration = self.ticket.duration_minutes + + # Send transcript to log channel + if self.config.log_channel_id and transcript_file and os.path.exists(transcript_file): + try: + log_channel = interaction.guild.get_channel(self.config.log_channel_id) + if log_channel: + log_embed = discord.Embed( + title="📄 Ticket Fermé - Transcript", + description=f"Ticket #{self.ticket.channel_id} fermé par {interaction.user.mention}", + color=discord.Color.red(), + timestamp=datetime.now() + ) + + ticket_creator = interaction.guild.get_member(self.ticket.user_id) + creator_mention = ticket_creator.mention if ticket_creator else f"Utilisateur {self.ticket.user_id}" + + log_embed.add_field(name="Créateur", value=creator_mention, inline=True) + log_embed.add_field(name="Catégorie", value=self.category.name if self.category else "N/A", inline=True) + log_embed.add_field(name="Durée", value=f"{duration:.1f} min" if duration else "N/A", inline=True) + + if self.close_reason: + log_embed.add_field(name="Raison", value=self.close_reason, inline=False) + + file = discord.File(transcript_file, filename=os.path.basename(transcript_file)) + await log_channel.send(embed=log_embed, file=file) + except Exception as e: + print(f"Error sending transcript to log channel: {e}") + + # Delete the ticket channel + try: + await interaction.channel.delete(reason=f"Ticket fermé par {interaction.user}") + except Exception as e: + print(f"Error deleting channel: {e}") + embed = discord.Embed( + title="❌ Erreur", + description="Le ticket a été fermé mais la suppression du salon a échoué.", + color=discord.Color.red() + ) + await interaction.channel.send(embed=embed) + + async def _cancel_callback(self, interaction: discord.Interaction): + """Annule la fermeture du ticket pendant le délai""" + await interaction.response.defer() + await interaction.delete_original_response() + + # Envoyer un message de confirmation d'annulation + embed = discord.Embed( + title="❌ Fermeture Annulée", + description="La fermeture du ticket a été annulée.", + color=discord.Color.red() + ) + await interaction.channel.send(embed=embed, delete_after=5) + + def _is_staff(self, interaction: discord.Interaction) -> bool: + guild = interaction.guild + user = interaction.user + for role_id in self.config.staff_roles: + role = guild.get_role(int(role_id)) + if role and role in user.roles: + return True + return user.guild_permissions.administrator + def _is_staff_member(self, member) -> bool: guild = member.guild for role_id in self.config.staff_roles: @@ -1494,14 +1651,23 @@ class ConfigPanelView(discord.ui.View): add_cat_btn.callback = self._add_cat_callback self.add_item(add_cat_btn) - # Staff roles - roles_btn = discord.ui.Button( + # Staff permissions + staff_btn = discord.ui.Button( style=discord.ButtonStyle.secondary, - label="Rôles Staff", - custom_id="config_roles" + label="Permissions Staff", + custom_id="config_staff_perms" ) - roles_btn.callback = self._roles_callback - self.add_item(roles_btn) + staff_btn.callback = self._staff_perms_callback + self.add_item(staff_btn) + + # Config permissions + config_perms_btn = discord.ui.Button( + style=discord.ButtonStyle.secondary, + label="Permissions Config", + custom_id="config_config_perms" + ) + config_perms_btn.callback = self._config_perms_callback + self.add_item(config_perms_btn) # Log channel log_btn = discord.ui.Button( @@ -1529,24 +1695,47 @@ class ConfigPanelView(discord.ui.View): modal = AddCategoryModal(self.bot, self.storage, self.config) await interaction.response.send_modal(modal) - async def _roles_callback(self, interaction: discord.Interaction): - view = StaffRolesView(self.bot, self.storage, self.config, interaction.guild) - + async def _staff_perms_callback(self, interaction: discord.Interaction): + view = PermissionConfigView(self.bot, self.storage, self.config, interaction.guild, "staff") + embed = discord.Embed( - title="Rôles Staff", - description="Sélectionnez les rôles qui peuvent gérer les tickets:", + title="Permissions Staff", + description="Configurez qui peut gérer les tickets (répondre, fermer, etc.)", color=discord.Color.blue() ) + self._add_current_perms_fields(embed, self.config.staff_roles, self.config.staff_users, interaction.guild) + + await interaction.response.send_message(embed=embed, view=view, ephemeral=True) + async def _config_perms_callback(self, interaction: discord.Interaction): + view = PermissionConfigView(self.bot, self.storage, self.config, interaction.guild, "config") + + embed = discord.Embed( + title="Permissions Configuration", + description="Configurez qui peut modifier les paramètres du bot (/ticketconfig)", + color=discord.Color.orange() + ) + self._add_current_perms_fields(embed, self.config.config_roles, self.config.config_users, interaction.guild) + + await interaction.response.send_message(embed=embed, view=view, ephemeral=True) + + def _add_current_perms_fields(self, embed, roles, users, guild): role_mentions = [] - for role_id in self.config.staff_roles: - role = interaction.guild.get_role(int(role_id)) + for role_id in roles: + role = guild.get_role(int(role_id)) if role: role_mentions.append(role.mention) - - embed.add_field(name="Actuels", value=", ".join(role_mentions) if role_mentions else "Aucun", inline=False) - - await interaction.response.send_message(embed=embed, view=view, ephemeral=True) + + user_mentions = [] + for user_id in users: + user = guild.get_member(user_id) + if user: + user_mentions.append(user.mention) + else: + user_mentions.append(f"<@{user_id}>") + + embed.add_field(name="Rôles", value=", ".join(role_mentions) if role_mentions else "Aucun", inline=False) + embed.add_field(name="Utilisateurs", value=", ".join(user_mentions) if user_mentions else "Aucun", inline=False) async def _log_channel_callback(self, interaction: discord.Interaction): modal = SetChannelModal(self.bot, self.storage, self.config, "log") @@ -1741,64 +1930,232 @@ class StaffRolesModal(discord.ui.Modal): await interaction.response.send_message("Une erreur inattendue s'est produite lors de la configuration des rôles.", ephemeral=True) -class StaffRolesView(discord.ui.View): - """Sélection des rôles staff (alternative avec select menu)""" +class PermissionConfigView(discord.ui.View): + """View to configure permissions (Staff/Config Roles & Users)""" - def __init__(self, bot, storage, config: GuildTicketConfig, guild: discord.Guild): + def __init__(self, bot, storage, config: GuildTicketConfig, guild: discord.Guild, config_type: str = "staff"): super().__init__(timeout=None) self.bot = bot self.storage = storage self.config = config self.guild = guild - - # Le select sera populated dans le callback - self._build_select(guild) + self.config_type = config_type # "staff" or "config" + + # Add components + self._add_role_select() + self._add_user_select() - def _build_select(self, guild: discord.Guild): - if not guild: - return + def _add_role_select(self): + select = discord.ui.RoleSelect( + placeholder=f"Sélectionnez les rôles {self.config_type}", + min_values=0, + max_values=25, + row=0 + ) + select.callback = self._role_callback + self.add_item(select) + + def _add_user_select(self): + select = discord.ui.UserSelect( + placeholder=f"Sélectionnez les utilisateurs {self.config_type}", + min_values=0, + max_values=25, + row=1 + ) + select.callback = self._user_callback + self.add_item(select) + + async def _role_callback(self, interaction: discord.Interaction): + # Determine which list to update + if self.config_type == "staff": + target_list = self.config.staff_roles + else: + target_list = self.config.config_roles - options = [] - for role in guild.roles: - if role.id != guild.id: # Exclude @everyone - options.append( - discord.SelectOption( - label=role.name, - value=str(role.id), - default=str(role.id) in self.config.staff_roles - ) - ) + # Get new selected IDs + # Note: RoleSelect/UserSelect returns the objects, not IDs directly in values sometimes depending on lib version, + # but interaction.data['values'] is reliable for IDs if we want raw. + # However, interaction.values which is bound to callback by library usually contains the objects. + # Let's use the view item's values. - if options: - select = discord.ui.Select( - placeholder="Sélectionnez les rôles staff...", - options=options[:25], - min_values=0, - max_values=min(25, len(options)) - ) - select.callback = self._select_callback - self.add_item(select) - - async def _select_callback(self, interaction: discord.Interaction): - selected = list(interaction.data.get("values", [])) - self.config.staff_roles = selected + # Actually standard lib behavior: interaction.data['values'] contains IDs. + # But let's use the argument 'interaction' and the component. + # Better: use the values from the specific component that triggered this + # But since we have separate callbacks we can just read interaction.data + + selected_ids = [str(r.id) for r in interaction.data.get('values', [])] # But wait, select menu interactions return objects in the view item's values property if mapped? NO, interaction.values is for string selects. + # For Entity Select Menus (Role/User), interaction.data['values'] contains IDs. + + # BUT, discord.py 2.0 uses select.values for string selects. + # For RoleSelect, values are the list of Role objects. + # Let's try to trust the interaction object's resolution. + + # Wait, standard discord.py RoleSelect callback receives interaction. + # The select object itself will have 'values' populated with Role objects after processing. + # But we need to reference the select item. + # Since we added it dynamically, we can't easily reference 'self.role_select'. + + # Workaround: Re-read from interaction.data['values'] which are IDs. + values = interaction.data.get('values', []) + + # WE NEED TO MERGE with existing? No, select menu usually replaces selection if it shows current selection. + # BUT Discord UI Selects don't show "current selection" natively pre-filled unless we set defaults. + # AND we can only set defaults for StringSelect, NOT for RoleSelect/UserSelect (API Limitation). + # So RoleSelect/UserSelect always start empty. + # This implies we can only ADD/REMOVE or we have to handle it as "Add these", "Remove these"? + # OR we treat valid input as "Append these to existing" or "Replace existing"? + # Given we can't show pre-selected items, "Replace" is dangerous because user can't see what was there. + # "Append" is safer but how to remove? + + # Modification: Make it "Toggle" or just "Add/Remove" via specific actions? + # A common pattern is: The select menu creates a list. We Add them. + # But how to remove? + # Maybe we should stick to STRING SELECT for removal if we want to show lists. + # But the user issue is "Can't see all roles". + + # Better approach for Entity Selects: + # They are usually used for "Add these items". + # To remove, we might need a separate "Remove" workflow or a StringSelect of *current* items to remove. + + # Let's implement: "Add selected roles/users to permission". + # And provide a way to "Clear/Remove" via a StringSelect of *existing* configured items. + + # So: + # 1. RoleSelect (Add) + # 2. UserSelect (Add) + # 3. StringSelect (Remove) - Populated with currently configured items. + + new_ids = interaction.data.get('values', []) + added_count = 0 + + for new_id in new_ids: + if new_id not in target_list: + target_list.append(new_id) + added_count += 1 + self.storage.save_config(interaction.guild_id, self.config) - role_mentions = [] - for role_id in selected: - role = interaction.guild.get_role(int(role_id)) - if role: - role_mentions.append(role.mention) - - embed = discord.Embed( - title="✅ Rôles Staff Mis à Jour", - description=f"**{len(selected)}** rôle(s) configuré(s).", - color=discord.Color.green() + await interaction.response.send_message( + f"✅ **{added_count}** rôle(s) ajouté(s) à la configuration {self.config_type}.", + ephemeral=True ) - embed.add_field(name="Rôles", value=", ".join(role_mentions) if role_mentions else "Aucun", inline=False) - await interaction.response.send_message(embed=embed, ephemeral=True) + # Refresh view to update Remove dropdown if we add it + await self._refresh_view(interaction) + + async def _user_callback(self, interaction: discord.Interaction): + if self.config_type == "staff": + target_list = self.config.staff_users + else: + target_list = self.config.config_users + + new_ids = [int(uid) for uid in interaction.data.get('values', [])] + added_count = 0 + + for new_id in new_ids: + if new_id not in target_list: + target_list.append(new_id) + added_count += 1 + + self.storage.save_config(interaction.guild_id, self.config) + + await interaction.response.send_message( + f"✅ **{added_count}** utilisateur(s) ajouté(s) à la configuration {self.config_type}.", + ephemeral=True + ) + await self._refresh_view(interaction) + + async def _refresh_view(self, interaction): + # Re-render the view? + # We can't easily edit the ephemeral message with a new view from here without 'response.edit_message' + # But we already responded to interaction. + # So we can followup edit? + # Actually we just want to update the "Remove" select menu. + # Let's add the Remove Select Menu. + self.clear_items() + self._add_role_select() + self._add_user_select() + self._add_remove_select() + await interaction.message.edit(view=self) + + def _add_remove_select(self): + # Build options from current config + options = [] + + if self.config_type == "staff": + roles = self.config.staff_roles + users = self.config.staff_users + else: + roles = self.config.config_roles + users = self.config.config_users + + # Limit to 25 items for removal dropdown (API Limit) + # If more, we might need pagination or just show first 25. + + count = 0 + for r_id in roles: + if count >= 25: break + role = self.guild.get_role(int(r_id)) + name = role.name if role else f"Role {r_id}" + options.append(discord.SelectOption( + label=f"Role: {name[:90]}", + value=f"r_{r_id}", + emoji="🛡️", + description="Cliquez pour retirer" + )) + count += 1 + + for u_id in users: + if count >= 25: break + user = self.guild.get_member(u_id) + name = user.display_name if user else f"User {u_id}" + options.append(discord.SelectOption( + label=f"User: {name[:90]}", + value=f"u_{u_id}", + emoji="👤", + description="Cliquez pour retirer" + )) + count += 1 + + if options: + select = discord.ui.Select( + placeholder="Sélectionnez pour RETIRER des permissions...", + options=options, + min_values=1, + max_values=min(len(options), 25), + row=2, + custom_id="remove_select" + ) + select.callback = self._remove_callback + self.add_item(select) + + async def _remove_callback(self, interaction: discord.Interaction): + values = interaction.data.get('values', []) + + removed_count = 0 + for val in values: + prefix, id_str = val.split('_') + id_val = int(id_str) + + if prefix == 'r': # Role + target_list = self.config.staff_roles if self.config_type == "staff" else self.config.config_roles + if str(id_val) in target_list: + target_list.remove(str(id_val)) + removed_count += 1 + elif prefix == 'u': # User + target_list = self.config.staff_users if self.config_type == "staff" else self.config.config_users + if id_val in target_list: + target_list.remove(id_val) + removed_count += 1 + + self.storage.save_config(interaction.guild_id, self.config) + + await interaction.response.send_message( + f"🗑️ **{removed_count}** permission(s) retirée(s).", + ephemeral=True + ) + await self._refresh_view(interaction) class TicketCommands(commands.Cog): @@ -1808,7 +2165,7 @@ class TicketCommands(commands.Cog): self.bot = bot self.storage = get_storage() - def cog_unload(self): + async def cog_unload(self): pass async def cog_load(self): @@ -1892,23 +2249,16 @@ class TicketCommands(commands.Cog): """Ouvre le panel de configuration complet du système de tickets""" await ctx.defer(ephemeral=True) + # Reload config config = self.storage.load_config(ctx.guild.id) - - # Vérifier si l'utilisateur peut accéder à la configuration - can_access = ctx.author.guild_permissions.administrator - if not can_access: - # Vérifier les rôles staff - for role_id in config.staff_roles: - role = ctx.guild.get_role(int(role_id)) - if role and role in ctx.author.roles: - can_access = True - break - - if not can_access: + # Vérifier permissions (admin ou config roles/users) + if not can_access_config(ctx, config): await ctx.send("❌ Cette commande est réservée aux administrateurs ou aux rôles configurés.", ephemeral=True) return + + embed = discord.Embed( title="⚙️ Configuration du Système de Tickets", description="Configurez complètement votre système de tickets depuis cette interface.\n\n" diff --git a/commandes/ticket/data/models.py b/commandes/ticket/data/models.py index 4bebb22..2446df1 100644 --- a/commandes/ticket/data/models.py +++ b/commandes/ticket/data/models.py @@ -293,7 +293,9 @@ class GuildTicketConfig: log_channel_id: Optional[int] = None archive_channel_id: Optional[int] = None staff_roles: List[str] = field(default_factory=list) + staff_users: List[int] = field(default_factory=list) config_roles: List[str] = field(default_factory=list) + config_users: List[int] = field(default_factory=list) max_tickets_per_user: int = 5 auto_close_enabled: bool = False auto_close_days: int = 7 @@ -312,8 +314,12 @@ class GuildTicketConfig: "default_category": self.default_category, "log_channel_id": self.log_channel_id, "archive_channel_id": self.archive_channel_id, + "log_channel_id": self.log_channel_id, + "archive_channel_id": self.archive_channel_id, "staff_roles": self.staff_roles, + "staff_users": self.staff_users, "config_roles": self.config_roles, + "config_users": self.config_users, "max_tickets_per_user": self.max_tickets_per_user, "auto_close_enabled": self.auto_close_enabled, "auto_close_days": self.auto_close_days, @@ -335,8 +341,11 @@ class GuildTicketConfig: default_category=data.get("default_category"), log_channel_id=data.get("log_channel_id"), archive_channel_id=data.get("archive_channel_id"), + staff_roles=data.get("staff_roles", []), + staff_users=data.get("staff_users", []), config_roles=data.get("config_roles", []), + config_users=data.get("config_users", []), max_tickets_per_user=data.get("max_tickets_per_user", 5), auto_close_enabled=data.get("auto_close_enabled", False), auto_close_days=data.get("auto_close_days", 7), diff --git a/commandes/ticket/staff_analytics.py b/commandes/ticket/staff_analytics.py index 0c53496..90c4745 100644 --- a/commandes/ticket/staff_analytics.py +++ b/commandes/ticket/staff_analytics.py @@ -25,6 +25,8 @@ class StaffAnalytics: async def get_staff_list(self, guild_id: int) -> List[Dict[str, Any]]: """Get list of staff members with their stats""" config = self.storage.load_config(guild_id) + # Invalidate cache to ensure we get the latest ticket data with new ratings + self.storage.invalidate_cache(guild_id) tickets = self.storage.get_guild_tickets(guild_id) staff_stats = defaultdict(lambda: { diff --git a/commandes/ticket/utils/permissions.py b/commandes/ticket/utils/permissions.py new file mode 100644 index 0000000..1a9a73d --- /dev/null +++ b/commandes/ticket/utils/permissions.py @@ -0,0 +1,92 @@ +""" +Ticket Permissions Utility +========================== +Centralized permission logic for the ticket system. +""" +import discord +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from ..data.models import GuildTicketConfig + +def is_staff(interaction: discord.Interaction, config: 'GuildTicketConfig') -> bool: + """ + Check if the user has staff permissions. + + Checks: + 1. Administrator permission + 2. Staff roles + 3. Staff users (specific user IDs) + """ + # Get user and permissions based on object type + user = getattr(interaction, "author", getattr(interaction, "user", None)) + if not user: + return False + + if user.guild_permissions.administrator: + return True + + + # Check specific users + if user.id in config.staff_users: + return True + + # Check roles + for role_id in config.staff_roles: + role = interaction.guild.get_role(int(role_id)) + if role and role in user.roles: + return True + + return False + +def can_access_config(interaction: discord.Interaction, config: 'GuildTicketConfig') -> bool: + """ + Check if the user can access the configuration. + + Checks: + 1. Administrator permission + 2. Config roles + 3. Config users + 4. Staff permissions (staff can often access basic config or view panels) + Note: This function is strictly for CONFIG access. + If staff should access config, add them to config roles or check is_staff too. + In this implementation, we check specific config permissions. + """ + # Get user and permissions based on object type + user = getattr(interaction, "author", getattr(interaction, "user", None)) + if not user: + return False + + if user.guild_permissions.administrator: + return True + + # Check specific users + if user.id in config.config_users: + return True + + # Check config roles + for role_id in config.config_roles: + role = interaction.guild.get_role(int(role_id)) + if role and role in user.roles: + return True + + # Also check legacy/migrated staff roles if they imply config access? + # For now, we keep it strict: config access requires explicit config permission. + + return False + +def is_whitelisted(interaction: discord.Interaction, config: 'GuildTicketConfig', bot) -> bool: + """ + Check if user is whitelisted or has staff/config permissions. + """ + # Staff and config users are always whitelisted + if is_staff(interaction, config) or can_access_config(interaction, config): + return True + + # Check whitelist cog + whitelist_cog = bot.get_cog("WhitelistMonitor") + if not whitelist_cog: + # If whitelist system not available, deny unless staff/config + return False + + return whitelist_cog.is_whitelisted(interaction.guild.id, interaction.user.id) diff --git a/data/logs/member_events_2026-01-10.json b/data/logs/member_events_2026-01-10.json new file mode 100644 index 0000000..917bc0f --- /dev/null +++ b/data/logs/member_events_2026-01-10.json @@ -0,0 +1,10 @@ +[ + { + "timestamp": "2026-01-10T16:19:17.701722", + "event_type": "member_join", + "member_id": 1459567012031234198, + "member_name": "fequjifenq_85720", + "member_discriminator": "0", + "member_avatar": null + } +] \ No newline at end of file diff --git a/data/tickets/backups/20260110_160305_1459563155859570692.json b/data/tickets/backups/20260110_160305_1459563155859570692.json new file mode 100644 index 0000000..1341251 --- /dev/null +++ b/data/tickets/backups/20260110_160305_1459563155859570692.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1459563155859570692, + "guild_id": 1369669999345537145, + "user_id": 971446412690722826, + "category_id": "3b0b9bc1", + "status": "open", + "priority": "normal", + "created_at": "2026-01-10T16:02:53.964008", + "closed_at": null, + "claimed_by": null, + "messages": [], + "reason": "egrrdhgdrh", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/backups/20260114_204715_1461084203054665812.json b/data/tickets/backups/20260114_204715_1461084203054665812.json new file mode 100644 index 0000000..312571d --- /dev/null +++ b/data/tickets/backups/20260114_204715_1461084203054665812.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461084203054665812, + "guild_id": 1369669999345537145, + "user_id": 971446412690722826, + "category_id": "91522e2c", + "status": "open", + "priority": "normal", + "created_at": "2026-01-14T20:46:59.791051", + "closed_at": null, + "claimed_by": null, + "messages": [], + "reason": "gregqssgg", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/backups/20260114_204726_1461084293995565242.json b/data/tickets/backups/20260114_204726_1461084293995565242.json new file mode 100644 index 0000000..4ff93b2 --- /dev/null +++ b/data/tickets/backups/20260114_204726_1461084293995565242.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461084293995565242, + "guild_id": 1369669999345537145, + "user_id": 971446412690722826, + "category_id": "3b0b9bc1", + "status": "open", + "priority": "normal", + "created_at": "2026-01-14T20:47:21.788375", + "closed_at": null, + "claimed_by": null, + "messages": [], + "reason": "fesefsfe", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/backups/20260114_204745_1461084293995565242.json b/data/tickets/backups/20260114_204745_1461084293995565242.json new file mode 100644 index 0000000..5d8c25c --- /dev/null +++ b/data/tickets/backups/20260114_204745_1461084293995565242.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461084293995565242, + "guild_id": 1369669999345537145, + "user_id": 971446412690722826, + "category_id": "3b0b9bc1", + "status": "claimed", + "priority": "normal", + "created_at": "2026-01-14T20:47:21.788375", + "closed_at": null, + "claimed_by": 971446412690722826, + "messages": [], + "reason": "fesefsfe", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/backups/20260114_204825_1461084511738532026.json b/data/tickets/backups/20260114_204825_1461084511738532026.json new file mode 100644 index 0000000..c585d59 --- /dev/null +++ b/data/tickets/backups/20260114_204825_1461084511738532026.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461084511738532026, + "guild_id": 1369669999345537145, + "user_id": 1459567012031234198, + "category_id": "3b0b9bc1", + "status": "open", + "priority": "normal", + "created_at": "2026-01-14T20:48:13.454435", + "closed_at": null, + "claimed_by": null, + "messages": [], + "reason": "dzqdqzgs", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/backups/20260114_204944_1461084844879778008.json b/data/tickets/backups/20260114_204944_1461084844879778008.json new file mode 100644 index 0000000..a99d837 --- /dev/null +++ b/data/tickets/backups/20260114_204944_1461084844879778008.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461084844879778008, + "guild_id": 1369669999345537145, + "user_id": 971446412690722826, + "category_id": "3b0b9bc1", + "status": "open", + "priority": "normal", + "created_at": "2026-01-14T20:49:32.964046", + "closed_at": null, + "claimed_by": null, + "messages": [], + "reason": "szfeq", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/backups/20260114_205938_config.json b/data/tickets/backups/20260114_205938_config.json new file mode 100644 index 0000000..c1dd27d --- /dev/null +++ b/data/tickets/backups/20260114_205938_config.json @@ -0,0 +1,56 @@ +{ + "1369669999345537145": { + "guild_id": 1369669999345537145, + "enabled": true, + "categories": [ + { + "id": "3b0b9bc1", + "name": "Mon cul", + "description": "Mon Anus", + "emoji": "🎫", + "color": "#3498db", + "staff_roles": [], + "welcome_message": "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu.", + "require_reason": false, + "max_tickets_per_user": 5, + "auto_close_days": 7, + "priority_enabled": true, + "allow_claims": true, + "survey_enabled": true + }, + { + "id": "91522e2c", + "name": "claudo", + "description": "un clochard", + "emoji": "📋", + "color": "#3498db", + "staff_roles": [], + "welcome_message": "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu.", + "require_reason": false, + "max_tickets_per_user": 5, + "auto_close_days": 7, + "priority_enabled": true, + "allow_claims": true, + "survey_enabled": true + } + ], + "default_category": null, + "log_channel_id": 1369669999677145103, + "archive_channel_id": null, + "staff_roles": [ + "1369769836028231730" + ], + "config_roles": [ + "1369769836028231730" + ], + "max_tickets_per_user": 5, + "auto_close_enabled": false, + "auto_close_days": 7, + "transcript_enabled": true, + "claim_enabled": true, + "survey_enabled": true, + "panel_channel_id": null, + "welcome_enabled": true, + "priority_enabled": true + } +} \ No newline at end of file diff --git a/data/tickets/backups/20260114_205954_config.json b/data/tickets/backups/20260114_205954_config.json new file mode 100644 index 0000000..df5d4d2 --- /dev/null +++ b/data/tickets/backups/20260114_205954_config.json @@ -0,0 +1,56 @@ +{ + "1369669999345537145": { + "guild_id": 1369669999345537145, + "enabled": true, + "categories": [ + { + "id": "3b0b9bc1", + "name": "Mon cul", + "description": "Mon Anus", + "emoji": "🎫", + "color": "#3498db", + "staff_roles": [], + "welcome_message": "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu.", + "require_reason": false, + "max_tickets_per_user": 5, + "auto_close_days": 7, + "priority_enabled": true, + "allow_claims": true, + "survey_enabled": true + }, + { + "id": "91522e2c", + "name": "claudo", + "description": "un clochard", + "emoji": "📋", + "color": "#3498db", + "staff_roles": [], + "welcome_message": "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu.", + "require_reason": false, + "max_tickets_per_user": 5, + "auto_close_days": 7, + "priority_enabled": true, + "allow_claims": true, + "survey_enabled": true + } + ], + "default_category": null, + "log_channel_id": 1369669999677145103, + "archive_channel_id": null, + "staff_roles": [ + "1369769836028231730" + ], + "staff_users": [], + "config_roles": [], + "config_users": [], + "max_tickets_per_user": 5, + "auto_close_enabled": false, + "auto_close_days": 7, + "transcript_enabled": true, + "claim_enabled": true, + "survey_enabled": true, + "panel_channel_id": null, + "welcome_enabled": true, + "priority_enabled": true + } +} \ No newline at end of file diff --git a/data/tickets/backups/20260114_210116_config.json b/data/tickets/backups/20260114_210116_config.json new file mode 100644 index 0000000..8297443 --- /dev/null +++ b/data/tickets/backups/20260114_210116_config.json @@ -0,0 +1,54 @@ +{ + "1369669999345537145": { + "guild_id": 1369669999345537145, + "enabled": true, + "categories": [ + { + "id": "3b0b9bc1", + "name": "Mon cul", + "description": "Mon Anus", + "emoji": "🎫", + "color": "#3498db", + "staff_roles": [], + "welcome_message": "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu.", + "require_reason": false, + "max_tickets_per_user": 5, + "auto_close_days": 7, + "priority_enabled": true, + "allow_claims": true, + "survey_enabled": true + }, + { + "id": "91522e2c", + "name": "claudo", + "description": "un clochard", + "emoji": "📋", + "color": "#3498db", + "staff_roles": [], + "welcome_message": "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu.", + "require_reason": false, + "max_tickets_per_user": 5, + "auto_close_days": 7, + "priority_enabled": true, + "allow_claims": true, + "survey_enabled": true + } + ], + "default_category": null, + "log_channel_id": 1369669999677145103, + "archive_channel_id": null, + "staff_roles": [], + "staff_users": [], + "config_roles": [], + "config_users": [], + "max_tickets_per_user": 5, + "auto_close_enabled": false, + "auto_close_days": 7, + "transcript_enabled": true, + "claim_enabled": true, + "survey_enabled": true, + "panel_channel_id": null, + "welcome_enabled": true, + "priority_enabled": true + } +} \ No newline at end of file diff --git a/data/tickets/backups/20260114_210450_1461088603273822394.json b/data/tickets/backups/20260114_210450_1461088603273822394.json new file mode 100644 index 0000000..bb327e7 --- /dev/null +++ b/data/tickets/backups/20260114_210450_1461088603273822394.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461088603273822394, + "guild_id": 1369669999345537145, + "user_id": 971446412690722826, + "category_id": "91522e2c", + "status": "open", + "priority": "normal", + "created_at": "2026-01-14T21:04:28.886631", + "closed_at": null, + "claimed_by": null, + "messages": [], + "reason": "dzqd", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/backups/20260114_210629_1461083997655400669.json b/data/tickets/backups/20260114_210629_1461083997655400669.json new file mode 100644 index 0000000..c13a626 --- /dev/null +++ b/data/tickets/backups/20260114_210629_1461083997655400669.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461083997655400669, + "guild_id": 1369669999345537145, + "user_id": 971446412690722826, + "category_id": "3b0b9bc1", + "status": "open", + "priority": "normal", + "created_at": "2026-01-14T20:46:10.826712", + "closed_at": null, + "claimed_by": null, + "messages": [], + "reason": "dzqzddqz", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/backups/20260114_210805_config.json b/data/tickets/backups/20260114_210805_config.json new file mode 100644 index 0000000..4411b25 --- /dev/null +++ b/data/tickets/backups/20260114_210805_config.json @@ -0,0 +1,56 @@ +{ + "1369669999345537145": { + "guild_id": 1369669999345537145, + "enabled": true, + "categories": [ + { + "id": "3b0b9bc1", + "name": "Mon cul", + "description": "Mon Anus", + "emoji": "🎫", + "color": "#3498db", + "staff_roles": [], + "welcome_message": "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu.", + "require_reason": false, + "max_tickets_per_user": 5, + "auto_close_days": 7, + "priority_enabled": true, + "allow_claims": true, + "survey_enabled": true + }, + { + "id": "91522e2c", + "name": "claudo", + "description": "un clochard", + "emoji": "📋", + "color": "#3498db", + "staff_roles": [], + "welcome_message": "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu.", + "require_reason": false, + "max_tickets_per_user": 5, + "auto_close_days": 7, + "priority_enabled": true, + "allow_claims": true, + "survey_enabled": true + } + ], + "default_category": null, + "log_channel_id": 1369669999677145103, + "archive_channel_id": null, + "staff_roles": [], + "staff_users": [], + "config_roles": [ + "1369769836028231730" + ], + "config_users": [], + "max_tickets_per_user": 5, + "auto_close_enabled": false, + "auto_close_days": 7, + "transcript_enabled": true, + "claim_enabled": true, + "survey_enabled": true, + "panel_channel_id": null, + "welcome_enabled": true, + "priority_enabled": true + } +} \ No newline at end of file diff --git a/data/tickets/backups/20260114_211234_1461089849527505151.json b/data/tickets/backups/20260114_211234_1461089849527505151.json new file mode 100644 index 0000000..9fa2e5b --- /dev/null +++ b/data/tickets/backups/20260114_211234_1461089849527505151.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461089849527505151, + "guild_id": 1369669999345537145, + "user_id": 1459567012031234198, + "category_id": "91522e2c", + "status": "open", + "priority": "normal", + "created_at": "2026-01-14T21:09:26.048196", + "closed_at": null, + "claimed_by": null, + "messages": [], + "reason": "dzqdzdq", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/backups/20260114_211528_1461089849527505151.json b/data/tickets/backups/20260114_211528_1461089849527505151.json new file mode 100644 index 0000000..7f62fe1 --- /dev/null +++ b/data/tickets/backups/20260114_211528_1461089849527505151.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461089849527505151, + "guild_id": 1369669999345537145, + "user_id": 1459567012031234198, + "category_id": "91522e2c", + "status": "closed", + "priority": "normal", + "created_at": "2026-01-14T21:09:26.048196", + "closed_at": "2026-01-14T21:12:34.847114", + "claimed_by": null, + "messages": [], + "reason": "dzqdzdq", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/backups/20260114_211531_1461089849527505151.json b/data/tickets/backups/20260114_211531_1461089849527505151.json new file mode 100644 index 0000000..3245e92 --- /dev/null +++ b/data/tickets/backups/20260114_211531_1461089849527505151.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461089849527505151, + "guild_id": 1369669999345537145, + "user_id": 1459567012031234198, + "category_id": "91522e2c", + "status": "closed", + "priority": "normal", + "created_at": "2026-01-14T21:09:26.048196", + "closed_at": "2026-01-14T21:15:28.028358", + "claimed_by": null, + "messages": [], + "reason": "dzqdzdq", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/backups/20260114_211535_1461089849527505151.json b/data/tickets/backups/20260114_211535_1461089849527505151.json new file mode 100644 index 0000000..d59b164 --- /dev/null +++ b/data/tickets/backups/20260114_211535_1461089849527505151.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461089849527505151, + "guild_id": 1369669999345537145, + "user_id": 1459567012031234198, + "category_id": "91522e2c", + "status": "closed", + "priority": "normal", + "created_at": "2026-01-14T21:09:26.048196", + "closed_at": "2026-01-14T21:15:31.970135", + "claimed_by": null, + "messages": [], + "reason": "dzqdzdq", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/backups/20260114_211539_1461089849527505151.json b/data/tickets/backups/20260114_211539_1461089849527505151.json new file mode 100644 index 0000000..739b1b3 --- /dev/null +++ b/data/tickets/backups/20260114_211539_1461089849527505151.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461089849527505151, + "guild_id": 1369669999345537145, + "user_id": 1459567012031234198, + "category_id": "91522e2c", + "status": "closed", + "priority": "normal", + "created_at": "2026-01-14T21:09:26.048196", + "closed_at": "2026-01-14T21:15:35.563739", + "claimed_by": null, + "messages": [], + "reason": "dzqdzdq", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/backups/20260114_211543_1461089849527505151.json b/data/tickets/backups/20260114_211543_1461089849527505151.json new file mode 100644 index 0000000..055fe29 --- /dev/null +++ b/data/tickets/backups/20260114_211543_1461089849527505151.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461089849527505151, + "guild_id": 1369669999345537145, + "user_id": 1459567012031234198, + "category_id": "91522e2c", + "status": "closed", + "priority": "normal", + "created_at": "2026-01-14T21:09:26.048196", + "closed_at": "2026-01-14T21:15:39.314476", + "claimed_by": null, + "messages": [], + "reason": "dzqdzdq", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/tickets/1459561776428351568.json b/data/tickets/tickets/1459561776428351568.json new file mode 100644 index 0000000..fa050fb --- /dev/null +++ b/data/tickets/tickets/1459561776428351568.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1459561776428351568, + "guild_id": 1369669999345537145, + "user_id": 971446412690722826, + "category_id": "3b0b9bc1", + "status": "open", + "priority": "normal", + "created_at": "2026-01-10T15:57:24.973539", + "closed_at": null, + "claimed_by": null, + "messages": [], + "reason": "yjutdgbfrd", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/tickets/1459563155859570692.json b/data/tickets/tickets/1459563155859570692.json new file mode 100644 index 0000000..4a2b640 --- /dev/null +++ b/data/tickets/tickets/1459563155859570692.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1459563155859570692, + "guild_id": 1369669999345537145, + "user_id": 971446412690722826, + "category_id": "3b0b9bc1", + "status": "closed", + "priority": "normal", + "created_at": "2026-01-10T16:02:53.964008", + "closed_at": "2026-01-10T16:03:05.092145", + "claimed_by": null, + "messages": [], + "reason": "egrrdhgdrh", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/tickets/1461083997655400669.json b/data/tickets/tickets/1461083997655400669.json new file mode 100644 index 0000000..4ac97f5 --- /dev/null +++ b/data/tickets/tickets/1461083997655400669.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461083997655400669, + "guild_id": 1369669999345537145, + "user_id": 971446412690722826, + "category_id": "3b0b9bc1", + "status": "closed", + "priority": "normal", + "created_at": "2026-01-14T20:46:10.826712", + "closed_at": "2026-01-14T21:06:29.364458", + "claimed_by": null, + "messages": [], + "reason": "dzqzddqz", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/tickets/1461084203054665812.json b/data/tickets/tickets/1461084203054665812.json new file mode 100644 index 0000000..dfc6428 --- /dev/null +++ b/data/tickets/tickets/1461084203054665812.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461084203054665812, + "guild_id": 1369669999345537145, + "user_id": 971446412690722826, + "category_id": "91522e2c", + "status": "closed", + "priority": "normal", + "created_at": "2026-01-14T20:46:59.791051", + "closed_at": "2026-01-14T20:47:15.286524", + "claimed_by": null, + "messages": [], + "reason": "gregqssgg", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/tickets/1461084293995565242.json b/data/tickets/tickets/1461084293995565242.json new file mode 100644 index 0000000..610f182 --- /dev/null +++ b/data/tickets/tickets/1461084293995565242.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461084293995565242, + "guild_id": 1369669999345537145, + "user_id": 971446412690722826, + "category_id": "3b0b9bc1", + "status": "closed", + "priority": "normal", + "created_at": "2026-01-14T20:47:21.788375", + "closed_at": "2026-01-14T20:47:45.988053", + "claimed_by": 971446412690722826, + "messages": [], + "reason": "fesefsfe", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/tickets/1461084511738532026.json b/data/tickets/tickets/1461084511738532026.json new file mode 100644 index 0000000..10fe85d --- /dev/null +++ b/data/tickets/tickets/1461084511738532026.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461084511738532026, + "guild_id": 1369669999345537145, + "user_id": 1459567012031234198, + "category_id": "3b0b9bc1", + "status": "closed", + "priority": "normal", + "created_at": "2026-01-14T20:48:13.454435", + "closed_at": "2026-01-14T20:48:25.136290", + "claimed_by": null, + "messages": [], + "reason": "dzqdqzgs", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/tickets/1461084844879778008.json b/data/tickets/tickets/1461084844879778008.json new file mode 100644 index 0000000..e24f810 --- /dev/null +++ b/data/tickets/tickets/1461084844879778008.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461084844879778008, + "guild_id": 1369669999345537145, + "user_id": 971446412690722826, + "category_id": "3b0b9bc1", + "status": "closed", + "priority": "normal", + "created_at": "2026-01-14T20:49:32.964046", + "closed_at": "2026-01-14T20:49:44.315370", + "claimed_by": null, + "messages": [], + "reason": "szfeq", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/tickets/1461088603273822394.json b/data/tickets/tickets/1461088603273822394.json new file mode 100644 index 0000000..631b449 --- /dev/null +++ b/data/tickets/tickets/1461088603273822394.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461088603273822394, + "guild_id": 1369669999345537145, + "user_id": 971446412690722826, + "category_id": "91522e2c", + "status": "closed", + "priority": "normal", + "created_at": "2026-01-14T21:04:28.886631", + "closed_at": "2026-01-14T21:04:50.756760", + "claimed_by": null, + "messages": [], + "reason": "dzqd", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/tickets/1461089849527505151.json b/data/tickets/tickets/1461089849527505151.json new file mode 100644 index 0000000..e6e65d9 --- /dev/null +++ b/data/tickets/tickets/1461089849527505151.json @@ -0,0 +1,16 @@ +{ + "channel_id": 1461089849527505151, + "guild_id": 1369669999345537145, + "user_id": 1459567012031234198, + "category_id": "91522e2c", + "status": "closed", + "priority": "normal", + "created_at": "2026-01-14T21:09:26.048196", + "closed_at": "2026-01-14T21:15:43.690109", + "claimed_by": null, + "messages": [], + "reason": "dzqdzdq", + "transcript_path": null, + "rating": null, + "feedback": null +} \ No newline at end of file diff --git a/data/tickets/transcripts/ticket_1459563155859570692_20260110_160305.html b/data/tickets/transcripts/ticket_1459563155859570692_20260110_160305.html new file mode 100644 index 0000000..5bf1fad --- /dev/null +++ b/data/tickets/transcripts/ticket_1459563155859570692_20260110_160305.html @@ -0,0 +1,210 @@ + + + + + + Transcript - Ticket #1459563155859570692 + + + +
+
+

🎫 Transcript de Ticket

+
+
+ 📌 ID: + 1459563155859570692 +
+
+ 📁 Catégorie: + 3b0b9bc1 +
+
+ ⚡ Priorité: + NORMAL +
+
+ 📊 Statut: + open +
+
+ 📅 Créé: + 10/01/2026 16:02 +
+
+
+ +
+

💬 Messages (0)

+ +
+ + +
+

📊 Statistiques

+
+
+ Durée + 0 min +
+
+ Messages + 0 +
+
+ Note + Pas de note +
+
+
+ + + +
+ + \ No newline at end of file diff --git a/data/tickets/transcripts/ticket_1461083997655400669_20260114_210629.html b/data/tickets/transcripts/ticket_1461083997655400669_20260114_210629.html new file mode 100644 index 0000000..1860b4f --- /dev/null +++ b/data/tickets/transcripts/ticket_1461083997655400669_20260114_210629.html @@ -0,0 +1,210 @@ + + + + + + Transcript - Ticket #1461083997655400669 + + + +
+
+

🎫 Transcript de Ticket

+
+
+ 📌 ID: + 1461083997655400669 +
+
+ 📁 Catégorie: + 3b0b9bc1 +
+
+ ⚡ Priorité: + NORMAL +
+
+ 📊 Statut: + open +
+
+ 📅 Créé: + 14/01/2026 20:46 +
+
+
+ +
+

💬 Messages (0)

+ +
+ + +
+

📊 Statistiques

+
+
+ Durée + 20 min +
+
+ Messages + 0 +
+
+ Note + Pas de note +
+
+
+ + + +
+ + \ No newline at end of file diff --git a/data/tickets/transcripts/ticket_1461084203054665812_20260114_204715.html b/data/tickets/transcripts/ticket_1461084203054665812_20260114_204715.html new file mode 100644 index 0000000..81f0f29 --- /dev/null +++ b/data/tickets/transcripts/ticket_1461084203054665812_20260114_204715.html @@ -0,0 +1,210 @@ + + + + + + Transcript - Ticket #1461084203054665812 + + + +
+
+

🎫 Transcript de Ticket

+
+
+ 📌 ID: + 1461084203054665812 +
+
+ 📁 Catégorie: + 91522e2c +
+
+ ⚡ Priorité: + NORMAL +
+
+ 📊 Statut: + open +
+
+ 📅 Créé: + 14/01/2026 20:46 +
+
+
+ +
+

💬 Messages (0)

+ +
+ + +
+

📊 Statistiques

+
+
+ Durée + 0 min +
+
+ Messages + 0 +
+
+ Note + Pas de note +
+
+
+ + + +
+ + \ No newline at end of file diff --git a/data/tickets/transcripts/ticket_1461084293995565242_20260114_204745.html b/data/tickets/transcripts/ticket_1461084293995565242_20260114_204745.html new file mode 100644 index 0000000..7cd52e9 --- /dev/null +++ b/data/tickets/transcripts/ticket_1461084293995565242_20260114_204745.html @@ -0,0 +1,237 @@ + + + + + + Transcript - Ticket #1461084293995565242 + + + +
+
+

🎫 Transcript de Ticket

+
+
+ 📌 ID: + 1461084293995565242 +
+
+ 📁 Catégorie: + 3b0b9bc1 +
+
+ ⚡ Priorité: + NORMAL +
+
+ 📊 Statut: + claimed +
+
+ 📅 Créé: + 14/01/2026 20:47 +
+
+
+ +
+

💬 Messages (3)

+ +
+
+ gameurpro12 + STAFF + 14/01/2026 19:47:33 +
+
rp;fepsŝfe
+
+ +
+
+ gameurpro12 + STAFF + 14/01/2026 19:47:34 +
+
fe,s,
+
+ +
+
+ gameurpro12 + STAFF + 14/01/2026 19:47:35 +
+
f,ekf,sem
+
+ +
+ + +
+

📊 Statistiques

+
+
+ Durée + 0 min +
+
+ Messages + 3 +
+
+ Note + Pas de note +
+
+
+ + + +
+ + \ No newline at end of file diff --git a/data/tickets/transcripts/ticket_1461084511738532026_20260114_204825.html b/data/tickets/transcripts/ticket_1461084511738532026_20260114_204825.html new file mode 100644 index 0000000..f28724e --- /dev/null +++ b/data/tickets/transcripts/ticket_1461084511738532026_20260114_204825.html @@ -0,0 +1,210 @@ + + + + + + Transcript - Ticket #1461084511738532026 + + + +
+
+

🎫 Transcript de Ticket

+
+
+ 📌 ID: + 1461084511738532026 +
+
+ 📁 Catégorie: + 3b0b9bc1 +
+
+ ⚡ Priorité: + NORMAL +
+
+ 📊 Statut: + open +
+
+ 📅 Créé: + 14/01/2026 20:48 +
+
+
+ +
+

💬 Messages (0)

+ +
+ + +
+

📊 Statistiques

+
+
+ Durée + 0 min +
+
+ Messages + 0 +
+
+ Note + Pas de note +
+
+
+ + + +
+ + \ No newline at end of file diff --git a/data/tickets/transcripts/ticket_1461084844879778008_20260114_204944.html b/data/tickets/transcripts/ticket_1461084844879778008_20260114_204944.html new file mode 100644 index 0000000..1b6e929 --- /dev/null +++ b/data/tickets/transcripts/ticket_1461084844879778008_20260114_204944.html @@ -0,0 +1,210 @@ + + + + + + Transcript - Ticket #1461084844879778008 + + + +
+
+

🎫 Transcript de Ticket

+
+
+ 📌 ID: + 1461084844879778008 +
+
+ 📁 Catégorie: + 3b0b9bc1 +
+
+ ⚡ Priorité: + NORMAL +
+
+ 📊 Statut: + open +
+
+ 📅 Créé: + 14/01/2026 20:49 +
+
+
+ +
+

💬 Messages (0)

+ +
+ + +
+

📊 Statistiques

+
+
+ Durée + 0 min +
+
+ Messages + 0 +
+
+ Note + Pas de note +
+
+
+ + + +
+ + \ No newline at end of file diff --git a/data/tickets/transcripts/ticket_1461088603273822394_20260114_210450.html b/data/tickets/transcripts/ticket_1461088603273822394_20260114_210450.html new file mode 100644 index 0000000..a7f5de7 --- /dev/null +++ b/data/tickets/transcripts/ticket_1461088603273822394_20260114_210450.html @@ -0,0 +1,273 @@ + + + + + + Transcript - Ticket #1461088603273822394 + + + +
+
+

🎫 Transcript de Ticket

+
+
+ 📌 ID: + 1461088603273822394 +
+
+ 📁 Catégorie: + 91522e2c +
+
+ ⚡ Priorité: + NORMAL +
+
+ 📊 Statut: + open +
+
+ 📅 Créé: + 14/01/2026 21:04 +
+
+
+ +
+

💬 Messages (7)

+ +
+
+ gameurpro12 + + 14/01/2026 20:04:33 +
+
oizdpqdzq,iodzq
+
+ +
+
+ gameurpro12 + + 14/01/2026 20:04:34 +
+
k,zdq
+
+ +
+
+ gameurpro12 + + 14/01/2026 20:04:34 +
+
dzq,dzq
+
+ +
+
+ gameurpro12 + + 14/01/2026 20:04:35 +
+
zdql,dzq
+
+ +
+
+ gameurpro12 + + 14/01/2026 20:04:35 +
+
dzql,
+
+ +
+
+ gameurpro12 + + 14/01/2026 20:04:35 +
+
dzq,lmdzqm
+
+ +
+
+ gameurpro12 + + 14/01/2026 20:04:36 +
+
zdq;pqzd
+
+ +
+ + +
+

📊 Statistiques

+
+
+ Durée + 0 min +
+
+ Messages + 7 +
+
+ Note + Pas de note +
+
+
+ + + +
+ + \ No newline at end of file diff --git a/data/tickets/transcripts/ticket_1461089849527505151_20260114_211234.html b/data/tickets/transcripts/ticket_1461089849527505151_20260114_211234.html new file mode 100644 index 0000000..9c442c2 --- /dev/null +++ b/data/tickets/transcripts/ticket_1461089849527505151_20260114_211234.html @@ -0,0 +1,219 @@ + + + + + + Transcript - Ticket #1461089849527505151 + + + +
+
+

🎫 Transcript de Ticket

+
+
+ 📌 ID: + 1461089849527505151 +
+
+ 📁 Catégorie: + 91522e2c +
+
+ ⚡ Priorité: + NORMAL +
+
+ 📊 Statut: + open +
+
+ 📅 Créé: + 14/01/2026 21:09 +
+
+
+ +
+

💬 Messages (1)

+ +
+
+ _.radi0dem0n._ + STAFF + 14/01/2026 20:11:46 +
+
aller XD
+
+ +
+ + +
+

📊 Statistiques

+
+
+ Durée + 3 min +
+
+ Messages + 1 +
+
+ Note + Pas de note +
+
+
+ + + +
+ + \ No newline at end of file diff --git a/data/tickets/transcripts/ticket_1461089849527505151_20260114_211528.html b/data/tickets/transcripts/ticket_1461089849527505151_20260114_211528.html new file mode 100644 index 0000000..7244d33 --- /dev/null +++ b/data/tickets/transcripts/ticket_1461089849527505151_20260114_211528.html @@ -0,0 +1,210 @@ + + + + + + Transcript - Ticket #1461089849527505151 + + + +
+
+

🎫 Transcript de Ticket

+
+
+ 📌 ID: + 1461089849527505151 +
+
+ 📁 Catégorie: + 91522e2c +
+
+ ⚡ Priorité: + NORMAL +
+
+ 📊 Statut: + closed +
+
+ 📅 Créé: + 14/01/2026 21:09 +
+
+
+ +
+

💬 Messages (0)

+ +
+ + +
+

📊 Statistiques

+
+
+ Durée + 3 min +
+
+ Messages + 0 +
+
+ Note + Pas de note +
+
+
+ + + +
+ + \ No newline at end of file diff --git a/data/tickets/transcripts/ticket_1461089849527505151_20260114_211531.html b/data/tickets/transcripts/ticket_1461089849527505151_20260114_211531.html new file mode 100644 index 0000000..c0db37e --- /dev/null +++ b/data/tickets/transcripts/ticket_1461089849527505151_20260114_211531.html @@ -0,0 +1,210 @@ + + + + + + Transcript - Ticket #1461089849527505151 + + + +
+
+

🎫 Transcript de Ticket

+
+
+ 📌 ID: + 1461089849527505151 +
+
+ 📁 Catégorie: + 91522e2c +
+
+ ⚡ Priorité: + NORMAL +
+
+ 📊 Statut: + closed +
+
+ 📅 Créé: + 14/01/2026 21:09 +
+
+
+ +
+

💬 Messages (0)

+ +
+ + +
+

📊 Statistiques

+
+
+ Durée + 6 min +
+
+ Messages + 0 +
+
+ Note + Pas de note +
+
+
+ + + +
+ + \ No newline at end of file diff --git a/data/tickets/transcripts/ticket_1461089849527505151_20260114_211535.html b/data/tickets/transcripts/ticket_1461089849527505151_20260114_211535.html new file mode 100644 index 0000000..c90dae5 --- /dev/null +++ b/data/tickets/transcripts/ticket_1461089849527505151_20260114_211535.html @@ -0,0 +1,210 @@ + + + + + + Transcript - Ticket #1461089849527505151 + + + +
+
+

🎫 Transcript de Ticket

+
+
+ 📌 ID: + 1461089849527505151 +
+
+ 📁 Catégorie: + 91522e2c +
+
+ ⚡ Priorité: + NORMAL +
+
+ 📊 Statut: + closed +
+
+ 📅 Créé: + 14/01/2026 21:09 +
+
+
+ +
+

💬 Messages (0)

+ +
+ + +
+

📊 Statistiques

+
+
+ Durée + 6 min +
+
+ Messages + 0 +
+
+ Note + Pas de note +
+
+
+ + + +
+ + \ No newline at end of file diff --git a/data/tickets/transcripts/ticket_1461089849527505151_20260114_211539.html b/data/tickets/transcripts/ticket_1461089849527505151_20260114_211539.html new file mode 100644 index 0000000..7a91257 --- /dev/null +++ b/data/tickets/transcripts/ticket_1461089849527505151_20260114_211539.html @@ -0,0 +1,210 @@ + + + + + + Transcript - Ticket #1461089849527505151 + + + +
+
+

🎫 Transcript de Ticket

+
+
+ 📌 ID: + 1461089849527505151 +
+
+ 📁 Catégorie: + 91522e2c +
+
+ ⚡ Priorité: + NORMAL +
+
+ 📊 Statut: + closed +
+
+ 📅 Créé: + 14/01/2026 21:09 +
+
+
+ +
+

💬 Messages (0)

+ +
+ + +
+

📊 Statistiques

+
+
+ Durée + 6 min +
+
+ Messages + 0 +
+
+ Note + Pas de note +
+
+
+ + + +
+ + \ No newline at end of file diff --git a/data/tickets/transcripts/ticket_1461089849527505151_20260114_211543.html b/data/tickets/transcripts/ticket_1461089849527505151_20260114_211543.html new file mode 100644 index 0000000..d6cc9dc --- /dev/null +++ b/data/tickets/transcripts/ticket_1461089849527505151_20260114_211543.html @@ -0,0 +1,210 @@ + + + + + + Transcript - Ticket #1461089849527505151 + + + +
+
+

🎫 Transcript de Ticket

+
+
+ 📌 ID: + 1461089849527505151 +
+
+ 📁 Catégorie: + 91522e2c +
+
+ ⚡ Priorité: + NORMAL +
+
+ 📊 Statut: + closed +
+
+ 📅 Créé: + 14/01/2026 21:09 +
+
+
+ +
+

💬 Messages (0)

+ +
+ + +
+

📊 Statistiques

+
+
+ Durée + 6 min +
+
+ Messages + 0 +
+
+ Note + Pas de note +
+
+
+ + + +
+ + \ No newline at end of file