Repository: Cog-Creators/Red-DiscordBot

Issue:
[Core] Exception thrown on help when blocked
# Command bugs

#### Command name

[p]help

#### What cog is this command from?

help.py

#### What were you expecting to happen?

For the bot to say in the invoking channel "I couldn't send the help message to you in DM. Either you blocked me or you disabled DMs in this server."

#### What actually happened?

```
019-05-15 16:23:44] [ERROR] red: Exception in command 'help'
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/discord/ext/commands/core.py", line 63, in wrapped
    ret = await coro(*args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/redbot/core/commands/help.py", line 513, in red_help
    await ctx.bot.send_help_for(ctx, thing_to_get_help_for)
  File "/usr/local/lib/python3.7/site-packages/redbot/core/bot.py", line 132, in send_help_for
    return await self._help_formatter.send_help(ctx, help_for)
  File "/usr/local/lib/python3.7/site-packages/redbot/core/commands/help.py", line 95, in send_help
    await self.format_bot_help(ctx)
  File "/usr/local/lib/python3.7/site-packages/redbot/core/commands/help.py", line 394, in format_bot_help
    await self.send_pages(ctx, pages, embed=False)
  File "/usr/local/lib/python3.7/site-packages/redbot/core/commands/help.py", line 500, in send_pages
    await destination.send(page)
  File "/usr/local/lib/python3.7/site-packages/discord/abc.py", line 776, in send
    data = await state.http.send_message(channel.id, content, tts=tts, embed=embed, nonce=nonce)
  File "/usr/local/lib/python3.7/site-packages/discord/http.py", line 218, in request
    raise Forbidden(r, data)
discord.errors.Forbidden: FORBIDDEN (status code: 403): Cannot send messages to this user
```

#### How can we reproduce this issue?

Block the bot.
Use the help command.



Discussion:

Resolving patch:
diff --git a/redbot/core/bot.py b/redbot/core/bot.py
--- a/redbot/core/bot.py
+++ b/redbot/core/bot.py
@@ -52,6 +52,10 @@ def __init__(self, *args, cli_flags=None, bot_dir: Path = Path.cwd(), **kwargs):
             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
--- 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 @@ async def send_help(self, ctx: Context, help_for: HelpTarget = None):
                 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 @@ def get_default_tagline(ctx: Context):
 
     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 @@ async def format_command_help(self, ctx: Context, obj: commands.Command):
                 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 @@ async def make_and_send_embeds(self, ctx, embed_dict: dict):
     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 @@ async def format_cog_help(self, ctx: Context, obj: commands.Cog):
                 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 @@ async def format_bot_help(self, ctx: Context):
                 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 @@ async def help_filter_func(
         """
         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 @@ async def command_not_found(self, ctx, help_for):
                 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 @@ async def subcommand_not_found(self, ctx, command, not_found):
         """
         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 @@ async def send_pages(
         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
--- a/redbot/core/core_commands.py
+++ b/redbot/core/core_commands.py
@@ -1080,6 +1080,80 @@ async def helpset(self, ctx: commands.Context):
         """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.
@@ -1602,7 +1676,7 @@ async def localblacklist_add(
         """
         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
 


Test patch: