diff --git a/tickets/cog.py b/tickets/cog.py index 9359b87..823005f 100644 --- a/tickets/cog.py +++ b/tickets/cog.py @@ -8,7 +8,17 @@ from discord.ext import commands from tickets import storage from tickets.data import TicketCategory -from tickets.ui import AddMemberButton, ClaimButton, CloseButton, PanelCreateButton +from tickets.ui import ( + AddMemberButton, + ClaimButton, + CloseButton, + LockButton, + PanelCreateButton, + PriorityButton, + StatsButton, + TransferButton, + _is_staff, +) logger = logging.getLogger("vision") @@ -134,7 +144,8 @@ class TicketManagementView(discord.ui.LayoutView): def __init__(self): super().__init__(timeout=None) container = discord.ui.Container() - container.add_item(discord.ui.ActionRow(ClaimButton(), AddMemberButton(), CloseButton())) + container.add_item(discord.ui.ActionRow(ClaimButton(), AddMemberButton(), TransferButton())) + container.add_item(discord.ui.ActionRow(PriorityButton(), LockButton(), StatsButton(), CloseButton())) self.add_item(container) @@ -150,6 +161,37 @@ class TicketSystem(commands.Cog): async def on_ready(self): logger.info("TicketSystem cog charge") + @commands.Cog.listener() + async def on_message(self, message: discord.Message): + if message.author.bot: + return + if not message.guild: + return + + ticket = storage.get_ticket(message.channel.id) + if not ticket: + return + + config = storage.get_config(message.guild.id) + member = message.guild.get_member(message.author.id) + if not member: + return + + is_staff = _is_staff(member, config) + + from tickets.data import TicketMessage + + msg = TicketMessage( + author_id=message.author.id, + author_name=str(message.author), + content=message.content, + timestamp=message.created_at.isoformat(), + attachments=[a.url for a in message.attachments], + is_staff=is_staff, + ) + ticket.messages.append(msg) + storage.save_ticket(ticket) + @ticket.command(name="setup", description="Envoyer le panneau de tickets") @app_commands.describe(channel="Canal pour le panneau") @app_commands.checks.has_permissions(manage_guild=True) diff --git a/tickets/data.py b/tickets/data.py index cfad81c..03b0d4e 100644 --- a/tickets/data.py +++ b/tickets/data.py @@ -9,6 +9,13 @@ class TicketStatus(str, Enum): CLOSED = "closed" +class TicketPriority(str, Enum): + LOW = "low" + NORMAL = "normal" + HIGH = "high" + URGENT = "urgent" + + @dataclass class TicketCategory: id: str @@ -54,6 +61,8 @@ class TicketData: closed_at: str | None = None closed_by: int | None = None claimed_by: int | None = None + locked: bool = False + priority: TicketPriority = TicketPriority.NORMAL messages: list[TicketMessage] = field(default_factory=list) reason: str = "" @@ -68,6 +77,8 @@ class TicketData: "closed_at": self.closed_at, "closed_by": self.closed_by, "claimed_by": self.claimed_by, + "locked": self.locked, + "priority": self.priority.value, "messages": [ { "author_id": m.author_id, @@ -86,6 +97,7 @@ class TicketData: def from_dict(cls, data: dict) -> TicketData: messages = [TicketMessage(**m) for m in data.pop("messages", [])] data["status"] = TicketStatus(data.get("status", "open")) + data["priority"] = TicketPriority(data.get("priority", "normal")) data.pop("transcript_path", None) data.pop("rating", None) data.pop("feedback", None) diff --git a/tickets/ui.py b/tickets/ui.py index cde08b8..052b2f3 100644 --- a/tickets/ui.py +++ b/tickets/ui.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import contextlib import logging +import os from datetime import datetime, timezone import discord @@ -189,6 +190,14 @@ class TicketModal(discord.ui.Modal, title="Nouveau Ticket"): discord.ui.ActionRow( ClaimButton(), AddMemberButton(), + TransferButton(), + ) + ) + container.add_item( + discord.ui.ActionRow( + PriorityButton(), + LockButton(), + StatsButton(), CloseButton(), ) ) @@ -330,6 +339,210 @@ class AddMemberSelectView(discord.ui.View): 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 :** " + 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__(