""" Ticket Transcript Generator =========================== Generates beautiful transcript files from ticket conversations. """ import json import os from datetime import datetime from typing import Optional, List, Dict, Any from disnake import User, Member from ..data.models import TicketData, TicketMessage class TranscriptGenerator: """ Generates formatted transcripts from ticket data. Features: - Multiple output formats (TXT, JSON, HTML) - Customizable templates - Attachment handling - Statistics inclusion """ TRANSCRIPT_DIR = "data/tickets/transcripts" def __init__(self): os.makedirs(self.TRANSCRIPT_DIR, exist_ok=True) def generate_txt( self, ticket: TicketData, messages: Optional[List[Dict[str, Any]]] = None, include_stats: bool = True ) -> str: """Generate a plain text transcript""" lines = [] # Header lines.append("=" * 60) lines.append("TRANSCRIPT DE TICKET") lines.append("=" * 60) lines.append("") lines.append(f"ID du salon: {ticket.channel_id}") lines.append(f"ID du serveur: {ticket.guild_id}") lines.append(f"Créé par: {ticket.user_id}") lines.append(f"Catégorie: {ticket.category_id}") lines.append(f"Statut: {ticket.status.value}") lines.append(f"Priorité: {ticket.priority.value}") lines.append(f"Créé le: {ticket.created_at.strftime('%Y-%m-%d %H:%M:%S')}") if ticket.closed_at: lines.append(f"Fermé le: {ticket.closed_at.strftime('%Y-%m-%d %H:%M:%S')}") if ticket.claimed_by: lines.append(f"Réclamé par: {ticket.claimed_by}") if ticket.reason: lines.append(f"Raison: {ticket.reason}") lines.append("") lines.append("-" * 60) lines.append("HISTORIQUE DES MESSAGES") lines.append("-" * 60) lines.append("") # Messages if messages: for msg in messages: timestamp = msg.get("timestamp", "") if isinstance(timestamp, str): try: dt = datetime.fromisoformat(timestamp) timestamp = dt.strftime("%Y-%m-%d %H:%M:%S") except ValueError: pass author = msg.get("author_name", "Inconnu") content = msg.get("content", "") is_staff = msg.get("is_staff", False) staff_marker = " [STAFF]" if is_staff else "" lines.append(f"[{timestamp}] {author}{staff_marker}") if content: lines.append(f" {content}") lines.append("") # Statistics if include_stats and ticket.duration_minutes: lines.append("-" * 60) lines.append("STATISTIQUES") lines.append("-" * 60) lines.append(f"Durée: {ticket.duration_minutes:.1f} minutes") lines.append(f"Nombre de messages: {len(messages) if messages else len(ticket.messages)}") if ticket.rating: stars = "⭐" * ticket.rating lines.append(f"Note: {stars} ({ticket.rating}/5)") if ticket.feedback: lines.append(f"Feedback: {ticket.feedback}") lines.append("") lines.append("=" * 60) lines.append(f"Généré le: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") lines.append("=" * 60) return "\n".join(lines) def generate_json( self, ticket: TicketData, messages: Optional[List[Dict[str, Any]]] = None, include_stats: bool = True ) -> Dict[str, Any]: """Generate a JSON transcript""" data = { "version": "1.0", "generated_at": datetime.now().isoformat(), "ticket": { "channel_id": ticket.channel_id, "guild_id": ticket.guild_id, "user_id": ticket.user_id, "category_id": ticket.category_id, "status": ticket.status.value, "priority": ticket.priority.value, "created_at": ticket.created_at.isoformat(), "closed_at": ticket.closed_at.isoformat() if ticket.closed_at else None, "claimed_by": ticket.claimed_by, "reason": ticket.reason, "rating": ticket.rating, "feedback": ticket.feedback }, "messages": messages or [m.to_dict() for m in ticket.messages] } if include_stats: data["statistics"] = { "duration_minutes": ticket.duration_minutes, "message_count": len(messages) if messages else len(ticket.messages), "duration_formatted": self._format_duration(ticket.duration_minutes) if ticket.duration_minutes else None } return data def generate_html( self, ticket: TicketData, messages: Optional[List[Dict[str, Any]]] = None, include_stats: bool = True, guild_name: str = "Serveur Discord" ) -> str: """Generate an HTML transcript with styling""" # Get priority color priority_colors = { "low": "#2ecc71", "normal": "#3498db", "high": "#e67e22", "urgent": "#e74c3c" } priority_color = priority_colors.get(ticket.priority.value, "#3498db") html_messages = "" if messages: for msg in messages: timestamp = msg.get("timestamp", "") if isinstance(timestamp, str): try: dt = datetime.fromisoformat(timestamp) timestamp = dt.strftime("%d/%m/%Y %H:%M:%S") except ValueError: pass author = msg.get("author_name", "Inconnu") content = msg.get("content", "").replace("\n", "
") is_staff = msg.get("is_staff", False) staff_class = " staff-message" if is_staff else "" staff_badge = 'STAFF' if is_staff else "" html_messages += f"""
{author} {staff_badge} {timestamp}
{content}
""" # Statistics section stats_html = "" if include_stats: duration = self._format_duration(ticket.duration_minutes) if ticket.duration_minutes else "N/A" rating_stars = "⭐" * ticket.rating if ticket.rating else "Pas de note" stats_html = f"""

📊 Statistiques

Durée {duration}
Messages {len(messages) if messages else len(ticket.messages)}
Note {rating_stars}
""" html = f""" Transcript - Ticket #{ticket.channel_id}

🎫 Transcript de Ticket

📌 ID: {ticket.channel_id}
📁 Catégorie: {ticket.category_id}
⚡ Priorité: {ticket.priority.value.upper()}
📊 Statut: {ticket.status.value}
📅 Créé: {ticket.created_at.strftime('%d/%m/%Y %H:%M')}

💬 Messages ({len(messages) if messages else len(ticket.messages)})

{html_messages}
{stats_html}
""" return html def save_transcript( self, ticket: TicketData, messages: Optional[List[Dict[str, Any]]] = None, format: str = "txt", include_stats: bool = True ) -> Optional[str]: """Save a transcript to file and return the path""" if format == "txt": content = self.generate_txt(ticket, messages, include_stats) ext = "txt" elif format == "json": content = json.dumps(self.generate_json(ticket, messages, include_stats), indent=2, ensure_ascii=False) ext = "json" elif format == "html": content = self.generate_html(ticket, messages, include_stats) ext = "html" else: return None # Generate filename timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"ticket_{ticket.channel_id}_{timestamp}.{ext}" filepath = os.path.join(self.TRANSCRIPT_DIR, filename) try: with open(filepath, 'w', encoding='utf-8') as f: f.write(content) return filepath except Exception as e: print(f"[TranscriptGenerator] Error saving transcript: {e}") return None def _format_duration(self, minutes: float) -> str: """Format duration in minutes to human-readable string""" if minutes < 60: return f"{minutes:.0f} min" elif minutes < 1440: hours = minutes / 60 return f"{hours:.1f} h" else: days = minutes / 1440 return f"{days:.1f} j" def get_transcript_path(self, channel_id: int) -> Optional[str]: """Get the most recent transcript for a channel""" if not os.path.exists(self.TRANSCRIPT_DIR): return None files = [f for f in os.listdir(self.TRANSCRIPT_DIR) if f.startswith(f"ticket_{channel_id}_")] if not files: return None latest = max(files, key=lambda x: os.path.getmtime(os.path.join(self.TRANSCRIPT_DIR, x))) return os.path.join(self.TRANSCRIPT_DIR, latest) def cleanup_old_transcripts(self, days: int = 30) -> int: """Remove transcripts older than X days""" if not os.path.exists(self.TRANSCRIPT_DIR): return 0 cutoff = datetime.now().timestamp() - (days * 86400) removed = 0 for filename in os.listdir(self.TRANSCRIPT_DIR): filepath = os.path.join(self.TRANSCRIPT_DIR, filename) if os.path.getmtime(filepath) < cutoff: os.remove(filepath) removed += 1 return removed # Singleton instance _transcript_instance: Optional[TranscriptGenerator] = None def get_transcript_generator() -> TranscriptGenerator: """Get the singleton transcript generator instance""" global _transcript_instance if _transcript_instance is None: _transcript_instance = TranscriptGenerator() return _transcript_instance