mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-14 09:40:10 +00:00
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:
parent
1a781014bd
commit
eec9f64026
16 changed files with 358 additions and 502 deletions
22
Cargo.lock
generated
22
Cargo.lock
generated
|
|
@ -569,6 +569,7 @@ name = "commands"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"ordermap",
|
||||||
"smol_str",
|
"smol_str",
|
||||||
"uniffi",
|
"uniffi",
|
||||||
]
|
]
|
||||||
|
|
@ -1341,6 +1342,12 @@ dependencies = [
|
||||||
"allocator-api2",
|
"allocator-api2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashlink"
|
name = "hashlink"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
|
|
@ -1719,12 +1726,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.2.6"
|
version = "2.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.14.5",
|
"hashbrown 0.15.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2235,6 +2242,15 @@ dependencies = [
|
||||||
"hashbrown 0.13.2",
|
"hashbrown 0.13.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordermap"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f80a48eb68b6a7da9829b8b0429011708f775af80676a91063d023a66a656106"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "os_info"
|
name = "os_info"
|
||||||
version = "3.8.2"
|
version = "3.8.2"
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,27 @@ namespace PluralKit.Bot;
|
||||||
|
|
||||||
public partial class CommandTree
|
public partial class CommandTree
|
||||||
{
|
{
|
||||||
public Task ExecuteCommand(Context ctx)
|
public Task ExecuteCommand(Context ctx, ResolvedParameters parameters)
|
||||||
{
|
{
|
||||||
switch (ctx.Parameters.Callback())
|
switch (parameters.Raw.Callback())
|
||||||
{
|
{
|
||||||
case "fun_thunder":
|
case "fun_thunder":
|
||||||
return ctx.Execute<Fun>(null, m => m.Thunder(ctx));
|
return ctx.Execute<Fun>(null, m => m.Thunder(ctx));
|
||||||
case "help":
|
case "help":
|
||||||
return ctx.Execute<Help>(Help, m => m.HelpRoot(ctx));
|
return ctx.Execute<Help>(Help, m => m.HelpRoot(ctx));
|
||||||
case "help_commands":
|
case "help_commands":
|
||||||
return ctx.Reply("For the list of commands, see the website: <https://pluralkit.me/commands>");
|
return ctx.Reply(
|
||||||
|
"For the list of commands, see the website: <https://pluralkit.me/commands>");
|
||||||
case "help_proxy":
|
case "help_proxy":
|
||||||
return ctx.Reply(
|
return ctx.Reply(
|
||||||
"The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying");
|
"The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying");
|
||||||
|
case "member_show":
|
||||||
|
return ctx.Execute<Member>(MemberInfo, m => m.ViewMember(ctx, parameters.MemberParams["target"]));
|
||||||
|
case "member_new":
|
||||||
|
return ctx.Execute<Member>(MemberNew, m => m.NewMember(ctx, parameters.Raw.Params()["name"]));
|
||||||
default:
|
default:
|
||||||
// remove compiler warning
|
return ctx.Reply(
|
||||||
return ctx.Reply($"{Emojis.Error} Parsed command {ctx.Parameters.Callback().AsCode()} not implemented in PluralKit.Bot!");
|
$"{Emojis.Error} Parsed command {parameters.Raw.Callback().AsCode()} not implemented in PluralKit.Bot!");
|
||||||
}
|
}
|
||||||
if (ctx.Match("system", "s"))
|
if (ctx.Match("system", "s"))
|
||||||
return HandleSystemCommand(ctx);
|
return HandleSystemCommand(ctx);
|
||||||
|
|
@ -224,43 +229,44 @@ public partial class CommandTree
|
||||||
// finally, parse commands that *can* take a system target
|
// finally, parse commands that *can* take a system target
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// try matching a system ID
|
// TODO: actually implement this
|
||||||
var target = await ctx.MatchSystem();
|
// // try matching a system ID
|
||||||
var previousPtr = ctx.Parameters._ptr;
|
// var target = await ctx.MatchSystem();
|
||||||
|
// var previousPtr = ctx.Parameters._ptr;
|
||||||
|
|
||||||
// if we have a parsed target and no more commands, don't bother with the command flow
|
// // if we have a parsed target and no more commands, don't bother with the command flow
|
||||||
// we skip the `target != null` check here since the argument isn't be popped if it's not a system
|
// // we skip the `target != null` check here since the argument isn't be popped if it's not a system
|
||||||
if (!ctx.HasNext())
|
// if (!ctx.HasNext())
|
||||||
{
|
// {
|
||||||
await ctx.Execute<System>(SystemInfo, m => m.Query(ctx, target ?? ctx.System));
|
// await ctx.Execute<System>(SystemInfo, m => m.Query(ctx, target ?? ctx.System));
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// hacky, but we need to CheckSystem(target) which throws a PKError
|
// // hacky, but we need to CheckSystem(target) which throws a PKError
|
||||||
// normally PKErrors are only handled in ctx.Execute
|
// // normally PKErrors are only handled in ctx.Execute
|
||||||
try
|
// try
|
||||||
{
|
// {
|
||||||
await HandleSystemCommandTargeted(ctx, target ?? ctx.System);
|
// await HandleSystemCommandTargeted(ctx, target ?? ctx.System);
|
||||||
}
|
// }
|
||||||
catch (PKError e)
|
// catch (PKError e)
|
||||||
{
|
// {
|
||||||
await ctx.Reply($"{Emojis.Error} {e.Message}");
|
// await ctx.Reply($"{Emojis.Error} {e.Message}");
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// if we *still* haven't matched anything, the user entered an invalid command name or system reference
|
// // if we *still* haven't matched anything, the user entered an invalid command name or system reference
|
||||||
if (ctx.Parameters._ptr == previousPtr)
|
// if (ctx.Parameters._ptr == previousPtr)
|
||||||
{
|
// {
|
||||||
if (!ctx.Parameters.Peek().TryParseHid(out _) && !ctx.Parameters.Peek().TryParseMention(out _))
|
// if (!ctx.Parameters.Peek().TryParseHid(out _) && !ctx.Parameters.Peek().TryParseMention(out _))
|
||||||
{
|
// {
|
||||||
await PrintCommandNotFoundError(ctx, SystemCommands);
|
// await PrintCommandNotFoundError(ctx, SystemCommands);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
var list = CreatePotentialCommandList(ctx.DefaultPrefix, SystemCommands);
|
// var list = CreatePotentialCommandList(ctx.DefaultPrefix, SystemCommands);
|
||||||
await ctx.Reply($"{Emojis.Error} {await CreateSystemNotFoundError(ctx)}\n\n"
|
// await ctx.Reply($"{Emojis.Error} {await CreateSystemNotFoundError(ctx)}\n\n"
|
||||||
+ $"Perhaps you meant to use one of the following commands?\n{list}");
|
// + $"Perhaps you meant to use one of the following commands?\n{list}");
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -324,20 +330,21 @@ public partial class CommandTree
|
||||||
|
|
||||||
private async Task HandleMemberCommand(Context ctx)
|
private async Task HandleMemberCommand(Context ctx)
|
||||||
{
|
{
|
||||||
if (ctx.Match("new", "n", "add", "create", "register"))
|
// TODO: implement
|
||||||
await ctx.Execute<Member>(MemberNew, m => m.NewMember(ctx));
|
// if (ctx.Match("new", "n", "add", "create", "register"))
|
||||||
else if (ctx.Match("list"))
|
// await ctx.Execute<Member>(MemberNew, m => m.NewMember(ctx));
|
||||||
await ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, ctx.System));
|
// else if (ctx.Match("list"))
|
||||||
else if (ctx.Match("commands", "help"))
|
// await ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, ctx.System));
|
||||||
await PrintCommandList(ctx, "members", MemberCommands);
|
// else if (ctx.Match("commands", "help"))
|
||||||
else if (await ctx.MatchMember() is PKMember target)
|
// await PrintCommandList(ctx, "members", MemberCommands);
|
||||||
await HandleMemberCommandTargeted(ctx, target);
|
// else if (await ctx.MatchMember() is PKMember target)
|
||||||
else if (!ctx.HasNext())
|
// await HandleMemberCommandTargeted(ctx, target);
|
||||||
await PrintCommandExpectedError(ctx, MemberNew, MemberInfo, MemberRename, MemberDisplayName,
|
// else if (!ctx.HasNext())
|
||||||
MemberServerName, MemberDesc, MemberPronouns,
|
// await PrintCommandExpectedError(ctx, MemberNew, MemberInfo, MemberRename, MemberDisplayName,
|
||||||
MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar);
|
// MemberServerName, MemberDesc, MemberPronouns,
|
||||||
else
|
// MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar);
|
||||||
await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Member", ctx.PopArgument())}");
|
// else
|
||||||
|
// await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Member", ctx.PopArgument())}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleMemberCommandTargeted(Context ctx, PKMember target)
|
private async Task HandleMemberCommandTargeted(Context ctx, PKMember target)
|
||||||
|
|
@ -408,59 +415,60 @@ public partial class CommandTree
|
||||||
|
|
||||||
private async Task HandleGroupCommand(Context ctx)
|
private async Task HandleGroupCommand(Context ctx)
|
||||||
{
|
{
|
||||||
// Commands with no group argument
|
// TODO: implement
|
||||||
if (ctx.Match("n", "new"))
|
// // Commands with no group argument
|
||||||
await ctx.Execute<Groups>(GroupNew, g => g.CreateGroup(ctx));
|
// if (ctx.Match("n", "new"))
|
||||||
else if (ctx.Match("list", "l"))
|
// await ctx.Execute<Groups>(GroupNew, g => g.CreateGroup(ctx));
|
||||||
await ctx.Execute<Groups>(GroupList, g => g.ListSystemGroups(ctx, null));
|
// else if (ctx.Match("list", "l"))
|
||||||
else if (ctx.Match("commands", "help"))
|
// await ctx.Execute<Groups>(GroupList, g => g.ListSystemGroups(ctx, null));
|
||||||
await PrintCommandList(ctx, "groups", GroupCommands);
|
// else if (ctx.Match("commands", "help"))
|
||||||
else if (await ctx.MatchGroup() is { } target)
|
// await PrintCommandList(ctx, "groups", GroupCommands);
|
||||||
{
|
// else if (await ctx.MatchGroup() is { } target)
|
||||||
// Commands with group argument
|
// {
|
||||||
if (ctx.Match("rename", "name", "changename", "setname", "rn"))
|
// // Commands with group argument
|
||||||
await ctx.Execute<Groups>(GroupRename, g => g.RenameGroup(ctx, target));
|
// if (ctx.Match("rename", "name", "changename", "setname", "rn"))
|
||||||
else if (ctx.Match("nick", "dn", "displayname", "nickname"))
|
// await ctx.Execute<Groups>(GroupRename, g => g.RenameGroup(ctx, target));
|
||||||
await ctx.Execute<Groups>(GroupDisplayName, g => g.GroupDisplayName(ctx, target));
|
// else if (ctx.Match("nick", "dn", "displayname", "nickname"))
|
||||||
else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro"))
|
// await ctx.Execute<Groups>(GroupDisplayName, g => g.GroupDisplayName(ctx, target));
|
||||||
await ctx.Execute<Groups>(GroupDesc, g => g.GroupDescription(ctx, target));
|
// else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro"))
|
||||||
else if (ctx.Match("add", "a"))
|
// await ctx.Execute<Groups>(GroupDesc, g => g.GroupDescription(ctx, target));
|
||||||
await ctx.Execute<GroupMember>(GroupAdd,
|
// else if (ctx.Match("add", "a"))
|
||||||
g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Add));
|
// await ctx.Execute<GroupMember>(GroupAdd,
|
||||||
else if (ctx.Match("remove", "rem"))
|
// g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Add));
|
||||||
await ctx.Execute<GroupMember>(GroupRemove,
|
// else if (ctx.Match("remove", "rem"))
|
||||||
g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Remove));
|
// await ctx.Execute<GroupMember>(GroupRemove,
|
||||||
else if (ctx.Match("members", "list", "ms", "l", "ls"))
|
// g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Remove));
|
||||||
await ctx.Execute<GroupMember>(GroupMemberList, g => g.ListGroupMembers(ctx, target));
|
// else if (ctx.Match("members", "list", "ms", "l", "ls"))
|
||||||
else if (ctx.Match("random", "rand", "r"))
|
// await ctx.Execute<GroupMember>(GroupMemberList, g => g.ListGroupMembers(ctx, target));
|
||||||
await ctx.Execute<Random>(GroupMemberRandom, r => r.GroupMember(ctx, target));
|
// else if (ctx.Match("random", "rand", "r"))
|
||||||
else if (ctx.Match("privacy"))
|
// await ctx.Execute<Random>(GroupMemberRandom, r => r.GroupMember(ctx, target));
|
||||||
await ctx.Execute<Groups>(GroupPrivacy, g => g.GroupPrivacy(ctx, target, null));
|
// else if (ctx.Match("privacy"))
|
||||||
else if (ctx.Match("public", "pub"))
|
// await ctx.Execute<Groups>(GroupPrivacy, g => g.GroupPrivacy(ctx, target, null));
|
||||||
await ctx.Execute<Groups>(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Public));
|
// else if (ctx.Match("public", "pub"))
|
||||||
else if (ctx.Match("private", "priv"))
|
// await ctx.Execute<Groups>(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Public));
|
||||||
await ctx.Execute<Groups>(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Private));
|
// else if (ctx.Match("private", "priv"))
|
||||||
else if (ctx.Match("delete", "destroy", "erase", "yeet"))
|
// await ctx.Execute<Groups>(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Private));
|
||||||
await ctx.Execute<Groups>(GroupDelete, g => g.DeleteGroup(ctx, target));
|
// else if (ctx.Match("delete", "destroy", "erase", "yeet"))
|
||||||
else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp"))
|
// await ctx.Execute<Groups>(GroupDelete, g => g.DeleteGroup(ctx, target));
|
||||||
await ctx.Execute<Groups>(GroupIcon, g => g.GroupIcon(ctx, target));
|
// else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp"))
|
||||||
else if (ctx.Match("banner", "splash", "cover"))
|
// await ctx.Execute<Groups>(GroupIcon, g => g.GroupIcon(ctx, target));
|
||||||
await ctx.Execute<Groups>(GroupBannerImage, g => g.GroupBannerImage(ctx, target));
|
// else if (ctx.Match("banner", "splash", "cover"))
|
||||||
else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown"))
|
// await ctx.Execute<Groups>(GroupBannerImage, g => g.GroupBannerImage(ctx, target));
|
||||||
await ctx.Execute<SystemFront>(GroupFrontPercent, g => g.FrontPercent(ctx, group: target));
|
// else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown"))
|
||||||
else if (ctx.Match("color", "colour"))
|
// await ctx.Execute<SystemFront>(GroupFrontPercent, g => g.FrontPercent(ctx, group: target));
|
||||||
await ctx.Execute<Groups>(GroupColor, g => g.GroupColor(ctx, target));
|
// else if (ctx.Match("color", "colour"))
|
||||||
else if (ctx.Match("id"))
|
// await ctx.Execute<Groups>(GroupColor, g => g.GroupColor(ctx, target));
|
||||||
await ctx.Execute<Groups>(GroupId, g => g.DisplayId(ctx, target));
|
// else if (ctx.Match("id"))
|
||||||
else if (!ctx.HasNext())
|
// await ctx.Execute<Groups>(GroupId, g => g.DisplayId(ctx, target));
|
||||||
await ctx.Execute<Groups>(GroupInfo, g => g.ShowGroupCard(ctx, target));
|
// else if (!ctx.HasNext())
|
||||||
else
|
// await ctx.Execute<Groups>(GroupInfo, g => g.ShowGroupCard(ctx, target));
|
||||||
await PrintCommandNotFoundError(ctx, GroupCommandsTargeted);
|
// else
|
||||||
}
|
// await PrintCommandNotFoundError(ctx, GroupCommandsTargeted);
|
||||||
else if (!ctx.HasNext())
|
// }
|
||||||
await PrintCommandExpectedError(ctx, GroupCommands);
|
// else if (!ctx.HasNext())
|
||||||
else
|
// await PrintCommandExpectedError(ctx, GroupCommands);
|
||||||
await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Group", ctx.PopArgument())}");
|
// else
|
||||||
|
// await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Group", ctx.PopArgument())}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleSwitchCommand(Context ctx)
|
private async Task HandleSwitchCommand(Context ctx)
|
||||||
|
|
|
||||||
|
|
@ -50,21 +50,6 @@ public class Context
|
||||||
DefaultPrefix = prefixes[0];
|
DefaultPrefix = prefixes[0];
|
||||||
Rest = provider.Resolve<DiscordApiClient>();
|
Rest = provider.Resolve<DiscordApiClient>();
|
||||||
Cluster = provider.Resolve<Cluster>();
|
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;
|
public readonly IDiscordCache Cache;
|
||||||
|
|
@ -90,7 +75,6 @@ public class Context
|
||||||
|
|
||||||
public readonly string CommandPrefix;
|
public readonly string CommandPrefix;
|
||||||
public readonly string DefaultPrefix;
|
public readonly string DefaultPrefix;
|
||||||
public readonly ParametersFFI Parameters;
|
|
||||||
|
|
||||||
internal readonly IDatabase Database;
|
internal readonly IDatabase Database;
|
||||||
internal readonly ModelRepository Repository;
|
internal readonly ModelRepository Repository;
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,15 @@ namespace PluralKit.Bot;
|
||||||
|
|
||||||
public static class ContextArgumentsExt
|
public static class ContextArgumentsExt
|
||||||
{
|
{
|
||||||
public static string PopArgument(this Context ctx) =>
|
public static string PopArgument(this Context ctx) => throw new PKError("todo: PopArgument");
|
||||||
ctx.Parameters.Pop();
|
|
||||||
|
|
||||||
public static string PeekArgument(this Context ctx) =>
|
public static string PeekArgument(this Context ctx) => throw new PKError("todo: PeekArgument");
|
||||||
ctx.Parameters.Peek();
|
|
||||||
|
|
||||||
public static string RemainderOrNull(this Context ctx, bool skipFlags = true) =>
|
public static string RemainderOrNull(this Context ctx, bool skipFlags = true) => throw new PKError("todo: RemainderOrNull");
|
||||||
ctx.Parameters.Remainder(skipFlags).Length == 0 ? null : ctx.Parameters.Remainder(skipFlags);
|
|
||||||
|
|
||||||
public static bool HasNext(this Context ctx, bool skipFlags = true) =>
|
public static bool HasNext(this Context ctx, bool skipFlags = true) => throw new PKError("todo: HasNext");
|
||||||
ctx.RemainderOrNull(skipFlags) != null;
|
|
||||||
|
|
||||||
public static string FullCommand(this Context ctx) =>
|
public static string FullCommand(this Context ctx) => throw new PKError("todo: FullCommand");
|
||||||
ctx.Parameters.FullCommand;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if the next parameter is equal to one of the given keywords and pops it from the stack. Case-insensitive.
|
/// 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>
|
/// </summary>
|
||||||
public static bool PeekMatch(this Context ctx, ref int ptr, string[] potentialMatches)
|
public static bool PeekMatch(this Context ctx, ref int ptr, string[] potentialMatches)
|
||||||
{
|
{
|
||||||
var arg = ctx.Parameters.PeekWithPtr(ref ptr);
|
throw new PKError("todo: PeekMatch");
|
||||||
foreach (var match in potentialMatches)
|
|
||||||
if (arg.Equals(match, StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -69,23 +59,14 @@ public static class ContextArgumentsExt
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool MatchMultiple(this Context ctx, params string[][] potentialParametersMatches)
|
public static bool MatchMultiple(this Context ctx, params string[][] potentialParametersMatches)
|
||||||
{
|
{
|
||||||
int ptr = ctx.Parameters._ptr;
|
throw new PKError("todo: MatchMultiple");
|
||||||
|
|
||||||
foreach (var param in potentialParametersMatches)
|
|
||||||
if (!ctx.PeekMatch(ref ptr, param)) return false;
|
|
||||||
|
|
||||||
ctx.Parameters._ptr = ptr;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool MatchFlag(this Context ctx, params string[] 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.
|
// 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
|
// Can assume the caller array only contains lowercase *and* the set below only contains lowercase
|
||||||
|
throw new NotImplementedException();
|
||||||
var flags = ctx.Parameters.Flags();
|
|
||||||
return potentialMatches.Any(potentialMatch => flags.Contains(potentialMatch));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool MatchClear(this Context ctx)
|
public static bool MatchClear(this Context ctx)
|
||||||
|
|
@ -100,11 +81,7 @@ public static class ContextArgumentsExt
|
||||||
|
|
||||||
public static ReplyFormat PeekMatchFormat(this Context ctx)
|
public static ReplyFormat PeekMatchFormat(this Context ctx)
|
||||||
{
|
{
|
||||||
int ptr1 = ctx.Parameters._ptr;
|
throw new PKError("todo: PeekMatchFormat");
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool MatchToggle(this Context ctx, bool? defaultValue = null)
|
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)
|
public static async Task<List<PKMember>> ParseMemberList(this Context ctx, SystemId? restrictToSystem)
|
||||||
{
|
{
|
||||||
var members = new List<PKMember>();
|
throw new NotImplementedException();
|
||||||
|
|
||||||
// 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)
|
public static async Task<List<PKGroup>> ParseGroupList(this Context ctx, SystemId? restrictToSystem)
|
||||||
{
|
{
|
||||||
var groups = new List<PKGroup>();
|
throw new NotImplementedException();
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,19 +34,15 @@ public static class ContextEntityArgumentsExt
|
||||||
return id != 0;
|
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)
|
public static async Task<PKSystem> MatchSystem(this Context ctx)
|
||||||
{
|
{
|
||||||
var system = await ctx.MatchSystemInner();
|
throw new NotImplementedException();
|
||||||
if (system != null) ctx.PopArgument();
|
|
||||||
return system;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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:
|
// System references can take three forms:
|
||||||
// - The direct user ID of an account connected to the system
|
// - The direct user ID of an account connected to the system
|
||||||
// - A @mention of an account connected to the system (<@uid>)
|
// - A @mention of an account connected to the system (<@uid>)
|
||||||
|
|
@ -63,10 +59,8 @@ public static class ContextEntityArgumentsExt
|
||||||
return null;
|
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
|
// Member references can have one of three forms, depending on
|
||||||
// whether you're in a system or not:
|
// whether you're in a system or not:
|
||||||
// - A member hid
|
// - A member hid
|
||||||
|
|
@ -75,7 +69,7 @@ public static class ContextEntityArgumentsExt
|
||||||
|
|
||||||
// Skip name / display name matching if the user does not have a system
|
// Skip name / display name matching if the user does not have a system
|
||||||
// or if they specifically request by-HID matching
|
// 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
|
// First, try finding by member name in system
|
||||||
if (await ctx.Repository.GetMemberByName(ctx.System.Id, input) is PKMember memberByName)
|
if (await ctx.Repository.GetMemberByName(ctx.System.Id, input) is PKMember memberByName)
|
||||||
|
|
@ -124,6 +118,11 @@ public static class ContextEntityArgumentsExt
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<PKMember> PeekMember(this Context ctx, SystemId? restrictToSystem = null)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to pop a member descriptor from the stack, returning it if present. If a member could not be
|
/// 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.
|
/// 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;
|
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;
|
var inputIsHid = HidUtils.ParseHid(input) != null;
|
||||||
|
|
||||||
if (isIDOnlyQuery)
|
if (isIDOnlyQuery)
|
||||||
|
|
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
namespace PluralKit.Bot;
|
|
||||||
|
|
||||||
public class Parameters
|
|
||||||
{
|
|
||||||
// Dictionary of (left, right) quote pairs
|
|
||||||
// Each char in the string is an individual quote, multi-char strings imply "one of the following chars"
|
|
||||||
private static readonly Dictionary<string, string> _quotePairs = new()
|
|
||||||
{
|
|
||||||
// Basic
|
|
||||||
{ "'", "'" }, // ASCII single quotes
|
|
||||||
{ "\"", "\"" }, // ASCII double quotes
|
|
||||||
|
|
||||||
// "Smart quotes"
|
|
||||||
// Specifically ignore the left/right status of the quotes and match any combination of them
|
|
||||||
// Left string also includes "low" quotes to allow for the low-high style used in some locales
|
|
||||||
{ "\u201C\u201D\u201F\u201E", "\u201C\u201D\u201F" }, // double quotes
|
|
||||||
{ "\u2018\u2019\u201B\u201A", "\u2018\u2019\u201B" }, // single quotes
|
|
||||||
|
|
||||||
// Chevrons (normal and "fullwidth" variants)
|
|
||||||
{ "\u00AB\u300A", "\u00BB\u300B" }, // double chevrons, pointing away (<<text>>)
|
|
||||||
{ "\u00BB\u300B", "\u00AB\u300A" }, // double chevrons, pointing together (>>text<<)
|
|
||||||
{ "\u2039\u3008", "\u203A\u3009" }, // single chevrons, pointing away (<text>)
|
|
||||||
{ "\u203A\u3009", "\u2039\u3008" }, // single chevrons, pointing together (>text<)
|
|
||||||
|
|
||||||
// Other
|
|
||||||
{ "\u300C\u300E", "\u300D\u300F" } // corner brackets (Japanese/Chinese)
|
|
||||||
};
|
|
||||||
|
|
||||||
private ISet<string> _flags; // Only parsed when requested first time
|
|
||||||
public int _ptr;
|
|
||||||
|
|
||||||
public string FullCommand { get; }
|
|
||||||
|
|
||||||
private struct WordPosition
|
|
||||||
{
|
|
||||||
// Start of the word
|
|
||||||
internal readonly int startPos;
|
|
||||||
|
|
||||||
// End of the word
|
|
||||||
internal readonly int endPos;
|
|
||||||
|
|
||||||
// How much to advance word pointer afterwards to point at the start of the *next* word
|
|
||||||
internal readonly int advanceAfterWord;
|
|
||||||
|
|
||||||
internal readonly bool wasQuoted;
|
|
||||||
|
|
||||||
public WordPosition(int startPos, int endPos, int advanceAfterWord, bool wasQuoted)
|
|
||||||
{
|
|
||||||
this.startPos = startPos;
|
|
||||||
this.endPos = endPos;
|
|
||||||
this.advanceAfterWord = advanceAfterWord;
|
|
||||||
this.wasQuoted = wasQuoted;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Parameters(string cmd)
|
|
||||||
{
|
|
||||||
// This is a SUPER dirty hack to avoid having to match both spaces and newlines in the word detection below
|
|
||||||
// Instead, we just add a space before every newline (which then gets stripped out later).
|
|
||||||
FullCommand = cmd.Replace("\n", " \n");
|
|
||||||
_ptr = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ParseFlags()
|
|
||||||
{
|
|
||||||
_flags = new HashSet<string>();
|
|
||||||
|
|
||||||
var ptr = 0;
|
|
||||||
while (NextWordPosition(ptr) is { } wp)
|
|
||||||
{
|
|
||||||
ptr = wp.endPos + wp.advanceAfterWord;
|
|
||||||
|
|
||||||
// Is this word a *flag* (as in, starts with a - AND is not quoted)
|
|
||||||
if (FullCommand[wp.startPos] != '-' || wp.wasQuoted) continue; // (if not, carry on w/ next word)
|
|
||||||
|
|
||||||
// Find the *end* of the flag start (technically allowing arbitrary amounts of dashes)
|
|
||||||
var flagNameStart = wp.startPos;
|
|
||||||
while (flagNameStart < FullCommand.Length && FullCommand[flagNameStart] == '-')
|
|
||||||
flagNameStart++;
|
|
||||||
|
|
||||||
// Then add the word to the flag set
|
|
||||||
var word = FullCommand.Substring(flagNameStart, wp.endPos - flagNameStart).Trim();
|
|
||||||
if (word.Length > 0)
|
|
||||||
_flags.Add(word.ToLowerInvariant());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Pop()
|
|
||||||
{
|
|
||||||
// Loop to ignore and skip past flags
|
|
||||||
while (NextWordPosition(_ptr) is { } pos)
|
|
||||||
{
|
|
||||||
_ptr = pos.endPos + pos.advanceAfterWord;
|
|
||||||
if (FullCommand[pos.startPos] == '-' && !pos.wasQuoted) continue;
|
|
||||||
return FullCommand.Substring(pos.startPos, pos.endPos - pos.startPos).Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Peek()
|
|
||||||
{
|
|
||||||
// temp ptr so we don't move the real ptr
|
|
||||||
int ptr = _ptr;
|
|
||||||
|
|
||||||
return PeekWithPtr(ref ptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string PeekWithPtr(ref int ptr)
|
|
||||||
{
|
|
||||||
// Loop to ignore and skip past flags
|
|
||||||
while (NextWordPosition(ptr) is { } pos)
|
|
||||||
{
|
|
||||||
ptr = pos.endPos + pos.advanceAfterWord;
|
|
||||||
if (FullCommand[pos.startPos] == '-' && !pos.wasQuoted) continue;
|
|
||||||
return FullCommand.Substring(pos.startPos, pos.endPos - pos.startPos).Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public ISet<string> Flags()
|
|
||||||
{
|
|
||||||
if (_flags == null) ParseFlags();
|
|
||||||
return _flags;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Remainder(bool skipFlags = true)
|
|
||||||
{
|
|
||||||
if (skipFlags)
|
|
||||||
// Skip all *leading* flags when taking the remainder
|
|
||||||
while (NextWordPosition(_ptr) is { } wp)
|
|
||||||
{
|
|
||||||
if (FullCommand[wp.startPos] != '-' || wp.wasQuoted) break;
|
|
||||||
_ptr = wp.endPos + wp.advanceAfterWord;
|
|
||||||
}
|
|
||||||
|
|
||||||
// *Then* get the remainder
|
|
||||||
return FullCommand.Substring(Math.Min(_ptr, FullCommand.Length)).Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private WordPosition? NextWordPosition(int position)
|
|
||||||
{
|
|
||||||
// Skip leading spaces before actual content
|
|
||||||
while (position < FullCommand.Length && FullCommand[position] == ' ') position++;
|
|
||||||
|
|
||||||
// Is this the end of the string?
|
|
||||||
if (FullCommand.Length <= position) return null;
|
|
||||||
|
|
||||||
// Is this a quoted word?
|
|
||||||
if (TryCheckQuote(FullCommand[position], out var endQuotes))
|
|
||||||
{
|
|
||||||
// We found a quoted word - find an instance of one of the corresponding end quotes
|
|
||||||
var endQuotePosition = -1;
|
|
||||||
for (var i = position + 1; i < FullCommand.Length; i++)
|
|
||||||
if (endQuotePosition == -1 && endQuotes.Contains(FullCommand[i]))
|
|
||||||
endQuotePosition = i; // need a break; don't feel like brackets tho lol
|
|
||||||
|
|
||||||
// Position after the end quote should be EOL or a space
|
|
||||||
// Otherwise we fallthrough to the unquoted word handler below
|
|
||||||
if (FullCommand.Length == endQuotePosition + 1 || FullCommand[endQuotePosition + 1] == ' ')
|
|
||||||
return new WordPosition(position + 1, endQuotePosition, 2, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not a quoted word, just find the next space and return if it's the end of the command
|
|
||||||
var wordEnd = FullCommand.IndexOf(' ', position + 1);
|
|
||||||
|
|
||||||
return wordEnd == -1
|
|
||||||
? new WordPosition(position, FullCommand.Length, 0, false)
|
|
||||||
: new WordPosition(position, wordEnd, 1, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryCheckQuote(char potentialLeftQuote, out string correspondingRightQuotes)
|
|
||||||
{
|
|
||||||
foreach (var (left, right) in _quotePairs)
|
|
||||||
if (left.Contains(potentialLeftQuote))
|
|
||||||
{
|
|
||||||
correspondingRightQuotes = right;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
correspondingRightQuotes = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
|
using PluralKit.Core;
|
||||||
using uniffi.commands;
|
using uniffi.commands;
|
||||||
|
|
||||||
namespace PluralKit.Bot;
|
namespace PluralKit.Bot;
|
||||||
|
|
||||||
public class ParametersFFI
|
public class Parameters
|
||||||
{
|
{
|
||||||
private string _cb { get; init; }
|
private string _cb { get; init; }
|
||||||
private List<string> _args { get; init; }
|
private List<string> _args { get; init; }
|
||||||
public int _ptr = -1;
|
|
||||||
private Dictionary<string, string?> _flags { get; init; }
|
private Dictionary<string, string?> _flags { get; init; }
|
||||||
|
private Dictionary<string, Parameter> _params { get; init; }
|
||||||
|
|
||||||
// just used for errors, temporarily
|
// just used for errors, temporarily
|
||||||
public string FullCommand { get; init; }
|
public string FullCommand { get; init; }
|
||||||
|
|
||||||
public ParametersFFI(string cmd)
|
public Parameters(string cmd)
|
||||||
{
|
{
|
||||||
FullCommand = cmd;
|
FullCommand = cmd;
|
||||||
var result = CommandsMethods.ParseCommand(cmd);
|
var result = CommandsMethods.ParseCommand(cmd);
|
||||||
|
|
@ -22,6 +23,7 @@ public class ParametersFFI
|
||||||
_cb = command.@commandRef;
|
_cb = command.@commandRef;
|
||||||
_args = command.@args;
|
_args = command.@args;
|
||||||
_flags = command.@flags;
|
_flags = command.@flags;
|
||||||
|
_params = command.@params;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -29,43 +31,67 @@ public class ParametersFFI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ResolvedParameters> ResolveParameters(Context ctx)
|
||||||
|
{
|
||||||
|
var parsed_members = await MemberParams().ToAsyncEnumerable().ToDictionaryAwaitAsync(async item => item.Key, async item =>
|
||||||
|
await ctx.ParseMember(this, item.Value) ?? throw new PKError(ctx.CreateNotFoundError(this, "Member", item.Value))
|
||||||
|
);
|
||||||
|
var parsed_systems = await SystemParams().ToAsyncEnumerable().ToDictionaryAwaitAsync(async item => item.Key, async item =>
|
||||||
|
await ctx.ParseSystem(item.Value) ?? throw new PKError(ctx.CreateNotFoundError(this, "System", item.Value))
|
||||||
|
);
|
||||||
|
return new ResolvedParameters(this, parsed_members, parsed_systems);
|
||||||
|
}
|
||||||
|
|
||||||
public string Callback()
|
public string Callback()
|
||||||
{
|
{
|
||||||
return _cb;
|
return _cb;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Pop()
|
public IDictionary<string, string> Flags()
|
||||||
{
|
{
|
||||||
if (_args.Count > _ptr + 1) Console.WriteLine($"pop: {_ptr + 1}, {_args[_ptr + 1]}");
|
return _flags;
|
||||||
else Console.WriteLine("pop: no more arguments");
|
|
||||||
if (_args.Count() == _ptr + 1) return "";
|
|
||||||
_ptr++;
|
|
||||||
return _args[_ptr];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Peek()
|
private Dictionary<string, string> Params(Func<ParameterKind, bool> filter)
|
||||||
{
|
{
|
||||||
if (_args.Count > _ptr + 1) Console.WriteLine($"peek: {_ptr + 1}, {_args[_ptr + 1]}");
|
return _params.Where(item => filter(item.Value.@kind)).ToDictionary(item => item.Key, item => item.Value.@raw);
|
||||||
else Console.WriteLine("peek: no more arguments");
|
|
||||||
if (_args.Count() == _ptr + 1) return "";
|
|
||||||
return _args[_ptr + 1];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// this might not work quite right
|
public IDictionary<string, string> Params()
|
||||||
public string PeekWithPtr(ref int ptr)
|
|
||||||
{
|
{
|
||||||
return _args[ptr];
|
return Params(_ => true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ISet<string> Flags()
|
public IDictionary<string, string> MemberParams()
|
||||||
{
|
{
|
||||||
return new HashSet<string>(_flags.Keys);
|
return Params(kind => kind == ParameterKind.MemberRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
// parsed differently in new commands, does this work right?
|
public IDictionary<string, string> SystemParams()
|
||||||
// note: skipFlags here does nothing
|
|
||||||
public string Remainder(bool skipFlags = false)
|
|
||||||
{
|
{
|
||||||
return Pop();
|
return Params(kind => kind == ParameterKind.SystemRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ResolvedParameters
|
||||||
|
{
|
||||||
|
public readonly Parameters Raw;
|
||||||
|
public readonly Dictionary<string, PKMember> MemberParams;
|
||||||
|
public readonly Dictionary<string, PKSystem> SystemParams;
|
||||||
|
|
||||||
|
public ResolvedParameters(Parameters parameters, Dictionary<string, PKMember> member_params, Dictionary<string, PKSystem> system_params)
|
||||||
|
{
|
||||||
|
Raw = parameters;
|
||||||
|
MemberParams = member_params;
|
||||||
|
SystemParams = system_params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move this to another file
|
||||||
|
public static class ParametersExt
|
||||||
|
{
|
||||||
|
public static bool HasFlag(this Parameters parameters, params string[] potentialMatches)
|
||||||
|
{
|
||||||
|
return potentialMatches.Any(parameters.Flags().ContainsKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -27,10 +27,10 @@ public class Member
|
||||||
_avatarHosting = avatarHosting;
|
_avatarHosting = avatarHosting;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task NewMember(Context ctx)
|
public async Task NewMember(Context ctx, string memberName)
|
||||||
{
|
{
|
||||||
if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix);
|
if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix);
|
||||||
var memberName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a member name.");
|
memberName = memberName ?? throw new PKSyntaxError("You must pass a member name.");
|
||||||
|
|
||||||
// Hard name length cap
|
// Hard name length cap
|
||||||
if (memberName.Length > Limits.MaxMemberNameLength)
|
if (memberName.Length > Limits.MaxMemberNameLength)
|
||||||
|
|
|
||||||
|
|
@ -137,8 +137,22 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
|
||||||
var system = await _repo.GetSystemByAccount(evt.Author.Id);
|
var system = await _repo.GetSystemByAccount(evt.Author.Id);
|
||||||
var config = system != null ? await _repo.GetSystemConfig(system.Id) : null;
|
var config = system != null ? await _repo.GetSystemConfig(system.Id) : null;
|
||||||
var guildConfig = guild != null ? await _repo.GetGuild(guild.Id) : null;
|
var guildConfig = guild != null ? await _repo.GetGuild(guild.Id) : null;
|
||||||
|
var ctx = new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, guildConfig, _config.Prefixes ?? BotConfig.DefaultPrefixes);
|
||||||
await _tree.ExecuteCommand(new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, guildConfig, _config.Prefixes ?? BotConfig.DefaultPrefixes));
|
try
|
||||||
|
{
|
||||||
|
var parameters = new Parameters(evt.Content?.Substring(cmdStart));
|
||||||
|
var resolved_parameters = await parameters.ResolveParameters(ctx);
|
||||||
|
await _tree.ExecuteCommand(ctx, resolved_parameters);
|
||||||
|
}
|
||||||
|
catch (PKError e)
|
||||||
|
{
|
||||||
|
// don't send an "invalid command" response if the guild has those turned off
|
||||||
|
if (!(ctx.GuildConfig != null && ctx.GuildConfig!.InvalidCommandResponseEnabled != true))
|
||||||
|
{
|
||||||
|
await ctx.Reply($"{Emojis.Error} {e.Message}");
|
||||||
|
}
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (PKError)
|
catch (PKError)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ lazy_static = { workspace = true }
|
||||||
|
|
||||||
uniffi = { version = "0.25" }
|
uniffi = { version = "0.25" }
|
||||||
smol_str = "0.3.2"
|
smol_str = "0.3.2"
|
||||||
|
ordermap = "0.5"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
uniffi = { version = "0.25", features = [ "build" ] }
|
uniffi = { version = "0.25", features = [ "build" ] }
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,21 @@ interface CommandResult {
|
||||||
Ok(ParsedCommand command);
|
Ok(ParsedCommand command);
|
||||||
Err(string error);
|
Err(string error);
|
||||||
};
|
};
|
||||||
|
[Enum]
|
||||||
|
interface ParameterKind {
|
||||||
|
MemberRef();
|
||||||
|
SystemRef();
|
||||||
|
MemberPrivacyTarget();
|
||||||
|
PrivacyLevel();
|
||||||
|
OpaqueString();
|
||||||
|
};
|
||||||
|
dictionary Parameter {
|
||||||
|
string raw;
|
||||||
|
ParameterKind kind;
|
||||||
|
};
|
||||||
dictionary ParsedCommand {
|
dictionary ParsedCommand {
|
||||||
string command_ref;
|
string command_ref;
|
||||||
sequence<string> args;
|
sequence<string> args;
|
||||||
|
record<string, Parameter> params;
|
||||||
record<string, string?> flags;
|
record<string, string?> flags;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,37 +10,37 @@ pub fn cmds() -> impl Iterator<Item = Command> {
|
||||||
|
|
||||||
[
|
[
|
||||||
command!(
|
command!(
|
||||||
[member, new, MemberRef],
|
[member, new, FullString("name")],
|
||||||
"member_new",
|
"member_new",
|
||||||
"Creates a new system member"
|
"Creates a new system member"
|
||||||
),
|
),
|
||||||
command!(
|
command!(
|
||||||
[member, MemberRef],
|
[member, MemberRef("target")],
|
||||||
"member_show",
|
"member_show",
|
||||||
"Shows information about a member"
|
"Shows information about a member"
|
||||||
),
|
),
|
||||||
command!(
|
command!(
|
||||||
[member, MemberRef, description],
|
[member, MemberRef("target"), description],
|
||||||
"member_desc_show",
|
"member_desc_show",
|
||||||
"Shows a member's description"
|
"Shows a member's description"
|
||||||
),
|
),
|
||||||
command!(
|
command!(
|
||||||
[member, MemberRef, description, FullString],
|
[member, MemberRef("target"), description, FullString("description")],
|
||||||
"member_desc_update",
|
"member_desc_update",
|
||||||
"Changes a member's description"
|
"Changes a member's description"
|
||||||
),
|
),
|
||||||
command!(
|
command!(
|
||||||
[member, MemberRef, privacy],
|
[member, MemberRef("target"), privacy],
|
||||||
"member_privacy_show",
|
"member_privacy_show",
|
||||||
"Displays a member's current privacy settings"
|
"Displays a member's current privacy settings"
|
||||||
),
|
),
|
||||||
command!(
|
command!(
|
||||||
[
|
[
|
||||||
member,
|
member,
|
||||||
MemberRef,
|
MemberRef("target"),
|
||||||
privacy,
|
privacy,
|
||||||
MemberPrivacyTarget,
|
MemberPrivacyTarget("privacy_target"),
|
||||||
PrivacyLevel
|
PrivacyLevel("new_privacy_level")
|
||||||
],
|
],
|
||||||
"member_privacy_update",
|
"member_privacy_update",
|
||||||
"Changes a member's privacy settings"
|
"Changes a member's privacy settings"
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ pub fn cmds() -> impl Iterator<Item = Command> {
|
||||||
),
|
),
|
||||||
command!([system, new], "system_new", "Creates a new system"),
|
command!([system, new], "system_new", "Creates a new system"),
|
||||||
command!(
|
command!(
|
||||||
[system, new, SystemRef],
|
[system, new, FullString("name")],
|
||||||
"system_new",
|
"system_new",
|
||||||
"Creates a new system"
|
"Creates a new system"
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ uniffi::include_scaffolding!("commands");
|
||||||
use core::panic;
|
use core::panic;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use smol_str::SmolStr;
|
use ordermap::OrderMap;
|
||||||
|
use smol_str::{format_smolstr, SmolStr};
|
||||||
use tree::TreeBranch;
|
use tree::TreeBranch;
|
||||||
|
|
||||||
pub use commands::Command;
|
pub use commands::Command;
|
||||||
|
|
@ -21,27 +22,70 @@ lazy_static::lazy_static! {
|
||||||
let mut tree = TreeBranch {
|
let mut tree = TreeBranch {
|
||||||
current_command_key: None,
|
current_command_key: None,
|
||||||
possible_tokens: vec![],
|
possible_tokens: vec![],
|
||||||
branches: HashMap::new(),
|
branches: OrderMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
crate::commands::all().iter().for_each(|x| tree.register_command(x.clone()));
|
crate::commands::all().into_iter().for_each(|x| tree.register_command(x));
|
||||||
|
|
||||||
tree.sort_tokens();
|
|
||||||
|
|
||||||
// println!("{{tree:#?}}");
|
|
||||||
|
|
||||||
tree
|
tree
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum CommandResult {
|
pub enum CommandResult {
|
||||||
Ok { command: ParsedCommand },
|
Ok { command: ParsedCommand },
|
||||||
Err { error: String },
|
Err { error: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ParameterKind {
|
||||||
|
MemberRef,
|
||||||
|
SystemRef,
|
||||||
|
MemberPrivacyTarget,
|
||||||
|
PrivacyLevel,
|
||||||
|
OpaqueString,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Parameter {
|
||||||
|
raw: String,
|
||||||
|
kind: ParameterKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parameter {
|
||||||
|
fn new(raw: impl ToString, kind: ParameterKind) -> Self {
|
||||||
|
Self {
|
||||||
|
raw: raw.to_string(),
|
||||||
|
kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! parameter_impl {
|
||||||
|
($($name:ident $kind:ident),*) => {
|
||||||
|
impl Parameter {
|
||||||
|
$(
|
||||||
|
fn $name(raw: impl ToString) -> Self {
|
||||||
|
Self::new(raw, $crate::ParameterKind::$kind)
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
parameter_impl! {
|
||||||
|
opaque OpaqueString,
|
||||||
|
member MemberRef,
|
||||||
|
system SystemRef,
|
||||||
|
member_privacy_target MemberPrivacyTarget,
|
||||||
|
privacy_level PrivacyLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct ParsedCommand {
|
pub struct ParsedCommand {
|
||||||
pub command_ref: String,
|
pub command_ref: String,
|
||||||
pub args: Vec<String>,
|
pub args: Vec<String>,
|
||||||
|
pub params: HashMap<String, Parameter>,
|
||||||
pub flags: HashMap<String, Option<String>>,
|
pub flags: HashMap<String, Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,9 +97,11 @@ fn parse_command(input: String) -> CommandResult {
|
||||||
let mut current_pos = 0;
|
let mut current_pos = 0;
|
||||||
|
|
||||||
let mut args: Vec<String> = Vec::new();
|
let mut args: Vec<String> = Vec::new();
|
||||||
|
let mut params: HashMap<String, Parameter> = HashMap::new();
|
||||||
let mut flags: HashMap<String, Option<String>> = HashMap::new();
|
let mut flags: HashMap<String, Option<String>> = HashMap::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
println!("{:?}", local_tree.possible_tokens);
|
||||||
let next = next_token(
|
let next = next_token(
|
||||||
local_tree.possible_tokens.clone(),
|
local_tree.possible_tokens.clone(),
|
||||||
input.clone(),
|
input.clone(),
|
||||||
|
|
@ -70,8 +116,22 @@ fn parse_command(input: String) -> CommandResult {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(arg) = arg {
|
if let Some(arg) = arg.as_ref() {
|
||||||
args.push(arg.into());
|
// get param name from token
|
||||||
|
// TODO: idk if this should be on token itself, doesn't feel right, but does work
|
||||||
|
let param = match &found_token {
|
||||||
|
Token::FullString(n) => Some((n, Parameter::opaque(arg))),
|
||||||
|
Token::MemberRef(n) => Some((n, Parameter::member(arg))),
|
||||||
|
Token::MemberPrivacyTarget(n) => Some((n, Parameter::member_privacy_target(arg))),
|
||||||
|
Token::SystemRef(n) => Some((n, Parameter::system(arg))),
|
||||||
|
Token::PrivacyLevel(n) => Some((n, Parameter::privacy_level(arg))),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
// insert arg as paramater if this is a parameter
|
||||||
|
if let Some((param_name, param)) = param {
|
||||||
|
params.insert(param_name.to_string(), param);
|
||||||
|
}
|
||||||
|
args.push(arg.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(next_tree) = local_tree.branches.get(&found_token) {
|
if let Some(next_tree) = local_tree.branches.get(&found_token) {
|
||||||
|
|
@ -82,9 +142,11 @@ fn parse_command(input: String) -> CommandResult {
|
||||||
}
|
}
|
||||||
Err(None) => {
|
Err(None) => {
|
||||||
if let Some(command_ref) = local_tree.current_command_key {
|
if let Some(command_ref) = local_tree.current_command_key {
|
||||||
|
println!("{command_ref} {params:?}");
|
||||||
return CommandResult::Ok {
|
return CommandResult::Ok {
|
||||||
command: ParsedCommand {
|
command: ParsedCommand {
|
||||||
command_ref: command_ref.into(),
|
command_ref: command_ref.into(),
|
||||||
|
params,
|
||||||
args,
|
args,
|
||||||
flags,
|
flags,
|
||||||
},
|
},
|
||||||
|
|
@ -136,19 +198,12 @@ fn next_token(
|
||||||
|
|
||||||
// iterate over tokens and run try_match
|
// iterate over tokens and run try_match
|
||||||
for token in possible_tokens {
|
for token in possible_tokens {
|
||||||
if let TokenMatchResult::Match(value) =
|
// for FullString just send the whole string
|
||||||
// for FullString just send the whole string
|
let input_to_match = param.clone().map(|v| v.0);
|
||||||
token.try_match(if matches!(token, Token::FullString) {
|
match token.try_match(input_to_match) {
|
||||||
if input.is_empty() {
|
TokenMatchResult::Match(value) => return Ok((token, value, param.map(|v| v.1).unwrap_or(current_pos))),
|
||||||
None
|
TokenMatchResult::MissingParameter { name } => return Err(Some(format_smolstr!("Missing parameter `{name}` in command `{input} [{name}]`."))),
|
||||||
} else {
|
TokenMatchResult::NoMatch => {}
|
||||||
Some(input.clone())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
param.clone().map(|v| v.0)
|
|
||||||
})
|
|
||||||
{
|
|
||||||
return Ok((token, value, param.map(|v| v.1).unwrap_or(current_pos)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
use smol_str::{SmolStr, ToSmolStr};
|
use smol_str::{SmolStr, ToSmolStr};
|
||||||
|
|
||||||
|
type ParamName = &'static str;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||||
pub enum Token {
|
pub enum Token {
|
||||||
/// Token used to represent a finished command (i.e. no more parameters required)
|
/// Token used to represent a finished command (i.e. no more parameters required)
|
||||||
|
|
@ -12,16 +14,16 @@ pub enum Token {
|
||||||
// todo!
|
// todo!
|
||||||
MultiValue(Vec<Vec<SmolStr>>),
|
MultiValue(Vec<Vec<SmolStr>>),
|
||||||
|
|
||||||
FullString,
|
FullString(ParamName),
|
||||||
|
|
||||||
/// Member reference (hid or member name)
|
/// Member reference (hid or member name)
|
||||||
MemberRef,
|
MemberRef(ParamName),
|
||||||
MemberPrivacyTarget,
|
MemberPrivacyTarget(ParamName),
|
||||||
|
|
||||||
/// System reference
|
/// System reference
|
||||||
SystemRef,
|
SystemRef(ParamName),
|
||||||
|
|
||||||
PrivacyLevel,
|
PrivacyLevel(ParamName),
|
||||||
|
|
||||||
// currently not included in command definitions
|
// currently not included in command definitions
|
||||||
// todo: flags with values
|
// todo: flags with values
|
||||||
|
|
@ -32,6 +34,9 @@ pub enum TokenMatchResult {
|
||||||
NoMatch,
|
NoMatch,
|
||||||
/// Token matched, optionally with a value.
|
/// Token matched, optionally with a value.
|
||||||
Match(Option<SmolStr>),
|
Match(Option<SmolStr>),
|
||||||
|
MissingParameter {
|
||||||
|
name: ParamName,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// move this somewhere else
|
// move this somewhere else
|
||||||
|
|
@ -43,36 +48,38 @@ impl Token {
|
||||||
if matches!(self, Self::Empty) && input.is_none() {
|
if matches!(self, Self::Empty) && input.is_none() {
|
||||||
return TokenMatchResult::Match(None);
|
return TokenMatchResult::Match(None);
|
||||||
} else if input.is_none() {
|
} else if input.is_none() {
|
||||||
return TokenMatchResult::NoMatch;
|
return match self {
|
||||||
|
Self::FullString(param_name) => TokenMatchResult::MissingParameter { name: param_name },
|
||||||
|
Self::MemberRef(param_name) => TokenMatchResult::MissingParameter { name: param_name },
|
||||||
|
Self::MemberPrivacyTarget(param_name) => TokenMatchResult::MissingParameter { name: param_name },
|
||||||
|
Self::SystemRef(param_name) => TokenMatchResult::MissingParameter { name: param_name },
|
||||||
|
Self::PrivacyLevel(param_name) => TokenMatchResult::MissingParameter { name: param_name },
|
||||||
|
_ => TokenMatchResult::NoMatch,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let input = input.unwrap();
|
let input = input.as_ref().map(|s| s.trim()).unwrap();
|
||||||
|
|
||||||
// try actually matching stuff
|
// try actually matching stuff
|
||||||
match self {
|
match self {
|
||||||
Self::Empty => return TokenMatchResult::NoMatch,
|
Self::Empty => return TokenMatchResult::NoMatch,
|
||||||
Self::Flag => unreachable!(), // matched upstream
|
Self::Flag => unreachable!(), // matched upstream
|
||||||
Self::Value(values) => {
|
Self::Value(values) if values.iter().any(|v| v.eq(input)) => {
|
||||||
for v in values {
|
return TokenMatchResult::Match(None);
|
||||||
if input.trim() == v {
|
|
||||||
// c# bot currently needs subcommands provided as arguments
|
|
||||||
// todo!: remove this
|
|
||||||
return TokenMatchResult::Match(Some(v.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Self::Value(_) => {}
|
||||||
Self::MultiValue(_) => todo!(),
|
Self::MultiValue(_) => todo!(),
|
||||||
Self::FullString => return TokenMatchResult::Match(Some(input)),
|
Self::FullString(_) => return TokenMatchResult::Match(Some(input.into())),
|
||||||
Self::SystemRef => return TokenMatchResult::Match(Some(input)),
|
Self::SystemRef(_) => return TokenMatchResult::Match(Some(input.into())),
|
||||||
Self::MemberRef => return TokenMatchResult::Match(Some(input)),
|
Self::MemberRef(_) => return TokenMatchResult::Match(Some(input.into())),
|
||||||
Self::MemberPrivacyTarget if MEMBER_PRIVACY_TARGETS.contains(&input.trim()) => {
|
Self::MemberPrivacyTarget(_) if MEMBER_PRIVACY_TARGETS.contains(&input) => {
|
||||||
return TokenMatchResult::Match(Some(input))
|
return TokenMatchResult::Match(Some(input.into()))
|
||||||
}
|
}
|
||||||
Self::MemberPrivacyTarget => {}
|
Self::MemberPrivacyTarget(_) => {}
|
||||||
Self::PrivacyLevel if input == "public" || input == "private" => {
|
Self::PrivacyLevel(_) if input == "public" || input == "private" => {
|
||||||
return TokenMatchResult::Match(Some(input))
|
return TokenMatchResult::Match(Some(input.into()))
|
||||||
}
|
}
|
||||||
Self::PrivacyLevel => {}
|
Self::PrivacyLevel(_) => {}
|
||||||
}
|
}
|
||||||
// note: must not add a _ case to the above match
|
// note: must not add a _ case to the above match
|
||||||
// instead, for conditional matches, also add generic cases with no return
|
// instead, for conditional matches, also add generic cases with no return
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
|
use ordermap::OrderMap;
|
||||||
use smol_str::SmolStr;
|
use smol_str::SmolStr;
|
||||||
|
|
||||||
use crate::{commands::Command, Token};
|
use crate::{commands::Command, Token};
|
||||||
use std::{cmp::Ordering, collections::HashMap};
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TreeBranch {
|
pub struct TreeBranch {
|
||||||
pub current_command_key: Option<SmolStr>,
|
pub current_command_key: Option<SmolStr>,
|
||||||
/// branches.keys(), but sorted by specificity
|
/// branches.keys(), but sorted by specificity
|
||||||
pub possible_tokens: Vec<Token>,
|
pub possible_tokens: Vec<Token>,
|
||||||
pub branches: HashMap<Token, TreeBranch>,
|
pub branches: OrderMap<Token, TreeBranch>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TreeBranch {
|
impl TreeBranch {
|
||||||
|
|
@ -20,7 +21,7 @@ impl TreeBranch {
|
||||||
current_branch = current_branch.branches.entry(token).or_insert(TreeBranch {
|
current_branch = current_branch.branches.entry(token).or_insert(TreeBranch {
|
||||||
current_command_key: None,
|
current_command_key: None,
|
||||||
possible_tokens: vec![],
|
possible_tokens: vec![],
|
||||||
branches: HashMap::new(),
|
branches: OrderMap::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// when we're out of tokens, add an Empty branch with the callback and no sub-branches
|
// when we're out of tokens, add an Empty branch with the callback and no sub-branches
|
||||||
|
|
@ -29,31 +30,8 @@ impl TreeBranch {
|
||||||
TreeBranch {
|
TreeBranch {
|
||||||
current_command_key: Some(command.cb),
|
current_command_key: Some(command.cb),
|
||||||
possible_tokens: vec![],
|
possible_tokens: vec![],
|
||||||
branches: HashMap::new(),
|
branches: OrderMap::new(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sort_tokens(&mut self) {
|
|
||||||
for branch in self.branches.values_mut() {
|
|
||||||
branch.sort_tokens();
|
|
||||||
}
|
|
||||||
|
|
||||||
// put Value tokens at the end
|
|
||||||
// i forget exactly how this works
|
|
||||||
// todo!: document this before PR mergs
|
|
||||||
self.possible_tokens = self
|
|
||||||
.branches
|
|
||||||
.keys()
|
|
||||||
.into_iter()
|
|
||||||
.map(|v| v.clone())
|
|
||||||
.collect();
|
|
||||||
self.possible_tokens.sort_by(|v, _| {
|
|
||||||
if matches!(v, Token::Value(_)) {
|
|
||||||
Ordering::Greater
|
|
||||||
} else {
|
|
||||||
Ordering::Less
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue