From d491a17ddd1d5ed456b1ab03b5e0c43bc66b7b6d Mon Sep 17 00:00:00 2001 From: Eaven Kimura Date: Mon, 22 Sep 2025 18:13:44 +0000 Subject: [PATCH] Improve UX functionality for server listing --- pterodisbot.py | 262 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 198 insertions(+), 64 deletions(-) diff --git a/pterodisbot.py b/pterodisbot.py index 63201da..fa71274 100644 --- a/pterodisbot.py +++ b/pterodisbot.py @@ -955,79 +955,213 @@ async def check_allowed_guild(interaction: discord.Interaction) -> bool: return False return True -@bot.tree.command(name="server_status", description="Show the status of a game server") -@app_commands.describe( - server_name="The name of the server to check", - channel="The channel to post the status in (defaults to current channel)" -) -async def server_status( - interaction: discord.Interaction, - server_name: str, - channel: Optional[discord.TextChannel] = None -): +@bot.tree.command(name="server_status", description="Get a list of available game servers to control") +async def server_status(interaction: discord.Interaction): """ - Slash command to display a server's status embed. - Creates or updates the status message for the specified server. + Slash command to display server status dashboard with interactive dropdown selection. + + This command provides a comprehensive server management interface by: + 1. Fetching current server list from Pterodactyl panel + 2. Generating real-time statistics (online/offline counts) + 3. Displaying an informational embed with server statistics + 4. Presenting an ephemeral dropdown menu with all available servers + 5. Handling server selection to create permanent status embeds in the channel + + Workflow: + - Validates guild permissions and defers ephemeral response + - Refreshes server cache from Pterodactyl API + - Calculates online/offline statistics by checking each server's state + - Creates statistics embed with visual server status breakdown + - Generates dropdown menu with all servers (name + description) + - Handles user selection via ephemeral dropdown interaction + - Creates permanent status embed in channel upon selection + - Manages embed tracking and cleanup of previous embeds + + Ephemeral Design: + - Initial dashboard and dropdown are ephemeral (visible only to user) + - Automatically disappears after use or timeout (3 minutes) + - No manual cleanup required for dropdown interface + - Only final server status embed is posted publicly + + Error Handling: + - Handles API failures during server enumeration + - Manages missing servers between selection and execution + - Provides user-friendly error messages for all failure scenarios + - Maintains comprehensive logging for troubleshooting + + Args: + interaction: Discord interaction object representing the command invocation + + Returns: + None: Sends ephemeral dashboard with dropdown, then public status embed on selection """ # Check if interaction is from allowed guild if not await check_allowed_guild(interaction): return - logger.info(f"Server status command invoked by {interaction.user.name} for '{server_name}'") - await interaction.response.defer() + logger.info(f"Server status command invoked by {interaction.user.name}") + await interaction.response.defer(ephemeral=True) - # Use specified channel or current channel - target_channel = channel or interaction.channel - logger.debug(f"Target channel: {target_channel.name} ({target_channel.id})") - - # Find the server by name in cache - server_data = None - server_id = None - - logger.debug(f"Searching cache for server '{server_name}'") - for s_id, s_data in bot.server_cache.items(): - if s_data['attributes']['name'].lower() == server_name.lower(): - server_data = s_data - server_id = s_id - break - - if not server_data: - logger.warning(f"Server '{server_name}' not found in cache") - await interaction.followup.send(f"Server '{server_name}' not found.", ephemeral=True) - return + try: + # Refresh server cache with current data from Pterodactyl panel + servers = await bot.pterodactyl_api.get_servers() + if not servers: + logger.warning("No servers found in Pterodactyl panel") + await interaction.followup.send("No servers found in the Pterodactyl panel.", ephemeral=True) + return - logger.info(f"Found server {server_name} ({server_id}), fetching resources") - - # Get current server status - resources = await bot.pterodactyl_api.get_server_resources(server_id) - - # Delete old embed if it exists - if server_id in bot.embed_locations: - logger.debug(f"Found existing embed for {server_id}, attempting to delete") - try: - old_location = bot.embed_locations[server_id] - old_channel = bot.get_channel(int(old_location['channel_id'])) - if old_channel: + bot.server_cache = {server['attributes']['identifier']: server for server in servers} + logger.debug(f"Refreshed server cache with {len(servers)} servers") + + # Count online/offline servers by checking each server's current state + online_count = 0 + offline_count = 0 + + # Check status for each server to generate accurate statistics + for server_id, server_data in bot.server_cache.items(): + resources = await bot.pterodactyl_api.get_server_resources(server_id) + current_state = resources.get('attributes', {}).get('current_state', 'offline') + + if current_state == 'running': + online_count += 1 + else: + offline_count += 1 + + # Create statistics embed with visual server status breakdown + stats_embed = discord.Embed( + title="đŸ—ī¸ Server Status Dashboard", + description="Select a server from the dropdown below to view its detailed status and controls.", + color=discord.Color.blue(), + timestamp=datetime.now() + ) + + stats_embed.add_field( + name="📊 Server Statistics", + value=f"**Total Servers:** {len(servers)}\n" + f"✅ **Online:** {online_count}\n" + f"❌ **Offline:** {offline_count}", + inline=False + ) + + stats_embed.add_field( + name="â„šī¸ How to Use", + value="Use the dropdown menu below to select a server. The server's status embed will be posted in this channel.", + inline=False + ) + + stats_embed.set_footer(text="Server status will update automatically") + + # Create dropdown menu options from available servers + server_options = [] + for server_id, server_data in bot.server_cache.items(): + server_name = server_data['attributes']['name'] + server_description = server_data['attributes'].get('description', 'No description') + + # Truncate description if too long for dropdown constraints + if len(server_description) > 50: + server_description = server_description[:47] + "..." + + server_options.append( + discord.SelectOption( + label=server_name, + value=server_id, + description=server_description + ) + ) + + # Create dropdown view with timeout for automatic cleanup + class ServerDropdownView(discord.ui.View): + def __init__(self, server_options, timeout=180): # 3 minute timeout for ephemeral cleanup + super().__init__(timeout=timeout) + self.server_options = server_options + self.add_item(ServerDropdown(server_options)) + + async def on_timeout(self): + # Clean up when dropdown times out (ephemeral auto-removal) + logger.debug("Server dropdown timed out and was automatically cleaned up") + + # Dropdown selection handler for server choice + class ServerDropdown(discord.ui.Select): + def __init__(self, server_options): + super().__init__( + placeholder="Select a server to display...", + options=server_options, + min_values=1, + max_values=1 + ) + + async def callback(self, interaction: discord.Interaction): + """ + Handle server selection from dropdown menu. + Creates permanent status embed in the channel for the selected server. + """ + await interaction.response.defer(ephemeral=True) + + selected_server_id = self.values[0] + server_data = bot.server_cache.get(selected_server_id) + + if not server_data: + await interaction.followup.send( + "❌ Selected server no longer available. Please try again.", + ephemeral=True + ) + return + + server_name = server_data['attributes']['name'] + logger.info(f"User {interaction.user.name} selected server: {server_name}") + try: - old_message = await old_channel.fetch_message(int(old_location['message_id'])) - await old_message.delete() - logger.debug(f"Deleted old embed for {server_id}") - except discord.NotFound: - logger.debug(f"Old embed for {server_id} already deleted") - except Exception as e: - logger.error(f"Failed to delete old embed: {str(e)}") - - # Create and send new embed - logger.info(f"Creating new embed for {server_name} in {target_channel.name}") - embed, view = await bot.get_server_status_embed(server_data, resources) - message = await target_channel.send(embed=embed, view=view) - await bot.track_new_embed(server_id, message) - - await interaction.followup.send( - f"Server status posted in {target_channel.mention}", - ephemeral=True - ) - logger.info(f"Successfully posted status for {server_name}") + # Get current server status for embed creation + resources = await bot.pterodactyl_api.get_server_resources(selected_server_id) + + # Delete old embed if it exists to prevent duplication + if selected_server_id in bot.embed_locations: + logger.debug(f"Found existing embed for {selected_server_id}, attempting to delete") + try: + old_location = bot.embed_locations[selected_server_id] + old_channel = bot.get_channel(int(old_location['channel_id'])) + if old_channel: + try: + old_message = await old_channel.fetch_message(int(old_location['message_id'])) + await old_message.delete() + logger.debug(f"Deleted old embed for {selected_server_id}") + except discord.NotFound: + logger.debug(f"Old embed for {selected_server_id} already deleted") + except Exception as e: + logger.error(f"Failed to delete old embed: {str(e)}") + + # Create and send new permanent status embed in channel + embed, view = await bot.get_server_status_embed(server_data, resources) + message = await interaction.channel.send(embed=embed, view=view) + await bot.track_new_embed(selected_server_id, message) + + await interaction.followup.send( + f"✅ **{server_name}** status has been posted in {interaction.channel.mention}", + ephemeral=True + ) + logger.info(f"Successfully posted status for {server_name}") + + except Exception as e: + logger.error(f"Failed to create status embed for {server_name}: {str(e)}") + await interaction.followup.send( + f"❌ Failed to create status embed for **{server_name}**: {str(e)}", + ephemeral=True + ) + + # Send the initial dashboard embed with dropdown (ephemeral - auto-cleaned) + await interaction.followup.send( + embed=stats_embed, + view=ServerDropdownView(server_options), + ephemeral=True + ) + logger.info(f"Sent server status dashboard to {interaction.user.name} with {len(server_options)} servers") + + except Exception as e: + logger.error(f"Server status command failed: {str(e)}") + await interaction.followup.send( + f"❌ Failed to load server status: {str(e)}", + ephemeral=True + ) @bot.tree.command(name="refresh_embeds", description="Refresh all server status embeds (admin only)") async def refresh_embeds(interaction: discord.Interaction):