diff --git a/redbot/core/bot.py b/redbot/core/bot.py index bd9273c63..611582439 100644 --- a/redbot/core/bot.py +++ b/redbot/core/bot.py @@ -86,6 +86,9 @@ class RedBase(BotBase): loop = asyncio.get_event_loop() loop.run_until_complete(self._dict_abuse(kwargs)) + if "command_not_found" not in kwargs: + kwargs["command_not_found"] = "Command {} not found.\n{}" + self.counter = Counter() self.uptime = None diff --git a/redbot/core/events.py b/redbot/core/events.py index 2d18a3c8e..86cf939ac 100644 --- a/redbot/core/events.py +++ b/redbot/core/events.py @@ -15,7 +15,8 @@ from discord.ext import commands from . import __version__ from .data_manager import storage_type -from .utils.chat_formatting import inline, bordered +from .utils.chat_formatting import inline, bordered, pagify, box +from .utils import fuzzy_command_search from colorama import Fore, Style, init log = logging.getLogger("red") @@ -221,7 +222,10 @@ def init_events(bot, cli_flags): if not hasattr(ctx.cog, "_{0.command.cog_name}__error".format(ctx)): await ctx.send(inline(message)) elif isinstance(error, commands.CommandNotFound): - pass + term = ctx.invoked_with + " " + if len(ctx.args) > 1: + term += " ".join(ctx.args[1:]) + await ctx.maybe_send_embed(fuzzy_command_search(ctx, ctx.invoked_with)) elif isinstance(error, commands.CheckFailure): await ctx.send("⛔ You are not authorized to issue that command.") elif isinstance(error, commands.NoPrivateMessage): diff --git a/redbot/core/help_formatter.py b/redbot/core/help_formatter.py index 30923293a..560ccec20 100644 --- a/redbot/core/help_formatter.py +++ b/redbot/core/help_formatter.py @@ -36,7 +36,8 @@ import sys import traceback from . import commands -from redbot.core.utils.chat_formatting import pagify +from redbot.core.utils.chat_formatting import pagify, box +from redbot.core.utils import fuzzy_command_search EMPTY_STRING = u"\u200b" @@ -277,11 +278,9 @@ class Help(formatter.HelpFormatter): def cmd_not_found(self, ctx, cmd, color=None): # Shortcut for a shortcut. Sue me + out = fuzzy_command_search(ctx, " ".join(ctx.args[1:])) embed = self.simple_embed( - ctx, - title=ctx.bot.command_not_found.format(cmd), - description="Commands are case sensitive. Please check your spelling and try again", - color=color, + ctx, title="Command {} not found.".format(cmd), description=out, color=color ) return embed @@ -324,7 +323,9 @@ async def help(ctx, *cmds: str): if use_embeds: await destination.send(embed=ctx.bot.formatter.cmd_not_found(ctx, name)) else: - await destination.send(ctx.bot.command_not_found.format(name)) + await destination.send( + ctx.bot.command_not_found.format(name, fuzzy_command_search(ctx, name)) + ) return if use_embeds: embeds = await ctx.bot.formatter.format_help_for(ctx, command) @@ -337,7 +338,9 @@ async def help(ctx, *cmds: str): if use_embeds: await destination.send(embed=ctx.bot.formatter.cmd_not_found(ctx, name)) else: - await destination.send(ctx.bot.command_not_found.format(name)) + await destination.send( + ctx.bot.command_not_found.format(name, fuzzy_command_search(ctx, name)) + ) return for key in cmds[1:]: @@ -348,7 +351,9 @@ async def help(ctx, *cmds: str): if use_embeds: await destination.send(embed=ctx.bot.formatter.cmd_not_found(ctx, key)) else: - await destination.send(ctx.bot.command_not_found.format(key)) + await destination.send( + ctx.bot.command_not_found.format(key, fuzzy_command_search(ctx, name)) + ) return except AttributeError: if use_embeds: diff --git a/redbot/core/utils/__init__.py b/redbot/core/utils/__init__.py index 59dffe0cb..5fbb8483c 100644 --- a/redbot/core/utils/__init__.py +++ b/redbot/core/utils/__init__.py @@ -1,8 +1,11 @@ -__all__ = ["TYPE_CHECKING", "NewType", "safe_delete"] +__all__ = ["TYPE_CHECKING", "NewType", "safe_delete", "fuzzy_command_search"] from pathlib import Path import os import shutil +from redbot.core import commands +from fuzzywuzzy import process +from .chat_formatting import box try: from typing import TYPE_CHECKING @@ -26,3 +29,15 @@ def safe_delete(pth: Path): for f in files: os.chmod(os.path.join(root, f), 0o755) shutil.rmtree(str(pth), ignore_errors=True) + + +def fuzzy_command_search(ctx: commands.Context, term: str): + out = "" + for pos, extracted in enumerate(process.extract(term, ctx.bot.walk_commands(), limit=5), 1): + out += "{0}. {1.prefix}{2.qualified_name}{3}\n".format( + pos, + ctx, + extracted[0], + " - {}".format(extracted[0].short_doc) if extracted[0].short_doc else "", + ) + return box(out, lang="Perhaps you wanted one of these?") diff --git a/requirements.txt b/requirements.txt index de1c3cb74..40efb6ee9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,6 @@ raven==6.5.0 colorama==0.3.9 jsonrpcserver pyyaml==3.12 +fuzzywuzzy[speedup]<=0.16.0 Red-Trivia>=1.1.1 async-timeout<3.0.0