diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 663f506b..d2891529 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -152,6 +152,13 @@ public partial class CommandTree Commands.SystemShowPrivacy(var param, _) => ctx.Execute(SystemPrivacy, m => m.ShowSystemPrivacy(ctx, ctx.System)), Commands.SystemChangePrivacyAll(var param, _) => ctx.Execute(SystemPrivacy, m => m.ChangeSystemPrivacyAll(ctx, ctx.System, param.level)), Commands.SystemChangePrivacy(var param, _) => ctx.Execute(SystemPrivacy, m => m.ChangeSystemPrivacy(ctx, ctx.System, param.privacy, param.level)), + Commands.SwitchOut(_, _) => ctx.Execute(SwitchOut, m => m.SwitchOut(ctx)), + Commands.SwitchDo(var param, _) => ctx.Execute(Switch, m => m.SwitchDo(ctx, param.targets)), + Commands.SwitchMove(var param, _) => ctx.Execute(SwitchMove, m => m.SwitchMove(ctx, param.@string)), + Commands.SwitchEdit(var param, var flags) => ctx.Execute(SwitchEdit, m => m.SwitchEdit(ctx, param.targets, false, flags.first, flags.remove, flags.append, flags.prepend)), + Commands.SwitchEditOut(_, _) => ctx.Execute(SwitchEditOut, m => m.SwitchEditOut(ctx)), + Commands.SwitchDelete(var param, var flags) => ctx.Execute(SwitchDelete, m => m.SwitchDelete(ctx, flags.all)), + Commands.SwitchCopy(var param, var flags) => ctx.Execute(SwitchCopy, m => m.SwitchEdit(ctx, param.targets, true, flags.first, flags.remove, flags.append, flags.prepend)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -521,26 +528,8 @@ public partial class CommandTree private async Task HandleSwitchCommand(Context ctx) { - if (ctx.Match("out")) - await ctx.Execute(SwitchOut, m => m.SwitchOut(ctx)); - else if (ctx.Match("move", "m", "shift", "offset")) - await ctx.Execute(SwitchMove, m => m.SwitchMove(ctx)); - else if (ctx.Match("edit", "e", "replace")) - if (ctx.Match("out")) - await ctx.Execute(SwitchEditOut, m => m.SwitchEditOut(ctx)); - else - await ctx.Execute(SwitchEdit, m => m.SwitchEdit(ctx)); - else if (ctx.Match("delete", "remove", "erase", "cancel", "yeet")) - await ctx.Execute(SwitchDelete, m => m.SwitchDelete(ctx)); - else if (ctx.Match("copy", "add", "duplicate", "dupe")) - await ctx.Execute(SwitchCopy, m => m.SwitchEdit(ctx, true)); - else if (ctx.Match("commands", "help")) - await PrintCommandList(ctx, "switching", SwitchCommands); - else if (ctx.HasNext()) // there are following arguments - await ctx.Execute(Switch, m => m.SwitchDo(ctx)); - else - await PrintCommandNotFoundError(ctx, Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut, - SwitchDelete, SwitchCopy, SystemFronter, SystemFrontHistory); + await PrintCommandNotFoundError(ctx, Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut, + SwitchDelete, SwitchCopy, SystemFronter, SystemFrontHistory); } private async Task CommandHelpRoot(Context ctx) diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index 28656ce6..2c753fde 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -1,3 +1,4 @@ +using Humanizer; using Myriad.Types; using PluralKit.Core; using uniffi.commands; @@ -8,6 +9,7 @@ namespace PluralKit.Bot; public abstract record Parameter() { public record MemberRef(PKMember member): Parameter; + public record MemberRefs(List members): Parameter; public record SystemRef(PKSystem system): Parameter; public record GuildRef(Guild guild): Parameter; public record MemberPrivacyTarget(MemberPrivacySubject target): Parameter; @@ -56,14 +58,21 @@ public class Parameters private async Task ResolveFfiParam(Context ctx, uniffi.commands.Parameter ffi_param) { + var byId = HasFlag("id", "by-id"); switch (ffi_param) { case uniffi.commands.Parameter.MemberRef memberRef: - var byId = HasFlag("id", "by-id"); return new Parameter.MemberRef( await ctx.ParseMember(memberRef.member, byId) ?? throw new PKError(ctx.CreateNotFoundError("Member", memberRef.member, byId)) ); + case uniffi.commands.Parameter.MemberRefs memberRefs: + return new Parameter.MemberRefs( + await memberRefs.members.ToAsyncEnumerable().SelectAwait(async m => + await ctx.ParseMember(m, byId) + ?? throw new PKError(ctx.CreateNotFoundError("Member", m, byId)) + ).ToListAsync() + ); case uniffi.commands.Parameter.SystemRef systemRef: // todo: do we need byId here? return new Parameter.SystemRef( diff --git a/PluralKit.Bot/Commands/Switch.cs b/PluralKit.Bot/Commands/Switch.cs index 83465bd4..624774da 100644 --- a/PluralKit.Bot/Commands/Switch.cs +++ b/PluralKit.Bot/Commands/Switch.cs @@ -8,11 +8,10 @@ namespace PluralKit.Bot; public class Switch { - public async Task SwitchDo(Context ctx) + public async Task SwitchDo(Context ctx, ICollection members) { ctx.CheckSystem(); - var members = await ctx.ParseMemberList(ctx.System.Id); await DoSwitchCommand(ctx, members); } @@ -21,7 +20,7 @@ public class Switch ctx.CheckSystem(); // Switch with no members = switch-out - await DoSwitchCommand(ctx, new PKMember[] { }); + await DoSwitchCommand(ctx, []); } private async Task DoSwitchCommand(Context ctx, ICollection members) @@ -57,12 +56,10 @@ public class Switch $"{Emojis.Success} Switch registered. Current fronters are now {string.Join(", ", members.Select(m => m.NameFor(ctx)))}."); } - public async Task SwitchMove(Context ctx) + public async Task SwitchMove(Context ctx, string timeToMove) { ctx.CheckSystem(); - var timeToMove = ctx.RemainderOrNull() ?? - throw new PKSyntaxError("Must pass a date or time to move the switch to."); var tz = TzdbDateTimeZoneSource.Default.ForId(ctx.Config?.UiTz ?? "UTC"); var result = DateUtils.ParseDateTime(timeToMove, true, tz); @@ -104,31 +101,29 @@ public class Switch await ctx.Reply($"{Emojis.Success} Switch moved to ({newSwitchDeltaStr} ago)."); } - public async Task SwitchEdit(Context ctx, bool newSwitch = false) + public async Task SwitchEdit(Context ctx, List newMembers, bool newSwitch = false, bool first = false, bool remove = false, bool append = false, bool prepend = false) { ctx.CheckSystem(); - var newMembers = await ctx.ParseMemberList(ctx.System.Id); - await using var conn = await ctx.Database.Obtain(); var currentSwitch = await ctx.Repository.GetLatestSwitch(ctx.System.Id); if (currentSwitch == null) throw Errors.NoRegisteredSwitches; var currentSwitchMembers = await ctx.Repository.GetSwitchMembers(conn, currentSwitch.Id).ToListAsync().AsTask(); - if (ctx.MatchFlag("first", "f")) + if (first) newMembers = FirstInSwitch(newMembers[0], currentSwitchMembers); - else if (ctx.MatchFlag("remove", "r")) + else if (remove) newMembers = RemoveFromSwitch(newMembers, currentSwitchMembers); - else if (ctx.MatchFlag("append", "a")) + else if (append) newMembers = AppendToSwitch(newMembers, currentSwitchMembers); - else if (ctx.MatchFlag("prepend", "p")) + else if (prepend) newMembers = PrependToSwitch(newMembers, currentSwitchMembers); if (newSwitch) { // if there's no edit flag, assume we're appending - if (!ctx.MatchFlag("first", "f", "remove", "r", "append", "a", "prepend", "p")) + if (!prepend && !append && !remove && !first) newMembers = AppendToSwitch(newMembers, currentSwitchMembers); await DoSwitchCommand(ctx, newMembers); } @@ -172,7 +167,7 @@ public class Switch public async Task SwitchEditOut(Context ctx) { ctx.CheckSystem(); - await DoEditCommand(ctx, new PKMember[] { }); + await DoEditCommand(ctx, []); } public async Task DoEditCommand(Context ctx, ICollection members) @@ -217,11 +212,11 @@ public class Switch await ctx.Reply($"{Emojis.Success} Switch edited. Current fronters are now {newSwitchMemberStr}."); } - public async Task SwitchDelete(Context ctx) + public async Task SwitchDelete(Context ctx, bool all) { ctx.CheckSystem(); - if (ctx.Match("all", "clear") || ctx.MatchFlag("all", "clear", "c")) + if (all) { // Subcommand: "delete all" var purgeMsg = diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index b7336a3e..28715d5f 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -26,8 +26,8 @@ pub fn all() -> impl Iterator { .chain(member::cmds()) .chain(config::cmds()) .chain(fun::cmds()) - .map(|cmd| cmd.flag(("plaintext", ["pt"]))) - .map(|cmd| cmd.flag(("raw", ["r"]))) + .chain(switch::cmds()) + .map(|cmd| cmd.flag(("plaintext", ["pt"])).flag(("raw", ["r"]))) } pub const RESET: (&str, [&str; 2]) = ("reset", ["clear", "default"]); diff --git a/crates/command_definitions/src/switch.rs b/crates/command_definitions/src/switch.rs index 8b137891..fce7f760 100644 --- a/crates/command_definitions/src/switch.rs +++ b/crates/command_definitions/src/switch.rs @@ -1 +1,31 @@ +use super::*; +pub fn cmds() -> impl Iterator { + let switch = ("switch", ["sw"]); + + let edit = ("edit", ["e", "replace"]); + let r#move = ("move", ["m", "shift", "offset"]); + let delete = ("delete", ["remove", "erase", "cancel", "yeet"]); + let copy = ("copy", ["add", "duplicate", "dupe"]); + let out = "out"; + + [ + command!(switch, out => "switch_out"), + command!(switch, r#move, OpaqueString => "switch_move"), // TODO: datetime parsing + command!(switch, delete => "switch_delete").flag(("all", ["clear", "c"])), + command!(switch, edit, out => "switch_edit_out"), + command!(switch, edit, MemberRefs => "switch_edit") + .flag(("first", ["f"])) + .flag(("remove", ["r"])) + .flag(("append", ["a"])) + .flag(("prepend", ["p"])), + command!(switch, copy, MemberRefs => "switch_copy") + .flag(("first", ["f"])) + .flag(("remove", ["r"])) + .flag(("append", ["a"])) + .flag(("prepend", ["p"])), + command!(switch, ("commands", ["help"]) => "switch_commands"), + command!(switch, MemberRefs => "switch_do"), + ] + .into_iter() +} diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index 282cfdc8..08971d99 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -11,6 +11,7 @@ use crate::token::{Token, TokenMatchResult}; pub enum ParameterValue { OpaqueString(String), MemberRef(String), + MemberRefs(Vec), SystemRef(String), GuildRef(String), MemberPrivacyTarget(String), @@ -39,10 +40,14 @@ impl Parameter { impl Display for Parameter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.kind { - ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => { + ParameterKind::OpaqueString => { write!(f, "[{}]", self.name) } + ParameterKind::OpaqueStringRemainder => { + write!(f, "[{}]...", self.name) + } ParameterKind::MemberRef => write!(f, ""), + ParameterKind::MemberRefs => write!(f, " ..."), ParameterKind::SystemRef => write!(f, ""), ParameterKind::GuildRef => write!(f, ""), ParameterKind::MemberPrivacyTarget => write!(f, ""), @@ -77,6 +82,7 @@ pub enum ParameterKind { OpaqueString, OpaqueStringRemainder, MemberRef, + MemberRefs, SystemRef, GuildRef, MemberPrivacyTarget, @@ -92,6 +98,7 @@ impl ParameterKind { ParameterKind::OpaqueString => "string", ParameterKind::OpaqueStringRemainder => "string", ParameterKind::MemberRef => "target", + ParameterKind::MemberRefs => "targets", ParameterKind::SystemRef => "target", ParameterKind::GuildRef => "target", ParameterKind::MemberPrivacyTarget => "member_privacy_target", @@ -103,7 +110,10 @@ impl ParameterKind { } pub(crate) fn remainder(&self) -> bool { - matches!(self, ParameterKind::OpaqueStringRemainder) + matches!( + self, + ParameterKind::OpaqueStringRemainder | ParameterKind::MemberRefs + ) } pub(crate) fn match_value(&self, input: &str) -> Result { @@ -113,12 +123,14 @@ impl ParameterKind { Ok(ParameterValue::OpaqueString(input.into())) } ParameterKind::MemberRef => Ok(ParameterValue::MemberRef(input.into())), + ParameterKind::MemberRefs => Ok(ParameterValue::MemberRefs( + input.split(' ').map(|s| s.trim().to_string()).collect(), + )), ParameterKind::SystemRef => Ok(ParameterValue::SystemRef(input.into())), ParameterKind::MemberPrivacyTarget => MemberPrivacyTargetKind::from_str(input) .map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into())), - ParameterKind::SystemPrivacyTarget => SystemPrivacyTargetKind::from_str(input).map( - |target| ParameterValue::SystemPrivacyTarget(target.as_ref().into()), - ), + ParameterKind::SystemPrivacyTarget => SystemPrivacyTargetKind::from_str(input) + .map(|target| ParameterValue::SystemPrivacyTarget(target.as_ref().into())), ParameterKind::PrivacyLevel => PrivacyLevelKind::from_str(input) .map(|level| ParameterValue::PrivacyLevel(level.as_ref().into())), ParameterKind::Toggle => { diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/bin/write_cs_glue.rs index dbee8d80..0559de88 100644 --- a/crates/commands/src/bin/write_cs_glue.rs +++ b/crates/commands/src/bin/write_cs_glue.rs @@ -167,6 +167,7 @@ fn get_param_ty(kind: ParameterKind) -> &'static str { match kind { ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => "string", ParameterKind::MemberRef => "PKMember", + ParameterKind::MemberRefs => "List", ParameterKind::SystemRef => "PKSystem", ParameterKind::MemberPrivacyTarget => "MemberPrivacySubject", ParameterKind::SystemPrivacyTarget => "SystemPrivacySubject", @@ -181,6 +182,7 @@ fn get_param_param_ty(kind: ParameterKind) -> &'static str { match kind { ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => "Opaque", ParameterKind::MemberRef => "Member", + ParameterKind::MemberRefs => "Members", ParameterKind::SystemRef => "System", ParameterKind::MemberPrivacyTarget => "MemberPrivacyTarget", ParameterKind::SystemPrivacyTarget => "SystemPrivacyTarget", diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index 5a368266..1a8da927 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -9,6 +9,7 @@ interface CommandResult { [Enum] interface Parameter { MemberRef(string member); + MemberRefs(sequence members); SystemRef(string system); GuildRef(string guild); MemberPrivacyTarget(string target); diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 92ca7e4f..bc79e0ca 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -23,6 +23,7 @@ pub enum CommandResult { #[derive(Debug, Clone)] pub enum Parameter { MemberRef { member: String }, + MemberRefs { members: Vec }, SystemRef { system: String }, GuildRef { guild: String }, MemberPrivacyTarget { target: String }, @@ -37,6 +38,7 @@ impl From for Parameter { fn from(value: ParameterValue) -> Self { match value { ParameterValue::MemberRef(member) => Self::MemberRef { member }, + ParameterValue::MemberRefs(members) => Self::MemberRefs { members }, ParameterValue::SystemRef(system) => Self::SystemRef { system }, ParameterValue::MemberPrivacyTarget(target) => Self::MemberPrivacyTarget { target }, ParameterValue::SystemPrivacyTarget(target) => Self::SystemPrivacyTarget { target },