@@ -5,7 +5,7 @@ namespace Discord.Interactions.Builders | |||
/// <summary> | |||
/// Represents a builder for creating <see cref="ComponentCommandInfo"/>. | |||
/// </summary> | |||
public sealed class ComponentCommandBuilder : CommandBuilder<ComponentCommandInfo, ComponentCommandBuilder, CommandParameterBuilder> | |||
public sealed class ComponentCommandBuilder : CommandBuilder<ComponentCommandInfo, ComponentCommandBuilder, ComponentCommandParameterBuilder> | |||
{ | |||
protected override ComponentCommandBuilder Instance => this; | |||
@@ -26,9 +26,9 @@ namespace Discord.Interactions.Builders | |||
/// <returns> | |||
/// The builder instance. | |||
/// </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); | |||
AddParameters(parameter); | |||
return this; | |||
@@ -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; | |||
} | |||
@@ -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> | |||
/// Represents the info class of an attribute based method for handling Component Interaction events. | |||
/// </summary> | |||
public class ComponentCommandInfo : CommandInfo<CommandParameterInfo> | |||
public class ComponentCommandInfo : CommandInfo<ComponentCommandParameterInfo> | |||
{ | |||
/// <inheritdoc/> | |||
public override IReadOnlyCollection<CommandParameterInfo> Parameters { get; } | |||
public override IReadOnlyCollection<ComponentCommandParameterInfo> Parameters { get; } | |||
/// <inheritdoc/> | |||
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); | |||
} | |||
@@ -66,8 +66,8 @@ namespace Discord.Interactions | |||
private readonly CommandMap<AutocompleteCommandInfo> _autocompleteCommandMap; | |||
private readonly CommandMap<ModalCommandInfo> _modalCommandMap; | |||
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, ModalInfo> _modalInfos = new(); | |||
private readonly SemaphoreSlim _lock; | |||
@@ -179,7 +179,10 @@ namespace Discord.Interactions | |||
_autoServiceScopes = config.AutoServiceScopes; | |||
_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(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<Type, TypeConverter> | |||
{ | |||
[typeof(TimeSpan)] = new TimeSpanConverter() | |||
}; | |||
_typeReaderMap = new TypeMap<TypeReader>(this); | |||
} | |||
/// <summary> | |||
@@ -769,61 +769,24 @@ namespace Discord.Interactions | |||
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> | |||
/// Add a concrete type <see cref="TypeConverter"/>. | |||
/// </summary> | |||
/// <typeparam name="T">Primary target <see cref="Type"/> of the <see cref="TypeConverter"/>.</typeparam> | |||
/// <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> | |||
/// Add a concrete type <see cref="TypeConverter"/>. | |||
/// </summary> | |||
/// <param name="type">Primary target <see cref="Type"/> of the <see cref="TypeConverter"/>.</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> | |||
/// 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> | |||
/// <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> | |||
/// Add a generic type <see cref="TypeConverter{T}"/>. | |||
/// </summary> | |||
/// <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> | |||
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> | |||
/// 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> | |||
/// <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> | |||
/// Loads and caches an <see cref="ModalInfo"/> for the provided <see cref="IModal"/>. | |||
/// Add a generic type <see cref="TypeReader{T}"/>. | |||
/// </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) | |||
{ | |||
@@ -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) | |||
@@ -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> | |||
/// Base class for creating TypeConverters. <see cref="InteractionService"/> uses TypeConverters to interface with Slash Command parameters. | |||
/// </summary> | |||
public abstract class TypeConverter | |||
public abstract class TypeConverter : ITypeHandler | |||
{ | |||
/// <summary> | |||
/// 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)); | |||
} | |||
} | |||
} |