PluralKit/PluralKit.Bot/CommandSystem/Context.cs

321 lines
12 KiB
C#
Raw Normal View History

2019-10-05 05:41:00 +00:00
using System;
using System.Collections.Generic;
2020-02-06 16:47:37 +00:00
using System.Linq;
2019-10-05 05:41:00 +00:00
using System.Threading.Tasks;
2019-12-21 20:51:41 +00:00
using App.Metrics;
2020-01-26 00:27:45 +00:00
using Autofac;
2019-10-05 05:41:00 +00:00
using DSharpPlus;
using DSharpPlus.Entities;
using DSharpPlus.Exceptions;
2020-04-24 19:50:28 +00:00
using PluralKit.Bot.Utils;
using PluralKit.Core;
2019-10-05 05:41:00 +00:00
namespace PluralKit.Bot
2019-10-05 05:41:00 +00:00
{
public class Context
{
2020-01-26 00:27:45 +00:00
private ILifetimeScope _provider;
2019-10-05 05:41:00 +00:00
2020-04-24 19:50:28 +00:00
private readonly DiscordRestClient _rest;
2019-10-05 05:41:00 +00:00
private readonly DiscordShardedClient _client;
private readonly DiscordClient _shard;
private readonly DiscordMessage _message;
2019-10-05 05:41:00 +00:00
private readonly Parameters _parameters;
private readonly MessageContext _messageContext;
2019-10-05 05:41:00 +00:00
private readonly IDataStore _data;
2019-10-05 05:41:00 +00:00
private readonly PKSystem _senderSystem;
2019-12-21 20:51:41 +00:00
private readonly IMetrics _metrics;
2019-10-05 05:41:00 +00:00
private Command _currentCommand;
public Context(ILifetimeScope provider, DiscordClient shard, DiscordMessage message, int commandParseOffset,
PKSystem senderSystem, MessageContext messageContext)
2019-10-05 05:41:00 +00:00
{
2020-04-24 19:50:28 +00:00
_rest = provider.Resolve<DiscordRestClient>();
2020-01-26 00:27:45 +00:00
_client = provider.Resolve<DiscordShardedClient>();
2019-10-05 05:41:00 +00:00
_message = message;
_shard = shard;
2020-01-26 00:27:45 +00:00
_data = provider.Resolve<IDataStore>();
2019-10-05 05:41:00 +00:00
_senderSystem = senderSystem;
_messageContext = messageContext;
2020-01-26 00:27:45 +00:00
_metrics = provider.Resolve<IMetrics>();
2019-10-05 05:41:00 +00:00
_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;
2019-10-05 05:41:00 +00:00
public DiscordShardedClient Client => _client;
public MessageContext MessageContext => _messageContext;
2020-04-24 19:50:28 +00:00
public DiscordRestClient Rest => _rest;
2019-10-05 05:41:00 +00:00
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;
2019-10-05 05:41:00 +00:00
public string FullCommand => _parameters.FullCommand;
public Task<DiscordMessage> Reply(string text = null, DiscordEmbed embed = null, IEnumerable<IMention> 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);
}
2019-10-05 05:41:00 +00:00
/// <summary>
/// Checks if the next parameter is equal to one of the given keywords. Case-insensitive.
/// </summary>
2020-01-11 15:49:20 +00:00
public bool Match(ref string used, params string[] potentialMatches)
2019-10-05 05:41:00 +00:00
{
2020-02-06 16:47:37 +00:00
var arg = PeekArgument();
2019-10-05 05:41:00 +00:00
foreach (var match in potentialMatches)
{
2020-02-06 16:47:37 +00:00
if (arg.Equals(match, StringComparison.InvariantCultureIgnoreCase))
2019-10-05 05:41:00 +00:00
{
2020-01-11 15:49:20 +00:00
used = PopArgument();
2019-10-05 05:41:00 +00:00
return true;
}
}
return false;
}
2020-01-11 15:49:20 +00:00
/// <summary>
/// Checks if the next parameter is equal to one of the given keywords. Case-insensitive.
/// </summary>
public bool Match(params string[] potentialMatches)
{
string used = null; // Unused and unreturned, we just yeet it
return Match(ref used, potentialMatches);
}
2020-02-06 16:47:37 +00:00
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));
}
2020-01-11 15:49:20 +00:00
2019-10-05 05:41:00 +00:00
public async Task Execute<T>(Command commandDef, Func<T, Task> handler)
{
_currentCommand = commandDef;
try
{
2020-01-26 00:27:45 +00:00
await handler(_provider.Resolve<T>());
2019-12-21 20:51:41 +00:00
_metrics.Measure.Meter.Mark(BotMetrics.CommandsRun);
2019-10-05 05:41:00 +00:00
}
catch (PKSyntaxError e)
{
2019-12-28 11:16:26 +00:00
await Reply($"{Emojis.Error} {e.Message}\n**Command usage:**\n> pk;{commandDef.Usage}");
2019-10-05 05:41:00 +00:00
}
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?");
}
2019-10-05 05:41:00 +00:00
}
public async Task<DiscordUser> MatchUser()
2019-10-05 05:41:00 +00:00
{
var text = PeekArgument();
if (text.TryParseMention(out var id))
return await Shard.GetUserAsync(id);
2019-10-05 05:41:00 +00:00
return null;
}
2020-01-25 17:08:35 +00:00
public bool MatchUserRaw(out ulong id)
{
id = 0;
var text = PeekArgument();
if (text.TryParseMention(out var mentionId))
2020-01-25 17:08:35 +00:00
id = mentionId;
2020-01-25 17:08:35 +00:00
return id != 0;
}
2019-10-05 05:41:00 +00:00
public Task<PKSystem> PeekSystem() => MatchSystemInner();
public async Task<PKSystem> MatchSystem()
{
var system = await MatchSystemInner();
if (system != null) PopArgument();
return system;
}
private async Task<PKSystem> 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);
2019-10-05 05:41:00 +00:00
// Finally, try HID parsing
var system = await _data.GetSystemByHid(input);
2019-10-05 05:41:00 +00:00
return system;
}
public async Task<PKMember> PeekMember()
{
var input = PeekArgument();
// Member references can have one of three forms, depending on
2019-10-05 05:41:00 +00:00
// 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*
2019-10-05 05:41:00 +00:00
// First, if we have a system, try finding by member name in system
if (_senderSystem != null && await _data.GetMemberByName(_senderSystem, input) is PKMember memberByName)
2019-10-05 05:41:00 +00:00
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;
2019-10-05 05:41:00 +00:00
// We didn't find anything, so we return null.
return null;
}
/// <summary>
/// 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.
/// </summary>
public async Task<PKMember> 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
2019-10-05 05:41:00 +00:00
}
if (_senderSystem != null)
return $"Member with name \"{input}\" not found. Note that a member ID is 5 characters long.";
2019-10-05 05:41:00 +00:00
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)
2019-10-05 05:41:00 +00:00
{
// 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)
2019-10-05 05:41:00 +00:00
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;
2019-10-05 05:41:00 +00:00
throw new PKError("This command can not be run in a DM.");
}
2020-01-11 15:49:20 +00:00
public LookupContext LookupContextFor(PKSystem target) =>
System?.Id == target.Id ? LookupContext.ByOwner : LookupContext.ByNonOwner;
2020-02-27 23:23:54 +00:00
2020-06-14 19:37:04 +00:00
public LookupContext LookupContextFor(SystemId systemId) =>
2020-02-27 23:23:54 +00:00
System?.Id == systemId ? LookupContext.ByOwner : LookupContext.ByNonOwner;
public LookupContext LookupContextFor(PKMember target) =>
System?.Id == target.System ? LookupContext.ByOwner : LookupContext.ByNonOwner;
2020-01-11 15:49:20 +00:00
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<DiscordChannel> MatchChannel()
2019-10-05 05:41:00 +00:00
{
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;
}
2019-10-05 05:41:00 +00:00
}
2020-05-05 14:03:46 +00:00
public IComponentContext Services => _provider;
2019-10-05 05:41:00 +00:00
}
}