diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs index 77569718a..ad2f07c73 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs @@ -38,6 +38,9 @@ namespace Discord.Interactions.Builders /// Type Type { get; } + /// + /// Get the assigned to this input. + /// ComponentTypeConverter TypeConverter { get; } /// diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs index 19674ac4d..7d1d96712 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs @@ -33,6 +33,7 @@ namespace Discord.Interactions.Builders /// public Type Type { get; private set; } + /// public ComponentTypeConverter TypeConverter { get; private set; } /// diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index ac727496c..07e62b1f1 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -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); } diff --git a/src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs b/src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs index 81f4d05c8..9bdf8539e 100644 --- a/src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs @@ -4,13 +4,32 @@ namespace Discord.Interactions.Builders { public class ComponentCommandParameterBuilder : ParameterBuilder { + /// + /// Get the assigned to this parameter, if is . + /// public ComponentTypeConverter TypeConverter { get; private set; } + + /// + /// Get the assigned to this parameter, if is . + /// public TypeReader TypeReader { get; private set; } + + /// + /// Gets whether this parameter is a CustomId segment or a Component value parameter. + /// public bool IsRouteSegmentParameter { get; private set; } + + /// protected override ComponentCommandParameterBuilder Instance => this; - public ComponentCommandParameterBuilder(ICommandBuilder command) : base(command) { } + internal ComponentCommandParameterBuilder(ICommandBuilder command) : base(command) { } + /// + /// Initializes a new . + /// + /// Parent command of this parameter. + /// Name of this command. + /// Type of this parameter. public ComponentCommandParameterBuilder(ICommandBuilder command, string name, Type type) : base(command, name, type) { } /// @@ -36,7 +55,14 @@ namespace Discord.Interactions.Builders return this; } - public ComponentCommandParameterBuilder SetAsRouteSegment(bool isRouteSegment) + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public ComponentCommandParameterBuilder SetIsRouteSegment(bool isRouteSegment) { IsRouteSegmentParameter = isRouteSegment; return this; diff --git a/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs index 40f93ebfe..2b9306fda 100644 --- a/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs @@ -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); } diff --git a/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs index e457562a7..05695f862 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs @@ -39,6 +39,9 @@ namespace Discord.Interactions /// public Type Type { get; } + /// + /// Gets the assigned to this component. + /// public ComponentTypeConverter TypeConverter { get; } /// diff --git a/src/Discord.Net.Interactions/Info/ModalInfo.cs b/src/Discord.Net.Interactions/Info/ModalInfo.cs index 087551df9..5130c26a1 100644 --- a/src/Discord.Net.Interactions/Info/ModalInfo.cs +++ b/src/Discord.Net.Interactions/Info/ModalInfo.cs @@ -62,10 +62,11 @@ namespace Discord.Interactions /// /// Creates an and fills it with provided message components. /// - /// that will be injected into the modal. + /// that will be injected into the modal. /// /// A filled with the provided components. /// + [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 ParseModalAsync(IInteractionContext context, IServiceProvider services = null, bool throwOnMissingField = false) + /// + /// Creates an and fills it with provided message components. + /// + /// Context of the that will be injected into the modal. + /// Services to be passed onto the s of the modal fiels. + /// Wheter or not this method should exit on encountering a missing modal field. + /// + /// A if a type conversion has failed, else a . + /// + public async Task 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 { diff --git a/src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs b/src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs index d281f681a..36b75ddb7 100644 --- a/src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs +++ b/src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs @@ -1,5 +1,4 @@ using Discord.Interactions.Builders; -using System; namespace Discord.Interactions { @@ -10,10 +9,19 @@ namespace Discord.Interactions { /// /// Gets the that will be used to convert a message component value into - /// . + /// , if is false. /// public ComponentTypeConverter TypeConverter { get; } + + /// + /// Gets the that will be used to convert a CustomId segment value into + /// , if is . + /// public TypeReader TypeReader { get; } + + /// + /// Gets whether this parameter is a CustomId segment or a component value parameter. + /// public bool IsRouteSegmentParameter { get; } internal ComponentCommandParameterInfo(ComponentCommandParameterBuilder builder, ICommandInfo command) : base(builder, command) diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index ec1c65318..1b7521828 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -181,30 +181,30 @@ namespace Discord.Interactions _autoServiceScopes = config.AutoServiceScopes; _restResponseCallback = config.RestResponseCallback; - _typeConverterMap = new TypeMap(this, new Dictionary - { - [typeof(TimeSpan)] = new TimeSpanConverter() - }, new Dictionary - { - [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(this, new Dictionary(), - new Dictionary + _typeConverterMap = new TypeMap(this, new ConcurrentDictionary + { + [typeof(TimeSpan)] = new TimeSpanConverter() + }, new ConcurrentDictionary + { + [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(this, new ConcurrentDictionary(), + new ConcurrentDictionary { [typeof(Array)] = typeof(DefaultArrayComponentConverter<>), [typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>) }); - _typeReaderMap = new TypeMap(this, new Dictionary(), - new Dictionary + _typeReaderMap = new TypeMap(this, new ConcurrentDictionary(), + new ConcurrentDictionary { [typeof(IChannel)] = typeof(DefaultChannelReader<>), [typeof(IRole)] = typeof(DefaultRoleReader<>), @@ -827,56 +827,84 @@ namespace Discord.Interactions _compTypeConverterMap.Get(type, services); /// - /// Add a concrete type . + /// Add a concrete type . /// - /// Primary target of the . - /// The instance. + /// Primary target of the . + /// The instance. public void AddComponentTypeConverter(ComponentTypeConverter converter) => AddComponentTypeConverter(typeof(T), converter); /// - /// Add a concrete type . + /// Add a concrete type . /// - /// Primary target of the . - /// The instance. + /// Primary target of the . + /// The instance. public void AddComponentTypeConverter(Type type, ComponentTypeConverter converter) => _compTypeConverterMap.AddConcrete(type, converter); /// - /// Add a generic type . + /// Add a generic type . /// - /// Generic Type constraint of the of the . - /// Type of the . - + /// Generic Type constraint of the of the . + /// Type of the . public void AddGenericComponentTypeConverter(Type converterType) => AddGenericComponentTypeConverter(typeof(T), converterType); /// - /// Add a generic type . + /// Add a generic type . /// - /// Generic Type constraint of the of the . - /// Type of the . + /// Generic Type constraint of the of the . + /// Type of the . public void AddGenericComponentTypeConverter(Type targetType, Type converterType) => _compTypeConverterMap.AddGeneric(targetType, converterType); - public Task SerializeValue(T obj, IServiceProvider services = null) => - _compTypeConverterMap.Get(typeof(T), services).SerializeAsync(obj); - internal TypeReader GetTypeReader(Type type, IServiceProvider services = null) => _typeReaderMap.Get(type, services); + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. public void AddTypeReader(TypeReader reader) => AddTypeReader(typeof(T), reader); + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. public void AddTypeReader(Type type, TypeReader reader) => _typeReaderMap.AddConcrete(type, reader); + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . public void AddGenericTypeReader(Type readerType) => AddGenericTypeReader(typeof(T), readerType); + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . public void AddGenericTypeReader(Type targetType, Type readerType) => _typeReaderMap.AddGeneric(targetType, readerType); + /// + /// Serialize an object using a into a to be placed in a Component CustomId. + /// + /// Type of the object to be serialized. + /// Object to be serialized. + /// Services that will be passed on to the TypeReader. + /// + /// A task representing the conversion process. The task result contains the result of the conversion. + /// + public Task SerializeValue(T obj, IServiceProvider services = null) => + _typeReaderMap.Get(typeof(T), services).SerializeAsync(obj); + /// /// Loads and caches an for the provided . /// diff --git a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/ComponentTypeConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/ComponentTypeConverter.cs index 283c94865..e406d4a26 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/ComponentTypeConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/ComponentTypeConverter.cs @@ -3,15 +3,36 @@ using System.Threading.Tasks; namespace Discord.Interactions { + /// + /// Base class for creating Component TypeConverters. uses TypeConverters to interface with Slash Command parameters. + /// public abstract class ComponentTypeConverter : ITypeConverter { + /// + /// Will be used to search for alternative TypeConverters whenever the Command Service encounters an unknown parameter type. + /// + /// An object type. + /// + /// The boolean result. + /// public abstract bool CanConvertTo(Type type); + + /// + /// Will be used to read the incoming payload before executing the method body. + /// + /// Command exexution context. + /// Recieved option payload. + /// Service provider that will be used to initialize the command module. + /// + /// The result of the read process. + /// public abstract Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services); - public virtual Task SerializeAsync(object obj) => Task.FromResult(obj.ToString()); } + /// public abstract class ComponentTypeConverter : ComponentTypeConverter { + /// public sealed override bool CanConvertTo(Type type) => typeof(T).IsAssignableFrom(type); } diff --git a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs index 043019013..87fc431c5 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs @@ -1,7 +1,5 @@ using System; -using System.Collections; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; namespace Discord.Interactions diff --git a/src/Discord.Net.Interactions/TypeReaders/DefaultSnowflakeReader.cs b/src/Discord.Net.Interactions/TypeReaders/DefaultSnowflakeReader.cs index db6c322e9..3b4e01165 100644 --- a/src/Discord.Net.Interactions/TypeReaders/DefaultSnowflakeReader.cs +++ b/src/Discord.Net.Interactions/TypeReaders/DefaultSnowflakeReader.cs @@ -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 SerializeAsync(object obj) => Task.FromResult((obj as ISnowflakeEntity)?.Id.ToString()); } internal sealed class DefaultUserReader : DefaultSnowflakeReader diff --git a/src/Discord.Net.Interactions/TypeReaders/EnumTypeReader.cs b/src/Discord.Net.Interactions/TypeReaders/EnumTypeReader.cs deleted file mode 100644 index 813959de3..000000000 --- a/src/Discord.Net.Interactions/TypeReaders/EnumTypeReader.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Discord.Interactions -{ - internal sealed class EnumTypeReader : TypeReader - where T : struct, Enum - { - public override Task ReadAsync(IInteractionContext context, string option, IServiceProvider services) - { - if (Enum.TryParse(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 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); - } - } -} diff --git a/src/Discord.Net.Interactions/TypeReaders/TypeReader.cs b/src/Discord.Net.Interactions/TypeReaders/TypeReader.cs index 024047a55..95dc3c8a5 100644 --- a/src/Discord.Net.Interactions/TypeReaders/TypeReader.cs +++ b/src/Discord.Net.Interactions/TypeReaders/TypeReader.cs @@ -3,15 +3,43 @@ using System.Threading.Tasks; namespace Discord.Interactions { + /// + /// Base class for creating TypeConverters. uses TypeConverters to interface with Slash Command parameters. + /// public abstract class TypeReader : ITypeConverter { + /// + /// Will be used to search for alternative TypeReaders whenever the Command Service encounters an unknown parameter type. + /// + /// An object type. + /// + /// The boolean result. + /// public abstract bool CanConvertTo(Type type); + + /// + /// Will be used to read the incoming payload before executing the method body. + /// + /// Command exexution context. + /// Recieved option payload. + /// Service provider that will be used to initialize the command module. + /// The result of the read process. public abstract Task ReadAsync(IInteractionContext context, string option, IServiceProvider services); + + /// + /// Will be used to serialize objects into strings. + /// + /// Object to be serialized. + /// + /// A task represting the conversion process. The result of the task contains the conversion result. + /// public virtual Task SerializeAsync(object obj) => Task.FromResult(obj.ToString()); } + /// public abstract class TypeReader : TypeReader { + /// public sealed override bool CanConvertTo(Type type) => typeof(T).IsAssignableFrom(type); }