* 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 methodpull/2179/head
@@ -5,7 +5,7 @@ namespace Discord.Interactions.Builders | |||||
/// <summary> | /// <summary> | ||||
/// Represents a builder for creating <see cref="ComponentCommandInfo"/>. | /// Represents a builder for creating <see cref="ComponentCommandInfo"/>. | ||||
/// </summary> | /// </summary> | ||||
public sealed class ComponentCommandBuilder : CommandBuilder<ComponentCommandInfo, ComponentCommandBuilder, CommandParameterBuilder> | |||||
public sealed class ComponentCommandBuilder : CommandBuilder<ComponentCommandInfo, ComponentCommandBuilder, ComponentCommandParameterBuilder> | |||||
{ | { | ||||
protected override ComponentCommandBuilder Instance => this; | protected override ComponentCommandBuilder Instance => this; | ||||
@@ -26,9 +26,9 @@ namespace Discord.Interactions.Builders | |||||
/// <returns> | /// <returns> | ||||
/// The builder instance. | /// The builder instance. | ||||
/// </returns> | /// </returns> | ||||
public override ComponentCommandBuilder AddParameter (Action<CommandParameterBuilder> configure) | |||||
public override ComponentCommandBuilder AddParameter (Action<ComponentCommandParameterBuilder> configure) | |||||
{ | { | ||||
var parameter = new CommandParameterBuilder(this); | |||||
var parameter = new ComponentCommandParameterBuilder(this); | |||||
configure(parameter); | configure(parameter); | ||||
AddParameters(parameter); | AddParameters(parameter); | ||||
return this; | return this; | ||||
@@ -39,6 +39,11 @@ namespace Discord.Interactions.Builders | |||||
Type Type { get; } | Type Type { get; } | ||||
/// <summary> | /// <summary> | ||||
/// Get the <see cref="ComponentTypeConverter"/> assigned to this input. | |||||
/// </summary> | |||||
ComponentTypeConverter TypeConverter { get; } | |||||
/// <summary> | |||||
/// Gets the default value of this input component. | /// Gets the default value of this input component. | ||||
/// </summary> | /// </summary> | ||||
object DefaultValue { get; } | object DefaultValue { get; } | ||||
@@ -34,6 +34,9 @@ namespace Discord.Interactions.Builders | |||||
public Type Type { get; private set; } | public Type Type { get; private set; } | ||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
public ComponentTypeConverter TypeConverter { get; private set; } | |||||
/// <inheritdoc/> | |||||
public object DefaultValue { get; set; } | public object DefaultValue { get; set; } | ||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
@@ -111,6 +114,7 @@ namespace Discord.Interactions.Builders | |||||
public TBuilder WithType(Type type) | public TBuilder WithType(Type type) | ||||
{ | { | ||||
Type = type; | Type = type; | ||||
TypeConverter = Modal._interactionService.GetComponentTypeConverter(type); | |||||
return Instance; | return Instance; | ||||
} | } | ||||
@@ -9,6 +9,7 @@ namespace Discord.Interactions.Builders | |||||
/// </summary> | /// </summary> | ||||
public class ModalBuilder | public class ModalBuilder | ||||
{ | { | ||||
internal readonly InteractionService _interactionService; | |||||
internal readonly List<IInputComponentBuilder> _components; | internal readonly List<IInputComponentBuilder> _components; | ||||
/// <summary> | /// <summary> | ||||
@@ -31,11 +32,12 @@ namespace Discord.Interactions.Builders | |||||
/// </summary> | /// </summary> | ||||
public IReadOnlyCollection<IInputComponentBuilder> Components => _components; | public IReadOnlyCollection<IInputComponentBuilder> Components => _components; | ||||
internal ModalBuilder(Type type) | |||||
internal ModalBuilder(Type type, InteractionService interactionService) | |||||
{ | { | ||||
if (!typeof(IModal).IsAssignableFrom(type)) | if (!typeof(IModal).IsAssignableFrom(type)) | ||||
throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); | throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); | ||||
_interactionService = interactionService; | |||||
_components = new(); | _components = new(); | ||||
} | } | ||||
@@ -43,7 +45,7 @@ namespace Discord.Interactions.Builders | |||||
/// Initializes a new <see cref="ModalBuilder"/> | /// Initializes a new <see cref="ModalBuilder"/> | ||||
/// </summary> | /// </summary> | ||||
/// <param name="modalInitializer">The initialization delegate for this modal.</param> | /// <param name="modalInitializer">The initialization delegate for this modal.</param> | ||||
public ModalBuilder(Type type, ModalInitializer modalInitializer) : this(type) | |||||
public ModalBuilder(Type type, ModalInitializer modalInitializer, InteractionService interactionService) : this(type, interactionService) | |||||
{ | { | ||||
ModalInitializer = modalInitializer; | ModalInitializer = modalInitializer; | ||||
} | } | ||||
@@ -231,9 +231,6 @@ namespace Discord.Interactions.Builders | |||||
private static void BuildComponentCommand (ComponentCommandBuilder builder, Func<IServiceProvider, IInteractionModuleBase> createInstance, MethodInfo methodInfo, | private static void BuildComponentCommand (ComponentCommandBuilder builder, Func<IServiceProvider, IInteractionModuleBase> createInstance, MethodInfo methodInfo, | ||||
InteractionService commandService, IServiceProvider services) | InteractionService commandService, IServiceProvider services) | ||||
{ | { | ||||
if (!methodInfo.GetParameters().All(x => x.ParameterType == typeof(string) || x.ParameterType == typeof(string[]))) | |||||
throw new InvalidOperationException($"Interaction method parameters all must be types of {typeof(string).Name} or {typeof(string[]).Name}"); | |||||
var attributes = methodInfo.GetCustomAttributes(); | var attributes = methodInfo.GetCustomAttributes(); | ||||
builder.MethodName = methodInfo.Name; | builder.MethodName = methodInfo.Name; | ||||
@@ -260,8 +257,10 @@ namespace Discord.Interactions.Builders | |||||
var parameters = methodInfo.GetParameters(); | var parameters = methodInfo.GetParameters(); | ||||
var wildCardCount = Regex.Matches(Regex.Escape(builder.Name), Regex.Escape(commandService._wildCardExp)).Count; | |||||
foreach (var parameter in parameters) | foreach (var parameter in parameters) | ||||
builder.AddParameter(x => BuildParameter(x, parameter)); | |||||
builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount)); | |||||
builder.Callback = CreateCallback(createInstance, methodInfo, commandService); | builder.Callback = CreateCallback(createInstance, methodInfo, commandService); | ||||
} | } | ||||
@@ -310,8 +309,8 @@ namespace Discord.Interactions.Builders | |||||
if (parameters.Count(x => typeof(IModal).IsAssignableFrom(x.ParameterType)) > 1) | if (parameters.Count(x => typeof(IModal).IsAssignableFrom(x.ParameterType)) > 1) | ||||
throw new InvalidOperationException($"A modal command can only have one {nameof(IModal)} parameter."); | throw new InvalidOperationException($"A modal command can only have one {nameof(IModal)} parameter."); | ||||
if (!parameters.All(x => x.ParameterType == typeof(string) || typeof(IModal).IsAssignableFrom(x.ParameterType))) | |||||
throw new InvalidOperationException($"All parameters of a modal command must be either a string or an implementation of {nameof(IModal)}"); | |||||
if (!typeof(IModal).IsAssignableFrom(parameters.Last().ParameterType)) | |||||
throw new InvalidOperationException($"Last parameter of a modal command must be an implementation of {nameof(IModal)}"); | |||||
var attributes = methodInfo.GetCustomAttributes(); | var attributes = methodInfo.GetCustomAttributes(); | ||||
@@ -464,6 +463,12 @@ namespace Discord.Interactions.Builders | |||||
builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower(); | builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower(); | ||||
} | } | ||||
private static void BuildComponentParameter(ComponentCommandParameterBuilder builder, ParameterInfo paramInfo, bool isComponentParam) | |||||
{ | |||||
builder.SetIsRouteSegment(!isComponentParam); | |||||
BuildParameter(builder, paramInfo); | |||||
} | |||||
private static void BuildParameter<TInfo, TBuilder> (ParameterBuilder<TInfo, TBuilder> builder, ParameterInfo paramInfo) | private static void BuildParameter<TInfo, TBuilder> (ParameterBuilder<TInfo, TBuilder> builder, ParameterInfo paramInfo) | ||||
where TInfo : class, IParameterInfo | where TInfo : class, IParameterInfo | ||||
where TBuilder : ParameterBuilder<TInfo, TBuilder> | where TBuilder : ParameterBuilder<TInfo, TBuilder> | ||||
@@ -495,7 +500,7 @@ namespace Discord.Interactions.Builders | |||||
#endregion | #endregion | ||||
#region Modals | #region Modals | ||||
public static ModalInfo BuildModalInfo(Type modalType) | |||||
public static ModalInfo BuildModalInfo(Type modalType, InteractionService interactionService) | |||||
{ | { | ||||
if (!typeof(IModal).IsAssignableFrom(modalType)) | if (!typeof(IModal).IsAssignableFrom(modalType)) | ||||
throw new InvalidOperationException($"{modalType.FullName} isn't an implementation of {typeof(IModal).FullName}"); | throw new InvalidOperationException($"{modalType.FullName} isn't an implementation of {typeof(IModal).FullName}"); | ||||
@@ -504,7 +509,7 @@ namespace Discord.Interactions.Builders | |||||
try | try | ||||
{ | { | ||||
var builder = new ModalBuilder(modalType) | |||||
var builder = new ModalBuilder(modalType, interactionService) | |||||
{ | { | ||||
Title = instance.Title | Title = instance.Title | ||||
}; | }; | ||||
@@ -0,0 +1,77 @@ | |||||
using System; | |||||
namespace Discord.Interactions.Builders | |||||
{ | |||||
/// <summary> | |||||
/// Represents a builder for creating <see cref="ComponentCommandParameterInfo"/>. | |||||
/// </summary> | |||||
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; | |||||
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/> | |||||
public override ComponentCommandParameterBuilder SetParameterType(Type type) => SetParameterType(type, null); | |||||
/// <summary> | |||||
/// Sets <see cref="ParameterBuilder{TInfo, TBuilder}.ParameterType"/>. | |||||
/// </summary> | |||||
/// <param name="type">New value of the <see cref="ParameterBuilder{TInfo, TBuilder}.ParameterType"/>.</param> | |||||
/// <param name="services">Service container to be used to resolve the dependencies of this parameters <see cref="Interactions.TypeConverter"/>.</param> | |||||
/// <returns> | |||||
/// The builder instance. | |||||
/// </returns> | |||||
public ComponentCommandParameterBuilder SetParameterType(Type type, IServiceProvider services) | |||||
{ | |||||
base.SetParameterType(type); | |||||
if (IsRouteSegmentParameter) | |||||
TypeReader = Command.Module.InteractionService.GetTypeReader(type); | |||||
else | |||||
TypeConverter = Command.Module.InteractionService.GetComponentTypeConverter(ParameterType, services); | |||||
return this; | |||||
} | |||||
/// <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; | |||||
} | |||||
internal override ComponentCommandParameterInfo Build(ICommandInfo command) | |||||
=> new(this, command); | |||||
} | |||||
} |
@@ -20,6 +20,11 @@ namespace Discord.Interactions.Builders | |||||
/// </summary> | /// </summary> | ||||
public bool IsModalParameter => Modal is not null; | public bool IsModalParameter => Modal is not null; | ||||
/// <summary> | |||||
/// Gets the <see cref="TypeReader"/> assigned to this parameter, if <see cref="IsModalParameter"/> is <see langword="true"/>. | |||||
/// </summary> | |||||
public TypeReader TypeReader { get; private set; } | |||||
internal ModalCommandParameterBuilder(ICommandBuilder command) : base(command) { } | internal ModalCommandParameterBuilder(ICommandBuilder command) : base(command) { } | ||||
/// <summary> | /// <summary> | ||||
@@ -34,7 +39,9 @@ namespace Discord.Interactions.Builders | |||||
public override ModalCommandParameterBuilder SetParameterType(Type type) | public override ModalCommandParameterBuilder SetParameterType(Type type) | ||||
{ | { | ||||
if (typeof(IModal).IsAssignableFrom(type)) | if (typeof(IModal).IsAssignableFrom(type)) | ||||
Modal = ModalUtils.GetOrAdd(type); | |||||
Modal = ModalUtils.GetOrAdd(type, Command.Module.InteractionService); | |||||
else | |||||
TypeReader = Command.Module.InteractionService.GetTypeReader(type); | |||||
return base.SetParameterType(type); | return base.SetParameterType(type); | ||||
} | } | ||||
@@ -0,0 +1,12 @@ | |||||
using System; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Interactions | |||||
{ | |||||
internal interface ITypeConverter<T> | |||||
{ | |||||
public bool CanConvertTo(Type type); | |||||
public Task<TypeConverterResult> ReadAsync(IInteractionContext context, T option, IServiceProvider services); | |||||
} | |||||
} |
@@ -41,14 +41,7 @@ namespace Discord.Interactions | |||||
if (context.Interaction is not IAutocompleteInteraction) | if (context.Interaction is not IAutocompleteInteraction) | ||||
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Autocomplete Interaction"); | return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Autocomplete Interaction"); | ||||
try | |||||
{ | |||||
return await RunAsync(context, Array.Empty<object>(), services).ConfigureAwait(false); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
return ExecuteResult.FromError(ex); | |||||
} | |||||
return await RunAsync(context, Array.Empty<object>(), services).ConfigureAwait(false); | |||||
} | } | ||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
@@ -123,10 +123,7 @@ namespace Discord.Interactions | |||||
return moduleResult; | return moduleResult; | ||||
var commandResult = await CheckGroups(_groupedPreconditions, "Command").ConfigureAwait(false); | var commandResult = await CheckGroups(_groupedPreconditions, "Command").ConfigureAwait(false); | ||||
if (!commandResult.IsSuccess) | |||||
return commandResult; | |||||
return PreconditionResult.FromSuccess(); | |||||
return !commandResult.IsSuccess ? commandResult : PreconditionResult.FromSuccess(); | |||||
} | } | ||||
protected async Task<IResult> RunAsync(IInteractionContext context, object[] args, IServiceProvider services) | protected async Task<IResult> RunAsync(IInteractionContext context, object[] args, IServiceProvider services) | ||||
@@ -140,8 +137,8 @@ namespace Discord.Interactions | |||||
using var scope = services?.CreateScope(); | using var scope = services?.CreateScope(); | ||||
return await ExecuteInternalAsync(context, args, scope?.ServiceProvider ?? EmptyServiceProvider.Instance).ConfigureAwait(false); | return await ExecuteInternalAsync(context, args, scope?.ServiceProvider ?? EmptyServiceProvider.Instance).ConfigureAwait(false); | ||||
} | } | ||||
else | |||||
return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); | |||||
return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); | |||||
} | } | ||||
case RunMode.Async: | case RunMode.Async: | ||||
_ = Task.Run(async () => | _ = Task.Run(async () => | ||||
@@ -170,20 +167,14 @@ namespace Discord.Interactions | |||||
{ | { | ||||
var preconditionResult = await CheckPreconditionsAsync(context, services).ConfigureAwait(false); | var preconditionResult = await CheckPreconditionsAsync(context, services).ConfigureAwait(false); | ||||
if (!preconditionResult.IsSuccess) | if (!preconditionResult.IsSuccess) | ||||
{ | |||||
await InvokeModuleEvent(context, preconditionResult).ConfigureAwait(false); | |||||
return preconditionResult; | |||||
} | |||||
return await InvokeEventAndReturn(context, preconditionResult).ConfigureAwait(false); | |||||
var index = 0; | var index = 0; | ||||
foreach (var parameter in Parameters) | foreach (var parameter in Parameters) | ||||
{ | { | ||||
var result = await parameter.CheckPreconditionsAsync(context, args[index++], services).ConfigureAwait(false); | var result = await parameter.CheckPreconditionsAsync(context, args[index++], services).ConfigureAwait(false); | ||||
if (!result.IsSuccess) | if (!result.IsSuccess) | ||||
{ | |||||
await InvokeModuleEvent(context, result).ConfigureAwait(false); | |||||
return result; | |||||
} | |||||
return await InvokeEventAndReturn(context, result).ConfigureAwait(false); | |||||
} | } | ||||
var task = _action(context, args, services, this); | var task = _action(context, args, services, this); | ||||
@@ -192,20 +183,16 @@ namespace Discord.Interactions | |||||
{ | { | ||||
var result = await resultTask.ConfigureAwait(false); | var result = await resultTask.ConfigureAwait(false); | ||||
await InvokeModuleEvent(context, result).ConfigureAwait(false); | await InvokeModuleEvent(context, result).ConfigureAwait(false); | ||||
if (result is RuntimeResult || result is ExecuteResult) | |||||
if (result is RuntimeResult or ExecuteResult) | |||||
return result; | return result; | ||||
} | } | ||||
else | else | ||||
{ | { | ||||
await task.ConfigureAwait(false); | await task.ConfigureAwait(false); | ||||
var result = ExecuteResult.FromSuccess(); | |||||
await InvokeModuleEvent(context, result).ConfigureAwait(false); | |||||
return result; | |||||
return await InvokeEventAndReturn(context, ExecuteResult.FromSuccess()).ConfigureAwait(false); | |||||
} | } | ||||
var failResult = ExecuteResult.FromError(InteractionCommandError.Unsuccessful, "Command execution failed for an unknown reason"); | |||||
await InvokeModuleEvent(context, failResult).ConfigureAwait(false); | |||||
return failResult; | |||||
return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.Unsuccessful, "Command execution failed for an unknown reason")).ConfigureAwait(false); | |||||
} | } | ||||
catch (Exception ex) | catch (Exception ex) | ||||
{ | { | ||||
@@ -234,6 +221,12 @@ namespace Discord.Interactions | |||||
} | } | ||||
} | } | ||||
protected async ValueTask<IResult> InvokeEventAndReturn(IInteractionContext context, IResult result) | |||||
{ | |||||
await InvokeModuleEvent(context, result).ConfigureAwait(false); | |||||
return result; | |||||
} | |||||
private static bool CheckTopLevel(ModuleInfo parent) | private static bool CheckTopLevel(ModuleInfo parent) | ||||
{ | { | ||||
var currentParent = parent; | var currentParent = parent; | ||||
@@ -1,5 +1,4 @@ | |||||
using Discord.Interactions.Builders; | using Discord.Interactions.Builders; | ||||
using Discord.WebSocket; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
@@ -11,10 +10,10 @@ namespace Discord.Interactions | |||||
/// <summary> | /// <summary> | ||||
/// Represents the info class of an attribute based method for handling Component Interaction events. | /// Represents the info class of an attribute based method for handling Component Interaction events. | ||||
/// </summary> | /// </summary> | ||||
public class ComponentCommandInfo : CommandInfo<CommandParameterInfo> | |||||
public class ComponentCommandInfo : CommandInfo<ComponentCommandParameterInfo> | |||||
{ | { | ||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
public override IReadOnlyCollection<CommandParameterInfo> Parameters { get; } | |||||
public override IReadOnlyCollection<ComponentCommandParameterInfo> Parameters { get; } | |||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
public override bool SupportsWildCards => true; | public override bool SupportsWildCards => true; | ||||
@@ -42,80 +41,46 @@ namespace Discord.Interactions | |||||
if (context.Interaction is not IComponentInteraction componentInteraction) | if (context.Interaction is not IComponentInteraction componentInteraction) | ||||
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Component Interaction"); | return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Component Interaction"); | ||||
var args = new List<string>(); | |||||
if (additionalArgs is not null) | |||||
args.AddRange(additionalArgs); | |||||
if (componentInteraction.Data?.Values is not null) | |||||
args.AddRange(componentInteraction.Data.Values); | |||||
return await ExecuteAsync(context, Parameters, args, services); | |||||
return await ExecuteAsync(context, Parameters, additionalArgs, componentInteraction.Data, services); | |||||
} | } | ||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
public async Task<IResult> ExecuteAsync(IInteractionContext context, IEnumerable<CommandParameterInfo> paramList, IEnumerable<string> values, | |||||
public async Task<IResult> ExecuteAsync(IInteractionContext context, IEnumerable<CommandParameterInfo> paramList, IEnumerable<string> wildcardCaptures, IComponentInteractionData data, | |||||
IServiceProvider services) | IServiceProvider services) | ||||
{ | { | ||||
var paramCount = paramList.Count(); | |||||
var captureCount = wildcardCaptures?.Count() ?? 0; | |||||
if (context.Interaction is not IComponentInteraction messageComponent) | if (context.Interaction is not IComponentInteraction messageComponent) | ||||
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Component Command Interaction"); | return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Component Command Interaction"); | ||||
try | try | ||||
{ | { | ||||
var strCount = Parameters.Count(x => x.ParameterType == typeof(string)); | |||||
var args = new object[paramCount]; | |||||
for (var i = 0; i < paramCount; i++) | |||||
{ | |||||
var parameter = Parameters.ElementAt(i); | |||||
var isCapture = i < captureCount; | |||||
if (strCount > values?.Count()) | |||||
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters"); | |||||
if (isCapture ^ parameter.IsRouteSegmentParameter) | |||||
return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Argument type and parameter type didn't match (Wild Card capture/Component value)")).ConfigureAwait(false); | |||||
var componentValues = messageComponent.Data?.Values; | |||||
var readResult = isCapture ? await parameter.TypeReader.ReadAsync(context, wildcardCaptures.ElementAt(i), services).ConfigureAwait(false) : | |||||
await parameter.TypeConverter.ReadAsync(context, data, services).ConfigureAwait(false); | |||||
var args = new object[Parameters.Count]; | |||||
if (!readResult.IsSuccess) | |||||
return await InvokeEventAndReturn(context, readResult).ConfigureAwait(false); | |||||
if (componentValues is not null) | |||||
{ | |||||
if (Parameters.Last().ParameterType == typeof(string[])) | |||||
args[args.Length - 1] = componentValues.ToArray(); | |||||
else | |||||
return ExecuteResult.FromError(InteractionCommandError.BadArgs, $"Select Menu Interaction handlers must accept a {typeof(string[]).FullName} as its last parameter"); | |||||
args[i] = readResult.Value; | |||||
} | } | ||||
for (var i = 0; i < strCount; i++) | |||||
args[i] = values.ElementAt(i); | |||||
return await RunAsync(context, args, services).ConfigureAwait(false); | return await RunAsync(context, args, services).ConfigureAwait(false); | ||||
} | } | ||||
catch (Exception ex) | catch (Exception ex) | ||||
{ | { | ||||
return ExecuteResult.FromError(ex); | |||||
} | |||||
} | |||||
private static object[] GenerateArgs(IEnumerable<CommandParameterInfo> paramList, IEnumerable<string> argList) | |||||
{ | |||||
var result = new object[paramList.Count()]; | |||||
for (var i = 0; i < paramList.Count(); i++) | |||||
{ | |||||
var parameter = paramList.ElementAt(i); | |||||
if (argList?.ElementAt(i) == null) | |||||
{ | |||||
if (!parameter.IsRequired) | |||||
result[i] = parameter.DefaultValue; | |||||
else | |||||
throw new InvalidOperationException($"Component Interaction handler is executed with too few args."); | |||||
} | |||||
else if (parameter.IsParameterArray) | |||||
{ | |||||
string[] paramArray = new string[argList.Count() - i]; | |||||
argList.ToArray().CopyTo(paramArray, i); | |||||
result[i] = paramArray; | |||||
} | |||||
else | |||||
result[i] = argList?.ElementAt(i); | |||||
return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false); | |||||
} | } | ||||
return result; | |||||
} | } | ||||
protected override Task InvokeModuleEvent(IInteractionContext context, IResult result) | protected override Task InvokeModuleEvent(IInteractionContext context, IResult result) | ||||
@@ -1,6 +1,7 @@ | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
using System.Diagnostics.Tracing; | |||||
using System.Linq; | using System.Linq; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
namespace Discord.Interactions | namespace Discord.Interactions | ||||
@@ -47,21 +48,38 @@ namespace Discord.Interactions | |||||
try | try | ||||
{ | { | ||||
var args = new List<object>(); | |||||
var args = new object[Parameters.Count]; | |||||
var captureCount = additionalArgs.Length; | |||||
if (additionalArgs is not null) | |||||
args.AddRange(additionalArgs); | |||||
for(var i = 0; i < Parameters.Count; i++) | |||||
{ | |||||
var parameter = Parameters.ElementAt(i); | |||||
var modal = Modal.CreateModal(modalInteraction, Module.CommandService._exitOnMissingModalField); | |||||
args.Add(modal); | |||||
if(i < captureCount) | |||||
{ | |||||
var readResult = await parameter.TypeReader.ReadAsync(context, additionalArgs[i], services).ConfigureAwait(false); | |||||
if (!readResult.IsSuccess) | |||||
return await InvokeEventAndReturn(context, readResult).ConfigureAwait(false); | |||||
return await RunAsync(context, args.ToArray(), services); | |||||
args[i] = readResult.Value; | |||||
} | |||||
else | |||||
{ | |||||
var modalResult = await Modal.CreateModalAsync(context, services, Module.CommandService._exitOnMissingModalField).ConfigureAwait(false); | |||||
if (!modalResult.IsSuccess) | |||||
return await InvokeEventAndReturn(context, modalResult).ConfigureAwait(false); | |||||
if (modalResult is not ParseResult parseResult) | |||||
return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason.")); | |||||
args[i] = parseResult.Value; | |||||
} | |||||
} | |||||
return await RunAsync(context, args, services); | |||||
} | } | ||||
catch (Exception ex) | catch (Exception ex) | ||||
{ | { | ||||
var result = ExecuteResult.FromError(ex); | |||||
await InvokeModuleEvent(context, result).ConfigureAwait(false); | |||||
return result; | |||||
return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false); | |||||
} | } | ||||
} | } | ||||
@@ -70,34 +70,27 @@ namespace Discord.Interactions | |||||
{ | { | ||||
try | try | ||||
{ | { | ||||
var args = new object[paramList.Count()]; | |||||
var slashCommandParameterInfos = paramList.ToList(); | |||||
var args = new object[slashCommandParameterInfos.Count]; | |||||
for (var i = 0; i < paramList.Count(); i++) | |||||
for (var i = 0; i < slashCommandParameterInfos.Count; i++) | |||||
{ | { | ||||
var parameter = paramList.ElementAt(i); | |||||
var parameter = slashCommandParameterInfos[i]; | |||||
var result = await ParseArgument(parameter, context, argList, services).ConfigureAwait(false); | var result = await ParseArgument(parameter, context, argList, services).ConfigureAwait(false); | ||||
if(!result.IsSuccess) | |||||
{ | |||||
var execResult = ExecuteResult.FromError(result); | |||||
await InvokeModuleEvent(context, execResult).ConfigureAwait(false); | |||||
return execResult; | |||||
} | |||||
if (!result.IsSuccess) | |||||
return await InvokeEventAndReturn(context, result).ConfigureAwait(false); | |||||
if (result is ParseResult parseResult) | |||||
args[i] = parseResult.Value; | |||||
else | |||||
if (result is not ParseResult parseResult) | |||||
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason."); | return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason."); | ||||
} | |||||
args[i] = parseResult.Value; | |||||
} | |||||
return await RunAsync(context, args, services).ConfigureAwait(false); | return await RunAsync(context, args, services).ConfigureAwait(false); | ||||
} | } | ||||
catch (Exception ex) | |||||
catch(Exception ex) | |||||
{ | { | ||||
var result = ExecuteResult.FromError(ex); | |||||
await InvokeModuleEvent(context, result).ConfigureAwait(false); | |||||
return result; | |||||
return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false); | |||||
} | } | ||||
} | } | ||||
@@ -115,37 +108,27 @@ namespace Discord.Interactions | |||||
if (!result.IsSuccess) | if (!result.IsSuccess) | ||||
return result; | return result; | ||||
if (result is ParseResult parseResult) | |||||
ctorArgs[i] = parseResult.Value; | |||||
else | |||||
if (result is not ParseResult parseResult) | |||||
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason."); | return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason."); | ||||
ctorArgs[i] = parseResult.Value; | |||||
} | } | ||||
return ParseResult.FromSuccess(parameterInfo._complexParameterInitializer(ctorArgs)); | return ParseResult.FromSuccess(parameterInfo._complexParameterInitializer(ctorArgs)); | ||||
} | } | ||||
else | |||||
{ | |||||
var arg = argList?.Find(x => string.Equals(x.Name, parameterInfo.Name, StringComparison.OrdinalIgnoreCase)); | |||||
if (arg == default) | |||||
{ | |||||
if (parameterInfo.IsRequired) | |||||
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters"); | |||||
else | |||||
return ParseResult.FromSuccess(parameterInfo.DefaultValue); | |||||
} | |||||
else | |||||
{ | |||||
var typeConverter = parameterInfo.TypeConverter; | |||||
var arg = argList?.Find(x => string.Equals(x.Name, parameterInfo.Name, StringComparison.OrdinalIgnoreCase)); | |||||
var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false); | |||||
if (arg == default) | |||||
return parameterInfo.IsRequired ? ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters") : | |||||
ParseResult.FromSuccess(parameterInfo.DefaultValue); | |||||
if (!readResult.IsSuccess) | |||||
return readResult; | |||||
var typeConverter = parameterInfo.TypeConverter; | |||||
var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false); | |||||
if (!readResult.IsSuccess) | |||||
return readResult; | |||||
return ParseResult.FromSuccess(readResult.Value); | |||||
} | |||||
} | |||||
return ParseResult.FromSuccess(readResult.Value); | |||||
} | } | ||||
protected override Task InvokeModuleEvent (IInteractionContext context, IResult result) | protected override Task InvokeModuleEvent (IInteractionContext context, IResult result) | ||||
@@ -40,6 +40,11 @@ namespace Discord.Interactions | |||||
public Type Type { get; } | public Type Type { get; } | ||||
/// <summary> | /// <summary> | ||||
/// Gets the <see cref="ComponentTypeConverter"/> assigned to this component. | |||||
/// </summary> | |||||
public ComponentTypeConverter TypeConverter { get; } | |||||
/// <summary> | |||||
/// Gets the default value of this component. | /// Gets the default value of this component. | ||||
/// </summary> | /// </summary> | ||||
public object DefaultValue { get; } | public object DefaultValue { get; } | ||||
@@ -57,6 +62,7 @@ namespace Discord.Interactions | |||||
IsRequired = builder.IsRequired; | IsRequired = builder.IsRequired; | ||||
ComponentType = builder.ComponentType; | ComponentType = builder.ComponentType; | ||||
Type = builder.Type; | Type = builder.Type; | ||||
TypeConverter = builder.TypeConverter; | |||||
DefaultValue = builder.DefaultValue; | DefaultValue = builder.DefaultValue; | ||||
Attributes = builder.Attributes.ToImmutableArray(); | Attributes = builder.Attributes.ToImmutableArray(); | ||||
} | } | ||||
@@ -2,6 +2,7 @@ using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
using System.Linq; | using System.Linq; | ||||
using System.Threading.Tasks; | |||||
namespace Discord.Interactions | namespace Discord.Interactions | ||||
{ | { | ||||
@@ -19,6 +20,7 @@ namespace Discord.Interactions | |||||
/// </summary> | /// </summary> | ||||
public class ModalInfo | public class ModalInfo | ||||
{ | { | ||||
internal readonly InteractionService _interactionService; | |||||
internal readonly ModalInitializer _initializer; | internal readonly ModalInitializer _initializer; | ||||
/// <summary> | /// <summary> | ||||
@@ -53,16 +55,18 @@ namespace Discord.Interactions | |||||
TextComponents = Components.OfType<TextInputComponentInfo>().ToImmutableArray(); | TextComponents = Components.OfType<TextInputComponentInfo>().ToImmutableArray(); | ||||
_interactionService = builder._interactionService; | |||||
_initializer = builder.ModalInitializer; | _initializer = builder.ModalInitializer; | ||||
} | } | ||||
/// <summary> | /// <summary> | ||||
/// Creates an <see cref="IModal"/> and fills it with provided message components. | /// Creates an <see cref="IModal"/> and fills it with provided message components. | ||||
/// </summary> | /// </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> | /// <returns> | ||||
/// A <see cref="IModal"/> filled with the provided components. | /// A <see cref="IModal"/> filled with the provided components. | ||||
/// </returns> | /// </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) | public IModal CreateModal(IModalInteraction modalInteraction, bool throwOnMissingField = false) | ||||
{ | { | ||||
var args = new object[Components.Count]; | var args = new object[Components.Count]; | ||||
@@ -86,5 +90,50 @@ namespace Discord.Interactions | |||||
return _initializer(args); | return _initializer(args); | ||||
} | } | ||||
/// <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) | |||||
return ParseResult.FromError(InteractionCommandError.Unsuccessful, "Provided context doesn't belong to a Modal Interaction."); | |||||
services ??= EmptyServiceProvider.Instance; | |||||
var args = new object[Components.Count]; | |||||
var components = modalInteraction.Data.Components.ToList(); | |||||
for (var i = 0; i < Components.Count; i++) | |||||
{ | |||||
var input = Components.ElementAt(i); | |||||
var component = components.Find(x => x.CustomId == input.CustomId); | |||||
if (component is null) | |||||
{ | |||||
if (!throwOnMissingField) | |||||
args[i] = input.DefaultValue; | |||||
else | |||||
return ParseResult.FromError(InteractionCommandError.BadArgs, $"Modal interaction is missing the required field: {input.CustomId}"); | |||||
} | |||||
else | |||||
{ | |||||
var readResult = await input.TypeConverter.ReadAsync(context, component, services).ConfigureAwait(false); | |||||
if (!readResult.IsSuccess) | |||||
return readResult; | |||||
args[i] = readResult.Value; | |||||
} | |||||
} | |||||
return ParseResult.FromSuccess(_initializer(args)); | |||||
} | |||||
} | } | ||||
} | } |
@@ -0,0 +1,34 @@ | |||||
using Discord.Interactions.Builders; | |||||
namespace Discord.Interactions | |||||
{ | |||||
/// <summary> | |||||
/// Represents the parameter info class for <see cref="ComponentCommandInfo"/> commands. | |||||
/// </summary> | |||||
public class ComponentCommandParameterInfo : CommandParameterInfo | |||||
{ | |||||
/// <summary> | |||||
/// Gets the <see cref="ComponentTypeConverter"/> that will be used to convert a message component value into | |||||
/// <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) | |||||
{ | |||||
TypeConverter = builder.TypeConverter; | |||||
TypeReader = builder.TypeReader; | |||||
IsRouteSegmentParameter = builder.IsRouteSegmentParameter; | |||||
} | |||||
} | |||||
} |
@@ -15,7 +15,12 @@ namespace Discord.Interactions | |||||
/// <summary> | /// <summary> | ||||
/// Gets whether this parameter is an <see cref="IModal"/> | /// Gets whether this parameter is an <see cref="IModal"/> | ||||
/// </summary> | /// </summary> | ||||
public bool IsModalParameter => Modal is not null; | |||||
public bool IsModalParameter { get; } | |||||
/// <summary> | |||||
/// Gets the <see cref="TypeReader"/> assigned to this parameter, if <see cref="IsModalParameter"/> is <see langword="true"/>. | |||||
/// </summary> | |||||
public TypeReader TypeReader { get; } | |||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
public new ModalCommandInfo Command => base.Command as ModalCommandInfo; | public new ModalCommandInfo Command => base.Command as ModalCommandInfo; | ||||
@@ -23,6 +28,8 @@ namespace Discord.Interactions | |||||
internal ModalCommandParameterInfo(ModalCommandParameterBuilder builder, ICommandInfo command) : base(builder, command) | internal ModalCommandParameterInfo(ModalCommandParameterBuilder builder, ICommandInfo command) : base(builder, command) | ||||
{ | { | ||||
Modal = builder.Modal; | Modal = builder.Modal; | ||||
IsModalParameter = builder.IsModalParameter; | |||||
TypeReader = builder.TypeReader; | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -3,6 +3,7 @@ using Discord.Logging; | |||||
using Discord.Rest; | using Discord.Rest; | ||||
using Discord.WebSocket; | using Discord.WebSocket; | ||||
using System; | using System; | ||||
using System.Collections; | |||||
using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Linq; | using System.Linq; | ||||
@@ -66,8 +67,9 @@ namespace Discord.Interactions | |||||
private readonly CommandMap<AutocompleteCommandInfo> _autocompleteCommandMap; | private readonly CommandMap<AutocompleteCommandInfo> _autocompleteCommandMap; | ||||
private readonly CommandMap<ModalCommandInfo> _modalCommandMap; | private readonly CommandMap<ModalCommandInfo> _modalCommandMap; | ||||
private readonly HashSet<ModuleInfo> _moduleDefs; | private readonly HashSet<ModuleInfo> _moduleDefs; | ||||
private readonly ConcurrentDictionary<Type, TypeConverter> _typeConverters; | |||||
private readonly ConcurrentDictionary<Type, Type> _genericTypeConverters; | |||||
private readonly TypeMap<TypeConverter, 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, IAutocompleteHandler> _autocompleteHandlers = new(); | ||||
private readonly ConcurrentDictionary<Type, ModalInfo> _modalInfos = new(); | private readonly ConcurrentDictionary<Type, ModalInfo> _modalInfos = new(); | ||||
private readonly SemaphoreSlim _lock; | private readonly SemaphoreSlim _lock; | ||||
@@ -179,22 +181,38 @@ namespace Discord.Interactions | |||||
_autoServiceScopes = config.AutoServiceScopes; | _autoServiceScopes = config.AutoServiceScopes; | ||||
_restResponseCallback = config.RestResponseCallback; | _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<>) | |||||
}); | |||||
_compTypeConverterMap = new TypeMap<ComponentTypeConverter, IComponentInteractionData>(this, new ConcurrentDictionary<Type, ComponentTypeConverter>(), | |||||
new ConcurrentDictionary<Type, Type> | |||||
{ | |||||
[typeof(Array)] = typeof(DefaultArrayComponentConverter<>), | |||||
[typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>) | |||||
}); | |||||
_typeConverters = new ConcurrentDictionary<Type, TypeConverter> | |||||
{ | |||||
[typeof(TimeSpan)] = new TimeSpanConverter() | |||||
}; | |||||
_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> | /// <summary> | ||||
@@ -293,7 +311,7 @@ namespace Discord.Interactions | |||||
public async Task<ModuleInfo> AddModuleAsync (Type type, IServiceProvider services) | public async Task<ModuleInfo> AddModuleAsync (Type type, IServiceProvider services) | ||||
{ | { | ||||
if (!typeof(IInteractionModuleBase).IsAssignableFrom(type)) | 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; | services ??= EmptyServiceProvider.Instance; | ||||
@@ -326,7 +344,7 @@ namespace Discord.Interactions | |||||
} | } | ||||
/// <summary> | /// <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> | /// </summary> | ||||
/// <param name="guildId">Id of the target guild.</param> | /// <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> | /// <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> | /// <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> | /// </summary> | ||||
/// <param name="guild">The target guild.</param> | /// <param name="guild">The target guild.</param> | ||||
/// <param name="modules">Modules to be registered to Discord.</param> | /// <param name="modules">Modules to be registered to Discord.</param> | ||||
@@ -449,7 +467,7 @@ namespace Discord.Interactions | |||||
} | } | ||||
/// <summary> | /// <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> | /// </summary> | ||||
/// <param name="modules">Modules to be registered to Discord.</param> | /// <param name="modules">Modules to be registered to Discord.</param> | ||||
/// <returns> | /// <returns> | ||||
@@ -677,7 +695,7 @@ namespace Discord.Interactions | |||||
public async Task<IResult> ExecuteCommandAsync (IInteractionContext context, IServiceProvider services) | public async Task<IResult> ExecuteCommandAsync (IInteractionContext context, IServiceProvider services) | ||||
{ | { | ||||
var interaction = context.Interaction; | var interaction = context.Interaction; | ||||
return interaction switch | return interaction switch | ||||
{ | { | ||||
ISlashCommandInteraction slashCommand => await ExecuteSlashCommandAsync(context, slashCommand, services).ConfigureAwait(false), | 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); | return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false); | ||||
} | } | ||||
internal TypeConverter GetTypeConverter (Type type, IServiceProvider services = null) | |||||
{ | |||||
if (_typeConverters.TryGetValue(type, out var specific)) | |||||
return specific; | |||||
else if (_genericTypeConverters.Any(x => x.Key.IsAssignableFrom(type) | |||||
|| (x.Key.IsGenericTypeDefinition && type.IsGenericType && x.Key.GetGenericTypeDefinition() == type.GetGenericTypeDefinition()))) | |||||
{ | |||||
services ??= EmptyServiceProvider.Instance; | |||||
var converterType = GetMostSpecificTypeConverter(type); | |||||
var converter = ReflectionUtils<TypeConverter>.CreateObject(converterType.MakeGenericType(type).GetTypeInfo(), this, services); | |||||
_typeConverters[type] = converter; | |||||
return converter; | |||||
} | |||||
else if (_typeConverters.Any(x => x.Value.CanConvertTo(type))) | |||||
return _typeConverters.First(x => x.Value.CanConvertTo(type)).Value; | |||||
throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type"); | |||||
} | |||||
internal TypeConverter GetTypeConverter(Type type, IServiceProvider services = null) | |||||
=> _typeConverterMap.Get(type, services); | |||||
/// <summary> | /// <summary> | ||||
/// Add a concrete type <see cref="TypeConverter"/>. | /// Add a concrete type <see cref="TypeConverter"/>. | ||||
/// </summary> | /// </summary> | ||||
/// <typeparam name="T">Primary target <see cref="Type"/> of the <see cref="TypeConverter"/>.</typeparam> | /// <typeparam name="T">Primary target <see cref="Type"/> of the <see cref="TypeConverter"/>.</typeparam> | ||||
/// <param name="converter">The <see cref="TypeConverter"/> instance.</param> | /// <param name="converter">The <see cref="TypeConverter"/> instance.</param> | ||||
public void AddTypeConverter<T> (TypeConverter converter) => | |||||
AddTypeConverter(typeof(T), converter); | |||||
public void AddTypeConverter<T>(TypeConverter converter) => | |||||
_typeConverterMap.AddConcrete<T>(converter); | |||||
/// <summary> | /// <summary> | ||||
/// Add a concrete type <see cref="TypeConverter"/>. | /// Add a concrete type <see cref="TypeConverter"/>. | ||||
/// </summary> | /// </summary> | ||||
/// <param name="type">Primary target <see cref="Type"/> of the <see cref="TypeConverter"/>.</param> | /// <param name="type">Primary target <see cref="Type"/> of the <see cref="TypeConverter"/>.</param> | ||||
/// <param name="converter">The <see cref="TypeConverter"/> instance.</param> | /// <param name="converter">The <see cref="TypeConverter"/> instance.</param> | ||||
public void AddTypeConverter (Type type, TypeConverter converter) | |||||
{ | |||||
if (!converter.CanConvertTo(type)) | |||||
throw new ArgumentException($"This {converter.GetType().FullName} cannot read {type.FullName} and cannot be registered as its {nameof(TypeConverter)}"); | |||||
_typeConverters[type] = converter; | |||||
} | |||||
public void AddTypeConverter(Type type, TypeConverter converter) => | |||||
_typeConverterMap.AddConcrete(type, converter); | |||||
/// <summary> | /// <summary> | ||||
/// Add a generic type <see cref="TypeConverter{T}"/>. | /// Add a generic type <see cref="TypeConverter{T}"/>. | ||||
@@ -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> | /// <typeparam name="T">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeConverter{T}"/>.</typeparam> | ||||
/// <param name="converterType">Type of the <see cref="TypeConverter{T}"/>.</param> | /// <param name="converterType">Type of the <see cref="TypeConverter{T}"/>.</param> | ||||
public void AddGenericTypeConverter<T> (Type converterType) => | |||||
AddGenericTypeConverter(typeof(T), converterType); | |||||
public void AddGenericTypeConverter<T>(Type converterType) => | |||||
_typeConverterMap.AddGeneric<T>(converterType); | |||||
/// <summary> | /// <summary> | ||||
/// Add a generic type <see cref="TypeConverter{T}"/>. | /// Add a generic type <see cref="TypeConverter{T}"/>. | ||||
/// </summary> | /// </summary> | ||||
/// <param name="targetType">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeConverter{T}"/>.</param> | /// <param name="targetType">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeConverter{T}"/>.</param> | ||||
/// <param name="converterType">Type of the <see cref="TypeConverter{T}"/>.</param> | /// <param name="converterType">Type of the <see cref="TypeConverter{T}"/>.</param> | ||||
public void AddGenericTypeConverter (Type targetType, Type converterType) | |||||
{ | |||||
if (!converterType.IsGenericTypeDefinition) | |||||
throw new ArgumentException($"{converterType.FullName} is not generic."); | |||||
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); | |||||
var genericArguments = converterType.GetGenericArguments(); | |||||
/// <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); | |||||
if (genericArguments.Count() > 1) | |||||
throw new InvalidOperationException($"Valid generic {converterType.FullName}s cannot have more than 1 generic type parameter"); | |||||
/// <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); | |||||
var constraints = genericArguments.SelectMany(x => x.GetGenericParameterConstraints()); | |||||
/// <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); | |||||
if (!constraints.Any(x => x.IsAssignableFrom(targetType))) | |||||
throw new InvalidOperationException($"This generic class does not support type {targetType.FullName}"); | |||||
/// <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); | |||||
_genericTypeConverters[targetType] = converterType; | |||||
/// <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) | |||||
{ | |||||
var serializedValues = new string[args.Length]; | |||||
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; | |||||
} | |||||
return string.Format(format, serializedValues); | |||||
} | } | ||||
/// <summary> | /// <summary> | ||||
@@ -870,7 +956,7 @@ namespace Discord.Interactions | |||||
if (_modalInfos.ContainsKey(type)) | if (_modalInfos.ContainsKey(type)) | ||||
throw new InvalidOperationException($"Modal type {type.FullName} already exists."); | 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) | internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null) | ||||
@@ -1016,7 +1102,7 @@ namespace Discord.Interactions | |||||
public ModuleInfo GetModuleInfo<TModule> ( ) where TModule : class | public ModuleInfo GetModuleInfo<TModule> ( ) where TModule : class | ||||
{ | { | ||||
if (!typeof(IInteractionModuleBase).IsAssignableFrom(typeof(TModule))) | 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)]; | var module = _typedModuleDefs[typeof(TModule)]; | ||||
@@ -1032,21 +1118,6 @@ namespace Discord.Interactions | |||||
_lock.Dispose(); | _lock.Dispose(); | ||||
} | } | ||||
private Type GetMostSpecificTypeConverter (Type type) | |||||
{ | |||||
if (_genericTypeConverters.TryGetValue(type, out var matching)) | |||||
return matching; | |||||
if (type.IsGenericType && _genericTypeConverters.TryGetValue(type.GetGenericTypeDefinition(), out var genericDefinition)) | |||||
return genericDefinition; | |||||
var typeInterfaces = type.GetInterfaces(); | |||||
var candidates = _genericTypeConverters.Where(x => x.Key.IsAssignableFrom(type)) | |||||
.OrderByDescending(x => typeInterfaces.Count(y => y.IsAssignableFrom(x.Key))); | |||||
return candidates.First().Value; | |||||
} | |||||
private void EnsureClientReady() | private void EnsureClientReady() | ||||
{ | { | ||||
if (RestClient?.CurrentUser is null || RestClient?.CurrentUser?.Id == 0) | if (RestClient?.CurrentUser is null || RestClient?.CurrentUser?.Id == 0) | ||||
@@ -31,7 +31,7 @@ namespace Discord.Interactions | |||||
/// <summary> | /// <summary> | ||||
/// Gets or sets the string expression that will be treated as a wild card. | /// Gets or sets the string expression that will be treated as a wild card. | ||||
/// </summary> | /// </summary> | ||||
public string WildCardExpression { get; set; } | |||||
public string WildCardExpression { get; set; } = "*"; | |||||
/// <summary> | /// <summary> | ||||
/// Gets or sets the option to use compiled lambda expressions to create module instances and execute commands. This method improves performance at the cost of memory. | /// Gets or sets the option to use compiled lambda expressions to create module instances and execute commands. This method improves performance at the cost of memory. | ||||
@@ -0,0 +1,92 @@ | |||||
using System; | |||||
using System.Collections.Concurrent; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Reflection; | |||||
namespace Discord.Interactions | |||||
{ | |||||
internal class TypeMap<TConverter, TData> | |||||
where TConverter : class, ITypeConverter<TData> | |||||
{ | |||||
private readonly ConcurrentDictionary<Type, TConverter> _concretes; | |||||
private readonly ConcurrentDictionary<Type, Type> _generics; | |||||
private readonly InteractionService _interactionService; | |||||
public TypeMap(InteractionService interactionService, IDictionary<Type, TConverter> 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 TConverter Get(Type type, IServiceProvider services = null) | |||||
{ | |||||
if (_concretes.TryGetValue(type, out var specific)) | |||||
return specific; | |||||
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<TConverter>.CreateObject(converterType.MakeGenericType(type).GetTypeInfo(), _interactionService, services); | |||||
_concretes[type] = converter; | |||||
return converter; | |||||
} | |||||
if (_concretes.Any(x => x.Value.CanConvertTo(type))) | |||||
return _concretes.First(x => x.Value.CanConvertTo(type)).Value; | |||||
throw new ArgumentException($"No type {typeof(TConverter).Name} is defined for this {type.FullName}", nameof(type)); | |||||
} | |||||
public void AddConcrete<TTarget>(TConverter converter) => | |||||
AddConcrete(typeof(TTarget), converter); | |||||
public void AddConcrete(Type type, TConverter 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.Length > 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; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,39 @@ | |||||
using System; | |||||
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); | |||||
} | |||||
/// <inheritdoc/> | |||||
public abstract class ComponentTypeConverter<T> : ComponentTypeConverter | |||||
{ | |||||
/// <inheritdoc/> | |||||
public sealed override bool CanConvertTo(Type type) => | |||||
typeof(T).IsAssignableFrom(type); | |||||
} | |||||
} |
@@ -0,0 +1,45 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Interactions | |||||
{ | |||||
internal sealed class DefaultArrayComponentConverter<T> : ComponentTypeConverter<T> | |||||
{ | |||||
private readonly TypeReader _typeReader; | |||||
private readonly Type _underlyingType; | |||||
public DefaultArrayComponentConverter(InteractionService interactionService) | |||||
{ | |||||
var type = typeof(T); | |||||
if (!type.IsArray) | |||||
throw new InvalidOperationException($"{nameof(DefaultArrayComponentConverter<T>)} cannot be used to convert a non-array type."); | |||||
_underlyingType = typeof(T).GetElementType(); | |||||
_typeReader = interactionService.GetTypeReader(_underlyingType); | |||||
} | |||||
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) | |||||
{ | |||||
var results = new List<TypeConverterResult>(); | |||||
foreach (var value in option.Values) | |||||
{ | |||||
var result = await _typeReader.ReadAsync(context, value, services).ConfigureAwait(false); | |||||
if (!result.IsSuccess) | |||||
return result; | |||||
results.Add(result); | |||||
} | |||||
var destination = Array.CreateInstance(_underlyingType, results.Count); | |||||
for (var i = 0; i < results.Count; i++) | |||||
destination.SetValue(results[i].Value, i); | |||||
return TypeConverterResult.FromSuccess(destination); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,26 @@ | |||||
using System; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Interactions | |||||
{ | |||||
internal sealed class DefaultValueComponentConverter<T> : ComponentTypeConverter<T> | |||||
where T : IConvertible | |||||
{ | |||||
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) | |||||
{ | |||||
try | |||||
{ | |||||
return option.Type switch | |||||
{ | |||||
ComponentType.SelectMenu => Task.FromResult(TypeConverterResult.FromSuccess(Convert.ChangeType(string.Join(",", option.Values), typeof(T)))), | |||||
ComponentType.TextInput => Task.FromResult(TypeConverterResult.FromSuccess(Convert.ChangeType(option.Value, typeof(T)))), | |||||
_ => Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option.Type} doesn't have a convertible value.")) | |||||
}; | |||||
} | |||||
catch (InvalidCastException castEx) | |||||
{ | |||||
return Task.FromResult(TypeConverterResult.FromError(castEx)); | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -6,7 +6,7 @@ namespace Discord.Interactions | |||||
/// <summary> | /// <summary> | ||||
/// Base class for creating TypeConverters. <see cref="InteractionService"/> uses TypeConverters to interface with Slash Command parameters. | /// Base class for creating TypeConverters. <see cref="InteractionService"/> uses TypeConverters to interface with Slash Command parameters. | ||||
/// </summary> | /// </summary> | ||||
public abstract class TypeConverter | |||||
public abstract class TypeConverter : ITypeConverter<IApplicationCommandInteractionDataOption> | |||||
{ | { | ||||
/// <summary> | /// <summary> | ||||
/// Will be used to search for alternative TypeConverters whenever the Command Service encounters an unknown parameter type. | /// Will be used to search for alternative TypeConverters whenever the Command Service encounters an unknown parameter type. |
@@ -0,0 +1,48 @@ | |||||
using System; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Interactions | |||||
{ | |||||
internal abstract class DefaultSnowflakeReader<T> : TypeReader<T> | |||||
where T : class, ISnowflakeEntity | |||||
{ | |||||
protected abstract Task<T> GetEntity(ulong id, IInteractionContext ctx); | |||||
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, string option, IServiceProvider services) | |||||
{ | |||||
if (!ulong.TryParse(option, out var snowflake)) | |||||
return TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option} isn't a valid snowflake thus cannot be converted into {typeof(T).Name}"); | |||||
var result = await GetEntity(snowflake, context).ConfigureAwait(false); | |||||
return result is not null ? | |||||
TypeConverterResult.FromSuccess(result) : TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option} must be a valid {typeof(T).Name} snowflake to be parsed."); | |||||
} | |||||
public override Task<string> SerializeAsync(object obj, IServiceProvider services) => Task.FromResult((obj as ISnowflakeEntity)?.Id.ToString()); | |||||
} | |||||
internal sealed class DefaultUserReader<T> : DefaultSnowflakeReader<T> | |||||
where T : class, IUser | |||||
{ | |||||
protected override async Task<T> GetEntity(ulong id, IInteractionContext ctx) => await ctx.Client.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T; | |||||
} | |||||
internal sealed class DefaultChannelReader<T> : DefaultSnowflakeReader<T> | |||||
where T : class, IChannel | |||||
{ | |||||
protected override async Task<T> GetEntity(ulong id, IInteractionContext ctx) => await ctx.Client.GetChannelAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T; | |||||
} | |||||
internal sealed class DefaultRoleReader<T> : DefaultSnowflakeReader<T> | |||||
where T : class, IRole | |||||
{ | |||||
protected override Task<T> GetEntity(ulong id, IInteractionContext ctx) => Task.FromResult(ctx.Guild?.GetRole(id) as T); | |||||
} | |||||
internal sealed class DefaultMessageReader<T> : DefaultSnowflakeReader<T> | |||||
where T : class, IMessage | |||||
{ | |||||
protected override async Task<T> GetEntity(ulong id, IInteractionContext ctx) => await ctx.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T; | |||||
} | |||||
} |
@@ -0,0 +1,22 @@ | |||||
using System; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Interactions | |||||
{ | |||||
internal sealed class DefaultValueReader<T> : TypeReader<T> | |||||
where T : IConvertible | |||||
{ | |||||
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, string option, IServiceProvider services) | |||||
{ | |||||
try | |||||
{ | |||||
var converted = Convert.ChangeType(option, typeof(T)); | |||||
return Task.FromResult(TypeConverterResult.FromSuccess(converted)); | |||||
} | |||||
catch (InvalidCastException castEx) | |||||
{ | |||||
return Task.FromResult(TypeConverterResult.FromError(castEx)); | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,25 @@ | |||||
using System; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Interactions | |||||
{ | |||||
internal sealed class EnumReader<T> : TypeReader<T> | |||||
where T : struct, Enum | |||||
{ | |||||
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, string option, IServiceProvider services) | |||||
{ | |||||
return Task.FromResult(Enum.TryParse<T>(option, out var result) ? | |||||
TypeConverterResult.FromSuccess(result) : TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Value {option} cannot be converted to {nameof(T)}")); | |||||
} | |||||
public override Task<string> SerializeAsync(object obj, IServiceProvider services) | |||||
{ | |||||
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); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,46 @@ | |||||
using System; | |||||
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 execution context.</param> | |||||
/// <param name="option">Received 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 representing the conversion process. The result of the task contains the conversion result. | |||||
/// </returns> | |||||
public virtual Task<string> SerializeAsync(object obj, IServiceProvider services) => Task.FromResult(obj.ToString()); | |||||
} | |||||
/// <inheritdoc/> | |||||
public abstract class TypeReader<T> : TypeReader | |||||
{ | |||||
/// <inheritdoc/> | |||||
public sealed override bool CanConvertTo(Type type) => | |||||
typeof(T).IsAssignableFrom(type); | |||||
} | |||||
} |
@@ -7,20 +7,20 @@ namespace Discord.Interactions | |||||
{ | { | ||||
internal static class ModalUtils | internal static class ModalUtils | ||||
{ | { | ||||
private static ConcurrentDictionary<Type, ModalInfo> _modalInfos = new(); | |||||
private static readonly ConcurrentDictionary<Type, ModalInfo> _modalInfos = new(); | |||||
public static IReadOnlyCollection<ModalInfo> Modals => _modalInfos.Values.ToReadOnlyCollection(); | public static IReadOnlyCollection<ModalInfo> Modals => _modalInfos.Values.ToReadOnlyCollection(); | ||||
public static ModalInfo GetOrAdd(Type type) | |||||
public static ModalInfo GetOrAdd(Type type, InteractionService interactionService) | |||||
{ | { | ||||
if (!typeof(IModal).IsAssignableFrom(type)) | if (!typeof(IModal).IsAssignableFrom(type)) | ||||
throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); | throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); | ||||
return _modalInfos.GetOrAdd(type, ModuleClassBuilder.BuildModalInfo(type)); | |||||
return _modalInfos.GetOrAdd(type, ModuleClassBuilder.BuildModalInfo(type, interactionService)); | |||||
} | } | ||||
public static ModalInfo GetOrAdd<T>() where T : class, IModal | |||||
=> GetOrAdd(typeof(T)); | |||||
public static ModalInfo GetOrAdd<T>(InteractionService interactionService) where T : class, IModal | |||||
=> GetOrAdd(typeof(T), interactionService); | |||||
public static bool TryGet(Type type, out ModalInfo modalInfo) | public static bool TryGet(Type type, out ModalInfo modalInfo) | ||||
{ | { | ||||