diff --git a/PluralKit.Bot/CommandSystem/ContextArgumentsExt.cs b/PluralKit.Bot/CommandSystem/ContextArgumentsExt.cs index 0a0c98b5..3e1b2572 100644 --- a/PluralKit.Bot/CommandSystem/ContextArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/ContextArgumentsExt.cs @@ -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> ParseGroupList(this Context ctx, SystemId? restrictToSystem) + { + var groups = new List(); + + // 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; + } } } diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index 5561fe99..466677ae 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -39,6 +39,9 @@ namespace PluralKit.Bot public static Command MemberProxy = new Command("member proxy", "member proxy [add|remove] [example proxy]", "Changes, adds, or removes a member's proxy tags"); public static Command MemberDelete = new Command("member delete", "member delete", "Deletes a member"); public static Command MemberAvatar = new Command("member avatar", "member avatar [url|@mention]", "Changes a member's avatar"); + public static Command MemberGroups = new Command("member group", "member group", "Shows the groups a member is in"); + public static Command MemberGroupAdd = new Command("member group", "member group add [group 2] [group 3...]", "Adds a member to one or more groups"); + public static Command MemberGroupRemove = new Command("member group", "member group remove [group 2] [group 3...]", "Removes a member from one or more groups"); public static Command MemberServerAvatar = new Command("member serveravatar", "member serveravatar [url|@mention]", "Changes a member's avatar in the current server"); public static Command MemberDisplayName = new Command("member displayname", "member displayname [display name]", "Changes a member's display name"); public static Command MemberServerName = new Command("member servername", "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(MemberDelete, m => m.Delete(ctx, target)); else if (ctx.Match("avatar", "profile", "picture", "icon", "image", "pfp", "pic")) await ctx.Execute(MemberAvatar, m => m.Avatar(ctx, target)); + else if (ctx.Match("group", "groups")) + if (ctx.Match("add", "a")) + await ctx.Execute(MemberGroupAdd, m => m.AddRemove(ctx, target, Groups.AddRemoveOperation.Add)); + else if (ctx.Match("remove", "rem")) + await ctx.Execute(MemberGroupRemove, m => m.AddRemove(ctx, target, Groups.AddRemoveOperation.Remove)); + else + await ctx.Execute(MemberGroups, m => m.List(ctx, target)); else if (ctx.Match("serveravatar", "servericon", "serverimage", "serverpfp", "serverpic", "savatar", "spic", "guildavatar", "guildpic", "guildicon", "sicon")) await ctx.Execute(MemberServerAvatar, m => m.ServerAvatar(ctx, target)); else if (ctx.Match("displayname", "dn", "dname", "nick", "nickname", "dispname")) diff --git a/PluralKit.Bot/Commands/MemberGroup.cs b/PluralKit.Bot/Commands/MemberGroup.cs new file mode 100644 index 00000000..482bbfa7 --- /dev/null +++ b/PluralKit.Bot/Commands/MemberGroup.cs @@ -0,0 +1,102 @@ +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; + } + + private String groupTerm(int groups) => groups == 1 ? "group" : "groups"; + + private String Response(List groupList, List 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 = groupList.Count - actionedOn.Count; + + if (notActionedOn == 0) + return $"{Emojis.Success} Member {opStr} {groupTerm(actionedOn.Count)}."; + else + return $"{Emojis.Success} Member {opStr} {actionedOn.Count} {groupTerm(actionedOn.Count)} (member already {inStr} {notActionedOn} {groupTerm(notActionedOn)})."; + } + + 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) + .ToList(); + + await using var conn = await _db.Obtain(); + var existingGroups = (await _repo.GetMemberGroups(conn, target.Id).ToListAsync()) + .Select(g => g.Id) + .ToList(); + + List 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(Response(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 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 2] [group 3...]`"; + } + + await ctx.Reply(msg, embed: (new DiscordEmbedBuilder().WithTitle($"{target.Name}'s groups").WithDescription(description)).Build()); + } + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Modules.cs b/PluralKit.Bot/Modules.cs index 5841149f..625fd40f 100644 --- a/PluralKit.Bot/Modules.cs +++ b/PluralKit.Bot/Modules.cs @@ -40,6 +40,7 @@ namespace PluralKit.Bot builder.RegisterType().AsSelf(); builder.RegisterType().AsSelf(); builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); builder.RegisterType().AsSelf(); builder.RegisterType().AsSelf(); builder.RegisterType().AsSelf(); diff --git a/PluralKit.Core/Database/Repository/ModelRepository.Group.cs b/PluralKit.Core/Database/Repository/ModelRepository.Group.cs index fdcc1e5a..7a88dba7 100644 --- a/PluralKit.Core/Database/Repository/ModelRepository.Group.cs +++ b/PluralKit.Core/Database/Repository/ModelRepository.Group.cs @@ -30,11 +30,6 @@ namespace PluralKit.Core return conn.QuerySingleOrDefaultAsync(query.ToString(), new {Id = id, PrivacyFilter = privacyFilter}); } - public IAsyncEnumerable GetMemberGroups(IPKConnection conn, MemberId id) => - conn.QueryStreamAsync( - "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 CreateGroup(IPKConnection conn, SystemId system, string name) { var group = await conn.QueryFirstAsync( diff --git a/PluralKit.Core/Database/Repository/ModelRepository.GroupMember.cs b/PluralKit.Core/Database/Repository/ModelRepository.GroupMember.cs new file mode 100644 index 00000000..7ac0d65c --- /dev/null +++ b/PluralKit.Core/Database/Repository/ModelRepository.GroupMember.cs @@ -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 GetMemberGroups(IPKConnection conn, MemberId id) => + conn.QueryStreamAsync( + "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 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 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() }); + } + + } +} \ No newline at end of file