From e20677ec727d326701c47bfd7284e97eb20831c9 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Wed, 24 Sep 2025 23:30:05 -0500 Subject: [PATCH] CLAUDE: Enhance voice channel validation for Major League teams MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update voice channel commands to require Major League team ownership for both public and private channels: ## Key Changes - **Major League Team Validation**: Added `_get_user_major_league_team()` method to filter teams by `RosterType.MAJOR_LEAGUE` - **Enhanced Requirements**: Both `/voice-channel public` and `/voice-channel private` now require Major League team ownership - **Improved Error Messages**: Updated error messages to clearly indicate Major League team requirement - **Schedule Integration**: Private channels now properly validate Major League teams for weekly game schedules ## Technical Implementation - **Team Filtering**: Uses `team.roster_type() == RosterType.MAJOR_LEAGUE` to identify 3-character abbreviation teams - **Service Integration**: Leverages existing team service and roster type logic from team model - **Backward Compatibility**: Maintains existing `_get_user_team()` method for potential future use ## Team Type Validation - **Major League**: 3-character abbreviations (NYY, BOS, LAD) - **Required for voice channels** - **Minor League**: 4+ characters ending in "MIL" (NYYMIL, BOSMIL) - **Not eligible** - **Injured List**: Ending in "IL" (NYYIL, BOSIL) - **Not eligible** ## Updated Tests - **Mock Team Updates**: Added `roster_type()` method mocking to test team objects - **Async Service Mocking**: Fixed team service mocks to return proper async results - **Error Message Assertions**: Updated test assertions for new error messages - **Type Safety**: Enhanced mock objects with proper Discord voice channel specifications ## Documentation Updates - **README.md**: Added comprehensive team validation logic explanation - **Architecture Documentation**: Detailed team type requirements and rationale - **Code Examples**: Included implementation snippets for team filtering logic **Rationale**: Voice channels are for scheduled gameplay between Major League teams. Minor League and Injured List teams don't participate in weekly games, so restricting access ensures proper schedule integration. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- commands/voice/README.md | 27 +++++++++++++++++++++++--- commands/voice/channels.py | 37 ++++++++++++++++++++++++++++-------- tests/test_commands_voice.py | 20 ++++++++++++------- 3 files changed, 66 insertions(+), 18 deletions(-) diff --git a/commands/voice/README.md b/commands/voice/README.md index cb8873b..db7d712 100644 --- a/commands/voice/README.md +++ b/commands/voice/README.md @@ -45,7 +45,7 @@ This directory contains Discord slash commands for creating and managing voice c ### 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 be on a current team +- **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`) @@ -55,7 +55,7 @@ This directory contains Discord slash commands for creating and managing voice c - **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 be on a current team + - 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`) @@ -69,11 +69,32 @@ This directory contains Discord slash commands for creating and managing voice c ## Architecture ### Command Flow -1. **Team Verification**: Check user has current team using `team_service` +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 diff --git a/commands/voice/channels.py b/commands/voice/channels.py index 7e7ce44..32148e5 100644 --- a/commands/voice/channels.py +++ b/commands/voice/channels.py @@ -17,6 +17,7 @@ from utils.logging import get_contextual_logger from utils.decorators import logged_command from constants import SBA_CURRENT_SEASON from views.embeds import EmbedTemplate +from models.team import RosterType logger = logging.getLogger(f'{__name__}.VoiceChannelCommands') @@ -63,6 +64,25 @@ class VoiceChannelCommands(commands.Cog): teams = await team_service.get_teams_by_owner(user_id, season) return teams[0] if teams else None + 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. + + Args: + user_id: Discord user ID + season: Season to check (defaults to current) + + Returns: + Major League Team object or None if not found + """ + season = season or SBA_CURRENT_SEASON + 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 + async def _create_tracked_channel( self, interaction: discord.Interaction, @@ -107,12 +127,12 @@ class VoiceChannelCommands(commands.Cog): """Create a public voice channel for gameplay.""" await interaction.response.defer() - # Verify user has a team - user_team = await self._get_user_team(interaction.user.id) + # Verify user has a Major League team + user_team = await self._get_user_major_league_team(interaction.user.id) if not user_team: embed = EmbedTemplate.error( - title="No Team Found", - description="❌ You must be on a team to create voice channels.\n\n" + title="No Major League Team Found", + description="❌ You must own a Major League team to create voice channels.\n\n" "Contact a league administrator if you believe this is an error." ) await interaction.followup.send(embed=embed, ephemeral=True) @@ -166,12 +186,13 @@ class VoiceChannelCommands(commands.Cog): """Create a private voice channel for team matchup.""" await interaction.response.defer() - # Verify user has a team - user_team = await self._get_user_team(interaction.user.id) + # Verify user has a Major League team + user_team = await self._get_user_major_league_team(interaction.user.id) if not user_team: embed = EmbedTemplate.error( - title="No Team Found", - description="❌ You must be on a team to create voice channels.\n\n" + title="No Major League Team Found", + description="❌ You must own a Major League team to create private voice channels.\n\n" + "Private channels are for scheduled games between Major League teams.\n" "Contact a league administrator if you believe this is an error." ) await interaction.followup.send(embed=embed, ephemeral=True) diff --git a/tests/test_commands_voice.py b/tests/test_commands_voice.py index 671063d..0196ef1 100644 --- a/tests/test_commands_voice.py +++ b/tests/test_commands_voice.py @@ -264,7 +264,7 @@ class TestVoiceChannelCleanupService: # Mock guild and channel mock_guild = MagicMock() mock_guild.id = 999 - mock_channel = AsyncMock() + mock_channel = AsyncMock(spec=discord.VoiceChannel) mock_channel.id = 123 mock_channel.members = [] # Empty channel @@ -357,6 +357,9 @@ class TestVoiceChannelCommands: mock_team.id = 1 mock_team.abbrev = "NYY" mock_team.lname = "New York Yankees" + # Mock roster_type method to return MAJOR_LEAGUE for NYY + from models.team import RosterType + mock_team.roster_type.return_value = RosterType.MAJOR_LEAGUE # Mock voice category mock_category = MagicMock() @@ -372,7 +375,7 @@ class TestVoiceChannelCommands: with patch.object(mock_interaction.guild, 'create_voice_channel', return_value=mock_channel) as mock_create: with patch('commands.voice.channels.random_codename', return_value="Phoenix"): with patch('discord.utils.get', return_value=mock_category): - mock_team_service.get_teams_by_owner.return_value = [mock_team] + mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team]) await voice_cog.create_public_channel.callback(voice_cog, mock_interaction) @@ -390,13 +393,13 @@ class TestVoiceChannelCommands: call_args = mock_interaction.followup.send.call_args assert 'embed' in call_args.kwargs embed = call_args.kwargs['embed'] - assert "Created public voice channel" in embed.title + assert "Voice Channel Created" in embed.title @pytest.mark.asyncio async def test_create_public_channel_no_team(self, voice_cog, mock_interaction): """Test public channel creation with no team.""" with patch('commands.voice.channels.team_service') as mock_team_service: - mock_team_service.get_teams_by_owner.return_value = [] + mock_team_service.get_teams_by_owner = AsyncMock(return_value=[]) await voice_cog.create_public_channel.callback(voice_cog, mock_interaction) @@ -408,7 +411,7 @@ class TestVoiceChannelCommands: call_args = mock_interaction.followup.send.call_args assert call_args.kwargs['ephemeral'] is True embed = call_args.kwargs['embed'] - assert "No Team Found" in embed.title + assert "No Major League Team Found" in embed.title @pytest.mark.asyncio async def test_create_private_channel_success(self, voice_cog, mock_interaction): @@ -419,6 +422,9 @@ class TestVoiceChannelCommands: mock_user_team.abbrev = "NYY" mock_user_team.lname = "New York Yankees" mock_user_team.sname = "Yankees" + # Mock roster_type method to return MAJOR_LEAGUE for NYY + from models.team import RosterType + mock_user_team.roster_type.return_value = RosterType.MAJOR_LEAGUE # Mock opponent team mock_opponent_team = MagicMock(spec=Team) @@ -456,8 +462,8 @@ class TestVoiceChannelCommands: with patch.object(mock_interaction.guild, 'create_voice_channel', return_value=mock_channel) as mock_create: with patch('discord.utils.get') as mock_utils_get: - mock_team_service.get_teams_by_owner.return_value = [mock_user_team] - mock_league_service.get_current.return_value = mock_current + mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_user_team]) + mock_league_service.get_current_state = AsyncMock(return_value=mock_current) mock_schedule.return_value = [mock_game] # Mock discord.utils.get calls