refactor: move ContextExts to own folder, CommandTree / command defs to CommandMeta folder
This commit is contained in:
140
PluralKit.Bot/CommandSystem/Context/Context.cs
Normal file
140
PluralKit.Bot/CommandSystem/Context/Context.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using App.Metrics;
|
||||
|
||||
using Autofac;
|
||||
|
||||
using Myriad.Cache;
|
||||
using Myriad.Extensions;
|
||||
using Myriad.Gateway;
|
||||
using Myriad.Rest;
|
||||
using Myriad.Rest.Types;
|
||||
using Myriad.Rest.Types.Requests;
|
||||
using Myriad.Types;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot;
|
||||
|
||||
public class Context
|
||||
{
|
||||
private readonly ILifetimeScope _provider;
|
||||
|
||||
private readonly Parameters _parameters;
|
||||
|
||||
private readonly IMetrics _metrics;
|
||||
private readonly CommandMessageService _commandMessageService;
|
||||
|
||||
private Command? _currentCommand;
|
||||
|
||||
public Context(ILifetimeScope provider, Shard shard, Guild? guild, Channel channel, MessageCreateEvent message, int commandParseOffset,
|
||||
PKSystem senderSystem, MessageContext messageContext)
|
||||
{
|
||||
Message = (Message)message;
|
||||
Shard = shard;
|
||||
Guild = guild;
|
||||
Channel = channel;
|
||||
System = senderSystem;
|
||||
MessageContext = messageContext;
|
||||
Cache = provider.Resolve<IDiscordCache>();
|
||||
Database = provider.Resolve<IDatabase>();
|
||||
Repository = provider.Resolve<ModelRepository>();
|
||||
_metrics = provider.Resolve<IMetrics>();
|
||||
_provider = provider;
|
||||
_commandMessageService = provider.Resolve<CommandMessageService>();
|
||||
_parameters = new Parameters(message.Content?.Substring(commandParseOffset));
|
||||
Rest = provider.Resolve<DiscordApiClient>();
|
||||
Cluster = provider.Resolve<Cluster>();
|
||||
}
|
||||
|
||||
public readonly IDiscordCache Cache;
|
||||
public readonly DiscordApiClient Rest;
|
||||
|
||||
public readonly Channel Channel;
|
||||
public User Author => Message.Author;
|
||||
public GuildMemberPartial Member => ((MessageCreateEvent)Message).Member;
|
||||
|
||||
public readonly Message Message;
|
||||
public readonly Guild Guild;
|
||||
public readonly Shard Shard;
|
||||
public readonly Cluster Cluster;
|
||||
public readonly MessageContext MessageContext;
|
||||
|
||||
public Task<PermissionSet> BotPermissions => Cache.PermissionsIn(Channel.Id);
|
||||
public Task<PermissionSet> UserPermissions => Cache.PermissionsFor((MessageCreateEvent)Message);
|
||||
|
||||
|
||||
public readonly PKSystem System;
|
||||
|
||||
public readonly Parameters Parameters;
|
||||
|
||||
internal readonly IDatabase Database;
|
||||
internal readonly ModelRepository Repository;
|
||||
|
||||
public async Task<Message> Reply(string text = null, Embed embed = null, AllowedMentions? mentions = null)
|
||||
{
|
||||
var botPerms = await BotPermissions;
|
||||
|
||||
if (!botPerms.HasFlag(PermissionSet.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 && !botPerms.HasFlag(PermissionSet.EmbedLinks))
|
||||
throw new PKError("PluralKit does not have permission to send embeds in this channel. Please ensure I have the **Embed Links** permission enabled.");
|
||||
|
||||
var msg = await Rest.CreateMessage(Channel.Id, new MessageRequest
|
||||
{
|
||||
Content = text,
|
||||
Embed = embed,
|
||||
// Default to an empty allowed mentions object instead of null (which means no mentions allowed)
|
||||
AllowedMentions = mentions ?? new AllowedMentions()
|
||||
});
|
||||
|
||||
if (embed != null)
|
||||
{
|
||||
// Sensitive information that might want to be deleted by :x: reaction is typically in an embed format (member cards, for example)
|
||||
// This may need to be changed at some point but works well enough for now
|
||||
await _commandMessageService.RegisterMessage(msg.Id, msg.ChannelId, Author.Id);
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
public async Task Execute<T>(Command? commandDef, Func<T, Task> handler)
|
||||
{
|
||||
_currentCommand = commandDef;
|
||||
|
||||
try
|
||||
{
|
||||
using (_metrics.Measure.Timer.Time(BotMetrics.CommandTime, new MetricTags("Command", commandDef?.Key ?? "null")))
|
||||
await handler(_provider.Resolve<T>());
|
||||
|
||||
_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 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 IComponentContext Services => _provider;
|
||||
}
|
139
PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs
Normal file
139
PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Myriad.Types;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the next parameter is equal to one of the given keywords. Case-insensitive.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the next parameter is equal to one of the given keywords. Case-insensitive.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
|
||||
public static async Task<bool> MatchClear(this Context ctx, string toClear = null)
|
||||
{
|
||||
var matched = ctx.Match("clear", "reset") || ctx.MatchFlag("c", "clear");
|
||||
if (matched && toClear != null)
|
||||
return await ctx.ConfirmClear(toClear);
|
||||
return matched;
|
||||
}
|
||||
|
||||
public static bool MatchRaw(this Context ctx) =>
|
||||
ctx.Match("r", "raw") || ctx.MatchFlag("r", "raw");
|
||||
|
||||
public static (ulong? messageId, ulong? channelId) MatchMessage(this Context ctx, bool parseRawMessageId)
|
||||
{
|
||||
if (ctx.Message.Type == Message.MessageType.Reply && ctx.Message.MessageReference?.MessageId != null)
|
||||
return (ctx.Message.MessageReference.MessageId, ctx.Message.MessageReference.ChannelId);
|
||||
|
||||
var word = ctx.PeekArgument();
|
||||
if (word == null)
|
||||
return (null, null);
|
||||
|
||||
if (parseRawMessageId && ulong.TryParse(word, out var mid))
|
||||
return (mid, null);
|
||||
|
||||
var match = Regex.Match(word, "https://(?:\\w+.)?discord(?:app)?.com/channels/\\d+/(\\d+)/(\\d+)");
|
||||
if (!match.Success)
|
||||
return (null, null);
|
||||
|
||||
var channelId = ulong.Parse(match.Groups[1].Value);
|
||||
var messageId = ulong.Parse(match.Groups[2].Value);
|
||||
ctx.PopArgument();
|
||||
return (messageId, channelId);
|
||||
}
|
||||
|
||||
public static async Task<List<PKMember>> ParseMemberList(this Context ctx, SystemId? restrictToSystem)
|
||||
{
|
||||
var members = new List<PKMember>();
|
||||
|
||||
// Loop through all the given arguments
|
||||
while (ctx.HasNext())
|
||||
{
|
||||
// and attempt to match a member
|
||||
var member = await ctx.MatchMember(restrictToSystem);
|
||||
|
||||
if (member == null)
|
||||
// if we can't, big error. Every member name must be valid.
|
||||
throw new PKError(ctx.CreateNotFoundError("Member", ctx.PopArgument()));
|
||||
|
||||
members.Add(member); // Then add to the final output list
|
||||
}
|
||||
|
||||
if (members.Count == 0) throw new PKSyntaxError("You must input at least one member.");
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
public static async Task<List<PKGroup>> ParseGroupList(this Context ctx, SystemId? restrictToSystem)
|
||||
{
|
||||
var groups = new List<PKGroup>();
|
||||
|
||||
// Loop through all the given arguments
|
||||
while (ctx.HasNext())
|
||||
{
|
||||
// and attempt to match a group
|
||||
var group = await ctx.MatchGroup(restrictToSystem);
|
||||
if (group == null)
|
||||
// if we can't, big error. Every group name must be valid.
|
||||
throw new PKError(ctx.CreateNotFoundError("Group", ctx.PopArgument()));
|
||||
|
||||
// todo: remove this, the database query enforces the restriction
|
||||
if (restrictToSystem != null && group.System != restrictToSystem)
|
||||
throw Errors.NotOwnGroupError; // TODO: name *which* group?
|
||||
|
||||
groups.Add(group); // Then add to the final output list
|
||||
}
|
||||
|
||||
if (groups.Count == 0) throw new PKSyntaxError("You must input at least one group.");
|
||||
|
||||
return groups;
|
||||
}
|
||||
}
|
61
PluralKit.Bot/CommandSystem/Context/ContextAvatarExt.cs
Normal file
61
PluralKit.Bot/CommandSystem/Context/ContextAvatarExt.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
#nullable enable
|
||||
using Myriad.Extensions;
|
||||
using Myriad.Types;
|
||||
|
||||
namespace PluralKit.Bot;
|
||||
|
||||
public static class ContextAvatarExt
|
||||
{
|
||||
public static async Task<ParsedImage?> MatchImage(this Context ctx)
|
||||
{
|
||||
// If we have a user @mention/ID, use their avatar
|
||||
if (await ctx.MatchUser() is { } user)
|
||||
{
|
||||
var url = user.AvatarUrl("png", 256);
|
||||
return new ParsedImage { Url = url, Source = AvatarSource.User, SourceUser = user };
|
||||
}
|
||||
|
||||
// If we have a positional argument, try to parse it as a URL
|
||||
var arg = ctx.RemainderOrNull();
|
||||
if (arg != null)
|
||||
{
|
||||
// Allow surrounding the URL with <angle brackets> to "de-embed"
|
||||
if (arg.StartsWith("<") && arg.EndsWith(">"))
|
||||
arg = arg.Substring(1, arg.Length - 2);
|
||||
|
||||
if (!Uri.TryCreate(arg, UriKind.Absolute, out var uri))
|
||||
throw Errors.InvalidUrl(arg);
|
||||
|
||||
if (uri.Scheme != "http" && uri.Scheme != "https")
|
||||
throw Errors.InvalidUrl(arg);
|
||||
|
||||
// ToString URL-decodes, which breaks URLs to spaces; AbsoluteUri doesn't
|
||||
return new ParsedImage { Url = uri.AbsoluteUri, Source = AvatarSource.Url };
|
||||
}
|
||||
|
||||
// If we have an attachment, use that
|
||||
if (ctx.Message.Attachments.FirstOrDefault() is { } attachment)
|
||||
{
|
||||
var url = attachment.ProxyUrl;
|
||||
return new ParsedImage { Url = url, Source = AvatarSource.Attachment };
|
||||
}
|
||||
|
||||
// We should only get here if there are no arguments (which would get parsed as URL + throw if error)
|
||||
// and if there are no attachments (which would have been caught just before)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public struct ParsedImage
|
||||
{
|
||||
public string Url;
|
||||
public AvatarSource Source;
|
||||
public User? SourceUser;
|
||||
}
|
||||
|
||||
public enum AvatarSource
|
||||
{
|
||||
Url,
|
||||
User,
|
||||
Attachment
|
||||
}
|
104
PluralKit.Bot/CommandSystem/Context/ContextChecksExt.cs
Normal file
104
PluralKit.Bot/CommandSystem/Context/ContextChecksExt.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using Autofac;
|
||||
|
||||
using Myriad.Extensions;
|
||||
using Myriad.Types;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot;
|
||||
|
||||
public static class ContextChecksExt
|
||||
{
|
||||
public static Context CheckGuildContext(this Context ctx)
|
||||
{
|
||||
if (ctx.Channel.GuildId != null) return ctx;
|
||||
throw new PKError("This command can not be run in a DM.");
|
||||
}
|
||||
|
||||
public static Context CheckDMContext(this Context ctx)
|
||||
{
|
||||
if (ctx.Channel.GuildId == null) return ctx;
|
||||
throw new PKError("This command must be run in a DM.");
|
||||
}
|
||||
|
||||
public static Context CheckSystemPrivacy(this Context ctx, PKSystem target, PrivacyLevel level)
|
||||
{
|
||||
if (level.CanAccess(ctx.LookupContextFor(target))) return ctx;
|
||||
throw new PKError("You do not have permission to access this information.");
|
||||
}
|
||||
|
||||
public static Context CheckOwnMember(this Context ctx, PKMember member)
|
||||
{
|
||||
if (member.System != ctx.System?.Id)
|
||||
throw Errors.NotOwnMemberError;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
public static Context CheckOwnGroup(this Context ctx, PKGroup group)
|
||||
{
|
||||
if (group.System != ctx.System?.Id)
|
||||
throw Errors.NotOwnGroupError;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
public static Context CheckSystem(this Context ctx)
|
||||
{
|
||||
if (ctx.System == null)
|
||||
throw Errors.NoSystemError;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
public static Context CheckNoSystem(this Context ctx)
|
||||
{
|
||||
if (ctx.System != null)
|
||||
throw Errors.ExistingSystemError;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
public static async Task<Context> CheckAuthorPermission(this Context ctx, PermissionSet neededPerms,
|
||||
string permissionName)
|
||||
{
|
||||
if ((await ctx.UserPermissions & neededPerms) != neededPerms)
|
||||
throw new PKError(
|
||||
$"You must have the \"{permissionName}\" permission in this server to use this command.");
|
||||
return ctx;
|
||||
}
|
||||
|
||||
public static async Task<bool> CheckPermissionsInGuildChannel(this Context ctx, Channel channel,
|
||||
PermissionSet neededPerms)
|
||||
{
|
||||
var guild = await ctx.Cache.GetGuild(channel.GuildId.Value);
|
||||
if (guild == null)
|
||||
return false;
|
||||
|
||||
var guildMember = ctx.Member;
|
||||
|
||||
if (ctx.Guild?.Id != channel.GuildId)
|
||||
{
|
||||
guildMember = await ctx.Rest.GetGuildMember(channel.GuildId.Value, ctx.Author.Id);
|
||||
if (guildMember == null)
|
||||
return false;
|
||||
}
|
||||
|
||||
var userPermissions = PermissionExtensions.PermissionsFor(guild, channel, ctx.Author.Id, guildMember);
|
||||
if ((userPermissions & neededPerms) == 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool CheckBotAdmin(this Context ctx)
|
||||
{
|
||||
var botConfig = ctx.Services.Resolve<BotConfig>();
|
||||
return botConfig.AdminRole != null && ctx.Member != null &&
|
||||
ctx.Member.Roles.Contains(botConfig.AdminRole.Value);
|
||||
}
|
||||
|
||||
public static Context AssertBotAdmin(this Context ctx)
|
||||
{
|
||||
if (!ctx.CheckBotAdmin())
|
||||
throw new PKError("This command is only usable by bot admins.");
|
||||
|
||||
return ctx;
|
||||
}
|
||||
}
|
180
PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs
Normal file
180
PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
using Myriad.Extensions;
|
||||
using Myriad.Types;
|
||||
|
||||
using PluralKit.Bot.Utils;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot;
|
||||
|
||||
public static class ContextEntityArgumentsExt
|
||||
{
|
||||
public static async Task<User> MatchUser(this Context ctx)
|
||||
{
|
||||
var text = ctx.PeekArgument();
|
||||
if (text.TryParseMention(out var id))
|
||||
return await ctx.Cache.GetOrFetchUser(ctx.Rest, 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<PKSystem> PeekSystem(this Context ctx) => ctx.MatchSystemInner();
|
||||
|
||||
public static async Task<PKSystem> MatchSystem(this Context ctx)
|
||||
{
|
||||
var system = await ctx.MatchSystemInner();
|
||||
if (system != null) ctx.PopArgument();
|
||||
return system;
|
||||
}
|
||||
|
||||
private static async Task<PKSystem> 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.Repository.GetSystemByAccount(id);
|
||||
|
||||
// Finally, try HID parsing
|
||||
var system = await ctx.Repository.GetSystemByHid(input);
|
||||
return system;
|
||||
}
|
||||
|
||||
public static async Task<PKMember> PeekMember(this Context ctx, SystemId? restrictToSystem = null)
|
||||
{
|
||||
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*
|
||||
|
||||
// Skip name / display name matching if the user does not have a system
|
||||
// or if they specifically request by-HID matching
|
||||
if (ctx.System != null && !ctx.MatchFlag("id", "by-id"))
|
||||
{
|
||||
// First, try finding by member name in system
|
||||
if (await ctx.Repository.GetMemberByName(ctx.System.Id, input) is PKMember memberByName)
|
||||
return memberByName;
|
||||
|
||||
// And if that fails, we try finding a member with a display name matching the argument from the system
|
||||
if (ctx.System != null &&
|
||||
await ctx.Repository.GetMemberByDisplayName(ctx.System.Id, input) is PKMember memberByDisplayName)
|
||||
return memberByDisplayName;
|
||||
}
|
||||
|
||||
// Finally (or if by-HID lookup is specified), try member HID parsing:
|
||||
if (await ctx.Repository.GetMemberByHid(input, restrictToSystem) is PKMember memberByHid)
|
||||
return memberByHid;
|
||||
|
||||
// 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 static async Task<PKMember> MatchMember(this Context ctx, SystemId? restrictToSystem = null)
|
||||
{
|
||||
// First, peek a member
|
||||
var member = await ctx.PeekMember(restrictToSystem);
|
||||
|
||||
// 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 async Task<PKGroup> PeekGroup(this Context ctx, SystemId? restrictToSystem = null)
|
||||
{
|
||||
var input = ctx.PeekArgument();
|
||||
|
||||
// see PeekMember for an explanation of the logic used here
|
||||
|
||||
if (ctx.System != null && !ctx.MatchFlag("id", "by-id"))
|
||||
{
|
||||
if (await ctx.Repository.GetGroupByName(ctx.System.Id, input) is { } byName)
|
||||
return byName;
|
||||
if (await ctx.Repository.GetGroupByDisplayName(ctx.System.Id, input) is { } byDisplayName)
|
||||
return byDisplayName;
|
||||
}
|
||||
|
||||
if (await ctx.Repository.GetGroupByHid(input, restrictToSystem) is { } byHid)
|
||||
return byHid;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static async Task<PKGroup> MatchGroup(this Context ctx, SystemId? restrictToSystem = null)
|
||||
{
|
||||
var group = await ctx.PeekGroup(restrictToSystem);
|
||||
if (group != null) ctx.PopArgument();
|
||||
return group;
|
||||
}
|
||||
|
||||
public static string CreateNotFoundError(this Context ctx, string entity, string input)
|
||||
{
|
||||
var isIDOnlyQuery = ctx.System == null || ctx.MatchFlag("id", "by-id");
|
||||
|
||||
if (isIDOnlyQuery)
|
||||
{
|
||||
if (input.Length == 5)
|
||||
return $"{entity} with ID \"{input}\" not found.";
|
||||
return $"{entity} not found. Note that a {entity.ToLower()} ID is 5 characters long.";
|
||||
}
|
||||
|
||||
if (input.Length == 5)
|
||||
return $"{entity} with ID or name \"{input}\" not found.";
|
||||
return $"{entity} with name \"{input}\" not found. Note that a {entity.ToLower()} ID is 5 characters long.";
|
||||
}
|
||||
|
||||
public static async Task<Channel> MatchChannel(this Context ctx)
|
||||
{
|
||||
if (!MentionUtils.TryParseChannel(ctx.PeekArgument(), out var id))
|
||||
return null;
|
||||
|
||||
if (!(await ctx.Cache.TryGetChannel(id) is Channel channel))
|
||||
return null;
|
||||
|
||||
if (!DiscordUtils.IsValidGuildChannel(channel))
|
||||
return null;
|
||||
|
||||
ctx.PopArgument();
|
||||
return channel;
|
||||
}
|
||||
|
||||
public static async Task<Guild> MatchGuild(this Context ctx)
|
||||
{
|
||||
try
|
||||
{
|
||||
var id = ulong.Parse(ctx.PeekArgument());
|
||||
var guild = await ctx.Cache.TryGetGuild(id);
|
||||
if (guild != null)
|
||||
ctx.PopArgument();
|
||||
|
||||
return guild;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
60
PluralKit.Bot/CommandSystem/Context/ContextPrivacyExt.cs
Normal file
60
PluralKit.Bot/CommandSystem/Context/ContextPrivacyExt.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot;
|
||||
|
||||
public static class ContextPrivacyExt
|
||||
{
|
||||
public static PrivacyLevel PopPrivacyLevel(this Context ctx)
|
||||
{
|
||||
if (ctx.Match("public", "show", "shown", "visible"))
|
||||
return PrivacyLevel.Public;
|
||||
|
||||
if (ctx.Match("private", "hide", "hidden"))
|
||||
return PrivacyLevel.Private;
|
||||
|
||||
if (!ctx.HasNext())
|
||||
throw new PKSyntaxError("You must pass a privacy level (`public` or `private`)");
|
||||
|
||||
throw new PKSyntaxError(
|
||||
$"Invalid privacy level {ctx.PopArgument().AsCode()} (must be `public` or `private`).");
|
||||
}
|
||||
|
||||
public static SystemPrivacySubject PopSystemPrivacySubject(this Context ctx)
|
||||
{
|
||||
if (!SystemPrivacyUtils.TryParseSystemPrivacy(ctx.PeekArgument(), out var subject))
|
||||
throw new PKSyntaxError(
|
||||
$"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `description`, `members`, `front`, `fronthistory`, `groups`, or `all`).");
|
||||
|
||||
ctx.PopArgument();
|
||||
return subject;
|
||||
}
|
||||
|
||||
public static MemberPrivacySubject PopMemberPrivacySubject(this Context ctx)
|
||||
{
|
||||
if (!MemberPrivacyUtils.TryParseMemberPrivacy(ctx.PeekArgument(), out var subject))
|
||||
throw new PKSyntaxError(
|
||||
$"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `name`, `description`, `avatar`, `birthday`, `pronouns`, `metadata`, `visibility`, or `all`).");
|
||||
|
||||
ctx.PopArgument();
|
||||
return subject;
|
||||
}
|
||||
|
||||
public static GroupPrivacySubject PopGroupPrivacySubject(this Context ctx)
|
||||
{
|
||||
if (!GroupPrivacyUtils.TryParseGroupPrivacy(ctx.PeekArgument(), out var subject))
|
||||
throw new PKSyntaxError(
|
||||
$"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `description`, `icon`, `visibility`, or `all`).");
|
||||
|
||||
ctx.PopArgument();
|
||||
return subject;
|
||||
}
|
||||
|
||||
public static bool MatchPrivateFlag(this Context ctx, LookupContext pctx)
|
||||
{
|
||||
var privacy = true;
|
||||
if (ctx.MatchFlag("a", "all")) privacy = false;
|
||||
if (pctx == LookupContext.ByNonOwner && !privacy) throw Errors.LookupNotAllowed;
|
||||
|
||||
return privacy;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user