@@ -42,19 +42,19 @@ namespace Discord.Interactions | |||||
if (context.Interaction is not IComponentInteraction componentInteraction) | if (context.Interaction is not IComponentInteraction componentInteraction) | ||||
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Component Interaction"); | return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Component Interaction"); | ||||
var args = new List<string>(); | |||||
var args = new List<object>(); | |||||
if (additionalArgs is not null) | if (additionalArgs is not null) | ||||
args.AddRange(additionalArgs); | args.AddRange(additionalArgs); | ||||
if (componentInteraction.Data?.Values is not null) | if (componentInteraction.Data?.Values is not null) | ||||
args.AddRange(componentInteraction.Data.Values); | |||||
args.Add(componentInteraction.Data.Values); | |||||
return await ExecuteAsync(context, Parameters, args, services); | return await ExecuteAsync(context, Parameters, args, services); | ||||
} | } | ||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
public async Task<IResult> ExecuteAsync(IInteractionContext context, IEnumerable<CommandParameterInfo> paramList, IEnumerable<string> values, | |||||
public async Task<IResult> ExecuteAsync(IInteractionContext context, IEnumerable<CommandParameterInfo> paramList, IEnumerable<object> values, | |||||
IServiceProvider services) | IServiceProvider services) | ||||
{ | { | ||||
if (context.Interaction is not IComponentInteraction messageComponent) | if (context.Interaction is not IComponentInteraction messageComponent) | ||||
@@ -62,27 +62,42 @@ namespace Discord.Interactions | |||||
try | try | ||||
{ | { | ||||
var strCount = Parameters.Count(x => x.ParameterType == typeof(string)); | |||||
var valueCount = values.Count(); | |||||
var args = new object[paramList.Count()]; | |||||
if (strCount > values?.Count()) | |||||
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters"); | |||||
for(var i = 0; i < paramList.Count(); i++) | |||||
{ | |||||
var parameter = Parameters.ElementAt(i); | |||||
if(i > valueCount) | |||||
{ | |||||
if (!parameter.IsRequired) | |||||
args[i] = parameter.DefaultValue; | |||||
else | |||||
{ | |||||
var result = ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters"); | |||||
await InvokeModuleEvent(context, result).ConfigureAwait(false); | |||||
return result; | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
var value = values.ElementAt(i); | |||||
var typeReader = parameter.TypeReader; | |||||
var componentValues = messageComponent.Data?.Values; | |||||
var readResult = await typeReader.ReadAsync(context, value, services).ConfigureAwait(false); | |||||
var args = new object[Parameters.Count]; | |||||
if (!readResult.IsSuccess) | |||||
{ | |||||
await InvokeModuleEvent(context, readResult).ConfigureAwait(false); | |||||
return readResult; | |||||
} | |||||
if (componentValues is not null) | |||||
{ | |||||
var lastParam = Parameters.Last(); | |||||
if (lastParam.ParameterType.IsArray) | |||||
args[args.Length - 1] = componentValues.Select(async x => await lastParam.TypeReader.ReadAsync(context, x, services).ConfigureAwait(false)).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] = await Parameters.ElementAt(i).TypeReader.ReadAsync(context, values.ElementAt(i), services).ConfigureAwait(false); | |||||
return await RunAsync(context, args, services).ConfigureAwait(false); | return await RunAsync(context, args, services).ConfigureAwait(false); | ||||
} | } | ||||
@@ -1,16 +1,18 @@ | |||||
using Discord.Interactions.Builders; | using Discord.Interactions.Builders; | ||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Interactions | namespace Discord.Interactions | ||||
{ | { | ||||
/// <summary> | |||||
/// Represents the parameter info class for <see cref="ComponentCommandInfo"/> commands. | |||||
/// </summary> | |||||
public class ComponentCommandParameterInfo : CommandParameterInfo | public class ComponentCommandParameterInfo : CommandParameterInfo | ||||
{ | { | ||||
/// <summary> | |||||
/// Gets the <see cref="TypeReader"/> that will be used to convert a message component value into | |||||
/// <see cref="CommandParameterInfo.ParameterType"/>. | |||||
/// </summary> | |||||
public TypeReader TypeReader { get; } | public TypeReader TypeReader { get; } | ||||
internal ComponentCommandParameterInfo(ComponentCommandParameterBuilder builder, ICommandInfo command) : base(builder, command) | internal ComponentCommandParameterInfo(ComponentCommandParameterBuilder builder, ICommandInfo command) : base(builder, command) | ||||
{ | { | ||||
TypeReader = builder.TypeReader; | TypeReader = builder.TypeReader; | ||||
@@ -0,0 +1,27 @@ | |||||
using System; | |||||
using System.Collections; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Interactions | |||||
{ | |||||
internal class ArrayReader<T> : TypeReader<T> where T : IEnumerable | |||||
{ | |||||
private readonly TypeReader _baseReader; | |||||
public ArrayReader(InteractionService interactionService) | |||||
{ | |||||
if() | |||||
interactionService.GetTypeReader(typeof) | |||||
} | |||||
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, object input, IServiceProvider services) | |||||
{ | |||||
if(input is IEnumerable enumerable) | |||||
return Task.FromResult(TypeConverterResult.FromSuccess(new )) | |||||
} | |||||
} | |||||
} |
@@ -14,27 +14,36 @@ namespace Discord.Interactions | |||||
/// final output; otherwise, an erroneous <see cref="TypeReaderResult"/> is returned. | /// final output; otherwise, an erroneous <see cref="TypeReaderResult"/> is returned. | ||||
/// </remarks> | /// </remarks> | ||||
/// <typeparam name="T">The type to be checked; must implement <see cref="IChannel"/>.</typeparam> | /// <typeparam name="T">The type to be checked; must implement <see cref="IChannel"/>.</typeparam> | ||||
public class ChannelTypeReader<T> : TypeReader<T> | |||||
where T : class, IChannel | |||||
internal class ChannelTypeReader<T> : TypeReader<T> where T : class, IChannel | |||||
{ | { | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, object input, IServiceProvider services) | public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, object input, IServiceProvider services) | ||||
{ | { | ||||
if (context.Guild is null) | |||||
if (context.Guild is not null) | |||||
{ | { | ||||
var str = input as string; | var str = input as string; | ||||
if (ulong.TryParse(str, out var channelId)) | if (ulong.TryParse(str, out var channelId)) | ||||
return TypeConverterResult.FromSuccess(await context.Guild.GetChannelAsync(channelId).ConfigureAwait(false)); | |||||
{ | |||||
var channel = await context.Guild.GetChannelAsync(channelId).ConfigureAwait(false); | |||||
if(channel is not null) | |||||
return TypeConverterResult.FromSuccess(channel as T); | |||||
} | |||||
if (MentionUtils.TryParseChannel(str, out channelId)) | if (MentionUtils.TryParseChannel(str, out channelId)) | ||||
return TypeConverterResult.FromSuccess(await context.Guild.GetChannelAsync(channelId).ConfigureAwait(false)); | |||||
{ | |||||
var channel = await context.Guild.GetChannelAsync(channelId).ConfigureAwait(false); | |||||
if(channel is not null) | |||||
return TypeConverterResult.FromSuccess(channel as T); | |||||
} | |||||
var channels = await context.Guild.GetChannelsAsync().ConfigureAwait(false); | var channels = await context.Guild.GetChannelsAsync().ConfigureAwait(false); | ||||
var nameMatch = channels.FirstOrDefault(x => string.Equals(x.Name, str, StringComparison.OrdinalIgnoreCase)); | var nameMatch = channels.FirstOrDefault(x => string.Equals(x.Name, str, StringComparison.OrdinalIgnoreCase)); | ||||
if (nameMatch is not null) | if (nameMatch is not null) | ||||
return TypeConverterResult.FromSuccess(nameMatch); | |||||
return TypeConverterResult.FromSuccess(nameMatch as T); | |||||
} | } | ||||
return TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, "Channel not found."); | return TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, "Channel not found."); | ||||
@@ -6,14 +6,20 @@ namespace Discord.Interactions | |||||
internal class EnumTypeReader<T> : TypeReader<T> where T : struct, Enum | internal class EnumTypeReader<T> : TypeReader<T> where T : struct, Enum | ||||
{ | { | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, string input, IServiceProvider services) | |||||
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, object input, IServiceProvider services) | |||||
{ | { | ||||
if (Enum.TryParse<T>(input, out var result)) | |||||
if (Enum.TryParse<T>(input as string, out var result)) | |||||
return Task.FromResult(TypeConverterResult.FromSuccess(result)); | return Task.FromResult(TypeConverterResult.FromSuccess(result)); | ||||
else | else | ||||
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Value {input} cannot be converted to {nameof(T)}")); | return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Value {input} cannot be converted to {nameof(T)}")); | ||||
} | } | ||||
public override string Serialize(object value) => value.ToString(); | |||||
public override string Serialize(object value) | |||||
{ | |||||
if (value is not Enum) | |||||
throw new ArgumentException($"{value} isn't an {nameof(Enum)}.", nameof(value)); | |||||
return value.ToString(); | |||||
} | |||||
} | } | ||||
} | } |
@@ -8,20 +8,18 @@ namespace Discord.Interactions | |||||
/// A <see cref="TypeReader"/> for parsing objects implementing <see cref="IMessage"/>. | /// A <see cref="TypeReader"/> for parsing objects implementing <see cref="IMessage"/>. | ||||
/// </summary> | /// </summary> | ||||
/// <typeparam name="T">The type to be checked; must implement <see cref="IMessage"/>.</typeparam> | /// <typeparam name="T">The type to be checked; must implement <see cref="IMessage"/>.</typeparam> | ||||
public class MessageTypeReader<T> : TypeReader | |||||
where T : class, IMessage | |||||
internal class MessageTypeReader<T> : TypeReader<T> where T : class, IMessage | |||||
{ | { | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services) | |||||
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, object input, IServiceProvider services) | |||||
{ | { | ||||
//By Id (1.0) | |||||
if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) | |||||
if (ulong.TryParse(input as string, NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) | |||||
{ | { | ||||
if (await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg) | if (await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg) | ||||
return TypeReaderResult.FromSuccess(msg); | |||||
return TypeConverterResult.FromSuccess(msg); | |||||
} | } | ||||
return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Message not found."); | |||||
return TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, "Message not found."); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -1,48 +1,44 @@ | |||||
using System; | using System; | ||||
using System.Collections.Generic; | |||||
using System.Globalization; | |||||
using System.Linq; | using System.Linq; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
namespace Discord.Commands | |||||
namespace Discord.Interactions | |||||
{ | { | ||||
/// <summary> | /// <summary> | ||||
/// A <see cref="TypeReader"/> for parsing objects implementing <see cref="IRole"/>. | /// A <see cref="TypeReader"/> for parsing objects implementing <see cref="IRole"/>. | ||||
/// </summary> | /// </summary> | ||||
/// <typeparam name="T">The type to be checked; must implement <see cref="IRole"/>.</typeparam> | /// <typeparam name="T">The type to be checked; must implement <see cref="IRole"/>.</typeparam> | ||||
public class RoleTypeReader<T> : TypeReader | |||||
where T : class, IRole | |||||
internal class RoleTypeReader<T> : TypeReader<T> where T : class, IRole | |||||
{ | { | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services) | |||||
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, object input, IServiceProvider services) | |||||
{ | { | ||||
if (context.Guild != null) | |||||
if (context.Guild is not null) | |||||
{ | { | ||||
var results = new Dictionary<ulong, TypeReaderValue>(); | |||||
var roles = context.Guild.Roles; | |||||
if (ulong.TryParse(input as string, out var id)) | |||||
{ | |||||
var role = context.Guild.GetRole(id); | |||||
//By Mention (1.0) | |||||
if (MentionUtils.TryParseRole(input, out var id)) | |||||
AddResult(results, context.Guild.GetRole(id) as T, 1.00f); | |||||
if (role is not null) | |||||
return Task.FromResult(TypeConverterResult.FromSuccess(role as T)); | |||||
} | |||||
//By Id (0.9) | |||||
if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) | |||||
AddResult(results, context.Guild.GetRole(id) as T, 0.90f); | |||||
if (MentionUtils.TryParseRole(input as string, out id)) | |||||
{ | |||||
var role = context.Guild.GetRole(id); | |||||
//By Name (0.7-0.8) | |||||
foreach (var role in roles.Where(x => string.Equals(input, x.Name, StringComparison.OrdinalIgnoreCase))) | |||||
AddResult(results, role as T, role.Name == input ? 0.80f : 0.70f); | |||||
if (role is not null) | |||||
return Task.FromResult(TypeConverterResult.FromSuccess(role as T)); | |||||
} | |||||
if (results.Count > 0) | |||||
return Task.FromResult(TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection())); | |||||
var channels = context.Guild.Roles; | |||||
var nameMatch = channels.First(x => string.Equals(x, input as string)); | |||||
if (nameMatch is not null) | |||||
return Task.FromResult(TypeConverterResult.FromSuccess(nameMatch as T)); | |||||
} | } | ||||
return Task.FromResult(TypeReaderResult.FromError(CommandError.ObjectNotFound, "Role not found.")); | |||||
} | |||||
private void AddResult(Dictionary<ulong, TypeReaderValue> results, T role, float score) | |||||
{ | |||||
if (role != null && !results.ContainsKey(role.Id)) | |||||
results.Add(role.Id, new TypeReaderValue(role, score)); | |||||
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, "Role not found.")); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -2,9 +2,9 @@ using System; | |||||
using System.Globalization; | using System.Globalization; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
namespace Discord.Commands | |||||
namespace Discord.Interactions | |||||
{ | { | ||||
internal class TimeSpanTypeReader : TypeReader | |||||
internal class TimeSpanTypeReader : TypeReader<TimeSpan> | |||||
{ | { | ||||
/// <summary> | /// <summary> | ||||
/// TimeSpan try parse formats. | /// TimeSpan try parse formats. | ||||
@@ -29,26 +29,28 @@ namespace Discord.Commands | |||||
}; | }; | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services) | |||||
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, object input, IServiceProvider services) | |||||
{ | { | ||||
if (string.IsNullOrEmpty(input)) | |||||
throw new ArgumentException(message: $"{nameof(input)} must not be null or empty.", paramName: nameof(input)); | |||||
var str = input as string; | |||||
var isNegative = input[0] == '-'; // Char for CultureInfo.InvariantCulture.NumberFormat.NegativeSign | |||||
if (string.IsNullOrEmpty(str)) | |||||
throw new ArgumentException($"{nameof(input)} must not be null or empty.", nameof(input)); | |||||
var isNegative = str[0] == '-'; // Char for CultureInfo.InvariantCulture.NumberFormat.NegativeSign | |||||
if (isNegative) | if (isNegative) | ||||
{ | { | ||||
input = input.Substring(1); | |||||
str = str.Substring(1); | |||||
} | } | ||||
if (TimeSpan.TryParseExact(input.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out var timeSpan)) | |||||
if (TimeSpan.TryParseExact(str.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out var timeSpan)) | |||||
{ | { | ||||
return isNegative | return isNegative | ||||
? Task.FromResult(TypeReaderResult.FromSuccess(-timeSpan)) | |||||
: Task.FromResult(TypeReaderResult.FromSuccess(timeSpan)); | |||||
? Task.FromResult(TypeConverterResult.FromSuccess(-timeSpan)) | |||||
: Task.FromResult(TypeConverterResult.FromSuccess(timeSpan)); | |||||
} | } | ||||
else | else | ||||
{ | { | ||||
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan")); | |||||
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ParseFailed, "Failed to parse TimeSpan")); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -9,7 +9,7 @@ namespace Discord.Interactions | |||||
/// <remarks> | /// <remarks> | ||||
/// <see cref="TypeReader"/>s are mainly used to parse message component values. For interfacing with Slash Command parameters use <see cref="TypeConverter"/>s instead. | /// <see cref="TypeReader"/>s are mainly used to parse message component values. For interfacing with Slash Command parameters use <see cref="TypeConverter"/>s instead. | ||||
/// </remarks> | /// </remarks> | ||||
public abstract class TypeReader : ITypeHandler | |||||
public abstract class TypeReader<T> : ITypeHandler | |||||
{ | { | ||||
/// <summary> | /// <summary> | ||||
/// Will be used to search for alternative TypeReaders whenever the Command Service encounters an unknown parameter type. | /// Will be used to search for alternative TypeReaders whenever the Command Service encounters an unknown parameter type. | ||||
@@ -25,19 +25,21 @@ namespace Discord.Interactions | |||||
/// <param name="input">Raw string input value.</param> | /// <param name="input">Raw string input value.</param> | ||||
/// <param name="services">Service provider that will be used to initialize the command module.</param> | /// <param name="services">Service provider that will be used to initialize the command module.</param> | ||||
/// <returns>The result of the read process.</returns> | /// <returns>The result of the read process.</returns> | ||||
public abstract Task<TypeConverterResult> ReadAsync(IInteractionContext context, object input, IServiceProvider services); | |||||
public abstract Task<TypeConverterResult> ReadAsync(IInteractionContext context, T input, IServiceProvider services); | |||||
/// <summary> | /// <summary> | ||||
/// Will be used to manipulate the outgoing command option, before the command gets registered to Discord. | /// Will be used to manipulate the outgoing command option, before the command gets registered to Discord. | ||||
/// </summary> | /// </summary> | ||||
public virtual string Serialize(object value) => null; | |||||
public virtual T Serialize(object value) => default; | |||||
} | } | ||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
public abstract class TypeReader<T> : TypeReader | |||||
public abstract class TypeReader<TGeneric, T> : TypeReader<T> | |||||
{ | { | ||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
public sealed override bool CanConvertTo(Type type) => | public sealed override bool CanConvertTo(Type type) => | ||||
typeof(T).IsAssignableFrom(type); | typeof(T).IsAssignableFrom(type); | ||||
} | } | ||||
public abstract class TypeReader : TypeReader<object> { } | |||||
} | } |
@@ -5,17 +5,16 @@ using System.Globalization; | |||||
using System.Linq; | using System.Linq; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
namespace Discord.Commands | |||||
namespace Discord.Interactions | |||||
{ | { | ||||
/// <summary> | /// <summary> | ||||
/// A <see cref="TypeReader"/> for parsing objects implementing <see cref="IUser"/>. | /// A <see cref="TypeReader"/> for parsing objects implementing <see cref="IUser"/>. | ||||
/// </summary> | /// </summary> | ||||
/// <typeparam name="T">The type to be checked; must implement <see cref="IUser"/>.</typeparam> | /// <typeparam name="T">The type to be checked; must implement <see cref="IUser"/>.</typeparam> | ||||
public class UserTypeReader<T> : TypeReader | |||||
where T : class, IUser | |||||
public class UserTypeReader<T> : TypeReader<T> where T : class, IUser | |||||
{ | { | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services) | |||||
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, string input, IServiceProvider services) | |||||
{ | { | ||||
var results = new Dictionary<ulong, TypeReaderValue>(); | var results = new Dictionary<ulong, TypeReaderValue>(); | ||||
IAsyncEnumerable<IUser> channelUsers = context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten(); // it's better | IAsyncEnumerable<IUser> channelUsers = context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten(); // it's better | ||||
@@ -85,11 +84,5 @@ namespace Discord.Commands | |||||
return TypeReaderResult.FromSuccess(results.Values.ToImmutableArray()); | return TypeReaderResult.FromSuccess(results.Values.ToImmutableArray()); | ||||
return TypeReaderResult.FromError(CommandError.ObjectNotFound, "User not found."); | return TypeReaderResult.FromError(CommandError.ObjectNotFound, "User not found."); | ||||
} | } | ||||
private void AddResult(Dictionary<ulong, TypeReaderValue> results, T user, float score) | |||||
{ | |||||
if (user != null && !results.ContainsKey(user.Id)) | |||||
results.Add(user.Id, new TypeReaderValue(user, score)); | |||||
} | |||||
} | } | ||||
} | } |