Merge pull request #258 from dev-kittens/feat/member-group
Group improvements
This commit is contained in:
		| @@ -86,8 +86,33 @@ namespace PluralKit.Bot | ||||
|                  | ||||
|                 members.Add(member); // Then add to the final output list | ||||
|             } | ||||
|             if (members.Count == 0) throw new PKSyntaxError($"You must input at least one member."); | ||||
|              | ||||
|             return members; | ||||
|         } | ||||
|  | ||||
|         public static async Task<List<PKGroup>> ParseGroupList(this Context ctx, SystemId? restrictToSystem) | ||||
|         { | ||||
|             var groups = new List<PKGroup>(); | ||||
|  | ||||
|             // Loop through all the given arguments | ||||
|             while (ctx.HasNext()) | ||||
|             { | ||||
|                 // and attempt to match a group  | ||||
|                 var group = await ctx.MatchGroup(); | ||||
|                 if (group == null) | ||||
|                     // if we can't, big error. Every group name must be valid. | ||||
|                     throw new PKError(ctx.CreateGroupNotFoundError(ctx.PopArgument())); | ||||
|  | ||||
|                 if (restrictToSystem != null && group.System != restrictToSystem) | ||||
|                     throw Errors.NotOwnGroupError; // TODO: name *which* group? | ||||
|                  | ||||
|                 groups.Add(group); // Then add to the final output list | ||||
|             } | ||||
|              | ||||
|             if (groups.Count == 0) throw new PKSyntaxError($"You must input at least one group."); | ||||
|  | ||||
|             return groups; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -39,6 +39,9 @@ namespace PluralKit.Bot | ||||
|         public static Command MemberProxy = new Command("member proxy", "member <member> proxy [add|remove] [example proxy]", "Changes, adds, or removes a member's proxy tags"); | ||||
|         public static Command MemberDelete = new Command("member delete", "member <member> delete", "Deletes a member"); | ||||
|         public static Command MemberAvatar = new Command("member avatar", "member <member> avatar [url|@mention]", "Changes a member's avatar"); | ||||
|         public static Command MemberGroups = new Command("member group", "member <member> group", "Shows the groups a member is in"); | ||||
|         public static Command MemberGroupAdd = new Command("member group", "member <member> group add <group> [group 2] [group 3...]", "Adds a member to one or more groups"); | ||||
|         public static Command MemberGroupRemove = new Command("member group", "member <member> group remove <group> [group 2] [group 3...]", "Removes a member from one or more groups"); | ||||
|         public static Command MemberServerAvatar = new Command("member serveravatar", "member <member> serveravatar [url|@mention]", "Changes a member's avatar in the current server"); | ||||
|         public static Command MemberDisplayName = new Command("member displayname", "member <member> displayname [display name]", "Changes a member's display name"); | ||||
|         public static Command MemberServerName = new Command("member servername", "member <member> servername [server name]", "Changes a member's display name in the current server"); | ||||
| @@ -89,8 +92,8 @@ namespace PluralKit.Bot | ||||
|  | ||||
|         public static Command[] MemberCommands = { | ||||
|             MemberInfo, MemberNew, MemberRename, MemberDisplayName, MemberServerName, MemberDesc, MemberPronouns, | ||||
|             MemberColor, MemberBirthday, MemberProxy, MemberKeepProxy, MemberDelete, MemberAvatar, MemberServerAvatar, MemberPrivacy, | ||||
|             MemberRandom | ||||
|             MemberColor, MemberBirthday, MemberProxy, MemberKeepProxy, MemberGroups, MemberGroupAdd, MemberGroupRemove, | ||||
|             MemberDelete, MemberAvatar, MemberServerAvatar, MemberPrivacy, MemberRandom | ||||
|         }; | ||||
|  | ||||
|         public static Command[] GroupCommands = | ||||
| @@ -328,6 +331,13 @@ namespace PluralKit.Bot | ||||
|                 await ctx.Execute<MemberEdit>(MemberDelete, m => m.Delete(ctx, target)); | ||||
|             else if (ctx.Match("avatar", "profile", "picture", "icon", "image", "pfp", "pic")) | ||||
|                 await ctx.Execute<MemberAvatar>(MemberAvatar, m => m.Avatar(ctx, target)); | ||||
|             else if (ctx.Match("group", "groups")) | ||||
|                 if (ctx.Match("add", "a")) | ||||
|                     await ctx.Execute<MemberGroup>(MemberGroupAdd, m => m.AddRemove(ctx, target, Groups.AddRemoveOperation.Add)); | ||||
|                 else if (ctx.Match("remove", "rem")) | ||||
|                     await ctx.Execute<MemberGroup>(MemberGroupRemove, m => m.AddRemove(ctx, target, Groups.AddRemoveOperation.Remove)); | ||||
|                 else  | ||||
|                     await ctx.Execute<MemberGroup>(MemberGroups, m => m.List(ctx, target)); | ||||
|             else if (ctx.Match("serveravatar", "servericon", "serverimage", "serverpfp", "serverpic", "savatar", "spic", "guildavatar", "guildpic", "guildicon", "sicon")) | ||||
|                 await ctx.Execute<MemberAvatar>(MemberServerAvatar, m => m.ServerAvatar(ctx, target)); | ||||
|             else if (ctx.Match("displayname", "dn", "dname", "nick", "nickname", "dispname")) | ||||
|   | ||||
| @@ -318,49 +318,38 @@ namespace PluralKit.Bot | ||||
|         { | ||||
|             ctx.CheckOwnGroup(target); | ||||
|  | ||||
|             var members = await ctx.ParseMemberList(ctx.System.Id); | ||||
|             var members = (await ctx.ParseMemberList(ctx.System.Id)) | ||||
|                 .Select(m => m.Id) | ||||
|                 .Distinct() | ||||
|                 .ToList(); | ||||
|              | ||||
|             await using var conn = await _db.Obtain(); | ||||
|              | ||||
|             var existingMembersInGroup = (await conn.QueryMemberList(target.System, | ||||
|                 new DatabaseViewsExt.MemberListQueryOptions {GroupFilter = target.Id})) | ||||
|                 .Select(m => m.Id.Value) | ||||
|                 .Distinct() | ||||
|                 .ToHashSet(); | ||||
|              | ||||
|             List<MemberId> toAction; | ||||
|  | ||||
|             if (op == AddRemoveOperation.Add) | ||||
|             { | ||||
|                 var membersNotInGroup = members | ||||
|                     .Where(m => !existingMembersInGroup.Contains(m.Id.Value)) | ||||
|                     .Select(m => m.Id) | ||||
|                     .Distinct() | ||||
|                 toAction = members | ||||
|                     .Where(m => !existingMembersInGroup.Contains(m.Value)) | ||||
|                     .ToList(); | ||||
|                 await _repo.AddMembersToGroup(conn, target.Id, membersNotInGroup); | ||||
|                  | ||||
|                 if (membersNotInGroup.Count == members.Count) | ||||
|                     await ctx.Reply(members.Count == 0 ? $"{Emojis.Success} Member added to group." : $"{Emojis.Success} {"members".ToQuantity(membersNotInGroup.Count)} added to group."); | ||||
|                 else | ||||
|                     if (membersNotInGroup.Count == 0) | ||||
|                         await ctx.Reply(members.Count == 1 ? $"{Emojis.Error} Member not added to group (member already in group)." : $"{Emojis.Error} No members added to group (members already in group)."); | ||||
|                     else | ||||
|                         await ctx.Reply($"{Emojis.Success} {"members".ToQuantity(membersNotInGroup.Count)} added to group ({"members".ToQuantity(members.Count - membersNotInGroup.Count)} already in group)."); | ||||
|                 await _repo.AddMembersToGroup(conn, target.Id, toAction); | ||||
|             } | ||||
|             else if (op == AddRemoveOperation.Remove) | ||||
|             { | ||||
|                 var membersInGroup = members | ||||
|                     .Where(m => existingMembersInGroup.Contains(m.Id.Value)) | ||||
|                     .Select(m => m.Id) | ||||
|                     .Distinct() | ||||
|                 toAction = members | ||||
|                     .Where(m => existingMembersInGroup.Contains(m.Value)) | ||||
|                     .ToList(); | ||||
|                 await _repo.RemoveMembersFromGroup(conn, target.Id, membersInGroup); | ||||
|                  | ||||
|                 if (membersInGroup.Count == members.Count) | ||||
|                     await ctx.Reply(members.Count == 0 ? $"{Emojis.Success} Member removed from group." : $"{Emojis.Success} {"members".ToQuantity(membersInGroup.Count)} removed from group."); | ||||
|                 else | ||||
|                     if (membersInGroup.Count == 0) | ||||
|                         await ctx.Reply(members.Count == 1 ? $"{Emojis.Error} Member not removed from group (member already not in group)." : $"{Emojis.Error} No members removed from group (members already not in group)."); | ||||
|                     else | ||||
|                         await ctx.Reply($"{Emojis.Success} {"members".ToQuantity(membersInGroup.Count)} removed from group ({"members".ToQuantity(members.Count - membersInGroup.Count)} already not in group)."); | ||||
|                 await _repo.RemoveMembersFromGroup(conn, target.Id, toAction); | ||||
|             } | ||||
|             else return; // otherwise toAction "may be undefined" | ||||
|  | ||||
|             await ctx.Reply(MiscUtils.GroupAddRemoveResponse<MemberId>(members, toAction, op)); | ||||
|         } | ||||
|  | ||||
|         public async Task ListGroupMembers(Context ctx, PKGroup target) | ||||
|   | ||||
							
								
								
									
										90
									
								
								PluralKit.Bot/Commands/MemberGroup.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								PluralKit.Bot/Commands/MemberGroup.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| using DSharpPlus.Entities; | ||||
|  | ||||
| using PluralKit.Core; | ||||
|  | ||||
| namespace PluralKit.Bot | ||||
| { | ||||
|     public class MemberGroup | ||||
|     { | ||||
|         private readonly IDatabase _db; | ||||
|         private readonly ModelRepository _repo; | ||||
|  | ||||
|         public MemberGroup(IDatabase db, ModelRepository repo) | ||||
|         { | ||||
|             _db = db; | ||||
|             _repo = repo; | ||||
|         } | ||||
|  | ||||
|         public async Task AddRemove(Context ctx, PKMember target, Groups.AddRemoveOperation op) | ||||
|         { | ||||
|             ctx.CheckSystem().CheckOwnMember(target); | ||||
|  | ||||
|             var groups = (await ctx.ParseGroupList(ctx.System.Id)) | ||||
|                 .Select(g => g.Id) | ||||
|                 .Distinct() | ||||
|                 .ToList(); | ||||
|  | ||||
|             await using var conn = await _db.Obtain(); | ||||
|             var existingGroups = (await _repo.GetMemberGroups(conn, target.Id).ToListAsync()) | ||||
|                 .Select(g => g.Id) | ||||
|                 .Distinct() | ||||
|                 .ToList(); | ||||
|  | ||||
|             List<GroupId> toAction; | ||||
|  | ||||
|             if (op == Groups.AddRemoveOperation.Add) | ||||
|             { | ||||
|                 toAction = groups | ||||
|                     .Where(group => !existingGroups.Contains(group)) | ||||
|                     .ToList(); | ||||
|  | ||||
|                 await _repo.AddGroupsToMember(conn, target.Id, toAction); | ||||
|             } | ||||
|             else if (op == Groups.AddRemoveOperation.Remove) | ||||
|             { | ||||
|                 toAction = groups | ||||
|                     .Where(group => existingGroups.Contains(group)) | ||||
|                     .ToList(); | ||||
|  | ||||
|                 await _repo.RemoveGroupsFromMember(conn, target.Id, toAction); | ||||
|             } | ||||
|             else return; // otherwise toAction "may be unassigned" | ||||
|  | ||||
|             await ctx.Reply(MiscUtils.GroupAddRemoveResponse<GroupId>(groups, toAction, op)); | ||||
|         } | ||||
|  | ||||
|         public async Task List(Context ctx, PKMember target) | ||||
|         { | ||||
|             await using var conn = await _db.Obtain(); | ||||
|  | ||||
|             var pctx = ctx.LookupContextFor(target.System); | ||||
|  | ||||
|             var groups = await _repo.GetMemberGroups(conn, target.Id) | ||||
|                 .Where(g => g.Visibility.CanAccess(pctx)) | ||||
|                 .OrderBy(g => g.Name, StringComparer.InvariantCultureIgnoreCase) | ||||
|                 .ToListAsync(); | ||||
|  | ||||
|             var description = ""; | ||||
|             var msg = ""; | ||||
|  | ||||
|             if (groups.Count == 0) | ||||
|                 description = "This member has no groups."; | ||||
|             else | ||||
|                 description = string.Join("\n", groups.Select(g => $"[`{g.Hid}`] **{g.DisplayName ?? g.Name}**")); | ||||
|              | ||||
|             if (pctx == LookupContext.ByOwner) | ||||
|             { | ||||
|                 msg += $"\n\nTo add this member to one or more groups, use `pk;m {target.Reference()} group add <group> [group 2] [group 3...]`"; | ||||
|                 if (groups.Count > 0) | ||||
|                     msg += $"\nTo remove this member from one or more groups, use `pk;m {target.Reference()} group remove <group> [group 2] [group 3...]`"; | ||||
|             } | ||||
|              | ||||
|             await ctx.Reply(msg, embed: (new DiscordEmbedBuilder().WithTitle($"{target.Name}'s groups").WithDescription(description)).Build()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -40,6 +40,7 @@ namespace PluralKit.Bot | ||||
|             builder.RegisterType<Member>().AsSelf(); | ||||
|             builder.RegisterType<MemberAvatar>().AsSelf(); | ||||
|             builder.RegisterType<MemberEdit>().AsSelf(); | ||||
|             builder.RegisterType<MemberGroup>().AsSelf(); | ||||
|             builder.RegisterType<MemberProxy>().AsSelf(); | ||||
|             builder.RegisterType<Misc>().AsSelf(); | ||||
|             builder.RegisterType<ServerConfig>().AsSelf(); | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| using System; | ||||
| using System.Linq; | ||||
| using System.Collections.Generic; | ||||
| using System.Net.Sockets; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| @@ -17,6 +18,37 @@ namespace PluralKit.Bot | ||||
|         public static string ProxyTagsString(this PKMember member, string separator = ", ") =>  | ||||
|             string.Join(separator, member.ProxyTags.Select(t => t.ProxyString.AsCode())); | ||||
|          | ||||
|          | ||||
|         private static String entityTerm<T>(int count, bool isTarget) | ||||
|         { | ||||
|             var ret = ""; | ||||
|             ret += isTarget ? "Member" : "Group"; | ||||
|             if (( | ||||
|                 (typeof(T) == typeof(GroupId) && !isTarget) || | ||||
|                 (typeof(T) == typeof(MemberId) && isTarget) | ||||
|             ) && count > 1) | ||||
|                 ret += "s"; | ||||
|             return ret; | ||||
|         } | ||||
|  | ||||
|         public static String GroupAddRemoveResponse<T>(List<T> entityList, List<T> actionedOn, Groups.AddRemoveOperation op) | ||||
|         { | ||||
|             var opStr = op == Groups.AddRemoveOperation.Add ? "added to" : "removed from"; | ||||
|             var inStr = op == Groups.AddRemoveOperation.Add ? "in" : "not in"; | ||||
|             var notActionedOn = entityList.Count - actionedOn.Count; | ||||
|  | ||||
|             var groupNotActionedPosStr = typeof(T) == typeof(GroupId) ? notActionedOn.ToString() + " " : ""; | ||||
|             var memberNotActionedPosStr = typeof(T) == typeof(MemberId) ? notActionedOn.ToString() + " " : ""; | ||||
|              | ||||
|             if (actionedOn.Count == 0) | ||||
|                 return $"{Emojis.Error} {entityTerm<T>(notActionedOn, true)} not {opStr} {entityTerm<T>(entityList.Count, false).ToLower()} ({entityTerm<T>(notActionedOn, true).ToLower()} already {inStr} {entityTerm<T>(entityList.Count, false).ToLower()})."; | ||||
|             else | ||||
|                 if (notActionedOn == 0) | ||||
|                     return $"{Emojis.Success} {entityTerm<T>(actionedOn.Count, true)} {opStr} {entityTerm<T>(actionedOn.Count, false).ToLower()}."; | ||||
|                 else | ||||
|                     return $"{Emojis.Success} {entityTerm<T>(actionedOn.Count, true)} {opStr} {actionedOn.Count} {entityTerm<T>(actionedOn.Count, false).ToLower()} ({memberNotActionedPosStr}{entityTerm<T>(actionedOn.Count, true).ToLower()} already {inStr} {groupNotActionedPosStr}{entityTerm<T>(notActionedOn, false).ToLower()})."; | ||||
|         } | ||||
|  | ||||
|         public static bool IsOurProblem(this Exception e) | ||||
|         { | ||||
|             // This function filters out sporadic errors out of our control from being reported to Sentry | ||||
|   | ||||
| @@ -30,11 +30,6 @@ namespace PluralKit.Core | ||||
|             return conn.QuerySingleOrDefaultAsync<int>(query.ToString(), new {Id = id, PrivacyFilter = privacyFilter}); | ||||
|         } | ||||
|          | ||||
|         public IAsyncEnumerable<PKGroup> GetMemberGroups(IPKConnection conn, MemberId id) => | ||||
|             conn.QueryStreamAsync<PKGroup>( | ||||
|                 "select groups.* from group_members inner join groups on group_members.group_id = groups.id where group_members.member_id = @Id", | ||||
|                 new {Id = id}); | ||||
|          | ||||
|         public async Task<PKGroup> CreateGroup(IPKConnection conn, SystemId system, string name) | ||||
|         { | ||||
|             var group = await conn.QueryFirstAsync<PKGroup>( | ||||
|   | ||||
| @@ -0,0 +1,40 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| using Dapper; | ||||
|  | ||||
| namespace PluralKit.Core | ||||
| { | ||||
|     public partial class ModelRepository | ||||
|     {         | ||||
|         public IAsyncEnumerable<PKGroup> GetMemberGroups(IPKConnection conn, MemberId id) => | ||||
|             conn.QueryStreamAsync<PKGroup>( | ||||
|                 "select groups.* from group_members inner join groups on group_members.group_id = groups.id where group_members.member_id = @Id", | ||||
|                 new {Id = id}); | ||||
|          | ||||
|  | ||||
|         public async Task AddGroupsToMember(IPKConnection conn, MemberId member, IReadOnlyCollection<GroupId> groups) | ||||
|         { | ||||
|             await using var w = | ||||
|                 conn.BeginBinaryImport("copy group_members (group_id, member_id) from stdin (format binary)"); | ||||
|             foreach (var group in groups) | ||||
|             { | ||||
|                 await w.StartRowAsync(); | ||||
|                 await w.WriteAsync(group.Value); | ||||
|                 await w.WriteAsync(member.Value); | ||||
|             } | ||||
|  | ||||
|             await w.CompleteAsync(); | ||||
|             _logger.Information("Added member {MemberId} to groups {GroupIds}", member, groups); | ||||
|         } | ||||
|  | ||||
|         public Task RemoveGroupsFromMember(IPKConnection conn, MemberId member, IReadOnlyCollection<GroupId> groups) | ||||
|         { | ||||
|             _logger.Information("Removed groups from {MemberId}: {GroupIds}", member, groups); | ||||
|             return conn.ExecuteAsync("delete from group_members where member_id = @Member and group_id = any(@Groups)", | ||||
|                 new {Member = @member, Groups = groups.ToArray() }); | ||||
|         } | ||||
|  | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user