diff --git a/PluralKit/Bot/Bot.cs b/PluralKit/Bot/Bot.cs index ae8d36ba..bc617b51 100644 --- a/PluralKit/Bot/Bot.cs +++ b/PluralKit/Bot/Bot.cs @@ -94,9 +94,12 @@ namespace PluralKit.Bot await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); _client.Ready += Ready; - _client.MessageReceived += MessageReceived; - _client.ReactionAdded += _proxy.HandleReactionAddedAsync; - _client.MessageDeleted += _proxy.HandleMessageDeletedAsync; + + // Deliberately wrapping in an async function *without* awaiting, we don't want to "block" since this'd hold up the main loop + // These handlers return Task so we gotta be careful not to return the Task itself (which would then be awaited) - kinda weird design but eh + _client.MessageReceived += async (msg) => MessageReceived(msg); + _client.ReactionAdded += async (message, channel, reaction) => _proxy.HandleReactionAddedAsync(message, channel, reaction); + _client.MessageDeleted += async (message, channel) => _proxy.HandleMessageDeletedAsync(message, channel); } private async Task UpdatePeriodic() @@ -118,9 +121,11 @@ namespace PluralKit.Bot if (!_result.IsSuccess) { // If this is a PKError (ie. thrown deliberately), show user facing message // If not, log as error - var pkError = (_result as ExecuteResult?)?.Exception as PKError; - if (pkError != null) { - await ctx.Message.Channel.SendMessageAsync("\u274C " + pkError.Message); + 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); } diff --git a/PluralKit/Bot/Commands/SystemCommands.cs b/PluralKit/Bot/Commands/SystemCommands.cs index b72b44b2..3f7175a8 100644 --- a/PluralKit/Bot/Commands/SystemCommands.cs +++ b/PluralKit/Bot/Commands/SystemCommands.cs @@ -30,7 +30,7 @@ namespace PluralKit.Bot.Commands public async Task New([Remainder] string systemName = null) { if (ContextEntity != null) throw OTHER_SYSTEM_CONTEXT_ERROR; - 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."); + 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); @@ -79,6 +79,19 @@ namespace PluralKit.Bot.Commands await Context.Channel.SendMessageAsync($"{Emojis.Success} System tag {(newTag != null ? "changed" : "cleared")}."); } + [Command("delete")] + public async Task Delete() { + if (ContextEntity != null) throw OTHER_SYSTEM_CONTEXT_ERROR; + if (Context.SenderSystem == null) throw NO_SYSTEM_ERROR; + + 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)); + if (reply.Content != Context.SenderSystem.Hid) throw new PKError($"System deletion cancelled. Note that you must reply with your system ID (`{Context.SenderSystem.Hid}`) *verbatim*."); + + await Systems.Delete(Context.SenderSystem); + await Context.Channel.SendMessageAsync($"{Emojis.Success} System deleted."); + } + public override async Task ReadContextParameterAsync(string value) { var res = await new PKSystemTypeReader().ReadAsync(Context, value, _services); diff --git a/PluralKit/Bot/Utils.cs b/PluralKit/Bot/Utils.cs index e81b7789..38aaac3b 100644 --- a/PluralKit/Bot/Utils.cs +++ b/PluralKit/Bot/Utils.cs @@ -162,33 +162,43 @@ 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.WaitForReaction(ctx.Message, message.Author, (r) => r.Emote.Name == Emojis.Success || r.Emote.Name == Emojis.Error); + var reaction = await ctx.AwaitReaction(ctx.Message, message.Author, (r) => r.Emote.Name == Emojis.Success || r.Emote.Name == Emojis.Error, timeout); return reaction.Emote.Name == Emojis.Success; } - public static async Task WaitForReaction(this ICommandContext ctx, IUserMessage message, IUser user = null, Func predicate = null, TimeSpan? timeout = null) { + public static async Task AwaitReaction(this ICommandContext ctx, IUserMessage message, IUser user = null, Func predicate = null, TimeSpan? timeout = null) { var tcs = new TaskCompletionSource(); - Task Inner(Cacheable _message, ISocketMessageChannel _channel, SocketReaction reaction) { - // Ignore reactions for different messages - if (message.Id != _message.Id) return Task.CompletedTask; - - // Ignore messages from other users if a user was defined - if (user != null && user.Id != reaction.UserId) return Task.CompletedTask; - - // Check the predicate, if true - accept the reaction - if (predicate?.Invoke(reaction) ?? true) { - tcs.SetResult(reaction); - } + if (message.Id != _message.Id) return Task.CompletedTask; // Ignore reactions for different messages + if (user != null && user.Id != reaction.UserId) return Task.CompletedTask; // Ignore messages from other users if a user was defined + if (predicate != null && !predicate.Invoke(reaction)) return Task.CompletedTask; // Check predicate + tcs.SetResult(reaction); return Task.CompletedTask; } - (ctx as BaseSocketClient).ReactionAdded += Inner; - + (ctx.Client as BaseSocketClient).ReactionAdded += Inner; try { return await (tcs.Task.TimeoutAfter(timeout)); } finally { - (ctx as BaseSocketClient).ReactionAdded -= Inner; + (ctx.Client as BaseSocketClient).ReactionAdded -= Inner; + } + } + + public static async Task AwaitMessage(this ICommandContext ctx, IMessageChannel channel, IUser user = null, Func predicate = null, TimeSpan? timeout = null) { + var tcs = new TaskCompletionSource(); + Task Inner(SocketMessage msg) { + if (channel != msg.Channel) return Task.CompletedTask; // Ignore messages in a different channel + if (user != null && user != msg.Author) return Task.CompletedTask; // Ignore messages from other users + if (predicate != null && !predicate.Invoke(msg)) return Task.CompletedTask; // Check predicate + tcs.SetResult(msg as IUserMessage); + return Task.CompletedTask; + } + + (ctx.Client as BaseSocketClient).MessageReceived += Inner; + try { + return await (tcs.Task.TimeoutAfter(timeout)); + } finally { + (ctx.Client as BaseSocketClient).MessageReceived -= Inner; } } }