feat: implement proper ("static") parameters handling command parser -> bot

feat: handle few more commands bot side
fix(commands): handle missing parameters and return error
refactor(commands): use ordermap instead of relying on a sort function to sort tokens
This commit is contained in:
dusk 2025-01-05 13:00:06 +09:00
parent 1a781014bd
commit eec9f64026
No known key found for this signature in database
16 changed files with 358 additions and 502 deletions

View file

@ -50,21 +50,6 @@ public class Context
DefaultPrefix = prefixes[0];
Rest = provider.Resolve<DiscordApiClient>();
Cluster = provider.Resolve<Cluster>();
try
{
Parameters = new ParametersFFI(message.Content?.Substring(commandParseOffset));
}
catch (PKError e)
{
// don't send an "invalid command" response if the guild has those turned off
if (!(GuildConfig != null && GuildConfig!.InvalidCommandResponseEnabled != true))
{
// todo: not this
Reply($"{Emojis.Error} {e.Message}");
}
throw;
}
}
public readonly IDiscordCache Cache;
@ -90,7 +75,6 @@ public class Context
public readonly string CommandPrefix;
public readonly string DefaultPrefix;
public readonly ParametersFFI Parameters;
internal readonly IDatabase Database;
internal readonly ModelRepository Repository;

View file

@ -8,20 +8,15 @@ namespace PluralKit.Bot;
public static class ContextArgumentsExt
{
public static string PopArgument(this Context ctx) =>
ctx.Parameters.Pop();
public static string PopArgument(this Context ctx) => throw new PKError("todo: PopArgument");
public static string PeekArgument(this Context ctx) =>
ctx.Parameters.Peek();
public static string PeekArgument(this Context ctx) => throw new PKError("todo: PeekArgument");
public static string RemainderOrNull(this Context ctx, bool skipFlags = true) =>
ctx.Parameters.Remainder(skipFlags).Length == 0 ? null : ctx.Parameters.Remainder(skipFlags);
public static string RemainderOrNull(this Context ctx, bool skipFlags = true) => throw new PKError("todo: RemainderOrNull");
public static bool HasNext(this Context ctx, bool skipFlags = true) =>
ctx.RemainderOrNull(skipFlags) != null;
public static bool HasNext(this Context ctx, bool skipFlags = true) => throw new PKError("todo: HasNext");
public static string FullCommand(this Context ctx) =>
ctx.Parameters.FullCommand;
public static string FullCommand(this Context ctx) => throw new PKError("todo: FullCommand");
/// <summary>
/// Checks if the next parameter is equal to one of the given keywords and pops it from the stack. Case-insensitive.
@ -53,12 +48,7 @@ public static class ContextArgumentsExt
/// </summary>
public static bool PeekMatch(this Context ctx, ref int ptr, string[] potentialMatches)
{
var arg = ctx.Parameters.PeekWithPtr(ref ptr);
foreach (var match in potentialMatches)
if (arg.Equals(match, StringComparison.InvariantCultureIgnoreCase))
return true;
return false;
throw new PKError("todo: PeekMatch");
}
/// <summary>
@ -69,23 +59,14 @@ public static class ContextArgumentsExt
/// </summary>
public static bool MatchMultiple(this Context ctx, params string[][] potentialParametersMatches)
{
int ptr = ctx.Parameters._ptr;
foreach (var param in potentialParametersMatches)
if (!ctx.PeekMatch(ref ptr, param)) return false;
ctx.Parameters._ptr = ptr;
return true;
throw new PKError("todo: MatchMultiple");
}
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));
throw new NotImplementedException();
}
public static bool MatchClear(this Context ctx)
@ -100,11 +81,7 @@ public static class ContextArgumentsExt
public static ReplyFormat PeekMatchFormat(this Context ctx)
{
int ptr1 = ctx.Parameters._ptr;
int ptr2 = ctx.Parameters._ptr;
if (ctx.PeekMatch(ref ptr1, new[] { "r", "raw" }) || ctx.MatchFlag("r", "raw")) return ReplyFormat.Raw;
if (ctx.PeekMatch(ref ptr2, new[] { "pt", "plaintext" }) || ctx.MatchFlag("pt", "plaintext")) return ReplyFormat.Plaintext;
return ReplyFormat.Standard;
throw new PKError("todo: PeekMatchFormat");
}
public static bool MatchToggle(this Context ctx, bool? defaultValue = null)
@ -153,49 +130,12 @@ public static class ContextArgumentsExt
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;
throw new NotImplementedException();
}
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;
throw new NotImplementedException();
}
}

View file

@ -34,19 +34,15 @@ public static class ContextEntityArgumentsExt
return id != 0;
}
public static Task<PKSystem> PeekSystem(this Context ctx) => ctx.MatchSystemInner();
public static Task<PKSystem> PeekSystem(this Context ctx) => throw new NotImplementedException();
public static async Task<PKSystem> MatchSystem(this Context ctx)
{
var system = await ctx.MatchSystemInner();
if (system != null) ctx.PopArgument();
return system;
throw new NotImplementedException();
}
private static async Task<PKSystem> MatchSystemInner(this Context ctx)
public static async Task<PKSystem> ParseSystem(this Context ctx, string input)
{
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>)
@ -63,10 +59,8 @@ public static class ContextEntityArgumentsExt
return null;
}
public static async Task<PKMember> PeekMember(this Context ctx, SystemId? restrictToSystem = null)
public static async Task<PKMember> ParseMember(this Context ctx, Parameters parameters, string input, 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
@ -75,7 +69,7 @@ public static class ContextEntityArgumentsExt
// 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"))
if (ctx.System != null && !parameters.HasFlag("id", "by-id"))
{
// First, try finding by member name in system
if (await ctx.Repository.GetMemberByName(ctx.System.Id, input) is PKMember memberByName)
@ -124,6 +118,11 @@ public static class ContextEntityArgumentsExt
return null;
}
public static async Task<PKMember> PeekMember(this Context ctx, SystemId? restrictToSystem = null)
{
throw new NotImplementedException();
}
/// <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.
@ -170,9 +169,9 @@ public static class ContextEntityArgumentsExt
return group;
}
public static string CreateNotFoundError(this Context ctx, string entity, string input)
public static string CreateNotFoundError(this Context ctx, Parameters parameters, string entity, string input)
{
var isIDOnlyQuery = ctx.System == null || ctx.MatchFlag("id", "by-id");
var isIDOnlyQuery = ctx.System == null || parameters.HasFlag("id", "by-id");
var inputIsHid = HidUtils.ParseHid(input) != null;
if (isIDOnlyQuery)