Browse Source

Feature: Component TypeConverters and CustomID TypeReaders (#2169)

* fix sharded client current user

* add custom setter to group property of module builder

* rename serilazation method

* init

* create typemap and default typereaders

* add default readers

* create typereader targetting flags

* seperate custom id readers with component typeconverters

* add typereaders

* add customid readers

* clean up component info argument parsing

* remove obsolete method

* add component typeconverters to modals

* fix build errors

* add inline docs

* bug fixes

* code cleanup and refactorings

* fix build errors

* add GenerateCustomIdString method to interaction service

* add GenerateCustomIdString method to interaction service

* add inline docs to componentparameterbuilder

* add inline docs to GenerateCustomIdStringAsync method
tags/3.4.1
Cenk Ergen GitHub 3 years ago
parent
commit
fb4250b88c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 812 additions and 238 deletions
  1. +3
    -3
      src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs
  2. +5
    -0
      src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs
  3. +4
    -0
      src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs
  4. +4
    -2
      src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs
  5. +13
    -8
      src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
  6. +77
    -0
      src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs
  7. +8
    -1
      src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs
  8. +12
    -0
      src/Discord.Net.Interactions/Entities/ITypeConverter.cs
  9. +1
    -8
      src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs
  10. +14
    -21
      src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs
  11. +21
    -56
      src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs
  12. +27
    -9
      src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs
  13. +23
    -40
      src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs
  14. +6
    -0
      src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs
  15. +50
    -1
      src/Discord.Net.Interactions/Info/ModalInfo.cs
  16. +34
    -0
      src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs
  17. +8
    -1
      src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs
  18. +152
    -81
      src/Discord.Net.Interactions/InteractionService.cs
  19. +1
    -1
      src/Discord.Net.Interactions/InteractionServiceConfig.cs
  20. +92
    -0
      src/Discord.Net.Interactions/Map/TypeMap.cs
  21. +39
    -0
      src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/ComponentTypeConverter.cs
  22. +45
    -0
      src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs
  23. +26
    -0
      src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultValueComponentConverter.cs
  24. +0
    -0
      src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultEntityTypeConverter.cs
  25. +0
    -0
      src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultValueConverter.cs
  26. +0
    -0
      src/Discord.Net.Interactions/TypeConverters/SlashCommands/EnumConverter.cs
  27. +0
    -0
      src/Discord.Net.Interactions/TypeConverters/SlashCommands/NullableConverter.cs
  28. +0
    -0
      src/Discord.Net.Interactions/TypeConverters/SlashCommands/TimeSpanConverter.cs
  29. +1
    -1
      src/Discord.Net.Interactions/TypeConverters/SlashCommands/TypeConverter.cs
  30. +48
    -0
      src/Discord.Net.Interactions/TypeReaders/DefaultSnowflakeReader.cs
  31. +22
    -0
      src/Discord.Net.Interactions/TypeReaders/DefaultValueReader.cs
  32. +25
    -0
      src/Discord.Net.Interactions/TypeReaders/EnumReader.cs
  33. +46
    -0
      src/Discord.Net.Interactions/TypeReaders/TypeReader.cs
  34. +5
    -5
      src/Discord.Net.Interactions/Utilities/ModalUtils.cs

+ 3
- 3
src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs View File

@@ -5,7 +5,7 @@ namespace Discord.Interactions.Builders
/// <summary>
/// Represents a builder for creating <see cref="ComponentCommandInfo"/>.
/// </summary>
public sealed class ComponentCommandBuilder : CommandBuilder<ComponentCommandInfo, ComponentCommandBuilder, CommandParameterBuilder>
public sealed class ComponentCommandBuilder : CommandBuilder<ComponentCommandInfo, ComponentCommandBuilder, ComponentCommandParameterBuilder>
{
protected override ComponentCommandBuilder Instance => this;

@@ -26,9 +26,9 @@ namespace Discord.Interactions.Builders
/// <returns>
/// The builder instance.
/// </returns>
public override ComponentCommandBuilder AddParameter (Action<CommandParameterBuilder> configure)
public override ComponentCommandBuilder AddParameter (Action<ComponentCommandParameterBuilder> configure)
{
var parameter = new CommandParameterBuilder(this);
var parameter = new ComponentCommandParameterBuilder(this);
configure(parameter);
AddParameters(parameter);
return this;


+ 5
- 0
src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs View File

@@ -38,6 +38,11 @@ namespace Discord.Interactions.Builders
/// </summary>
Type Type { get; }

/// <summary>
/// Get the <see cref="ComponentTypeConverter"/> assigned to this input.
/// </summary>
ComponentTypeConverter TypeConverter { get; }

/// <summary>
/// Gets the default value of this input component.
/// </summary>


+ 4
- 0
src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs View File

@@ -33,6 +33,9 @@ namespace Discord.Interactions.Builders
/// <inheritdoc/>
public Type Type { get; private set; }

/// <inheritdoc/>
public ComponentTypeConverter TypeConverter { get; private set; }

/// <inheritdoc/>
public object DefaultValue { get; set; }

@@ -111,6 +114,7 @@ namespace Discord.Interactions.Builders
public TBuilder WithType(Type type)
{
Type = type;
TypeConverter = Modal._interactionService.GetComponentTypeConverter(type);
return Instance;
}



+ 4
- 2
src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs View File

@@ -9,6 +9,7 @@ namespace Discord.Interactions.Builders
/// </summary>
public class ModalBuilder
{
internal readonly InteractionService _interactionService;
internal readonly List<IInputComponentBuilder> _components;

/// <summary>
@@ -31,11 +32,12 @@ namespace Discord.Interactions.Builders
/// </summary>
public IReadOnlyCollection<IInputComponentBuilder> Components => _components;

internal ModalBuilder(Type type)
internal ModalBuilder(Type type, InteractionService interactionService)
{
if (!typeof(IModal).IsAssignableFrom(type))
throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type));

_interactionService = interactionService;
_components = new();
}

@@ -43,7 +45,7 @@ namespace Discord.Interactions.Builders
/// Initializes a new <see cref="ModalBuilder"/>
/// </summary>
/// <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;
}


+ 13
- 8
src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs View File

@@ -231,9 +231,6 @@ namespace Discord.Interactions.Builders
private static void BuildComponentCommand (ComponentCommandBuilder builder, Func<IServiceProvider, IInteractionModuleBase> createInstance, MethodInfo methodInfo,
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();

builder.MethodName = methodInfo.Name;
@@ -260,8 +257,10 @@ namespace Discord.Interactions.Builders

var parameters = methodInfo.GetParameters();

var wildCardCount = Regex.Matches(Regex.Escape(builder.Name), Regex.Escape(commandService._wildCardExp)).Count;

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);
}
@@ -310,8 +309,8 @@ namespace Discord.Interactions.Builders
if (parameters.Count(x => typeof(IModal).IsAssignableFrom(x.ParameterType)) > 1)
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();

@@ -464,6 +463,12 @@ namespace Discord.Interactions.Builders
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)
where TInfo : class, IParameterInfo
where TBuilder : ParameterBuilder<TInfo, TBuilder>
@@ -495,7 +500,7 @@ namespace Discord.Interactions.Builders
#endregion

#region Modals
public static ModalInfo BuildModalInfo(Type modalType)
public static ModalInfo BuildModalInfo(Type modalType, InteractionService interactionService)
{
if (!typeof(IModal).IsAssignableFrom(modalType))
throw new InvalidOperationException($"{modalType.FullName} isn't an implementation of {typeof(IModal).FullName}");
@@ -504,7 +509,7 @@ namespace Discord.Interactions.Builders

try
{
var builder = new ModalBuilder(modalType)
var builder = new ModalBuilder(modalType, interactionService)
{
Title = instance.Title
};


+ 77
- 0
src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs View File

@@ -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);
}
}

+ 8
- 1
src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs View File

@@ -20,6 +20,11 @@ namespace Discord.Interactions.Builders
/// </summary>
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) { }

/// <summary>
@@ -34,7 +39,9 @@ namespace Discord.Interactions.Builders
public override ModalCommandParameterBuilder SetParameterType(Type 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);
}


+ 12
- 0
src/Discord.Net.Interactions/Entities/ITypeConverter.cs View File

@@ -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);
}
}

+ 1
- 8
src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs View File

@@ -41,14 +41,7 @@ namespace Discord.Interactions
if (context.Interaction is not IAutocompleteInteraction)
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/>


+ 14
- 21
src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs View File

@@ -123,10 +123,7 @@ namespace Discord.Interactions
return moduleResult;

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)
@@ -140,8 +137,8 @@ namespace Discord.Interactions
using var scope = services?.CreateScope();
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:
_ = Task.Run(async () =>
@@ -170,20 +167,14 @@ namespace Discord.Interactions
{
var preconditionResult = await CheckPreconditionsAsync(context, services).ConfigureAwait(false);
if (!preconditionResult.IsSuccess)
{
await InvokeModuleEvent(context, preconditionResult).ConfigureAwait(false);
return preconditionResult;
}
return await InvokeEventAndReturn(context, preconditionResult).ConfigureAwait(false);

var index = 0;
foreach (var parameter in Parameters)
{
var result = await parameter.CheckPreconditionsAsync(context, args[index++], services).ConfigureAwait(false);
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);
@@ -192,20 +183,16 @@ namespace Discord.Interactions
{
var result = await resultTask.ConfigureAwait(false);
await InvokeModuleEvent(context, result).ConfigureAwait(false);
if (result is RuntimeResult || result is ExecuteResult)
if (result is RuntimeResult or ExecuteResult)
return result;
}
else
{
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)
{
@@ -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)
{
var currentParent = parent;


+ 21
- 56
src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs View File

@@ -1,5 +1,4 @@
using Discord.Interactions.Builders;
using Discord.WebSocket;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -11,10 +10,10 @@ namespace Discord.Interactions
/// <summary>
/// Represents the info class of an attribute based method for handling Component Interaction events.
/// </summary>
public class ComponentCommandInfo : CommandInfo<CommandParameterInfo>
public class ComponentCommandInfo : CommandInfo<ComponentCommandParameterInfo>
{
/// <inheritdoc/>
public override IReadOnlyCollection<CommandParameterInfo> Parameters { get; }
public override IReadOnlyCollection<ComponentCommandParameterInfo> Parameters { get; }

/// <inheritdoc/>
public override bool SupportsWildCards => true;
@@ -42,80 +41,46 @@ namespace Discord.Interactions
if (context.Interaction is not IComponentInteraction componentInteraction)
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/>
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)
{
var paramCount = paramList.Count();
var captureCount = wildcardCaptures?.Count() ?? 0;

if (context.Interaction is not IComponentInteraction messageComponent)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Component Command Interaction");

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);
}
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)


+ 27
- 9
src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.Tracing;
using System.Linq;
using System.Threading.Tasks;
namespace Discord.Interactions
@@ -47,21 +48,38 @@ namespace Discord.Interactions

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)
{
var result = ExecuteResult.FromError(ex);
await InvokeModuleEvent(context, result).ConfigureAwait(false);
return result;
return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false);
}
}



+ 23
- 40
src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs View File

@@ -70,34 +70,27 @@ namespace Discord.Interactions
{
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);

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.");
}

args[i] = parseResult.Value;
}
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)
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.");

ctorArgs[i] = parseResult.Value;
}

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)


+ 6
- 0
src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs View File

@@ -39,6 +39,11 @@ namespace Discord.Interactions
/// </summary>
public Type Type { get; }

/// <summary>
/// Gets the <see cref="ComponentTypeConverter"/> assigned to this component.
/// </summary>
public ComponentTypeConverter TypeConverter { get; }

/// <summary>
/// Gets the default value of this component.
/// </summary>
@@ -57,6 +62,7 @@ namespace Discord.Interactions
IsRequired = builder.IsRequired;
ComponentType = builder.ComponentType;
Type = builder.Type;
TypeConverter = builder.TypeConverter;
DefaultValue = builder.DefaultValue;
Attributes = builder.Attributes.ToImmutableArray();
}


+ 50
- 1
src/Discord.Net.Interactions/Info/ModalInfo.cs View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.Interactions
{
@@ -19,6 +20,7 @@ namespace Discord.Interactions
/// </summary>
public class ModalInfo
{
internal readonly InteractionService _interactionService;
internal readonly ModalInitializer _initializer;

/// <summary>
@@ -53,16 +55,18 @@ namespace Discord.Interactions

TextComponents = Components.OfType<TextInputComponentInfo>().ToImmutableArray();

_interactionService = builder._interactionService;
_initializer = builder.ModalInitializer;
}

/// <summary>
/// Creates an <see cref="IModal"/> and fills it with provided message components.
/// </summary>
/// <param name="components"><see cref="IModalInteraction"/> that will be injected into the modal.</param>
/// <param name="modalInteraction"><see cref="IModalInteraction"/> that will be injected into the modal.</param>
/// <returns>
/// A <see cref="IModal"/> filled with the provided components.
/// </returns>
[Obsolete("This method is no longer supported with the introduction of Component TypeConverters, please use the CreateModalAsync method.")]
public IModal CreateModal(IModalInteraction modalInteraction, bool throwOnMissingField = false)
{
var args = new object[Components.Count];
@@ -86,5 +90,50 @@ namespace Discord.Interactions

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));
}
}
}

+ 34
- 0
src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs View File

@@ -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;
}
}
}

+ 8
- 1
src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs View File

@@ -15,7 +15,12 @@ namespace Discord.Interactions
/// <summary>
/// Gets whether this parameter is an <see cref="IModal"/>
/// </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/>
public new ModalCommandInfo Command => base.Command as ModalCommandInfo;
@@ -23,6 +28,8 @@ namespace Discord.Interactions
internal ModalCommandParameterInfo(ModalCommandParameterBuilder builder, ICommandInfo command) : base(builder, command)
{
Modal = builder.Modal;
IsModalParameter = builder.IsModalParameter;
TypeReader = builder.TypeReader;
}
}
}

+ 152
- 81
src/Discord.Net.Interactions/InteractionService.cs View File

@@ -3,6 +3,7 @@ using Discord.Logging;
using Discord.Rest;
using Discord.WebSocket;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -66,8 +67,9 @@ namespace Discord.Interactions
private readonly CommandMap<AutocompleteCommandInfo> _autocompleteCommandMap;
private readonly CommandMap<ModalCommandInfo> _modalCommandMap;
private readonly HashSet<ModuleInfo> _moduleDefs;
private readonly ConcurrentDictionary<Type, TypeConverter> _typeConverters;
private readonly ConcurrentDictionary<Type, Type> _genericTypeConverters;
private readonly TypeMap<TypeConverter, IApplicationCommandInteractionDataOption> _typeConverterMap;
private readonly TypeMap<ComponentTypeConverter, IComponentInteractionData> _compTypeConverterMap;
private readonly TypeMap<TypeReader, string> _typeReaderMap;
private readonly ConcurrentDictionary<Type, IAutocompleteHandler> _autocompleteHandlers = new();
private readonly ConcurrentDictionary<Type, ModalInfo> _modalInfos = new();
private readonly SemaphoreSlim _lock;
@@ -179,22 +181,38 @@ namespace Discord.Interactions
_autoServiceScopes = config.AutoServiceScopes;
_restResponseCallback = config.RestResponseCallback;

_genericTypeConverters = new ConcurrentDictionary<Type, Type>
{
[typeof(IChannel)] = typeof(DefaultChannelConverter<>),
[typeof(IRole)] = typeof(DefaultRoleConverter<>),
[typeof(IAttachment)] = typeof(DefaultAttachmentConverter<>),
[typeof(IUser)] = typeof(DefaultUserConverter<>),
[typeof(IMentionable)] = typeof(DefaultMentionableConverter<>),
[typeof(IConvertible)] = typeof(DefaultValueConverter<>),
[typeof(Enum)] = typeof(EnumConverter<>),
[typeof(Nullable<>)] = typeof(NullableConverter<>),
};
_typeConverterMap = new TypeMap<TypeConverter, IApplicationCommandInteractionDataOption>(this, new ConcurrentDictionary<Type, TypeConverter>
{
[typeof(TimeSpan)] = new TimeSpanConverter()
}, new ConcurrentDictionary<Type, Type>
{
[typeof(IChannel)] = typeof(DefaultChannelConverter<>),
[typeof(IRole)] = typeof(DefaultRoleConverter<>),
[typeof(IAttachment)] = typeof(DefaultAttachmentConverter<>),
[typeof(IUser)] = typeof(DefaultUserConverter<>),
[typeof(IMentionable)] = typeof(DefaultMentionableConverter<>),
[typeof(IConvertible)] = typeof(DefaultValueConverter<>),
[typeof(Enum)] = typeof(EnumConverter<>),
[typeof(Nullable<>)] = typeof(NullableConverter<>)
});

_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>
@@ -293,7 +311,7 @@ namespace Discord.Interactions
public async Task<ModuleInfo> AddModuleAsync (Type type, IServiceProvider services)
{
if (!typeof(IInteractionModuleBase).IsAssignableFrom(type))
throw new ArgumentException("Type parameter must be a type of Slash Module", "T");
throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(type));

services ??= EmptyServiceProvider.Instance;

@@ -326,7 +344,7 @@ namespace Discord.Interactions
}

/// <summary>
/// Register Application Commands from <see cref="ContextCommands"/> and <see cref="SlashCommands"/> to a guild.
/// Register Application Commands from <see cref="ContextCommands"/> and <see cref="SlashCommands"/> to a guild.
/// </summary>
/// <param name="guildId">Id of the target guild.</param>
/// <param name="deleteMissing">If <see langword="false"/>, this operation will not delete the commands that are missing from <see cref="InteractionService"/>.</param>
@@ -422,7 +440,7 @@ namespace Discord.Interactions
}

/// <summary>
/// Register Application Commands from modules provided in <paramref name="modules"/> to a guild.
/// Register Application Commands from modules provided in <paramref name="modules"/> to a guild.
/// </summary>
/// <param name="guild">The target guild.</param>
/// <param name="modules">Modules to be registered to Discord.</param>
@@ -449,7 +467,7 @@ namespace Discord.Interactions
}

/// <summary>
/// Register Application Commands from modules provided in <paramref name="modules"/> as global commands.
/// Register Application Commands from modules provided in <paramref name="modules"/> as global commands.
/// </summary>
/// <param name="modules">Modules to be registered to Discord.</param>
/// <returns>
@@ -677,7 +695,7 @@ namespace Discord.Interactions
public async Task<IResult> ExecuteCommandAsync (IInteractionContext context, IServiceProvider services)
{
var interaction = context.Interaction;
return interaction switch
{
ISlashCommandInteraction slashCommand => await ExecuteSlashCommandAsync(context, slashCommand, services).ConfigureAwait(false),
@@ -781,47 +799,24 @@ namespace Discord.Interactions
return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false);
}

internal TypeConverter GetTypeConverter (Type type, IServiceProvider services = null)
{
if (_typeConverters.TryGetValue(type, out var specific))
return specific;
else if (_genericTypeConverters.Any(x => x.Key.IsAssignableFrom(type)
|| (x.Key.IsGenericTypeDefinition && type.IsGenericType && x.Key.GetGenericTypeDefinition() == type.GetGenericTypeDefinition())))
{
services ??= EmptyServiceProvider.Instance;

var converterType = GetMostSpecificTypeConverter(type);
var converter = ReflectionUtils<TypeConverter>.CreateObject(converterType.MakeGenericType(type).GetTypeInfo(), this, services);
_typeConverters[type] = converter;
return converter;
}

else if (_typeConverters.Any(x => x.Value.CanConvertTo(type)))
return _typeConverters.First(x => x.Value.CanConvertTo(type)).Value;

throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type");
}
internal TypeConverter GetTypeConverter(Type type, IServiceProvider services = null)
=> _typeConverterMap.Get(type, services);

/// <summary>
/// Add a concrete type <see cref="TypeConverter"/>.
/// </summary>
/// <typeparam name="T">Primary target <see cref="Type"/> of the <see cref="TypeConverter"/>.</typeparam>
/// <param name="converter">The <see cref="TypeConverter"/> instance.</param>
public void AddTypeConverter<T> (TypeConverter converter) =>
AddTypeConverter(typeof(T), converter);
public void AddTypeConverter<T>(TypeConverter converter) =>
_typeConverterMap.AddConcrete<T>(converter);

/// <summary>
/// Add a concrete type <see cref="TypeConverter"/>.
/// </summary>
/// <param name="type">Primary target <see cref="Type"/> of the <see cref="TypeConverter"/>.</param>
/// <param name="converter">The <see cref="TypeConverter"/> instance.</param>
public void AddTypeConverter (Type type, TypeConverter converter)
{
if (!converter.CanConvertTo(type))
throw new ArgumentException($"This {converter.GetType().FullName} cannot read {type.FullName} and cannot be registered as its {nameof(TypeConverter)}");

_typeConverters[type] = converter;
}
public void AddTypeConverter(Type type, TypeConverter converter) =>
_typeConverterMap.AddConcrete(type, converter);

/// <summary>
/// Add a generic type <see cref="TypeConverter{T}"/>.
@@ -829,30 +824,121 @@ namespace Discord.Interactions
/// <typeparam name="T">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeConverter{T}"/>.</typeparam>
/// <param name="converterType">Type of the <see cref="TypeConverter{T}"/>.</param>

public void AddGenericTypeConverter<T> (Type converterType) =>
AddGenericTypeConverter(typeof(T), converterType);
public void AddGenericTypeConverter<T>(Type converterType) =>
_typeConverterMap.AddGeneric<T>(converterType);

/// <summary>
/// Add a generic type <see cref="TypeConverter{T}"/>.
/// </summary>
/// <param name="targetType">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeConverter{T}"/>.</param>
/// <param name="converterType">Type of the <see cref="TypeConverter{T}"/>.</param>
public void AddGenericTypeConverter (Type targetType, Type converterType)
{
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>
@@ -870,7 +956,7 @@ namespace Discord.Interactions
if (_modalInfos.ContainsKey(type))
throw new InvalidOperationException($"Modal type {type.FullName} already exists.");

return ModalUtils.GetOrAdd(type);
return ModalUtils.GetOrAdd(type, this);
}

internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null)
@@ -1016,7 +1102,7 @@ namespace Discord.Interactions
public ModuleInfo GetModuleInfo<TModule> ( ) where TModule : class
{
if (!typeof(IInteractionModuleBase).IsAssignableFrom(typeof(TModule)))
throw new ArgumentException("Type parameter must be a type of Slash Module", "TModule");
throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(TModule));

var module = _typedModuleDefs[typeof(TModule)];

@@ -1032,21 +1118,6 @@ namespace Discord.Interactions
_lock.Dispose();
}

private Type GetMostSpecificTypeConverter (Type type)
{
if (_genericTypeConverters.TryGetValue(type, out var matching))
return matching;

if (type.IsGenericType && _genericTypeConverters.TryGetValue(type.GetGenericTypeDefinition(), out var genericDefinition))
return genericDefinition;

var typeInterfaces = type.GetInterfaces();
var candidates = _genericTypeConverters.Where(x => x.Key.IsAssignableFrom(type))
.OrderByDescending(x => typeInterfaces.Count(y => y.IsAssignableFrom(x.Key)));

return candidates.First().Value;
}

private void EnsureClientReady()
{
if (RestClient?.CurrentUser is null || RestClient?.CurrentUser?.Id == 0)


+ 1
- 1
src/Discord.Net.Interactions/InteractionServiceConfig.cs View File

@@ -31,7 +31,7 @@ namespace Discord.Interactions
/// <summary>
/// Gets or sets the string expression that will be treated as a wild card.
/// </summary>
public string WildCardExpression { get; set; }
public string WildCardExpression { get; set; } = "*";

/// <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.


+ 92
- 0
src/Discord.Net.Interactions/Map/TypeMap.cs View File

@@ -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;
}
}
}

+ 39
- 0
src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/ComponentTypeConverter.cs View File

@@ -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);
}
}

+ 45
- 0
src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs View File

@@ -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);
}
}
}

+ 26
- 0
src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultValueComponentConverter.cs View File

@@ -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));
}
}
}
}

src/Discord.Net.Interactions/TypeConverters/DefaultEntityTypeConverter.cs → src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultEntityTypeConverter.cs View File


src/Discord.Net.Interactions/TypeConverters/DefaultValueConverter.cs → src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultValueConverter.cs View File


src/Discord.Net.Interactions/TypeConverters/EnumConverter.cs → src/Discord.Net.Interactions/TypeConverters/SlashCommands/EnumConverter.cs View File


src/Discord.Net.Interactions/TypeConverters/NullableConverter.cs → src/Discord.Net.Interactions/TypeConverters/SlashCommands/NullableConverter.cs View File


src/Discord.Net.Interactions/TypeConverters/TimeSpanConverter.cs → src/Discord.Net.Interactions/TypeConverters/SlashCommands/TimeSpanConverter.cs View File


src/Discord.Net.Interactions/TypeConverters/TypeConverter.cs → src/Discord.Net.Interactions/TypeConverters/SlashCommands/TypeConverter.cs View File

@@ -6,7 +6,7 @@ namespace Discord.Interactions
/// <summary>
/// Base class for creating TypeConverters. <see cref="InteractionService"/> uses TypeConverters to interface with Slash Command parameters.
/// </summary>
public abstract class TypeConverter
public abstract class TypeConverter : ITypeConverter<IApplicationCommandInteractionDataOption>
{
/// <summary>
/// Will be used to search for alternative TypeConverters whenever the Command Service encounters an unknown parameter type.

+ 48
- 0
src/Discord.Net.Interactions/TypeReaders/DefaultSnowflakeReader.cs View File

@@ -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;
}
}

+ 22
- 0
src/Discord.Net.Interactions/TypeReaders/DefaultValueReader.cs View File

@@ -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));
}
}
}
}

+ 25
- 0
src/Discord.Net.Interactions/TypeReaders/EnumReader.cs View File

@@ -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);
}
}
}

+ 46
- 0
src/Discord.Net.Interactions/TypeReaders/TypeReader.cs View File

@@ -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);
}
}

+ 5
- 5
src/Discord.Net.Interactions/Utilities/ModalUtils.cs View File

@@ -7,20 +7,20 @@ namespace Discord.Interactions
{
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 ModalInfo GetOrAdd(Type type)
public static ModalInfo GetOrAdd(Type type, InteractionService interactionService)
{
if (!typeof(IModal).IsAssignableFrom(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)
{


Loading…
Cancel
Save