summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSadie Powell <sadie@witchery.services>2024-02-29 21:56:14 +0000
committerSadie Powell <sadie@witchery.services>2024-02-29 21:56:14 +0000
commit1c1645096f953aa06b5809be6b302015b5bc73d6 (patch)
treebe03513aa05c44ea017ca4ecb1e6c497dd231857
parente0ac5509b4020cb6e78c6b7c591ef2e7f6314ced (diff)
If a user runs an invalid command try to suggest a valid one.
-rw-r--r--data/anope.example.conf6
-rw-r--r--include/anope.h6
-rw-r--r--language/anope.en_US.po12
-rw-r--r--src/command.cpp54
-rw-r--r--src/misc.cpp36
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()];
+}