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.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.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...
ctx.Reply(
@ -521,26 +528,8 @@ public partial class CommandTree
private async Task HandleSwitchCommand(Context ctx)
{
if (ctx.Match("out"))
await ctx.Execute<Switch>(SwitchOut, m => m.SwitchOut(ctx));
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);
await PrintCommandNotFoundError(ctx, Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut,
SwitchDelete, SwitchCopy, SystemFronter, SystemFrontHistory);
}
private async Task CommandHelpRoot(Context ctx)

View file

@ -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<PKMember> 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<Parameter?> 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(

View file

@ -8,11 +8,10 @@ namespace PluralKit.Bot;
public class Switch
{
public async Task SwitchDo(Context ctx)
public async Task SwitchDo(Context ctx, ICollection<PKMember> 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<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)))}.");
}
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 <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();
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<PKMember> 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 =

View file

@ -26,8 +26,8 @@ pub fn all() -> impl Iterator<Item = Command> {
.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"]);

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 {
OpaqueString(String),
MemberRef(String),
MemberRefs(Vec<String>),
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, "<target member>"),
ParameterKind::MemberRefs => write!(f, "<member 1> <member 2> <member 3>..."),
ParameterKind::SystemRef => write!(f, "<target system>"),
ParameterKind::GuildRef => write!(f, "<target guild>"),
ParameterKind::MemberPrivacyTarget => write!(f, "<privacy target>"),
@ -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<ParameterValue, SmolStr> {
@ -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 => {

View file

@ -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<PKMember>",
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",

View file

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

View file

@ -23,6 +23,7 @@ pub enum CommandResult {
#[derive(Debug, Clone)]
pub enum Parameter {
MemberRef { member: String },
MemberRefs { members: Vec<String> },
SystemRef { system: String },
GuildRef { guild: String },
MemberPrivacyTarget { target: String },
@ -37,6 +38,7 @@ impl From<ParameterValue> 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 },