Major Features Added: • Admin Management System: Complete admin command suite with user moderation, system control, and bot maintenance tools • Enhanced Player Commands: Added batting/pitching statistics with concurrent API calls and improved embed design • League Standings: Full standings system with division grouping, playoff picture, and wild card visualization • Game Schedules: Comprehensive schedule system with team filtering, series organization, and proper home/away indicators New Admin Commands (12 total): • /admin-status, /admin-help, /admin-reload, /admin-sync, /admin-clear • /admin-announce, /admin-maintenance • /admin-timeout, /admin-untimeout, /admin-kick, /admin-ban, /admin-unban, /admin-userinfo Enhanced Player Display: • Team logo positioned beside player name using embed author • Smart thumbnail priority: fancycard → headshot → team logo fallback • Concurrent batting/pitching stats fetching for performance • Rich statistics display with team colors and comprehensive metrics New Models & Services: • BattingStats, PitchingStats, TeamStandings, Division, Game models • StatsService, StandingsService, ScheduleService for data management • CustomCommand system with CRUD operations and cleanup tasks Bot Architecture Improvements: • Admin commands integrated into bot.py with proper loading • Permission checks and safety guards for moderation commands • Enhanced error handling and comprehensive audit logging • All 227 tests passing with new functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
202 lines
8.0 KiB
Python
202 lines
8.0 KiB
Python
"""
|
|
Team roster commands for Discord Bot v2.0
|
|
"""
|
|
import logging
|
|
from typing import Optional, Dict, Any, List
|
|
|
|
import discord
|
|
from discord.ext import commands
|
|
|
|
from services import team_service, player_service
|
|
from models.team import Team
|
|
from constants import SBA_CURRENT_SEASON
|
|
from utils.logging import get_contextual_logger
|
|
from utils.decorators import logged_command
|
|
from exceptions import BotException
|
|
from views.embeds import EmbedTemplate, EmbedColors
|
|
|
|
|
|
class TeamRosterCommands(commands.Cog):
|
|
"""Team roster command handlers."""
|
|
|
|
def __init__(self, bot: commands.Bot):
|
|
self.bot = bot
|
|
self.logger = get_contextual_logger(f'{__name__}.TeamRosterCommands')
|
|
self.logger.info("TeamRosterCommands cog initialized")
|
|
|
|
@discord.app_commands.command(name="roster", description="Display team roster")
|
|
@discord.app_commands.describe(
|
|
abbrev="Team abbreviation (e.g., NYY, BOS, LAD)",
|
|
roster_type="Roster week: current or next (defaults to current)"
|
|
)
|
|
@discord.app_commands.choices(roster_type=[
|
|
discord.app_commands.Choice(name="Current Week", value="current"),
|
|
discord.app_commands.Choice(name="Next Week", value="next")
|
|
])
|
|
@logged_command("/roster")
|
|
async def team_roster(self, interaction: discord.Interaction, abbrev: str,
|
|
roster_type: str = "current"):
|
|
"""Display team roster with position breakdowns."""
|
|
await interaction.response.defer()
|
|
|
|
# Get team by abbreviation
|
|
team = await team_service.get_team_by_abbrev(abbrev, SBA_CURRENT_SEASON)
|
|
|
|
if team is None:
|
|
self.logger.info("Team not found", team_abbrev=abbrev)
|
|
embed = EmbedTemplate.error(
|
|
title="Team Not Found",
|
|
description=f"No team found with abbreviation '{abbrev.upper()}'"
|
|
)
|
|
await interaction.followup.send(embed=embed)
|
|
return
|
|
|
|
# Get roster data
|
|
roster_data = await team_service.get_team_roster(team.id, roster_type)
|
|
|
|
if not roster_data:
|
|
embed = EmbedTemplate.error(
|
|
title="Roster Not Available",
|
|
description=f"No {roster_type} roster data available for {team.abbrev}"
|
|
)
|
|
await interaction.followup.send(embed=embed)
|
|
return
|
|
|
|
# Create roster embeds
|
|
embeds = await self._create_roster_embeds(team, roster_data, roster_type)
|
|
|
|
# Send first embed and follow up with others if needed
|
|
await interaction.followup.send(embed=embeds[0])
|
|
for embed in embeds[1:]:
|
|
await interaction.followup.send(embed=embed)
|
|
|
|
async def _create_roster_embeds(self, team: Team, roster_data: Dict[str, Any],
|
|
roster_type: str) -> List[discord.Embed]:
|
|
"""Create embeds for team roster data."""
|
|
embeds = []
|
|
|
|
# Main roster embed
|
|
embed = EmbedTemplate.create_base_embed(
|
|
title=f"{team.abbrev} - {roster_type.title()} Roster",
|
|
description=f"{team.lname} roster breakdown",
|
|
color=int(team.color, 16) if team.color else EmbedColors.PRIMARY
|
|
)
|
|
|
|
# Position counts for active roster
|
|
if 'active' in roster_data:
|
|
active_roster = roster_data['active']
|
|
|
|
# Batting positions
|
|
batting_positions = ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH']
|
|
batting_counts = []
|
|
for pos in batting_positions:
|
|
count = active_roster.get(pos, 0)
|
|
batting_counts.append(f"**{pos}:** {count}")
|
|
|
|
# Pitching positions
|
|
pitching_positions = ['SP', 'RP', 'CP']
|
|
pitching_counts = []
|
|
for pos in pitching_positions:
|
|
count = active_roster.get(pos, 0)
|
|
pitching_counts.append(f"**{pos}:** {count}")
|
|
|
|
# Add position count fields
|
|
embed.add_field(
|
|
name="Batting Positions",
|
|
value="\n".join(batting_counts),
|
|
inline=True
|
|
)
|
|
embed.add_field(
|
|
name="Pitching Positions",
|
|
value="\n".join(pitching_counts),
|
|
inline=True
|
|
)
|
|
|
|
# Total WAR
|
|
total_war = active_roster.get('WARa', 0)
|
|
embed.add_field(
|
|
name="Total sWAR",
|
|
value=f"{total_war:.1f}" if isinstance(total_war, (int, float)) else str(total_war),
|
|
inline=True
|
|
)
|
|
|
|
# Add injury list summaries
|
|
if 'shortil' in roster_data and roster_data['shortil']:
|
|
short_il_count = len(roster_data['shortil'].get('players', []))
|
|
embed.add_field(name="Minor League", value=f"{short_il_count} players", inline=True)
|
|
|
|
if 'longil' in roster_data and roster_data['longil']:
|
|
long_il_count = len(roster_data['longil'].get('players', []))
|
|
embed.add_field(name="Injured List", value=f"{long_il_count} players", inline=True)
|
|
|
|
embeds.append(embed)
|
|
|
|
# Create detailed player list embeds if there are players
|
|
for roster_name, roster_info in roster_data.items():
|
|
if roster_name in ['active', 'shortil', 'longil'] and 'players' in roster_info:
|
|
players = roster_info['players']
|
|
if players:
|
|
player_embed = self._create_player_list_embed(
|
|
team, roster_name, players
|
|
)
|
|
embeds.append(player_embed)
|
|
|
|
return embeds
|
|
|
|
def _create_player_list_embed(self, team: Team, roster_name: str,
|
|
players: List[Dict[str, Any]]) -> discord.Embed:
|
|
"""Create an embed with detailed player list."""
|
|
roster_titles = {
|
|
'active': 'Active Roster',
|
|
'shortil': 'Minor League',
|
|
'longil': 'Injured List'
|
|
}
|
|
|
|
embed = EmbedTemplate.create_base_embed(
|
|
title=f"{team.abbrev} - {roster_titles.get(roster_name, roster_name.title())}",
|
|
color=int(team.color, 16) if team.color else EmbedColors.PRIMARY
|
|
)
|
|
|
|
# Group players by position for better organization
|
|
batters = []
|
|
pitchers = []
|
|
|
|
for player in players:
|
|
name = player.get('name', 'Unknown')
|
|
positions = player.get('positions', [])
|
|
war = player.get('WARa', 0)
|
|
|
|
# Format WAR display
|
|
war_str = f"{war:.1f}" if isinstance(war, (int, float)) else str(war)
|
|
|
|
# Determine if pitcher or batter
|
|
is_pitcher = any(pos in ['SP', 'RP', 'CP'] for pos in positions)
|
|
|
|
player_line = f"**{name}** ({'/'.join(positions)}) - WAR: {war_str}"
|
|
|
|
if is_pitcher:
|
|
pitchers.append(player_line)
|
|
else:
|
|
batters.append(player_line)
|
|
|
|
# Add player lists to embed
|
|
if batters:
|
|
# Split long lists into multiple fields if needed
|
|
batter_chunks = self._chunk_list(batters, 10)
|
|
for i, chunk in enumerate(batter_chunks):
|
|
field_name = "Batters" if i == 0 else f"Batters (cont.)"
|
|
embed.add_field(name=field_name, value="\n".join(chunk), inline=False)
|
|
|
|
if pitchers:
|
|
pitcher_chunks = self._chunk_list(pitchers, 10)
|
|
for i, chunk in enumerate(pitcher_chunks):
|
|
field_name = "Pitchers" if i == 0 else f"Pitchers (cont.)"
|
|
embed.add_field(name=field_name, value="\n".join(chunk), inline=False)
|
|
|
|
embed.set_footer(text=f"Total players: {len(players)}")
|
|
|
|
return embed
|
|
|
|
def _chunk_list(self, lst: List[str], chunk_size: int) -> List[List[str]]:
|
|
"""Split a list into chunks of specified size."""
|
|
return [lst[i:i + chunk_size] for i in range(0, len(lst), chunk_size)] |