major-domo-v2/services/injury_service.py
Cal Corum 0c6f7c8ffe CLAUDE: Fix GroupCog interaction bug and GIF display issues
This commit addresses critical bugs in the injury command system and
establishes best practices for Discord command groups.

## Critical Fixes

### 1. GroupCog → app_commands.Group Migration
- **Problem**: `commands.GroupCog` has a duplicate interaction processing bug
  causing "404 Unknown interaction" errors when deferring responses
- **Root Cause**: GroupCog triggers command handler twice, consuming the
  interaction token before the second execution can respond
- **Solution**: Migrated InjuryCog to InjuryGroup using `app_commands.Group`
  pattern (same as ChartManageGroup and ChartCategoryGroup)
- **Result**: Reliable command execution, no more 404 errors

### 2. GiphyService GIF URL Fix
- **Problem**: Giphy service returned web page URLs (https://giphy.com/gifs/...)
  instead of direct image URLs, preventing Discord embed display
- **Root Cause**: Code accessed `data.url` instead of `data.images.original.url`
- **Solution**: Updated both `get_disappointment_gif()` and `get_gif()` methods
  to use correct API response path for embeddable GIF URLs
- **Result**: GIFs now display correctly in Discord embeds

## Documentation

### Command Groups Best Practices (commands/README.md)
Added comprehensive section documenting:
- **Critical Warning**: Never use `commands.GroupCog` - use `app_commands.Group`
- **Technical Explanation**: Why GroupCog fails (duplicate execution bug)
- **Migration Guide**: Step-by-step conversion from GroupCog to Group
- **Comparison Table**: Key differences between the two approaches
- **Working Examples**: References to ChartManageGroup, InjuryGroup patterns

## Architecture Changes

### Injury Commands (`commands/injuries/`)
- Converted from `commands.GroupCog` to `app_commands.Group`
- Registration via `bot.tree.add_command()` instead of `bot.add_cog()`
- Removed workarounds for GroupCog duplicate interaction issues
- Clean defer/response pattern with `@logged_command` decorator

### GiphyService (`services/giphy_service.py`)
- Centralized from `commands/soak/giphy_service.py`
- Now returns direct GIF image URLs for Discord embeds
- Maintains Trump GIF filtering (legacy behavior)
- Added gif_url to log output for debugging

### Configuration (`config.py`)
- Added `giphy_api_key` and `giphy_translate_url` settings
- Environment variable support via `GIPHY_API_KEY`
- Default values provided for out-of-box functionality

## Files Changed
- commands/injuries/: New InjuryGroup with app_commands.Group pattern
- services/giphy_service.py: Centralized service with GIF URL fix
- commands/soak/giphy_service.py: Backwards compatibility wrapper
- commands/README.md: Command groups best practices documentation
- config.py: Giphy configuration settings
- services/__init__.py: GiphyService exports

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 22:15:42 -05:00

197 lines
5.8 KiB
Python

"""
Injury service for Discord Bot v2.0
Handles injury-related operations including checking, creating, and clearing injuries.
"""
import logging
from typing import Optional, List
from services.base_service import BaseService
from models.injury import Injury
from exceptions import APIException
logger = logging.getLogger(f'{__name__}.InjuryService')
class InjuryService(BaseService[Injury]):
"""
Service for injury-related operations.
Features:
- Get active injuries for a player
- Create new injury records
- Clear active injuries
- Season-specific filtering
"""
def __init__(self):
"""Initialize injury service."""
super().__init__(Injury, 'injuries')
logger.debug("InjuryService initialized")
async def get_active_injury(self, player_id: int, season: int) -> Optional[Injury]:
"""
Get the active injury for a player in a specific season.
Args:
player_id: Player identifier
season: Season number
Returns:
Active Injury instance or None if player has no active injury
"""
try:
params = [
('player_id', str(player_id)),
('season', str(season)),
('is_active', 'true')
]
injuries = await self.get_all_items(params=params)
if injuries:
logger.debug(f"Found active injury for player {player_id} in season {season}")
return injuries[0]
logger.debug(f"No active injury found for player {player_id} in season {season}")
return None
except Exception as e:
logger.error(f"Error getting active injury for player {player_id}: {e}")
return None
async def get_injuries_by_player(self, player_id: int, season: int, active_only: bool = False) -> List[Injury]:
"""
Get all injuries for a player in a specific season.
Args:
player_id: Player identifier
season: Season number
active_only: If True, only return active injuries
Returns:
List of injuries for the player
"""
try:
params = [
('player_id', str(player_id)),
('season', str(season))
]
if active_only:
params.append(('is_active', 'true'))
injuries = await self.get_all_items(params=params)
logger.debug(f"Retrieved {len(injuries)} injuries for player {player_id}")
return injuries
except Exception as e:
logger.error(f"Error getting injuries for player {player_id}: {e}")
return []
async def get_injuries_by_team(self, team_id: int, season: int, active_only: bool = True) -> List[Injury]:
"""
Get all injuries for a team in a specific season.
Args:
team_id: Team identifier
season: Season number
active_only: If True, only return active injuries
Returns:
List of injuries for the team
"""
try:
params = [
('team_id', str(team_id)),
('season', str(season))
]
if active_only:
params.append(('is_active', 'true'))
injuries = await self.get_all_items(params=params)
logger.debug(f"Retrieved {len(injuries)} injuries for team {team_id}")
return injuries
except Exception as e:
logger.error(f"Error getting injuries for team {team_id}: {e}")
return []
async def create_injury(
self,
season: int,
player_id: int,
total_games: int,
start_week: int,
start_game: int,
end_week: int,
end_game: int
) -> Optional[Injury]:
"""
Create a new injury record.
Args:
season: Season number
player_id: Player identifier
total_games: Total games player will be out
start_week: Week injury started
start_game: Game number injury started (1-4)
end_week: Week player returns
end_game: Game number player returns (1-4)
Returns:
Created Injury instance or None on failure
"""
try:
injury_data = {
'season': season,
'player_id': player_id,
'total_games': total_games,
'start_week': start_week,
'start_game': start_game,
'end_week': end_week,
'end_game': end_game,
'is_active': True
}
injury = await self.create(injury_data)
if injury:
logger.info(f"Created injury for player {player_id}: {total_games} games")
return injury
logger.error(f"Failed to create injury for player {player_id}")
return None
except Exception as e:
logger.error(f"Error creating injury for player {player_id}: {e}")
return None
async def clear_injury(self, injury_id: int) -> bool:
"""
Clear (deactivate) an injury.
Args:
injury_id: Injury identifier
Returns:
True if successfully cleared, False otherwise
"""
try:
updated_injury = await self.patch(injury_id, {'is_active': False})
if updated_injury:
logger.info(f"Cleared injury {injury_id}")
return True
logger.error(f"Failed to clear injury {injury_id}")
return False
except Exception as e:
logger.error(f"Error clearing injury {injury_id}: {e}")
return False
# Global service instance
injury_service = InjuryService()