Première version non terminer du nouveau système de ticket § ajout du système de boutons persistant (non tester)
This commit is contained in:
parent
10173132a2
commit
93e2f9bb44
15 changed files with 3788 additions and 209 deletions
29
commandes/ticket/data/__init__.py
Normal file
29
commandes/ticket/data/__init__.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"""
|
||||
Ticket Data Module
|
||||
==================
|
||||
Data management for the ticket system.
|
||||
"""
|
||||
from .models import (
|
||||
TicketStatus,
|
||||
Priority,
|
||||
TicketCategory,
|
||||
TicketMessage,
|
||||
TicketData,
|
||||
TicketStats,
|
||||
GuildTicketConfig
|
||||
)
|
||||
from .storage import TicketStorage, get_storage, reset_storage
|
||||
|
||||
__all__ = [
|
||||
"TicketStatus",
|
||||
"Priority",
|
||||
"TicketCategory",
|
||||
"TicketMessage",
|
||||
"TicketData",
|
||||
"TicketStats",
|
||||
"GuildTicketConfig",
|
||||
"TicketStorage",
|
||||
"get_storage",
|
||||
"reset_storage",
|
||||
]
|
||||
|
||||
353
commandes/ticket/data/models.py
Normal file
353
commandes/ticket/data/models.py
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
"""
|
||||
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
|
||||
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)
|
||||
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,
|
||||
"staff_roles": self.staff_roles,
|
||||
"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", []),
|
||||
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
|
||||
|
||||
323
commandes/ticket/data/storage.py
Normal file
323
commandes/ticket/data/storage.py
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
"""
|
||||
Ticket Storage Utility
|
||||
======================
|
||||
Modular storage system for ticket data with caching support.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional, Any, List
|
||||
from threading import Lock
|
||||
|
||||
from .models import TicketData, GuildTicketConfig, TicketStatus
|
||||
|
||||
|
||||
class TicketStorage:
|
||||
"""
|
||||
Thread-safe storage manager for ticket data.
|
||||
|
||||
Features:
|
||||
- Automatic file creation
|
||||
- In-memory caching with write-through
|
||||
- Backup creation before writes
|
||||
- Data validation on load
|
||||
"""
|
||||
|
||||
def __init__(self, base_path: str = "data/tickets"):
|
||||
self.base_path = base_path
|
||||
self.config_path = os.path.join(base_path, "config.json")
|
||||
self.tickets_path = os.path.join(base_path, "tickets")
|
||||
self.backup_path = os.path.join(base_path, "backups")
|
||||
self.cache: Dict[str, Any] = {}
|
||||
self.cache_lock = Lock()
|
||||
self._ensure_directories()
|
||||
|
||||
def _ensure_directories(self) -> None:
|
||||
"""Create necessary directories"""
|
||||
for path in [self.base_path, self.tickets_path, self.backup_path]:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
def _create_backup(self, file_path: str) -> str:
|
||||
"""Create a timestamped backup of a file"""
|
||||
if not os.path.exists(file_path):
|
||||
return ""
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = os.path.basename(file_path)
|
||||
backup_file = os.path.join(self.backup_path, f"{timestamp}_{filename}")
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as src:
|
||||
with open(backup_file, 'w', encoding='utf-8') as dst:
|
||||
dst.write(src.read())
|
||||
return backup_file
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _get_cache_key(self, guild_id: int, key_type: str) -> str:
|
||||
"""Generate a cache key"""
|
||||
return f"{guild_id}_{key_type}"
|
||||
|
||||
# --- Config Methods ---
|
||||
|
||||
def load_config(self, guild_id: int) -> GuildTicketConfig:
|
||||
"""Load guild ticket configuration"""
|
||||
cache_key = self._get_cache_key(guild_id, "config")
|
||||
|
||||
with self.cache_lock:
|
||||
if cache_key in self.cache:
|
||||
return self.cache[cache_key]
|
||||
|
||||
try:
|
||||
if os.path.exists(self.config_path):
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
all_configs = json.load(f)
|
||||
|
||||
config_data = all_configs.get(str(guild_id), {})
|
||||
config = GuildTicketConfig.from_dict(config_data)
|
||||
config.guild_id = guild_id
|
||||
else:
|
||||
config = GuildTicketConfig(guild_id=guild_id)
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
print(f"[TicketStorage] Error loading config for guild {guild_id}: {e}")
|
||||
config = GuildTicketConfig(guild_id=guild_id)
|
||||
|
||||
with self.cache_lock:
|
||||
self.cache[cache_key] = config
|
||||
|
||||
return config
|
||||
|
||||
def save_config(self, guild_id: int, config: GuildTicketConfig) -> bool:
|
||||
"""Save guild ticket configuration"""
|
||||
cache_key = self._get_cache_key(guild_id, "config")
|
||||
|
||||
try:
|
||||
# Create backup
|
||||
self._create_backup(self.config_path)
|
||||
|
||||
# Load all configs
|
||||
all_configs = {}
|
||||
if os.path.exists(self.config_path):
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
all_configs = json.load(f)
|
||||
|
||||
# Update config
|
||||
all_configs[str(guild_id)] = config.to_dict()
|
||||
|
||||
# Save
|
||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(all_configs, f, indent=4, ensure_ascii=False)
|
||||
|
||||
# Update cache
|
||||
with self.cache_lock:
|
||||
self.cache[cache_key] = config
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[TicketStorage] Error saving config for guild {guild_id}: {e}")
|
||||
return False
|
||||
|
||||
# --- Ticket Methods ---
|
||||
|
||||
def get_ticket_path(self, channel_id: int) -> str:
|
||||
"""Get the file path for a ticket"""
|
||||
return os.path.join(self.tickets_path, f"{channel_id}.json")
|
||||
|
||||
def load_ticket(self, channel_id: int) -> Optional[TicketData]:
|
||||
"""Load a ticket by channel ID"""
|
||||
cache_key = self._get_cache_key(channel_id, "ticket")
|
||||
|
||||
with self.cache_lock:
|
||||
if cache_key in self.cache:
|
||||
return self.cache[cache_key]
|
||||
|
||||
path = self.get_ticket_path(channel_id)
|
||||
|
||||
try:
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
ticket = TicketData.from_dict(data)
|
||||
else:
|
||||
return None
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
print(f"[TicketStorage] Error loading ticket {channel_id}: {e}")
|
||||
return None
|
||||
|
||||
with self.cache_lock:
|
||||
self.cache[cache_key] = ticket
|
||||
|
||||
return ticket
|
||||
|
||||
def save_ticket(self, ticket: TicketData) -> bool:
|
||||
"""Save a ticket"""
|
||||
cache_key = self._get_cache_key(ticket.channel_id, "ticket")
|
||||
|
||||
try:
|
||||
# Create backup
|
||||
self._create_backup(self.get_ticket_path(ticket.channel_id))
|
||||
|
||||
path = self.get_ticket_path(ticket.channel_id)
|
||||
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
json.dump(ticket.to_dict(), f, indent=4, ensure_ascii=False)
|
||||
|
||||
with self.cache_lock:
|
||||
self.cache[cache_key] = ticket
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[TicketStorage] Error saving ticket {ticket.channel_id}: {e}")
|
||||
return False
|
||||
|
||||
def delete_ticket(self, channel_id: int) -> bool:
|
||||
"""Delete a ticket file"""
|
||||
cache_key = self._get_cache_key(channel_id, "ticket")
|
||||
path = self.get_ticket_path(channel_id)
|
||||
|
||||
try:
|
||||
if os.path.exists(path):
|
||||
# Create backup before delete
|
||||
self._create_backup(path)
|
||||
os.remove(path)
|
||||
|
||||
with self.cache_lock:
|
||||
self.cache.pop(cache_key, None)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[TicketStorage] Error deleting ticket {channel_id}: {e}")
|
||||
return False
|
||||
|
||||
def get_guild_tickets(self, guild_id: int, status: Optional[TicketStatus] = None) -> List[TicketData]:
|
||||
"""Get all tickets for a guild, optionally filtered by status"""
|
||||
tickets = []
|
||||
|
||||
if not os.path.exists(self.tickets_path):
|
||||
return tickets
|
||||
|
||||
for filename in os.listdir(self.tickets_path):
|
||||
if not filename.endswith('.json'):
|
||||
continue
|
||||
|
||||
try:
|
||||
channel_id = int(filename.replace('.json', ''))
|
||||
ticket = self.load_ticket(channel_id)
|
||||
|
||||
if ticket and ticket.guild_id == guild_id:
|
||||
if status is None or ticket.status == status:
|
||||
tickets.append(ticket)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
return tickets
|
||||
|
||||
def get_user_tickets(self, guild_id: int, user_id: int, open_only: bool = True) -> List[TicketData]:
|
||||
"""Get all tickets for a user"""
|
||||
tickets = self.get_guild_tickets(guild_id)
|
||||
user_tickets = [t for t in tickets if t.user_id == user_id]
|
||||
|
||||
if open_only:
|
||||
user_tickets = [t for t in user_tickets if t.is_open]
|
||||
|
||||
return user_tickets
|
||||
|
||||
def count_user_tickets(self, guild_id: int, user_id: int, category_id: Optional[str] = None) -> int:
|
||||
"""Count tickets for a user, optionally in a specific category"""
|
||||
tickets = self.get_user_tickets(guild_id, user_id, open_only=True)
|
||||
|
||||
if category_id:
|
||||
tickets = [t for t in tickets if t.category_id == category_id]
|
||||
|
||||
return len(tickets)
|
||||
|
||||
# --- Bulk Operations ---
|
||||
|
||||
def get_all_guild_ids(self) -> List[int]:
|
||||
"""Get all guild IDs with ticket configurations"""
|
||||
if not os.path.exists(self.config_path):
|
||||
return []
|
||||
|
||||
try:
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
configs = json.load(f)
|
||||
return [int(gid) for gid in configs.keys()]
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
return []
|
||||
|
||||
def cleanup_cache(self, max_size: int = 1000) -> int:
|
||||
"""Clear cache if it exceeds max size"""
|
||||
with self.cache_lock:
|
||||
if len(self.cache) <= max_size:
|
||||
return 0
|
||||
|
||||
# Clear all non-config keys (keep recent)
|
||||
removed = 0
|
||||
keys_to_remove = [k for k in self.cache.keys() if not k.endswith("_config")]
|
||||
|
||||
for key in keys_to_remove[:len(keys_to_remove) - max_size]:
|
||||
del self.cache[key]
|
||||
removed += 1
|
||||
|
||||
return removed
|
||||
|
||||
def invalidate_cache(self, guild_id: Optional[int] = None) -> None:
|
||||
"""Invalidate cache for a guild or all"""
|
||||
with self.cache_lock:
|
||||
if guild_id is None:
|
||||
self.cache.clear()
|
||||
else:
|
||||
keys_to_remove = [k for k in self.cache.keys() if k.startswith(str(guild_id))]
|
||||
for key in keys_to_remove:
|
||||
del self.cache[key]
|
||||
|
||||
# --- Statistics ---
|
||||
|
||||
def get_storage_stats(self) -> Dict[str, Any]:
|
||||
"""Get storage statistics"""
|
||||
stats = {
|
||||
"total_tickets": 0,
|
||||
"total_backups": 0,
|
||||
"config_size_bytes": 0,
|
||||
"cache_size": 0,
|
||||
"tickets_by_status": {}
|
||||
}
|
||||
|
||||
# Count tickets
|
||||
if os.path.exists(self.tickets_path):
|
||||
for filename in os.listdir(self.tickets_path):
|
||||
if filename.endswith('.json'):
|
||||
stats["total_tickets"] += 1
|
||||
|
||||
# Count backups
|
||||
if os.path.exists(self.backup_path):
|
||||
stats["total_backups"] = len(os.listdir(self.backup_path))
|
||||
|
||||
# Config size
|
||||
if os.path.exists(self.config_path):
|
||||
stats["config_size_bytes"] = os.path.getsize(self.config_path)
|
||||
|
||||
# Cache size
|
||||
with self.cache_lock:
|
||||
stats["cache_size"] = len(self.cache)
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_storage_instance: Optional[TicketStorage] = None
|
||||
|
||||
|
||||
def get_storage() -> TicketStorage:
|
||||
"""Get the singleton storage instance"""
|
||||
global _storage_instance
|
||||
if _storage_instance is None:
|
||||
_storage_instance = TicketStorage()
|
||||
return _storage_instance
|
||||
|
||||
|
||||
def reset_storage() -> None:
|
||||
"""Reset the storage instance (for testing)"""
|
||||
global _storage_instance
|
||||
_storage_instance = None
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue