From 95fc7e9f60d4047e5d736ea116ddcb5fdc9e8587 Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 30 Sep 2025 18:45:35 +0000 Subject: [PATCH] implement parse list options and related commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 142 +++++++++--------- PluralKit.Bot/Commands/GroupMember.cs | 10 +- PluralKit.Bot/Commands/Groups.cs | 6 +- .../Commands/Lists/ContextListExt.cs | 92 +----------- PluralKit.Bot/Commands/Lists/ListOptions.cs | 1 + PluralKit.Bot/Commands/Random.cs | 8 +- PluralKit.Bot/Commands/SystemList.cs | 8 +- crates/command_definitions/src/group.rs | 31 +++- crates/command_definitions/src/lib.rs | 3 + crates/command_definitions/src/member.rs | 16 +- crates/command_definitions/src/random.rs | 4 +- crates/command_definitions/src/system.rs | 35 ++++- crates/command_definitions/src/utils.rs | 52 +++++++ crates/command_parser/src/command.rs | 45 ++++-- crates/command_parser/src/flag.rs | 16 +- crates/command_parser/src/lib.rs | 2 +- crates/commands/src/bin/write_cs_glue.rs | 93 +++++++++++- crates/commands/src/main.rs | 2 +- 18 files changed, 367 insertions(+), 199 deletions(-) create mode 100644 crates/command_definitions/src/utils.rs diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 036df4bb..85cf1ac2 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -170,11 +170,21 @@ public partial class CommandTree flags.group ? ctx.Execute(GroupRandom, m => m.Group(ctx, param.target, flags.all, flags.show_embed)) : ctx.Execute(MemberRandom, m => m.Member(ctx, param.target, flags.all, flags.show_embed)), - Commands.GroupRandomMember(var param, var flags) => ctx.Execute(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags.all, flags.show_embed)), + Commands.GroupRandomMember(var param, var flags) => ctx.Execute(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags)), Commands.SystemLink => ctx.Execute(Link, m => m.LinkSystem(ctx)), Commands.SystemUnlink(var param, _) => ctx.Execute(Unlink, m => m.UnlinkAccount(ctx, param.target)), - Commands.MembersList => ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System)), - Commands.SystemMembersList(var param, _) => ctx.Execute(SystemList, m => m.MemberList(ctx, param.target)), + Commands.SystemMembersListSelf(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System, null, flags)), + Commands.SystemMembersSearchSelf(var param, var flags) => ctx.Execute(SystemFind, m => m.MemberList(ctx, ctx.System, param.query, flags)), + Commands.SystemMembersList(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, param.target, null, flags)), + Commands.SystemMembersSearch(var param, var flags) => ctx.Execute(SystemFind, m => m.MemberList(ctx, param.target, param.query, flags)), + Commands.MemberListGroups(var param, var flags) => ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, param.target, null, flags)), + Commands.MemberSearchGroups(var param, var flags) => ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, param.target, param.query, flags)), + Commands.GroupListMembers(var param, var flags) => ctx.Execute(GroupMemberList, m => m.ListGroupMembers(ctx, param.target, null, flags)), + Commands.GroupSearchMembers(var param, var flags) => ctx.Execute(GroupMemberList, m => m.ListGroupMembers(ctx, param.target, param.query, flags)), + Commands.SystemListGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, param.target, null, flags)), + Commands.SystemSearchGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, param.target, param.query, flags)), + Commands.GroupListGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, ctx.System, null, flags)), + Commands.GroupSearchGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, ctx.System, param.query, flags)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -196,8 +206,6 @@ public partial class CommandTree return HandleConfigCommand(ctx); if (ctx.Match("serverconfig", "guildconfig", "scfg")) return HandleServerConfigCommand(ctx); - if (ctx.Match("list", "find", "members", "search", "query", "l", "f", "fd", "ls")) - return ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System)); if (ctx.Match("token")) if (ctx.Match("refresh", "renew", "invalidate", "reroll", "regen")) return ctx.Execute(TokenRefresh, m => m.RefreshToken(ctx)); @@ -412,20 +420,13 @@ public partial class CommandTree private async Task HandleSystemCommandTargeted(Context ctx, PKSystem target) { - if (ctx.Match("find", "search", "query", "fd", "s")) - await ctx.CheckSystem(target).Execute(SystemFind, m => m.MemberList(ctx, target)); // TODO: this lmao (ParseListOptions) - else if (ctx.Match("groups", "gs")) - await ctx.CheckSystem(target).Execute(GroupList, g => g.ListSystemGroups(ctx, target)); - else if (ctx.Match("id")) + if (ctx.Match("id")) await ctx.CheckSystem(target).Execute(SystemId, m => m.DisplayId(ctx, target)); } private async Task HandleMemberCommand(Context ctx) { - // TODO: implement - if (ctx.Match("list")) - await ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System)); - else if (ctx.Match("commands", "help")) + if (ctx.Match("commands", "help")) await PrintCommandList(ctx, "members", MemberCommands); else if (await ctx.MatchMember() is PKMember target) await HandleMemberCommandTargeted(ctx, target); @@ -447,72 +448,63 @@ public partial class CommandTree else if (ctx.Match("remove", "rem")) await ctx.Execute(MemberGroupRemove, m => m.AddRemoveGroups(ctx, target, Groups.AddRemoveOperation.Remove)); + else if (ctx.Match("id")) + await ctx.Execute(MemberId, m => m.DisplayId(ctx, target)); else - await ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, target)); - else if (ctx.Match("id")) - await ctx.Execute(MemberId, m => m.DisplayId(ctx, target)); - else - await PrintCommandNotFoundError(ctx, MemberInfo, MemberRename, MemberDisplayName, MemberServerName, - MemberDesc, MemberPronouns, MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar, - SystemList); + await PrintCommandNotFoundError(ctx, MemberInfo, MemberRename, MemberDisplayName, MemberServerName, + MemberDesc, MemberPronouns, MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar, + SystemList); } private async Task HandleGroupCommand(Context ctx) { - // TODO: implement - // // Commands with no group argument - // if (ctx.Match("n", "new")) - // await ctx.Execute(GroupNew, g => g.CreateGroup(ctx)); - // else if (ctx.Match("list", "l")) - // await ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, null)); - // else if (ctx.Match("commands", "help")) - // await PrintCommandList(ctx, "groups", GroupCommands); - // else if (await ctx.MatchGroup() is { } target) - // { - // // Commands with group argument - // if (ctx.Match("rename", "name", "changename", "setname", "rn")) - // await ctx.Execute(GroupRename, g => g.RenameGroup(ctx, target)); - // else if (ctx.Match("nick", "dn", "displayname", "nickname")) - // await ctx.Execute(GroupDisplayName, g => g.GroupDisplayName(ctx, target)); - // else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro")) - // await ctx.Execute(GroupDesc, g => g.GroupDescription(ctx, target)); - // else if (ctx.Match("add", "a")) - // await ctx.Execute(GroupAdd, - // g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Add)); - // else if (ctx.Match("remove", "rem")) - // await ctx.Execute(GroupRemove, - // g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Remove)); - // else if (ctx.Match("members", "list", "ms", "l", "ls")) - // await ctx.Execute(GroupMemberList, g => g.ListGroupMembers(ctx, target)); - // else if (ctx.Match("random", "rand", "r")) - // await ctx.Execute(GroupMemberRandom, r => r.GroupMember(ctx, target)); - // else if (ctx.Match("privacy")) - // await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, null)); - // else if (ctx.Match("public", "pub")) - // await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Public)); - // else if (ctx.Match("private", "priv")) - // await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Private)); - // else if (ctx.Match("delete", "destroy", "erase", "yeet")) - // await ctx.Execute(GroupDelete, g => g.DeleteGroup(ctx, target)); - // else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) - // await ctx.Execute(GroupIcon, g => g.GroupIcon(ctx, target)); - // else if (ctx.Match("banner", "splash", "cover")) - // await ctx.Execute(GroupBannerImage, g => g.GroupBannerImage(ctx, target)); - // else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown")) - // await ctx.Execute(GroupFrontPercent, g => g.FrontPercent(ctx, group: target)); - // else if (ctx.Match("color", "colour")) - // await ctx.Execute(GroupColor, g => g.GroupColor(ctx, target)); - // else if (ctx.Match("id")) - // await ctx.Execute(GroupId, g => g.DisplayId(ctx, target)); - // else if (!ctx.HasNext()) - // await ctx.Execute(GroupInfo, g => g.ShowGroupCard(ctx, target)); - // else - // await PrintCommandNotFoundError(ctx, GroupCommandsTargeted); - // } - // else if (!ctx.HasNext()) - // await PrintCommandExpectedError(ctx, GroupCommands); - // else - // await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Group", ctx.PopArgument())}"); + // Commands with no group argument + if (ctx.Match("n", "new")) + await ctx.Execute(GroupNew, g => g.CreateGroup(ctx)); + else if (ctx.Match("commands", "help")) + await PrintCommandList(ctx, "groups", GroupCommands); + else if (await ctx.MatchGroup() is { } target) + { + // Commands with group argument + if (ctx.Match("rename", "name", "changename", "setname", "rn")) + await ctx.Execute(GroupRename, g => g.RenameGroup(ctx, target)); + else if (ctx.Match("nick", "dn", "displayname", "nickname")) + await ctx.Execute(GroupDisplayName, g => g.GroupDisplayName(ctx, target)); + else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro")) + await ctx.Execute(GroupDesc, g => g.GroupDescription(ctx, target)); + else if (ctx.Match("add", "a")) + await ctx.Execute(GroupAdd, + g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Add)); + else if (ctx.Match("remove", "rem")) + await ctx.Execute(GroupRemove, + g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Remove)); + else if (ctx.Match("privacy")) + await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, null)); + else if (ctx.Match("public", "pub")) + await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Public)); + else if (ctx.Match("private", "priv")) + await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Private)); + else if (ctx.Match("delete", "destroy", "erase", "yeet")) + await ctx.Execute(GroupDelete, g => g.DeleteGroup(ctx, target)); + else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) + await ctx.Execute(GroupIcon, g => g.GroupIcon(ctx, target)); + else if (ctx.Match("banner", "splash", "cover")) + await ctx.Execute(GroupBannerImage, g => g.GroupBannerImage(ctx, target)); + else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown")) + await ctx.Execute(GroupFrontPercent, g => g.FrontPercent(ctx, group: target)); + else if (ctx.Match("color", "colour")) + await ctx.Execute(GroupColor, g => g.GroupColor(ctx, target)); + else if (ctx.Match("id")) + await ctx.Execute(GroupId, g => g.DisplayId(ctx, target)); + else if (!ctx.HasNext()) + await ctx.Execute(GroupInfo, g => g.ShowGroupCard(ctx, target)); + else + await PrintCommandNotFoundError(ctx, GroupCommandsTargeted); + } + else if (!ctx.HasNext()) + await PrintCommandExpectedError(ctx, GroupCommands); + else + await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Group", ctx.PopArgument())}"); } private async Task HandleSwitchCommand(Context ctx) diff --git a/PluralKit.Bot/Commands/GroupMember.cs b/PluralKit.Bot/Commands/GroupMember.cs index b30abb24..e5ccc1bd 100644 --- a/PluralKit.Bot/Commands/GroupMember.cs +++ b/PluralKit.Bot/Commands/GroupMember.cs @@ -51,11 +51,12 @@ public class GroupMember groups.Count - toAction.Count)); } - public async Task ListMemberGroups(Context ctx, PKMember target) + public async Task ListMemberGroups(Context ctx, PKMember target, string? query, IHasListOptions flags) { var targetSystem = await ctx.Repository.GetSystem(target.System); - var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System), ctx.LookupContextFor(target.System)); + var opts = flags.GetListOptions(ctx, target.System); opts.MemberFilter = target.Id; + opts.Search = query; var title = new StringBuilder($"Groups containing {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`) in "); if (ctx.Guild != null) @@ -137,15 +138,16 @@ public class GroupMember members.Count - toAction.Count)); } - public async Task ListGroupMembers(Context ctx, PKGroup target) + public async Task ListGroupMembers(Context ctx, PKGroup target, string? query, IHasListOptions flags) { // see global system list for explanation of how privacy settings are used here var targetSystem = await GetGroupSystem(ctx, target); ctx.CheckSystemPrivacy(targetSystem.Id, target.ListPrivacy); - var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System), ctx.LookupContextFor(target.System)); + var opts = flags.GetListOptions(ctx, target.System); opts.GroupFilter = target.Id; + opts.Search = query; var title = new StringBuilder($"Members of {target.DisplayName ?? target.Name} (`{target.DisplayHid(ctx.Config)}`) in "); if (ctx.Guild != null) diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index e350ee58..8289525b 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -478,7 +478,7 @@ public class Groups } } - public async Task ListSystemGroups(Context ctx, PKSystem system) + public async Task ListSystemGroups(Context ctx, PKSystem system, string? query, IHasListOptions flags) { if (system == null) { @@ -492,7 +492,9 @@ public class Groups // - ParseListOptions checks list access privacy and sets the privacy filter (which members show up in list) // - RenderGroupList checks the indivual privacy for each member (NameFor, etc) // the own system is always allowed to look up their list - var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(system.Id), ctx.LookupContextFor(system.Id)); + var opts = flags.GetListOptions(ctx, system.Id); + opts.Search = query; + await ctx.RenderGroupList( ctx.LookupContextFor(system.Id), system.Id, diff --git a/PluralKit.Bot/Commands/Lists/ContextListExt.cs b/PluralKit.Bot/Commands/Lists/ContextListExt.cs index 15257218..875a1862 100644 --- a/PluralKit.Bot/Commands/Lists/ContextListExt.cs +++ b/PluralKit.Bot/Commands/Lists/ContextListExt.cs @@ -9,95 +9,13 @@ using PluralKit.Core; namespace PluralKit.Bot; +public interface IHasListOptions +{ + ListOptions GetListOptions(Context ctx, SystemId system); +} + public static class ContextListExt { - public static ListOptions ParseListOptions(this Context ctx, LookupContext directLookupCtx, LookupContext lookupContext) - { - var p = new ListOptions(); - - // Short or long list? (parse this first, as it can potentially take a positional argument) - var isFull = ctx.Match("f", "full", "big", "details", "long") || ctx.MatchFlag("f", "full"); - p.Type = isFull ? ListType.Long : ListType.Short; - - // Search query - if (ctx.HasNext()) - p.Search = ctx.RemainderOrNull(); - - // Include description in search? - if (ctx.MatchFlag( - "search-description", - "filter-description", - "in-description", - "sd", - "description", - "desc" - )) - p.SearchDescription = true; - - // Sort property (default is by name, but adding a flag anyway, 'cause why not) - if (ctx.MatchFlag("by-name", "bn")) p.SortProperty = SortProperty.Name; - if (ctx.MatchFlag("by-display-name", "bdn")) p.SortProperty = SortProperty.DisplayName; - if (ctx.MatchFlag("by-id", "bid")) p.SortProperty = SortProperty.Hid; - if (ctx.MatchFlag("by-message-count", "bmc")) p.SortProperty = SortProperty.MessageCount; - if (ctx.MatchFlag("by-created", "bc", "bcd")) p.SortProperty = SortProperty.CreationDate; - if (ctx.MatchFlag("by-last-fronted", "by-last-front", "by-last-switch", "blf", "bls")) - p.SortProperty = SortProperty.LastSwitch; - if (ctx.MatchFlag("by-last-message", "blm", "blp")) p.SortProperty = SortProperty.LastMessage; - if (ctx.MatchFlag("by-birthday", "by-birthdate", "bbd")) p.SortProperty = SortProperty.Birthdate; - if (ctx.MatchFlag("random", "rand")) p.SortProperty = SortProperty.Random; - - // Sort reverse? - if (ctx.MatchFlag("r", "rev", "reverse")) - p.Reverse = true; - - // Privacy filter (default is public only) - if (ctx.MatchFlag("a", "all")) p.PrivacyFilter = null; - if (ctx.MatchFlag("private-only", "po")) p.PrivacyFilter = PrivacyLevel.Private; - - // PERM CHECK: If we're trying to access non-public members of another system, error - if (p.PrivacyFilter != PrivacyLevel.Public && directLookupCtx != LookupContext.ByOwner) - // TODO: should this just return null instead of throwing or something? >.> - throw Errors.NotOwnInfo; - - //this is for searching - p.Context = lookupContext; - - // Additional fields to include in the search results - if (ctx.MatchFlag("with-last-switch", "with-last-fronted", "with-last-front", "wls", "wlf")) - p.IncludeLastSwitch = true; - if (ctx.MatchFlag("with-last-message", "with-last-proxy", "wlm", "wlp")) - p.IncludeLastMessage = true; - if (ctx.MatchFlag("with-message-count", "wmc")) - p.IncludeMessageCount = true; - if (ctx.MatchFlag("with-created", "wc")) - p.IncludeCreated = true; - if (ctx.MatchFlag("with-avatar", "with-image", "with-icon", "wa", "wi", "ia", "ii", "img")) - p.IncludeAvatar = true; - if (ctx.MatchFlag("with-pronouns", "wp", "wprns")) - p.IncludePronouns = true; - if (ctx.MatchFlag("with-displayname", "wdn")) - p.IncludeDisplayName = true; - if (ctx.MatchFlag("with-birthday", "wbd", "wb")) - p.IncludeBirthday = true; - - // Always show the sort property, too (unless this is the short list and we are already showing something else) - if (p.Type != ListType.Short || p.includedCount == 0) - { - if (p.SortProperty == SortProperty.DisplayName) p.IncludeDisplayName = true; - if (p.SortProperty == SortProperty.MessageCount) p.IncludeMessageCount = true; - if (p.SortProperty == SortProperty.CreationDate) p.IncludeCreated = true; - if (p.SortProperty == SortProperty.LastSwitch) p.IncludeLastSwitch = true; - if (p.SortProperty == SortProperty.LastMessage) p.IncludeLastMessage = true; - if (p.SortProperty == SortProperty.Birthdate) p.IncludeBirthday = true; - } - - // Make sure the options are valid - p.AssertIsValid(); - - // Done! - return p; - } - public static async Task RenderMemberList(this Context ctx, LookupContext lookupCtx, SystemId system, string embedTitle, string color, ListOptions opts) { diff --git a/PluralKit.Bot/Commands/Lists/ListOptions.cs b/PluralKit.Bot/Commands/Lists/ListOptions.cs index 991b0a8e..f225da75 100644 --- a/PluralKit.Bot/Commands/Lists/ListOptions.cs +++ b/PluralKit.Bot/Commands/Lists/ListOptions.cs @@ -184,6 +184,7 @@ public static class ListOptionsExt // the check for multiple *sorting* property flags is done in SortProperty setter } + } public enum SortProperty diff --git a/PluralKit.Bot/Commands/Random.cs b/PluralKit.Bot/Commands/Random.cs index 7c451afa..2356ce4a 100644 --- a/PluralKit.Bot/Commands/Random.cs +++ b/PluralKit.Bot/Commands/Random.cs @@ -82,11 +82,11 @@ public class Random components: await _embeds.CreateGroupMessageComponents(ctx, target, groups.ToArray()[randInt])); } - public async Task GroupMember(Context ctx, PKGroup group, bool all, bool showEmbed = false) + public async Task GroupMember(Context ctx, PKGroup group, GroupRandomMemberFlags flags) { ctx.CheckSystemPrivacy(group.System, group.ListPrivacy); - var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(group.System), ctx.LookupContextFor(group.System)); + var opts = flags.GetListOptions(ctx, group.System); opts.GroupFilter = group.Id; var members = await ctx.Database.Execute(conn => conn.QueryMemberList(group.System, opts.ToQueryOptions())); @@ -96,7 +96,7 @@ public class Random "This group has no members!" + (ctx.System?.Id == group.System ? " Please add at least one member to this group before using this command." : "")); - if (!all) + if (!flags.all) members = members.Where(g => g.MemberVisibility == PrivacyLevel.Public); else ctx.CheckOwnGroup(group); @@ -112,7 +112,7 @@ public class Random var randInt = randGen.Next(ms.Count); - if (showEmbed) + if (flags.show_embed) { await ctx.Reply( text: EmbedService.LEGACY_EMBED_WARNING, diff --git a/PluralKit.Bot/Commands/SystemList.cs b/PluralKit.Bot/Commands/SystemList.cs index 6fc3ff75..f100ac05 100644 --- a/PluralKit.Bot/Commands/SystemList.cs +++ b/PluralKit.Bot/Commands/SystemList.cs @@ -8,16 +8,20 @@ namespace PluralKit.Bot; public class SystemList { - public async Task MemberList(Context ctx, PKSystem target) + public async Task MemberList(Context ctx, PKSystem target, string? query, IHasListOptions flags) { + ctx.CheckSystem(target); + if (target == null) throw Errors.NoSystemError(ctx.DefaultPrefix); ctx.CheckSystemPrivacy(target.Id, target.MemberListPrivacy); + var opts = flags.GetListOptions(ctx, target.Id); + opts.Search = query; + // explanation of privacy lookup here: // - ParseListOptions checks list access privacy and sets the privacy filter (which members show up in list) // - RenderMemberList checks the indivual privacy for each member (NameFor, etc) // the own system is always allowed to look up their list - var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.Id), ctx.LookupContextFor(target.Id)); await ctx.RenderMemberList( ctx.LookupContextFor(target.Id), target.Id, diff --git a/crates/command_definitions/src/group.rs b/crates/command_definitions/src/group.rs index 44847ee6..f884ed3b 100644 --- a/crates/command_definitions/src/group.rs +++ b/crates/command_definitions/src/group.rs @@ -1,11 +1,38 @@ use command_parser::token::TokensIterator; +use crate::utils::get_list_flags; + use super::*; -pub fn group() -> (&'static str, [&'static str; 1]) { - ("group", ["g"]) +pub fn group() -> (&'static str, [&'static str; 2]) { + ("group", ["g", "groups"]) } pub fn targeted() -> TokensIterator { tokens!(group(), GroupRef) } + +pub fn cmds() -> impl Iterator { + let group = group(); + let group_target = targeted(); + + let apply_list_opts = |cmd: Command| cmd.flags(get_list_flags()); + + let group_list_members = tokens!(group_target, ("members", ["list", "ls"])); + let group_list_members_cmd = [ + command!(group_list_members => "group_list_members"), + command!(group_list_members, "list" => "group_list_members"), + command!(group_list_members, ("search", ["find", "query"]), ("query", OpaqueStringRemainder) => "group_search_members"), + ] + .into_iter() + .map(apply_list_opts); + + let system_groups_cmd = [ + command!(group, ("list", ["ls"]) => "group_list_groups"), + command!(group, ("search", ["find", "query"]), ("query", OpaqueStringRemainder) => "group_search_groups"), + ] + .into_iter() + .map(apply_list_opts); + + system_groups_cmd.chain(group_list_members_cmd) +} diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index 34f3b4d9..ceb05a49 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -18,11 +18,14 @@ pub mod server_config; pub mod switch; pub mod system; +pub mod utils; + use command_parser::{command, command::Command, parameter::ParameterKind::*, tokens}; pub fn all() -> impl Iterator { (help::cmds()) .chain(system::cmds()) + .chain(group::cmds()) .chain(member::cmds()) .chain(config::cmds()) .chain(fun::cmds()) diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index 14ad283b..00b90b6a 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -1,5 +1,7 @@ use command_parser::token::TokensIterator; +use crate::utils::get_list_flags; + use super::*; pub fn member() -> (&'static str, [&'static str; 1]) { @@ -291,6 +293,16 @@ pub fn cmds() -> impl Iterator { .chain(member_webhook_avatar_cmd) .chain(member_server_avatar_cmd); + let member_group = tokens!(member_target, group::group()); + let member_list_group_cmds = [ + command!(member_group => "member_list_groups"), + command!(member_group, "list" => "member_list_groups"), + command!(member_group, ("search", ["find", "query"]), ("query", OpaqueStringRemainder) => "member_search_groups"), + ] + .into_iter() + .map(|cmd| cmd.flags(get_list_flags())); + let member_group_cmds = member_list_group_cmds; + let member_delete_cmd = [command!(member_target, delete => "member_delete").help("Deletes a member")].into_iter(); @@ -298,10 +310,7 @@ pub fn cmds() -> impl Iterator { [command!(member_target, "soulscream" => "member_soulscream").show_in_suggestions(false)] .into_iter(); - let member_list = [command!(member, "list" => "members_list")].into_iter(); - member_new_cmd - .chain(member_list) .chain(member_info_cmd) .chain(member_name_cmd) .chain(member_description_cmd) @@ -318,4 +327,5 @@ pub fn cmds() -> impl Iterator { .chain(member_message_settings_cmd) .chain(member_delete_cmd) .chain(member_easter_eggs) + .chain(member_group_cmds) } diff --git a/crates/command_definitions/src/random.rs b/crates/command_definitions/src/random.rs index 2f48c9f0..e62701a5 100644 --- a/crates/command_definitions/src/random.rs +++ b/crates/command_definitions/src/random.rs @@ -1,3 +1,5 @@ +use crate::utils::get_list_flags; + use super::*; pub fn cmds() -> impl Iterator { @@ -7,7 +9,7 @@ pub fn cmds() -> impl Iterator { [ command!(random => "random_self").flag(group), command!(system::targeted(), random => "system_random").flag(group), - command!(group::targeted(), random => "group_random_member"), + command!(group::targeted(), random => "group_random_member").flags(get_list_flags()), ] .into_iter() .map(|cmd| cmd.flag(("all", ["a"]))) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index fcb3c20f..86fcf55e 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -1,5 +1,7 @@ use command_parser::token::TokensIterator; +use crate::utils::get_list_flags; + use super::*; pub fn cmds() -> impl Iterator { @@ -251,8 +253,33 @@ pub fn edit() -> impl Iterator { ] .into_iter(); - let system_list = - [command!(system_target, ("members", ["list"]) => "system_members_list")].into_iter(); + let system_list = ("members", ["list"]); + let system_search = tokens!( + ("search", ["query", "find"]), + ("query", OpaqueStringRemainder), + ); + let add_list_flags = |cmd: Command| cmd.flags(get_list_flags()); + let system_list_cmd = [ + command!(system_target, system_list => "system_members_list"), + command!(system_target, system_search => "system_members_search"), + ] + .into_iter() + .map(add_list_flags); + let system_list_self_cmd = [ + command!(system, system_list => "system_members_list_self"), + command!(system, system_search => "system_members_search_self"), + ] + .into_iter() + .map(add_list_flags); + + let system_groups = tokens!(system_target, ("groups", ["gs"])); + let system_groups_cmd = [ + command!(system_groups => "system_list_groups"), + command!(system_groups, ("list", ["ls"]) => "system_list_groups"), + command!(system_groups, ("search", ["find", "query"]), ("query", OpaqueStringRemainder) => "system_search_groups"), + ] + .into_iter() + .map(add_list_flags); system_new_cmd .chain(system_name_self_cmd) @@ -265,6 +292,7 @@ pub fn edit() -> impl Iterator { .chain(system_avatar_self_cmd) .chain(system_server_avatar_self_cmd) .chain(system_banner_self_cmd) + .chain(system_list_self_cmd) .chain(system_delete) .chain(system_privacy_cmd) .chain(system_proxy_cmd) @@ -281,5 +309,6 @@ pub fn edit() -> impl Iterator { .chain(system_info_cmd) .chain(system_front_cmd) .chain(system_link) - .chain(system_list) + .chain(system_list_cmd) + .chain(system_groups_cmd) } diff --git a/crates/command_definitions/src/utils.rs b/crates/command_definitions/src/utils.rs new file mode 100644 index 00000000..8fd2d2c5 --- /dev/null +++ b/crates/command_definitions/src/utils.rs @@ -0,0 +1,52 @@ +use command_parser::flag::Flag; + +pub fn get_list_flags() -> [Flag; 22] { + [ + // Short or long list + Flag::from(("full", ["f", "big", "details", "long"])), + // Search description + Flag::from(( + "search-description", + [ + "filter-description", + "in-description", + "sd", + "description", + "desc", + ], + )), + // Sort properties + Flag::from(("by-name", ["bn"])), + Flag::from(("by-display-name", ["bdn"])), + Flag::from(("by-id", ["bid"])), + Flag::from(("by-message-count", ["bmc"])), + Flag::from(("by-created", ["bc", "bcd"])), + Flag::from(( + "by-last-fronted", + ["by-last-front", "by-last-switch", "blf", "bls"], + )), + Flag::from(("by-last-message", ["blm", "blp"])), + Flag::from(("by-birthday", ["by-birthdate", "bbd"])), + Flag::from(("random", ["rand"])), + // Sort reverse + Flag::from(("reverse", ["r", "rev"])), + // Privacy filter + Flag::from(("all", ["a"])), + Flag::from(("private-only", ["po"])), + // Additional fields to include + Flag::from(( + "with-last-switch", + ["with-last-fronted", "with-last-front", "wls", "wlf"], + )), + Flag::from(("with-last-message", ["with-last-proxy", "wlm", "wlp"])), + Flag::from(("with-message-count", ["wmc"])), + Flag::from(("with-created", ["wc"])), + Flag::from(( + "with-avatar", + ["with-image", "with-icon", "wa", "wi", "ia", "ii", "img"], + )), + Flag::from(("with-pronouns", ["wp", "wprns"])), + Flag::from(("with-displayname", ["wdn"])), + Flag::from(("with-birthday", ["wbd", "wb"])), + ] +} diff --git a/crates/command_parser/src/command.rs b/crates/command_parser/src/command.rs index a8376cf2..6ae62e60 100644 --- a/crates/command_parser/src/command.rs +++ b/crates/command_parser/src/command.rs @@ -11,7 +11,7 @@ use crate::{flag::Flag, token::Token}; pub struct Command { // TODO: fix hygiene pub tokens: Vec, - pub flags: Vec, + pub flags: HashSet, pub help: SmolStr, pub cb: SmolStr, pub show_in_suggestions: bool, @@ -34,7 +34,7 @@ impl Command { } } Self { - flags: Vec::new(), + flags: HashSet::new(), help: SmolStr::new_static(""), cb: cb.into(), show_in_suggestions: true, @@ -54,34 +54,57 @@ impl Command { self } + pub fn flags(mut self, flags: impl IntoIterator>) -> Self { + self.flags.extend(flags.into_iter().map(Into::into)); + self + } + pub fn flag(mut self, flag: impl Into) -> Self { - self.flags.push(flag.into()); + self.flags.insert(flag.into()); self } pub fn hidden_flag(mut self, flag: impl Into) -> Self { let flag = flag.into(); self.hidden_flags.insert(flag.get_name().into()); - self.flags.push(flag); + self.flags.insert(flag); self } } impl Display for Command { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let visible_flags = self + .flags + .iter() + .filter(|flag| !self.hidden_flags.contains(flag.get_name())) + .collect::>(); let write_flags = |f: &mut std::fmt::Formatter<'_>, space: bool| { - for flag in &self.flags { - if self.hidden_flags.contains(flag.get_name()) { - continue; + if visible_flags.is_empty() { + return Ok(()); + } + write!(f, "{}(", space.then_some(" ").unwrap_or(""))?; + let mut written = 0; + let max_flags = visible_flags.len().min(5); + for flag in &visible_flags { + if written > max_flags { + break; } + write!(f, "{flag}")?; + if max_flags - 1 > written { + write!(f, " ")?; + } + written += 1; + } + if visible_flags.len() > written { + let rest_count = visible_flags.len() - written; write!( f, - "{}[{flag}]{}", - space.then_some(" ").unwrap_or(""), - space.then_some("").unwrap_or(" ") + " ...and {rest_count} flag{}...", + (rest_count > 1).then_some("s").unwrap_or(""), )?; } - std::fmt::Result::Ok(()) + write!(f, "){}", space.then_some("").unwrap_or(" ")) }; for (idx, token) in self.tokens.iter().enumerate() { diff --git a/crates/command_parser/src/flag.rs b/crates/command_parser/src/flag.rs index ca61f64d..9fa8ad38 100644 --- a/crates/command_parser/src/flag.rs +++ b/crates/command_parser/src/flag.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use std::{fmt::Display, hash::Hash}; use smol_str::SmolStr; @@ -28,6 +28,20 @@ impl Display for Flag { } } +impl PartialEq for Flag { + fn eq(&self, other: &Self) -> bool { + self.name.eq(&other.name) + } +} + +impl Eq for Flag {} + +impl Hash for Flag { + fn hash(&self, state: &mut H) { + self.name.hash(state); + } +} + #[derive(Debug)] pub enum FlagMatchError { ValueMatchFailed(FlagValueMatchError), diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index c02fc463..5c170f02 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -1,7 +1,7 @@ #![feature(anonymous_lifetime_in_impl_trait)] pub mod command; -mod flag; +pub mod flag; pub mod parameter; mod string; pub mod token; diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/bin/write_cs_glue.rs index 1d0e61df..d5eeb526 100644 --- a/crates/commands/src/bin/write_cs_glue.rs +++ b/crates/commands/src/bin/write_cs_glue.rs @@ -1,4 +1,4 @@ -use std::{env, fmt::Write, fs, path::PathBuf, str::FromStr}; +use std::{collections::HashSet, env, fmt::Write, fs, path::PathBuf, str::FromStr}; use command_parser::{ parameter::{Parameter, ParameterKind}, @@ -20,16 +20,26 @@ fn main() -> Result<(), Box> { writeln!(&mut glue, "using Myriad.Types;")?; writeln!(&mut glue, "namespace PluralKit.Bot;\n")?; + let mut commands_seen = HashSet::new(); let mut record_fields = String::new(); for command in &commands { + if commands_seen.contains(&command.cb) { + continue; + } writeln!( &mut record_fields, r#"public record {command_name}({command_name}Params parameters, {command_name}Flags flags): Commands;"#, command_name = command_callback_to_name(&command.cb), )?; + commands_seen.insert(command.cb.clone()); } + + commands_seen.clear(); let mut match_branches = String::new(); for command in &commands { + if commands_seen.contains(&command.cb) { + continue; + } let mut command_params_init = String::new(); let command_params = find_parameters(&command.tokens); for param in &command_params { @@ -68,6 +78,7 @@ fn main() -> Result<(), Box> { command_name = command_callback_to_name(&command.cb), command_callback = command.cb, )?; + commands_seen.insert(command.cb.clone()); } write!( &mut glue, @@ -87,7 +98,12 @@ fn main() -> Result<(), Box> { }} "#, )?; + + commands_seen.clear(); for command in &commands { + if commands_seen.contains(&command.cb) { + continue; + } let mut command_params_fields = String::new(); let command_params = find_parameters(&command.tokens); for param in &command_params { @@ -133,6 +149,76 @@ fn main() -> Result<(), Box> { )?; } command_reply_format.push_str("return ReplyFormat.Standard;\n"); + let mut command_list_options = String::new(); + let mut command_list_options_class = String::new(); + let list_flags = command_definitions::utils::get_list_flags(); + if list_flags.iter().all(|flag| command.flags.contains(&flag)) { + write!(&mut command_list_options_class, ": IHasListOptions")?; + writeln!( + &mut command_list_options, + r#" + public ListOptions GetListOptions(Context ctx, SystemId target) + {{ + var directLookupCtx = ctx.DirectLookupContextFor(target); + var lookupCtx = ctx.LookupContextFor(target); + + var p = new ListOptions(); + p.Type = full ? ListType.Long : ListType.Short; + // Search description filter + p.SearchDescription = search_description; + + // Sort property + if (by_name) p.SortProperty = SortProperty.Name; + if (by_display_name) p.SortProperty = SortProperty.DisplayName; + if (by_id) p.SortProperty = SortProperty.Hid; + if (by_message_count) p.SortProperty = SortProperty.MessageCount; + if (by_created) p.SortProperty = SortProperty.CreationDate; + if (by_last_fronted) p.SortProperty = SortProperty.LastSwitch; + if (by_last_message) p.SortProperty = SortProperty.LastMessage; + if (by_birthday) p.SortProperty = SortProperty.Birthdate; + if (random) p.SortProperty = SortProperty.Random; + + // Sort reverse + p.Reverse = reverse; + + // Privacy filter + if (all) p.PrivacyFilter = null; + if (private_only) p.PrivacyFilter = PrivacyLevel.Private; + // PERM CHECK: If we're trying to access non-public members of another system, error + if (p.PrivacyFilter != PrivacyLevel.Public && directLookupCtx != LookupContext.ByOwner) + // TODO: should this just return null instead of throwing or something? >.> + throw Errors.NotOwnInfo; + + // this is for searching + p.Context = lookupCtx; + + // Additional fields to include + p.IncludeLastSwitch = with_last_switch; + p.IncludeLastMessage = with_last_message; + p.IncludeMessageCount = with_message_count; + p.IncludeCreated = with_created; + p.IncludeAvatar = with_avatar; + p.IncludePronouns = with_pronouns; + p.IncludeDisplayName = with_displayname; + p.IncludeBirthday = with_birthday; + + // Always show the sort property (unless short list and already showing something else) + if (p.Type != ListType.Short || p.includedCount == 0) + {{ + if (p.SortProperty == SortProperty.DisplayName) p.IncludeDisplayName = true; + if (p.SortProperty == SortProperty.MessageCount) p.IncludeMessageCount = true; + if (p.SortProperty == SortProperty.CreationDate) p.IncludeCreated = true; + if (p.SortProperty == SortProperty.LastSwitch) p.IncludeLastSwitch = true; + if (p.SortProperty == SortProperty.LastMessage) p.IncludeLastMessage = true; + if (p.SortProperty == SortProperty.Birthdate) p.IncludeBirthday = true; + }} + + p.AssertIsValid(); + return p; + }} + "#, + )?; + } write!( &mut glue, r#" @@ -140,7 +226,7 @@ fn main() -> Result<(), Box> { {{ {command_params_fields} }} - public class {command_name}Flags + public class {command_name}Flags {command_list_options_class} {{ {command_flags_fields} @@ -148,10 +234,13 @@ fn main() -> Result<(), Box> { {{ {command_reply_format} }} + + {command_list_options} }} "#, command_name = command_callback_to_name(&command.cb), )?; + commands_seen.insert(command.cb.clone()); } fs::write(write_location, glue)?; Ok(()) diff --git a/crates/commands/src/main.rs b/crates/commands/src/main.rs index 376d2469..07128bfe 100644 --- a/crates/commands/src/main.rs +++ b/crates/commands/src/main.rs @@ -17,7 +17,7 @@ fn main() { } } else { for command in command_definitions::all() { - println!("{} - {}", command, command.help); + println!("{} => {} - {}", command.cb, command, command.help); } } }