Fix: création de catégorie système de ticket kuby
This commit is contained in:
parent
9051093fb3
commit
eb07cb67a2
2 changed files with 320 additions and 185 deletions
|
|
@ -41,6 +41,9 @@ from src.logger import kuby_logger
|
||||||
# V2 RICH COMPONENTS LOADED - 14:10
|
# V2 RICH COMPONENTS LOADED - 14:10
|
||||||
kuby_logger.info("🎫 [TicketSystem] Chargement de la version V2 Rich Components...")
|
kuby_logger.info("🎫 [TicketSystem] Chargement de la version V2 Rich Components...")
|
||||||
|
|
||||||
|
# Discord limite le nombre de composants par message ; 25 couvre les selects Discord.
|
||||||
|
MAX_PANEL_CATEGORIES = 25
|
||||||
|
|
||||||
# Add a task for sending feedback reminders
|
# Add a task for sending feedback reminders
|
||||||
class FeedbackReminderTask:
|
class FeedbackReminderTask:
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
|
@ -153,12 +156,8 @@ class TicketPanelView(disnake.ui.View):
|
||||||
|
|
||||||
async def _create_callback(self, interaction: disnake.Interaction):
|
async def _create_callback(self, interaction: disnake.Interaction):
|
||||||
"""Affiche les catégories pour créer un ticket"""
|
"""Affiche les catégories pour créer un ticket"""
|
||||||
# Reload config to ensure we have latest permissions/categories
|
self.storage.invalidate_cache(interaction.guild_id)
|
||||||
self.config = self.storage.load_config(interaction.guild_id)
|
self.config = self.storage.load_config(interaction.guild_id)
|
||||||
|
|
||||||
# Reload config to ensure we have latest permissions/categories
|
|
||||||
self.config = self.storage.load_config(interaction.guild_id)
|
|
||||||
|
|
||||||
|
|
||||||
if not self.config.enabled:
|
if not self.config.enabled:
|
||||||
await interaction.response.send_message("Le système de tickets est désactivé.", ephemeral=True)
|
await interaction.response.send_message("Le système de tickets est désactivé.", ephemeral=True)
|
||||||
|
|
@ -167,45 +166,26 @@ class TicketPanelView(disnake.ui.View):
|
||||||
if not self.config.categories:
|
if not self.config.categories:
|
||||||
await interaction.response.send_message("Aucune catégorie configurée. Utilisez la configuration.", ephemeral=True)
|
await interaction.response.send_message("Aucune catégorie configurée. Utilisez la configuration.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Build V2 Container for category selection
|
components = build_category_selection_components(self.config)
|
||||||
sections = [
|
|
||||||
disnake.ui.TextDisplay(content="## 🎫 Créer un Ticket"),
|
|
||||||
disnake.ui.Separator(),
|
|
||||||
]
|
|
||||||
|
|
||||||
for cat in self.config.categories[:5]:
|
|
||||||
desc = cat.description or "Aucune description"
|
|
||||||
sections.append(
|
|
||||||
disnake.ui.Section(
|
|
||||||
f"**{cat.emoji} {cat.name}**\n{desc}",
|
|
||||||
accessory=disnake.ui.Button(
|
|
||||||
label=cat.name,
|
|
||||||
style=disnake.ButtonStyle.primary,
|
|
||||||
custom_id=f"catv2_{cat.id}_{int(disnake.utils.time_snowflake(disnake.utils.utcnow()) / 1000)}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
components=[disnake.ui.Container(*sections)],
|
components=components,
|
||||||
flags=disnake.MessageFlags(is_components_v2=True),
|
flags=disnake.MessageFlags(is_components_v2=True),
|
||||||
ephemeral=True
|
ephemeral=True
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
# Fallback: texte simple avec view legacy
|
|
||||||
if interaction.response.is_done():
|
if interaction.response.is_done():
|
||||||
await interaction.followup.send(
|
await interaction.followup.send(
|
||||||
f"**🎫 Créer un Ticket**\n\nSélectionnez une catégorie ci-dessous:",
|
"**🎫 Créer un Ticket**\n\nSélectionnez une catégorie ci-dessous:",
|
||||||
view=CategorySelectionView(self.bot, self.config),
|
view=CategorySelectionView(self.bot, self.config),
|
||||||
ephemeral=True
|
ephemeral=True
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
f"**🎫 Créer un Ticket**\n\nSélectionnez une catégorie ci-dessous:",
|
"**🎫 Créer un Ticket**\n\nSélectionnez une catégorie ci-dessous:",
|
||||||
view=CategorySelectionView(self.bot, self.config),
|
view=CategorySelectionView(self.bot, self.config),
|
||||||
ephemeral=True
|
ephemeral=True
|
||||||
)
|
)
|
||||||
|
|
@ -477,6 +457,104 @@ class TicketPanelView(disnake.ui.View):
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
async def refresh_ticket_panel(bot, storage, guild_id: int) -> bool:
|
||||||
|
"""Met à jour le message du panel de création de tickets s'il est enregistré."""
|
||||||
|
config = storage.load_config(guild_id)
|
||||||
|
if not config.panel_channel_id or not config.panel_message_id:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
channel = bot.get_channel(config.panel_channel_id)
|
||||||
|
if channel is None:
|
||||||
|
channel = await bot.fetch_channel(config.panel_channel_id)
|
||||||
|
message = await channel.fetch_message(config.panel_message_id)
|
||||||
|
view = TicketPanelView(bot, config, storage)
|
||||||
|
await message.edit(components=view.get_components_v2())
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
kuby_logger.warning(f"[TicketSystem] Échec du rafraîchissement du panel: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def build_category_selection_components(config: GuildTicketConfig) -> list:
|
||||||
|
"""Construit le sélecteur de catégories (menu déroulant, jusqu'à 25 options).
|
||||||
|
|
||||||
|
Un Container V2 ne peut contenir que 10 composants enfants ; une Section+Separator
|
||||||
|
par catégorie dépassait rapidement cette limite. Le menu déroulant affiche toutes
|
||||||
|
les catégories dans un seul composant.
|
||||||
|
"""
|
||||||
|
categories = config.categories[:MAX_PANEL_CATEGORIES]
|
||||||
|
if not categories:
|
||||||
|
return []
|
||||||
|
|
||||||
|
options = []
|
||||||
|
for cat in categories:
|
||||||
|
option_kwargs = {
|
||||||
|
"label": cat.name[:100],
|
||||||
|
"value": str(cat.id)[:100],
|
||||||
|
}
|
||||||
|
description = (cat.description or "").strip()
|
||||||
|
if description:
|
||||||
|
option_kwargs["description"] = description[:100]
|
||||||
|
if cat.emoji:
|
||||||
|
option_kwargs["emoji"] = cat.emoji
|
||||||
|
options.append(disnake.SelectOption(**option_kwargs))
|
||||||
|
|
||||||
|
category_select = disnake.ui.StringSelect(
|
||||||
|
custom_id="ticket_category_select",
|
||||||
|
placeholder="Choisissez une catégorie...",
|
||||||
|
options=options,
|
||||||
|
min_values=1,
|
||||||
|
max_values=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
return [
|
||||||
|
disnake.ui.Container(
|
||||||
|
disnake.ui.TextDisplay(
|
||||||
|
content="# 📂 Créer un Ticket\n\n"
|
||||||
|
"Sélectionnez la catégorie correspondant à votre demande."
|
||||||
|
),
|
||||||
|
disnake.ui.ActionRow(category_select),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def open_ticket_for_category(bot, interaction: disnake.Interaction, config: GuildTicketConfig, category_id: str):
|
||||||
|
"""Ouvre le modal de création de ticket pour la catégorie choisie."""
|
||||||
|
category = config.get_category(category_id)
|
||||||
|
if not category:
|
||||||
|
await interaction.response.send_message("Catégorie non trouvée.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
storage = get_storage()
|
||||||
|
user_tickets = storage.get_user_tickets(interaction.guild_id, interaction.user.id, open_only=True)
|
||||||
|
|
||||||
|
active_tickets = []
|
||||||
|
for ticket in user_tickets:
|
||||||
|
if interaction.guild.get_channel(ticket.channel_id):
|
||||||
|
active_tickets.append(ticket)
|
||||||
|
else:
|
||||||
|
ticket.status = TicketStatus.CLOSED
|
||||||
|
storage.save_ticket(ticket)
|
||||||
|
|
||||||
|
if len(active_tickets) >= config.max_tickets_per_user:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"Vous avez atteint la limite de {config.max_tickets_per_user} tickets.",
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if category.is_recruitment and category.questions:
|
||||||
|
modal = RecruitmentQuestionsModal(bot, config, category)
|
||||||
|
else:
|
||||||
|
modal = TicketPriorityModal(bot, config, category)
|
||||||
|
|
||||||
|
if hasattr(modal, "title") and modal.title:
|
||||||
|
modal.title = str(modal.title)[:45]
|
||||||
|
|
||||||
|
await interaction.response.send_modal(modal)
|
||||||
|
|
||||||
|
|
||||||
class CategoryButton(disnake.ui.Button):
|
class CategoryButton(disnake.ui.Button):
|
||||||
def __init__(self, category, view_instance):
|
def __init__(self, category, view_instance):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
|
@ -502,7 +580,7 @@ class CategorySelectionView(disnake.ui.View):
|
||||||
self.id_gen = int(datetime.now().timestamp())
|
self.id_gen = int(datetime.now().timestamp())
|
||||||
|
|
||||||
# Ajouter boutons de catégories
|
# Ajouter boutons de catégories
|
||||||
for category in config.categories[:5]:
|
for category in config.categories[:MAX_PANEL_CATEGORIES]:
|
||||||
self.add_item(CategoryButton(category, self))
|
self.add_item(CategoryButton(category, self))
|
||||||
|
|
||||||
def _is_staff(self, interaction: disnake.Interaction) -> bool:
|
def _is_staff(self, interaction: disnake.Interaction) -> bool:
|
||||||
|
|
@ -517,40 +595,7 @@ class CategorySelectionView(disnake.ui.View):
|
||||||
async def _on_category_select(self, interaction: disnake.Interaction, category_id: str):
|
async def _on_category_select(self, interaction: disnake.Interaction, category_id: str):
|
||||||
"""Handle category selection"""
|
"""Handle category selection"""
|
||||||
try:
|
try:
|
||||||
|
await open_ticket_for_category(self.bot, interaction, self.config, category_id)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
active_tickets = []
|
|
||||||
for t in user_tickets:
|
|
||||||
if interaction.guild.get_channel(t.channel_id):
|
|
||||||
active_tickets.append(t)
|
|
||||||
else:
|
|
||||||
# Automatically clean up phantom tickets
|
|
||||||
t.status = TicketStatus.CLOSED
|
|
||||||
storage.save_ticket(t)
|
|
||||||
|
|
||||||
if len(active_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
|
|
||||||
|
|
||||||
# Check if this is a recruitment category
|
|
||||||
if category.is_recruitment and category.questions:
|
|
||||||
modal = RecruitmentQuestionsModal(self.bot, self.config, category)
|
|
||||||
else:
|
|
||||||
modal = TicketPriorityModal(self.bot, self.config, category)
|
|
||||||
await interaction.response.send_modal(modal)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
kuby_logger.error(f"[CategorySelection] Error: {e}", exc_info=True)
|
kuby_logger.error(f"[CategorySelection] Error: {e}", exc_info=True)
|
||||||
try:
|
try:
|
||||||
|
|
@ -720,10 +765,13 @@ class RecruitmentQuestionsModal(disnake.ui.Modal):
|
||||||
self.question_inputs = {}
|
self.question_inputs = {}
|
||||||
|
|
||||||
for i, question in enumerate(category.questions):
|
for i, question in enumerate(category.questions):
|
||||||
|
# Discord constraints:
|
||||||
|
# - placeholder length <= 100
|
||||||
|
# We clamp the dynamic placeholder (questions come from config).
|
||||||
input_field = disnake.ui.TextInput(
|
input_field = disnake.ui.TextInput(
|
||||||
label=f"Question {i+1}",
|
label=f"Question {i+1}", # <= 45 by construction
|
||||||
custom_id=f"recruitment_q_{i}",
|
custom_id=f"recruitment_q_{i}",
|
||||||
placeholder=question,
|
placeholder=_clamp_text(question, 100, ""),
|
||||||
required=True,
|
required=True,
|
||||||
max_length=1000,
|
max_length=1000,
|
||||||
style=disnake.TextInputStyle.paragraph
|
style=disnake.TextInputStyle.paragraph
|
||||||
|
|
@ -2050,6 +2098,7 @@ class CategoriesSetupView(disnake.ui.View):
|
||||||
# Remove category
|
# Remove category
|
||||||
self.config.categories = [cat for cat in self.config.categories if cat.id != cat_id]
|
self.config.categories = [cat for cat in self.config.categories if cat.id != cat_id]
|
||||||
self.storage.save_config(interaction.guild_id, self.config)
|
self.storage.save_config(interaction.guild_id, self.config)
|
||||||
|
await refresh_ticket_panel(self.bot, self.storage, interaction.guild_id)
|
||||||
|
|
||||||
await interaction.response.send_message("✅ Catégorie supprimée!", ephemeral=True)
|
await interaction.response.send_message("✅ Catégorie supprimée!", ephemeral=True)
|
||||||
|
|
||||||
|
|
@ -2300,34 +2349,92 @@ class ConfigPanelView(disnake.ui.View):
|
||||||
await interaction.response.send_modal(modal)
|
await interaction.response.send_modal(modal)
|
||||||
|
|
||||||
|
|
||||||
|
def _clamp_text(s: Any, max_len: int, default: str = "") -> str:
|
||||||
|
"""Discord TextInput constraints:
|
||||||
|
- label: 1..45
|
||||||
|
- placeholder: <=100
|
||||||
|
"""
|
||||||
|
if s is None:
|
||||||
|
return default
|
||||||
|
s = str(s)
|
||||||
|
if max_len <= 0:
|
||||||
|
return ""
|
||||||
|
if len(s) > max_len:
|
||||||
|
s = s[:max_len]
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
class AddCategoryModal(disnake.ui.Modal):
|
class AddCategoryModal(disnake.ui.Modal):
|
||||||
"""Modal pour ajouter une catégorie"""
|
"""Modal pour ajouter une catégorie"""
|
||||||
def __init__(self, bot, storage, config: GuildTicketConfig):
|
def __init__(self, bot, storage, config: GuildTicketConfig):
|
||||||
self.name_input = disnake.ui.TextInput(label="Nom", custom_id="add_cat_name", placeholder="Ex: Support", required=True)
|
# label max 45, placeholder max 100
|
||||||
self.desc_input = disnake.ui.TextInput(label="Description", custom_id="add_cat_desc", placeholder="Description...", required=False, style=disnake.TextInputStyle.paragraph)
|
name_label = _clamp_text("Nom", 45, "Nom")
|
||||||
self.emoji_input = disnake.ui.TextInput(label="Emoji", custom_id="add_cat_emoji", placeholder="🎫", required=False, max_length=5)
|
name_placeholder = _clamp_text("Ex: Support", 100, "")
|
||||||
|
|
||||||
|
desc_label = _clamp_text("Description", 45, "Description")
|
||||||
|
desc_placeholder = _clamp_text("Description...", 100, "")
|
||||||
|
|
||||||
|
emoji_label = _clamp_text("Emoji", 45, "Emoji")
|
||||||
|
emoji_placeholder = _clamp_text("🎫", 100, "🎫")
|
||||||
|
|
||||||
|
recruitment_label = _clamp_text("Recrutement? (oui/non)", 45, "Recrutement?")
|
||||||
|
recruitment_placeholder = _clamp_text("non", 100, "non")
|
||||||
|
|
||||||
|
questions_label = _clamp_text(
|
||||||
|
"Questions recrutement (+ optionnel: ID salon réponses)",
|
||||||
|
45,
|
||||||
|
"Questions recrutement"
|
||||||
|
)
|
||||||
|
# This was the most likely culprit: multi-line placeholder very long
|
||||||
|
questions_placeholder = _clamp_text(
|
||||||
|
"Quel âge avez-vous? / Pourquoi vouloir rejoindre? / Expériences? / Optionnel: ID salon réponses: 123456789012345678",
|
||||||
|
100,
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
|
self.name_input = disnake.ui.TextInput(
|
||||||
|
label=name_label,
|
||||||
|
custom_id="add_cat_name",
|
||||||
|
placeholder=name_placeholder,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.desc_input = disnake.ui.TextInput(
|
||||||
|
label=desc_label,
|
||||||
|
custom_id="add_cat_desc",
|
||||||
|
placeholder=desc_placeholder,
|
||||||
|
required=False,
|
||||||
|
style=disnake.TextInputStyle.paragraph,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.emoji_input = disnake.ui.TextInput(
|
||||||
|
label=emoji_label,
|
||||||
|
custom_id="add_cat_emoji",
|
||||||
|
placeholder=emoji_placeholder,
|
||||||
|
required=False,
|
||||||
|
max_length=5,
|
||||||
|
)
|
||||||
|
|
||||||
# Recruitment fields
|
# Recruitment fields
|
||||||
self.is_recruitment_input = disnake.ui.TextInput(
|
self.is_recruitment_input = disnake.ui.TextInput(
|
||||||
label="Recrutement? (oui/non)",
|
label=recruitment_label,
|
||||||
custom_id="add_cat_recruitment",
|
custom_id="add_cat_recruitment",
|
||||||
placeholder="non",
|
placeholder=recruitment_placeholder,
|
||||||
required=False,
|
required=False,
|
||||||
max_length=5
|
max_length=5,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# IMPORTANT:
|
||||||
|
# Disnake Modal has a maximum number of components.
|
||||||
|
# Keep this modal <= 5 components by merging "questions" + "responses channel id" into one input.
|
||||||
self.questions_input = disnake.ui.TextInput(
|
self.questions_input = disnake.ui.TextInput(
|
||||||
label="Questions recrutement (une par ligne)",
|
label=questions_label, # <=45 enforced
|
||||||
custom_id="add_cat_questions",
|
custom_id="add_cat_questions",
|
||||||
placeholder="Quel age avez-vous?\nPourquoi voulez-vous rejoindre?\nExperiences precedentes?",
|
placeholder=questions_placeholder, # <=100 enforced
|
||||||
required=False,
|
required=False,
|
||||||
style=disnake.TextInputStyle.paragraph
|
style=disnake.TextInputStyle.paragraph,
|
||||||
)
|
|
||||||
self.responses_channel_input = disnake.ui.TextInput(
|
|
||||||
label="ID salon pour les réponses",
|
|
||||||
custom_id="add_cat_responses_channel",
|
|
||||||
placeholder="ID du salon staff où envoyer les réponses",
|
|
||||||
required=False,
|
|
||||||
max_length=20
|
|
||||||
)
|
)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
title="Ajouter une Catégorie",
|
title="Ajouter une Catégorie",
|
||||||
components=[
|
components=[
|
||||||
|
|
@ -2336,9 +2443,9 @@ class AddCategoryModal(disnake.ui.Modal):
|
||||||
self.emoji_input,
|
self.emoji_input,
|
||||||
self.is_recruitment_input,
|
self.is_recruitment_input,
|
||||||
self.questions_input,
|
self.questions_input,
|
||||||
self.responses_channel_input
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.storage = storage
|
self.storage = storage
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
@ -2353,13 +2460,23 @@ class AddCategoryModal(disnake.ui.Modal):
|
||||||
emoji_val = interaction.text_values.get("add_cat_emoji", "🎫")
|
emoji_val = interaction.text_values.get("add_cat_emoji", "🎫")
|
||||||
is_recruitment_val = interaction.text_values.get("add_cat_recruitment", "non")
|
is_recruitment_val = interaction.text_values.get("add_cat_recruitment", "non")
|
||||||
questions_val = interaction.text_values.get("add_cat_questions", "")
|
questions_val = interaction.text_values.get("add_cat_questions", "")
|
||||||
responses_channel_val = interaction.text_values.get("add_cat_responses_channel", "")
|
|
||||||
|
|
||||||
is_recruitment = is_recruitment_val.lower() in ["oui", "yes", "o", "y", "1", "true"]
|
is_recruitment = is_recruitment_val.lower() in ["oui", "yes", "o", "y", "1", "true"]
|
||||||
questions = [q.strip() for q in questions_val.split("\n") if q.strip()] if questions_val else []
|
|
||||||
|
# Parse:
|
||||||
|
# - questions are all non-empty lines
|
||||||
|
# - if the LAST non-empty line is a digit-only id => treat it as responses_channel_id
|
||||||
|
raw_lines = [ln.strip() for ln in (questions_val or "").split("\n") if ln.strip()]
|
||||||
responses_channel_id = None
|
responses_channel_id = None
|
||||||
if responses_channel_val and responses_channel_val.isdigit():
|
|
||||||
responses_channel_id = int(responses_channel_val)
|
if raw_lines:
|
||||||
|
last = raw_lines[-1]
|
||||||
|
if last.isdigit():
|
||||||
|
# treat last line as channel id, remove it from questions
|
||||||
|
responses_channel_id = int(last)
|
||||||
|
raw_lines = raw_lines[:-1]
|
||||||
|
|
||||||
|
questions = raw_lines if raw_lines else []
|
||||||
|
|
||||||
category = TicketCategory(
|
category = TicketCategory(
|
||||||
id=cat_id,
|
id=cat_id,
|
||||||
|
|
@ -2368,11 +2485,14 @@ class AddCategoryModal(disnake.ui.Modal):
|
||||||
emoji=emoji_val,
|
emoji=emoji_val,
|
||||||
is_recruitment=is_recruitment,
|
is_recruitment=is_recruitment,
|
||||||
questions=questions,
|
questions=questions,
|
||||||
responses_channel_id=responses_channel_id
|
responses_channel_id=responses_channel_id if is_recruitment else None
|
||||||
)
|
)
|
||||||
|
|
||||||
self.config.categories.append(category)
|
self.storage.invalidate_cache(interaction.guild_id)
|
||||||
self.storage.save_config(interaction.guild_id, self.config)
|
config = self.storage.load_config(interaction.guild_id)
|
||||||
|
config.categories.append(category)
|
||||||
|
self.storage.save_config(interaction.guild_id, config)
|
||||||
|
self.config = config
|
||||||
|
|
||||||
recruitment_info = ""
|
recruitment_info = ""
|
||||||
if is_recruitment:
|
if is_recruitment:
|
||||||
|
|
@ -2380,7 +2500,18 @@ class AddCategoryModal(disnake.ui.Modal):
|
||||||
if responses_channel_id:
|
if responses_channel_id:
|
||||||
recruitment_info += f"\n📝 Salon réponses: <#{responses_channel_id}>"
|
recruitment_info += f"\n📝 Salon réponses: <#{responses_channel_id}>"
|
||||||
|
|
||||||
await interaction.response.send_message(
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
|
||||||
|
if interaction.message:
|
||||||
|
try:
|
||||||
|
manager_view = CategoryManagerView(self.bot, self.storage, config)
|
||||||
|
await interaction.message.edit(components=manager_view.get_components_v2())
|
||||||
|
except Exception as e:
|
||||||
|
kuby_logger.warning(f"[AddCategoryModal] Impossible de rafraîchir la liste des catégories: {e}")
|
||||||
|
|
||||||
|
await refresh_ticket_panel(self.bot, self.storage, interaction.guild_id)
|
||||||
|
|
||||||
|
await interaction.followup.send(
|
||||||
f"Catégorie **{name_val}** créée!{recruitment_info}",
|
f"Catégorie **{name_val}** créée!{recruitment_info}",
|
||||||
ephemeral=True
|
ephemeral=True
|
||||||
)
|
)
|
||||||
|
|
@ -2406,6 +2537,7 @@ class EditCategoryModal(disnake.ui.Modal):
|
||||||
self.category.description = interaction.text_values.get("edit_cat_desc", "")
|
self.category.description = interaction.text_values.get("edit_cat_desc", "")
|
||||||
self.category.emoji = interaction.text_values.get("edit_cat_emoji", "🎫")
|
self.category.emoji = interaction.text_values.get("edit_cat_emoji", "🎫")
|
||||||
self.storage.save_config(interaction.guild_id, self.config)
|
self.storage.save_config(interaction.guild_id, self.config)
|
||||||
|
await refresh_ticket_panel(self.bot, self.storage, interaction.guild_id)
|
||||||
|
|
||||||
container = disnake.ui.Container(
|
container = disnake.ui.Container(
|
||||||
disnake.ui.TextDisplay(content=f"✅ Catégorie **{self.category.name}** mise à jour !"),
|
disnake.ui.TextDisplay(content=f"✅ Catégorie **{self.category.name}** mise à jour !"),
|
||||||
|
|
@ -2507,17 +2639,22 @@ class CategoryManagerView(disnake.ui.View):
|
||||||
description=desc_val,
|
description=desc_val,
|
||||||
emoji=emoji_val
|
emoji=emoji_val
|
||||||
)
|
)
|
||||||
self.config.categories.append(category)
|
self.storage.invalidate_cache(interaction.guild_id)
|
||||||
self.storage.save_config(interaction.guild_id, self.config)
|
config = self.storage.load_config(interaction.guild_id)
|
||||||
|
config.categories.append(category)
|
||||||
|
self.storage.save_config(interaction.guild_id, config)
|
||||||
|
self.config = config
|
||||||
|
self.parent_view.config = config
|
||||||
|
|
||||||
# Refresh parent view
|
# Refresh parent view
|
||||||
self.parent_view._add_category_select()
|
self.parent_view._add_category_select()
|
||||||
|
await refresh_ticket_panel(self.bot, self.storage, interaction.guild_id)
|
||||||
# On doit utiliser edit_message sur l'interaction d'origine ou renvoyer un embed
|
# On doit utiliser edit_message sur l'interaction d'origine ou renvoyer un embed
|
||||||
await interaction.response.edit_message(
|
await interaction.response.edit_message(
|
||||||
embed=self.parent_view._get_embed(),
|
embed=self.parent_view._get_embed(),
|
||||||
view=self.parent_view
|
view=self.parent_view
|
||||||
)
|
)
|
||||||
await interaction.followup.send(f"Catégorie **{self.name.value}** créée!", ephemeral=True)
|
await interaction.followup.send(f"Catégorie **{name_val}** créée!", ephemeral=True)
|
||||||
|
|
||||||
modal = AddCategoryManagerModal(self.bot, self.storage, self.config, self)
|
modal = AddCategoryManagerModal(self.bot, self.storage, self.config, self)
|
||||||
await interaction.response.send_modal(modal)
|
await interaction.response.send_modal(modal)
|
||||||
|
|
@ -2667,6 +2804,7 @@ class DeleteCategoryConfirmationView(disnake.ui.View):
|
||||||
if self.category in self.config.categories:
|
if self.category in self.config.categories:
|
||||||
self.config.categories.remove(self.category)
|
self.config.categories.remove(self.category)
|
||||||
self.storage.save_config(interaction.guild_id, self.config)
|
self.storage.save_config(interaction.guild_id, self.config)
|
||||||
|
await refresh_ticket_panel(self.bot, self.storage, interaction.guild_id)
|
||||||
|
|
||||||
self.parent_view._add_category_select()
|
self.parent_view._add_category_select()
|
||||||
await interaction.response.edit_message(
|
await interaction.response.edit_message(
|
||||||
|
|
@ -3191,31 +3329,11 @@ class TicketCommands(commands.Cog):
|
||||||
# ✅ BUGFIX: Sélection de catégorie en V2 => custom_id = catv2_{cat.id}_{snowflake}
|
# ✅ BUGFIX: Sélection de catégorie en V2 => custom_id = catv2_{cat.id}_{snowflake}
|
||||||
if custom_id.startswith("catv2_"):
|
if custom_id.startswith("catv2_"):
|
||||||
try:
|
try:
|
||||||
# format: catv2_<catid>_<suffix>
|
|
||||||
parts = custom_id.split("_")
|
parts = custom_id.split("_")
|
||||||
if len(parts) >= 3:
|
cat_id = "_".join(parts[1:-1]) if len(parts) >= 3 else (parts[1] if len(parts) > 1 else None)
|
||||||
cat_id = "_".join(parts[1:-1])
|
await open_ticket_for_category(self.bot, inter, config, cat_id)
|
||||||
else:
|
|
||||||
cat_id = parts[1] if len(parts) > 1 else None
|
|
||||||
|
|
||||||
cat_id = parts[1]
|
|
||||||
category = next((c for c in config.categories if str(c.id) == str(cat_id)), None)
|
|
||||||
if not category:
|
|
||||||
await inter.response.send_message("Catégorie non trouvée.", ephemeral=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check if recruitment category with questions
|
|
||||||
if category.is_recruitment and category.questions:
|
|
||||||
modal = RecruitmentQuestionsModal(self.bot, config, category)
|
|
||||||
else:
|
|
||||||
modal = TicketPriorityModal(self.bot, config, category)
|
|
||||||
|
|
||||||
# Sécurité anti-crash : Limite stricte de Discord à 45 caractères pour le titre
|
|
||||||
if hasattr(modal, "title") and modal.title:
|
|
||||||
modal.title = str(modal.title)[:45]
|
|
||||||
await inter.response.send_modal(modal)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
kuby_logger.error(f"[TicketSystem] cat| routing error: {e}", exc_info=True)
|
kuby_logger.error(f"[TicketSystem] catv2_ routing error: {e}", exc_info=True)
|
||||||
if not inter.response.is_done():
|
if not inter.response.is_done():
|
||||||
await inter.response.send_message(f"Erreur interne: {e}", ephemeral=True)
|
await inter.response.send_message(f"Erreur interne: {e}", ephemeral=True)
|
||||||
|
|
||||||
|
|
@ -3225,56 +3343,83 @@ class TicketCommands(commands.Cog):
|
||||||
try:
|
try:
|
||||||
parts = str(custom_id).split("_")
|
parts = str(custom_id).split("_")
|
||||||
cat_id = "_".join(parts[1:-1]) if len(parts) >= 3 else (parts[1] if len(parts) > 1 else None)
|
cat_id = "_".join(parts[1:-1]) if len(parts) >= 3 else (parts[1] if len(parts) > 1 else None)
|
||||||
|
await open_ticket_for_category(self.bot, inter, config, cat_id)
|
||||||
category = next((c for c in config.categories if str(c.id) == str(cat_id)), None)
|
|
||||||
if not category:
|
|
||||||
await inter.response.send_message("Catégorie non trouvée.", ephemeral=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
if category.is_recruitment and category.questions:
|
|
||||||
modal = RecruitmentQuestionsModal(self.bot, config, category)
|
|
||||||
else:
|
|
||||||
modal = TicketPriorityModal(self.bot, config, category)
|
|
||||||
|
|
||||||
# Sécurité anti-crash : Limite stricte de Discord à 45 caractères pour le titre
|
|
||||||
if hasattr(modal, "title") and modal.title:
|
|
||||||
modal.title = str(modal.title)[:45]
|
|
||||||
await inter.response.send_modal(modal)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
kuby_logger.error(f"[TicketSystem] catv2_ routing error: {e}", exc_info=True)
|
kuby_logger.error(f"[TicketSystem] cat_ routing error: {e}", exc_info=True)
|
||||||
if not inter.response.is_done():
|
if not inter.response.is_done():
|
||||||
await inter.response.send_message(f"Erreur interne: {e}", ephemeral=True)
|
await inter.response.send_message(f"Erreur interne: {e}", ephemeral=True)
|
||||||
|
|
||||||
elif custom_id == "ticket_create":
|
elif custom_id == "ticket_create":
|
||||||
# Construction du layout de sélection Premium V2
|
# Debug stockage/cache (pour diagnostiquer "catégorie non trouvée")
|
||||||
category_items = [
|
try:
|
||||||
disnake.ui.TextDisplay(
|
kuby_logger.info(f"[TicketSystem][ticket_create] guild.id={getattr(inter.guild,'id',None)} guild_id={getattr(inter,'guild_id',None)}")
|
||||||
content="# 📂 Sélectionner une Catégorie - Omega Kube\n\n"
|
except Exception:
|
||||||
"Veuillez choisir la catégorie qui correspond au motif de votre demande pour ouvrir un ticket."
|
pass
|
||||||
|
|
||||||
|
self.storage.invalidate_cache(inter.guild.id)
|
||||||
|
config = self.storage.load_config(inter.guild.id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cats = getattr(config, "categories", None) or []
|
||||||
|
# Affiche aussi des infos pour diagnostiquer le storage
|
||||||
|
storage_obj = self.storage
|
||||||
|
config_path = getattr(storage_obj, "config_path", None)
|
||||||
|
kuby_logger.info(
|
||||||
|
f"[TicketSystem][ticket_create] storage.config_path={config_path} "
|
||||||
|
f"guild_id={inter.guild.id} "
|
||||||
|
f"categories_len={len(cats)} categories_ids={[getattr(c,'id',None) for c in cats[:5]]}"
|
||||||
)
|
)
|
||||||
]
|
except Exception:
|
||||||
|
pass
|
||||||
# Génération dynamique des sections avec Emojis et Descriptions configurées
|
|
||||||
for cat in config.categories[:5]:
|
if not config.categories:
|
||||||
cat_emoji = getattr(cat, "emoji", "") or ""
|
# Au cas où cache désynchronisé => invalider aussi avec guild_id
|
||||||
cat_desc = getattr(cat, "description", "") or getattr(cat, "desc", "") or ""
|
try:
|
||||||
|
self.storage.invalidate_cache(inter.guild_id)
|
||||||
category_items.append(disnake.ui.Separator(divider=True))
|
config = self.storage.load_config(inter.guild_id)
|
||||||
category_items.append(
|
cats = getattr(config, "categories", None) or []
|
||||||
disnake.ui.Section(
|
kuby_logger.info(f"[TicketSystem][ticket_create] (after invalidate guild_id) categories_len={len(cats)}")
|
||||||
f"### {cat_emoji} {cat.name}\n{cat_desc}" if cat_desc else f"### {cat_emoji} {cat.name}",
|
except Exception:
|
||||||
accessory=disnake.ui.Button(
|
pass
|
||||||
label="Choisir",
|
|
||||||
style=disnake.ButtonStyle.blurple,
|
if not config.categories:
|
||||||
custom_id=f"cat_{cat.id}_select"
|
await inter.response.send_message(
|
||||||
)
|
"Aucune catégorie configurée (data/tickets/config.json semble vide pour cette guild). "
|
||||||
|
"Allez dans la configuration et ajoutez au moins 1 catégorie.",
|
||||||
|
ephemeral=True
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
components = build_category_selection_components(config)
|
||||||
|
try:
|
||||||
|
await inter.response.send_message(
|
||||||
|
components=components,
|
||||||
|
flags=disnake.MessageFlags(is_components_v2=True),
|
||||||
|
ephemeral=True
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
await inter.response.send_message(
|
kuby_logger.error(f"[TicketSystem] ticket_create UI error: {e}", exc_info=True)
|
||||||
components=[disnake.ui.Container(*category_items)],
|
if inter.response.is_done():
|
||||||
ephemeral=True
|
await inter.followup.send(
|
||||||
)
|
"**🎫 Créer un Ticket**\n\nSélectionnez une catégorie ci-dessous:",
|
||||||
|
view=CategorySelectionView(self.bot, config),
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await inter.response.send_message(
|
||||||
|
"**🎫 Créer un Ticket**\n\nSélectionnez une catégorie ci-dessous:",
|
||||||
|
view=CategorySelectionView(self.bot, config),
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
|
elif custom_id == "ticket_category_select":
|
||||||
|
self.storage.invalidate_cache(inter.guild.id)
|
||||||
|
config = self.storage.load_config(inter.guild.id)
|
||||||
|
cat_id = inter.values[0] if inter.values else None
|
||||||
|
if not cat_id:
|
||||||
|
await inter.response.send_message("Catégorie non trouvée.", ephemeral=True)
|
||||||
|
return
|
||||||
|
await open_ticket_for_category(self.bot, inter, config, cat_id)
|
||||||
elif custom_id == "ticket_my_tickets":
|
elif custom_id == "ticket_my_tickets":
|
||||||
# Afficher les tickets de l'utilisateur
|
# Afficher les tickets de l'utilisateur
|
||||||
user_tickets = self.storage.get_user_tickets(inter.guild.id, inter.user.id)
|
user_tickets = self.storage.get_user_tickets(inter.guild.id, inter.user.id)
|
||||||
|
|
@ -3568,33 +3713,15 @@ class TicketCommands(commands.Cog):
|
||||||
return
|
return
|
||||||
|
|
||||||
elif custom_id.startswith("ticket_config_cat_del_"):
|
elif custom_id.startswith("ticket_config_cat_del_"):
|
||||||
# Le bouton "✅ Confirmer" a custom_id="ticket_config_cat_del_confirm_title"
|
# Ne pas traiter ticket_config_cat_del_confirm_title ici :
|
||||||
# et ne doit pas être traité comme un "legacy delete cat" (sinon cat_id=confirm_title).
|
# la suppression est déjà gérée par DeleteCategoryConfirmationView.confirm_callback.
|
||||||
|
# Traiter à nouveau ici provoquait une double suppression (2 catégories supprimées).
|
||||||
if custom_id == "ticket_config_cat_del_confirm_title":
|
if custom_id == "ticket_config_cat_del_confirm_title":
|
||||||
|
# Juste reconstruire l'UI parent à partir de l'état actuel.
|
||||||
config = self.storage.load_config(inter.guild.id)
|
config = self.storage.load_config(inter.guild.id)
|
||||||
if not config:
|
if config and config.categories is not None:
|
||||||
await inter.response.send_message("Erreur: config introuvable.", ephemeral=True)
|
view = CategoryManagerView(self.bot, self.storage, config)
|
||||||
return
|
await inter.response.edit_message(components=view.get_components_v2())
|
||||||
|
|
||||||
# ⚠️ Dans ce mode, on reconstruit la vue parent et on supprime
|
|
||||||
# la catégorie courante stockée dans le message via l’indexation.
|
|
||||||
# Ici, la catégorie exacte n'est pas encodée dans le custom_id,
|
|
||||||
# donc on supprime le dernier élément si la liste n’est pas vide (fallback).
|
|
||||||
# (Meilleur: corriger l’encodage en index au moment de créer le bouton.)
|
|
||||||
if not config.categories:
|
|
||||||
await inter.response.send_message("Aucune catégorie à supprimer.", ephemeral=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Fallback: supprimer le dernier (ce comportement évite le "rien ne se passe")
|
|
||||||
# et rend le bouton fonctionnel immédiatement.
|
|
||||||
category = config.categories[-1]
|
|
||||||
if category in config.categories:
|
|
||||||
config.categories.remove(category)
|
|
||||||
self.storage.save_config(inter.guild.id, config)
|
|
||||||
|
|
||||||
# Rebuild UI
|
|
||||||
view = CategoryManagerView(self.bot, self.storage, config)
|
|
||||||
await inter.response.edit_message(components=view.get_components_v2())
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Recharger config pour éviter les mismatch (panel vs état storage)
|
# Recharger config pour éviter les mismatch (panel vs état storage)
|
||||||
|
|
@ -3826,7 +3953,11 @@ class TicketCommands(commands.Cog):
|
||||||
target = channel or inter.channel
|
target = channel or inter.channel
|
||||||
|
|
||||||
view = TicketPanelView(self.bot, config, self.storage)
|
view = TicketPanelView(self.bot, config, self.storage)
|
||||||
await target.send(components=view.get_components_v2())
|
panel_message = await target.send(components=view.get_components_v2())
|
||||||
|
|
||||||
|
config.panel_channel_id = target.id
|
||||||
|
config.panel_message_id = panel_message.id
|
||||||
|
self.storage.save_config(inter.guild.id, config)
|
||||||
|
|
||||||
await inter.edit_original_response(content=f"Panel envoyé dans {target.mention}")
|
await inter.edit_original_response(content=f"Panel envoyé dans {target.mention}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -301,6 +301,7 @@ class GuildTicketConfig:
|
||||||
claim_enabled: Whether claiming is enabled by default
|
claim_enabled: Whether claiming is enabled by default
|
||||||
survey_enabled: Whether surveys are enabled by default
|
survey_enabled: Whether surveys are enabled by default
|
||||||
panel_channel_id: Channel for the ticket panel
|
panel_channel_id: Channel for the ticket panel
|
||||||
|
panel_message_id: Message ID of the ticket panel (for auto-refresh)
|
||||||
welcome_enabled: Whether welcome messages are shown
|
welcome_enabled: Whether welcome messages are shown
|
||||||
priority_enabled: Whether priority selection is enabled
|
priority_enabled: Whether priority selection is enabled
|
||||||
"""
|
"""
|
||||||
|
|
@ -322,6 +323,7 @@ class GuildTicketConfig:
|
||||||
claim_enabled: bool = True
|
claim_enabled: bool = True
|
||||||
survey_enabled: bool = True
|
survey_enabled: bool = True
|
||||||
panel_channel_id: Optional[int] = None
|
panel_channel_id: Optional[int] = None
|
||||||
|
panel_message_id: Optional[int] = None
|
||||||
welcome_enabled: bool = True
|
welcome_enabled: bool = True
|
||||||
priority_enabled: bool = True
|
priority_enabled: bool = True
|
||||||
|
|
||||||
|
|
@ -346,6 +348,7 @@ class GuildTicketConfig:
|
||||||
"claim_enabled": self.claim_enabled,
|
"claim_enabled": self.claim_enabled,
|
||||||
"survey_enabled": self.survey_enabled,
|
"survey_enabled": self.survey_enabled,
|
||||||
"panel_channel_id": self.panel_channel_id,
|
"panel_channel_id": self.panel_channel_id,
|
||||||
|
"panel_message_id": self.panel_message_id,
|
||||||
"welcome_enabled": self.welcome_enabled,
|
"welcome_enabled": self.welcome_enabled,
|
||||||
"priority_enabled": self.priority_enabled
|
"priority_enabled": self.priority_enabled
|
||||||
}
|
}
|
||||||
|
|
@ -372,6 +375,7 @@ class GuildTicketConfig:
|
||||||
claim_enabled=data.get("claim_enabled", True),
|
claim_enabled=data.get("claim_enabled", True),
|
||||||
survey_enabled=data.get("survey_enabled", True),
|
survey_enabled=data.get("survey_enabled", True),
|
||||||
panel_channel_id=data.get("panel_channel_id"),
|
panel_channel_id=data.get("panel_channel_id"),
|
||||||
|
panel_message_id=data.get("panel_message_id"),
|
||||||
welcome_enabled=data.get("welcome_enabled", True),
|
welcome_enabled=data.get("welcome_enabled", True),
|
||||||
priority_enabled=data.get("priority_enabled", True)
|
priority_enabled=data.get("priority_enabled", True)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue