diff --git a/PluralKit.Bot/CommandMeta/CommandParseErrors.cs b/PluralKit.Bot/CommandMeta/CommandParseErrors.cs index 334a271d..fb415b95 100644 --- a/PluralKit.Bot/CommandMeta/CommandParseErrors.cs +++ b/PluralKit.Bot/CommandMeta/CommandParseErrors.cs @@ -1,40 +1,16 @@ -using Humanizer; - using Myriad.Types; -using PluralKit.Core; - namespace PluralKit.Bot; public partial class CommandTree { - private async Task PrintCommandNotFoundError(Context ctx, params Command[] potentialCommands) + private async Task PrintCommandList(Context ctx, string subject, string commands) { - var commandListStr = CreatePotentialCommandList(ctx.DefaultPrefix, potentialCommands); - await ctx.Reply( - $"{Emojis.Error} Unknown command `{ctx.DefaultPrefix}{ctx.FullCommand().Truncate(100)}`. Perhaps you meant to use one of the following commands?\n{commandListStr}\n\nFor a full list of possible commands, see ."); - } - - private async Task PrintCommandExpectedError(Context ctx, params Command[] potentialCommands) - { - var commandListStr = CreatePotentialCommandList(ctx.DefaultPrefix, potentialCommands); - await ctx.Reply( - $"{Emojis.Error} You need to pass a command. Perhaps you meant to use one of the following commands?\n{commandListStr}\n\nFor a full list of possible commands, see ."); - } - - private static string CreatePotentialCommandList(string prefix, params Command[] potentialCommands) - { - return string.Join("\n", potentialCommands.Select(cmd => $"- **{prefix}{cmd.Usage}** - *{cmd.Description}*")); - } - - private async Task PrintCommandList(Context ctx, string subject, params Command[] commands) - { - var str = CreatePotentialCommandList(ctx.DefaultPrefix, commands); await ctx.Reply( $"Here is a list of commands related to {subject}:", embed: new Embed() { - Description = $"{str}\nFor a full list of possible commands, see .", + Description = $"{commands}\nFor a full list of possible commands, see .", Color = DiscordUtils.Blue, } ); diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 2aa901ca..e8c3eaa5 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -8,6 +8,7 @@ public partial class CommandTree { return command switch { + Commands.CommandsList(var param, _) => PrintCommandList(ctx, param.subject, Parameters.GetRelatedCommands(ctx.DefaultPrefix, param.subject)), Commands.Dashboard => ctx.Execute(Dashboard, m => m.Dashboard(ctx)), Commands.Explain => ctx.Execute(Explain, m => m.Explain(ctx)), Commands.Help(_, var flags) => ctx.Execute(Help, m => m.HelpRoot(ctx, flags.show_embed)), @@ -330,70 +331,5 @@ public partial class CommandTree ctx.Reply( $"{Emojis.Error} Parsed command {ctx.Parameters.Callback().AsCode()} not implemented in PluralKit.Bot!"), }; - // Legacy command routing - these are kept for backwards compatibility until fully migrated to new system - if (ctx.Match("commands", "cmd", "c")) - return CommandHelpRoot(ctx); - } - - private async Task CommandHelpRoot(Context ctx) - { - if (!ctx.HasNext()) - { - await ctx.Reply( - "Available command help targets: `system`, `member`, `group`, `switch`, `config`, `autoproxy`, `log`, `blacklist`." - + $"\n- **{ctx.DefaultPrefix}commands ** - *View commands related to a help target.*" - + "\n\nFor the full list of commands, see the website: "); - return; - } - - switch (ctx.PeekArgument()) - { - case "system": - case "systems": - case "s": - case "account": - case "acc": - await PrintCommandList(ctx, "systems", SystemCommands); - break; - case "member": - case "members": - case "m": - await PrintCommandList(ctx, "members", MemberCommands); - break; - case "group": - case "groups": - case "g": - await PrintCommandList(ctx, "groups", GroupCommands); - break; - case "switch": - case "switches": - case "switching": - case "sw": - await PrintCommandList(ctx, "switching", SwitchCommands); - break; - case "log": - await PrintCommandList(ctx, "message logging", LogCommands); - break; - case "blacklist": - case "bl": - await PrintCommandList(ctx, "channel blacklisting", BlacklistCommands); - break; - case "config": - case "cfg": - await PrintCommandList(ctx, "settings", ConfigCommands); - break; - case "serverconfig": - case "guildconfig": - case "scfg": - await PrintCommandList(ctx, "server settings", ServerConfigCommands); - break; - case "autoproxy": - case "ap": - await PrintCommandList(ctx, "autoproxy", AutoproxyCommands); - break; - default: - await ctx.Reply("For the full list of commands, see the website: "); - break; - } } } \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index b359e80b..1add6fe2 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -55,6 +55,11 @@ public class Parameters } } + public static string GetRelatedCommands(string prefix, string subject) + { + return CommandsMethods.GetRelatedCommands(prefix, subject); + } + public string Callback() { return _cb; diff --git a/crates/command_definitions/src/commands.rs b/crates/command_definitions/src/commands.rs deleted file mode 100644 index 8b137891..00000000 --- a/crates/command_definitions/src/commands.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/command_definitions/src/help.rs b/crates/command_definitions/src/help.rs index 8991cf72..95e73d5c 100644 --- a/crates/command_definitions/src/help.rs +++ b/crates/command_definitions/src/help.rs @@ -3,6 +3,7 @@ use super::*; pub fn cmds() -> impl Iterator { let help = ("help", ["h"]); [ + command!(("commands", ["cmd", "c"]), ("subject", OpaqueString) => "commands_list"), command!(("dashboard", ["dash"]) => "dashboard"), command!("explain" => "explain"), command!(help => "help").help("Shows the help command"), diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index 3445d6a6..065e95b3 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -1,7 +1,6 @@ pub mod admin; pub mod api; pub mod autoproxy; -pub mod commands; pub mod config; pub mod debug; pub mod fun; diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index d736b61f..db065cc5 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -90,13 +90,12 @@ pub fn parse_command( None => { let mut error = format!("Unknown command `{prefix}{input}`."); - let wrote_possible_commands = fmt_possible_commands( - &mut error, - &prefix, - &input, - local_tree.possible_commands(usize::MAX), - ); - if wrote_possible_commands.not() { + let possible_commands = + rank_possible_commands(&input, local_tree.possible_commands(usize::MAX)); + if possible_commands.is_empty().not() { + error.push_str(" Perhaps you meant one of the following commands:\n"); + fmt_commands_list(&mut error, &prefix, possible_commands); + } else { // add a space between the unknown command and "for a list of all possible commands" // message if we didn't add any possible suggestions error.push_str(" "); @@ -278,78 +277,70 @@ fn next_token<'a>( // todo: should probably move this somewhere else /// returns true if wrote possible commands, false if not -fn fmt_possible_commands( - f: &mut String, - prefix: &str, +fn rank_possible_commands( input: &str, - mut possible_commands: impl Iterator, -) -> bool { - if let Some(first) = possible_commands.next() { - let mut commands_with_scores: Vec<(&Command, String, f64, bool)> = std::iter::once(first) - .chain(possible_commands) - .filter(|cmd| cmd.show_in_suggestions) - .flat_map(|cmd| { - let versions = generate_command_versions(cmd); - versions.into_iter().map(move |(version, is_alias)| { - let similarity = strsim::jaro_winkler(&input, &version); - (cmd, version, similarity, is_alias) - }) + possible_commands: impl IntoIterator, +) -> Vec<(Command, String, bool)> { + let mut commands_with_scores: Vec<(&Command, String, f64, bool)> = possible_commands + .into_iter() + .filter(|cmd| cmd.show_in_suggestions) + .flat_map(|cmd| { + let versions = generate_command_versions(cmd); + versions.into_iter().map(move |(version, is_alias)| { + let similarity = strsim::jaro_winkler(&input, &version); + (cmd, version, similarity, is_alias) }) - .collect(); + }) + .collect(); - commands_with_scores - .sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)); + commands_with_scores.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)); - // remove duplicate commands - let mut seen_commands = std::collections::HashSet::new(); - let mut best_commands = Vec::new(); - for (cmd, version, score, is_alias) in commands_with_scores { - if seen_commands.insert(cmd) { - best_commands.push((cmd, version, score, is_alias)); - } + // remove duplicate commands + let mut seen_commands = std::collections::HashSet::new(); + let mut best_commands = Vec::new(); + for (cmd, version, score, is_alias) in commands_with_scores { + if seen_commands.insert(cmd) { + best_commands.push((cmd, version, score, is_alias)); } - - const MIN_SCORE_THRESHOLD: f64 = 0.8; - if best_commands.is_empty() || best_commands[0].2 < MIN_SCORE_THRESHOLD { - return false; - } - - // if score falls off too much, don't show - let mut falloff_threshold: f64 = 0.2; - let best_score = best_commands[0].2; - - let mut commands_to_show = Vec::new(); - for (command, version, score, is_alias) in best_commands.iter().take(MAX_SUGGESTIONS) { - let delta = best_score - score; - falloff_threshold -= delta; - if delta > falloff_threshold { - break; - } - commands_to_show.push((command, version, score, is_alias)); - } - - if commands_to_show.is_empty() { - return false; - } - - f.push_str(" Perhaps you meant one of the following commands:\n"); - for (command, version, _score, is_alias) in commands_to_show { - writeln!( - f, - "- **{prefix}{version}**{alias} - *{help}*", - help = command.help, - alias = is_alias - .then(|| format!( - " (alias of **{prefix}{base_version}**)", - base_version = build_command_string(command, None) - )) - .unwrap_or_else(String::new), - ) - .expect("oom"); - } - return true; } - return false; + + const MIN_SCORE_THRESHOLD: f64 = 0.8; + if best_commands.is_empty() || best_commands[0].2 < MIN_SCORE_THRESHOLD { + return Vec::new(); + } + + // if score falls off too much, don't show + let mut falloff_threshold: f64 = 0.2; + let best_score = best_commands[0].2; + + let mut commands_to_show = Vec::new(); + for (command, version, score, is_alias) in best_commands.into_iter().take(MAX_SUGGESTIONS) { + let delta = best_score - score; + falloff_threshold -= delta; + if delta > falloff_threshold { + break; + } + commands_to_show.push((command.clone(), version, is_alias)); + } + + commands_to_show +} + +fn fmt_commands_list(f: &mut String, prefix: &str, commands_to_show: Vec<(Command, String, bool)>) { + for (command, version, is_alias) in commands_to_show { + writeln!( + f, + "- **{prefix}{version}**{alias} - *{help}*", + help = command.help, + alias = is_alias + .then(|| format!( + " (alias of **{prefix}{base_version}**)", + base_version = build_command_string(&command, None) + )) + .unwrap_or_else(String::new), + ) + .expect("oom"); + } } fn generate_command_versions(cmd: &Command) -> Vec<(String, bool)> { diff --git a/crates/command_parser/src/token.rs b/crates/command_parser/src/token.rs index f158f269..e87c702a 100644 --- a/crates/command_parser/src/token.rs +++ b/crates/command_parser/src/token.rs @@ -36,10 +36,10 @@ pub enum TokenMatchResult { // a: because we want to differentiate between no match and match failure (it matched with an error) // "no match" has a different charecteristic because we want to continue matching other tokens... // ...while "match failure" means we should stop matching and return the error -type TryMatchResult = Option; +pub type TryMatchResult = Option; impl Token { - pub(super) fn try_match(&self, input: Option<&str>) -> TryMatchResult { + pub fn try_match(&self, input: Option<&str>) -> TryMatchResult { let input = match input { Some(input) => input, None => { diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index f531f870..3bffa3cb 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -1,5 +1,6 @@ namespace commands { CommandResult parse_command(string prefix, string input); + string get_related_commands(string prefix, string input); }; [Enum] interface CommandResult { diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index c78b0c69..959683c7 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -1,6 +1,6 @@ -use std::collections::HashMap; +use std::{collections::HashMap, fmt::Write, usize}; -use command_parser::{parameter::ParameterValue, Tree}; +use command_parser::{parameter::ParameterValue, token::TokenMatchResult, Tree}; uniffi::include_scaffolding!("commands"); @@ -143,3 +143,22 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { }, ) } + +pub fn get_related_commands(prefix: String, input: String) -> String { + let mut s = String::new(); + for command in command_definitions::all() { + if command.tokens.first().map_or(false, |token| { + token + .try_match(Some(&input)) + .map_or(false, |r| matches!(r, TokenMatchResult::MatchedValue)) + }) { + writeln!( + &mut s, + "- **{prefix}{command}** - *{help}*", + help = command.help + ) + .unwrap(); + } + } + s +} diff --git a/crates/commands/src/main.rs b/crates/commands/src/main.rs index 07128bfe..325fc469 100644 --- a/crates/commands/src/main.rs +++ b/crates/commands/src/main.rs @@ -4,6 +4,16 @@ use command_parser::Tree; use commands::COMMAND_TREE; fn main() { + parse(); +} + +fn related() { + let cmd = std::env::args().nth(1).unwrap(); + let related = commands::get_related_commands("pk;".to_string(), cmd); + println!("Related commands:\n{related}"); +} + +fn parse() { let cmd = std::env::args() .skip(1) .intersperse(" ".to_string())