| @@ -1,4 +1,5 @@ | |||||
| using System; | using System; | ||||
| using System.Runtime.CompilerServices; | |||||
| namespace Discord.Interactions | namespace Discord.Interactions | ||||
| { | { | ||||
| @@ -28,6 +29,11 @@ namespace Discord.Interactions | |||||
| /// </summary> | /// </summary> | ||||
| public RunMode RunMode { get; } | public RunMode RunMode { get; } | ||||
| /// <summary> | |||||
| /// Gets or sets whether the <see cref="CustomId"/> should be treated as a raw Regex pattern. | |||||
| /// </summary> | |||||
| public bool TreatAsRegex { get; set; } = false; | |||||
| /// <summary> | /// <summary> | ||||
| /// Create a command for component interaction handling. | /// Create a command for component interaction handling. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -28,6 +28,11 @@ namespace Discord.Interactions | |||||
| /// </summary> | /// </summary> | ||||
| public RunMode RunMode { get; } | public RunMode RunMode { get; } | ||||
| /// <summary> | |||||
| /// Gets or sets whether the <see cref="CustomId"/> should be treated as a raw Regex pattern. | |||||
| /// </summary> | |||||
| public bool TreatAsRegex { get; set; } = false; | |||||
| /// <summary> | /// <summary> | ||||
| /// Create a command for modal interaction handling. | /// Create a command for modal interaction handling. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -35,6 +35,9 @@ namespace Discord.Interactions.Builders | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool IgnoreGroupNames { get; set; } | public bool IgnoreGroupNames { get; set; } | ||||
| /// <inheritdoc/> | |||||
| public bool TreatNameAsRegex { get; set; } | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public RunMode RunMode { get; set; } | public RunMode RunMode { get; set; } | ||||
| @@ -117,6 +120,19 @@ namespace Discord.Interactions.Builders | |||||
| return Instance; | return Instance; | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Sets <see cref="TreatNameAsRegex"/>. | |||||
| /// </summary> | |||||
| /// <param name="value">New value of the <see cref="TreatNameAsRegex"/>.</param> | |||||
| /// <returns> | |||||
| /// The builder instance. | |||||
| /// </returns> | |||||
| public TBuilder WithNameAsRegex (bool value) | |||||
| { | |||||
| TreatNameAsRegex = value; | |||||
| return Instance; | |||||
| } | |||||
| /// <summary> | /// <summary> | ||||
| /// Adds parameter builders to <see cref="Parameters"/>. | /// Adds parameter builders to <see cref="Parameters"/>. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -163,6 +179,10 @@ namespace Discord.Interactions.Builders | |||||
| ICommandBuilder ICommandBuilder.SetRunMode (RunMode runMode) => | ICommandBuilder ICommandBuilder.SetRunMode (RunMode runMode) => | ||||
| SetRunMode(runMode); | SetRunMode(runMode); | ||||
| /// <inheritdoc/> | |||||
| ICommandBuilder ICommandBuilder.WithNameAsRegex(bool value) => | |||||
| WithNameAsRegex(value); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| ICommandBuilder ICommandBuilder.AddParameters (params IParameterBuilder[] parameters) => | ICommandBuilder ICommandBuilder.AddParameters (params IParameterBuilder[] parameters) => | ||||
| AddParameters(parameters as TParamBuilder); | AddParameters(parameters as TParamBuilder); | ||||
| @@ -34,6 +34,11 @@ namespace Discord.Interactions.Builders | |||||
| /// </summary> | /// </summary> | ||||
| bool IgnoreGroupNames { get; set; } | bool IgnoreGroupNames { get; set; } | ||||
| /// <summary> | |||||
| /// Gets or sets whether the <see cref="Name"/> should be directly used as a Regex pattern. | |||||
| /// </summary> | |||||
| bool TreatNameAsRegex { get; set; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets or sets the run mode this command gets executed with. | /// Gets or sets the run mode this command gets executed with. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -90,6 +95,15 @@ namespace Discord.Interactions.Builders | |||||
| /// </returns> | /// </returns> | ||||
| ICommandBuilder SetRunMode (RunMode runMode); | ICommandBuilder SetRunMode (RunMode runMode); | ||||
| /// <summary> | |||||
| /// Sets <see cref="TreatNameAsRegex"/>. | |||||
| /// </summary> | |||||
| /// <param name="value">New value of the <see cref="TreatNameAsRegex"/>.</param> | |||||
| /// <returns> | |||||
| /// The builder instance. | |||||
| /// </returns> | |||||
| ICommandBuilder WithNameAsRegex(bool value); | |||||
| /// <summary> | /// <summary> | ||||
| /// Adds parameter builders to <see cref="Parameters"/>. | /// Adds parameter builders to <see cref="Parameters"/>. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -274,6 +274,7 @@ namespace Discord.Interactions.Builders | |||||
| builder.Name = interaction.CustomId; | builder.Name = interaction.CustomId; | ||||
| builder.RunMode = interaction.RunMode; | builder.RunMode = interaction.RunMode; | ||||
| builder.IgnoreGroupNames = interaction.IgnoreGroupNames; | builder.IgnoreGroupNames = interaction.IgnoreGroupNames; | ||||
| builder.TreatNameAsRegex = interaction.TreatAsRegex; | |||||
| } | } | ||||
| break; | break; | ||||
| case PreconditionAttribute precondition: | case PreconditionAttribute precondition: | ||||
| @@ -287,7 +288,7 @@ namespace Discord.Interactions.Builders | |||||
| var parameters = methodInfo.GetParameters(); | var parameters = methodInfo.GetParameters(); | ||||
| var wildCardCount = Regex.Matches(Regex.Escape(builder.Name), Regex.Escape(commandService._wildCardExp)).Count; | |||||
| var wildCardCount = RegexUtils.GetWildCardCount(builder.Name, commandService._wildCardExp); | |||||
| foreach (var parameter in parameters) | foreach (var parameter in parameters) | ||||
| builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount)); | builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount)); | ||||
| @@ -355,6 +356,7 @@ namespace Discord.Interactions.Builders | |||||
| builder.Name = modal.CustomId; | builder.Name = modal.CustomId; | ||||
| builder.RunMode = modal.RunMode; | builder.RunMode = modal.RunMode; | ||||
| builder.IgnoreGroupNames = modal.IgnoreGroupNames; | builder.IgnoreGroupNames = modal.IgnoreGroupNames; | ||||
| builder.TreatNameAsRegex = modal.TreatAsRegex; | |||||
| } | } | ||||
| break; | break; | ||||
| case PreconditionAttribute precondition: | case PreconditionAttribute precondition: | ||||
| @@ -66,6 +66,8 @@ namespace Discord.Interactions | |||||
| /// <inheritdoc cref="ICommandInfo.Parameters"/> | /// <inheritdoc cref="ICommandInfo.Parameters"/> | ||||
| public abstract IReadOnlyList<TParameter> Parameters { get; } | public abstract IReadOnlyList<TParameter> Parameters { get; } | ||||
| public bool TreatNameAsRegex { get; } | |||||
| internal CommandInfo(Builders.ICommandBuilder builder, ModuleInfo module, InteractionService commandService) | internal CommandInfo(Builders.ICommandBuilder builder, ModuleInfo module, InteractionService commandService) | ||||
| { | { | ||||
| CommandService = commandService; | CommandService = commandService; | ||||
| @@ -78,6 +80,7 @@ namespace Discord.Interactions | |||||
| RunMode = builder.RunMode != RunMode.Default ? builder.RunMode : commandService._runMode; | RunMode = builder.RunMode != RunMode.Default ? builder.RunMode : commandService._runMode; | ||||
| Attributes = builder.Attributes.ToImmutableArray(); | Attributes = builder.Attributes.ToImmutableArray(); | ||||
| Preconditions = builder.Preconditions.ToImmutableArray(); | Preconditions = builder.Preconditions.ToImmutableArray(); | ||||
| TreatNameAsRegex = builder.TreatNameAsRegex && SupportsWildCards; | |||||
| _action = builder.Callback; | _action = builder.Callback; | ||||
| _groupedPreconditions = builder.Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal); | _groupedPreconditions = builder.Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal); | ||||
| @@ -65,6 +65,8 @@ namespace Discord.Interactions | |||||
| /// </summary> | /// </summary> | ||||
| IReadOnlyCollection<IParameterInfo> Parameters { get; } | IReadOnlyCollection<IParameterInfo> Parameters { get; } | ||||
| bool TreatNameAsRegex { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Executes the command with the provided context. | /// Executes the command with the provided context. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -2,14 +2,13 @@ using System; | |||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Text; | |||||
| using System.Text.RegularExpressions; | using System.Text.RegularExpressions; | ||||
| namespace Discord.Interactions | namespace Discord.Interactions | ||||
| { | { | ||||
| internal class CommandMapNode<T> where T : class, ICommandInfo | internal class CommandMapNode<T> where T : class, ICommandInfo | ||||
| { | |||||
| private const string RegexWildCardExp = "(\\S+)?"; | |||||
| { | |||||
| private readonly string _wildCardStr = "*"; | private readonly string _wildCardStr = "*"; | ||||
| private readonly ConcurrentDictionary<string, CommandMapNode<T>> _nodes; | private readonly ConcurrentDictionary<string, CommandMapNode<T>> _nodes; | ||||
| private readonly ConcurrentDictionary<string, T> _commands; | private readonly ConcurrentDictionary<string, T> _commands; | ||||
| @@ -35,10 +34,8 @@ namespace Discord.Interactions | |||||
| { | { | ||||
| if (keywords.Count == index + 1) | if (keywords.Count == index + 1) | ||||
| { | { | ||||
| if (commandInfo.SupportsWildCards && commandInfo.Name.Contains(_wildCardStr)) | |||||
| if (commandInfo.SupportsWildCards && RegexUtils.TryBuildRegexPattern(commandInfo, _wildCardStr, out var patternStr)) | |||||
| { | { | ||||
| var escapedStr = RegexUtils.EscapeExcluding(commandInfo.Name, _wildCardStr.ToArray()); | |||||
| var patternStr = "\\A" + escapedStr.Replace(_wildCardStr, RegexWildCardExp) + "\\Z"; | |||||
| var regex = new Regex(patternStr, RegexOptions.Singleline | RegexOptions.Compiled); | var regex = new Regex(patternStr, RegexOptions.Singleline | RegexOptions.Compiled); | ||||
| if (!_wildCardCommands.TryAdd(regex, commandInfo)) | if (!_wildCardCommands.TryAdd(regex, commandInfo)) | ||||
| @@ -1,3 +1,4 @@ | |||||
| using Discord.Interactions; | |||||
| using System; | using System; | ||||
| using System.Linq; | using System.Linq; | ||||
| @@ -81,5 +82,37 @@ namespace System.Text.RegularExpressions | |||||
| { | { | ||||
| return (ch <= '|' && _category[ch] >= E); | return (ch <= '|' && _category[ch] >= E); | ||||
| } | } | ||||
| internal static int GetWildCardCount(string input, string wildCardExpression) | |||||
| { | |||||
| var escapedWildCard = Regex.Escape(wildCardExpression); | |||||
| var match = Regex.Matches(input, $@"(?<!\\){escapedWildCard}|(?<!\\){{[0-9]+(?:,[0-9]*)?(?<!\\)}}"); | |||||
| return match.Count; | |||||
| } | |||||
| internal static bool TryBuildRegexPattern<T>(T commandInfo, string wildCardStr, out string pattern) where T: class, ICommandInfo | |||||
| { | |||||
| if (commandInfo.TreatNameAsRegex) | |||||
| { | |||||
| pattern = commandInfo.Name; | |||||
| return true; | |||||
| } | |||||
| if (GetWildCardCount(commandInfo.Name, wildCardStr) == 0) | |||||
| { | |||||
| pattern = null; | |||||
| return false; | |||||
| } | |||||
| var escapedWildCard = Regex.Escape(wildCardStr); | |||||
| var unquantified = Regex.Replace(commandInfo.Name, $@"(?<!\\){escapedWildCard}(?<delimiter>[^{escapedWildCard}]?)", | |||||
| @"([^\n\t${delimiter}]+)${delimiter}"); | |||||
| var quantified = Regex.Replace(unquantified, $@"(?<!\\){{(?<start>[0-9]+)(?<end>,[0-9]*)?(?<!\\)}}(?<delimiter>[^{escapedWildCard}]?)", | |||||
| @"([^\n\t${delimiter}]{${start}${end}})${delimiter}"); | |||||
| pattern = "\\A" + quantified + "\\Z"; | |||||
| return true; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||