diff options
author | Sadie Powell <sadie@witchery.services> | 2024-02-29 21:56:14 +0000 |
---|---|---|
committer | Sadie Powell <sadie@witchery.services> | 2024-02-29 21:56:14 +0000 |
commit | 1c1645096f953aa06b5809be6b302015b5bc73d6 (patch) | |
tree | be03513aa05c44ea017ca4ecb1e6c497dd231857 | |
parent | e0ac5509b4020cb6e78c6b7c591ef2e7f6314ced (diff) |
If a user runs an invalid command try to suggest a valid one.
-rw-r--r-- | data/anope.example.conf | 6 | ||||
-rw-r--r-- | include/anope.h | 6 | ||||
-rw-r--r-- | language/anope.en_US.po | 12 | ||||
-rw-r--r-- | src/command.cpp | 54 | ||||
-rw-r--r-- | src/misc.cpp | 36 |
5 files changed, 103 insertions, 11 deletions
diff --git a/data/anope.example.conf b/data/anope.example.conf index fb90a2f08..86c124e66 100644 --- a/data/anope.example.conf +++ b/data/anope.example.conf @@ -523,6 +523,12 @@ options */ hideregisteredcommands = yes + /* + * If set, the maximum difference between an invalid and valid command name to allow + * as a suggestion. Defaults to 4. + */ + didyoumeandifference = 4 + /* The regex engine to use, as provided by the regex modules. * Leave commented to disable regex matching. * diff --git a/include/anope.h b/include/anope.h index 6031dceac..1a01db189 100644 --- a/include/anope.h +++ b/include/anope.h @@ -557,6 +557,12 @@ namespace Anope * @param len The length of the string returned */ extern CoreExport Anope::string Random(size_t len); + + /** Calculates the levenshtein distance between two strings. + * @param s1 The first string. + * @param s2 The second string. + */ + extern CoreExport size_t Distance(const Anope::string &s1, const Anope::string &s2); } /** sepstream allows for splitting token separated lists. diff --git a/language/anope.en_US.po b/language/anope.en_US.po index 8fe37eb9b..4c7e5601f 100644 --- a/language/anope.en_US.po +++ b/language/anope.en_US.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Anope\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-02-29 16:12+0000\n" -"PO-Revision-Date: 2024-02-29 16:13+0000\n" +"POT-Creation-Date: 2024-02-29 21:53+0000\n" +"PO-Revision-Date: 2024-02-29 21:53+0000\n" "Last-Translator: Sadie Powell <sadie@witchery.services>\n" "Language-Team: English\n" "Language: en_US\n" @@ -7955,6 +7955,14 @@ msgid "Unknown command %s. \"%s%s HELP\" for help." msgstr "Unknown command %s. \"%s%s HELP\" for help." #, c-format +msgid "Unknown command %s. Did you mean %s?" +msgstr "Unknown command %s. Did you mean %s?" + +#, c-format +msgid "Unknown command %s. Did you mean %s? \"%s%s HELP\" for help." +msgstr "Unknown command %s. Did you mean %s? \"%s%s HELP\" for help." + +#, c-format msgid "Unknown mode character %c ignored." msgstr "Unknown mode character %c ignored." diff --git a/src/command.cpp b/src/command.cpp index c9372c40f..e7912864c 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -201,11 +201,53 @@ void Command::OnSyntaxError(CommandSource &source, const Anope::string &subcomma source.Reply(MORE_INFO, Config->StrictPrivmsg.c_str(), source.service->nick.c_str(), source.command.c_str()); } +namespace +{ + void HandleUnknownCommand(CommandSource& source, const Anope::string &message) + { + // Try to find a similar command. + size_t distance = Config->GetBlock("options")->Get<size_t>("didyoumeandifference", "4"); + Anope::string similar; + auto umessage = message.upper(); + for (const auto &[command, info] : source.service->commands) + { + if (info.hide || command == message) + continue; // Don't suggest a hidden alias or a missing command. + + size_t dist = Anope::Distance(umessage, command); + if (dist < distance) + { + distance = dist; + similar = command; + } + } + + bool has_help = source.service->commands.find("HELP") != source.service->commands.end(); + if (has_help && similar.empty()) + { + source.Reply(_("Unknown command \002%s\002. \"%s%s HELP\" for help."), message.c_str(), + Config->StrictPrivmsg.c_str(), source.service->nick.c_str()); + } + else if (has_help) + { + source.Reply(_("Unknown command \002%s\002. Did you mean \002%s\002? \"%s%s HELP\" for help."), + message.c_str(), similar.c_str(), Config->StrictPrivmsg.c_str(), source.service->nick.c_str()); + } + else if (similar.empty()) + { + source.Reply(_("Unknown command \002%s\002. Did you mean \002%s\002?"), message.c_str(), similar.c_str()); + } + else + { + source.Reply(_("Unknown command \002%s\002."), message.c_str()); + } + } +} + void Command::Run(CommandSource &source, const Anope::string &message) { std::vector<Anope::string> params; spacesepstream(message).GetTokens(params); - bool has_help = source.service->commands.find("HELP") != source.service->commands.end(); CommandInfo::map::const_iterator it = source.service->commands.end(); unsigned count = 0; @@ -222,10 +264,7 @@ void Command::Run(CommandSource &source, const Anope::string &message) if (it == source.service->commands.end()) { - if (has_help) - source.Reply(_("Unknown command \002%s\002. \"%s%s HELP\" for help."), message.c_str(), Config->StrictPrivmsg.c_str(), source.service->nick.c_str()); - else - source.Reply(_("Unknown command \002%s\002."), message.c_str()); + HandleUnknownCommand(source, message); return; } @@ -233,10 +272,7 @@ void Command::Run(CommandSource &source, const Anope::string &message) ServiceReference<Command> c("Command", info.name); if (!c) { - if (has_help) - source.Reply(_("Unknown command \002%s\002. \"%s%s HELP\" for help."), message.c_str(), Config->StrictPrivmsg.c_str(), source.service->nick.c_str()); - else - source.Reply(_("Unknown command \002%s\002."), message.c_str()); + HandleUnknownCommand(source, message); Log(source.service) << "Command " << it->first << " exists on me, but its service " << info.name << " was not found!"; return; } diff --git a/src/misc.cpp b/src/misc.cpp index b44176ecd..b40c69844 100644 --- a/src/misc.cpp +++ b/src/misc.cpp @@ -20,6 +20,7 @@ #include "sockets.h" #include <cerrno> +#include <numeric> #include <sys/stat.h> #include <sys/types.h> #ifndef _WIN32 @@ -747,3 +748,38 @@ Anope::string Anope::Random(size_t len) buf.append(chars[rand() % sizeof(chars)]); return buf; } + +// Implementation of https://en.wikipedia.org/wiki/Levenshtein_distance +size_t Anope::Distance(const Anope::string &s1, const Anope::string &s2) +{ + if (s1.empty()) + return s2.length(); + if (s2.empty()) + return s1.length(); + + std::vector<size_t> costs(s2.length() + 1); + std::iota(costs.begin(), costs.end(), 0); + + size_t i = 0; + for (const auto c1 : s1) + { + costs[0] = i + 1; + size_t corner = i; + size_t j = 0; + for (const auto &c2 : s2) + { + size_t upper = costs[j + 1]; + if (c1 == c2) + costs[j + 1] = corner; + else + { + size_t t = upper < corner ? upper : corner; + costs[j + 1] = (costs[j] < t ? costs[j] : t) + 1; + } + corner = upper; + j++; + } + i++; + } + return costs[s2.length()]; +} |