Improve UX functionality for server listing
All checks were successful
Docker Build and Push (Multi-architecture) / build-and-push (push) Successful in 21s
All checks were successful
Docker Build and Push (Multi-architecture) / build-and-push (push) Successful in 21s
This commit is contained in:
224
pterodisbot.py
224
pterodisbot.py
@@ -955,80 +955,214 @@ 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)
|
||||
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")
|
||||
bot.server_cache = {server['attributes']['identifier']: server for server in servers}
|
||||
logger.debug(f"Refreshed server cache with {len(servers)} servers")
|
||||
|
||||
# Get current server status
|
||||
# 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}")
|
||||
|
||||
# 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]
|
||||
# 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 {server_id}")
|
||||
logger.debug(f"Deleted old embed for {selected_server_id}")
|
||||
except discord.NotFound:
|
||||
logger.debug(f"Old embed for {server_id} already deleted")
|
||||
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 embed
|
||||
logger.info(f"Creating new embed for {server_name} in {target_channel.name}")
|
||||
# Create and send new permanent status embed in channel
|
||||
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)
|
||||
message = await interaction.channel.send(embed=embed, view=view)
|
||||
await bot.track_new_embed(selected_server_id, message)
|
||||
|
||||
await interaction.followup.send(
|
||||
f"Server status posted in {target_channel.mention}",
|
||||
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):
|
||||
"""Slash command to refresh all server embeds."""
|
||||
|
||||
Reference in New Issue
Block a user