implement parse list options and related commands

This commit is contained in:
dusk 2025-09-30 18:45:35 +00:00
parent 3e7898e5cc
commit 95fc7e9f60
No known key found for this signature in database
18 changed files with 367 additions and 199 deletions

View file

@ -170,11 +170,21 @@ public partial class CommandTree
flags.group flags.group
? ctx.Execute<Random>(GroupRandom, m => m.Group(ctx, param.target, flags.all, flags.show_embed)) ? ctx.Execute<Random>(GroupRandom, m => m.Group(ctx, param.target, flags.all, flags.show_embed))
: ctx.Execute<Random>(MemberRandom, m => m.Member(ctx, param.target, flags.all, flags.show_embed)), : ctx.Execute<Random>(MemberRandom, m => m.Member(ctx, param.target, flags.all, flags.show_embed)),
Commands.GroupRandomMember(var param, var flags) => ctx.Execute<Random>(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags.all, flags.show_embed)), Commands.GroupRandomMember(var param, var flags) => ctx.Execute<Random>(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags)),
Commands.SystemLink => ctx.Execute<SystemLink>(Link, m => m.LinkSystem(ctx)), Commands.SystemLink => ctx.Execute<SystemLink>(Link, m => m.LinkSystem(ctx)),
Commands.SystemUnlink(var param, _) => ctx.Execute<SystemLink>(Unlink, m => m.UnlinkAccount(ctx, param.target)), Commands.SystemUnlink(var param, _) => ctx.Execute<SystemLink>(Unlink, m => m.UnlinkAccount(ctx, param.target)),
Commands.MembersList => ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, ctx.System)), Commands.SystemMembersListSelf(var param, var flags) => ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, ctx.System, null, flags)),
Commands.SystemMembersList(var param, _) => ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, param.target)), Commands.SystemMembersSearchSelf(var param, var flags) => ctx.Execute<SystemList>(SystemFind, m => m.MemberList(ctx, ctx.System, param.query, flags)),
Commands.SystemMembersList(var param, var flags) => ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, param.target, null, flags)),
Commands.SystemMembersSearch(var param, var flags) => ctx.Execute<SystemList>(SystemFind, m => m.MemberList(ctx, param.target, param.query, flags)),
Commands.MemberListGroups(var param, var flags) => ctx.Execute<GroupMember>(MemberGroups, m => m.ListMemberGroups(ctx, param.target, null, flags)),
Commands.MemberSearchGroups(var param, var flags) => ctx.Execute<GroupMember>(MemberGroups, m => m.ListMemberGroups(ctx, param.target, param.query, flags)),
Commands.GroupListMembers(var param, var flags) => ctx.Execute<GroupMember>(GroupMemberList, m => m.ListGroupMembers(ctx, param.target, null, flags)),
Commands.GroupSearchMembers(var param, var flags) => ctx.Execute<GroupMember>(GroupMemberList, m => m.ListGroupMembers(ctx, param.target, param.query, flags)),
Commands.SystemListGroups(var param, var flags) => ctx.Execute<Groups>(GroupList, g => g.ListSystemGroups(ctx, param.target, null, flags)),
Commands.SystemSearchGroups(var param, var flags) => ctx.Execute<Groups>(GroupList, g => g.ListSystemGroups(ctx, param.target, param.query, flags)),
Commands.GroupListGroups(var param, var flags) => ctx.Execute<Groups>(GroupList, g => g.ListSystemGroups(ctx, ctx.System, null, flags)),
Commands.GroupSearchGroups(var param, var flags) => ctx.Execute<Groups>(GroupList, g => g.ListSystemGroups(ctx, ctx.System, param.query, flags)),
_ => _ =>
// 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(
@ -196,8 +206,6 @@ public partial class CommandTree
return HandleConfigCommand(ctx); return HandleConfigCommand(ctx);
if (ctx.Match("serverconfig", "guildconfig", "scfg")) if (ctx.Match("serverconfig", "guildconfig", "scfg"))
return HandleServerConfigCommand(ctx); return HandleServerConfigCommand(ctx);
if (ctx.Match("list", "find", "members", "search", "query", "l", "f", "fd", "ls"))
return ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, ctx.System));
if (ctx.Match("token")) if (ctx.Match("token"))
if (ctx.Match("refresh", "renew", "invalidate", "reroll", "regen")) if (ctx.Match("refresh", "renew", "invalidate", "reroll", "regen"))
return ctx.Execute<Api>(TokenRefresh, m => m.RefreshToken(ctx)); return ctx.Execute<Api>(TokenRefresh, m => m.RefreshToken(ctx));
@ -412,20 +420,13 @@ public partial class CommandTree
private async Task HandleSystemCommandTargeted(Context ctx, PKSystem target) private async Task HandleSystemCommandTargeted(Context ctx, PKSystem target)
{ {
if (ctx.Match("find", "search", "query", "fd", "s")) if (ctx.Match("id"))
await ctx.CheckSystem(target).Execute<SystemList>(SystemFind, m => m.MemberList(ctx, target)); // TODO: this lmao (ParseListOptions)
else if (ctx.Match("groups", "gs"))
await ctx.CheckSystem(target).Execute<Groups>(GroupList, g => g.ListSystemGroups(ctx, target));
else if (ctx.Match("id"))
await ctx.CheckSystem(target).Execute<System>(SystemId, m => m.DisplayId(ctx, target)); await ctx.CheckSystem(target).Execute<System>(SystemId, m => m.DisplayId(ctx, target));
} }
private async Task HandleMemberCommand(Context ctx) private async Task HandleMemberCommand(Context ctx)
{ {
// TODO: implement if (ctx.Match("commands", "help"))
if (ctx.Match("list"))
await ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, ctx.System));
else if (ctx.Match("commands", "help"))
await PrintCommandList(ctx, "members", MemberCommands); await PrintCommandList(ctx, "members", MemberCommands);
else if (await ctx.MatchMember() is PKMember target) else if (await ctx.MatchMember() is PKMember target)
await HandleMemberCommandTargeted(ctx, target); await HandleMemberCommandTargeted(ctx, target);
@ -447,72 +448,63 @@ public partial class CommandTree
else if (ctx.Match("remove", "rem")) else if (ctx.Match("remove", "rem"))
await ctx.Execute<GroupMember>(MemberGroupRemove, await ctx.Execute<GroupMember>(MemberGroupRemove,
m => m.AddRemoveGroups(ctx, target, Groups.AddRemoveOperation.Remove)); m => m.AddRemoveGroups(ctx, target, Groups.AddRemoveOperation.Remove));
else if (ctx.Match("id"))
await ctx.Execute<Member>(MemberId, m => m.DisplayId(ctx, target));
else else
await ctx.Execute<GroupMember>(MemberGroups, m => m.ListMemberGroups(ctx, target)); await PrintCommandNotFoundError(ctx, MemberInfo, MemberRename, MemberDisplayName, MemberServerName,
else if (ctx.Match("id")) MemberDesc, MemberPronouns, MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar,
await ctx.Execute<Member>(MemberId, m => m.DisplayId(ctx, target)); SystemList);
else
await PrintCommandNotFoundError(ctx, MemberInfo, MemberRename, MemberDisplayName, MemberServerName,
MemberDesc, MemberPronouns, MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar,
SystemList);
} }
private async Task HandleGroupCommand(Context ctx) private async Task HandleGroupCommand(Context ctx)
{ {
// TODO: implement // Commands with no group argument
// // Commands with no group argument if (ctx.Match("n", "new"))
// if (ctx.Match("n", "new")) await ctx.Execute<Groups>(GroupNew, g => g.CreateGroup(ctx));
// await ctx.Execute<Groups>(GroupNew, g => g.CreateGroup(ctx)); else if (ctx.Match("commands", "help"))
// else if (ctx.Match("list", "l")) await PrintCommandList(ctx, "groups", GroupCommands);
// await ctx.Execute<Groups>(GroupList, g => g.ListSystemGroups(ctx, null)); else if (await ctx.MatchGroup() is { } target)
// else if (ctx.Match("commands", "help")) {
// await PrintCommandList(ctx, "groups", GroupCommands); // Commands with group argument
// else if (await ctx.MatchGroup() is { } target) if (ctx.Match("rename", "name", "changename", "setname", "rn"))
// { await ctx.Execute<Groups>(GroupRename, g => g.RenameGroup(ctx, target));
// // Commands with group argument else if (ctx.Match("nick", "dn", "displayname", "nickname"))
// if (ctx.Match("rename", "name", "changename", "setname", "rn")) await ctx.Execute<Groups>(GroupDisplayName, g => g.GroupDisplayName(ctx, target));
// await ctx.Execute<Groups>(GroupRename, g => g.RenameGroup(ctx, target)); else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro"))
// else if (ctx.Match("nick", "dn", "displayname", "nickname")) await ctx.Execute<Groups>(GroupDesc, g => g.GroupDescription(ctx, target));
// await ctx.Execute<Groups>(GroupDisplayName, g => g.GroupDisplayName(ctx, target)); else if (ctx.Match("add", "a"))
// else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro")) await ctx.Execute<GroupMember>(GroupAdd,
// await ctx.Execute<Groups>(GroupDesc, g => g.GroupDescription(ctx, target)); g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Add));
// else if (ctx.Match("add", "a")) else if (ctx.Match("remove", "rem"))
// await ctx.Execute<GroupMember>(GroupAdd, await ctx.Execute<GroupMember>(GroupRemove,
// g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Add)); g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Remove));
// else if (ctx.Match("remove", "rem")) else if (ctx.Match("privacy"))
// await ctx.Execute<GroupMember>(GroupRemove, await ctx.Execute<Groups>(GroupPrivacy, g => g.GroupPrivacy(ctx, target, null));
// g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Remove)); else if (ctx.Match("public", "pub"))
// else if (ctx.Match("members", "list", "ms", "l", "ls")) await ctx.Execute<Groups>(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Public));
// await ctx.Execute<GroupMember>(GroupMemberList, g => g.ListGroupMembers(ctx, target)); else if (ctx.Match("private", "priv"))
// else if (ctx.Match("random", "rand", "r")) await ctx.Execute<Groups>(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Private));
// await ctx.Execute<Random>(GroupMemberRandom, r => r.GroupMember(ctx, target)); else if (ctx.Match("delete", "destroy", "erase", "yeet"))
// else if (ctx.Match("privacy")) await ctx.Execute<Groups>(GroupDelete, g => g.DeleteGroup(ctx, target));
// await ctx.Execute<Groups>(GroupPrivacy, g => g.GroupPrivacy(ctx, target, null)); else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp"))
// else if (ctx.Match("public", "pub")) await ctx.Execute<Groups>(GroupIcon, g => g.GroupIcon(ctx, target));
// await ctx.Execute<Groups>(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Public)); else if (ctx.Match("banner", "splash", "cover"))
// else if (ctx.Match("private", "priv")) await ctx.Execute<Groups>(GroupBannerImage, g => g.GroupBannerImage(ctx, target));
// await ctx.Execute<Groups>(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Private)); else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown"))
// else if (ctx.Match("delete", "destroy", "erase", "yeet")) await ctx.Execute<SystemFront>(GroupFrontPercent, g => g.FrontPercent(ctx, group: target));
// await ctx.Execute<Groups>(GroupDelete, g => g.DeleteGroup(ctx, target)); else if (ctx.Match("color", "colour"))
// else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) await ctx.Execute<Groups>(GroupColor, g => g.GroupColor(ctx, target));
// await ctx.Execute<Groups>(GroupIcon, g => g.GroupIcon(ctx, target)); else if (ctx.Match("id"))
// else if (ctx.Match("banner", "splash", "cover")) await ctx.Execute<Groups>(GroupId, g => g.DisplayId(ctx, target));
// await ctx.Execute<Groups>(GroupBannerImage, g => g.GroupBannerImage(ctx, target)); else if (!ctx.HasNext())
// else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown")) await ctx.Execute<Groups>(GroupInfo, g => g.ShowGroupCard(ctx, target));
// await ctx.Execute<SystemFront>(GroupFrontPercent, g => g.FrontPercent(ctx, group: target)); else
// else if (ctx.Match("color", "colour")) await PrintCommandNotFoundError(ctx, GroupCommandsTargeted);
// await ctx.Execute<Groups>(GroupColor, g => g.GroupColor(ctx, target)); }
// else if (ctx.Match("id")) else if (!ctx.HasNext())
// await ctx.Execute<Groups>(GroupId, g => g.DisplayId(ctx, target)); await PrintCommandExpectedError(ctx, GroupCommands);
// else if (!ctx.HasNext()) else
// await ctx.Execute<Groups>(GroupInfo, g => g.ShowGroupCard(ctx, target)); await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Group", ctx.PopArgument())}");
// 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) private async Task HandleSwitchCommand(Context ctx)

View file

@ -51,11 +51,12 @@ public class GroupMember
groups.Count - toAction.Count)); 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 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.MemberFilter = target.Id;
opts.Search = query;
var title = new StringBuilder($"Groups containing {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`) in "); var title = new StringBuilder($"Groups containing {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`) in ");
if (ctx.Guild != null) if (ctx.Guild != null)
@ -137,15 +138,16 @@ public class GroupMember
members.Count - toAction.Count)); 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 // see global system list for explanation of how privacy settings are used here
var targetSystem = await GetGroupSystem(ctx, target); var targetSystem = await GetGroupSystem(ctx, target);
ctx.CheckSystemPrivacy(targetSystem.Id, target.ListPrivacy); 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.GroupFilter = target.Id;
opts.Search = query;
var title = new StringBuilder($"Members of {target.DisplayName ?? target.Name} (`{target.DisplayHid(ctx.Config)}`) in "); var title = new StringBuilder($"Members of {target.DisplayName ?? target.Name} (`{target.DisplayHid(ctx.Config)}`) in ");
if (ctx.Guild != null) if (ctx.Guild != null)

View file

@ -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) 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) // - 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) // - RenderGroupList checks the indivual privacy for each member (NameFor, etc)
// the own system is always allowed to look up their list // 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( await ctx.RenderGroupList(
ctx.LookupContextFor(system.Id), ctx.LookupContextFor(system.Id),
system.Id, system.Id,

View file

@ -9,95 +9,13 @@ using PluralKit.Core;
namespace PluralKit.Bot; namespace PluralKit.Bot;
public interface IHasListOptions
{
ListOptions GetListOptions(Context ctx, SystemId system);
}
public static class ContextListExt 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, public static async Task RenderMemberList(this Context ctx, LookupContext lookupCtx,
SystemId system, string embedTitle, string color, ListOptions opts) SystemId system, string embedTitle, string color, ListOptions opts)
{ {

View file

@ -184,6 +184,7 @@ public static class ListOptionsExt
// the check for multiple *sorting* property flags is done in SortProperty setter // the check for multiple *sorting* property flags is done in SortProperty setter
} }
} }
public enum SortProperty public enum SortProperty

View file

@ -82,11 +82,11 @@ public class Random
components: await _embeds.CreateGroupMessageComponents(ctx, target, groups.ToArray()[randInt])); 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); 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; opts.GroupFilter = group.Id;
var members = await ctx.Database.Execute(conn => conn.QueryMemberList(group.System, opts.ToQueryOptions())); 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!" "This group has no members!"
+ (ctx.System?.Id == group.System ? " Please add at least one member to this group before using this command." : "")); + (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); members = members.Where(g => g.MemberVisibility == PrivacyLevel.Public);
else else
ctx.CheckOwnGroup(group); ctx.CheckOwnGroup(group);
@ -112,7 +112,7 @@ public class Random
var randInt = randGen.Next(ms.Count); var randInt = randGen.Next(ms.Count);
if (showEmbed) if (flags.show_embed)
{ {
await ctx.Reply( await ctx.Reply(
text: EmbedService.LEGACY_EMBED_WARNING, text: EmbedService.LEGACY_EMBED_WARNING,

View file

@ -8,16 +8,20 @@ namespace PluralKit.Bot;
public class SystemList 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); if (target == null) throw Errors.NoSystemError(ctx.DefaultPrefix);
ctx.CheckSystemPrivacy(target.Id, target.MemberListPrivacy); ctx.CheckSystemPrivacy(target.Id, target.MemberListPrivacy);
var opts = flags.GetListOptions(ctx, target.Id);
opts.Search = query;
// explanation of privacy lookup here: // explanation of privacy lookup here:
// - ParseListOptions checks list access privacy and sets the privacy filter (which members show up in list) // - 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) // - RenderMemberList checks the indivual privacy for each member (NameFor, etc)
// the own system is always allowed to look up their list // 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( await ctx.RenderMemberList(
ctx.LookupContextFor(target.Id), ctx.LookupContextFor(target.Id),
target.Id, target.Id,

View file

@ -1,11 +1,38 @@
use command_parser::token::TokensIterator; use command_parser::token::TokensIterator;
use crate::utils::get_list_flags;
use super::*; use super::*;
pub fn group() -> (&'static str, [&'static str; 1]) { pub fn group() -> (&'static str, [&'static str; 2]) {
("group", ["g"]) ("group", ["g", "groups"])
} }
pub fn targeted() -> TokensIterator { pub fn targeted() -> TokensIterator {
tokens!(group(), GroupRef) tokens!(group(), GroupRef)
} }
pub fn cmds() -> impl Iterator<Item = Command> {
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)
}

View file

@ -18,11 +18,14 @@ pub mod server_config;
pub mod switch; pub mod switch;
pub mod system; pub mod system;
pub mod utils;
use command_parser::{command, command::Command, parameter::ParameterKind::*, tokens}; use command_parser::{command, command::Command, parameter::ParameterKind::*, tokens};
pub fn all() -> impl Iterator<Item = Command> { pub fn all() -> impl Iterator<Item = Command> {
(help::cmds()) (help::cmds())
.chain(system::cmds()) .chain(system::cmds())
.chain(group::cmds())
.chain(member::cmds()) .chain(member::cmds())
.chain(config::cmds()) .chain(config::cmds())
.chain(fun::cmds()) .chain(fun::cmds())

View file

@ -1,5 +1,7 @@
use command_parser::token::TokensIterator; use command_parser::token::TokensIterator;
use crate::utils::get_list_flags;
use super::*; use super::*;
pub fn member() -> (&'static str, [&'static str; 1]) { pub fn member() -> (&'static str, [&'static str; 1]) {
@ -291,6 +293,16 @@ pub fn cmds() -> impl Iterator<Item = Command> {
.chain(member_webhook_avatar_cmd) .chain(member_webhook_avatar_cmd)
.chain(member_server_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 = let member_delete_cmd =
[command!(member_target, delete => "member_delete").help("Deletes a member")].into_iter(); [command!(member_target, delete => "member_delete").help("Deletes a member")].into_iter();
@ -298,10 +310,7 @@ pub fn cmds() -> impl Iterator<Item = Command> {
[command!(member_target, "soulscream" => "member_soulscream").show_in_suggestions(false)] [command!(member_target, "soulscream" => "member_soulscream").show_in_suggestions(false)]
.into_iter(); .into_iter();
let member_list = [command!(member, "list" => "members_list")].into_iter();
member_new_cmd member_new_cmd
.chain(member_list)
.chain(member_info_cmd) .chain(member_info_cmd)
.chain(member_name_cmd) .chain(member_name_cmd)
.chain(member_description_cmd) .chain(member_description_cmd)
@ -318,4 +327,5 @@ pub fn cmds() -> impl Iterator<Item = Command> {
.chain(member_message_settings_cmd) .chain(member_message_settings_cmd)
.chain(member_delete_cmd) .chain(member_delete_cmd)
.chain(member_easter_eggs) .chain(member_easter_eggs)
.chain(member_group_cmds)
} }

View file

@ -1,3 +1,5 @@
use crate::utils::get_list_flags;
use super::*; use super::*;
pub fn cmds() -> impl Iterator<Item = Command> { pub fn cmds() -> impl Iterator<Item = Command> {
@ -7,7 +9,7 @@ pub fn cmds() -> impl Iterator<Item = Command> {
[ [
command!(random => "random_self").flag(group), command!(random => "random_self").flag(group),
command!(system::targeted(), random => "system_random").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() .into_iter()
.map(|cmd| cmd.flag(("all", ["a"]))) .map(|cmd| cmd.flag(("all", ["a"])))

View file

@ -1,5 +1,7 @@
use command_parser::token::TokensIterator; use command_parser::token::TokensIterator;
use crate::utils::get_list_flags;
use super::*; use super::*;
pub fn cmds() -> impl Iterator<Item = Command> { pub fn cmds() -> impl Iterator<Item = Command> {
@ -251,8 +253,33 @@ pub fn edit() -> impl Iterator<Item = Command> {
] ]
.into_iter(); .into_iter();
let system_list = let system_list = ("members", ["list"]);
[command!(system_target, ("members", ["list"]) => "system_members_list")].into_iter(); 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 system_new_cmd
.chain(system_name_self_cmd) .chain(system_name_self_cmd)
@ -265,6 +292,7 @@ pub fn edit() -> impl Iterator<Item = Command> {
.chain(system_avatar_self_cmd) .chain(system_avatar_self_cmd)
.chain(system_server_avatar_self_cmd) .chain(system_server_avatar_self_cmd)
.chain(system_banner_self_cmd) .chain(system_banner_self_cmd)
.chain(system_list_self_cmd)
.chain(system_delete) .chain(system_delete)
.chain(system_privacy_cmd) .chain(system_privacy_cmd)
.chain(system_proxy_cmd) .chain(system_proxy_cmd)
@ -281,5 +309,6 @@ pub fn edit() -> impl Iterator<Item = Command> {
.chain(system_info_cmd) .chain(system_info_cmd)
.chain(system_front_cmd) .chain(system_front_cmd)
.chain(system_link) .chain(system_link)
.chain(system_list) .chain(system_list_cmd)
.chain(system_groups_cmd)
} }

View file

@ -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"])),
]
}

View file

@ -11,7 +11,7 @@ use crate::{flag::Flag, token::Token};
pub struct Command { pub struct Command {
// TODO: fix hygiene // TODO: fix hygiene
pub tokens: Vec<Token>, pub tokens: Vec<Token>,
pub flags: Vec<Flag>, pub flags: HashSet<Flag>,
pub help: SmolStr, pub help: SmolStr,
pub cb: SmolStr, pub cb: SmolStr,
pub show_in_suggestions: bool, pub show_in_suggestions: bool,
@ -34,7 +34,7 @@ impl Command {
} }
} }
Self { Self {
flags: Vec::new(), flags: HashSet::new(),
help: SmolStr::new_static("<no help text>"), help: SmolStr::new_static("<no help text>"),
cb: cb.into(), cb: cb.into(),
show_in_suggestions: true, show_in_suggestions: true,
@ -54,34 +54,57 @@ impl Command {
self self
} }
pub fn flags(mut self, flags: impl IntoIterator<Item = impl Into<Flag>>) -> Self {
self.flags.extend(flags.into_iter().map(Into::into));
self
}
pub fn flag(mut self, flag: impl Into<Flag>) -> Self { pub fn flag(mut self, flag: impl Into<Flag>) -> Self {
self.flags.push(flag.into()); self.flags.insert(flag.into());
self self
} }
pub fn hidden_flag(mut self, flag: impl Into<Flag>) -> Self { pub fn hidden_flag(mut self, flag: impl Into<Flag>) -> Self {
let flag = flag.into(); let flag = flag.into();
self.hidden_flags.insert(flag.get_name().into()); self.hidden_flags.insert(flag.get_name().into());
self.flags.push(flag); self.flags.insert(flag);
self self
} }
} }
impl Display for Command { impl Display for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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::<Vec<_>>();
let write_flags = |f: &mut std::fmt::Formatter<'_>, space: bool| { let write_flags = |f: &mut std::fmt::Formatter<'_>, space: bool| {
for flag in &self.flags { if visible_flags.is_empty() {
if self.hidden_flags.contains(flag.get_name()) { return Ok(());
continue; }
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!( write!(
f, f,
"{}[{flag}]{}", " ...and {rest_count} flag{}...",
space.then_some(" ").unwrap_or(""), (rest_count > 1).then_some("s").unwrap_or(""),
space.then_some("").unwrap_or(" ")
)?; )?;
} }
std::fmt::Result::Ok(()) write!(f, "){}", space.then_some("").unwrap_or(" "))
}; };
for (idx, token) in self.tokens.iter().enumerate() { for (idx, token) in self.tokens.iter().enumerate() {

View file

@ -1,4 +1,4 @@
use std::fmt::Display; use std::{fmt::Display, hash::Hash};
use smol_str::SmolStr; 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<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
#[derive(Debug)] #[derive(Debug)]
pub enum FlagMatchError { pub enum FlagMatchError {
ValueMatchFailed(FlagValueMatchError), ValueMatchFailed(FlagValueMatchError),

View file

@ -1,7 +1,7 @@
#![feature(anonymous_lifetime_in_impl_trait)] #![feature(anonymous_lifetime_in_impl_trait)]
pub mod command; pub mod command;
mod flag; pub mod flag;
pub mod parameter; pub mod parameter;
mod string; mod string;
pub mod token; pub mod token;

View file

@ -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::{ use command_parser::{
parameter::{Parameter, ParameterKind}, parameter::{Parameter, ParameterKind},
@ -20,16 +20,26 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
writeln!(&mut glue, "using Myriad.Types;")?; writeln!(&mut glue, "using Myriad.Types;")?;
writeln!(&mut glue, "namespace PluralKit.Bot;\n")?; writeln!(&mut glue, "namespace PluralKit.Bot;\n")?;
let mut commands_seen = HashSet::new();
let mut record_fields = String::new(); let mut record_fields = String::new();
for command in &commands { for command in &commands {
if commands_seen.contains(&command.cb) {
continue;
}
writeln!( writeln!(
&mut record_fields, &mut record_fields,
r#"public record {command_name}({command_name}Params parameters, {command_name}Flags flags): Commands;"#, r#"public record {command_name}({command_name}Params parameters, {command_name}Flags flags): Commands;"#,
command_name = command_callback_to_name(&command.cb), command_name = command_callback_to_name(&command.cb),
)?; )?;
commands_seen.insert(command.cb.clone());
} }
commands_seen.clear();
let mut match_branches = String::new(); let mut match_branches = String::new();
for command in &commands { for command in &commands {
if commands_seen.contains(&command.cb) {
continue;
}
let mut command_params_init = String::new(); let mut command_params_init = String::new();
let command_params = find_parameters(&command.tokens); let command_params = find_parameters(&command.tokens);
for param in &command_params { for param in &command_params {
@ -68,6 +78,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
command_name = command_callback_to_name(&command.cb), command_name = command_callback_to_name(&command.cb),
command_callback = command.cb, command_callback = command.cb,
)?; )?;
commands_seen.insert(command.cb.clone());
} }
write!( write!(
&mut glue, &mut glue,
@ -87,7 +98,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}} }}
"#, "#,
)?; )?;
commands_seen.clear();
for command in &commands { for command in &commands {
if commands_seen.contains(&command.cb) {
continue;
}
let mut command_params_fields = String::new(); let mut command_params_fields = String::new();
let command_params = find_parameters(&command.tokens); let command_params = find_parameters(&command.tokens);
for param in &command_params { for param in &command_params {
@ -133,6 +149,76 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
)?; )?;
} }
command_reply_format.push_str("return ReplyFormat.Standard;\n"); 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!( write!(
&mut glue, &mut glue,
r#" r#"
@ -140,7 +226,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
{{ {{
{command_params_fields} {command_params_fields}
}} }}
public class {command_name}Flags public class {command_name}Flags {command_list_options_class}
{{ {{
{command_flags_fields} {command_flags_fields}
@ -148,10 +234,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
{{ {{
{command_reply_format} {command_reply_format}
}} }}
{command_list_options}
}} }}
"#, "#,
command_name = command_callback_to_name(&command.cb), command_name = command_callback_to_name(&command.cb),
)?; )?;
commands_seen.insert(command.cb.clone());
} }
fs::write(write_location, glue)?; fs::write(write_location, glue)?;
Ok(()) Ok(())

View file

@ -17,7 +17,7 @@ fn main() {
} }
} else { } else {
for command in command_definitions::all() { for command in command_definitions::all() {
println!("{} - {}", command, command.help); println!("{} => {} - {}", command.cb, command, command.help);
} }
} }
} }