From 1575d4f096860806dc17b4b993c415a129f12717 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 20 Oct 2025 15:10:48 -0500 Subject: [PATCH] CLAUDE: Add /jump command and improve dice rolling with team colors, plus admin and type safety fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added /jump command for baserunner stealing mechanics with pickoff/balk detection. Enhanced dice rolling commands with team color support in embeds. Improved /admin-sync with local/global options and prefix command fallback. Fixed type safety issues in admin commands and injury management. Updated config for expanded draft rounds and testing mode. Key changes: - commands/dice/rolls.py: New /jump and !j commands with special cases for pickoff (d20=1) and balk (d20=2) - commands/dice/rolls.py: Added team/channel color support to /ab and dice embeds - commands/dice/rolls.py: Added pitcher position to /fielding command with proper range/error charts - commands/admin/management.py: Enhanced /admin-sync with local/clear options and !admin-sync prefix fallback - commands/admin/management.py: Fixed Member type checking and channel type validation - commands/injuries/management.py: Fixed responder team detection for injury clearing - models/custom_command.py: Made creator_id optional for execute endpoint compatibility - config.py: Updated draft_rounds to 32 and enabled testing mode - services/transaction_builder.py: Adjusted ML roster limit to 26 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- bot.py | 6 +- commands/admin/management.py | 152 +++++++++++++++++--- commands/dice/rolls.py | 244 +++++++++++++++++++++++++++++--- commands/injuries/management.py | 7 +- config.py | 4 +- models/custom_command.py | 2 +- services/transaction_builder.py | 2 +- 7 files changed, 369 insertions(+), 48 deletions(-) diff --git a/bot.py b/bot.py index 82af6ae..99234c5 100644 --- a/bot.py +++ b/bot.py @@ -93,7 +93,7 @@ class SBABot(commands.Bot): # Initialize cleanup tasks await self._setup_background_tasks() - # Smart command syncing: auto-sync in development if changes detected + # Smart command syncing: auto-sync in development if changes detected; !admin-sync for first sync config = get_config() if config.is_development: if await self._should_sync_commands(): @@ -104,7 +104,7 @@ class SBABot(commands.Bot): self.logger.info("Development mode: no command changes detected, skipping sync") else: self.logger.info("Production mode: commands loaded but not auto-synced") - self.logger.info("Use /sync command to manually sync when needed") + self.logger.info("Use /admin-sync command to manually sync when needed") async def _load_command_packages(self): """Load all command packages with resilient error handling.""" @@ -290,7 +290,7 @@ class SBABot(commands.Bot): async def _sync_commands(self): """Internal method to sync commands.""" config = get_config() - if config.guild_id: + if config.testing and config.guild_id: guild = discord.Object(id=config.guild_id) self.tree.copy_global_to(guild=guild) synced = await self.tree.sync(guild=guild) diff --git a/commands/admin/management.py b/commands/admin/management.py index 8791bd0..eff9029 100644 --- a/commands/admin/management.py +++ b/commands/admin/management.py @@ -3,7 +3,6 @@ Admin Management Commands Administrative commands for league management and bot maintenance. """ -from typing import Optional, Union import asyncio import discord @@ -25,6 +24,14 @@ class AdminCommands(commands.Cog): async def interaction_check(self, interaction: discord.Interaction) -> bool: """Check if user has admin permissions.""" + # Check if interaction is from a guild and user is a Member + if not isinstance(interaction.user, discord.Member): + await interaction.response.send_message( + "❌ Admin commands can only be used in a server.", + ephemeral=True + ) + return False + if not interaction.user.guild_permissions.administrator: await interaction.response.send_message( "❌ You need administrator permissions to use admin commands.", @@ -196,20 +203,64 @@ class AdminCommands(commands.Cog): name="admin-sync", description="Sync application commands with Discord" ) + @app_commands.describe( + local="Sync to this guild only (fast, for development)", + clear_local="Clear locally synced commands (does not sync after clearing)" + ) @logged_command("/admin-sync") - async def admin_sync(self, interaction: discord.Interaction): + async def admin_sync( + self, + interaction: discord.Interaction, + local: bool = False, + clear_local: bool = False + ): """Sync slash commands with Discord API.""" await interaction.response.defer() - + try: - synced_commands = await self.bot.tree.sync() - + # Clear local commands if requested + if clear_local: + if not interaction.guild_id: + raise ValueError("Cannot clear local commands outside of a guild") + + self.logger.info(f"Clearing local commands for guild {interaction.guild_id}") + self.bot.tree.clear_commands(guild=discord.Object(id=interaction.guild_id)) + + embed = EmbedTemplate.create_base_embed( + title="✅ Local Commands Cleared", + description=f"Cleared all commands synced to this guild", + color=EmbedColors.SUCCESS + ) + embed.add_field( + name="Clear Details", + value=f"**Guild ID:** {interaction.guild_id}\n" + f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}\n" + f"**Note:** Commands not synced after clearing", + inline=False + ) + await interaction.followup.send(embed=embed) + return + + # Determine sync target + if local: + if not interaction.guild_id: + raise ValueError("Cannot sync locally outside of a guild") + guild = discord.Object(id=interaction.guild_id) + sync_type = "local guild" + else: + guild = None + sync_type = "globally" + + # Perform sync + self.logger.info(f"Syncing commands {sync_type}") + synced_commands = await self.bot.tree.sync(guild=guild) + embed = EmbedTemplate.create_base_embed( title="✅ Commands Synced Successfully", - description=f"Synced {len(synced_commands)} application commands", + description=f"Synced {len(synced_commands)} application commands {sync_type}", color=EmbedColors.SUCCESS ) - + # Show some of the synced commands command_names = [cmd.name for cmd in synced_commands[:10]] embed.add_field( @@ -218,23 +269,73 @@ class AdminCommands(commands.Cog): (f"\n... and {len(synced_commands) - 10} more" if len(synced_commands) > 10 else ""), inline=False ) - + embed.add_field( name="Sync Details", value=f"**Total Commands:** {len(synced_commands)}\n" - f"**Guild ID:** {interaction.guild_id}\n" + f"**Sync Type:** {sync_type.title()}\n" + f"**Guild ID:** {interaction.guild_id or 'N/A'}\n" f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}", inline=False ) - + except Exception as e: + self.logger.error(f"Sync failed: {e}", exc_info=True) embed = EmbedTemplate.create_base_embed( title="❌ Sync Failed", description=f"Failed to sync commands: {str(e)}", color=EmbedColors.ERROR ) - + await interaction.followup.send(embed=embed) + + @commands.command(name="admin-sync") + @commands.has_permissions(administrator=True) + async def admin_sync_prefix(self, ctx: commands.Context): + """ + Prefix command version of admin-sync for bootstrap scenarios. + + Use this when slash commands aren't synced yet and you can't access /admin-sync. + """ + self.logger.info(f"Prefix command !admin-sync invoked by {ctx.author} in {ctx.guild}") + + try: + synced_commands = await self.bot.tree.sync() + + embed = EmbedTemplate.create_base_embed( + title="✅ Commands Synced Successfully", + description=f"Synced {len(synced_commands)} application commands", + color=EmbedColors.SUCCESS + ) + + # Show some of the synced commands + command_names = [cmd.name for cmd in synced_commands[:10]] + embed.add_field( + name="Synced Commands", + value="\n".join([f"• /{name}" for name in command_names]) + + (f"\n... and {len(synced_commands) - 10} more" if len(synced_commands) > 10 else ""), + inline=False + ) + + embed.add_field( + name="Sync Details", + value=f"**Total Commands:** {len(synced_commands)}\n" + f"**Guild ID:** {ctx.guild.id}\n" + f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}", + inline=False + ) + + embed.set_footer(text="💡 Use /admin-sync (slash command) for future syncs") + + except Exception as e: + self.logger.error(f"Prefix command sync failed: {e}", exc_info=True) + embed = EmbedTemplate.create_base_embed( + title="❌ Sync Failed", + description=f"Failed to sync commands: {str(e)}", + color=EmbedColors.ERROR + ) + + await ctx.send(embed=embed) @app_commands.command( name="admin-clear", @@ -254,7 +355,15 @@ class AdminCommands(commands.Cog): return await interaction.response.defer() - + + # Verify channel type supports purge + if not isinstance(interaction.channel, (discord.TextChannel, discord.Thread, discord.VoiceChannel, discord.StageChannel)): + await interaction.followup.send( + "❌ Cannot purge messages in this channel type.", + ephemeral=True + ) + return + try: deleted = await interaction.channel.purge(limit=count) @@ -276,10 +385,11 @@ class AdminCommands(commands.Cog): # Send confirmation and auto-delete after 5 seconds message = await interaction.followup.send(embed=embed) await asyncio.sleep(5) - try: - await message.delete() - except discord.NotFound: - pass # Message already deleted + if message: + try: + await message.delete() + except discord.NotFound: + pass # Message already deleted except discord.Forbidden: await interaction.followup.send( @@ -320,10 +430,12 @@ class AdminCommands(commands.Cog): text=f"Announcement by {interaction.user.display_name}", icon_url=interaction.user.display_avatar.url ) - - content = "@everyone" if mention_everyone else None - - await interaction.followup.send(content=content, embed=embed) + + # Send with or without mention based on flag + if mention_everyone: + await interaction.followup.send(content="@everyone", embed=embed) + else: + await interaction.followup.send(embed=embed) # Log the announcement self.logger.info( diff --git a/commands/dice/rolls.py b/commands/dice/rolls.py index e51e5da..745a362 100644 --- a/commands/dice/rolls.py +++ b/commands/dice/rolls.py @@ -11,8 +11,12 @@ from dataclasses import dataclass import discord from discord.ext import commands +from models.team import Team +from services.team_service import team_service +from utils import team_utils from utils.logging import get_contextual_logger from utils.decorators import logged_command +from utils.team_utils import get_user_major_league_team from views.embeds import EmbedColors, EmbedTemplate @@ -93,13 +97,20 @@ class DiceRollCommands(commands.Cog): async def ab_dice(self, interaction: discord.Interaction): """Roll the standard baseball at-bat dice combination.""" await interaction.response.defer() + embed_color = await self._get_channel_embed_color(interaction) # Use the standard baseball dice combination dice_notation = "1d6;2d6;1d20" roll_results = self._parse_and_roll_multiple_dice(dice_notation) # Create embed for the roll results - embed = self._create_multi_roll_embed(dice_notation, roll_results, interaction.user, set_author=False) + embed = self._create_multi_roll_embed( + dice_notation, + roll_results, + interaction.user, + set_author=False, + embed_color=embed_color + ) embed.title = f'At bat roll for {interaction.user.display_name}' await interaction.followup.send(embed=embed) @@ -107,6 +118,10 @@ class DiceRollCommands(commands.Cog): async def ab_dice_prefix(self, ctx: commands.Context): """Roll baseball at-bat dice using prefix commands (!ab, !atbat).""" self.logger.info(f"At Bat dice command started by {ctx.author.display_name}") + team = await get_user_major_league_team(user_id=ctx.author.id) + embed_color = EmbedColors.PRIMARY + if team is not None and team.color is not None: + embed_color = int(team.color,16) # Use the standard baseball dice combination dice_notation = "1d6;2d6;1d20" @@ -115,7 +130,7 @@ class DiceRollCommands(commands.Cog): self.logger.info("At Bat dice rolled successfully", roll_count=len(roll_results)) # Create embed for the roll results - embed = self._create_multi_roll_embed(dice_notation, roll_results, ctx.author) + embed = self._create_multi_roll_embed(dice_notation, roll_results, ctx.author, set_author=False, embed_color=embed_color) embed.title = f'At bat roll for {ctx.author.display_name}' await ctx.send(embed=embed) @@ -158,6 +173,7 @@ class DiceRollCommands(commands.Cog): position="Defensive position" ) @discord.app_commands.choices(position=[ + discord.app_commands.Choice(name="Pitcher (P)", value="P"), discord.app_commands.Choice(name="Catcher (C)", value="C"), discord.app_commands.Choice(name="First Base (1B)", value="1B"), discord.app_commands.Choice(name="Second Base (2B)", value="2B"), @@ -212,6 +228,82 @@ class DiceRollCommands(commands.Cog): embed = self._create_fielding_embed(parsed_position, roll_results, ctx.author) await ctx.send(embed=embed) + @discord.app_commands.command( + name="jump", + description="Roll for baserunner's jump before stealing" + ) + @logged_command("/jump") + async def jump_dice(self, interaction: discord.Interaction): + """Roll to check for a baserunner's jump before attempting to steal a base.""" + await interaction.response.defer() + embed_color = await self._get_channel_embed_color(interaction) + + # Roll 1d20 for pickoff/balk check + check_roll = random.randint(1, 20) + + # Roll 2d6 for jump rating + jump_result = self._parse_and_roll_single_dice("2d6") + + # Roll another 1d20 for pickoff/balk resolution + resolution_roll = random.randint(1, 20) + + # Create embed based on check roll + embed = self._create_jump_embed( + check_roll, + jump_result, + resolution_roll, + interaction.user, + embed_color, + show_author=False + ) + await interaction.followup.send(embed=embed) + + @commands.command(name="j", aliases=["jump"]) + async def jump_dice_prefix(self, ctx: commands.Context): + """Roll for baserunner's jump using prefix commands (!j, !jump).""" + self.logger.info(f"Jump command started by {ctx.author.display_name}") + team = await get_user_major_league_team(user_id=ctx.author.id) + embed_color = EmbedColors.PRIMARY + if team is not None and team.color is not None: + embed_color = int(team.color, 16) + + # Roll 1d20 for pickoff/balk check + check_roll = random.randint(1, 20) + + # Roll 2d6 for jump rating + jump_result = self._parse_and_roll_single_dice("2d6") + + # Roll another 1d20 for pickoff/balk resolution + resolution_roll = random.randint(1, 20) + + self.logger.info("Jump dice rolled successfully", check=check_roll, jump=jump_result.total if jump_result else None, resolution=resolution_roll) + + # Create embed based on check roll + embed = self._create_jump_embed( + check_roll, + jump_result, + resolution_roll, + ctx.author, + embed_color + ) + await ctx.send(embed=embed) + + async def _get_channel_embed_color(self, interaction: discord.Interaction) -> int: + # Check if channel is a type that has a name attribute (DMChannel doesn't have one) + if isinstance(interaction.channel, (discord.TextChannel, discord.VoiceChannel, discord.Thread)): + channel_starter = interaction.channel.name[:6] + if '-' in channel_starter: + abbrev = channel_starter.split('-')[0] + channel_team = await team_service.get_team_by_abbrev(abbrev) + if channel_team is not None and channel_team.color is not None: + return int(channel_team.color,16) + + team = await get_user_major_league_team(user_id=interaction.user.id) + if team is not None and team.color is not None: + return int(team.color,16) + + return EmbedColors.PRIMARY + def _parse_position(self, position: str) -> str | None: """Parse and validate fielding position input for prefix commands.""" if not position: @@ -221,6 +313,7 @@ class DiceRollCommands(commands.Cog): # Map common inputs to standard position names position_map = { + 'P': 'P', 'PITCHER': 'P', 'C': 'C', 'CATCHER': 'C', '1': '1B', '1B': '1B', 'FIRST': '1B', 'FIRSTBASE': '1B', '2': '2B', '2B': '2B', 'SECOND': '2B', 'SECONDBASE': '2B', @@ -266,8 +359,8 @@ class DiceRollCommands(commands.Cog): # Add fielding check summary range_result = self._get_range_result(position, d20_result) embed.add_field( - name=f"{position} Fielding Check Summary", - value=f"```\nRange Result\n 1 | 2 | 3 | 4 | 5\n{range_result}```", + name=f"{position} Range Result", + value=f"```md\n 1 | 2 | 3 | 4 | 5\n{range_result}```", inline=False ) @@ -280,26 +373,87 @@ class DiceRollCommands(commands.Cog): inline=False ) - # Add help commands - embed.add_field( - name="Help Commands", - value="Run ! for full chart readout (e.g. !g1 or !do3)", - inline=False + # # Add help commands + # embed.add_field( + # name="Help Commands", + # value="Run ! for full chart readout (e.g. !g1 or !do3)", + # inline=False + # ) + + # # Add references + # embed.add_field( + # name="References", + # value="Range Chart / Error Chart / Result Reference", + # inline=False + # ) + + return embed + + def _create_jump_embed( + self, + check_roll: int, + jump_result: DiceRoll | None, + resolution_roll: int, + user: discord.User | discord.Member, + embed_color: int = EmbedColors.PRIMARY, + show_author: bool = True + ) -> discord.Embed: + """Create an embed for jump roll results.""" + # Create base embed + embed = EmbedTemplate.create_base_embed( + title=f"Jump roll for {user.name}", + color=embed_color ) - # Add references - embed.add_field( - name="References", - value="Range Chart / Error Chart / Result Reference", - inline=False - ) + if show_author: + # Set user info + embed.set_author( + name=user.name, + icon_url=user.display_avatar.url + ) + + # Check for pickoff or balk + if check_roll == 1: + # Pickoff attempt + embed.add_field( + name="Special", + value="```md\nCheck pickoff```", + inline=False + ) + embed.add_field( + name="Pickoff roll", + value=f"```md\n# {resolution_roll}\nDetails:[1d20 ({resolution_roll})]```", + inline=False + ) + elif check_roll == 2: + # Balk + embed.add_field( + name="Special", + value="```md\nCheck balk```", + inline=False + ) + embed.add_field( + name="Balk roll", + value=f"```md\n# {resolution_roll}\nDetails:[1d20 ({resolution_roll})]```", + inline=False + ) + else: + # Normal jump - show 2d6 result + if jump_result: + rolls_str = ' '.join(str(r) for r in jump_result.rolls) + embed.add_field( + name="Result", + value=f"```md\n# {jump_result.total}\nDetails:[2d6 ({rolls_str})]```", + inline=False + ) return embed def _get_range_result(self, position: str, d20_roll: int) -> str: """Get the range result display for a position and d20 roll.""" - # Infield positions share the same range chart - if position in ['1B', '2B', '3B', 'SS']: + if position == 'P': + return self._get_pitcher_range(d20_roll) + elif position in ['1B', '2B', '3B', 'SS']: return self._get_infield_range(d20_roll) elif position in ['LF', 'CF', 'RF']: return self._get_outfield_range(d20_roll) @@ -385,10 +539,38 @@ class DiceRollCommands(commands.Cog): } return catcher_ranges.get(d20_roll, 'Unknown') + def _get_pitcher_range(self, d20_roll: int) -> str: + """Get pitcher range result based on d20 roll.""" + pitcher_ranges = { + 1: 'G3 ------SI1------', + 2: 'G3 ------SI1------', + 3: '--G3--- ----SI1----', + 4: '----G3----- --SI1--', + 5: '------G3------- SI1', + 6: '------G3------- SI1', + 7: '--------G3---------', + 8: 'G2 ------G3-------', + 9: 'G2 ------G3-------', + 10: 'G1 G2 ----G3-----', + 11: 'G1 G2 ----G3-----', + 12: 'G1 G2 ----G3-----', + 13: '--G1--- G2 --G3---', + 14: '--G1--- --G2--- G3', + 15: '--G1--- ----G2-----', + 16: '--G1--- ----G2-----', + 17: '----G1----- --G2---', + 18: '----G1----- --G2---', + 19: '------G1------- G2', + 20: '--------G1---------' + } + return pitcher_ranges.get(d20_roll, 'Unknown') + def _get_error_result(self, position: str, d6_total: int) -> str: """Get the error result for a position and 3d6 total.""" # Get the appropriate error chart - if position == '1B': + if position == 'P': + return self._get_pitcher_error(d6_total) + elif position == '1B': return self._get_1b_error(d6_total) elif position == '2B': return self._get_2b_error(d6_total) @@ -560,6 +742,28 @@ class DiceRollCommands(commands.Cog): } return errors.get(d6_total, 'No error') + def _get_pitcher_error(self, d6_total: int) -> str: + """Get Pitcher error result based on 3d6 total.""" + errors = { + 18: '2-base error for e4 -> e12, e19 -> e28, e34 -> e43, e46 -> e48', + 17: '2-base error for e13 -> e28, e44 -> e50', + 16: '2-base error for e30 -> e48, e50, e51\n1-base error for e8, e11, e16, e23', + 15: '2-base error for e50, e51\n1-base error for e10 -> e12, e19, e20, e24, e26, e30, e35, e38, e40, e46, e47', + 14: '1-base error for e4, e14, e18, e21, e22, e26, e31, e35, e42, e43, e48 -> e51', + 13: '1-base error for e6, e13, e14, e21, e22, e26, e27, e30 -> 34, e38 -> e51', + 12: '1-base error for e7, e11, e12, e15 -> e19, e22 -> e51', + 11: '1-base error for e10, e13, e15, e17, e18, e20, e21, e23, e24, e27 -> 38, e40, e42, e44 -> e51', + 10: '1-base error for e20, e23, e24, e27 -> e51', + 9: '1-base error for e16, e19, e26, e28, e34 -> e36, e39 -> e51', + 8: '1-base error for e22, e33, e38, e39, e43 -> e51', + 7: '1-base error for e14, e21, e36, e39, e42 -> e44, e47 -> e51', + 6: '1-base error for e8, e22, e38, e39, e43 -> e51', + 5: 'No error', + 4: '1-base error for e15, e16, e40', + 3: '2-base error for e8 -> e12, e26 -> e28, e39 -> e43\n1-base error for e2, e3, e7, e14, e15' + } + return errors.get(d6_total, 'No error') + def _parse_and_roll_multiple_dice(self, dice_notation: str) -> list[DiceRoll]: """Parse dice notation (supports multiple rolls) and return roll results.""" # Split by semicolon for multiple rolls @@ -637,11 +841,11 @@ class DiceRollCommands(commands.Cog): return [first_d6_result, second_result, third_result] - def _create_multi_roll_embed(self, dice_notation: str, roll_results: list[DiceRoll], user: discord.User | discord.Member, set_author: bool = True) -> discord.Embed: + def _create_multi_roll_embed(self, dice_notation: str, roll_results: list[DiceRoll], user: discord.User | discord.Member, set_author: bool = True, embed_color: int = EmbedColors.PRIMARY) -> discord.Embed: """Create an embed for multiple dice roll results.""" embed = EmbedTemplate.create_base_embed( title="🎲 Dice Roll", - color=EmbedColors.PRIMARY + color=embed_color ) if set_author: diff --git a/commands/injuries/management.py b/commands/injuries/management.py index 215a9f9..b668e7f 100644 --- a/commands/injuries/management.py +++ b/commands/injuries/management.py @@ -20,10 +20,12 @@ from config import get_config from models.current import Current from models.injury import Injury from models.player import Player +from models.team import RosterType from services.player_service import player_service from services.injury_service import injury_service from services.league_service import league_service from services.giphy_service import GiphyService +from utils import team_utils from utils.logging import get_contextual_logger from utils.decorators import logged_command from utils.autocomplete import player_autocomplete @@ -608,6 +610,9 @@ class InjuryGroup(app_commands.Group): inline=True ) + if player.team.roster_type() != RosterType.MAJOR_LEAGUE: + responder_team = await team_utils.get_user_major_league_team(interaction.user.id) + # Create callback for confirmation async def clear_confirm_callback(button_interaction: discord.Interaction): """Handle confirmation to clear injury.""" @@ -666,7 +671,7 @@ class InjuryGroup(app_commands.Group): view = ConfirmationView( user_id=interaction.user.id, timeout=180.0, # 3 minutes for confirmation - responders=[player.team.gmid, player.team.gmid2] if player.team else None, + responders=[responder_team.gmid, responder_team.gmid2] if responder_team else None, confirm_callback=clear_confirm_callback, confirm_label="Clear Injury", cancel_label="Cancel" diff --git a/config.py b/config.py index 81cfea7..de71b54 100644 --- a/config.py +++ b/config.py @@ -46,7 +46,7 @@ class BotConfig(BaseSettings): # Draft Constants default_pick_minutes: int = 10 - draft_rounds: int = 25 + draft_rounds: int = 32 # Special Team IDs free_agent_team_id: int = 498 @@ -64,7 +64,7 @@ class BotConfig(BaseSettings): # Application settings log_level: str = "INFO" environment: str = "development" - testing: bool = False + testing: bool = True # Google Sheets settings sheets_credentials_path: str = "/app/data/major-domo-service-creds.json" diff --git a/models/custom_command.py b/models/custom_command.py index 3498395..ac8602b 100644 --- a/models/custom_command.py +++ b/models/custom_command.py @@ -27,7 +27,7 @@ class CustomCommand(SBABaseModel): id: int = Field(..., description="Database ID") # type: ignore name: str = Field(..., description="Command name (unique)") content: str = Field(..., description="Command response content") - creator_id: int = Field(..., description="ID of the creator") + 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") # Timestamps diff --git a/services/transaction_builder.py b/services/transaction_builder.py index 0b4a830..cf1f66f 100644 --- a/services/transaction_builder.py +++ b/services/transaction_builder.py @@ -380,7 +380,7 @@ class TransactionBuilder: ml_limit = 26 mil_limit = 6 else: - ml_limit = 25 + ml_limit = 26 mil_limit = 14 # Validate roster limits