Improve UX functionality for server listing
All checks were successful
Docker Build and Push (Multi-architecture) / build-and-push (push) Successful in 21s

This commit is contained in:
2025-09-22 18:13:44 +00:00
parent 01f6f2b201
commit d491a17ddd

View File

@@ -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):