from __future__ import annotations import asyncio import contextlib import logging 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) container = discord.ui.Container() 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(CloseButton())) view = discord.ui.LayoutView() view.add_item(container) 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}>" await channel.send(content=mention, 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 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}")