@@ -1,4 +1,5 @@ | |||
using System; | |||
using System.Runtime.CompilerServices; | |||
namespace Discord.Interactions | |||
{ | |||
@@ -29,6 +30,11 @@ namespace Discord.Interactions | |||
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> | |||
/// Create a command for component interaction handling. | |||
/// </summary> | |||
/// <param name="customId">String to compare the Message Component CustomIDs with.</param> | |||
@@ -29,6 +29,11 @@ namespace Discord.Interactions | |||
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> | |||
/// Create a command for modal interaction handling. | |||
/// </summary> | |||
/// <param name="customId">String to compare the modal CustomIDs with.</param> | |||
@@ -36,6 +36,9 @@ namespace Discord.Interactions.Builders | |||
public bool IgnoreGroupNames { get; set; } | |||
/// <inheritdoc/> | |||
public bool TreatNameAsRegex { get; set; } | |||
/// <inheritdoc/> | |||
public RunMode RunMode { get; set; } | |||
/// <inheritdoc/> | |||
@@ -118,6 +121,19 @@ namespace Discord.Interactions.Builders | |||
} | |||
/// <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> | |||
/// Adds parameter builders to <see cref="Parameters"/>. | |||
/// </summary> | |||
/// <param name="parameters">New parameter builders to be added to <see cref="Parameters"/>.</param> | |||
@@ -164,6 +180,10 @@ namespace Discord.Interactions.Builders | |||
SetRunMode(runMode); | |||
/// <inheritdoc/> | |||
ICommandBuilder ICommandBuilder.WithNameAsRegex(bool value) => | |||
WithNameAsRegex(value); | |||
/// <inheritdoc/> | |||
ICommandBuilder ICommandBuilder.AddParameters (params IParameterBuilder[] parameters) => | |||
AddParameters(parameters as TParamBuilder); | |||
@@ -35,6 +35,11 @@ namespace Discord.Interactions.Builders | |||
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> | |||
/// Gets or sets the run mode this command gets executed with. | |||
/// </summary> | |||
RunMode RunMode { get; set; } | |||
@@ -91,6 +96,15 @@ namespace Discord.Interactions.Builders | |||
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> | |||
/// Adds parameter builders to <see cref="Parameters"/>. | |||
/// </summary> | |||
/// <param name="parameters">New parameter builders to be added to <see cref="Parameters"/>.</param> | |||
@@ -274,6 +274,7 @@ namespace Discord.Interactions.Builders | |||
builder.Name = interaction.CustomId; | |||
builder.RunMode = interaction.RunMode; | |||
builder.IgnoreGroupNames = interaction.IgnoreGroupNames; | |||
builder.TreatNameAsRegex = interaction.TreatAsRegex; | |||
} | |||
break; | |||
case PreconditionAttribute precondition: | |||
@@ -287,7 +288,7 @@ namespace Discord.Interactions.Builders | |||
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) | |||
builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount)); | |||
@@ -355,6 +356,7 @@ namespace Discord.Interactions.Builders | |||
builder.Name = modal.CustomId; | |||
builder.RunMode = modal.RunMode; | |||
builder.IgnoreGroupNames = modal.IgnoreGroupNames; | |||
builder.TreatNameAsRegex = modal.TreatAsRegex; | |||
} | |||
break; | |||
case PreconditionAttribute precondition: | |||
@@ -66,6 +66,8 @@ namespace Discord.Interactions | |||
/// <inheritdoc cref="ICommandInfo.Parameters"/> | |||
public abstract IReadOnlyList<TParameter> Parameters { get; } | |||
public bool TreatNameAsRegex { get; } | |||
internal CommandInfo(Builders.ICommandBuilder builder, ModuleInfo module, InteractionService commandService) | |||
{ | |||
CommandService = commandService; | |||
@@ -78,6 +80,7 @@ namespace Discord.Interactions | |||
RunMode = builder.RunMode != RunMode.Default ? builder.RunMode : commandService._runMode; | |||
Attributes = builder.Attributes.ToImmutableArray(); | |||
Preconditions = builder.Preconditions.ToImmutableArray(); | |||
TreatNameAsRegex = builder.TreatNameAsRegex && SupportsWildCards; | |||
_action = builder.Callback; | |||
_groupedPreconditions = builder.Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal); | |||
@@ -65,6 +65,8 @@ namespace Discord.Interactions | |||
/// </summary> | |||
IReadOnlyCollection<IParameterInfo> Parameters { get; } | |||
bool TreatNameAsRegex { get; } | |||
/// <summary> | |||
/// Executes the command with the provided context. | |||
/// </summary> | |||
@@ -2,14 +2,13 @@ using System; | |||
using System.Collections.Concurrent; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Text.RegularExpressions; | |||
namespace Discord.Interactions | |||
{ | |||
internal class CommandMapNode<T> where T : class, ICommandInfo | |||
{ | |||
private const string RegexWildCardExp = "(\\S+)?"; | |||
{ | |||
private readonly string _wildCardStr = "*"; | |||
private readonly ConcurrentDictionary<string, CommandMapNode<T>> _nodes; | |||
private readonly ConcurrentDictionary<string, T> _commands; | |||
@@ -35,10 +34,8 @@ namespace Discord.Interactions | |||
{ | |||
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); | |||
if (!_wildCardCommands.TryAdd(regex, commandInfo)) | |||
@@ -1,3 +1,4 @@ | |||
using Discord.Interactions; | |||
using System; | |||
using System.Linq; | |||
@@ -81,5 +82,37 @@ namespace System.Text.RegularExpressions | |||
{ | |||
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; | |||
} | |||
} | |||
} |