Add injury log posting and fix view interaction permissions
Features: - Post injury announcements to #sba-network-news when injuries are logged - Update #injury-log channel with two embeds: - All injuries grouped by Major League team with return dates - All injuries grouped by return week, sorted ascending - Auto-purge old messages before posting updated injury log Bug Fixes: - Fix BaseView interaction_check logic that incorrectly rejected command users - Old: Rejected if (not user_id match) OR (not in responders) - New: Allow if (user_id match) OR (in responders) - Filter None values from responders list (handles missing gmid2) Changes: - services/injury_service.py: Add get_all_active_injuries_raw() method - utils/injury_log.py: New utility for injury channel posting - views/modals.py: Call injury posting after successful injury logging - views/base.py: Fix interaction authorization logic - config.py: Update to Season 13 Players role 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8ab5e025b2
commit
62541ac750
52
CLAUDE.md
52
CLAUDE.md
@ -477,6 +477,56 @@ REDIS_URL=redis://localhost:6379 # Empty disables caching
|
||||
REDIS_CACHE_TTL=300 # Default TTL in seconds
|
||||
```
|
||||
|
||||
## 🐳 Docker Hub Configuration
|
||||
|
||||
### 🚨 CRITICAL: Correct Repository Name
|
||||
|
||||
**Discord Bot v2.0 Docker Hub Repository:**
|
||||
```
|
||||
manticorum67/major-domo-discordapp
|
||||
```
|
||||
|
||||
**⚠️ IMPORTANT: There is NO DASH between "discord" and "app"**
|
||||
|
||||
### Common Mistakes to Avoid
|
||||
```bash
|
||||
# ❌ WRONG - Has extra dash
|
||||
manticorum67/major-domo-discord-app
|
||||
|
||||
# ❌ WRONG - Uses v2 suffix
|
||||
manticorum67/major-domo-discordapp-v2
|
||||
|
||||
# ❌ WRONG - Different project name pattern
|
||||
manticorum67/major-domo-discord-app-v2
|
||||
|
||||
# ✅ CORRECT - Use this exact name
|
||||
manticorum67/major-domo-discordapp
|
||||
```
|
||||
|
||||
### Build and Push Commands
|
||||
```bash
|
||||
# Build with version tag
|
||||
docker build -t manticorum67/major-domo-discordapp:X.Y.Z .
|
||||
|
||||
# Tag as latest
|
||||
docker tag manticorum67/major-domo-discordapp:X.Y.Z manticorum67/major-domo-discordapp:latest
|
||||
|
||||
# Push both tags
|
||||
docker push manticorum67/major-domo-discordapp:X.Y.Z
|
||||
docker push manticorum67/major-domo-discordapp:latest
|
||||
```
|
||||
|
||||
### Version Tagging Convention
|
||||
- **Version tags**: `manticorum67/major-domo-discordapp:2.24.0`
|
||||
- **Latest tag**: `manticorum67/major-domo-discordapp:latest`
|
||||
- **Development tag**: `manticorum67/major-domo-discordapp:dev`
|
||||
|
||||
### Related Repositories
|
||||
| Component | Docker Hub Repository |
|
||||
|-----------|----------------------|
|
||||
| Discord Bot v2 | `manticorum67/major-domo-discordapp` |
|
||||
| Database API | `manticorum67/major-domo-database` |
|
||||
|
||||
## 📊 Monitoring and Logs
|
||||
|
||||
### Log Files
|
||||
@ -687,7 +737,7 @@ creator_id: Optional[int] = Field(None, description="ID of the creator (may be m
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** October 2025
|
||||
**Last Updated:** December 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
|
||||
|
||||
|
||||
@ -70,7 +70,7 @@ class BotConfig(BaseSettings):
|
||||
|
||||
# Role Names
|
||||
help_editor_role_name: str = "Help Editor"
|
||||
sba_players_role_name: str = "Season 12 Players"
|
||||
sba_players_role_name: str = "Season 13 Players"
|
||||
|
||||
# Channel Names
|
||||
sba_network_news_channel: str = "sba-network-news"
|
||||
|
||||
@ -201,6 +201,40 @@ class InjuryService(BaseService[Injury]):
|
||||
logger.error(f"Error clearing injury {injury_id}: {e}")
|
||||
return False
|
||||
|
||||
async def get_all_active_injuries_raw(self, season: int) -> list[dict]:
|
||||
"""
|
||||
Get all active injuries for a season with raw API response data.
|
||||
|
||||
This method returns the raw API response which includes nested player
|
||||
objects with team information, needed for injury log displays.
|
||||
|
||||
Args:
|
||||
season: Season number
|
||||
|
||||
Returns:
|
||||
List of raw injury dictionaries from API with nested player/team data
|
||||
"""
|
||||
try:
|
||||
client = await self.get_client()
|
||||
params = [
|
||||
('season', str(season)),
|
||||
('is_active', 'true'),
|
||||
('sort', 'return-asc')
|
||||
]
|
||||
|
||||
response = await client.get(self.endpoint, params=params)
|
||||
|
||||
if response and 'injuries' in response:
|
||||
logger.debug(f"Retrieved {len(response['injuries'])} active injuries for season {season}")
|
||||
return response['injuries']
|
||||
|
||||
logger.debug(f"No active injuries found for season {season}")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting all active injuries for season {season}: {e}")
|
||||
return []
|
||||
|
||||
|
||||
# Global service instance
|
||||
injury_service = InjuryService()
|
||||
|
||||
357
utils/injury_log.py
Normal file
357
utils/injury_log.py
Normal file
@ -0,0 +1,357 @@
|
||||
"""
|
||||
Injury Log Posting Utility
|
||||
|
||||
Provides functions for posting injury information to Discord channels:
|
||||
- #injury-log: Two embeds showing current injuries by team and by return week
|
||||
- #sba-network-news: Individual injury announcements
|
||||
"""
|
||||
from typing import Optional, Dict, List, Any
|
||||
from collections import defaultdict
|
||||
|
||||
import discord
|
||||
|
||||
from config import get_config
|
||||
from models.player import Player
|
||||
from models.team import Team
|
||||
from services.injury_service import injury_service
|
||||
from services.team_service import team_service
|
||||
from views.embeds import EmbedTemplate, EmbedColors
|
||||
from utils.logging import get_contextual_logger
|
||||
|
||||
logger = get_contextual_logger(f'{__name__}')
|
||||
|
||||
|
||||
async def get_major_league_team_name(team_data: dict, season: int) -> str:
|
||||
"""
|
||||
Get the Major League team name from player's team data.
|
||||
|
||||
Args:
|
||||
team_data: Team dictionary from API response
|
||||
season: Current season number
|
||||
|
||||
Returns:
|
||||
Major League team short name (sname)
|
||||
"""
|
||||
if not team_data:
|
||||
return "Unknown"
|
||||
|
||||
abbrev = team_data.get('abbrev', '')
|
||||
|
||||
# If abbreviation is 3 chars or less, it's already ML
|
||||
if len(abbrev) <= 3:
|
||||
return team_data.get('sname', abbrev)
|
||||
|
||||
# Extract base abbreviation for MiL/IL teams
|
||||
abbrev_lower = abbrev.lower()
|
||||
if abbrev_lower.endswith('mil'):
|
||||
base_abbrev = abbrev[:-3]
|
||||
elif abbrev_lower.endswith('il'):
|
||||
base_abbrev = abbrev[:-2]
|
||||
else:
|
||||
return team_data.get('sname', abbrev)
|
||||
|
||||
# Look up the ML team
|
||||
try:
|
||||
ml_team = await team_service.get_team_by_abbrev(base_abbrev, season)
|
||||
if ml_team:
|
||||
return ml_team.sname
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get ML team for {abbrev}: {e}")
|
||||
|
||||
return team_data.get('sname', abbrev)
|
||||
|
||||
|
||||
async def update_injury_log_channel(
|
||||
bot: discord.Client,
|
||||
season: int
|
||||
) -> bool:
|
||||
"""
|
||||
Update the #injury-log channel with current injuries.
|
||||
|
||||
Creates two embeds:
|
||||
1. Current injuries grouped by Major League team
|
||||
2. Current injuries grouped by return week
|
||||
|
||||
Args:
|
||||
bot: Discord bot instance
|
||||
season: Current season number
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
config = get_config()
|
||||
guild = bot.get_guild(config.guild_id)
|
||||
|
||||
if not guild:
|
||||
logger.warning(f"Could not find guild {config.guild_id}")
|
||||
return False
|
||||
|
||||
channel = discord.utils.get(guild.text_channels, name='injury-log')
|
||||
if not channel:
|
||||
logger.warning("Could not find #injury-log channel")
|
||||
return False
|
||||
|
||||
# Get all active injuries
|
||||
injuries_raw = await injury_service.get_all_active_injuries_raw(season)
|
||||
|
||||
if not injuries_raw:
|
||||
logger.info("No active injuries found for injury log update")
|
||||
# Still update the channel with "no injuries" message
|
||||
await _clear_and_post_no_injuries(channel, season)
|
||||
return True
|
||||
|
||||
# Group injuries by team and by week
|
||||
injuries_by_team: Dict[str, List[dict]] = defaultdict(list)
|
||||
injuries_by_week: Dict[int, List[dict]] = defaultdict(list)
|
||||
|
||||
for injury in injuries_raw:
|
||||
player = injury.get('player', {})
|
||||
team_data = player.get('team', {})
|
||||
|
||||
# Get ML team name for grouping
|
||||
ml_team_name = await get_major_league_team_name(team_data, season)
|
||||
|
||||
injuries_by_team[ml_team_name].append({
|
||||
'name': player.get('name', 'Unknown'),
|
||||
'il_return': player.get('il_return', 'TBD'),
|
||||
'end_week': injury.get('end_week', 0)
|
||||
})
|
||||
|
||||
end_week = injury.get('end_week', 0)
|
||||
injuries_by_week[end_week].append({
|
||||
'name': player.get('name', 'Unknown'),
|
||||
'il_return': player.get('il_return', 'TBD')
|
||||
})
|
||||
|
||||
# Create team embed
|
||||
team_embed = EmbedTemplate.create_base_embed(
|
||||
title="🏥 Current Injuries by Team",
|
||||
description="Player Name (Return Date)",
|
||||
color=EmbedColors.WARNING,
|
||||
timestamp=True
|
||||
)
|
||||
team_embed.set_thumbnail(url=config.sba_logo_url)
|
||||
|
||||
# Sort teams alphabetically and add fields
|
||||
for team_name in sorted(injuries_by_team.keys()):
|
||||
players = injuries_by_team[team_name]
|
||||
team_string = '\n'.join(
|
||||
f"{p['name']} ({p['il_return']})"
|
||||
for p in players
|
||||
)
|
||||
|
||||
# Discord field value limit is 1024 chars
|
||||
if len(team_string) > 1024:
|
||||
team_string = team_string[:1020] + "..."
|
||||
|
||||
team_embed.add_field(
|
||||
name=f"{team_name} ({len(players)})",
|
||||
value=team_string,
|
||||
inline=True
|
||||
)
|
||||
|
||||
team_embed.set_footer(
|
||||
text=f"SBa Season {season} • {len(injuries_raw)} active injuries",
|
||||
icon_url=config.sba_logo_url
|
||||
)
|
||||
|
||||
# Create week embed
|
||||
week_embed = EmbedTemplate.create_base_embed(
|
||||
title="📅 Current Injuries by Return Week",
|
||||
description="Player Name (Return Date)",
|
||||
color=EmbedColors.INFO,
|
||||
timestamp=True
|
||||
)
|
||||
week_embed.set_thumbnail(url=config.sba_logo_url)
|
||||
|
||||
# Sort weeks numerically and add fields
|
||||
for week_num in sorted(injuries_by_week.keys()):
|
||||
players = injuries_by_week[week_num]
|
||||
week_string = '\n'.join(
|
||||
f"{p['name']} ({p['il_return']})"
|
||||
for p in players
|
||||
)
|
||||
|
||||
# Discord field value limit is 1024 chars
|
||||
if len(week_string) > 1024:
|
||||
week_string = week_string[:1020] + "..."
|
||||
|
||||
week_embed.add_field(
|
||||
name=f"Week {week_num} ({len(players)})",
|
||||
value=week_string,
|
||||
inline=True
|
||||
)
|
||||
|
||||
week_embed.set_footer(
|
||||
text=f"SBa Season {season} • Sorted by earliest return",
|
||||
icon_url=config.sba_logo_url
|
||||
)
|
||||
|
||||
# Clear old messages and post new ones
|
||||
try:
|
||||
await channel.purge(limit=25)
|
||||
except discord.errors.Forbidden:
|
||||
logger.warning("Could not purge messages in #injury-log (missing permissions)")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error purging messages in #injury-log: {e}")
|
||||
|
||||
await channel.send(embed=team_embed)
|
||||
await channel.send(embed=week_embed)
|
||||
|
||||
logger.info(f"Updated injury log: {len(injuries_raw)} injuries across {len(injuries_by_team)} teams")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating injury log channel: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def _clear_and_post_no_injuries(channel: discord.TextChannel, season: int) -> None:
|
||||
"""Post a 'no injuries' message when there are no active injuries."""
|
||||
config = get_config()
|
||||
|
||||
try:
|
||||
await channel.purge(limit=25)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
embed = EmbedTemplate.create_base_embed(
|
||||
title="🏥 Current Injuries",
|
||||
description="No active injuries at this time.",
|
||||
color=EmbedColors.SUCCESS,
|
||||
timestamp=True
|
||||
)
|
||||
embed.set_thumbnail(url=config.sba_logo_url)
|
||||
embed.set_footer(
|
||||
text=f"SBa Season {season}",
|
||||
icon_url=config.sba_logo_url
|
||||
)
|
||||
|
||||
await channel.send(embed=embed)
|
||||
|
||||
|
||||
async def post_injury_news(
|
||||
bot: discord.Client,
|
||||
player: Player,
|
||||
injury_games: int,
|
||||
return_date: str,
|
||||
season: int
|
||||
) -> bool:
|
||||
"""
|
||||
Post an injury announcement to #sba-network-news.
|
||||
|
||||
Args:
|
||||
bot: Discord bot instance
|
||||
player: Player object who was injured
|
||||
injury_games: Number of games player will miss
|
||||
return_date: Return date in w##g# format
|
||||
season: Current season number
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
config = get_config()
|
||||
guild = bot.get_guild(config.guild_id)
|
||||
|
||||
if not guild:
|
||||
logger.warning(f"Could not find guild {config.guild_id}")
|
||||
return False
|
||||
|
||||
channel = discord.utils.get(
|
||||
guild.text_channels,
|
||||
name=config.sba_network_news_channel
|
||||
)
|
||||
if not channel:
|
||||
logger.warning(f"Could not find #{config.sba_network_news_channel} channel")
|
||||
return False
|
||||
|
||||
# Determine team info
|
||||
team_name = "Unknown Team"
|
||||
team_color = EmbedColors.WARNING
|
||||
team_thumbnail = None
|
||||
|
||||
if player.team:
|
||||
team_name = player.team.sname or player.team.lname
|
||||
if player.team.color:
|
||||
try:
|
||||
team_color = int(player.team.color, 16)
|
||||
except ValueError:
|
||||
pass
|
||||
team_thumbnail = player.team.thumbnail
|
||||
|
||||
# Create news embed
|
||||
embed = EmbedTemplate.create_base_embed(
|
||||
title="🚑 Injury Report",
|
||||
description=f"**{player.name}** has been placed on the injured list.",
|
||||
color=team_color,
|
||||
timestamp=True
|
||||
)
|
||||
|
||||
embed.add_field(
|
||||
name="Player",
|
||||
value=f"{player.name} ({player.primary_position})",
|
||||
inline=True
|
||||
)
|
||||
|
||||
embed.add_field(
|
||||
name="Team",
|
||||
value=team_name,
|
||||
inline=True
|
||||
)
|
||||
|
||||
embed.add_field(
|
||||
name="Duration",
|
||||
value=f"{injury_games} game{'s' if injury_games != 1 else ''}",
|
||||
inline=True
|
||||
)
|
||||
|
||||
embed.add_field(
|
||||
name="Expected Return",
|
||||
value=return_date,
|
||||
inline=True
|
||||
)
|
||||
|
||||
if team_thumbnail:
|
||||
embed.set_thumbnail(url=team_thumbnail)
|
||||
|
||||
embed.set_footer(
|
||||
text=f"SBa Season {season}",
|
||||
icon_url=config.sba_logo_url
|
||||
)
|
||||
|
||||
await channel.send(embed=embed)
|
||||
logger.info(f"Posted injury news for {player.name}: {injury_games} games")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error posting injury news: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def post_injury_and_update_log(
|
||||
bot: discord.Client,
|
||||
player: Player,
|
||||
injury_games: int,
|
||||
return_date: str,
|
||||
season: int
|
||||
) -> None:
|
||||
"""
|
||||
Convenience function to post injury news and update injury log.
|
||||
|
||||
This is the main entry point for injury logging after an injury is recorded.
|
||||
It handles both the news announcement and the full injury log update.
|
||||
|
||||
Args:
|
||||
bot: Discord bot instance
|
||||
player: Player object who was injured
|
||||
injury_games: Number of games player will miss
|
||||
return_date: Return date in w##g# format
|
||||
season: Current season number
|
||||
"""
|
||||
# Post to sba-network-news
|
||||
await post_injury_news(bot, player, injury_games, return_date, season)
|
||||
|
||||
# Update injury-log channel with all current injuries
|
||||
await update_injury_log_channel(bot, season)
|
||||
@ -32,18 +32,33 @@ class BaseView(discord.ui.View):
|
||||
self.created_at = datetime.now(timezone.utc)
|
||||
|
||||
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
||||
"""Check if user is authorized to interact with this view."""
|
||||
"""Check if user is authorized to interact with this view.
|
||||
|
||||
Authorization logic:
|
||||
- If no restrictions set (user_id and responders both None), allow all
|
||||
- If user_id is set, the original command user can interact
|
||||
- If responders is set, anyone in the responders list can interact
|
||||
- User only needs to match ONE condition to be authorized
|
||||
"""
|
||||
# No restrictions - allow everyone
|
||||
if self.user_id is None and self.responders is None:
|
||||
return True
|
||||
|
||||
if (self.user_id is not None and interaction.user.id != self.user_id) or (self.responders is not None and interaction.user.id not in self.responders):
|
||||
await interaction.response.send_message(
|
||||
"❌ You cannot interact with this menu.",
|
||||
ephemeral=True
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# Check if user is authorized by either condition
|
||||
is_command_user = self.user_id is not None and interaction.user.id == self.user_id
|
||||
is_authorized_responder = (
|
||||
self.responders is not None and
|
||||
interaction.user.id in [r for r in self.responders if r is not None]
|
||||
)
|
||||
|
||||
if is_command_user or is_authorized_responder:
|
||||
return True
|
||||
|
||||
await interaction.response.send_message(
|
||||
"❌ You cannot interact with this menu.",
|
||||
ephemeral=True
|
||||
)
|
||||
return False
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
"""Handle view timeout."""
|
||||
|
||||
@ -652,6 +652,22 @@ class BatterInjuryModal(BaseModal):
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
# Post injury news and update injury log channel
|
||||
try:
|
||||
from utils.injury_log import post_injury_and_update_log
|
||||
await post_injury_and_update_log(
|
||||
bot=interaction.client,
|
||||
player=self.player,
|
||||
injury_games=self.injury_games,
|
||||
return_date=return_date,
|
||||
season=self.season
|
||||
)
|
||||
except Exception as log_error:
|
||||
self.logger.warning(
|
||||
f"Failed to post injury to channels (injury was still logged): {log_error}",
|
||||
player_id=self.player.id
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to create batter injury", error=e, player_id=self.player.id)
|
||||
embed = EmbedTemplate.error(
|
||||
@ -864,6 +880,22 @@ class PitcherRestModal(BaseModal):
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
# Post injury news and update injury log channel
|
||||
try:
|
||||
from utils.injury_log import post_injury_and_update_log
|
||||
await post_injury_and_update_log(
|
||||
bot=interaction.client,
|
||||
player=self.player,
|
||||
injury_games=total_injury_games,
|
||||
return_date=return_date,
|
||||
season=self.season
|
||||
)
|
||||
except Exception as log_error:
|
||||
self.logger.warning(
|
||||
f"Failed to post injury to channels (injury was still logged): {log_error}",
|
||||
player_id=self.player.id
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to create pitcher injury", error=e, player_id=self.player.id)
|
||||
embed = EmbedTemplate.error(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user