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>
12 KiB
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 teamleague_service.get_current_state()- Get current season/week infoschedule_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.lnameDiscord roles) - Everyone else can connect but only listen
- Team members can connect and speak (using
- 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.looppattern) - Empty Threshold: 5 minutes empty before deletion
- Restart Resilience: JSON file persistence survives bot restarts
- Safe Startup: Uses
@before_loopto 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
- Major League Team Verification: Check user owns a Major League team using
team_service - Channel Creation: Create voice channel with appropriate permissions
- Tracking Registration: Add channel to cleanup service tracking
- User Feedback: Send success embed with channel details
Team Validation Logic
The voice channel system validates that users own Major League teams specifically:
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
# 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
# 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_loopensures 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:
-
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)"
-
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)"
-
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)"
-
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.pyimportsScorecardTrackerfromcommands.gameplay.scorecard_tracker- All cleanup paths check for
text_channel_idand 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
{
"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 deletiondata_file: JSON persistence file path (default: "data/voice_channels.json")- Task Pattern: Uses discord.py
@tasks.loopwith@before_loopfor 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
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.pycommand 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