Feature: Component TypeConverters and CustomID TypeReaders (#2169)
* fix sharded client current user * add custom setter to group property of module builder * rename serilazation method * init * create typemap and default typereaders * add default readers * create typereader targetting flags * seperate custom id readers with component typeconverters * add typereaders * add customid readers * clean up component info argument parsing * remove obsolete method * add component typeconverters to modals * fix build errors * add inline docs * bug fixes * code cleanup and refactorings * fix build errors * add GenerateCustomIdString method to interaction service * add GenerateCustomIdString method to interaction service * add inline docs to componentparameterbuilder * add inline docs to GenerateCustomIdStringAsync method
This commit is contained in:
@@ -3,6 +3,7 @@ using Discord.Logging;
|
||||
using Discord.Rest;
|
||||
using Discord.WebSocket;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -66,8 +67,9 @@ namespace Discord.Interactions
|
||||
private readonly CommandMap<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, IApplicationCommandInteractionDataOption> _typeConverterMap;
|
||||
private readonly TypeMap<ComponentTypeConverter, IComponentInteractionData> _compTypeConverterMap;
|
||||
private readonly TypeMap<TypeReader, string> _typeReaderMap;
|
||||
private readonly ConcurrentDictionary<Type, IAutocompleteHandler> _autocompleteHandlers = new();
|
||||
private readonly ConcurrentDictionary<Type, ModalInfo> _modalInfos = new();
|
||||
private readonly SemaphoreSlim _lock;
|
||||
@@ -179,22 +181,38 @@ namespace Discord.Interactions
|
||||
_autoServiceScopes = config.AutoServiceScopes;
|
||||
_restResponseCallback = config.RestResponseCallback;
|
||||
|
||||
_genericTypeConverters = new ConcurrentDictionary<Type, Type>
|
||||
{
|
||||
[typeof(IChannel)] = typeof(DefaultChannelConverter<>),
|
||||
[typeof(IRole)] = typeof(DefaultRoleConverter<>),
|
||||
[typeof(IAttachment)] = typeof(DefaultAttachmentConverter<>),
|
||||
[typeof(IUser)] = typeof(DefaultUserConverter<>),
|
||||
[typeof(IMentionable)] = typeof(DefaultMentionableConverter<>),
|
||||
[typeof(IConvertible)] = typeof(DefaultValueConverter<>),
|
||||
[typeof(Enum)] = typeof(EnumConverter<>),
|
||||
[typeof(Nullable<>)] = typeof(NullableConverter<>),
|
||||
};
|
||||
_typeConverterMap = new TypeMap<TypeConverter, IApplicationCommandInteractionDataOption>(this, new ConcurrentDictionary<Type, TypeConverter>
|
||||
{
|
||||
[typeof(TimeSpan)] = new TimeSpanConverter()
|
||||
}, new ConcurrentDictionary<Type, Type>
|
||||
{
|
||||
[typeof(IChannel)] = typeof(DefaultChannelConverter<>),
|
||||
[typeof(IRole)] = typeof(DefaultRoleConverter<>),
|
||||
[typeof(IAttachment)] = typeof(DefaultAttachmentConverter<>),
|
||||
[typeof(IUser)] = typeof(DefaultUserConverter<>),
|
||||
[typeof(IMentionable)] = typeof(DefaultMentionableConverter<>),
|
||||
[typeof(IConvertible)] = typeof(DefaultValueConverter<>),
|
||||
[typeof(Enum)] = typeof(EnumConverter<>),
|
||||
[typeof(Nullable<>)] = typeof(NullableConverter<>)
|
||||
});
|
||||
|
||||
_typeConverters = new ConcurrentDictionary<Type, TypeConverter>
|
||||
{
|
||||
[typeof(TimeSpan)] = new TimeSpanConverter()
|
||||
};
|
||||
_compTypeConverterMap = new TypeMap<ComponentTypeConverter, IComponentInteractionData>(this, new ConcurrentDictionary<Type, ComponentTypeConverter>(),
|
||||
new ConcurrentDictionary<Type, Type>
|
||||
{
|
||||
[typeof(Array)] = typeof(DefaultArrayComponentConverter<>),
|
||||
[typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>)
|
||||
});
|
||||
|
||||
_typeReaderMap = new TypeMap<TypeReader, string>(this, new ConcurrentDictionary<Type, TypeReader>(),
|
||||
new ConcurrentDictionary<Type, Type>
|
||||
{
|
||||
[typeof(IChannel)] = typeof(DefaultChannelReader<>),
|
||||
[typeof(IRole)] = typeof(DefaultRoleReader<>),
|
||||
[typeof(IUser)] = typeof(DefaultUserReader<>),
|
||||
[typeof(IMessage)] = typeof(DefaultMessageReader<>),
|
||||
[typeof(IConvertible)] = typeof(DefaultValueReader<>),
|
||||
[typeof(Enum)] = typeof(EnumReader<>)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -293,7 +311,7 @@ namespace Discord.Interactions
|
||||
public async Task<ModuleInfo> AddModuleAsync (Type type, IServiceProvider services)
|
||||
{
|
||||
if (!typeof(IInteractionModuleBase).IsAssignableFrom(type))
|
||||
throw new ArgumentException("Type parameter must be a type of Slash Module", "T");
|
||||
throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(type));
|
||||
|
||||
services ??= EmptyServiceProvider.Instance;
|
||||
|
||||
@@ -326,7 +344,7 @@ namespace Discord.Interactions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register Application Commands from <see cref="ContextCommands"/> and <see cref="SlashCommands"/> to a guild.
|
||||
/// Register Application Commands from <see cref="ContextCommands"/> and <see cref="SlashCommands"/> to a guild.
|
||||
/// </summary>
|
||||
/// <param name="guildId">Id of the target guild.</param>
|
||||
/// <param name="deleteMissing">If <see langword="false"/>, this operation will not delete the commands that are missing from <see cref="InteractionService"/>.</param>
|
||||
@@ -422,7 +440,7 @@ namespace Discord.Interactions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register Application Commands from modules provided in <paramref name="modules"/> to a guild.
|
||||
/// Register Application Commands from modules provided in <paramref name="modules"/> to a guild.
|
||||
/// </summary>
|
||||
/// <param name="guild">The target guild.</param>
|
||||
/// <param name="modules">Modules to be registered to Discord.</param>
|
||||
@@ -449,7 +467,7 @@ namespace Discord.Interactions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register Application Commands from modules provided in <paramref name="modules"/> as global commands.
|
||||
/// Register Application Commands from modules provided in <paramref name="modules"/> as global commands.
|
||||
/// </summary>
|
||||
/// <param name="modules">Modules to be registered to Discord.</param>
|
||||
/// <returns>
|
||||
@@ -677,7 +695,7 @@ namespace Discord.Interactions
|
||||
public async Task<IResult> ExecuteCommandAsync (IInteractionContext context, IServiceProvider services)
|
||||
{
|
||||
var interaction = context.Interaction;
|
||||
|
||||
|
||||
return interaction switch
|
||||
{
|
||||
ISlashCommandInteraction slashCommand => await ExecuteSlashCommandAsync(context, slashCommand, services).ConfigureAwait(false),
|
||||
@@ -781,47 +799,24 @@ namespace Discord.Interactions
|
||||
return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal TypeConverter GetTypeConverter (Type type, IServiceProvider services = null)
|
||||
{
|
||||
if (_typeConverters.TryGetValue(type, out var specific))
|
||||
return specific;
|
||||
else if (_genericTypeConverters.Any(x => x.Key.IsAssignableFrom(type)
|
||||
|| (x.Key.IsGenericTypeDefinition && type.IsGenericType && x.Key.GetGenericTypeDefinition() == type.GetGenericTypeDefinition())))
|
||||
{
|
||||
services ??= EmptyServiceProvider.Instance;
|
||||
|
||||
var converterType = GetMostSpecificTypeConverter(type);
|
||||
var converter = ReflectionUtils<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}"/>.
|
||||
@@ -829,30 +824,121 @@ 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)
|
||||
public void AddGenericTypeConverter(Type targetType, Type converterType) =>
|
||||
_typeConverterMap.AddGeneric(targetType, converterType);
|
||||
|
||||
internal ComponentTypeConverter GetComponentTypeConverter(Type type, IServiceProvider services = null) =>
|
||||
_compTypeConverterMap.Get(type, services);
|
||||
|
||||
/// <summary>
|
||||
/// Add a concrete type <see cref="ComponentTypeConverter"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Primary target <see cref="Type"/> of the <see cref="ComponentTypeConverter"/>.</typeparam>
|
||||
/// <param name="converter">The <see cref="ComponentTypeConverter"/> instance.</param>
|
||||
public void AddComponentTypeConverter<T>(ComponentTypeConverter converter) =>
|
||||
AddComponentTypeConverter(typeof(T), converter);
|
||||
|
||||
/// <summary>
|
||||
/// Add a concrete type <see cref="ComponentTypeConverter"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">Primary target <see cref="Type"/> of the <see cref="ComponentTypeConverter"/>.</param>
|
||||
/// <param name="converter">The <see cref="ComponentTypeConverter"/> instance.</param>
|
||||
public void AddComponentTypeConverter(Type type, ComponentTypeConverter converter) =>
|
||||
_compTypeConverterMap.AddConcrete(type, converter);
|
||||
|
||||
/// <summary>
|
||||
/// Add a generic type <see cref="ComponentTypeConverter{T}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Generic Type constraint of the <see cref="Type"/> of the <see cref="ComponentTypeConverter{T}"/>.</typeparam>
|
||||
/// <param name="converterType">Type of the <see cref="ComponentTypeConverter{T}"/>.</param>
|
||||
public void AddGenericComponentTypeConverter<T>(Type converterType) =>
|
||||
AddGenericComponentTypeConverter(typeof(T), converterType);
|
||||
|
||||
/// <summary>
|
||||
/// Add a generic type <see cref="ComponentTypeConverter{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="targetType">Generic Type constraint of the <see cref="Type"/> of the <see cref="ComponentTypeConverter{T}"/>.</param>
|
||||
/// <param name="converterType">Type of the <see cref="ComponentTypeConverter{T}"/>.</param>
|
||||
public void AddGenericComponentTypeConverter(Type targetType, Type converterType) =>
|
||||
_compTypeConverterMap.AddGeneric(targetType, converterType);
|
||||
|
||||
internal TypeReader GetTypeReader(Type type, IServiceProvider services = null) =>
|
||||
_typeReaderMap.Get(type, services);
|
||||
|
||||
/// <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) =>
|
||||
AddTypeReader(typeof(T), reader);
|
||||
|
||||
/// <summary>
|
||||
/// Add a concrete type <see cref="TypeReader"/>.
|
||||
/// </summary>
|
||||
/// <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 reader) =>
|
||||
_typeReaderMap.AddConcrete(type, reader);
|
||||
|
||||
/// <summary>
|
||||
/// Add a generic type <see cref="TypeReader{T}"/>.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
public void AddGenericTypeReader<T>(Type readerType) =>
|
||||
AddGenericTypeReader(typeof(T), readerType);
|
||||
|
||||
/// <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) =>
|
||||
_typeReaderMap.AddGeneric(targetType, readerType);
|
||||
|
||||
/// <summary>
|
||||
/// Serialize an object using a <see cref="TypeReader"/> into a <see cref="string"/> to be placed in a Component CustomId.
|
||||
/// </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 <see cref="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) =>
|
||||
_typeReaderMap.Get(typeof(T), services).SerializeAsync(obj, services);
|
||||
|
||||
/// <summary>
|
||||
/// Serialize and format multiple objects into a Custom Id string.
|
||||
/// </summary>
|
||||
/// <param name="format">A composite format string.</param>
|
||||
/// <param name="services">>Services that will be passed on to the <see cref="TypeReader"/>s.</param>
|
||||
/// <param name="args">Objects to be serialized.</param>
|
||||
/// <returns>
|
||||
/// A task representing the conversion process. The task result contains the result of the conversion.
|
||||
/// </returns>
|
||||
public async Task<string> GenerateCustomIdStringAsync(string format, IServiceProvider services, params object[] args)
|
||||
{
|
||||
if (!converterType.IsGenericTypeDefinition)
|
||||
throw new ArgumentException($"{converterType.FullName} is not generic.");
|
||||
var serializedValues = new string[args.Length];
|
||||
|
||||
var genericArguments = converterType.GetGenericArguments();
|
||||
for(var i = 0; i < args.Length; i++)
|
||||
{
|
||||
var arg = args[i];
|
||||
var typeReader = _typeReaderMap.Get(arg.GetType(), null);
|
||||
var result = await typeReader.SerializeAsync(arg, services).ConfigureAwait(false);
|
||||
serializedValues[i] = result;
|
||||
}
|
||||
|
||||
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}");
|
||||
|
||||
_genericTypeConverters[targetType] = converterType;
|
||||
return string.Format(format, serializedValues);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -870,7 +956,7 @@ namespace Discord.Interactions
|
||||
if (_modalInfos.ContainsKey(type))
|
||||
throw new InvalidOperationException($"Modal type {type.FullName} already exists.");
|
||||
|
||||
return ModalUtils.GetOrAdd(type);
|
||||
return ModalUtils.GetOrAdd(type, this);
|
||||
}
|
||||
|
||||
internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null)
|
||||
@@ -1016,7 +1102,7 @@ namespace Discord.Interactions
|
||||
public ModuleInfo GetModuleInfo<TModule> ( ) where TModule : class
|
||||
{
|
||||
if (!typeof(IInteractionModuleBase).IsAssignableFrom(typeof(TModule)))
|
||||
throw new ArgumentException("Type parameter must be a type of Slash Module", "TModule");
|
||||
throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(TModule));
|
||||
|
||||
var module = _typedModuleDefs[typeof(TModule)];
|
||||
|
||||
@@ -1032,21 +1118,6 @@ namespace Discord.Interactions
|
||||
_lock.Dispose();
|
||||
}
|
||||
|
||||
private Type GetMostSpecificTypeConverter (Type type)
|
||||
{
|
||||
if (_genericTypeConverters.TryGetValue(type, out var matching))
|
||||
return matching;
|
||||
|
||||
if (type.IsGenericType && _genericTypeConverters.TryGetValue(type.GetGenericTypeDefinition(), out var genericDefinition))
|
||||
return genericDefinition;
|
||||
|
||||
var typeInterfaces = type.GetInterfaces();
|
||||
var candidates = _genericTypeConverters.Where(x => x.Key.IsAssignableFrom(type))
|
||||
.OrderByDescending(x => typeInterfaces.Count(y => y.IsAssignableFrom(x.Key)));
|
||||
|
||||
return candidates.First().Value;
|
||||
}
|
||||
|
||||
private void EnsureClientReady()
|
||||
{
|
||||
if (RestClient?.CurrentUser is null || RestClient?.CurrentUser?.Id == 0)
|
||||
|
||||
Reference in New Issue
Block a user