rename all readme files
This commit is contained in:
parent
e8b8878781
commit
216f8112d2
@ -1,555 +0,0 @@
|
|||||||
# Commands Package Documentation
|
|
||||||
**Discord Bot v2.0 - Scalable Command Architecture**
|
|
||||||
|
|
||||||
This document outlines the command architecture, patterns, and best practices established for the SBA Discord Bot v2.0.
|
|
||||||
|
|
||||||
## 📁 Architecture Overview
|
|
||||||
|
|
||||||
### **Package Structure**
|
|
||||||
```
|
|
||||||
commands/
|
|
||||||
├── README.md # This documentation
|
|
||||||
├── __init__.py # Future: Global command utilities
|
|
||||||
└── players/ # Player-related commands
|
|
||||||
├── __init__.py # Package setup with resilient loading
|
|
||||||
└── info.py # Player information commands
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Future Expansion (Phase 2+)**
|
|
||||||
```
|
|
||||||
commands/
|
|
||||||
├── README.md
|
|
||||||
├── __init__.py
|
|
||||||
├── players/ # ✅ COMPLETED
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── info.py # /player command
|
|
||||||
│ ├── search.py # /player-search, /player-lookup
|
|
||||||
│ ├── stats.py # /player-stats, /player-compare
|
|
||||||
│ └── rankings.py # /player-rankings, /leaderboard
|
|
||||||
├── teams/ # 🔄 PLANNED
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── roster.py # /team-roster, /team-depth
|
|
||||||
│ ├── stats.py # /team-stats, /team-leaders
|
|
||||||
│ └── schedule.py # /team-schedule, /team-record
|
|
||||||
├── league/ # 🔄 PLANNED
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── standings.py # /standings, /playoff-race
|
|
||||||
│ ├── schedule.py # /schedule, /scores
|
|
||||||
│ └── leaders.py # /leaders, /awards
|
|
||||||
├── draft/ # 🔄 PLANNED
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── picks.py # /draft-pick, /draft-order
|
|
||||||
│ ├── board.py # /draft-board, /draft-list
|
|
||||||
│ └── timer.py # /draft-status, /draft-timer
|
|
||||||
├── transactions/ # 🔄 PLANNED
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── trades.py # /trade, /trade-history
|
|
||||||
│ ├── waivers.py # /waivers, /free-agents
|
|
||||||
│ └── history.py # /transaction-history
|
|
||||||
├── admin/ # 🔄 PLANNED
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── league.py # /admin-season, /admin-week
|
|
||||||
│ ├── draft.py # /admin-draft, /admin-timer
|
|
||||||
│ └── system.py # /health, /sync-commands
|
|
||||||
└── utils/ # 🔄 PLANNED
|
|
||||||
├── __init__.py
|
|
||||||
├── dice.py # /roll, /dice
|
|
||||||
└── fun.py # Fun/misc commands
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🏗️ Design Principles
|
|
||||||
|
|
||||||
### **1. Single Responsibility**
|
|
||||||
- Each file handles 2-4 closely related commands
|
|
||||||
- Clear logical grouping by domain (players, teams, etc.)
|
|
||||||
- Focused functionality reduces complexity
|
|
||||||
|
|
||||||
### **2. Resilient Loading**
|
|
||||||
- One failed cog doesn't break the entire package
|
|
||||||
- Loop-based loading with comprehensive error handling
|
|
||||||
- Clear logging for debugging and monitoring
|
|
||||||
|
|
||||||
### **3. Scalable Architecture**
|
|
||||||
- Easy to add new packages and cogs
|
|
||||||
- Consistent patterns across all command groups
|
|
||||||
- Future-proof structure for bot growth
|
|
||||||
|
|
||||||
### **4. Modern Discord.py Patterns**
|
|
||||||
- Application commands (slash commands) only
|
|
||||||
- Proper error handling with user-friendly messages
|
|
||||||
- Async/await throughout
|
|
||||||
- Type hints and comprehensive documentation
|
|
||||||
|
|
||||||
## 🔧 Implementation Patterns
|
|
||||||
|
|
||||||
### **Command Package Structure**
|
|
||||||
|
|
||||||
#### **Individual Command File (e.g., `players/info.py`)**
|
|
||||||
```python
|
|
||||||
"""
|
|
||||||
Player Information Commands
|
|
||||||
|
|
||||||
Implements slash commands for displaying player information and statistics.
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
from services.player_service import player_service
|
|
||||||
from exceptions import BotException
|
|
||||||
|
|
||||||
logger = logging.getLogger(f'{__name__}.PlayerInfoCommands')
|
|
||||||
|
|
||||||
|
|
||||||
class PlayerInfoCommands(commands.Cog):
|
|
||||||
"""Player information and statistics command handlers."""
|
|
||||||
|
|
||||||
def __init__(self, bot: commands.Bot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
@discord.app_commands.command(
|
|
||||||
name="player",
|
|
||||||
description="Display player information and statistics"
|
|
||||||
)
|
|
||||||
@discord.app_commands.describe(
|
|
||||||
name="Player name to search for",
|
|
||||||
season="Season to show stats for (defaults to current season)"
|
|
||||||
)
|
|
||||||
async def player_info(
|
|
||||||
self,
|
|
||||||
interaction: discord.Interaction,
|
|
||||||
name: str,
|
|
||||||
season: Optional[int] = None
|
|
||||||
):
|
|
||||||
"""Display player card with statistics."""
|
|
||||||
try:
|
|
||||||
# Always defer for potentially slow API calls
|
|
||||||
await interaction.response.defer()
|
|
||||||
|
|
||||||
# Command implementation here
|
|
||||||
# Use logger for error logging
|
|
||||||
# Create Discord embeds for responses
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Player info command error: {e}", exc_info=True)
|
|
||||||
error_msg = "❌ Error retrieving player information."
|
|
||||||
|
|
||||||
if interaction.response.is_done():
|
|
||||||
await interaction.followup.send(error_msg, ephemeral=True)
|
|
||||||
else:
|
|
||||||
await interaction.response.send_message(error_msg, ephemeral=True)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot: commands.Bot):
|
|
||||||
"""Load the player info commands cog."""
|
|
||||||
await bot.add_cog(PlayerInfoCommands(bot))
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Package __init__.py with Resilient Loading**
|
|
||||||
```python
|
|
||||||
"""
|
|
||||||
Player Commands Package
|
|
||||||
|
|
||||||
This package contains all player-related Discord commands organized into focused modules.
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
from .info import PlayerInfoCommands
|
|
||||||
# Future imports:
|
|
||||||
# from .search import PlayerSearchCommands
|
|
||||||
# from .stats import PlayerStatsCommands
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup_players(bot: commands.Bot):
|
|
||||||
"""
|
|
||||||
Setup all player command modules.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: (successful_count, failed_count, failed_modules)
|
|
||||||
"""
|
|
||||||
# Define all player command cogs to load
|
|
||||||
player_cogs = [
|
|
||||||
("PlayerInfoCommands", PlayerInfoCommands),
|
|
||||||
# Future cogs:
|
|
||||||
# ("PlayerSearchCommands", PlayerSearchCommands),
|
|
||||||
# ("PlayerStatsCommands", PlayerStatsCommands),
|
|
||||||
]
|
|
||||||
|
|
||||||
successful = 0
|
|
||||||
failed = 0
|
|
||||||
failed_modules = []
|
|
||||||
|
|
||||||
for cog_name, cog_class in player_cogs:
|
|
||||||
try:
|
|
||||||
await bot.add_cog(cog_class(bot))
|
|
||||||
logger.info(f"✅ Loaded {cog_name}")
|
|
||||||
successful += 1
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"❌ Failed to load {cog_name}: {e}", exc_info=True)
|
|
||||||
failed += 1
|
|
||||||
failed_modules.append(cog_name)
|
|
||||||
|
|
||||||
# Log summary
|
|
||||||
if failed == 0:
|
|
||||||
logger.info(f"🎉 All {successful} player command modules loaded successfully")
|
|
||||||
else:
|
|
||||||
logger.warning(f"⚠️ Player commands loaded with issues: {successful} successful, {failed} failed")
|
|
||||||
|
|
||||||
return successful, failed, failed_modules
|
|
||||||
|
|
||||||
|
|
||||||
# Export the setup function for easy importing
|
|
||||||
__all__ = ['setup_players', 'PlayerInfoCommands']
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔄 Smart Command Syncing
|
|
||||||
|
|
||||||
### **Hash-Based Change Detection**
|
|
||||||
The bot implements smart command syncing that only updates Discord when commands actually change:
|
|
||||||
|
|
||||||
**Development Mode:**
|
|
||||||
- Automatically detects command changes using SHA-256 hashing
|
|
||||||
- Only syncs when changes are detected
|
|
||||||
- Saves hash to `.last_command_hash` for comparison
|
|
||||||
- Prevents unnecessary Discord API calls
|
|
||||||
|
|
||||||
**Production Mode:**
|
|
||||||
- No automatic syncing
|
|
||||||
- Commands must be manually synced using `/sync` command
|
|
||||||
- Prevents accidental command updates in production
|
|
||||||
|
|
||||||
### **How It Works**
|
|
||||||
1. **Hash Generation**: Creates hash of command names, descriptions, and parameters
|
|
||||||
2. **Comparison**: Compares current hash with stored hash from `.last_command_hash`
|
|
||||||
3. **Conditional Sync**: Only syncs if hashes differ or no previous hash exists
|
|
||||||
4. **Hash Storage**: Saves new hash after successful sync
|
|
||||||
|
|
||||||
### **Benefits**
|
|
||||||
- ✅ **API Efficiency**: Avoids Discord rate limits
|
|
||||||
- ✅ **Development Speed**: Fast restarts when no command changes
|
|
||||||
- ✅ **Production Safety**: No accidental command updates
|
|
||||||
- ✅ **Consistency**: Commands stay consistent across restarts
|
|
||||||
|
|
||||||
## 🚀 Bot Integration
|
|
||||||
|
|
||||||
### **Command Loading in bot.py**
|
|
||||||
```python
|
|
||||||
async def setup_hook(self):
|
|
||||||
"""Called when the bot is starting up."""
|
|
||||||
# Load command packages
|
|
||||||
await self._load_command_packages()
|
|
||||||
|
|
||||||
# Smart command syncing: auto-sync in development if changes detected
|
|
||||||
config = get_config()
|
|
||||||
if config.is_development:
|
|
||||||
if await self._should_sync_commands():
|
|
||||||
self.logger.info("Development mode: changes detected, syncing commands...")
|
|
||||||
await self._sync_commands()
|
|
||||||
await self._save_command_hash()
|
|
||||||
else:
|
|
||||||
self.logger.info("Development mode: no command changes detected, skipping sync")
|
|
||||||
else:
|
|
||||||
self.logger.info("Production mode: commands loaded but not auto-synced")
|
|
||||||
|
|
||||||
async def _load_command_packages(self):
|
|
||||||
"""Load all command packages with resilient error handling."""
|
|
||||||
from commands.players import setup_players
|
|
||||||
|
|
||||||
# Define command packages to load
|
|
||||||
command_packages = [
|
|
||||||
("players", setup_players),
|
|
||||||
# Future packages:
|
|
||||||
# ("teams", setup_teams),
|
|
||||||
# ("league", setup_league),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Loop-based loading with error isolation
|
|
||||||
for package_name, setup_func in command_packages:
|
|
||||||
try:
|
|
||||||
successful, failed, failed_modules = await setup_func(self)
|
|
||||||
# Log results
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"❌ Failed to load {package_name} package: {e}")
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Development Guidelines
|
|
||||||
|
|
||||||
### **Adding New Command Packages**
|
|
||||||
|
|
||||||
#### **1. Create Package Structure**
|
|
||||||
```bash
|
|
||||||
mkdir commands/teams
|
|
||||||
touch commands/teams/__init__.py
|
|
||||||
touch commands/teams/roster.py
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **2. Implement Command Module**
|
|
||||||
- Follow the pattern from `players/info.py`
|
|
||||||
- Use module-level logger: `logger = logging.getLogger(f'{__name__}.ClassName')`
|
|
||||||
- Always defer responses: `await interaction.response.defer()`
|
|
||||||
- Comprehensive error handling with user-friendly messages
|
|
||||||
- Type hints and docstrings
|
|
||||||
|
|
||||||
#### **3. Create Package Setup Function**
|
|
||||||
- Follow the pattern from `players/__init__.py`
|
|
||||||
- Use loop-based cog loading with error isolation
|
|
||||||
- Return tuple: `(successful, failed, failed_modules)`
|
|
||||||
- Comprehensive logging with emojis for quick scanning
|
|
||||||
|
|
||||||
#### **4. Register in Bot**
|
|
||||||
- Add import to `_load_command_packages()` in `bot.py`
|
|
||||||
- Add to `command_packages` list
|
|
||||||
- Test in development environment
|
|
||||||
|
|
||||||
### **Adding Commands to Existing Packages**
|
|
||||||
|
|
||||||
#### **1. Create New Command Module**
|
|
||||||
```python
|
|
||||||
# commands/players/search.py
|
|
||||||
class PlayerSearchCommands(commands.Cog):
|
|
||||||
# Implementation
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def setup(bot: commands.Bot):
|
|
||||||
await bot.add_cog(PlayerSearchCommands(bot))
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **2. Update Package __init__.py**
|
|
||||||
```python
|
|
||||||
from .search import PlayerSearchCommands
|
|
||||||
|
|
||||||
# Add to player_cogs list
|
|
||||||
player_cogs = [
|
|
||||||
("PlayerInfoCommands", PlayerInfoCommands),
|
|
||||||
("PlayerSearchCommands", PlayerSearchCommands), # New cog
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **3. Test Import Structure**
|
|
||||||
```python
|
|
||||||
# Verify imports work
|
|
||||||
from commands.players import setup_players
|
|
||||||
from commands.players.search import PlayerSearchCommands
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 Best Practices
|
|
||||||
|
|
||||||
### **Command Implementation**
|
|
||||||
1. **Always defer responses** for API calls: `await interaction.response.defer()`
|
|
||||||
2. **Use ephemeral responses** for errors: `ephemeral=True`
|
|
||||||
3. **Comprehensive error handling** with try/except blocks
|
|
||||||
4. **User-friendly error messages** with emojis
|
|
||||||
5. **Proper logging** with context and stack traces
|
|
||||||
6. **Type hints** on all parameters and return values
|
|
||||||
7. **Descriptive docstrings** for commands and methods
|
|
||||||
|
|
||||||
### **Package Organization**
|
|
||||||
1. **2-4 commands per file** maximum
|
|
||||||
2. **Logical grouping** by functionality/domain
|
|
||||||
3. **Consistent naming** patterns across packages
|
|
||||||
4. **Module-level logging** for clean, consistent logs
|
|
||||||
5. **Loop-based cog loading** for error resilience
|
|
||||||
6. **Comprehensive return values** from setup functions
|
|
||||||
|
|
||||||
### **Error Handling**
|
|
||||||
1. **Package-level isolation** - one failed cog doesn't break the package
|
|
||||||
2. **Clear error logging** with stack traces for debugging
|
|
||||||
3. **User-friendly messages** that don't expose internal errors
|
|
||||||
4. **Graceful degradation** when possible
|
|
||||||
5. **Metric reporting** for monitoring (success/failure counts)
|
|
||||||
|
|
||||||
## 📊 Monitoring & Metrics
|
|
||||||
|
|
||||||
### **Startup Logging**
|
|
||||||
The command loading system provides comprehensive metrics:
|
|
||||||
|
|
||||||
```
|
|
||||||
INFO - Loading players commands...
|
|
||||||
INFO - ✅ Loaded PlayerInfoCommands
|
|
||||||
INFO - 🎉 All 1 player command modules loaded successfully
|
|
||||||
INFO - ✅ players commands loaded successfully (1 cogs)
|
|
||||||
INFO - 🎉 All command packages loaded successfully (1 total cogs)
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Error Scenarios**
|
|
||||||
```
|
|
||||||
ERROR - ❌ Failed to load PlayerInfoCommands: <error details>
|
|
||||||
WARNING - ⚠️ Player commands loaded with issues: 0 successful, 1 failed
|
|
||||||
WARNING - Failed modules: PlayerInfoCommands
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Command Sync Logging**
|
|
||||||
```
|
|
||||||
INFO - Development mode: changes detected, syncing commands...
|
|
||||||
INFO - Synced 1 commands to guild 123456789
|
|
||||||
```
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
```
|
|
||||||
INFO - Development mode: no command changes detected, skipping sync
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Troubleshooting
|
|
||||||
|
|
||||||
### **Common Issues**
|
|
||||||
|
|
||||||
#### **Import Errors**
|
|
||||||
- Check that `__init__.py` files exist in all packages
|
|
||||||
- Verify cog class names match imports
|
|
||||||
- Ensure service dependencies are available
|
|
||||||
|
|
||||||
#### **Command Not Loading**
|
|
||||||
- Check logs for specific error messages
|
|
||||||
- Verify cog is added to the package's cog list
|
|
||||||
- Test individual module imports in Python REPL
|
|
||||||
|
|
||||||
#### **Commands Not Syncing**
|
|
||||||
- Check if running in development mode (`config.is_development`)
|
|
||||||
- Verify `.last_command_hash` file permissions
|
|
||||||
- Use manual `/sync` command for troubleshooting
|
|
||||||
- Check Discord API rate limits
|
|
||||||
|
|
||||||
#### **Performance Issues**
|
|
||||||
- Monitor command loading times in logs
|
|
||||||
- Check for unnecessary API calls during startup
|
|
||||||
- Verify hash-based sync is working correctly
|
|
||||||
|
|
||||||
### **Debugging Tips**
|
|
||||||
1. **Use the logs** - comprehensive logging shows exactly what's happening
|
|
||||||
2. **Test imports individually** - isolate package/module issues
|
|
||||||
3. **Check hash file** - verify command change detection is working
|
|
||||||
4. **Monitor Discord API** - watch for rate limiting or errors
|
|
||||||
5. **Use development mode** - auto-sync helps debug command issues
|
|
||||||
|
|
||||||
## 📦 Command Groups Pattern
|
|
||||||
|
|
||||||
### **⚠️ CRITICAL: Use `app_commands.Group`, NOT `commands.GroupCog`**
|
|
||||||
|
|
||||||
Discord.py provides two ways to create command groups (e.g., `/injury roll`, `/injury clear`):
|
|
||||||
1. **`app_commands.Group`** ✅ **RECOMMENDED - Use this pattern**
|
|
||||||
2. **`commands.GroupCog`** ❌ **AVOID - Has interaction timing issues**
|
|
||||||
|
|
||||||
### **Why `commands.GroupCog` Fails**
|
|
||||||
|
|
||||||
`commands.GroupCog` has a critical bug that causes **duplicate interaction processing**, leading to:
|
|
||||||
- **404 "Unknown interaction" errors** when trying to defer/respond
|
|
||||||
- **Interaction already acknowledged errors** in error handlers
|
|
||||||
- **Commands fail randomly** even with proper error handling
|
|
||||||
|
|
||||||
**Root Cause:** GroupCog triggers the command handler twice for a single interaction, causing the first execution to consume the interaction token before the second execution can respond.
|
|
||||||
|
|
||||||
### **✅ Correct Pattern: `app_commands.Group`**
|
|
||||||
|
|
||||||
Use the same pattern as `ChartCategoryGroup` and `ChartManageGroup`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from discord import app_commands
|
|
||||||
from discord.ext import commands
|
|
||||||
from utils.decorators import logged_command
|
|
||||||
|
|
||||||
class InjuryGroup(app_commands.Group):
|
|
||||||
"""Injury management command group."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(
|
|
||||||
name="injury",
|
|
||||||
description="Injury management commands"
|
|
||||||
)
|
|
||||||
self.logger = get_contextual_logger(f'{__name__}.InjuryGroup')
|
|
||||||
|
|
||||||
@app_commands.command(name="roll", description="Roll for injury")
|
|
||||||
@logged_command("/injury roll")
|
|
||||||
async def injury_roll(self, interaction: discord.Interaction, player_name: str):
|
|
||||||
"""Roll for injury using player's injury rating."""
|
|
||||||
await interaction.response.defer()
|
|
||||||
|
|
||||||
# Command implementation
|
|
||||||
# No try/catch needed - @logged_command handles it
|
|
||||||
|
|
||||||
async def setup(bot: commands.Bot):
|
|
||||||
"""Setup function for loading the injury commands."""
|
|
||||||
bot.tree.add_command(InjuryGroup())
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Key Differences**
|
|
||||||
|
|
||||||
| Feature | `app_commands.Group` ✅ | `commands.GroupCog` ❌ |
|
|
||||||
|---------|------------------------|------------------------|
|
|
||||||
| **Registration** | `bot.tree.add_command(Group())` | `await bot.add_cog(Cog(bot))` |
|
|
||||||
| **Initialization** | `__init__(self)` no bot param | `__init__(self, bot)` requires bot |
|
|
||||||
| **Decorator Support** | `@logged_command` works perfectly | Causes duplicate execution |
|
|
||||||
| **Interaction Handling** | Single execution, reliable | Duplicate execution, 404 errors |
|
|
||||||
| **Recommended Use** | ✅ All command groups | ❌ Never use |
|
|
||||||
|
|
||||||
### **Migration from GroupCog to Group**
|
|
||||||
|
|
||||||
If you have an existing `commands.GroupCog`, convert it:
|
|
||||||
|
|
||||||
1. **Change class inheritance:**
|
|
||||||
```python
|
|
||||||
# Before
|
|
||||||
class InjuryCog(commands.GroupCog, name="injury"):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# After
|
|
||||||
class InjuryGroup(app_commands.Group):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(name="injury", description="...")
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Update registration:**
|
|
||||||
```python
|
|
||||||
# Before
|
|
||||||
await bot.add_cog(InjuryCog(bot))
|
|
||||||
|
|
||||||
# After
|
|
||||||
bot.tree.add_command(InjuryGroup())
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Remove duplicate interaction checks:**
|
|
||||||
```python
|
|
||||||
# Before (needed for GroupCog bug workaround)
|
|
||||||
if not interaction.response.is_done():
|
|
||||||
await interaction.response.defer()
|
|
||||||
|
|
||||||
# After (clean, simple)
|
|
||||||
await interaction.response.defer()
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Working Examples**
|
|
||||||
|
|
||||||
**Good examples to reference:**
|
|
||||||
- `commands/utilities/charts.py` - `ChartManageGroup` and `ChartCategoryGroup`
|
|
||||||
- `commands/injuries/management.py` - `InjuryGroup`
|
|
||||||
|
|
||||||
Both use `app_commands.Group` successfully with `@logged_command` decorators.
|
|
||||||
|
|
||||||
## 🚦 Future Enhancements
|
|
||||||
|
|
||||||
### **Planned Features**
|
|
||||||
- **Permission Decorators**: Role-based command restrictions per package
|
|
||||||
- **Dynamic Loading**: Hot-reload commands without bot restart
|
|
||||||
- **Usage Metrics**: Command usage tracking and analytics
|
|
||||||
- **Rate Limiting**: Per-command rate limiting for resource management
|
|
||||||
|
|
||||||
### **Architecture Improvements**
|
|
||||||
- **Shared Utilities**: Common embed builders, decorators, helpers
|
|
||||||
- **Configuration**: Per-package configuration and feature flags
|
|
||||||
- **Testing**: Automated testing for command packages
|
|
||||||
- **Documentation**: Auto-generated command documentation
|
|
||||||
- **Monitoring**: Health checks and performance metrics per package
|
|
||||||
|
|
||||||
This architecture provides a solid foundation for scaling the Discord bot while maintaining code quality, reliability, and developer productivity.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated:** Phase 2.1 - Command Package Conversion
|
|
||||||
**Next Review:** After implementing teams/ and league/ packages
|
|
||||||
@ -1,433 +0,0 @@
|
|||||||
# Help Commands System
|
|
||||||
|
|
||||||
**Last Updated:** January 2025
|
|
||||||
**Status:** ✅ Fully Implemented
|
|
||||||
**Location:** `commands/help/`
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Help Commands System provides a comprehensive, admin-managed help system for the Discord server. Administrators and designated "Help Editors" can create, edit, and manage custom help topics covering league documentation, resources, FAQs, links, and guides. This system replaces the originally planned `/links` command with a more flexible and powerful solution.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
### `/help [topic]`
|
|
||||||
**Description:** View a help topic or list all available topics
|
|
||||||
**Parameters:**
|
|
||||||
- `topic` (optional): Name of the help topic to view
|
|
||||||
|
|
||||||
**Behavior:**
|
|
||||||
- **With topic**: Displays the specified help topic with formatted content
|
|
||||||
- **Without topic**: Shows a paginated list of all available help topics organized by category
|
|
||||||
- Automatically increments view count when a topic is viewed
|
|
||||||
|
|
||||||
**Permissions:** Available to all server members
|
|
||||||
|
|
||||||
**Examples:**
|
|
||||||
```
|
|
||||||
/help trading-rules
|
|
||||||
/help
|
|
||||||
```
|
|
||||||
|
|
||||||
### `/help-create`
|
|
||||||
**Description:** Create a new help topic
|
|
||||||
**Permissions:** Administrators + "Help Editor" role
|
|
||||||
|
|
||||||
**Modal Fields:**
|
|
||||||
- **Topic Name**: URL-safe name (2-32 chars, letters/numbers/dashes only)
|
|
||||||
- **Display Title**: Human-readable title (1-200 chars)
|
|
||||||
- **Category**: Optional category (rules/guides/resources/info/faq)
|
|
||||||
- **Content**: Help content with markdown support (1-4000 chars)
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Real-time validation of all fields
|
|
||||||
- Preview before final creation
|
|
||||||
- Automatic duplicate detection
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```
|
|
||||||
Topic Name: trading-rules
|
|
||||||
Display Title: Trading Rules & Guidelines
|
|
||||||
Category: rules
|
|
||||||
Content: [Detailed trading rules with markdown formatting]
|
|
||||||
```
|
|
||||||
|
|
||||||
### `/help-edit <topic>`
|
|
||||||
**Description:** Edit an existing help topic
|
|
||||||
**Parameters:**
|
|
||||||
- `topic` (required): Name of the help topic to edit
|
|
||||||
|
|
||||||
**Permissions:** Administrators + "Help Editor" role
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Pre-populated modal with current values
|
|
||||||
- Shows preview of changes before saving
|
|
||||||
- Tracks last editor and update timestamp
|
|
||||||
- Autocomplete for topic names
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```
|
|
||||||
/help-edit trading-rules
|
|
||||||
```
|
|
||||||
|
|
||||||
### `/help-delete <topic>`
|
|
||||||
**Description:** Delete a help topic (soft delete)
|
|
||||||
**Parameters:**
|
|
||||||
- `topic` (required): Name of the help topic to delete
|
|
||||||
|
|
||||||
**Permissions:** Administrators + "Help Editor" role
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Confirmation dialog before deletion
|
|
||||||
- Shows topic statistics (view count)
|
|
||||||
- Soft delete (can be restored later)
|
|
||||||
- Autocomplete for topic names
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```
|
|
||||||
/help-delete trading-rules
|
|
||||||
```
|
|
||||||
|
|
||||||
### `/help-list [category] [show_deleted]`
|
|
||||||
**Description:** Browse all help topics
|
|
||||||
**Parameters:**
|
|
||||||
- `category` (optional): Filter by category
|
|
||||||
- `show_deleted` (optional): Show soft-deleted topics (admin only)
|
|
||||||
|
|
||||||
**Permissions:** Available to all (show_deleted requires admin/help editor)
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Organized display by category
|
|
||||||
- Shows view counts
|
|
||||||
- Paginated interface for many topics
|
|
||||||
- Filtered views by category
|
|
||||||
|
|
||||||
**Examples:**
|
|
||||||
```
|
|
||||||
/help-list
|
|
||||||
/help-list category:rules
|
|
||||||
/help-list show_deleted:true
|
|
||||||
```
|
|
||||||
|
|
||||||
## Permission System
|
|
||||||
|
|
||||||
### Roles with Help Edit Permissions
|
|
||||||
1. **Server Administrators** - Full access to all help commands
|
|
||||||
2. **Help Editor Role** - Designated role with editing permissions
|
|
||||||
- Role name: "Help Editor" (configurable in `constants.py`)
|
|
||||||
- Can create, edit, and delete help topics
|
|
||||||
- Cannot view deleted topics unless also admin
|
|
||||||
|
|
||||||
### Permission Checks
|
|
||||||
```python
|
|
||||||
def has_help_edit_permission(interaction: discord.Interaction) -> bool:
|
|
||||||
"""Check if user can edit help commands."""
|
|
||||||
# Admin check
|
|
||||||
if interaction.user.guild_permissions.administrator:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Help Editor role check
|
|
||||||
role = discord.utils.get(interaction.guild.roles, name=HELP_EDITOR_ROLE_NAME)
|
|
||||||
if role and role in interaction.user.roles:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Components
|
|
||||||
|
|
||||||
**Models** (`models/help_command.py`):
|
|
||||||
- `HelpCommand`: Main data model with validation
|
|
||||||
- `HelpCommandSearchFilters`: Search/filtering parameters
|
|
||||||
- `HelpCommandSearchResult`: Paginated search results
|
|
||||||
- `HelpCommandStats`: Statistics and analytics
|
|
||||||
|
|
||||||
**Service Layer** (`services/help_commands_service.py`):
|
|
||||||
- `HelpCommandsService`: CRUD operations and business logic
|
|
||||||
- `help_commands_service`: Global service instance
|
|
||||||
- Integrates with BaseService for API calls
|
|
||||||
|
|
||||||
**Views** (`views/help_commands.py`):
|
|
||||||
- `HelpCommandCreateModal`: Interactive creation modal
|
|
||||||
- `HelpCommandEditModal`: Interactive editing modal
|
|
||||||
- `HelpCommandDeleteConfirmView`: Deletion confirmation
|
|
||||||
- `HelpCommandListView`: Paginated topic browser
|
|
||||||
- `create_help_topic_embed()`: Formatted topic display
|
|
||||||
|
|
||||||
**Commands** (`commands/help/main.py`):
|
|
||||||
- `HelpCommands`: Cog with all command handlers
|
|
||||||
- Permission checking integration
|
|
||||||
- Autocomplete for topic names
|
|
||||||
- Error handling and user feedback
|
|
||||||
|
|
||||||
### Data Flow
|
|
||||||
|
|
||||||
1. **User Interaction** → Discord slash command
|
|
||||||
2. **Permission Check** → Validate user permissions
|
|
||||||
3. **Modal Display** → Interactive data input (for create/edit)
|
|
||||||
4. **Service Call** → Business logic and validation
|
|
||||||
5. **API Request** → Database operations via API
|
|
||||||
6. **Response** → Formatted embed with success/error message
|
|
||||||
|
|
||||||
### Database Integration
|
|
||||||
|
|
||||||
**API Endpoints** (via `../database/app/routers_v3/help_commands.py`):
|
|
||||||
- `GET /api/v3/help_commands` - List with filters
|
|
||||||
- `GET /api/v3/help_commands/{id}` - Get by ID
|
|
||||||
- `GET /api/v3/help_commands/by_name/{name}` - Get by name
|
|
||||||
- `POST /api/v3/help_commands` - Create
|
|
||||||
- `PUT /api/v3/help_commands/{id}` - Update
|
|
||||||
- `DELETE /api/v3/help_commands/{id}` - Soft delete
|
|
||||||
- `PATCH /api/v3/help_commands/{id}/restore` - Restore
|
|
||||||
- `PATCH /api/v3/help_commands/by_name/{name}/view` - Increment views
|
|
||||||
- `GET /api/v3/help_commands/autocomplete` - Autocomplete
|
|
||||||
- `GET /api/v3/help_commands/stats` - Statistics
|
|
||||||
|
|
||||||
**Database Table** (`help_commands`):
|
|
||||||
```sql
|
|
||||||
CREATE TABLE help_commands (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name TEXT UNIQUE NOT NULL,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
category TEXT,
|
|
||||||
created_by_discord_id BIGINT NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMP,
|
|
||||||
last_modified_by BIGINT,
|
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
|
||||||
view_count INTEGER DEFAULT 0,
|
|
||||||
display_order INTEGER DEFAULT 0
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### Soft Delete
|
|
||||||
- Topics are never permanently deleted from the database
|
|
||||||
- `is_active` flag controls visibility
|
|
||||||
- Admins can restore deleted topics (future enhancement)
|
|
||||||
- Full audit trail preserved
|
|
||||||
|
|
||||||
### View Tracking
|
|
||||||
- Automatic view count increment when topics are accessed
|
|
||||||
- Statistics available via API
|
|
||||||
- Most viewed topics tracked
|
|
||||||
|
|
||||||
### Category Organization
|
|
||||||
- Optional categorization of topics
|
|
||||||
- Suggested categories:
|
|
||||||
- `rules` - League rules and regulations
|
|
||||||
- `guides` - How-to guides and tutorials
|
|
||||||
- `resources` - Links to external resources
|
|
||||||
- `info` - General league information
|
|
||||||
- `faq` - Frequently asked questions
|
|
||||||
|
|
||||||
### Markdown Support
|
|
||||||
- Full markdown formatting in content
|
|
||||||
- Support for:
|
|
||||||
- Headers
|
|
||||||
- Bold/italic text
|
|
||||||
- Lists (ordered and unordered)
|
|
||||||
- Links
|
|
||||||
- Code blocks
|
|
||||||
- Blockquotes
|
|
||||||
|
|
||||||
### Autocomplete
|
|
||||||
- Fast topic name suggestions
|
|
||||||
- Searches across names and titles
|
|
||||||
- Limited to 25 suggestions for performance
|
|
||||||
|
|
||||||
## Use Cases
|
|
||||||
|
|
||||||
### Example Help Topics
|
|
||||||
|
|
||||||
**Trading Rules** (`/help trading-rules`):
|
|
||||||
```markdown
|
|
||||||
# Trading Rules & Guidelines
|
|
||||||
|
|
||||||
## Trade Deadline
|
|
||||||
All trades must be completed by Week 15 of the regular season.
|
|
||||||
|
|
||||||
## Restrictions
|
|
||||||
- Maximum 3 trades per team per season
|
|
||||||
- All trades must be approved by league commissioner
|
|
||||||
- No trading draft picks beyond 2 seasons ahead
|
|
||||||
|
|
||||||
## How to Propose a Trade
|
|
||||||
Use the `/trade` command to propose a trade. Both teams must accept before the trade is processed.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Discord Links** (`/help links`):
|
|
||||||
```markdown
|
|
||||||
# Important League Links
|
|
||||||
|
|
||||||
## Website
|
|
||||||
https://sba-league.com
|
|
||||||
|
|
||||||
## Google Sheet
|
|
||||||
https://docs.google.com/spreadsheets/...
|
|
||||||
|
|
||||||
## Discord Invite
|
|
||||||
https://discord.gg/...
|
|
||||||
|
|
||||||
## Rules Document
|
|
||||||
https://docs.google.com/document/...
|
|
||||||
```
|
|
||||||
|
|
||||||
**How to Trade** (`/help how-to-trade`):
|
|
||||||
```markdown
|
|
||||||
# How to Use the Trade System
|
|
||||||
|
|
||||||
1. Type `/trade` to start a new trade proposal
|
|
||||||
2. Select the team you want to trade with
|
|
||||||
3. Add players/picks to the trade
|
|
||||||
4. Submit for review
|
|
||||||
5. Both teams must accept
|
|
||||||
6. Commissioner approves
|
|
||||||
7. Trade is processed!
|
|
||||||
|
|
||||||
For more information, see `/help trading-rules`
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
### Common Errors
|
|
||||||
|
|
||||||
**Topic Not Found**:
|
|
||||||
```
|
|
||||||
❌ Topic Not Found
|
|
||||||
No help topic named 'xyz' exists.
|
|
||||||
Use /help to see available topics.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Permission Denied**:
|
|
||||||
```
|
|
||||||
❌ Permission Denied
|
|
||||||
Only administrators and users with the Help Editor role can create help topics.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Topic Already Exists**:
|
|
||||||
```
|
|
||||||
❌ Topic Already Exists
|
|
||||||
A help topic named 'trading-rules' already exists.
|
|
||||||
Try a different name.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Validation Errors**:
|
|
||||||
- Topic name too short/long
|
|
||||||
- Invalid characters in topic name
|
|
||||||
- Content too long (>4000 chars)
|
|
||||||
- Title too long (>200 chars)
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### For Administrators
|
|
||||||
|
|
||||||
1. **Use Clear Topic Names**
|
|
||||||
- Use lowercase with hyphens: `trading-rules`, `how-to-draft`
|
|
||||||
- Keep names short but descriptive
|
|
||||||
- Avoid special characters
|
|
||||||
|
|
||||||
2. **Organize by Category**
|
|
||||||
- Consistent category naming
|
|
||||||
- Group related topics together
|
|
||||||
- Use standard categories (rules, guides, resources, info, faq)
|
|
||||||
|
|
||||||
3. **Write Clear Content**
|
|
||||||
- Use markdown formatting for readability
|
|
||||||
- Keep content concise and focused
|
|
||||||
- Link to related topics when appropriate
|
|
||||||
- Update regularly to keep information current
|
|
||||||
|
|
||||||
4. **Monitor Usage**
|
|
||||||
- Check view counts to see popular topics
|
|
||||||
- Update frequently accessed topics
|
|
||||||
- Archive outdated information
|
|
||||||
|
|
||||||
### For Users
|
|
||||||
|
|
||||||
1. **Browse Topics**
|
|
||||||
- Use `/help` to see all available topics
|
|
||||||
- Use `/help-list` to browse by category
|
|
||||||
- Use autocomplete to find topics quickly
|
|
||||||
|
|
||||||
2. **Request New Topics**
|
|
||||||
- Contact admins or help editors
|
|
||||||
- Suggest topics that would be useful
|
|
||||||
- Provide draft content if possible
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Test Coverage
|
|
||||||
- ✅ Model validation tests
|
|
||||||
- ✅ Service layer CRUD operations
|
|
||||||
- ✅ Permission checking
|
|
||||||
- ✅ Autocomplete functionality
|
|
||||||
- ✅ Soft delete behavior
|
|
||||||
- ✅ View count incrementing
|
|
||||||
|
|
||||||
### Test Files
|
|
||||||
- `tests/test_models_help_command.py`
|
|
||||||
- `tests/test_services_help_commands.py`
|
|
||||||
- `tests/test_commands_help.py`
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
### Planned Features (Post-Launch)
|
|
||||||
- Restore command for deleted topics (`/help-restore <topic>`)
|
|
||||||
- Statistics dashboard (`/help-stats`)
|
|
||||||
- Search functionality across all content
|
|
||||||
- Topic versioning and change history
|
|
||||||
- Attachments support (images, files)
|
|
||||||
- Related topics linking
|
|
||||||
- User feedback and ratings
|
|
||||||
- Full-text search in content
|
|
||||||
|
|
||||||
### Potential Improvements
|
|
||||||
- Rich embed support with custom colors
|
|
||||||
- Topic aliases (multiple names for same topic)
|
|
||||||
- Scheduled topic updates
|
|
||||||
- Topic templates for common formats
|
|
||||||
- Import/export functionality
|
|
||||||
- Bulk operations for admins
|
|
||||||
|
|
||||||
## Migration from Legacy System
|
|
||||||
|
|
||||||
If migrating from an older help/links system:
|
|
||||||
|
|
||||||
1. **Export existing content** from old system
|
|
||||||
2. **Create help topics** using `/help-create`
|
|
||||||
3. **Test all topics** for formatting and accuracy
|
|
||||||
4. **Update documentation** to reference new commands
|
|
||||||
5. **Train help editors** on new system
|
|
||||||
6. **Announce to users** with usage instructions
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
### For Users
|
|
||||||
- Use `/help` to browse available topics
|
|
||||||
- Contact server admins for topic requests
|
|
||||||
- Report broken links or outdated information
|
|
||||||
|
|
||||||
### For Administrators
|
|
||||||
- Review the implementation plan in `.claude/HELP_COMMANDS_PLAN.md`
|
|
||||||
- Check database migration docs in `.claude/DATABASE_MIGRATION_HELP_COMMANDS.md`
|
|
||||||
- See main project documentation in `CLAUDE.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Implementation Details:**
|
|
||||||
- **Models:** `models/help_command.py`
|
|
||||||
- **Service:** `services/help_commands_service.py`
|
|
||||||
- **Views:** `views/help_commands.py`
|
|
||||||
- **Commands:** `commands/help/main.py`
|
|
||||||
- **Constants:** `constants.py` (HELP_EDITOR_ROLE_NAME)
|
|
||||||
- **Tests:** `tests/test_*_help*.py`
|
|
||||||
|
|
||||||
**Related Documentation:**
|
|
||||||
- Implementation Plan: `.claude/HELP_COMMANDS_PLAN.md`
|
|
||||||
- Database Migration: `.claude/DATABASE_MIGRATION_HELP_COMMANDS.md`
|
|
||||||
- Project Overview: `CLAUDE.md`
|
|
||||||
- Roadmap: `PRE_LAUNCH_ROADMAP.md`
|
|
||||||
@ -1,494 +0,0 @@
|
|||||||
# Injury Commands
|
|
||||||
|
|
||||||
**Command Group:** `/injury`
|
|
||||||
**Permission Required:** SBA Players role (for set-new and clear)
|
|
||||||
**Subcommands:** roll, set-new, clear
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The injury command family provides comprehensive player injury management for the SBA league. Team managers can roll for injuries using official Strat-o-Matic injury tables, record confirmed injuries, and clear injuries when players return.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
### `/injury roll`
|
|
||||||
|
|
||||||
Roll for injury based on a player's injury rating using 3d6 dice and official injury tables.
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
```
|
|
||||||
/injury roll <player_name>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Parameters:**
|
|
||||||
- `player_name` (required, autocomplete): Name of the player - uses smart autocomplete prioritizing your team's players
|
|
||||||
|
|
||||||
**Injury Rating Format:**
|
|
||||||
The player's `injury_rating` field contains both the games played and rating in format `#p##`:
|
|
||||||
- **Format**: `1p70`, `4p50`, `2p65`, etc.
|
|
||||||
- **First character**: Games played in current series (1-6)
|
|
||||||
- **Remaining characters**: Injury rating (p70, p65, p60, p50, p40, p30, p20)
|
|
||||||
|
|
||||||
**Examples:**
|
|
||||||
- `1p70` = 1 game played, p70 rating
|
|
||||||
- `4p50` = 4 games played, p50 rating
|
|
||||||
- `2p65` = 2 games played, p65 rating
|
|
||||||
|
|
||||||
**Dice Roll:**
|
|
||||||
- Rolls 3d6 (3-18 range)
|
|
||||||
- Automatically extracts games played and rating from player's injury_rating field
|
|
||||||
- Looks up result in official Strat-o-Matic injury tables
|
|
||||||
- Returns injury duration based on rating and games played
|
|
||||||
|
|
||||||
**Possible Results:**
|
|
||||||
- **OK**: No injury
|
|
||||||
- **REM**: Remainder of game (batters) or Fatigued (pitchers)
|
|
||||||
- **Number**: Games player will miss (1-24 games)
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```
|
|
||||||
/injury roll Mike Trout
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response Fields:**
|
|
||||||
- **Roll**: Total rolled and individual dice (e.g., "15 (3d6: 5 + 5 + 5)")
|
|
||||||
- **Player**: Player name and position
|
|
||||||
- **Injury Rating**: Full rating with parsed details (e.g., "4p50 (p50, 4 games)")
|
|
||||||
- **Result**: Injury outcome (OK, REM, or number of games)
|
|
||||||
- **Team**: Player's current team
|
|
||||||
|
|
||||||
**Response Colors:**
|
|
||||||
- **Green**: OK (no injury)
|
|
||||||
- **Gold**: REM (remainder of game/fatigued)
|
|
||||||
- **Orange**: Number of games (injury occurred)
|
|
||||||
|
|
||||||
**Error Handling:**
|
|
||||||
If a player's `injury_rating` is not in the correct format, an error message will be displayed:
|
|
||||||
```
|
|
||||||
Invalid Injury Rating Format
|
|
||||||
{Player} has an invalid injury rating: `{rating}`
|
|
||||||
|
|
||||||
Expected format: #p## (e.g., 1p70, 4p50)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `/injury set-new`
|
|
||||||
|
|
||||||
Record a new injury for a player on your team.
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
```
|
|
||||||
/injury set-new <player_name> <this_week> <this_game> <injury_games>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Parameters:**
|
|
||||||
- `player_name` (required): Name of the player to injure
|
|
||||||
- `this_week` (required): Current week number
|
|
||||||
- `this_game` (required): Current game number (1-4)
|
|
||||||
- `injury_games` (required): Total number of games player will be out
|
|
||||||
|
|
||||||
**Validation:**
|
|
||||||
- Player must exist in current season
|
|
||||||
- Player cannot already have an active injury
|
|
||||||
- Game number must be between 1 and 4
|
|
||||||
- Injury duration must be at least 1 game
|
|
||||||
|
|
||||||
**Automatic Calculations:**
|
|
||||||
The command automatically calculates:
|
|
||||||
1. Injury start date (adjusts for game 4 edge case)
|
|
||||||
2. Return date based on injury duration
|
|
||||||
3. Week rollover when games exceed 4 per week
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```
|
|
||||||
/injury set-new Mike Trout 5 2 4
|
|
||||||
```
|
|
||||||
This records an injury occurring in week 5, game 2, with player out for 4 games (returns week 6, game 2).
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
- Confirmation embed with injury details
|
|
||||||
- Player's name, position, and team
|
|
||||||
- Total games missed
|
|
||||||
- Calculated return date
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `/injury clear`
|
|
||||||
|
|
||||||
Clear a player's active injury and mark them as eligible to play.
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
```
|
|
||||||
/injury clear <player_name>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Parameters:**
|
|
||||||
- `player_name` (required): Name of the player whose injury to clear
|
|
||||||
|
|
||||||
**Validation:**
|
|
||||||
- Player must exist in current season
|
|
||||||
- Player must have an active injury
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```
|
|
||||||
/injury clear Mike Trout
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
- Confirmation that injury was cleared
|
|
||||||
- Shows previous return date
|
|
||||||
- Shows total games that were missed
|
|
||||||
- Player's team information
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Date Format
|
|
||||||
|
|
||||||
All injury dates use the format `w##g#`:
|
|
||||||
- `w##` = Week number (zero-padded to 2 digits)
|
|
||||||
- `g#` = Game number (1-4)
|
|
||||||
|
|
||||||
**Examples:**
|
|
||||||
- `w05g2` = Week 5, Game 2
|
|
||||||
- `w12g4` = Week 12, Game 4
|
|
||||||
- `w01g1` = Week 1, Game 1
|
|
||||||
|
|
||||||
## Injury Calculation Logic
|
|
||||||
|
|
||||||
### Basic Calculation
|
|
||||||
|
|
||||||
For an injury of N games starting at week W, game G:
|
|
||||||
|
|
||||||
1. **Calculate weeks and remaining games:**
|
|
||||||
```
|
|
||||||
out_weeks = floor(N / 4)
|
|
||||||
out_games = N % 4
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Calculate return date:**
|
|
||||||
```
|
|
||||||
return_week = W + out_weeks
|
|
||||||
return_game = G + 1 + out_games
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Handle week rollover:**
|
|
||||||
```
|
|
||||||
if return_game > 4:
|
|
||||||
return_week += 1
|
|
||||||
return_game -= 4
|
|
||||||
```
|
|
||||||
|
|
||||||
### Special Cases
|
|
||||||
|
|
||||||
#### Game 4 Edge Case
|
|
||||||
If injury occurs during game 4, the start date is adjusted:
|
|
||||||
```
|
|
||||||
start_week = W + 1
|
|
||||||
start_game = 1
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Examples
|
|
||||||
|
|
||||||
**Example 1: Simple injury (same week)**
|
|
||||||
- Current: Week 5, Game 1
|
|
||||||
- Injury: 2 games
|
|
||||||
- Return: Week 5, Game 4
|
|
||||||
|
|
||||||
**Example 2: Week rollover**
|
|
||||||
- Current: Week 5, Game 3
|
|
||||||
- Injury: 3 games
|
|
||||||
- Return: Week 6, Game 3
|
|
||||||
|
|
||||||
**Example 3: Multi-week injury**
|
|
||||||
- Current: Week 5, Game 2
|
|
||||||
- Injury: 8 games
|
|
||||||
- Return: Week 7, Game 3
|
|
||||||
|
|
||||||
**Example 4: Game 4 start**
|
|
||||||
- Current: Week 5, Game 4
|
|
||||||
- Injury: 2 games
|
|
||||||
- Start: Week 6, Game 1
|
|
||||||
- Return: Week 6, Game 3
|
|
||||||
|
|
||||||
## Database Schema
|
|
||||||
|
|
||||||
### Injury Model
|
|
||||||
|
|
||||||
```python
|
|
||||||
class Injury(SBABaseModel):
|
|
||||||
id: int # Injury ID
|
|
||||||
season: int # Season number
|
|
||||||
player_id: int # Player ID
|
|
||||||
total_games: int # Total games player will be out
|
|
||||||
start_week: int # Week injury started
|
|
||||||
start_game: int # Game number injury started (1-4)
|
|
||||||
end_week: int # Week player returns
|
|
||||||
end_game: int # Game number player returns (1-4)
|
|
||||||
is_active: bool # Whether injury is currently active
|
|
||||||
```
|
|
||||||
|
|
||||||
### API Integration
|
|
||||||
|
|
||||||
The commands interact with the following API endpoints:
|
|
||||||
|
|
||||||
- `GET /api/v3/injuries` - Query injuries with filters
|
|
||||||
- `POST /api/v3/injuries` - Create new injury record
|
|
||||||
- `PATCH /api/v3/injuries/{id}` - Update injury (clear active status)
|
|
||||||
- `PATCH /api/v3/players/{id}` - Update player's il_return field
|
|
||||||
|
|
||||||
## Service Layer
|
|
||||||
|
|
||||||
### InjuryService
|
|
||||||
|
|
||||||
**Location:** `services/injury_service.py`
|
|
||||||
|
|
||||||
**Key Methods:**
|
|
||||||
- `get_active_injury(player_id, season)` - Get active injury for player
|
|
||||||
- `get_injuries_by_player(player_id, season, active_only)` - Get all injuries for player
|
|
||||||
- `get_injuries_by_team(team_id, season, active_only)` - Get team injuries
|
|
||||||
- `create_injury(...)` - Create new injury record
|
|
||||||
- `clear_injury(injury_id)` - Deactivate injury
|
|
||||||
|
|
||||||
## Permissions
|
|
||||||
|
|
||||||
### Required Roles
|
|
||||||
|
|
||||||
**For `/injury check`:**
|
|
||||||
- No role required (available to all users)
|
|
||||||
|
|
||||||
**For `/injury set-new` and `/injury clear`:**
|
|
||||||
- **SBA Players** role required
|
|
||||||
- Configured via `SBA_PLAYERS_ROLE_NAME` environment variable
|
|
||||||
|
|
||||||
### Permission Checks
|
|
||||||
|
|
||||||
The commands use `has_player_role()` method to verify user has appropriate role:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def has_player_role(self, interaction: discord.Interaction) -> bool:
|
|
||||||
"""Check if user has the SBA Players role."""
|
|
||||||
player_role = discord.utils.get(
|
|
||||||
interaction.guild.roles,
|
|
||||||
name=get_config().sba_players_role_name
|
|
||||||
)
|
|
||||||
return player_role in interaction.user.roles if player_role else False
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
### Common Errors
|
|
||||||
|
|
||||||
**Player Not Found:**
|
|
||||||
```
|
|
||||||
❌ Player Not Found
|
|
||||||
I did not find anybody named **{player_name}**.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Already Injured:**
|
|
||||||
```
|
|
||||||
❌ Already Injured
|
|
||||||
Hm. It looks like {player_name} is already hurt.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Not Injured:**
|
|
||||||
```
|
|
||||||
❌ No Active Injury
|
|
||||||
{player_name} isn't injured.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Invalid Input:**
|
|
||||||
```
|
|
||||||
❌ Invalid Input
|
|
||||||
Game number must be between 1 and 4.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Permission Denied:**
|
|
||||||
```
|
|
||||||
❌ Permission Denied
|
|
||||||
This command requires the **SBA Players** role.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logging
|
|
||||||
|
|
||||||
All injury commands use the `@logged_command` decorator for automatic logging:
|
|
||||||
|
|
||||||
```python
|
|
||||||
@app_commands.command(name="check")
|
|
||||||
@logged_command("/injury check")
|
|
||||||
async def injury_check(self, interaction, player_name: str):
|
|
||||||
# Command implementation
|
|
||||||
```
|
|
||||||
|
|
||||||
**Log Context:**
|
|
||||||
- Command name
|
|
||||||
- User ID and username
|
|
||||||
- Player name
|
|
||||||
- Season
|
|
||||||
- Injury details (duration, dates)
|
|
||||||
- Success/failure status
|
|
||||||
|
|
||||||
**Example Log:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"level": "INFO",
|
|
||||||
"command": "/injury set-new",
|
|
||||||
"user_id": "123456789",
|
|
||||||
"player_name": "Mike Trout",
|
|
||||||
"season": 12,
|
|
||||||
"injury_games": 4,
|
|
||||||
"return_date": "w06g2",
|
|
||||||
"message": "Injury set for Mike Trout"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Test Coverage
|
|
||||||
|
|
||||||
**Location:** `tests/test_services_injury.py`
|
|
||||||
|
|
||||||
**Test Categories:**
|
|
||||||
1. **Model Tests** (5 tests) - Injury model creation and properties
|
|
||||||
2. **Service Tests** (8 tests) - InjuryService CRUD operations with API mocking
|
|
||||||
3. **Roll Logic Tests** (8 tests) - Injury rating parsing, table lookup, and dice roll logic
|
|
||||||
4. **Calculation Tests** (5 tests) - Date calculation logic for injury duration
|
|
||||||
|
|
||||||
**Total:** 26 comprehensive tests
|
|
||||||
|
|
||||||
**Running Tests:**
|
|
||||||
```bash
|
|
||||||
# Run all injury tests
|
|
||||||
python -m pytest tests/test_services_injury.py -v
|
|
||||||
|
|
||||||
# Run specific test class
|
|
||||||
python -m pytest tests/test_services_injury.py::TestInjuryService -v
|
|
||||||
python -m pytest tests/test_services_injury.py::TestInjuryRollLogic -v
|
|
||||||
|
|
||||||
# Run with coverage
|
|
||||||
python -m pytest tests/test_services_injury.py --cov=services.injury_service --cov=commands.injuries
|
|
||||||
```
|
|
||||||
|
|
||||||
## Injury Roll Tables
|
|
||||||
|
|
||||||
### Table Structure
|
|
||||||
|
|
||||||
The injury tables are based on official Strat-o-Matic rules with the following structure:
|
|
||||||
|
|
||||||
**Ratings:** p70, p65, p60, p50, p40, p30, p20 (higher is better)
|
|
||||||
**Games Played:** 1-6 games in current series
|
|
||||||
**Roll:** 3d6 (results from 3-18)
|
|
||||||
|
|
||||||
### Rating Availability by Games Played
|
|
||||||
|
|
||||||
Not all ratings are available for all games played combinations:
|
|
||||||
|
|
||||||
- **1 game**: All ratings (p70-p20)
|
|
||||||
- **2 games**: All ratings (p70-p20)
|
|
||||||
- **3 games**: p65-p20 (p70 exempt)
|
|
||||||
- **4 games**: p60-p20 (p70, p65 exempt)
|
|
||||||
- **5 games**: p60-p20 (p70, p65 exempt)
|
|
||||||
- **6 games**: p40-p20 (p70, p65, p60, p50 exempt)
|
|
||||||
|
|
||||||
When a rating/games combination has no table, the result is automatically "OK" (no injury).
|
|
||||||
|
|
||||||
### Example Table (p65, 1 game):
|
|
||||||
|
|
||||||
| Roll | Result |
|
|
||||||
|------|--------|
|
|
||||||
| 3 | 2 |
|
|
||||||
| 4 | 2 |
|
|
||||||
| 5 | OK |
|
|
||||||
| 6 | REM |
|
|
||||||
| 7 | 1 |
|
|
||||||
| ... | ... |
|
|
||||||
| 18 | 12 |
|
|
||||||
|
|
||||||
## UI/UX Design
|
|
||||||
|
|
||||||
### Embed Colors
|
|
||||||
|
|
||||||
- **Roll (OK):** Green - No injury
|
|
||||||
- **Roll (REM):** Gold - Remainder of game/Fatigued
|
|
||||||
- **Roll (Injury):** Orange - Number of games
|
|
||||||
- **Set New:** Success (green) - `EmbedTemplate.success()`
|
|
||||||
- **Clear:** Success (green) - `EmbedTemplate.success()`
|
|
||||||
- **Errors:** Error (red) - `EmbedTemplate.error()`
|
|
||||||
|
|
||||||
### Response Format
|
|
||||||
|
|
||||||
All successful responses use Discord embeds with:
|
|
||||||
- Clear title indicating action/status
|
|
||||||
- Well-organized field layout
|
|
||||||
- Team information when applicable
|
|
||||||
- Consistent formatting for dates
|
|
||||||
|
|
||||||
## Integration with Player Model
|
|
||||||
|
|
||||||
The Player model includes injury-related fields:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class Player(SBABaseModel):
|
|
||||||
# ... other fields ...
|
|
||||||
pitcher_injury: Optional[int] # Pitcher injury rating
|
|
||||||
injury_rating: Optional[str] # General injury rating
|
|
||||||
il_return: Optional[str] # Injured list return date (w##g#)
|
|
||||||
```
|
|
||||||
|
|
||||||
When an injury is set or cleared, the player's `il_return` field is automatically updated via PlayerService.
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
Possible improvements for future versions:
|
|
||||||
|
|
||||||
1. **Injury History** - View player's injury history for a season
|
|
||||||
2. **Team Injury Report** - List all injuries for a team
|
|
||||||
3. **Injury Notifications** - Automatic notifications when players return from injury
|
|
||||||
4. **Injury Statistics** - Track injury trends and statistics
|
|
||||||
5. **Injury Chart Image** - Display the official injury chart as an embed image
|
|
||||||
|
|
||||||
## Migration from Legacy
|
|
||||||
|
|
||||||
### Legacy Commands
|
|
||||||
|
|
||||||
The legacy injury commands were located in:
|
|
||||||
- `discord-app/cogs/players.py` - `set_injury_slash()` and `clear_injury_slash()`
|
|
||||||
- `discord-app/cogs/players.py` - `injury_roll_slash()` with manual rating/games input
|
|
||||||
|
|
||||||
### Key Improvements
|
|
||||||
|
|
||||||
1. **Cleaner Command Structure:** Using GroupCog for organized subcommands (`/injury roll`, `/injury set-new`, `/injury clear`)
|
|
||||||
2. **Simplified Interface:** Single parameter for injury roll - games played automatically extracted from player data
|
|
||||||
3. **Smart Injury Ratings:** Automatically reads and parses player's injury rating from database
|
|
||||||
4. **Player Autocomplete:** Modern autocomplete with team prioritization for better UX
|
|
||||||
5. **Better Error Handling:** User-friendly error messages via EmbedTemplate with format validation
|
|
||||||
6. **Improved Logging:** Automatic logging via @logged_command decorator
|
|
||||||
7. **Service Layer:** Separated business logic from command handlers
|
|
||||||
8. **Type Safety:** Full type hints and Pydantic models
|
|
||||||
9. **Testability:** Comprehensive unit tests (26 tests) with mocked API calls
|
|
||||||
10. **Modern UI:** Consistent embed-based responses with color coding
|
|
||||||
11. **Official Tables:** Complete Strat-o-Matic injury tables built into the command
|
|
||||||
|
|
||||||
### Migration Details
|
|
||||||
|
|
||||||
**Old:** `/injuryroll <rating> <games>` - Manual rating and games selection
|
|
||||||
**New:** `/injury roll <player>` - Single parameter, automatic rating and games extraction from player's `injury_rating` field
|
|
||||||
|
|
||||||
**Old:** `/setinjury <player> <week> <game> <duration>`
|
|
||||||
**New:** `/injury set-new <player> <week> <game> <duration>` - Same functionality, better naming
|
|
||||||
|
|
||||||
**Old:** `/clearinjury <player>`
|
|
||||||
**New:** `/injury clear <player>` - Same functionality, better naming
|
|
||||||
|
|
||||||
### Database Field Update
|
|
||||||
|
|
||||||
The `injury_rating` field format has changed to include games played:
|
|
||||||
- **Old Format**: `p65`, `p70`, etc. (rating only)
|
|
||||||
- **New Format**: `1p70`, `4p50`, `2p65`, etc. (games + rating)
|
|
||||||
|
|
||||||
Players must have their `injury_rating` field updated to the new format for the `/injury roll` command to work.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated:** January 2025
|
|
||||||
**Version:** 2.0
|
|
||||||
**Status:** Active
|
|
||||||
@ -1,151 +0,0 @@
|
|||||||
# League Commands
|
|
||||||
|
|
||||||
This directory contains Discord slash commands related to league-wide information and statistics.
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
### `info.py`
|
|
||||||
- **Command**: `/league`
|
|
||||||
- **Description**: Display current league status and information
|
|
||||||
- **Functionality**: Shows current season/week, phase (regular season/playoffs/offseason), transaction status, trade deadlines, and league configuration
|
|
||||||
- **Service Dependencies**: `league_service.get_current_state()`
|
|
||||||
- **Key Features**:
|
|
||||||
- Dynamic phase detection (offseason, playoffs, regular season)
|
|
||||||
- Transaction freeze status
|
|
||||||
- Trade deadline and playoff schedule information
|
|
||||||
- Draft pick trading status
|
|
||||||
|
|
||||||
### `standings.py`
|
|
||||||
- **Commands**:
|
|
||||||
- `/standings` - Display league standings by division
|
|
||||||
- `/playoff-picture` - Show current playoff picture and wild card race
|
|
||||||
- **Parameters**:
|
|
||||||
- `season`: Optional season number (defaults to current)
|
|
||||||
- `division`: Optional division filter for standings
|
|
||||||
- **Service Dependencies**: `standings_service`
|
|
||||||
- **Key Features**:
|
|
||||||
- Division-based standings display
|
|
||||||
- Games behind calculations
|
|
||||||
- Recent form statistics (home record, last 8 games, current streak)
|
|
||||||
- Playoff cutoff visualization
|
|
||||||
- Wild card race tracking
|
|
||||||
|
|
||||||
### `schedule.py`
|
|
||||||
- **Commands**:
|
|
||||||
- `/schedule` - Display game schedules
|
|
||||||
- `/results` - Show recent game results
|
|
||||||
- **Parameters**:
|
|
||||||
- `season`: Optional season number (defaults to current)
|
|
||||||
- `week`: Optional specific week filter
|
|
||||||
- `team`: Optional team abbreviation filter
|
|
||||||
- **Service Dependencies**: `schedule_service`
|
|
||||||
- **Key Features**:
|
|
||||||
- Weekly schedule views
|
|
||||||
- Team-specific schedule filtering
|
|
||||||
- Series grouping and summary
|
|
||||||
- Recent/upcoming game overview
|
|
||||||
- Game completion tracking
|
|
||||||
|
|
||||||
### `submit_scorecard.py`
|
|
||||||
- **Command**: `/submit-scorecard`
|
|
||||||
- **Description**: Submit Google Sheets scorecards with game results and play-by-play data
|
|
||||||
- **Parameters**:
|
|
||||||
- `sheet_url`: Full URL to the Google Sheets scorecard
|
|
||||||
- **Required Role**: `Season 12 Players`
|
|
||||||
- **Service Dependencies**:
|
|
||||||
- `SheetsService` - Google Sheets data extraction
|
|
||||||
- `game_service` - Game CRUD operations
|
|
||||||
- `play_service` - Play-by-play data management
|
|
||||||
- `decision_service` - Pitching decision management
|
|
||||||
- `standings_service` - Standings recalculation
|
|
||||||
- `league_service` - Current state retrieval
|
|
||||||
- `team_service` - Team lookup
|
|
||||||
- `player_service` - Player lookup for results display
|
|
||||||
- **Key Features**:
|
|
||||||
- **Scorecard Validation**: Checks sheet access and version compatibility
|
|
||||||
- **Permission Control**: Only GMs of playing teams can submit
|
|
||||||
- **Duplicate Detection**: Identifies already-played games with confirmation dialog
|
|
||||||
- **Transaction Rollback**: Full rollback support at 3 states:
|
|
||||||
- `PLAYS_POSTED`: Deletes plays on error
|
|
||||||
- `GAME_PATCHED`: Wipes game and deletes plays on error
|
|
||||||
- `COMPLETE`: All data committed successfully
|
|
||||||
- **Data Extraction**: Reads 68 fields from Playtable, 14 fields from Pitcherstats, box score, and game metadata
|
|
||||||
- **Results Display**: Rich embed with box score, pitching decisions, and top 3 key plays by WPA
|
|
||||||
- **Automated Standings**: Triggers standings recalculation after successful submission
|
|
||||||
- **News Channel Posting**: Automatically posts results to configured channel
|
|
||||||
|
|
||||||
**Workflow (14 Phases)**:
|
|
||||||
1. Validate scorecard access and version
|
|
||||||
2. Extract game metadata from Setup tab
|
|
||||||
3. Lookup teams and match managers
|
|
||||||
4. Check user permissions (must be GM of one team or bot owner)
|
|
||||||
5. Check for duplicate games (with confirmation if found)
|
|
||||||
6. Find scheduled game in database
|
|
||||||
7. Read play-by-play data (up to 297 plays)
|
|
||||||
8. Submit plays to database
|
|
||||||
9. Read box score
|
|
||||||
10. Update game with scores and managers
|
|
||||||
11. Read pitching decisions (up to 27 pitchers)
|
|
||||||
12. Submit decisions to database
|
|
||||||
13. Create and post results embed to news channel
|
|
||||||
14. Recalculate league standings
|
|
||||||
|
|
||||||
**Error Handling**:
|
|
||||||
- User-friendly error messages for common issues
|
|
||||||
- Graceful rollback on validation errors
|
|
||||||
- API error parsing for actionable feedback
|
|
||||||
- Non-critical errors (key plays, standings) don't fail submission
|
|
||||||
|
|
||||||
**Configuration**:
|
|
||||||
- `sheets_credentials_path` (in config.py): Path to Google service account credentials JSON (set via `SHEETS_CREDENTIALS_PATH` env var)
|
|
||||||
- `SBA_NETWORK_NEWS_CHANNEL`: Channel name for results posting
|
|
||||||
- `SBA_PLAYERS_ROLE_NAME`: Role required to submit scorecards
|
|
||||||
|
|
||||||
## Architecture Notes
|
|
||||||
|
|
||||||
### Decorator Usage
|
|
||||||
All commands use the `@logged_command` decorator pattern:
|
|
||||||
- Eliminates boilerplate logging code
|
|
||||||
- Provides consistent error handling
|
|
||||||
- Automatic request tracing and timing
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
- Graceful fallbacks for missing data
|
|
||||||
- User-friendly error messages
|
|
||||||
- Ephemeral responses for errors
|
|
||||||
|
|
||||||
### Embed Structure
|
|
||||||
- Uses `EmbedTemplate` for consistent styling
|
|
||||||
- Color coding based on context (success/error/info)
|
|
||||||
- Rich formatting with team logos and thumbnails
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
1. **No league data available**: Check `league_service.get_current_state()` API endpoint
|
|
||||||
2. **Standings not loading**: Verify `standings_service.get_standings_by_division()` returns valid data
|
|
||||||
3. **Schedule commands failing**: Ensure `schedule_service` methods are properly handling season/week parameters
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
- `services.league_service`
|
|
||||||
- `services.standings_service`
|
|
||||||
- `services.schedule_service`
|
|
||||||
- `services.sheets_service` (NEW) - Google Sheets integration
|
|
||||||
- `services.game_service` (NEW) - Game management
|
|
||||||
- `services.play_service` (NEW) - Play-by-play data
|
|
||||||
- `services.decision_service` (NEW) - Pitching decisions
|
|
||||||
- `services.team_service`
|
|
||||||
- `services.player_service`
|
|
||||||
- `utils.decorators.logged_command`
|
|
||||||
- `utils.discord_helpers` (NEW) - Channel and message utilities
|
|
||||||
- `utils.team_utils`
|
|
||||||
- `views.embeds.EmbedTemplate`
|
|
||||||
- `views.confirmations.ConfirmationView` (NEW) - Reusable confirmation dialog
|
|
||||||
- `constants.SBA_CURRENT_SEASON`
|
|
||||||
- `config.BotConfig.sheets_credentials_path` (NEW) - Google Sheets credentials path
|
|
||||||
- `constants.SBA_NETWORK_NEWS_CHANNEL` (NEW)
|
|
||||||
- `constants.SBA_PLAYERS_ROLE_NAME` (NEW)
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
Run tests with: `python -m pytest tests/test_commands_league.py -v`
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
# Player Commands
|
|
||||||
|
|
||||||
This directory contains Discord slash commands for player information and statistics.
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
### `info.py`
|
|
||||||
- **Command**: `/player`
|
|
||||||
- **Description**: Display comprehensive player information and statistics
|
|
||||||
- **Parameters**:
|
|
||||||
- `name` (required): Player name to search for
|
|
||||||
- `season` (optional): Season for statistics (defaults to current season)
|
|
||||||
- **Service Dependencies**:
|
|
||||||
- `player_service.get_players_by_name()`
|
|
||||||
- `player_service.search_players_fuzzy()`
|
|
||||||
- `player_service.get_player()`
|
|
||||||
- `stats_service.get_player_stats()`
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
### Player Search
|
|
||||||
- **Exact Name Matching**: Primary search method using player name
|
|
||||||
- **Fuzzy Search Fallback**: If no exact match, suggests similar player names
|
|
||||||
- **Multiple Player Handling**: When multiple players match, attempts exact match or asks user to be more specific
|
|
||||||
- **Suggestion System**: Shows up to 10 suggested players with positions when no exact match found
|
|
||||||
|
|
||||||
### Player Information Display
|
|
||||||
- **Basic Info**: Name, position(s), team, season
|
|
||||||
- **Statistics Integration**:
|
|
||||||
- Batting stats (AVG/OBP/SLG, OPS, wOBA, HR, RBI, runs, etc.)
|
|
||||||
- Pitching stats (W-L record, ERA, WHIP, strikeouts, saves, etc.)
|
|
||||||
- Two-way player detection and display
|
|
||||||
- **Visual Elements**:
|
|
||||||
- Team logo as author icon
|
|
||||||
- Player card image as main image
|
|
||||||
- Thumbnail priority: fancy card → headshot → team logo
|
|
||||||
- Team color theming for embed
|
|
||||||
|
|
||||||
### Advanced Features
|
|
||||||
- **Concurrent Data Fetching**: Player data and statistics retrieved in parallel for performance
|
|
||||||
- **sWAR Display**: Shows Strat-o-Matic WAR value
|
|
||||||
- **Multi-Position Support**: Displays all eligible positions
|
|
||||||
- **Rich Error Handling**: Graceful fallbacks when data is unavailable
|
|
||||||
|
|
||||||
## Architecture Notes
|
|
||||||
|
|
||||||
### Search Logic Flow
|
|
||||||
1. Search by exact name in specified season
|
|
||||||
2. If no results, try fuzzy search across all players
|
|
||||||
3. If single result, display player card
|
|
||||||
4. If multiple results, attempt exact name match
|
|
||||||
5. If still multiple, show disambiguation list
|
|
||||||
|
|
||||||
### Performance Optimizations
|
|
||||||
- `asyncio.gather()` for concurrent API calls
|
|
||||||
- Efficient player data and statistics retrieval
|
|
||||||
- Lazy loading of optional player images
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
- No players found: Suggests fuzzy matches
|
|
||||||
- Multiple matches: Provides clarification options
|
|
||||||
- Missing data: Shows partial information with clear indicators
|
|
||||||
- API failures: Graceful degradation with fallback data
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
1. **Player not found**:
|
|
||||||
- Check player name spelling
|
|
||||||
- Verify player exists in the specified season
|
|
||||||
- Use fuzzy search suggestions
|
|
||||||
|
|
||||||
2. **Statistics not loading**:
|
|
||||||
- Verify `stats_service.get_player_stats()` API endpoint
|
|
||||||
- Check if player has statistics for the requested season
|
|
||||||
- Ensure season parameter is valid
|
|
||||||
|
|
||||||
3. **Images not displaying**:
|
|
||||||
- Check player image URLs in database
|
|
||||||
- Verify team thumbnail URLs
|
|
||||||
- Ensure image hosting is accessible
|
|
||||||
|
|
||||||
4. **Performance issues**:
|
|
||||||
- Monitor concurrent API call efficiency
|
|
||||||
- Check database query performance
|
|
||||||
- Verify embed size limits
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
- `services.player_service`
|
|
||||||
- `services.stats_service`
|
|
||||||
- `utils.decorators.logged_command`
|
|
||||||
- `views.embeds.EmbedTemplate`
|
|
||||||
- `constants.SBA_CURRENT_SEASON`
|
|
||||||
- `exceptions.BotException`
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
Run tests with: `python -m pytest tests/test_commands_players.py -v`
|
|
||||||
|
|
||||||
## Database Requirements
|
|
||||||
- Player records with name, positions, team associations
|
|
||||||
- Statistics tables for batting and pitching
|
|
||||||
- Image URLs for player cards, headshots, and fancy cards
|
|
||||||
- Team logo and color information
|
|
||||||
@ -1,424 +0,0 @@
|
|||||||
# Player Image Management Commands
|
|
||||||
|
|
||||||
**Last Updated:** January 2025
|
|
||||||
**Status:** ✅ Fully Implemented
|
|
||||||
**Location:** `commands/profile/`
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Player Image Management system allows users to update player fancy card and headshot images for players on teams they own. Administrators can update any player's images.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
### `/set-image <image_type> <player_name> <image_url>`
|
|
||||||
**Description:** Update a player's fancy card or headshot image
|
|
||||||
|
|
||||||
**Parameters:**
|
|
||||||
- `image_type` (choice): Choose "Fancy Card" or "Headshot"
|
|
||||||
- **Fancy Card**: Shows as thumbnail in player cards (takes priority)
|
|
||||||
- **Headshot**: Shows as thumbnail if no fancy card exists
|
|
||||||
- `player_name` (string with autocomplete): Player to update
|
|
||||||
- `image_url` (string): Direct URL to the image file
|
|
||||||
|
|
||||||
**Permissions:**
|
|
||||||
- **Regular Users**: Can update images for players on teams they own (ML/MiL/IL)
|
|
||||||
- **Administrators**: Can update any player's images (bypasses organization check)
|
|
||||||
|
|
||||||
**Usage Examples:**
|
|
||||||
```
|
|
||||||
/set-image fancy-card "Mike Trout" https://example.com/cards/trout.png
|
|
||||||
/set-image headshot "Shohei Ohtani" https://example.com/headshots/ohtani.jpg
|
|
||||||
```
|
|
||||||
|
|
||||||
## Permission System
|
|
||||||
|
|
||||||
### Regular Users
|
|
||||||
Users can update images for players in their organization:
|
|
||||||
- **Major League team players** - Direct team ownership
|
|
||||||
- **Minor League team players** - Owned via organizational affiliation
|
|
||||||
- **Injured List team players** - Owned via organizational affiliation
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
If you own the NYY team, you can update images for players on:
|
|
||||||
- NYY (Major League)
|
|
||||||
- NYYMIL (Minor League)
|
|
||||||
- NYYIL (Injured List)
|
|
||||||
|
|
||||||
### Administrators
|
|
||||||
Administrators have unrestricted access to update any player's images regardless of team ownership.
|
|
||||||
|
|
||||||
### Permission Check Logic
|
|
||||||
```python
|
|
||||||
# Check order:
|
|
||||||
1. Is user an administrator? → Grant access
|
|
||||||
2. Does user own any teams? → Continue check
|
|
||||||
3. Does player belong to user's organization? → Grant access
|
|
||||||
4. Otherwise → Deny access
|
|
||||||
```
|
|
||||||
|
|
||||||
## URL Requirements
|
|
||||||
|
|
||||||
### Format Validation
|
|
||||||
URLs must meet the following criteria:
|
|
||||||
- **Protocol**: Must start with `http://` or `https://`
|
|
||||||
- **Extension**: Must end with valid image extension:
|
|
||||||
- `.jpg`, `.jpeg` - JPEG format
|
|
||||||
- `.png` - PNG format
|
|
||||||
- `.gif` - GIF format (includes animated GIFs)
|
|
||||||
- `.webp` - WebP format
|
|
||||||
- **Length**: Maximum 500 characters
|
|
||||||
- **Query parameters**: Allowed (e.g., `?size=large`)
|
|
||||||
|
|
||||||
**Valid Examples:**
|
|
||||||
```
|
|
||||||
https://example.com/image.jpg
|
|
||||||
https://cdn.discord.com/attachments/123/456/player.png
|
|
||||||
https://i.imgur.com/abc123.webp
|
|
||||||
https://example.com/image.jpg?size=large&format=original
|
|
||||||
```
|
|
||||||
|
|
||||||
**Invalid Examples:**
|
|
||||||
```
|
|
||||||
example.com/image.jpg ❌ Missing protocol
|
|
||||||
ftp://example.com/image.jpg ❌ Wrong protocol
|
|
||||||
https://example.com/document.pdf ❌ Wrong extension
|
|
||||||
https://example.com/page ❌ No extension
|
|
||||||
```
|
|
||||||
|
|
||||||
### Accessibility Testing
|
|
||||||
After format validation, the bot tests URL accessibility:
|
|
||||||
- **HTTP HEAD Request**: Checks if URL is reachable
|
|
||||||
- **Status Code**: Must return 200 OK
|
|
||||||
- **Content-Type**: Must return `image/*` header
|
|
||||||
- **Timeout**: 5 seconds maximum
|
|
||||||
|
|
||||||
**Common Accessibility Errors:**
|
|
||||||
- `404 Not Found` - Image doesn't exist at URL
|
|
||||||
- `403 Forbidden` - Permission denied
|
|
||||||
- `Timeout` - Server too slow or unresponsive
|
|
||||||
- `Wrong content-type` - URL points to webpage, not image
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
### Step-by-Step Process
|
|
||||||
|
|
||||||
1. **User invokes command**
|
|
||||||
```
|
|
||||||
/set-image fancy-card "Mike Trout" https://example.com/card.png
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **URL Format Validation**
|
|
||||||
- Checks protocol, extension, length
|
|
||||||
- If invalid: Shows error with requirements
|
|
||||||
|
|
||||||
3. **URL Accessibility Test**
|
|
||||||
- HTTP HEAD request to URL
|
|
||||||
- Checks status code and content-type
|
|
||||||
- If inaccessible: Shows error with troubleshooting tips
|
|
||||||
|
|
||||||
4. **Player Lookup**
|
|
||||||
- Searches for player by name
|
|
||||||
- Handles multiple matches (asks for exact name)
|
|
||||||
- If not found: Shows error
|
|
||||||
|
|
||||||
5. **Permission Check**
|
|
||||||
- Admin check → Grant access
|
|
||||||
- Organization ownership check → Grant/deny access
|
|
||||||
- If denied: Shows permission error
|
|
||||||
|
|
||||||
6. **Preview with Confirmation**
|
|
||||||
- Shows embed with new image as thumbnail
|
|
||||||
- Displays current vs new image info
|
|
||||||
- **Confirm Update** button → Proceed
|
|
||||||
- **Cancel** button → Abort
|
|
||||||
|
|
||||||
7. **Database Update**
|
|
||||||
- Updates `vanity_card` or `headshot` field
|
|
||||||
- If failure: Shows error
|
|
||||||
|
|
||||||
8. **Success Message**
|
|
||||||
- Confirms update
|
|
||||||
- Shows new image
|
|
||||||
- Displays updated player info
|
|
||||||
|
|
||||||
## Field Mapping
|
|
||||||
|
|
||||||
| Choice | Database Field | Display Priority | Notes |
|
|
||||||
|--------|----------------|------------------|-------|
|
|
||||||
| Fancy Card | `vanity_card` | 1st (highest) | Custom fancy player card |
|
|
||||||
| Headshot | `headshot` | 2nd | Player headshot photo |
|
|
||||||
| *(default)* | `team.thumbnail` | 3rd (fallback) | Team logo |
|
|
||||||
|
|
||||||
**Display Logic in Player Cards:**
|
|
||||||
```
|
|
||||||
IF player.vanity_card exists:
|
|
||||||
Show vanity_card as thumbnail
|
|
||||||
ELSE IF player.headshot exists:
|
|
||||||
Show headshot as thumbnail
|
|
||||||
ELSE IF player.team.thumbnail exists:
|
|
||||||
Show team logo as thumbnail
|
|
||||||
ELSE:
|
|
||||||
No thumbnail
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### For Users
|
|
||||||
|
|
||||||
#### Choosing Image URLs
|
|
||||||
✅ **DO:**
|
|
||||||
- Use reliable image hosting (Discord CDN, Imgur, established hosts)
|
|
||||||
- Use direct image links (right-click image → "Copy Image Address")
|
|
||||||
- Test URLs in browser before submitting
|
|
||||||
- Use permanent URLs, not temporary upload links
|
|
||||||
|
|
||||||
❌ **DON'T:**
|
|
||||||
- Use image hosting page URLs (must be direct image file)
|
|
||||||
- Use temporary or expiring URLs
|
|
||||||
- Use images from unreliable hosts
|
|
||||||
- Use extremely large images (impacts Discord performance)
|
|
||||||
|
|
||||||
#### Image Recommendations
|
|
||||||
**Fancy Cards:**
|
|
||||||
- Recommended size: 400x600px (or similar 2:3 aspect ratio)
|
|
||||||
- Format: PNG or JPEG
|
|
||||||
- File size: < 2MB for best performance
|
|
||||||
- Style: Custom designs, player stats, artistic renditions
|
|
||||||
|
|
||||||
**Headshots:**
|
|
||||||
- Recommended size: 256x256px (square aspect ratio)
|
|
||||||
- Format: PNG or JPEG with transparent background
|
|
||||||
- File size: < 500KB
|
|
||||||
- Style: Professional headshot, clean background
|
|
||||||
|
|
||||||
#### Finding Good Image URLs
|
|
||||||
1. **Discord CDN** (best option):
|
|
||||||
- Upload image to Discord
|
|
||||||
- Right-click → Copy Link
|
|
||||||
- Paste as image URL
|
|
||||||
|
|
||||||
2. **Imgur**:
|
|
||||||
- Upload to Imgur
|
|
||||||
- Right-click image → Copy Image Address
|
|
||||||
- Use direct link (ends with `.png` or `.jpg`)
|
|
||||||
|
|
||||||
3. **Other hosts**:
|
|
||||||
- Ensure stable, permanent hosting
|
|
||||||
- Verify URL accessibility before using
|
|
||||||
|
|
||||||
### For Administrators
|
|
||||||
|
|
||||||
#### Managing Player Images
|
|
||||||
- Set consistent style guidelines for your league
|
|
||||||
- Use standard image dimensions for uniformity
|
|
||||||
- Maintain backup copies of custom images
|
|
||||||
- Document image sources for attribution
|
|
||||||
|
|
||||||
#### Troubleshooting User Issues
|
|
||||||
Common problems and solutions:
|
|
||||||
|
|
||||||
| Issue | Cause | Solution |
|
|
||||||
|-------|-------|----------|
|
|
||||||
| "URL not accessible" | Host down, URL expired | Ask for new URL from stable host |
|
|
||||||
| "Not a valid image" | URL points to webpage | Get direct image link |
|
|
||||||
| "Permission denied" | User doesn't own team | Verify team ownership |
|
|
||||||
| "Player not found" | Typo in name | Use autocomplete feature |
|
|
||||||
|
|
||||||
## Error Messages
|
|
||||||
|
|
||||||
### Format Errors
|
|
||||||
```
|
|
||||||
❌ Invalid URL Format
|
|
||||||
URL must start with http:// or https://
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
• Must start with `http://` or `https://`
|
|
||||||
• Must end with `.jpg`, `.jpeg`, `.png`, `.gif`, or `.webp`
|
|
||||||
• Maximum 500 characters
|
|
||||||
```
|
|
||||||
|
|
||||||
### Accessibility Errors
|
|
||||||
```
|
|
||||||
❌ URL Not Accessible
|
|
||||||
URL returned status 404
|
|
||||||
|
|
||||||
Please check:
|
|
||||||
• URL is correct and not expired
|
|
||||||
• Image host is online
|
|
||||||
• URL points directly to an image file
|
|
||||||
• URL is publicly accessible
|
|
||||||
```
|
|
||||||
|
|
||||||
### Permission Errors
|
|
||||||
```
|
|
||||||
❌ Permission Denied
|
|
||||||
You don't own a team in the NYY organization
|
|
||||||
|
|
||||||
You can only update images for players on teams you own.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Player Not Found
|
|
||||||
```
|
|
||||||
❌ Player Not Found
|
|
||||||
No player found matching 'Mike Trut' in the current season.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Multiple Players Found
|
|
||||||
```
|
|
||||||
🔍 Multiple Players Found
|
|
||||||
Multiple players match 'Mike':
|
|
||||||
• Mike Trout (OF)
|
|
||||||
• Mike Zunino (C)
|
|
||||||
|
|
||||||
Please use the exact name from autocomplete.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Technical Implementation
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
```
|
|
||||||
commands/profile/
|
|
||||||
├── __init__.py # Package setup
|
|
||||||
├── images.py # Main command implementation
|
|
||||||
│ ├── validate_url_format() # Format validation
|
|
||||||
│ ├── test_url_accessibility() # Accessibility testing
|
|
||||||
│ ├── can_edit_player_image() # Permission checking
|
|
||||||
│ ├── ImageUpdateConfirmView # Confirmation UI
|
|
||||||
│ ├── player_name_autocomplete() # Autocomplete function
|
|
||||||
│ └── ImageCommands # Command cog
|
|
||||||
└── README.md # This file
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
- `aiohttp` - Async HTTP requests for URL testing
|
|
||||||
- `discord.py` - Discord bot framework
|
|
||||||
- `player_service` - Player CRUD operations
|
|
||||||
- `team_service` - Team queries and ownership
|
|
||||||
- Standard bot utilities (logging, decorators, embeds)
|
|
||||||
|
|
||||||
### Database Fields
|
|
||||||
**Player Model** (`models/player.py`):
|
|
||||||
```python
|
|
||||||
vanity_card: Optional[str] = Field(None, description="Custom vanity card URL")
|
|
||||||
headshot: Optional[str] = Field(None, description="Player headshot URL")
|
|
||||||
```
|
|
||||||
|
|
||||||
Both fields are optional and store direct image URLs.
|
|
||||||
|
|
||||||
### API Integration
|
|
||||||
**Update Operation:**
|
|
||||||
```python
|
|
||||||
# Update player image
|
|
||||||
update_data = {"vanity_card": "https://example.com/card.png"}
|
|
||||||
updated_player = await player_service.update_player(player_id, update_data)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Endpoints Used:**
|
|
||||||
- `GET /api/v3/players?name={name}&season={season}` - Player search
|
|
||||||
- `PATCH /api/v3/players/{player_id}?vanity_card={url}` - Update player data
|
|
||||||
- `GET /api/v3/teams?owner_id={user_id}&season={season}` - User's teams
|
|
||||||
|
|
||||||
**Important Note:**
|
|
||||||
The player PATCH endpoint uses **query parameters** instead of JSON body for data updates. The `player_service.update_player()` method automatically handles this by setting `use_query_params=True` when calling the API client.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Test Coverage
|
|
||||||
**Test File:** `tests/test_commands_profile_images.py`
|
|
||||||
|
|
||||||
**Test Categories:**
|
|
||||||
1. **URL Format Validation** (10 tests)
|
|
||||||
- Valid formats (JPG, PNG, WebP, with query params)
|
|
||||||
- Invalid protocols (no protocol, FTP)
|
|
||||||
- Invalid extensions (PDF, no extension)
|
|
||||||
- URL length limits
|
|
||||||
|
|
||||||
2. **URL Accessibility** (5 tests)
|
|
||||||
- Successful access
|
|
||||||
- 404 errors
|
|
||||||
- Wrong content-type
|
|
||||||
- Timeouts
|
|
||||||
- Connection errors
|
|
||||||
|
|
||||||
3. **Permission Checking** (7 tests)
|
|
||||||
- Admin access to all players
|
|
||||||
- User access to owned teams
|
|
||||||
- User access to MiL/IL players
|
|
||||||
- Denial for other organizations
|
|
||||||
- Denial for users without teams
|
|
||||||
- Players without team assignment
|
|
||||||
|
|
||||||
4. **Integration Tests** (3 tests)
|
|
||||||
- Command structure validation
|
|
||||||
- Field mapping logic
|
|
||||||
|
|
||||||
### Running Tests
|
|
||||||
```bash
|
|
||||||
# Run all image management tests
|
|
||||||
python -m pytest tests/test_commands_profile_images.py -v
|
|
||||||
|
|
||||||
# Run specific test class
|
|
||||||
python -m pytest tests/test_commands_profile_images.py::TestURLValidation -v
|
|
||||||
|
|
||||||
# Run with coverage
|
|
||||||
python -m pytest tests/test_commands_profile_images.py --cov=commands.profile
|
|
||||||
```
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
### Planned Features (Post-Launch)
|
|
||||||
- **Image size validation**: Check image dimensions
|
|
||||||
- **Image upload support**: Upload images directly instead of URLs
|
|
||||||
- **Bulk image updates**: Update multiple players at once
|
|
||||||
- **Image preview history**: See previous images
|
|
||||||
- **Image moderation**: Admin approval queue for user submissions
|
|
||||||
- **Default images**: Set default fancy cards per team
|
|
||||||
- **Image gallery**: View all player images for a team
|
|
||||||
|
|
||||||
### Potential Improvements
|
|
||||||
- **Automatic image optimization**: Resize/compress large images
|
|
||||||
- **CDN integration**: Auto-upload to Discord CDN for permanence
|
|
||||||
- **Image templates**: Pre-designed templates users can fill in
|
|
||||||
- **Batch operations**: Admin tool to set multiple images
|
|
||||||
- **Image analytics**: Track which images are most viewed
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
**Problem:** "URL not accessible" but URL works in browser
|
|
||||||
- **Cause:** Content-Delivery-Network (CDN) may require browser headers
|
|
||||||
- **Solution:** Use Discord CDN or Imgur instead
|
|
||||||
|
|
||||||
**Problem:** Permission denied even though I own the team
|
|
||||||
- **Cause:** Season mismatch or ownership data not synced
|
|
||||||
- **Solution:** Contact admin to verify team ownership data
|
|
||||||
|
|
||||||
**Problem:** Image appears broken in Discord
|
|
||||||
- **Cause:** Discord can't load the image (blocked, wrong format, too large)
|
|
||||||
- **Solution:** Try different host or smaller file size
|
|
||||||
|
|
||||||
**Problem:** Autocomplete doesn't show player
|
|
||||||
- **Cause:** Player doesn't exist in current season
|
|
||||||
- **Solution:** Verify player name and season
|
|
||||||
|
|
||||||
### Support
|
|
||||||
|
|
||||||
For issues or questions:
|
|
||||||
1. Check this README for solutions
|
|
||||||
2. Review error messages carefully (they include troubleshooting steps)
|
|
||||||
3. Contact server administrators
|
|
||||||
4. Check bot logs for detailed error information
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Implementation Details:**
|
|
||||||
- **Commands:** `commands/profile/images.py`
|
|
||||||
- **Tests:** `tests/test_commands_profile_images.py`
|
|
||||||
- **Models:** `models/player.py` (vanity_card, headshot fields)
|
|
||||||
- **Services:** `services/player_service.py`, `services/team_service.py`
|
|
||||||
|
|
||||||
**Related Documentation:**
|
|
||||||
- **Bot Architecture:** `/discord-app-v2/CLAUDE.md`
|
|
||||||
- **Command Patterns:** `/discord-app-v2/commands/README.md`
|
|
||||||
- **Testing Guide:** `/discord-app-v2/tests/README.md`
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
# Team Commands
|
|
||||||
|
|
||||||
This directory contains Discord slash commands for team information and roster management.
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
### `info.py`
|
|
||||||
- **Commands**:
|
|
||||||
- `/team` - Display comprehensive team information
|
|
||||||
- `/teams` - List all teams in a season
|
|
||||||
- **Parameters**:
|
|
||||||
- `abbrev` (required for `/team`): Team abbreviation (e.g., NYY, BOS, LAD)
|
|
||||||
- `season` (optional): Season to display (defaults to current season)
|
|
||||||
- **Service Dependencies**:
|
|
||||||
- `team_service.get_team_by_abbrev()`
|
|
||||||
- `team_service.get_teams_by_season()`
|
|
||||||
- `team_service.get_team_standings_position()`
|
|
||||||
|
|
||||||
### `roster.py`
|
|
||||||
- **Command**: `/roster`
|
|
||||||
- **Description**: Display detailed team roster with position breakdowns
|
|
||||||
- **Parameters**:
|
|
||||||
- `abbrev` (required): Team abbreviation
|
|
||||||
- `roster_type` (optional): "current" or "next" week roster (defaults to current)
|
|
||||||
- **Service Dependencies**:
|
|
||||||
- `team_service.get_team_by_abbrev()`
|
|
||||||
- `team_service.get_team_roster()`
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
### Team Information Display (`info.py`)
|
|
||||||
- **Comprehensive Team Data**:
|
|
||||||
- Team names (long name, short name, abbreviation)
|
|
||||||
- Stadium information
|
|
||||||
- Division assignment
|
|
||||||
- Team colors and logos
|
|
||||||
- **Standings Integration**:
|
|
||||||
- Win-loss record and winning percentage
|
|
||||||
- Games behind division leader
|
|
||||||
- Current standings position
|
|
||||||
- **Visual Elements**:
|
|
||||||
- Team color theming for embeds
|
|
||||||
- Team logo thumbnails
|
|
||||||
- Consistent branding across displays
|
|
||||||
|
|
||||||
### Team Listing (`/teams`)
|
|
||||||
- **Season Overview**: All teams organized by division
|
|
||||||
- **Division Grouping**: Automatically groups teams by division ID
|
|
||||||
- **Fallback Display**: Shows simple list if division data unavailable
|
|
||||||
- **Team Count**: Total team summary
|
|
||||||
|
|
||||||
### Roster Management (`roster.py`)
|
|
||||||
- **Multi-Week Support**: Current and next week roster views
|
|
||||||
- **Position Breakdown**:
|
|
||||||
- Batting positions (C, 1B, 2B, 3B, SS, LF, CF, RF, DH)
|
|
||||||
- Pitching positions (SP, RP, CP)
|
|
||||||
- Position player counts and totals
|
|
||||||
- **Advanced Features**:
|
|
||||||
- Total sWAR calculation and display
|
|
||||||
- Minor League (shortil) player tracking
|
|
||||||
- Injured List (longil) player management
|
|
||||||
- Detailed player lists with positions and WAR values
|
|
||||||
|
|
||||||
### Roster Display Structure
|
|
||||||
- **Summary Embed**: Position counts and totals
|
|
||||||
- **Detailed Player Lists**: Separate embeds for each roster type
|
|
||||||
- **Player Organization**: Batters and pitchers grouped separately
|
|
||||||
- **Chunked Display**: Long player lists split across multiple fields
|
|
||||||
|
|
||||||
## Architecture Notes
|
|
||||||
|
|
||||||
### Embed Design
|
|
||||||
- **Team Color Integration**: Uses team hex colors for embed theming
|
|
||||||
- **Fallback Colors**: Default colors when team colors unavailable
|
|
||||||
- **Thumbnail Priority**: Team logos displayed consistently
|
|
||||||
- **Multi-Embed Support**: Complex data split across multiple embeds
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
- **Team Not Found**: Clear messaging with season context
|
|
||||||
- **Missing Roster Data**: Graceful handling of unavailable data
|
|
||||||
- **API Failures**: Fallback to partial information display
|
|
||||||
|
|
||||||
### Performance Considerations
|
|
||||||
- **Concurrent Data Fetching**: Standings and roster data retrieved in parallel
|
|
||||||
- **Efficient Roster Processing**: Position grouping and calculations optimized
|
|
||||||
- **Chunked Player Lists**: Prevents Discord embed size limits
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
1. **Team not found**:
|
|
||||||
- Verify team abbreviation spelling
|
|
||||||
- Check if team exists in the specified season
|
|
||||||
- Ensure abbreviation matches database format
|
|
||||||
|
|
||||||
2. **Roster data missing**:
|
|
||||||
- Verify `team_service.get_team_roster()` API endpoint
|
|
||||||
- Check if roster data exists for the requested week type
|
|
||||||
- Ensure team ID is correctly passed to roster service
|
|
||||||
|
|
||||||
3. **Position counts incorrect**:
|
|
||||||
- Verify roster data structure and position field names
|
|
||||||
- Check sWAR calculation logic
|
|
||||||
- Ensure player position arrays are properly parsed
|
|
||||||
|
|
||||||
4. **Standings not displaying**:
|
|
||||||
- Check `get_team_standings_position()` API response
|
|
||||||
- Verify standings data structure matches expected format
|
|
||||||
- Ensure error handling for malformed standings data
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
- `services.team_service`
|
|
||||||
- `models.team.Team`
|
|
||||||
- `utils.decorators.logged_command`
|
|
||||||
- `views.embeds.EmbedTemplate`
|
|
||||||
- `constants.SBA_CURRENT_SEASON`
|
|
||||||
- `exceptions.BotException`
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
Run tests with: `python -m pytest tests/test_commands_teams.py -v`
|
|
||||||
|
|
||||||
## Database Requirements
|
|
||||||
- Team records with abbreviations, names, colors, logos
|
|
||||||
- Division assignment and organization
|
|
||||||
- Roster data with position assignments and player details
|
|
||||||
- Standings calculations and team statistics
|
|
||||||
- Stadium and venue information
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
- Team statistics and performance metrics
|
|
||||||
- Historical team data and comparisons
|
|
||||||
- Roster change tracking and transaction history
|
|
||||||
- Advanced roster analytics and projections
|
|
||||||
@ -1,250 +0,0 @@
|
|||||||
# Transaction Commands
|
|
||||||
|
|
||||||
This directory contains Discord slash commands for transaction management and roster legality checking.
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
### `management.py`
|
|
||||||
- **Commands**:
|
|
||||||
- `/mymoves` - View user's pending and scheduled transactions
|
|
||||||
- `/legal` - Check roster legality for current and next week
|
|
||||||
- **Service Dependencies**:
|
|
||||||
- `transaction_service` (multiple methods for transaction retrieval)
|
|
||||||
- `roster_service` (roster validation and retrieval)
|
|
||||||
- `team_service.get_teams_by_owner()` and `get_team_by_abbrev()`
|
|
||||||
|
|
||||||
### `dropadd.py`
|
|
||||||
- **Commands**:
|
|
||||||
- `/dropadd` - Interactive transaction builder for single-team roster moves
|
|
||||||
- `/cleartransaction` - Clear current transaction builder
|
|
||||||
- **Service Dependencies**:
|
|
||||||
- `transaction_builder` (transaction creation and validation)
|
|
||||||
- `player_service.search_players()` (player autocomplete)
|
|
||||||
- `team_service.get_teams_by_owner()`
|
|
||||||
|
|
||||||
### `trade.py` *(NEW)*
|
|
||||||
- **Commands**:
|
|
||||||
- `/trade initiate` - Start a new multi-team trade
|
|
||||||
- `/trade add-team` - Add additional teams to trade (3+ team trades)
|
|
||||||
- `/trade add-player` - Add player exchanges between teams
|
|
||||||
- `/trade supplementary` - Add internal organizational moves for roster legality
|
|
||||||
- `/trade view` - View current trade status
|
|
||||||
- `/trade clear` - Clear current trade
|
|
||||||
- **Service Dependencies**:
|
|
||||||
- `trade_builder` (multi-team trade management)
|
|
||||||
- `player_service.search_players()` (player autocomplete)
|
|
||||||
- `team_service.get_teams_by_owner()`, `get_team_by_abbrev()`, and `get_team()`
|
|
||||||
- **Channel Management**:
|
|
||||||
- Automatically creates private discussion channels for trades
|
|
||||||
- Uses `TradeChannelManager` and `TradeChannelTracker` for channel lifecycle
|
|
||||||
- Requires bot to have `Manage Channels` permission at server level
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
### Transaction Status Display (`/mymoves`)
|
|
||||||
- **User Team Detection**: Automatically finds user's team by Discord ID
|
|
||||||
- **Transaction Categories**:
|
|
||||||
- **Pending**: Transactions awaiting processing
|
|
||||||
- **Frozen**: Scheduled transactions ready for processing
|
|
||||||
- **Processed**: Recently completed transactions
|
|
||||||
- **Cancelled**: Optional display of cancelled transactions
|
|
||||||
- **Status Visualization**:
|
|
||||||
- Status emojis for each transaction type
|
|
||||||
- Week numbering and move descriptions
|
|
||||||
- Transaction count summaries
|
|
||||||
- **Smart Limiting**: Shows recent transactions (last 5 pending, 3 frozen/processed, 2 cancelled)
|
|
||||||
|
|
||||||
### Roster Legality Checking (`/legal`)
|
|
||||||
- **Dual Roster Validation**: Checks both current and next week rosters
|
|
||||||
- **Flexible Team Selection**:
|
|
||||||
- Auto-detects user's team
|
|
||||||
- Allows manual team specification via abbreviation
|
|
||||||
- **Comprehensive Validation**:
|
|
||||||
- Player count verification (active roster + IL)
|
|
||||||
- sWAR calculations and limits
|
|
||||||
- League rule compliance checking
|
|
||||||
- Error and warning categorization
|
|
||||||
- **Parallel Processing**: Roster retrieval and validation run concurrently
|
|
||||||
|
|
||||||
### Multi-Team Trade System (`/trade`) *(NEW)*
|
|
||||||
- **Trade Initiation**: Start trades between multiple teams using proper Discord command groups
|
|
||||||
- **Team Management**: Add/remove teams to create complex multi-team trades (2+ teams supported)
|
|
||||||
- **Player Exchanges**: Add cross-team player movements with source and destination validation
|
|
||||||
- **Supplementary Moves**: Add internal organizational moves for roster legality compliance
|
|
||||||
- **Interactive UI**: Rich Discord embeds with validation feedback and trade status
|
|
||||||
- **Real-time Validation**: Live roster checking across all participating teams
|
|
||||||
- **Authority Model**: Major League team owners control all players in their organization (ML/MiL/IL)
|
|
||||||
|
|
||||||
#### Trade Command Workflow:
|
|
||||||
1. **`/trade initiate other_team:LAA`** - Start trade between your team and LAA
|
|
||||||
- Creates a private discussion channel for the trade
|
|
||||||
- Only you see the ephemeral response
|
|
||||||
2. **`/trade add-team other_team:BOS`** - Add BOS for 3-team trade
|
|
||||||
- Updates are posted to the trade channel if executed elsewhere
|
|
||||||
- Other team members can see the progress
|
|
||||||
3. **`/trade add-player player_name:"Mike Trout" destination_team:BOS`** - Exchange players
|
|
||||||
- Trade embed updates posted to dedicated channel automatically
|
|
||||||
- Keeps all participants informed of changes
|
|
||||||
4. **`/trade supplementary player_name:"Player X" destination:ml`** - Internal roster moves
|
|
||||||
- Channel receives real-time updates
|
|
||||||
5. **`/trade view`** - Review complete trade with validation
|
|
||||||
- Posts current state to trade channel if viewed elsewhere
|
|
||||||
6. **Submit via interactive UI** - Trade submission through Discord buttons
|
|
||||||
|
|
||||||
**Channel Behavior**:
|
|
||||||
- Commands executed **in** the trade channel: Only ephemeral response to user
|
|
||||||
- Commands executed **outside** trade channel: Ephemeral response to user + public post to trade channel
|
|
||||||
- This ensures all participating teams stay informed of trade progress
|
|
||||||
|
|
||||||
#### Autocomplete System:
|
|
||||||
- **Team Initiation**: Only Major League teams (ML team owners initiate trades)
|
|
||||||
- **Player Destinations**: All roster types (ML/MiL/IL) available for player placement
|
|
||||||
- **Player Search**: Prioritizes user's team players, supports fuzzy name matching
|
|
||||||
- **Smart Filtering**: Context-aware suggestions based on user permissions
|
|
||||||
|
|
||||||
#### Trade Channel Management (`trade_channels.py`, `trade_channel_tracker.py`):
|
|
||||||
- **Automatic Channel Creation**: Private discussion channels created when trades are initiated
|
|
||||||
- **Channel Naming**: Format `trade-{team1}-{team2}-{short_id}` (e.g., `trade-wv-por-681f`)
|
|
||||||
- **Permission Management**:
|
|
||||||
- Channel hidden from @everyone
|
|
||||||
- Only participating team roles can view/message
|
|
||||||
- Bot has view and send message permissions
|
|
||||||
- Created in "Transactions" category (if it exists)
|
|
||||||
- **Channel Tracking**: JSON-based persistence for cleanup and management
|
|
||||||
- **Multi-Team Support**: Channels automatically update when teams are added to trades
|
|
||||||
- **Automatic Cleanup**: Channels deleted when trades are cleared
|
|
||||||
- **Smart Updates**: When trade commands are executed outside the dedicated trade channel, the trade embed is automatically posted to the trade channel (non-ephemeral) for visibility
|
|
||||||
|
|
||||||
**Bot Permission Requirements**:
|
|
||||||
- Server-level `Manage Channels` - Required to create/delete trade channels
|
|
||||||
- Server-level `Manage Permissions` - Optional, for enhanced permission management
|
|
||||||
- **Note**: Bot should NOT have these permissions in channel-specific overwrites (causes Discord API error 50013)
|
|
||||||
|
|
||||||
**Recent Fix (January 2025)**:
|
|
||||||
- Removed `manage_channels` and `manage_permissions` from bot's channel-specific overwrites
|
|
||||||
- Discord prohibits bots from granting themselves elevated permissions in channel overwrites
|
|
||||||
- Server-level permissions are sufficient for all channel management operations
|
|
||||||
|
|
||||||
### Advanced Transaction Features
|
|
||||||
- **Concurrent Data Fetching**: Multiple transaction types retrieved in parallel
|
|
||||||
- **Owner-Based Filtering**: Transactions filtered by team ownership
|
|
||||||
- **Status Tracking**: Real-time transaction status with emoji indicators
|
|
||||||
- **Team Integration**: Team logos and colors in transaction displays
|
|
||||||
|
|
||||||
## Architecture Notes
|
|
||||||
|
|
||||||
### Permission Model
|
|
||||||
- **Team Ownership**: Commands use Discord user ID to determine team ownership
|
|
||||||
- **Cross-Team Viewing**: `/legal` allows checking other teams' roster status
|
|
||||||
- **Access Control**: Users can only view their own transactions via `/mymoves`
|
|
||||||
|
|
||||||
### Data Processing
|
|
||||||
- **Async Operations**: Heavy use of `asyncio.gather()` for performance
|
|
||||||
- **Error Resilience**: Graceful handling of missing roster data
|
|
||||||
- **Validation Pipeline**: Multi-step roster validation with detailed feedback
|
|
||||||
|
|
||||||
### Embed Structure
|
|
||||||
- **Status-Based Coloring**: Success (green) vs Error (red) color coding
|
|
||||||
- **Information Hierarchy**: Important information prioritized in embed layout
|
|
||||||
- **Team Branding**: Consistent use of team thumbnails and colors
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
1. **User team not found**:
|
|
||||||
- Verify user has team ownership record in database
|
|
||||||
- Check Discord user ID mapping to team ownership
|
|
||||||
- Ensure current season team assignments are correct
|
|
||||||
|
|
||||||
2. **Transaction data missing**:
|
|
||||||
- Verify `transaction_service` API endpoints are functional
|
|
||||||
- Check transaction status filtering logic
|
|
||||||
- Ensure transaction records exist for the team/season
|
|
||||||
|
|
||||||
3. **Roster validation failing**:
|
|
||||||
- Check `roster_service.get_current_roster()` and `get_next_roster()` responses
|
|
||||||
- Verify roster validation rules and logic
|
|
||||||
- Ensure player data integrity in roster records
|
|
||||||
|
|
||||||
4. **Legal command errors**:
|
|
||||||
- Verify team abbreviation exists in database
|
|
||||||
- Check roster data availability for both current and next weeks
|
|
||||||
- Ensure validation service handles edge cases properly
|
|
||||||
|
|
||||||
5. **Trade channel creation fails** *(Fixed January 2025)*:
|
|
||||||
- Error: `Discord error: Missing Permissions. Code: 50013`
|
|
||||||
- **Root Cause**: Bot was trying to grant itself `manage_channels` and `manage_permissions` in channel-specific permission overwrites
|
|
||||||
- **Fix**: Removed elevated permissions from channel overwrites (line 74-77 in `trade_channels.py`)
|
|
||||||
- **Verification**: Bot only needs server-level `Manage Channels` permission
|
|
||||||
- Channels now create successfully with basic bot permissions (view, send messages, read history)
|
|
||||||
|
|
||||||
6. **AttributeError when adding players to trades** *(Fixed January 2025)*:
|
|
||||||
- Error: `'TeamService' object has no attribute 'get_team_by_id'`
|
|
||||||
- **Root Cause**: Code was calling non-existent method `team_service.get_team_by_id()`
|
|
||||||
- **Fix**: Changed to correct method name `team_service.get_team()` (line 201 in `trade_builder.py`)
|
|
||||||
- **Location**: `services/trade_builder.py` and test mocks in `tests/test_services_trade_builder.py`
|
|
||||||
- All 18 trade builder tests pass after fix
|
|
||||||
|
|
||||||
### Service Dependencies
|
|
||||||
- `services.transaction_service`:
|
|
||||||
- `get_pending_transactions()`
|
|
||||||
- `get_frozen_transactions()`
|
|
||||||
- `get_processed_transactions()`
|
|
||||||
- `get_team_transactions()`
|
|
||||||
- `services.roster_service`:
|
|
||||||
- `get_current_roster()`
|
|
||||||
- `get_next_roster()`
|
|
||||||
- `validate_roster()`
|
|
||||||
- `services.team_service`:
|
|
||||||
- `get_teams_by_owner()`
|
|
||||||
- `get_team_by_abbrev()`
|
|
||||||
- `get_teams_by_season()` *(trade autocomplete)*
|
|
||||||
- `services.trade_builder` *(NEW)*:
|
|
||||||
- `TradeBuilder` class for multi-team transaction management
|
|
||||||
- `get_trade_builder()` and `clear_trade_builder()` cache functions
|
|
||||||
- `TradeValidationResult` for comprehensive trade validation
|
|
||||||
- `services.player_service`:
|
|
||||||
- `search_players()` for autocomplete functionality
|
|
||||||
|
|
||||||
### Core Dependencies
|
|
||||||
- `utils.decorators.logged_command`
|
|
||||||
- `views.embeds.EmbedTemplate`
|
|
||||||
- `views.trade_embed` *(NEW)*: Trade-specific UI components
|
|
||||||
- `utils.autocomplete` *(ENHANCED)*: Player and team autocomplete functions
|
|
||||||
- `utils.team_utils` *(NEW)*: Shared team validation utilities
|
|
||||||
- `constants.SBA_CURRENT_SEASON`
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
Run tests with:
|
|
||||||
- `python -m pytest tests/test_commands_transactions.py -v` (management commands)
|
|
||||||
- `python -m pytest tests/test_models_trade.py -v` *(NEW)* (trade models)
|
|
||||||
- `python -m pytest tests/test_services_trade_builder.py -v` *(NEW)* (trade builder service)
|
|
||||||
|
|
||||||
## Database Requirements
|
|
||||||
- Team ownership mapping (Discord user ID to team)
|
|
||||||
- Transaction records with status tracking
|
|
||||||
- Roster data for current and next weeks
|
|
||||||
- Player assignments and position information
|
|
||||||
- League rules and validation criteria
|
|
||||||
|
|
||||||
## Recent Enhancements *(NEW)*
|
|
||||||
- ✅ **Multi-Team Trade System**: Complete `/trade` command group for 2+ team trades
|
|
||||||
- ✅ **Enhanced Autocomplete**: Major League team filtering and smart player suggestions
|
|
||||||
- ✅ **Shared Utilities**: Reusable team validation and autocomplete functions
|
|
||||||
- ✅ **Comprehensive Testing**: Factory-based tests for trade models and services
|
|
||||||
- ✅ **Interactive Trade UI**: Rich Discord embeds with real-time validation
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
- **Trade Submission Integration**: Connect trade system to transaction processing pipeline
|
|
||||||
- **Advanced transaction analytics and history
|
|
||||||
- **Trade Approval Workflow**: Multi-party trade approval system
|
|
||||||
- **Roster optimization suggestions
|
|
||||||
- **Automated roster validation alerts
|
|
||||||
- **Trade History Tracking**: Complete audit trail for multi-team trades
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
- User authentication via Discord IDs
|
|
||||||
- Team ownership verification for sensitive operations
|
|
||||||
- Transaction privacy (users can only see their own transactions)
|
|
||||||
- Input validation for team abbreviations and parameters
|
|
||||||
@ -1,235 +0,0 @@
|
|||||||
# Utility Commands
|
|
||||||
|
|
||||||
This directory contains general utility commands that enhance the user experience for the SBA Discord bot.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
### `/weather [team_abbrev]`
|
|
||||||
|
|
||||||
**Description**: Roll ballpark weather for gameplay.
|
|
||||||
|
|
||||||
**Usage**:
|
|
||||||
- `/weather` - Roll weather for your team or current channel's team
|
|
||||||
- `/weather NYY` - Roll weather for a specific team
|
|
||||||
|
|
||||||
**Features**:
|
|
||||||
- **Smart Team Resolution** (3-tier priority):
|
|
||||||
1. Explicit team abbreviation parameter
|
|
||||||
2. Channel name parsing (e.g., `NYY-Yankee Stadium` → `NYY`)
|
|
||||||
3. User's owned team (fallback)
|
|
||||||
|
|
||||||
- **Season Display**:
|
|
||||||
- Weeks 1-5: 🌼 Spring
|
|
||||||
- Weeks 6-14: 🏖️ Summer
|
|
||||||
- Weeks 15+: 🍂 Fall
|
|
||||||
|
|
||||||
- **Time of Day Logic**:
|
|
||||||
- Based on games played this week
|
|
||||||
- Division weeks: [1, 3, 6, 14, 16, 18]
|
|
||||||
- 0/2 games OR (1 game in division week): 🌙 Night
|
|
||||||
- 1/3 games: 🌞 Day
|
|
||||||
- 4+ games: 🕸️ Spidey Time (special case)
|
|
||||||
|
|
||||||
- **Weather Roll**: Random d20 (1-20) displayed in markdown format
|
|
||||||
|
|
||||||
**Embed Layout**:
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────┐
|
|
||||||
│ 🌤️ Weather Check │
|
|
||||||
│ [Team Colors] │
|
|
||||||
├─────────────────────────────────┤
|
|
||||||
│ Season: 🌼 Spring │
|
|
||||||
│ Time of Day: 🌙 Night │
|
|
||||||
│ Week: 5 | Games Played: 2/4 │
|
|
||||||
├─────────────────────────────────┤
|
|
||||||
│ Weather Roll │
|
|
||||||
│ ```md │
|
|
||||||
│ # 14 │
|
|
||||||
│ Details: [1d20 (14)] │
|
|
||||||
│ ``` │
|
|
||||||
├─────────────────────────────────┤
|
|
||||||
│ [Stadium Image] │
|
|
||||||
└─────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Implementation Details**:
|
|
||||||
- **File**: `commands/utilities/weather.py`
|
|
||||||
- **Service Dependencies**:
|
|
||||||
- `LeagueService` - Current league state
|
|
||||||
- `ScheduleService` - Week schedule and games
|
|
||||||
- `TeamService` - Team resolution
|
|
||||||
- **Logging**: Uses `@logged_command` decorator for automatic logging
|
|
||||||
- **Error Handling**: Graceful fallback with user-friendly error messages
|
|
||||||
|
|
||||||
**Examples**:
|
|
||||||
|
|
||||||
1. In a team channel (`#NYY-Yankee-Stadium`):
|
|
||||||
```
|
|
||||||
/weather
|
|
||||||
→ Automatically uses NYY from channel name
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Explicit team:
|
|
||||||
```
|
|
||||||
/weather BOS
|
|
||||||
→ Shows weather for Boston team
|
|
||||||
```
|
|
||||||
|
|
||||||
3. As team owner:
|
|
||||||
```
|
|
||||||
/weather
|
|
||||||
→ Defaults to your owned team if not in a team channel
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Command Pattern
|
|
||||||
|
|
||||||
All utility commands follow the standard bot architecture:
|
|
||||||
|
|
||||||
```python
|
|
||||||
@discord.app_commands.command(name="command")
|
|
||||||
@discord.app_commands.describe(param="Description")
|
|
||||||
@logged_command("/command")
|
|
||||||
async def command_handler(self, interaction, param: str):
|
|
||||||
await interaction.response.defer()
|
|
||||||
# Command logic using services
|
|
||||||
await interaction.followup.send(embed=embed)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Service Layer
|
|
||||||
|
|
||||||
Utility commands leverage the service layer for all data access:
|
|
||||||
- **No direct database calls** - all data through services
|
|
||||||
- **Async operations** - proper async/await patterns
|
|
||||||
- **Error handling** - graceful degradation with user feedback
|
|
||||||
|
|
||||||
### Embed Templates
|
|
||||||
|
|
||||||
Use `EmbedTemplate` from `views.embeds` for consistent styling:
|
|
||||||
- Team colors via `team.color`
|
|
||||||
- Standard error/success/info templates
|
|
||||||
- Image support (thumbnails and full images)
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
All utility commands have comprehensive test coverage:
|
|
||||||
|
|
||||||
**Weather Command** (`tests/test_commands_weather.py` - 20 tests):
|
|
||||||
- Team resolution (3-tier priority)
|
|
||||||
- Season calculation
|
|
||||||
- Time of day logic (including division weeks)
|
|
||||||
- Weather roll randomization
|
|
||||||
- Embed formatting and layout
|
|
||||||
- Error handling scenarios
|
|
||||||
|
|
||||||
**Charts Command** (`tests/test_commands_charts.py` - 26 tests):
|
|
||||||
- Chart service operations (loading, adding, updating, removing)
|
|
||||||
- Chart display (single and multi-image)
|
|
||||||
- Autocomplete functionality
|
|
||||||
- Admin command operations
|
|
||||||
- Error handling (invalid charts, categories)
|
|
||||||
- JSON persistence
|
|
||||||
|
|
||||||
### `/charts <chart-name>`
|
|
||||||
|
|
||||||
**Description**: Display gameplay charts and infographics from the league library.
|
|
||||||
|
|
||||||
**Usage**:
|
|
||||||
- `/charts rest` - Display pitcher rest chart
|
|
||||||
- `/charts defense` - Display defense chart
|
|
||||||
- `/charts hit-and-run` - Display hit and run strategy chart
|
|
||||||
|
|
||||||
**Features**:
|
|
||||||
- **Autocomplete**: Smart chart name suggestions with category display
|
|
||||||
- **Multi-image Support**: Automatically sends multiple images for complex charts
|
|
||||||
- **Categorized Library**: Charts organized by gameplay, defense, reference, and stats
|
|
||||||
- **Proper Embeds**: Charts displayed in formatted Discord embeds with descriptions
|
|
||||||
|
|
||||||
**Available Charts** (12 total):
|
|
||||||
- **Gameplay**: rest, sac-bunt, squeeze-bunt, hit-and-run, g1, g2, g3, groundball, fly-b
|
|
||||||
- **Defense**: rob-hr, defense, block-plate
|
|
||||||
|
|
||||||
**Admin Commands**:
|
|
||||||
|
|
||||||
Administrators can manage the chart library using these commands:
|
|
||||||
|
|
||||||
- `/chart-add <key> <name> <category> <url> [description]` - Add a new chart
|
|
||||||
- `/chart-remove <key>` - Remove a chart from the library
|
|
||||||
- `/chart-list [category]` - List all charts (optionally filtered by category)
|
|
||||||
- `/chart-update <key> [name] [category] [url] [description]` - Update chart properties
|
|
||||||
|
|
||||||
**Implementation Details**:
|
|
||||||
- **Files**:
|
|
||||||
- `commands/utilities/charts.py` - Command handlers
|
|
||||||
- `services/chart_service.py` - Chart management service
|
|
||||||
- `data/charts.json` - Chart definitions storage
|
|
||||||
- **Service**: `ChartService` - Manages chart loading, saving, and retrieval
|
|
||||||
- **Categories**: gameplay, defense, reference, stats
|
|
||||||
- **Logging**: Uses `@logged_command` decorator for automatic logging
|
|
||||||
|
|
||||||
**Examples**:
|
|
||||||
|
|
||||||
1. Display a single-image chart:
|
|
||||||
```
|
|
||||||
/charts defense
|
|
||||||
→ Shows defense chart embed with image
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Display multi-image chart:
|
|
||||||
```
|
|
||||||
/charts hit-and-run
|
|
||||||
→ Shows first image in response, additional images in followups
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Admin: Add new chart:
|
|
||||||
```
|
|
||||||
/chart-add steal-chart "Steal Chart" gameplay https://example.com/steal.png
|
|
||||||
→ Adds new chart to the library
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Admin: List charts by category:
|
|
||||||
```
|
|
||||||
/chart-list gameplay
|
|
||||||
→ Shows all gameplay charts
|
|
||||||
```
|
|
||||||
|
|
||||||
**Data Structure** (`data/charts.json`):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"charts": {
|
|
||||||
"chart-key": {
|
|
||||||
"name": "Display Name",
|
|
||||||
"category": "gameplay",
|
|
||||||
"description": "Chart description",
|
|
||||||
"urls": ["https://example.com/image.png"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"categories": {
|
|
||||||
"gameplay": "Gameplay Mechanics",
|
|
||||||
"defense": "Defensive Play"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Future Commands
|
|
||||||
|
|
||||||
Planned utility commands (see PRE_LAUNCH_ROADMAP.md):
|
|
||||||
|
|
||||||
- `/links <resource-name>` - Quick access to league resources
|
|
||||||
|
|
||||||
## Development Guidelines
|
|
||||||
|
|
||||||
When adding new utility commands:
|
|
||||||
|
|
||||||
1. **Follow existing patterns** - Use weather.py as a reference
|
|
||||||
2. **Use @logged_command** - Automatic logging and error handling
|
|
||||||
3. **Service layer only** - No direct database access
|
|
||||||
4. **Comprehensive tests** - Cover all edge cases
|
|
||||||
5. **User-friendly errors** - Clear, actionable error messages
|
|
||||||
6. **Document in README** - Update this file with new commands
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated**: January 2025
|
|
||||||
**Maintainer**: Major Domo Bot Development Team
|
|
||||||
@ -1,251 +0,0 @@
|
|||||||
# Voice Channel Commands
|
|
||||||
|
|
||||||
This directory contains Discord slash commands for creating and managing voice channels for gameplay.
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
### `channels.py`
|
|
||||||
- **Commands**:
|
|
||||||
- `/voice-channel public` - Create a public voice channel for gameplay
|
|
||||||
- `/voice-channel private` - Create a private team vs team voice channel
|
|
||||||
- **Description**: Main command implementation with VoiceChannelCommands cog
|
|
||||||
- **Service Dependencies**:
|
|
||||||
- `team_service.get_teams_by_owner()` - Verify user has a team
|
|
||||||
- `league_service.get_current_state()` - Get current season/week info
|
|
||||||
- `schedule_service.get_team_schedule()` - Find opponent for private channels
|
|
||||||
- **Deprecated Commands**:
|
|
||||||
- `!vc`, `!voice`, `!gameplay` → Shows migration message to `/voice-channel public`
|
|
||||||
- `!private` → Shows migration message to `/voice-channel private`
|
|
||||||
|
|
||||||
### `cleanup_service.py`
|
|
||||||
- **Class**: `VoiceChannelCleanupService`
|
|
||||||
- **Description**: Manages automatic cleanup of bot-created voice channels
|
|
||||||
- **Features**:
|
|
||||||
- Restart-resilient channel tracking using JSON persistence
|
|
||||||
- Configurable cleanup intervals and empty thresholds
|
|
||||||
- Background monitoring loop with error recovery
|
|
||||||
- Startup verification to clean stale tracking entries
|
|
||||||
|
|
||||||
### `tracker.py`
|
|
||||||
- **Class**: `VoiceChannelTracker`
|
|
||||||
- **Description**: JSON-based persistent tracking of voice channels
|
|
||||||
- **Features**:
|
|
||||||
- Channel creation and status tracking
|
|
||||||
- Empty duration monitoring with datetime handling
|
|
||||||
- Cleanup candidate identification
|
|
||||||
- Automatic stale entry removal
|
|
||||||
|
|
||||||
### `__init__.py`
|
|
||||||
- **Function**: `setup_voice(bot)`
|
|
||||||
- **Description**: Package initialization with resilient cog loading
|
|
||||||
- **Integration**: Follows established bot architecture patterns
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
### Public Voice Channels (`/voice-channel public`)
|
|
||||||
- **Permissions**: Everyone can connect and speak
|
|
||||||
- **Naming**: Random codename generation (e.g., "Gameplay Phoenix", "Gameplay Thunder")
|
|
||||||
- **Requirements**: User must own a Major League team (3-character abbreviations like NYY, BOS)
|
|
||||||
- **Auto-cleanup**: Configurable threshold (default: empty for configured minutes)
|
|
||||||
|
|
||||||
### Private Voice Channels (`/voice-channel private`)
|
|
||||||
- **Permissions**:
|
|
||||||
- Team members can connect and speak (using `team.lname` Discord roles)
|
|
||||||
- Everyone else can connect but only listen
|
|
||||||
- **Naming**: Automatic "{Away} vs {Home}" format based on current week's schedule
|
|
||||||
- **Opponent Detection**: Uses current league week to find scheduled opponent
|
|
||||||
- **Requirements**:
|
|
||||||
- User must own a Major League team (3-character abbreviations like NYY, BOS)
|
|
||||||
- Team must have upcoming games in current week
|
|
||||||
- **Role Integration**: Finds Discord roles matching team full names (`team.lname`)
|
|
||||||
|
|
||||||
### Automatic Cleanup System
|
|
||||||
- **Monitoring Interval**: Configurable (default: 60 seconds)
|
|
||||||
- **Empty Threshold**: Configurable (default: 5 minutes empty before deletion)
|
|
||||||
- **Restart Resilience**: JSON file persistence survives bot restarts
|
|
||||||
- **Startup Verification**: Validates tracked channels still exist on bot startup
|
|
||||||
- **Graceful Error Handling**: Continues operation even if individual operations fail
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Command Flow
|
|
||||||
1. **Major League Team Verification**: Check user owns a Major League team using `team_service`
|
|
||||||
2. **Channel Creation**: Create voice channel with appropriate permissions
|
|
||||||
3. **Tracking Registration**: Add channel to cleanup service tracking
|
|
||||||
4. **User Feedback**: Send success embed with channel details
|
|
||||||
|
|
||||||
### Team Validation Logic
|
|
||||||
The voice channel system validates that users own **Major League teams** specifically:
|
|
||||||
|
|
||||||
```python
|
|
||||||
async def _get_user_major_league_team(self, user_id: int, season: Optional[int] = None):
|
|
||||||
"""Get the user's Major League team for schedule/game purposes."""
|
|
||||||
teams = await team_service.get_teams_by_owner(user_id, season)
|
|
||||||
|
|
||||||
# Filter to only Major League teams (3-character abbreviations)
|
|
||||||
major_league_teams = [team for team in teams if team.roster_type() == RosterType.MAJOR_LEAGUE]
|
|
||||||
|
|
||||||
return major_league_teams[0] if major_league_teams else None
|
|
||||||
```
|
|
||||||
|
|
||||||
**Team Types:**
|
|
||||||
- **Major League**: 3-character abbreviations (e.g., NYY, BOS, LAD) - **Required for voice channels**
|
|
||||||
- **Minor League**: 4+ characters ending in "MIL" (e.g., NYYMIL, BOSMIL) - **Not eligible**
|
|
||||||
- **Injured List**: Ending in "IL" (e.g., NYYIL, BOSIL) - **Not eligible**
|
|
||||||
|
|
||||||
**Rationale:** Only Major League teams participate in weekly scheduled games, so voice channel creation is restricted to active Major League team owners.
|
|
||||||
|
|
||||||
### Permission System
|
|
||||||
```python
|
|
||||||
# Public channels - everyone can speak
|
|
||||||
overwrites = {
|
|
||||||
guild.default_role: discord.PermissionOverwrite(speak=True, connect=True)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Private channels - team roles only can speak
|
|
||||||
overwrites = {
|
|
||||||
guild.default_role: discord.PermissionOverwrite(speak=False, connect=True),
|
|
||||||
user_team_role: discord.PermissionOverwrite(speak=True, connect=True),
|
|
||||||
opponent_team_role: discord.PermissionOverwrite(speak=True, connect=True)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cleanup Service Integration
|
|
||||||
```python
|
|
||||||
# Bot initialization (bot.py)
|
|
||||||
from commands.voice.cleanup_service import VoiceChannelCleanupService
|
|
||||||
self.voice_cleanup_service = VoiceChannelCleanupService()
|
|
||||||
asyncio.create_task(self.voice_cleanup_service.start_monitoring(self))
|
|
||||||
|
|
||||||
# Channel tracking
|
|
||||||
if hasattr(self.bot, 'voice_cleanup_service'):
|
|
||||||
cleanup_service = self.bot.voice_cleanup_service
|
|
||||||
cleanup_service.tracker.add_channel(channel, channel_type, interaction.user.id)
|
|
||||||
```
|
|
||||||
|
|
||||||
### JSON Data Structure
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"voice_channels": {
|
|
||||||
"123456789": {
|
|
||||||
"channel_id": "123456789",
|
|
||||||
"guild_id": "987654321",
|
|
||||||
"name": "Gameplay Phoenix",
|
|
||||||
"type": "public",
|
|
||||||
"created_at": "2025-01-15T10:30:00",
|
|
||||||
"last_checked": "2025-01-15T10:35:00",
|
|
||||||
"empty_since": "2025-01-15T10:32:00",
|
|
||||||
"creator_id": "111222333"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Cleanup Service Settings
|
|
||||||
- **`cleanup_interval`**: How often to check channels (default: 60 seconds)
|
|
||||||
- **`empty_threshold`**: Minutes empty before deletion (default: 5 minutes)
|
|
||||||
- **`data_file`**: JSON persistence file path (default: "data/voice_channels.json")
|
|
||||||
|
|
||||||
### Channel Categories
|
|
||||||
- Channels are created in the "Voice Channels" category if it exists
|
|
||||||
- Falls back to no category if "Voice Channels" category not found
|
|
||||||
|
|
||||||
### Random Codenames
|
|
||||||
```python
|
|
||||||
CODENAMES = [
|
|
||||||
"Phoenix", "Thunder", "Lightning", "Storm", "Blaze", "Frost", "Shadow", "Nova",
|
|
||||||
"Viper", "Falcon", "Wolf", "Eagle", "Tiger", "Shark", "Bear", "Dragon",
|
|
||||||
"Alpha", "Beta", "Gamma", "Delta", "Echo", "Foxtrot", "Golf", "Hotel",
|
|
||||||
"Crimson", "Azure", "Emerald", "Golden", "Silver", "Bronze", "Platinum", "Diamond"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
### Common Scenarios
|
|
||||||
- **No Team Found**: User-friendly message directing to contact league administrator
|
|
||||||
- **No Upcoming Games**: Informative message about being between series
|
|
||||||
- **Missing Discord Roles**: Warning in embed about teams without speaking permissions
|
|
||||||
- **Permission Errors**: Clear message to contact server administrator
|
|
||||||
- **League Info Unavailable**: Graceful fallback with retry suggestion
|
|
||||||
|
|
||||||
### Service Dependencies
|
|
||||||
- **Graceful Degradation**: Voice channels work without cleanup service
|
|
||||||
- **API Failures**: Comprehensive error handling for external service calls
|
|
||||||
- **Discord Errors**: Specific handling for Forbidden, NotFound, etc.
|
|
||||||
|
|
||||||
## Testing Coverage
|
|
||||||
|
|
||||||
### Test Files
|
|
||||||
- **`tests/test_commands_voice.py`**: Comprehensive test suite covering:
|
|
||||||
- VoiceChannelTracker JSON persistence and datetime handling
|
|
||||||
- VoiceChannelCleanupService restart resilience and monitoring
|
|
||||||
- VoiceChannelCommands slash command functionality
|
|
||||||
- Error scenarios and edge cases
|
|
||||||
- Deprecated command migration messages
|
|
||||||
|
|
||||||
### Mock Objects
|
|
||||||
- Discord guild, channels, roles, and interactions
|
|
||||||
- Team service responses and player data
|
|
||||||
- Schedule service responses and game data
|
|
||||||
- League service current state information
|
|
||||||
|
|
||||||
## Integration Points
|
|
||||||
|
|
||||||
### Bot Integration
|
|
||||||
- **Package Loading**: Integrated into `bot.py` command package loading sequence
|
|
||||||
- **Background Tasks**: Cleanup service started in `_setup_background_tasks()`
|
|
||||||
- **Shutdown Handling**: Cleanup service stopped in `bot.close()`
|
|
||||||
|
|
||||||
### Service Layer
|
|
||||||
- **Team Service**: User team verification and ownership lookup
|
|
||||||
- **League Service**: Current season/week information retrieval
|
|
||||||
- **Schedule Service**: Team schedule and opponent detection
|
|
||||||
|
|
||||||
### Discord Integration
|
|
||||||
- **Application Commands**: Modern slash command interface with command groups
|
|
||||||
- **Permission Overwrites**: Fine-grained voice channel permission control
|
|
||||||
- **Embed Templates**: Consistent styling using established embed patterns
|
|
||||||
- **Error Handling**: Integration with global application command error handler
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
### Creating Public Channel
|
|
||||||
```
|
|
||||||
/voice-channel public
|
|
||||||
```
|
|
||||||
**Result**: Creates "Gameplay [Codename]" with public speaking permissions
|
|
||||||
|
|
||||||
### Creating Private Channel
|
|
||||||
```
|
|
||||||
/voice-channel private
|
|
||||||
```
|
|
||||||
**Result**: Creates "[Away] vs [Home]" with team-only speaking permissions
|
|
||||||
|
|
||||||
### Migration from Old Commands
|
|
||||||
```
|
|
||||||
!vc
|
|
||||||
```
|
|
||||||
**Result**: Shows deprecation message suggesting `/voice-channel public`
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
### Potential Features
|
|
||||||
- **Channel Limits**: Per-user or per-team channel creation limits
|
|
||||||
- **Custom Names**: Allow users to specify custom channel names
|
|
||||||
- **Extended Permissions**: More granular permission control options
|
|
||||||
- **Channel Templates**: Predefined setups for different game types
|
|
||||||
- **Integration Webhooks**: Notifications when channels are created/deleted
|
|
||||||
|
|
||||||
### Configuration Options
|
|
||||||
- **Environment Variables**: Make cleanup intervals configurable via env vars
|
|
||||||
- **Per-Guild Settings**: Different settings for different Discord servers
|
|
||||||
- **Role Mapping**: Custom role name patterns for team permissions
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated**: January 2025
|
|
||||||
**Architecture**: Modern async Discord.py with JSON persistence
|
|
||||||
**Dependencies**: discord.py, team_service, league_service, schedule_service
|
|
||||||
657
models/README.md
657
models/README.md
@ -1,657 +0,0 @@
|
|||||||
# Models Directory
|
|
||||||
|
|
||||||
The models directory contains Pydantic data models for Discord Bot v2.0, providing type-safe representations of all SBA (Strat-o-Matic Baseball Association) entities. All models inherit from `SBABaseModel` and follow consistent validation patterns.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Pydantic Foundation
|
|
||||||
All models use Pydantic v2 with:
|
|
||||||
- **Automatic validation** of field types and constraints
|
|
||||||
- **Serialization/deserialization** for API interactions
|
|
||||||
- **Type safety** with full IDE support
|
|
||||||
- **JSON schema generation** for documentation
|
|
||||||
- **Field validation** with custom validators
|
|
||||||
|
|
||||||
### Base Model (`base.py`)
|
|
||||||
The foundation for all SBA models:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class SBABaseModel(BaseModel):
|
|
||||||
model_config = {
|
|
||||||
"validate_assignment": True,
|
|
||||||
"use_enum_values": True,
|
|
||||||
"arbitrary_types_allowed": True,
|
|
||||||
"json_encoders": {datetime: lambda v: v.isoformat() if v else None}
|
|
||||||
}
|
|
||||||
|
|
||||||
id: Optional[int] = None
|
|
||||||
created_at: Optional[datetime] = None
|
|
||||||
updated_at: Optional[datetime] = None
|
|
||||||
```
|
|
||||||
|
|
||||||
### Breaking Changes (August 2025)
|
|
||||||
**Database entities now require `id` fields** since they're always fetched from the database:
|
|
||||||
- `Player` model: `id: int = Field(..., description="Player ID from database")`
|
|
||||||
- `Team` model: `id: int = Field(..., description="Team ID from database")`
|
|
||||||
|
|
||||||
### Game Submission Models (January 2025)
|
|
||||||
|
|
||||||
New models for comprehensive game data submission from Google Sheets scorecards:
|
|
||||||
|
|
||||||
#### Play Model (`play.py`)
|
|
||||||
Represents a single play in a baseball game with complete statistics and game state.
|
|
||||||
|
|
||||||
**Key Features:**
|
|
||||||
- **92 total fields** supporting comprehensive play-by-play tracking
|
|
||||||
- **68 fields from scorecard**: All data read from Google Sheets Playtable
|
|
||||||
- **Required fields**: game_id, play_num, pitcher_id, on_base_code, inning details, outs, scores
|
|
||||||
- **Base running**: Tracks up to 3 runners with starting and ending positions
|
|
||||||
- **Statistics**: PA, AB, H, HR, RBI, BB, SO, SB, CS, errors, and 20+ more
|
|
||||||
- **Advanced metrics**: WPA, RE24, ballpark effects
|
|
||||||
- **Descriptive text generation**: Automatic play descriptions for key plays display
|
|
||||||
|
|
||||||
**Field Validators:**
|
|
||||||
```python
|
|
||||||
@field_validator('on_first_final')
|
|
||||||
@classmethod
|
|
||||||
def no_final_if_no_runner_one(cls, v, info):
|
|
||||||
"""Ensure on_first_final is None if no runner on first."""
|
|
||||||
if info.data.get('on_first_id') is None:
|
|
||||||
return None
|
|
||||||
return v
|
|
||||||
```
|
|
||||||
|
|
||||||
**Usage Example:**
|
|
||||||
```python
|
|
||||||
play = Play(
|
|
||||||
id=1234,
|
|
||||||
game_id=567,
|
|
||||||
play_num=1,
|
|
||||||
pitcher_id=100,
|
|
||||||
batter_id=101,
|
|
||||||
on_base_code="000",
|
|
||||||
inning_half="top",
|
|
||||||
inning_num=1,
|
|
||||||
batting_order=1,
|
|
||||||
starting_outs=0,
|
|
||||||
away_score=0,
|
|
||||||
home_score=0,
|
|
||||||
homerun=1,
|
|
||||||
rbi=1,
|
|
||||||
wpa=0.15
|
|
||||||
)
|
|
||||||
|
|
||||||
# Generate human-readable description
|
|
||||||
description = play.descriptive_text(away_team, home_team)
|
|
||||||
# Output: "Top 1: (NYY) homers"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Field Categories:**
|
|
||||||
- **Game Context**: game_id, play_num, inning_half, inning_num, starting_outs
|
|
||||||
- **Players**: batter_id, pitcher_id, catcher_id, defender_id, runner_id
|
|
||||||
- **Base Runners**: on_first_id, on_second_id, on_third_id (with _final positions)
|
|
||||||
- **Offensive Stats**: pa, ab, hit, rbi, double, triple, homerun, bb, so, hbp, sac
|
|
||||||
- **Defensive Stats**: outs, error, wild_pitch, passed_ball, pick_off, balk
|
|
||||||
- **Advanced**: wpa, re24_primary, re24_running, ballpark effects (bphr, bpfo, bp1b, bplo)
|
|
||||||
- **Pitching**: pitcher_rest_outs, inherited_runners, inherited_scored, on_hook_for_loss
|
|
||||||
|
|
||||||
**API-Populated Nested Objects:**
|
|
||||||
|
|
||||||
The Play model includes optional nested object fields for all ID references. These are populated by the API endpoint to provide complete context without additional lookups:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class Play(SBABaseModel):
|
|
||||||
# ID field with corresponding optional object
|
|
||||||
game_id: int = Field(..., description="Game ID this play belongs to")
|
|
||||||
game: Optional[Game] = Field(None, description="Game object (API-populated)")
|
|
||||||
|
|
||||||
pitcher_id: int = Field(..., description="Pitcher ID")
|
|
||||||
pitcher: Optional[Player] = Field(None, description="Pitcher object (API-populated)")
|
|
||||||
|
|
||||||
batter_id: Optional[int] = Field(None, description="Batter ID")
|
|
||||||
batter: Optional[Player] = Field(None, description="Batter object (API-populated)")
|
|
||||||
|
|
||||||
# ... and so on for all player/team IDs
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pattern Details:**
|
|
||||||
- **Placement**: Optional object field immediately follows its corresponding ID field
|
|
||||||
- **Naming**: Object field uses singular form of ID field name (e.g., `batter_id` → `batter`)
|
|
||||||
- **API Population**: Database endpoint includes nested objects in response
|
|
||||||
- **Future Enhancement**: Validators could ensure consistency between ID and object fields
|
|
||||||
|
|
||||||
**ID Fields with Nested Objects:**
|
|
||||||
- `game_id` → `game: Optional[Game]`
|
|
||||||
- `pitcher_id` → `pitcher: Optional[Player]`
|
|
||||||
- `batter_id` → `batter: Optional[Player]`
|
|
||||||
- `batter_team_id` → `batter_team: Optional[Team]`
|
|
||||||
- `pitcher_team_id` → `pitcher_team: Optional[Team]`
|
|
||||||
- `on_first_id` → `on_first: Optional[Player]`
|
|
||||||
- `on_second_id` → `on_second: Optional[Player]`
|
|
||||||
- `on_third_id` → `on_third: Optional[Player]`
|
|
||||||
- `catcher_id` → `catcher: Optional[Player]`
|
|
||||||
- `catcher_team_id` → `catcher_team: Optional[Team]`
|
|
||||||
- `defender_id` → `defender: Optional[Player]`
|
|
||||||
- `defender_team_id` → `defender_team: Optional[Team]`
|
|
||||||
- `runner_id` → `runner: Optional[Player]`
|
|
||||||
- `runner_team_id` → `runner_team: Optional[Team]`
|
|
||||||
|
|
||||||
**Usage Example:**
|
|
||||||
```python
|
|
||||||
# API returns play with nested objects populated
|
|
||||||
play = await play_service.get_play(play_id=123)
|
|
||||||
|
|
||||||
# Access nested objects directly without additional lookups
|
|
||||||
if play.batter:
|
|
||||||
print(f"Batter: {play.batter.name}")
|
|
||||||
if play.pitcher:
|
|
||||||
print(f"Pitcher: {play.pitcher.name}")
|
|
||||||
if play.game:
|
|
||||||
print(f"Game: {play.game.matchup_display}")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Decision Model (`decision.py`)
|
|
||||||
Tracks pitching decisions (wins, losses, saves, holds) for game results.
|
|
||||||
|
|
||||||
**Key Features:**
|
|
||||||
- **Pitching decisions**: Win, Loss, Save, Hold, Blown Save flags
|
|
||||||
- **Game metadata**: game_id, season, week, game_num
|
|
||||||
- **Pitcher workload**: rest_ip, rest_required, inherited runners
|
|
||||||
- **Human-readable repr**: Shows decision type (W/L/SV/HLD/BS)
|
|
||||||
|
|
||||||
**Usage Example:**
|
|
||||||
```python
|
|
||||||
decision = Decision(
|
|
||||||
id=456,
|
|
||||||
game_id=567,
|
|
||||||
season=12,
|
|
||||||
week=5,
|
|
||||||
game_num=2,
|
|
||||||
pitcher_id=200,
|
|
||||||
team_id=10,
|
|
||||||
win=1, # Winning pitcher
|
|
||||||
is_start=True,
|
|
||||||
rest_ip=7.0,
|
|
||||||
rest_required=4
|
|
||||||
)
|
|
||||||
|
|
||||||
print(decision)
|
|
||||||
# Output: Decision(pitcher_id=200, game_id=567, type=W)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Field Categories:**
|
|
||||||
- **Game Context**: game_id, season, week, game_num
|
|
||||||
- **Pitcher**: pitcher_id, team_id
|
|
||||||
- **Decisions**: win, loss, hold, is_save, b_save (all 0 or 1)
|
|
||||||
- **Workload**: is_start, irunners, irunners_scored, rest_ip, rest_required
|
|
||||||
|
|
||||||
**Data Pipeline:**
|
|
||||||
```
|
|
||||||
Google Sheets Scorecard
|
|
||||||
↓
|
|
||||||
SheetsService.read_playtable_data() → 68 fields per play
|
|
||||||
↓
|
|
||||||
PlayService.create_plays_batch() → Validate with Play model
|
|
||||||
↓
|
|
||||||
Database API /plays endpoint
|
|
||||||
↓
|
|
||||||
PlayService.get_top_plays_by_wpa() → Return Play objects
|
|
||||||
↓
|
|
||||||
Play.descriptive_text() → Human-readable descriptions
|
|
||||||
```
|
|
||||||
|
|
||||||
## Model Categories
|
|
||||||
|
|
||||||
### Core Entities
|
|
||||||
|
|
||||||
#### League Structure
|
|
||||||
- **`team.py`** - Team information, abbreviations, divisions, and organizational affiliates
|
|
||||||
- **`division.py`** - Division structure and organization
|
|
||||||
- **`manager.py`** - Team managers and ownership
|
|
||||||
- **`standings.py`** - Team standings and rankings
|
|
||||||
|
|
||||||
#### Player Data
|
|
||||||
- **`player.py`** - Core player information and identifiers
|
|
||||||
- **`sbaplayer.py`** - Extended SBA-specific player data
|
|
||||||
- **`batting_stats.py`** - Batting statistics and performance metrics
|
|
||||||
- **`pitching_stats.py`** - Pitching statistics and performance metrics
|
|
||||||
- **`roster.py`** - Team roster assignments and positions
|
|
||||||
|
|
||||||
#### Game Operations
|
|
||||||
- **`game.py`** - Individual game results and scheduling
|
|
||||||
- **`play.py`** (NEW - January 2025) - Play-by-play data for game submissions
|
|
||||||
- **`decision.py`** (NEW - January 2025) - Pitching decisions and game results
|
|
||||||
- **`transaction.py`** - Player transactions (trades, waivers, etc.)
|
|
||||||
|
|
||||||
#### Draft System
|
|
||||||
- **`draft_pick.py`** - Individual draft pick information
|
|
||||||
- **`draft_data.py`** - Draft round and selection data
|
|
||||||
- **`draft_list.py`** - Complete draft lists and results
|
|
||||||
|
|
||||||
#### Custom Features
|
|
||||||
- **`custom_command.py`** - User-created Discord commands
|
|
||||||
|
|
||||||
#### Trade System
|
|
||||||
- **`trade.py`** - Multi-team trade structures and validation
|
|
||||||
|
|
||||||
### Legacy Models
|
|
||||||
- **`current.py`** - Legacy model definitions for backward compatibility
|
|
||||||
|
|
||||||
## Model Validation Patterns
|
|
||||||
|
|
||||||
### Required Fields
|
|
||||||
Models distinguish between required and optional fields:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class Player(SBABaseModel):
|
|
||||||
id: int = Field(..., description="Player ID from database") # Required
|
|
||||||
name: str = Field(..., description="Player full name") # Required
|
|
||||||
team_id: Optional[int] = None # Optional
|
|
||||||
position: Optional[str] = None # Optional
|
|
||||||
```
|
|
||||||
|
|
||||||
### Field Constraints
|
|
||||||
Models use Pydantic validators for data integrity:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class BattingStats(SBABaseModel):
|
|
||||||
at_bats: int = Field(ge=0, description="At bats (non-negative)")
|
|
||||||
hits: int = Field(ge=0, le=Field('at_bats'), description="Hits (cannot exceed at_bats)")
|
|
||||||
|
|
||||||
@field_validator('batting_average')
|
|
||||||
@classmethod
|
|
||||||
def validate_batting_average(cls, v):
|
|
||||||
if v is not None and not 0.0 <= v <= 1.0:
|
|
||||||
raise ValueError('Batting average must be between 0.0 and 1.0')
|
|
||||||
return v
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Validators
|
|
||||||
Models implement business logic validation:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class Transaction(SBABaseModel):
|
|
||||||
transaction_type: str
|
|
||||||
player_id: int
|
|
||||||
from_team_id: Optional[int] = None
|
|
||||||
to_team_id: Optional[int] = None
|
|
||||||
|
|
||||||
@model_validator(mode='after')
|
|
||||||
def validate_team_requirements(self):
|
|
||||||
if self.transaction_type == 'trade':
|
|
||||||
if not self.from_team_id or not self.to_team_id:
|
|
||||||
raise ValueError('Trade transactions require both from_team_id and to_team_id')
|
|
||||||
return self
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Integration
|
|
||||||
|
|
||||||
### Data Transformation
|
|
||||||
Models provide methods for API interaction:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class Player(SBABaseModel):
|
|
||||||
@classmethod
|
|
||||||
def from_api_data(cls, data: Dict[str, Any]):
|
|
||||||
"""Create model instance from API response data."""
|
|
||||||
if not data:
|
|
||||||
raise ValueError(f"Cannot create {cls.__name__} from empty data")
|
|
||||||
return cls(**data)
|
|
||||||
|
|
||||||
def to_dict(self, exclude_none: bool = True) -> Dict[str, Any]:
|
|
||||||
"""Convert model to dictionary for API requests."""
|
|
||||||
return self.model_dump(exclude_none=exclude_none)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Serialization Examples
|
|
||||||
Models handle various data formats:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# From API JSON
|
|
||||||
player_data = {"id": 123, "name": "Player Name", "team_id": 5}
|
|
||||||
player = Player.from_api_data(player_data)
|
|
||||||
|
|
||||||
# To API JSON
|
|
||||||
api_payload = player.to_dict(exclude_none=True)
|
|
||||||
|
|
||||||
# JSON string serialization
|
|
||||||
json_string = player.model_dump_json()
|
|
||||||
|
|
||||||
# From JSON string
|
|
||||||
player_copy = Player.model_validate_json(json_string)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Requirements
|
|
||||||
|
|
||||||
### Model Validation Testing
|
|
||||||
All model tests must provide complete data:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def test_player_creation():
|
|
||||||
# ✅ Correct - provides required ID field
|
|
||||||
player = Player(
|
|
||||||
id=123,
|
|
||||||
name="Test Player",
|
|
||||||
team_id=5,
|
|
||||||
position="1B"
|
|
||||||
)
|
|
||||||
assert player.id == 123
|
|
||||||
|
|
||||||
def test_incomplete_data():
|
|
||||||
# ❌ This will fail - missing required ID
|
|
||||||
with pytest.raises(ValidationError):
|
|
||||||
Player(name="Test Player") # Missing required id field
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Data Patterns
|
|
||||||
Use helper functions for consistent test data:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def create_test_player(**overrides) -> Player:
|
|
||||||
"""Create a test player with default values."""
|
|
||||||
defaults = {
|
|
||||||
"id": 123,
|
|
||||||
"name": "Test Player",
|
|
||||||
"team_id": 1,
|
|
||||||
"position": "1B"
|
|
||||||
}
|
|
||||||
defaults.update(overrides)
|
|
||||||
return Player(**defaults)
|
|
||||||
|
|
||||||
def test_player_with_stats():
|
|
||||||
player = create_test_player(name="Star Player")
|
|
||||||
assert player.name == "Star Player"
|
|
||||||
assert player.id == 123 # Default from helper
|
|
||||||
```
|
|
||||||
|
|
||||||
## Field Types and Constraints
|
|
||||||
|
|
||||||
### Common Field Patterns
|
|
||||||
|
|
||||||
#### Identifiers
|
|
||||||
```python
|
|
||||||
id: int = Field(..., description="Database primary key")
|
|
||||||
player_id: int = Field(..., description="Foreign key to player")
|
|
||||||
team_id: Optional[int] = Field(None, description="Foreign key to team")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Names and Text
|
|
||||||
```python
|
|
||||||
name: str = Field(..., min_length=1, max_length=100)
|
|
||||||
abbreviation: str = Field(..., min_length=2, max_length=5)
|
|
||||||
description: Optional[str] = Field(None, max_length=500)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Statistics
|
|
||||||
```python
|
|
||||||
games_played: int = Field(ge=0, description="Games played (non-negative)")
|
|
||||||
batting_average: Optional[float] = Field(None, ge=0.0, le=1.0)
|
|
||||||
era: Optional[float] = Field(None, ge=0.0, description="Earned run average")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Dates and Times
|
|
||||||
```python
|
|
||||||
game_date: Optional[datetime] = None
|
|
||||||
created_at: Optional[datetime] = None
|
|
||||||
season_year: int = Field(..., ge=1900, le=2100)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Model Relationships
|
|
||||||
|
|
||||||
### Foreign Key Patterns
|
|
||||||
Models reference related entities via ID fields:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class Player(SBABaseModel):
|
|
||||||
id: int
|
|
||||||
team_id: Optional[int] = None # References Team.id
|
|
||||||
|
|
||||||
class BattingStats(SBABaseModel):
|
|
||||||
player_id: int # References Player.id
|
|
||||||
season: int
|
|
||||||
team_id: int # References Team.id
|
|
||||||
```
|
|
||||||
|
|
||||||
### Nested Objects
|
|
||||||
Some models contain nested structures:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class CustomCommand(SBABaseModel):
|
|
||||||
name: str
|
|
||||||
creator: Manager # Nested Manager object
|
|
||||||
response: str
|
|
||||||
|
|
||||||
class DraftPick(SBABaseModel):
|
|
||||||
pick_number: int
|
|
||||||
player: Optional[Player] = None # Optional nested Player
|
|
||||||
team: Team # Required nested Team
|
|
||||||
```
|
|
||||||
|
|
||||||
## Validation Error Handling
|
|
||||||
|
|
||||||
### Common Validation Errors
|
|
||||||
- **Missing required fields** - Provide all required model fields
|
|
||||||
- **Type mismatches** - Ensure field types match model definitions
|
|
||||||
- **Constraint violations** - Check field validators and constraints
|
|
||||||
- **Invalid nested objects** - Validate all nested model data
|
|
||||||
|
|
||||||
### Error Examples
|
|
||||||
```python
|
|
||||||
try:
|
|
||||||
player = Player(name="Test") # Missing required id
|
|
||||||
except ValidationError as e:
|
|
||||||
print(e.errors())
|
|
||||||
# [{'type': 'missing', 'loc': ('id',), 'msg': 'Field required'}]
|
|
||||||
|
|
||||||
try:
|
|
||||||
stats = BattingStats(hits=5, at_bats=3) # hits > at_bats
|
|
||||||
except ValidationError as e:
|
|
||||||
print(e.errors())
|
|
||||||
# Constraint violation error
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
|
|
||||||
### Model Instantiation
|
|
||||||
- Use `model_validate()` for external data
|
|
||||||
- Use `model_construct()` for trusted internal data (faster)
|
|
||||||
- Cache model instances when possible
|
|
||||||
- Avoid repeated validation of the same data
|
|
||||||
|
|
||||||
### Memory Usage
|
|
||||||
- Models are relatively lightweight
|
|
||||||
- Nested objects can increase memory footprint
|
|
||||||
- Consider using `__slots__` for high-volume models
|
|
||||||
- Use `exclude_none=True` to reduce serialization size
|
|
||||||
|
|
||||||
## Development Guidelines
|
|
||||||
|
|
||||||
### Adding New Models
|
|
||||||
1. **Inherit from SBABaseModel** for consistency
|
|
||||||
2. **Define required fields explicitly** with proper types
|
|
||||||
3. **Add field descriptions** for documentation
|
|
||||||
4. **Include validation rules** for data integrity
|
|
||||||
5. **Provide `from_api_data()` class method** if needed
|
|
||||||
6. **Write comprehensive tests** covering edge cases
|
|
||||||
|
|
||||||
## Team Model Enhancements (January 2025)
|
|
||||||
|
|
||||||
### Organizational Affiliate Methods
|
|
||||||
The Team model now includes methods to work with organizational affiliates (Major League, Minor League, and Injured List teams):
|
|
||||||
|
|
||||||
```python
|
|
||||||
class Team(SBABaseModel):
|
|
||||||
async def major_league_affiliate(self) -> 'Team':
|
|
||||||
"""Get the major league team for this organization via API call."""
|
|
||||||
|
|
||||||
async def minor_league_affiliate(self) -> 'Team':
|
|
||||||
"""Get the minor league team for this organization via API call."""
|
|
||||||
|
|
||||||
async def injured_list_affiliate(self) -> 'Team':
|
|
||||||
"""Get the injured list team for this organization via API call."""
|
|
||||||
|
|
||||||
def is_same_organization(self, other_team: 'Team') -> bool:
|
|
||||||
"""Check if this team and another team are from the same organization."""
|
|
||||||
```
|
|
||||||
|
|
||||||
### Usage Examples
|
|
||||||
|
|
||||||
#### Organizational Relationships
|
|
||||||
```python
|
|
||||||
# Get affiliate teams
|
|
||||||
por_team = await team_service.get_team_by_abbrev("POR", 12)
|
|
||||||
por_mil = await por_team.minor_league_affiliate() # Returns "PORMIL" team
|
|
||||||
por_il = await por_team.injured_list_affiliate() # Returns "PORIL" team
|
|
||||||
|
|
||||||
# Check organizational relationships
|
|
||||||
assert por_team.is_same_organization(por_mil) # True
|
|
||||||
assert por_team.is_same_organization(por_il) # True
|
|
||||||
|
|
||||||
# Different organizations
|
|
||||||
nyy_team = await team_service.get_team_by_abbrev("NYY", 12)
|
|
||||||
assert not por_team.is_same_organization(nyy_team) # False
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Roster Type Detection
|
|
||||||
```python
|
|
||||||
# Determine roster type from team abbreviation
|
|
||||||
assert por_team.roster_type() == RosterType.MAJOR_LEAGUE # "POR"
|
|
||||||
assert por_mil.roster_type() == RosterType.MINOR_LEAGUE # "PORMIL"
|
|
||||||
assert por_il.roster_type() == RosterType.INJURED_LIST # "PORIL"
|
|
||||||
|
|
||||||
# Handle edge cases
|
|
||||||
bhm_il = Team(abbrev="BHMIL") # BHM + IL, not BH + MIL
|
|
||||||
assert bhm_il.roster_type() == RosterType.INJURED_LIST
|
|
||||||
```
|
|
||||||
|
|
||||||
### Implementation Notes
|
|
||||||
- **API Integration**: Affiliate methods make actual API calls to fetch team data
|
|
||||||
- **Error Handling**: Methods raise `ValueError` if affiliate teams cannot be found
|
|
||||||
- **Edge Cases**: Correctly handles teams like "BHMIL" (Birmingham IL)
|
|
||||||
- **Performance**: Base abbreviation extraction is cached internally
|
|
||||||
|
|
||||||
### Model Evolution
|
|
||||||
- **Backward compatibility** - Add optional fields for new features
|
|
||||||
- **Migration patterns** - Handle schema changes gracefully
|
|
||||||
- **Version management** - Document breaking changes
|
|
||||||
- **API alignment** - Keep models synchronized with API
|
|
||||||
|
|
||||||
### Testing Strategy
|
|
||||||
- **Unit tests** for individual model validation
|
|
||||||
- **Integration tests** with service layer
|
|
||||||
- **Edge case testing** for validation rules
|
|
||||||
- **Performance tests** for large data sets
|
|
||||||
|
|
||||||
## Trade Model Enhancements (January 2025)
|
|
||||||
|
|
||||||
### Multi-Team Trade Support
|
|
||||||
The Trade model now supports complex multi-team player exchanges with proper organizational authority handling:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class Trade(SBABaseModel):
|
|
||||||
def get_participant_by_organization(self, team: Team) -> Optional[TradeParticipant]:
|
|
||||||
"""Find participant by organization affiliation.
|
|
||||||
|
|
||||||
Major League team owners control their entire organization (ML/MiL/IL),
|
|
||||||
so if a ML team is participating, their MiL and IL teams are also valid.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cross_team_moves(self) -> List[TradeMove]:
|
|
||||||
"""Get all moves that cross team boundaries (deduplicated)."""
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Features
|
|
||||||
|
|
||||||
#### Organizational Authority Model
|
|
||||||
```python
|
|
||||||
# ML team owners can trade from/to any affiliate
|
|
||||||
wv_team = Team(abbrev="WV") # Major League
|
|
||||||
wv_mil = Team(abbrev="WVMIL") # Minor League
|
|
||||||
wv_il = Team(abbrev="WVIL") # Injured List
|
|
||||||
|
|
||||||
# If WV is participating in trade, WVMIL and WVIL moves are valid
|
|
||||||
trade.add_participant(wv_team) # Add ML team
|
|
||||||
# Now can move players to/from WVMIL and WVIL
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Deduplication Fix
|
|
||||||
```python
|
|
||||||
# Before: Each move appeared twice (giving + receiving perspective)
|
|
||||||
cross_moves = trade.cross_team_moves # Would show duplicates
|
|
||||||
|
|
||||||
# After: Clean single view of each player exchange
|
|
||||||
cross_moves = trade.cross_team_moves # Shows each move once
|
|
||||||
```
|
|
||||||
|
|
||||||
### Trade Move Descriptions
|
|
||||||
Enhanced move descriptions with clear team-to-team visualization:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Team-to-team trade
|
|
||||||
"🔄 Mike Trout: WV (ML) → NY (ML)"
|
|
||||||
|
|
||||||
# Free agency signing
|
|
||||||
"➕ Mike Trout: FA → WV (ML)"
|
|
||||||
|
|
||||||
# Release to free agency
|
|
||||||
"➖ Mike Trout: WV (ML) → FA"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Usage Examples
|
|
||||||
|
|
||||||
#### Basic Trade Setup
|
|
||||||
```python
|
|
||||||
# Create trade
|
|
||||||
trade = Trade(trade_id="abc123", participants=[], status=TradeStatus.DRAFT)
|
|
||||||
|
|
||||||
# Add participating teams
|
|
||||||
wv_participant = trade.add_participant(wv_team)
|
|
||||||
ny_participant = trade.add_participant(ny_team)
|
|
||||||
|
|
||||||
# Create player moves
|
|
||||||
move = TradeMove(
|
|
||||||
player=player,
|
|
||||||
from_team=wv_team,
|
|
||||||
to_team=ny_team,
|
|
||||||
source_team=wv_team,
|
|
||||||
destination_team=ny_team
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Organizational Flexibility
|
|
||||||
```python
|
|
||||||
# Trade builder allows MiL/IL destinations when ML team participates
|
|
||||||
builder = TradeBuilder(user_id, wv_team) # WV is participating
|
|
||||||
builder.add_team(ny_team)
|
|
||||||
|
|
||||||
# This now works - can send player to NYMIL
|
|
||||||
success, error = await builder.add_player_move(
|
|
||||||
player=player,
|
|
||||||
from_team=wv_team,
|
|
||||||
to_team=ny_mil_team, # Minor league affiliate
|
|
||||||
from_roster=RosterType.MAJOR_LEAGUE,
|
|
||||||
to_roster=RosterType.MINOR_LEAGUE
|
|
||||||
)
|
|
||||||
assert success # ✅ Works due to organizational authority
|
|
||||||
```
|
|
||||||
|
|
||||||
### Implementation Notes
|
|
||||||
- **Deduplication**: `cross_team_moves` now uses only `moves_giving` to avoid showing same move twice
|
|
||||||
- **Organizational Lookup**: Trade participants can be found by any team in the organization
|
|
||||||
- **Validation**: Trade balance validation ensures moves are properly matched
|
|
||||||
- **UI Integration**: Embeds show clean, deduplicated player exchange lists
|
|
||||||
|
|
||||||
### Breaking Changes Fixed
|
|
||||||
- **Team Roster Type Detection**: Updated logic to handle edge cases like "BHMIL" correctly
|
|
||||||
- **Autocomplete Functions**: Fixed invalid parameter passing in team filtering
|
|
||||||
- **Trade Participant Validation**: Now properly handles organizational affiliates
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Next Steps for AI Agents:**
|
|
||||||
1. Review existing model implementations for patterns
|
|
||||||
2. Understand the validation rules and field constraints
|
|
||||||
3. Check the service layer integration in `/services`
|
|
||||||
4. Follow the testing patterns with complete model data
|
|
||||||
5. Consider the API data format when creating new models
|
|
||||||
@ -1,394 +0,0 @@
|
|||||||
# Services Directory
|
|
||||||
|
|
||||||
The services directory contains the service layer for Discord Bot v2.0, providing clean abstractions for API interactions and business logic. All services inherit from `BaseService` and follow consistent patterns for data operations.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Service Layer Pattern
|
|
||||||
Services act as the interface between Discord commands and the external API, providing:
|
|
||||||
- **Data validation** using Pydantic models
|
|
||||||
- **Error handling** with consistent exception patterns
|
|
||||||
- **Caching support** via Redis decorators
|
|
||||||
- **Type safety** with generic TypeVar support
|
|
||||||
- **Logging integration** with structured logging
|
|
||||||
|
|
||||||
### Base Service (`base_service.py`)
|
|
||||||
The foundation for all services, providing:
|
|
||||||
- **Generic CRUD operations** (Create, Read, Update, Delete)
|
|
||||||
- **API client management** with connection pooling
|
|
||||||
- **Response format handling** for API responses
|
|
||||||
- **Cache key generation** and management
|
|
||||||
- **Error handling** with APIException wrapping
|
|
||||||
|
|
||||||
```python
|
|
||||||
class BaseService(Generic[T]):
|
|
||||||
def __init__(self, model_class: Type[T], endpoint: str)
|
|
||||||
async def get_by_id(self, object_id: int) -> Optional[T]
|
|
||||||
async def get_all(self, params: Optional[List[tuple]] = None) -> Tuple[List[T], int]
|
|
||||||
async def create(self, model_data: Dict[str, Any]) -> Optional[T]
|
|
||||||
async def update(self, object_id: int, model_data: Dict[str, Any]) -> Optional[T]
|
|
||||||
async def patch(self, object_id: int, model_data: Dict[str, Any], use_query_params: bool = False) -> Optional[T]
|
|
||||||
async def delete(self, object_id: int) -> bool
|
|
||||||
```
|
|
||||||
|
|
||||||
**PATCH vs PUT Operations:**
|
|
||||||
- `update()` uses HTTP PUT for full resource replacement
|
|
||||||
- `patch()` uses HTTP PATCH for partial updates
|
|
||||||
- `use_query_params=True` sends data as URL query parameters instead of JSON body
|
|
||||||
|
|
||||||
**When to use `use_query_params=True`:**
|
|
||||||
Some API endpoints (notably the player PATCH endpoint) expect data as query parameters instead of JSON body. Example:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Standard PATCH with JSON body
|
|
||||||
await base_service.patch(object_id, {"field": "value"})
|
|
||||||
# → PATCH /api/v3/endpoint/{id} with JSON: {"field": "value"}
|
|
||||||
|
|
||||||
# PATCH with query parameters
|
|
||||||
await base_service.patch(object_id, {"field": "value"}, use_query_params=True)
|
|
||||||
# → PATCH /api/v3/endpoint/{id}?field=value
|
|
||||||
```
|
|
||||||
|
|
||||||
## Service Files
|
|
||||||
|
|
||||||
### Core Entity Services
|
|
||||||
- **`player_service.py`** - Player data operations and search functionality
|
|
||||||
- **`team_service.py`** - Team information and roster management
|
|
||||||
- **`league_service.py`** - League-wide data and current season info
|
|
||||||
- **`standings_service.py`** - Team standings and division rankings
|
|
||||||
- **`schedule_service.py`** - Game scheduling and results
|
|
||||||
- **`stats_service.py`** - Player statistics (batting, pitching, fielding)
|
|
||||||
- **`roster_service.py`** - Team roster composition and position assignments
|
|
||||||
|
|
||||||
#### TeamService Key Methods
|
|
||||||
The `TeamService` provides team data operations with specific method names:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class TeamService(BaseService[Team]):
|
|
||||||
async def get_team(team_id: int) -> Optional[Team] # ✅ Correct method name
|
|
||||||
async def get_teams_by_owner(owner_id: int, season: Optional[int], roster_type: Optional[str]) -> List[Team]
|
|
||||||
async def get_team_by_abbrev(abbrev: str, season: Optional[int]) -> Optional[Team]
|
|
||||||
async def get_teams_by_season(season: int) -> List[Team]
|
|
||||||
async def get_team_roster(team_id: int, roster_type: str = 'current') -> Optional[Dict[str, Any]]
|
|
||||||
```
|
|
||||||
|
|
||||||
**⚠️ Common Mistake (Fixed January 2025)**:
|
|
||||||
- **Incorrect**: `team_service.get_team_by_id(team_id)` ❌ (method does not exist)
|
|
||||||
- **Correct**: `team_service.get_team(team_id)` ✅
|
|
||||||
|
|
||||||
This naming inconsistency was fixed in `services/trade_builder.py` line 201 and corresponding test mocks.
|
|
||||||
|
|
||||||
### Transaction Services
|
|
||||||
- **`transaction_service.py`** - Player transaction operations (trades, waivers, etc.)
|
|
||||||
- **`transaction_builder.py`** - Complex transaction building and validation
|
|
||||||
|
|
||||||
### Game Submission Services (NEW - January 2025)
|
|
||||||
- **`game_service.py`** - Game CRUD operations and scorecard submission support
|
|
||||||
- **`play_service.py`** - Play-by-play data management for game submissions
|
|
||||||
- **`decision_service.py`** - Pitching decision operations for game results
|
|
||||||
- **`sheets_service.py`** - Google Sheets integration for scorecard reading
|
|
||||||
|
|
||||||
#### GameService Key Methods
|
|
||||||
```python
|
|
||||||
class GameService(BaseService[Game]):
|
|
||||||
async def find_duplicate_game(season: int, week: int, game_num: int,
|
|
||||||
away_team_id: int, home_team_id: int) -> Optional[Game]
|
|
||||||
async def find_scheduled_game(season: int, week: int,
|
|
||||||
away_team_id: int, home_team_id: int) -> Optional[Game]
|
|
||||||
async def wipe_game_data(game_id: int) -> bool # Transaction rollback support
|
|
||||||
async def update_game_result(game_id: int, away_score: int, home_score: int,
|
|
||||||
away_manager_id: int, home_manager_id: int,
|
|
||||||
game_num: int, scorecard_url: str) -> Game
|
|
||||||
```
|
|
||||||
|
|
||||||
#### PlayService Key Methods
|
|
||||||
```python
|
|
||||||
class PlayService:
|
|
||||||
async def create_plays_batch(plays: List[Dict[str, Any]]) -> bool
|
|
||||||
async def delete_plays_for_game(game_id: int) -> bool # Transaction rollback
|
|
||||||
async def get_top_plays_by_wpa(game_id: int, limit: int = 3) -> List[Play]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### DecisionService Key Methods
|
|
||||||
```python
|
|
||||||
class DecisionService:
|
|
||||||
async def create_decisions_batch(decisions: List[Dict[str, Any]]) -> bool
|
|
||||||
async def delete_decisions_for_game(game_id: int) -> bool # Transaction rollback
|
|
||||||
def find_winning_losing_pitchers(decisions_data: List[Dict[str, Any]])
|
|
||||||
-> Tuple[Optional[int], Optional[int], Optional[int], List[int], List[int]]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### SheetsService Key Methods
|
|
||||||
```python
|
|
||||||
class SheetsService:
|
|
||||||
async def open_scorecard(sheet_url: str) -> pygsheets.Spreadsheet
|
|
||||||
async def read_setup_data(scorecard: pygsheets.Spreadsheet) -> Dict[str, Any]
|
|
||||||
async def read_playtable_data(scorecard: pygsheets.Spreadsheet) -> List[Dict[str, Any]]
|
|
||||||
async def read_pitching_decisions(scorecard: pygsheets.Spreadsheet) -> List[Dict[str, Any]]
|
|
||||||
async def read_box_score(scorecard: pygsheets.Spreadsheet) -> Dict[str, List[int]]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Transaction Rollback Pattern:**
|
|
||||||
The game submission services implement a 3-state transaction rollback pattern:
|
|
||||||
1. **PLAYS_POSTED**: Plays submitted → Rollback: Delete plays
|
|
||||||
2. **GAME_PATCHED**: Game updated → Rollback: Wipe game + Delete plays
|
|
||||||
3. **COMPLETE**: All data committed → No rollback needed
|
|
||||||
|
|
||||||
**Usage Example:**
|
|
||||||
```python
|
|
||||||
# Create plays (state: PLAYS_POSTED)
|
|
||||||
await play_service.create_plays_batch(plays_data)
|
|
||||||
rollback_state = "PLAYS_POSTED"
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Update game (state: GAME_PATCHED)
|
|
||||||
await game_service.update_game_result(game_id, ...)
|
|
||||||
rollback_state = "GAME_PATCHED"
|
|
||||||
|
|
||||||
# Create decisions (state: COMPLETE)
|
|
||||||
await decision_service.create_decisions_batch(decisions_data)
|
|
||||||
rollback_state = "COMPLETE"
|
|
||||||
except APIException as e:
|
|
||||||
# Rollback based on current state
|
|
||||||
if rollback_state == "GAME_PATCHED":
|
|
||||||
await game_service.wipe_game_data(game_id)
|
|
||||||
await play_service.delete_plays_for_game(game_id)
|
|
||||||
elif rollback_state == "PLAYS_POSTED":
|
|
||||||
await play_service.delete_plays_for_game(game_id)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Features
|
|
||||||
- **`custom_commands_service.py`** - User-created custom Discord commands
|
|
||||||
- **`help_commands_service.py`** - Admin-managed help system and documentation
|
|
||||||
|
|
||||||
## Caching Integration
|
|
||||||
|
|
||||||
Services support optional Redis caching via decorators:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from utils.decorators import cached_api_call, cached_single_item
|
|
||||||
|
|
||||||
class PlayerService(BaseService[Player]):
|
|
||||||
@cached_api_call(ttl=600) # Cache for 10 minutes
|
|
||||||
async def get_players_by_team(self, team_id: int, season: int) -> List[Player]:
|
|
||||||
return await self.get_all_items(params=[('team_id', team_id), ('season', season)])
|
|
||||||
|
|
||||||
@cached_single_item(ttl=300) # Cache for 5 minutes
|
|
||||||
async def get_player_by_name(self, name: str) -> Optional[Player]:
|
|
||||||
players = await self.get_by_field('name', name)
|
|
||||||
return players[0] if players else None
|
|
||||||
```
|
|
||||||
|
|
||||||
### Caching Features
|
|
||||||
- **Graceful degradation** - Works without Redis
|
|
||||||
- **Automatic key generation** based on method parameters
|
|
||||||
- **TTL support** with configurable expiration
|
|
||||||
- **Cache invalidation** patterns for data updates
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
All services use consistent error handling:
|
|
||||||
|
|
||||||
```python
|
|
||||||
try:
|
|
||||||
result = await some_service.get_data()
|
|
||||||
return result
|
|
||||||
except APIException as e:
|
|
||||||
logger.error("API error occurred", error=e)
|
|
||||||
raise # Re-raise for command handlers
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Unexpected error", error=e)
|
|
||||||
raise APIException(f"Service operation failed: {e}")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Exception Types
|
|
||||||
- **`APIException`** - API communication errors
|
|
||||||
- **`ValueError`** - Data validation errors
|
|
||||||
- **`ConnectionError`** - Network connectivity issues
|
|
||||||
|
|
||||||
## Usage Patterns
|
|
||||||
|
|
||||||
### Service Initialization
|
|
||||||
Services are typically initialized once and reused:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# In services/__init__.py
|
|
||||||
from .player_service import PlayerService
|
|
||||||
from models.player import Player
|
|
||||||
|
|
||||||
player_service = PlayerService(Player, 'players')
|
|
||||||
```
|
|
||||||
|
|
||||||
### Command Integration
|
|
||||||
Services integrate with Discord commands via the `@logged_command` decorator:
|
|
||||||
|
|
||||||
```python
|
|
||||||
@discord.app_commands.command(name="player")
|
|
||||||
@logged_command("/player")
|
|
||||||
async def player_info(self, interaction: discord.Interaction, name: str):
|
|
||||||
player = await player_service.get_player_by_name(name)
|
|
||||||
if not player:
|
|
||||||
await interaction.followup.send("Player not found")
|
|
||||||
return
|
|
||||||
|
|
||||||
embed = create_player_embed(player)
|
|
||||||
await interaction.followup.send(embed=embed)
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Response Format
|
|
||||||
|
|
||||||
Services handle the standard API response format:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"count": 150,
|
|
||||||
"players": [
|
|
||||||
{"id": 1, "name": "Player Name", ...},
|
|
||||||
{"id": 2, "name": "Another Player", ...}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The `BaseService._extract_items_and_count_from_response()` method automatically parses this format and returns typed model instances.
|
|
||||||
|
|
||||||
## Development Guidelines
|
|
||||||
|
|
||||||
### Adding New Services
|
|
||||||
1. **Inherit from BaseService** with appropriate model type
|
|
||||||
2. **Define specific business methods** beyond CRUD operations
|
|
||||||
3. **Add caching decorators** for expensive operations
|
|
||||||
4. **Include comprehensive logging** with structured context
|
|
||||||
5. **Handle edge cases** and provide meaningful error messages
|
|
||||||
|
|
||||||
### Service Method Patterns
|
|
||||||
- **Query methods** should return `List[T]` or `Optional[T]`
|
|
||||||
- **Mutation methods** should return the updated model or `None`
|
|
||||||
- **Search methods** should accept flexible parameters
|
|
||||||
- **Bulk operations** should handle batching efficiently
|
|
||||||
|
|
||||||
### Testing Services
|
|
||||||
- Use `aioresponses` for HTTP client mocking
|
|
||||||
- Test both success and error scenarios
|
|
||||||
- Validate model parsing and transformation
|
|
||||||
- Verify caching behavior when Redis is available
|
|
||||||
|
|
||||||
## Environment Integration
|
|
||||||
|
|
||||||
Services respect environment configuration:
|
|
||||||
- **`DB_URL`** - Database API endpoint
|
|
||||||
- **`API_TOKEN`** - Authentication token
|
|
||||||
- **`REDIS_URL`** - Optional caching backend
|
|
||||||
- **`LOG_LEVEL`** - Logging verbosity
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
|
|
||||||
### Optimization Strategies
|
|
||||||
- **Connection pooling** via global API client
|
|
||||||
- **Response caching** for frequently accessed data
|
|
||||||
- **Batch operations** for bulk data processing
|
|
||||||
- **Lazy loading** for expensive computations
|
|
||||||
|
|
||||||
### Monitoring
|
|
||||||
- All operations are logged with timing information
|
|
||||||
- Cache hit/miss ratios are tracked
|
|
||||||
- API error rates are monitored
|
|
||||||
- Service response times are measured
|
|
||||||
|
|
||||||
## Transaction Builder Enhancements (January 2025)
|
|
||||||
|
|
||||||
### Enhanced sWAR Calculations
|
|
||||||
The `TransactionBuilder` now includes comprehensive sWAR (sum of WARA) tracking for both current moves and pre-existing transactions:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class TransactionBuilder:
|
|
||||||
async def validate_transaction(self, next_week: Optional[int] = None) -> RosterValidationResult:
|
|
||||||
"""
|
|
||||||
Validate transaction with optional pre-existing transaction analysis.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
next_week: Week to check for existing transactions (includes pre-existing analysis)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
RosterValidationResult with projected roster counts and sWAR values
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pre-existing Transaction Support
|
|
||||||
When `next_week` is provided, the transaction builder:
|
|
||||||
- **Fetches existing transactions** for the specified week via API
|
|
||||||
- **Calculates roster impact** of scheduled moves using organizational team matching
|
|
||||||
- **Tracks sWAR changes** separately for Major League and Minor League rosters
|
|
||||||
- **Provides contextual display** for user transparency
|
|
||||||
|
|
||||||
#### Usage Examples
|
|
||||||
```python
|
|
||||||
# Basic validation (current functionality)
|
|
||||||
validation = await builder.validate_transaction()
|
|
||||||
|
|
||||||
# Enhanced validation with pre-existing transactions
|
|
||||||
current_week = await league_service.get_current_week()
|
|
||||||
validation = await builder.validate_transaction(next_week=current_week + 1)
|
|
||||||
|
|
||||||
# Access enhanced data
|
|
||||||
print(f"Projected ML sWAR: {validation.major_league_swar}")
|
|
||||||
print(f"Pre-existing impact: {validation.pre_existing_transactions_note}")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Enhanced RosterValidationResult
|
|
||||||
New fields provide complete transaction context:
|
|
||||||
|
|
||||||
```python
|
|
||||||
@dataclass
|
|
||||||
class RosterValidationResult:
|
|
||||||
# Existing fields...
|
|
||||||
major_league_swar: float = 0.0
|
|
||||||
minor_league_swar: float = 0.0
|
|
||||||
pre_existing_ml_swar_change: float = 0.0
|
|
||||||
pre_existing_mil_swar_change: float = 0.0
|
|
||||||
pre_existing_transaction_count: int = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def major_league_swar_status(self) -> str:
|
|
||||||
"""Formatted sWAR display with emoji."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pre_existing_transactions_note(self) -> str:
|
|
||||||
"""User-friendly note about pre-existing moves impact."""
|
|
||||||
```
|
|
||||||
|
|
||||||
### Organizational Team Matching
|
|
||||||
Transaction processing now uses sophisticated team matching:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Enhanced logic using Team.is_same_organization()
|
|
||||||
if transaction.oldteam.is_same_organization(self.team):
|
|
||||||
# Accurately determine which roster the player is leaving
|
|
||||||
from_roster_type = transaction.oldteam.roster_type()
|
|
||||||
|
|
||||||
if from_roster_type == RosterType.MAJOR_LEAGUE:
|
|
||||||
# Update ML roster and sWAR
|
|
||||||
elif from_roster_type == RosterType.MINOR_LEAGUE:
|
|
||||||
# Update MiL roster and sWAR
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Improvements
|
|
||||||
- **Accurate Roster Detection**: Uses `Team.roster_type()` instead of assumptions
|
|
||||||
- **Organization Awareness**: Properly handles PORMIL, PORIL transactions for POR team
|
|
||||||
- **Separate sWAR Tracking**: ML and MiL sWAR changes tracked independently
|
|
||||||
- **Performance Optimization**: Pre-existing transactions loaded once and cached
|
|
||||||
- **User Transparency**: Clear display of how pre-existing moves affect calculations
|
|
||||||
|
|
||||||
### Implementation Details
|
|
||||||
- **Backwards Compatible**: All existing functionality preserved
|
|
||||||
- **Optional Enhancement**: `next_week` parameter is optional
|
|
||||||
- **Error Handling**: Graceful fallback if pre-existing transactions cannot be loaded
|
|
||||||
- **Caching**: Transaction and roster data cached to avoid repeated API calls
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Next Steps for AI Agents:**
|
|
||||||
1. Review existing service implementations for patterns
|
|
||||||
2. Check the corresponding model definitions in `/models`
|
|
||||||
3. Understand the caching decorators in `/utils/decorators.py`
|
|
||||||
4. Follow the error handling patterns established in `BaseService`
|
|
||||||
5. Use structured logging with contextual information
|
|
||||||
6. Consider pre-existing transaction impact when building new transaction features
|
|
||||||
364
tasks/README.md
364
tasks/README.md
@ -1,364 +0,0 @@
|
|||||||
# Tasks Directory
|
|
||||||
|
|
||||||
The tasks directory contains automated background tasks for Discord Bot v2.0. These tasks handle periodic maintenance, data cleanup, and scheduled operations that run independently of user interactions.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Task System Design
|
|
||||||
Tasks in Discord Bot v2.0 follow these patterns:
|
|
||||||
- **Discord.py tasks** using the `@tasks.loop` decorator
|
|
||||||
- **Structured logging** with contextual information
|
|
||||||
- **Error handling** with graceful degradation
|
|
||||||
- **Guild-specific operations** respecting bot permissions
|
|
||||||
- **Configurable intervals** via task decorators
|
|
||||||
|
|
||||||
### Base Task Pattern
|
|
||||||
All tasks follow a consistent structure:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from discord.ext import tasks
|
|
||||||
from utils.logging import get_contextual_logger
|
|
||||||
|
|
||||||
class ExampleTask:
|
|
||||||
def __init__(self, bot: commands.Bot):
|
|
||||||
self.bot = bot
|
|
||||||
self.logger = get_contextual_logger(f'{__name__}.ExampleTask')
|
|
||||||
self.task_loop.start()
|
|
||||||
|
|
||||||
def cog_unload(self):
|
|
||||||
"""Stop the task when cog is unloaded."""
|
|
||||||
self.task_loop.cancel()
|
|
||||||
|
|
||||||
@tasks.loop(hours=24) # Run daily
|
|
||||||
async def task_loop(self):
|
|
||||||
"""Main task implementation."""
|
|
||||||
try:
|
|
||||||
# Task logic here
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error("Task failed", error=e)
|
|
||||||
|
|
||||||
@task_loop.before_loop
|
|
||||||
async def before_task(self):
|
|
||||||
"""Wait for bot to be ready before starting."""
|
|
||||||
await self.bot.wait_until_ready()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Current Tasks
|
|
||||||
|
|
||||||
### Custom Command Cleanup (`custom_command_cleanup.py`)
|
|
||||||
**Purpose:** Automated cleanup system for user-created custom commands
|
|
||||||
|
|
||||||
**Schedule:** Daily (24 hours)
|
|
||||||
|
|
||||||
**Operations:**
|
|
||||||
- **Warning Phase:** Notifies users about commands at risk (unused for 60+ days)
|
|
||||||
- **Deletion Phase:** Removes commands unused for 90+ days
|
|
||||||
- **Admin Reporting:** Sends cleanup summaries to admin channels
|
|
||||||
|
|
||||||
#### Key Features
|
|
||||||
- **User Notifications:** Direct messages to command creators
|
|
||||||
- **Grace Period:** 30-day warning before deletion
|
|
||||||
- **Admin Transparency:** Optional summary reports
|
|
||||||
- **Bulk Operations:** Efficient batch processing
|
|
||||||
- **Error Resilience:** Continues operation despite individual failures
|
|
||||||
|
|
||||||
#### Configuration
|
|
||||||
The cleanup task respects guild settings and permissions:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Configuration via get_config()
|
|
||||||
guild_id = config.guild_id # Target guild
|
|
||||||
admin_channels = ['admin', 'bot-logs'] # Admin notification channels
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Notification System
|
|
||||||
**Warning Embed (30 days before deletion):**
|
|
||||||
- Lists commands at risk
|
|
||||||
- Shows days since last use
|
|
||||||
- Provides usage instructions
|
|
||||||
- Links to command management
|
|
||||||
|
|
||||||
**Deletion Embed (after deletion):**
|
|
||||||
- Lists deleted commands
|
|
||||||
- Shows final usage statistics
|
|
||||||
- Provides recreation instructions
|
|
||||||
- Explains cleanup policy
|
|
||||||
|
|
||||||
#### Admin Summary
|
|
||||||
Optional admin channel reporting includes:
|
|
||||||
- Number of warnings sent
|
|
||||||
- Number of commands deleted
|
|
||||||
- Current system statistics
|
|
||||||
- Next cleanup schedule
|
|
||||||
|
|
||||||
## Task Lifecycle
|
|
||||||
|
|
||||||
### Initialization
|
|
||||||
Tasks are initialized when the bot starts:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# In bot startup
|
|
||||||
def setup_cleanup_task(bot: commands.Bot) -> CustomCommandCleanupTask:
|
|
||||||
return CustomCommandCleanupTask(bot)
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
cleanup_task = setup_cleanup_task(bot)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Execution Flow
|
|
||||||
1. **Bot Ready Check:** Wait for `bot.wait_until_ready()`
|
|
||||||
2. **Guild Validation:** Verify bot has access to configured guild
|
|
||||||
3. **Permission Checks:** Ensure bot can send messages/DMs
|
|
||||||
4. **Main Operation:** Execute task logic with error handling
|
|
||||||
5. **Logging:** Record operation results and performance metrics
|
|
||||||
6. **Cleanup:** Reset state for next iteration
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
Tasks implement comprehensive error handling:
|
|
||||||
|
|
||||||
```python
|
|
||||||
async def task_operation(self):
|
|
||||||
try:
|
|
||||||
# Main task logic
|
|
||||||
result = await self.perform_operation()
|
|
||||||
self.logger.info("Task completed", result=result)
|
|
||||||
except SpecificException as e:
|
|
||||||
self.logger.warning("Recoverable error", error=e)
|
|
||||||
# Continue with degraded functionality
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error("Task failed", error=e)
|
|
||||||
# Task will retry on next interval
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Patterns
|
|
||||||
|
|
||||||
### Creating New Tasks
|
|
||||||
|
|
||||||
1. **Inherit from Base Pattern**
|
|
||||||
```python
|
|
||||||
class NewTask:
|
|
||||||
def __init__(self, bot: commands.Bot):
|
|
||||||
self.bot = bot
|
|
||||||
self.logger = get_contextual_logger(f'{__name__}.NewTask')
|
|
||||||
self.main_loop.start()
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Configure Task Schedule**
|
|
||||||
```python
|
|
||||||
@tasks.loop(minutes=30) # Every 30 minutes
|
|
||||||
# or
|
|
||||||
@tasks.loop(hours=6) # Every 6 hours
|
|
||||||
# or
|
|
||||||
@tasks.loop(time=datetime.time(hour=3)) # Daily at 3 AM UTC
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Implement Before Loop**
|
|
||||||
```python
|
|
||||||
@main_loop.before_loop
|
|
||||||
async def before_loop(self):
|
|
||||||
await self.bot.wait_until_ready()
|
|
||||||
self.logger.info("Task initialized and ready")
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Add Cleanup Handling**
|
|
||||||
```python
|
|
||||||
def cog_unload(self):
|
|
||||||
self.main_loop.cancel()
|
|
||||||
self.logger.info("Task stopped")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task Categories
|
|
||||||
|
|
||||||
#### Maintenance Tasks
|
|
||||||
- **Data cleanup** (expired records, unused resources)
|
|
||||||
- **Cache management** (clear stale entries, optimize storage)
|
|
||||||
- **Log rotation** (archive old logs, manage disk space)
|
|
||||||
|
|
||||||
#### User Management
|
|
||||||
- **Inactive user cleanup** (remove old user data)
|
|
||||||
- **Permission auditing** (validate role assignments)
|
|
||||||
- **Usage analytics** (collect usage statistics)
|
|
||||||
|
|
||||||
#### System Monitoring
|
|
||||||
- **Health checks** (verify system components)
|
|
||||||
- **Performance monitoring** (track response times)
|
|
||||||
- **Error rate tracking** (monitor failure rates)
|
|
||||||
|
|
||||||
### Task Configuration
|
|
||||||
|
|
||||||
#### Environment Variables
|
|
||||||
Tasks respect standard bot configuration:
|
|
||||||
```python
|
|
||||||
GUILD_ID=12345... # Target Discord guild
|
|
||||||
LOG_LEVEL=INFO # Logging verbosity
|
|
||||||
REDIS_URL=redis://... # Optional caching backend
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Runtime Configuration
|
|
||||||
Tasks use the central config system:
|
|
||||||
```python
|
|
||||||
from config import get_config
|
|
||||||
|
|
||||||
config = get_config()
|
|
||||||
guild = self.bot.get_guild(config.guild_id)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logging and Monitoring
|
|
||||||
|
|
||||||
### Structured Logging
|
|
||||||
Tasks use contextual logging for observability:
|
|
||||||
|
|
||||||
```python
|
|
||||||
self.logger.info(
|
|
||||||
"Cleanup task starting",
|
|
||||||
guild_id=guild.id,
|
|
||||||
commands_at_risk=len(at_risk_commands)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.logger.warning(
|
|
||||||
"User DM failed",
|
|
||||||
user_id=user.id,
|
|
||||||
reason="DMs disabled"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.logger.error(
|
|
||||||
"Task operation failed",
|
|
||||||
operation="delete_commands",
|
|
||||||
error=str(e)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Performance Tracking
|
|
||||||
Tasks log timing and performance metrics:
|
|
||||||
|
|
||||||
```python
|
|
||||||
start_time = datetime.utcnow()
|
|
||||||
# ... task operations ...
|
|
||||||
duration = (datetime.utcnow() - start_time).total_seconds()
|
|
||||||
|
|
||||||
self.logger.info(
|
|
||||||
"Task completed",
|
|
||||||
duration_seconds=duration,
|
|
||||||
operations_completed=operation_count
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Recovery
|
|
||||||
Tasks implement retry logic and graceful degradation:
|
|
||||||
|
|
||||||
```python
|
|
||||||
async def process_with_retry(self, operation, max_retries=3):
|
|
||||||
for attempt in range(max_retries):
|
|
||||||
try:
|
|
||||||
return await operation()
|
|
||||||
except RecoverableError as e:
|
|
||||||
if attempt == max_retries - 1:
|
|
||||||
raise
|
|
||||||
await asyncio.sleep(2 ** attempt) # Exponential backoff
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Strategies
|
|
||||||
|
|
||||||
### Unit Testing Tasks
|
|
||||||
```python
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_custom_command_cleanup():
|
|
||||||
# Mock bot and services
|
|
||||||
bot = AsyncMock()
|
|
||||||
task = CustomCommandCleanupTask(bot)
|
|
||||||
|
|
||||||
# Mock service responses
|
|
||||||
with patch('services.custom_commands_service') as mock_service:
|
|
||||||
mock_service.get_commands_needing_warning.return_value = []
|
|
||||||
|
|
||||||
# Test task execution
|
|
||||||
await task.cleanup_task()
|
|
||||||
|
|
||||||
# Verify service calls
|
|
||||||
mock_service.get_commands_needing_warning.assert_called_once()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Integration Testing
|
|
||||||
```python
|
|
||||||
@pytest.mark.integration
|
|
||||||
async def test_cleanup_task_with_real_data():
|
|
||||||
# Test with actual Discord bot instance
|
|
||||||
# Use test guild and test data
|
|
||||||
# Verify real Discord API interactions
|
|
||||||
```
|
|
||||||
|
|
||||||
### Performance Testing
|
|
||||||
```python
|
|
||||||
@pytest.mark.performance
|
|
||||||
async def test_cleanup_task_performance():
|
|
||||||
# Test with large datasets
|
|
||||||
# Measure execution time
|
|
||||||
# Verify memory usage
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
### Permission Validation
|
|
||||||
Tasks verify bot permissions before operations:
|
|
||||||
|
|
||||||
```python
|
|
||||||
async def check_permissions(self, guild: discord.Guild) -> bool:
|
|
||||||
"""Verify bot has required permissions."""
|
|
||||||
bot_member = guild.me
|
|
||||||
|
|
||||||
# Check for required permissions
|
|
||||||
if not bot_member.guild_permissions.send_messages:
|
|
||||||
self.logger.warning("Missing send_messages permission")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
```
|
|
||||||
|
|
||||||
### Data Privacy
|
|
||||||
Tasks handle user data responsibly:
|
|
||||||
- **Minimal data access** - Only access required data
|
|
||||||
- **Secure logging** - Avoid logging sensitive information
|
|
||||||
- **GDPR compliance** - Respect user data rights
|
|
||||||
- **Permission respect** - Honor user privacy settings
|
|
||||||
|
|
||||||
### Rate Limiting
|
|
||||||
Tasks implement Discord API rate limiting:
|
|
||||||
|
|
||||||
```python
|
|
||||||
async def send_notifications_with_rate_limiting(self, notifications):
|
|
||||||
"""Send notifications with rate limiting."""
|
|
||||||
for notification in notifications:
|
|
||||||
try:
|
|
||||||
await self.send_notification(notification)
|
|
||||||
await asyncio.sleep(1) # Avoid rate limits
|
|
||||||
except discord.HTTPException as e:
|
|
||||||
if e.status == 429: # Rate limited
|
|
||||||
retry_after = e.response.headers.get('Retry-After', 60)
|
|
||||||
await asyncio.sleep(int(retry_after))
|
|
||||||
```
|
|
||||||
|
|
||||||
## Future Task Ideas
|
|
||||||
|
|
||||||
### Potential Additions
|
|
||||||
- **Database maintenance** - Optimize database performance
|
|
||||||
- **Backup automation** - Create data backups
|
|
||||||
- **Usage analytics** - Generate usage reports
|
|
||||||
- **Health monitoring** - System health checks
|
|
||||||
- **Cache warming** - Pre-populate frequently accessed data
|
|
||||||
|
|
||||||
### Scalability Patterns
|
|
||||||
- **Task queues** - Distribute work across multiple workers
|
|
||||||
- **Sharding support** - Handle multiple Discord guilds
|
|
||||||
- **Load balancing** - Distribute task execution
|
|
||||||
- **Monitoring integration** - External monitoring systems
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Next Steps for AI Agents:**
|
|
||||||
1. Review the existing cleanup task implementation
|
|
||||||
2. Understand the Discord.py tasks framework
|
|
||||||
3. Follow the structured logging patterns
|
|
||||||
4. Implement proper error handling and recovery
|
|
||||||
5. Consider guild permissions and user privacy
|
|
||||||
6. Test tasks thoroughly before deployment
|
|
||||||
293
tests/README.md
293
tests/README.md
@ -1,293 +0,0 @@
|
|||||||
# Testing Guide for Discord Bot v2.0
|
|
||||||
|
|
||||||
This document provides guidance on testing strategies, patterns, and lessons learned during the development of the Discord Bot v2.0 test suite.
|
|
||||||
|
|
||||||
## Test Structure Overview
|
|
||||||
|
|
||||||
```
|
|
||||||
tests/
|
|
||||||
├── README.md # This guide
|
|
||||||
├── __init__.py # Test package
|
|
||||||
├── fixtures/ # Test data fixtures
|
|
||||||
├── test_config.py # Configuration tests
|
|
||||||
├── test_constants.py # Constants tests
|
|
||||||
├── test_exceptions.py # Exception handling tests
|
|
||||||
├── test_models.py # Pydantic model tests
|
|
||||||
├── test_services.py # Service layer tests (25 tests)
|
|
||||||
└── test_api_client_with_aioresponses.py # API client HTTP tests (19 tests)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Total Coverage**: 44 comprehensive tests covering all core functionality.
|
|
||||||
|
|
||||||
## Key Testing Patterns
|
|
||||||
|
|
||||||
### 1. HTTP Testing with aioresponsesf
|
|
||||||
|
|
||||||
**✅ Recommended Approach:**
|
|
||||||
```python
|
|
||||||
from aioresponses import aioresponses
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_api_request(api_client):
|
|
||||||
with aioresponses() as m:
|
|
||||||
m.get(
|
|
||||||
"https://api.example.com/v3/players/1",
|
|
||||||
payload={"id": 1, "name": "Test Player"},
|
|
||||||
status=200
|
|
||||||
)
|
|
||||||
|
|
||||||
result = await api_client.get("players", object_id=1)
|
|
||||||
assert result["name"] == "Test Player"
|
|
||||||
```
|
|
||||||
|
|
||||||
**❌ Avoid Complex AsyncMock:**
|
|
||||||
We initially tried mocking aiohttp's async context managers manually with AsyncMock, which led to complex, brittle tests that failed due to coroutine protocol issues.
|
|
||||||
|
|
||||||
### 2. Service Layer Testing
|
|
||||||
|
|
||||||
**✅ Complete Model Data:**
|
|
||||||
Always provide complete model data that satisfies Pydantic validation:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def create_player_data(self, player_id: int, name: str, **kwargs):
|
|
||||||
"""Create complete player data for testing."""
|
|
||||||
base_data = {
|
|
||||||
'id': player_id,
|
|
||||||
'name': name,
|
|
||||||
'wara': 2.5, # Required field
|
|
||||||
'season': 12, # Required field
|
|
||||||
'team_id': team_id, # Required field
|
|
||||||
'image': f'https://example.com/player{player_id}.jpg', # Required field
|
|
||||||
'pos_1': position, # Required field
|
|
||||||
}
|
|
||||||
base_data.update(kwargs)
|
|
||||||
return base_data
|
|
||||||
```
|
|
||||||
|
|
||||||
**❌ Partial Model Data:**
|
|
||||||
Providing incomplete data leads to Pydantic validation errors that are hard to debug.
|
|
||||||
|
|
||||||
### 3. API Response Format Testing
|
|
||||||
|
|
||||||
Our API returns responses in this format:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"count": 25,
|
|
||||||
"players": [...]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**✅ Test Both Formats:**
|
|
||||||
```python
|
|
||||||
# Test the count + list format
|
|
||||||
mock_data = {
|
|
||||||
"count": 2,
|
|
||||||
"players": [player1_data, player2_data]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test single object format (for get_by_id)
|
|
||||||
mock_data = player1_data
|
|
||||||
```
|
|
||||||
|
|
||||||
## Lessons Learned
|
|
||||||
|
|
||||||
### 1. aiohttp Testing Complexity
|
|
||||||
|
|
||||||
**Problem**: Manually mocking aiohttp's async context managers is extremely complex and error-prone.
|
|
||||||
|
|
||||||
**Solution**: Use `aioresponses` library specifically designed for this purpose.
|
|
||||||
|
|
||||||
**Code Example**:
|
|
||||||
```bash
|
|
||||||
pip install aioresponses>=0.7.4
|
|
||||||
```
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Clean, readable, reliable
|
|
||||||
with aioresponses() as m:
|
|
||||||
m.get("https://api.example.com/endpoint", payload=expected_data)
|
|
||||||
result = await client.get("endpoint")
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Pydantic Model Validation in Tests
|
|
||||||
|
|
||||||
**Problem**: Our models have many required fields. Partial test data causes validation errors.
|
|
||||||
|
|
||||||
**Solution**: Create helper functions that generate complete, valid model data.
|
|
||||||
|
|
||||||
**Pattern**:
|
|
||||||
```python
|
|
||||||
def create_model_data(self, id: int, name: str, **overrides):
|
|
||||||
"""Create complete model data with all required fields."""
|
|
||||||
base_data = {
|
|
||||||
# All required fields with sensible defaults
|
|
||||||
'id': id,
|
|
||||||
'name': name,
|
|
||||||
'required_field1': 'default_value',
|
|
||||||
'required_field2': 42,
|
|
||||||
}
|
|
||||||
base_data.update(overrides)
|
|
||||||
return base_data
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Async Context Manager Mocking
|
|
||||||
|
|
||||||
**Problem**: This doesn't work reliably:
|
|
||||||
```python
|
|
||||||
# ❌ Brittle and complex
|
|
||||||
mock_session.get.return_value.__aenter__ = AsyncMock(return_value=mock_response)
|
|
||||||
mock_session.get.return_value.__aexit__ = AsyncMock(return_value=None)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution**: Use specialized libraries or patch at higher levels:
|
|
||||||
```python
|
|
||||||
# ✅ Clean with aioresponses
|
|
||||||
with aioresponses() as m:
|
|
||||||
m.get("url", payload=data)
|
|
||||||
# Test the actual HTTP call
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Service Layer Mocking Strategy
|
|
||||||
|
|
||||||
**✅ Mock at the Client Level:**
|
|
||||||
```python
|
|
||||||
@pytest.fixture
|
|
||||||
def player_service_instance(self, mock_client):
|
|
||||||
service = PlayerService()
|
|
||||||
service._client = mock_client # Inject mock client
|
|
||||||
return service
|
|
||||||
```
|
|
||||||
|
|
||||||
This allows testing service logic while controlling API responses.
|
|
||||||
|
|
||||||
### 5. Global Instance Testing
|
|
||||||
|
|
||||||
**Pattern for Singleton Services:**
|
|
||||||
```python
|
|
||||||
def test_global_service_independence():
|
|
||||||
service1 = PlayerService()
|
|
||||||
service2 = PlayerService()
|
|
||||||
|
|
||||||
# Should be different instances
|
|
||||||
assert service1 is not service2
|
|
||||||
# But same configuration
|
|
||||||
assert service1.endpoint == service2.endpoint
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Anti-Patterns to Avoid
|
|
||||||
|
|
||||||
### 1. ❌ Incomplete Test Data
|
|
||||||
```python
|
|
||||||
# This will fail Pydantic validation
|
|
||||||
mock_data = {'id': 1, 'name': 'Test'} # Missing required fields
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. ❌ Complex Manual Mocking
|
|
||||||
```python
|
|
||||||
# Avoid complex AsyncMock setups for HTTP clients
|
|
||||||
mock_response = AsyncMock()
|
|
||||||
mock_response.__aenter__ = AsyncMock(...) # Too complex
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. ❌ Testing Implementation Details
|
|
||||||
```python
|
|
||||||
# Don't test internal method calls
|
|
||||||
assert mock_client.get.call_count == 2 # Brittle
|
|
||||||
# Instead test behavior
|
|
||||||
assert len(result) == 2 # What matters to users
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. ❌ Mixing Test Concerns
|
|
||||||
```python
|
|
||||||
# Don't test multiple unrelated things in one test
|
|
||||||
def test_everything(): # Too broad
|
|
||||||
# Test HTTP client
|
|
||||||
# Test service logic
|
|
||||||
# Test model validation
|
|
||||||
# All in one test - hard to debug
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices Summary
|
|
||||||
|
|
||||||
### ✅ Do:
|
|
||||||
1. **Use aioresponses** for HTTP client testing
|
|
||||||
2. **Create complete model data** with helper functions
|
|
||||||
3. **Test behavior, not implementation** details
|
|
||||||
4. **Mock at appropriate levels** (client level for services)
|
|
||||||
5. **Use realistic data** that matches actual API responses
|
|
||||||
6. **Test error scenarios** as thoroughly as happy paths
|
|
||||||
7. **Keep tests focused** on single responsibilities
|
|
||||||
|
|
||||||
### ❌ Don't:
|
|
||||||
1. **Manually mock async context managers** - use specialized tools
|
|
||||||
2. **Use partial model data** - always provide complete valid data
|
|
||||||
3. **Test implementation details** - focus on behavior
|
|
||||||
4. **Mix multiple concerns** in single tests
|
|
||||||
5. **Ignore error paths** - test failure scenarios
|
|
||||||
6. **Skip integration scenarios** - test realistic workflows
|
|
||||||
|
|
||||||
## Running Tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run all tests
|
|
||||||
pytest
|
|
||||||
|
|
||||||
# Run specific test files
|
|
||||||
pytest tests/test_services.py
|
|
||||||
pytest tests/test_api_client_with_aioresponses.py
|
|
||||||
|
|
||||||
# Run with coverage
|
|
||||||
pytest --cov=api --cov=services
|
|
||||||
|
|
||||||
# Run with verbose output
|
|
||||||
pytest -v
|
|
||||||
|
|
||||||
# Run specific test patterns
|
|
||||||
pytest -k "test_player" -v
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding New Tests
|
|
||||||
|
|
||||||
### For New API Endpoints:
|
|
||||||
1. Add aioresponses-based tests in `test_api_client_with_aioresponses.py`
|
|
||||||
2. Follow existing patterns for success/error scenarios
|
|
||||||
|
|
||||||
### For New Services:
|
|
||||||
1. Add service tests in `test_services.py`
|
|
||||||
2. Create helper functions for complete model data
|
|
||||||
3. Mock at the client level, not HTTP level
|
|
||||||
|
|
||||||
### For New Models:
|
|
||||||
1. Add model tests in `test_models.py`
|
|
||||||
2. Test validation, serialization, and edge cases
|
|
||||||
3. Use `from_api_data()` pattern for realistic data
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
Core testing dependencies in `requirements.txt`:
|
|
||||||
```
|
|
||||||
pytest>=7.0.0
|
|
||||||
pytest-asyncio>=0.21.0
|
|
||||||
pytest-mock>=3.10.0
|
|
||||||
aioresponses>=0.7.4 # Essential for HTTP testing
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting Common Issues
|
|
||||||
|
|
||||||
### "coroutine object does not support async context manager"
|
|
||||||
- **Cause**: Manually mocking aiohttp async context managers
|
|
||||||
- **Solution**: Use aioresponses instead of manual mocking
|
|
||||||
|
|
||||||
### "ValidationError: Field required"
|
|
||||||
- **Cause**: Incomplete test data for Pydantic models
|
|
||||||
- **Solution**: Use helper functions that provide all required fields
|
|
||||||
|
|
||||||
### "AssertionError: Regex pattern did not match"
|
|
||||||
- **Cause**: Exception message doesn't match expected pattern
|
|
||||||
- **Solution**: Check actual error message and adjust test expectations
|
|
||||||
|
|
||||||
### Tests hanging or timing out
|
|
||||||
- **Cause**: Unclosed aiohttp sessions or improper async handling
|
|
||||||
- **Solution**: Ensure proper session cleanup and use async context managers
|
|
||||||
|
|
||||||
This guide should help maintain high-quality, reliable tests as the project grows!
|
|
||||||
941
utils/README.md
941
utils/README.md
@ -1,941 +0,0 @@
|
|||||||
# Utils Package Documentation
|
|
||||||
**Discord Bot v2.0 - Utility Functions and Helpers**
|
|
||||||
|
|
||||||
This package contains utility functions, helpers, and shared components used throughout the Discord bot application.
|
|
||||||
|
|
||||||
## 📋 Table of Contents
|
|
||||||
|
|
||||||
1. [**Structured Logging**](#-structured-logging) - Contextual logging with Discord integration
|
|
||||||
2. [**Redis Caching**](#-redis-caching) - Optional performance caching system
|
|
||||||
3. [**Command Decorators**](#-command-decorators) - Boilerplate reduction decorators
|
|
||||||
4. [**Future Utilities**](#-future-utilities) - Planned utility modules
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 Structured Logging
|
|
||||||
|
|
||||||
**Location:** `utils/logging.py`
|
|
||||||
**Purpose:** Provides hybrid logging system with contextual information for Discord bot debugging and monitoring.
|
|
||||||
|
|
||||||
### **Quick Start**
|
|
||||||
|
|
||||||
```python
|
|
||||||
from utils.logging import get_contextual_logger, set_discord_context
|
|
||||||
|
|
||||||
class YourCommandCog(commands.Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
self.logger = get_contextual_logger(f'{__name__}.YourCommandCog')
|
|
||||||
|
|
||||||
async def your_command(self, interaction: discord.Interaction, param: str):
|
|
||||||
# Set Discord context for all subsequent log entries
|
|
||||||
set_discord_context(
|
|
||||||
interaction=interaction,
|
|
||||||
command="/your-command",
|
|
||||||
param_value=param
|
|
||||||
)
|
|
||||||
|
|
||||||
# Start operation timing and get trace ID
|
|
||||||
trace_id = self.logger.start_operation("your_command_operation")
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.logger.info("Command started")
|
|
||||||
|
|
||||||
# Your command logic here
|
|
||||||
result = await some_api_call(param)
|
|
||||||
self.logger.debug("API call completed", result_count=len(result))
|
|
||||||
|
|
||||||
self.logger.info("Command completed successfully")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error("Command failed", error=e)
|
|
||||||
self.logger.end_operation(trace_id, "failed")
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
self.logger.end_operation(trace_id, "completed")
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Key Features**
|
|
||||||
|
|
||||||
#### **🎯 Contextual Information**
|
|
||||||
Every log entry automatically includes:
|
|
||||||
- **Discord Context**: User ID, guild ID, guild name, channel ID
|
|
||||||
- **Command Context**: Command name, parameters
|
|
||||||
- **Operation Context**: Trace ID, operation name, execution duration
|
|
||||||
- **Custom Fields**: Additional context via keyword arguments
|
|
||||||
|
|
||||||
#### **⏱️ Automatic Timing & Tracing**
|
|
||||||
```python
|
|
||||||
trace_id = self.logger.start_operation("complex_operation")
|
|
||||||
# ... do work ...
|
|
||||||
self.logger.info("Operation in progress") # Includes duration_ms in extras
|
|
||||||
# ... more work ...
|
|
||||||
self.logger.end_operation(trace_id, "completed") # Final timing log
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key Behavior:**
|
|
||||||
- **`trace_id`**: Promoted to **standard JSON key** (root level) for easy filtering
|
|
||||||
- **`duration_ms`**: Available in **extras** when timing is active (optional field)
|
|
||||||
- **Context**: All operation context preserved throughout the async operation
|
|
||||||
|
|
||||||
#### **🔗 Request Tracing**
|
|
||||||
Track a single request through all log entries using trace IDs:
|
|
||||||
```bash
|
|
||||||
# Find all logs for a specific request (trace_id is now a standard key)
|
|
||||||
jq 'select(.trace_id == "abc12345")' logs/discord_bot_v2.json
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **📤 Hybrid Output**
|
|
||||||
- **Console**: Human-readable for development
|
|
||||||
- **Traditional File** (`discord_bot_v2.log`): Human-readable with debug info
|
|
||||||
- **JSON File** (`discord_bot_v2.json`): Structured for analysis
|
|
||||||
|
|
||||||
### **API Reference**
|
|
||||||
|
|
||||||
#### **Core Functions**
|
|
||||||
|
|
||||||
**`get_contextual_logger(logger_name: str) -> ContextualLogger`**
|
|
||||||
```python
|
|
||||||
# Get a logger instance for your module
|
|
||||||
logger = get_contextual_logger(f'{__name__}.MyClass')
|
|
||||||
```
|
|
||||||
|
|
||||||
**`set_discord_context(interaction=None, user_id=None, guild_id=None, **kwargs)`**
|
|
||||||
```python
|
|
||||||
# Set context from Discord interaction (recommended)
|
|
||||||
set_discord_context(interaction=interaction, command="/player", player_name="Mike Trout")
|
|
||||||
|
|
||||||
# Or set context manually
|
|
||||||
set_discord_context(user_id="123456", guild_id="987654", custom_field="value")
|
|
||||||
```
|
|
||||||
|
|
||||||
**`clear_context()`**
|
|
||||||
```python
|
|
||||||
# Clear the current logging context (usually not needed)
|
|
||||||
clear_context()
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **ContextualLogger Methods**
|
|
||||||
|
|
||||||
**`start_operation(operation_name: str = None) -> str`**
|
|
||||||
```python
|
|
||||||
# Start timing and get trace ID
|
|
||||||
trace_id = logger.start_operation("player_search")
|
|
||||||
```
|
|
||||||
|
|
||||||
**`end_operation(trace_id: str, operation_result: str = "completed")`**
|
|
||||||
```python
|
|
||||||
# End operation and log final duration
|
|
||||||
logger.end_operation(trace_id, "completed")
|
|
||||||
# or
|
|
||||||
logger.end_operation(trace_id, "failed")
|
|
||||||
```
|
|
||||||
|
|
||||||
**`info(message: str, **kwargs)`**
|
|
||||||
```python
|
|
||||||
logger.info("Player found", player_id=123, team_name="Yankees")
|
|
||||||
```
|
|
||||||
|
|
||||||
**`debug(message: str, **kwargs)`**
|
|
||||||
```python
|
|
||||||
logger.debug("API call started", endpoint="players/search", timeout=30)
|
|
||||||
```
|
|
||||||
|
|
||||||
**`warning(message: str, **kwargs)`**
|
|
||||||
```python
|
|
||||||
logger.warning("Multiple players found", candidates=["Player A", "Player B"])
|
|
||||||
```
|
|
||||||
|
|
||||||
**`error(message: str, error: Exception = None, **kwargs)`**
|
|
||||||
```python
|
|
||||||
# With exception
|
|
||||||
logger.error("API call failed", error=e, retry_count=3)
|
|
||||||
|
|
||||||
# Without exception
|
|
||||||
logger.error("Validation failed", field="player_name", value="invalid")
|
|
||||||
```
|
|
||||||
|
|
||||||
**`exception(message: str, **kwargs)`**
|
|
||||||
```python
|
|
||||||
# Automatically captures current exception
|
|
||||||
try:
|
|
||||||
risky_operation()
|
|
||||||
except:
|
|
||||||
logger.exception("Unexpected error in operation", operation_id=123)
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Output Examples**
|
|
||||||
|
|
||||||
#### **Console Output (Development)**
|
|
||||||
```
|
|
||||||
2025-08-14 14:32:15,123 - commands.players.info.PlayerInfoCommands - INFO - Player info command started
|
|
||||||
2025-08-14 14:32:16,456 - commands.players.info.PlayerInfoCommands - DEBUG - Starting player search
|
|
||||||
2025-08-14 14:32:18,789 - commands.players.info.PlayerInfoCommands - INFO - Command completed successfully
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **JSON Output (Monitoring & Analysis)**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"timestamp": "2025-08-15T14:32:15.123Z",
|
|
||||||
"level": "INFO",
|
|
||||||
"logger": "commands.players.info.PlayerInfoCommands",
|
|
||||||
"message": "Player info command started",
|
|
||||||
"function": "player_info",
|
|
||||||
"line": 50,
|
|
||||||
"trace_id": "abc12345",
|
|
||||||
"context": {
|
|
||||||
"user_id": "123456789",
|
|
||||||
"guild_id": "987654321",
|
|
||||||
"guild_name": "SBA League",
|
|
||||||
"channel_id": "555666777",
|
|
||||||
"command": "/player",
|
|
||||||
"player_name": "Mike Trout",
|
|
||||||
"season": 12,
|
|
||||||
"trace_id": "abc12345",
|
|
||||||
"operation": "player_info_command"
|
|
||||||
},
|
|
||||||
"extra": {
|
|
||||||
"duration_ms": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Error Output with Exception**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"timestamp": "2025-08-15T14:32:18.789Z",
|
|
||||||
"level": "ERROR",
|
|
||||||
"logger": "commands.players.info.PlayerInfoCommands",
|
|
||||||
"message": "API call failed",
|
|
||||||
"function": "player_info",
|
|
||||||
"line": 125,
|
|
||||||
"trace_id": "abc12345",
|
|
||||||
"exception": {
|
|
||||||
"type": "APITimeout",
|
|
||||||
"message": "Request timed out after 30s",
|
|
||||||
"traceback": "Traceback (most recent call last):\n File ..."
|
|
||||||
},
|
|
||||||
"context": {
|
|
||||||
"user_id": "123456789",
|
|
||||||
"guild_id": "987654321",
|
|
||||||
"command": "/player",
|
|
||||||
"player_name": "Mike Trout",
|
|
||||||
"trace_id": "abc12345",
|
|
||||||
"operation": "player_info_command"
|
|
||||||
},
|
|
||||||
"extra": {
|
|
||||||
"duration_ms": 30000,
|
|
||||||
"retry_count": 3,
|
|
||||||
"endpoint": "players/search"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Advanced Usage Patterns**
|
|
||||||
|
|
||||||
#### **API Call Logging**
|
|
||||||
```python
|
|
||||||
async def fetch_player_data(self, player_name: str):
|
|
||||||
self.logger.debug("API call started",
|
|
||||||
api_endpoint="players/search",
|
|
||||||
search_term=player_name,
|
|
||||||
timeout_ms=30000)
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = await api_client.get("players", params=[("name", player_name)])
|
|
||||||
self.logger.info("API call successful",
|
|
||||||
results_found=len(result) if result else 0,
|
|
||||||
response_size_kb=len(str(result)) // 1024)
|
|
||||||
return result
|
|
||||||
|
|
||||||
except TimeoutError as e:
|
|
||||||
self.logger.error("API timeout",
|
|
||||||
error=e,
|
|
||||||
endpoint="players/search",
|
|
||||||
search_term=player_name)
|
|
||||||
raise
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Performance Monitoring**
|
|
||||||
```python
|
|
||||||
async def complex_operation(self, data):
|
|
||||||
trace_id = self.logger.start_operation("complex_operation")
|
|
||||||
|
|
||||||
# Step 1
|
|
||||||
self.logger.debug("Processing step 1", step="validation")
|
|
||||||
validate_data(data)
|
|
||||||
|
|
||||||
# Step 2
|
|
||||||
self.logger.debug("Processing step 2", step="transformation")
|
|
||||||
processed = transform_data(data)
|
|
||||||
|
|
||||||
# Step 3
|
|
||||||
self.logger.debug("Processing step 3", step="persistence")
|
|
||||||
result = await save_data(processed)
|
|
||||||
|
|
||||||
self.logger.info("Complex operation completed",
|
|
||||||
input_size=len(data),
|
|
||||||
output_size=len(result),
|
|
||||||
steps_completed=3)
|
|
||||||
|
|
||||||
# Final log automatically includes total duration_ms
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Error Context Enrichment**
|
|
||||||
```python
|
|
||||||
async def handle_player_command(self, interaction, player_name):
|
|
||||||
set_discord_context(
|
|
||||||
interaction=interaction,
|
|
||||||
command="/player",
|
|
||||||
player_name=player_name,
|
|
||||||
# Add additional context that helps debugging
|
|
||||||
user_permissions=interaction.user.guild_permissions.administrator,
|
|
||||||
guild_member_count=len(interaction.guild.members),
|
|
||||||
request_timestamp=discord.utils.utcnow().isoformat()
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Command logic
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
# Error logs will include all the above context automatically
|
|
||||||
self.logger.error("Player command failed",
|
|
||||||
error=e,
|
|
||||||
# Additional error-specific context
|
|
||||||
error_code="PLAYER_NOT_FOUND",
|
|
||||||
suggestion="Try using the full player name")
|
|
||||||
raise
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Querying JSON Logs**
|
|
||||||
|
|
||||||
#### **Using jq for Analysis**
|
|
||||||
|
|
||||||
**Find all errors:**
|
|
||||||
```bash
|
|
||||||
jq 'select(.level == "ERROR")' logs/discord_bot_v2.json
|
|
||||||
```
|
|
||||||
|
|
||||||
**Find slow operations (>5 seconds):**
|
|
||||||
```bash
|
|
||||||
jq 'select(.extra.duration_ms > 5000)' logs/discord_bot_v2.json
|
|
||||||
```
|
|
||||||
|
|
||||||
**Track a specific user's activity:**
|
|
||||||
```bash
|
|
||||||
jq 'select(.context.user_id == "123456789")' logs/discord_bot_v2.json
|
|
||||||
```
|
|
||||||
|
|
||||||
**Find API timeout errors:**
|
|
||||||
```bash
|
|
||||||
jq 'select(.exception.type == "APITimeout")' logs/discord_bot_v2.json
|
|
||||||
```
|
|
||||||
|
|
||||||
**Get error summary by type:**
|
|
||||||
```bash
|
|
||||||
jq -r 'select(.level == "ERROR") | .exception.type' logs/discord_bot_v2.json | sort | uniq -c
|
|
||||||
```
|
|
||||||
|
|
||||||
**Trace a complete request:**
|
|
||||||
```bash
|
|
||||||
jq 'select(.trace_id == "abc12345")' logs/discord_bot_v2.json | jq -s 'sort_by(.timestamp)'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Performance Analysis**
|
|
||||||
|
|
||||||
**Average command execution time:**
|
|
||||||
```bash
|
|
||||||
jq -r 'select(.message == "Command completed successfully") | .extra.duration_ms' logs/discord_bot_v2.json | awk '{sum+=$1; n++} END {print sum/n}'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Most active users:**
|
|
||||||
```bash
|
|
||||||
jq -r '.context.user_id' logs/discord_bot_v2.json | sort | uniq -c | sort -nr | head -10
|
|
||||||
```
|
|
||||||
|
|
||||||
**Command usage statistics:**
|
|
||||||
```bash
|
|
||||||
jq -r '.context.command' logs/discord_bot_v2.json | sort | uniq -c | sort -nr
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Best Practices**
|
|
||||||
|
|
||||||
#### **✅ Do:**
|
|
||||||
1. **Always set Discord context** at the start of command handlers
|
|
||||||
2. **Use start_operation()** for timing critical operations
|
|
||||||
3. **Call end_operation()** to complete operation timing
|
|
||||||
4. **Include relevant context** in log messages via keyword arguments
|
|
||||||
5. **Log at appropriate levels** (debug for detailed flow, info for milestones, warning for recoverable issues, error for failures)
|
|
||||||
6. **Include error context** when logging exceptions
|
|
||||||
7. **Use trace_id for correlation** - it's automatically available as a standard key
|
|
||||||
|
|
||||||
#### **❌ Don't:**
|
|
||||||
1. **Don't log sensitive information** (passwords, tokens, personal data)
|
|
||||||
2. **Don't over-log in tight loops** (use sampling or conditional logging)
|
|
||||||
3. **Don't use string formatting in log messages** (use keyword arguments instead)
|
|
||||||
4. **Don't forget to handle exceptions** in logging code itself
|
|
||||||
5. **Don't manually add trace_id to log messages** - it's handled automatically
|
|
||||||
|
|
||||||
#### **🎯 Trace ID & Duration Guidelines:**
|
|
||||||
- **`trace_id`**: Automatically promoted to standard key when operation is active
|
|
||||||
- **`duration_ms`**: Appears in extras for logs during timed operations
|
|
||||||
- **Operation flow**: Always call `start_operation()` → log messages → `end_operation()`
|
|
||||||
- **Query logs**: Use `jq 'select(.trace_id == "xyz")'` for request tracing
|
|
||||||
|
|
||||||
#### **Performance Considerations**
|
|
||||||
- JSON serialization adds minimal overhead (~1-2ms per log entry)
|
|
||||||
- Context variables are async-safe and thread-local
|
|
||||||
- Log rotation prevents disk space issues
|
|
||||||
- Structured queries are much faster than grep on large files
|
|
||||||
|
|
||||||
### **Troubleshooting**
|
|
||||||
|
|
||||||
#### **Common Issues**
|
|
||||||
|
|
||||||
**Logs not appearing:**
|
|
||||||
- Check log level configuration in environment
|
|
||||||
- Verify logs/ directory permissions
|
|
||||||
- Ensure handlers are properly configured
|
|
||||||
|
|
||||||
**JSON serialization errors:**
|
|
||||||
- Avoid logging complex objects directly
|
|
||||||
- Convert objects to strings or dicts before logging
|
|
||||||
- The JSONFormatter handles most common types automatically
|
|
||||||
|
|
||||||
**Context not appearing in logs:**
|
|
||||||
- Ensure `set_discord_context()` is called before logging
|
|
||||||
- Context is tied to the current async task
|
|
||||||
- Check that context is not cleared prematurely
|
|
||||||
|
|
||||||
**Performance issues:**
|
|
||||||
- Monitor log file sizes and rotation
|
|
||||||
- Consider reducing log level in production
|
|
||||||
- Use sampling for high-frequency operations
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 Redis Caching
|
|
||||||
|
|
||||||
**Location:** `utils/cache.py`
|
|
||||||
**Purpose:** Optional Redis-based caching system to improve performance for expensive API operations.
|
|
||||||
|
|
||||||
### **Quick Start**
|
|
||||||
|
|
||||||
```python
|
|
||||||
# In your service - caching is added via decorators
|
|
||||||
from utils.decorators import cached_api_call, cached_single_item
|
|
||||||
|
|
||||||
class PlayerService(BaseService[Player]):
|
|
||||||
@cached_api_call(ttl=600) # Cache for 10 minutes
|
|
||||||
async def get_players_by_team(self, team_id: int, season: int) -> List[Player]:
|
|
||||||
# Existing method - no changes needed
|
|
||||||
return await self.get_all_items(params=[('team_id', team_id), ('season', season)])
|
|
||||||
|
|
||||||
@cached_single_item(ttl=300) # Cache for 5 minutes
|
|
||||||
async def get_player(self, player_id: int) -> Optional[Player]:
|
|
||||||
# Existing method - no changes needed
|
|
||||||
return await self.get_by_id(player_id)
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Configuration**
|
|
||||||
|
|
||||||
**Environment Variables** (optional):
|
|
||||||
```bash
|
|
||||||
REDIS_URL=redis://localhost:6379 # Empty string disables caching
|
|
||||||
REDIS_CACHE_TTL=300 # Default TTL in seconds
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Key Features**
|
|
||||||
|
|
||||||
- **Graceful Fallback**: Works perfectly without Redis installed/configured
|
|
||||||
- **Zero Breaking Changes**: All existing functionality preserved
|
|
||||||
- **Selective Caching**: Add decorators only to expensive methods
|
|
||||||
- **Automatic Key Generation**: Cache keys based on method parameters
|
|
||||||
- **Intelligent Invalidation**: Cache patterns for data modification
|
|
||||||
|
|
||||||
### **Available Decorators**
|
|
||||||
|
|
||||||
**`@cached_api_call(ttl=None, cache_key_suffix="")`**
|
|
||||||
- For methods returning `List[T]`
|
|
||||||
- Caches full result sets (e.g., team rosters, player searches)
|
|
||||||
|
|
||||||
**`@cached_single_item(ttl=None, cache_key_suffix="")`**
|
|
||||||
- For methods returning `Optional[T]`
|
|
||||||
- Caches individual entities (e.g., specific players, teams)
|
|
||||||
|
|
||||||
**`@cache_invalidate("pattern1", "pattern2")`**
|
|
||||||
- For data modification methods
|
|
||||||
- Clears related cache entries when data changes
|
|
||||||
|
|
||||||
### **Usage Examples**
|
|
||||||
|
|
||||||
#### **Team Roster Caching**
|
|
||||||
```python
|
|
||||||
@cached_api_call(ttl=600, cache_key_suffix="roster")
|
|
||||||
async def get_players_by_team(self, team_id: int, season: int) -> List[Player]:
|
|
||||||
# 500+ players cached for 10 minutes
|
|
||||||
# Cache key: sba:players_get_players_by_team_roster_<hash>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Search Results Caching**
|
|
||||||
```python
|
|
||||||
@cached_api_call(ttl=180, cache_key_suffix="search")
|
|
||||||
async def get_players_by_name(self, name: str, season: int) -> List[Player]:
|
|
||||||
# Search results cached for 3 minutes
|
|
||||||
# Reduces API load for common player searches
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Cache Invalidation**
|
|
||||||
```python
|
|
||||||
@cache_invalidate("by_team", "search")
|
|
||||||
async def update_player(self, player_id: int, updates: dict) -> Optional[Player]:
|
|
||||||
# Clears team roster and search caches when player data changes
|
|
||||||
result = await self.update_by_id(player_id, updates)
|
|
||||||
return result
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Performance Impact**
|
|
||||||
|
|
||||||
**Memory Usage:**
|
|
||||||
- ~1-5MB per cached team roster (500 players)
|
|
||||||
- ~1KB per cached individual player
|
|
||||||
|
|
||||||
**Performance Gains:**
|
|
||||||
- 80-90% reduction in API calls for repeated queries
|
|
||||||
- ~50-200ms response time improvement for large datasets
|
|
||||||
- Significant reduction in database/API server load
|
|
||||||
|
|
||||||
### **Implementation Details**
|
|
||||||
|
|
||||||
**Cache Manager** (`utils/cache.py`):
|
|
||||||
- Redis connection management with auto-reconnection
|
|
||||||
- JSON serialization/deserialization
|
|
||||||
- TTL-based expiration
|
|
||||||
- Prefix-based cache invalidation
|
|
||||||
|
|
||||||
**Base Service Integration**:
|
|
||||||
- Automatic cache key generation from method parameters
|
|
||||||
- Model serialization/deserialization
|
|
||||||
- Error handling and fallback to API calls
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Command Decorators
|
|
||||||
|
|
||||||
**Location:** `utils/decorators.py`
|
|
||||||
**Purpose:** Decorators to reduce boilerplate code in Discord commands and service methods.
|
|
||||||
|
|
||||||
### **Command Logging Decorator**
|
|
||||||
|
|
||||||
**`@logged_command(command_name=None, log_params=True, exclude_params=None)`**
|
|
||||||
|
|
||||||
Automatically handles comprehensive logging for Discord commands:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from utils.decorators import logged_command
|
|
||||||
|
|
||||||
class PlayerCommands(commands.Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
self.logger = get_contextual_logger(f'{__name__}.PlayerCommands')
|
|
||||||
|
|
||||||
@discord.app_commands.command(name="player")
|
|
||||||
@logged_command("/player", exclude_params=["sensitive_data"])
|
|
||||||
async def player_info(self, interaction, player_name: str, season: int = None):
|
|
||||||
# Clean business logic only - no logging boilerplate needed
|
|
||||||
player = await player_service.search_player(player_name, season)
|
|
||||||
embed = create_player_embed(player)
|
|
||||||
await interaction.followup.send(embed=embed)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Automatic Discord context setting with interaction details
|
|
||||||
- Operation timing with trace ID generation
|
|
||||||
- Parameter logging with exclusion support
|
|
||||||
- Error handling and re-raising
|
|
||||||
- Preserves Discord.py command registration compatibility
|
|
||||||
|
|
||||||
### **Caching Decorators**
|
|
||||||
|
|
||||||
See [Redis Caching](#-redis-caching) section above for caching decorator documentation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Discord Helpers
|
|
||||||
|
|
||||||
**Location:** `utils/discord_helpers.py` (NEW - January 2025)
|
|
||||||
**Purpose:** Common Discord-related helper functions for channel lookups, message sending, and formatting.
|
|
||||||
|
|
||||||
### **Available Functions**
|
|
||||||
|
|
||||||
#### **`get_channel_by_name(bot, channel_name)`**
|
|
||||||
Get a text channel by name from the configured guild:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from utils.discord_helpers import get_channel_by_name
|
|
||||||
|
|
||||||
# In your command or cog
|
|
||||||
channel = await get_channel_by_name(self.bot, "sba-network-news")
|
|
||||||
if channel:
|
|
||||||
await channel.send("Message content")
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Retrieves guild ID from environment (`GUILD_ID`)
|
|
||||||
- Returns `TextChannel` object or `None` if not found
|
|
||||||
- Handles errors gracefully with logging
|
|
||||||
- Works across all guilds the bot is in
|
|
||||||
|
|
||||||
#### **`send_to_channel(bot, channel_name, content=None, embed=None)`**
|
|
||||||
Send a message to a channel by name:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from utils.discord_helpers import send_to_channel
|
|
||||||
|
|
||||||
# Send text message
|
|
||||||
success = await send_to_channel(
|
|
||||||
self.bot,
|
|
||||||
"sba-network-news",
|
|
||||||
content="Game results posted!"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Send embed
|
|
||||||
success = await send_to_channel(
|
|
||||||
self.bot,
|
|
||||||
"sba-network-news",
|
|
||||||
embed=results_embed
|
|
||||||
)
|
|
||||||
|
|
||||||
# Send both
|
|
||||||
success = await send_to_channel(
|
|
||||||
self.bot,
|
|
||||||
"sba-network-news",
|
|
||||||
content="Check out these results:",
|
|
||||||
embed=results_embed
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Combined channel lookup and message sending
|
|
||||||
- Supports text content, embeds, or both
|
|
||||||
- Returns `True` on success, `False` on failure
|
|
||||||
- Comprehensive error logging
|
|
||||||
- Non-critical - doesn't raise exceptions
|
|
||||||
|
|
||||||
#### **`format_key_plays(plays, away_team, home_team)`**
|
|
||||||
Format top plays into embed field text for game results:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from utils.discord_helpers import format_key_plays
|
|
||||||
from services.play_service import play_service
|
|
||||||
|
|
||||||
# Get top 3 plays by WPA
|
|
||||||
top_plays = await play_service.get_top_plays_by_wpa(game_id, limit=3)
|
|
||||||
|
|
||||||
# Format for display
|
|
||||||
key_plays_text = format_key_plays(top_plays, away_team, home_team)
|
|
||||||
|
|
||||||
# Add to embed
|
|
||||||
if key_plays_text:
|
|
||||||
embed.add_field(name="Key Plays", value=key_plays_text, inline=False)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output Example:**
|
|
||||||
```
|
|
||||||
Top 3: (NYY) homers in 2 runs, NYY up 3-1
|
|
||||||
Bot 5: (BOS) doubles scoring 1 run, tied at 3
|
|
||||||
Top 9: (NYY) singles scoring 1 run, NYY up 4-3
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Uses `Play.descriptive_text()` for human-readable descriptions
|
|
||||||
- Adds score context after each play
|
|
||||||
- Shows which team is leading or if tied
|
|
||||||
- Returns empty string if no plays provided
|
|
||||||
- Handles RBI adjustments for accurate score display
|
|
||||||
|
|
||||||
### **Real-World Usage**
|
|
||||||
|
|
||||||
#### **Scorecard Submission Results Posting**
|
|
||||||
From `commands/league/submit_scorecard.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Create results embed
|
|
||||||
results_embed = await self._create_results_embed(
|
|
||||||
away_team, home_team, box_score, setup_data,
|
|
||||||
current, sheet_url, wp_id, lp_id, sv_id, hold_ids, game_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# Post to news channel automatically
|
|
||||||
await send_to_channel(
|
|
||||||
self.bot,
|
|
||||||
SBA_NETWORK_NEWS_CHANNEL, # "sba-network-news"
|
|
||||||
content=None,
|
|
||||||
embed=results_embed
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Configuration**
|
|
||||||
|
|
||||||
These functions rely on environment variables:
|
|
||||||
- **`GUILD_ID`**: Discord server ID where channels should be found
|
|
||||||
- **`SBA_NETWORK_NEWS_CHANNEL`**: Channel name for game results (constant)
|
|
||||||
|
|
||||||
### **Error Handling**
|
|
||||||
|
|
||||||
All functions handle errors gracefully:
|
|
||||||
- **Channel not found**: Logs warning and returns `None` or `False`
|
|
||||||
- **Missing GUILD_ID**: Logs error and returns `None` or `False`
|
|
||||||
- **Send failures**: Logs error with details and returns `False`
|
|
||||||
- **Empty data**: Returns empty string or `False` without errors
|
|
||||||
|
|
||||||
### **Testing Considerations**
|
|
||||||
|
|
||||||
When testing commands that use these utilities:
|
|
||||||
- Mock `get_channel_by_name()` to return test channel objects
|
|
||||||
- Mock `send_to_channel()` to verify message content
|
|
||||||
- Mock `format_key_plays()` to verify play formatting logic
|
|
||||||
- Use test guild IDs in environment variables
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Future Utilities
|
|
||||||
|
|
||||||
Additional utility modules planned for future implementation:
|
|
||||||
|
|
||||||
### **Permission Utilities** (Planned)
|
|
||||||
- Permission checking decorators
|
|
||||||
- Role validation helpers
|
|
||||||
- User authorization utilities
|
|
||||||
|
|
||||||
### **API Utilities** (Planned)
|
|
||||||
- Rate limiting decorators
|
|
||||||
- Response caching mechanisms
|
|
||||||
- Retry logic with exponential backoff
|
|
||||||
- Request validation helpers
|
|
||||||
|
|
||||||
### **Data Processing** (Planned)
|
|
||||||
- CSV/JSON export utilities
|
|
||||||
- Statistical calculation helpers
|
|
||||||
- Date/time formatting for baseball seasons
|
|
||||||
- Text processing and search utilities
|
|
||||||
|
|
||||||
### **Testing Utilities** (Planned)
|
|
||||||
- Mock Discord objects for testing
|
|
||||||
- Fixture generators for common test data
|
|
||||||
- Assertion helpers for Discord responses
|
|
||||||
- Test database setup and teardown
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Usage Examples by Module
|
|
||||||
|
|
||||||
### **Logging Integration in Commands**
|
|
||||||
|
|
||||||
```python
|
|
||||||
# commands/teams/roster.py
|
|
||||||
from utils.logging import get_contextual_logger, set_discord_context
|
|
||||||
|
|
||||||
class TeamRosterCommands(commands.Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
self.logger = get_contextual_logger(f'{__name__}.TeamRosterCommands')
|
|
||||||
|
|
||||||
@discord.app_commands.command(name="roster")
|
|
||||||
async def team_roster(self, interaction, team_name: str, season: int = None):
|
|
||||||
set_discord_context(
|
|
||||||
interaction=interaction,
|
|
||||||
command="/roster",
|
|
||||||
team_name=team_name,
|
|
||||||
season=season
|
|
||||||
)
|
|
||||||
|
|
||||||
trace_id = self.logger.start_operation("team_roster_command")
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.logger.info("Team roster command started")
|
|
||||||
|
|
||||||
# Command implementation
|
|
||||||
team = await team_service.find_team(team_name)
|
|
||||||
self.logger.debug("Team found", team_id=team.id, team_abbreviation=team.abbrev)
|
|
||||||
|
|
||||||
players = await team_service.get_roster(team.id, season)
|
|
||||||
self.logger.info("Roster retrieved", player_count=len(players))
|
|
||||||
|
|
||||||
# Create and send response
|
|
||||||
embed = create_roster_embed(team, players)
|
|
||||||
await interaction.followup.send(embed=embed)
|
|
||||||
|
|
||||||
self.logger.info("Team roster command completed")
|
|
||||||
|
|
||||||
except TeamNotFoundError as e:
|
|
||||||
self.logger.warning("Team not found", search_term=team_name)
|
|
||||||
await interaction.followup.send(f"❌ Team '{team_name}' not found", ephemeral=True)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error("Team roster command failed", error=e)
|
|
||||||
await interaction.followup.send("❌ Error retrieving team roster", ephemeral=True)
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Service Layer Logging**
|
|
||||||
|
|
||||||
```python
|
|
||||||
# services/team_service.py
|
|
||||||
from utils.logging import get_contextual_logger
|
|
||||||
|
|
||||||
class TeamService(BaseService[Team]):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(Team, 'teams')
|
|
||||||
self.logger = get_contextual_logger(f'{__name__}.TeamService')
|
|
||||||
|
|
||||||
async def find_team(self, team_name: str) -> Team:
|
|
||||||
self.logger.debug("Starting team search", search_term=team_name)
|
|
||||||
|
|
||||||
# Try exact match first
|
|
||||||
teams = await self.get_by_field('name', team_name)
|
|
||||||
if len(teams) == 1:
|
|
||||||
self.logger.debug("Exact team match found", team_id=teams[0].id)
|
|
||||||
return teams[0]
|
|
||||||
|
|
||||||
# Try abbreviation match
|
|
||||||
teams = await self.get_by_field('abbrev', team_name.upper())
|
|
||||||
if len(teams) == 1:
|
|
||||||
self.logger.debug("Team abbreviation match found", team_id=teams[0].id)
|
|
||||||
return teams[0]
|
|
||||||
|
|
||||||
# Try fuzzy search
|
|
||||||
all_teams = await self.get_all_items()
|
|
||||||
matches = [t for t in all_teams if team_name.lower() in t.name.lower()]
|
|
||||||
|
|
||||||
if len(matches) == 0:
|
|
||||||
self.logger.warning("No team matches found", search_term=team_name)
|
|
||||||
raise TeamNotFoundError(f"No team found matching '{team_name}'")
|
|
||||||
elif len(matches) > 1:
|
|
||||||
match_names = [t.name for t in matches]
|
|
||||||
self.logger.warning("Multiple team matches found",
|
|
||||||
search_term=team_name,
|
|
||||||
matches=match_names)
|
|
||||||
raise MultipleTeamsFoundError(f"Multiple teams found: {', '.join(match_names)}")
|
|
||||||
|
|
||||||
self.logger.debug("Fuzzy team match found", team_id=matches[0].id)
|
|
||||||
return matches[0]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
utils/
|
|
||||||
├── README.md # This documentation
|
|
||||||
├── __init__.py # Package initialization
|
|
||||||
├── cache.py # Redis caching system
|
|
||||||
├── decorators.py # Command and caching decorators
|
|
||||||
├── logging.py # Structured logging implementation
|
|
||||||
└── random_gen.py # Random generation utilities
|
|
||||||
|
|
||||||
# Future files:
|
|
||||||
├── discord_helpers.py # Discord utility functions
|
|
||||||
├── api_utils.py # API helper functions
|
|
||||||
├── data_processing.py # Data manipulation utilities
|
|
||||||
└── testing.py # Testing helper functions
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 Autocomplete Functions
|
|
||||||
|
|
||||||
**Location:** `utils/autocomplete.py`
|
|
||||||
**Purpose:** Shared autocomplete functions for Discord slash command parameters.
|
|
||||||
|
|
||||||
### **Available Functions**
|
|
||||||
|
|
||||||
#### **Player Autocomplete**
|
|
||||||
```python
|
|
||||||
async def player_autocomplete(interaction: discord.Interaction, current: str) -> List[discord.app_commands.Choice]:
|
|
||||||
"""Autocomplete for player names with priority ordering."""
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Fuzzy name matching with word boundaries
|
|
||||||
- Prioritizes exact matches and starts-with matches
|
|
||||||
- Limits to 25 results (Discord limit)
|
|
||||||
- Handles API errors gracefully
|
|
||||||
|
|
||||||
#### **Team Autocomplete (All Teams)**
|
|
||||||
```python
|
|
||||||
async def team_autocomplete(interaction: discord.Interaction, current: str) -> List[discord.app_commands.Choice]:
|
|
||||||
"""Autocomplete for all team abbreviations."""
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Matches team abbreviations (e.g., "WV", "NY", "WVMIL")
|
|
||||||
- Case-insensitive matching
|
|
||||||
- Includes full team names in display
|
|
||||||
|
|
||||||
#### **Major League Team Autocomplete**
|
|
||||||
```python
|
|
||||||
async def major_league_team_autocomplete(interaction: discord.Interaction, current: str) -> List[discord.app_commands.Choice]:
|
|
||||||
"""Autocomplete for Major League teams only (filtered by roster type)."""
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Filters to only Major League teams (≤3 character abbreviations)
|
|
||||||
- Uses Team model's `roster_type()` method for accurate filtering
|
|
||||||
- Excludes Minor League (MiL) and Injured List (IL) teams
|
|
||||||
|
|
||||||
### **Usage in Commands**
|
|
||||||
|
|
||||||
```python
|
|
||||||
from utils.autocomplete import player_autocomplete, major_league_team_autocomplete
|
|
||||||
|
|
||||||
class RosterCommands(commands.Cog):
|
|
||||||
@discord.app_commands.command(name="roster")
|
|
||||||
@discord.app_commands.describe(
|
|
||||||
team="Team abbreviation",
|
|
||||||
player="Player name (optional)"
|
|
||||||
)
|
|
||||||
async def roster_command(
|
|
||||||
self,
|
|
||||||
interaction: discord.Interaction,
|
|
||||||
team: str,
|
|
||||||
player: Optional[str] = None
|
|
||||||
):
|
|
||||||
# Command logic here
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Autocomplete decorators
|
|
||||||
@roster_command.autocomplete('team')
|
|
||||||
async def roster_team_autocomplete(self, interaction, current):
|
|
||||||
return await major_league_team_autocomplete(interaction, current)
|
|
||||||
|
|
||||||
@roster_command.autocomplete('player')
|
|
||||||
async def roster_player_autocomplete(self, interaction, current):
|
|
||||||
return await player_autocomplete(interaction, current)
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Recent Fixes (January 2025)**
|
|
||||||
|
|
||||||
#### **Team Filtering Issue**
|
|
||||||
- **Problem**: `major_league_team_autocomplete` was passing invalid `roster_type` parameter to API
|
|
||||||
- **Solution**: Removed parameter and implemented client-side filtering using `team.roster_type()` method
|
|
||||||
- **Benefit**: More accurate team filtering that respects edge cases like "BHMIL" vs "BHMMIL"
|
|
||||||
|
|
||||||
#### **Test Coverage**
|
|
||||||
- Added comprehensive test suite in `tests/test_utils_autocomplete.py`
|
|
||||||
- Tests cover all functions, error handling, and edge cases
|
|
||||||
- Validates prioritization logic and result limits
|
|
||||||
|
|
||||||
### **Implementation Notes**
|
|
||||||
|
|
||||||
- **Shared Functions**: Autocomplete logic centralized to avoid duplication across commands
|
|
||||||
- **Error Handling**: Functions return empty lists on API errors rather than crashing
|
|
||||||
- **Performance**: Uses cached service calls where possible
|
|
||||||
- **Discord Limits**: Respects 25-choice limit for autocomplete responses
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated:** January 2025 - Added Autocomplete Functions and Fixed Team Filtering
|
|
||||||
**Next Update:** When additional utility modules are added
|
|
||||||
|
|
||||||
For questions or improvements to the logging system, check the implementation in `utils/logging.py` or refer to the JSON log outputs in `logs/discord_bot_v2.json`.
|
|
||||||
585
views/README.md
585
views/README.md
@ -1,585 +0,0 @@
|
|||||||
# Views Directory
|
|
||||||
|
|
||||||
The views directory contains Discord UI components for Discord Bot v2.0, providing consistent visual interfaces and interactive elements. This includes embeds, modals, buttons, select menus, and other Discord UI components.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Component-Based UI Design
|
|
||||||
Views in Discord Bot v2.0 follow these principles:
|
|
||||||
- **Consistent styling** via centralized templates
|
|
||||||
- **Reusable components** for common UI patterns
|
|
||||||
- **Error handling** with graceful degradation
|
|
||||||
- **User interaction tracking** and validation
|
|
||||||
- **Accessibility** with proper labeling and feedback
|
|
||||||
|
|
||||||
### Base Components
|
|
||||||
All view components inherit from Discord.py base classes with enhanced functionality:
|
|
||||||
- **BaseView** - Enhanced discord.ui.View with logging and user validation
|
|
||||||
- **BaseModal** - Enhanced discord.ui.Modal with error handling
|
|
||||||
- **EmbedTemplate** - Centralized embed creation with consistent styling
|
|
||||||
|
|
||||||
## View Components
|
|
||||||
|
|
||||||
### Base View System (`base.py`)
|
|
||||||
|
|
||||||
#### BaseView Class
|
|
||||||
Foundation for all interactive views:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class BaseView(discord.ui.View):
|
|
||||||
def __init__(self, timeout=180.0, user_id=None):
|
|
||||||
super().__init__(timeout=timeout)
|
|
||||||
self.user_id = user_id
|
|
||||||
self.logger = get_contextual_logger(f'{__name__}.BaseView')
|
|
||||||
|
|
||||||
async def interaction_check(self, interaction) -> bool:
|
|
||||||
"""Validate user permissions for interaction."""
|
|
||||||
|
|
||||||
async def on_timeout(self) -> None:
|
|
||||||
"""Handle view timeout gracefully."""
|
|
||||||
|
|
||||||
async def on_error(self, interaction, error, item) -> None:
|
|
||||||
"""Handle view errors with user feedback."""
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ConfirmationView Class (Updated January 2025)
|
|
||||||
Reusable confirmation dialog with Confirm/Cancel buttons (`confirmations.py`):
|
|
||||||
|
|
||||||
**Key Features:**
|
|
||||||
- **User restriction**: Only specified users can interact
|
|
||||||
- **Customizable labels and styles**: Flexible button appearance
|
|
||||||
- **Timeout handling**: Automatic cleanup after timeout
|
|
||||||
- **Three-state result**: `True` (confirmed), `False` (cancelled), `None` (timeout)
|
|
||||||
- **Clean interface**: Automatically removes buttons after interaction
|
|
||||||
|
|
||||||
**Usage Pattern:**
|
|
||||||
```python
|
|
||||||
from views.confirmations import ConfirmationView
|
|
||||||
|
|
||||||
# Create confirmation dialog
|
|
||||||
view = ConfirmationView(
|
|
||||||
responders=[interaction.user], # Only this user can interact
|
|
||||||
timeout=30.0, # 30 second timeout
|
|
||||||
confirm_label="Yes, delete", # Custom label
|
|
||||||
cancel_label="No, keep it" # Custom label
|
|
||||||
)
|
|
||||||
|
|
||||||
# Send confirmation
|
|
||||||
await interaction.edit_original_response(
|
|
||||||
content="⚠️ Are you sure you want to delete this?",
|
|
||||||
view=view
|
|
||||||
)
|
|
||||||
|
|
||||||
# Wait for user response
|
|
||||||
await view.wait()
|
|
||||||
|
|
||||||
# Check result
|
|
||||||
if view.confirmed is True:
|
|
||||||
# User clicked Confirm
|
|
||||||
await interaction.edit_original_response(
|
|
||||||
content="✅ Deleted successfully",
|
|
||||||
view=None
|
|
||||||
)
|
|
||||||
elif view.confirmed is False:
|
|
||||||
# User clicked Cancel
|
|
||||||
await interaction.edit_original_response(
|
|
||||||
content="❌ Cancelled",
|
|
||||||
view=None
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Timeout occurred (view.confirmed is None)
|
|
||||||
await interaction.edit_original_response(
|
|
||||||
content="⏱️ Request timed out",
|
|
||||||
view=None
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Real-World Example (Scorecard Submission):**
|
|
||||||
```python
|
|
||||||
# From commands/league/submit_scorecard.py
|
|
||||||
if duplicate_game:
|
|
||||||
view = ConfirmationView(
|
|
||||||
responders=[interaction.user],
|
|
||||||
timeout=30.0
|
|
||||||
)
|
|
||||||
await interaction.edit_original_response(
|
|
||||||
content=(
|
|
||||||
f"⚠️ This game has already been played!\n"
|
|
||||||
f"Would you like me to wipe the old one and re-submit?"
|
|
||||||
),
|
|
||||||
view=view
|
|
||||||
)
|
|
||||||
await view.wait()
|
|
||||||
|
|
||||||
if view.confirmed:
|
|
||||||
# User confirmed - proceed with wipe and resubmit
|
|
||||||
await wipe_old_data()
|
|
||||||
else:
|
|
||||||
# User cancelled - exit gracefully
|
|
||||||
return
|
|
||||||
```
|
|
||||||
|
|
||||||
**Configuration Options:**
|
|
||||||
```python
|
|
||||||
ConfirmationView(
|
|
||||||
responders=[user1, user2], # Multiple users allowed
|
|
||||||
timeout=60.0, # Custom timeout
|
|
||||||
confirm_label="Approve", # Custom confirm text
|
|
||||||
cancel_label="Reject", # Custom cancel text
|
|
||||||
confirm_style=discord.ButtonStyle.red, # Custom button style
|
|
||||||
cancel_style=discord.ButtonStyle.grey # Custom button style
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### PaginationView Class
|
|
||||||
Multi-page navigation for large datasets:
|
|
||||||
|
|
||||||
```python
|
|
||||||
pages = [embed1, embed2, embed3]
|
|
||||||
pagination = PaginationView(
|
|
||||||
pages=pages,
|
|
||||||
user_id=interaction.user.id,
|
|
||||||
show_page_numbers=True
|
|
||||||
)
|
|
||||||
await interaction.followup.send(embed=pagination.get_current_embed(), view=pagination)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Embed Templates (`embeds.py`)
|
|
||||||
|
|
||||||
#### EmbedTemplate Class
|
|
||||||
Centralized embed creation with consistent styling:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Success embed
|
|
||||||
embed = EmbedTemplate.success(
|
|
||||||
title="Operation Completed",
|
|
||||||
description="Your request was processed successfully."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Error embed
|
|
||||||
embed = EmbedTemplate.error(
|
|
||||||
title="Operation Failed",
|
|
||||||
description="Please check your input and try again."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Warning embed
|
|
||||||
embed = EmbedTemplate.warning(
|
|
||||||
title="Careful!",
|
|
||||||
description="This action cannot be undone."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Info embed
|
|
||||||
embed = EmbedTemplate.info(
|
|
||||||
title="Information",
|
|
||||||
description="Here's what you need to know."
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### EmbedColors Dataclass
|
|
||||||
Consistent color scheme across all embeds:
|
|
||||||
|
|
||||||
```python
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class EmbedColors:
|
|
||||||
PRIMARY: int = 0xa6ce39 # SBA green
|
|
||||||
SUCCESS: int = 0x28a745 # Green
|
|
||||||
WARNING: int = 0xffc107 # Yellow
|
|
||||||
ERROR: int = 0xdc3545 # Red
|
|
||||||
INFO: int = 0x17a2b8 # Blue
|
|
||||||
SECONDARY: int = 0x6c757d # Gray
|
|
||||||
```
|
|
||||||
|
|
||||||
### Modal Forms (`modals.py`)
|
|
||||||
|
|
||||||
#### BaseModal Class
|
|
||||||
Foundation for interactive forms:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class BaseModal(discord.ui.Modal):
|
|
||||||
def __init__(self, title: str, timeout=300.0):
|
|
||||||
super().__init__(title=title, timeout=timeout)
|
|
||||||
self.logger = get_contextual_logger(f'{__name__}.BaseModal')
|
|
||||||
self.result = None
|
|
||||||
|
|
||||||
async def on_submit(self, interaction):
|
|
||||||
"""Handle form submission."""
|
|
||||||
|
|
||||||
async def on_error(self, interaction, error):
|
|
||||||
"""Handle form errors."""
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Usage Pattern
|
|
||||||
```python
|
|
||||||
class CustomCommandModal(BaseModal):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(title="Create Custom Command")
|
|
||||||
|
|
||||||
name = discord.ui.TextInput(
|
|
||||||
label="Command Name",
|
|
||||||
placeholder="Enter command name...",
|
|
||||||
required=True,
|
|
||||||
max_length=50
|
|
||||||
)
|
|
||||||
|
|
||||||
response = discord.ui.TextInput(
|
|
||||||
label="Response",
|
|
||||||
placeholder="Enter command response...",
|
|
||||||
style=discord.TextStyle.paragraph,
|
|
||||||
required=True,
|
|
||||||
max_length=2000
|
|
||||||
)
|
|
||||||
|
|
||||||
async def on_submit(self, interaction):
|
|
||||||
# Process form data
|
|
||||||
command_data = {
|
|
||||||
"name": self.name.value,
|
|
||||||
"response": self.response.value
|
|
||||||
}
|
|
||||||
# Handle creation logic
|
|
||||||
```
|
|
||||||
|
|
||||||
### Common UI Elements (`common.py`)
|
|
||||||
|
|
||||||
#### Shared Components
|
|
||||||
- **Loading indicators** for async operations
|
|
||||||
- **Status messages** for operation feedback
|
|
||||||
- **Navigation elements** for multi-step processes
|
|
||||||
- **Validation displays** for form errors
|
|
||||||
|
|
||||||
### Specialized Views
|
|
||||||
|
|
||||||
#### Custom Commands (`custom_commands.py`)
|
|
||||||
Views specific to custom command management:
|
|
||||||
- Command creation forms
|
|
||||||
- Command listing with actions
|
|
||||||
- Bulk management interfaces
|
|
||||||
|
|
||||||
#### Transaction Management (`transaction_embed.py`)
|
|
||||||
Views for player transaction interfaces:
|
|
||||||
- Transaction builder with interactive controls
|
|
||||||
- Comprehensive validation and sWAR display
|
|
||||||
- Pre-existing transaction context
|
|
||||||
- Approval/submission workflows
|
|
||||||
|
|
||||||
## Styling Guidelines
|
|
||||||
|
|
||||||
### Embed Consistency
|
|
||||||
All embeds should use EmbedTemplate methods:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# ✅ Consistent styling
|
|
||||||
embed = EmbedTemplate.success("Player Added", "Player successfully added to roster")
|
|
||||||
|
|
||||||
# ❌ Inconsistent styling
|
|
||||||
embed = discord.Embed(title="Player Added", color=0x00ff00)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Color Usage
|
|
||||||
Use the standard color palette:
|
|
||||||
- **PRIMARY (SBA Green)** - Default for neutral information
|
|
||||||
- **SUCCESS (Green)** - Successful operations
|
|
||||||
- **ERROR (Red)** - Errors and failures
|
|
||||||
- **WARNING (Yellow)** - Warnings and cautions
|
|
||||||
- **INFO (Blue)** - General information
|
|
||||||
- **SECONDARY (Gray)** - Less important information
|
|
||||||
|
|
||||||
### User Feedback
|
|
||||||
Provide clear feedback for all user interactions:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Loading state
|
|
||||||
embed = EmbedTemplate.info("Processing", "Please wait while we process your request...")
|
|
||||||
|
|
||||||
# Success state
|
|
||||||
embed = EmbedTemplate.success("Complete", "Your request has been processed successfully.")
|
|
||||||
|
|
||||||
# Error state with helpful information
|
|
||||||
embed = EmbedTemplate.error(
|
|
||||||
"Request Failed",
|
|
||||||
"The player name was not found. Please check your spelling and try again."
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Interactive Components
|
|
||||||
|
|
||||||
### Button Patterns
|
|
||||||
|
|
||||||
#### Action Buttons
|
|
||||||
```python
|
|
||||||
@discord.ui.button(label="Confirm", style=discord.ButtonStyle.success, emoji="✅")
|
|
||||||
async def confirm_button(self, interaction, button):
|
|
||||||
self.increment_interaction_count()
|
|
||||||
# Handle confirmation
|
|
||||||
await interaction.response.edit_message(content="Confirmed!", view=None)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Navigation Buttons
|
|
||||||
```python
|
|
||||||
@discord.ui.button(emoji="◀️", style=discord.ButtonStyle.primary)
|
|
||||||
async def previous_page(self, interaction, button):
|
|
||||||
self.current_page = max(0, self.current_page - 1)
|
|
||||||
await interaction.response.edit_message(embed=self.get_current_embed(), view=self)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Select Menu Patterns
|
|
||||||
|
|
||||||
#### Option Selection
|
|
||||||
```python
|
|
||||||
@discord.ui.select(placeholder="Choose an option...")
|
|
||||||
async def select_option(self, interaction, select):
|
|
||||||
selected_value = select.values[0]
|
|
||||||
# Handle selection
|
|
||||||
await interaction.response.send_message(f"You selected: {selected_value}")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Dynamic Options
|
|
||||||
```python
|
|
||||||
class PlayerSelectMenu(discord.ui.Select):
|
|
||||||
def __init__(self, players: List[Player]):
|
|
||||||
options = [
|
|
||||||
discord.SelectOption(
|
|
||||||
label=player.name,
|
|
||||||
value=str(player.id),
|
|
||||||
description=f"{player.position} - {player.team.abbrev}"
|
|
||||||
)
|
|
||||||
for player in players[:25] # Discord limit
|
|
||||||
]
|
|
||||||
super().__init__(placeholder="Select a player...", options=options)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
### View Error Handling
|
|
||||||
All views implement comprehensive error handling:
|
|
||||||
|
|
||||||
```python
|
|
||||||
async def on_error(self, interaction, error, item):
|
|
||||||
"""Handle view errors gracefully."""
|
|
||||||
self.logger.error("View error", error=error, item_type=type(item).__name__)
|
|
||||||
|
|
||||||
try:
|
|
||||||
embed = EmbedTemplate.error(
|
|
||||||
"Interaction Error",
|
|
||||||
"Something went wrong. Please try again."
|
|
||||||
)
|
|
||||||
|
|
||||||
if not interaction.response.is_done():
|
|
||||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
|
||||||
else:
|
|
||||||
await interaction.followup.send(embed=embed, ephemeral=True)
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error("Failed to send error message", error=e)
|
|
||||||
```
|
|
||||||
|
|
||||||
### User Input Validation
|
|
||||||
Forms validate user input before processing:
|
|
||||||
|
|
||||||
```python
|
|
||||||
async def on_submit(self, interaction):
|
|
||||||
# Validate input
|
|
||||||
if len(self.name.value) < 2:
|
|
||||||
embed = EmbedTemplate.error(
|
|
||||||
"Invalid Input",
|
|
||||||
"Command name must be at least 2 characters long."
|
|
||||||
)
|
|
||||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Process valid input
|
|
||||||
await self.create_command(interaction)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Accessibility Features
|
|
||||||
|
|
||||||
### User-Friendly Labels
|
|
||||||
- **Clear button labels** with descriptive text
|
|
||||||
- **Helpful placeholders** in form fields
|
|
||||||
- **Descriptive error messages** with actionable guidance
|
|
||||||
- **Consistent emoji usage** for visual recognition
|
|
||||||
|
|
||||||
### Permission Validation
|
|
||||||
Views respect user permissions and provide appropriate feedback:
|
|
||||||
|
|
||||||
```python
|
|
||||||
async def interaction_check(self, interaction) -> bool:
|
|
||||||
"""Check if user can interact with this view."""
|
|
||||||
if self.user_id and interaction.user.id != self.user_id:
|
|
||||||
await interaction.response.send_message(
|
|
||||||
"❌ You cannot interact with this menu.",
|
|
||||||
ephemeral=True
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
|
|
||||||
### View Lifecycle Management
|
|
||||||
- **Timeout handling** prevents orphaned views
|
|
||||||
- **Resource cleanup** in view destructors
|
|
||||||
- **Interaction tracking** for usage analytics
|
|
||||||
- **Memory management** for large datasets
|
|
||||||
|
|
||||||
### Efficient Updates
|
|
||||||
```python
|
|
||||||
# ✅ Efficient - Only update what changed
|
|
||||||
await interaction.response.edit_message(embed=new_embed, view=self)
|
|
||||||
|
|
||||||
# ❌ Inefficient - Sends new message
|
|
||||||
await interaction.response.send_message(embed=new_embed, view=new_view)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Strategies
|
|
||||||
|
|
||||||
### View Testing
|
|
||||||
```python
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_confirmation_view():
|
|
||||||
view = ConfirmationView(user_id=123)
|
|
||||||
|
|
||||||
# Mock interaction
|
|
||||||
interaction = Mock()
|
|
||||||
interaction.user.id = 123
|
|
||||||
|
|
||||||
# Test button click
|
|
||||||
await view.confirm_button.callback(interaction)
|
|
||||||
|
|
||||||
assert view.result is True
|
|
||||||
```
|
|
||||||
|
|
||||||
### Modal Testing
|
|
||||||
```python
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_custom_command_modal():
|
|
||||||
modal = CustomCommandModal()
|
|
||||||
|
|
||||||
# Set form values
|
|
||||||
modal.name.value = "test"
|
|
||||||
modal.response.value = "Test response"
|
|
||||||
|
|
||||||
# Mock interaction
|
|
||||||
interaction = Mock()
|
|
||||||
|
|
||||||
# Test form submission
|
|
||||||
await modal.on_submit(interaction)
|
|
||||||
|
|
||||||
# Verify processing
|
|
||||||
assert modal.result is not None
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Guidelines
|
|
||||||
|
|
||||||
### Creating New Views
|
|
||||||
1. **Inherit from base classes** for consistency
|
|
||||||
2. **Use EmbedTemplate** for all embed creation
|
|
||||||
3. **Implement proper error handling** in all interactions
|
|
||||||
4. **Add user permission checks** where appropriate
|
|
||||||
5. **Include comprehensive logging** with context
|
|
||||||
6. **Follow timeout patterns** to prevent resource leaks
|
|
||||||
|
|
||||||
### View Composition
|
|
||||||
- **Keep views focused** on single responsibilities
|
|
||||||
- **Use composition** over complex inheritance
|
|
||||||
- **Separate business logic** from UI logic
|
|
||||||
- **Make views testable** with dependency injection
|
|
||||||
|
|
||||||
### UI Guidelines
|
|
||||||
- **Follow Discord design patterns** for familiarity
|
|
||||||
- **Use consistent colors** from EmbedColors
|
|
||||||
- **Provide clear user feedback** for all actions
|
|
||||||
- **Handle edge cases** gracefully
|
|
||||||
- **Consider mobile users** in layout design
|
|
||||||
|
|
||||||
## Transaction Embed Enhancements (January 2025)
|
|
||||||
|
|
||||||
### Enhanced Display Features
|
|
||||||
The transaction embed now provides comprehensive information for better decision-making:
|
|
||||||
|
|
||||||
#### New Embed Sections
|
|
||||||
```python
|
|
||||||
async def create_transaction_embed(builder: TransactionBuilder) -> discord.Embed:
|
|
||||||
"""
|
|
||||||
Creates enhanced transaction embed with sWAR and pre-existing transaction context.
|
|
||||||
"""
|
|
||||||
# Existing sections...
|
|
||||||
|
|
||||||
# NEW: Team Cost (sWAR) Display
|
|
||||||
swar_status = f"{validation.major_league_swar_status}\n{validation.minor_league_swar_status}"
|
|
||||||
embed.add_field(name="Team sWAR", value=swar_status, inline=False)
|
|
||||||
|
|
||||||
# NEW: Pre-existing Transaction Context (when applicable)
|
|
||||||
if validation.pre_existing_transactions_note:
|
|
||||||
embed.add_field(
|
|
||||||
name="📋 Transaction Context",
|
|
||||||
value=validation.pre_existing_transactions_note,
|
|
||||||
inline=False
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Enhanced Information Display
|
|
||||||
|
|
||||||
#### sWAR Tracking
|
|
||||||
- **Major League sWAR**: Projected team cost for ML roster
|
|
||||||
- **Minor League sWAR**: Projected team cost for MiL roster
|
|
||||||
- **Formatted Display**: Uses 📊 emoji with 1 decimal precision
|
|
||||||
|
|
||||||
#### Pre-existing Transaction Context
|
|
||||||
Dynamic context display based on scheduled moves:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Example displays:
|
|
||||||
"ℹ️ **Pre-existing Moves**: 3 scheduled moves (+3.7 sWAR)"
|
|
||||||
"ℹ️ **Pre-existing Moves**: 2 scheduled moves (-2.5 sWAR)"
|
|
||||||
"ℹ️ **Pre-existing Moves**: 1 scheduled moves (no sWAR impact)"
|
|
||||||
# No display when no pre-existing moves (clean interface)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Complete Embed Structure
|
|
||||||
The enhanced transaction embed now includes:
|
|
||||||
|
|
||||||
1. **Current Moves** - List of moves in transaction builder
|
|
||||||
2. **Roster Status** - Legal/illegal roster counts with limits
|
|
||||||
3. **Team Cost (sWAR)** - sWAR for both rosters
|
|
||||||
4. **Transaction Context** - Pre-existing moves impact (conditional)
|
|
||||||
5. **Errors/Suggestions** - Validation feedback and recommendations
|
|
||||||
|
|
||||||
### Usage Examples
|
|
||||||
|
|
||||||
#### Basic Transaction Display
|
|
||||||
```python
|
|
||||||
# Standard transaction without pre-existing moves
|
|
||||||
builder = get_transaction_builder(user_id, team)
|
|
||||||
embed = await create_transaction_embed(builder)
|
|
||||||
# Shows: moves, roster status, sWAR, errors/suggestions
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Enhanced Context Display
|
|
||||||
```python
|
|
||||||
# Transaction with pre-existing moves context
|
|
||||||
validation = await builder.validate_transaction(next_week=current_week + 1)
|
|
||||||
embed = await create_transaction_embed(builder)
|
|
||||||
# Shows: all above + pre-existing transaction impact
|
|
||||||
```
|
|
||||||
|
|
||||||
### User Experience Improvements
|
|
||||||
- **Complete Context**: Users see full impact including scheduled moves
|
|
||||||
- **Visual Clarity**: Consistent emoji usage and formatting
|
|
||||||
- **Conditional Display**: Context only shown when relevant
|
|
||||||
- **Decision Support**: sWAR projections help strategic planning
|
|
||||||
|
|
||||||
### Implementation Notes
|
|
||||||
- **Backwards Compatible**: Existing embed functionality preserved
|
|
||||||
- **Conditional Sections**: Pre-existing context only appears when applicable
|
|
||||||
- **Performance**: Validation data cached to avoid repeated calculations
|
|
||||||
- **Accessibility**: Clear visual hierarchy with emojis and formatting
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Next Steps for AI Agents:**
|
|
||||||
1. Review existing view implementations for patterns
|
|
||||||
2. Understand the Discord UI component system
|
|
||||||
3. Follow the EmbedTemplate system for consistent styling
|
|
||||||
4. Implement proper error handling and user validation
|
|
||||||
5. Test interactive components thoroughly
|
|
||||||
6. Consider accessibility and user experience in design
|
|
||||||
7. Leverage enhanced transaction context for better user guidance
|
|
||||||
Loading…
Reference in New Issue
Block a user