From 6c5cb8cea76cef69ad68f255dafe84b122e3c497 Mon Sep 17 00:00:00 2001 From: Ske Date: Mon, 6 Jul 2020 19:50:39 +0200 Subject: [PATCH] Add group name/description/list commands --- .../CommandSystem/ContextChecksExt.cs | 7 ++ .../ContextEntityArgumentsExt.cs | 15 +++ PluralKit.Bot/Commands/CommandTree.cs | 21 +++- PluralKit.Bot/Commands/Groups.cs | 101 +++++++++++++++++- PluralKit.Bot/Errors.cs | 3 +- PluralKit.Core/Models/ModelQueryExt.cs | 3 + PluralKit.Core/Models/Patch/ModelPatchExt.cs | 8 ++ 7 files changed, 152 insertions(+), 6 deletions(-) diff --git a/PluralKit.Bot/CommandSystem/ContextChecksExt.cs b/PluralKit.Bot/CommandSystem/ContextChecksExt.cs index 46fb6499..86d62f59 100644 --- a/PluralKit.Bot/CommandSystem/ContextChecksExt.cs +++ b/PluralKit.Bot/CommandSystem/ContextChecksExt.cs @@ -25,6 +25,13 @@ namespace PluralKit.Bot return ctx; } + public static Context CheckOwnGroup(this Context ctx, PKGroup group) + { + if (group.System != ctx.System?.Id) + throw Errors.NotOwnMemberError; + return ctx; + } + public static Context CheckSystem(this Context ctx) { if (ctx.System == null) diff --git a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs index ed8d31a8..89952702 100644 --- a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs @@ -133,6 +133,21 @@ namespace PluralKit.Bot return $"Member not found. Note that a member ID is 5 characters long."; } + public static string CreateGroupNotFoundError(this Context ctx, string input) + { + // TODO: does this belong here? + if (input.Length == 5) + { + if (ctx.System != null) + return $"Group with ID or name \"{input}\" not found."; + return $"Group with ID \"{input}\" not found."; // Accounts without systems can't query by name + } + + if (ctx.System != null) + return $"Group with name \"{input}\" not found. Note that a group ID is 5 characters long."; + return $"Group not found. Note that a group ID is 5 characters long."; + } + public static async Task MatchChannel(this Context ctx) { if (!MentionUtils.TryParseChannel(ctx.PeekArgument(), out var id)) diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index efc75548..7947b390 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -47,6 +47,9 @@ namespace PluralKit.Bot public static Command MemberPrivacy = new Command("member privacy", "member privacy ", "Changes a members's privacy settings"); public static Command GroupInfo = new Command("group", "group ", "Looks up information about a group"); public static Command GroupNew = new Command("group new", "group new ", "Creates a new group"); + public static Command GroupList = new Command("group list", "group list", "Lists all groups in this system"); + public static Command GroupRename = new Command("group rename", "group name ", "Renames a group"); + public static Command GroupDesc = new Command("group description", "group description [description]", "Changes a group's description"); public static Command Switch = new Command("switch", "switch [member 2] [member 3...]", "Registers a switch"); public static Command SwitchOut = new Command("switch out", "switch out", "Registers a switch with no members"); public static Command SwitchMove = new Command("switch move", "switch move ", "Moves the latest switch in time"); @@ -321,12 +324,24 @@ namespace PluralKit.Bot // Commands with no group argument if (ctx.Match("n", "new")) await ctx.Execute(GroupNew, g => g.CreateGroup(ctx)); - - if (await ctx.MatchGroup() is {} group) + else if (ctx.Match("list", "l")) + await ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, null)); + else if (await ctx.MatchGroup() is {} target) { // Commands with group argument - await ctx.Execute(GroupInfo, g => g.ShowGroupCard(ctx, group)); + if (ctx.Match("rename", "name", "changename", "setname")) + await ctx.Execute(GroupRename, g => g.RenameGroup(ctx, target)); + else if (ctx.Match("description", "info", "bio", "text", "desc")) + await ctx.Execute(GroupDesc, g => g.GroupDescription(ctx, target)); + else if (!ctx.HasNext()) + await ctx.Execute(GroupInfo, g => g.ShowGroupCard(ctx, target)); + else + await PrintCommandNotFoundError(ctx, GroupInfo, GroupRename, GroupDesc); } + else if (!ctx.HasNext()) + await PrintCommandNotFoundError(ctx, GroupInfo, GroupList, GroupNew, GroupRename, GroupDesc); + else + await ctx.Reply($"{Emojis.Error} {ctx.CreateGroupNotFoundError(ctx.PopArgument())}"); } private async Task HandleSwitchCommand(Context ctx) diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index 898d2193..14af6b6a 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -1,4 +1,7 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; using DSharpPlus.Entities; @@ -29,6 +32,100 @@ namespace PluralKit.Bot await ctx.Reply($"{Emojis.Success} Group \"**{groupName}**\" (`{newGroup.Hid}`) registered!\nYou can now start adding members to the group:\n- **pk;group {newGroup.Hid} add **"); } + public async Task RenameGroup(Context ctx, PKGroup target) + { + ctx.CheckOwnGroup(target); + + var newName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a new group name."); + if (newName.Length > Limits.MaxGroupNameLength) + throw new PKError($"New group name too long ({newName.Length}/{Limits.MaxMemberNameLength} characters)."); + + await using var conn = await _db.Obtain(); + await conn.UpdateGroup(target.Id, new GroupPatch {Name = newName}); + + await ctx.Reply($"{Emojis.Success} Group name changed from \"**{target.Name}**\" to \"**{newName}**\"."); + } + + public async Task GroupDescription(Context ctx, PKGroup target) + { + if (ctx.MatchClear()) + { + ctx.CheckOwnGroup(target); + + var patch = new GroupPatch {Description = Partial.Null()}; + await _db.Execute(conn => conn.UpdateGroup(target.Id, patch)); + await ctx.Reply($"{Emojis.Success} Group description cleared."); + } + else if (!ctx.HasNext()) + { + if (target.Description == null) + if (ctx.System?.Id == target.System) + await ctx.Reply($"This group does not have a description set. To set one, type `pk;group {target.Hid} description `."); + else + await ctx.Reply("This group does not have a description set."); + else if (ctx.MatchFlag("r", "raw")) + await ctx.Reply($"```\n{target.Description}\n```"); + else + await ctx.Reply(embed: new DiscordEmbedBuilder() + .WithTitle("Group description") + .WithDescription(target.Description) + .AddField("\u200B", $"To print the description with formatting, type `pk;group {target.Hid} description -raw`." + + (ctx.System?.Id == target.System ? $" To clear it, type `pk;group {target.Hid} description -clear`." : "")) + .Build()); + } + else + { + ctx.CheckOwnGroup(target); + + var description = ctx.RemainderOrNull().NormalizeLineEndSpacing(); + if (description.IsLongerThan(Limits.MaxDescriptionLength)) + throw Errors.DescriptionTooLongError(description.Length); + + var patch = new GroupPatch {Description = Partial.Present(description)}; + await _db.Execute(conn => conn.UpdateGroup(target.Id, patch)); + + await ctx.Reply($"{Emojis.Success} Group description changed."); + } + } + + public async Task ListSystemGroups(Context ctx, PKSystem system) + { + if (system == null) + { + ctx.CheckSystem(); + system = ctx.System; + } + + // TODO: integrate with the normal "search" system + await using var conn = await _db.Obtain(); + + var groups = (await conn.QueryGroupsInSystem(system.Id)).ToList(); + 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 `."); + else + await ctx.Reply($"This system has no groups."); + return; + } + + var title = system.Name != null ? $"Groups of {system.Name} (`{system.Hid}`)" : $"Groups of `{system.Hid}`"; + await ctx.Paginate(groups.ToAsyncEnumerable(), groups.Count, 25, title, Renderer); + + Task Renderer(DiscordEmbedBuilder eb, IEnumerable page) + { + var sb = new StringBuilder(); + foreach (var g in page) + { + sb.Append($"[`{g.Hid}`] **{g.Name}**\n"); + } + + eb.WithDescription(sb.ToString()); + eb.WithFooter($"{groups.Count} total"); + return Task.CompletedTask; + } + } + public async Task ShowGroupCard(Context ctx, PKGroup target) { await using var conn = await _db.Obtain(); @@ -41,7 +138,7 @@ namespace PluralKit.Bot var eb = new DiscordEmbedBuilder() .WithAuthor(nameField) - .WithDescription(target.Description) + .AddField("Description", target.Description) .WithFooter($"System ID: {system.Hid} | Group ID: {target.Hid} | Created on {target.Created.FormatZoned(system)}"); await ctx.Reply(embed: eb.Build()); diff --git a/PluralKit.Bot/Errors.cs b/PluralKit.Bot/Errors.cs index 2b6c47ab..58c673a6 100644 --- a/PluralKit.Bot/Errors.cs +++ b/PluralKit.Bot/Errors.cs @@ -34,6 +34,7 @@ namespace PluralKit.Bot { public static PKError NotOwnSystemError => new PKError($"You can only run this command on your own system."); public static PKError NotOwnMemberError => new PKError($"You can only run this command on your own member."); + public static PKError NotOwnGroupError => new PKError($"You can only run this command on your own group."); public static PKError NoSystemError => new PKError("You do not have a system registered with PluralKit. To create one, type `pk;system new`."); public static PKError ExistingSystemError => new PKError("You already have a system registered with PluralKit. To view it, type `pk;system`. If you'd like to delete your system and start anew, type `pk;system delete`, or if you'd like to unlink this account from it, type `pk;unlink`."); public static PKError MissingMemberError => new PKSyntaxError("You need to specify a member to run this command on."); @@ -105,7 +106,7 @@ namespace PluralKit.Bot { public static PKError ProxyNameTooShort(string name) => new PKError($"The webhook's name, `{name}`, is shorter than two characters, and thus cannot be proxied. Please change the member name or use a longer system tag."); public static PKError ProxyNameTooLong(string name) => new PKError($"The webhook's name, {name}, is too long ({name.Length} > {Limits.MaxProxyNameLength} characters), and thus cannot be proxied. Please change the member name, display name or server display name, or use a shorter system tag."); - public static PKError ProxyTagAlreadyExists(ProxyTag tagToAdd, PKMember member) => new PKError($"That member already has the proxy tag ``{tagToAdd.ProxyString.EscapeBacktickPair()}``. The member currently has these tags: {member.ProxyTagsString()}"); + public static PKError ProxyTagAlreadyExists(ProxyTag tagToAdd, PKMember member) => new PKError($"That member already has the proxy tag `` {tagToAdd.ProxyString.EscapeBacktickPair()}``. The member currently has these tags: {member.ProxyTagsString()}"); public static PKError ProxyTagDoesNotExist(ProxyTag tagToRemove, PKMember member) => new PKError($"That member does not have the proxy tag ``{tagToRemove.ProxyString.EscapeBacktickPair()}``. The member currently has these tags: {member.ProxyTagsString()}"); public static PKError LegacyAlreadyHasProxyTag(ProxyTag requested, PKMember member) => new PKError($"This member already has more than one proxy tag set: {member.ProxyTagsString()}\nConsider using the ``pk;member {member.Hid} proxy add {requested.ProxyString.EscapeBacktickPair()}`` command instead."); public static PKError EmptyProxyTags(PKMember member) => new PKError($"The example proxy `text` is equivalent to having no proxy tags at all, since there are no symbols or brackets on either end. If you'd like to clear your proxy tags, use `pk;member {member.Hid} proxy clear`."); diff --git a/PluralKit.Core/Models/ModelQueryExt.cs b/PluralKit.Core/Models/ModelQueryExt.cs index abe06644..a99d58f8 100644 --- a/PluralKit.Core/Models/ModelQueryExt.cs +++ b/PluralKit.Core/Models/ModelQueryExt.cs @@ -35,6 +35,9 @@ namespace PluralKit.Core public static Task QueryGroupByHid(this IPKConnection conn, string hid) => conn.QueryFirstOrDefaultAsync("select * from groups where hid = @hid", new {hid = hid.ToLowerInvariant()}); + public static Task> QueryGroupsInSystem(this IPKConnection conn, SystemId system) => + conn.QueryAsync("select * from groups where system = @System", new {System = system}); + public static Task QueryOrInsertGuildConfig(this IPKConnection conn, ulong guild) => conn.QueryFirstAsync("insert into servers (id) values (@guild) on conflict (id) do update set id = @guild returning *", new {guild}); diff --git a/PluralKit.Core/Models/Patch/ModelPatchExt.cs b/PluralKit.Core/Models/Patch/ModelPatchExt.cs index e2f882d2..f98fa94c 100644 --- a/PluralKit.Core/Models/Patch/ModelPatchExt.cs +++ b/PluralKit.Core/Models/Patch/ModelPatchExt.cs @@ -65,5 +65,13 @@ namespace PluralKit.Core conn.QueryFirstAsync( "insert into groups (hid, system, name) values (find_free_group_hid(), @System, @Name) returning *", new {System = system, Name = name}); + + public static Task UpdateGroup(this IPKConnection conn, GroupId id, GroupPatch patch) + { + var (query, pms) = patch.Apply(UpdateQueryBuilder.Update("groups", "id = @id")) + .WithConstant("id", id) + .Build("returning *"); + return conn.QueryFirstAsync(query, pms); + } } } \ No newline at end of file