diff --git a/src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs
index e42dfabce..dd857498c 100644
--- a/src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs
@@ -5,7 +5,7 @@ namespace Discord.Interactions.Builders
///
/// Represents a builder for creating .
///
- public sealed class ComponentCommandBuilder : CommandBuilder
+ public sealed class ComponentCommandBuilder : CommandBuilder
{
protected override ComponentCommandBuilder Instance => this;
@@ -26,9 +26,9 @@ namespace Discord.Interactions.Builders
///
/// The builder instance.
///
- public override ComponentCommandBuilder AddParameter (Action configure)
+ public override ComponentCommandBuilder AddParameter (Action configure)
{
- var parameter = new CommandParameterBuilder(this);
+ var parameter = new ComponentCommandParameterBuilder(this);
configure(parameter);
AddParameters(parameter);
return this;
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs
index 37cd861c4..ad2f07c73 100644
--- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs
@@ -38,6 +38,11 @@ namespace Discord.Interactions.Builders
///
Type Type { get; }
+ ///
+ /// Get the assigned to this input.
+ ///
+ ComponentTypeConverter TypeConverter { get; }
+
///
/// Gets the default value of this input component.
///
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs
index c2b9b0645..7d1d96712 100644
--- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs
@@ -33,6 +33,9 @@ namespace Discord.Interactions.Builders
///
public Type Type { get; private set; }
+ ///
+ public ComponentTypeConverter TypeConverter { get; private set; }
+
///
public object DefaultValue { get; set; }
@@ -111,6 +114,7 @@ namespace Discord.Interactions.Builders
public TBuilder WithType(Type type)
{
Type = type;
+ TypeConverter = Modal._interactionService.GetComponentTypeConverter(type);
return Instance;
}
diff --git a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs
index e120e78be..fc1dbdc0e 100644
--- a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs
@@ -9,6 +9,7 @@ namespace Discord.Interactions.Builders
///
public class ModalBuilder
{
+ internal readonly InteractionService _interactionService;
internal readonly List _components;
///
@@ -31,11 +32,12 @@ namespace Discord.Interactions.Builders
///
public IReadOnlyCollection Components => _components;
- internal ModalBuilder(Type type)
+ internal ModalBuilder(Type type, InteractionService interactionService)
{
if (!typeof(IModal).IsAssignableFrom(type))
throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type));
+ _interactionService = interactionService;
_components = new();
}
@@ -43,7 +45,7 @@ namespace Discord.Interactions.Builders
/// Initializes a new
///
/// The initialization delegate for this modal.
- public ModalBuilder(Type type, ModalInitializer modalInitializer) : this(type)
+ public ModalBuilder(Type type, ModalInitializer modalInitializer, InteractionService interactionService) : this(type, interactionService)
{
ModalInitializer = modalInitializer;
}
diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
index 88a34f3b2..b2317d1f3 100644
--- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
@@ -231,9 +231,6 @@ namespace Discord.Interactions.Builders
private static void BuildComponentCommand (ComponentCommandBuilder builder, Func createInstance, MethodInfo methodInfo,
InteractionService commandService, IServiceProvider services)
{
- if (!methodInfo.GetParameters().All(x => x.ParameterType == typeof(string) || x.ParameterType == typeof(string[])))
- throw new InvalidOperationException($"Interaction method parameters all must be types of {typeof(string).Name} or {typeof(string[]).Name}");
-
var attributes = methodInfo.GetCustomAttributes();
builder.MethodName = methodInfo.Name;
@@ -260,8 +257,10 @@ namespace Discord.Interactions.Builders
var parameters = methodInfo.GetParameters();
+ var wildCardCount = Regex.Matches(Regex.Escape(builder.Name), Regex.Escape(commandService._wildCardExp)).Count;
+
foreach (var parameter in parameters)
- builder.AddParameter(x => BuildParameter(x, parameter));
+ builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount));
builder.Callback = CreateCallback(createInstance, methodInfo, commandService);
}
@@ -310,8 +309,8 @@ namespace Discord.Interactions.Builders
if (parameters.Count(x => typeof(IModal).IsAssignableFrom(x.ParameterType)) > 1)
throw new InvalidOperationException($"A modal command can only have one {nameof(IModal)} parameter.");
- if (!parameters.All(x => x.ParameterType == typeof(string) || typeof(IModal).IsAssignableFrom(x.ParameterType)))
- throw new InvalidOperationException($"All parameters of a modal command must be either a string or an implementation of {nameof(IModal)}");
+ if (!typeof(IModal).IsAssignableFrom(parameters.Last().ParameterType))
+ throw new InvalidOperationException($"Last parameter of a modal command must be an implementation of {nameof(IModal)}");
var attributes = methodInfo.GetCustomAttributes();
@@ -464,6 +463,12 @@ namespace Discord.Interactions.Builders
builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower();
}
+ private static void BuildComponentParameter(ComponentCommandParameterBuilder builder, ParameterInfo paramInfo, bool isComponentParam)
+ {
+ builder.SetIsRouteSegment(!isComponentParam);
+ BuildParameter(builder, paramInfo);
+ }
+
private static void BuildParameter (ParameterBuilder builder, ParameterInfo paramInfo)
where TInfo : class, IParameterInfo
where TBuilder : ParameterBuilder
@@ -495,7 +500,7 @@ namespace Discord.Interactions.Builders
#endregion
#region Modals
- public static ModalInfo BuildModalInfo(Type modalType)
+ public static ModalInfo BuildModalInfo(Type modalType, InteractionService interactionService)
{
if (!typeof(IModal).IsAssignableFrom(modalType))
throw new InvalidOperationException($"{modalType.FullName} isn't an implementation of {typeof(IModal).FullName}");
@@ -504,7 +509,7 @@ namespace Discord.Interactions.Builders
try
{
- var builder = new ModalBuilder(modalType)
+ var builder = new ModalBuilder(modalType, interactionService)
{
Title = instance.Title
};
diff --git a/src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs b/src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs
new file mode 100644
index 000000000..d9f1463c3
--- /dev/null
+++ b/src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs
@@ -0,0 +1,77 @@
+using System;
+
+namespace Discord.Interactions.Builders
+{
+ ///
+ /// Represents a builder for creating .
+ ///
+ public class ComponentCommandParameterBuilder : ParameterBuilder
+ {
+ ///
+ /// Get the assigned to this parameter, if is .
+ ///
+ public ComponentTypeConverter TypeConverter { get; private set; }
+
+ ///
+ /// Get the assigned to this parameter, if is .
+ ///
+ public TypeReader TypeReader { get; private set; }
+
+ ///
+ /// Gets whether this parameter is a CustomId segment or a Component value parameter.
+ ///
+ public bool IsRouteSegmentParameter { get; private set; }
+
+ ///
+ protected override ComponentCommandParameterBuilder Instance => this;
+
+ internal ComponentCommandParameterBuilder(ICommandBuilder command) : base(command) { }
+
+ ///
+ /// Initializes a new .
+ ///
+ /// Parent command of this parameter.
+ /// Name of this command.
+ /// Type of this parameter.
+ public ComponentCommandParameterBuilder(ICommandBuilder command, string name, Type type) : base(command, name, type) { }
+
+ ///
+ public override ComponentCommandParameterBuilder SetParameterType(Type type) => SetParameterType(type, null);
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ /// Service container to be used to resolve the dependencies of this parameters .
+ ///
+ /// The builder instance.
+ ///
+ public ComponentCommandParameterBuilder SetParameterType(Type type, IServiceProvider services)
+ {
+ base.SetParameterType(type);
+
+ if (IsRouteSegmentParameter)
+ TypeReader = Command.Module.InteractionService.GetTypeReader(type);
+ else
+ TypeConverter = Command.Module.InteractionService.GetComponentTypeConverter(ParameterType, services);
+
+ return this;
+ }
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ public ComponentCommandParameterBuilder SetIsRouteSegment(bool isRouteSegment)
+ {
+ IsRouteSegmentParameter = isRouteSegment;
+ return this;
+ }
+
+ internal override ComponentCommandParameterInfo Build(ICommandInfo command)
+ => new(this, command);
+ }
+}
diff --git a/src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs b/src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs
index a0315e1ea..8cb9b3ab9 100644
--- a/src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs
@@ -20,6 +20,11 @@ namespace Discord.Interactions.Builders
///
public bool IsModalParameter => Modal is not null;
+ ///
+ /// Gets the assigned to this parameter, if is .
+ ///
+ public TypeReader TypeReader { get; private set; }
+
internal ModalCommandParameterBuilder(ICommandBuilder command) : base(command) { }
///
@@ -34,7 +39,9 @@ namespace Discord.Interactions.Builders
public override ModalCommandParameterBuilder SetParameterType(Type type)
{
if (typeof(IModal).IsAssignableFrom(type))
- Modal = ModalUtils.GetOrAdd(type);
+ Modal = ModalUtils.GetOrAdd(type, Command.Module.InteractionService);
+ else
+ TypeReader = Command.Module.InteractionService.GetTypeReader(type);
return base.SetParameterType(type);
}
diff --git a/src/Discord.Net.Interactions/Entities/ITypeConverter.cs b/src/Discord.Net.Interactions/Entities/ITypeConverter.cs
new file mode 100644
index 000000000..c692b29cb
--- /dev/null
+++ b/src/Discord.Net.Interactions/Entities/ITypeConverter.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Discord.Interactions
+{
+ internal interface ITypeConverter
+ {
+ public bool CanConvertTo(Type type);
+
+ public Task ReadAsync(IInteractionContext context, T option, IServiceProvider services);
+ }
+}
diff --git a/src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs
index 712b058a3..9e30c55f4 100644
--- a/src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs
+++ b/src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs
@@ -41,14 +41,7 @@ namespace Discord.Interactions
if (context.Interaction is not IAutocompleteInteraction)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Autocomplete Interaction");
- try
- {
- return await RunAsync(context, Array.Empty
public Type Type { get; }
+ ///
+ /// Gets the assigned to this component.
+ ///
+ public ComponentTypeConverter TypeConverter { get; }
+
///
/// Gets the default value of this component.
///
@@ -57,6 +62,7 @@ namespace Discord.Interactions
IsRequired = builder.IsRequired;
ComponentType = builder.ComponentType;
Type = builder.Type;
+ TypeConverter = builder.TypeConverter;
DefaultValue = builder.DefaultValue;
Attributes = builder.Attributes.ToImmutableArray();
}
diff --git a/src/Discord.Net.Interactions/Info/ModalInfo.cs b/src/Discord.Net.Interactions/Info/ModalInfo.cs
index edc31373e..5130c26a1 100644
--- a/src/Discord.Net.Interactions/Info/ModalInfo.cs
+++ b/src/Discord.Net.Interactions/Info/ModalInfo.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
+using System.Threading.Tasks;
namespace Discord.Interactions
{
@@ -19,6 +20,7 @@ namespace Discord.Interactions
///
public class ModalInfo
{
+ internal readonly InteractionService _interactionService;
internal readonly ModalInitializer _initializer;
///
@@ -53,16 +55,18 @@ namespace Discord.Interactions
TextComponents = Components.OfType().ToImmutableArray();
+ _interactionService = builder._interactionService;
_initializer = builder.ModalInitializer;
}
///
/// Creates an and fills it with provided message components.
///
- /// that will be injected into the modal.
+ /// that will be injected into the modal.
///
/// A filled with the provided components.
///
+ [Obsolete("This method is no longer supported with the introduction of Component TypeConverters, please use the CreateModalAsync method.")]
public IModal CreateModal(IModalInteraction modalInteraction, bool throwOnMissingField = false)
{
var args = new object[Components.Count];
@@ -86,5 +90,50 @@ namespace Discord.Interactions
return _initializer(args);
}
+
+ ///
+ /// Creates an and fills it with provided message components.
+ ///
+ /// Context of the that will be injected into the modal.
+ /// Services to be passed onto the s of the modal fiels.
+ /// Wheter or not this method should exit on encountering a missing modal field.
+ ///
+ /// A if a type conversion has failed, else a .
+ ///
+ public async Task CreateModalAsync(IInteractionContext context, IServiceProvider services = null, bool throwOnMissingField = false)
+ {
+ if (context.Interaction is not IModalInteraction modalInteraction)
+ return ParseResult.FromError(InteractionCommandError.Unsuccessful, "Provided context doesn't belong to a Modal Interaction.");
+
+ services ??= EmptyServiceProvider.Instance;
+
+ var args = new object[Components.Count];
+ var components = modalInteraction.Data.Components.ToList();
+
+ for (var i = 0; i < Components.Count; i++)
+ {
+ var input = Components.ElementAt(i);
+ var component = components.Find(x => x.CustomId == input.CustomId);
+
+ if (component is null)
+ {
+ if (!throwOnMissingField)
+ args[i] = input.DefaultValue;
+ else
+ return ParseResult.FromError(InteractionCommandError.BadArgs, $"Modal interaction is missing the required field: {input.CustomId}");
+ }
+ else
+ {
+ var readResult = await input.TypeConverter.ReadAsync(context, component, services).ConfigureAwait(false);
+
+ if (!readResult.IsSuccess)
+ return readResult;
+
+ args[i] = readResult.Value;
+ }
+ }
+
+ return ParseResult.FromSuccess(_initializer(args));
+ }
}
}
diff --git a/src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs b/src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs
new file mode 100644
index 000000000..36b75ddb7
--- /dev/null
+++ b/src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs
@@ -0,0 +1,34 @@
+using Discord.Interactions.Builders;
+
+namespace Discord.Interactions
+{
+ ///
+ /// Represents the parameter info class for commands.
+ ///
+ public class ComponentCommandParameterInfo : CommandParameterInfo
+ {
+ ///
+ /// Gets the that will be used to convert a message component value into
+ /// , if is false.
+ ///
+ public ComponentTypeConverter TypeConverter { get; }
+
+ ///
+ /// Gets the that will be used to convert a CustomId segment value into
+ /// , if is .
+ ///
+ public TypeReader TypeReader { get; }
+
+ ///
+ /// Gets whether this parameter is a CustomId segment or a component value parameter.
+ ///
+ public bool IsRouteSegmentParameter { get; }
+
+ internal ComponentCommandParameterInfo(ComponentCommandParameterBuilder builder, ICommandInfo command) : base(builder, command)
+ {
+ TypeConverter = builder.TypeConverter;
+ TypeReader = builder.TypeReader;
+ IsRouteSegmentParameter = builder.IsRouteSegmentParameter;
+ }
+ }
+}
diff --git a/src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs b/src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs
index 28162e109..cafb0b7f5 100644
--- a/src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs
+++ b/src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs
@@ -15,7 +15,12 @@ namespace Discord.Interactions
///
/// Gets whether this parameter is an
///
- public bool IsModalParameter => Modal is not null;
+ public bool IsModalParameter { get; }
+
+ ///
+ /// Gets the assigned to this parameter, if is .
+ ///
+ public TypeReader TypeReader { get; }
///
public new ModalCommandInfo Command => base.Command as ModalCommandInfo;
@@ -23,6 +28,8 @@ namespace Discord.Interactions
internal ModalCommandParameterInfo(ModalCommandParameterBuilder builder, ICommandInfo command) : base(builder, command)
{
Modal = builder.Modal;
+ IsModalParameter = builder.IsModalParameter;
+ TypeReader = builder.TypeReader;
}
}
}
diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs
index bf56eddc5..927e39735 100644
--- a/src/Discord.Net.Interactions/InteractionService.cs
+++ b/src/Discord.Net.Interactions/InteractionService.cs
@@ -3,6 +3,7 @@ using Discord.Logging;
using Discord.Rest;
using Discord.WebSocket;
using System;
+using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -66,8 +67,9 @@ namespace Discord.Interactions
private readonly CommandMap _autocompleteCommandMap;
private readonly CommandMap _modalCommandMap;
private readonly HashSet _moduleDefs;
- private readonly ConcurrentDictionary _typeConverters;
- private readonly ConcurrentDictionary _genericTypeConverters;
+ private readonly TypeMap _typeConverterMap;
+ private readonly TypeMap _compTypeConverterMap;
+ private readonly TypeMap _typeReaderMap;
private readonly ConcurrentDictionary _autocompleteHandlers = new();
private readonly ConcurrentDictionary _modalInfos = new();
private readonly SemaphoreSlim _lock;
@@ -179,22 +181,38 @@ namespace Discord.Interactions
_autoServiceScopes = config.AutoServiceScopes;
_restResponseCallback = config.RestResponseCallback;
- _genericTypeConverters = new ConcurrentDictionary
- {
- [typeof(IChannel)] = typeof(DefaultChannelConverter<>),
- [typeof(IRole)] = typeof(DefaultRoleConverter<>),
- [typeof(IAttachment)] = typeof(DefaultAttachmentConverter<>),
- [typeof(IUser)] = typeof(DefaultUserConverter<>),
- [typeof(IMentionable)] = typeof(DefaultMentionableConverter<>),
- [typeof(IConvertible)] = typeof(DefaultValueConverter<>),
- [typeof(Enum)] = typeof(EnumConverter<>),
- [typeof(Nullable<>)] = typeof(NullableConverter<>),
- };
+ _typeConverterMap = new TypeMap(this, new ConcurrentDictionary
+ {
+ [typeof(TimeSpan)] = new TimeSpanConverter()
+ }, new ConcurrentDictionary
+ {
+ [typeof(IChannel)] = typeof(DefaultChannelConverter<>),
+ [typeof(IRole)] = typeof(DefaultRoleConverter<>),
+ [typeof(IAttachment)] = typeof(DefaultAttachmentConverter<>),
+ [typeof(IUser)] = typeof(DefaultUserConverter<>),
+ [typeof(IMentionable)] = typeof(DefaultMentionableConverter<>),
+ [typeof(IConvertible)] = typeof(DefaultValueConverter<>),
+ [typeof(Enum)] = typeof(EnumConverter<>),
+ [typeof(Nullable<>)] = typeof(NullableConverter<>)
+ });
+
+ _compTypeConverterMap = new TypeMap(this, new ConcurrentDictionary(),
+ new ConcurrentDictionary
+ {
+ [typeof(Array)] = typeof(DefaultArrayComponentConverter<>),
+ [typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>)
+ });
- _typeConverters = new ConcurrentDictionary
- {
- [typeof(TimeSpan)] = new TimeSpanConverter()
- };
+ _typeReaderMap = new TypeMap(this, new ConcurrentDictionary(),
+ new ConcurrentDictionary
+ {
+ [typeof(IChannel)] = typeof(DefaultChannelReader<>),
+ [typeof(IRole)] = typeof(DefaultRoleReader<>),
+ [typeof(IUser)] = typeof(DefaultUserReader<>),
+ [typeof(IMessage)] = typeof(DefaultMessageReader<>),
+ [typeof(IConvertible)] = typeof(DefaultValueReader<>),
+ [typeof(Enum)] = typeof(EnumReader<>)
+ });
}
///
@@ -293,7 +311,7 @@ namespace Discord.Interactions
public async Task AddModuleAsync (Type type, IServiceProvider services)
{
if (!typeof(IInteractionModuleBase).IsAssignableFrom(type))
- throw new ArgumentException("Type parameter must be a type of Slash Module", "T");
+ throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(type));
services ??= EmptyServiceProvider.Instance;
@@ -326,7 +344,7 @@ namespace Discord.Interactions
}
///
- /// Register Application Commands from and to a guild.
+ /// Register Application Commands from and to a guild.
///
/// Id of the target guild.
/// If , this operation will not delete the commands that are missing from .
@@ -422,7 +440,7 @@ namespace Discord.Interactions
}
///
- /// Register Application Commands from modules provided in to a guild.
+ /// Register Application Commands from modules provided in to a guild.
///
/// The target guild.
/// Modules to be registered to Discord.
@@ -449,7 +467,7 @@ namespace Discord.Interactions
}
///
- /// Register Application Commands from modules provided in as global commands.
+ /// Register Application Commands from modules provided in as global commands.
///
/// Modules to be registered to Discord.
///
@@ -677,7 +695,7 @@ namespace Discord.Interactions
public async Task ExecuteCommandAsync (IInteractionContext context, IServiceProvider services)
{
var interaction = context.Interaction;
-
+
return interaction switch
{
ISlashCommandInteraction slashCommand => await ExecuteSlashCommandAsync(context, slashCommand, services).ConfigureAwait(false),
@@ -781,47 +799,24 @@ namespace Discord.Interactions
return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false);
}
- internal TypeConverter GetTypeConverter (Type type, IServiceProvider services = null)
- {
- if (_typeConverters.TryGetValue(type, out var specific))
- return specific;
- else if (_genericTypeConverters.Any(x => x.Key.IsAssignableFrom(type)
- || (x.Key.IsGenericTypeDefinition && type.IsGenericType && x.Key.GetGenericTypeDefinition() == type.GetGenericTypeDefinition())))
- {
- services ??= EmptyServiceProvider.Instance;
-
- var converterType = GetMostSpecificTypeConverter(type);
- var converter = ReflectionUtils.CreateObject(converterType.MakeGenericType(type).GetTypeInfo(), this, services);
- _typeConverters[type] = converter;
- return converter;
- }
-
- else if (_typeConverters.Any(x => x.Value.CanConvertTo(type)))
- return _typeConverters.First(x => x.Value.CanConvertTo(type)).Value;
-
- throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type");
- }
+ internal TypeConverter GetTypeConverter(Type type, IServiceProvider services = null)
+ => _typeConverterMap.Get(type, services);
///
/// Add a concrete type .
///
/// Primary target of the .
/// The instance.
- public void AddTypeConverter (TypeConverter converter) =>
- AddTypeConverter(typeof(T), converter);
+ public void AddTypeConverter(TypeConverter converter) =>
+ _typeConverterMap.AddConcrete(converter);
///
/// Add a concrete type .
///
/// Primary target of the .
/// The instance.
- public void AddTypeConverter (Type type, TypeConverter converter)
- {
- if (!converter.CanConvertTo(type))
- throw new ArgumentException($"This {converter.GetType().FullName} cannot read {type.FullName} and cannot be registered as its {nameof(TypeConverter)}");
-
- _typeConverters[type] = converter;
- }
+ public void AddTypeConverter(Type type, TypeConverter converter) =>
+ _typeConverterMap.AddConcrete(type, converter);
///
/// Add a generic type .
@@ -829,30 +824,121 @@ namespace Discord.Interactions
/// Generic Type constraint of the of the .
/// Type of the .
- public void AddGenericTypeConverter (Type converterType) =>
- AddGenericTypeConverter(typeof(T), converterType);
+ public void AddGenericTypeConverter(Type converterType) =>
+ _typeConverterMap.AddGeneric(converterType);
///
/// Add a generic type .
///
/// Generic Type constraint of the of the .
/// Type of the .
- public void AddGenericTypeConverter (Type targetType, Type converterType)
- {
- if (!converterType.IsGenericTypeDefinition)
- throw new ArgumentException($"{converterType.FullName} is not generic.");
+ public void AddGenericTypeConverter(Type targetType, Type converterType) =>
+ _typeConverterMap.AddGeneric(targetType, converterType);
+
+ internal ComponentTypeConverter GetComponentTypeConverter(Type type, IServiceProvider services = null) =>
+ _compTypeConverterMap.Get(type, services);
+
+ ///
+ /// Add a concrete type .
+ ///
+ /// Primary target of the .
+ /// The instance.
+ public void AddComponentTypeConverter(ComponentTypeConverter converter) =>
+ AddComponentTypeConverter(typeof(T), converter);
+
+ ///
+ /// Add a concrete type .
+ ///
+ /// Primary target of the .
+ /// The instance.
+ public void AddComponentTypeConverter(Type type, ComponentTypeConverter converter) =>
+ _compTypeConverterMap.AddConcrete(type, converter);
+
+ ///
+ /// Add a generic type .
+ ///
+ /// Generic Type constraint of the of the .
+ /// Type of the .
+ public void AddGenericComponentTypeConverter(Type converterType) =>
+ AddGenericComponentTypeConverter(typeof(T), converterType);
+
+ ///
+ /// Add a generic type .
+ ///
+ /// Generic Type constraint of the of the .
+ /// Type of the .
+ public void AddGenericComponentTypeConverter(Type targetType, Type converterType) =>
+ _compTypeConverterMap.AddGeneric(targetType, converterType);
+
+ internal TypeReader GetTypeReader(Type type, IServiceProvider services = null) =>
+ _typeReaderMap.Get(type, services);
+
+ ///
+ /// Add a concrete type .
+ ///
+ /// Primary target of the .
+ /// The instance.
+ public void AddTypeReader(TypeReader reader) =>
+ AddTypeReader(typeof(T), reader);
- var genericArguments = converterType.GetGenericArguments();
+ ///
+ /// Add a concrete type .
+ ///
+ /// Primary target of the .
+ /// The instance.
+ public void AddTypeReader(Type type, TypeReader reader) =>
+ _typeReaderMap.AddConcrete(type, reader);
- if (genericArguments.Count() > 1)
- throw new InvalidOperationException($"Valid generic {converterType.FullName}s cannot have more than 1 generic type parameter");
+ ///
+ /// Add a generic type .
+ ///
+ /// Generic Type constraint of the of the .
+ /// Type of the .
+ public void AddGenericTypeReader(Type readerType) =>
+ AddGenericTypeReader(typeof(T), readerType);
- var constraints = genericArguments.SelectMany(x => x.GetGenericParameterConstraints());
+ ///
+ /// Add a generic type .
+ ///
+ /// Generic Type constraint of the of the .
+ /// Type of the .
+ public void AddGenericTypeReader(Type targetType, Type readerType) =>
+ _typeReaderMap.AddGeneric(targetType, readerType);
- if (!constraints.Any(x => x.IsAssignableFrom(targetType)))
- throw new InvalidOperationException($"This generic class does not support type {targetType.FullName}");
+ ///
+ /// Serialize an object using a into a to be placed in a Component CustomId.
+ ///
+ /// Type of the object to be serialized.
+ /// Object to be serialized.
+ /// Services that will be passed on to the .
+ ///
+ /// A task representing the conversion process. The task result contains the result of the conversion.
+ ///
+ public Task SerializeValueAsync(T obj, IServiceProvider services) =>
+ _typeReaderMap.Get(typeof(T), services).SerializeAsync(obj, services);
- _genericTypeConverters[targetType] = converterType;
+ ///
+ /// Serialize and format multiple objects into a Custom Id string.
+ ///
+ /// A composite format string.
+ /// >Services that will be passed on to the s.
+ /// Objects to be serialized.
+ ///
+ /// A task representing the conversion process. The task result contains the result of the conversion.
+ ///
+ public async Task GenerateCustomIdStringAsync(string format, IServiceProvider services, params object[] args)
+ {
+ var serializedValues = new string[args.Length];
+
+ for(var i = 0; i < args.Length; i++)
+ {
+ var arg = args[i];
+ var typeReader = _typeReaderMap.Get(arg.GetType(), null);
+ var result = await typeReader.SerializeAsync(arg, services).ConfigureAwait(false);
+ serializedValues[i] = result;
+ }
+
+ return string.Format(format, serializedValues);
}
///
@@ -870,7 +956,7 @@ namespace Discord.Interactions
if (_modalInfos.ContainsKey(type))
throw new InvalidOperationException($"Modal type {type.FullName} already exists.");
- return ModalUtils.GetOrAdd(type);
+ return ModalUtils.GetOrAdd(type, this);
}
internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null)
@@ -1016,7 +1102,7 @@ namespace Discord.Interactions
public ModuleInfo GetModuleInfo ( ) where TModule : class
{
if (!typeof(IInteractionModuleBase).IsAssignableFrom(typeof(TModule)))
- throw new ArgumentException("Type parameter must be a type of Slash Module", "TModule");
+ throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(TModule));
var module = _typedModuleDefs[typeof(TModule)];
@@ -1032,21 +1118,6 @@ namespace Discord.Interactions
_lock.Dispose();
}
- private Type GetMostSpecificTypeConverter (Type type)
- {
- if (_genericTypeConverters.TryGetValue(type, out var matching))
- return matching;
-
- if (type.IsGenericType && _genericTypeConverters.TryGetValue(type.GetGenericTypeDefinition(), out var genericDefinition))
- return genericDefinition;
-
- var typeInterfaces = type.GetInterfaces();
- var candidates = _genericTypeConverters.Where(x => x.Key.IsAssignableFrom(type))
- .OrderByDescending(x => typeInterfaces.Count(y => y.IsAssignableFrom(x.Key)));
-
- return candidates.First().Value;
- }
-
private void EnsureClientReady()
{
if (RestClient?.CurrentUser is null || RestClient?.CurrentUser?.Id == 0)
diff --git a/src/Discord.Net.Interactions/InteractionServiceConfig.cs b/src/Discord.Net.Interactions/InteractionServiceConfig.cs
index 136cba24c..b6576a49f 100644
--- a/src/Discord.Net.Interactions/InteractionServiceConfig.cs
+++ b/src/Discord.Net.Interactions/InteractionServiceConfig.cs
@@ -31,7 +31,7 @@ namespace Discord.Interactions
///
/// Gets or sets the string expression that will be treated as a wild card.
///
- public string WildCardExpression { get; set; }
+ public string WildCardExpression { get; set; } = "*";
///
/// Gets or sets the option to use compiled lambda expressions to create module instances and execute commands. This method improves performance at the cost of memory.
diff --git a/src/Discord.Net.Interactions/Map/TypeMap.cs b/src/Discord.Net.Interactions/Map/TypeMap.cs
new file mode 100644
index 000000000..ef1ef4a53
--- /dev/null
+++ b/src/Discord.Net.Interactions/Map/TypeMap.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+namespace Discord.Interactions
+{
+ internal class TypeMap
+ where TConverter : class, ITypeConverter
+ {
+ private readonly ConcurrentDictionary _concretes;
+ private readonly ConcurrentDictionary _generics;
+ private readonly InteractionService _interactionService;
+
+ public TypeMap(InteractionService interactionService, IDictionary concretes = null, IDictionary generics = null)
+ {
+ _interactionService = interactionService;
+ _concretes = concretes is not null ? new(concretes) : new();
+ _generics = generics is not null ? new(generics) : new();
+ }
+
+ internal TConverter Get(Type type, IServiceProvider services = null)
+ {
+ if (_concretes.TryGetValue(type, out var specific))
+ return specific;
+
+ if (_generics.Any(x => x.Key.IsAssignableFrom(type)
+ || x.Key.IsGenericTypeDefinition && type.IsGenericType && x.Key.GetGenericTypeDefinition() == type.GetGenericTypeDefinition()))
+ {
+ services ??= EmptyServiceProvider.Instance;
+
+ var converterType = GetMostSpecific(type);
+ var converter = ReflectionUtils.CreateObject(converterType.MakeGenericType(type).GetTypeInfo(), _interactionService, services);
+ _concretes[type] = converter;
+ return converter;
+ }
+
+ if (_concretes.Any(x => x.Value.CanConvertTo(type)))
+ return _concretes.First(x => x.Value.CanConvertTo(type)).Value;
+
+ throw new ArgumentException($"No type {typeof(TConverter).Name} is defined for this {type.FullName}", nameof(type));
+ }
+
+ public void AddConcrete(TConverter converter) =>
+ AddConcrete(typeof(TTarget), converter);
+
+ public void AddConcrete(Type type, TConverter converter)
+ {
+ if (!converter.CanConvertTo(type))
+ throw new ArgumentException($"This {converter.GetType().FullName} cannot read {type.FullName} and cannot be registered as its {nameof(TypeConverter)}");
+
+ _concretes[type] = converter;
+ }
+
+ public void AddGeneric(Type converterType) =>
+ AddGeneric(typeof(TTarget), converterType);
+
+ public void AddGeneric(Type targetType, Type converterType)
+ {
+ if (!converterType.IsGenericTypeDefinition)
+ throw new ArgumentException($"{converterType.FullName} is not generic.");
+
+ var genericArguments = converterType.GetGenericArguments();
+
+ if (genericArguments.Length > 1)
+ throw new InvalidOperationException($"Valid generic {converterType.FullName}s cannot have more than 1 generic type parameter");
+
+ var constraints = genericArguments.SelectMany(x => x.GetGenericParameterConstraints());
+
+ if (!constraints.Any(x => x.IsAssignableFrom(targetType)))
+ throw new InvalidOperationException($"This generic class does not support type {targetType.FullName}");
+
+ _generics[targetType] = converterType;
+ }
+
+ private Type GetMostSpecific(Type type)
+ {
+ if (_generics.TryGetValue(type, out var matching))
+ return matching;
+
+ if (type.IsGenericType && _generics.TryGetValue(type.GetGenericTypeDefinition(), out var genericDefinition))
+ return genericDefinition;
+
+ var typeInterfaces = type.GetInterfaces();
+ var candidates = _generics.Where(x => x.Key.IsAssignableFrom(type))
+ .OrderByDescending(x => typeInterfaces.Count(y => y.IsAssignableFrom(x.Key)));
+
+ return candidates.First().Value;
+ }
+ }
+}
diff --git a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/ComponentTypeConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/ComponentTypeConverter.cs
new file mode 100644
index 000000000..e406d4a26
--- /dev/null
+++ b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/ComponentTypeConverter.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Discord.Interactions
+{
+ ///
+ /// Base class for creating Component TypeConverters. uses TypeConverters to interface with Slash Command parameters.
+ ///
+ public abstract class ComponentTypeConverter : ITypeConverter
+ {
+ ///
+ /// Will be used to search for alternative TypeConverters whenever the Command Service encounters an unknown parameter type.
+ ///
+ /// An object type.
+ ///
+ /// The boolean result.
+ ///
+ public abstract bool CanConvertTo(Type type);
+
+ ///
+ /// Will be used to read the incoming payload before executing the method body.
+ ///
+ /// Command exexution context.
+ /// Recieved option payload.
+ /// Service provider that will be used to initialize the command module.
+ ///
+ /// The result of the read process.
+ ///
+ public abstract Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services);
+ }
+
+ ///
+ public abstract class ComponentTypeConverter : ComponentTypeConverter
+ {
+ ///
+ public sealed override bool CanConvertTo(Type type) =>
+ typeof(T).IsAssignableFrom(type);
+ }
+}
diff --git a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs
new file mode 100644
index 000000000..87fc431c5
--- /dev/null
+++ b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Discord.Interactions
+{
+ internal sealed class DefaultArrayComponentConverter : ComponentTypeConverter
+ {
+ private readonly TypeReader _typeReader;
+ private readonly Type _underlyingType;
+
+ public DefaultArrayComponentConverter(InteractionService interactionService)
+ {
+ var type = typeof(T);
+
+ if (!type.IsArray)
+ throw new InvalidOperationException($"{nameof(DefaultArrayComponentConverter)} cannot be used to convert a non-array type.");
+
+ _underlyingType = typeof(T).GetElementType();
+ _typeReader = interactionService.GetTypeReader(_underlyingType);
+ }
+
+ public override async Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
+ {
+ var results = new List();
+
+ foreach (var value in option.Values)
+ {
+ var result = await _typeReader.ReadAsync(context, value, services).ConfigureAwait(false);
+
+ if (!result.IsSuccess)
+ return result;
+
+ results.Add(result);
+ }
+
+ var destination = Array.CreateInstance(_underlyingType, results.Count);
+
+ for (var i = 0; i < results.Count; i++)
+ destination.SetValue(results[i].Value, i);
+
+ return TypeConverterResult.FromSuccess(destination);
+ }
+ }
+}
diff --git a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultValueComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultValueComponentConverter.cs
new file mode 100644
index 000000000..9ed82c6ed
--- /dev/null
+++ b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultValueComponentConverter.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Discord.Interactions
+{
+ internal sealed class DefaultValueComponentConverter : ComponentTypeConverter
+ where T : IConvertible
+ {
+ public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
+ {
+ try
+ {
+ return option.Type switch
+ {
+ ComponentType.SelectMenu => Task.FromResult(TypeConverterResult.FromSuccess(Convert.ChangeType(string.Join(",", option.Values), typeof(T)))),
+ ComponentType.TextInput => Task.FromResult(TypeConverterResult.FromSuccess(Convert.ChangeType(option.Value, typeof(T)))),
+ _ => Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option.Type} doesn't have a convertible value."))
+ };
+ }
+ catch (InvalidCastException castEx)
+ {
+ return Task.FromResult(TypeConverterResult.FromError(castEx));
+ }
+ }
+ }
+}
diff --git a/src/Discord.Net.Interactions/TypeConverters/DefaultEntityTypeConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultEntityTypeConverter.cs
similarity index 100%
rename from src/Discord.Net.Interactions/TypeConverters/DefaultEntityTypeConverter.cs
rename to src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultEntityTypeConverter.cs
diff --git a/src/Discord.Net.Interactions/TypeConverters/DefaultValueConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultValueConverter.cs
similarity index 100%
rename from src/Discord.Net.Interactions/TypeConverters/DefaultValueConverter.cs
rename to src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultValueConverter.cs
diff --git a/src/Discord.Net.Interactions/TypeConverters/EnumConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/EnumConverter.cs
similarity index 100%
rename from src/Discord.Net.Interactions/TypeConverters/EnumConverter.cs
rename to src/Discord.Net.Interactions/TypeConverters/SlashCommands/EnumConverter.cs
diff --git a/src/Discord.Net.Interactions/TypeConverters/NullableConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/NullableConverter.cs
similarity index 100%
rename from src/Discord.Net.Interactions/TypeConverters/NullableConverter.cs
rename to src/Discord.Net.Interactions/TypeConverters/SlashCommands/NullableConverter.cs
diff --git a/src/Discord.Net.Interactions/TypeConverters/TimeSpanConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/TimeSpanConverter.cs
similarity index 100%
rename from src/Discord.Net.Interactions/TypeConverters/TimeSpanConverter.cs
rename to src/Discord.Net.Interactions/TypeConverters/SlashCommands/TimeSpanConverter.cs
diff --git a/src/Discord.Net.Interactions/TypeConverters/TypeConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/TypeConverter.cs
similarity index 95%
rename from src/Discord.Net.Interactions/TypeConverters/TypeConverter.cs
rename to src/Discord.Net.Interactions/TypeConverters/SlashCommands/TypeConverter.cs
index 360b6ce4a..09cbc56d4 100644
--- a/src/Discord.Net.Interactions/TypeConverters/TypeConverter.cs
+++ b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/TypeConverter.cs
@@ -6,7 +6,7 @@ namespace Discord.Interactions
///
/// Base class for creating TypeConverters. uses TypeConverters to interface with Slash Command parameters.
///
- public abstract class TypeConverter
+ public abstract class TypeConverter : ITypeConverter
{
///
/// Will be used to search for alternative TypeConverters whenever the Command Service encounters an unknown parameter type.
diff --git a/src/Discord.Net.Interactions/TypeReaders/DefaultSnowflakeReader.cs b/src/Discord.Net.Interactions/TypeReaders/DefaultSnowflakeReader.cs
new file mode 100644
index 000000000..e2ac1efbd
--- /dev/null
+++ b/src/Discord.Net.Interactions/TypeReaders/DefaultSnowflakeReader.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Discord.Interactions
+{
+ internal abstract class DefaultSnowflakeReader : TypeReader
+ where T : class, ISnowflakeEntity
+ {
+ protected abstract Task GetEntity(ulong id, IInteractionContext ctx);
+
+ public override async Task ReadAsync(IInteractionContext context, string option, IServiceProvider services)
+ {
+ if (!ulong.TryParse(option, out var snowflake))
+ return TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option} isn't a valid snowflake thus cannot be converted into {typeof(T).Name}");
+
+ var result = await GetEntity(snowflake, context).ConfigureAwait(false);
+
+ return result is not null ?
+ TypeConverterResult.FromSuccess(result) : TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option} must be a valid {typeof(T).Name} snowflake to be parsed.");
+ }
+
+ public override Task SerializeAsync(object obj, IServiceProvider services) => Task.FromResult((obj as ISnowflakeEntity)?.Id.ToString());
+ }
+
+ internal sealed class DefaultUserReader : DefaultSnowflakeReader
+ where T : class, IUser
+ {
+ protected override async Task GetEntity(ulong id, IInteractionContext ctx) => await ctx.Client.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T;
+ }
+
+ internal sealed class DefaultChannelReader : DefaultSnowflakeReader
+ where T : class, IChannel
+ {
+ protected override async Task GetEntity(ulong id, IInteractionContext ctx) => await ctx.Client.GetChannelAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T;
+ }
+
+ internal sealed class DefaultRoleReader : DefaultSnowflakeReader
+ where T : class, IRole
+ {
+ protected override Task GetEntity(ulong id, IInteractionContext ctx) => Task.FromResult(ctx.Guild?.GetRole(id) as T);
+ }
+
+ internal sealed class DefaultMessageReader : DefaultSnowflakeReader
+ where T : class, IMessage
+ {
+ protected override async Task GetEntity(ulong id, IInteractionContext ctx) => await ctx.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T;
+ }
+}
diff --git a/src/Discord.Net.Interactions/TypeReaders/DefaultValueReader.cs b/src/Discord.Net.Interactions/TypeReaders/DefaultValueReader.cs
new file mode 100644
index 000000000..e833382a6
--- /dev/null
+++ b/src/Discord.Net.Interactions/TypeReaders/DefaultValueReader.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Discord.Interactions
+{
+ internal sealed class DefaultValueReader : TypeReader
+ where T : IConvertible
+ {
+ public override Task ReadAsync(IInteractionContext context, string option, IServiceProvider services)
+ {
+ try
+ {
+ var converted = Convert.ChangeType(option, typeof(T));
+ return Task.FromResult(TypeConverterResult.FromSuccess(converted));
+ }
+ catch (InvalidCastException castEx)
+ {
+ return Task.FromResult(TypeConverterResult.FromError(castEx));
+ }
+ }
+ }
+}
diff --git a/src/Discord.Net.Interactions/TypeReaders/EnumReader.cs b/src/Discord.Net.Interactions/TypeReaders/EnumReader.cs
new file mode 100644
index 000000000..df6f2ac33
--- /dev/null
+++ b/src/Discord.Net.Interactions/TypeReaders/EnumReader.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Discord.Interactions
+{
+ internal sealed class EnumReader : TypeReader
+ where T : struct, Enum
+ {
+ public override Task ReadAsync(IInteractionContext context, string option, IServiceProvider services)
+ {
+ return Task.FromResult(Enum.TryParse(option, out var result) ?
+ TypeConverterResult.FromSuccess(result) : TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Value {option} cannot be converted to {nameof(T)}"));
+ }
+
+ public override Task SerializeAsync(object obj, IServiceProvider services)
+ {
+ var name = Enum.GetName(typeof(T), obj);
+
+ if (name is null)
+ throw new ArgumentException($"Enum name cannot be parsed from {obj}");
+
+ return Task.FromResult(name);
+ }
+ }
+}
diff --git a/src/Discord.Net.Interactions/TypeReaders/TypeReader.cs b/src/Discord.Net.Interactions/TypeReaders/TypeReader.cs
new file mode 100644
index 000000000..e518e0208
--- /dev/null
+++ b/src/Discord.Net.Interactions/TypeReaders/TypeReader.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Discord.Interactions
+{
+ ///
+ /// Base class for creating TypeConverters. uses TypeConverters to interface with Slash Command parameters.
+ ///
+ public abstract class TypeReader : ITypeConverter
+ {
+ ///
+ /// Will be used to search for alternative TypeReaders whenever the Command Service encounters an unknown parameter type.
+ ///
+ /// An object type.
+ ///
+ /// The boolean result.
+ ///
+ public abstract bool CanConvertTo(Type type);
+
+ ///
+ /// Will be used to read the incoming payload before executing the method body.
+ ///
+ /// Command execution context.
+ /// Received option payload.
+ /// Service provider that will be used to initialize the command module.
+ /// The result of the read process.
+ public abstract Task ReadAsync(IInteractionContext context, string option, IServiceProvider services);
+
+ ///
+ /// Will be used to serialize objects into strings.
+ ///
+ /// Object to be serialized.
+ ///
+ /// A task representing the conversion process. The result of the task contains the conversion result.
+ ///
+ public virtual Task SerializeAsync(object obj, IServiceProvider services) => Task.FromResult(obj.ToString());
+ }
+
+ ///
+ public abstract class TypeReader : TypeReader
+ {
+ ///
+ public sealed override bool CanConvertTo(Type type) =>
+ typeof(T).IsAssignableFrom(type);
+ }
+}
diff --git a/src/Discord.Net.Interactions/Utilities/ModalUtils.cs b/src/Discord.Net.Interactions/Utilities/ModalUtils.cs
index d42cc2fe9..e2d028e1f 100644
--- a/src/Discord.Net.Interactions/Utilities/ModalUtils.cs
+++ b/src/Discord.Net.Interactions/Utilities/ModalUtils.cs
@@ -7,20 +7,20 @@ namespace Discord.Interactions
{
internal static class ModalUtils
{
- private static ConcurrentDictionary _modalInfos = new();
+ private static readonly ConcurrentDictionary _modalInfos = new();
public static IReadOnlyCollection Modals => _modalInfos.Values.ToReadOnlyCollection();
- public static ModalInfo GetOrAdd(Type type)
+ public static ModalInfo GetOrAdd(Type type, InteractionService interactionService)
{
if (!typeof(IModal).IsAssignableFrom(type))
throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type));
- return _modalInfos.GetOrAdd(type, ModuleClassBuilder.BuildModalInfo(type));
+ return _modalInfos.GetOrAdd(type, ModuleClassBuilder.BuildModalInfo(type, interactionService));
}
- public static ModalInfo GetOrAdd() where T : class, IModal
- => GetOrAdd(typeof(T));
+ public static ModalInfo GetOrAdd(InteractionService interactionService) where T : class, IModal
+ => GetOrAdd(typeof(T), interactionService);
public static bool TryGet(Type type, out ModalInfo modalInfo)
{