Various additional tweaks/additions to groups
This commit is contained in:
		| @@ -50,6 +50,7 @@ namespace PluralKit.Bot | ||||
|         public static Command GroupList = new Command("group list", "group list", "Lists all groups in this system"); | ||||
|         public static Command GroupMemberList = new Command("group members", "group <group> list", "Lists all members in a group"); | ||||
|         public static Command GroupRename = new Command("group rename", "group <group> name <new name>", "Renames a group"); | ||||
|         public static Command GroupDisplayName = new Command("group displayname", "group <member> displayname [display name]", "Changes a group's display name"); | ||||
|         public static Command GroupDesc = new Command("group description", "group <group> description [description]", "Changes a group's description"); | ||||
|         public static Command GroupAdd = new Command("group add", "group <group> add <member> [member 2] [member 3...]", "Adds one or more members to a group"); | ||||
|         public static Command GroupRemove = new Command("group remove", "group <group> remove <member> [member 2] [member 3...]", "Removes one or more members from a group"); | ||||
| @@ -355,6 +356,8 @@ namespace PluralKit.Bot | ||||
|                 // Commands with group argument | ||||
|                 if (ctx.Match("rename", "name", "changename", "setname")) | ||||
|                     await ctx.Execute<Groups>(GroupRename, g => g.RenameGroup(ctx, target)); | ||||
|                 else if (ctx.Match("dn", "displayname", "nickname")) | ||||
|                     await ctx.Execute<Groups>(GroupDisplayName, g => g.GroupDisplayName(ctx, target)); | ||||
|                 else if (ctx.Match("description", "info", "bio", "text", "desc")) | ||||
|                     await ctx.Execute<Groups>(GroupDesc, g => g.GroupDescription(ctx, target)); | ||||
|                 else if (ctx.Match("add", "a")) | ||||
|   | ||||
| @@ -80,7 +80,43 @@ namespace PluralKit.Bot | ||||
|  | ||||
|             await conn.UpdateGroup(target.Id, new GroupPatch {Name = newName}); | ||||
|  | ||||
|             await ctx.Reply($"{Emojis.Success} Group name changed from \"**{target.Name}**\" to \"**{newName}**\"."); | ||||
|             await ctx.Reply($"{Emojis.Success} Group name changed from **{target.Name}** to **{newName}**."); | ||||
|         } | ||||
|  | ||||
|         public async Task GroupDisplayName(Context ctx, PKGroup target) | ||||
|         { | ||||
|             if (ctx.MatchClear()) | ||||
|             { | ||||
|                 ctx.CheckOwnGroup(target); | ||||
|                  | ||||
|                 var patch = new GroupPatch {DisplayName = Partial<string>.Null()}; | ||||
|                 await _db.Execute(conn => conn.UpdateGroup(target.Id, patch)); | ||||
|  | ||||
|                 await ctx.Reply($"{Emojis.Success} Group display name cleared."); | ||||
|             } | ||||
|             else if (!ctx.HasNext()) | ||||
|             { | ||||
|                 // No perms check, display name isn't covered by member privacy  | ||||
|                 var eb = new DiscordEmbedBuilder() | ||||
|                     .AddField("Name", target.Name) | ||||
|                     .AddField("Display Name", target.DisplayName ?? "*(none)*"); | ||||
|                  | ||||
|                 if (ctx.System?.Id == target.System) | ||||
|                     eb.WithDescription($"To change display name, type `pk;group {GroupReference(target)} displayname <display name>`.\nTo clear it, type `pk;group {GroupReference(target)} displayname -clear`."); | ||||
|                  | ||||
|                 await ctx.Reply(embed: eb.Build()); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 ctx.CheckOwnGroup(target); | ||||
|                  | ||||
|                 var newDisplayName = ctx.RemainderOrNull(); | ||||
|                  | ||||
|                 var patch = new GroupPatch {DisplayName = Partial<string>.Present(newDisplayName)}; | ||||
|                 await _db.Execute(conn => conn.UpdateGroup(target.Id, patch)); | ||||
|  | ||||
|                 await ctx.Reply($"{Emojis.Success} Group display name changed."); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         public async Task GroupDescription(Context ctx, PKGroup target) | ||||
| @@ -195,8 +231,7 @@ namespace PluralKit.Bot | ||||
|                 system = ctx.System; | ||||
|             } | ||||
|              | ||||
|             // should this be split off to a separate permission? | ||||
|             ctx.CheckSystemPrivacy(system, system.MemberListPrivacy); | ||||
|             ctx.CheckSystemPrivacy(system, system.GroupListPrivacy); | ||||
|              | ||||
|             // TODO: integrate with the normal "search" system | ||||
|             await using var conn = await _db.Obtain(); | ||||
| @@ -210,7 +245,6 @@ namespace PluralKit.Bot | ||||
|                     throw new PKError("You do not have permission to access this information."); | ||||
|             } | ||||
|              | ||||
|  | ||||
|             var groups = (await conn.QueryGroupsInSystem(system.Id)) | ||||
|                 .Where(g => g.Visibility.CanAccess(pctx)) | ||||
|                 .ToList(); | ||||
| @@ -218,9 +252,10 @@ namespace PluralKit.Bot | ||||
|             if (groups.Count == 0) | ||||
|             { | ||||
|                 if (system.Id == ctx.System?.Id) | ||||
|                     await ctx.Reply($"This system has no groups. To create one, use the command `pk;group new <name>`."); | ||||
|                     await ctx.Reply("This system has no groups. To create one, use the command `pk;group new <name>`."); | ||||
|                 else | ||||
|                     await ctx.Reply($"This system has no groups."); | ||||
|                     await ctx.Reply("This system has no groups."); | ||||
|                  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
| @@ -229,7 +264,13 @@ namespace PluralKit.Bot | ||||
|              | ||||
|             Task Renderer(DiscordEmbedBuilder eb, IEnumerable<PKGroup> page) | ||||
|             { | ||||
|                 eb.WithSimpleLineContent(page.Select(g => $"[`{g.Hid}`] **{g.Name}**")); | ||||
|                 eb.WithSimpleLineContent(page.Select(g => | ||||
|                 { | ||||
|                     if (g.DisplayName != null) | ||||
|                         return $"[`{g.Hid}`] **{g.Name}** ({g.DisplayName})"; | ||||
|                     else | ||||
|                         return $"[`{g.Hid}`] **{g.Name}**"; | ||||
|                 })); | ||||
|                 eb.WithFooter($"{groups.Count} total."); | ||||
|                 return Task.CompletedTask; | ||||
|             } | ||||
| @@ -251,10 +292,17 @@ namespace PluralKit.Bot | ||||
|                 .WithAuthor(nameField, iconUrl: DiscordUtils.WorkaroundForUrlBug(target.IconFor(pctx))) | ||||
|                 .WithFooter($"System ID: {system.Hid} | Group ID: {target.Hid} | Created on {target.Created.FormatZoned(system)}"); | ||||
|  | ||||
|             if (memberCount == 0) | ||||
|             if (target.DisplayName != null) | ||||
|                 eb.AddField("Display Name", target.DisplayName); | ||||
|  | ||||
|             if (target.ListPrivacy.CanAccess(pctx)) | ||||
|             { | ||||
|                 if (memberCount == 0 && pctx == LookupContext.ByOwner) | ||||
|                     // Only suggest the add command if this is actually the owner lol | ||||
|                     eb.AddField("Members (0)", $"Add one with `pk;group {GroupReference(target)} add <member>`!", true); | ||||
|                 else | ||||
|                     eb.AddField($"Members ({memberCount})", $"(see `pk;group {GroupReference(target)} list`)", true); | ||||
|             } | ||||
|  | ||||
|             if (target.DescriptionFor(pctx) is {} desc) | ||||
|                 eb.AddField("Description", desc); | ||||
| @@ -275,14 +323,15 @@ namespace PluralKit.Bot | ||||
|              | ||||
|             var existingMembersInGroup = (await conn.QueryMemberList(target.System, | ||||
|                 new DatabaseViewsExt.MemberListQueryOptions {GroupFilter = target.Id})) | ||||
|                 .Select(m => m.Id) | ||||
|                 .Select(m => m.Id.Value) | ||||
|                 .ToHashSet(); | ||||
|              | ||||
|             if (op == AddRemoveOperation.Add) | ||||
|             { | ||||
|                 var membersNotInGroup = members | ||||
|                     .Where(m => !existingMembersInGroup.Contains(m.Id)) | ||||
|                     .Where(m => !existingMembersInGroup.Contains(m.Id.Value)) | ||||
|                     .Select(m => m.Id) | ||||
|                     .Distinct() | ||||
|                     .ToList(); | ||||
|                 await conn.AddMembersToGroup(target.Id, membersNotInGroup); | ||||
|                  | ||||
| @@ -294,8 +343,9 @@ namespace PluralKit.Bot | ||||
|             else if (op == AddRemoveOperation.Remove) | ||||
|             { | ||||
|                 var membersInGroup = members | ||||
|                     .Where(m => existingMembersInGroup.Contains(m.Id)) | ||||
|                     .Where(m => existingMembersInGroup.Contains(m.Id.Value)) | ||||
|                     .Select(m => m.Id) | ||||
|                     .Distinct() | ||||
|                     .ToList(); | ||||
|                 await conn.RemoveMembersFromGroup(target.Id, membersInGroup); | ||||
|                  | ||||
| @@ -311,12 +361,12 @@ namespace PluralKit.Bot | ||||
|             await using var conn = await _db.Obtain(); | ||||
|              | ||||
|             var targetSystem = await GetGroupSystem(ctx, target, conn); | ||||
|             ctx.CheckSystemPrivacy(targetSystem, target.Visibility); | ||||
|             ctx.CheckSystemPrivacy(targetSystem, target.ListPrivacy); | ||||
|              | ||||
|             var opts = ctx.ParseMemberListOptions(ctx.LookupContextFor(target.System)); | ||||
|             opts.GroupFilter = target.Id; | ||||
|  | ||||
|             var title = new StringBuilder($"Members of {target.Name} (`{target.Hid}`) in "); | ||||
|             var title = new StringBuilder($"Members of {target.DisplayName ?? target.Name} (`{target.Hid}`) in "); | ||||
|             if (targetSystem.Name != null)  | ||||
|                 title.Append($"{targetSystem.Name} (`{targetSystem.Hid}`)"); | ||||
|             else | ||||
| @@ -363,8 +413,9 @@ namespace PluralKit.Bot | ||||
|                     .WithTitle($"Current privacy settings for {target.Name}") | ||||
|                     .AddField("Description", target.DescriptionPrivacy.Explanation()) | ||||
|                     .AddField("Icon", target.IconPrivacy.Explanation()) | ||||
|                     .AddField("Member list", target.ListPrivacy.Explanation()) | ||||
|                     .AddField("Visibility", target.Visibility.Explanation()) | ||||
|                     .WithDescription($"To edit privacy settings, use the command:\n> pk;group **{GroupReference(target)}** privacy **<subject>** **<level>**\n\n- `subject` is one of `description`, `icon`, `visibility`, or `all`\n- `level` is either `public` or `private`.") | ||||
|                     .WithDescription($"To edit privacy settings, use the command:\n> pk;group **{GroupReference(target)}** privacy **<subject>** **<level>**\n\n- `subject` is one of `description`, `icon`, `members`, `visibility`, or `all`\n- `level` is either `public` or `private`.") | ||||
|                     .Build());  | ||||
|                 return; | ||||
|             } | ||||
| @@ -387,6 +438,7 @@ namespace PluralKit.Bot | ||||
|                 { | ||||
|                     GroupPrivacySubject.Description => "description privacy", | ||||
|                     GroupPrivacySubject.Icon => "icon privacy", | ||||
|                     GroupPrivacySubject.List => "member list", | ||||
|                     GroupPrivacySubject.Visibility => "visibility", | ||||
|                     _ => throw new ArgumentOutOfRangeException($"Unknown privacy subject {subject}") | ||||
|                 }; | ||||
| @@ -396,10 +448,12 @@ namespace PluralKit.Bot | ||||
|                     (GroupPrivacySubject.Description, PrivacyLevel.Private) => "This group's description is now hidden from other systems.", | ||||
|                     (GroupPrivacySubject.Icon, PrivacyLevel.Private) => "This group's icon is now hidden from other systems.", | ||||
|                     (GroupPrivacySubject.Visibility, PrivacyLevel.Private) => "This group is now hidden from group lists and member cards.", | ||||
|                     (GroupPrivacySubject.List, PrivacyLevel.Private) => "This group's member list is now hidden from other systems.", | ||||
|                      | ||||
|                     (GroupPrivacySubject.Description, PrivacyLevel.Public) => "This group's description is no longer hidden from other systems.", | ||||
|                     (GroupPrivacySubject.Icon, PrivacyLevel.Public) => "This group's icon is no longer hidden from other systems.", | ||||
|                     (GroupPrivacySubject.Visibility, PrivacyLevel.Public) => "This group is no longer hidden from group lists and member cards.", | ||||
|                     (GroupPrivacySubject.List, PrivacyLevel.Public) => "This group's member list is no longer hidden from other systems.", | ||||
|                      | ||||
|                     _ => throw new InvalidOperationException($"Invalid subject/level tuple ({subject}, {level})") | ||||
|                 }; | ||||
|   | ||||
| @@ -268,9 +268,10 @@ namespace PluralKit.Bot | ||||
|                     .WithTitle("Current privacy settings for your system") | ||||
|                     .AddField("Description", ctx.System.DescriptionPrivacy.Explanation()) | ||||
|                     .AddField("Member list", ctx.System.MemberListPrivacy.Explanation()) | ||||
|                     .AddField("Group list", ctx.System.GroupListPrivacy.Explanation()) | ||||
|                     .AddField("Current fronter(s)", ctx.System.FrontPrivacy.Explanation()) | ||||
|                     .AddField("Front/switch history", ctx.System.FrontHistoryPrivacy.Explanation()) | ||||
|                     .WithDescription("To edit privacy settings, use the command:\n`pk;system privacy <subject> <level>`\n\n- `subject` is one of `description`, `list`, `front`, `fronthistory`, or `all` \n- `level` is either `public` or `private`."); | ||||
|                     .WithDescription("To edit privacy settings, use the command:\n`pk;system privacy <subject> <level>`\n\n- `subject` is one of `description`, `list`, `front`, `fronthistory`, `groups`, or `all` \n- `level` is either `public` or `private`."); | ||||
|                 return ctx.Reply(embed: eb.Build()); | ||||
|             } | ||||
|  | ||||
| @@ -291,6 +292,7 @@ namespace PluralKit.Bot | ||||
|                     SystemPrivacySubject.Front => "front", | ||||
|                     SystemPrivacySubject.FrontHistory => "front history", | ||||
|                     SystemPrivacySubject.MemberList => "member list", | ||||
|                     SystemPrivacySubject.GroupList => "group list", | ||||
|                     _ => "" | ||||
|                 }; | ||||
|  | ||||
| @@ -304,7 +306,7 @@ namespace PluralKit.Bot | ||||
|  | ||||
|                 var msg = level switch | ||||
|                 { | ||||
|                     PrivacyLevel.Private => $"All system privacy settings have been set to **{level.LevelName()}**. Other accounts will now not be able to view your member list, front history, or system description.", | ||||
|                     PrivacyLevel.Private => $"All system privacy settings have been set to **{level.LevelName()}**. Other accounts will now not be able to view your member list, group list, front history, or system description.", | ||||
|                     PrivacyLevel.Public => $"All system privacy settings have been set to **{level.LevelName()}**. Other accounts will now be able to view everything.", | ||||
|                     _ => "" | ||||
|                 }; | ||||
|   | ||||
| @@ -147,8 +147,8 @@ namespace PluralKit.Bot { | ||||
|             { | ||||
|                 // More than 5 groups show in "compact" format without ID | ||||
|                 var content = groups.Count > 5 | ||||
|                     ? string.Join(", ", groups.Select(g => g.Name)) | ||||
|                     : string.Join("\n", groups.Select(g => $"[`{g.Hid}`] **{g.Name}**")); | ||||
|                     ? string.Join(", ", groups.Select(g => g.DisplayName ?? g.Name)) | ||||
|                     : string.Join("\n", groups.Select(g => $"[`{g.Hid}`] **{g.DisplayName ?? g.Name}**")); | ||||
|                 eb.AddField($"Groups ({groups.Count})", content.Truncate(1000)); | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -7,12 +7,14 @@ create table groups ( | ||||
|     system int not null references systems(id) on delete cascade, | ||||
|      | ||||
|     name text not null, | ||||
|     display_name text, | ||||
|     description text, | ||||
|     icon text, | ||||
|      | ||||
|     -- Description columns follow the same pattern as usual: 1 = public, 2 = private | ||||
|     description_privacy integer check (description_privacy in (1, 2)) not null default 1, | ||||
|     icon_privacy integer check (icon_privacy in (1, 2)) not null default 1, | ||||
|     list_privacy integer check (list_privacy in (1, 2)) not null default 1, | ||||
|     visibility integer check (visibility in (1, 2)) not null default 1, | ||||
|  | ||||
|     created timestamp with time zone not null default (current_timestamp at time zone 'utc') | ||||
| @@ -24,4 +26,6 @@ create table group_members ( | ||||
|     primary key (group_id, member_id) | ||||
| ); | ||||
|  | ||||
| alter table systems add column group_list_privacy integer check (group_list_privacy in (1, 2)) not null default systems.member_list_privacy; | ||||
|  | ||||
| update info set schema_version = 9; | ||||
|   | ||||
| @@ -10,11 +10,13 @@ namespace PluralKit.Core | ||||
|         public SystemId System { get; } | ||||
|  | ||||
|         public string Name { get; } = null!; | ||||
|         public string? DisplayName { get; } | ||||
|         public string? Description { get; } | ||||
|         public string? Icon { get; } | ||||
|  | ||||
|         public PrivacyLevel DescriptionPrivacy { get; } | ||||
|         public PrivacyLevel IconPrivacy { get; } | ||||
|         public PrivacyLevel ListPrivacy { get; } | ||||
|         public PrivacyLevel Visibility { get; } | ||||
|          | ||||
|         public Instant Created { get; } | ||||
|   | ||||
| @@ -22,6 +22,7 @@ namespace PluralKit.Core { | ||||
|         public PrivacyLevel MemberListPrivacy { get;} | ||||
|         public PrivacyLevel FrontPrivacy { get; } | ||||
|         public PrivacyLevel FrontHistoryPrivacy { get; } | ||||
|         public PrivacyLevel GroupListPrivacy { get; } | ||||
|          | ||||
|         [JsonIgnore] public DateTimeZone Zone => DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz); | ||||
|     } | ||||
|   | ||||
| @@ -4,19 +4,23 @@ namespace PluralKit.Core | ||||
|     public class GroupPatch: PatchObject | ||||
|     { | ||||
|         public Partial<string> Name { get; set; } | ||||
|         public Partial<string?> DisplayName { get; set; } | ||||
|         public Partial<string?> Description { get; set; } | ||||
|         public Partial<string?> Icon { get; set; } | ||||
|          | ||||
|         public Partial<PrivacyLevel> DescriptionPrivacy { get; set; } | ||||
|         public Partial<PrivacyLevel> IconPrivacy { get; set; } | ||||
|         public Partial<PrivacyLevel> ListPrivacy { get; set; } | ||||
|         public Partial<PrivacyLevel> Visibility { get; set; } | ||||
|  | ||||
|         public override UpdateQueryBuilder Apply(UpdateQueryBuilder b) => b | ||||
|             .With("name", Name) | ||||
|             .With("display_name", DisplayName) | ||||
|             .With("description", Description) | ||||
|             .With("icon", Icon) | ||||
|             .With("description_privacy", DescriptionPrivacy) | ||||
|             .With("icon_privacy", IconPrivacy) | ||||
|             .With("list_privacy", ListPrivacy) | ||||
|             .With("visibility", Visibility); | ||||
|     } | ||||
| } | ||||
| @@ -11,6 +11,7 @@ namespace PluralKit.Core | ||||
|         public Partial<string> UiTz { get; set; } | ||||
|         public Partial<PrivacyLevel> DescriptionPrivacy { get; set; } | ||||
|         public Partial<PrivacyLevel> MemberListPrivacy { get; set; } | ||||
|         public Partial<PrivacyLevel> GroupListPrivacy { get; set; } | ||||
|         public Partial<PrivacyLevel> FrontPrivacy { get; set; } | ||||
|         public Partial<PrivacyLevel> FrontHistoryPrivacy { get; set; } | ||||
|         public Partial<bool> PingsEnabled { get; set; } | ||||
| @@ -24,6 +25,7 @@ namespace PluralKit.Core | ||||
|             .With("ui_tz", UiTz) | ||||
|             .With("description_privacy", DescriptionPrivacy) | ||||
|             .With("member_list_privacy", MemberListPrivacy) | ||||
|             .With("group_list_privacy", GroupListPrivacy) | ||||
|             .With("front_privacy", FrontPrivacy) | ||||
|             .With("front_history_privacy", FrontHistoryPrivacy) | ||||
|             .With("pings_enabled", PingsEnabled); | ||||
|   | ||||
| @@ -6,6 +6,7 @@ namespace PluralKit.Core | ||||
|     { | ||||
|         Description, | ||||
|         Icon, | ||||
|         List, | ||||
|         Visibility | ||||
|     } | ||||
|      | ||||
| @@ -18,6 +19,7 @@ namespace PluralKit.Core | ||||
|             { | ||||
|                 GroupPrivacySubject.Description => group.DescriptionPrivacy = level, | ||||
|                 GroupPrivacySubject.Icon => group.IconPrivacy = level, | ||||
|                 GroupPrivacySubject.List => group.ListPrivacy = level, | ||||
|                 GroupPrivacySubject.Visibility => group.Visibility = level, | ||||
|                 _ => throw new ArgumentOutOfRangeException($"Unknown privacy subject {subject}") | ||||
|             }; | ||||
| @@ -52,9 +54,13 @@ namespace PluralKit.Core | ||||
|                 case "hidden": | ||||
|                 case "shown": | ||||
|                 case "visible": | ||||
|                 case "list": | ||||
|                     subject = GroupPrivacySubject.Visibility; | ||||
|                     break; | ||||
|                 case "list": | ||||
|                 case "listing": | ||||
|                 case "members": | ||||
|                     subject = GroupPrivacySubject.List; | ||||
|                     break; | ||||
|                 default: | ||||
|                     subject = default; | ||||
|                     return false; | ||||
|   | ||||
| @@ -16,8 +16,8 @@ namespace PluralKit.Core | ||||
|         public static string Explanation(this PrivacyLevel level) => | ||||
|             level switch | ||||
|             { | ||||
|                 PrivacyLevel.Private => "**Private** (visible only when queried by you)", | ||||
|                 PrivacyLevel.Public => "**Public** (visible to everyone)", | ||||
|                 PrivacyLevel.Private => "Private *(visible only when queried by you)*", | ||||
|                 PrivacyLevel.Public => "Public *(visible to everyone)*", | ||||
|                 _ => throw new ArgumentOutOfRangeException(nameof(level), level, null) | ||||
|             }; | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,7 @@ namespace PluralKit.Core | ||||
|     { | ||||
|         Description, | ||||
|         MemberList, | ||||
|         GroupList, | ||||
|         Front, | ||||
|         FrontHistory | ||||
|     } | ||||
| @@ -21,6 +22,7 @@ namespace PluralKit.Core | ||||
|                 SystemPrivacySubject.Front => system.FrontPrivacy = level, | ||||
|                 SystemPrivacySubject.FrontHistory => system.FrontHistoryPrivacy = level, | ||||
|                 SystemPrivacySubject.MemberList => system.MemberListPrivacy = level, | ||||
|                 SystemPrivacySubject.GroupList => system.GroupListPrivacy = level, | ||||
|                 _ => throw new ArgumentOutOfRangeException($"Unknown privacy subject {subject}") | ||||
|             }; | ||||
|              | ||||
| @@ -61,6 +63,10 @@ namespace PluralKit.Core | ||||
|                 case "fh": | ||||
|                     subject = SystemPrivacySubject.FrontHistory; | ||||
|                     break; | ||||
|                 case "groups": | ||||
|                 case "gs": | ||||
|                     subject = SystemPrivacySubject.GroupList; | ||||
|                     break; | ||||
|                 default: | ||||
|                     subject = default; | ||||
|                     return false; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user