Fix du système de permissions
This commit is contained in:
parent
dfec6b2418
commit
c330b9a1cc
48 changed files with 4229 additions and 243 deletions
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue