major-domo-v2/models/custom_command.py
Cal Corum 7b41520054 CLAUDE: Major bot enhancements - Admin commands, player stats, standings, schedules
Major Features Added:
• Admin Management System: Complete admin command suite with user moderation, system control, and bot maintenance tools
• Enhanced Player Commands: Added batting/pitching statistics with concurrent API calls and improved embed design
• League Standings: Full standings system with division grouping, playoff picture, and wild card visualization
• Game Schedules: Comprehensive schedule system with team filtering, series organization, and proper home/away indicators

New Admin Commands (12 total):
• /admin-status, /admin-help, /admin-reload, /admin-sync, /admin-clear
• /admin-announce, /admin-maintenance
• /admin-timeout, /admin-untimeout, /admin-kick, /admin-ban, /admin-unban, /admin-userinfo

Enhanced Player Display:
• Team logo positioned beside player name using embed author
• Smart thumbnail priority: fancycard → headshot → team logo fallback
• Concurrent batting/pitching stats fetching for performance
• Rich statistics display with team colors and comprehensive metrics

New Models & Services:
• BattingStats, PitchingStats, TeamStandings, Division, Game models
• StatsService, StandingsService, ScheduleService for data management
• CustomCommand system with CRUD operations and cleanup tasks

Bot Architecture Improvements:
• Admin commands integrated into bot.py with proper loading
• Permission checks and safety guards for moderation commands
• Enhanced error handling and comprehensive audit logging
• All 227 tests passing with new functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-28 15:32:38 -05:00

236 lines
8.4 KiB
Python

"""
Custom Command models for Discord Bot v2.0
Modern Pydantic models for the custom command system with full type safety.
"""
from datetime import datetime
from typing import Optional, Dict, Any
import re
from pydantic import BaseModel, Field, field_validator
from models.base import SBABaseModel
class CustomCommandCreator(SBABaseModel):
"""Creator of custom commands."""
id: int = Field(..., description="Database ID") # type: ignore
discord_id: int = Field(..., description="Discord user ID")
username: str = Field(..., description="Discord username")
display_name: Optional[str] = Field(None, description="Discord display name")
created_at: datetime = Field(..., description="When creator was first recorded") # type: ignore
total_commands: int = Field(0, description="Total commands created by this user")
active_commands: int = Field(0, description="Currently active commands")
class CustomCommand(SBABaseModel):
"""A custom command created by a user."""
id: int = Field(..., description="Database ID") # type: ignore
name: str = Field(..., description="Command name (unique)")
content: str = Field(..., description="Command response content")
creator_id: int = Field(..., description="ID of the creator")
creator: Optional[CustomCommandCreator] = Field(None, description="Creator details")
# Timestamps
created_at: datetime = Field(..., description="When command was created") # type: ignore
updated_at: Optional[datetime] = Field(None, description="When command was last updated") # type: ignore
last_used: Optional[datetime] = Field(None, description="When command was last executed")
# Usage tracking
use_count: int = Field(0, description="Total times command has been used")
warning_sent: bool = Field(False, description="Whether cleanup warning was sent")
# Metadata
is_active: bool = Field(True, description="Whether command is currently active")
tags: Optional[list[str]] = Field(None, description="Optional tags for categorization")
@field_validator('name')
@classmethod
def validate_name(cls, v):
"""Validate command name."""
if not v or len(v.strip()) == 0:
raise ValueError("Command name cannot be empty")
name = v.strip().lower()
# Length validation
if len(name) < 2:
raise ValueError("Command name must be at least 2 characters")
if len(name) > 32:
raise ValueError("Command name cannot exceed 32 characters")
# Character validation - only allow alphanumeric, dashes, underscores
if not re.match(r'^[a-z0-9_-]+$', name):
raise ValueError("Command name can only contain letters, numbers, dashes, and underscores")
# Reserved names
reserved = {
'help', 'ping', 'info', 'list', 'create', 'delete', 'edit',
'admin', 'mod', 'owner', 'bot', 'system', 'config'
}
if name in reserved:
raise ValueError(f"'{name}' is a reserved command name")
return name.lower()
@field_validator('content')
@classmethod
def validate_content(cls, v):
"""Validate command content."""
if not v or len(v.strip()) == 0:
raise ValueError("Command content cannot be empty")
content = v.strip()
# Length validation
if len(content) > 2000:
raise ValueError("Command content cannot exceed 2000 characters")
# Basic content filtering
prohibited = ['@everyone', '@here']
content_lower = content.lower()
for term in prohibited:
if term in content_lower:
raise ValueError(f"Command content cannot contain '{term}'")
return content
@property
def days_since_last_use(self) -> Optional[int]:
"""Calculate days since last use."""
if not self.last_used:
return None
return (datetime.now() - self.last_used).days
@property
def is_eligible_for_warning(self) -> bool:
"""Check if command is eligible for deletion warning."""
if not self.last_used or self.warning_sent:
return False
return self.days_since_last_use >= 60 # type: ignore
@property
def is_eligible_for_deletion(self) -> bool:
"""Check if command is eligible for deletion."""
if not self.last_used:
return False
return self.days_since_last_use >= 90 # type: ignore
@property
def popularity_score(self) -> float:
"""Calculate popularity score based on usage and recency."""
if self.use_count == 0:
return 0.0
# Base score from usage
base_score = min(self.use_count / 10.0, 10.0) # Max 10 points from usage
# Recency modifier
if self.last_used:
days_ago = self.days_since_last_use
if days_ago <= 7: # type: ignore
recency_modifier = 1.5 # Recent use bonus
elif days_ago <= 30: # type: ignore
recency_modifier = 1.0 # No modifier
elif days_ago <= 60: # type: ignore
recency_modifier = 0.7 # Slight penalty
else:
recency_modifier = 0.3 # Old command penalty
else:
recency_modifier = 0.1 # Never used
return base_score * recency_modifier
class CustomCommandSearchFilters(BaseModel):
"""Filters for searching custom commands."""
name_contains: Optional[str] = None
creator_id: Optional[int] = None
creator_name: Optional[str] = None
min_uses: Optional[int] = None
max_days_unused: Optional[int] = None
has_tags: Optional[list[str]] = None
is_active: bool = True
# Sorting options
sort_by: str = Field('name', description="Sort field: name, created_at, last_used, use_count, popularity")
sort_desc: bool = Field(False, description="Sort in descending order")
# Pagination
page: int = Field(1, description="Page number (1-based)")
page_size: int = Field(25, description="Items per page")
@field_validator('sort_by')
@classmethod
def validate_sort_by(cls, v):
"""Validate sort field."""
valid_sorts = {'name', 'created_at', 'last_used', 'use_count', 'popularity', 'creator'}
if v not in valid_sorts:
raise ValueError(f"sort_by must be one of: {', '.join(valid_sorts)}")
return v
@field_validator('page')
@classmethod
def validate_page(cls, v):
"""Validate page number."""
if v < 1:
raise ValueError("Page number must be >= 1")
return v
@field_validator('page_size')
@classmethod
def validate_page_size(cls, v):
"""Validate page size."""
if v < 1 or v > 100:
raise ValueError("Page size must be between 1 and 100")
return v
class CustomCommandSearchResult(BaseModel):
"""Result of a custom command search."""
commands: list[CustomCommand]
total_count: int
page: int
page_size: int
total_pages: int
has_more: bool
@property
def start_index(self) -> int:
"""Get the starting index for this page."""
return (self.page - 1) * self.page_size + 1
@property
def end_index(self) -> int:
"""Get the ending index for this page."""
return min(self.page * self.page_size, self.total_count)
class CustomCommandStats(BaseModel):
"""Statistics about custom commands."""
total_commands: int
active_commands: int
total_creators: int
total_uses: int
# Usage statistics
most_popular_command: Optional[CustomCommand] = None
most_active_creator: Optional[CustomCommandCreator] = None
recent_commands_count: int = 0 # Commands created in last 7 days
# Cleanup statistics
commands_needing_warning: int = 0
commands_eligible_for_deletion: int = 0
@property
def average_uses_per_command(self) -> float:
"""Calculate average uses per command."""
if self.active_commands == 0:
return 0.0
return self.total_uses / self.active_commands
@property
def average_commands_per_creator(self) -> float:
"""Calculate average commands per creator."""
if self.total_creators == 0:
return 0.0
return self.active_commands / self.total_creators