diff --git a/PluralKit/Bot/Bot.cs b/PluralKit/Bot/Bot.cs index 463077a9..ae8d36ba 100644 --- a/PluralKit/Bot/Bot.cs +++ b/PluralKit/Bot/Bot.cs @@ -68,7 +68,6 @@ namespace PluralKit.Bot .AddSingleton() .BuildServiceProvider(); } - class Bot { private IServiceProvider _services; @@ -117,7 +116,14 @@ namespace PluralKit.Bot private async Task CommandExecuted(Optional cmd, ICommandContext ctx, IResult _result) { if (!_result.IsSuccess) { - await ctx.Message.Channel.SendMessageAsync("\u274C " + _result.ErrorReason); + // 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); + } 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 cfe72dd9..b72b44b2 100644 --- a/PluralKit/Bot/Commands/SystemCommands.cs +++ b/PluralKit/Bot/Commands/SystemCommands.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using System.Runtime.Serialization; using System.Threading.Tasks; using Dapper; using Discord.Commands; @@ -13,67 +15,68 @@ namespace PluralKit.Bot.Commands public MemberStore Members {get; set;} public EmbedService EmbedService {get; set;} - private RuntimeResult NO_SYSTEM_ERROR => PKResult.Error($"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 RuntimeResult OTHER_SYSTEM_CONTEXT_ERROR => PKResult.Error("You can only run this command on your own system."); + 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) { + public async Task Query(PKSystem system = null) { if (system == null) system = Context.SenderSystem; - if (system == null) return NO_SYSTEM_ERROR; + if (system == null) throw NO_SYSTEM_ERROR; await Context.Channel.SendMessageAsync(embed: await EmbedService.CreateSystemEmbed(system)); - return PKResult.Success(); } [Command("new")] - public async Task New([Remainder] string systemName = null) + public async Task New([Remainder] string systemName = null) { - if (ContextEntity != null) return OTHER_SYSTEM_CONTEXT_ERROR; - if (Context.SenderSystem != null) return PKResult.Error("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 (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."); 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."); - return PKResult.Success(); } [Command("name")] - public async Task Name([Remainder] string newSystemName = null) { - if (ContextEntity != null) return OTHER_SYSTEM_CONTEXT_ERROR; - if (Context.SenderSystem == null) return NO_SYSTEM_ERROR; - if (newSystemName != null && newSystemName.Length > 250) return PKResult.Error($"Your chosen system name is too long. ({newSystemName.Length} > 250 characters)"); + 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 (newSystemName != null && newSystemName.Length > 250) throw new PKError($"Your chosen system name is too long. ({newSystemName.Length} > 250 characters)"); Context.SenderSystem.Name = newSystemName; await Systems.Save(Context.SenderSystem); - return PKResult.Success(); + await Context.Channel.SendMessageAsync($"{Emojis.Success} System name {(newSystemName != null ? "changed" : "cleared")}."); } [Command("description")] - public async Task Description([Remainder] string newDescription = null) { - if (ContextEntity != null) return OTHER_SYSTEM_CONTEXT_ERROR; - if (Context.SenderSystem == null) return NO_SYSTEM_ERROR; - if (newDescription != null && newDescription.Length > 1000) return PKResult.Error($"Your chosen description is too long. ({newDescription.Length} > 250 characters)"); + 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 (newDescription != null && newDescription.Length > 1000) throw new PKError($"Your chosen description is too long. ({newDescription.Length} > 250 characters)"); Context.SenderSystem.Description = newDescription; await Systems.Save(Context.SenderSystem); - return PKResult.Success("uwu"); + await Context.Channel.SendMessageAsync($"{Emojis.Success} System description {(newDescription != null ? "changed" : "cleared")}."); } [Command("tag")] - public async Task Tag([Remainder] string newTag = null) { - if (ContextEntity != null) return OTHER_SYSTEM_CONTEXT_ERROR; - if (Context.SenderSystem == null) return NO_SYSTEM_ERROR; + 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 (newTag.Length > 30) throw new PKError($"Your chosen description is too long. ({newTag.Length} > 30 characters)"); Context.SenderSystem.Tag = newTag; + // Check unproxyable messages *after* changing the tag (so it's seen in the method) but *before* we save to DB (so we can cancel) var unproxyableMembers = await Members.GetUnproxyableMembers(Context.SenderSystem); - //if (unproxyableMembers.Count > 0) { - throw new Exception("sdjsdflsdf"); - //} + 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."); + } await Systems.Save(Context.SenderSystem); - return PKResult.Success("uwu"); + await Context.Channel.SendMessageAsync($"{Emojis.Success} System tag {(newTag != null ? "changed" : "cleared")}."); } public override async Task ReadContextParameterAsync(string value) diff --git a/PluralKit/Bot/Utils.cs b/PluralKit/Bot/Utils.cs index cf61901f..e81b7789 100644 --- a/PluralKit/Bot/Utils.cs +++ b/PluralKit/Bot/Utils.cs @@ -159,13 +159,43 @@ namespace PluralKit.Bot } } - public class PKResult : RuntimeResult - { - public PKResult(CommandError? error, string reason) : base(error, reason) - { + 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); + return reaction.Emote.Name == Emojis.Success; } - public static RuntimeResult Error(string reason) => new PKResult(CommandError.Unsuccessful, reason); - public static RuntimeResult Success(string reason = null) => new PKResult(null, reason); + public static async Task WaitForReaction(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); + } + return Task.CompletedTask; + } + + (ctx as BaseSocketClient).ReactionAdded += Inner; + + try { + return await (tcs.Task.TimeoutAfter(timeout)); + } finally { + (ctx as BaseSocketClient).ReactionAdded -= Inner; + } + } + } + class PKError : Exception + { + public PKError(string message) : base(message) + { + } } } \ No newline at end of file diff --git a/PluralKit/Stores.cs b/PluralKit/Stores.cs index 2b943387..e9cd4e9f 100644 --- a/PluralKit/Stores.cs +++ b/PluralKit/Stores.cs @@ -37,12 +37,12 @@ namespace PluralKit { } public async Task Save(PKSystem system) { - await conn.UpdateAsync(system); + await conn.ExecuteAsync("update systems set name = @Name, description = @Description, tag = @Tag, avatar_url = @AvatarUrl, token = @Token, ui_tz = @UiTz where id = @Id", system); } public async Task Delete(PKSystem system) { - await conn.DeleteAsync(system); - } + await conn.ExecuteAsync("delete from systems where id = @Id", system); + } public async Task> GetLinkedAccountIds(PKSystem system) { diff --git a/PluralKit/Utils.cs b/PluralKit/Utils.cs index 5190cee7..d5d2856f 100644 --- a/PluralKit/Utils.cs +++ b/PluralKit/Utils.cs @@ -1,5 +1,6 @@ using System; using System.Data; +using System.Threading; using System.Threading.Tasks; using Dapper; using Discord; @@ -28,5 +29,24 @@ namespace PluralKit if (str.Length < maxLength) return str; return str.Substring(0, maxLength - ellipsis.Length) + ellipsis; } + + public static async Task TimeoutAfter(this Task task, TimeSpan? timeout) { + // https://stackoverflow.com/a/22078975 + using (var timeoutCancellationTokenSource = new CancellationTokenSource()) { + var completedTask = await Task.WhenAny(task, Task.Delay(timeout ?? TimeSpan.FromMilliseconds(-1), timeoutCancellationTokenSource.Token)); + if (completedTask == task) { + timeoutCancellationTokenSource.Cancel(); + return await task; // Very important in order to propagate exceptions + } else { + throw new TimeoutException(); + } + } + } + } + + public static class Emojis { + public static readonly string Warn = "\u26A0"; + public static readonly string Success = "\u2705"; + public static readonly string Error = "\u274C"; } } \ No newline at end of file