mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-14 17:50:13 +00:00
implement command root
This commit is contained in:
parent
c1ed7487d7
commit
15ffd16c01
11 changed files with 107 additions and 170 deletions
|
|
@ -1,40 +1,16 @@
|
||||||
using Humanizer;
|
|
||||||
|
|
||||||
using Myriad.Types;
|
using Myriad.Types;
|
||||||
|
|
||||||
using PluralKit.Core;
|
|
||||||
|
|
||||||
namespace PluralKit.Bot;
|
namespace PluralKit.Bot;
|
||||||
|
|
||||||
public partial class CommandTree
|
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 <https://pluralkit.me/commands>.");
|
|
||||||
}
|
|
||||||
|
|
||||||
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 <https://pluralkit.me/commands>.");
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
await ctx.Reply(
|
||||||
$"Here is a list of commands related to {subject}:",
|
$"Here is a list of commands related to {subject}:",
|
||||||
embed: new Embed()
|
embed: new Embed()
|
||||||
{
|
{
|
||||||
Description = $"{str}\nFor a full list of possible commands, see <https://pluralkit.me/commands>.",
|
Description = $"{commands}\nFor a full list of possible commands, see <https://pluralkit.me/commands>.",
|
||||||
Color = DiscordUtils.Blue,
|
Color = DiscordUtils.Blue,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ public partial class CommandTree
|
||||||
{
|
{
|
||||||
return command switch
|
return command switch
|
||||||
{
|
{
|
||||||
|
Commands.CommandsList(var param, _) => PrintCommandList(ctx, param.subject, Parameters.GetRelatedCommands(ctx.DefaultPrefix, param.subject)),
|
||||||
Commands.Dashboard => ctx.Execute<Help>(Dashboard, m => m.Dashboard(ctx)),
|
Commands.Dashboard => ctx.Execute<Help>(Dashboard, m => m.Dashboard(ctx)),
|
||||||
Commands.Explain => ctx.Execute<Help>(Explain, m => m.Explain(ctx)),
|
Commands.Explain => ctx.Execute<Help>(Explain, m => m.Explain(ctx)),
|
||||||
Commands.Help(_, var flags) => ctx.Execute<Help>(Help, m => m.HelpRoot(ctx, flags.show_embed)),
|
Commands.Help(_, var flags) => ctx.Execute<Help>(Help, m => m.HelpRoot(ctx, flags.show_embed)),
|
||||||
|
|
@ -330,70 +331,5 @@ public partial class CommandTree
|
||||||
ctx.Reply(
|
ctx.Reply(
|
||||||
$"{Emojis.Error} Parsed command {ctx.Parameters.Callback().AsCode()} not implemented in PluralKit.Bot!"),
|
$"{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 <target>** - *View commands related to a help target.*"
|
|
||||||
+ "\n\nFor the full list of commands, see the website: <https://pluralkit.me/commands>");
|
|
||||||
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: <https://pluralkit.me/commands>");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -55,6 +55,11 @@ public class Parameters
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GetRelatedCommands(string prefix, string subject)
|
||||||
|
{
|
||||||
|
return CommandsMethods.GetRelatedCommands(prefix, subject);
|
||||||
|
}
|
||||||
|
|
||||||
public string Callback()
|
public string Callback()
|
||||||
{
|
{
|
||||||
return _cb;
|
return _cb;
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
|
|
||||||
|
|
@ -3,6 +3,7 @@ use super::*;
|
||||||
pub fn cmds() -> impl Iterator<Item = Command> {
|
pub fn cmds() -> impl Iterator<Item = Command> {
|
||||||
let help = ("help", ["h"]);
|
let help = ("help", ["h"]);
|
||||||
[
|
[
|
||||||
|
command!(("commands", ["cmd", "c"]), ("subject", OpaqueString) => "commands_list"),
|
||||||
command!(("dashboard", ["dash"]) => "dashboard"),
|
command!(("dashboard", ["dash"]) => "dashboard"),
|
||||||
command!("explain" => "explain"),
|
command!("explain" => "explain"),
|
||||||
command!(help => "help").help("Shows the help command"),
|
command!(help => "help").help("Shows the help command"),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod autoproxy;
|
pub mod autoproxy;
|
||||||
pub mod commands;
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod debug;
|
pub mod debug;
|
||||||
pub mod fun;
|
pub mod fun;
|
||||||
|
|
|
||||||
|
|
@ -90,13 +90,12 @@ pub fn parse_command(
|
||||||
None => {
|
None => {
|
||||||
let mut error = format!("Unknown command `{prefix}{input}`.");
|
let mut error = format!("Unknown command `{prefix}{input}`.");
|
||||||
|
|
||||||
let wrote_possible_commands = fmt_possible_commands(
|
let possible_commands =
|
||||||
&mut error,
|
rank_possible_commands(&input, local_tree.possible_commands(usize::MAX));
|
||||||
&prefix,
|
if possible_commands.is_empty().not() {
|
||||||
&input,
|
error.push_str(" Perhaps you meant one of the following commands:\n");
|
||||||
local_tree.possible_commands(usize::MAX),
|
fmt_commands_list(&mut error, &prefix, possible_commands);
|
||||||
);
|
} else {
|
||||||
if wrote_possible_commands.not() {
|
|
||||||
// add a space between the unknown command and "for a list of all possible commands"
|
// add a space between the unknown command and "for a list of all possible commands"
|
||||||
// message if we didn't add any possible suggestions
|
// message if we didn't add any possible suggestions
|
||||||
error.push_str(" ");
|
error.push_str(" ");
|
||||||
|
|
@ -278,78 +277,70 @@ fn next_token<'a>(
|
||||||
|
|
||||||
// todo: should probably move this somewhere else
|
// todo: should probably move this somewhere else
|
||||||
/// returns true if wrote possible commands, false if not
|
/// returns true if wrote possible commands, false if not
|
||||||
fn fmt_possible_commands(
|
fn rank_possible_commands(
|
||||||
f: &mut String,
|
|
||||||
prefix: &str,
|
|
||||||
input: &str,
|
input: &str,
|
||||||
mut possible_commands: impl Iterator<Item = &Command>,
|
possible_commands: impl IntoIterator<Item = &Command>,
|
||||||
) -> bool {
|
) -> Vec<(Command, String, bool)> {
|
||||||
if let Some(first) = possible_commands.next() {
|
let mut commands_with_scores: Vec<(&Command, String, f64, bool)> = possible_commands
|
||||||
let mut commands_with_scores: Vec<(&Command, String, f64, bool)> = std::iter::once(first)
|
.into_iter()
|
||||||
.chain(possible_commands)
|
.filter(|cmd| cmd.show_in_suggestions)
|
||||||
.filter(|cmd| cmd.show_in_suggestions)
|
.flat_map(|cmd| {
|
||||||
.flat_map(|cmd| {
|
let versions = generate_command_versions(cmd);
|
||||||
let versions = generate_command_versions(cmd);
|
versions.into_iter().map(move |(version, is_alias)| {
|
||||||
versions.into_iter().map(move |(version, is_alias)| {
|
let similarity = strsim::jaro_winkler(&input, &version);
|
||||||
let similarity = strsim::jaro_winkler(&input, &version);
|
(cmd, version, similarity, is_alias)
|
||||||
(cmd, version, similarity, is_alias)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect();
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
commands_with_scores
|
commands_with_scores.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
|
|
||||||
|
|
||||||
// remove duplicate commands
|
// remove duplicate commands
|
||||||
let mut seen_commands = std::collections::HashSet::new();
|
let mut seen_commands = std::collections::HashSet::new();
|
||||||
let mut best_commands = Vec::new();
|
let mut best_commands = Vec::new();
|
||||||
for (cmd, version, score, is_alias) in commands_with_scores {
|
for (cmd, version, score, is_alias) in commands_with_scores {
|
||||||
if seen_commands.insert(cmd) {
|
if seen_commands.insert(cmd) {
|
||||||
best_commands.push((cmd, version, score, is_alias));
|
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)> {
|
fn generate_command_versions(cmd: &Command) -> Vec<(String, bool)> {
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,10 @@ pub enum TokenMatchResult {
|
||||||
// a: because we want to differentiate between no match and match failure (it matched with an error)
|
// 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...
|
// "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
|
// ...while "match failure" means we should stop matching and return the error
|
||||||
type TryMatchResult = Option<TokenMatchResult>;
|
pub type TryMatchResult = Option<TokenMatchResult>;
|
||||||
|
|
||||||
impl Token {
|
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 {
|
let input = match input {
|
||||||
Some(input) => input,
|
Some(input) => input,
|
||||||
None => {
|
None => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
namespace commands {
|
namespace commands {
|
||||||
CommandResult parse_command(string prefix, string input);
|
CommandResult parse_command(string prefix, string input);
|
||||||
|
string get_related_commands(string prefix, string input);
|
||||||
};
|
};
|
||||||
[Enum]
|
[Enum]
|
||||||
interface CommandResult {
|
interface CommandResult {
|
||||||
|
|
|
||||||
|
|
@ -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");
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,16 @@ use command_parser::Tree;
|
||||||
use commands::COMMAND_TREE;
|
use commands::COMMAND_TREE;
|
||||||
|
|
||||||
fn main() {
|
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()
|
let cmd = std::env::args()
|
||||||
.skip(1)
|
.skip(1)
|
||||||
.intersperse(" ".to_string())
|
.intersperse(" ".to_string())
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue