discordbot/cogs/help_cog.py
2025-04-25 14:03:49 -06:00

457 lines
21 KiB
Python

import discord
from discord.ext import commands
from discord import app_commands
import asyncio
# Define friendly names for cogs
COG_DISPLAY_NAMES = {
"AICog": "🤖 AI Chat",
"AudioCog": "🎵 Audio Player",
"GamesCog": "🎮 Games",
"HelpCog": "❓ Help",
"MultiConversationCog": "🤖 Multi-Conversation AI Chat",
"MessageCog": "💬 Messages",
"LevelingCog": "⭐ Leveling System",
"MarriageCog": "💍 Marriage System",
"ModerationCog": "🛡️ Moderation",
"PingCog": "🏓 Ping",
"RandomCog": "🎲 Random Image (NSFW)",
"RoleCreatorCog": "✨ Role Management (Owner Only)",
"RoleSelectorCog": "🎭 Role Selection (Owner Only)",
"RoleplayCog": "💋 Roleplay",
"Rule34Cog": "🔞 Rule34 Search (NSFW)",
"ShellCommandCog": "🖥️ Shell Command",
"SystemCheckCog": "📊 System Status",
"WebdriverTorsoCog": "🌐 Webdriver Torso",
"CommandDebugCog": "🐛 Command Debug (Owner Only)",
"CommandFixCog": "🐛 Command Fix (Owner Only)",
"TTSProviderCog": "🗣️ TTS Provider",
"RandomTimeoutCog": "⏰ Random Timeout",
"SyncCog": "🔄 Command Sync (Owner Only)",
# Add other cogs here as needed
}
class HelpSelect(discord.ui.Select):
def __init__(self, view: 'HelpView', start_index=0, max_options=24):
self.help_view = view
# Always include General Overview option
options = [discord.SelectOption(label="General Overview", description="Go back to the main help page.", value="-1")] # Value -1 for overview page
# Calculate end index, ensuring we don't go past the end of the cogs list
end_index = min(start_index + max_options, len(view.cogs))
# Add cog options for this page of the select menu
for i in range(start_index, end_index):
cog = view.cogs[i]
display_name = COG_DISPLAY_NAMES.get(cog.qualified_name, cog.qualified_name)
# Truncate description if too long for Discord API limit (100 chars)
# Use a relative index (i - start_index) as the value to avoid confusion
# when navigating between pages
relative_index = i - start_index
options.append(discord.SelectOption(label=display_name, value=str(relative_index)))
# Store the range of cogs this select menu covers
self.start_index = start_index
self.end_index = end_index
super().__init__(placeholder="Select a category...", min_values=1, max_values=1, options=options)
async def callback(self, interaction: discord.Interaction):
selected_value = int(self.values[0])
if selected_value == -1: # General Overview selected
self.help_view.current_page = 0
else:
# The value is a relative index (0-based) within the current page of options
# We need to convert it to an absolute index in the cogs list
actual_cog_index = selected_value + self.start_index
# Debug information
print(f"Selected value: {selected_value}, start_index: {self.start_index}, actual_cog_index: {actual_cog_index}")
# Make sure the index is valid
if 0 <= actual_cog_index < len(self.help_view.cogs):
self.help_view.current_page = actual_cog_index + 1 # +1 because page 0 is overview
else:
# If the index is invalid, go to the overview page
self.help_view.current_page = 0
await interaction.response.send_message(f"That category is no longer available. Showing overview. (Debug: value={selected_value}, start={self.start_index}, actual={actual_cog_index}, max={len(self.help_view.cogs)})", ephemeral=True)
# Ensure current_page is within valid range
if self.help_view.current_page >= len(self.help_view.pages):
self.help_view.current_page = 0
self.help_view._update_buttons()
self.help_view._update_select_menu()
# Update the placeholder to show the current selection
if self.help_view.current_page == 0:
current_option_label = "General Overview"
else:
cog_index = self.help_view.current_page - 1
if 0 <= cog_index < len(self.help_view.cogs):
current_option_label = COG_DISPLAY_NAMES.get(self.help_view.cogs[cog_index].qualified_name, self.help_view.cogs[cog_index].qualified_name)
else:
current_option_label = "Select a category..."
self.placeholder = current_option_label
try:
await interaction.response.edit_message(embed=self.help_view.pages[self.help_view.current_page], view=self.help_view)
except Exception as e:
# If we can't edit the message, try to defer or send a new message
try:
await interaction.response.defer()
print(f"Error in help command: {e}")
except:
pass
class HelpView(discord.ui.View):
def __init__(self, bot: commands.Bot, timeout=180):
super().__init__(timeout=timeout)
self.bot = bot
self.current_page = 0 # Current page in the embed pages
self.current_select_page = 0 # Current page of the select menu
self.max_select_options = 24 # Maximum number of cog options per select menu (25 total with General Overview)
# Filter cogs and sort them using the display name mapping
self.cogs = sorted(
[cog for _, cog in bot.cogs.items() if cog.get_commands()],
key=lambda cog: COG_DISPLAY_NAMES.get(cog.qualified_name, cog.qualified_name) # Sort alphabetically by display name
)
# Calculate total number of select menu pages needed
self.total_select_pages = (len(self.cogs) + self.max_select_options - 1) // self.max_select_options
# Create pages after total_select_pages is defined
self.pages = self._create_pages()
# Add components in order: Select, Previous/Next Page, Previous/Next Category
self._update_select_menu() # Initialize the select menu with the first page of options
# Buttons are added via decorators later
self._update_buttons() # Initial button state
def _create_overview_page(self):
# Create the overview page (page 0)
embed = discord.Embed(
title="Help Command",
description=f"Use the buttons below to navigate through command categories.\nTotal Categories: {len(self.cogs)}\nUse the Categories buttons to navigate between pages of categories.",
color=discord.Color.blue()
)
# Calculate how many cogs are shown in the current select page
start_index = self.current_select_page * self.max_select_options
end_index = min(start_index + self.max_select_options, len(self.cogs))
current_range = f"{start_index + 1}-{end_index}" if len(self.cogs) > self.max_select_options else f"1-{len(self.cogs)}"
# Add information about which cogs are currently visible
if len(self.cogs) > self.max_select_options:
embed.add_field(
name="Currently Showing",
value=f"Categories {current_range} of {len(self.cogs)}",
inline=False
)
embed.set_footer(text="Page 0 / {} | Category Page {} / {}".format(len(self.cogs), self.current_select_page + 1, self.total_select_pages))
return embed
def _create_pages(self):
pages = []
# Page 0: General overview
pages.append(self._create_overview_page())
# Subsequent pages: One per cog
for i, cog in enumerate(self.cogs):
try:
cog_name = cog.qualified_name
# Get the friendly display name, falling back to the original name
display_name = COG_DISPLAY_NAMES.get(cog_name, cog_name)
cog_commands = cog.get_commands()
embed = discord.Embed(
title=f"{display_name} Commands", # Use the display name here
description=f"Commands available in the {display_name} category:",
color=discord.Color.green() # Or assign colors dynamically
)
for command in cog_commands:
# Skip subcommands for now, just show top-level commands in the cog
if isinstance(command, commands.Group):
# If it's a group, list its subcommands or just the group name
sub_cmds = ", ".join([f"`{sub.name}`" for sub in command.commands])
if sub_cmds:
embed.add_field(name=f"`{command.name}` (Group)", value=f"Subcommands: {sub_cmds}\n{command.short_doc or 'No description'}", inline=False)
else:
embed.add_field(name=f"`{command.name}` (Group)", value=f"{command.short_doc or 'No description'}", inline=False)
elif command.parent is None: # Only show top-level commands
signature = f"{command.name} {command.signature}"
embed.add_field(
name=f"`{signature.strip()}`",
value=command.short_doc or "No description provided.",
inline=False
)
embed.set_footer(text=f"Page {i + 1} / {len(self.cogs)} | Category Page {self.current_select_page + 1} / {self.total_select_pages}")
pages.append(embed)
except Exception as e:
# If there's an error creating a page for a cog, log it and continue
print(f"Error creating help page for cog {i}: {e}")
# Create a simple error page for this cog
error_embed = discord.Embed(
title=f"Error displaying commands",
description=f"There was an error displaying commands for this category.\nPlease try again or contact the bot owner if the issue persists.",
color=discord.Color.red()
)
error_embed.set_footer(text=f"Page {i + 1} / {len(self.cogs)} | Category Page {self.current_select_page + 1} / {self.total_select_pages}")
pages.append(error_embed)
return pages
def _update_select_menu(self):
# Remove existing select menu if it exists
for item in self.children.copy():
if isinstance(item, HelpSelect):
self.remove_item(item)
# Calculate the starting index for this page of the select menu
start_index = self.current_select_page * self.max_select_options
# Check if the currently selected cog is in the current select page range
current_cog_in_view = False
if self.current_page > 0: # If a cog is selected (not overview)
cog_index = self.current_page - 1 # Convert page to cog index
# Check if this cog is in the current select page range
if start_index <= cog_index < start_index + self.max_select_options:
current_cog_in_view = True
# If the current cog is not in view and we're not on the overview page,
# adjust the select page to include the current cog
if not current_cog_in_view and self.current_page > 0:
cog_index = self.current_page - 1
self.current_select_page = cog_index // self.max_select_options
# Recalculate start_index
start_index = self.current_select_page * self.max_select_options
# Create and add the new select menu
self.select_menu = HelpSelect(self, start_index, self.max_select_options)
self.add_item(self.select_menu)
# Update the placeholder to show the current selection
if self.current_page == 0:
current_option_label = "General Overview"
else:
cog_index = self.current_page - 1
if 0 <= cog_index < len(self.cogs):
current_option_label = COG_DISPLAY_NAMES.get(self.cogs[cog_index].qualified_name, self.cogs[cog_index].qualified_name)
else:
current_option_label = "Select a category..."
self.select_menu.placeholder = current_option_label
def _update_buttons(self):
# Find the buttons by their custom_id
prev_page_button = None
next_page_button = None
prev_category_button = None
next_category_button = None
# First check if buttons have been added yet
if len(self.children) <= 1: # Only select menu exists
return # Buttons will be added by decorators later
for item in self.children:
if hasattr(item, 'custom_id'):
if item.custom_id == 'prev_page':
prev_page_button = item
elif item.custom_id == 'next_page':
next_page_button = item
elif item.custom_id == 'prev_category':
prev_category_button = item
elif item.custom_id == 'next_category':
next_category_button = item
# Update page navigation buttons
if prev_page_button:
prev_page_button.disabled = self.current_page == 0
if next_page_button:
next_page_button.disabled = self.current_page == len(self.pages) - 1
# Update category navigation buttons
if prev_category_button:
prev_category_button.disabled = self.current_select_page == 0
if next_category_button:
next_category_button.disabled = self.current_select_page == self.total_select_pages - 1
@discord.ui.button(label="Previous", style=discord.ButtonStyle.grey, row=1, custom_id="prev_page")
async def previous_button(self, interaction: discord.Interaction, _: discord.ui.Button):
if self.current_page > 0:
self.current_page -= 1
self._update_buttons()
self._update_select_menu()
# Ensure current_page is within valid range
if self.current_page >= len(self.pages):
self.current_page = 0
try:
await interaction.response.edit_message(embed=self.pages[self.current_page], view=self)
except Exception as e:
try:
await interaction.response.defer()
print(f"Error in help command previous button: {e}")
except:
pass
else:
await interaction.response.defer()
@discord.ui.button(label="Next", style=discord.ButtonStyle.grey, row=1, custom_id="next_page")
async def next_button(self, interaction: discord.Interaction, _: discord.ui.Button):
if self.current_page < len(self.pages) - 1:
self.current_page += 1
self._update_buttons()
self._update_select_menu()
# Ensure current_page is within valid range
if self.current_page >= len(self.pages):
self.current_page = 0
try:
await interaction.response.edit_message(embed=self.pages[self.current_page], view=self)
except Exception as e:
try:
await interaction.response.defer()
print(f"Error in help command next button: {e}")
except:
pass
else:
await interaction.response.defer()
@discord.ui.button(label="◀ Categories", style=discord.ButtonStyle.primary, row=2, custom_id="prev_category")
async def prev_category_button(self, interaction: discord.Interaction, _: discord.ui.Button):
if self.current_select_page > 0:
# Store the current page before updating
old_page = self.current_page
# Update the select page
self.current_select_page -= 1
# If we're on a cog page, check if we need to adjust the current page
if old_page > 0:
cog_index = old_page - 1
start_index = self.current_select_page * self.max_select_options
end_index = min(start_index + self.max_select_options, len(self.cogs))
# If the current cog is no longer in the visible range, go to the overview page
if cog_index < start_index or cog_index >= end_index:
self.current_page = 0
# Update UI elements
self._update_buttons()
self._update_select_menu()
# If on the overview page, recreate it to update the category information
if self.current_page == 0:
# Recreate the overview page with updated category info
self.pages[0] = self._create_overview_page()
# Ensure current_page is within valid range
if self.current_page >= len(self.pages):
self.current_page = 0
try:
await interaction.response.edit_message(embed=self.pages[self.current_page], view=self)
except Exception as e:
try:
await interaction.response.defer()
print(f"Error in help command prev category button: {e}")
except:
pass
else:
await interaction.response.defer()
@discord.ui.button(label="Categories ▶", style=discord.ButtonStyle.primary, row=2, custom_id="next_category")
async def next_category_button(self, interaction: discord.Interaction, _: discord.ui.Button):
if self.current_select_page < self.total_select_pages - 1:
# Store the current page before updating
old_page = self.current_page
# Update the select page
self.current_select_page += 1
# If we're on a cog page, check if we need to adjust the current page
if old_page > 0:
cog_index = old_page - 1
start_index = self.current_select_page * self.max_select_options
end_index = min(start_index + self.max_select_options, len(self.cogs))
# If the current cog is no longer in the visible range, go to the overview page
if cog_index < start_index or cog_index >= end_index:
self.current_page = 0
# Update UI elements
self._update_buttons()
self._update_select_menu()
# If on the overview page, recreate it to update the category information
if self.current_page == 0:
# Recreate the overview page with updated category info
self.pages[0] = self._create_overview_page()
# Ensure current_page is within valid range
if self.current_page >= len(self.pages):
self.current_page = 0
try:
await interaction.response.edit_message(embed=self.pages[self.current_page], view=self)
except Exception as e:
try:
await interaction.response.defer()
print(f"Error in help command next category button: {e}")
except:
pass
else:
await interaction.response.defer()
class HelpCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
# Remove the default help command before adding the custom one
original_help_command = bot.get_command('help')
if original_help_command:
bot.remove_command(original_help_command.name)
@commands.hybrid_command(name="help", description="Shows this help message.")
async def help_command(self, ctx: commands.Context, command_name: str = None):
"""Displays an interactive help message with command categories or details about a specific command."""
try:
if command_name:
command = self.bot.get_command(command_name)
if command:
embed = discord.Embed(
title=f"Help for `{command.name}`",
description=command.help or "No detailed description provided.",
color=discord.Color.blue()
)
embed.add_field(name="Usage", value=f"`{command.name} {command.signature}`", inline=False)
if isinstance(command, commands.Group):
subcommands = "\n".join([f"`{sub.name}`: {sub.short_doc or 'No description'}" for sub in command.commands])
embed.add_field(name="Subcommands", value=subcommands or "None", inline=False)
await ctx.send(embed=embed, ephemeral=True)
else:
await ctx.send(f"Command `{command_name}` not found.", ephemeral=True)
else:
view = HelpView(self.bot)
await ctx.send(embed=view.pages[0], view=view, ephemeral=True) # Send ephemeral so only user sees it
except Exception as e:
# If there's an error, send a simple error message
print(f"Error in help command: {e}")
await ctx.send(f"An error occurred while displaying the help command. Please try again or contact the bot owner if the issue persists.", ephemeral=True)
@commands.Cog.listener()
async def on_ready(self):
print(f'{self.__class__.__name__} cog has been loaded.')
async def setup(bot: commands.Bot):
# Ensure the cog is added only after the bot is ready enough to have cogs attribute
# Or handle potential race conditions if setup is called very early
await bot.add_cog(HelpCog(bot))