@@ -38,6 +38,9 @@ namespace Discord.Interactions.Builders | |||
/// </summary> | |||
Type Type { get; } | |||
/// <summary> | |||
/// Get the <see cref="ComponentTypeConverter"/> assigned to this input. | |||
/// </summary> | |||
ComponentTypeConverter TypeConverter { get; } | |||
/// <summary> | |||
@@ -33,6 +33,7 @@ namespace Discord.Interactions.Builders | |||
/// <inheritdoc/> | |||
public Type Type { get; private set; } | |||
/// <inheritdoc/> | |||
public ComponentTypeConverter TypeConverter { get; private set; } | |||
/// <inheritdoc/> | |||
@@ -446,8 +446,7 @@ namespace Discord.Interactions.Builders | |||
private static void BuildComponentParameter(ComponentCommandParameterBuilder builder, ParameterInfo paramInfo, bool isComponentParam) | |||
{ | |||
builder.SetAsRouteSegment(!isComponentParam); | |||
builder.SetIsRouteSegment(!isComponentParam); | |||
BuildParameter(builder, paramInfo); | |||
} | |||
@@ -4,13 +4,32 @@ namespace Discord.Interactions.Builders | |||
{ | |||
public class ComponentCommandParameterBuilder : ParameterBuilder<ComponentCommandParameterInfo, ComponentCommandParameterBuilder> | |||
{ | |||
/// <summary> | |||
/// Get the <see cref="ComponentTypeConverter"/> assigned to this parameter, if <see cref="IsRouteSegmentParameter"/> is <see langword="false"/>. | |||
/// </summary> | |||
public ComponentTypeConverter TypeConverter { get; private set; } | |||
/// <summary> | |||
/// Get the <see cref="Discord.Interactions.TypeReader"/> assigned to this parameter, if <see cref="IsRouteSegmentParameter"/> is <see langword="true"/>. | |||
/// </summary> | |||
public TypeReader TypeReader { get; private set; } | |||
/// <summary> | |||
/// Gets whether this parameter is a CustomId segment or a Component value parameter. | |||
/// </summary> | |||
public bool IsRouteSegmentParameter { get; private set; } | |||
/// <inheritdoc/> | |||
protected override ComponentCommandParameterBuilder Instance => this; | |||
public ComponentCommandParameterBuilder(ICommandBuilder command) : base(command) { } | |||
internal ComponentCommandParameterBuilder(ICommandBuilder command) : base(command) { } | |||
/// <summary> | |||
/// Initializes a new <see cref="ComponentCommandParameterBuilder"/>. | |||
/// </summary> | |||
/// <param name="command">Parent command of this parameter.</param> | |||
/// <param name="name">Name of this command.</param> | |||
/// <param name="type">Type of this parameter.</param> | |||
public ComponentCommandParameterBuilder(ICommandBuilder command, string name, Type type) : base(command, name, type) { } | |||
/// <inheritdoc/> | |||
@@ -36,7 +55,14 @@ namespace Discord.Interactions.Builders | |||
return this; | |||
} | |||
public ComponentCommandParameterBuilder SetAsRouteSegment(bool isRouteSegment) | |||
/// <summary> | |||
/// Sets <see cref="IsRouteSegmentParameter"/>. | |||
/// </summary> | |||
/// <param name="isRouteSegment">New value of the <see cref="IsRouteSegmentParameter"/>.</param> | |||
/// <returns> | |||
/// The builder instance. | |||
/// </returns> | |||
public ComponentCommandParameterBuilder SetIsRouteSegment(bool isRouteSegment) | |||
{ | |||
IsRouteSegmentParameter = isRouteSegment; | |||
return this; | |||
@@ -52,15 +52,18 @@ namespace Discord.Interactions | |||
if (additionalArgs is not null) | |||
args.AddRange(additionalArgs); | |||
var modalResult = await Modal.ParseModalAsync(context, services, Module.CommandService._exitOnMissingModalField).ConfigureAwait(false); | |||
var modalResult = await Modal.CreateModalAsync(context, services, Module.CommandService._exitOnMissingModalField).ConfigureAwait(false); | |||
if(!modalResult.IsSuccess || modalResult is not ParseResult parseResult) | |||
if(!modalResult.IsSuccess) | |||
{ | |||
await InvokeModuleEvent(context, modalResult).ConfigureAwait(false); | |||
return modalResult; | |||
} | |||
args.Add(parseResult.Value); | |||
if(modalResult is ParseResult parseResult) | |||
args.Add(parseResult.Value); | |||
else | |||
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason."); | |||
return await RunAsync(context, args.ToArray(), services); | |||
} | |||
@@ -39,6 +39,9 @@ namespace Discord.Interactions | |||
/// </summary> | |||
public Type Type { get; } | |||
/// <summary> | |||
/// Gets the <see cref="ComponentTypeConverter"/> assigned to this component. | |||
/// </summary> | |||
public ComponentTypeConverter TypeConverter { get; } | |||
/// <summary> | |||
@@ -62,10 +62,11 @@ namespace Discord.Interactions | |||
/// <summary> | |||
/// Creates an <see cref="IModal"/> and fills it with provided message components. | |||
/// </summary> | |||
/// <param name="components"><see cref="IModalInteraction"/> that will be injected into the modal.</param> | |||
/// <param name="modalInteraction"><see cref="IModalInteraction"/> that will be injected into the modal.</param> | |||
/// <returns> | |||
/// A <see cref="IModal"/> filled with the provided components. | |||
/// </returns> | |||
[Obsolete("This method is no longer supported with the introduction of Component TypeConverters, please use the CreateModalAsync method.")] | |||
public IModal CreateModal(IModalInteraction modalInteraction, bool throwOnMissingField = false) | |||
{ | |||
var args = new object[Components.Count]; | |||
@@ -90,10 +91,19 @@ namespace Discord.Interactions | |||
return _initializer(args); | |||
} | |||
internal async Task<IResult> ParseModalAsync(IInteractionContext context, IServiceProvider services = null, bool throwOnMissingField = false) | |||
/// <summary> | |||
/// Creates an <see cref="IModal"/> and fills it with provided message components. | |||
/// </summary> | |||
/// <param name="context">Context of the <see cref="IModalInteraction"/> that will be injected into the modal.</param> | |||
/// <param name="services">Services to be passed onto the <see cref="ComponentTypeConverter"/>s of the modal fiels.</param> | |||
/// <param name="throwOnMissingField">Wheter or not this method should exit on encountering a missing modal field.</param> | |||
/// <returns> | |||
/// A <see cref="TypeConverterResult"/> if a type conversion has failed, else a <see cref="ParseResult"/>. | |||
/// </returns> | |||
public async Task<IResult> CreateModalAsync(IInteractionContext context, IServiceProvider services = null, bool throwOnMissingField = false) | |||
{ | |||
if (context.Interaction is not IModalInteraction modalInteraction) | |||
throw new InvalidOperationException("Provided context doesn't belong to a Modal Interaction."); | |||
return ParseResult.FromError(InteractionCommandError.Unsuccessful, "Provided context doesn't belong to a Modal Interaction."); | |||
services ??= EmptyServiceProvider.Instance; | |||
@@ -110,7 +120,7 @@ namespace Discord.Interactions | |||
if (!throwOnMissingField) | |||
args[i] = input.DefaultValue; | |||
else | |||
throw new InvalidOperationException($"Modal interaction is missing the required field: {input.CustomId}"); | |||
return ParseResult.FromError(InteractionCommandError.BadArgs, $"Modal interaction is missing the required field: {input.CustomId}"); | |||
} | |||
else | |||
{ | |||
@@ -1,5 +1,4 @@ | |||
using Discord.Interactions.Builders; | |||
using System; | |||
namespace Discord.Interactions | |||
{ | |||
@@ -10,10 +9,19 @@ namespace Discord.Interactions | |||
{ | |||
/// <summary> | |||
/// Gets the <see cref="ComponentTypeConverter"/> that will be used to convert a message component value into | |||
/// <see cref="CommandParameterInfo.ParameterType"/>. | |||
/// <see cref="CommandParameterInfo.ParameterType"/>, if <see cref="IsRouteSegmentParameter"/> is false. | |||
/// </summary> | |||
public ComponentTypeConverter TypeConverter { get; } | |||
/// <summary> | |||
/// Gets the <see cref="TypeReader"/> that will be used to convert a CustomId segment value into | |||
/// <see cref="CommandParameterInfo.ParameterType"/>, if <see cref="IsRouteSegmentParameter"/> is <see langword="true"/>. | |||
/// </summary> | |||
public TypeReader TypeReader { get; } | |||
/// <summary> | |||
/// Gets whether this parameter is a CustomId segment or a component value parameter. | |||
/// </summary> | |||
public bool IsRouteSegmentParameter { get; } | |||
internal ComponentCommandParameterInfo(ComponentCommandParameterBuilder builder, ICommandInfo command) : base(builder, command) | |||
@@ -181,30 +181,30 @@ namespace Discord.Interactions | |||
_autoServiceScopes = config.AutoServiceScopes; | |||
_restResponseCallback = config.RestResponseCallback; | |||
_typeConverterMap = new TypeMap<TypeConverter, IApplicationCommandInteractionDataOption>(this, new Dictionary<Type, TypeConverter> | |||
{ | |||
[typeof(TimeSpan)] = new TimeSpanConverter() | |||
}, new Dictionary<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<>), | |||
}); | |||
_compTypeConverterMap = new TypeMap<ComponentTypeConverter, IComponentInteractionData>(this, new Dictionary<Type, ComponentTypeConverter>(), | |||
new Dictionary<Type, Type> | |||
_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<>), | |||
}); | |||
_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 Dictionary<Type, TypeReader>(), | |||
new Dictionary<Type, Type> | |||
_typeReaderMap = new TypeMap<TypeReader, string>(this, new ConcurrentDictionary<Type, TypeReader>(), | |||
new ConcurrentDictionary<Type, Type> | |||
{ | |||
[typeof(IChannel)] = typeof(DefaultChannelReader<>), | |||
[typeof(IRole)] = typeof(DefaultRoleReader<>), | |||
@@ -827,56 +827,84 @@ namespace Discord.Interactions | |||
_compTypeConverterMap.Get(type, services); | |||
/// <summary> | |||
/// Add a concrete type <see cref="TypeReader"/>. | |||
/// Add a concrete type <see cref="ComponentTypeConverter"/>. | |||
/// </summary> | |||
/// <typeparam name="T">Primary target <see cref="Type"/> of the <see cref="TypeReader"/>.</typeparam> | |||
/// <param name="converter">The <see cref="TypeReader"/> instance.</param> | |||
/// <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="TypeReader"/>. | |||
/// Add a concrete type <see cref="ComponentTypeConverter"/>. | |||
/// </summary> | |||
/// <param name="type">Primary target <see cref="Type"/> of the <see cref="TypeReader"/>.</param> | |||
/// <param name="converter">The <see cref="TypeReader"/> instance.</param> | |||
/// <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="CompTypeConverter{T}"/>. | |||
/// Add a generic type <see cref="ComponentTypeConverter{T}"/>. | |||
/// </summary> | |||
/// <typeparam name="T">Generic Type constraint of the <see cref="Type"/> of the <see cref="CompTypeConverter{T}"/>.</typeparam> | |||
/// <param name="converterType">Type of the <see cref="CompTypeConverter{T}"/>.</param> | |||
/// <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="CompTypeConverter{T}"/>. | |||
/// Add a generic type <see cref="ComponentTypeConverter{T}"/>. | |||
/// </summary> | |||
/// <param name="targetType">Generic Type constraint of the <see cref="Type"/> of the <see cref="CompTypeConverter{T}"/>.</param> | |||
/// <param name="converterType">Type of the <see cref="CompTypeConverter{T}"/>.</param> | |||
/// <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); | |||
public Task<string> SerializeValue<T>(T obj, IServiceProvider services = null) => | |||
_compTypeConverterMap.Get(typeof(T), services).SerializeAsync(obj); | |||
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="converter">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="converter">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 TypeReader.</param> | |||
/// <returns> | |||
/// A task representing the conversion process. The task result contains the result of the conversion. | |||
/// </returns> | |||
public Task<string> SerializeValue<T>(T obj, IServiceProvider services = null) => | |||
_typeReaderMap.Get(typeof(T), services).SerializeAsync(obj); | |||
/// <summary> | |||
/// Loads and caches an <see cref="ModalInfo"/> for the provided <see cref="IModal"/>. | |||
/// </summary> | |||
@@ -3,15 +3,36 @@ using System.Threading.Tasks; | |||
namespace Discord.Interactions | |||
{ | |||
/// <summary> | |||
/// Base class for creating Component TypeConverters. <see cref="InteractionService"/> uses TypeConverters to interface with Slash Command parameters. | |||
/// </summary> | |||
public abstract class ComponentTypeConverter : ITypeConverter<IComponentInteractionData> | |||
{ | |||
/// <summary> | |||
/// Will be used to search for alternative TypeConverters whenever the Command Service encounters an unknown parameter type. | |||
/// </summary> | |||
/// <param name="type">An object type.</param> | |||
/// <returns> | |||
/// The boolean result. | |||
/// </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="option">Recieved option payload.</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, IComponentInteractionData option, IServiceProvider services); | |||
public virtual Task<string> SerializeAsync(object obj) => Task.FromResult(obj.ToString()); | |||
} | |||
/// <inheritdoc/> | |||
public abstract class ComponentTypeConverter<T> : ComponentTypeConverter | |||
{ | |||
/// <inheritdoc/> | |||
public sealed override bool CanConvertTo(Type type) => | |||
typeof(T).IsAssignableFrom(type); | |||
} | |||
@@ -1,7 +1,5 @@ | |||
using System; | |||
using System.Collections; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Discord.Interactions | |||
@@ -20,6 +20,8 @@ namespace Discord.Interactions | |||
else | |||
return TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option} must be a valid {typeof(T).Name} snowflake to be parsed."); | |||
} | |||
public override Task<string> SerializeAsync(object obj) => Task.FromResult((obj as ISnowflakeEntity)?.Id.ToString()); | |||
} | |||
internal sealed class DefaultUserReader<T> : DefaultSnowflakeReader<T> | |||
@@ -1,27 +0,0 @@ | |||
using System; | |||
using System.Threading.Tasks; | |||
namespace Discord.Interactions | |||
{ | |||
internal sealed class EnumTypeReader<T> : TypeReader<T> | |||
where T : struct, Enum | |||
{ | |||
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, string option, IServiceProvider services) | |||
{ | |||
if (Enum.TryParse<T>(option, out var result)) | |||
return Task.FromResult(TypeConverterResult.FromSuccess(result)); | |||
else | |||
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Value {option} cannot be converted to {nameof(T)}")); | |||
} | |||
public override Task<string> SerializeAsync(object obj) | |||
{ | |||
var name = Enum.GetName(typeof(T), obj); | |||
if (name is null) | |||
throw new ArgumentException($"Enum name cannot be parsed from {obj}"); | |||
return Task.FromResult(name); | |||
} | |||
} | |||
} |
@@ -3,15 +3,43 @@ using System.Threading.Tasks; | |||
namespace Discord.Interactions | |||
{ | |||
/// <summary> | |||
/// Base class for creating TypeConverters. <see cref="InteractionService"/> uses TypeConverters to interface with Slash Command parameters. | |||
/// </summary> | |||
public abstract class TypeReader : ITypeConverter<string> | |||
{ | |||
/// <summary> | |||
/// Will be used to search for alternative TypeReaders whenever the Command Service encounters an unknown parameter type. | |||
/// </summary> | |||
/// <param name="type">An object type.</param> | |||
/// <returns> | |||
/// The boolean result. | |||
/// </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="option">Recieved option payload.</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, string option, IServiceProvider services); | |||
/// <summary> | |||
/// Will be used to serialize objects into strings. | |||
/// </summary> | |||
/// <param name="obj">Object to be serialized.</param> | |||
/// <returns> | |||
/// A task represting the conversion process. The result of the task contains the conversion result. | |||
/// </returns> | |||
public virtual Task<string> SerializeAsync(object obj) => Task.FromResult(obj.ToString()); | |||
} | |||
/// <inheritdoc/> | |||
public abstract class TypeReader<T> : TypeReader | |||
{ | |||
/// <inheritdoc/> | |||
public sealed override bool CanConvertTo(Type type) => | |||
typeof(T).IsAssignableFrom(type); | |||
} | |||