Correction des multiples bugs
This commit is contained in:
parent
5fe70d754b
commit
07198c20b7
15 changed files with 707 additions and 348 deletions
|
|
@ -39,6 +39,17 @@ from .staff_analytics import StaffAnalytics, StaffStatsView
|
|||
from .utils.permissions import is_staff, is_whitelisted, can_access_config
|
||||
from src.logger import kuby_logger
|
||||
|
||||
# Add a task for sending feedback reminders
|
||||
class FeedbackReminderTask:
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.reminders = {} # Store active reminders
|
||||
|
||||
async def schedule_feedback_reminder(self, user_id: int, ticket_id: int, delay_hours: int = 24):
|
||||
"""Schedule a feedback reminder after a ticket is closed"""
|
||||
# This would be implemented with a proper task scheduler
|
||||
pass
|
||||
|
||||
|
||||
class TicketPanelView(discord.ui.View):
|
||||
"""Panel principal avec toutes les options intégrées"""
|
||||
|
|
@ -83,7 +94,16 @@ class TicketPanelView(discord.ui.View):
|
|||
)
|
||||
analytics_btn.callback = self._analytics_callback
|
||||
self.add_item(analytics_btn)
|
||||
|
||||
|
||||
# Staff Ratings (staff only)
|
||||
staff_ratings_btn = discord.ui.Button(
|
||||
style=discord.ButtonStyle.success,
|
||||
label="⭐ Notation du Staff",
|
||||
custom_id="ticket_staff_ratings"
|
||||
)
|
||||
staff_ratings_btn.callback = self._staff_ratings_callback
|
||||
self.add_item(staff_ratings_btn)
|
||||
|
||||
# Configuration (admin only)
|
||||
config_btn = discord.ui.Button(
|
||||
style=discord.ButtonStyle.secondary,
|
||||
|
|
@ -267,6 +287,85 @@ class TicketPanelView(discord.ui.View):
|
|||
|
||||
await interaction.response.send_message(embed=embed, view=config_view, ephemeral=True)
|
||||
|
||||
async def _staff_ratings_callback(self, interaction: discord.Interaction):
|
||||
"""Display staff ratings (staff only)"""
|
||||
# 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
|
||||
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
|
||||
analytics = StaffAnalytics(self.bot)
|
||||
|
||||
# Get staff list for individual stats
|
||||
staff_list = await analytics.get_staff_list(interaction.guild_id)
|
||||
|
||||
# Create main embed with overall stats
|
||||
embed = discord.Embed(
|
||||
title="⭐ Notation du Staff",
|
||||
description="Performance globale du staff",
|
||||
color=discord.Color.blue(),
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
|
||||
# Calculate overall stats
|
||||
tickets = self.storage.get_guild_tickets(interaction.guild_id)
|
||||
total_tickets = len(tickets)
|
||||
open_tickets = sum(1 for t in tickets if t.status in [TicketStatus.OPEN, TicketStatus.CLAIMED])
|
||||
closed_tickets = sum(1 for t in tickets if t.status == TicketStatus.CLOSED)
|
||||
claimed_tickets = sum(1 for t in tickets if t.claimed_by is not None)
|
||||
|
||||
# Basic stats
|
||||
embed.add_field(
|
||||
name="📈 Statistiques Générales",
|
||||
value=f"**Total:** {total_tickets}\n"
|
||||
f"**Ouverts:** {open_tickets}\n"
|
||||
f"**Fermés:** {closed_tickets}\n"
|
||||
f"**Réclamés:** {claimed_tickets}",
|
||||
inline=True
|
||||
)
|
||||
|
||||
# Staff performance summary
|
||||
if staff_list:
|
||||
total_ratings = sum(s['total_ratings'] for s in staff_list)
|
||||
avg_rating = sum(s['average_rating'] for s in staff_list if s['total_ratings'] > 0) / len([s for s in staff_list if s['total_ratings'] > 0]) if any(s['total_ratings'] > 0 for s in staff_list) else 0
|
||||
|
||||
embed.add_field(
|
||||
name="👥 Équipe Staff",
|
||||
value=f"**Membres actifs:** {len(staff_list)}\n"
|
||||
f"**Évaluations totales:** {total_ratings}\n"
|
||||
f"**Note moyenne globale:** {avg_rating:.1f}⭐" if avg_rating > 0 else "**Note moyenne globale:** N/A",
|
||||
inline=True
|
||||
)
|
||||
|
||||
# Send main embed
|
||||
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||
|
||||
# Create and send staff rating chart
|
||||
try:
|
||||
if staff_list:
|
||||
rating_chart = analytics.create_staff_rating_chart(staff_list)
|
||||
await interaction.followup.send(
|
||||
"📊 **Évaluations des Membres du Staff**",
|
||||
file=discord.File(rating_chart, 'staff_ratings.png'),
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
# Add individual staff stats view
|
||||
stats_view = StaffStatsView(self.bot, staff_list, analytics)
|
||||
stats_embed = discord.Embed(
|
||||
title="📈 Statistiques Individuelles",
|
||||
description="Cliquez sur un membre du staff pour voir ses statistiques détaillées:",
|
||||
color=discord.Color.purple()
|
||||
)
|
||||
await interaction.followup.send(embed=stats_embed, view=stats_view, ephemeral=True)
|
||||
|
||||
except Exception as e:
|
||||
await interaction.followup.send(f"❌ Erreur lors de la génération des graphiques: {e}", ephemeral=True)
|
||||
|
||||
|
||||
def _is_staff(self, interaction: discord.Interaction) -> bool:
|
||||
return is_staff(interaction, self.config)
|
||||
|
|
@ -449,11 +548,15 @@ class TicketPriorityModal(discord.ui.Modal):
|
|||
guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True)
|
||||
}
|
||||
|
||||
# Add staff roles
|
||||
for role_id in self.config.staff_roles:
|
||||
role = guild.get_role(int(role_id))
|
||||
if role and role not in overwrites:
|
||||
overwrites[role] = discord.PermissionOverwrite(read_messages=True, send_messages=True)
|
||||
# Add staff roles (category-specific roles override global ones)
|
||||
staff_roles_to_add = self.category.staff_roles if self.category.staff_roles else self.config.staff_roles
|
||||
for role_id in staff_roles_to_add:
|
||||
try:
|
||||
role = guild.get_role(int(role_id))
|
||||
if role and role not in overwrites:
|
||||
overwrites[role] = discord.PermissionOverwrite(read_messages=True, send_messages=True)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
# Create channel name using username and category without numbers
|
||||
safe_username = "".join(c for c in user.display_name if not c.isdigit()).strip()
|
||||
|
|
@ -582,11 +685,11 @@ class TicketManagementView(discord.ui.View):
|
|||
self.ticket = ticket
|
||||
self.category = config.get_category(ticket.category_id)
|
||||
|
||||
# Claim (if enabled and ticket is open)
|
||||
if config.claim_enabled and self.category and self.category.allow_claims and ticket.status == TicketStatus.OPEN:
|
||||
# Claim (if enabled and ticket is open or claimed)
|
||||
if config.claim_enabled and self.category and self.category.allow_claims and ticket.status in [TicketStatus.OPEN, TicketStatus.CLAIMED]:
|
||||
claim_btn = discord.ui.Button(
|
||||
style=discord.ButtonStyle.primary,
|
||||
label="Réclamer",
|
||||
label="Réclamer" if ticket.status == TicketStatus.OPEN else "Transférer",
|
||||
custom_id="ticket_claim"
|
||||
)
|
||||
claim_btn.callback = self._claim_callback
|
||||
|
|
@ -697,24 +800,33 @@ class TicketManagementView(discord.ui.View):
|
|||
await interaction.response.send_message("Réservé au staff.", ephemeral=True)
|
||||
return
|
||||
|
||||
if self.ticket.claimed_by:
|
||||
await interaction.response.send_message("Ce ticket est déjà réclamé.", ephemeral=True)
|
||||
if self.ticket.claimed_by == interaction.user.id:
|
||||
await interaction.response.send_message("Vous avez déjà réclamé ce ticket.", ephemeral=True)
|
||||
return
|
||||
|
||||
old_claimer = self.ticket.claimed_by
|
||||
self.ticket.claimed_by = interaction.user.id
|
||||
self.ticket.status = TicketStatus.CLAIMED
|
||||
self.storage.save_ticket(self.ticket)
|
||||
|
||||
# Update view (remove claim button)
|
||||
# Update view (change label)
|
||||
for item in self.children:
|
||||
if item.custom_id == "ticket_claim":
|
||||
self.remove_item(item)
|
||||
item.label = "Transférer"
|
||||
break
|
||||
|
||||
# Force UI update
|
||||
await interaction.response.edit_message(view=self)
|
||||
|
||||
embed = TicketEmbedFormatter.ticket_claimed(self.ticket, interaction.user)
|
||||
if old_claimer:
|
||||
embed = discord.Embed(
|
||||
title="🔄 Ticket Transféré",
|
||||
description=f"Le ticket a été transféré de <@{old_claimer}> à {interaction.user.mention}",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
else:
|
||||
embed = TicketEmbedFormatter.ticket_claimed(self.ticket, interaction.user)
|
||||
|
||||
await interaction.channel.send(embed=embed)
|
||||
|
||||
async def _reopen_callback(self, interaction: discord.Interaction):
|
||||
|
|
@ -762,20 +874,32 @@ class TicketManagementView(discord.ui.View):
|
|||
await interaction.response.send_message("Ticket rouvert!", ephemeral=True)
|
||||
|
||||
def _is_staff(self, interaction: discord.Interaction) -> bool:
|
||||
return is_staff(interaction, self.config)
|
||||
return is_staff(interaction, self.config, self.category)
|
||||
|
||||
def _is_staff_member(self, member) -> bool:
|
||||
# Create a fake interaction-like check or manual check
|
||||
"""Check if a specific member is staff, considering category roles"""
|
||||
if not member:
|
||||
return False
|
||||
|
||||
# Handle cases where member is a User (left the guild)
|
||||
if not isinstance(member, discord.Member):
|
||||
return member.id in self.config.staff_users
|
||||
|
||||
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
|
||||
# Check category-specific staff roles first
|
||||
staff_roles = self.category.staff_roles if self.category and self.category.staff_roles else self.config.staff_roles
|
||||
for role_id in staff_roles:
|
||||
try:
|
||||
role = member.guild.get_role(int(role_id))
|
||||
if role and role in member.roles:
|
||||
return True
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
continue
|
||||
return False
|
||||
|
||||
|
||||
|
|
@ -788,6 +912,7 @@ class CloseConfirmationView(discord.ui.View):
|
|||
self.storage = storage
|
||||
self.config = config
|
||||
self.ticket = ticket
|
||||
self.category = config.get_category(ticket.category_id)
|
||||
|
||||
@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):
|
||||
|
|
@ -825,13 +950,7 @@ class CloseConfirmationView(discord.ui.View):
|
|||
await interaction.delete_original_response()
|
||||
|
||||
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
|
||||
return is_staff(interaction, self.config, self.category)
|
||||
|
||||
|
||||
class CloseDelayView(discord.ui.View):
|
||||
|
|
@ -934,13 +1053,24 @@ class CloseDelayView(discord.ui.View):
|
|||
if self.close_reason:
|
||||
dm_embed.add_field(name="Raison de fermeture", value=self.close_reason, inline=False)
|
||||
|
||||
# Create a special view with a "Noter le staff" button
|
||||
view = FeedbackView(self.bot, self.storage, self.ticket)
|
||||
|
||||
# Add a button to directly open the feedback form
|
||||
note_staff_btn = discord.ui.Button(
|
||||
label="⭐ Noter le staff",
|
||||
style=discord.ButtonStyle.primary,
|
||||
custom_id="note_staff_direct"
|
||||
)
|
||||
note_staff_btn.callback = self._note_staff_callback
|
||||
view.add_item(note_staff_btn)
|
||||
|
||||
await user.send(embed=dm_embed, view=view)
|
||||
except Exception as e:
|
||||
print(f"Failed to send feedback DM: {e}")
|
||||
|
||||
# Send transcript to log channel
|
||||
if self.config.log_channel_id and transcript_file and os.path.exists(transcript_file):
|
||||
if self.config.log_channel_id:
|
||||
try:
|
||||
log_channel = interaction.guild.get_channel(self.config.log_channel_id)
|
||||
if log_channel:
|
||||
|
|
@ -961,8 +1091,12 @@ class CloseDelayView(discord.ui.View):
|
|||
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)
|
||||
if transcript_file and os.path.exists(transcript_file):
|
||||
file = discord.File(transcript_file, filename=os.path.basename(transcript_file))
|
||||
await log_channel.send(embed=log_embed, file=file)
|
||||
else:
|
||||
log_embed.add_field(name="⚠️ Transcript", value="Le transcript n'a pas pu être généré (l'utilisateur a peut-être quitté le serveur).", inline=False)
|
||||
await log_channel.send(embed=log_embed)
|
||||
except Exception as e:
|
||||
print(f"Error sending transcript to log channel: {e}")
|
||||
|
||||
|
|
@ -977,6 +1111,11 @@ class CloseDelayView(discord.ui.View):
|
|||
color=discord.Color.red()
|
||||
)
|
||||
await interaction.channel.send(embed=embed)
|
||||
|
||||
async def _note_staff_callback(self, interaction: discord.Interaction):
|
||||
"""Handle direct staff rating button"""
|
||||
# This would open a direct feedback form
|
||||
await interaction.response.send_message("Vous pouvez maintenant noter le staff directement !", ephemeral=True)
|
||||
|
||||
async def _cancel_callback(self, interaction: discord.Interaction):
|
||||
"""Annule la fermeture du ticket pendant le délai"""
|
||||
|
|
@ -992,20 +1131,32 @@ class CloseDelayView(discord.ui.View):
|
|||
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
|
||||
return is_staff(interaction, self.config, self.category)
|
||||
|
||||
def _is_staff_member(self, member) -> bool:
|
||||
guild = member.guild
|
||||
for role_id in self.config.staff_roles:
|
||||
role = guild.get_role(int(role_id))
|
||||
if role and role in member.roles:
|
||||
return True
|
||||
"""Check if a specific member is staff, considering category roles"""
|
||||
if not member:
|
||||
return False
|
||||
|
||||
# Handle cases where member is a User (left the guild)
|
||||
if not isinstance(member, discord.Member):
|
||||
return member.id in self.config.staff_users
|
||||
|
||||
if member.guild_permissions.administrator:
|
||||
return True
|
||||
|
||||
if member.id in self.config.staff_users:
|
||||
return True
|
||||
|
||||
# Check category-specific staff roles first
|
||||
staff_roles = self.category.staff_roles if self.category and self.category.staff_roles else self.config.staff_roles
|
||||
for role_id in staff_roles:
|
||||
try:
|
||||
role = member.guild.get_role(int(role_id))
|
||||
if role and role in member.roles:
|
||||
return True
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
continue
|
||||
return False
|
||||
|
||||
|
||||
|
|
@ -1125,7 +1276,7 @@ class CloseTicketModal(discord.ui.Modal):
|
|||
duration = self.ticket.duration_minutes
|
||||
|
||||
# Send transcript to log channel if configured
|
||||
if self.config.log_channel_id and transcript_file and os.path.exists(transcript_file):
|
||||
if self.config.log_channel_id:
|
||||
try:
|
||||
log_channel = interaction.guild.get_channel(self.config.log_channel_id)
|
||||
if log_channel:
|
||||
|
|
@ -1149,8 +1300,12 @@ class CloseTicketModal(discord.ui.Modal):
|
|||
log_embed.add_field(name="Raison de fermeture", value=self.reason.value, inline=False)
|
||||
|
||||
# Send transcript file
|
||||
file = discord.File(transcript_file, filename=os.path.basename(transcript_file))
|
||||
await log_channel.send(embed=log_embed, file=file)
|
||||
if transcript_file and os.path.exists(transcript_file):
|
||||
file = discord.File(transcript_file, filename=os.path.basename(transcript_file))
|
||||
await log_channel.send(embed=log_embed, file=file)
|
||||
else:
|
||||
log_embed.add_field(name="⚠️ Transcript", value="Le transcript n'a pas pu être généré (l'utilisateur a peut-être quitté le serveur).", inline=False)
|
||||
await log_channel.send(embed=log_embed)
|
||||
except Exception as e:
|
||||
print(f"Error sending transcript to log channel: {e}")
|
||||
|
||||
|
|
@ -1163,13 +1318,32 @@ class CloseTicketModal(discord.ui.Modal):
|
|||
await interaction.followup.send("Erreur lors de la suppression du canal, mais le ticket a été fermé.", ephemeral=True)
|
||||
|
||||
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
|
||||
return is_staff(interaction, self.config, self.category)
|
||||
|
||||
def _is_staff_member(self, member) -> bool:
|
||||
"""Check if a specific member is staff, considering category roles"""
|
||||
if not member:
|
||||
return False
|
||||
|
||||
# Handle cases where member is a User (left the guild)
|
||||
if not isinstance(member, discord.Member):
|
||||
return member.id in self.config.staff_users
|
||||
|
||||
if member.guild_permissions.administrator:
|
||||
return True
|
||||
|
||||
if member.id in self.config.staff_users:
|
||||
return True
|
||||
|
||||
staff_roles = self.category.staff_roles if self.category and self.category.staff_roles else self.config.staff_roles
|
||||
for role_id in staff_roles:
|
||||
try:
|
||||
role = member.guild.get_role(int(role_id))
|
||||
if role and role in member.roles:
|
||||
return True
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
continue
|
||||
return False
|
||||
|
||||
|
||||
class TicketSetupView(discord.ui.View):
|
||||
|
|
@ -2039,7 +2213,8 @@ class CategoryActionView(discord.ui.View):
|
|||
description=f"**ID:** `{self.category.id}`\n"
|
||||
f"**Emoji:** {self.category.emoji}\n"
|
||||
f"**Description:** {self.category.description or 'Aucune'}\n"
|
||||
f"**Couleur:** `{self.category.color}`",
|
||||
f"**Couleur:** `{self.category.color}`\n"
|
||||
f"**Staff Spécifique:** {', '.join([f'<@&{r}>' for r in self.category.staff_roles]) if self.category.staff_roles else 'Global (Tous les staffs)'}",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
return embed
|
||||
|
|
@ -2049,6 +2224,18 @@ class CategoryActionView(discord.ui.View):
|
|||
modal = EditCategoryModal(self.bot, self.storage, self.config, self.category, self)
|
||||
await interaction.response.send_modal(modal)
|
||||
|
||||
@discord.ui.button(label="🛡️ Permissions", style=discord.ButtonStyle.success)
|
||||
async def permissions_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
view = CategoryPermissionsView(self.bot, self.storage, self.config, self.category, self)
|
||||
embed = discord.Embed(
|
||||
title=f"Permissions: {self.category.name}",
|
||||
description="Choisissez les rôles qui auront accès à cette catégorie de tickets.\n\n"
|
||||
"⚠️ **Si vous sélectionnez des rôles ici, seuls ces rôles (et les admins) auront accès aux tickets de cette catégorie.** "
|
||||
"Les rôles staff globaux n'y auront plus accès.",
|
||||
color=discord.Color.green()
|
||||
)
|
||||
await interaction.response.edit_message(embed=embed, view=view)
|
||||
|
||||
@discord.ui.button(label="🗑️ Supprimer", style=discord.ButtonStyle.danger)
|
||||
async def delete_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
view = DeleteCategoryConfirmationView(self.bot, self.storage, self.config, self.category, self.parent_view)
|
||||
|
|
@ -2065,6 +2252,30 @@ class CategoryActionView(discord.ui.View):
|
|||
self.parent_view._add_category_select()
|
||||
await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view)
|
||||
|
||||
class CategoryPermissionsView(discord.ui.View):
|
||||
"""Vue pour configurer les rôles staff d'une catégorie"""
|
||||
|
||||
def __init__(self, bot, storage, config, category, parent_view):
|
||||
super().__init__(timeout=None)
|
||||
self.bot = bot
|
||||
self.storage = storage
|
||||
self.config = config
|
||||
self.category = category
|
||||
self.parent_view = parent_view
|
||||
|
||||
@discord.ui.select(cls=discord.ui.RoleSelect, placeholder="Sélectionner les rôles staff (Gestion)", min_values=0, max_values=10)
|
||||
async def role_select_callback(self, interaction: discord.Interaction, select: discord.ui.RoleSelect):
|
||||
role_ids = [str(role.id) for role in select.values]
|
||||
self.category.staff_roles = role_ids
|
||||
self.storage.save_config(interaction.guild_id, self.config)
|
||||
|
||||
# Refresh embed
|
||||
await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view)
|
||||
|
||||
@discord.ui.button(label="⬅️ Annuler", style=discord.ButtonStyle.secondary)
|
||||
async def back_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view)
|
||||
|
||||
|
||||
class DeleteCategoryConfirmationView(discord.ui.View):
|
||||
def __init__(self, bot, storage, config, category, parent_view):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue