diff --git a/redbot/core/bot.py b/redbot/core/bot.py index f122ee5ff..877c54eb7 100644 --- a/redbot/core/bot.py +++ b/redbot/core/bot.py @@ -52,6 +52,10 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): custom_info=None, help__page_char_limit=1000, help__max_pages_in_guild=2, + help__use_menus=False, + help__show_hidden=False, + help__verify_checks=True, + help__verify_exists=False, help__tagline="", disabled_commands=[], disabled_command_msg="That command is disabled.", diff --git a/redbot/core/commands/help.py b/redbot/core/commands/help.py index a447bcab1..f1f590e21 100644 --- a/redbot/core/commands/help.py +++ b/redbot/core/commands/help.py @@ -77,14 +77,6 @@ class RedHelpFormatter: should not need or want a shared state. """ - # Class vars for things which should be configurable at a later date but aren't now - # Technically, someone can just use a cog to switch these in real time for now. - - USE_MENU = False - CONFIRM_UNAVAILABLE_COMMAND_EXISTENCES = False - SHOW_HIDDEN = False - VERIFY_CHECKS = True - async def send_help(self, ctx: Context, help_for: HelpTarget = None): """ This delegates to other functions. @@ -102,7 +94,7 @@ class RedHelpFormatter: await self.command_not_found(ctx, help_for) return except NoSubCommand as exc: - if self.CONFIRM_UNAVAILABLE_COMMAND_EXISTENCES: + if await ctx.bot.db.help.verify_exists(): await self.subcommand_not_found(ctx, exc.last, exc.not_found) return help_for = exc.last @@ -138,7 +130,7 @@ class RedHelpFormatter: async def format_command_help(self, ctx: Context, obj: commands.Command): - send = self.CONFIRM_UNAVAILABLE_COMMAND_EXISTENCES + send = await ctx.bot.db.help.verify_exists() if not send: async for _ in self.help_filter_func(ctx, (obj,), bypass_hidden=True): # This is a really lazy option for not @@ -182,8 +174,14 @@ class RedHelpFormatter: emb["fields"].append(field) if subcommands: + + def shorten_line(a_line: str) -> str: + if len(a_line) < 70: # embed max width needs to be lower + return a_line + return a_line[:67] + "..." + subtext = "\n".join( - f"**{name}** {command.short_doc}" + shorten_line(f"**{name}** {command.short_doc}") for name, command in sorted(subcommands.items()) ) for i, page in enumerate(pagify(subtext, page_length=1000, shorten_by=0)): @@ -272,7 +270,7 @@ class RedHelpFormatter: async def format_cog_help(self, ctx: Context, obj: commands.Cog): coms = await self.get_cog_help_mapping(ctx, obj) - if not (coms or self.CONFIRM_UNAVAILABLE_COMMAND_EXISTENCES): + if not (coms or await ctx.bot.db.help.verify_exists()): return description = obj.help @@ -286,8 +284,15 @@ class RedHelpFormatter: emb["embed"]["title"] = f"*{description[:2044]}*" if coms: + + def shorten_line(a_line: str) -> str: + if len(a_line) < 70: # embed max width needs to be lower + return a_line + return a_line[:67] + "..." + command_text = "\n".join( - f"**{name}** {command.short_doc}" for name, command in sorted(coms.items()) + shorten_line(f"**{name}** {command.short_doc}") + for name, command in sorted(coms.items()) ) for i, page in enumerate(pagify(command_text, page_length=1000, shorten_by=0)): if i == 0: @@ -347,8 +352,14 @@ class RedHelpFormatter: else: title = f"**__No Category:__**" + def shorten_line(a_line: str) -> str: + if len(a_line) < 70: # embed max width needs to be lower + return a_line + return a_line[:67] + "..." + cog_text = "\n".join( - f"**{name}** {command.short_doc}" for name, command in sorted(data.items()) + shorten_line(f"**{name}** {command.short_doc}") + for name, command in sorted(data.items()) ) for i, page in enumerate(pagify(cog_text, page_length=1000, shorten_by=0)): @@ -399,17 +410,25 @@ class RedHelpFormatter: """ This does most of actual filtering. """ + + show_hidden = bypass_hidden or await ctx.bot.db.help.show_hidden() + verify_checks = await ctx.bot.db.help.verify_checks() + # TODO: Settings for this in core bot db for obj in objects: - if self.VERIFY_CHECKS and not (self.SHOW_HIDDEN or bypass_hidden): + if verify_checks and not show_hidden: # Default Red behavior, can_see includes a can_run check. if await obj.can_see(ctx): yield obj - elif self.VERIFY_CHECKS: - if await obj.can_run(ctx): + elif verify_checks: + try: + can_run = await obj.can_run(ctx) + except discord.DiscordException: + can_run = False + if can_run: yield obj - elif not (self.SHOW_HIDDEN or bypass_hidden): - if getattr(obj, "hidden", False): # Cog compatibility + elif not show_hidden: + if not getattr(obj, "hidden", False): # Cog compatibility yield obj else: yield obj @@ -430,8 +449,8 @@ class RedHelpFormatter: await ctx.send(embed=ret) else: await ctx.send(ret) - elif self.CONFIRM_UNAVAILABLE_COMMAND_EXISTENCES: - ret = T_("Command *{command_name}* not found.").format(command_name=help_for) + elif await ctx.bot.db.help.verify_exists(): + ret = T_("Help topic for *{command_name}* not found.").format(command_name=help_for) if use_embeds: ret = discord.Embed(color=(await ctx.embed_color()), description=ret) ret.set_author(name=f"{ctx.me.display_name} Help Menu", icon_url=ctx.me.avatar_url) @@ -445,10 +464,17 @@ class RedHelpFormatter: """ Sends an error """ - ret = T_("Command *{command_name}* has no subcommands.").format( - command_name=command.qualified_name + ret = T_("Command *{command_name}* has no subcommand named *{not_found}*.").format( + command_name=command.qualified_name, not_found=not_found[0] ) - await ctx.send(ret) + if await ctx.embed_requested(): + ret = discord.Embed(color=(await ctx.embed_color()), description=ret) + ret.set_author(name=f"{ctx.me.display_name} Help Menu", icon_url=ctx.me.avatar_url) + tagline = (await ctx.bot.db.help.tagline()) or self.get_default_tagline(ctx) + ret.set_footer(text=tagline) + await ctx.send(embed=ret) + else: + await ctx.send(ret) @staticmethod def parse_command(ctx, help_for: str): @@ -487,19 +513,40 @@ class RedHelpFormatter: Sends pages based on settings. """ - if not self.USE_MENU: + if not ( + ctx.channel.permissions_for(ctx.me).add_reactions and await ctx.bot.db.help.use_menus() + ): max_pages_in_guild = await ctx.bot.db.help.max_pages_in_guild() destination = ctx.author if len(pages) > max_pages_in_guild else ctx if embed: for page in pages: - await destination.send(embed=page) + try: + await destination.send(embed=page) + except discord.Forbidden: + await ctx.send( + T_( + "I couldn't send the help message to you in DM. " + "Either you blocked me or you disabled DMs in this server." + ) + ) else: for page in pages: - await destination.send(page) + try: + await destination.send(page) + except discord.Forbidden: + await ctx.send( + T_( + "I couldn't send the help message to you in DM. " + "Either you blocked me or you disabled DMs in this server." + ) + ) else: - await menus.menu(ctx, pages, menus.DEFAULT_CONTROLS) + if len(pages) > 1: + await menus.menu(ctx, pages, menus.DEFAULT_CONTROLS) + else: + await menus.menu(ctx, pages, {"\N{CROSS MARK}": menus.close_menu}) @commands.command(name="help", hidden=True, i18n=T_) diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py index 6552bd4ee..6adc88b74 100644 --- a/redbot/core/core_commands.py +++ b/redbot/core/core_commands.py @@ -1083,6 +1083,80 @@ class Core(commands.Cog, CoreLogic): """Manage settings for the help command.""" pass + @helpset.command(name="usemenus") + async def helpset_usemenus(self, ctx: commands.Context, use_menus: bool = None): + """ + Allows the help command to be sent as a paginated menu instead of seperate + messages. + + This defaults to False. + Using this without a setting will toggle. + """ + if use_menus is None: + use_menus = not await ctx.bot.db.help.use_menus() + await ctx.bot.db.help.use_menus.set(use_menus) + if use_menus: + await ctx.send(_("Help will use menus.")) + else: + await ctx.send(_("Help will not use menus.")) + + @helpset.command(name="showhidden") + async def helpset_showhidden(self, ctx: commands.Context, show_hidden: bool = None): + """ + This allows the help command to show hidden commands + + This defaults to False. + Using this without a setting will toggle. + """ + if show_hidden is None: + show_hidden = not await ctx.bot.db.help.show_hidden() + await ctx.bot.db.help.show_hidden.set(show_hidden) + if show_hidden: + await ctx.send(_("Help will not filter hidden commands")) + else: + await ctx.send(_("Help will filter hidden commands.")) + + @helpset.command(name="verifychecks") + async def helpset_permfilter(self, ctx: commands.Context, verify: bool = None): + """ + Sets if commands which can't be run in the current context should be + filtered from help + + Defaults to True. + Using this without a setting will toggle. + """ + if verify is None: + verify = not await ctx.bot.db.help.verify_checks() + await ctx.bot.db.help.verify_checks.set(verify) + if verify: + await ctx.send(_("Help will only show for commands which can be run.")) + else: + await ctx.send(_("Help will show up without checking if the commands can be run.")) + + @helpset.command(name="verifyexists") + async def helpset_verifyexists(self, ctx: commands.Context, verify: bool = None): + """ + This allows the bot to respond indicating the existence of a specific + help topic even if the user can't use it. + + Note: This setting on it's own does not fully prevent command enumeration. + + Defaults to False. + Using this without a setting will toggle. + """ + if verify is None: + verify = not await ctx.bot.db.help.verify_exists() + await ctx.bot.db.help.verify_exists.set(verify) + if verify: + await ctx.send(_("Help will verify the existence of help topics.")) + else: + await ctx.send( + _( + "Help will only verify the existence of " + "help topics via fuzzy help (if enabled)." + ) + ) + @helpset.command(name="pagecharlimit") async def helpset_pagecharlimt(self, ctx: commands.Context, limit: int): """Set the character limit for each page in the help message. @@ -1605,7 +1679,7 @@ class Core(commands.Cog, CoreLogic): """ user = isinstance(user_or_role, discord.Member) - if user and await ctx.bot.is_owner(obj): + if user and await ctx.bot.is_owner(user_or_role): await ctx.send(_("You cannot blacklist an owner!")) return