VisionApp/utils/modmail_transcript.py
Mathis b736f4cc8b
Some checks failed
CI/CD / lint (push) Has been cancelled
CI/CD / test (push) Has been cancelled
CI/CD / deploy (push) Has been cancelled
refactor(modmail): centralize config into single /modmail config panel
- Remove 5 redundant commands (setup, addcategory, removecategory, enable, disable)
- Single /modmail config opens interactive Components V2 panel
- Channel select for ticket category, role select for staff, channel select for logs
- Toggle enable/disable button
- Categories sub-panel: add via modal, remove via select
- Fix lint (unused imports/vars, contextlib.suppress) + ruff format
2026-09-19 15:30:07 +02:00

215 lines
5.5 KiB
Python

import html
from datetime import datetime, timezone
def _format_timestamp(dt: datetime) -> str:
return dt.strftime("%d/%m/%Y %H:%M:%S")
def _avatar_url(user) -> str:
if user.avatar:
return user.avatar.url
return f"https://cdn.discordapp.com/embed/avatars/{user.discriminator % 5}.png"
def generate_transcript(messages: list[dict], ticket_info: dict) -> str:
"""Génère un transcript HTML stylé.
messages: [{"author": discord.User, "content": str, "timestamp": datetime, "attachments": list}]
ticket_info: {"category": str, "user_id": int, "channel_id": int}
"""
now = datetime.now(timezone.utc)
date_str = _format_timestamp(now)
messages_html = ""
for msg in messages:
author = msg["author"]
content = html.escape(msg["content"]) if msg["content"] else ""
timestamp = _format_timestamp(msg["timestamp"])
avatar = _avatar_url(author)
is_staff = msg.get("is_staff", False)
staff_badge = '<span class="badge badge-staff">STAFF</span>' if is_staff else ""
attachments_html = ""
for att in msg.get("attachments", []):
if att.url.lower().endswith((".png", ".jpg", ".jpeg", ".gif", ".webp")):
attachments_html += (
f'<div class="attachment"><img src="{html.escape(att.url)}" alt="attachment" /></div>'
)
else:
attachments_html += (
f'<div class="attachment"><a href="{html.escape(att.url)}">{html.escape(att.filename)}</a></div>'
)
messages_html += f"""
<div class="message {"staff" if is_staff else "user"}">
<div class="msg-header">
<img class="avatar" src="{avatar}" alt="{html.escape(str(author))}" />
<span class="author">{html.escape(str(author))}</span>
{staff_badge}
<span class="timestamp">{timestamp}</span>
</div>
<div class="msg-content">{content}</div>
{attachments_html}
</div>
"""
if not messages_html:
messages_html = '<div class="empty">Aucun message dans ce ticket.</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_info.get("channel_id", 0)}</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: linear-gradient(135deg, #5865f2, #7289da);
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
text-align: center;
}}
.header h1 {{
font-size: 24px;
color: #fff;
margin-bottom: 8px;
}}
.header .meta {{
font-size: 14px;
color: #d4d4d4;
}}
.header .meta span {{
background: rgba(255,255,255,0.15);
padding: 2px 10px;
border-radius: 20px;
margin: 0 4px;
font-size: 13px;
}}
.messages {{
max-width: 800px;
margin: 0 auto;
}}
.message {{
padding: 12px 16px;
margin-bottom: 4px;
border-radius: 8px;
transition: background 0.15s;
}}
.message:hover {{
background: rgba(255,255,255,0.03);
}}
.message.staff {{
border-left: 3px solid #57f287;
background: rgba(87, 242, 135, 0.03);
}}
.message.user {{
border-left: 3px solid #5865f2;
}}
.msg-header {{
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 6px;
}}
.avatar {{
width: 32px;
height: 32px;
border-radius: 50%;
}}
.author {{
font-weight: 600;
font-size: 15px;
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: 12px;
color: #72767d;
margin-left: auto;
}}
.msg-content {{
font-size: 14px;
line-height: 1.5;
color: #dcddde;
white-space: pre-wrap;
word-break: break-word;
margin-left: 42px;
}}
.attachment {{
margin-left: 42px;
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;
font-size: 16px;
}}
.footer {{
text-align: center;
color: #4f545c;
font-size: 12px;
margin-top: 32px;
padding: 16px;
border-top: 1px solid #2f3136;
}}
</style>
</head>
<body>
<div class="header">
<h1>Transcript — Ticket #{ticket_info.get("channel_id", 0)}</h1>
<div class="meta">
<span>Catégorie: {html.escape(ticket_info.get("category", "N/A"))}</span>
<span>Utilisateur: {ticket_info.get("user_id", "N/A")}</span>
<span>Généré le: {date_str}</span>
</div>
</div>
<div class="messages">
{messages_html}
</div>
<div class="footer">
Vision Studio — Transcript généré automatiquement
</div>
</body>
</html>"""
def save_transcript(html_content: str, channel_id: int) -> str:
from config import DATA_DIR
transcripts_dir = DATA_DIR / "transcripts"
transcripts_dir.mkdir(parents=True, exist_ok=True)
path = transcripts_dir / f"ticket_{channel_id}_{int(datetime.now(timezone.utc).timestamp())}.html"
with open(path, "w", encoding="utf-8") as f:
f.write(html_content)
return str(path)