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