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/Parameters/ComponentCommandParameterBuilder.cs b/src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs
index 666fbef88..56de1fc36 100644
--- a/src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs
@@ -1,8 +1,4 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace Discord.Interactions.Builders
{
@@ -29,7 +25,7 @@ namespace Discord.Interactions.Builders
public ComponentCommandParameterBuilder SetParameterType(Type type, IServiceProvider services = null)
{
base.SetParameterType(type);
- TypeReader = Command.Module.InteractionService.GetTypeConverter(ParameterType, services);
+ TypeReader = Command.Module.InteractionService.GetTypeReader(ParameterType, services);
return this;
}
diff --git a/src/Discord.Net.Interactions/Entities/ITypeHandler.cs b/src/Discord.Net.Interactions/Entities/ITypeHandler.cs
new file mode 100644
index 000000000..4626ed850
--- /dev/null
+++ b/src/Discord.Net.Interactions/Entities/ITypeHandler.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace Discord.Interactions
+{
+ internal interface ITypeHandler
+ {
+ public bool CanConvertTo(Type type);
+ }
+}
diff --git a/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs
index 0e43af3a8..87fc1664e 100644
--- a/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs
+++ b/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs
@@ -11,10 +11,10 @@ namespace Discord.Interactions
///
/// Represents the info class of an attribute based method for handling Component Interaction events.
///
- public class ComponentCommandInfo : CommandInfo
+ public class ComponentCommandInfo : CommandInfo
{
///
- public override IReadOnlyCollection Parameters { get; }
+ public override IReadOnlyCollection Parameters { get; }
///
public override bool SupportsWildCards => true;
@@ -73,14 +73,16 @@ namespace Discord.Interactions
if (componentValues is not null)
{
- if (Parameters.Last().ParameterType == typeof(string[]))
- args[args.Length - 1] = componentValues.ToArray();
+ var lastParam = Parameters.Last();
+
+ if (lastParam.ParameterType.IsArray)
+ args[args.Length - 1] = componentValues.Select(async x => await lastParam.TypeReader.ReadAsync(context, x, services).ConfigureAwait(false)).ToArray();
else
return ExecuteResult.FromError(InteractionCommandError.BadArgs, $"Select Menu Interaction handlers must accept a {typeof(string[]).FullName} as its last parameter");
}
for (var i = 0; i < strCount; i++)
- args[i] = values.ElementAt(i);
+ args[i] = await Parameters.ElementAt(i).TypeReader.ReadAsync(context, values.ElementAt(i), services).ConfigureAwait(false);
return await RunAsync(context, args, services).ConfigureAwait(false);
}
diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs
index f8202096f..38fbb7549 100644
--- a/src/Discord.Net.Interactions/InteractionService.cs
+++ b/src/Discord.Net.Interactions/InteractionService.cs
@@ -66,8 +66,8 @@ 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 _typeReaderMap;
private readonly ConcurrentDictionary _autocompleteHandlers = new();
private readonly ConcurrentDictionary _modalInfos = new();
private readonly SemaphoreSlim _lock;
@@ -179,7 +179,10 @@ namespace Discord.Interactions
_autoServiceScopes = config.AutoServiceScopes;
_restResponseCallback = config.RestResponseCallback;
- _genericTypeConverters = new ConcurrentDictionary
+ _typeConverterMap = new TypeMap(this, new Dictionary
+ {
+ [typeof(TimeSpan)] = new TimeSpanConverter()
+ }, new Dictionary
{
[typeof(IChannel)] = typeof(DefaultChannelConverter<>),
[typeof(IRole)] = typeof(DefaultRoleConverter<>),
@@ -189,12 +192,9 @@ namespace Discord.Interactions
[typeof(IConvertible)] = typeof(DefaultValueConverter<>),
[typeof(Enum)] = typeof(EnumConverter<>),
[typeof(Nullable<>)] = typeof(NullableConverter<>),
- };
+ });
- _typeConverters = new ConcurrentDictionary
- {
- [typeof(TimeSpan)] = new TimeSpanConverter()
- };
+ _typeReaderMap = new TypeMap(this);
}
///
@@ -769,61 +769,24 @@ namespace Discord.Interactions
return await commandResult.Command.ExecuteAsync(context, services).ConfigureAwait(false);
}
- private async Task ExecuteModalCommandAsync(IInteractionContext context, string input, IServiceProvider services)
- {
- var result = _modalCommandMap.GetCommand(input);
-
- if (!result.IsSuccess)
- {
- await _cmdLogger.DebugAsync($"Unknown custom interaction id, skipping execution ({input.ToUpper()})");
-
- await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false);
- return result;
- }
- 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 .
@@ -831,58 +794,55 @@ 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.");
-
- var genericArguments = converterType.GetGenericArguments();
+ public void AddGenericTypeConverter(Type targetType, Type converterType) =>
+ _typeConverterMap.AddGeneric(targetType, converterType);
- if (genericArguments.Count() > 1)
- throw new InvalidOperationException($"Valid generic {converterType.FullName}s cannot have more than 1 generic type parameter");
+ internal TypeReader GetTypeReader(Type type, IServiceProvider services = null)
+ => _typeReaderMap.Get(type, services);
- 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}");
+ ///
+ /// Add a concrete type .
+ ///
+ /// Primary target of the .
+ /// The instance.
+ public void AddTypeReader(TypeReader reader) =>
+ _typeReaderMap.AddConcrete(reader);
///
- /// Serialize an object using a into a to be placed in a Component CustomId.
+ /// Add a concrete type .
///
- /// Type of the object to be serialized.
- /// Object to be serialized.
- /// Services that will be passed on to the TypeReader.
- ///
- /// A task representing the conversion process. The task result contains the result of the conversion.
- ///
- public Task SerializeValueAsync(T obj, IServiceProvider services = null) =>
- _typeReaderMap.Get(typeof(T), services).SerializeAsync(obj);
+ /// Primary target of the .
+ /// The instance.
+ public void AddTypeReader(Type type, TypeReader converter) =>
+ _typeReaderMap.AddConcrete(type, converter);
///
- /// Loads and caches an for the provided .
+ /// Add a generic type .
///
- /// Type of to be loaded.
- ///
- /// The built instance.
- ///
- ///
- public ModalInfo AddModalInfo() where T : class, IModal
- {
- var type = typeof(T);
+ /// Generic Type constraint of the of the .
+ /// Type of the .
- if (_modalInfos.ContainsKey(type))
- throw new InvalidOperationException($"Modal type {type.FullName} already exists.");
+ public void AddGenericTypeReader(Type readerType) =>
+ _typeReaderMap.AddGeneric(readerType);
- return ModalUtils.GetOrAdd(type);
- }
+ ///
+ /// Add a generic type .
+ ///
+ /// Generic Type constraint of the of the .
+ /// Type of the .
+ public void AddGenericTypeReader(Type targetType, Type readerType) =>
+ _typeConverterMap.AddGeneric(targetType, readerType);
+
+ public string SerializeWithTypeReader(object obj, IServiceProvider services = null) =>
+ _typeReaderMap.Get(typeof(T), services)?.Serialize(obj);
internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null)
{
@@ -1043,21 +1003,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/Map/TypeMap.cs b/src/Discord.Net.Interactions/Map/TypeMap.cs
new file mode 100644
index 000000000..d08423657
--- /dev/null
+++ b/src/Discord.Net.Interactions/Map/TypeMap.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+namespace Discord.Interactions
+{
+ internal class TypeMap where T : class, ITypeHandler
+ {
+ 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 T Get(Type type, IServiceProvider services = null)
+ {
+ if (_concretes.TryGetValue(type, out var specific))
+ return specific;
+
+ else 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;
+ }
+
+ else if (_concretes.Any(x => x.Value.CanConvertTo(type)))
+ return _concretes.First(x => x.Value.CanConvertTo(type)).Value;
+
+ throw new ArgumentException($"No type {nameof(T)} is defined for this {type.FullName}", "type");
+ }
+
+ public void AddConcrete(T converter) =>
+ AddConcrete(typeof(TTarget), converter);
+
+ public void AddConcrete(Type type, T 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.Count() > 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/TypeConverter.cs b/src/Discord.Net.Interactions/TypeConverters/TypeConverter.cs
index 360b6ce4a..8361831be 100644
--- a/src/Discord.Net.Interactions/TypeConverters/TypeConverter.cs
+++ b/src/Discord.Net.Interactions/TypeConverters/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 : ITypeHandler
{
///
/// 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/ChannelTypeReader.cs b/src/Discord.Net.Interactions/TypeReaders/ChannelTypeReader.cs
new file mode 100644
index 000000000..8b9ef8d54
--- /dev/null
+++ b/src/Discord.Net.Interactions/TypeReaders/ChannelTypeReader.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Discord.Interactions
+{
+ ///
+ /// A for parsing objects implementing .
+ ///
+ ///
+ /// This is shipped with Discord.Net and is used by default to parse any
+ /// implemented object within a command. The TypeReader will attempt to first parse the
+ /// input by mention, then the snowflake identifier, then by name; the highest candidate will be chosen as the
+ /// final output; otherwise, an erroneous is returned.
+ ///
+ /// The type to be checked; must implement .
+ public class ChannelTypeReader : TypeReader
+ where T : class, IChannel
+ {
+ ///
+ public override async Task ReadAsync(IInteractionContext context, object input, IServiceProvider services)
+ {
+ if (context.Guild is null)
+ {
+ var str = input as string;
+
+ if (ulong.TryParse(str, out var channelId))
+ return TypeConverterResult.FromSuccess(await context.Guild.GetChannelAsync(channelId).ConfigureAwait(false));
+
+ if (MentionUtils.TryParseChannel(str, out channelId))
+ return TypeConverterResult.FromSuccess(await context.Guild.GetChannelAsync(channelId).ConfigureAwait(false));
+
+ var channels = await context.Guild.GetChannelsAsync().ConfigureAwait(false);
+ var nameMatch = channels.FirstOrDefault(x => string.Equals(x.Name, str, StringComparison.OrdinalIgnoreCase));
+
+ if (nameMatch is not null)
+ return TypeConverterResult.FromSuccess(nameMatch);
+ }
+
+ return TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, "Channel not found.");
+ }
+
+ public override string Serialize(object value) => (value as IChannel)?.Id.ToString();
+ }
+}
diff --git a/src/Discord.Net.Interactions/TypeReaders/EnumTypeReader.cs b/src/Discord.Net.Interactions/TypeReaders/EnumTypeReader.cs
new file mode 100644
index 000000000..4c9773b5b
--- /dev/null
+++ b/src/Discord.Net.Interactions/TypeReaders/EnumTypeReader.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Discord.Interactions
+{
+ internal class EnumTypeReader : TypeReader where T : struct, Enum
+ {
+ ///
+ public override Task ReadAsync(IInteractionContext context, string input, IServiceProvider services)
+ {
+ if (Enum.TryParse(input, out var result))
+ return Task.FromResult(TypeConverterResult.FromSuccess(result));
+ else
+ return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Value {input} cannot be converted to {nameof(T)}"));
+ }
+
+ public override string Serialize(object value) => value.ToString();
+ }
+}
diff --git a/src/Discord.Net.Interactions/TypeReaders/MessageTypeReader.cs b/src/Discord.Net.Interactions/TypeReaders/MessageTypeReader.cs
new file mode 100644
index 000000000..67100d7d0
--- /dev/null
+++ b/src/Discord.Net.Interactions/TypeReaders/MessageTypeReader.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Globalization;
+using System.Threading.Tasks;
+
+namespace Discord.Interactions
+{
+ ///
+ /// A for parsing objects implementing .
+ ///
+ /// The type to be checked; must implement .
+ public class MessageTypeReader : TypeReader
+ where T : class, IMessage
+ {
+ ///
+ public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services)
+ {
+ //By Id (1.0)
+ if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out ulong id))
+ {
+ if (await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg)
+ return TypeReaderResult.FromSuccess(msg);
+ }
+
+ return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Message not found.");
+ }
+ }
+}
diff --git a/src/Discord.Net.Interactions/TypeReaders/NullableTypeReader.cs b/src/Discord.Net.Interactions/TypeReaders/NullableTypeReader.cs
new file mode 100644
index 000000000..f68bf6e2c
--- /dev/null
+++ b/src/Discord.Net.Interactions/TypeReaders/NullableTypeReader.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+
+namespace Discord.Commands
+{
+ internal static class NullableTypeReader
+ {
+ public static TypeReader Create(Type type, TypeReader reader)
+ {
+ var constructor = typeof(NullableTypeReader<>).MakeGenericType(type).GetTypeInfo().DeclaredConstructors.First();
+ return (TypeReader)constructor.Invoke(new object[] { reader });
+ }
+ }
+
+ internal class NullableTypeReader : TypeReader
+ where T : struct
+ {
+ private readonly TypeReader _baseTypeReader;
+
+ public NullableTypeReader(TypeReader baseTypeReader)
+ {
+ _baseTypeReader = baseTypeReader;
+ }
+
+ ///
+ public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services)
+ {
+ if (string.Equals(input, "null", StringComparison.OrdinalIgnoreCase) || string.Equals(input, "nothing", StringComparison.OrdinalIgnoreCase))
+ return TypeReaderResult.FromSuccess(new T?());
+ return await _baseTypeReader.ReadAsync(context, input, services).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/src/Discord.Net.Interactions/TypeReaders/RoleTypeReader.cs b/src/Discord.Net.Interactions/TypeReaders/RoleTypeReader.cs
new file mode 100644
index 000000000..4c9aaf4d8
--- /dev/null
+++ b/src/Discord.Net.Interactions/TypeReaders/RoleTypeReader.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Discord.Commands
+{
+ ///
+ /// A for parsing objects implementing .
+ ///
+ /// The type to be checked; must implement .
+ public class RoleTypeReader : TypeReader
+ where T : class, IRole
+ {
+ ///
+ public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services)
+ {
+ if (context.Guild != null)
+ {
+ var results = new Dictionary();
+ var roles = context.Guild.Roles;
+
+ //By Mention (1.0)
+ if (MentionUtils.TryParseRole(input, out var id))
+ AddResult(results, context.Guild.GetRole(id) as T, 1.00f);
+
+ //By Id (0.9)
+ if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id))
+ AddResult(results, context.Guild.GetRole(id) as T, 0.90f);
+
+ //By Name (0.7-0.8)
+ foreach (var role in roles.Where(x => string.Equals(input, x.Name, StringComparison.OrdinalIgnoreCase)))
+ AddResult(results, role as T, role.Name == input ? 0.80f : 0.70f);
+
+ if (results.Count > 0)
+ return Task.FromResult(TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection()));
+ }
+ return Task.FromResult(TypeReaderResult.FromError(CommandError.ObjectNotFound, "Role not found."));
+ }
+
+ private void AddResult(Dictionary results, T role, float score)
+ {
+ if (role != null && !results.ContainsKey(role.Id))
+ results.Add(role.Id, new TypeReaderValue(role, score));
+ }
+ }
+}
diff --git a/src/Discord.Net.Interactions/TypeReaders/TimeSpanTypeReader.cs b/src/Discord.Net.Interactions/TypeReaders/TimeSpanTypeReader.cs
new file mode 100644
index 000000000..5448553b3
--- /dev/null
+++ b/src/Discord.Net.Interactions/TypeReaders/TimeSpanTypeReader.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Globalization;
+using System.Threading.Tasks;
+
+namespace Discord.Commands
+{
+ internal class TimeSpanTypeReader : TypeReader
+ {
+ ///
+ /// TimeSpan try parse formats.
+ ///
+ private static readonly string[] Formats =
+ {
+ "%d'd'%h'h'%m'm'%s's'", // 4d3h2m1s
+ "%d'd'%h'h'%m'm'", // 4d3h2m
+ "%d'd'%h'h'%s's'", // 4d3h 1s
+ "%d'd'%h'h'", // 4d3h
+ "%d'd'%m'm'%s's'", // 4d 2m1s
+ "%d'd'%m'm'", // 4d 2m
+ "%d'd'%s's'", // 4d 1s
+ "%d'd'", // 4d
+ "%h'h'%m'm'%s's'", // 3h2m1s
+ "%h'h'%m'm'", // 3h2m
+ "%h'h'%s's'", // 3h 1s
+ "%h'h'", // 3h
+ "%m'm'%s's'", // 2m1s
+ "%m'm'", // 2m
+ "%s's'", // 1s
+ };
+
+ ///
+ public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services)
+ {
+ if (string.IsNullOrEmpty(input))
+ throw new ArgumentException(message: $"{nameof(input)} must not be null or empty.", paramName: nameof(input));
+
+ var isNegative = input[0] == '-'; // Char for CultureInfo.InvariantCulture.NumberFormat.NegativeSign
+ if (isNegative)
+ {
+ input = input.Substring(1);
+ }
+
+ if (TimeSpan.TryParseExact(input.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out var timeSpan))
+ {
+ return isNegative
+ ? Task.FromResult(TypeReaderResult.FromSuccess(-timeSpan))
+ : Task.FromResult(TypeReaderResult.FromSuccess(timeSpan));
+ }
+ else
+ {
+ return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan"));
+ }
+ }
+ }
+}
diff --git a/src/Discord.Net.Interactions/TypeReaders/TypeReader.cs b/src/Discord.Net.Interactions/TypeReaders/TypeReader.cs
new file mode 100644
index 000000000..62276c973
--- /dev/null
+++ b/src/Discord.Net.Interactions/TypeReaders/TypeReader.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Discord.Interactions
+{
+ ///
+ /// Base class for creating s. uses s to parse string values into entities.
+ ///
+ ///
+ /// s are mainly used to parse message component values. For interfacing with Slash Command parameters use s instead.
+ ///
+ public abstract class TypeReader : ITypeHandler
+ {
+ ///
+ /// Will be used to search for alternative TypeReaders whenever the Command Service encounters an unknown parameter type.
+ ///
+ ///
+ ///
+ public abstract bool CanConvertTo(Type type);
+
+ ///
+ /// Will be used to read the incoming payload before executing the method body.
+ ///
+ /// Command exexution context.
+ /// Raw string input value.
+ /// Service provider that will be used to initialize the command module.
+ /// The result of the read process.
+ public abstract Task ReadAsync(IInteractionContext context, object input, IServiceProvider services);
+
+ ///
+ /// Will be used to manipulate the outgoing command option, before the command gets registered to Discord.
+ ///
+ public virtual string Serialize(object value) => null;
+ }
+
+ ///
+ public abstract class TypeReader : TypeReader
+ {
+ ///
+ public sealed override bool CanConvertTo(Type type) =>
+ typeof(T).IsAssignableFrom(type);
+ }
+}
diff --git a/src/Discord.Net.Interactions/TypeReaders/UserTypeReader.cs b/src/Discord.Net.Interactions/TypeReaders/UserTypeReader.cs
new file mode 100644
index 000000000..c0104e341
--- /dev/null
+++ b/src/Discord.Net.Interactions/TypeReaders/UserTypeReader.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Discord.Commands
+{
+ ///
+ /// A for parsing objects implementing .
+ ///
+ /// The type to be checked; must implement .
+ public class UserTypeReader : TypeReader
+ where T : class, IUser
+ {
+ ///
+ public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services)
+ {
+ var results = new Dictionary();
+ IAsyncEnumerable channelUsers = context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten(); // it's better
+ IReadOnlyCollection guildUsers = ImmutableArray.Create();
+
+ if (context.Guild != null)
+ guildUsers = await context.Guild.GetUsersAsync(CacheMode.CacheOnly).ConfigureAwait(false);
+
+ //By Mention (1.0)
+ if (MentionUtils.TryParseUser(input, out var id))
+ {
+ if (context.Guild != null)
+ AddResult(results, await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f);
+ else
+ AddResult(results, await context.Channel.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f);
+ }
+
+ //By Id (0.9)
+ if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id))
+ {
+ if (context.Guild != null)
+ AddResult(results, await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f);
+ else
+ AddResult(results, await context.Channel.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f);
+ }
+
+ //By Username + Discriminator (0.7-0.85)
+ int index = input.LastIndexOf('#');
+ if (index >= 0)
+ {
+ string username = input.Substring(0, index);
+ if (ushort.TryParse(input.Substring(index + 1), out ushort discriminator))
+ {
+ var channelUser = await channelUsers.FirstOrDefaultAsync(x => x.DiscriminatorValue == discriminator &&
+ string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)).ConfigureAwait(false);
+ AddResult(results, channelUser as T, channelUser?.Username == username ? 0.85f : 0.75f);
+
+ var guildUser = guildUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator &&
+ string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase));
+ AddResult(results, guildUser as T, guildUser?.Username == username ? 0.80f : 0.70f);
+ }
+ }
+
+ //By Username (0.5-0.6)
+ {
+ await channelUsers
+ .Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))
+ .ForEachAsync(channelUser => AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f))
+ .ConfigureAwait(false);
+
+ foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)))
+ AddResult(results, guildUser as T, guildUser.Username == input ? 0.60f : 0.50f);
+ }
+
+ //By Nickname (0.5-0.6)
+ {
+ await channelUsers
+ .Where(x => string.Equals(input, (x as IGuildUser)?.Nickname, StringComparison.OrdinalIgnoreCase))
+ .ForEachAsync(channelUser => AddResult(results, channelUser as T, (channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f))
+ .ConfigureAwait(false);
+
+ foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Nickname, StringComparison.OrdinalIgnoreCase)))
+ AddResult(results, guildUser as T, guildUser.Nickname == input ? 0.60f : 0.50f);
+ }
+
+ if (results.Count > 0)
+ return TypeReaderResult.FromSuccess(results.Values.ToImmutableArray());
+ return TypeReaderResult.FromError(CommandError.ObjectNotFound, "User not found.");
+ }
+
+ private void AddResult(Dictionary results, T user, float score)
+ {
+ if (user != null && !results.ContainsKey(user.Id))
+ results.Add(user.Id, new TypeReaderValue(user, score));
+ }
+ }
+}