mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-04 04:56:49 +00:00
feat: better parameters handling, implement multi-token matching
This commit is contained in:
parent
b29c51f103
commit
482c923507
14 changed files with 521 additions and 251 deletions
|
|
@ -4,30 +4,29 @@ namespace PluralKit.Bot;
|
|||
|
||||
public partial class CommandTree
|
||||
{
|
||||
public Task ExecuteCommand(Context ctx, ResolvedParameters parameters)
|
||||
public Task ExecuteCommand(Context ctx)
|
||||
{
|
||||
switch (parameters.Raw.Callback())
|
||||
return ctx.Parameters.Callback() switch
|
||||
{
|
||||
case "help":
|
||||
return ctx.Execute<Help>(Help, m => m.HelpRoot(ctx));
|
||||
case "help_commands":
|
||||
return ctx.Reply(
|
||||
"For the list of commands, see the website: <https://pluralkit.me/commands>");
|
||||
case "help_proxy":
|
||||
return ctx.Reply(
|
||||
"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"]));
|
||||
case "fun_thunder":
|
||||
return ctx.Execute<Fun>(null, m => m.Thunder(ctx));
|
||||
case "fun_meow":
|
||||
return ctx.Execute<Fun>(null, m => m.Meow(ctx));
|
||||
default:
|
||||
return ctx.Reply(
|
||||
$"{Emojis.Error} Parsed command {parameters.Raw.Callback().AsCode()} not implemented in PluralKit.Bot!");
|
||||
}
|
||||
"help" => ctx.Execute<Help>(Help, m => m.HelpRoot(ctx)),
|
||||
"help_commands" => ctx.Reply(
|
||||
"For the list of commands, see the website: <https://pluralkit.me/commands>"),
|
||||
"help_proxy" => ctx.Reply(
|
||||
"The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying"),
|
||||
"member_show" => ctx.Execute<Member>(MemberInfo, m => m.ViewMember(ctx)),
|
||||
"member_new" => ctx.Execute<Member>(MemberNew, m => m.NewMember(ctx)),
|
||||
"member_soulscream" => ctx.Execute<Member>(MemberInfo, m => m.Soulscream(ctx)),
|
||||
"cfg_ap_account_show" => ctx.Execute<Config>(null, m => m.ViewAutoproxyAccount(ctx)),
|
||||
"cfg_ap_account_update" => ctx.Execute<Config>(null, m => m.EditAutoproxyAccount(ctx)),
|
||||
"cfg_ap_timeout_show" => ctx.Execute<Config>(null, m => m.ViewAutoproxyTimeout(ctx)),
|
||||
"cfg_ap_timeout_update" => ctx.Execute<Config>(null, m => m.EditAutoproxyTimeout(ctx)),
|
||||
"fun_thunder" => ctx.Execute<Fun>(null, m => m.Thunder(ctx)),
|
||||
"fun_meow" => ctx.Execute<Fun>(null, m => m.Meow(ctx)),
|
||||
_ =>
|
||||
// this should only ever occur when deving if commands are not implemented...
|
||||
ctx.Reply(
|
||||
$"{Emojis.Error} Parsed command {ctx.Parameters.Callback().AsCode()} not implemented in PluralKit.Bot!"),
|
||||
};
|
||||
if (ctx.Match("system", "s"))
|
||||
return HandleSystemCommand(ctx);
|
||||
if (ctx.Match("member", "m"))
|
||||
|
|
@ -405,10 +404,6 @@ public partial class CommandTree
|
|||
await ctx.Execute<MemberEdit>(MemberPrivacy, m => m.Privacy(ctx, target, PrivacyLevel.Private));
|
||||
else if (ctx.Match("public", "shown", "show", "unhide", "unhidden"))
|
||||
await ctx.Execute<MemberEdit>(MemberPrivacy, m => m.Privacy(ctx, target, PrivacyLevel.Public));
|
||||
else if (ctx.Match("soulscream"))
|
||||
await ctx.Execute<Member>(MemberInfo, m => m.Soulscream(ctx, target));
|
||||
else if (!ctx.HasNext()) // Bare command
|
||||
await ctx.Execute<Member>(MemberInfo, m => m.ViewMember(ctx, target));
|
||||
else
|
||||
await PrintCommandNotFoundError(ctx, MemberInfo, MemberRename, MemberDisplayName, MemberServerName,
|
||||
MemberDesc, MemberPronouns, MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar,
|
||||
|
|
@ -576,10 +571,6 @@ public partial class CommandTree
|
|||
if (!ctx.HasNext())
|
||||
return ctx.Execute<Config>(null, m => m.ShowConfig(ctx));
|
||||
|
||||
if (ctx.MatchMultiple(new[] { "autoproxy", "ap" }, new[] { "account", "ac" }))
|
||||
return ctx.Execute<Config>(null, m => m.AutoproxyAccount(ctx));
|
||||
if (ctx.MatchMultiple(new[] { "autoproxy", "ap" }, new[] { "timeout", "tm" }))
|
||||
return ctx.Execute<Config>(null, m => m.AutoproxyTimeout(ctx));
|
||||
if (ctx.Match("timezone", "zone", "tz"))
|
||||
return ctx.Execute<Config>(null, m => m.SystemTimezone(ctx));
|
||||
if (ctx.Match("ping"))
|
||||
|
|
|
|||
|
|
@ -26,11 +26,9 @@ public class Context
|
|||
private readonly IMetrics _metrics;
|
||||
private readonly CommandMessageService _commandMessageService;
|
||||
|
||||
private Command? _currentCommand;
|
||||
|
||||
public Context(ILifetimeScope provider, int shardId, Guild? guild, Channel channel, MessageCreateEvent message,
|
||||
int commandParseOffset, PKSystem senderSystem, SystemConfig config,
|
||||
GuildConfig? guildConfig, string[] prefixes)
|
||||
GuildConfig? guildConfig, string[] prefixes, Parameters parameters)
|
||||
{
|
||||
Message = (Message)message;
|
||||
ShardId = shardId;
|
||||
|
|
@ -50,6 +48,7 @@ public class Context
|
|||
DefaultPrefix = prefixes[0];
|
||||
Rest = provider.Resolve<DiscordApiClient>();
|
||||
Cluster = provider.Resolve<Cluster>();
|
||||
Parameters = parameters;
|
||||
}
|
||||
|
||||
public readonly IDiscordCache Cache;
|
||||
|
|
@ -75,6 +74,7 @@ public class Context
|
|||
|
||||
public readonly string CommandPrefix;
|
||||
public readonly string DefaultPrefix;
|
||||
public readonly Parameters Parameters;
|
||||
|
||||
internal readonly IDatabase Database;
|
||||
internal readonly ModelRepository Repository;
|
||||
|
|
@ -111,8 +111,6 @@ public class Context
|
|||
|
||||
public async Task Execute<T>(Command? commandDef, Func<T, Task> handler, bool deprecated = false)
|
||||
{
|
||||
_currentCommand = commandDef;
|
||||
|
||||
if (deprecated && commandDef != null)
|
||||
{
|
||||
await Reply($"{Emojis.Warn} Server configuration has moved to `{DefaultPrefix}serverconfig`. The command you are trying to run is now `{DefaultPrefix}{commandDef.Key}`.");
|
||||
|
|
@ -153,8 +151,8 @@ public class Context
|
|||
|
||||
public LookupContext LookupContextFor(SystemId systemId)
|
||||
{
|
||||
var hasPrivateOverride = this.MatchFlag("private", "priv");
|
||||
var hasPublicOverride = this.MatchFlag("public", "pub");
|
||||
var hasPrivateOverride = Parameters.HasFlag("private", "priv");
|
||||
var hasPublicOverride = Parameters.HasFlag("public", "pub");
|
||||
|
||||
if (hasPrivateOverride && hasPublicOverride)
|
||||
throw new PKError("Cannot match both public and private flags at the same time.");
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ public static class ContextEntityArgumentsExt
|
|||
return null;
|
||||
}
|
||||
|
||||
public static async Task<PKMember> ParseMember(this Context ctx, Parameters parameters, string input, SystemId? restrictToSystem = null)
|
||||
public static async Task<PKMember> ParseMember(this Context ctx, string input, bool byId, SystemId? restrictToSystem = null)
|
||||
{
|
||||
// Member references can have one of three forms, depending on
|
||||
// whether you're in a system or not:
|
||||
|
|
@ -69,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 && !parameters.HasFlag("id", "by-id"))
|
||||
if (ctx.System != null && !byId)
|
||||
{
|
||||
// First, try finding by member name in system
|
||||
if (await ctx.Repository.GetMemberByName(ctx.System.Id, input) is PKMember memberByName)
|
||||
|
|
@ -169,9 +169,9 @@ public static class ContextEntityArgumentsExt
|
|||
return group;
|
||||
}
|
||||
|
||||
public static string CreateNotFoundError(this Context ctx, Parameters parameters, string entity, string input)
|
||||
public static string CreateNotFoundError(this Context ctx, string entity, string input, bool byId = false)
|
||||
{
|
||||
var isIDOnlyQuery = ctx.System == null || parameters.HasFlag("id", "by-id");
|
||||
var isIDOnlyQuery = ctx.System == null || byId;
|
||||
var inputIsHid = HidUtils.ParseHid(input) != null;
|
||||
|
||||
if (isIDOnlyQuery)
|
||||
|
|
|
|||
64
PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs
Normal file
64
PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot;
|
||||
|
||||
public static class ContextParametersExt
|
||||
{
|
||||
public static async Task<string?> ParamResolveOpaque(this Context ctx, string param_name)
|
||||
{
|
||||
return await ctx.Parameters.ResolveParameter(
|
||||
ctx, param_name,
|
||||
param => (param as Parameter.Opaque)?.value
|
||||
);
|
||||
}
|
||||
|
||||
public static async Task<PKMember?> ParamResolveMember(this Context ctx, string param_name)
|
||||
{
|
||||
return await ctx.Parameters.ResolveParameter(
|
||||
ctx, param_name,
|
||||
param => (param as Parameter.MemberRef)?.member
|
||||
);
|
||||
}
|
||||
|
||||
public static async Task<PKSystem?> ParamResolveSystem(this Context ctx, string param_name)
|
||||
{
|
||||
return await ctx.Parameters.ResolveParameter(
|
||||
ctx, param_name,
|
||||
param => (param as Parameter.SystemRef)?.system
|
||||
);
|
||||
}
|
||||
|
||||
public static async Task<MemberPrivacySubject?> ParamResolveMemberPrivacyTarget(this Context ctx, string param_name)
|
||||
{
|
||||
return await ctx.Parameters.ResolveParameter(
|
||||
ctx, param_name,
|
||||
param => (param as Parameter.MemberPrivacyTarget)?.target
|
||||
);
|
||||
}
|
||||
|
||||
public static async Task<string?> ParamResolvePrivacyLevel(this Context ctx, string param_name)
|
||||
{
|
||||
return await ctx.Parameters.ResolveParameter(
|
||||
ctx, param_name,
|
||||
param => (param as Parameter.PrivacyLevel)?.level
|
||||
);
|
||||
}
|
||||
|
||||
public static async Task<bool?> ParamResolveToggle(this Context ctx, string param_name)
|
||||
{
|
||||
return await ctx.Parameters.ResolveParameter(
|
||||
ctx, param_name,
|
||||
param => (param as Parameter.Toggle)?.value
|
||||
);
|
||||
}
|
||||
|
||||
// this can never really be false (either it's present and is true or it's not present)
|
||||
// but we keep it nullable for consistency with the other methods
|
||||
public static async Task<bool?> ParamResolveReset(this Context ctx, string param_name)
|
||||
{
|
||||
return await ctx.Parameters.ResolveParameter<bool?>(
|
||||
ctx, param_name,
|
||||
param => param is Parameter.Reset
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,27 @@
|
|||
using System.Diagnostics;
|
||||
using PluralKit.Core;
|
||||
using uniffi.commands;
|
||||
|
||||
namespace PluralKit.Bot;
|
||||
|
||||
// corresponds to the ffi Paramater type, but with stricter types (also avoiding exposing ffi types!)
|
||||
public abstract record Parameter()
|
||||
{
|
||||
public record MemberRef(PKMember member): Parameter;
|
||||
public record SystemRef(PKSystem system): Parameter;
|
||||
public record MemberPrivacyTarget(MemberPrivacySubject target): Parameter;
|
||||
public record PrivacyLevel(string level): Parameter;
|
||||
public record Toggle(bool value): Parameter;
|
||||
public record Opaque(string value): Parameter;
|
||||
public record Reset(): Parameter;
|
||||
}
|
||||
|
||||
public class Parameters
|
||||
{
|
||||
private string _cb { get; init; }
|
||||
private List<string> _args { get; init; }
|
||||
private Dictionary<string, string?> _flags { get; init; }
|
||||
private Dictionary<string, Parameter> _params { get; init; }
|
||||
private Dictionary<string, uniffi.commands.Parameter> _params { get; init; }
|
||||
|
||||
// just used for errors, temporarily
|
||||
public string FullCommand { get; init; }
|
||||
|
|
@ -31,68 +44,61 @@ public class Parameters
|
|||
}
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
return _cb;
|
||||
}
|
||||
|
||||
public IDictionary<string, string> Flags()
|
||||
public bool HasFlag(params string[] potentialMatches)
|
||||
{
|
||||
return _flags;
|
||||
return potentialMatches.Any(_flags.ContainsKey);
|
||||
}
|
||||
|
||||
private Dictionary<string, string> Params(Func<ParameterKind, bool> filter)
|
||||
// resolves a single parameter
|
||||
private async Task<Parameter?> ResolveParameter(Context ctx, string param_name)
|
||||
{
|
||||
return _params.Where(item => filter(item.Value.@kind)).ToDictionary(item => item.Key, item => item.Value.@raw);
|
||||
if (!_params.ContainsKey(param_name)) return null;
|
||||
switch (_params[param_name])
|
||||
{
|
||||
case uniffi.commands.Parameter.MemberRef memberRef:
|
||||
var byId = HasFlag("id", "by-id");
|
||||
return new Parameter.MemberRef(
|
||||
await ctx.ParseMember(memberRef.member, byId)
|
||||
?? throw new PKError(ctx.CreateNotFoundError("Member", memberRef.member, byId))
|
||||
);
|
||||
case uniffi.commands.Parameter.SystemRef systemRef:
|
||||
// todo: do we need byId here?
|
||||
return new Parameter.SystemRef(
|
||||
await ctx.ParseSystem(systemRef.system)
|
||||
?? throw new PKError(ctx.CreateNotFoundError("System", systemRef.system))
|
||||
);
|
||||
case uniffi.commands.Parameter.MemberPrivacyTarget memberPrivacyTarget:
|
||||
// this should never really fail...
|
||||
// todo: we shouldn't have *three* different MemberPrivacyTarget types (rust, ffi, c#) syncing the cases will be annoying...
|
||||
if (!MemberPrivacyUtils.TryParseMemberPrivacy(memberPrivacyTarget.target, out var target))
|
||||
throw new PKError($"Invalid member privacy target {memberPrivacyTarget.target}");
|
||||
return new Parameter.MemberPrivacyTarget(target);
|
||||
case uniffi.commands.Parameter.PrivacyLevel privacyLevel:
|
||||
return new Parameter.PrivacyLevel(privacyLevel.level);
|
||||
case uniffi.commands.Parameter.Toggle toggle:
|
||||
return new Parameter.Toggle(toggle.toggle);
|
||||
case uniffi.commands.Parameter.OpaqueString opaque:
|
||||
return new Parameter.Opaque(opaque.raw);
|
||||
case uniffi.commands.Parameter.Reset _:
|
||||
return new Parameter.Reset();
|
||||
}
|
||||
// this should also never happen
|
||||
throw new PKError($"Unknown parameter type for parameter {param_name}");
|
||||
}
|
||||
|
||||
public IDictionary<string, string> Params()
|
||||
public async Task<T> ResolveParameter<T>(Context ctx, string param_name, Func<Parameter, T?> extract_func)
|
||||
{
|
||||
return Params(_ => true);
|
||||
}
|
||||
|
||||
public IDictionary<string, string> MemberParams()
|
||||
{
|
||||
return Params(kind => kind == ParameterKind.MemberRef);
|
||||
}
|
||||
|
||||
public IDictionary<string, string> SystemParams()
|
||||
{
|
||||
return Params(kind => kind == ParameterKind.SystemRef);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: im not really sure if this should be the way to go
|
||||
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);
|
||||
var param = await ResolveParameter(ctx, param_name);
|
||||
// todo: i think this should return null for everything...?
|
||||
if (param == null) return default;
|
||||
return extract_func(param)
|
||||
// this should never really happen (hopefully!), but in case the parameter names dont match up (typos...) between rust <-> c#...
|
||||
// (it would be very cool to have this statically checked somehow..?)
|
||||
?? throw new PKError($"Parameter {param_name.AsCode()} was not found for command {Callback().AsCode()} -- this is a bug!!");
|
||||
}
|
||||
}
|
||||
|
|
@ -190,17 +190,17 @@ public class Config
|
|||
}
|
||||
private string EnabledDisabled(bool value) => value ? "enabled" : "disabled";
|
||||
|
||||
public async Task AutoproxyAccount(Context ctx)
|
||||
public async Task ViewAutoproxyAccount(Context ctx)
|
||||
{
|
||||
var allowAutoproxy = await ctx.Repository.GetAutoproxyEnabled(ctx.Author.Id);
|
||||
|
||||
if (!ctx.HasNext())
|
||||
{
|
||||
await ctx.Reply($"Autoproxy is currently **{EnabledDisabled(allowAutoproxy)}** for account <@{ctx.Author.Id}>.");
|
||||
return;
|
||||
}
|
||||
|
||||
var allow = ctx.MatchToggle(true);
|
||||
public async Task EditAutoproxyAccount(Context ctx)
|
||||
{
|
||||
var allowAutoproxy = await ctx.Repository.GetAutoproxyEnabled(ctx.Author.Id);
|
||||
var allow = await ctx.ParamResolveToggle("toggle") ?? throw new PKSyntaxError("You need to specify whether to enable or disable autoproxy for this account.");
|
||||
|
||||
var statusString = EnabledDisabled(allow);
|
||||
if (allowAutoproxy == allow)
|
||||
|
|
@ -213,10 +213,7 @@ public class Config
|
|||
await ctx.Reply($"{Emojis.Success} Autoproxy {statusString} for account <@{ctx.Author.Id}>.");
|
||||
}
|
||||
|
||||
|
||||
public async Task AutoproxyTimeout(Context ctx)
|
||||
{
|
||||
if (!ctx.HasNext())
|
||||
public async Task ViewAutoproxyTimeout(Context ctx)
|
||||
{
|
||||
var timeout = ctx.Config.LatchTimeout.HasValue
|
||||
? Duration.FromSeconds(ctx.Config.LatchTimeout.Value)
|
||||
|
|
@ -228,16 +225,22 @@ public class Config
|
|||
await ctx.Reply("Latch timeout is currently **disabled** for your system. Latch mode autoproxy will never time out.");
|
||||
else
|
||||
await ctx.Reply($"The current latch timeout duration for your system is {timeout.Value.ToTimeSpan().Humanize(4)}.");
|
||||
return;
|
||||
}
|
||||
|
||||
public async Task EditAutoproxyTimeout(Context ctx)
|
||||
{
|
||||
var _newTimeout = await ctx.ParamResolveOpaque("timeout");
|
||||
var _reset = await ctx.ParamResolveReset("reset");
|
||||
var _toggle = await ctx.ParamResolveToggle("toggle");
|
||||
|
||||
Duration? newTimeout;
|
||||
Duration overflow = Duration.Zero;
|
||||
if (ctx.Match("off", "stop", "cancel", "no", "disable", "remove")) newTimeout = Duration.Zero;
|
||||
else if (ctx.MatchClear()) newTimeout = null;
|
||||
if (_toggle == false) newTimeout = Duration.Zero;
|
||||
else if (_reset == true) newTimeout = null;
|
||||
else
|
||||
{
|
||||
var timeoutStr = ctx.RemainderOrNull();
|
||||
// todo: we should parse date in the command parser
|
||||
var timeoutStr = _newTimeout;
|
||||
var timeoutPeriod = DateUtils.ParsePeriod(timeoutStr);
|
||||
if (timeoutPeriod == null) throw new PKError($"Could not parse '{timeoutStr}' as a valid duration. Try using a syntax such as \"3h5m\" (i.e. 3 hours and 5 minutes).");
|
||||
if (timeoutPeriod.Value.TotalHours > 100000)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Net;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Web;
|
||||
|
||||
using Dapper;
|
||||
|
|
@ -27,8 +28,10 @@ public class Member
|
|||
_avatarHosting = avatarHosting;
|
||||
}
|
||||
|
||||
public async Task NewMember(Context ctx, string memberName)
|
||||
public async Task NewMember(Context ctx)
|
||||
{
|
||||
var memberName = await ctx.ParamResolveOpaque("name");
|
||||
|
||||
if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix);
|
||||
memberName = memberName ?? throw new PKSyntaxError("You must pass a member name.");
|
||||
|
||||
|
|
@ -124,17 +127,19 @@ public class Member
|
|||
$"{Emojis.Warn} You are approaching the per-system member limit ({memberCount} / {memberLimit} members). Once you reach this limit, you will be unable to create new members until existing members are deleted, or you can ask for your limit to be raised in the PluralKit support server: <https://discord.gg/PczBt78>");
|
||||
}
|
||||
|
||||
public async Task ViewMember(Context ctx, PKMember target)
|
||||
public async Task ViewMember(Context ctx)
|
||||
{
|
||||
var target = await ctx.ParamResolveMember("target");
|
||||
var system = await ctx.Repository.GetSystem(target.System);
|
||||
await ctx.Reply(
|
||||
embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone));
|
||||
}
|
||||
|
||||
public async Task Soulscream(Context ctx, PKMember target)
|
||||
public async Task Soulscream(Context ctx)
|
||||
{
|
||||
// this is for a meme, please don't take this code seriously. :)
|
||||
|
||||
var target = await ctx.ParamResolveMember("target");
|
||||
var name = target.NameFor(ctx.LookupContextFor(target.System));
|
||||
var encoded = HttpUtility.UrlEncode(name);
|
||||
|
||||
|
|
|
|||
|
|
@ -137,23 +137,29 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
|
|||
var system = await _repo.GetSystemByAccount(evt.Author.Id);
|
||||
var config = system != null ? await _repo.GetSystemConfig(system.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);
|
||||
|
||||
// parse parameters
|
||||
Parameters parameters;
|
||||
try
|
||||
{
|
||||
var parameters = new Parameters(evt.Content?.Substring(cmdStart));
|
||||
var resolved_parameters = await parameters.ResolveParameters(ctx);
|
||||
await _tree.ExecuteCommand(ctx, resolved_parameters);
|
||||
parameters = new Parameters(evt.Content?.Substring(cmdStart));
|
||||
}
|
||||
catch (PKError e)
|
||||
{
|
||||
// don't send an "invalid command" response if the guild has those turned off
|
||||
// TODO: only dont send command not found, not every parse error (eg. missing params, syntax error...)
|
||||
if (!(ctx.GuildConfig != null && ctx.GuildConfig!.InvalidCommandResponseEnabled != true))
|
||||
if (!(guildConfig != null && guildConfig!.InvalidCommandResponseEnabled != true))
|
||||
{
|
||||
await ctx.Reply($"{Emojis.Error} {e.Message}");
|
||||
await _rest.CreateMessage(channel.Id, new MessageRequest
|
||||
{
|
||||
Content = $"{Emojis.Error} {e.Message}",
|
||||
});
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
var ctx = new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, guildConfig, _config.Prefixes ?? BotConfig.DefaultPrefixes, parameters);
|
||||
await _tree.ExecuteCommand(ctx);
|
||||
}
|
||||
catch (PKError)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ pub fn all() -> Vec<Command> {
|
|||
(help::cmds())
|
||||
.chain(system::cmds())
|
||||
.chain(member::cmds())
|
||||
.chain(config::cmds())
|
||||
.chain(fun::cmds())
|
||||
.collect()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,16 +7,14 @@ interface CommandResult {
|
|||
Err(string error);
|
||||
};
|
||||
[Enum]
|
||||
interface ParameterKind {
|
||||
MemberRef();
|
||||
SystemRef();
|
||||
MemberPrivacyTarget();
|
||||
PrivacyLevel();
|
||||
OpaqueString();
|
||||
};
|
||||
dictionary Parameter {
|
||||
string raw;
|
||||
ParameterKind kind;
|
||||
interface Parameter {
|
||||
MemberRef(string member);
|
||||
SystemRef(string system);
|
||||
MemberPrivacyTarget(string target);
|
||||
PrivacyLevel(string level);
|
||||
OpaqueString(string raw);
|
||||
Toggle(boolean toggle);
|
||||
Reset();
|
||||
};
|
||||
dictionary ParsedCommand {
|
||||
string command_ref;
|
||||
|
|
|
|||
|
|
@ -1 +1,32 @@
|
|||
use super::*;
|
||||
|
||||
pub fn cmds() -> impl Iterator<Item = Command> {
|
||||
use Token::*;
|
||||
|
||||
let cfg = ["config", "cfg"];
|
||||
let autoproxy = ["autoproxy", "ap"];
|
||||
|
||||
[
|
||||
command!(
|
||||
[cfg, autoproxy, ["account", "ac"]],
|
||||
"cfg_ap_account_show",
|
||||
"Shows autoproxy status for the account"
|
||||
),
|
||||
command!(
|
||||
[cfg, autoproxy, ["account", "ac"], Toggle("toggle")],
|
||||
"cfg_ap_account_update",
|
||||
"Toggles autoproxy for the account"
|
||||
),
|
||||
command!(
|
||||
[cfg, autoproxy, ["timeout", "tm"]],
|
||||
"cfg_ap_timeout_show",
|
||||
"Shows the autoproxy timeout"
|
||||
),
|
||||
command!(
|
||||
[cfg, autoproxy, ["timeout", "tm"], [Toggle("toggle"), Reset("reset"), FullString("timeout")]],
|
||||
"cfg_ap_timeout_update",
|
||||
"Sets the autoproxy timeout"
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ pub fn cmds() -> impl Iterator<Item = Command> {
|
|||
"member_show",
|
||||
"Shows information about a member"
|
||||
),
|
||||
command!(
|
||||
[member, MemberRef("target"), "soulscream"],
|
||||
"member_soulscream",
|
||||
"todo"
|
||||
),
|
||||
command!(
|
||||
[member, MemberRef("target"), description],
|
||||
"member_desc_show",
|
||||
|
|
|
|||
|
|
@ -36,48 +36,15 @@ pub enum CommandResult {
|
|||
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, Clone)]
|
||||
pub enum Parameter {
|
||||
MemberRef { member: String },
|
||||
SystemRef { system: String },
|
||||
MemberPrivacyTarget { target: String },
|
||||
PrivacyLevel { level: String },
|
||||
OpaqueString { raw: String },
|
||||
Toggle { toggle: bool },
|
||||
Reset,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -111,27 +78,17 @@ fn parse_command(input: String) -> CommandResult {
|
|||
Ok((found_token, arg, new_pos)) => {
|
||||
current_pos = new_pos;
|
||||
if let Token::Flag = found_token {
|
||||
flags.insert(arg.unwrap().into(), None);
|
||||
flags.insert(arg.unwrap().raw.into(), None);
|
||||
// don't try matching flags as tree elements
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(arg) = arg.as_ref() {
|
||||
// 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);
|
||||
if let Some((param_name, param)) = arg.param.as_ref() {
|
||||
params.insert(param_name.to_string(), param.clone());
|
||||
}
|
||||
args.push(arg.to_string());
|
||||
args.push(arg.raw.to_string());
|
||||
}
|
||||
|
||||
if let Some(next_tree) = local_tree.branches.get(&found_token) {
|
||||
|
|
@ -178,7 +135,7 @@ fn next_token(
|
|||
possible_tokens: Vec<Token>,
|
||||
input: SmolStr,
|
||||
current_pos: usize,
|
||||
) -> Result<(Token, Option<SmolStr>, usize), Option<SmolStr>> {
|
||||
) -> Result<(Token, Option<TokenMatchedValue>, usize), Option<SmolStr>> {
|
||||
// get next parameter, matching quotes
|
||||
let param = crate::string::next_param(input.clone(), current_pos);
|
||||
println!("matched: {param:?}\n---");
|
||||
|
|
@ -191,7 +148,10 @@ fn next_token(
|
|||
{
|
||||
return Ok((
|
||||
Token::Flag,
|
||||
Some(value.trim_start_matches('-').into()),
|
||||
Some(TokenMatchedValue {
|
||||
raw: value,
|
||||
param: None,
|
||||
}),
|
||||
new_pos,
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use smol_str::{SmolStr, ToSmolStr};
|
||||
|
||||
use crate::Parameter;
|
||||
|
||||
type ParamName = &'static str;
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
|
|
@ -8,86 +12,180 @@ pub enum Token {
|
|||
// todo: this is likely not the right way to represent this
|
||||
Empty,
|
||||
|
||||
/// A bot-defined value ("member" in `pk;member MyName`)
|
||||
Value(Vec<SmolStr>),
|
||||
/// A command defined by multiple values
|
||||
// todo!
|
||||
MultiValue(Vec<Vec<SmolStr>>),
|
||||
/// multi-token matching
|
||||
Any(Vec<Token>),
|
||||
|
||||
/// A bot-defined command / subcommand (usually) (eg. "member" in `pk;member MyName`)
|
||||
Value(Vec<SmolStr>),
|
||||
|
||||
/// Opaque string (eg. "name" in `pk;member new name`)
|
||||
FullString(ParamName),
|
||||
|
||||
/// Member reference (hid or member name)
|
||||
MemberRef(ParamName),
|
||||
/// todo: doc
|
||||
MemberPrivacyTarget(ParamName),
|
||||
|
||||
/// System reference
|
||||
SystemRef(ParamName),
|
||||
|
||||
/// todo: doc
|
||||
PrivacyLevel(ParamName),
|
||||
|
||||
// currently not included in command definitions
|
||||
/// on, off; yes, no; true, false
|
||||
Toggle(ParamName),
|
||||
|
||||
/// reset, clear, default
|
||||
Reset(ParamName),
|
||||
|
||||
// todo: currently not included in command definitions
|
||||
// todo: flags with values
|
||||
Flag,
|
||||
}
|
||||
|
||||
// #[macro_export]
|
||||
// macro_rules! any {
|
||||
// ($($token:expr),+) => {
|
||||
// Token::Any(vec![$($token.to_token()),+])
|
||||
// };
|
||||
// }
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TokenMatchResult {
|
||||
/// Token did not match.
|
||||
NoMatch,
|
||||
/// Token matched, optionally with a value.
|
||||
Match(Option<SmolStr>),
|
||||
MissingParameter {
|
||||
name: ParamName,
|
||||
},
|
||||
Match(Option<TokenMatchedValue>),
|
||||
/// A required parameter was missing.
|
||||
MissingParameter { name: ParamName },
|
||||
}
|
||||
|
||||
// move this somewhere else
|
||||
const MEMBER_PRIVACY_TARGETS: &[&str] = &["visibility", "name", "todo"];
|
||||
#[derive(Debug)]
|
||||
pub struct TokenMatchedValue {
|
||||
pub raw: SmolStr,
|
||||
pub param: Option<(ParamName, Parameter)>,
|
||||
}
|
||||
|
||||
impl TokenMatchResult {
|
||||
fn new_match(raw: impl Into<SmolStr>) -> Self {
|
||||
Self::Match(Some(TokenMatchedValue {
|
||||
raw: raw.into(),
|
||||
param: None,
|
||||
}))
|
||||
}
|
||||
|
||||
fn new_match_param(raw: impl Into<SmolStr>, param_name: ParamName, param: Parameter) -> Self {
|
||||
Self::Match(Some(TokenMatchedValue {
|
||||
raw: raw.into(),
|
||||
param: Some((param_name, param)),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Token {
|
||||
pub fn try_match(&self, input: Option<SmolStr>) -> TokenMatchResult {
|
||||
// short circuit on empty things
|
||||
if matches!(self, Self::Empty) && input.is_none() {
|
||||
return TokenMatchResult::Match(None);
|
||||
} else if input.is_none() {
|
||||
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,
|
||||
}
|
||||
}
|
||||
use TokenMatchResult::*;
|
||||
|
||||
let input = input.as_ref().map(|s| s.trim()).unwrap();
|
||||
let input = match input {
|
||||
Some(input) => input,
|
||||
None => {
|
||||
// short circuit on:
|
||||
return match self {
|
||||
// empty token
|
||||
Self::Empty => Match(None),
|
||||
// missing paramaters
|
||||
Self::FullString(param_name)
|
||||
| Self::MemberRef(param_name)
|
||||
| Self::MemberPrivacyTarget(param_name)
|
||||
| Self::SystemRef(param_name)
|
||||
| Self::PrivacyLevel(param_name)
|
||||
| Self::Toggle(param_name)
|
||||
| Self::Reset(param_name) => MissingParameter { name: param_name },
|
||||
Self::Any(tokens) => tokens.is_empty().then_some(NoMatch).unwrap_or_else(|| {
|
||||
let mut results = tokens.iter().map(|t| t.try_match(None));
|
||||
results.find(|r| !matches!(r, NoMatch)).unwrap_or(NoMatch)
|
||||
}),
|
||||
// everything else doesnt match if no input anyway
|
||||
Token::Value(_) => NoMatch,
|
||||
Token::Flag => NoMatch,
|
||||
// don't add a _ match here!
|
||||
};
|
||||
}
|
||||
};
|
||||
let input = input.trim();
|
||||
|
||||
// try actually matching stuff
|
||||
match self {
|
||||
Self::Empty => return TokenMatchResult::NoMatch,
|
||||
Self::Flag => unreachable!(), // matched upstream
|
||||
Self::Value(values) if values.iter().any(|v| v.eq(input)) => {
|
||||
return TokenMatchResult::Match(None);
|
||||
}
|
||||
Self::Value(_) => {}
|
||||
Self::MultiValue(_) => todo!(),
|
||||
Self::FullString(_) => return TokenMatchResult::Match(Some(input.into())),
|
||||
Self::SystemRef(_) => return TokenMatchResult::Match(Some(input.into())),
|
||||
Self::MemberRef(_) => return TokenMatchResult::Match(Some(input.into())),
|
||||
Self::MemberPrivacyTarget(_) if MEMBER_PRIVACY_TARGETS.contains(&input) => {
|
||||
return TokenMatchResult::Match(Some(input.into()))
|
||||
}
|
||||
Self::MemberPrivacyTarget(_) => {}
|
||||
Self::PrivacyLevel(_) if input == "public" || input == "private" => {
|
||||
return TokenMatchResult::Match(Some(input.into()))
|
||||
}
|
||||
Self::PrivacyLevel(_) => {}
|
||||
}
|
||||
// note: must not add a _ case to the above match
|
||||
// instead, for conditional matches, also add generic cases with no return
|
||||
Self::Empty => NoMatch,
|
||||
Self::Flag => unreachable!(), // matched upstream (dusk: i don't really like this tbh)
|
||||
Self::Any(tokens) => tokens
|
||||
.iter()
|
||||
.map(|t| t.try_match(Some(input.into())))
|
||||
.find(|r| !matches!(r, NoMatch))
|
||||
.unwrap_or(NoMatch),
|
||||
Self::Value(values) => values
|
||||
.iter()
|
||||
.any(|v| v.eq(input))
|
||||
.then(|| TokenMatchResult::new_match(input))
|
||||
.unwrap_or(NoMatch),
|
||||
Self::FullString(param_name) => TokenMatchResult::new_match_param(
|
||||
input,
|
||||
param_name,
|
||||
Parameter::OpaqueString { raw: input.into() },
|
||||
),
|
||||
Self::SystemRef(param_name) => TokenMatchResult::new_match_param(
|
||||
input,
|
||||
param_name,
|
||||
Parameter::SystemRef {
|
||||
system: input.into(),
|
||||
},
|
||||
),
|
||||
Self::MemberRef(param_name) => TokenMatchResult::new_match_param(
|
||||
input,
|
||||
param_name,
|
||||
Parameter::MemberRef {
|
||||
member: input.into(),
|
||||
},
|
||||
),
|
||||
Self::MemberPrivacyTarget(param_name) => match MemberPrivacyTarget::from_str(input) {
|
||||
Ok(target) => TokenMatchResult::new_match_param(
|
||||
input,
|
||||
param_name,
|
||||
Parameter::MemberPrivacyTarget {
|
||||
target: target.as_ref().into(),
|
||||
},
|
||||
),
|
||||
Err(_) => NoMatch,
|
||||
},
|
||||
Self::PrivacyLevel(param_name) => match PrivacyLevel::from_str(input) {
|
||||
Ok(level) => TokenMatchResult::new_match_param(
|
||||
input,
|
||||
param_name,
|
||||
Parameter::PrivacyLevel {
|
||||
level: level.as_ref().into(),
|
||||
},
|
||||
),
|
||||
Err(_) => NoMatch,
|
||||
},
|
||||
|
||||
return TokenMatchResult::NoMatch;
|
||||
Self::Toggle(param_name) => match Toggle::from_str(input) {
|
||||
Ok(t) => TokenMatchResult::new_match_param(
|
||||
input,
|
||||
param_name,
|
||||
Parameter::Toggle { toggle: t.0 },
|
||||
),
|
||||
Err(_) => NoMatch,
|
||||
},
|
||||
Self::Reset(param_name) => match Reset::from_str(input) {
|
||||
Ok(_) => TokenMatchResult::new_match_param(input, param_name, Parameter::Reset),
|
||||
Err(_) => NoMatch,
|
||||
},
|
||||
// don't add a _ match here!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience trait to convert types into [`Token`]s.
|
||||
pub trait ToToken {
|
||||
fn to_token(&self) -> Token;
|
||||
}
|
||||
|
|
@ -109,3 +207,107 @@ impl ToToken for [&str] {
|
|||
Token::Value(self.into_iter().map(|s| s.to_smolstr()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToToken for [Token] {
|
||||
fn to_token(&self) -> Token {
|
||||
Token::Any(self.into_iter().map(|s| s.clone()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub enum MemberPrivacyTarget {
|
||||
Visibility,
|
||||
Name,
|
||||
// todo
|
||||
}
|
||||
|
||||
impl AsRef<str> for MemberPrivacyTarget {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
Self::Visibility => "visibility",
|
||||
Self::Name => "name",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MemberPrivacyTarget {
|
||||
// todo: figure out how to represent these errors best
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"visibility" => Ok(Self::Visibility),
|
||||
"name" => Ok(Self::Name),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub enum PrivacyLevel {
|
||||
Public,
|
||||
Private,
|
||||
}
|
||||
|
||||
impl AsRef<str> for PrivacyLevel {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
Self::Public => "public",
|
||||
Self::Private => "private",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PrivacyLevel {
|
||||
type Err = (); // todo
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"public" => Ok(Self::Public),
|
||||
"private" => Ok(Self::Private),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub struct Toggle(bool);
|
||||
|
||||
impl AsRef<str> for Toggle {
|
||||
fn as_ref(&self) -> &str {
|
||||
// on / off better than others for docs and stuff?
|
||||
self.0.then_some("on").unwrap_or("off")
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Toggle {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"on" | "yes" | "true" | "enable" | "enabled" => Ok(Self(true)),
|
||||
"off" | "no" | "false" | "disable" | "disabled" => Ok(Self(false)),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub struct Reset;
|
||||
|
||||
impl AsRef<str> for Reset {
|
||||
fn as_ref(&self) -> &str {
|
||||
"reset"
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Reset {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"reset" | "clear" | "default" => Ok(Self),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue