diff --git a/PluralKit.Bot/CommandSystem/Context.cs b/PluralKit.Bot/CommandSystem/Context.cs index ef68229d..b259b9ec 100644 --- a/PluralKit.Bot/CommandSystem/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using App.Metrics; @@ -9,9 +8,7 @@ using Autofac; using DSharpPlus; using DSharpPlus.Entities; -using DSharpPlus.Exceptions; -using PluralKit.Bot.Utils; using PluralKit.Core; namespace PluralKit.Bot @@ -59,12 +56,11 @@ namespace PluralKit.Bot public DiscordRestClient Rest => _rest; public PKSystem System => _senderSystem; + + public Parameters Parameters => _parameters; - public string PopArgument() => _parameters.Pop(); - public string PeekArgument() => _parameters.Peek(); - public string RemainderOrNull(bool skipFlags = true) => _parameters.Remainder(skipFlags).Length == 0 ? null : _parameters.Remainder(skipFlags); - public bool HasNext(bool skipFlags = true) => RemainderOrNull(skipFlags) != null; - public string FullCommand => _parameters.FullCommand; + // TODO: this is just here so the extension methods can access it; should it be public/private/? + internal IDataStore DataStore => _data; public Task Reply(string text = null, DiscordEmbed embed = null, IEnumerable mentions = null) { @@ -76,42 +72,6 @@ namespace PluralKit.Bot throw new PKError("PluralKit does not have permission to send embeds in this channel. Please ensure I have the **Embed Links** permission enabled."); return Channel.SendMessageFixedAsync(text, embed: embed, mentions: mentions); } - - /// - /// Checks if the next parameter is equal to one of the given keywords. Case-insensitive. - /// - public bool Match(ref string used, params string[] potentialMatches) - { - var arg = PeekArgument(); - foreach (var match in potentialMatches) - { - if (arg.Equals(match, StringComparison.InvariantCultureIgnoreCase)) - { - used = PopArgument(); - return true; - } - } - - return false; - } - - /// - /// Checks if the next parameter is equal to one of the given keywords. Case-insensitive. - /// - public bool Match(params string[] potentialMatches) - { - string used = null; // Unused and unreturned, we just yeet it - return Match(ref used, potentialMatches); - } - - public bool MatchFlag(params string[] potentialMatches) - { - // Flags are *ALWAYS PARSED LOWERCASE*. This means we skip out on a "ToLower" call here. - // Can assume the caller array only contains lowercase *and* the set below only contains lowercase - - var flags = _parameters.Flags(); - return potentialMatches.Any(potentialMatch => flags.Contains(potentialMatch)); - } public async Task Execute(Command commandDef, Func handler) { @@ -137,109 +97,6 @@ namespace PluralKit.Bot } } - public async Task MatchUser() - { - var text = PeekArgument(); - if (text.TryParseMention(out var id)) - return await Shard.GetUserAsync(id); - return null; - } - - public bool MatchUserRaw(out ulong id) - { - id = 0; - - var text = PeekArgument(); - if (text.TryParseMention(out var mentionId)) - id = mentionId; - - return id != 0; - } - - public Task PeekSystem() => MatchSystemInner(); - - public async Task MatchSystem() - { - var system = await MatchSystemInner(); - if (system != null) PopArgument(); - return system; - } - - private async Task MatchSystemInner() - { - var input = PeekArgument(); - - // System references can take three forms: - // - The direct user ID of an account connected to the system - // - A @mention of an account connected to the system (<@uid>) - // - A system hid - - // Direct IDs and mentions are both handled by the below method: - if (input.TryParseMention(out var id)) - return await _data.GetSystemByAccount(id); - - // Finally, try HID parsing - var system = await _data.GetSystemByHid(input); - return system; - } - - public async Task PeekMember() - { - var input = PeekArgument(); - - // Member references can have one of three forms, depending on - // whether you're in a system or not: - // - A member hid - // - A textual name of a member *in your own system* - // - a textual display name of a member *in your own system* - - // First, if we have a system, try finding by member name in system - if (_senderSystem != null && await _data.GetMemberByName(_senderSystem, input) is PKMember memberByName) - return memberByName; - - // Then, try member HID parsing: - if (await _data.GetMemberByHid(input) is PKMember memberByHid) - return memberByHid; - - // And if that again fails, we try finding a member with a display name matching the argument from the system - if (_senderSystem != null && await _data.GetMemberByDisplayName(_senderSystem, input) is PKMember memberByDisplayName) - return memberByDisplayName; - - // We didn't find anything, so we return null. - return null; - } - - /// - /// Attempts to pop a member descriptor from the stack, returning it if present. If a member could not be - /// resolved by the next word in the argument stack, does *not* touch the stack, and returns null. - /// - public async Task MatchMember() - { - // First, peek a member - var member = await PeekMember(); - - // If the peek was successful, we've used up the next argument, so we pop that just to get rid of it. - if (member != null) PopArgument(); - - // Finally, we return the member value. - return member; - } - - public string CreateMemberNotFoundError(string input) - { - // TODO: does this belong here? - if (input.Length == 5) - { - if (_senderSystem != null) - return $"Member with ID or name \"{input}\" not found."; - return $"Member with ID \"{input}\" not found."; // Accounts without systems can't query by name - } - - if (_senderSystem != null) - return $"Member with name \"{input}\" not found. Note that a member ID is 5 characters long."; - return $"Member not found. Note that a member ID is 5 characters long."; - } - public LookupContext LookupContextFor(PKSystem target) => System?.Id == target.Id ? LookupContext.ByOwner : LookupContext.ByNonOwner; @@ -248,30 +105,7 @@ namespace PluralKit.Bot public LookupContext LookupContextFor(PKMember target) => System?.Id == target.System ? LookupContext.ByOwner : LookupContext.ByNonOwner; - - public async Task MatchChannel() - { - if (!MentionUtils.TryParseChannel(PeekArgument(), out var channel)) - return null; - - try - { - var discordChannel = await _shard.GetChannelAsync(channel); - if (discordChannel.Type != ChannelType.Text) return null; - - PopArgument(); - return discordChannel; - } - catch (NotFoundException) - { - return null; - } - catch (UnauthorizedException) - { - return null; - } - } - + public IComponentContext Services => _provider; } } \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/ContextArgumentsExt.cs b/PluralKit.Bot/CommandSystem/ContextArgumentsExt.cs new file mode 100644 index 00000000..0a8ac3bb --- /dev/null +++ b/PluralKit.Bot/CommandSystem/ContextArgumentsExt.cs @@ -0,0 +1,59 @@ +using System; +using System.Linq; + +namespace PluralKit.Bot +{ + public static class ContextArgumentsExt + { + public static string PopArgument(this Context ctx) => + ctx.Parameters.Pop(); + + public static string PeekArgument(this Context ctx) => + ctx.Parameters.Peek(); + + public static string RemainderOrNull(this Context ctx, bool skipFlags = true) => + ctx.Parameters.Remainder(skipFlags).Length == 0 ? null : ctx.Parameters.Remainder(skipFlags); + + public static bool HasNext(this Context ctx, bool skipFlags = true) => + ctx.RemainderOrNull(skipFlags) != null; + + public static string FullCommand(this Context ctx) => + ctx.Parameters.FullCommand; + + /// + /// Checks if the next parameter is equal to one of the given keywords. Case-insensitive. + /// + public static bool Match(this Context ctx, ref string used, params string[] potentialMatches) + { + var arg = ctx.PeekArgument(); + foreach (var match in potentialMatches) + { + if (arg.Equals(match, StringComparison.InvariantCultureIgnoreCase)) + { + used = ctx.PopArgument(); + return true; + } + } + + return false; + } + + /// + /// Checks if the next parameter is equal to one of the given keywords. Case-insensitive. + /// + public static bool Match(this Context ctx, params string[] potentialMatches) + { + string used = null; // Unused and unreturned, we just yeet it + return ctx.Match(ref used, potentialMatches); + } + + public static bool MatchFlag(this Context ctx, params string[] potentialMatches) + { + // Flags are *ALWAYS PARSED LOWERCASE*. This means we skip out on a "ToLower" call here. + // Can assume the caller array only contains lowercase *and* the set below only contains lowercase + + var flags = ctx.Parameters.Flags(); + return potentialMatches.Any(potentialMatch => flags.Contains(potentialMatch)); + } + } +} \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/ContextChecks.cs b/PluralKit.Bot/CommandSystem/ContextChecksExt.cs similarity index 94% rename from PluralKit.Bot/CommandSystem/ContextChecks.cs rename to PluralKit.Bot/CommandSystem/ContextChecksExt.cs index 54bb45a6..46fb6499 100644 --- a/PluralKit.Bot/CommandSystem/ContextChecks.cs +++ b/PluralKit.Bot/CommandSystem/ContextChecksExt.cs @@ -1,12 +1,10 @@ -using System.Threading.Channels; - -using DSharpPlus; +using DSharpPlus; using PluralKit.Core; namespace PluralKit.Bot { - public static class ContextChecks + public static class ContextChecksExt { public static Context CheckGuildContext(this Context ctx) { diff --git a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs new file mode 100644 index 00000000..eacb8e27 --- /dev/null +++ b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs @@ -0,0 +1,140 @@ +using System.Threading.Tasks; + +using DSharpPlus; +using DSharpPlus.Entities; +using DSharpPlus.Exceptions; + +using PluralKit.Bot.Utils; +using PluralKit.Core; + +namespace PluralKit.Bot +{ + public static class ContextEntityArgumentsExt + { + public static async Task MatchUser(this Context ctx) + { + var text = ctx.PeekArgument(); + if (text.TryParseMention(out var id)) + return await ctx.Shard.GetUserAsync(id); + return null; + } + + public static bool MatchUserRaw(this Context ctx, out ulong id) + { + id = 0; + + var text = ctx.PeekArgument(); + if (text.TryParseMention(out var mentionId)) + id = mentionId; + + return id != 0; + } + + public static Task PeekSystem(this Context ctx) => ctx.MatchSystemInner(); + + public static async Task MatchSystem(this Context ctx) + { + var system = await ctx.MatchSystemInner(); + if (system != null) ctx.PopArgument(); + return system; + } + + private static async Task MatchSystemInner(this Context ctx) + { + var input = ctx.PeekArgument(); + + // System references can take three forms: + // - The direct user ID of an account connected to the system + // - A @mention of an account connected to the system (<@uid>) + // - A system hid + + // Direct IDs and mentions are both handled by the below method: + if (input.TryParseMention(out var id)) + return await ctx.DataStore.GetSystemByAccount(id); + + // Finally, try HID parsing + var system = await ctx.DataStore.GetSystemByHid(input); + return system; + } + + public static async Task PeekMember(this Context ctx) + { + var input = ctx.PeekArgument(); + + // Member references can have one of three forms, depending on + // whether you're in a system or not: + // - A member hid + // - A textual name of a member *in your own system* + // - a textual display name of a member *in your own system* + + // First, if we have a system, try finding by member name in system + if (ctx.System != null && await ctx.DataStore.GetMemberByName(ctx.System, input) is PKMember memberByName) + return memberByName; + + // Then, try member HID parsing: + if (await ctx.DataStore.GetMemberByHid(input) is PKMember memberByHid) + return memberByHid; + + // And if that again fails, we try finding a member with a display name matching the argument from the system + if (ctx.System != null && await ctx.DataStore.GetMemberByDisplayName(ctx.System, input) is PKMember memberByDisplayName) + return memberByDisplayName; + + // We didn't find anything, so we return null. + return null; + } + + /// + /// Attempts to pop a member descriptor from the stack, returning it if present. If a member could not be + /// resolved by the next word in the argument stack, does *not* touch the stack, and returns null. + /// + public static async Task MatchMember(this Context ctx) + { + // First, peek a member + var member = await ctx.PeekMember(); + + // If the peek was successful, we've used up the next argument, so we pop that just to get rid of it. + if (member != null) ctx.PopArgument(); + + // Finally, we return the member value. + return member; + } + + public static string CreateMemberNotFoundError(this Context ctx, string input) + { + // TODO: does this belong here? + if (input.Length == 5) + { + if (ctx.System != null) + return $"Member with ID or name \"{input}\" not found."; + return $"Member with ID \"{input}\" not found."; // Accounts without systems can't query by name + } + + if (ctx.System != null) + return $"Member with name \"{input}\" not found. Note that a member ID is 5 characters long."; + return $"Member not found. Note that a member ID is 5 characters long."; + } + + public static async Task MatchChannel(this Context ctx) + { + if (!MentionUtils.TryParseChannel(ctx.PeekArgument(), out var channel)) + return null; + + try + { + var discordChannel = await ctx.Shard.GetChannelAsync(channel); + if (discordChannel.Type != ChannelType.Text) return null; + + ctx.PopArgument(); + return discordChannel; + } + catch (NotFoundException) + { + return null; + } + catch (UnauthorizedException) + { + return null; + } + } + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index f1aec8b0..fbb69720 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -327,7 +327,7 @@ namespace PluralKit.Bot { var commandListStr = CreatePotentialCommandList(potentialCommands); await ctx.Reply( - $"{Emojis.Error} Unknown command `pk;{ctx.FullCommand}`. Perhaps you meant to use one of the following commands?\n{commandListStr}\n\nFor a full list of possible commands, see ."); + $"{Emojis.Error} Unknown command `pk;{ctx.FullCommand()}`. Perhaps you meant to use one of the following commands?\n{commandListStr}\n\nFor a full list of possible commands, see ."); } private async Task PrintCommandExpectedError(Context ctx, params Command[] potentialCommands)