@@ -5,7 +5,7 @@ namespace Discord.Interactions.Builders | |||||
/// <summary> | /// <summary> | ||||
/// Represents a builder for creating <see cref="ComponentCommandInfo"/>. | /// Represents a builder for creating <see cref="ComponentCommandInfo"/>. | ||||
/// </summary> | /// </summary> | ||||
public sealed class ComponentCommandBuilder : CommandBuilder<ComponentCommandInfo, ComponentCommandBuilder, CommandParameterBuilder> | |||||
public sealed class ComponentCommandBuilder : CommandBuilder<ComponentCommandInfo, ComponentCommandBuilder, ComponentCommandParameterBuilder> | |||||
{ | { | ||||
protected override ComponentCommandBuilder Instance => this; | protected override ComponentCommandBuilder Instance => this; | ||||
@@ -26,9 +26,9 @@ namespace Discord.Interactions.Builders | |||||
/// <returns> | /// <returns> | ||||
/// The builder instance. | /// The builder instance. | ||||
/// </returns> | /// </returns> | ||||
public override ComponentCommandBuilder AddParameter (Action<CommandParameterBuilder> configure) | |||||
public override ComponentCommandBuilder AddParameter (Action<ComponentCommandParameterBuilder> configure) | |||||
{ | { | ||||
var parameter = new CommandParameterBuilder(this); | |||||
var parameter = new ComponentCommandParameterBuilder(this); | |||||
configure(parameter); | configure(parameter); | ||||
AddParameters(parameter); | AddParameters(parameter); | ||||
return this; | return this; | ||||
@@ -1,8 +1,4 @@ | |||||
using System; | using System; | ||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Interactions.Builders | namespace Discord.Interactions.Builders | ||||
{ | { | ||||
@@ -29,7 +25,7 @@ namespace Discord.Interactions.Builders | |||||
public ComponentCommandParameterBuilder SetParameterType(Type type, IServiceProvider services = null) | public ComponentCommandParameterBuilder SetParameterType(Type type, IServiceProvider services = null) | ||||
{ | { | ||||
base.SetParameterType(type); | base.SetParameterType(type); | ||||
TypeReader = Command.Module.InteractionService.GetTypeConverter(ParameterType, services); | |||||
TypeReader = Command.Module.InteractionService.GetTypeReader(ParameterType, services); | |||||
return this; | return this; | ||||
} | } | ||||
@@ -0,0 +1,9 @@ | |||||
using System; | |||||
namespace Discord.Interactions | |||||
{ | |||||
internal interface ITypeHandler | |||||
{ | |||||
public bool CanConvertTo(Type type); | |||||
} | |||||
} |
@@ -11,10 +11,10 @@ namespace Discord.Interactions | |||||
/// <summary> | /// <summary> | ||||
/// Represents the info class of an attribute based method for handling Component Interaction events. | /// Represents the info class of an attribute based method for handling Component Interaction events. | ||||
/// </summary> | /// </summary> | ||||
public class ComponentCommandInfo : CommandInfo<CommandParameterInfo> | |||||
public class ComponentCommandInfo : CommandInfo<ComponentCommandParameterInfo> | |||||
{ | { | ||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
public override IReadOnlyCollection<CommandParameterInfo> Parameters { get; } | |||||
public override IReadOnlyCollection<ComponentCommandParameterInfo> Parameters { get; } | |||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
public override bool SupportsWildCards => true; | public override bool SupportsWildCards => true; | ||||
@@ -73,14 +73,16 @@ namespace Discord.Interactions | |||||
if (componentValues is not null) | 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 | else | ||||
return ExecuteResult.FromError(InteractionCommandError.BadArgs, $"Select Menu Interaction handlers must accept a {typeof(string[]).FullName} as its last parameter"); | 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++) | 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); | return await RunAsync(context, args, services).ConfigureAwait(false); | ||||
} | } | ||||
@@ -66,8 +66,8 @@ namespace Discord.Interactions | |||||
private readonly CommandMap<AutocompleteCommandInfo> _autocompleteCommandMap; | private readonly CommandMap<AutocompleteCommandInfo> _autocompleteCommandMap; | ||||
private readonly CommandMap<ModalCommandInfo> _modalCommandMap; | private readonly CommandMap<ModalCommandInfo> _modalCommandMap; | ||||
private readonly HashSet<ModuleInfo> _moduleDefs; | private readonly HashSet<ModuleInfo> _moduleDefs; | ||||
private readonly ConcurrentDictionary<Type, TypeConverter> _typeConverters; | |||||
private readonly ConcurrentDictionary<Type, Type> _genericTypeConverters; | |||||
private readonly TypeMap<TypeConverter> _typeConverterMap; | |||||
private readonly TypeMap<TypeReader> _typeReaderMap; | |||||
private readonly ConcurrentDictionary<Type, IAutocompleteHandler> _autocompleteHandlers = new(); | private readonly ConcurrentDictionary<Type, IAutocompleteHandler> _autocompleteHandlers = new(); | ||||
private readonly ConcurrentDictionary<Type, ModalInfo> _modalInfos = new(); | private readonly ConcurrentDictionary<Type, ModalInfo> _modalInfos = new(); | ||||
private readonly SemaphoreSlim _lock; | private readonly SemaphoreSlim _lock; | ||||
@@ -179,7 +179,10 @@ namespace Discord.Interactions | |||||
_autoServiceScopes = config.AutoServiceScopes; | _autoServiceScopes = config.AutoServiceScopes; | ||||
_restResponseCallback = config.RestResponseCallback; | _restResponseCallback = config.RestResponseCallback; | ||||
_genericTypeConverters = new ConcurrentDictionary<Type, Type> | |||||
_typeConverterMap = new TypeMap<TypeConverter>(this, new Dictionary<Type, TypeConverter> | |||||
{ | |||||
[typeof(TimeSpan)] = new TimeSpanConverter() | |||||
}, new Dictionary<Type, Type> | |||||
{ | { | ||||
[typeof(IChannel)] = typeof(DefaultChannelConverter<>), | [typeof(IChannel)] = typeof(DefaultChannelConverter<>), | ||||
[typeof(IRole)] = typeof(DefaultRoleConverter<>), | [typeof(IRole)] = typeof(DefaultRoleConverter<>), | ||||
@@ -189,12 +192,9 @@ namespace Discord.Interactions | |||||
[typeof(IConvertible)] = typeof(DefaultValueConverter<>), | [typeof(IConvertible)] = typeof(DefaultValueConverter<>), | ||||
[typeof(Enum)] = typeof(EnumConverter<>), | [typeof(Enum)] = typeof(EnumConverter<>), | ||||
[typeof(Nullable<>)] = typeof(NullableConverter<>), | [typeof(Nullable<>)] = typeof(NullableConverter<>), | ||||
}; | |||||
}); | |||||
_typeConverters = new ConcurrentDictionary<Type, TypeConverter> | |||||
{ | |||||
[typeof(TimeSpan)] = new TimeSpanConverter() | |||||
}; | |||||
_typeReaderMap = new TypeMap<TypeReader>(this); | |||||
} | } | ||||
/// <summary> | /// <summary> | ||||
@@ -769,61 +769,24 @@ namespace Discord.Interactions | |||||
return await commandResult.Command.ExecuteAsync(context, services).ConfigureAwait(false); | return await commandResult.Command.ExecuteAsync(context, services).ConfigureAwait(false); | ||||
} | } | ||||
private async Task<IResult> 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<TypeConverter>.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); | |||||
/// <summary> | /// <summary> | ||||
/// Add a concrete type <see cref="TypeConverter"/>. | /// Add a concrete type <see cref="TypeConverter"/>. | ||||
/// </summary> | /// </summary> | ||||
/// <typeparam name="T">Primary target <see cref="Type"/> of the <see cref="TypeConverter"/>.</typeparam> | /// <typeparam name="T">Primary target <see cref="Type"/> of the <see cref="TypeConverter"/>.</typeparam> | ||||
/// <param name="converter">The <see cref="TypeConverter"/> instance.</param> | /// <param name="converter">The <see cref="TypeConverter"/> instance.</param> | ||||
public void AddTypeConverter<T> (TypeConverter converter) => | |||||
AddTypeConverter(typeof(T), converter); | |||||
public void AddTypeConverter<T>(TypeConverter converter) => | |||||
_typeConverterMap.AddConcrete<T>(converter); | |||||
/// <summary> | /// <summary> | ||||
/// Add a concrete type <see cref="TypeConverter"/>. | /// Add a concrete type <see cref="TypeConverter"/>. | ||||
/// </summary> | /// </summary> | ||||
/// <param name="type">Primary target <see cref="Type"/> of the <see cref="TypeConverter"/>.</param> | /// <param name="type">Primary target <see cref="Type"/> of the <see cref="TypeConverter"/>.</param> | ||||
/// <param name="converter">The <see cref="TypeConverter"/> instance.</param> | /// <param name="converter">The <see cref="TypeConverter"/> instance.</param> | ||||
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); | |||||
/// <summary> | /// <summary> | ||||
/// Add a generic type <see cref="TypeConverter{T}"/>. | /// Add a generic type <see cref="TypeConverter{T}"/>. | ||||
@@ -831,58 +794,55 @@ namespace Discord.Interactions | |||||
/// <typeparam name="T">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeConverter{T}"/>.</typeparam> | /// <typeparam name="T">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeConverter{T}"/>.</typeparam> | ||||
/// <param name="converterType">Type of the <see cref="TypeConverter{T}"/>.</param> | /// <param name="converterType">Type of the <see cref="TypeConverter{T}"/>.</param> | ||||
public void AddGenericTypeConverter<T> (Type converterType) => | |||||
AddGenericTypeConverter(typeof(T), converterType); | |||||
public void AddGenericTypeConverter<T>(Type converterType) => | |||||
_typeConverterMap.AddGeneric<T>(converterType); | |||||
/// <summary> | /// <summary> | ||||
/// Add a generic type <see cref="TypeConverter{T}"/>. | /// Add a generic type <see cref="TypeConverter{T}"/>. | ||||
/// </summary> | /// </summary> | ||||
/// <param name="targetType">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeConverter{T}"/>.</param> | /// <param name="targetType">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeConverter{T}"/>.</param> | ||||
/// <param name="converterType">Type of the <see cref="TypeConverter{T}"/>.</param> | /// <param name="converterType">Type of the <see cref="TypeConverter{T}"/>.</param> | ||||
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}"); | |||||
/// <summary> | |||||
/// Add a concrete type <see cref="TypeReader"/>. | |||||
/// </summary> | |||||
/// <typeparam name="T">Primary target <see cref="Type"/> of the <see cref="TypeReader"/>.</typeparam> | |||||
/// <param name="reader">The <see cref="TypeReader"/> instance.</param> | |||||
public void AddTypeReader<T>(TypeReader reader) => | |||||
_typeReaderMap.AddConcrete<T>(reader); | |||||
/// <summary> | /// <summary> | ||||
/// Serialize an object using a <see cref="TypeReader"/> into a <see cref="string"/> to be placed in a Component CustomId. | |||||
/// Add a concrete type <see cref="TypeReader"/>. | |||||
/// </summary> | /// </summary> | ||||
/// <typeparam name="T">Type of the object to be serialized.</typeparam> | |||||
/// <param name="obj">Object to be serialized.</param> | |||||
/// <param name="services">Services that will be passed on to the TypeReader.</param> | |||||
/// <returns> | |||||
/// A task representing the conversion process. The task result contains the result of the conversion. | |||||
/// </returns> | |||||
public Task<string> SerializeValueAsync<T>(T obj, IServiceProvider services = null) => | |||||
_typeReaderMap.Get(typeof(T), services).SerializeAsync(obj); | |||||
/// <param name="type">Primary target <see cref="Type"/> of the <see cref="TypeReader"/>.</param> | |||||
/// <param name="reader">The <see cref="TypeReader"/> instance.</param> | |||||
public void AddTypeReader(Type type, TypeReader converter) => | |||||
_typeReaderMap.AddConcrete(type, converter); | |||||
/// <summary> | /// <summary> | ||||
/// Loads and caches an <see cref="ModalInfo"/> for the provided <see cref="IModal"/>. | |||||
/// Add a generic type <see cref="TypeReader{T}"/>. | |||||
/// </summary> | /// </summary> | ||||
/// <typeparam name="T">Type of <see cref="IModal"/> to be loaded.</typeparam> | |||||
/// <returns> | |||||
/// The built <see cref="ModalInfo"/> instance. | |||||
/// </returns> | |||||
/// <exception cref="InvalidOperationException"></exception> | |||||
public ModalInfo AddModalInfo<T>() where T : class, IModal | |||||
{ | |||||
var type = typeof(T); | |||||
/// <typeparam name="T">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeReader{T}"/>.</typeparam> | |||||
/// <param name="readerType">Type of the <see cref="TypeReader{T}"/>.</param> | |||||
if (_modalInfos.ContainsKey(type)) | |||||
throw new InvalidOperationException($"Modal type {type.FullName} already exists."); | |||||
public void AddGenericTypeReader<T>(Type readerType) => | |||||
_typeReaderMap.AddGeneric<T>(readerType); | |||||
return ModalUtils.GetOrAdd(type); | |||||
} | |||||
/// <summary> | |||||
/// Add a generic type <see cref="TypeReader{T}"/>. | |||||
/// </summary> | |||||
/// <param name="targetType">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeReader{T}"/>.</param> | |||||
/// <param name="readerType">Type of the <see cref="TypeReader{T}"/>.</param> | |||||
public void AddGenericTypeReader(Type targetType, Type readerType) => | |||||
_typeConverterMap.AddGeneric(targetType, readerType); | |||||
public string SerializeWithTypeReader<T>(object obj, IServiceProvider services = null) => | |||||
_typeReaderMap.Get(typeof(T), services)?.Serialize(obj); | |||||
internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null) | internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null) | ||||
{ | { | ||||
@@ -1043,21 +1003,6 @@ namespace Discord.Interactions | |||||
_lock.Dispose(); | _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() | private void EnsureClientReady() | ||||
{ | { | ||||
if (RestClient?.CurrentUser is null || RestClient?.CurrentUser?.Id == 0) | if (RestClient?.CurrentUser is null || RestClient?.CurrentUser?.Id == 0) | ||||
@@ -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<T> where T : class, ITypeHandler | |||||
{ | |||||
private readonly ConcurrentDictionary<Type, T> _concretes; | |||||
private readonly ConcurrentDictionary<Type, Type> _generics; | |||||
private readonly InteractionService _interactionService; | |||||
public TypeMap(InteractionService interactionService, IDictionary<Type, T> concretes = null, IDictionary<Type, Type> 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<T>.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<TTarget>(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<TTarget>(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; | |||||
} | |||||
} | |||||
} |
@@ -6,7 +6,7 @@ namespace Discord.Interactions | |||||
/// <summary> | /// <summary> | ||||
/// Base class for creating TypeConverters. <see cref="InteractionService"/> uses TypeConverters to interface with Slash Command parameters. | /// Base class for creating TypeConverters. <see cref="InteractionService"/> uses TypeConverters to interface with Slash Command parameters. | ||||
/// </summary> | /// </summary> | ||||
public abstract class TypeConverter | |||||
public abstract class TypeConverter : ITypeHandler | |||||
{ | { | ||||
/// <summary> | /// <summary> | ||||
/// Will be used to search for alternative TypeConverters whenever the Command Service encounters an unknown parameter type. | /// Will be used to search for alternative TypeConverters whenever the Command Service encounters an unknown parameter type. | ||||
@@ -0,0 +1,45 @@ | |||||
using System; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Interactions | |||||
{ | |||||
/// <summary> | |||||
/// A <see cref="TypeReader"/> for parsing objects implementing <see cref="IChannel"/>. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// This <see cref="TypeReader"/> is shipped with Discord.Net and is used by default to parse any | |||||
/// <see cref="IChannel"/> 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 <see cref="TypeReaderResult"/> is returned. | |||||
/// </remarks> | |||||
/// <typeparam name="T">The type to be checked; must implement <see cref="IChannel"/>.</typeparam> | |||||
public class ChannelTypeReader<T> : TypeReader<T> | |||||
where T : class, IChannel | |||||
{ | |||||
/// <inheritdoc /> | |||||
public override async Task<TypeConverterResult> 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(); | |||||
} | |||||
} |
@@ -0,0 +1,19 @@ | |||||
using System; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Interactions | |||||
{ | |||||
internal class EnumTypeReader<T> : TypeReader<T> where T : struct, Enum | |||||
{ | |||||
/// <inheritdoc /> | |||||
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, string input, IServiceProvider services) | |||||
{ | |||||
if (Enum.TryParse<T>(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(); | |||||
} | |||||
} |
@@ -0,0 +1,27 @@ | |||||
using System; | |||||
using System.Globalization; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Interactions | |||||
{ | |||||
/// <summary> | |||||
/// A <see cref="TypeReader"/> for parsing objects implementing <see cref="IMessage"/>. | |||||
/// </summary> | |||||
/// <typeparam name="T">The type to be checked; must implement <see cref="IMessage"/>.</typeparam> | |||||
public class MessageTypeReader<T> : TypeReader | |||||
where T : class, IMessage | |||||
{ | |||||
/// <inheritdoc /> | |||||
public override async Task<TypeReaderResult> 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."); | |||||
} | |||||
} | |||||
} |
@@ -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<T> : TypeReader | |||||
where T : struct | |||||
{ | |||||
private readonly TypeReader _baseTypeReader; | |||||
public NullableTypeReader(TypeReader baseTypeReader) | |||||
{ | |||||
_baseTypeReader = baseTypeReader; | |||||
} | |||||
/// <inheritdoc /> | |||||
public override async Task<TypeReaderResult> 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); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,48 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Globalization; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Commands | |||||
{ | |||||
/// <summary> | |||||
/// A <see cref="TypeReader"/> for parsing objects implementing <see cref="IRole"/>. | |||||
/// </summary> | |||||
/// <typeparam name="T">The type to be checked; must implement <see cref="IRole"/>.</typeparam> | |||||
public class RoleTypeReader<T> : TypeReader | |||||
where T : class, IRole | |||||
{ | |||||
/// <inheritdoc /> | |||||
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services) | |||||
{ | |||||
if (context.Guild != null) | |||||
{ | |||||
var results = new Dictionary<ulong, TypeReaderValue>(); | |||||
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<ulong, TypeReaderValue> results, T role, float score) | |||||
{ | |||||
if (role != null && !results.ContainsKey(role.Id)) | |||||
results.Add(role.Id, new TypeReaderValue(role, score)); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,55 @@ | |||||
using System; | |||||
using System.Globalization; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Commands | |||||
{ | |||||
internal class TimeSpanTypeReader : TypeReader | |||||
{ | |||||
/// <summary> | |||||
/// TimeSpan try parse formats. | |||||
/// </summary> | |||||
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 | |||||
}; | |||||
/// <inheritdoc /> | |||||
public override Task<TypeReaderResult> 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")); | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,43 @@ | |||||
using System; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Interactions | |||||
{ | |||||
/// <summary> | |||||
/// Base class for creating <see cref="TypeReader"/>s. <see cref="InteractionService"/> uses <see cref="TypeReader"/>s to parse string values into entities. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// <see cref="TypeReader"/>s are mainly used to parse message component values. For interfacing with Slash Command parameters use <see cref="TypeConverter"/>s instead. | |||||
/// </remarks> | |||||
public abstract class TypeReader : ITypeHandler | |||||
{ | |||||
/// <summary> | |||||
/// Will be used to search for alternative TypeReaders whenever the Command Service encounters an unknown parameter type. | |||||
/// </summary> | |||||
/// <param name="type"></param> | |||||
/// <returns></returns> | |||||
public abstract bool CanConvertTo(Type type); | |||||
/// <summary> | |||||
/// Will be used to read the incoming payload before executing the method body. | |||||
/// </summary> | |||||
/// <param name="context">Command exexution context.</param> | |||||
/// <param name="input">Raw string input value.</param> | |||||
/// <param name="services">Service provider that will be used to initialize the command module.</param> | |||||
/// <returns>The result of the read process.</returns> | |||||
public abstract Task<TypeConverterResult> ReadAsync(IInteractionContext context, object input, IServiceProvider services); | |||||
/// <summary> | |||||
/// Will be used to manipulate the outgoing command option, before the command gets registered to Discord. | |||||
/// </summary> | |||||
public virtual string Serialize(object value) => null; | |||||
} | |||||
/// <inheritdoc/> | |||||
public abstract class TypeReader<T> : TypeReader | |||||
{ | |||||
/// <inheritdoc/> | |||||
public sealed override bool CanConvertTo(Type type) => | |||||
typeof(T).IsAssignableFrom(type); | |||||
} | |||||
} |
@@ -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 | |||||
{ | |||||
/// <summary> | |||||
/// A <see cref="TypeReader"/> for parsing objects implementing <see cref="IUser"/>. | |||||
/// </summary> | |||||
/// <typeparam name="T">The type to be checked; must implement <see cref="IUser"/>.</typeparam> | |||||
public class UserTypeReader<T> : TypeReader | |||||
where T : class, IUser | |||||
{ | |||||
/// <inheritdoc /> | |||||
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services) | |||||
{ | |||||
var results = new Dictionary<ulong, TypeReaderValue>(); | |||||
IAsyncEnumerable<IUser> channelUsers = context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten(); // it's better | |||||
IReadOnlyCollection<IGuildUser> guildUsers = ImmutableArray.Create<IGuildUser>(); | |||||
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<ulong, TypeReaderValue> results, T user, float score) | |||||
{ | |||||
if (user != null && !results.ContainsKey(user.Id)) | |||||
results.Add(user.Id, new TypeReaderValue(user, score)); | |||||
} | |||||
} | |||||
} |