Kuby/commandes/ticket/data/models.py

366 lines
14 KiB
Python

"""
Ticket Data Models
==================
Data models for the modular ticket system.
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, List, Dict, Any
from enum import Enum
class TicketStatus(Enum):
"""Ticket status enumeration"""
OPEN = "open"
CLAIMED = "claimed"
CLOSED = "closed"
ARCHIVED = "archived"
class Priority(Enum):
"""Ticket priority levels"""
LOW = "low"
NORMAL = "normal"
HIGH = "high"
URGENT = "urgent"
class TicketCategory:
"""
Represents a ticket category with customizable settings.
Attributes:
id: Unique identifier for the category
name: Display name
description: Description shown in panel
emoji: Emoji icon for the category
color: Embed color (hex)
staff_roles: List of role IDs who can access this category
welcome_message: Custom welcome message for this category
require_reason: Whether to require a reason when creating
max_tickets_per_user: Maximum tickets per user in this category
auto_close_days: Days before auto-closing inactive tickets
priority_enabled: Whether priority selection is enabled
allow_claims: Whether staff can claim tickets
survey_enabled: Whether satisfaction survey is enabled
"""
def __init__(
self,
id: str,
name: str,
description: str = "",
emoji: str = "🎫",
color: str = "#3498db",
staff_roles: List[str] = None,
welcome_message: str = None,
require_reason: bool = False,
max_tickets_per_user: int = 5,
auto_close_days: int = 7,
priority_enabled: bool = True,
allow_claims: bool = True,
survey_enabled: bool = True
):
self.id = id
self.name = name
self.description = description
self.emoji = emoji
self.color = color
self.staff_roles = staff_roles or []
self.welcome_message = welcome_message or "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu."
self.require_reason = require_reason
self.max_tickets_per_user = max_tickets_per_user
self.auto_close_days = auto_close_days
self.priority_enabled = priority_enabled
self.allow_claims = allow_claims
self.survey_enabled = survey_enabled
def to_dict(self) -> Dict[str, Any]:
"""Convert category to dictionary for storage"""
return {
"id": self.id,
"name": self.name,
"description": self.description,
"emoji": self.emoji,
"color": self.color,
"staff_roles": self.staff_roles,
"welcome_message": self.welcome_message,
"require_reason": self.require_reason,
"max_tickets_per_user": self.max_tickets_per_user,
"auto_close_days": self.auto_close_days,
"priority_enabled": self.priority_enabled,
"allow_claims": self.allow_claims,
"survey_enabled": self.survey_enabled
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'TicketCategory':
"""Create category from dictionary"""
return cls(
id=data.get("id", ""),
name=data.get("name", ""),
description=data.get("description", ""),
emoji=data.get("emoji", "🎫"),
color=data.get("color", "#3498db"),
staff_roles=data.get("staff_roles", []),
welcome_message=data.get("welcome_message"),
require_reason=data.get("require_reason", False),
max_tickets_per_user=data.get("max_tickets_per_user", 5),
auto_close_days=data.get("auto_close_days", 7),
priority_enabled=data.get("priority_enabled", True),
allow_claims=data.get("allow_claims", True),
survey_enabled=data.get("survey_enabled", True)
)
@dataclass
class TicketMessage:
"""Represents a message in a ticket"""
id: str
author_id: int
author_name: str
content: str
timestamp: datetime
attachments: List[str] = field(default_factory=list)
is_staff: bool = False
def to_dict(self) -> Dict[str, Any]:
return {
"id": self.id,
"author_id": self.author_id,
"author_name": self.author_name,
"content": self.content,
"timestamp": self.timestamp.isoformat(),
"attachments": self.attachments,
"is_staff": self.is_staff
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'TicketMessage':
return cls(
id=data.get("id", ""),
author_id=data.get("author_id", 0),
author_name=data.get("author_name", ""),
content=data.get("content", ""),
timestamp=datetime.fromisoformat(data.get("timestamp", datetime.now().isoformat())),
attachments=data.get("attachments", []),
is_staff=data.get("is_staff", False)
)
@dataclass
class TicketData:
"""
Main ticket data structure.
Attributes:
channel_id: Discord channel ID
guild_id: Discord guild ID
user_id: User who created the ticket
category_id: Ticket category
status: Current ticket status
priority: Ticket priority level
created_at: Creation timestamp
closed_at: Closing timestamp
claimed_by: Staff member who claimed the ticket
messages: List of messages in the ticket
reason: Reason for creating the ticket
transcript_path: Path to generated transcript
rating: Satisfaction rating (1-5)
feedback: User feedback text
"""
channel_id: int
guild_id: int
user_id: int
category_id: str
status: TicketStatus = TicketStatus.OPEN
priority: Priority = Priority.NORMAL
created_at: datetime = field(default_factory=datetime.now)
closed_at: Optional[datetime] = None
claimed_by: Optional[int] = None
messages: List[TicketMessage] = field(default_factory=list)
reason: str = ""
transcript_path: Optional[str] = None
rating: Optional[int] = None
feedback: Optional[str] = None
@property
def is_open(self) -> bool:
return self.status in [TicketStatus.OPEN, TicketStatus.CLAIMED]
@property
def duration_minutes(self) -> Optional[float]:
"""Get ticket duration in minutes"""
end = self.closed_at or datetime.now()
return (end - self.created_at).total_seconds() / 60
def to_dict(self) -> Dict[str, Any]:
return {
"channel_id": self.channel_id,
"guild_id": self.guild_id,
"user_id": self.user_id,
"category_id": self.category_id,
"status": self.status.value,
"priority": self.priority.value,
"created_at": self.created_at.isoformat(),
"closed_at": self.closed_at.isoformat() if self.closed_at else None,
"claimed_by": self.claimed_by,
"messages": [m.to_dict() for m in self.messages],
"reason": self.reason,
"transcript_path": self.transcript_path,
"rating": self.rating,
"feedback": self.feedback
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'TicketData':
messages = [TicketMessage.from_dict(m) for m in data.get("messages", [])]
return cls(
channel_id=data.get("channel_id", 0),
guild_id=data.get("guild_id", 0),
user_id=data.get("user_id", 0),
category_id=data.get("category_id", ""),
status=TicketStatus(data.get("status", "open")),
priority=Priority(data.get("priority", "normal")),
created_at=datetime.fromisoformat(data.get("created_at", datetime.now().isoformat())),
closed_at=datetime.fromisoformat(data["closed_at"]) if data.get("closed_at") else None,
claimed_by=data.get("claimed_by"),
messages=messages,
reason=data.get("reason", ""),
transcript_path=data.get("transcript_path"),
rating=data.get("rating"),
feedback=data.get("feedback")
)
@dataclass
class TicketStats:
"""Statistics for a ticket system"""
total_tickets: int = 0
open_tickets: int = 0
closed_tickets: int = 0
claimed_tickets: int = 0
average_duration_minutes: float = 0
average_rating: float = 0
tickets_by_category: Dict[str, int] = field(default_factory=dict)
tickets_by_priority: Dict[str, int] = field(default_factory=dict)
tickets_by_day: Dict[str, int] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
return {
"total_tickets": self.total_tickets,
"open_tickets": self.open_tickets,
"closed_tickets": self.closed_tickets,
"claimed_tickets": self.claimed_tickets,
"average_duration_minutes": self.average_duration_minutes,
"average_rating": self.average_rating,
"tickets_by_category": self.tickets_by_category,
"tickets_by_priority": self.tickets_by_priority,
"tickets_by_day": self.tickets_by_day
}
@dataclass
class GuildTicketConfig:
"""
Guild-wide ticket configuration.
Attributes:
guild_id: Discord guild ID
enabled: Whether the ticket system is enabled
categories: List of ticket categories
default_category: Default category ID
log_channel_id: Channel for ticket logs
archive_channel_id: Channel for archived transcripts
staff_roles: Global staff roles (can manage tickets, access analytics, etc.)
config_roles: Roles that can access the configuration panel (/ticketconfig)
max_tickets_per_user: Global max tickets per user
auto_close_enabled: Whether auto-close is enabled
auto_close_days: Days before auto-close
transcript_enabled: Whether transcripts are generated
claim_enabled: Whether claiming is enabled by default
survey_enabled: Whether surveys are enabled by default
panel_channel_id: Channel for the ticket panel
welcome_enabled: Whether welcome messages are shown
priority_enabled: Whether priority selection is enabled
"""
guild_id: int
enabled: bool = True
categories: List[TicketCategory] = field(default_factory=list)
default_category: Optional[str] = None
log_channel_id: Optional[int] = None
archive_channel_id: Optional[int] = None
staff_roles: List[str] = field(default_factory=list)
staff_users: List[int] = field(default_factory=list)
config_roles: List[str] = field(default_factory=list)
config_users: List[int] = field(default_factory=list)
max_tickets_per_user: int = 5
auto_close_enabled: bool = False
auto_close_days: int = 7
transcript_enabled: bool = True
claim_enabled: bool = True
survey_enabled: bool = True
panel_channel_id: Optional[int] = None
welcome_enabled: bool = True
priority_enabled: bool = True
def to_dict(self) -> Dict[str, Any]:
return {
"guild_id": self.guild_id,
"enabled": self.enabled,
"categories": [c.to_dict() for c in self.categories],
"default_category": self.default_category,
"log_channel_id": self.log_channel_id,
"archive_channel_id": self.archive_channel_id,
"log_channel_id": self.log_channel_id,
"archive_channel_id": self.archive_channel_id,
"staff_roles": self.staff_roles,
"staff_users": self.staff_users,
"config_roles": self.config_roles,
"config_users": self.config_users,
"max_tickets_per_user": self.max_tickets_per_user,
"auto_close_enabled": self.auto_close_enabled,
"auto_close_days": self.auto_close_days,
"transcript_enabled": self.transcript_enabled,
"claim_enabled": self.claim_enabled,
"survey_enabled": self.survey_enabled,
"panel_channel_id": self.panel_channel_id,
"welcome_enabled": self.welcome_enabled,
"priority_enabled": self.priority_enabled
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'GuildTicketConfig':
categories = [TicketCategory.from_dict(c) for c in data.get("categories", [])]
return cls(
guild_id=data.get("guild_id", 0),
enabled=data.get("enabled", True),
categories=categories,
default_category=data.get("default_category"),
log_channel_id=data.get("log_channel_id"),
archive_channel_id=data.get("archive_channel_id"),
staff_roles=data.get("staff_roles", []),
staff_users=data.get("staff_users", []),
config_roles=data.get("config_roles", []),
config_users=data.get("config_users", []),
max_tickets_per_user=data.get("max_tickets_per_user", 5),
auto_close_enabled=data.get("auto_close_enabled", False),
auto_close_days=data.get("auto_close_days", 7),
transcript_enabled=data.get("transcript_enabled", True),
claim_enabled=data.get("claim_enabled", True),
survey_enabled=data.get("survey_enabled", True),
panel_channel_id=data.get("panel_channel_id"),
welcome_enabled=data.get("welcome_enabled", True),
priority_enabled=data.get("priority_enabled", True)
)
def get_category(self, category_id: str) -> Optional[TicketCategory]:
"""Get a category by ID"""
for cat in self.categories:
if cat.id == category_id:
return cat
return None