major-domo-v2/commands/voice/CLAUDE.md
Cal Corum 32cb5da632 CLAUDE: Refactor voice cleanup service to use @tasks.loop pattern
Problem:
- Voice cleanup service used manual while loop instead of @tasks.loop
- Did not wait for bot readiness before starting
- Startup verification could miss stale entries
- Manual channel deletions did not unpublish associated scorecards

Changes:
- Refactored VoiceChannelCleanupService to use @tasks.loop(minutes=1)
- Added @before_loop decorator with await bot.wait_until_ready()
- Updated bot.py to use setup_voice_cleanup() pattern
- Fixed scorecard unpublishing for manually deleted channels
- Fixed scorecard unpublishing for wrong channel type scenarios
- Updated cleanup interval from 60 seconds to 1 minute (same behavior)
- Changed cleanup reason message from "15+ minutes" to "5+ minutes" (matches actual threshold)

Benefits:
- Safe startup: cleanup waits for bot to be fully ready
- Reliable stale cleanup: startup verification guaranteed to run
- Complete cleanup: scorecards unpublished in all scenarios
- Consistent pattern: follows same pattern as other background tasks
- Better error handling: integrated with discord.py task lifecycle

Testing:
- All 19 voice command tests passing
- Updated test fixtures to handle new task pattern
- Fixed test assertions for new cleanup reason message

Documentation:
- Updated commands/voice/CLAUDE.md with new architecture
- Documented all four cleanup scenarios for scorecards
- Added task lifecycle information
- Updated configuration section

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 00:05:35 -05:00

307 lines
12 KiB
Markdown

# Voice Channel Commands
This directory contains Discord slash commands for creating and managing voice channels for gameplay.
## Files
### `channels.py`
- **Commands**:
- `/voice-channel public` - Create a public voice channel for gameplay
- `/voice-channel private` - Create a private team vs team voice channel
- **Description**: Main command implementation with VoiceChannelCommands cog
- **Service Dependencies**:
- `team_service.get_teams_by_owner()` - Verify user has a team
- `league_service.get_current_state()` - Get current season/week info
- `schedule_service.get_team_schedule()` - Find opponent for private channels
- **Deprecated Commands**:
- `!vc`, `!voice`, `!gameplay` → Shows migration message to `/voice-channel public`
- `!private` → Shows migration message to `/voice-channel private`
### `cleanup_service.py`
- **Class**: `VoiceChannelCleanupService`
- **Description**: Manages automatic cleanup of bot-created voice channels
- **Features**:
- Restart-resilient channel tracking using JSON persistence
- Configurable cleanup intervals and empty thresholds
- Background monitoring loop with error recovery
- Startup verification to clean stale tracking entries
### `tracker.py`
- **Class**: `VoiceChannelTracker`
- **Description**: JSON-based persistent tracking of voice channels
- **Features**:
- Channel creation and status tracking
- Empty duration monitoring with datetime handling
- Cleanup candidate identification
- Automatic stale entry removal
### `__init__.py`
- **Function**: `setup_voice(bot)`
- **Description**: Package initialization with resilient cog loading
- **Integration**: Follows established bot architecture patterns
## Key Features
### 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 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`)
- **Permissions**:
- Team members can connect and speak (using `team.lname` Discord roles)
- Everyone else can connect but only listen
- **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 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`)
### Automatic Cleanup System
- **Monitoring Interval**: 1 minute (using `@tasks.loop` pattern)
- **Empty Threshold**: 5 minutes empty before deletion
- **Restart Resilience**: JSON file persistence survives bot restarts
- **Safe Startup**: Uses `@before_loop` to wait for bot readiness before starting
- **Startup Verification**: Validates tracked channels still exist and cleans stale entries on bot startup
- **Manual Deletion Handling**: Detects manually deleted channels and cleans up tracking
- **Graceful Error Handling**: Continues operation even if individual operations fail
- **Scorecard Cleanup**: Automatically unpublishes scorecards in all cleanup scenarios:
- Normal cleanup (channel empty for 5+ minutes)
- Manual deletion (user deletes channel)
- Startup verification (stale entries on bot restart)
- Wrong channel type (corrupted tracking data)
## Architecture
### Command Flow
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
overwrites = {
guild.default_role: discord.PermissionOverwrite(speak=True, connect=True)
}
# Private channels - team roles only can speak
overwrites = {
guild.default_role: discord.PermissionOverwrite(speak=False, connect=True),
user_team_role: discord.PermissionOverwrite(speak=True, connect=True),
opponent_team_role: discord.PermissionOverwrite(speak=True, connect=True)
}
```
### Cleanup Service Integration
```python
# Bot initialization (bot.py)
from commands.voice.cleanup_service import setup_voice_cleanup
self.voice_cleanup_service = setup_voice_cleanup(self)
# The service uses @tasks.loop pattern with @before_loop
# It automatically waits for bot readiness before starting
# Channel tracking
if hasattr(self.bot, 'voice_cleanup_service'):
cleanup_service = self.bot.voice_cleanup_service
cleanup_service.tracker.add_channel(channel, channel_type, interaction.user.id)
```
**Task Lifecycle**:
- **Initialization**: `VoiceChannelCleanupService(bot)` creates instance
- **Startup Wait**: `@before_loop` ensures bot is ready before first cycle
- **Verification**: First cycle runs `verify_tracked_channels()` to clean stale entries
- **Monitoring**: Runs every 1 minute checking all tracked channels
- **Shutdown**: `cog_unload()` cancels the cleanup loop gracefully
### Scorecard Cleanup Integration
The cleanup service automatically unpublishes any scorecard associated with a voice channel when that channel is removed from tracking. This prevents the live scorebug tracker from continuing to update scores for games that no longer have active voice channels.
**Cleanup Scenarios**:
1. **Normal Cleanup** (channel empty for 5+ minutes):
- Cleanup service deletes the voice channel
- Unpublishes associated scorecard
- Logs: `"📋 Unpublished scorecard from text channel [id] (voice channel cleanup)"`
2. **Manual Deletion** (user deletes channel):
- Next cleanup cycle detects missing channel
- Removes from tracking and unpublishes scorecard
- Logs: `"📋 Unpublished scorecard from text channel [id] (manually deleted voice channel)"`
3. **Startup Verification** (stale entries on bot restart):
- Bot startup detects channels that no longer exist
- Cleans up tracking and unpublishes scorecards
- Logs: `"📋 Unpublished scorecard from text channel [id] (stale voice channel)"`
4. **Wrong Channel Type** (corrupted tracking data):
- Tracked channel exists but is not a voice channel
- Removes from tracking and unpublishes scorecard
- Logs: `"📋 Unpublished scorecard from text channel [id] (wrong channel type)"`
**Integration Points**:
- `cleanup_service.py` imports `ScorecardTracker` from `commands.gameplay.scorecard_tracker`
- All cleanup paths check for `text_channel_id` and unpublish if found
- Recovery time: Maximum 1 minute delay for manual deletions
**Example Logging**:
```
✅ Cleaned up empty voice channel: Gameplay Phoenix (ID: 123456789)
📋 Unpublished scorecard from text channel 987654321 (voice channel cleanup)
```
### JSON Data Structure
```json
{
"voice_channels": {
"123456789": {
"channel_id": "123456789",
"guild_id": "987654321",
"name": "Gameplay Phoenix",
"type": "public",
"created_at": "2025-01-15T10:30:00",
"last_checked": "2025-01-15T10:35:00",
"empty_since": "2025-01-15T10:32:00",
"creator_id": "111222333",
"text_channel_id": "555666777"
}
}
}
```
**Note**: The `text_channel_id` field links the voice channel to its associated text channel, enabling automatic scorecard cleanup when the voice channel is deleted.
## Configuration
### Cleanup Service Settings
- **Monitoring Loop**: `@tasks.loop(minutes=1)` - runs every 1 minute
- **`empty_threshold`**: 5 minutes empty before deletion
- **`data_file`**: JSON persistence file path (default: "data/voice_channels.json")
- **Task Pattern**: Uses discord.py `@tasks.loop` with `@before_loop` for safe startup
### Channel Categories
- Channels are created in the "Voice Channels" category if it exists
- Falls back to no category if "Voice Channels" category not found
### Random Codenames
```python
CODENAMES = [
"Phoenix", "Thunder", "Lightning", "Storm", "Blaze", "Frost", "Shadow", "Nova",
"Viper", "Falcon", "Wolf", "Eagle", "Tiger", "Shark", "Bear", "Dragon",
"Alpha", "Beta", "Gamma", "Delta", "Echo", "Foxtrot", "Golf", "Hotel",
"Crimson", "Azure", "Emerald", "Golden", "Silver", "Bronze", "Platinum", "Diamond"
]
```
## Error Handling
### Common Scenarios
- **No Team Found**: User-friendly message directing to contact league administrator
- **No Upcoming Games**: Informative message about being between series
- **Missing Discord Roles**: Warning in embed about teams without speaking permissions
- **Permission Errors**: Clear message to contact server administrator
- **League Info Unavailable**: Graceful fallback with retry suggestion
### Service Dependencies
- **Graceful Degradation**: Voice channels work without cleanup service
- **API Failures**: Comprehensive error handling for external service calls
- **Discord Errors**: Specific handling for Forbidden, NotFound, etc.
## Testing Coverage
### Test Files
- **`tests/test_commands_voice.py`**: Comprehensive test suite covering:
- VoiceChannelTracker JSON persistence and datetime handling
- VoiceChannelCleanupService restart resilience and monitoring
- VoiceChannelCommands slash command functionality
- Error scenarios and edge cases
- Deprecated command migration messages
### Mock Objects
- Discord guild, channels, roles, and interactions
- Team service responses and player data
- Schedule service responses and game data
- League service current state information
## Integration Points
### Bot Integration
- **Package Loading**: Integrated into `bot.py` command package loading sequence
- **Background Tasks**: Cleanup service started in `_setup_background_tasks()`
- **Shutdown Handling**: Cleanup service stopped in `bot.close()`
### Service Layer
- **Team Service**: User team verification and ownership lookup
- **League Service**: Current season/week information retrieval
- **Schedule Service**: Team schedule and opponent detection
### Discord Integration
- **Application Commands**: Modern slash command interface with command groups
- **Permission Overwrites**: Fine-grained voice channel permission control
- **Embed Templates**: Consistent styling using established embed patterns
- **Error Handling**: Integration with global application command error handler
## Usage Examples
### Creating Public Channel
```
/voice-channel public
```
**Result**: Creates "Gameplay [Codename]" with public speaking permissions
### Creating Private Channel
```
/voice-channel private
```
**Result**: Creates "[Away] vs [Home]" with team-only speaking permissions
### Migration from Old Commands
```
!vc
```
**Result**: Shows deprecation message suggesting `/voice-channel public`
## Future Enhancements
### Potential Features
- **Channel Limits**: Per-user or per-team channel creation limits
- **Custom Names**: Allow users to specify custom channel names
- **Extended Permissions**: More granular permission control options
- **Channel Templates**: Predefined setups for different game types
- **Integration Webhooks**: Notifications when channels are created/deleted
### Configuration Options
- **Environment Variables**: Make cleanup intervals configurable via env vars
- **Per-Guild Settings**: Different settings for different Discord servers
- **Role Mapping**: Custom role name patterns for team permissions
---
**Last Updated**: January 2025
**Architecture**: Modern async Discord.py with JSON persistence
**Dependencies**: discord.py, team_service, league_service, schedule_service