CLAUDE: Add comprehensive CLAUDE.md documentation files for AI agent guidance
Adding 17 CLAUDE.md files across the project to provide detailed context and implementation guidelines for AI development agents: Root Documentation: - CLAUDE.md - Main project guide with Git workflow requirements Component Documentation: - commands/CLAUDE.md - Command architecture and patterns - models/CLAUDE.md - Pydantic models and validation - services/CLAUDE.md - Service layer and API interactions - tasks/CLAUDE.md - Background tasks and automation - tests/CLAUDE.md - Testing strategies and patterns - utils/CLAUDE.md - Utility functions and decorators - views/CLAUDE.md - Discord UI components and embeds Command Package Documentation: - commands/help/CLAUDE.md - Help system implementation - commands/injuries/CLAUDE.md - Injury management commands - commands/league/CLAUDE.md - League-wide commands - commands/players/CLAUDE.md - Player information commands - commands/profile/CLAUDE.md - User profile commands - commands/teams/CLAUDE.md - Team information commands - commands/transactions/CLAUDE.md - Transaction management - commands/utilities/CLAUDE.md - Utility commands - commands/voice/CLAUDE.md - Voice channel management Key Updates: - Updated .gitignore to track CLAUDE.md files in version control - Added Git Workflow section requiring branch-based development - Documented all architectural patterns and best practices - Included comprehensive command/service implementation guides These files provide essential context for AI agents working on the codebase, ensuring consistent patterns, proper error handling, and maintainable code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
64e60232dd
commit
ca325142d8
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,7 +13,7 @@ logs/
|
||||
|
||||
# Claude files & directories
|
||||
.claude/
|
||||
CLAUDE.md
|
||||
# CLAUDE.md
|
||||
.env*
|
||||
|
||||
# Distribution / packaging
|
||||
|
||||
665
CLAUDE.md
Normal file
665
CLAUDE.md
Normal file
@ -0,0 +1,665 @@
|
||||
# 🚨 CRITICAL: @ MENTION HANDLING 🚨
|
||||
When ANY file is mentioned with @ syntax, you MUST IMMEDIATELY call Read tool on that file BEFORE responding.
|
||||
You will see automatic loads of any @ mentioned filed, this is NOT ENOUGH, it only loads the file contents.
|
||||
You MUST perform Read tool calls on the files directly, even if they were @ included.
|
||||
This is NOT optional - it loads required CLAUDE.md context. along the file path.
|
||||
See @./.claude/force-claude-reads.md for details.
|
||||
|
||||
---
|
||||
|
||||
# CLAUDE.md - Discord Bot v2.0
|
||||
|
||||
This file provides comprehensive guidance to Claude Code (claude.ai/code) when working with the Discord Bot v2.0 codebase.
|
||||
|
||||
**🔍 IMPORTANT:** Always check CLAUDE.md files in the current and parent directories for specific reference information and implementation details before making changes or additions to the codebase.
|
||||
|
||||
## 📚 Documentation References
|
||||
|
||||
### Core Documentation Files
|
||||
- **[commands/CLAUDE.md](commands/CLAUDE.md)** - Command architecture, patterns, and implementation guidelines
|
||||
- **[services/CLAUDE.md](services/CLAUDE.md)** - Service layer architecture, BaseService patterns, and API interactions
|
||||
- **[models/CLAUDE.md](models/CLAUDE.md)** - Pydantic models, validation patterns, and data structures
|
||||
- **[views/CLAUDE.md](views/CLAUDE.md)** - Discord UI components, embeds, modals, and interactive elements
|
||||
- **[tasks/CLAUDE.md](tasks/CLAUDE.md)** - Background tasks, automated cleanup, and scheduled operations
|
||||
- **[tests/CLAUDE.md](tests/CLAUDE.md)** - Testing strategies, patterns, and lessons learned
|
||||
- **[utils/CLAUDE.md](utils/CLAUDE.md)** - Utility functions, logging system, caching, and decorators
|
||||
|
||||
### Command-Specific Documentation
|
||||
- **[commands/league/CLAUDE.md](commands/league/CLAUDE.md)** - League-wide commands (/league, /standings, /schedule)
|
||||
- **[commands/players/CLAUDE.md](commands/players/CLAUDE.md)** - Player information commands (/player)
|
||||
- **[commands/teams/CLAUDE.md](commands/teams/CLAUDE.md)** - Team information and roster commands (/team, /teams, /roster)
|
||||
- **[commands/transactions/CLAUDE.md](commands/transactions/CLAUDE.md)** - Transaction management commands (/mymoves, /legal)
|
||||
- **[commands/voice/CLAUDE.md](commands/voice/CLAUDE.md)** - Voice channel management commands (/voice-channel)
|
||||
- **[commands/help/CLAUDE.md](commands/help/CLAUDE.md)** - Help system commands (/help, /help-create, /help-edit, /help-delete, /help-list)
|
||||
|
||||
## 🏗️ Project Overview
|
||||
|
||||
**Discord Bot v2.0** is a comprehensive Discord bot for managing a Strat-o-Matic Baseball Association (SBA) fantasy league. Built with discord.py and modern async Python patterns.
|
||||
|
||||
### Core Architecture
|
||||
- **Command System**: Modular, package-based command organization
|
||||
- **Logging**: Structured logging with Discord context integration
|
||||
- **Services**: Clean service layer for API interactions
|
||||
- **Caching**: Optional Redis caching for performance
|
||||
- **Error Handling**: Comprehensive error handling with user-friendly messages
|
||||
|
||||
## 🚀 Development Commands
|
||||
|
||||
### Local Development
|
||||
```bash
|
||||
# Start the bot
|
||||
python bot.py
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run tests
|
||||
python -m pytest --tb=short -q
|
||||
|
||||
# Run specific test files
|
||||
python -m pytest tests/test_commands_transactions.py -v --tb=short
|
||||
python -m pytest tests/test_services.py -v
|
||||
```
|
||||
|
||||
### Testing Commands
|
||||
```bash
|
||||
# Full test suite (should pass all tests)
|
||||
python -m pytest --tb=short -q
|
||||
|
||||
# Test specific components
|
||||
python -m pytest tests/test_utils_decorators.py -v
|
||||
python -m pytest tests/test_models.py -v
|
||||
python -m pytest tests/test_api_client_with_aioresponses.py -v
|
||||
```
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
discord-app-v2/
|
||||
├── CLAUDE.md files (check these first!)
|
||||
├── bot.py # Main bot entry point
|
||||
├── commands/ # Discord slash commands
|
||||
│ ├── CLAUDE.md # 🔍 Command architecture guide
|
||||
│ ├── league/ # League-wide commands
|
||||
│ │ ├── CLAUDE.md # 🔍 League command reference
|
||||
│ │ └── info.py # /league command
|
||||
│ ├── players/ # Player information commands
|
||||
│ │ ├── CLAUDE.md # 🔍 Player command reference
|
||||
│ │ └── info.py # /player command
|
||||
│ ├── teams/ # Team information commands
|
||||
│ │ ├── CLAUDE.md # 🔍 Team command reference
|
||||
│ │ ├── info.py # /team, /teams commands
|
||||
│ │ └── roster.py # /roster command
|
||||
│ ├── transactions/ # Transaction management
|
||||
│ │ ├── CLAUDE.md # 🔍 Transaction command reference
|
||||
│ │ └── management.py # /mymoves, /legal commands
|
||||
│ └── voice/ # Voice channel management
|
||||
│ ├── CLAUDE.md # 🔍 Voice command reference
|
||||
│ ├── channels.py # /voice-channel commands
|
||||
│ ├── cleanup_service.py # Automatic channel cleanup
|
||||
│ └── tracker.py # JSON-based channel tracking
|
||||
├── services/ # API service layer
|
||||
│ └── CLAUDE.md # 🔍 Service layer documentation
|
||||
├── models/ # Pydantic data models
|
||||
│ └── CLAUDE.md # 🔍 Model patterns and validation
|
||||
├── tasks/ # Background automated tasks
|
||||
│ └── CLAUDE.md # 🔍 Task system documentation
|
||||
├── utils/ # Utility functions and helpers
|
||||
│ ├── CLAUDE.md # 🔍 Utils documentation
|
||||
│ ├── logging.py # Structured logging system
|
||||
│ ├── decorators.py # Command and caching decorators
|
||||
│ └── cache.py # Redis caching system
|
||||
├── views/ # Discord UI components
|
||||
│ └── CLAUDE.md # 🔍 UI components and embeds
|
||||
├── tests/ # Test suite
|
||||
│ └── CLAUDE.md # 🔍 Testing guide and patterns
|
||||
└── logs/ # Log files
|
||||
```
|
||||
|
||||
## 🔧 Key Development Patterns
|
||||
|
||||
### 1. Command Implementation with @logged_command Decorator
|
||||
|
||||
**✅ Recommended Pattern:**
|
||||
```python
|
||||
from utils.decorators import logged_command
|
||||
from utils.logging import get_contextual_logger
|
||||
|
||||
class YourCommandCog(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.logger = get_contextual_logger(f'{__name__}.YourCommandCog') # Required
|
||||
|
||||
@discord.app_commands.command(name="example")
|
||||
@logged_command("/example") # Add this decorator
|
||||
async def your_command(self, interaction, param: str):
|
||||
# Only business logic - no try/catch/finally boilerplate needed
|
||||
# Decorator handles all logging, timing, and error handling
|
||||
result = await some_service.get_data(param)
|
||||
embed = create_embed(result)
|
||||
await interaction.followup.send(embed=embed)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Eliminates ~15-20 lines of boilerplate per command
|
||||
- Automatic trace ID generation and request correlation
|
||||
- Consistent error handling and operation timing
|
||||
- Standardized logging patterns
|
||||
|
||||
### 2. Service Layer with Caching
|
||||
|
||||
**✅ Service Implementation:**
|
||||
```python
|
||||
from utils.decorators import cached_api_call
|
||||
from services.base_service import BaseService
|
||||
|
||||
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)])
|
||||
```
|
||||
|
||||
### 3. Structured Logging
|
||||
|
||||
**✅ Logging Pattern:**
|
||||
```python
|
||||
from utils.logging import get_contextual_logger, set_discord_context
|
||||
|
||||
# In command handlers
|
||||
set_discord_context(interaction=interaction, command="/example", param_value=param)
|
||||
trace_id = self.logger.start_operation("operation_name")
|
||||
|
||||
self.logger.info("Operation started", context_param=value)
|
||||
# ... business logic ...
|
||||
self.logger.end_operation(trace_id, "completed")
|
||||
```
|
||||
|
||||
### 4. Autocomplete Pattern (REQUIRED)
|
||||
|
||||
**✅ Recommended Pattern - Standalone Function:**
|
||||
```python
|
||||
# Define autocomplete function OUTSIDE the class
|
||||
async def entity_name_autocomplete(
|
||||
interaction: discord.Interaction,
|
||||
current: str,
|
||||
) -> List[app_commands.Choice[str]]:
|
||||
"""Autocomplete for entity names."""
|
||||
try:
|
||||
# Get matching entities from service
|
||||
entities = await service.get_entities_for_autocomplete(current, limit=25)
|
||||
|
||||
return [
|
||||
app_commands.Choice(name=entity.display_name, value=entity.name)
|
||||
for entity in entities
|
||||
]
|
||||
except Exception:
|
||||
# Return empty list on error to avoid breaking autocomplete
|
||||
return []
|
||||
|
||||
|
||||
class YourCommandCog(commands.Cog):
|
||||
@app_commands.command(name="command")
|
||||
@app_commands.autocomplete(parameter_name=entity_name_autocomplete) # Reference function
|
||||
@logged_command("/command")
|
||||
async def your_command(self, interaction, parameter_name: str):
|
||||
# ... command implementation ...
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Consistent pattern across all commands
|
||||
- Reusable autocomplete functions
|
||||
- Cleaner code organization
|
||||
- Easier testing and maintenance
|
||||
|
||||
**❌ Avoid Method-Based Autocomplete:**
|
||||
```python
|
||||
# DON'T USE THIS PATTERN
|
||||
class YourCommandCog(commands.Cog):
|
||||
@app_commands.command(name="command")
|
||||
async def your_command(self, interaction, parameter: str):
|
||||
pass
|
||||
|
||||
@your_command.autocomplete('parameter') # Avoid this pattern
|
||||
async def parameter_autocomplete(self, interaction, current: str):
|
||||
pass
|
||||
```
|
||||
|
||||
**Reference Implementation:**
|
||||
- See `commands/players/info.py:20-67` for the canonical example
|
||||
- See `commands/help/main.py:30-48` for help topic autocomplete
|
||||
|
||||
### 5. Discord Embed Usage - Emoji Best Practices
|
||||
|
||||
**CRITICAL:** Avoid double emoji in Discord embeds by following these guidelines:
|
||||
|
||||
#### Template Method Emoji Prefixes
|
||||
|
||||
The following `EmbedTemplate` methods **automatically add emoji prefixes** to titles:
|
||||
|
||||
- `EmbedTemplate.success()` → adds `✅` prefix
|
||||
- `EmbedTemplate.error()` → adds `❌` prefix
|
||||
- `EmbedTemplate.warning()` → adds `⚠️` prefix
|
||||
- `EmbedTemplate.info()` → adds `ℹ️` prefix
|
||||
- `EmbedTemplate.loading()` → adds `⏳` prefix
|
||||
|
||||
#### ✅ CORRECT Usage Patterns
|
||||
|
||||
```python
|
||||
# ✅ CORRECT - Using template method without emoji in title
|
||||
embed = EmbedTemplate.success(
|
||||
title="Operation Completed",
|
||||
description="Your action was successful"
|
||||
)
|
||||
# Result: "✅ Operation Completed"
|
||||
|
||||
# ✅ CORRECT - Custom emoji with create_base_embed
|
||||
embed = EmbedTemplate.create_base_embed(
|
||||
title="🎉 Special Success Message",
|
||||
description="Using a different emoji",
|
||||
color=EmbedColors.SUCCESS
|
||||
)
|
||||
# Result: "🎉 Special Success Message"
|
||||
```
|
||||
|
||||
#### ❌ WRONG Usage Patterns
|
||||
|
||||
```python
|
||||
# ❌ WRONG - Double emoji
|
||||
embed = EmbedTemplate.success(
|
||||
title="✅ Operation Completed", # Will result in "✅ ✅ Operation Completed"
|
||||
description="Your action was successful"
|
||||
)
|
||||
|
||||
# ❌ WRONG - Emoji in title with auto-prefix method
|
||||
embed = EmbedTemplate.error(
|
||||
title="❌ Failed", # Will result in "❌ ❌ Failed"
|
||||
description="Something went wrong"
|
||||
)
|
||||
```
|
||||
|
||||
#### Rules for Embed Creation
|
||||
|
||||
1. **When using template methods** (`success()`, `error()`, `warning()`, `info()`, `loading()`):
|
||||
- ❌ **DON'T** include emojis in the title parameter
|
||||
- ✅ **DO** use plain text titles
|
||||
- The template method will add the appropriate emoji automatically
|
||||
|
||||
2. **When you want custom emojis**:
|
||||
- ✅ **DO** use `EmbedTemplate.create_base_embed()`
|
||||
- ✅ **DO** specify the appropriate color parameter
|
||||
- You have full control over the title including custom emojis
|
||||
|
||||
3. **Code review checklist**:
|
||||
- Check all `EmbedTemplate.success/error/warning/info/loading()` calls
|
||||
- Verify titles don't contain emojis
|
||||
- Ensure `create_base_embed()` is used for custom emoji titles
|
||||
|
||||
#### Common Mistakes to Avoid
|
||||
|
||||
```python
|
||||
# ❌ BAD: Double warning emoji
|
||||
embed = EmbedTemplate.warning(
|
||||
title="⚠️ Delete Command" # Results in "⚠️ ⚠️ Delete Command"
|
||||
)
|
||||
|
||||
# ✅ GOOD: Template adds emoji automatically
|
||||
embed = EmbedTemplate.warning(
|
||||
title="Delete Command" # Results in "⚠️ Delete Command"
|
||||
)
|
||||
|
||||
# ❌ BAD: Different emoji with auto-prefix method
|
||||
embed = EmbedTemplate.error(
|
||||
title="🗑️ Deleted" # Results in "❌ 🗑️ Deleted"
|
||||
)
|
||||
|
||||
# ✅ GOOD: Use create_base_embed for custom emoji
|
||||
embed = EmbedTemplate.create_base_embed(
|
||||
title="🗑️ Deleted", # Results in "🗑️ Deleted"
|
||||
color=EmbedColors.ERROR
|
||||
)
|
||||
```
|
||||
|
||||
**Reference Files**:
|
||||
- `views/embeds.py` - EmbedTemplate implementation
|
||||
- `DOUBLE_EMOJI_AUDIT.md` - Complete audit of double emoji issues (January 2025)
|
||||
- Fixed files: `tasks/custom_command_cleanup.py`, `views/help_commands.py`, `views/custom_commands.py`
|
||||
|
||||
## 🎯 Development Guidelines
|
||||
|
||||
### Git Workflow - CRITICAL
|
||||
|
||||
**🚨 NEVER make code changes directly to the `main` branch. Always work in a separate branch.**
|
||||
|
||||
#### Branch Workflow
|
||||
1. **Create a new branch** for all code changes:
|
||||
```bash
|
||||
git checkout -b feature/your-feature-name
|
||||
# or
|
||||
git checkout -b fix/bug-description
|
||||
```
|
||||
|
||||
2. **Make your changes** in the feature branch
|
||||
|
||||
3. **Commit your changes** following the commit message format:
|
||||
```bash
|
||||
git commit -m "CLAUDE: Your commit message
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
4. **Push your branch** to the remote repository:
|
||||
```bash
|
||||
git push -u origin feature/your-feature-name
|
||||
```
|
||||
|
||||
5. **Create a Pull Request** to merge into `main` when ready
|
||||
|
||||
**Why This Matters:**
|
||||
- Protects the `main` branch from broken code
|
||||
- Allows code review before merging
|
||||
- Makes it easy to revert changes if needed
|
||||
- Enables parallel development on multiple features
|
||||
- Maintains a clean, stable main branch
|
||||
|
||||
**Exception:** Only emergency hotfixes or critical production issues may warrant direct commits to `main`, and only with explicit authorization.
|
||||
|
||||
### Before Making Changes
|
||||
1. **Check CLAUDE.md files** in current and parent directories
|
||||
2. **Review existing patterns** in similar commands/services
|
||||
3. **Follow the @logged_command decorator pattern** for new commands
|
||||
4. **Use structured logging** with contextual information
|
||||
5. **Add appropriate caching** for expensive operations
|
||||
|
||||
### Command Development
|
||||
- **Always use `@logged_command` decorator** for Discord commands
|
||||
- **Ensure command class has `self.logger`** in `__init__`
|
||||
- **Focus on business logic** - decorator handles boilerplate
|
||||
- **Use EmbedTemplate** for consistent Discord embed styling
|
||||
- **CRITICAL: Follow emoji best practices** - see "Discord Embed Usage - Emoji Best Practices" section
|
||||
- **Follow error handling patterns** from existing commands
|
||||
- **Use standalone functions for autocomplete** - see Autocomplete Pattern below
|
||||
|
||||
### Testing Requirements
|
||||
- **All tests should pass**: Run `python -m pytest --tb=short -q`
|
||||
- **Use aioresponses** for HTTP client testing (see `tests/CLAUDE.md`)
|
||||
- **Provide complete model data** that satisfies Pydantic validation
|
||||
- **Follow testing patterns** documented in `tests/CLAUDE.md`
|
||||
|
||||
### Model Requirements
|
||||
- **Database entities require `id` fields** since they're always fetched from database
|
||||
- **Use explicit None checks** (`if obj is None:`) for better type safety
|
||||
- **Required fields eliminate Optional type issues**
|
||||
|
||||
## 🔍 Key Files to Reference
|
||||
|
||||
### When Adding New Commands
|
||||
1. **Read `commands/CLAUDE.md`** - Command architecture and patterns
|
||||
2. **Check existing command files** in relevant package (league/players/teams/transactions)
|
||||
3. **Review package-specific CLAUDE.md** files for implementation details
|
||||
4. **Follow `@logged_command` decorator patterns**
|
||||
|
||||
### When Working with Services
|
||||
1. **Check `services/` directory** for existing service patterns
|
||||
2. **Use `BaseService[T]` inheritance** for consistent API interactions
|
||||
3. **Add caching decorators** for expensive operations
|
||||
4. **Follow error handling patterns**
|
||||
|
||||
### When Writing Tests
|
||||
1. **Read `tests/CLAUDE.md`** thoroughly - contains crucial testing patterns
|
||||
2. **Use aioresponses** for HTTP mocking, not manual AsyncMock
|
||||
3. **Provide complete model data** with helper functions
|
||||
4. **Test both success and error scenarios**
|
||||
|
||||
### When Working with Utilities
|
||||
1. **Read `utils/CLAUDE.md`** for logging, caching, and decorators
|
||||
2. **Use structured logging** with `get_contextual_logger()`
|
||||
3. **Leverage caching decorators** for performance optimization
|
||||
4. **Follow established patterns** for Discord context setting
|
||||
|
||||
## 🚨 Critical Reminders
|
||||
|
||||
### Decorator Migration (Completed)
|
||||
- **All commands use `@logged_command`** - eliminates boilerplate logging code
|
||||
- **Standardizes error handling** and operation timing across all commands
|
||||
- **See existing commands** for implementation patterns
|
||||
|
||||
### Model Breaking Changes (Implemented)
|
||||
- **Database entities require `id` fields** - test cases must provide ID values
|
||||
- **Use `Player(id=123, ...)` and `Team(id=456, ...)` in tests**
|
||||
- **No more Optional[int] warnings** - improved PyLance type safety
|
||||
|
||||
### Redis Caching (Available)
|
||||
- **Optional caching infrastructure** - graceful fallback without Redis
|
||||
- **Use `@cached_api_call()` and `@cached_single_item()` decorators**
|
||||
- **Zero breaking changes** - all existing functionality preserved
|
||||
|
||||
### Code Quality Requirements
|
||||
- **Pylance warnings are disabled for optional values** - always either return a value or raise an Exception
|
||||
- **Use "Raise or Return" pattern** - do not return optional values unless specifically required
|
||||
- **Favor Python dataclasses** over standard classes unless there's a technical limitation
|
||||
|
||||
## 🧪 Testing Strategy
|
||||
|
||||
### Current Test Coverage
|
||||
- **44 comprehensive tests** covering core functionality
|
||||
- **HTTP testing with aioresponses** - reliable async HTTP mocking
|
||||
- **Service layer testing** - complete model validation
|
||||
- **All tests should pass** - run `python -m pytest --tb=short -q`
|
||||
|
||||
### Test Files by Component
|
||||
- `test_utils_decorators.py` - Decorator functionality
|
||||
- `test_models.py` - Pydantic model validation
|
||||
- `test_services.py` - Service layer operations (25 tests)
|
||||
- `test_api_client_with_aioresponses.py` - HTTP client operations (19 tests)
|
||||
|
||||
## 🔧 Environment Variables
|
||||
|
||||
### Required
|
||||
```bash
|
||||
BOT_TOKEN=your_discord_bot_token
|
||||
API_TOKEN=your_database_api_token
|
||||
DB_URL=http://localhost:8000
|
||||
GUILD_ID=your_discord_server_id
|
||||
LOG_LEVEL=INFO
|
||||
```
|
||||
|
||||
### Optional (Caching)
|
||||
```bash
|
||||
REDIS_URL=redis://localhost:6379 # Empty disables caching
|
||||
REDIS_CACHE_TTL=300 # Default TTL in seconds
|
||||
```
|
||||
|
||||
## 📊 Monitoring and Logs
|
||||
|
||||
### Log Files
|
||||
- **`logs/discord_bot_v2.log`** - Human-readable logs
|
||||
- **`logs/discord_bot_v2.json`** - Structured JSON logs for analysis
|
||||
|
||||
### Log Analysis Examples
|
||||
```bash
|
||||
# Find all errors
|
||||
jq 'select(.level == "ERROR")' logs/discord_bot_v2.json
|
||||
|
||||
# Track specific request
|
||||
jq 'select(.trace_id == "abc12345")' logs/discord_bot_v2.json
|
||||
|
||||
# Find slow operations
|
||||
jq 'select(.extra.duration_ms > 5000)' logs/discord_bot_v2.json
|
||||
```
|
||||
|
||||
## 🎯 Future AI Agent Instructions
|
||||
|
||||
### Always Start Here
|
||||
1. **Read this CLAUDE.md file completely**
|
||||
2. **Check CLAUDE.md files** in current and parent directories
|
||||
3. **Review relevant package-specific documentation**
|
||||
4. **Understand existing patterns** before implementing changes
|
||||
|
||||
### Documentation Priority
|
||||
1. **Current directory CLAUDE.md** - Most specific context
|
||||
2. **Parent directory CLAUDE.md** - Package-level context
|
||||
3. **Root project CLAUDE.md** - Overall project guidance
|
||||
4. **Component-specific CLAUDE.md** files - Detailed implementation guides
|
||||
|
||||
### Implementation Checklist
|
||||
- [ ] Reviewed relevant CLAUDE.md files
|
||||
- [ ] Followed existing command/service patterns
|
||||
- [ ] Used `@logged_command` decorator for new commands
|
||||
- [ ] Added structured logging with context
|
||||
- [ ] Included appropriate error handling
|
||||
- [ ] **Followed Discord Embed emoji best practices** (no double emojis)
|
||||
- [ ] Added tests following established patterns
|
||||
- [ ] Verified all tests pass
|
||||
|
||||
## 🔄 Recent Major Enhancements (January 2025)
|
||||
|
||||
### Custom Help Commands System (January 2025)
|
||||
**Comprehensive admin-managed help system for league documentation**:
|
||||
|
||||
- **Full CRUD Operations**: Create, read, update, and delete help topics via Discord commands
|
||||
- **Permission System**: Administrators and "Help Editor" role can manage content
|
||||
- **Rich Features**:
|
||||
- Markdown-formatted content (up to 4000 characters)
|
||||
- Category organization (rules, guides, resources, info, faq)
|
||||
- View tracking and analytics
|
||||
- Soft delete with restore capability
|
||||
- Full audit trail (creator, editor, timestamps)
|
||||
- Autocomplete for topic discovery
|
||||
- Paginated list views
|
||||
- **Interactive UI**: Modals for creation/editing, confirmation dialogs for deletion
|
||||
- **Replaces**: Planned `/links` command with more flexible solution
|
||||
- **Commands**: `/help`, `/help-create`, `/help-edit`, `/help-delete`, `/help-list`
|
||||
- **Documentation**: See `commands/help/CLAUDE.md` for complete reference
|
||||
- **Database**: Requires `help_commands` table migration (see `.claude/DATABASE_MIGRATION_HELP_COMMANDS.md`)
|
||||
|
||||
**Key Components**:
|
||||
- **Model**: `models/help_command.py` - Pydantic models with validation
|
||||
- **Service**: `services/help_commands_service.py` - Business logic and API integration
|
||||
- **Views**: `views/help_commands.py` - Interactive modals and list views
|
||||
- **Commands**: `commands/help/main.py` - Command handlers with @logged_command decorator
|
||||
|
||||
### Enhanced Transaction Builder with sWAR Tracking
|
||||
**Major upgrade to transaction management system**:
|
||||
|
||||
- **Comprehensive sWAR Calculations**: Now tracks Major League and Minor League sWAR projections
|
||||
- **Pre-existing Transaction Support**: Accounts for scheduled moves when validating new transactions
|
||||
- **Organizational Team Matching**: Proper handling of PORMIL, PORIL transactions for POR teams
|
||||
- **Enhanced User Interface**: Transaction embeds show complete context including pre-existing moves
|
||||
|
||||
### Team Model Organizational Affiliates
|
||||
**New Team model methods for organizational relationships**:
|
||||
|
||||
- **`team.major_league_affiliate()`**: Get ML team via API
|
||||
- **`team.minor_league_affiliate()`**: Get MiL team via API
|
||||
- **`team.injured_list_affiliate()`**: Get IL team via API
|
||||
- **`team.is_same_organization()`**: Check if teams belong to same organization
|
||||
|
||||
### Key Benefits for Users
|
||||
- **Complete Transaction Context**: Users see impact of both current and scheduled moves
|
||||
- **Accurate sWAR Projections**: Better strategic decision-making with team strength data
|
||||
- **Improved Validation**: Proper roster type detection for all transaction types
|
||||
- **Enhanced UX**: Clean, informative displays with contextual information
|
||||
|
||||
### Technical Improvements
|
||||
- **API Efficiency**: Cached data to avoid repeated calls
|
||||
- **Backwards Compatibility**: All existing functionality preserved
|
||||
- **Error Handling**: Graceful fallbacks and proper exception handling
|
||||
- **Testing**: Comprehensive test coverage for new functionality
|
||||
|
||||
### Critical Bug Fixes (January 2025)
|
||||
|
||||
#### 1. Trade Channel Creation Permission Error (Fixed)
|
||||
**Issue**: Trade channels failed to create with Discord API error 50013 "Missing Permissions"
|
||||
|
||||
**Root Cause**: The bot was attempting to grant itself `manage_channels` and `manage_permissions` in channel-specific permission overwrites during channel creation. Discord prohibits bots from self-granting elevated permissions in channel overwrites as a security measure.
|
||||
|
||||
**Fix**: Removed `manage_channels` and `manage_permissions` from bot's channel-specific overwrites in `commands/transactions/trade_channels.py:74-77`. The bot's server-level permissions are sufficient for all channel management operations.
|
||||
|
||||
**Impact**: Trade discussion channels now create successfully. All channel management features (create, delete, permission updates) work correctly with server-level permissions only.
|
||||
|
||||
**Files Changed**:
|
||||
- `commands/transactions/trade_channels.py` - Simplified bot permission overwrites
|
||||
- `commands/transactions/CLAUDE.md` - Documented permission requirements and fix
|
||||
|
||||
#### 2. TeamService Method Name AttributeError (Fixed)
|
||||
**Issue**: Bot crashed with `AttributeError: 'TeamService' object has no attribute 'get_team_by_id'` when adding players to trades
|
||||
|
||||
**Root Cause**: Code was calling non-existent method `team_service.get_team_by_id()`. The correct method name is `team_service.get_team()`.
|
||||
|
||||
**Fix**: Updated method call in `services/trade_builder.py:201` and all corresponding test mocks in `tests/test_services_trade_builder.py`.
|
||||
|
||||
**Impact**: Adding players to trades now works correctly. All 18 trade builder tests pass.
|
||||
|
||||
**Files Changed**:
|
||||
- `services/trade_builder.py` - Changed `get_team_by_id()` to `get_team()`
|
||||
- `tests/test_services_trade_builder.py` - Updated all test mocks
|
||||
- `services/CLAUDE.md` - Documented correct TeamService method names
|
||||
|
||||
**Prevention**: Added clear documentation in `services/CLAUDE.md` showing correct TeamService method signatures to prevent future confusion.
|
||||
|
||||
### Critical Bug Fixes (October 2025)
|
||||
|
||||
#### 1. Custom Command Execution Validation Error (Fixed)
|
||||
**Issue**: Custom commands failed to execute with Pydantic validation error: `creator_id Field required`
|
||||
|
||||
**Root Cause**: The API's `/custom_commands/by_name/{name}/execute` endpoint returns command data **without** the `creator_id` field, but the `CustomCommand` model required this field. This caused validation to fail when parsing the execute endpoint response.
|
||||
|
||||
**Fix**: Made `creator_id` field optional in the `CustomCommand` model:
|
||||
```python
|
||||
# Before
|
||||
creator_id: int = Field(..., description="ID of the creator")
|
||||
|
||||
# After
|
||||
creator_id: Optional[int] = Field(None, description="ID of the creator (may be missing from execute endpoint)")
|
||||
```
|
||||
|
||||
**Impact**: Custom commands (`/cc`) now execute successfully. The execute endpoint only needs to return command content, not creator information. Permission checks (edit/delete) use different endpoints that include full creator data.
|
||||
|
||||
**Files Changed**:
|
||||
- `models/custom_command.py:30` - Made `creator_id` optional
|
||||
- `models/CLAUDE.md` - Documented Custom Command Model changes
|
||||
|
||||
**Why This Design Works**:
|
||||
- Execute endpoint: Returns minimal data (name, content, use_count) for fast execution
|
||||
- Get/Create/Update/Delete endpoints: Return complete data including creator information
|
||||
- Permission checks always use endpoints with full creator data
|
||||
|
||||
#### 2. Admin Commands Type Safety Issues (Fixed)
|
||||
**Issue**: Multiple Pylance type errors in `commands/admin/management.py` causing potential runtime errors
|
||||
|
||||
**Root Causes**:
|
||||
1. Accessing `guild_permissions` on `User` type (only exists on `Member`)
|
||||
2. Passing `Optional[int]` to `discord.Object(id=...)` which requires `int`
|
||||
3. Calling `purge()` on channel types that don't support it
|
||||
4. Calling `delete()` on potentially `None` message
|
||||
5. Passing `None` to `content` parameter that requires `str`
|
||||
|
||||
**Fixes**:
|
||||
1. Added `isinstance(interaction.user, discord.Member)` check before accessing `guild_permissions`
|
||||
2. Added explicit None check: `if not interaction.guild_id: raise ValueError(...)`
|
||||
3. Added channel type validation: `isinstance(interaction.channel, (discord.TextChannel, discord.Thread, ...))`
|
||||
4. Added None check: `if message:` before calling `message.delete()`
|
||||
5. Changed from conditional variable to explicit if/else branches for `content` parameter
|
||||
6. Removed unused imports (`Optional`, `Union`)
|
||||
|
||||
**Impact**: All admin commands now have proper type safety. No more Pylance warnings, and code is more robust against edge cases.
|
||||
|
||||
**Files Changed**:
|
||||
- `commands/admin/management.py:26-42` - Guild permissions and member checks
|
||||
- `commands/admin/management.py:246-253` - Guild ID validation
|
||||
- `commands/admin/management.py:361-366` - Channel type validation
|
||||
- `commands/admin/management.py:389-393` - Message None check
|
||||
- `commands/admin/management.py:436-439` - Content parameter handling
|
||||
- `commands/admin/management.py:6` - Removed unused imports
|
||||
|
||||
**Prevention**: Follow Discord.py type patterns - always check for `Member` vs `User`, validate optional IDs before use, and verify channel types support the operations you're calling.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** October 2025
|
||||
**Maintenance:** Keep this file synchronized with CLAUDE.md files when making significant architectural changes
|
||||
**Next Review:** When major new features or patterns are added
|
||||
|
||||
This CLAUDE.md file serves as the central reference point for AI development agents. Always consult the referenced CLAUDE.md files for the most detailed and current information about specific components and implementation patterns.
|
||||
699
commands/CLAUDE.md
Normal file
699
commands/CLAUDE.md
Normal file
@ -0,0 +1,699 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
### **Current Implementation Status (October 2025)**
|
||||
|
||||
#### **Implemented Packages**
|
||||
```
|
||||
commands/
|
||||
├── README.md # This documentation
|
||||
├── __init__.py
|
||||
├── players/ # ✅ COMPLETED - Player information
|
||||
│ ├── README.md # Player commands documentation
|
||||
│ ├── __init__.py
|
||||
│ └── info.py # /player command
|
||||
├── teams/ # ✅ COMPLETED - Team information
|
||||
│ ├── README.md # Team commands documentation
|
||||
│ ├── __init__.py
|
||||
│ ├── info.py # /team, /teams commands
|
||||
│ └── roster.py # /roster command
|
||||
├── league/ # ✅ COMPLETED - League-wide features
|
||||
│ ├── README.md # League commands documentation
|
||||
│ ├── __init__.py
|
||||
│ ├── info.py # /league command
|
||||
│ ├── standings.py # /standings command
|
||||
│ ├── schedule.py # /schedule command
|
||||
│ └── submit_scorecard.py # /submit-scorecard command
|
||||
├── transactions/ # ✅ COMPLETED - Trade & roster moves
|
||||
│ ├── README.md # Transaction commands documentation
|
||||
│ ├── __init__.py
|
||||
│ ├── management.py # /mymoves, /legal commands
|
||||
│ ├── trade.py # /trade command with builder UI
|
||||
│ ├── trade_channels.py # /trade-channel commands
|
||||
│ ├── trade_channel_tracker.py # Channel tracking service
|
||||
│ └── dropadd.py # /dropadd command
|
||||
├── admin/ # ✅ COMPLETED - Admin tools
|
||||
│ ├── __init__.py
|
||||
│ ├── management.py # /sync, /shutdown commands
|
||||
│ └── users.py # User management commands
|
||||
├── custom_commands/ # ✅ COMPLETED - Custom command system
|
||||
│ ├── __init__.py
|
||||
│ └── main.py # /custom commands CRUD
|
||||
├── help/ # ✅ COMPLETED - Help system
|
||||
│ ├── README.md # Help commands documentation
|
||||
│ ├── __init__.py
|
||||
│ └── main.py # /help, /help-create, /help-edit, etc.
|
||||
├── profile/ # ✅ COMPLETED - User profiles
|
||||
│ ├── README.md # Profile commands documentation
|
||||
│ ├── __init__.py
|
||||
│ └── main.py # /profile commands
|
||||
├── injuries/ # ✅ COMPLETED - Injury management
|
||||
│ ├── README.md # Injury commands documentation
|
||||
│ ├── __init__.py
|
||||
│ └── management.py # /injury commands
|
||||
├── dice/ # ✅ COMPLETED - Dice rolling
|
||||
│ ├── __init__.py
|
||||
│ └── rolls.py # /roll, /dice commands
|
||||
├── voice/ # ✅ COMPLETED - Voice channel management
|
||||
│ ├── README.md # Voice commands documentation
|
||||
│ ├── __init__.py
|
||||
│ ├── channels.py # /voice-channel commands
|
||||
│ ├── tracker.py # Channel tracking
|
||||
│ └── cleanup_service.py # Automated cleanup task
|
||||
├── utilities/ # ✅ COMPLETED - Utility commands
|
||||
│ ├── README.md # Utilities documentation
|
||||
│ ├── __init__.py
|
||||
│ └── charts.py # /chart commands
|
||||
├── soak/ # ✅ COMPLETED - SOAK features
|
||||
│ ├── __init__.py
|
||||
│ └── main.py # SOAK-related commands
|
||||
└── examples/ # 📚 REFERENCE - Migration examples
|
||||
├── __init__.py
|
||||
├── migration_example.py # Example migration patterns
|
||||
└── enhanced_player.py # Enhanced command example
|
||||
```
|
||||
|
||||
#### **Package Documentation Quick Reference**
|
||||
|
||||
Each package has its own detailed README.md with implementation specifics:
|
||||
|
||||
| Package | README | Key Commands |
|
||||
|---------|--------|--------------|
|
||||
| **players/** | [README.md](players/README.md) | `/player` - Player information and stats |
|
||||
| **teams/** | [README.md](teams/README.md) | `/team`, `/teams`, `/roster` - Team info and rosters |
|
||||
| **league/** | [README.md](league/README.md) | `/league`, `/standings`, `/schedule`, `/submit-scorecard` |
|
||||
| **transactions/** | [README.md](transactions/README.md) | `/trade`, `/mymoves`, `/legal`, `/trade-channel`, `/dropadd` |
|
||||
| **help/** | [README.md](help/README.md) | `/help`, `/help-create`, `/help-edit`, `/help-delete`, `/help-list` |
|
||||
| **profile/** | [README.md](profile/README.md) | `/profile` - User profile management |
|
||||
| **injuries/** | [README.md](injuries/README.md) | `/injury` - Injury management and tracking |
|
||||
| **voice/** | [README.md](voice/README.md) | `/voice-channel` - Voice channel creation and cleanup |
|
||||
| **utilities/** | [README.md](utilities/README.md) | `/chart` - Chart and utility commands |
|
||||
|
||||
#### **Future Expansion Ideas**
|
||||
- **Draft System** - `/draft` commands for draft management
|
||||
- **Advanced Stats** - `/player-compare`, `/player-rankings`, `/leaderboard`
|
||||
- **Team Analytics** - `/team-stats`, `/team-leaders`
|
||||
- **League Leaders** - `/leaders`, `/awards`
|
||||
- **Historical Data** - `/history`, `/records`
|
||||
|
||||
## 🏗️ 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
|
||||
from commands.teams import setup_teams
|
||||
from commands.league import setup_league
|
||||
from commands.custom_commands import setup_custom_commands
|
||||
from commands.admin import setup_admin
|
||||
from commands.transactions import setup_transactions
|
||||
from commands.dice import setup_dice
|
||||
from commands.voice import setup_voice
|
||||
from commands.utilities import setup_utilities
|
||||
from commands.help import setup_help_commands
|
||||
from commands.profile import setup_profile_commands
|
||||
from commands.soak import setup_soak
|
||||
from commands.injuries import setup_injuries
|
||||
|
||||
# Define command packages to load (current implementation)
|
||||
command_packages = [
|
||||
("players", setup_players),
|
||||
("teams", setup_teams),
|
||||
("league", setup_league),
|
||||
("custom_commands", setup_custom_commands),
|
||||
("admin", setup_admin),
|
||||
("transactions", setup_transactions),
|
||||
("dice", setup_dice),
|
||||
("voice", setup_voice),
|
||||
("utilities", setup_utilities),
|
||||
("help", setup_help_commands),
|
||||
("profile", setup_profile_commands),
|
||||
("soak", setup_soak),
|
||||
("injuries", setup_injuries),
|
||||
]
|
||||
|
||||
# Loop-based loading with error isolation
|
||||
total_successful = 0
|
||||
total_failed = 0
|
||||
|
||||
for package_name, setup_func in command_packages:
|
||||
try:
|
||||
successful, failed, failed_modules = await setup_func(self)
|
||||
total_successful += successful
|
||||
total_failed += failed
|
||||
|
||||
if failed == 0:
|
||||
self.logger.info(f"✅ {package_name} commands loaded successfully ({successful} cogs)")
|
||||
else:
|
||||
self.logger.warning(
|
||||
f"⚠️ {package_name} commands loaded with issues: "
|
||||
f"{successful} successful, {failed} failed"
|
||||
)
|
||||
if failed_modules:
|
||||
self.logger.warning(f"Failed modules: {', '.join(failed_modules)}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"❌ Failed to load {package_name} package: {e}", exc_info=True)
|
||||
total_failed += 1
|
||||
|
||||
# Log final summary
|
||||
if total_failed == 0:
|
||||
self.logger.info(f"🎉 All command packages loaded successfully ({total_successful} total cogs)")
|
||||
else:
|
||||
self.logger.warning(
|
||||
f"⚠️ Command loading completed with issues: "
|
||||
f"{total_successful} successful, {total_failed} failed"
|
||||
)
|
||||
```
|
||||
|
||||
## 📋 Development Guidelines
|
||||
|
||||
### **Adding New Command Packages**
|
||||
|
||||
**Before Starting:** Review existing package README files for reference implementations:
|
||||
- Check [Package Documentation Quick Reference](#package-documentation-quick-reference) for relevant examples
|
||||
- Study similar packages for patterns (e.g., review `teams/` README when creating roster commands)
|
||||
- Follow the `@logged_command` decorator pattern shown in existing commands
|
||||
|
||||
#### **1. Create Package Structure**
|
||||
```bash
|
||||
mkdir commands/your_package
|
||||
touch commands/your_package/__init__.py
|
||||
touch commands/your_package/your_command.py
|
||||
touch commands/your_package/README.md # Document your package!
|
||||
```
|
||||
|
||||
#### **2. Implement Command Module**
|
||||
- Follow the pattern from existing packages (e.g., `players/info.py`, `teams/info.py`)
|
||||
- **Use `@logged_command` decorator** - eliminates boilerplate error handling
|
||||
- Use contextual logger: `self.logger = get_contextual_logger(f'{__name__}.ClassName')`
|
||||
- Always defer responses: `await interaction.response.defer()`
|
||||
- Type hints and comprehensive docstrings
|
||||
- See `commands/examples/` for migration patterns
|
||||
|
||||
#### **3. Create Package Setup Function**
|
||||
- Follow the pattern from existing `__init__.py` files (e.g., `players/__init__.py`, `teams/__init__.py`)
|
||||
- Use loop-based cog loading with error isolation
|
||||
- Return tuple: `(successful, failed, failed_modules)`
|
||||
- Comprehensive logging with emojis for quick scanning
|
||||
|
||||
#### **4. Document Your Package**
|
||||
- Create a `README.md` in your package directory
|
||||
- Document commands, patterns, and implementation details
|
||||
- Add to the [Package Documentation Quick Reference](#package-documentation-quick-reference) table
|
||||
|
||||
#### **5. Register in Bot**
|
||||
- Add import to `_load_command_packages()` in `bot.py`
|
||||
- Add to `command_packages` list
|
||||
- Test in development environment
|
||||
- Verify commands sync correctly
|
||||
|
||||
### **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 for all packages:
|
||||
|
||||
```
|
||||
INFO - Loading players commands...
|
||||
INFO - ✅ Loaded PlayerInfoCommands
|
||||
INFO - 🎉 All 1 player command modules loaded successfully
|
||||
INFO - ✅ players commands loaded successfully (1 cogs)
|
||||
|
||||
INFO - Loading teams commands...
|
||||
INFO - ✅ Loaded TeamInfoCommands
|
||||
INFO - ✅ Loaded TeamRosterCommands
|
||||
INFO - 🎉 All 2 team command modules loaded successfully
|
||||
INFO - ✅ teams commands loaded successfully (2 cogs)
|
||||
|
||||
[... similar output for all 13 packages ...]
|
||||
|
||||
INFO - 🎉 All command packages loaded successfully (N 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
|
||||
```
|
||||
|
||||
### **Package-Specific Logs**
|
||||
Each package maintains its own logging with package-level context. Check individual package README files for specific logging patterns and monitoring guidance.
|
||||
|
||||
### **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.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Current Status Summary
|
||||
|
||||
**Total Packages Implemented:** 13
|
||||
- ✅ players - Player information commands
|
||||
- ✅ teams - Team information and roster commands
|
||||
- ✅ league - League-wide commands (standings, schedule, etc.)
|
||||
- ✅ transactions - Trade and roster management
|
||||
- ✅ admin - Admin and system commands
|
||||
- ✅ custom_commands - Custom command system
|
||||
- ✅ help - Help documentation system
|
||||
- ✅ profile - User profile management
|
||||
- ✅ injuries - Injury tracking and management
|
||||
- ✅ dice - Dice rolling utilities
|
||||
- ✅ voice - Voice channel management
|
||||
- ✅ utilities - Chart and utility commands
|
||||
- ✅ soak - SOAK feature commands
|
||||
|
||||
**Architecture Maturity:** Production-ready with comprehensive error handling, logging, and monitoring
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** October 2025 - Documentation updated to reflect all implemented packages
|
||||
**Next Review:** When new major command packages are added
|
||||
433
commands/help/CLAUDE.md
Normal file
433
commands/help/CLAUDE.md
Normal file
@ -0,0 +1,433 @@
|
||||
# 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`
|
||||
519
commands/injuries/CLAUDE.md
Normal file
519
commands/injuries/CLAUDE.md
Normal file
@ -0,0 +1,519 @@
|
||||
# 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, autocomplete): Name of the player whose injury to clear - uses smart autocomplete prioritizing your team's players
|
||||
|
||||
**Validation:**
|
||||
- Player must exist in current season
|
||||
- Player must have an active injury
|
||||
|
||||
**User Flow:**
|
||||
1. Command issued with player name
|
||||
2. **Confirmation embed displayed** showing:
|
||||
- Player name and position
|
||||
- Team name and abbreviation
|
||||
- Expected return date
|
||||
- Total games missed
|
||||
- Team thumbnail (if available)
|
||||
3. User prompted: "Is {Player Name} cleared to return?"
|
||||
4. Two buttons presented:
|
||||
- **"Clear Injury"** → Proceeds with clearing the injury
|
||||
- **"Cancel"** → Cancels the operation
|
||||
5. After confirmation, injury is cleared and success message displayed
|
||||
|
||||
**Example:**
|
||||
```
|
||||
/injury clear Mike Trout
|
||||
```
|
||||
|
||||
**Confirmation Embed:**
|
||||
- Title: Player name
|
||||
- Description: "Is **{Player Name}** cleared to return?"
|
||||
- Fields: Player info, team, expected return date, games missed
|
||||
- Buttons: "Clear Injury" / "Cancel"
|
||||
- Timeout: 3 minutes
|
||||
|
||||
**Success Response (after confirmation):**
|
||||
- Confirmation that injury was cleared
|
||||
- Shows previous return date
|
||||
- Shows total games that were missed
|
||||
- Player's team information
|
||||
|
||||
**Responders:**
|
||||
- Command issuer
|
||||
- Team GM(s) - can also confirm/cancel on behalf of team
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
151
commands/league/CLAUDE.md
Normal file
151
commands/league/CLAUDE.md
Normal file
@ -0,0 +1,151 @@
|
||||
# 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`
|
||||
173
commands/players/CLAUDE.md
Normal file
173
commands/players/CLAUDE.md
Normal file
@ -0,0 +1,173 @@
|
||||
# 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, sWAR, injury status
|
||||
- **Injury Indicator**: 🤕 emoji in title if player has active injury
|
||||
- **Injury Information**:
|
||||
- Injury Rating (always displayed)
|
||||
- Injury Return date (displayed only when injured, format: w##g#)
|
||||
- **Statistics Integration**:
|
||||
- **Batting stats** displayed in two inline fields with rounded box code blocks:
|
||||
- **Rate Stats**: AVG, OBP, SLG, OPS, wOBA
|
||||
- **Counting Stats**: HR, RBI, R, AB, H, BB, SO
|
||||
- **Pitching stats** displayed in two inline fields with rounded box code blocks:
|
||||
- **Record Stats**: G-GS, W-L, H-SV, ERA, WHIP, IP
|
||||
- **Counting Stats**: SO, BB, H
|
||||
- Two-way player detection and display
|
||||
- **Visual Elements**:
|
||||
- Player name as embed title (with 🤕 emoji if injured)
|
||||
- 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`
|
||||
|
||||
## Stat Display Format (January 2025)
|
||||
|
||||
### Batting Stats
|
||||
Stats are displayed in two side-by-side inline fields using rounded box code blocks:
|
||||
|
||||
**Rate Stats (Left):**
|
||||
```
|
||||
╭─────────────╮
|
||||
│ AVG .305 │
|
||||
│ OBP .385 │
|
||||
│ SLG .545 │
|
||||
│ OPS .830 │
|
||||
│ wOBA .355 │
|
||||
╰─────────────╯
|
||||
```
|
||||
|
||||
**Counting Stats (Right):**
|
||||
```
|
||||
╭───────────╮
|
||||
│ HR 25 │
|
||||
│ RBI 80 │
|
||||
│ R 95 │
|
||||
│ AB 450 │
|
||||
│ H 137 │
|
||||
│ BB 55 │
|
||||
│ SO 98 │
|
||||
╰───────────╯
|
||||
```
|
||||
|
||||
### Pitching Stats
|
||||
Stats are displayed in two side-by-side inline fields using rounded box code blocks:
|
||||
|
||||
**Record Stats (Left):**
|
||||
```
|
||||
╭─────────────╮
|
||||
│ G-GS 28-28 │
|
||||
│ W-L 12-8 │
|
||||
│ H-SV 3-0 │
|
||||
│ ERA 3.45 │
|
||||
│ WHIP 1.25 │
|
||||
│ IP 165.1 │
|
||||
╰─────────────╯
|
||||
```
|
||||
|
||||
**Counting Stats (Right):**
|
||||
```
|
||||
╭────────╮
|
||||
│ SO 185 │
|
||||
│ BB 48 │
|
||||
│ H 145 │
|
||||
╰────────╯
|
||||
```
|
||||
|
||||
**Design Features:**
|
||||
- Compact, professional appearance using rounded box characters (`╭╮╰╯─│`)
|
||||
- Right-aligned numeric values for clean alignment
|
||||
- Inline fields allow side-by-side display
|
||||
- Empty field separator above stats for visual spacing
|
||||
- Consistent styling between batting and pitching displays
|
||||
|
||||
## Database Requirements
|
||||
- Player records with name, positions, team associations
|
||||
- Player injury data (injury_rating, il_return fields)
|
||||
- Statistics tables for batting and pitching
|
||||
- Image URLs for player cards, headshots, and fancy cards
|
||||
- Team logo and color information
|
||||
424
commands/profile/CLAUDE.md
Normal file
424
commands/profile/CLAUDE.md
Normal file
@ -0,0 +1,424 @@
|
||||
# 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`
|
||||
134
commands/teams/CLAUDE.md
Normal file
134
commands/teams/CLAUDE.md
Normal file
@ -0,0 +1,134 @@
|
||||
# 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
|
||||
328
commands/transactions/CLAUDE.md
Normal file
328
commands/transactions/CLAUDE.md
Normal file
@ -0,0 +1,328 @@
|
||||
# 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 (scheduled for next week)
|
||||
- `/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()`
|
||||
|
||||
### `ilmove.py` *(NEW - October 2025)*
|
||||
- **Commands**:
|
||||
- `/ilmove` - Interactive transaction builder for **real-time** roster moves (executed immediately for THIS week)
|
||||
- `/clearilmove` - Clear current IL move transaction builder
|
||||
- **Service Dependencies**:
|
||||
- `transaction_builder` (transaction creation and validation - **shared with /dropadd**)
|
||||
- `transaction_service.create_transaction_batch()` (POST transactions to database)
|
||||
- `player_service.search_players()` (player autocomplete)
|
||||
- `player_service.update_player_team()` (immediate team assignment updates)
|
||||
- `team_service.get_teams_by_owner()`
|
||||
- **Key Differences from /dropadd**:
|
||||
- **Week**: Creates transactions for THIS week (current) instead of next week
|
||||
- **Execution**: Immediately POSTs to database and updates player teams
|
||||
- **Use Case**: Real-time IL moves, activations, emergency roster changes
|
||||
- **UI**: Same interactive transaction builder with "immediate" submission handler
|
||||
|
||||
### `trade.py`
|
||||
- **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
|
||||
|
||||
### Real-Time IL Move System (`/ilmove`) *(NEW - October 2025)*
|
||||
- **Immediate Execution**: Transactions posted to database and players moved in real-time
|
||||
- **Current Week Transactions**: All moves effective for THIS week, not next week
|
||||
- **Shared Transaction Builder**: Uses same builder as `/dropadd` for consistency
|
||||
- **Interactive UI**: Identical transaction builder interface with validation
|
||||
- **Maximum Code Reuse**: 95% code shared with `/dropadd`, only submission differs
|
||||
|
||||
#### IL Move Command Workflow:
|
||||
1. **`/ilmove player:"Mike Trout" destination:"Injured List"`** - Add first move
|
||||
- Shows transaction builder with current move
|
||||
- Validates roster legality in real-time
|
||||
- Instructions say "Use `/ilmove` to add more moves"
|
||||
2. **`/ilmove player:"Shohei Ohtani" destination:"Major League"`** - Add second move
|
||||
- Adds to same transaction builder (shared with user)
|
||||
- Shows updated roster projections with both moves
|
||||
- Validates combined impact on roster limits
|
||||
3. **Submit via "Submit Transaction" button** - Click to execute
|
||||
- Shows CONFIRM modal (type "CONFIRM")
|
||||
- **Immediately POSTs** all transactions to database API
|
||||
- **Immediately updates** each player's team_id in database
|
||||
- Success message shows database transaction IDs
|
||||
4. **Players show on new teams instantly** - Real-time roster changes
|
||||
|
||||
#### IL Move vs Drop/Add Comparison:
|
||||
| Feature | `/dropadd` | `/ilmove` |
|
||||
|---------|------------|-----------|
|
||||
| **Effective Week** | Next week (current + 1) | THIS week (current) |
|
||||
| **Execution** | Scheduled (background task) | **Immediate** (real-time) |
|
||||
| **Database POST** | No (builder only) | **Yes** (POSTs to API) |
|
||||
| **Player Teams** | Unchanged until processing | **Updated immediately** |
|
||||
| **Use Case** | Future planning | IL moves, activations |
|
||||
| **Destination Options** | ML, MiL, FA | ML, MiL, **IL** |
|
||||
| **Builder Shared** | Yes (same TransactionBuilder) | Yes (same TransactionBuilder) |
|
||||
|
||||
#### Real-Time Execution Flow:
|
||||
1. **User submits transaction** → Modal confirmation
|
||||
2. **Create Transaction objects** → For current week
|
||||
3. **POST to database API** → `transaction_service.create_transaction_batch()`
|
||||
4. **Update player teams** → `player_service.update_player_team()` for each player
|
||||
5. **Success confirmation** → Shows actual database IDs and completed status
|
||||
|
||||
**Important Notes**:
|
||||
- `/ilmove` only works for players **on your roster** (not FA signings)
|
||||
- Players must exist on ML, MiL, or IL rosters
|
||||
- Cannot use `/ilmove` to sign free agents (use `/dropadd` for that)
|
||||
- All roster legality rules still apply (limits, sWAR, etc.)
|
||||
|
||||
### Multi-Team Trade System (`/trade`)
|
||||
- **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()`
|
||||
- `create_transaction_batch()` *(NEW - for /ilmove)* - POST transactions to database
|
||||
- `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`:
|
||||
- `TradeBuilder` class for multi-team transaction management
|
||||
- `get_trade_builder()` and `clear_trade_builder()` cache functions
|
||||
- `TradeValidationResult` for comprehensive trade validation
|
||||
- `services.transaction_builder`:
|
||||
- `TransactionBuilder` class for single-team roster moves (shared by /dropadd and /ilmove)
|
||||
- `get_transaction_builder()` and `clear_transaction_builder()` cache functions
|
||||
- `RosterValidationResult` for roster legality validation
|
||||
- `services.player_service`:
|
||||
- `search_players()` for autocomplete functionality
|
||||
- `update_player_team()` *(NEW - for /ilmove)* - Update player team assignments
|
||||
|
||||
### 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
|
||||
|
||||
### October 2025
|
||||
- ✅ **Real-Time IL Move System (`/ilmove`)**: Immediate transaction execution for current week roster changes
|
||||
- 95% code reuse with `/dropadd` via shared `TransactionBuilder`
|
||||
- Immediate database POST and player team updates
|
||||
- Same interactive UI with "immediate" submission handler
|
||||
- Supports multiple moves in single transaction
|
||||
|
||||
### Previous Enhancements
|
||||
- ✅ **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
|
||||
235
commands/utilities/CLAUDE.md
Normal file
235
commands/utilities/CLAUDE.md
Normal file
@ -0,0 +1,235 @@
|
||||
# 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
|
||||
251
commands/voice/CLAUDE.md
Normal file
251
commands/voice/CLAUDE.md
Normal file
@ -0,0 +1,251 @@
|
||||
# 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
|
||||
691
models/CLAUDE.md
Normal file
691
models/CLAUDE.md
Normal file
@ -0,0 +1,691 @@
|
||||
# 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
|
||||
|
||||
### Custom Command Model (October 2025)
|
||||
|
||||
**Optional Creator ID Field**
|
||||
|
||||
The `CustomCommand` model now has an optional `creator_id` field to support different API endpoints:
|
||||
|
||||
```python
|
||||
class CustomCommand(SBABaseModel):
|
||||
id: int = Field(..., description="Database ID")
|
||||
name: str = Field(..., description="Command name (unique)")
|
||||
content: str = Field(..., description="Command response content")
|
||||
creator_id: Optional[int] = Field(None, description="ID of the creator (may be missing from execute endpoint)")
|
||||
creator: Optional[CustomCommandCreator] = Field(None, description="Creator details")
|
||||
```
|
||||
|
||||
**Why Optional:**
|
||||
- The `/custom_commands/by_name/{name}/execute` API endpoint returns command data **without** `creator_id`
|
||||
- This endpoint only needs to return the command content for execution
|
||||
- Other endpoints (get, create, update, delete) include full creator information
|
||||
|
||||
**Usage Pattern:**
|
||||
```python
|
||||
# Execute endpoint - creator_id may be None
|
||||
command, content = await custom_commands_service.execute_command("BPFO")
|
||||
# command.creator_id might be None, but command.content is always present
|
||||
|
||||
# Get endpoint - creator_id is always present
|
||||
command = await custom_commands_service.get_command_by_name("BPFO")
|
||||
# command.creator_id is populated, command.creator has full details
|
||||
```
|
||||
|
||||
**Important Note:**
|
||||
This change maintains backward compatibility while fixing validation errors when executing custom commands. Permission checks (edit/delete) use endpoints that return complete creator information.
|
||||
|
||||
#### 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
|
||||
607
services/CLAUDE.md
Normal file
607
services/CLAUDE.md
Normal file
@ -0,0 +1,607 @@
|
||||
# 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 Layer Abstraction - CRITICAL BEST PRACTICE
|
||||
|
||||
**NEVER bypass the service layer by directly accessing the API client.** This is a critical architectural principle that must be followed in all code.
|
||||
|
||||
### ❌ Anti-Pattern: Direct Client Access
|
||||
|
||||
```python
|
||||
# BAD: Bypassing service layer
|
||||
async def my_task():
|
||||
client = await some_service.get_client()
|
||||
await client.patch(f'endpoint/{id}', data={'field': 'value'}) # ❌ WRONG
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
1. **Breaks Abstraction** - Services exist to abstract API details
|
||||
2. **Harder to Test** - Can't easily mock individual operations
|
||||
3. **Duplicated Logic** - Same API calls repeated in multiple places
|
||||
4. **Maintenance Nightmare** - API changes require updates everywhere
|
||||
5. **Missing Validation** - Services provide business logic validation
|
||||
6. **No Caching** - Bypass caching decorators on service methods
|
||||
|
||||
### ✅ Correct Pattern: Use Service Methods
|
||||
|
||||
```python
|
||||
# GOOD: Using service layer
|
||||
async def my_task():
|
||||
updated = await some_service.update_item(id, {'field': 'value'}) # ✅ CORRECT
|
||||
```
|
||||
|
||||
### When Service Methods Don't Exist
|
||||
|
||||
**If a service method doesn't exist for your use case:**
|
||||
|
||||
1. **Add the method to the service** (preferred approach)
|
||||
2. **Document it properly** with docstrings
|
||||
3. **Use existing BaseService methods** when possible
|
||||
|
||||
#### Example: Adding Missing Service Method
|
||||
|
||||
```python
|
||||
# In league_service.py
|
||||
class LeagueService(BaseService[Current]):
|
||||
async def update_current_state(
|
||||
self,
|
||||
week: Optional[int] = None,
|
||||
freeze: Optional[bool] = None
|
||||
) -> Optional[Current]:
|
||||
"""
|
||||
Update current league state (week and/or freeze status).
|
||||
|
||||
Args:
|
||||
week: New week number (None to leave unchanged)
|
||||
freeze: New freeze status (None to leave unchanged)
|
||||
|
||||
Returns:
|
||||
Updated Current object or None if update failed
|
||||
"""
|
||||
update_data = {}
|
||||
if week is not None:
|
||||
update_data['week'] = week
|
||||
if freeze is not None:
|
||||
update_data['freeze'] = freeze
|
||||
|
||||
# Use BaseService patch method
|
||||
return await self.patch(current_id=1, model_data=update_data)
|
||||
```
|
||||
|
||||
Then use it in tasks/commands:
|
||||
|
||||
```python
|
||||
# In task code
|
||||
updated_current = await league_service.update_current_state(
|
||||
week=new_week,
|
||||
freeze=True
|
||||
)
|
||||
```
|
||||
|
||||
### Real-World Example: Transaction Freeze Task
|
||||
|
||||
**❌ BEFORE (Bad - Direct Client Access):**
|
||||
```python
|
||||
# Anti-pattern: bypassing services
|
||||
client = await league_service.get_client()
|
||||
await client.patch(f'current/{current_id}', data={'week': new_week, 'freeze': True})
|
||||
|
||||
client = await transaction_service.get_client()
|
||||
response = await client.get('transactions', params=[...])
|
||||
moves_data = response.get('transactions', [])
|
||||
transactions = [Transaction.from_api_data(move) for move in moves_data]
|
||||
|
||||
await client.patch(f'transactions/{move.id}', data={'frozen': False})
|
||||
```
|
||||
|
||||
**✅ AFTER (Good - Using Service Methods):**
|
||||
```python
|
||||
# Proper pattern: using service layer
|
||||
updated_current = await league_service.update_current_state(
|
||||
week=new_week,
|
||||
freeze=True
|
||||
)
|
||||
|
||||
transactions = await transaction_service.get_frozen_transactions_by_week(
|
||||
season=current.season,
|
||||
week_start=current.week,
|
||||
week_end=current.week + 1
|
||||
)
|
||||
|
||||
await transaction_service.unfreeze_transaction(move.id)
|
||||
```
|
||||
|
||||
### Benefits of Service Layer Approach
|
||||
|
||||
1. **Testability** - Mock `league_service.update_current_state()` easily
|
||||
2. **Consistency** - All API calls go through services
|
||||
3. **Maintainability** - API changes only need service updates
|
||||
4. **Validation** - Services add business logic validation
|
||||
5. **Reusability** - Other code can use the same service methods
|
||||
6. **Abstraction** - Tasks don't need to know about API structure
|
||||
7. **Caching** - Service methods can be cached with decorators
|
||||
8. **Error Handling** - Consistent exception handling
|
||||
|
||||
### Code Review Checklist
|
||||
|
||||
When reviewing code, **reject any PR that:**
|
||||
- ✘ Calls `await service.get_client()` outside of service layer
|
||||
- ✘ Makes direct API calls in commands, tasks, or views
|
||||
- ✘ Parses API responses outside of services
|
||||
- ✘ Uses `client.get()`, `client.post()`, `client.patch()` outside services
|
||||
|
||||
**Accept only code that:**
|
||||
- ✓ Uses service methods for ALL API interactions
|
||||
- ✓ Adds new service methods when needed
|
||||
- ✓ Properly documents new service methods
|
||||
- ✓ Uses BaseService inherited methods when appropriate
|
||||
|
||||
## 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
|
||||
|
||||
#### TransactionService Key Methods (October 2025 Update)
|
||||
```python
|
||||
class TransactionService(BaseService[Transaction]):
|
||||
# Transaction retrieval methods
|
||||
async def get_team_transactions(...) -> List[Transaction]
|
||||
async def get_pending_transactions(...) -> List[Transaction]
|
||||
async def get_frozen_transactions(...) -> List[Transaction]
|
||||
async def get_processed_transactions(...) -> List[Transaction]
|
||||
|
||||
# NEW: Real-time transaction creation (for /ilmove)
|
||||
async def create_transaction_batch(transactions: List[Transaction]) -> List[Transaction]:
|
||||
"""
|
||||
Create multiple transactions via API POST (for immediate execution).
|
||||
|
||||
This is used for real-time transactions (like IL moves) that need to be
|
||||
posted to the database immediately rather than scheduled for later processing.
|
||||
|
||||
Args:
|
||||
transactions: List of Transaction objects to create
|
||||
|
||||
Returns:
|
||||
List of created Transaction objects with API-assigned IDs
|
||||
|
||||
Raises:
|
||||
APIException: If transaction creation fails
|
||||
"""
|
||||
```
|
||||
|
||||
**Usage Example:**
|
||||
```python
|
||||
# Create transactions for immediate execution
|
||||
created_transactions = await transaction_service.create_transaction_batch(transactions)
|
||||
|
||||
# Each transaction now has a database-assigned ID
|
||||
for txn in created_transactions:
|
||||
print(f"Created transaction {txn.id}: {txn.move_description}")
|
||||
```
|
||||
|
||||
#### PlayerService Key Methods (October 2025 Update)
|
||||
```python
|
||||
class PlayerService(BaseService[Player]):
|
||||
# Player search and retrieval methods
|
||||
async def get_player(...) -> Optional[Player]
|
||||
async def search_players(...) -> List[Player]
|
||||
async def get_players_by_team(...) -> List[Player]
|
||||
|
||||
# NEW: Team assignment updates (for /ilmove)
|
||||
async def update_player_team(player_id: int, new_team_id: int) -> Optional[Player]:
|
||||
"""
|
||||
Update a player's team assignment (for real-time IL moves).
|
||||
|
||||
This is used for immediate roster changes where the player needs to show
|
||||
up on their new team right away, rather than waiting for transaction processing.
|
||||
|
||||
Args:
|
||||
player_id: Player ID to update
|
||||
new_team_id: New team ID to assign
|
||||
|
||||
Returns:
|
||||
Updated player instance or None
|
||||
|
||||
Raises:
|
||||
APIException: If player update fails
|
||||
"""
|
||||
```
|
||||
|
||||
**Usage Example:**
|
||||
```python
|
||||
# Update player team assignment immediately
|
||||
updated_player = await player_service.update_player_team(
|
||||
player_id=player.id,
|
||||
new_team_id=new_team.id
|
||||
)
|
||||
|
||||
# Player now shows on new team in all queries
|
||||
print(f"{updated_player.name} now on team {updated_player.team_id}")
|
||||
```
|
||||
|
||||
### 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
|
||||
498
tasks/CLAUDE.md
Normal file
498
tasks/CLAUDE.md
Normal file
@ -0,0 +1,498 @@
|
||||
# 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
|
||||
- **Service layer integration** - ALWAYS use service methods, never direct API client access
|
||||
|
||||
### 🚨 CRITICAL: Service Layer Usage in Tasks
|
||||
|
||||
**Tasks MUST use the service layer for ALL API interactions.** Never bypass services by directly accessing the API client.
|
||||
|
||||
#### ❌ Anti-Pattern: Direct Client Access in Tasks
|
||||
|
||||
```python
|
||||
# BAD: Don't do this in tasks
|
||||
async def my_background_task(self):
|
||||
client = await some_service.get_client() # ❌ WRONG
|
||||
response = await client.get('endpoint', params=[...])
|
||||
await client.patch(f'endpoint/{id}', data={'field': 'value'})
|
||||
```
|
||||
|
||||
**Why this is bad:**
|
||||
1. Breaks service layer abstraction
|
||||
2. Makes testing harder (can't mock service methods)
|
||||
3. Duplicates API logic across codebase
|
||||
4. Misses service-level validation and caching
|
||||
5. Creates maintenance nightmares when API changes
|
||||
|
||||
#### ✅ Correct Pattern: Use Service Methods
|
||||
|
||||
```python
|
||||
# GOOD: Always use service methods
|
||||
async def my_background_task(self):
|
||||
items = await some_service.get_items_by_criteria(...) # ✅ CORRECT
|
||||
updated = await some_service.update_item(id, data) # ✅ CORRECT
|
||||
```
|
||||
|
||||
#### When Service Methods Don't Exist
|
||||
|
||||
If you need functionality that doesn't exist in a service:
|
||||
|
||||
1. **Add the method to the appropriate service** (preferred)
|
||||
2. **Use existing BaseService methods** when possible
|
||||
3. **Document the new method** with clear docstrings
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
# In services/league_service.py
|
||||
async def update_current_state(
|
||||
self,
|
||||
week: Optional[int] = None,
|
||||
freeze: Optional[bool] = None
|
||||
) -> Optional[Current]:
|
||||
"""Update current league state (week and/or freeze status)."""
|
||||
update_data = {}
|
||||
if week is not None:
|
||||
update_data['week'] = week
|
||||
if freeze is not None:
|
||||
update_data['freeze'] = freeze
|
||||
return await self.patch(current_id=1, model_data=update_data)
|
||||
|
||||
# In tasks/transaction_freeze.py
|
||||
async def _begin_freeze(self, current: Current):
|
||||
updated_current = await league_service.update_current_state(
|
||||
week=new_week,
|
||||
freeze=True
|
||||
) # ✅ Using service method
|
||||
```
|
||||
|
||||
See `services/CLAUDE.md` for complete service layer best practices.
|
||||
|
||||
### 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
|
||||
|
||||
### Transaction Freeze/Thaw (`transaction_freeze.py`)
|
||||
**Purpose:** Automated weekly system for freezing transactions and processing contested player acquisitions
|
||||
|
||||
**Schedule:** Every minute (checks for specific times to trigger actions)
|
||||
|
||||
**Operations:**
|
||||
- **Freeze Begin (Monday 00:00):**
|
||||
- Increments league week
|
||||
- Sets freeze flag to True
|
||||
- Runs regular transactions for the new week
|
||||
- Announces freeze period in #transaction-log
|
||||
- Posts weekly schedule info to #weekly-info (weeks 1-18 only)
|
||||
|
||||
- **Freeze End (Saturday 00:00):**
|
||||
- Processes frozen transactions with priority resolution
|
||||
- Resolves contested players (multiple teams want same player)
|
||||
- Uses team standings to determine priority (worst teams get first priority)
|
||||
- Cancels losing transactions and notifies GMs via DM
|
||||
- Unfreezes winning transactions
|
||||
- Posts successful transactions to #transaction-log
|
||||
- Announces thaw period
|
||||
|
||||
#### Key Features
|
||||
- **Priority Resolution:** Uses team win percentage with random tiebreaker
|
||||
- **Offseason Mode:** Respects `offseason_flag` config to skip operations
|
||||
- **Contested Transaction Handling:** Fair resolution system for player conflicts
|
||||
- **GM Notifications:** Direct messages to managers about cancelled moves
|
||||
- **Comprehensive Logging:** Detailed logs for all freeze/thaw operations
|
||||
- **Error Recovery:** Owner notifications on failures
|
||||
|
||||
#### Configuration
|
||||
The freeze task respects configuration settings:
|
||||
|
||||
```python
|
||||
# config.py settings
|
||||
offseason_flag: bool = False # When True, disables freeze/thaw operations
|
||||
guild_id: int # Target guild for operations
|
||||
```
|
||||
|
||||
**Environment Variables:**
|
||||
- `OFFSEASON_FLAG=true` - Enables offseason mode (skips freeze/thaw)
|
||||
- `GUILD_ID` - Discord server ID
|
||||
|
||||
#### Transaction Priority Logic
|
||||
When multiple teams try to acquire the same player:
|
||||
1. Calculate team win percentage from standings
|
||||
2. Add small random component (5 decimal precision) for tiebreaking
|
||||
3. Sort by priority (lowest win% = highest priority)
|
||||
4. Team with lowest priority wins the player
|
||||
5. All other teams have their transactions cancelled
|
||||
|
||||
**Tiebreaker Formula:**
|
||||
```python
|
||||
tiebreaker = team_win_percentage + random(0.00010000 to 0.00099999)
|
||||
```
|
||||
|
||||
This ensures worst-record teams get priority while maintaining fairness through randomization.
|
||||
|
||||
#### Channel Requirements
|
||||
- **#transaction-log** - Announcements and transaction posts
|
||||
- **#weekly-info** - Weekly schedule information (cleared and updated)
|
||||
|
||||
#### Error Handling
|
||||
- Comprehensive try/catch blocks with structured logging
|
||||
- Owner DM notifications on failures
|
||||
- Prevents duplicate error messages with warning flag
|
||||
- Graceful degradation if channels not found
|
||||
- Continues operation despite individual transaction failures
|
||||
|
||||
### 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/CLAUDE.md
Normal file
293
tests/CLAUDE.md
Normal file
@ -0,0 +1,293 @@
|
||||
# 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/CLAUDE.md
Normal file
941
utils/CLAUDE.md
Normal file
@ -0,0 +1,941 @@
|
||||
# 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`.
|
||||
709
views/CLAUDE.md
Normal file
709
views/CLAUDE.md
Normal file
@ -0,0 +1,709 @@
|
||||
# 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
|
||||
|
||||
#### Player Information (`players.py`)
|
||||
Interactive views for player information display with toggleable statistics:
|
||||
|
||||
**PlayerStatsView** - Toggle batting and pitching statistics independently
|
||||
|
||||
```python
|
||||
from views.players import PlayerStatsView
|
||||
|
||||
# Create interactive player stats view
|
||||
view = PlayerStatsView(
|
||||
player=player_with_team,
|
||||
season=search_season,
|
||||
batting_stats=batting_stats,
|
||||
pitching_stats=pitching_stats,
|
||||
user_id=interaction.user.id
|
||||
)
|
||||
|
||||
# Get initial embed with stats hidden
|
||||
embed = await view.get_initial_embed()
|
||||
|
||||
# Send with interactive view
|
||||
await interaction.followup.send(embed=embed, view=view)
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- **Basic Info Always Visible**: Name, position, team, sWAR, injury status displayed by default
|
||||
- **Stats Hidden by Default**: Batting and pitching stats are hidden until user clicks toggle buttons
|
||||
- **Independent Toggles**: Users can show/hide batting and pitching stats separately
|
||||
- **Conditional Buttons**: Buttons only appear if corresponding stats are available
|
||||
- **User Restriction**: Only the user who ran the command can toggle stats
|
||||
- **Timeout Handling**: View times out after 5 minutes with graceful cleanup
|
||||
- **Professional UI**: Uses baseball emojis (💥 batting impact, ⚾ pitching) and primary button style
|
||||
- **Dynamic Updates**: Embed updates in-place when buttons are clicked
|
||||
|
||||
**Button Behavior:**
|
||||
- **Initial State**: "Show Batting Stats" and "Show Pitching Stats"
|
||||
- **Toggled State**: "Hide Batting Stats" and "Hide Pitching Stats"
|
||||
- **Visual Feedback**: Button labels change to reflect current state
|
||||
- **Clean Interface**: Only relevant buttons are shown based on available data
|
||||
|
||||
**Implementation Notes:**
|
||||
- Inherits from `BaseView` for consistent error handling and logging
|
||||
- Stats formatting matches existing player card design with rounded box code blocks
|
||||
- Preserves all player card features (images, thumbnails, team colors)
|
||||
- Comprehensive logging for debugging and monitoring
|
||||
|
||||
#### Transaction Management (`transaction_embed.py`) (Updated October 2025)
|
||||
Views for player transaction interfaces with dual-mode submission support:
|
||||
- **Transaction builder** with interactive controls
|
||||
- **Comprehensive validation** and sWAR display
|
||||
- **Pre-existing transaction** context
|
||||
- **Dual submission modes**: Scheduled (/dropadd) and Immediate (/ilmove)
|
||||
- **Dynamic UI instructions**: Context-aware command references
|
||||
|
||||
**Key Classes:**
|
||||
```python
|
||||
class TransactionEmbedView(discord.ui.View):
|
||||
def __init__(
|
||||
self,
|
||||
builder: TransactionBuilder,
|
||||
user_id: int,
|
||||
submission_handler: str = "scheduled", # "scheduled" or "immediate"
|
||||
command_name: str = "/dropadd" # Command for UI instructions
|
||||
):
|
||||
"""
|
||||
Interactive transaction builder view supporting both scheduled and immediate execution.
|
||||
|
||||
Args:
|
||||
builder: TransactionBuilder instance
|
||||
user_id: Discord user ID (for permission checking)
|
||||
submission_handler: "scheduled" for /dropadd, "immediate" for /ilmove
|
||||
command_name: Command name shown in "Add More Moves" instructions
|
||||
"""
|
||||
|
||||
async def create_transaction_embed(
|
||||
builder: TransactionBuilder,
|
||||
command_name: str = "/dropadd"
|
||||
) -> discord.Embed:
|
||||
"""
|
||||
Create transaction builder embed with context-aware instructions.
|
||||
|
||||
Args:
|
||||
builder: TransactionBuilder instance
|
||||
command_name: Command name for "Add More Moves" instruction
|
||||
|
||||
Returns:
|
||||
Discord embed showing transaction state with appropriate instructions
|
||||
"""
|
||||
```
|
||||
|
||||
**Submission Handler Behavior:**
|
||||
- **"scheduled" mode** (/dropadd):
|
||||
- Creates transactions for NEXT week
|
||||
- No database POST - stays in memory
|
||||
- Background task processes later
|
||||
- Instructions say "Use `/dropadd` to add more moves"
|
||||
|
||||
- **"immediate" mode** (/ilmove):
|
||||
- Creates transactions for THIS week
|
||||
- Immediately POSTs to database API
|
||||
- Immediately updates player teams
|
||||
- Instructions say "Use `/ilmove` to add more moves"
|
||||
|
||||
**Usage Examples:**
|
||||
```python
|
||||
# For /dropadd (scheduled submission)
|
||||
embed = await create_transaction_embed(builder, command_name="/dropadd")
|
||||
view = TransactionEmbedView(
|
||||
builder,
|
||||
user_id,
|
||||
submission_handler="scheduled",
|
||||
command_name="/dropadd"
|
||||
)
|
||||
|
||||
# For /ilmove (immediate submission)
|
||||
embed = await create_transaction_embed(builder, command_name="/ilmove")
|
||||
view = TransactionEmbedView(
|
||||
builder,
|
||||
user_id,
|
||||
submission_handler="immediate",
|
||||
command_name="/ilmove"
|
||||
)
|
||||
```
|
||||
|
||||
**Implementation Notes:**
|
||||
- **95% code reuse** between /dropadd and /ilmove
|
||||
- **Same TransactionBuilder** instance shared between both commands
|
||||
- **Dynamic embed description**: Changes based on command_name
|
||||
- **Context propagation**: command_name passed through all UI components
|
||||
- **Backwards compatible**: Default parameters maintain /dropadd behavior
|
||||
|
||||
## 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