All checks were successful
ci / ci (push) Successful in 45s
- Boutons: Réclamer, Transférer, Verrouiller/Déverrouiller, Ajouter, Fermer - Accents français partout (catégorie, équipe, configuré, etc.) - Retiré PriorityButton et StatsButton (trop chargé) - Layout: Gestion (Réclamer/Transférer/Verrouiller) → Ajouter/Fermer
619 lines
23 KiB
Python
619 lines
23 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import contextlib
|
|
import logging
|
|
import os
|
|
from datetime import datetime, timezone
|
|
|
|
import discord
|
|
|
|
from tickets import storage
|
|
from tickets.data import TicketCategory, TicketData, TicketStatus
|
|
from tickets.transcript import generate_transcript
|
|
|
|
logger = logging.getLogger("vision")
|
|
|
|
|
|
def _now_iso() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def _is_staff(member: discord.Member, config) -> bool:
|
|
if member.guild_permissions.administrator:
|
|
return True
|
|
if config.staff_role_id and config.staff_role_id in [r.id for r in member.roles]:
|
|
return True
|
|
return any(cat.staff_role_id and cat.staff_role_id in [r.id for r in member.roles] for cat in config.categories)
|
|
|
|
|
|
async def _send_log(guild: discord.Guild, config, embed: discord.Embed):
|
|
if config.log_channel_id:
|
|
ch = guild.get_channel(config.log_channel_id)
|
|
if ch:
|
|
with contextlib.suppress(discord.HTTPException):
|
|
await ch.send(embed=embed)
|
|
|
|
|
|
class PanelCreateButton(discord.ui.Button):
|
|
def __init__(self):
|
|
super().__init__(
|
|
label="Créer un ticket",
|
|
style=discord.ButtonStyle.primary,
|
|
custom_id="ticket_panel_create",
|
|
)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
config = storage.get_config(interaction.guild_id)
|
|
if not config.enabled:
|
|
await interaction.response.send_message("Le système de tickets est désactivé.", ephemeral=True)
|
|
return
|
|
|
|
categories = config.categories
|
|
if not categories:
|
|
await interaction.response.send_message("Aucune catégorie configurée.", ephemeral=True)
|
|
return
|
|
|
|
if len(categories) == 1:
|
|
cat = categories[0]
|
|
await interaction.response.send_modal(TicketModal(cat.id, cat.name))
|
|
return
|
|
|
|
await interaction.response.send_message(
|
|
"Choisissez une catégorie :",
|
|
view=CategorySelectView(categories),
|
|
ephemeral=True,
|
|
)
|
|
|
|
|
|
class CategorySelect(discord.ui.Select):
|
|
def __init__(self, categories: list[TicketCategory]):
|
|
options = [
|
|
discord.SelectOption(
|
|
label=c.name,
|
|
value=c.id,
|
|
emoji=c.emoji or None,
|
|
description=c.description[:100] if c.description else None,
|
|
)
|
|
for c in categories[:25]
|
|
]
|
|
super().__init__(
|
|
placeholder="Sélectionnez une catégorie...",
|
|
options=options,
|
|
custom_id="ticket_category_select",
|
|
)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
cat_id = self.values[0]
|
|
config = storage.get_config(interaction.guild_id)
|
|
cat = next((c for c in config.categories if c.id == cat_id), None)
|
|
if not cat:
|
|
await interaction.response.send_message("Catégorie introuvable.", ephemeral=True)
|
|
return
|
|
await interaction.response.send_modal(TicketModal(cat.id, cat.name))
|
|
|
|
|
|
class CategorySelectView(discord.ui.View):
|
|
def __init__(self, categories: list[TicketCategory]):
|
|
super().__init__(timeout=300)
|
|
self.add_item(CategorySelect(categories))
|
|
|
|
|
|
class TicketModal(discord.ui.Modal, title="Nouveau Ticket"):
|
|
reason = discord.ui.TextInput(
|
|
label="Décris ton problème",
|
|
style=discord.TextStyle.paragraph,
|
|
placeholder="Explique brièvement ta demande...",
|
|
required=True,
|
|
max_length=2000,
|
|
)
|
|
|
|
def __init__(self, category_id: str, category_name: str):
|
|
super().__init__()
|
|
self.category_id = category_id
|
|
self.category_name = category_name
|
|
|
|
async def on_submit(self, interaction: discord.Interaction):
|
|
await interaction.response.defer(ephemeral=True)
|
|
config = storage.get_config(interaction.guild_id)
|
|
guild = interaction.guild
|
|
|
|
cat = next((c for c in config.categories if c.id == self.category_id), None)
|
|
if not cat:
|
|
await interaction.followup.send("Catégorie introuvable.", ephemeral=True)
|
|
return
|
|
|
|
overwrites = {
|
|
guild.default_role: discord.PermissionOverwrite(view_channel=False),
|
|
guild.me: discord.PermissionOverwrite(
|
|
view_channel=True,
|
|
send_messages=True,
|
|
read_message_history=True,
|
|
attach_files=True,
|
|
manage_channels=True,
|
|
),
|
|
interaction.user: discord.PermissionOverwrite(
|
|
view_channel=True,
|
|
send_messages=True,
|
|
read_message_history=True,
|
|
),
|
|
}
|
|
if config.staff_role_id:
|
|
staff_role = guild.get_role(config.staff_role_id)
|
|
if staff_role:
|
|
overwrites[staff_role] = discord.PermissionOverwrite(
|
|
view_channel=True,
|
|
send_messages=True,
|
|
read_message_history=True,
|
|
attach_files=True,
|
|
)
|
|
if cat.staff_role_id:
|
|
cat_role = guild.get_role(cat.staff_role_id)
|
|
if cat_role:
|
|
overwrites[cat_role] = discord.PermissionOverwrite(
|
|
view_channel=True,
|
|
send_messages=True,
|
|
read_message_history=True,
|
|
attach_files=True,
|
|
)
|
|
|
|
discord_cat = guild.get_channel(cat.discord_category_id) if cat.discord_category_id else None
|
|
safe_name = interaction.user.name.lower().replace(" ", "-")[:20]
|
|
channel = await guild.create_text_channel(
|
|
f"ticket-{safe_name}",
|
|
category=discord_cat,
|
|
overwrites=overwrites,
|
|
topic=f"Ticket de {interaction.user} | {cat.name}",
|
|
)
|
|
|
|
ticket = TicketData(
|
|
channel_id=channel.id,
|
|
guild_id=guild.id,
|
|
user_id=interaction.user.id,
|
|
category_id=cat.id,
|
|
created_at=_now_iso(),
|
|
reason=self.reason.value,
|
|
)
|
|
storage.save_ticket(ticket)
|
|
|
|
mention = interaction.user.mention
|
|
if config.staff_role_id:
|
|
mention += f" <@&{config.staff_role_id}>"
|
|
if cat.staff_role_id:
|
|
mention += f" <@&{cat.staff_role_id}>"
|
|
|
|
container = discord.ui.Container()
|
|
container.add_item(discord.ui.TextDisplay(mention))
|
|
container.add_item(discord.ui.Separator())
|
|
container.add_item(
|
|
discord.ui.TextDisplay(
|
|
f"## Ticket — {cat.name}\n"
|
|
f"**Utilisateur :** {interaction.user.mention}\n"
|
|
f"**Catégorie :** {cat.emoji} {cat.name}\n"
|
|
f"**Raison :**\n{self.reason.value}"
|
|
)
|
|
)
|
|
container.add_item(discord.ui.Separator())
|
|
container.add_item(discord.ui.TextDisplay("### Gestion"))
|
|
container.add_item(
|
|
discord.ui.ActionRow(
|
|
ClaimButton(),
|
|
TransferButton(),
|
|
LockButton(),
|
|
)
|
|
)
|
|
container.add_item(discord.ui.Separator())
|
|
container.add_item(
|
|
discord.ui.ActionRow(
|
|
AddMemberButton(),
|
|
CloseButton(),
|
|
)
|
|
)
|
|
view = discord.ui.LayoutView()
|
|
view.add_item(container)
|
|
|
|
await channel.send(view=view)
|
|
|
|
await interaction.followup.send(f"Ticket créé : {channel.mention}", ephemeral=True)
|
|
|
|
embed = discord.Embed(
|
|
title="Ticket créé",
|
|
description=f"**Catégorie :** {cat.name}\n**Utilisateur :** {interaction.user.mention}\n**Canal :** {channel.mention}",
|
|
color=discord.Color.green(),
|
|
timestamp=datetime.now(timezone.utc),
|
|
)
|
|
await _send_log(guild, config, embed)
|
|
|
|
|
|
class ClaimButton(discord.ui.Button):
|
|
def __init__(self):
|
|
super().__init__(
|
|
label="Réclamer",
|
|
style=discord.ButtonStyle.success,
|
|
custom_id="ticket_claim",
|
|
)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
config = storage.get_config(interaction.guild_id)
|
|
ticket = storage.get_ticket(interaction.channel.id)
|
|
if not ticket:
|
|
await interaction.response.send_message("Ce n'est pas un ticket.", ephemeral=True)
|
|
return
|
|
|
|
member = interaction.guild.get_member(interaction.user.id)
|
|
if not member or not _is_staff(member, config):
|
|
await interaction.response.send_message("Seul le staff peut réclamer un ticket.", ephemeral=True)
|
|
return
|
|
|
|
if ticket.claimed_by:
|
|
if ticket.claimed_by == interaction.user.id:
|
|
ticket.claimed_by = None
|
|
storage.save_ticket(ticket)
|
|
await interaction.response.send_message(
|
|
f"{interaction.user.mention} a retiré son réclamation.", ephemeral=True
|
|
)
|
|
else:
|
|
await interaction.response.send_message(
|
|
f"Ce ticket est déjà réclamé par <@{ticket.claimed_by}>.", ephemeral=True
|
|
)
|
|
return
|
|
|
|
ticket.claimed_by = interaction.user.id
|
|
storage.save_ticket(ticket)
|
|
|
|
embed = discord.Embed(
|
|
title="Ticket claim",
|
|
description=f"Réclamé par {interaction.user.mention}",
|
|
color=discord.Color.green(),
|
|
timestamp=datetime.now(timezone.utc),
|
|
)
|
|
await _send_log(interaction.guild, config, embed)
|
|
|
|
container = discord.ui.Container()
|
|
container.add_item(discord.ui.TextDisplay(f"## Ticket réclamé\nRéclamé par {interaction.user.mention}"))
|
|
view = discord.ui.LayoutView()
|
|
view.add_item(container)
|
|
await interaction.response.send_message(view=view)
|
|
|
|
|
|
class AddMemberButton(discord.ui.Button):
|
|
def __init__(self):
|
|
super().__init__(
|
|
label="Ajouter un membre",
|
|
style=discord.ButtonStyle.secondary,
|
|
custom_id="ticket_add_member",
|
|
)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
config = storage.get_config(interaction.guild_id)
|
|
ticket = storage.get_ticket(interaction.channel.id)
|
|
if not ticket:
|
|
await interaction.response.send_message("Ce n'est pas un ticket.", ephemeral=True)
|
|
return
|
|
|
|
member = interaction.guild.get_member(interaction.user.id)
|
|
if not member or not _is_staff(member, config):
|
|
await interaction.response.send_message("Seul le staff peut ajouter des membres.", ephemeral=True)
|
|
return
|
|
|
|
await interaction.response.send_message(
|
|
"Sélectionnez le membre à ajouter :",
|
|
view=AddMemberSelectView(),
|
|
ephemeral=True,
|
|
)
|
|
|
|
|
|
class AddMemberSelect(discord.ui.UserSelect):
|
|
def __init__(self):
|
|
super().__init__(
|
|
placeholder="Sélectionnez un membre...",
|
|
custom_id="ticket_add_member_select",
|
|
min_values=1,
|
|
max_values=1,
|
|
)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
user = self.values[0]
|
|
await interaction.channel.set_permissions(
|
|
user,
|
|
view_channel=True,
|
|
send_messages=True,
|
|
read_message_history=True,
|
|
)
|
|
await interaction.response.edit_message(
|
|
content=f"{user.mention} a été ajouté au ticket.",
|
|
view=None,
|
|
)
|
|
embed = discord.Embed(
|
|
title="Membre ajouté",
|
|
description=f"{user.mention} a été ajouté par {interaction.user.mention}",
|
|
color=discord.Color.blue(),
|
|
timestamp=datetime.now(timezone.utc),
|
|
)
|
|
await _send_log(interaction.guild, storage.get_config(interaction.guild_id), embed)
|
|
|
|
|
|
class AddMemberSelectView(discord.ui.View):
|
|
def __init__(self):
|
|
super().__init__(timeout=300)
|
|
self.add_item(AddMemberSelect())
|
|
|
|
|
|
class TransferButton(discord.ui.Button):
|
|
def __init__(self):
|
|
super().__init__(
|
|
label="Transférer",
|
|
style=discord.ButtonStyle.secondary,
|
|
custom_id="ticket_transfer",
|
|
)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
config = storage.get_config(interaction.guild_id)
|
|
ticket = storage.get_ticket(interaction.channel.id)
|
|
if not ticket:
|
|
await interaction.response.send_message("Ce n'est pas un ticket.", ephemeral=True)
|
|
return
|
|
member = interaction.guild.get_member(interaction.user.id)
|
|
if not member or not _is_staff(member, config):
|
|
await interaction.response.send_message("Seul le staff peut transférer.", ephemeral=True)
|
|
return
|
|
await interaction.response.send_message(
|
|
"Sélectionnez le staff :",
|
|
view=TransferSelectView(),
|
|
ephemeral=True,
|
|
)
|
|
|
|
|
|
class TransferSelect(discord.ui.UserSelect):
|
|
def __init__(self):
|
|
super().__init__(
|
|
placeholder="Sélectionnez un staff...",
|
|
custom_id="ticket_transfer_select",
|
|
min_values=1,
|
|
max_values=1,
|
|
)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
user = self.values[0]
|
|
ticket = storage.get_ticket(interaction.channel.id)
|
|
if ticket:
|
|
ticket.claimed_by = user.id
|
|
storage.save_ticket(ticket)
|
|
await interaction.response.edit_message(
|
|
content=f"Ticket transféré à {user.mention}.",
|
|
view=None,
|
|
)
|
|
embed = discord.Embed(
|
|
title="Ticket transféré",
|
|
description=f"Transféré à {user.mention} par {interaction.user.mention}",
|
|
color=discord.Color.blue(),
|
|
timestamp=datetime.now(timezone.utc),
|
|
)
|
|
await _send_log(interaction.guild, storage.get_config(interaction.guild_id), embed)
|
|
|
|
|
|
class TransferSelectView(discord.ui.View):
|
|
def __init__(self):
|
|
super().__init__(timeout=300)
|
|
self.add_item(TransferSelect())
|
|
|
|
|
|
class PriorityButton(discord.ui.Button):
|
|
def __init__(self):
|
|
super().__init__(
|
|
label="Priorite",
|
|
style=discord.ButtonStyle.secondary,
|
|
custom_id="ticket_priority",
|
|
)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
config = storage.get_config(interaction.guild_id)
|
|
ticket = storage.get_ticket(interaction.channel.id)
|
|
if not ticket:
|
|
await interaction.response.send_message("Ce n'est pas un ticket.", ephemeral=True)
|
|
return
|
|
member = interaction.guild.get_member(interaction.user.id)
|
|
if not member or not _is_staff(member, config):
|
|
await interaction.response.send_message("Seul le staff peut changer la priorité.", ephemeral=True)
|
|
return
|
|
await interaction.response.send_message(
|
|
"Sélectionnez la priorité :",
|
|
view=PrioritySelectView(),
|
|
ephemeral=True,
|
|
)
|
|
|
|
|
|
class PrioritySelect(discord.ui.Select):
|
|
def __init__(self):
|
|
super().__init__(
|
|
placeholder="Sélectionnez la priorité...",
|
|
custom_id="ticket_priority_select",
|
|
options=[
|
|
discord.SelectOption(label="Basse", value="low", emoji="🟢"),
|
|
discord.SelectOption(label="Normale", value="normal", emoji="🔵"),
|
|
discord.SelectOption(label="Haute", value="high", emoji="🟠"),
|
|
discord.SelectOption(label="Urgente", value="urgent", emoji="🔴"),
|
|
],
|
|
)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
from tickets.data import TicketPriority
|
|
|
|
ticket = storage.get_ticket(interaction.channel.id)
|
|
if ticket:
|
|
ticket.priority = TicketPriority(self.values[0])
|
|
storage.save_ticket(ticket)
|
|
await interaction.response.edit_message(
|
|
content=f"Priorite definie sur **{self.values[0]}**.",
|
|
view=None,
|
|
)
|
|
embed = discord.Embed(
|
|
title="Priorite changee",
|
|
description=f"Priorite : **{self.values[0]}** par {interaction.user.mention}",
|
|
color=discord.Color.blue(),
|
|
timestamp=datetime.now(timezone.utc),
|
|
)
|
|
await _send_log(interaction.guild, storage.get_config(interaction.guild_id), embed)
|
|
|
|
|
|
class PrioritySelectView(discord.ui.View):
|
|
def __init__(self):
|
|
super().__init__(timeout=300)
|
|
self.add_item(PrioritySelect())
|
|
|
|
|
|
class LockButton(discord.ui.Button):
|
|
def __init__(self):
|
|
super().__init__(
|
|
label="Verrouiller",
|
|
style=discord.ButtonStyle.secondary,
|
|
custom_id="ticket_lock",
|
|
)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
config = storage.get_config(interaction.guild_id)
|
|
ticket = storage.get_ticket(interaction.channel.id)
|
|
if not ticket:
|
|
await interaction.response.send_message("Ce n'est pas un ticket.", ephemeral=True)
|
|
return
|
|
member = interaction.guild.get_member(interaction.user.id)
|
|
if not member or not _is_staff(member, config):
|
|
await interaction.response.send_message("Seul le staff peut verrouiller/déverrouiller.", ephemeral=True)
|
|
return
|
|
|
|
ticket.locked = not ticket.locked
|
|
storage.save_ticket(ticket)
|
|
|
|
overwrites = interaction.channel.overwrites_for(interaction.guild.default_role)
|
|
overwrites.send_messages = not ticket.locked
|
|
await interaction.channel.set_permissions(interaction.guild.default_role, overwrite=overwrites)
|
|
|
|
self.label = "Déverrouiller" if ticket.locked else "Verrouiller"
|
|
|
|
await interaction.response.send_message(
|
|
f"Ticket {'verrouillé' if ticket.locked else 'déverrouillé'} par {interaction.user.mention}."
|
|
)
|
|
|
|
embed = discord.Embed(
|
|
title=f"Ticket {'verrouillé' if ticket.locked else 'déverrouillé'}",
|
|
description=f"Par {interaction.user.mention}",
|
|
color=discord.Color.orange() if ticket.locked else discord.Color.green(),
|
|
timestamp=datetime.now(timezone.utc),
|
|
)
|
|
await _send_log(interaction.guild, config, embed)
|
|
|
|
|
|
class StatsButton(discord.ui.Button):
|
|
def __init__(self):
|
|
super().__init__(
|
|
label="Infos",
|
|
style=discord.ButtonStyle.secondary,
|
|
custom_id="ticket_stats",
|
|
)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
ticket = storage.get_ticket(interaction.channel.id)
|
|
if not ticket:
|
|
await interaction.response.send_message("Ce n'est pas un ticket.", ephemeral=True)
|
|
return
|
|
config = storage.get_config(interaction.guild_id)
|
|
cat = next((c for c in config.categories if c.id == ticket.category_id), None)
|
|
|
|
user = interaction.guild.get_member(ticket.user_id)
|
|
claimed = interaction.guild.get_member(ticket.claimed_by) if ticket.claimed_by else None
|
|
msg_count = len(ticket.messages)
|
|
|
|
container = discord.ui.Container()
|
|
container.add_item(
|
|
discord.ui.TextDisplay(
|
|
f"## Infos Ticket\n"
|
|
f"**Categorie :** {cat.emoji} {cat.name if cat else ticket.category_id}\n"
|
|
f"**Utilisateur :** {user.mention if user else ticket.user_id}\n"
|
|
f"**Claim par :** {claimed.mention if claimed else 'Non claim'}\n"
|
|
f"**Priorite :** {ticket.priority.value}\n"
|
|
f"**Statut :** {'Lock' if ticket.locked else 'Ouvert'}\n"
|
|
f"**Messages :** {msg_count}\n"
|
|
f"**Cree :** <t:{int(os.path.fromisoformat(ticket.created_at).timestamp())}:R>"
|
|
if ticket.created_at
|
|
else ""
|
|
)
|
|
)
|
|
view = discord.ui.LayoutView()
|
|
view.add_item(container)
|
|
await interaction.response.send_message(view=view, ephemeral=True)
|
|
|
|
|
|
class CloseButton(discord.ui.Button):
|
|
def __init__(self):
|
|
super().__init__(
|
|
label="Fermer le ticket",
|
|
style=discord.ButtonStyle.danger,
|
|
custom_id="ticket_close",
|
|
)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
config = storage.get_config(interaction.guild_id)
|
|
ticket = storage.get_ticket(interaction.channel.id)
|
|
if not ticket:
|
|
await interaction.response.send_message("Ce n'est pas un ticket.", ephemeral=True)
|
|
return
|
|
|
|
member = interaction.guild.get_member(interaction.user.id)
|
|
if not member or not _is_staff(member, config):
|
|
await interaction.response.send_message("Seul le staff peut fermer un ticket.", ephemeral=True)
|
|
return
|
|
|
|
await interaction.response.defer(ephemeral=True)
|
|
|
|
ticket.status = TicketStatus.CLOSED
|
|
ticket.closed_at = _now_iso()
|
|
ticket.closed_by = interaction.user.id
|
|
|
|
if config.transcript_enabled:
|
|
html_content = generate_transcript(ticket, interaction.guild.name)
|
|
transcript_file = discord.File(
|
|
fp=__import__("io").BytesIO(html_content.encode()),
|
|
filename=f"ticket_{ticket.channel_id}.html",
|
|
)
|
|
if config.log_channel_id:
|
|
ch = interaction.guild.get_channel(config.log_channel_id)
|
|
if ch:
|
|
with contextlib.suppress(discord.HTTPException):
|
|
await ch.send(
|
|
f"Ticket fermé par {interaction.user.mention}",
|
|
file=transcript_file,
|
|
)
|
|
|
|
for msg in ticket.messages:
|
|
if not msg.is_staff:
|
|
user = interaction.guild.get_member(msg.author_id)
|
|
if user:
|
|
with contextlib.suppress(discord.HTTPException):
|
|
await user.send(
|
|
"Ton ticket a été fermé.",
|
|
file=discord.File(
|
|
fp=__import__("io").BytesIO(html_content.encode()),
|
|
filename=f"ticket_{ticket.channel_id}.html",
|
|
),
|
|
)
|
|
break
|
|
|
|
storage.save_ticket(ticket)
|
|
|
|
embed = discord.Embed(
|
|
title="Ticket fermé",
|
|
description=f"**Fermé par :** {interaction.user.mention}\n**Catégorie :** {ticket.category_id}",
|
|
color=discord.Color.red(),
|
|
timestamp=datetime.now(timezone.utc),
|
|
)
|
|
await _send_log(interaction.guild, config, embed)
|
|
|
|
container = discord.ui.Container()
|
|
container.add_item(discord.ui.TextDisplay("## Ticket fermé\nCe ticket sera supprimé dans 5 secondes."))
|
|
view = discord.ui.LayoutView()
|
|
view.add_item(container)
|
|
await interaction.followup.send(view=view)
|
|
|
|
await asyncio.sleep(5)
|
|
with contextlib.suppress(discord.HTTPException):
|
|
await interaction.channel.delete(reason=f"Ticket fermé par {interaction.user}")
|