From 8359df09e9043d3b492feabe63c58ca8183615f5 Mon Sep 17 00:00:00 2001 From: Ske Date: Sat, 27 Apr 2019 16:30:34 +0200 Subject: [PATCH] bot: add member creation command --- PluralKit/Bot/Bot.cs | 25 ++++++++----- PluralKit/Bot/Commands/MemberCommands.cs | 47 ++++++++++++++++++++++++ PluralKit/Bot/Commands/SystemCommands.cs | 29 +++++++-------- PluralKit/Bot/Utils.cs | 16 ++++++-- PluralKit/Stores.cs | 12 ++---- PluralKit/Utils.cs | 1 + 6 files changed, 95 insertions(+), 35 deletions(-) create mode 100644 PluralKit/Bot/Commands/MemberCommands.cs diff --git a/PluralKit/Bot/Bot.cs b/PluralKit/Bot/Bot.cs index bc617b51..287f5a0e 100644 --- a/PluralKit/Bot/Bot.cs +++ b/PluralKit/Bot/Bot.cs @@ -118,16 +118,23 @@ namespace PluralKit.Bot private async Task CommandExecuted(Optional cmd, ICommandContext ctx, IResult _result) { + // TODO: refactor this entire block, it's fugly. if (!_result.IsSuccess) { - // If this is a PKError (ie. thrown deliberately), show user facing message - // If not, log as error - var exception = (_result as ExecuteResult?)?.Exception; - if (exception is PKError) { - await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} {exception.Message}"); - } else if (exception is TimeoutException) { - await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} Operation timed out. Try being faster"); - } else { - HandleRuntimeError(ctx.Message as SocketMessage, (_result as ExecuteResult?)?.Exception); + if (_result.Error == CommandError.Unsuccessful || _result.Error == CommandError.Exception) { + // If this is a PKError (ie. thrown deliberately), show user facing message + // If not, log as error + var exception = (_result as ExecuteResult?)?.Exception; + if (exception is PKError) { + await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} {exception.Message}"); + } else if (exception is TimeoutException) { + await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} Operation timed out. Try being faster next time :)"); + } else { + HandleRuntimeError(ctx.Message as SocketMessage, (_result as ExecuteResult?)?.Exception); + } + } else if ((_result.Error == CommandError.BadArgCount || _result.Error == CommandError.MultipleMatches) && cmd.IsSpecified) { + await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} {_result.ErrorReason}\n**Usage: **pk;{cmd.Value.Remarks}"); + } else if (_result.Error == CommandError.UnknownCommand || _result.Error == CommandError.UnmetPrecondition) { + await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} {_result.ErrorReason}"); } } } diff --git a/PluralKit/Bot/Commands/MemberCommands.cs b/PluralKit/Bot/Commands/MemberCommands.cs new file mode 100644 index 00000000..e6246ede --- /dev/null +++ b/PluralKit/Bot/Commands/MemberCommands.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using Discord.Commands; + +namespace PluralKit.Bot.Commands +{ + [Group("member")] + public class MemberCommands : ContextParameterModuleBase + { + public MemberStore Members { get; set; } + + public override string Prefix => "member"; + public override string ContextNoun => "member"; + + [Command("new")] + [Remarks("member new ")] + public async Task NewMember([Remainder] string memberName) { + if (Context.SenderSystem == null) Context.RaiseNoSystemError(); + if (ContextEntity != null) RaiseNoContextError(); + + // Warn if member name will be unproxyable (with/without tag) + var maxLength = Context.SenderSystem.Tag != null ? 32 - Context.SenderSystem.Tag.Length - 1 : 32; + if (memberName.Length > maxLength) { + var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} Member name too long ({memberName.Length} > {maxLength} characters), this member will be unproxyable. Do you want to create it anyway? (You can change the name later)"); + if (!await Context.PromptYesNo(msg)) throw new PKError("Member creation cancelled."); + } + + // Warn if there's already a member by this name + var existingMember = await Members.GetByName(Context.SenderSystem, memberName); + if (existingMember != null) { + var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.Name}\" (`{existingMember.Hid}`). Do you want to create another member with the same name?"); + if (!await Context.PromptYesNo(msg)) throw new PKError("Member creation cancelled."); + } + + // Create the member + var member = await Members.Create(Context.SenderSystem, memberName); + + await Context.Channel.SendMessageAsync($"{Emojis.Success} Member \"{memberName}\" (`{member.Hid}`) registered! Type `pk;help member` for a list of commands to edit this member."); + if (memberName.Contains(" ")) await Context.Channel.SendMessageAsync($"{Emojis.Note} Note that this member's name contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it."); + } + + public override async Task ReadContextParameterAsync(string value) + { + var res = await new PKMemberTypeReader().ReadAsync(Context, value, _services); + return res.IsSuccess ? res.BestMatch as PKMember : null; + } + } +} \ No newline at end of file diff --git a/PluralKit/Bot/Commands/SystemCommands.cs b/PluralKit/Bot/Commands/SystemCommands.cs index 3f7175a8..d8db5332 100644 --- a/PluralKit/Bot/Commands/SystemCommands.cs +++ b/PluralKit/Bot/Commands/SystemCommands.cs @@ -11,17 +11,16 @@ namespace PluralKit.Bot.Commands public class SystemCommands : ContextParameterModuleBase { public override string Prefix => "system"; + public override string ContextNoun => "system"; + public SystemStore Systems {get; set;} public MemberStore Members {get; set;} public EmbedService EmbedService {get; set;} - private PKError NO_SYSTEM_ERROR => new PKError($"You do not have a system registered with PluralKit. To create one, type `pk;system new`. If you already have a system registered on another account, type `pk;link {Context.User.Mention}` from that account to link it here."); - private PKError OTHER_SYSTEM_CONTEXT_ERROR => new PKError("You can only run this command on your own system."); - [Command] public async Task Query(PKSystem system = null) { if (system == null) system = Context.SenderSystem; - if (system == null) throw NO_SYSTEM_ERROR; + if (system == null) Context.RaiseNoSystemError(); await Context.Channel.SendMessageAsync(embed: await EmbedService.CreateSystemEmbed(system)); } @@ -29,19 +28,19 @@ namespace PluralKit.Bot.Commands [Command("new")] public async Task New([Remainder] string systemName = null) { - if (ContextEntity != null) throw OTHER_SYSTEM_CONTEXT_ERROR; + if (ContextEntity != null) RaiseNoContextError(); if (Context.SenderSystem != null) throw 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`."); var system = await Systems.Create(systemName); await Systems.Link(system, Context.User.Id); - await ReplyAsync("Your system has been created. Type `pk;system` to view it, and type `pk;help` for more information about commands you can use now."); + await Context.Channel.SendMessageAsync($"{Emojis.Success} Your system has been created. Type `pk;system` to view it, and type `pk;help` for more information about commands you can use now."); } [Command("name")] public async Task Name([Remainder] string newSystemName = null) { - if (ContextEntity != null) throw OTHER_SYSTEM_CONTEXT_ERROR; - if (Context.SenderSystem == null) throw NO_SYSTEM_ERROR; + if (ContextEntity != null) RaiseNoContextError(); + if (Context.SenderSystem == null) Context.RaiseNoSystemError(); if (newSystemName != null && newSystemName.Length > 250) throw new PKError($"Your chosen system name is too long. ({newSystemName.Length} > 250 characters)"); Context.SenderSystem.Name = newSystemName; @@ -51,8 +50,8 @@ namespace PluralKit.Bot.Commands [Command("description")] public async Task Description([Remainder] string newDescription = null) { - if (ContextEntity != null) throw OTHER_SYSTEM_CONTEXT_ERROR; - if (Context.SenderSystem == null) throw NO_SYSTEM_ERROR; + if (ContextEntity != null) RaiseNoContextError(); + if (Context.SenderSystem == null) Context.RaiseNoSystemError(); if (newDescription != null && newDescription.Length > 1000) throw new PKError($"Your chosen description is too long. ({newDescription.Length} > 250 characters)"); Context.SenderSystem.Description = newDescription; @@ -62,8 +61,8 @@ namespace PluralKit.Bot.Commands [Command("tag")] public async Task Tag([Remainder] string newTag = null) { - if (ContextEntity != null) throw OTHER_SYSTEM_CONTEXT_ERROR; - if (Context.SenderSystem == null) throw NO_SYSTEM_ERROR; + if (ContextEntity != null) RaiseNoContextError(); + if (Context.SenderSystem == null) Context.RaiseNoSystemError(); if (newTag.Length > 30) throw new PKError($"Your chosen description is too long. ({newTag.Length} > 30 characters)"); Context.SenderSystem.Tag = newTag; @@ -72,7 +71,7 @@ namespace PluralKit.Bot.Commands var unproxyableMembers = await Members.GetUnproxyableMembers(Context.SenderSystem); if (unproxyableMembers.Count > 0) { var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} Changing your system tag to '{newTag}' will result in the following members being unproxyable, since the tag would bring their name over 32 characters:\n**{string.Join(", ", unproxyableMembers.Select((m) => m.Name))}**\nDo you want to continue anyway?"); - if (!await Context.PromptYesNo(msg, TimeSpan.FromMinutes(5))) throw new PKError("Tag change cancelled."); + if (!await Context.PromptYesNo(msg)) throw new PKError("Tag change cancelled."); } await Systems.Save(Context.SenderSystem); @@ -81,8 +80,8 @@ namespace PluralKit.Bot.Commands [Command("delete")] public async Task Delete() { - if (ContextEntity != null) throw OTHER_SYSTEM_CONTEXT_ERROR; - if (Context.SenderSystem == null) throw NO_SYSTEM_ERROR; + if (ContextEntity != null) RaiseNoContextError(); + if (Context.SenderSystem == null) Context.RaiseNoSystemError(); var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} Are you sure you want to delete your system? If so, reply to this message with your system's ID (`{Context.SenderSystem.Hid}`).\n**Note: this action is permanent.**"); var reply = await Context.AwaitMessage(Context.Channel, Context.User, timeout: TimeSpan.FromMinutes(1)); diff --git a/PluralKit/Bot/Utils.cs b/PluralKit/Bot/Utils.cs index 38aaac3b..4b262df7 100644 --- a/PluralKit/Bot/Utils.cs +++ b/PluralKit/Bot/Utils.cs @@ -112,6 +112,11 @@ namespace PluralKit.Bot public void SetContextEntity(object entity) { _entity = entity; } + + public void RaiseNoSystemError() + { + throw new PKError($"You do not have a system registered with PluralKit. To create one, type `pk;system new`. If you already have a system registered on another account, type `pk;link {User.Mention}` from that account to link it here."); + } } public abstract class ContextParameterModuleBase : ModuleBase where T: class @@ -120,6 +125,7 @@ namespace PluralKit.Bot public CommandService _commands { get; set; } public abstract string Prefix { get; } + public abstract string ContextNoun { get; } public abstract Task ReadContextParameterAsync(string value); public T ContextEntity => Context.GetContextEntity(); @@ -141,6 +147,10 @@ namespace PluralKit.Bot cb.AddParameter("rest", (pb) => pb.WithDefault("").WithIsRemainder(true)); }); } + + public void RaiseNoContextError() { + throw new PKError($"You can only run this command on your own {ContextNoun}."); + } } public class ContextParameterFallbackPreconditionAttribute : PreconditionAttribute @@ -160,9 +170,9 @@ namespace PluralKit.Bot } public static class ContextExt { - public static async Task PromptYesNo(this ICommandContext ctx, IMessage message, TimeSpan? timeout = null) { - await ctx.Message.AddReactionsAsync(new[] {new Emoji(Emojis.Success), new Emoji(Emojis.Error)}); - var reaction = await ctx.AwaitReaction(ctx.Message, message.Author, (r) => r.Emote.Name == Emojis.Success || r.Emote.Name == Emojis.Error, timeout); + public static async Task PromptYesNo(this ICommandContext ctx, IUserMessage message, TimeSpan? timeout = null) { + await message.AddReactionsAsync(new[] {new Emoji(Emojis.Success), new Emoji(Emojis.Error)}); + var reaction = await ctx.AwaitReaction(message, ctx.User, (r) => r.Emote.Name == Emojis.Success || r.Emote.Name == Emojis.Error, timeout ?? TimeSpan.FromMinutes(1)); return reaction.Emote.Name == Emojis.Success; } diff --git a/PluralKit/Stores.cs b/PluralKit/Stores.cs index e9cd4e9f..ec9ba5e6 100644 --- a/PluralKit/Stores.cs +++ b/PluralKit/Stores.cs @@ -60,7 +60,7 @@ namespace PluralKit { public async Task Create(PKSystem system, string name) { // TODO: handle collision var hid = Utils.GenerateHid(); - return await conn.QuerySingleAsync("insert into members (hid, system, name) values (@Hid, @SystemId, @Name) returning *", new { + return await conn.QuerySingleAsync("insert into members (hid, system, name) values (@Hid, @SystemId, @Name) returning *", new { Hid = hid, SystemID = system.Id, Name = name @@ -68,15 +68,11 @@ namespace PluralKit { } public async Task GetByHid(string hid) { - return await conn.QuerySingleAsync("select * from members where hid = @Hid", new { Hid = hid.ToLower() }); + return await conn.QuerySingleOrDefaultAsync("select * from members where hid = @Hid", new { Hid = hid.ToLower() }); } - public async Task GetByName(string name) { - return await conn.QuerySingleAsync("select * from members where lower(name) = lower(@Name)", new { Name = name }); - } - - public async Task GetByNameConstrained(PKSystem system, string name) { - return await conn.QuerySingleAsync("select * from members where lower(name) = @Name and system = @SystemID", new { Name = name, SystemID = system.Id }); + public async Task GetByName(PKSystem system, string name) { + return await conn.QuerySingleOrDefaultAsync("select * from members where lower(name) = @Name and system = @SystemID", new { Name = name, SystemID = system.Id }); } public async Task> GetUnproxyableMembers(PKSystem system) { diff --git a/PluralKit/Utils.cs b/PluralKit/Utils.cs index d5d2856f..09a52eff 100644 --- a/PluralKit/Utils.cs +++ b/PluralKit/Utils.cs @@ -48,5 +48,6 @@ namespace PluralKit public static readonly string Warn = "\u26A0"; public static readonly string Success = "\u2705"; public static readonly string Error = "\u274C"; + public static readonly string Note = "\u2757"; } } \ No newline at end of file