feat(ticket): rewrite ticket system from scratch inspired by Kuby
New minimal ticket system (dev only):
- Panel with create ticket button (Components V2, no accent colors)
- Category selection via dropdown or direct modal (1 cat)
- Ticket channel creation with permissions, staff role, welcome message
- Close flow: staff check, transcript HTML (dark theme), log, auto-delete
- /ticket setup, /ticket config (interactive panel)
- Storage: JSON per-ticket + config.json (singleton pattern)
- Modular architecture: data.py, storage.py, ui.py, transcript.py, cog.py
- Removed old modmail system
2026-09-20 20:04:10 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import html
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
|
|
|
|
from tickets.data import TicketData
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _format_ts(dt_str: str) -> str:
|
|
|
|
|
try:
|
|
|
|
|
dt = datetime.fromisoformat(dt_str)
|
|
|
|
|
return dt.strftime("%d/%m/%Y %H:%M:%S")
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
return dt_str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_transcript(ticket: TicketData, guild_name: str = "") -> str:
|
|
|
|
|
now = datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S")
|
|
|
|
|
|
|
|
|
|
messages_html = ""
|
|
|
|
|
for msg in ticket.messages:
|
|
|
|
|
content = html.escape(msg.content) if msg.content else ""
|
|
|
|
|
ts = _format_ts(msg.timestamp)
|
|
|
|
|
badge = '<span class="badge badge-staff">STAFF</span>' if msg.is_staff else ""
|
|
|
|
|
|
|
|
|
|
att_html = ""
|
2026-10-02 19:34:11 +02:00
|
|
|
for att in msg.attachments:
|
feat(ticket): rewrite ticket system from scratch inspired by Kuby
New minimal ticket system (dev only):
- Panel with create ticket button (Components V2, no accent colors)
- Category selection via dropdown or direct modal (1 cat)
- Ticket channel creation with permissions, staff role, welcome message
- Close flow: staff check, transcript HTML (dark theme), log, auto-delete
- /ticket setup, /ticket config (interactive panel)
- Storage: JSON per-ticket + config.json (singleton pattern)
- Modular architecture: data.py, storage.py, ui.py, transcript.py, cog.py
- Removed old modmail system
2026-09-20 20:04:10 +02:00
|
|
|
if any(att.lower().endswith(ext) for ext in (".png", ".jpg", ".jpeg", ".gif", ".webp")):
|
|
|
|
|
att_html += f'<div class="attachment"><img src="{html.escape(att)}" /></div>'
|
|
|
|
|
else:
|
|
|
|
|
att_html += f'<div class="attachment"><a href="{html.escape(att)}">{html.escape(att.rsplit("/", 1)[-1])}</a></div>'
|
|
|
|
|
|
|
|
|
|
messages_html += f"""
|
|
|
|
|
<div class="message {"staff" if msg.is_staff else "user"}">
|
|
|
|
|
<div class="msg-header">
|
|
|
|
|
<span class="author">{html.escape(msg.author_name)}</span>
|
|
|
|
|
{badge}
|
|
|
|
|
<span class="timestamp">{ts}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="msg-content">{content}</div>
|
|
|
|
|
{att_html}
|
|
|
|
|
</div>
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
if not messages_html:
|
|
|
|
|
messages_html = '<div class="empty">Aucun message.</div>'
|
|
|
|
|
|
|
|
|
|
return f"""<!DOCTYPE html>
|
|
|
|
|
<html lang="fr">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<title>Transcript — Ticket #{ticket.channel_id}</title>
|
|
|
|
|
<style>
|
|
|
|
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
|
|
|
body {{
|
|
|
|
|
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|
|
|
|
background: #0b0d11;
|
|
|
|
|
color: #dcddde;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
}}
|
|
|
|
|
.header {{
|
|
|
|
|
background: #1a1d23;
|
|
|
|
|
border: 1px solid #2f3136;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 24px;
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}}
|
|
|
|
|
.header h1 {{
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
color: #fff;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}}
|
|
|
|
|
.header .meta {{
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #72767d;
|
|
|
|
|
}}
|
|
|
|
|
.header .meta span {{
|
|
|
|
|
background: #2f3136;
|
|
|
|
|
padding: 2px 10px;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
margin: 0 4px;
|
|
|
|
|
}}
|
|
|
|
|
.messages {{
|
|
|
|
|
max-width: 800px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
}}
|
|
|
|
|
.message {{
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
}}
|
|
|
|
|
.message:hover {{
|
|
|
|
|
background: rgba(255,255,255,0.02);
|
|
|
|
|
}}
|
|
|
|
|
.message.staff {{
|
|
|
|
|
border-left: 3px solid #57f287;
|
|
|
|
|
}}
|
|
|
|
|
.message.user {{
|
|
|
|
|
border-left: 3px solid #5865f2;
|
|
|
|
|
}}
|
|
|
|
|
.msg-header {{
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}}
|
|
|
|
|
.author {{
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: #fff;
|
|
|
|
|
}}
|
|
|
|
|
.badge {{
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
}}
|
|
|
|
|
.badge-staff {{ background: #57f287; color: #000; }}
|
|
|
|
|
.timestamp {{
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: #4f545c;
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
}}
|
|
|
|
|
.msg-content {{
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
color: #dcddde;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
margin-left: 0;
|
|
|
|
|
}}
|
|
|
|
|
.attachment {{
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
}}
|
|
|
|
|
.attachment img {{
|
|
|
|
|
max-width: 400px;
|
|
|
|
|
max-height: 300px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px solid #2f3136;
|
|
|
|
|
}}
|
|
|
|
|
.attachment a {{ color: #00aff4; text-decoration: none; }}
|
|
|
|
|
.empty {{
|
|
|
|
|
text-align: center;
|
|
|
|
|
color: #72767d;
|
|
|
|
|
padding: 40px;
|
|
|
|
|
}}
|
|
|
|
|
.footer {{
|
|
|
|
|
text-align: center;
|
|
|
|
|
color: #4f545c;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
margin-top: 32px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
border-top: 1px solid #2f3136;
|
|
|
|
|
}}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div class="header">
|
|
|
|
|
<h1>Ticket #{ticket.channel_id}</h1>
|
|
|
|
|
<div class="meta">
|
|
|
|
|
<span>Categorie: {html.escape(ticket.category_id)}</span>
|
|
|
|
|
<span>User: {ticket.user_id}</span>
|
|
|
|
|
<span>Cree: {_format_ts(ticket.created_at)}</span>
|
|
|
|
|
{f"<span>Ferme: {_format_ts(ticket.closed_at)}</span>" if ticket.closed_at else ""}
|
|
|
|
|
<span>Genere: {now}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="messages">{messages_html}</div>
|
|
|
|
|
<div class="footer">Vision Studio — Transcript</div>
|
|
|
|
|
</body>
|
|
|
|
|
</html>"""
|