From 9c59413cfa0887ef9b2667f747fb7f2b80b09921 Mon Sep 17 00:00:00 2001 From: latte Date: Sun, 25 Jan 2026 17:15:17 +0100 Subject: [PATCH] added help --- src/guardden/bot.py | 3 +- src/guardden/cogs/help.py | 292 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 src/guardden/cogs/help.py diff --git a/src/guardden/bot.py b/src/guardden/bot.py index ed13f2a..57e6b67 100644 --- a/src/guardden/bot.py +++ b/src/guardden/bot.py @@ -35,7 +35,7 @@ class GuardDen(commands.Bot): super().__init__( command_prefix=self._get_prefix, intents=intents, - help_command=commands.DefaultHelpCommand(), + help_command=None, # Set by help cog ) # Services @@ -120,6 +120,7 @@ class GuardDen(commands.Bot): "guardden.cogs.verification", "guardden.cogs.health", "guardden.cogs.wordlist_sync", + "guardden.cogs.help", ] failed_cogs = [] diff --git a/src/guardden/cogs/help.py b/src/guardden/cogs/help.py new file mode 100644 index 0000000..9841490 --- /dev/null +++ b/src/guardden/cogs/help.py @@ -0,0 +1,292 @@ +"""Custom help command for GuardDen.""" + +import logging + +import discord +from discord.ext import commands + +from guardden.bot import GuardDen + +logger = logging.getLogger(__name__) + + +class GuardDenHelpCommand(commands.HelpCommand): + """Custom help command with embed formatting and permission filtering.""" + + # Friendly category names with emojis + CATEGORY_NAMES = { + "Moderation": "🛡️ Moderation", + "Admin": "⚙️ Server Configuration", + "Automod": "🤖 Automatic Moderation", + "AiModeration": "🧠 AI Moderation", + "Verification": "✅ Member Verification", + "Health": "💊 System Health", + "WordlistSync": "📝 Wordlist Sync", + } + + # Category descriptions + CATEGORY_DESCRIPTIONS = { + "Moderation": "Server moderation tools", + "Admin": "Bot settings and configuration", + "Automod": "Automatic content filtering rules", + "AiModeration": "AI-powered content moderation", + "Verification": "New member verification system", + "Health": "System diagnostics", + "WordlistSync": "Wordlist synchronization", + } + + def get_command_signature(self, command: commands.Command) -> str: + """Get the command signature showing usage.""" + parent = command.full_parent_name + alias = command.name if not parent else f"{parent} {command.name}" + return f"{self.context.clean_prefix}{alias} {command.signature}" + + def get_cog_display_name(self, cog_name: str) -> str: + """Get user-friendly display name for a cog.""" + return self.CATEGORY_NAMES.get(cog_name, cog_name) + + def get_cog_description(self, cog_name: str) -> str: + """Get description for a cog.""" + return self.CATEGORY_DESCRIPTIONS.get(cog_name, "Commands") + + async def send_bot_help(self, mapping: dict) -> None: + """Send the main help menu showing all categories.""" + embed = discord.Embed( + title="GuardDen Help Menu", + description="A comprehensive Discord moderation bot", + color=discord.Color.blue(), + ) + + # Filter and display categories + for cog, cog_commands in mapping.items(): + if cog is None: + continue + + # Filter commands the user can actually run + filtered = await self.filter_commands(cog_commands, sort=True) + if not filtered: + continue + + cog_name = cog.qualified_name + display_name = self.get_cog_display_name(cog_name) + description = self.get_cog_description(cog_name) + + embed.add_field( + name=display_name, + value=description, + inline=False, + ) + + # Add usage instructions + prefix = self.context.clean_prefix + embed.add_field( + name="Usage", + value=f"Use `{prefix}help ` for category commands\n" + f"Use `{prefix}help ` for detailed help", + inline=False, + ) + + embed.set_footer(text=f"Prefix: {prefix} (customizable per server)") + + channel = self.get_destination() + await channel.send(embed=embed) + + async def send_cog_help(self, cog: commands.Cog) -> None: + """Send help for a specific category/cog.""" + # Filter commands the user can run + filtered = await self.filter_commands(cog.get_commands(), sort=True) + + if not filtered: + await self.get_destination().send( + f"No commands available in this category or you lack permissions to use them." + ) + return + + cog_name = cog.qualified_name + display_name = self.get_cog_display_name(cog_name) + + embed = discord.Embed( + title=f"{display_name} Commands", + description=cog.description or "Commands in this category", + color=discord.Color.gold() if "Admin" in display_name else discord.Color.blue(), + ) + + # Group commands by type (regular vs groups) + for command in filtered: + # Get command signature + signature = self.get_command_signature(command) + + # Build description + desc_parts = [] + if command.help: + desc_parts.append(command.help.split("\n")[0]) # First line only + if command.aliases: + desc_parts.append(f"*Aliases: {', '.join(command.aliases)}*") + + description = "\n".join(desc_parts) if desc_parts else "No description available" + + embed.add_field( + name=signature, + value=description, + inline=False, + ) + + # Add permission requirements if applicable + if hasattr(cog, "cog_check"): + # Try to determine permissions + footer_parts = [] + + # Check common permission patterns + if "Admin" in display_name or "Config" in display_name: + footer_parts.append("Requires: Administrator permission") + elif "Moderation" in display_name: + footer_parts.append("Requires: Kick Members or Ban Members permission") + + if footer_parts: + embed.set_footer(text=" | ".join(footer_parts)) + + channel = self.get_destination() + await channel.send(embed=embed) + + async def send_group_help(self, group: commands.Group) -> None: + """Send help for a command group.""" + embed = discord.Embed( + title=f"Command Group: {group.qualified_name}", + description=group.help or "No description available", + color=discord.Color.blurple(), + ) + + # Add usage + signature = self.get_command_signature(group) + embed.add_field( + name="Usage", + value=f"`{signature}`", + inline=False, + ) + + # List subcommands + filtered = await self.filter_commands(group.commands, sort=True) + if filtered: + subcommands_text = [] + for command in filtered: + sig = f"{self.context.clean_prefix}{command.qualified_name} {command.signature}" + desc = command.help.split("\n")[0] if command.help else "No description" + subcommands_text.append(f"`{sig}`\n{desc}") + + embed.add_field( + name="Subcommands", + value="\n\n".join(subcommands_text[:10]), # Limit to 10 to avoid embed size limits + inline=False, + ) + + if len(filtered) > 10: + embed.add_field( + name="More...", + value=f"And {len(filtered) - 10} more subcommands", + inline=False, + ) + + # Add aliases + if group.aliases: + embed.add_field( + name="Aliases", + value=", ".join(f"`{alias}`" for alias in group.aliases), + inline=False, + ) + + channel = self.get_destination() + await channel.send(embed=embed) + + async def send_command_help(self, command: commands.Command) -> None: + """Send help for a specific command.""" + embed = discord.Embed( + title=f"Command: {command.qualified_name}", + description=command.help or "No description available", + color=discord.Color.green(), + ) + + # Add usage + signature = self.get_command_signature(command) + embed.add_field( + name="Usage", + value=f"`{signature}`", + inline=False, + ) + + # Add aliases + if command.aliases: + embed.add_field( + name="Aliases", + value=", ".join(f"`{alias}`" for alias in command.aliases), + inline=False, + ) + + # Add parameter details if available + if command.clean_params: + params_text = [] + for param_name, param in command.clean_params.items(): + # Determine if required or optional + if param.default is param.empty: + params_text.append(f"`{param_name}` - Required parameter") + else: + default_val = param.default if param.default is not None else "None" + params_text.append(f"`{param_name}` - Optional (default: {default_val})") + + if params_text: + embed.add_field( + name="Parameters", + value="\n".join(params_text), + inline=False, + ) + + # Add permission requirements + footer_parts = [] + if hasattr(command.callback, "__commands_checks__"): + # Try to extract permission requirements + checks = command.callback.__commands_checks__ + for check in checks: + check_name = getattr(check, "__name__", "") + if "has_permissions" in check_name: + footer_parts.append("Requires specific permissions") + elif "is_owner" in check_name: + footer_parts.append("Requires bot owner") + elif "guild_only" in check_name: + footer_parts.append("Guild only (no DMs)") + + if command.cog: + cog_name = command.cog.qualified_name + footer_parts.append(f"Category: {self.get_cog_display_name(cog_name)}") + + if footer_parts: + embed.set_footer(text=" | ".join(footer_parts)) + + channel = self.get_destination() + await channel.send(embed=embed) + + async def send_error_message(self, error: str) -> None: + """Send an error message.""" + embed = discord.Embed( + title="Help Error", + description=error, + color=discord.Color.red(), + ) + embed.set_footer(text=f"Use {self.context.clean_prefix}help for available commands") + + channel = self.get_destination() + await channel.send(embed=embed) + + async def command_not_found(self, string: str) -> str: + """Handle command not found error.""" + return f"No command or category called `{string}` found." + + async def subcommand_not_found(self, command: commands.Command, string: str) -> str: + """Handle subcommand not found error.""" + if isinstance(command, commands.Group) and len(command.all_commands) > 0: + return f"Command `{command.qualified_name}` has no subcommand named `{string}`." + return f"Command `{command.qualified_name}` has no subcommands." + + +async def setup(bot: GuardDen) -> None: + """Set up the help command.""" + bot.help_command = GuardDenHelpCommand() + logger.info("Custom help command loaded")