From 1196d87fe796d0f84c2cf61b0bd6d1763bce7993 Mon Sep 17 00:00:00 2001 From: dusk Date: Thu, 4 Sep 2025 01:22:34 +0300 Subject: [PATCH] feat: implement member proxy commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 9 +- PluralKit.Bot/Commands/MemberProxy.cs | 225 +++++++++++------------ crates/command_definitions/src/member.rs | 74 +++++--- 3 files changed, 159 insertions(+), 149 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index e550a81e..a90ad7f5 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -44,6 +44,11 @@ public partial class CommandTree Commands.MemberServerKeepproxyShow(var param, _) => ctx.Execute(MemberServerKeepProxy, m => m.ShowServerKeepProxy(ctx, param.target)), Commands.MemberServerKeepproxyUpdate(var param, _) => ctx.Execute(MemberServerKeepProxy, m => m.ChangeServerKeepProxy(ctx, param.target, param.value)), Commands.MemberServerKeepproxyClear(var param, var flags) => ctx.Execute(MemberServerKeepProxy, m => m.ClearServerKeepProxy(ctx, param.target, flags.yes)), + Commands.MemberProxyShow(var param, _) => ctx.Execute(MemberProxy, m => m.ShowProxy(ctx, param.target)), + Commands.MemberProxyClear(var param, var flags) => ctx.Execute(MemberProxy, m => m.ClearProxy(ctx, param.target)), + Commands.MemberProxyAdd(var param, _) => ctx.Execute(MemberProxy, m => m.AddProxy(ctx, param.target, param.tag)), + Commands.MemberProxyRemove(var param, _) => ctx.Execute(MemberProxy, m => m.RemoveProxy(ctx, param.target, param.tag)), + Commands.MemberProxySet(var param, _) => ctx.Execute(MemberProxy, m => m.SetProxy(ctx, param.target, param.tags)), Commands.MemberTtsShow(var param, _) => ctx.Execute(MemberTts, m => m.ShowTts(ctx, param.target)), Commands.MemberTtsUpdate(var param, _) => ctx.Execute(MemberTts, m => m.ChangeTts(ctx, param.target, param.value)), Commands.MemberAutoproxyShow(var param, _) => ctx.Execute(MemberAutoproxy, m => m.ShowAutoproxy(ctx, param.target)), @@ -430,9 +435,7 @@ public partial class CommandTree private async Task HandleMemberCommandTargeted(Context ctx, PKMember target) { // Commands that have a member target (eg. pk;member delete) - if (ctx.Match("proxy", "tags", "proxytags", "brackets")) - await ctx.Execute(MemberProxy, m => m.Proxy(ctx, target)); - else if (ctx.Match("avatar", "profile", "picture", "icon", "image", "pfp", "pic")) + if (ctx.Match("avatar", "profile", "picture", "icon", "image", "pfp", "pic")) await ctx.Execute(MemberAvatar, m => m.Avatar(ctx, target)); else if (ctx.Match("proxyavatar", "proxypfp", "webhookavatar", "webhookpfp", "pa", "pavatar", "ppfp")) await ctx.Execute(MemberAvatar, m => m.WebhookAvatar(ctx, target)); diff --git a/PluralKit.Bot/Commands/MemberProxy.cs b/PluralKit.Bot/Commands/MemberProxy.cs index e1bcb19b..d2b128d8 100644 --- a/PluralKit.Bot/Commands/MemberProxy.cs +++ b/PluralKit.Bot/Commands/MemberProxy.cs @@ -6,133 +6,120 @@ namespace PluralKit.Bot; public class MemberProxy { - public async Task Proxy(Context ctx, PKMember target) + public async Task ShowProxy(Context ctx, PKMember target) + { + if (target.ProxyTags.Count == 0) + await ctx.Reply("This member does not have any proxy tags."); + else + await ctx.Reply($"This member's proxy tags are:\n{target.ProxyTagsString("\n")}"); + } + + public async Task ClearProxy(Context ctx, PKMember target) { ctx.CheckSystem().CheckOwnMember(target); - ProxyTag ParseProxyTags(string exampleProxy) + // If we already have multiple tags, this would clear everything, so prompt that + if (target.ProxyTags.Count > 1) { - // // Make sure there's one and only one instance of "text" in the example proxy given - var prefixAndSuffix = exampleProxy.Split("text"); - if (prefixAndSuffix.Length == 1) prefixAndSuffix = prefixAndSuffix[0].Split("TEXT"); - if (prefixAndSuffix.Length < 2) throw Errors.ProxyMustHaveText; - if (prefixAndSuffix.Length > 2) throw Errors.ProxyMultipleText; - return new ProxyTag(prefixAndSuffix[0], prefixAndSuffix[1]); - } - - async Task WarnOnConflict(ProxyTag newTag) - { - var query = "select * from (select *, (unnest(proxy_tags)).prefix as prefix, (unnest(proxy_tags)).suffix as suffix from members where system = @System) as _ where prefix is not distinct from @Prefix and suffix is not distinct from @Suffix and id != @Existing"; - var conflicts = (await ctx.Database.Execute(conn => conn.QueryAsync(query, - new { newTag.Prefix, newTag.Suffix, Existing = target.Id, system = target.System }))).ToList(); - - if (conflicts.Count <= 0) return true; - - var conflictList = conflicts.Select(m => $"- **{m.NameFor(ctx)}**"); - var msg = $"{Emojis.Warn} The following members have conflicting proxy tags:\n{string.Join('\n', conflictList)}\nDo you want to proceed anyway?"; - return await ctx.PromptYesNo(msg, "Proceed"); - } - - // "Sub"command: clear flag - if (ctx.MatchClear()) - { - // If we already have multiple tags, this would clear everything, so prompt that - if (target.ProxyTags.Count > 1) - { - var msg = $"{Emojis.Warn} You already have multiple proxy tags set: {target.ProxyTagsString()}\nDo you want to clear them all?"; - if (!await ctx.PromptYesNo(msg, "Clear")) - throw Errors.GenericCancelled(); - } - - var patch = new MemberPatch { ProxyTags = Partial.Present(new ProxyTag[0]) }; - await ctx.Repository.UpdateMember(target.Id, patch); - - await ctx.Reply($"{Emojis.Success} Proxy tags cleared."); - } - // "Sub"command: no arguments; will print proxy tags - else if (!ctx.HasNext(false)) - { - if (target.ProxyTags.Count == 0) - await ctx.Reply("This member does not have any proxy tags."); - else - await ctx.Reply($"This member's proxy tags are:\n{target.ProxyTagsString("\n")}"); - } - // Subcommand: "add" - else if (ctx.Match("add", "append")) - { - if (!ctx.HasNext(false)) - throw new PKSyntaxError("You must pass an example proxy to add (eg. `[text]` or `J:text`)."); - - var tagToAdd = ParseProxyTags(ctx.RemainderOrNull(false).NormalizeLineEndSpacing()); - if (tagToAdd.IsEmpty) throw Errors.EmptyProxyTags(target, ctx); - if (target.ProxyTags.Contains(tagToAdd)) - throw Errors.ProxyTagAlreadyExists(tagToAdd, target); - if (tagToAdd.ProxyString.Length > Limits.MaxProxyTagLength) - throw new PKError( - $"Proxy tag too long ({tagToAdd.ProxyString.Length} > {Limits.MaxProxyTagLength} characters)."); - - if (!await WarnOnConflict(tagToAdd)) + var msg = $"{Emojis.Warn} You already have multiple proxy tags set: {target.ProxyTagsString()}\nDo you want to clear them all?"; + if (!await ctx.PromptYesNo(msg, "Clear")) throw Errors.GenericCancelled(); - - var newTags = target.ProxyTags.ToList(); - newTags.Add(tagToAdd); - var patch = new MemberPatch { ProxyTags = Partial.Present(newTags.ToArray()) }; - await ctx.Repository.UpdateMember(target.Id, patch); - - await ctx.Reply($"{Emojis.Success} Added proxy tags {tagToAdd.ProxyString.AsCode()} (using {tagToAdd.ProxyString.Length}/{Limits.MaxProxyTagLength} characters)."); } - // Subcommand: "remove" - else if (ctx.Match("remove", "delete")) + + var patch = new MemberPatch { ProxyTags = Partial.Present(new ProxyTag[0]) }; + await ctx.Repository.UpdateMember(target.Id, patch); + + await ctx.Reply($"{Emojis.Success} Proxy tags cleared."); + } + + public async Task AddProxy(Context ctx, PKMember target, string proxyString) + { + ctx.CheckSystem().CheckOwnMember(target); + + var tagToAdd = ParseProxyTag(proxyString); + if (tagToAdd.IsEmpty) throw Errors.EmptyProxyTags(target, ctx); + if (target.ProxyTags.Contains(tagToAdd)) + throw Errors.ProxyTagAlreadyExists(tagToAdd, target); + if (tagToAdd.ProxyString.Length > Limits.MaxProxyTagLength) + throw new PKError( + $"Proxy tag too long ({tagToAdd.ProxyString.Length} > {Limits.MaxProxyTagLength} characters)."); + + if (!await WarnOnConflict(ctx, target, tagToAdd)) + throw Errors.GenericCancelled(); + + var newTags = target.ProxyTags.ToList(); + newTags.Add(tagToAdd); + var patch = new MemberPatch { ProxyTags = Partial.Present(newTags.ToArray()) }; + await ctx.Repository.UpdateMember(target.Id, patch); + + await ctx.Reply($"{Emojis.Success} Added proxy tags {tagToAdd.ProxyString.AsCode()} (using {tagToAdd.ProxyString.Length}/{Limits.MaxProxyTagLength} characters)."); + } + + public async Task RemoveProxy(Context ctx, PKMember target, string proxyString) + { + ctx.CheckSystem().CheckOwnMember(target); + + var tagToRemove = ParseProxyTag(proxyString); + if (tagToRemove.IsEmpty) throw Errors.EmptyProxyTags(target, ctx); + if (!target.ProxyTags.Contains(tagToRemove)) + throw Errors.ProxyTagDoesNotExist(tagToRemove, target); + + var newTags = target.ProxyTags.ToList(); + newTags.Remove(tagToRemove); + var patch = new MemberPatch { ProxyTags = Partial.Present(newTags.ToArray()) }; + await ctx.Repository.UpdateMember(target.Id, patch); + + await ctx.Reply($"{Emojis.Success} Removed proxy tags {tagToRemove.ProxyString.AsCode()}."); + } + + public async Task SetProxy(Context ctx, PKMember target, string proxyString) + { + ctx.CheckSystem().CheckOwnMember(target); + + var requestedTag = ParseProxyTag(proxyString); + if (requestedTag.IsEmpty) throw Errors.EmptyProxyTags(target, ctx); + + if (target.ProxyTags.Count > 1) { - if (!ctx.HasNext(false)) - throw new PKSyntaxError("You must pass a proxy tag to remove (eg. `[text]` or `J:text`)."); - - var remainder = ctx.RemainderOrNull(false); - var tagToRemove = ParseProxyTags(remainder.NormalizeLineEndSpacing()); - if (tagToRemove.IsEmpty) throw Errors.EmptyProxyTags(target, ctx); - if (!target.ProxyTags.Contains(tagToRemove)) - { - // Legacy support for when line endings weren't normalized - tagToRemove = ParseProxyTags(remainder); - if (!target.ProxyTags.Contains(tagToRemove)) - throw Errors.ProxyTagDoesNotExist(tagToRemove, target); - } - - - var newTags = target.ProxyTags.ToList(); - newTags.Remove(tagToRemove); - var patch = new MemberPatch { ProxyTags = Partial.Present(newTags.ToArray()) }; - await ctx.Repository.UpdateMember(target.Id, patch); - - await ctx.Reply($"{Emojis.Success} Removed proxy tags {tagToRemove.ProxyString.AsCode()}."); - } - // Subcommand: bare proxy tag given - else - { - var requestedTag = ParseProxyTags(ctx.RemainderOrNull(false).NormalizeLineEndSpacing()); - if (requestedTag.IsEmpty) throw Errors.EmptyProxyTags(target, ctx); - - // This is mostly a legacy command, so it's gonna warn if there's - // already more than one proxy tag. - if (target.ProxyTags.Count > 1) - { - var msg = $"This member already has more than one proxy tag set: {target.ProxyTagsString()}\nDo you want to replace them?"; - if (!await ctx.PromptYesNo(msg, "Replace")) - throw Errors.GenericCancelled(); - } - - if (requestedTag.ProxyString.Length > Limits.MaxProxyTagLength) - throw new PKError( - $"Proxy tag too long ({requestedTag.ProxyString.Length} > {Limits.MaxProxyTagLength} characters)."); - - if (!await WarnOnConflict(requestedTag)) + var msg = $"This member already has more than one proxy tag set: {target.ProxyTagsString()}\nDo you want to replace them?"; + if (!await ctx.PromptYesNo(msg, "Replace")) throw Errors.GenericCancelled(); - - var newTags = new[] { requestedTag }; - var patch = new MemberPatch { ProxyTags = Partial.Present(newTags) }; - await ctx.Repository.UpdateMember(target.Id, patch); - - await ctx.Reply($"{Emojis.Success} Member proxy tags set to {requestedTag.ProxyString.AsCode()} (using {requestedTag.ProxyString.Length}/{Limits.MaxProxyTagLength} characters)."); } + + if (requestedTag.ProxyString.Length > Limits.MaxProxyTagLength) + throw new PKError( + $"Proxy tag too long ({requestedTag.ProxyString.Length} > {Limits.MaxProxyTagLength} characters)."); + + if (!await WarnOnConflict(ctx, target, requestedTag)) + throw Errors.GenericCancelled(); + + var newTags = new[] { requestedTag }; + var patch = new MemberPatch { ProxyTags = Partial.Present(newTags) }; + await ctx.Repository.UpdateMember(target.Id, patch); + + await ctx.Reply($"{Emojis.Success} Member proxy tags set to {requestedTag.ProxyString.AsCode()} (using {requestedTag.ProxyString.Length}/{Limits.MaxProxyTagLength} characters)."); + } + + private ProxyTag ParseProxyTag(string proxyString) + { + // Make sure there's one and only one instance of "text" in the example proxy given + var prefixAndSuffix = proxyString.Split("text"); + if (prefixAndSuffix.Length == 1) prefixAndSuffix = prefixAndSuffix[0].Split("TEXT"); + if (prefixAndSuffix.Length < 2) throw Errors.ProxyMustHaveText; + if (prefixAndSuffix.Length > 2) throw Errors.ProxyMultipleText; + return new ProxyTag(prefixAndSuffix[0], prefixAndSuffix[1]); + } + + private async Task WarnOnConflict(Context ctx, PKMember target, ProxyTag newTag) + { + var query = "select * from (select *, (unnest(proxy_tags)).prefix as prefix, (unnest(proxy_tags)).suffix as suffix from members where system = @System) as _ where prefix is not distinct from @Prefix and suffix is not distinct from @Suffix and id != @Existing"; + var conflicts = (await ctx.Database.Execute(conn => conn.QueryAsync(query, + new { newTag.Prefix, newTag.Suffix, Existing = target.Id, system = target.System }))).ToList(); + + if (conflicts.Count <= 0) return true; + + var conflictList = conflicts.Select(m => $"- **{m.NameFor(ctx)}**"); + var msg = $"{Emojis.Warn} The following members have conflicting proxy tags:\n{string.Join('\n', conflictList)}\nDo you want to proceed anyway?"; + return await ctx.PromptYesNo(msg, "Proceed"); } } \ No newline at end of file diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index 4268499e..0a13eea1 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -17,20 +17,20 @@ pub fn cmds() -> impl Iterator { let keep_proxy = ("keepproxy", ["kp"]); let server_keep_proxy = ("serverkeepproxy", ["skp"]); let autoproxy = ("autoproxy", ["ap"]); + let proxy = ("proxy", ["tags", "proxytags", "brackets"]); let tts = ("tts", ["texttospeech"]); let delete = ("delete", ["del", "remove"]); - // Group commands by functionality let member_new_cmd = [ command!(member, new, ("name", OpaqueString) => "member_new") .help("Creates a new system member"), - ].into_iter(); + ] + .into_iter(); - let member_info_cmd = [ - command!(member_target => "member_show") - .flag("pt") - .help("Shows information about a member"), - ].into_iter(); + let member_info_cmd = [command!(member_target => "member_show") + .flag("pt") + .help("Shows information about a member")] + .into_iter(); let member_name_cmd = { let member_name = tokens!(member_target, name); @@ -38,7 +38,8 @@ pub fn cmds() -> impl Iterator { command!(member_name => "member_name_show").help("Shows a member's name"), command!(member_name, ("name", OpaqueStringRemainder) => "member_name_update") .help("Changes a member's name"), - ].into_iter() + ] + .into_iter() }; let member_description_cmd = { @@ -50,7 +51,8 @@ pub fn cmds() -> impl Iterator { .help("Clears a member's description"), command!(member_desc, ("description", OpaqueStringRemainder) => "member_desc_update") .help("Changes a member's description"), - ].into_iter() + ] + .into_iter() }; let member_privacy_cmd = { @@ -63,7 +65,8 @@ pub fn cmds() -> impl Iterator { => "member_privacy_update" ) .help("Changes a member's privacy settings"), - ].into_iter() + ] + .into_iter() }; let member_pronouns_cmd = { @@ -82,40 +85,40 @@ pub fn cmds() -> impl Iterator { let member_banner_cmd = { let member_banner = tokens!(member_target, banner); [ - command!(member_banner => "member_banner_show") - .help("Shows a member's banner image"), + command!(member_banner => "member_banner_show").help("Shows a member's banner image"), command!(member_banner, ("banner", Avatar) => "member_banner_update") .help("Changes a member's banner image"), command!(member_banner, ("clear", ["c"]) => "member_banner_clear") .flag(("yes", ["y"])) .help("Clears a member's banner image"), - ].into_iter() + ] + .into_iter() }; let member_color_cmd = { let member_color = tokens!(member_target, color); [ - command!(member_color => "member_color_show") - .help("Shows a member's color"), + command!(member_color => "member_color_show").help("Shows a member's color"), command!(member_color, ("color", OpaqueString) => "member_color_update") .help("Changes a member's color"), command!(member_color, ("clear", ["c"]) => "member_color_clear") .flag(("yes", ["y"])) .help("Clears a member's color"), - ].into_iter() + ] + .into_iter() }; let member_birthday_cmd = { let member_birthday = tokens!(member_target, birthday); [ - command!(member_birthday => "member_birthday_show") - .help("Shows a member's birthday"), + command!(member_birthday => "member_birthday_show").help("Shows a member's birthday"), command!(member_birthday, ("birthday", OpaqueString) => "member_birthday_update") .help("Changes a member's birthday"), command!(member_birthday, ("clear", ["c"]) => "member_birthday_clear") .flag(("yes", ["y"])) .help("Clears a member's birthday"), - ].into_iter() + ] + .into_iter() }; let member_display_name_cmd = { @@ -144,6 +147,23 @@ pub fn cmds() -> impl Iterator { ].into_iter() }; + let member_proxy_cmd = { + let member_proxy = tokens!(member_target, proxy); + [ + command!(member_proxy => "member_proxy_show") + .help("Shows a member's proxy tags"), + command!(member_proxy, ("tags", OpaqueString) => "member_proxy_set") + .help("Sets a member's proxy tags"), + command!(member_proxy, ("add", ["a"]), ("tag", OpaqueString) => "member_proxy_add") + .help("Adds proxy tag to a member"), + command!(member_proxy, ("remove", ["r", "rm"]), ("tag", OpaqueString) => "member_proxy_remove") + .help("Removes proxy tag from a member"), + command!(member_proxy, ("clear", ["c"]) => "member_proxy_clear") + .flag(("yes", ["y"])) + .help("Clears all proxy tags from a member"), + ].into_iter() + }; + let member_proxy_settings_cmd = { let member_keep_proxy = tokens!(member_target, keep_proxy); let member_server_keep_proxy = tokens!(member_target, server_keep_proxy); @@ -174,17 +194,16 @@ pub fn cmds() -> impl Iterator { .help("Shows whether a member can be autoproxied"), command!(member_autoproxy, ("value", Toggle) => "member_autoproxy_update") .help("Changes whether a member can be autoproxied"), - ].into_iter() + ] + .into_iter() }; - let member_delete_cmd = [ - command!(member_target, delete => "member_delete") - .help("Deletes a member"), - ].into_iter(); + let member_delete_cmd = + [command!(member_target, delete => "member_delete").help("Deletes a member")].into_iter(); - let member_easter_eggs = [ - command!(member_target, "soulscream" => "member_soulscream").show_in_suggestions(false), - ].into_iter(); + let member_easter_eggs = + [command!(member_target, "soulscream" => "member_soulscream").show_in_suggestions(false)] + .into_iter(); member_new_cmd .chain(member_info_cmd) @@ -197,6 +216,7 @@ pub fn cmds() -> impl Iterator { .chain(member_birthday_cmd) .chain(member_display_name_cmd) .chain(member_server_name_cmd) + .chain(member_proxy_cmd) .chain(member_proxy_settings_cmd) .chain(member_message_settings_cmd) .chain(member_delete_cmd)