523 lines
18 KiB
Python
523 lines
18 KiB
Python
|
|
"""
|
|||
|
|
Ticket Admin Views
|
|||
|
|
==================
|
|||
|
|
Admin panel views for bulk management and configuration.
|
|||
|
|
"""
|
|||
|
|
from datetime import datetime
|
|||
|
|
from typing import Optional, List, Dict, Any
|
|||
|
|
|
|||
|
|
import discord
|
|||
|
|
from discord.ui import View, Button, Select, Modal, TextInput
|
|||
|
|
from discord import Interaction, ButtonStyle, Color
|
|||
|
|
|
|||
|
|
from ..data.models import (
|
|||
|
|
TicketData,
|
|||
|
|
TicketCategory,
|
|||
|
|
TicketStatus,
|
|||
|
|
TicketStats,
|
|||
|
|
GuildTicketConfig
|
|||
|
|
)
|
|||
|
|
from ..data.storage import get_storage
|
|||
|
|
from ..utils.formatters import TicketEmbedFormatter
|
|||
|
|
from ..utils.transcript import get_transcript_generator
|
|||
|
|
|
|||
|
|
|
|||
|
|
class AdminPanelView(View):
|
|||
|
|
"""
|
|||
|
|
Main admin panel for ticket management.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
def __init__(self, bot, config: GuildTicketConfig):
|
|||
|
|
super().__init__(timeout=None)
|
|||
|
|
self.bot = bot
|
|||
|
|
self.config = config
|
|||
|
|
|
|||
|
|
# Stats button
|
|||
|
|
stats_btn = Button(
|
|||
|
|
style=ButtonStyle.blurple,
|
|||
|
|
label="📊 Statistiques",
|
|||
|
|
custom_id="admin_stats"
|
|||
|
|
)
|
|||
|
|
stats_btn.callback = self._stats_callback
|
|||
|
|
self.add_item(stats_btn)
|
|||
|
|
|
|||
|
|
# Bulk close button
|
|||
|
|
bulk_close_btn = Button(
|
|||
|
|
style=ButtonStyle.red,
|
|||
|
|
label="🔴 Fermeture Massive",
|
|||
|
|
custom_id="admin_bulk_close"
|
|||
|
|
)
|
|||
|
|
bulk_close_btn.callback = self._bulk_close_callback
|
|||
|
|
self.add_item(bulk_close_btn)
|
|||
|
|
|
|||
|
|
# Config button
|
|||
|
|
config_btn = Button(
|
|||
|
|
style=ButtonStyle.secondary,
|
|||
|
|
label="⚙️ Configuration",
|
|||
|
|
custom_id="admin_config"
|
|||
|
|
)
|
|||
|
|
config_btn.callback = self._config_callback
|
|||
|
|
self.add_item(config_btn)
|
|||
|
|
|
|||
|
|
# Clean old tickets button
|
|||
|
|
cleanup_btn = Button(
|
|||
|
|
style=ButtonStyle.secondary,
|
|||
|
|
label="🧹 Nettoyer",
|
|||
|
|
custom_id="admin_cleanup"
|
|||
|
|
)
|
|||
|
|
cleanup_btn.callback = self._cleanup_callback
|
|||
|
|
self.add_item(cleanup_btn)
|
|||
|
|
|
|||
|
|
async def _stats_callback(self, interaction: Interaction):
|
|||
|
|
"""Show statistics"""
|
|||
|
|
await interaction.response.defer(ephemeral=True)
|
|||
|
|
|
|||
|
|
storage = get_storage()
|
|||
|
|
stats = self._calculate_stats(interaction.guild_id)
|
|||
|
|
|
|||
|
|
embed = TicketEmbedFormatter.stats_embed(stats, interaction.guild.name)
|
|||
|
|
|
|||
|
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
|||
|
|
|
|||
|
|
async def _bulk_close_callback(self, interaction: Interaction):
|
|||
|
|
"""Show bulk close options"""
|
|||
|
|
await interaction.response.defer(ephemeral=True)
|
|||
|
|
|
|||
|
|
# Get open tickets
|
|||
|
|
storage = get_storage()
|
|||
|
|
tickets = storage.get_guild_tickets(interaction.guild_id, TicketStatus.OPEN)
|
|||
|
|
|
|||
|
|
if not tickets:
|
|||
|
|
await interaction.followup.send("Aucun ticket ouvert à fermer.", ephemeral=True)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# Show selection view
|
|||
|
|
view = BulkCloseView(self.bot, self.config, tickets)
|
|||
|
|
embed = discord.Embed(
|
|||
|
|
title="🔴 Fermeture Massive",
|
|||
|
|
description=f"Nombre de tickets ouverts: **{len(tickets)}**\n\n"
|
|||
|
|
"Sélectionnez les tickets à fermer:",
|
|||
|
|
color=Color.red()
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
await interaction.followup.send(embed=embed, view=view, ephemeral=True)
|
|||
|
|
|
|||
|
|
async def _config_callback(self, interaction: Interaction):
|
|||
|
|
"""Show configuration options"""
|
|||
|
|
await interaction.response.defer(ephemeral=True)
|
|||
|
|
|
|||
|
|
view = ConfigPanelView(self.bot, self.config)
|
|||
|
|
|
|||
|
|
embed = discord.Embed(
|
|||
|
|
title="⚙️ Configuration des Tickets",
|
|||
|
|
color=Color.blue()
|
|||
|
|
)
|
|||
|
|
embed.add_field(name="Tickets activés", value="✅" if self.config.enabled else "❌", inline=True)
|
|||
|
|
embed.add_field(name="Catégories", value=str(len(self.config.categories)), inline=True)
|
|||
|
|
embed.add_field(name="Rôles Staff", value=str(len(self.config.staff_roles)), inline=True)
|
|||
|
|
embed.add_field(name="Canal Logs", value=f"<#{self.config.log_channel_id}>" if self.config.log_channel_id else "Non configuré", inline=True)
|
|||
|
|
embed.add_field(name="Limite par utilisateur", value=str(self.config.max_tickets_per_user), inline=True)
|
|||
|
|
embed.add_field(name="Auto-close", value="✅" if self.config.auto_close_enabled else "❌", inline=True)
|
|||
|
|
|
|||
|
|
await interaction.followup.send(embed=embed, view=view, ephemeral=True)
|
|||
|
|
|
|||
|
|
async def _cleanup_callback(self, interaction: Interaction):
|
|||
|
|
"""Clean up old closed tickets"""
|
|||
|
|
await interaction.response.defer(ephemeral=True)
|
|||
|
|
|
|||
|
|
storage = get_storage()
|
|||
|
|
removed = await self._cleanup_closed_tickets(interaction.guild_id)
|
|||
|
|
|
|||
|
|
await interaction.followup.send(
|
|||
|
|
f"✅ {removed} tickets fermés ont été supprimés.",
|
|||
|
|
ephemeral=True
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def _calculate_stats(self, guild_id: int) -> TicketStats:
|
|||
|
|
"""Calculate ticket statistics"""
|
|||
|
|
storage = get_storage()
|
|||
|
|
tickets = storage.get_guild_tickets(guild_id)
|
|||
|
|
|
|||
|
|
stats = TicketStats()
|
|||
|
|
stats.total_tickets = len(tickets)
|
|||
|
|
|
|||
|
|
total_duration = 0
|
|||
|
|
closed_count = 0
|
|||
|
|
rating_sum = 0
|
|||
|
|
rating_count = 0
|
|||
|
|
|
|||
|
|
for ticket in tickets:
|
|||
|
|
# By status
|
|||
|
|
if ticket.status == TicketStatus.OPEN:
|
|||
|
|
stats.open_tickets += 1
|
|||
|
|
elif ticket.status == TicketStatus.CLAIMED:
|
|||
|
|
stats.claimed_tickets += 1
|
|||
|
|
stats.open_tickets += 1
|
|||
|
|
elif ticket.status == TicketStatus.CLOSED:
|
|||
|
|
stats.closed_tickets += 1
|
|||
|
|
closed_count += 1
|
|||
|
|
|
|||
|
|
# By category
|
|||
|
|
stats.tickets_by_category[ticket.category_id] = \
|
|||
|
|
stats.tickets_by_category.get(ticket.category_id, 0) + 1
|
|||
|
|
|
|||
|
|
# By priority
|
|||
|
|
stats.tickets_by_priority[ticket.priority.value] = \
|
|||
|
|
stats.tickets_by_priority.get(ticket.priority.value, 0) + 1
|
|||
|
|
|
|||
|
|
# By day
|
|||
|
|
day = ticket.created_at.strftime("%Y-%m-%d")
|
|||
|
|
stats.tickets_by_day[day] = stats.tickets_by_day.get(day, 0) + 1
|
|||
|
|
|
|||
|
|
# Duration
|
|||
|
|
if ticket.duration_minutes:
|
|||
|
|
total_duration += ticket.duration_minutes
|
|||
|
|
|
|||
|
|
# Rating
|
|||
|
|
if ticket.rating:
|
|||
|
|
rating_sum += ticket.rating
|
|||
|
|
rating_count += 1
|
|||
|
|
|
|||
|
|
# Averages
|
|||
|
|
if closed_count > 0:
|
|||
|
|
stats.average_duration_minutes = total_duration / closed_count
|
|||
|
|
|
|||
|
|
if rating_count > 0:
|
|||
|
|
stats.average_rating = rating_sum / rating_count
|
|||
|
|
|
|||
|
|
return stats
|
|||
|
|
|
|||
|
|
async def _cleanup_closed_tickets(self, guild_id: int, days: int = 30) -> int:
|
|||
|
|
"""Remove closed tickets older than X days"""
|
|||
|
|
storage = get_storage()
|
|||
|
|
tickets = storage.get_guild_tickets(guild_id, TicketStatus.CLOSED)
|
|||
|
|
|
|||
|
|
cutoff = datetime.now().timestamp() - (days * 86400)
|
|||
|
|
removed = 0
|
|||
|
|
|
|||
|
|
for ticket in tickets:
|
|||
|
|
if ticket.closed_at and ticket.closed_at.timestamp() < cutoff:
|
|||
|
|
# Delete channel if it exists
|
|||
|
|
channel = self.bot.get_channel(ticket.channel_id)
|
|||
|
|
if channel:
|
|||
|
|
try:
|
|||
|
|
await channel.delete()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# Delete ticket data
|
|||
|
|
storage.delete_ticket(ticket.channel_id)
|
|||
|
|
removed += 1
|
|||
|
|
|
|||
|
|
return removed
|
|||
|
|
|
|||
|
|
|
|||
|
|
class BulkCloseView(View):
|
|||
|
|
"""View for selecting tickets to bulk close"""
|
|||
|
|
|
|||
|
|
def __init__(self, bot, config: GuildTicketConfig, tickets: List[TicketData]):
|
|||
|
|
super().__init__(timeout=None) # Persistent view
|
|||
|
|
self.bot = bot
|
|||
|
|
self.config = config
|
|||
|
|
self.tickets = tickets
|
|||
|
|
self.selected_tickets = set()
|
|||
|
|
|
|||
|
|
# Create select with tickets (max 25 per select)
|
|||
|
|
options = []
|
|||
|
|
for ticket in tickets[:25]:
|
|||
|
|
channel = self.bot.get_channel(ticket.channel_id)
|
|||
|
|
channel_name = channel.name if channel else f"ticket_{ticket.channel_id}"
|
|||
|
|
options.append(
|
|||
|
|
discord.SelectOption(
|
|||
|
|
label=channel_name[:100],
|
|||
|
|
value=str(ticket.channel_id),
|
|||
|
|
description=f"Priorité: {ticket.priority.value}"
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if options:
|
|||
|
|
select = Select(
|
|||
|
|
placeholder="Sélectionnez les tickets à fermer...",
|
|||
|
|
options=options,
|
|||
|
|
min_values=1,
|
|||
|
|
max_values=len(options)
|
|||
|
|
)
|
|||
|
|
select.callback = self._select_callback
|
|||
|
|
self.add_item(select)
|
|||
|
|
|
|||
|
|
# Confirm button
|
|||
|
|
confirm_btn = Button(
|
|||
|
|
style=ButtonStyle.red,
|
|||
|
|
label="Confirmer la fermeture",
|
|||
|
|
custom_id="bulk_confirm"
|
|||
|
|
)
|
|||
|
|
confirm_btn.callback = self._confirm_callback
|
|||
|
|
self.add_item(confirm_btn)
|
|||
|
|
|
|||
|
|
async def _select_callback(self, interaction: Interaction):
|
|||
|
|
"""Handle ticket selection"""
|
|||
|
|
self.selected_tickets = set(int(v) for v in interaction.data["values"])
|
|||
|
|
await interaction.response.send_message(
|
|||
|
|
f"*{len(self.selected_tickets)} ticket(s) sélectionné(s)*",
|
|||
|
|
ephemeral=True
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
async def _confirm_callback(self, interaction: Interaction):
|
|||
|
|
"""Confirm bulk close"""
|
|||
|
|
await interaction.response.defer(ephemeral=True)
|
|||
|
|
|
|||
|
|
if not self.selected_tickets:
|
|||
|
|
await interaction.followup.send("Aucun ticket sélectionné.", ephemeral=True)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
storage = get_storage()
|
|||
|
|
closed_count = 0
|
|||
|
|
|
|||
|
|
for channel_id in self.selected_tickets:
|
|||
|
|
ticket = storage.load_ticket(channel_id)
|
|||
|
|
if ticket:
|
|||
|
|
ticket.status = TicketStatus.CLOSED
|
|||
|
|
ticket.closed_at = datetime.now()
|
|||
|
|
storage.save_ticket(ticket)
|
|||
|
|
|
|||
|
|
# Update channel
|
|||
|
|
channel = interaction.guild.get_channel(channel_id)
|
|||
|
|
if channel:
|
|||
|
|
# Update permissions
|
|||
|
|
overwrites = channel.overwrites.copy()
|
|||
|
|
for target, perms in overwrites.items():
|
|||
|
|
if isinstance(target, discord.Member) and target.id != interaction.guild.me.id:
|
|||
|
|
overwrites[target] = discord.PermissionOverwrite(
|
|||
|
|
read_messages=True,
|
|||
|
|
send_messages=False
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
await channel.edit(
|
|||
|
|
name=f"closed-{channel.name}",
|
|||
|
|
overwrites=overwrites
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
await channel.send(f"Fermé par bulk close (Admin: {interaction.user.mention})")
|
|||
|
|
|
|||
|
|
closed_count += 1
|
|||
|
|
|
|||
|
|
await interaction.followup.send(
|
|||
|
|
f"✅ {closed_count} ticket(s) fermé(s).",
|
|||
|
|
ephemeral=True
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ConfigPanelView(View):
|
|||
|
|
"""Configuration panel view"""
|
|||
|
|
|
|||
|
|
def __init__(self, bot, config: GuildTicketConfig):
|
|||
|
|
super().__init__(timeout=None)
|
|||
|
|
self.bot = bot
|
|||
|
|
self.config = config
|
|||
|
|
|
|||
|
|
# Toggle enabled
|
|||
|
|
toggle_btn = Button(
|
|||
|
|
style=ButtonStyle.green if config.enabled else ButtonStyle.red,
|
|||
|
|
label="Activer/Désactiver",
|
|||
|
|
custom_id="config_toggle"
|
|||
|
|
)
|
|||
|
|
toggle_btn.callback = self._toggle_callback
|
|||
|
|
self.add_item(toggle_btn)
|
|||
|
|
|
|||
|
|
# Add category
|
|||
|
|
add_cat_btn = Button(
|
|||
|
|
style=ButtonStyle.blurple,
|
|||
|
|
label="➕ Ajouter Catégorie",
|
|||
|
|
custom_id="config_add_category"
|
|||
|
|
)
|
|||
|
|
add_cat_btn.callback = self._add_category_callback
|
|||
|
|
self.add_item(add_cat_btn)
|
|||
|
|
|
|||
|
|
# Edit roles
|
|||
|
|
roles_btn = Button(
|
|||
|
|
style=ButtonStyle.secondary,
|
|||
|
|
label="👥 Gérer Rôles Staff",
|
|||
|
|
custom_id="config_roles"
|
|||
|
|
)
|
|||
|
|
roles_btn.callback = self._roles_callback
|
|||
|
|
self.add_item(roles_btn)
|
|||
|
|
|
|||
|
|
# Toggle auto-close
|
|||
|
|
auto_close_btn = Button(
|
|||
|
|
style=ButtonStyle.green if config.auto_close_enabled else ButtonStyle.red,
|
|||
|
|
label="Auto-close",
|
|||
|
|
custom_id="config_auto_close"
|
|||
|
|
)
|
|||
|
|
auto_close_btn.callback = self._auto_close_callback
|
|||
|
|
self.add_item(auto_close_btn)
|
|||
|
|
|
|||
|
|
async def _toggle_callback(self, interaction: Interaction):
|
|||
|
|
"""Toggle ticket system"""
|
|||
|
|
self.config.enabled = not self.config.enabled
|
|||
|
|
|
|||
|
|
storage = get_storage()
|
|||
|
|
storage.save_config(interaction.guild_id, self.config)
|
|||
|
|
|
|||
|
|
status = "activé" if self.config.enabled else "désactivé"
|
|||
|
|
await interaction.response.send_message(
|
|||
|
|
f"Le système de tickets est maintenant **{status}**.",
|
|||
|
|
ephemeral=True
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
async def _add_category_callback(self, interaction: Interaction):
|
|||
|
|
"""Show add category modal"""
|
|||
|
|
modal = AddCategoryModal(self.bot, self.config)
|
|||
|
|
await interaction.response.send_modal(modal)
|
|||
|
|
|
|||
|
|
async def _roles_callback(self, interaction: Interaction):
|
|||
|
|
"""Show role management"""
|
|||
|
|
view = RolesManagementView(self.bot, self.config)
|
|||
|
|
|
|||
|
|
embed = discord.Embed(
|
|||
|
|
title="👥 Gestion des Rôles Staff",
|
|||
|
|
description="Sélectionnez les rôles qui auront accès aux tickets:",
|
|||
|
|
color=Color.blue()
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Show current roles
|
|||
|
|
role_mentions = []
|
|||
|
|
for role_id in self.config.staff_roles:
|
|||
|
|
role = interaction.guild.get_role(int(role_id))
|
|||
|
|
if role:
|
|||
|
|
role_mentions.append(role.mention)
|
|||
|
|
|
|||
|
|
embed.add_field(
|
|||
|
|
name="Rôles actuels",
|
|||
|
|
value=", ".join(role_mentions) if role_mentions else "Aucun",
|
|||
|
|
inline=False
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
|
|||
|
|
|
|||
|
|
async def _auto_close_callback(self, interaction: Interaction):
|
|||
|
|
"""Toggle auto-close"""
|
|||
|
|
self.config.auto_close_enabled = not self.config.auto_close_enabled
|
|||
|
|
|
|||
|
|
storage = get_storage()
|
|||
|
|
storage.save_config(interaction.guild_id, self.config)
|
|||
|
|
|
|||
|
|
status = "activé" if self.config.auto_close_enabled else "désactivé"
|
|||
|
|
await interaction.response.send_message(
|
|||
|
|
f"L'auto-close est maintenant **{status}**.",
|
|||
|
|
ephemeral=True
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class AddCategoryModal(Modal):
|
|||
|
|
"""Modal for adding a new category"""
|
|||
|
|
|
|||
|
|
def __init__(self, bot, config: GuildTicketConfig):
|
|||
|
|
super().__init__(title="➕ Nouvelle Catégorie")
|
|||
|
|
self.bot = bot
|
|||
|
|
self.config = config
|
|||
|
|
|
|||
|
|
self.name = TextInput(
|
|||
|
|
label="Nom",
|
|||
|
|
placeholder="Ex: Support",
|
|||
|
|
required=True
|
|||
|
|
)
|
|||
|
|
self.add_item(self.name)
|
|||
|
|
|
|||
|
|
self.description = TextInput(
|
|||
|
|
label="Description",
|
|||
|
|
placeholder="Description de la catégorie...",
|
|||
|
|
required=False,
|
|||
|
|
style=discord.TextStyle.paragraph
|
|||
|
|
)
|
|||
|
|
self.add_item(self.description)
|
|||
|
|
|
|||
|
|
self.emoji = TextInput(
|
|||
|
|
label="Emoji",
|
|||
|
|
placeholder="🎫",
|
|||
|
|
required=False
|
|||
|
|
)
|
|||
|
|
self.add_item(self.emoji)
|
|||
|
|
|
|||
|
|
self.welcome_msg = TextInput(
|
|||
|
|
label="Message de bienvenue",
|
|||
|
|
placeholder="Message affiché quand un ticket est créé...",
|
|||
|
|
required=False,
|
|||
|
|
style=discord.TextStyle.paragraph
|
|||
|
|
)
|
|||
|
|
self.add_item(self.welcome_msg)
|
|||
|
|
|
|||
|
|
async def on_submit(self, interaction: Interaction):
|
|||
|
|
# Generate ID
|
|||
|
|
import uuid
|
|||
|
|
cat_id = str(uuid.uuid4())[:8]
|
|||
|
|
|
|||
|
|
category = TicketCategory(
|
|||
|
|
id=cat_id,
|
|||
|
|
name=self.name.value,
|
|||
|
|
description=self.description.value or "",
|
|||
|
|
emoji=self.emoji.value or "🎫",
|
|||
|
|
welcome_message=self.welcome_msg.value or "Merci d'avoir créé un ticket."
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
self.config.categories.append(category)
|
|||
|
|
|
|||
|
|
if not self.config.default_category:
|
|||
|
|
self.config.default_category = cat_id
|
|||
|
|
|
|||
|
|
storage = get_storage()
|
|||
|
|
storage.save_config(interaction.guild_id, self.config)
|
|||
|
|
|
|||
|
|
await interaction.response.send_message(
|
|||
|
|
f"Catégorie **{self.name.value}** créée avec succès!",
|
|||
|
|
ephemeral=True
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class RolesManagementView(View):
|
|||
|
|
"""View for managing staff roles"""
|
|||
|
|
|
|||
|
|
def __init__(self, bot, config: GuildTicketConfig):
|
|||
|
|
super().__init__(timeout=None)
|
|||
|
|
self.bot = bot
|
|||
|
|
self.config = config
|
|||
|
|
|
|||
|
|
# Create select with guild roles
|
|||
|
|
guild = bot.guilds[0] if bot.guilds else None
|
|||
|
|
options = []
|
|||
|
|
|
|||
|
|
if guild:
|
|||
|
|
for role in guild.roles:
|
|||
|
|
if role.id != guild.id: # Skip @everyone
|
|||
|
|
selected = str(role.id) in config.staff_roles
|
|||
|
|
options.append(
|
|||
|
|
discord.SelectOption(
|
|||
|
|
label=role.name,
|
|||
|
|
value=str(role.id),
|
|||
|
|
default=selected
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if options:
|
|||
|
|
select = Select(
|
|||
|
|
placeholder="Sélectionnez les rôles staff...",
|
|||
|
|
options=options[:25]
|
|||
|
|
)
|
|||
|
|
select.callback = self._select_callback
|
|||
|
|
self.add_item(select)
|
|||
|
|
|
|||
|
|
async def _select_callback(self, interaction: Interaction):
|
|||
|
|
"""Handle role selection"""
|
|||
|
|
selected_ids = set(interaction.data["values"])
|
|||
|
|
|
|||
|
|
# Update config
|
|||
|
|
self.config.staff_roles = list(selected_ids)
|
|||
|
|
|
|||
|
|
storage = get_storage()
|
|||
|
|
storage.save_config(interaction.guild_id, self.config)
|
|||
|
|
|
|||
|
|
await interaction.response.send_message(
|
|||
|
|
f"*{len(selected_ids)} rôle(s) mis à jour.*",
|
|||
|
|
ephemeral=True
|
|||
|
|
)
|
|||
|
|
|