using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using App.Metrics; using Autofac; using DSharpPlus; using DSharpPlus.Entities; using DSharpPlus.Exceptions; using PluralKit.Bot.Utils; using PluralKit.Core; namespace PluralKit.Bot { public class Context { private ILifetimeScope _provider; private readonly DiscordRestClient _rest; private readonly DiscordShardedClient _client; private readonly DiscordClient _shard; private readonly DiscordMessage _message; private readonly Parameters _parameters; private readonly MessageContext _messageContext; private readonly IDataStore _data; private readonly PKSystem _senderSystem; private readonly IMetrics _metrics; private Command _currentCommand; public Context(ILifetimeScope provider, DiscordClient shard, DiscordMessage message, int commandParseOffset, PKSystem senderSystem, MessageContext messageContext) { _rest = provider.Resolve(); _client = provider.Resolve(); _message = message; _shard = shard; _data = provider.Resolve(); _senderSystem = senderSystem; _messageContext = messageContext; _metrics = provider.Resolve(); _provider = provider; _parameters = new Parameters(message.Content.Substring(commandParseOffset)); } public DiscordUser Author => _message.Author; public DiscordChannel Channel => _message.Channel; public DiscordMessage Message => _message; public DiscordGuild Guild => _message.Channel.Guild; public DiscordClient Shard => _shard; public DiscordShardedClient Client => _client; public MessageContext MessageContext => _messageContext; public DiscordRestClient Rest => _rest; public PKSystem System => _senderSystem; 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; public Task Reply(string text = null, DiscordEmbed embed = null, IEnumerable mentions = null) { if (!this.BotHasAllPermissions(Permissions.SendMessages)) // Will be "swallowed" during the error handler anyway, this message is never shown. throw new PKError("PluralKit does not have permission to send messages in this channel."); if (embed != null && !this.BotHasAllPermissions(Permissions.EmbedLinks)) 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) { _currentCommand = commandDef; try { await handler(_provider.Resolve()); _metrics.Measure.Meter.Mark(BotMetrics.CommandsRun); } catch (PKSyntaxError e) { await Reply($"{Emojis.Error} {e.Message}\n**Command usage:**\n> pk;{commandDef.Usage}"); } catch (PKError e) { await Reply($"{Emojis.Error} {e.Message}"); } catch (TimeoutException) { // Got a complaint the old error was a bit too patronizing. Hopefully this is better? await Reply($"{Emojis.Error} Operation timed out, sorry. Try again, perhaps?"); } } 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 Context CheckSystem() { if (_senderSystem == null) throw Errors.NoSystemError; return this; } public Context CheckNoSystem() { if (_senderSystem != null) throw Errors.ExistingSystemError; return this; } public Context CheckOwnMember(PKMember member) { if (member.System != _senderSystem.Id) throw Errors.NotOwnMemberError; return this; } public Context CheckAuthorPermission(Permissions neededPerms, string permissionName) { // TODO: can we always assume Author is a DiscordMember? I would think so, given they always come from a // message received event... var hasPerms = Channel.PermissionsInSync(Author); if ((hasPerms & neededPerms) != neededPerms) throw new PKError($"You must have the \"{permissionName}\" permission in this server to use this command."); return this; } public Context CheckGuildContext() { if (Channel.Guild != null) return this; throw new PKError("This command can not be run in a DM."); } public LookupContext LookupContextFor(PKSystem target) => System?.Id == target.Id ? LookupContext.ByOwner : LookupContext.ByNonOwner; public LookupContext LookupContextFor(SystemId systemId) => System?.Id == systemId ? LookupContext.ByOwner : LookupContext.ByNonOwner; public LookupContext LookupContextFor(PKMember target) => System?.Id == target.System ? LookupContext.ByOwner : LookupContext.ByNonOwner; public Context CheckSystemPrivacy(PKSystem target, PrivacyLevel level) { if (level.CanAccess(LookupContextFor(target))) return this; throw new PKError("You do not have permission to access this information."); } 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; } }