VisionApp/tickets/ui.py
2026-09-26 13:17:11 +02:00

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="Creer 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 systeme de tickets est desactive.", ephemeral=True)
return
categories = config.categories
if not categories:
await interaction.response.send_message("Aucune categorie configuree.", 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 categorie :",
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="Selectionnez une categorie...",
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("Categorie 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="Decris ton probleme",
style=discord.TextStyle.paragraph,
placeholder="Explique brievement 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("Categorie 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"**Categorie :** {cat.emoji} {cat.name}\n"
f"**Raison :**\n{self.reason.value}"
)
)
container.add_item(discord.ui.Separator())
container.add_item(
discord.ui.ActionRow(
ClaimButton(),
AddMemberButton(),
TransferButton(),
)
)
container.add_item(
discord.ui.ActionRow(
PriorityButton(),
LockButton(),
StatsButton(),
CloseButton(),
)
)
view = discord.ui.LayoutView()
view.add_item(container)
await channel.send(view=view)
await interaction.followup.send(f"Ticket cree : {channel.mention}", ephemeral=True)
embed = discord.Embed(
title="Ticket cree",
description=f"**Categorie :** {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="Claim",
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 claim 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 retire son claim.", ephemeral=True
)
else:
await interaction.response.send_message(
f"Ce ticket est deja claim 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"Claim 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 claim\nClaim 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(
"Selectionnez le membre a ajouter :",
view=AddMemberSelectView(),
ephemeral=True,
)
class AddMemberSelect(discord.ui.UserSelect):
def __init__(self):
super().__init__(
placeholder="Selectionnez 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 ete ajoute au ticket.",
view=None,
)
embed = discord.Embed(
title="Membre ajoute",
description=f"{user.mention} a ete ajoute 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="Transfer",
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 transferer.", ephemeral=True)
return
await interaction.response.send_message(
"Selectionnez le staff :",
view=TransferSelectView(),
ephemeral=True,
)
class TransferSelect(discord.ui.UserSelect):
def __init__(self):
super().__init__(
placeholder="Selectionnez 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 transfere a {user.mention}.",
view=None,
)
embed = discord.Embed(
title="Ticket transfere",
description=f"Transfere a {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 priorite.", ephemeral=True)
return
await interaction.response.send_message(
"Selectionnez la priorite :",
view=PrioritySelectView(),
ephemeral=True,
)
class PrioritySelect(discord.ui.Select):
def __init__(self):
super().__init__(
placeholder="Selectionnez la priorite...",
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="Lock",
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 lock/unlock.", 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 = "Unlock" if ticket.locked else "Lock"
await interaction.response.send_message(
f"Ticket {'locked' if ticket.locked else 'unlocked'} par {interaction.user.mention}."
)
embed = discord.Embed(
title=f"Ticket {'locked' if ticket.locked else 'unlocked'}",
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 = generate_transcript(ticket, interaction.guild.name)
transcript_file = discord.File(
fp=__import__("io").BytesIO(html.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 ferme 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 ete ferme.",
file=discord.File(
fp=__import__("io").BytesIO(html.encode()),
filename=f"ticket_{ticket.channel_id}.html",
),
)
break
storage.save_ticket(ticket)
embed = discord.Embed(
title="Ticket ferme",
description=f"**Ferme par :** {interaction.user.mention}\n**Categorie :** {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 ferme\nCe ticket sera supprime 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 ferme par {interaction.user}")