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", "DictionaryCog": "📖 Dictionary", "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))