diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index cefdf524..73be80cc 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -130,6 +130,9 @@ namespace PluralKit.Bot 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 if (_result is PreconditionResult) + { + await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} {_result.ErrorReason}"); } else { HandleRuntimeError((_result as ExecuteResult?)?.Exception); } diff --git a/PluralKit.Bot/Commands/LinkCommands.cs b/PluralKit.Bot/Commands/LinkCommands.cs new file mode 100644 index 00000000..cae4e3c6 --- /dev/null +++ b/PluralKit.Bot/Commands/LinkCommands.cs @@ -0,0 +1,50 @@ +using System.Linq; +using System.Threading.Tasks; +using Discord; +using Discord.Commands; + +namespace PluralKit.Bot.Commands +{ + public class LinkCommands: ModuleBase + { + public SystemStore Systems { get; set; } + + + [Command("link")] + [Remarks("link ")] + [MustHaveSystem] + public async Task LinkSystem(IUser account) + { + var accountIds = await Systems.GetLinkedAccountIds(Context.SenderSystem); + if (accountIds.Contains(account.Id)) throw Errors.AccountAlreadyLinked; + + var existingAccount = await Systems.GetByAccount(account.Id); + if (existingAccount != null) throw Errors.AccountInOtherSystem(existingAccount); + + var msg = await Context.Channel.SendMessageAsync( + $"{account.Mention}, please confirm the link by clicking the {Emojis.Success} reaction on this message."); + if (!await Context.PromptYesNo(msg, user: account)) throw Errors.MemberLinkCancelled; + await Systems.Link(Context.SenderSystem, account.Id); + await Context.Channel.SendMessageAsync($"{Emojis.Success} Account linked to system."); + } + + [Command("unlink")] + [Remarks("unlink [account]")] + [MustHaveSystem] + public async Task UnlinkAccount(IUser account = null) + { + if (account == null) account = Context.User; + + var accountIds = (await Systems.GetLinkedAccountIds(Context.SenderSystem)).ToList(); + if (!accountIds.Contains(account.Id)) throw Errors.AccountNotLinked; + if (accountIds.Count == 1) throw Errors.UnlinkingLastAccount; + + var msg = await Context.Channel.SendMessageAsync( + $"Are you sure you want to unlink {account.Mention} from your system?"); + if (!await Context.PromptYesNo(msg)) throw Errors.MemberUnlinkCancelled; + + await Systems.Unlink(Context.SenderSystem, account.Id); + await Context.Channel.SendMessageAsync($"{Emojis.Success} Account unlinked."); + } + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Commands/MiscCommands.cs b/PluralKit.Bot/Commands/MiscCommands.cs index e44d9f39..d678c701 100644 --- a/PluralKit.Bot/Commands/MiscCommands.cs +++ b/PluralKit.Bot/Commands/MiscCommands.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Discord; using Discord.Commands; -namespace PluralKit.Bot { +namespace PluralKit.Bot.Commands { public class MiscCommands: ModuleBase { [Command("invite")] [Remarks("invite")] diff --git a/PluralKit.Bot/Commands/SystemCommands.cs b/PluralKit.Bot/Commands/SystemCommands.cs index 82b16e11..031ce9cf 100644 --- a/PluralKit.Bot/Commands/SystemCommands.cs +++ b/PluralKit.Bot/Commands/SystemCommands.cs @@ -20,7 +20,7 @@ namespace PluralKit.Bot.Commands [Command] public async Task Query(PKSystem system = null) { if (system == null) system = Context.SenderSystem; - if (system == null) throw Errors.NotOwnSystemError; + if (system == null) throw Errors.NoSystemError; await Context.Channel.SendMessageAsync(embed: await EmbedService.CreateSystemEmbed(system)); } diff --git a/PluralKit.Bot/ContextUtils.cs b/PluralKit.Bot/ContextUtils.cs index 39f3ed44..9d9f448e 100644 --- a/PluralKit.Bot/ContextUtils.cs +++ b/PluralKit.Bot/ContextUtils.cs @@ -8,9 +8,9 @@ using Discord.WebSocket; namespace PluralKit.Bot { public static class ContextUtils { - public static async Task PromptYesNo(this ICommandContext ctx, IUserMessage message, TimeSpan? timeout = null) { + public static async Task PromptYesNo(this ICommandContext ctx, IUserMessage message, IUser user = null, 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)); + var reaction = await ctx.AwaitReaction(message, user ?? 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.Bot/Errors.cs b/PluralKit.Bot/Errors.cs index 599db57d..e79f77c3 100644 --- a/PluralKit.Bot/Errors.cs +++ b/PluralKit.Bot/Errors.cs @@ -28,5 +28,12 @@ namespace PluralKit.Bot { public static PKError AvatarNotAnImage(string mimeType) => new PKError($"The given link does not point to an image{(mimeType != null ? $" ({mimeType})" : "")}. Make sure you're using a direct link (ending in .jpg, .png, .gif)."); public static PKError AvatarDimensionsTooLarge(int width, int height) => new PKError($"Image too large ({width}x{height} > {Limits.AvatarDimensionLimit}x{Limits.AvatarDimensionLimit}), try resizing the image."); public static PKError InvalidUrl(string url) => new PKError($"The given URL is invalid."); + + public static PKError AccountAlreadyLinked => new PKError("That account is already linked to your system."); + public static PKError AccountNotLinked => new PKError("That account isn't linked to your system."); + public static PKError AccountInOtherSystem(PKSystem system) => new PKError($"The mentioned account is already linked to another system (see `pk;system {system.Hid}`)."); + public static PKError UnlinkingLastAccount => new PKError("Since this is the only account linked to this system, you cannot unlink it (as that would leave your system account-less)."); + public static PKError MemberLinkCancelled => new PKError("Member link cancelled."); + public static PKError MemberUnlinkCancelled => new PKError("Member unlink cancelled."); } } \ No newline at end of file diff --git a/PluralKit.Bot/Utils.cs b/PluralKit.Bot/Utils.cs index 68c6457c..4d1d0bbf 100644 --- a/PluralKit.Bot/Utils.cs +++ b/PluralKit.Bot/Utils.cs @@ -66,7 +66,7 @@ namespace PluralKit.Bot public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { var client = services.GetService(); - var conn = services.GetService(); + var systems = services.GetService(); // System references can take three forms: // - The direct user ID of an account connected to the system @@ -74,20 +74,20 @@ namespace PluralKit.Bot // - A system hid // First, try direct user ID parsing - if (ulong.TryParse(input, out var idFromNumber)) return await FindSystemByAccountHelper(idFromNumber, client, conn); + if (ulong.TryParse(input, out var idFromNumber)) return await FindSystemByAccountHelper(idFromNumber, client, systems); // Then, try mention parsing. - if (MentionUtils.TryParseUser(input, out var idFromMention)) return await FindSystemByAccountHelper(idFromMention, client, conn); + if (MentionUtils.TryParseUser(input, out var idFromMention)) return await FindSystemByAccountHelper(idFromMention, client, systems); // Finally, try HID parsing - var res = await conn.QuerySingleOrDefaultAsync("select * from systems where hid = @Hid", new { Hid = input }); + var res = await systems.GetByHid(input); if (res != null) return TypeReaderResult.FromSuccess(res); return TypeReaderResult.FromError(CommandError.ObjectNotFound, $"System with ID `{input}` not found."); } - async Task FindSystemByAccountHelper(ulong id, IDiscordClient client, IDbConnection conn) + async Task FindSystemByAccountHelper(ulong id, IDiscordClient client, SystemStore systems) { - var foundByAccountId = await conn.QuerySingleOrDefaultAsync("select * from accounts, systems where accounts.system = system.id and accounts.id = @Id", new { Id = id }); + var foundByAccountId = await systems.GetByAccount(id); if (foundByAccountId != null) return TypeReaderResult.FromSuccess(foundByAccountId); // We didn't find any, so we try to resolve the user ID to find the associated account, diff --git a/PluralKit.Core/Stores.cs b/PluralKit.Core/Stores.cs index 5b27b5b7..9cca11fa 100644 --- a/PluralKit.Core/Stores.cs +++ b/PluralKit.Core/Stores.cs @@ -23,9 +23,13 @@ namespace PluralKit { public async Task Link(PKSystem system, ulong accountId) { await conn.ExecuteAsync("insert into accounts (uid, system) values (@Id, @SystemId)", new { Id = accountId, SystemId = system.Id }); } + + public async Task Unlink(PKSystem system, ulong accountId) { + await conn.ExecuteAsync("delete from accounts where uid = @Id and system = @SystemId", new { Id = accountId, SystemId = system.Id }); + } public async Task GetByAccount(ulong accountId) { - return await conn.QuerySingleOrDefaultAsync("select systems.* from systems, accounts where accounts.system = system.id and accounts.uid = @Id", new { Id = accountId }); + return await conn.QuerySingleOrDefaultAsync("select systems.* from systems, accounts where accounts.system = systems.id and accounts.uid = @Id", new { Id = accountId }); } public async Task GetByHid(string hid) {