feat(commands): add cs codegen to statically use params and flags in bot code, remove Any

This commit is contained in:
dusk 2025-01-21 12:36:54 +09:00
parent 0c012e98b5
commit 07e8a4851a
No known key found for this signature in database
20 changed files with 297 additions and 417 deletions

1
.gitignore vendored
View file

@ -32,6 +32,7 @@ logs/
recipe.json recipe.json
.docker-bin/ .docker-bin/
PluralKit.Bot/commands.cs PluralKit.Bot/commands.cs
PluralKit.Bot/commandtypes.cs
# nix # nix
.nix-process-compose .nix-process-compose

View file

@ -4,24 +4,26 @@ namespace PluralKit.Bot;
public partial class CommandTree public partial class CommandTree
{ {
public Task ExecuteCommand(Context ctx) public Task ExecuteCommand(Context ctx, Commands command)
{ {
return ctx.Parameters.Callback() switch return command switch
{ {
"help" => ctx.Execute<Help>(Help, m => m.HelpRoot(ctx)), Commands.Help => ctx.Execute<Help>(Help, m => m.HelpRoot(ctx)),
"help_commands" => ctx.Reply( Commands.HelpCommands => ctx.Reply(
"For the list of commands, see the website: <https://pluralkit.me/commands>"), "For the list of commands, see the website: <https://pluralkit.me/commands>"),
"help_proxy" => ctx.Reply( Commands.HelpProxy => ctx.Reply(
"The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying"), "The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying"),
"member_show" => ctx.Execute<Member>(MemberInfo, m => m.ViewMember(ctx)), Commands.MemberShow(MemberShowParams param, _) => ctx.Execute<Member>(MemberInfo, m => m.ViewMember(ctx, param.target)),
"member_new" => ctx.Execute<Member>(MemberNew, m => m.NewMember(ctx)), Commands.MemberNew(MemberNewParams param, _) => ctx.Execute<Member>(MemberNew, m => m.NewMember(ctx, param.name)),
"member_soulscream" => ctx.Execute<Member>(MemberInfo, m => m.Soulscream(ctx)), Commands.MemberSoulscream(MemberSoulscreamParams param, _) => ctx.Execute<Member>(MemberInfo, m => m.Soulscream(ctx, param.target)),
"cfg_ap_account_show" => ctx.Execute<Config>(null, m => m.ViewAutoproxyAccount(ctx)), Commands.CfgApAccountShow => ctx.Execute<Config>(null, m => m.ViewAutoproxyAccount(ctx)),
"cfg_ap_account_update" => ctx.Execute<Config>(null, m => m.EditAutoproxyAccount(ctx)), Commands.CfgApAccountUpdate(CfgApAccountUpdateParams param, _) => ctx.Execute<Config>(null, m => m.EditAutoproxyAccount(ctx, param.toggle)),
"cfg_ap_timeout_show" => ctx.Execute<Config>(null, m => m.ViewAutoproxyTimeout(ctx)), Commands.CfgApTimeoutShow => ctx.Execute<Config>(null, m => m.ViewAutoproxyTimeout(ctx)),
"cfg_ap_timeout_update" => ctx.Execute<Config>(null, m => m.EditAutoproxyTimeout(ctx)), Commands.CfgApTimeoutOff => ctx.Execute<Config>(null, m => m.DisableAutoproxyTimeout(ctx)),
"fun_thunder" => ctx.Execute<Fun>(null, m => m.Thunder(ctx)), Commands.CfgApTimeoutReset => ctx.Execute<Config>(null, m => m.ResetAutoproxyTimeout(ctx)),
"fun_meow" => ctx.Execute<Fun>(null, m => m.Meow(ctx)), Commands.CfgApTimeoutUpdate(CfgApTimeoutUpdateParams param, _) => ctx.Execute<Config>(null, m => m.EditAutoproxyTimeout(ctx, param.timeout)),
Commands.FunThunder => ctx.Execute<Fun>(null, m => m.Thunder(ctx)),
Commands.FunMeow => ctx.Execute<Fun>(null, m => m.Meow(ctx)),
_ => _ =>
// this should only ever occur when deving if commands are not implemented... // this should only ever occur when deving if commands are not implemented...
ctx.Reply( ctx.Reply(

View file

@ -197,10 +197,9 @@ public class Config
await ctx.Reply($"Autoproxy is currently **{EnabledDisabled(allowAutoproxy)}** for account <@{ctx.Author.Id}>."); await ctx.Reply($"Autoproxy is currently **{EnabledDisabled(allowAutoproxy)}** for account <@{ctx.Author.Id}>.");
} }
public async Task EditAutoproxyAccount(Context ctx) public async Task EditAutoproxyAccount(Context ctx, bool allow)
{ {
var allowAutoproxy = await ctx.Repository.GetAutoproxyEnabled(ctx.Author.Id); var allowAutoproxy = await ctx.Repository.GetAutoproxyEnabled(ctx.Author.Id);
var allow = await ctx.ParamResolveToggle("toggle") ?? throw new PKSyntaxError("You need to specify whether to enable or disable autoproxy for this account.");
var statusString = EnabledDisabled(allow); var statusString = EnabledDisabled(allow);
if (allowAutoproxy == allow) if (allowAutoproxy == allow)
@ -227,41 +226,44 @@ public class Config
await ctx.Reply($"The current latch timeout duration for your system is {timeout.Value.ToTimeSpan().Humanize(4)}."); await ctx.Reply($"The current latch timeout duration for your system is {timeout.Value.ToTimeSpan().Humanize(4)}.");
} }
public async Task EditAutoproxyTimeout(Context ctx) public async Task DisableAutoproxyTimeout(Context ctx)
{ {
var _newTimeout = await ctx.ParamResolveOpaque("timeout"); await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { LatchTimeout = (int)Duration.Zero.TotalSeconds });
var _reset = await ctx.ParamResolveToggle("reset");
var _toggle = await ctx.ParamResolveToggle("toggle");
Duration? newTimeout; await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out.");
}
public async Task ResetAutoproxyTimeout(Context ctx)
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { LatchTimeout = null });
await ctx.Reply($"{Emojis.Success} Latch timeout reset to default ({ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)}).");
}
public async Task EditAutoproxyTimeout(Context ctx, string timeout)
{
Duration newTimeout;
Duration overflow = Duration.Zero; Duration overflow = Duration.Zero;
if (_toggle == false) newTimeout = Duration.Zero; // todo: we should parse date in the command parser
else if (_reset == true) newTimeout = null; var timeoutStr = timeout;
else var timeoutPeriod = DateUtils.ParsePeriod(timeoutStr)
?? throw new PKError($"Could not parse '{timeoutStr}' as a valid duration. Try using a syntax such as \"3h5m\" (i.e. 3 hours and 5 minutes).");
if (timeoutPeriod.TotalHours > 100000)
{ {
// todo: we should parse date in the command parser // sanity check to prevent seconds overflow if someone types in 999999999
var timeoutStr = _newTimeout; overflow = timeoutPeriod;
var timeoutPeriod = DateUtils.ParsePeriod(timeoutStr); newTimeout = Duration.Zero;
if (timeoutPeriod == null) throw new PKError($"Could not parse '{timeoutStr}' as a valid duration. Try using a syntax such as \"3h5m\" (i.e. 3 hours and 5 minutes).");
if (timeoutPeriod.Value.TotalHours > 100000)
{
// sanity check to prevent seconds overflow if someone types in 999999999
overflow = timeoutPeriod.Value;
newTimeout = Duration.Zero;
}
else newTimeout = timeoutPeriod;
} }
else newTimeout = timeoutPeriod;
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { LatchTimeout = (int?)newTimeout?.TotalSeconds }); await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { LatchTimeout = (int?)newTimeout.TotalSeconds });
if (newTimeout == null) if (newTimeout == Duration.Zero && overflow != Duration.Zero)
await ctx.Reply($"{Emojis.Success} Latch timeout reset to default ({ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)}).");
else if (newTimeout == Duration.Zero && overflow != Duration.Zero)
await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out. ({overflow.ToTimeSpan().Humanize(4)} is too long)"); await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out. ({overflow.ToTimeSpan().Humanize(4)} is too long)");
else if (newTimeout == Duration.Zero) else if (newTimeout == Duration.Zero)
await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out."); await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out.");
else else
await ctx.Reply($"{Emojis.Success} Latch timeout set to {newTimeout.Value!.ToTimeSpan().Humanize(4)}."); await ctx.Reply($"{Emojis.Success} Latch timeout set to {newTimeout.ToTimeSpan().Humanize(4)}.");
} }
public async Task SystemTimezone(Context ctx) public async Task SystemTimezone(Context ctx)

View file

@ -28,10 +28,8 @@ public class Member
_avatarHosting = avatarHosting; _avatarHosting = avatarHosting;
} }
public async Task NewMember(Context ctx) public async Task NewMember(Context ctx, string? memberName)
{ {
var memberName = await ctx.ParamResolveOpaque("name");
if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix); if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix);
memberName = memberName ?? throw new PKSyntaxError("You must pass a member name."); memberName = memberName ?? throw new PKSyntaxError("You must pass a member name.");
@ -122,19 +120,17 @@ public class Member
await ctx.Reply(replyStr); await ctx.Reply(replyStr);
} }
public async Task ViewMember(Context ctx) public async Task ViewMember(Context ctx, PKMember target)
{ {
var target = await ctx.ParamResolveMember("target");
var system = await ctx.Repository.GetSystem(target.System); var system = await ctx.Repository.GetSystem(target.System);
await ctx.Reply( await ctx.Reply(
embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone)); embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone));
} }
public async Task Soulscream(Context ctx) public async Task Soulscream(Context ctx, PKMember target)
{ {
// this is for a meme, please don't take this code seriously. :) // this is for a meme, please don't take this code seriously. :)
var target = await ctx.ParamResolveMember("target");
var name = target.NameFor(ctx.LookupContextFor(target.System)); var name = target.NameFor(ctx.LookupContextFor(target.System));
var encoded = HttpUtility.UrlEncode(name); var encoded = HttpUtility.UrlEncode(name);

View file

@ -159,7 +159,19 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
} }
var ctx = new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, guildConfig, _config.Prefixes ?? BotConfig.DefaultPrefixes, parameters); var ctx = new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, guildConfig, _config.Prefixes ?? BotConfig.DefaultPrefixes, parameters);
await _tree.ExecuteCommand(ctx);
Commands command;
try
{
command = await Commands.FromContext(ctx);
}
catch (PKError e)
{
await ctx.Reply($"{Emojis.Error} {e.Message}");
throw;
}
await _tree.ExecuteCommand(ctx, command);
} }
catch (PKError) catch (PKError)
{ {

View file

@ -1,29 +1,25 @@
use command_parser::parameter;
use super::*; use super::*;
pub fn cmds() -> impl Iterator<Item = Command> { pub fn cmds() -> impl Iterator<Item = Command> {
let cfg = ["config", "cfg"]; let ap = tokens!(["config", "cfg"], ["autoproxy", "ap"]);
let autoproxy = ["autoproxy", "ap"];
let ap_account = concat_tokens!(ap, [["account", "ac"]]);
let ap_timeout = concat_tokens!(ap, [["timeout", "tm"]]);
[ [
command!([cfg, autoproxy, ["account", "ac"]], "cfg_ap_account_show") command!(ap_account => "cfg_ap_account_show")
.help("Shows autoproxy status for the account"), .help("Shows autoproxy status for the account"),
command!( command!(ap_account, Toggle => "cfg_ap_account_update")
[cfg, autoproxy, ["account", "ac"], Toggle], .help("Toggles autoproxy for the account"),
"cfg_ap_account_update" command!(ap_timeout => "cfg_ap_timeout_show").help("Shows the autoproxy timeout"),
) command!(ap_timeout, parameter::RESET => "cfg_ap_timeout_reset")
.help("Toggles autoproxy for the account"), .help("Resets the autoproxy timeout"),
command!([cfg, autoproxy, ["timeout", "tm"]], "cfg_ap_timeout_show") command!(ap_timeout, parameter::DISABLE => "cfg_ap_timeout_off")
.help("Shows the autoproxy timeout"), .help("Disables the autoproxy timeout"),
command!( command!(ap_timeout, ("timeout", OpaqueString) => "cfg_ap_timeout_update")
[ .help("Sets the autoproxy timeout"),
cfg,
autoproxy,
["timeout", "tm"],
any!(Disable, Reset, ("timeout", OpaqueString::SINGLE)) // todo: we should parse duration / time values
],
"cfg_ap_timeout_update"
)
.help("Sets the autoproxy timeout"),
] ]
.into_iter() .into_iter()
} }

View file

@ -2,8 +2,8 @@ use super::*;
pub fn cmds() -> impl Iterator<Item = Command> { pub fn cmds() -> impl Iterator<Item = Command> {
[ [
command!(["thunder"], "fun_thunder"), command!(["thunder"] => "fun_thunder"),
command!(["meow"], "fun_meow"), command!(["meow"] => "fun_meow"),
] ]
.into_iter() .into_iter()
} }

View file

@ -3,9 +3,9 @@ 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!([help], "help").help("Shows the help command"), command!([help] => "help").help("Shows the help command"),
command!([help, "commands"], "help_commands").help("help commands"), command!([help, "commands"] => "help_commands").help("help commands"),
command!([help, "proxy"], "help_proxy").help("help proxy"), command!([help, "proxy"] => "help_proxy").help("help proxy"),
] ]
.into_iter() .into_iter()
} }

View file

@ -18,7 +18,9 @@ pub mod server_config;
pub mod switch; pub mod switch;
pub mod system; pub mod system;
use command_parser::{any, command, command::Command, parameter::*}; use command_parser::{
command, command::Command, concat_tokens, parameter::ParameterKind::*, tokens,
};
pub fn all() -> impl Iterator<Item = Command> { pub fn all() -> impl Iterator<Item = Command> {
(help::cmds()) (help::cmds())

View file

@ -6,38 +6,27 @@ pub fn cmds() -> impl Iterator<Item = Command> {
let privacy = ["privacy", "priv"]; let privacy = ["privacy", "priv"];
let new = ["new", "n"]; let new = ["new", "n"];
let member_target = tokens!(member, MemberRef);
let member_desc = concat_tokens!(member_target, [description]);
let member_privacy = concat_tokens!(member_target, [privacy]);
[ [
command!([member, new, ("name", OpaqueString::SINGLE)], "member_new") command!([member, new, ("name", OpaqueString)] => "member_new")
.help("Creates a new system member"), .help("Creates a new system member"),
command!([member, MemberRef], "member_show") command!(member_target => "member_show")
.help("Shows information about a member") .flag("pt")
.value_flag("pt", Disable), .help("Shows information about a member"),
command!([member, MemberRef, description], "member_desc_show") command!(member_desc => "member_desc_show").help("Shows a member's description"),
.help("Shows a member's description"), command!(member_desc, ("description", OpaqueStringRemainder) => "member_desc_update")
command!( .help("Changes a member's description"),
[ command!(member_privacy => "member_privacy_show")
member,
MemberRef,
description,
("description", OpaqueString::REMAINDER)
],
"member_desc_update"
)
.help("Changes a member's description"),
command!([member, MemberRef, privacy], "member_privacy_show")
.help("Displays a member's current privacy settings"), .help("Displays a member's current privacy settings"),
command!( command!(
[ member_privacy, MemberPrivacyTarget, ("new_privacy_level", PrivacyLevel)
member, => "member_privacy_update"
MemberRef,
privacy,
MemberPrivacyTarget,
("new_privacy_level", PrivacyLevel)
],
"member_privacy_update"
) )
.help("Changes a member's privacy settings"), .help("Changes a member's privacy settings"),
command!([member, MemberRef, "soulscream"], "member_soulscream").show_in_suggestions(false), command!(member_target, "soulscream" => "member_soulscream").show_in_suggestions(false),
] ]
.into_iter() .into_iter()
} }

View file

@ -4,11 +4,13 @@ pub fn cmds() -> impl Iterator<Item = Command> {
let system = ["system", "s"]; let system = ["system", "s"];
let new = ["new", "n"]; let new = ["new", "n"];
let system_new = tokens!(system, new);
[ [
command!([system], "system_show").help("Shows information about your system"), command!([system] => "system_show").help("Shows information about your system"),
command!([system, new], "system_new").help("Creates a new system"), command!(system_new => "system_new").help("Creates a new system"),
command!([system, new, ("name", OpaqueString::SINGLE)], "system_new") command!(system_new, ("name", OpaqueString) => "system_new_name")
.help("Creates a new system"), .help("Creates a new system (using the provided name)"),
] ]
.into_iter() .into_iter()
} }

View file

@ -26,7 +26,7 @@ impl Command {
for (idx, token) in tokens.iter().enumerate().rev() { for (idx, token) in tokens.iter().enumerate().rev() {
match token { match token {
// we want flags to go before any parameters // we want flags to go before any parameters
Token::Parameter(_, _) | Token::Any(_) => { Token::Parameter(_) => {
parse_flags_before = idx; parse_flags_before = idx;
was_parameter = true; was_parameter = true;
} }
@ -62,7 +62,7 @@ impl Command {
self self
} }
pub fn value_flag(mut self, name: impl Into<SmolStr>, value: impl Parameter + 'static) -> Self { pub fn value_flag(mut self, name: impl Into<SmolStr>, value: ParameterKind) -> Self {
self.flags.push(Flag::new(name).with_value(value)); self.flags.push(Flag::new(name).with_value(value));
self self
} }
@ -95,7 +95,27 @@ impl Display for Command {
// (and something like &dyn Trait would require everything to be referenced which doesnt look nice anyway) // (and something like &dyn Trait would require everything to be referenced which doesnt look nice anyway)
#[macro_export] #[macro_export]
macro_rules! command { macro_rules! command {
([$($v:expr),+], $cb:expr$(,)*) => { ([$($v:expr),+] => $cb:expr$(,)*) => {
$crate::command::Command::new([$($crate::token::Token::from($v)),*], $cb) $crate::command::Command::new($crate::tokens!($($v),+), $cb)
};
($tokens:expr => $cb:expr$(,)*) => {
$crate::command::Command::new($tokens.clone(), $cb)
};
($tokens:expr, $($v:expr),+ => $cb:expr$(,)*) => {
$crate::command::Command::new($crate::concat_tokens!($tokens.clone(), [$($v),+]), $cb)
};
}
#[macro_export]
macro_rules! tokens {
($($v:expr),+$(,)*) => {
[$($crate::token::Token::from($v)),+]
};
}
#[macro_export]
macro_rules! concat_tokens {
($tokens:expr, [$($v:expr),+]$(,)*) => {
$tokens.clone().into_iter().chain($crate::tokens!($($v),+).into_iter()).collect::<Vec<_>>()
}; };
} }

View file

@ -1,8 +1,8 @@
use std::{fmt::Display, sync::Arc}; use std::fmt::Display;
use smol_str::SmolStr; use smol_str::SmolStr;
use crate::parameter::{Parameter, ParameterValue}; use crate::parameter::{ParameterKind, ParameterValue};
#[derive(Debug)] #[derive(Debug)]
pub enum FlagValueMatchError { pub enum FlagValueMatchError {
@ -13,7 +13,7 @@ pub enum FlagValueMatchError {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Flag { pub struct Flag {
name: SmolStr, name: SmolStr,
value: Option<Arc<dyn Parameter>>, value: Option<ParameterKind>,
} }
impl Display for Flag { impl Display for Flag {
@ -42,8 +42,8 @@ impl Flag {
} }
} }
pub fn with_value(mut self, param: impl Parameter + 'static) -> Self { pub fn with_value(mut self, param: ParameterKind) -> Self {
self.value = Some(Arc::new(param)); self.value = Some(param);
self self
} }
@ -51,13 +51,17 @@ impl Flag {
&self.name &self.name
} }
pub fn value_kind(&self) -> Option<ParameterKind> {
self.value
}
pub fn try_match(&self, input_name: &str, input_value: Option<&str>) -> TryMatchFlagResult { pub fn try_match(&self, input_name: &str, input_value: Option<&str>) -> TryMatchFlagResult {
// if not matching flag then skip anymore matching // if not matching flag then skip anymore matching
if self.name != input_name { if self.name != input_name {
return None; return None;
} }
// get token to try matching with, if flag doesn't have one then that means it is matched (it is without any value) // get token to try matching with, if flag doesn't have one then that means it is matched (it is without any value)
let Some(value) = self.value.as_deref() else { let Some(value) = self.value.as_ref() else {
return Some(Ok(None)); return Some(Ok(None));
}; };
// check if we have a non-empty flag value, we return error if not (because flag requested a value) // check if we have a non-empty flag value, we return error if not (because flag requested a value)

View file

@ -77,21 +77,6 @@ pub fn parse_command(
TokenMatchError::MissingParameter { name } => { TokenMatchError::MissingParameter { name } => {
format!("Expected parameter `{name}` in command `{prefix}{input} {token}`.") format!("Expected parameter `{name}` in command `{prefix}{input} {token}`.")
} }
TokenMatchError::MissingAny { tokens } => {
let mut msg = format!("Expected one of ");
for (idx, token) in tokens.iter().enumerate() {
write!(&mut msg, "`{token}`").expect("oom");
if idx < tokens.len() - 1 {
if tokens.len() > 2 && idx == tokens.len() - 2 {
msg.push_str(" or ");
} else {
msg.push_str(", ");
}
}
}
write!(&mut msg, " in command `{prefix}{input} {token}`.").expect("oom");
msg
}
TokenMatchError::ParameterMatchError { input: raw, msg } => { TokenMatchError::ParameterMatchError { input: raw, msg } => {
format!("Parameter `{raw}` in command `{prefix}{input}` could not be parsed: {msg}.") format!("Parameter `{raw}` in command `{prefix}{input}` could not be parsed: {msg}.")
} }
@ -254,12 +239,9 @@ fn next_token<'a>(
// iterate over tokens and run try_match // iterate over tokens and run try_match
for token in possible_tokens { for token in possible_tokens {
let is_match_remaining_token = let is_match_remaining_token =
|token: &Token| matches!(token, Token::Parameter(_, param) if param.remainder()); |token: &Token| matches!(token, Token::Parameter(param) if param.kind().remainder());
// check if this is a token that matches the rest of the input // check if this is a token that matches the rest of the input
let match_remaining = is_match_remaining_token(token) let match_remaining = is_match_remaining_token(token);
// check for Any here if it has a "match remainder" token in it
// if there is a "match remainder" token in a command there shouldn't be a command descending from that
|| matches!(token, Token::Any(ref tokens) if tokens.iter().any(is_match_remaining_token));
// either use matched param or rest of the input if matching remaining // either use matched param or rest of the input if matching remaining
let input_to_match = matched.as_ref().map(|v| { let input_to_match = matched.as_ref().map(|v| {
match_remaining match_remaining

View file

@ -2,89 +2,108 @@ use std::{fmt::Debug, str::FromStr};
use smol_str::SmolStr; use smol_str::SmolStr;
use crate::token::ParamName;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ParameterValue { pub enum ParameterValue {
OpaqueString(String),
MemberRef(String), MemberRef(String),
SystemRef(String), SystemRef(String),
MemberPrivacyTarget(String), MemberPrivacyTarget(String),
PrivacyLevel(String), PrivacyLevel(String),
OpaqueString(String),
Toggle(bool), Toggle(bool),
} }
pub trait Parameter: Debug + Send + Sync { #[derive(Debug, Clone, PartialEq, Eq, Hash)]
fn remainder(&self) -> bool { pub struct Parameter {
false name: SmolStr,
} kind: ParameterKind,
fn default_name(&self) -> ParamName;
fn format(&self, f: &mut std::fmt::Formatter, name: &str) -> std::fmt::Result;
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr>;
} }
#[derive(Debug, Clone, Eq, Hash, PartialEq)] impl Parameter {
pub struct OpaqueString(bool); pub fn name(&self) -> &str {
&self.name
impl OpaqueString {
pub const SINGLE: Self = Self(false);
pub const REMAINDER: Self = Self(true);
}
impl Parameter for OpaqueString {
fn remainder(&self) -> bool {
self.0
} }
fn default_name(&self) -> ParamName { pub fn kind(&self) -> ParameterKind {
"string" self.kind
}
fn format(&self, f: &mut std::fmt::Formatter, name: &str) -> std::fmt::Result {
write!(f, "[{name}]")
}
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
Ok(ParameterValue::OpaqueString(input.into()))
} }
} }
#[derive(Debug, Clone, Eq, Hash, PartialEq)] impl From<ParameterKind> for Parameter {
pub struct MemberRef; fn from(value: ParameterKind) -> Self {
Parameter {
impl Parameter for MemberRef { name: value.default_name().into(),
fn default_name(&self) -> ParamName { kind: value,
"member" }
}
fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result {
write!(f, "<target member>")
}
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
Ok(ParameterValue::MemberRef(input.into()))
} }
} }
#[derive(Debug, Clone, Eq, Hash, PartialEq)] impl From<(&str, ParameterKind)> for Parameter {
pub struct SystemRef; fn from((name, kind): (&str, ParameterKind)) -> Self {
Parameter {
impl Parameter for SystemRef { name: name.into(),
fn default_name(&self) -> ParamName { kind,
"system" }
}
fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result {
write!(f, "<target system>")
}
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
Ok(ParameterValue::SystemRef(input.into()))
} }
} }
#[derive(Debug, Clone, Eq, Hash, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MemberPrivacyTarget; pub enum ParameterKind {
OpaqueString,
OpaqueStringRemainder,
MemberRef,
SystemRef,
MemberPrivacyTarget,
PrivacyLevel,
Toggle,
}
impl ParameterKind {
pub(crate) fn default_name(&self) -> &str {
match self {
ParameterKind::OpaqueString => "string",
ParameterKind::OpaqueStringRemainder => "string",
ParameterKind::MemberRef => "target",
ParameterKind::SystemRef => "target",
ParameterKind::MemberPrivacyTarget => "member_privacy_target",
ParameterKind::PrivacyLevel => "privacy_level",
ParameterKind::Toggle => "toggle",
}
}
pub(crate) fn remainder(&self) -> bool {
matches!(self, ParameterKind::OpaqueStringRemainder)
}
pub(crate) fn format(&self, f: &mut std::fmt::Formatter, param_name: &str) -> std::fmt::Result {
match self {
ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => {
write!(f, "[{param_name}]")
}
ParameterKind::MemberRef => write!(f, "<target member>"),
ParameterKind::SystemRef => write!(f, "<target system>"),
ParameterKind::MemberPrivacyTarget => write!(f, "<privacy target>"),
ParameterKind::PrivacyLevel => write!(f, "[privacy level]"),
ParameterKind::Toggle => write!(f, "on/off"),
}
}
pub(crate) fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
match self {
ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => {
Ok(ParameterValue::OpaqueString(input.into()))
}
ParameterKind::MemberRef => Ok(ParameterValue::MemberRef(input.into())),
ParameterKind::SystemRef => Ok(ParameterValue::SystemRef(input.into())),
ParameterKind::MemberPrivacyTarget => MemberPrivacyTargetKind::from_str(input)
.map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into())),
ParameterKind::PrivacyLevel => PrivacyLevelKind::from_str(input)
.map(|level| ParameterValue::PrivacyLevel(level.as_ref().into())),
ParameterKind::Toggle => {
Toggle::from_str(input).map(|t| ParameterValue::Toggle(t.into()))
}
}
}
}
pub enum MemberPrivacyTargetKind { pub enum MemberPrivacyTargetKind {
Visibility, Visibility,
@ -135,24 +154,6 @@ impl FromStr for MemberPrivacyTargetKind {
} }
} }
impl Parameter for MemberPrivacyTarget {
fn default_name(&self) -> ParamName {
"member_privacy_target"
}
fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result {
write!(f, "<privacy target>")
}
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
MemberPrivacyTargetKind::from_str(input)
.map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into()))
}
}
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
pub struct PrivacyLevel;
pub enum PrivacyLevelKind { pub enum PrivacyLevelKind {
Public, Public,
Private, Private,
@ -179,140 +180,34 @@ impl FromStr for PrivacyLevelKind {
} }
} }
impl Parameter for PrivacyLevel { pub const ENABLE: [&str; 5] = ["on", "yes", "true", "enable", "enabled"];
fn default_name(&self) -> ParamName { pub const DISABLE: [&str; 5] = ["off", "no", "false", "disable", "disabled"];
"privacy_level"
}
fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result {
write!(f, "[privacy level]")
}
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
PrivacyLevelKind::from_str(input)
.map(|level| ParameterValue::PrivacyLevel(level.as_ref().into()))
}
}
#[derive(Debug, Clone, Eq, Hash, PartialEq)] #[derive(Debug, Clone, Eq, Hash, PartialEq)]
pub struct Reset; pub enum Toggle {
On,
impl AsRef<str> for Reset { Off,
fn as_ref(&self) -> &str {
"reset"
}
} }
impl FromStr for Reset { impl FromStr for Toggle {
type Err = SmolStr; type Err = SmolStr;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s { match s {
"reset" | "clear" | "default" => Ok(Self), ref s if ENABLE.contains(s) => Ok(Self::On),
_ => Err("not reset".into()), ref s if DISABLE.contains(s) => Ok(Self::Off),
_ => Err("invalid toggle, must be on/off".into()),
} }
} }
} }
impl Parameter for Reset { impl Into<bool> for Toggle {
fn default_name(&self) -> ParamName {
"reset"
}
fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result {
write!(f, "reset")
}
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
Self::from_str(input).map(|_| ParameterValue::Toggle(true))
}
}
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
pub struct Toggle;
impl Parameter for Toggle {
fn default_name(&self) -> ParamName {
"toggle"
}
fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result {
write!(f, "on/off")
}
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
Enable::from_str(input)
.map(Into::<bool>::into)
.or_else(|_| Disable::from_str(input).map(Into::<bool>::into))
.map(ParameterValue::Toggle)
.map_err(|_| "invalid toggle".into())
}
}
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
pub struct Enable;
impl FromStr for Enable {
type Err = SmolStr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"on" | "yes" | "true" | "enable" | "enabled" => Ok(Self),
_ => Err("invalid enable".into()),
}
}
}
impl Parameter for Enable {
fn default_name(&self) -> ParamName {
"enable"
}
fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result {
write!(f, "on")
}
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
Self::from_str(input).map(|e| ParameterValue::Toggle(e.into()))
}
}
impl Into<bool> for Enable {
fn into(self) -> bool { fn into(self) -> bool {
true match self {
} Toggle::On => true,
} Toggle::Off => false,
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
pub struct Disable;
impl FromStr for Disable {
type Err = SmolStr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"off" | "no" | "false" | "disable" | "disabled" => Ok(Self),
_ => Err("invalid disable".into()),
} }
} }
} }
impl Into<bool> for Disable { pub const RESET: [&str; 3] = ["reset", "clear", "default"];
fn into(self) -> bool {
false
}
}
impl Parameter for Disable {
fn default_name(&self) -> ParamName {
"disable"
}
fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result {
write!(f, "off")
}
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
Self::from_str(input).map(|e| ParameterValue::Toggle(e.into()))
}
}

View file

@ -1,76 +1,35 @@
use std::{ use std::{
fmt::{Debug, Display}, fmt::{Debug, Display},
hash::Hash,
ops::Not, ops::Not,
sync::Arc,
}; };
use smol_str::SmolStr; use smol_str::SmolStr;
use crate::parameter::{Parameter, ParameterValue}; use crate::parameter::{Parameter, ParameterKind, ParameterValue};
pub type ParamName = &'static str; #[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone)]
pub enum Token { pub enum Token {
/// Token used to represent a finished command (i.e. no more parameters required) /// Token used to represent a finished command (i.e. no more parameters required)
// todo: this is likely not the right way to represent this // todo: this is likely not the right way to represent this
Empty, Empty,
/// multi-token matching
/// todo: FullString tokens don't work properly in this (they don't get passed the rest of the input)
Any(Vec<Token>),
/// A bot-defined command / subcommand (usually) (eg. "member" in `pk;member MyName`) /// A bot-defined command / subcommand (usually) (eg. "member" in `pk;member MyName`)
Value(Vec<SmolStr>), Value(Vec<SmolStr>),
/// A parameter that must be provided a value /// A parameter that must be provided a value
Parameter(ParamName, Arc<dyn Parameter>), Parameter(Parameter),
}
#[macro_export]
macro_rules! any {
($($t:expr),+) => {
$crate::token::Token::Any(vec![$($crate::token::Token::from($t)),+])
};
}
impl PartialEq for Token {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Any(l0), Self::Any(r0)) => l0 == r0,
(Self::Value(l0), Self::Value(r0)) => l0 == r0,
(Self::Parameter(l0, _), Self::Parameter(r0, _)) => l0 == r0,
(Self::Empty, Self::Empty) => true,
_ => false,
}
}
}
impl Eq for Token {}
impl Hash for Token {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
core::mem::discriminant(self).hash(state);
match self {
Token::Empty => {}
Token::Any(vec) => vec.hash(state),
Token::Value(vec) => vec.hash(state),
Token::Parameter(name, _) => name.hash(state),
}
}
} }
#[derive(Debug)] #[derive(Debug)]
pub enum TokenMatchError { pub enum TokenMatchError {
ParameterMatchError { input: SmolStr, msg: SmolStr }, ParameterMatchError { input: SmolStr, msg: SmolStr },
MissingParameter { name: ParamName }, MissingParameter { name: SmolStr },
MissingAny { tokens: Vec<Token> },
} }
#[derive(Debug)] #[derive(Debug)]
pub(super) struct TokenMatchValue { pub(super) struct TokenMatchValue {
pub raw: SmolStr, pub raw: SmolStr,
pub param: Option<(ParamName, ParameterValue)>, pub param: Option<(SmolStr, ParameterValue)>,
} }
impl TokenMatchValue { impl TokenMatchValue {
@ -83,12 +42,12 @@ impl TokenMatchValue {
fn new_match_param( fn new_match_param(
raw: impl Into<SmolStr>, raw: impl Into<SmolStr>,
param_name: ParamName, param_name: impl Into<SmolStr>,
param: ParameterValue, param: ParameterValue,
) -> TryMatchResult { ) -> TryMatchResult {
Some(Ok(Some(Self { Some(Ok(Some(Self {
raw: raw.into(), raw: raw.into(),
param: Some((param_name, param)), param: Some((param_name.into(), param)),
}))) })))
} }
} }
@ -113,14 +72,9 @@ impl Token {
// empty token // empty token
Self::Empty => Some(Ok(None)), Self::Empty => Some(Ok(None)),
// missing paramaters // missing paramaters
Self::Parameter(name, _) => { Self::Parameter(param) => Some(Err(TokenMatchError::MissingParameter {
Some(Err(TokenMatchError::MissingParameter { name })) name: param.name().into(),
} })),
Self::Any(tokens) => tokens.is_empty().then_some(None).unwrap_or_else(|| {
Some(Err(TokenMatchError::MissingAny {
tokens: tokens.clone(),
}))
}),
// everything else doesnt match if no input anyway // everything else doesnt match if no input anyway
Self::Value(_) => None, Self::Value(_) => None,
// don't add a _ match here! // don't add a _ match here!
@ -132,18 +86,13 @@ impl Token {
// try actually matching stuff // try actually matching stuff
match self { match self {
Self::Empty => None, Self::Empty => None,
Self::Any(tokens) => tokens
.iter()
.map(|t| t.try_match(Some(input)))
.find(|r| !matches!(r, None))
.unwrap_or(None),
Self::Value(values) => values Self::Value(values) => values
.iter() .iter()
.any(|v| v.eq(input)) .any(|v| v.eq(input))
.then(|| TokenMatchValue::new_match(input)) .then(|| TokenMatchValue::new_match(input))
.unwrap_or(None), .unwrap_or(None),
Self::Parameter(name, param) => match param.match_value(input) { Self::Parameter(param) => match param.kind().match_value(input) {
Ok(matched) => TokenMatchValue::new_match_param(input, name, matched), Ok(matched) => TokenMatchValue::new_match_param(input, param.name(), matched),
Err(err) => Some(Err(TokenMatchError::ParameterMatchError { Err(err) => Some(Err(TokenMatchError::ParameterMatchError {
input: input.into(), input: input.into(),
msg: err, msg: err,
@ -157,19 +106,9 @@ impl Display for Token {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Token::Empty => write!(f, ""), Token::Empty => write!(f, ""),
Token::Any(vec) => {
write!(f, "(")?;
for (i, token) in vec.iter().enumerate() {
if i != 0 {
write!(f, "|")?;
}
write!(f, "{}", token)?;
}
write!(f, ")")
}
Token::Value(vec) if vec.is_empty().not() => write!(f, "{}", vec.first().unwrap()), Token::Value(vec) if vec.is_empty().not() => write!(f, "{}", vec.first().unwrap()),
Token::Value(_) => Ok(()), // if value token has no values (lol), don't print anything Token::Value(_) => Ok(()), // if value token has no values (lol), don't print anything
Token::Parameter(name, param) => param.format(f, name), Token::Parameter(param) => param.kind().format(f, param.name()),
} }
} }
} }
@ -186,14 +125,20 @@ impl<const L: usize> From<[&str; L]> for Token {
} }
} }
impl<P: Parameter + 'static> From<P> for Token { impl From<Parameter> for Token {
fn from(value: P) -> Self { fn from(value: Parameter) -> Self {
Token::Parameter(value.default_name(), Arc::new(value)) Token::Parameter(value)
} }
} }
impl<P: Parameter + 'static> From<(ParamName, P)> for Token { impl From<ParameterKind> for Token {
fn from(value: (ParamName, P)) -> Self { fn from(value: ParameterKind) -> Self {
Token::Parameter(value.0, Arc::new(value.1)) Token::from(Parameter::from(value))
}
}
impl From<(&str, ParameterKind)> for Token {
fn from(value: (&str, ParameterKind)) -> Self {
Token::from(Parameter::from(value))
} }
} }

View file

@ -38,15 +38,15 @@ impl TreeBranch {
); );
} }
pub(super) fn command(&self) -> Option<Command> { pub fn command(&self) -> Option<Command> {
self.current_command.clone() self.current_command.clone()
} }
pub(super) fn possible_tokens(&self) -> impl Iterator<Item = &Token> { pub fn possible_tokens(&self) -> impl Iterator<Item = &Token> {
self.branches.keys() self.branches.keys()
} }
pub(super) fn possible_commands(&self, max_depth: usize) -> impl Iterator<Item = &Command> { pub fn possible_commands(&self, max_depth: usize) -> impl Iterator<Item = &Command> {
// dusk: i am too lazy to write an iterator for this without using recursion so we box everything // dusk: i am too lazy to write an iterator for this without using recursion so we box everything
fn box_iter<'a>( fn box_iter<'a>(
iter: impl Iterator<Item = &'a Command> + 'a, iter: impl Iterator<Item = &'a Command> + 'a,
@ -69,7 +69,11 @@ impl TreeBranch {
commands commands
} }
pub(super) fn get_branch(&self, token: &Token) -> Option<&TreeBranch> { pub fn get_branch(&self, token: &Token) -> Option<&Self> {
self.branches.get(token) self.branches.get(token)
} }
pub fn branches(&self) -> impl Iterator<Item = (&Token, &Self)> {
self.branches.iter()
}
} }

View file

@ -2,6 +2,11 @@
name = "commands" name = "commands"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
default-run = "commands"
[[bin]]
name = "write_cs_glue"
path = "src/bin/write_cs_glue.rs"
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]

View file

@ -1,5 +1,8 @@
#![feature(iter_intersperse)] #![feature(iter_intersperse)]
use command_parser::{token::Token, Tree};
use commands::COMMAND_TREE;
fn main() { fn main() {
let cmd = std::env::args() let cmd = std::env::args()
.skip(1) .skip(1)
@ -18,3 +21,21 @@ fn main() {
} }
} }
} }
fn print_tree(tree: &Tree, depth: usize) {
println!();
for (token, branch) in tree.branches() {
for _ in 0..depth {
print!(" ");
}
for _ in 0..depth {
print!("-");
}
print!("> {token:?}");
if matches!(token, Token::Empty) {
println!(": {}", branch.command().unwrap().cb)
} else {
print_tree(branch, depth + 1)
}
}
}

View file

@ -96,6 +96,8 @@
cp -f "$commandslib" obj/ cp -f "$commandslib" obj/
fi fi
uniffi-bindgen-cs "$commandslib" --library --out-dir="''${2:-./PluralKit.Bot}" uniffi-bindgen-cs "$commandslib" --library --out-dir="''${2:-./PluralKit.Bot}"
cargo run --package commands --bin write_cs_glue -- "''${2:-./PluralKit.Bot}"/commandtypes.cs
dotnet format ./PluralKit.Bot/PluralKit.Bot.csproj
''; '';
}; };
}; };