feat: implement switch commands

This commit is contained in:
dusk 2025-09-24 21:32:42 +03:00
parent 15191171f5
commit 10dd499835
No known key found for this signature in database
9 changed files with 85 additions and 45 deletions

View file

@ -152,6 +152,13 @@ public partial class CommandTree
Commands.SystemShowPrivacy(var param, _) => ctx.Execute<SystemEdit>(SystemPrivacy, m => m.ShowSystemPrivacy(ctx, ctx.System)), Commands.SystemShowPrivacy(var param, _) => ctx.Execute<SystemEdit>(SystemPrivacy, m => m.ShowSystemPrivacy(ctx, ctx.System)),
Commands.SystemChangePrivacyAll(var param, _) => ctx.Execute<SystemEdit>(SystemPrivacy, m => m.ChangeSystemPrivacyAll(ctx, ctx.System, param.level)), Commands.SystemChangePrivacyAll(var param, _) => ctx.Execute<SystemEdit>(SystemPrivacy, m => m.ChangeSystemPrivacyAll(ctx, ctx.System, param.level)),
Commands.SystemChangePrivacy(var param, _) => ctx.Execute<SystemEdit>(SystemPrivacy, m => m.ChangeSystemPrivacy(ctx, ctx.System, param.privacy, param.level)), Commands.SystemChangePrivacy(var param, _) => ctx.Execute<SystemEdit>(SystemPrivacy, m => m.ChangeSystemPrivacy(ctx, ctx.System, param.privacy, param.level)),
Commands.SwitchOut(_, _) => ctx.Execute<Switch>(SwitchOut, m => m.SwitchOut(ctx)),
Commands.SwitchDo(var param, _) => ctx.Execute<Switch>(Switch, m => m.SwitchDo(ctx, param.targets)),
Commands.SwitchMove(var param, _) => ctx.Execute<Switch>(SwitchMove, m => m.SwitchMove(ctx, param.@string)),
Commands.SwitchEdit(var param, var flags) => ctx.Execute<Switch>(SwitchEdit, m => m.SwitchEdit(ctx, param.targets, false, flags.first, flags.remove, flags.append, flags.prepend)),
Commands.SwitchEditOut(_, _) => ctx.Execute<Switch>(SwitchEditOut, m => m.SwitchEditOut(ctx)),
Commands.SwitchDelete(var param, var flags) => ctx.Execute<Switch>(SwitchDelete, m => m.SwitchDelete(ctx, flags.all)),
Commands.SwitchCopy(var param, var flags) => ctx.Execute<Switch>(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... // this should only ever occur when deving if commands are not implemented...
ctx.Reply( ctx.Reply(
@ -521,26 +528,8 @@ public partial class CommandTree
private async Task HandleSwitchCommand(Context ctx) private async Task HandleSwitchCommand(Context ctx)
{ {
if (ctx.Match("out")) await PrintCommandNotFoundError(ctx, Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut,
await ctx.Execute<Switch>(SwitchOut, m => m.SwitchOut(ctx)); SwitchDelete, SwitchCopy, SystemFronter, SystemFrontHistory);
else if (ctx.Match("move", "m", "shift", "offset"))
await ctx.Execute<Switch>(SwitchMove, m => m.SwitchMove(ctx));
else if (ctx.Match("edit", "e", "replace"))
if (ctx.Match("out"))
await ctx.Execute<Switch>(SwitchEditOut, m => m.SwitchEditOut(ctx));
else
await ctx.Execute<Switch>(SwitchEdit, m => m.SwitchEdit(ctx));
else if (ctx.Match("delete", "remove", "erase", "cancel", "yeet"))
await ctx.Execute<Switch>(SwitchDelete, m => m.SwitchDelete(ctx));
else if (ctx.Match("copy", "add", "duplicate", "dupe"))
await ctx.Execute<Switch>(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>(Switch, m => m.SwitchDo(ctx));
else
await PrintCommandNotFoundError(ctx, Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut,
SwitchDelete, SwitchCopy, SystemFronter, SystemFrontHistory);
} }
private async Task CommandHelpRoot(Context ctx) private async Task CommandHelpRoot(Context ctx)

View file

@ -1,3 +1,4 @@
using Humanizer;
using Myriad.Types; using Myriad.Types;
using PluralKit.Core; using PluralKit.Core;
using uniffi.commands; using uniffi.commands;
@ -8,6 +9,7 @@ namespace PluralKit.Bot;
public abstract record Parameter() public abstract record Parameter()
{ {
public record MemberRef(PKMember member): Parameter; public record MemberRef(PKMember member): Parameter;
public record MemberRefs(List<PKMember> members): Parameter;
public record SystemRef(PKSystem system): Parameter; public record SystemRef(PKSystem system): Parameter;
public record GuildRef(Guild guild): Parameter; public record GuildRef(Guild guild): Parameter;
public record MemberPrivacyTarget(MemberPrivacySubject target): Parameter; public record MemberPrivacyTarget(MemberPrivacySubject target): Parameter;
@ -56,14 +58,21 @@ public class Parameters
private async Task<Parameter?> ResolveFfiParam(Context ctx, uniffi.commands.Parameter ffi_param) private async Task<Parameter?> ResolveFfiParam(Context ctx, uniffi.commands.Parameter ffi_param)
{ {
var byId = HasFlag("id", "by-id");
switch (ffi_param) switch (ffi_param)
{ {
case uniffi.commands.Parameter.MemberRef memberRef: case uniffi.commands.Parameter.MemberRef memberRef:
var byId = HasFlag("id", "by-id");
return new Parameter.MemberRef( return new Parameter.MemberRef(
await ctx.ParseMember(memberRef.member, byId) await ctx.ParseMember(memberRef.member, byId)
?? throw new PKError(ctx.CreateNotFoundError("Member", 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: case uniffi.commands.Parameter.SystemRef systemRef:
// todo: do we need byId here? // todo: do we need byId here?
return new Parameter.SystemRef( return new Parameter.SystemRef(

View file

@ -8,11 +8,10 @@ namespace PluralKit.Bot;
public class Switch public class Switch
{ {
public async Task SwitchDo(Context ctx) public async Task SwitchDo(Context ctx, ICollection<PKMember> members)
{ {
ctx.CheckSystem(); ctx.CheckSystem();
var members = await ctx.ParseMemberList(ctx.System.Id);
await DoSwitchCommand(ctx, members); await DoSwitchCommand(ctx, members);
} }
@ -21,7 +20,7 @@ public class Switch
ctx.CheckSystem(); ctx.CheckSystem();
// Switch with no members = switch-out // Switch with no members = switch-out
await DoSwitchCommand(ctx, new PKMember[] { }); await DoSwitchCommand(ctx, []);
} }
private async Task DoSwitchCommand(Context ctx, ICollection<PKMember> members) private async Task DoSwitchCommand(Context ctx, ICollection<PKMember> members)
@ -57,12 +56,10 @@ public class Switch
$"{Emojis.Success} Switch registered. Current fronters are now {string.Join(", ", members.Select(m => m.NameFor(ctx)))}."); $"{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(); 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 tz = TzdbDateTimeZoneSource.Default.ForId(ctx.Config?.UiTz ?? "UTC");
var result = DateUtils.ParseDateTime(timeToMove, true, tz); var result = DateUtils.ParseDateTime(timeToMove, true, tz);
@ -104,31 +101,29 @@ public class Switch
await ctx.Reply($"{Emojis.Success} Switch moved to <t:{newSwitchTime}> ({newSwitchDeltaStr} ago)."); await ctx.Reply($"{Emojis.Success} Switch moved to <t:{newSwitchTime}> ({newSwitchDeltaStr} ago).");
} }
public async Task SwitchEdit(Context ctx, bool newSwitch = false) public async Task SwitchEdit(Context ctx, List<PKMember> newMembers, bool newSwitch = false, bool first = false, bool remove = false, bool append = false, bool prepend = false)
{ {
ctx.CheckSystem(); ctx.CheckSystem();
var newMembers = await ctx.ParseMemberList(ctx.System.Id);
await using var conn = await ctx.Database.Obtain(); await using var conn = await ctx.Database.Obtain();
var currentSwitch = await ctx.Repository.GetLatestSwitch(ctx.System.Id); var currentSwitch = await ctx.Repository.GetLatestSwitch(ctx.System.Id);
if (currentSwitch == null) if (currentSwitch == null)
throw Errors.NoRegisteredSwitches; throw Errors.NoRegisteredSwitches;
var currentSwitchMembers = await ctx.Repository.GetSwitchMembers(conn, currentSwitch.Id).ToListAsync().AsTask(); var currentSwitchMembers = await ctx.Repository.GetSwitchMembers(conn, currentSwitch.Id).ToListAsync().AsTask();
if (ctx.MatchFlag("first", "f")) if (first)
newMembers = FirstInSwitch(newMembers[0], currentSwitchMembers); newMembers = FirstInSwitch(newMembers[0], currentSwitchMembers);
else if (ctx.MatchFlag("remove", "r")) else if (remove)
newMembers = RemoveFromSwitch(newMembers, currentSwitchMembers); newMembers = RemoveFromSwitch(newMembers, currentSwitchMembers);
else if (ctx.MatchFlag("append", "a")) else if (append)
newMembers = AppendToSwitch(newMembers, currentSwitchMembers); newMembers = AppendToSwitch(newMembers, currentSwitchMembers);
else if (ctx.MatchFlag("prepend", "p")) else if (prepend)
newMembers = PrependToSwitch(newMembers, currentSwitchMembers); newMembers = PrependToSwitch(newMembers, currentSwitchMembers);
if (newSwitch) if (newSwitch)
{ {
// if there's no edit flag, assume we're appending // 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); newMembers = AppendToSwitch(newMembers, currentSwitchMembers);
await DoSwitchCommand(ctx, newMembers); await DoSwitchCommand(ctx, newMembers);
} }
@ -172,7 +167,7 @@ public class Switch
public async Task SwitchEditOut(Context ctx) public async Task SwitchEditOut(Context ctx)
{ {
ctx.CheckSystem(); ctx.CheckSystem();
await DoEditCommand(ctx, new PKMember[] { }); await DoEditCommand(ctx, []);
} }
public async Task DoEditCommand(Context ctx, ICollection<PKMember> members) public async Task DoEditCommand(Context ctx, ICollection<PKMember> members)
@ -217,11 +212,11 @@ public class Switch
await ctx.Reply($"{Emojis.Success} Switch edited. Current fronters are now {newSwitchMemberStr}."); 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(); ctx.CheckSystem();
if (ctx.Match("all", "clear") || ctx.MatchFlag("all", "clear", "c")) if (all)
{ {
// Subcommand: "delete all" // Subcommand: "delete all"
var purgeMsg = var purgeMsg =

View file

@ -26,8 +26,8 @@ pub fn all() -> impl Iterator<Item = Command> {
.chain(member::cmds()) .chain(member::cmds())
.chain(config::cmds()) .chain(config::cmds())
.chain(fun::cmds()) .chain(fun::cmds())
.map(|cmd| cmd.flag(("plaintext", ["pt"]))) .chain(switch::cmds())
.map(|cmd| cmd.flag(("raw", ["r"]))) .map(|cmd| cmd.flag(("plaintext", ["pt"])).flag(("raw", ["r"])))
} }
pub const RESET: (&str, [&str; 2]) = ("reset", ["clear", "default"]); pub const RESET: (&str, [&str; 2]) = ("reset", ["clear", "default"]);

View file

@ -1 +1,31 @@
use super::*;
pub fn cmds() -> impl Iterator<Item = Command> {
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()
}

View file

@ -11,6 +11,7 @@ use crate::token::{Token, TokenMatchResult};
pub enum ParameterValue { pub enum ParameterValue {
OpaqueString(String), OpaqueString(String),
MemberRef(String), MemberRef(String),
MemberRefs(Vec<String>),
SystemRef(String), SystemRef(String),
GuildRef(String), GuildRef(String),
MemberPrivacyTarget(String), MemberPrivacyTarget(String),
@ -39,10 +40,14 @@ impl Parameter {
impl Display for Parameter { impl Display for Parameter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.kind { match self.kind {
ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => { ParameterKind::OpaqueString => {
write!(f, "[{}]", self.name) write!(f, "[{}]", self.name)
} }
ParameterKind::OpaqueStringRemainder => {
write!(f, "[{}]...", self.name)
}
ParameterKind::MemberRef => write!(f, "<target member>"), ParameterKind::MemberRef => write!(f, "<target member>"),
ParameterKind::MemberRefs => write!(f, "<member 1> <member 2> <member 3>..."),
ParameterKind::SystemRef => write!(f, "<target system>"), ParameterKind::SystemRef => write!(f, "<target system>"),
ParameterKind::GuildRef => write!(f, "<target guild>"), ParameterKind::GuildRef => write!(f, "<target guild>"),
ParameterKind::MemberPrivacyTarget => write!(f, "<privacy target>"), ParameterKind::MemberPrivacyTarget => write!(f, "<privacy target>"),
@ -77,6 +82,7 @@ pub enum ParameterKind {
OpaqueString, OpaqueString,
OpaqueStringRemainder, OpaqueStringRemainder,
MemberRef, MemberRef,
MemberRefs,
SystemRef, SystemRef,
GuildRef, GuildRef,
MemberPrivacyTarget, MemberPrivacyTarget,
@ -92,6 +98,7 @@ impl ParameterKind {
ParameterKind::OpaqueString => "string", ParameterKind::OpaqueString => "string",
ParameterKind::OpaqueStringRemainder => "string", ParameterKind::OpaqueStringRemainder => "string",
ParameterKind::MemberRef => "target", ParameterKind::MemberRef => "target",
ParameterKind::MemberRefs => "targets",
ParameterKind::SystemRef => "target", ParameterKind::SystemRef => "target",
ParameterKind::GuildRef => "target", ParameterKind::GuildRef => "target",
ParameterKind::MemberPrivacyTarget => "member_privacy_target", ParameterKind::MemberPrivacyTarget => "member_privacy_target",
@ -103,7 +110,10 @@ impl ParameterKind {
} }
pub(crate) fn remainder(&self) -> bool { 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<ParameterValue, SmolStr> { pub(crate) fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
@ -113,12 +123,14 @@ impl ParameterKind {
Ok(ParameterValue::OpaqueString(input.into())) Ok(ParameterValue::OpaqueString(input.into()))
} }
ParameterKind::MemberRef => Ok(ParameterValue::MemberRef(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::SystemRef => Ok(ParameterValue::SystemRef(input.into())),
ParameterKind::MemberPrivacyTarget => MemberPrivacyTargetKind::from_str(input) ParameterKind::MemberPrivacyTarget => MemberPrivacyTargetKind::from_str(input)
.map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into())), .map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into())),
ParameterKind::SystemPrivacyTarget => SystemPrivacyTargetKind::from_str(input).map( ParameterKind::SystemPrivacyTarget => SystemPrivacyTargetKind::from_str(input)
|target| ParameterValue::SystemPrivacyTarget(target.as_ref().into()), .map(|target| ParameterValue::SystemPrivacyTarget(target.as_ref().into())),
),
ParameterKind::PrivacyLevel => PrivacyLevelKind::from_str(input) ParameterKind::PrivacyLevel => PrivacyLevelKind::from_str(input)
.map(|level| ParameterValue::PrivacyLevel(level.as_ref().into())), .map(|level| ParameterValue::PrivacyLevel(level.as_ref().into())),
ParameterKind::Toggle => { ParameterKind::Toggle => {

View file

@ -167,6 +167,7 @@ fn get_param_ty(kind: ParameterKind) -> &'static str {
match kind { match kind {
ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => "string", ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => "string",
ParameterKind::MemberRef => "PKMember", ParameterKind::MemberRef => "PKMember",
ParameterKind::MemberRefs => "List<PKMember>",
ParameterKind::SystemRef => "PKSystem", ParameterKind::SystemRef => "PKSystem",
ParameterKind::MemberPrivacyTarget => "MemberPrivacySubject", ParameterKind::MemberPrivacyTarget => "MemberPrivacySubject",
ParameterKind::SystemPrivacyTarget => "SystemPrivacySubject", ParameterKind::SystemPrivacyTarget => "SystemPrivacySubject",
@ -181,6 +182,7 @@ fn get_param_param_ty(kind: ParameterKind) -> &'static str {
match kind { match kind {
ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => "Opaque", ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => "Opaque",
ParameterKind::MemberRef => "Member", ParameterKind::MemberRef => "Member",
ParameterKind::MemberRefs => "Members",
ParameterKind::SystemRef => "System", ParameterKind::SystemRef => "System",
ParameterKind::MemberPrivacyTarget => "MemberPrivacyTarget", ParameterKind::MemberPrivacyTarget => "MemberPrivacyTarget",
ParameterKind::SystemPrivacyTarget => "SystemPrivacyTarget", ParameterKind::SystemPrivacyTarget => "SystemPrivacyTarget",

View file

@ -9,6 +9,7 @@ interface CommandResult {
[Enum] [Enum]
interface Parameter { interface Parameter {
MemberRef(string member); MemberRef(string member);
MemberRefs(sequence<string> members);
SystemRef(string system); SystemRef(string system);
GuildRef(string guild); GuildRef(string guild);
MemberPrivacyTarget(string target); MemberPrivacyTarget(string target);

View file

@ -23,6 +23,7 @@ pub enum CommandResult {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Parameter { pub enum Parameter {
MemberRef { member: String }, MemberRef { member: String },
MemberRefs { members: Vec<String> },
SystemRef { system: String }, SystemRef { system: String },
GuildRef { guild: String }, GuildRef { guild: String },
MemberPrivacyTarget { target: String }, MemberPrivacyTarget { target: String },
@ -37,6 +38,7 @@ impl From<ParameterValue> for Parameter {
fn from(value: ParameterValue) -> Self { fn from(value: ParameterValue) -> Self {
match value { match value {
ParameterValue::MemberRef(member) => Self::MemberRef { member }, ParameterValue::MemberRef(member) => Self::MemberRef { member },
ParameterValue::MemberRefs(members) => Self::MemberRefs { members },
ParameterValue::SystemRef(system) => Self::SystemRef { system }, ParameterValue::SystemRef(system) => Self::SystemRef { system },
ParameterValue::MemberPrivacyTarget(target) => Self::MemberPrivacyTarget { target }, ParameterValue::MemberPrivacyTarget(target) => Self::MemberPrivacyTarget { target },
ParameterValue::SystemPrivacyTarget(target) => Self::SystemPrivacyTarget { target }, ParameterValue::SystemPrivacyTarget(target) => Self::SystemPrivacyTarget { target },