2024-09-13 16:02:30 +09:00
|
|
|
#![feature(let_chains)]
|
2025-01-12 04:23:46 +09:00
|
|
|
#![feature(anonymous_lifetime_in_impl_trait)]
|
2024-09-13 16:02:30 +09:00
|
|
|
|
2025-01-05 00:58:48 +09:00
|
|
|
pub mod commands;
|
2024-09-13 16:02:30 +09:00
|
|
|
mod string;
|
|
|
|
|
mod token;
|
2025-01-04 07:35:04 +09:00
|
|
|
mod tree;
|
2024-09-13 16:02:30 +09:00
|
|
|
|
2025-01-04 07:35:04 +09:00
|
|
|
uniffi::include_scaffolding!("commands");
|
2024-11-02 14:55:06 -07:00
|
|
|
|
2025-01-04 07:35:04 +09:00
|
|
|
use core::panic;
|
|
|
|
|
use std::collections::HashMap;
|
2025-01-11 22:38:29 +09:00
|
|
|
use std::fmt::Write;
|
2025-01-12 04:23:46 +09:00
|
|
|
use std::ops::Not;
|
2024-11-02 14:55:06 -07:00
|
|
|
|
2025-01-12 04:23:46 +09:00
|
|
|
use smol_str::SmolStr;
|
2025-01-04 07:35:04 +09:00
|
|
|
use tree::TreeBranch;
|
2024-11-02 14:55:06 -07:00
|
|
|
|
2025-01-04 07:35:04 +09:00
|
|
|
pub use commands::Command;
|
|
|
|
|
pub use token::*;
|
2024-11-02 14:55:06 -07:00
|
|
|
|
2025-01-11 23:11:15 +09:00
|
|
|
// todo: this should come from the bot probably
|
|
|
|
|
const MAX_SUGGESTIONS: usize = 7;
|
|
|
|
|
|
2024-11-02 14:55:06 -07:00
|
|
|
lazy_static::lazy_static! {
|
2025-01-05 00:58:48 +09:00
|
|
|
pub static ref COMMAND_TREE: TreeBranch = {
|
2025-01-11 19:51:45 +09:00
|
|
|
let mut tree = TreeBranch::empty();
|
2024-11-02 14:55:06 -07:00
|
|
|
|
2025-01-05 13:00:06 +09:00
|
|
|
crate::commands::all().into_iter().for_each(|x| tree.register_command(x));
|
2024-11-02 14:55:06 -07:00
|
|
|
|
|
|
|
|
tree
|
|
|
|
|
};
|
2024-09-13 16:02:30 +09:00
|
|
|
}
|
|
|
|
|
|
2025-01-05 13:00:06 +09:00
|
|
|
#[derive(Debug)]
|
2024-09-13 16:02:30 +09:00
|
|
|
pub enum CommandResult {
|
|
|
|
|
Ok { command: ParsedCommand },
|
|
|
|
|
Err { error: String },
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-07 23:15:18 +09:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub enum Parameter {
|
|
|
|
|
MemberRef { member: String },
|
|
|
|
|
SystemRef { system: String },
|
2025-01-08 18:31:59 +09:00
|
|
|
MemberPrivacyTarget { target: String },
|
|
|
|
|
PrivacyLevel { level: String },
|
|
|
|
|
OpaqueString { raw: String },
|
2025-01-07 23:15:18 +09:00
|
|
|
Toggle { toggle: bool },
|
|
|
|
|
Reset,
|
2025-01-05 13:00:06 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
2024-09-13 16:02:30 +09:00
|
|
|
pub struct ParsedCommand {
|
|
|
|
|
pub command_ref: String,
|
|
|
|
|
pub args: Vec<String>,
|
2025-01-05 13:00:06 +09:00
|
|
|
pub params: HashMap<String, Parameter>,
|
2024-09-13 16:02:30 +09:00
|
|
|
pub flags: HashMap<String, Option<String>>,
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-11 22:38:29 +09:00
|
|
|
pub fn parse_command(prefix: String, input: String) -> CommandResult {
|
2025-01-04 02:49:04 +09:00
|
|
|
let input: SmolStr = input.into();
|
2024-09-13 16:02:30 +09:00
|
|
|
let mut local_tree: TreeBranch = COMMAND_TREE.clone();
|
|
|
|
|
|
|
|
|
|
// end position of all currently matched tokens
|
|
|
|
|
let mut current_pos = 0;
|
|
|
|
|
|
|
|
|
|
let mut args: Vec<String> = Vec::new();
|
2025-01-05 13:00:06 +09:00
|
|
|
let mut params: HashMap<String, Parameter> = HashMap::new();
|
2024-09-13 16:02:30 +09:00
|
|
|
let mut flags: HashMap<String, Option<String>> = HashMap::new();
|
|
|
|
|
|
|
|
|
|
loop {
|
2025-01-11 20:25:41 +09:00
|
|
|
let possible_tokens = local_tree.possible_tokens().cloned().collect::<Vec<_>>();
|
|
|
|
|
println!("possible: {:?}", possible_tokens);
|
2025-01-12 04:23:46 +09:00
|
|
|
let next = next_token(possible_tokens.clone(), input.clone(), current_pos);
|
2025-01-05 13:12:02 +09:00
|
|
|
println!("next: {:?}", next);
|
2025-01-04 07:35:04 +09:00
|
|
|
match next {
|
2025-01-12 04:23:46 +09:00
|
|
|
Some(Ok((found_token, arg, new_pos))) => {
|
2024-09-13 16:02:30 +09:00
|
|
|
current_pos = new_pos;
|
|
|
|
|
if let Token::Flag = found_token {
|
2025-01-07 23:15:18 +09:00
|
|
|
flags.insert(arg.unwrap().raw.into(), None);
|
2024-09-13 16:02:30 +09:00
|
|
|
// don't try matching flags as tree elements
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-05 13:00:06 +09:00
|
|
|
if let Some(arg) = arg.as_ref() {
|
|
|
|
|
// insert arg as paramater if this is a parameter
|
2025-01-07 23:15:18 +09:00
|
|
|
if let Some((param_name, param)) = arg.param.as_ref() {
|
|
|
|
|
params.insert(param_name.to_string(), param.clone());
|
2025-01-05 13:00:06 +09:00
|
|
|
}
|
2025-01-07 23:15:18 +09:00
|
|
|
args.push(arg.raw.to_string());
|
2024-09-13 16:02:30 +09:00
|
|
|
}
|
|
|
|
|
|
2025-01-11 20:25:41 +09:00
|
|
|
if let Some(next_tree) = local_tree.get_branch(&found_token) {
|
2024-09-13 16:02:30 +09:00
|
|
|
local_tree = next_tree.clone();
|
|
|
|
|
} else {
|
|
|
|
|
panic!("found token could not match tree, at {input}");
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-12 04:23:46 +09:00
|
|
|
Some(Err((token, err))) => {
|
|
|
|
|
let error_msg = match err {
|
|
|
|
|
TokenMatchError::MissingParameter { name } => {
|
|
|
|
|
format!("Expected parameter `{name}` in command `{prefix}{input} {token}`.")
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
return CommandResult::Err { error: error_msg };
|
|
|
|
|
}
|
|
|
|
|
None => {
|
2025-01-11 22:38:29 +09:00
|
|
|
if let Some(command) = local_tree.command() {
|
|
|
|
|
println!("{} {params:?}", command.cb);
|
2024-09-13 16:02:30 +09:00
|
|
|
return CommandResult::Ok {
|
|
|
|
|
command: ParsedCommand {
|
2025-01-11 22:38:29 +09:00
|
|
|
command_ref: command.cb.into(),
|
2025-01-05 13:00:06 +09:00
|
|
|
params,
|
2024-09-13 16:02:30 +09:00
|
|
|
args,
|
|
|
|
|
flags,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-01-08 18:31:59 +09:00
|
|
|
|
2025-01-11 22:38:29 +09:00
|
|
|
let mut error = format!("Unknown command `{prefix}{input}`.");
|
|
|
|
|
|
2025-01-12 04:23:46 +09:00
|
|
|
if fmt_possible_commands(&mut error, &prefix, local_tree.possible_commands(2)).not()
|
|
|
|
|
{
|
|
|
|
|
error.push_str(" ");
|
2025-01-11 22:38:29 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
error.push_str(
|
2025-01-12 04:23:46 +09:00
|
|
|
"For a list of all possible commands, see <https://pluralkit.me/commands>.",
|
2025-01-11 22:38:29 +09:00
|
|
|
);
|
2025-01-11 20:25:41 +09:00
|
|
|
|
2024-09-13 16:02:30 +09:00
|
|
|
// todo: check if last token is a common incorrect unquote (multi-member names etc)
|
|
|
|
|
// todo: check if this is a system name in pk;s command
|
2025-01-11 22:38:29 +09:00
|
|
|
return CommandResult::Err { error };
|
2024-09-13 16:02:30 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-04 07:35:04 +09:00
|
|
|
|
|
|
|
|
/// Find the next token from an either raw or partially parsed command string
|
|
|
|
|
///
|
|
|
|
|
/// Returns:
|
2025-01-12 04:23:46 +09:00
|
|
|
/// - nothing (none matched)
|
2025-01-04 07:35:04 +09:00
|
|
|
/// - matched token, to move deeper into the tree
|
|
|
|
|
/// - matched value (if this command matched an user-provided value such as a member name)
|
|
|
|
|
/// - end position of matched token
|
2025-01-12 04:23:46 +09:00
|
|
|
/// - error when matching
|
2025-01-04 07:35:04 +09:00
|
|
|
fn next_token(
|
|
|
|
|
possible_tokens: Vec<Token>,
|
|
|
|
|
input: SmolStr,
|
|
|
|
|
current_pos: usize,
|
2025-01-12 04:23:46 +09:00
|
|
|
) -> Option<Result<(Token, Option<TokenMatchedValue>, usize), (Token, TokenMatchError)>> {
|
2025-01-04 07:35:04 +09:00
|
|
|
// get next parameter, matching quotes
|
|
|
|
|
let param = crate::string::next_param(input.clone(), current_pos);
|
|
|
|
|
println!("matched: {param:?}\n---");
|
|
|
|
|
|
|
|
|
|
// try checking if this is a flag
|
|
|
|
|
// todo!: this breaks full text matching if the full text starts with a flag
|
|
|
|
|
// (but that's kinda already broken anyway)
|
|
|
|
|
if let Some((value, new_pos)) = param.clone()
|
|
|
|
|
&& value.starts_with('-')
|
|
|
|
|
{
|
2025-01-12 04:23:46 +09:00
|
|
|
return Some(Ok((
|
2025-01-04 07:35:04 +09:00
|
|
|
Token::Flag,
|
2025-01-07 23:15:18 +09:00
|
|
|
Some(TokenMatchedValue {
|
|
|
|
|
raw: value,
|
|
|
|
|
param: None,
|
|
|
|
|
}),
|
2025-01-04 07:35:04 +09:00
|
|
|
new_pos,
|
2025-01-12 04:23:46 +09:00
|
|
|
)));
|
2025-01-04 07:35:04 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// iterate over tokens and run try_match
|
|
|
|
|
for token in possible_tokens {
|
2025-01-05 13:00:06 +09:00
|
|
|
// for FullString just send the whole string
|
|
|
|
|
let input_to_match = param.clone().map(|v| v.0);
|
|
|
|
|
match token.try_match(input_to_match) {
|
2025-01-12 04:23:46 +09:00
|
|
|
Some(Ok(value)) => {
|
|
|
|
|
return Some(Ok((
|
|
|
|
|
token,
|
|
|
|
|
value,
|
|
|
|
|
param.map(|v| v.1).unwrap_or(current_pos),
|
2025-01-08 18:31:59 +09:00
|
|
|
)))
|
|
|
|
|
}
|
2025-01-12 04:23:46 +09:00
|
|
|
Some(Err(err)) => {
|
|
|
|
|
return Some(Err((token, err)));
|
|
|
|
|
}
|
|
|
|
|
None => {} // continue matching until we exhaust all tokens
|
2025-01-04 07:35:04 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-12 04:23:46 +09:00
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
mut possible_commands: impl Iterator<Item = &Command>,
|
|
|
|
|
) -> bool {
|
|
|
|
|
if let Some(first) = possible_commands.next() {
|
|
|
|
|
f.push_str(" Perhaps you meant to use one of the commands below:\n");
|
|
|
|
|
for command in std::iter::once(first).chain(possible_commands.take(MAX_SUGGESTIONS - 1)) {
|
|
|
|
|
if !command.show_in_suggestions {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
writeln!(f, "- **{prefix}{command}** - *{}*", command.help).expect("oom");
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
2025-01-04 07:35:04 +09:00
|
|
|
}
|