Browse Source

create typemap and default typereaders

pull/2169/head
Cenk Ergen 3 years ago
parent
commit
88b258ba99
15 changed files with 526 additions and 116 deletions
  1. +3
    -3
      src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs
  2. +1
    -5
      src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs
  3. +9
    -0
      src/Discord.Net.Interactions/Entities/ITypeHandler.cs
  4. +7
    -5
      src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs
  5. +47
    -102
      src/Discord.Net.Interactions/InteractionService.cs
  6. +91
    -0
      src/Discord.Net.Interactions/Map/TypeMap.cs
  7. +1
    -1
      src/Discord.Net.Interactions/TypeConverters/TypeConverter.cs
  8. +45
    -0
      src/Discord.Net.Interactions/TypeReaders/ChannelTypeReader.cs
  9. +19
    -0
      src/Discord.Net.Interactions/TypeReaders/EnumTypeReader.cs
  10. +27
    -0
      src/Discord.Net.Interactions/TypeReaders/MessageTypeReader.cs
  11. +35
    -0
      src/Discord.Net.Interactions/TypeReaders/NullableTypeReader.cs
  12. +48
    -0
      src/Discord.Net.Interactions/TypeReaders/RoleTypeReader.cs
  13. +55
    -0
      src/Discord.Net.Interactions/TypeReaders/TimeSpanTypeReader.cs
  14. +43
    -0
      src/Discord.Net.Interactions/TypeReaders/TypeReader.cs
  15. +95
    -0
      src/Discord.Net.Interactions/TypeReaders/UserTypeReader.cs

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

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

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


+ 1
- 5
src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs View File

@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.Interactions.Builders
{
@@ -29,7 +25,7 @@ namespace Discord.Interactions.Builders
public ComponentCommandParameterBuilder SetParameterType(Type type, IServiceProvider services = null)
{
base.SetParameterType(type);
TypeReader = Command.Module.InteractionService.GetTypeConverter(ParameterType, services);
TypeReader = Command.Module.InteractionService.GetTypeReader(ParameterType, services);
return this;
}



+ 9
- 0
src/Discord.Net.Interactions/Entities/ITypeHandler.cs View File

@@ -0,0 +1,9 @@
using System;

namespace Discord.Interactions
{
internal interface ITypeHandler
{
public bool CanConvertTo(Type type);
}
}

+ 7
- 5
src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs View File

@@ -11,10 +11,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;
@@ -73,14 +73,16 @@ namespace Discord.Interactions

if (componentValues is not null)
{
if (Parameters.Last().ParameterType == typeof(string[]))
args[args.Length - 1] = componentValues.ToArray();
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");
}

for (var i = 0; i < strCount; i++)
args[i] = values.ElementAt(i);
args[i] = await Parameters.ElementAt(i).TypeReader.ReadAsync(context, values.ElementAt(i), services).ConfigureAwait(false);

return await RunAsync(context, args, services).ConfigureAwait(false);
}


+ 47
- 102
src/Discord.Net.Interactions/InteractionService.cs View File

@@ -66,8 +66,8 @@ 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> _typeConverterMap;
private readonly TypeMap<TypeReader> _typeReaderMap;
private readonly ConcurrentDictionary<Type, IAutocompleteHandler> _autocompleteHandlers = new();
private readonly ConcurrentDictionary<Type, ModalInfo> _modalInfos = new();
private readonly SemaphoreSlim _lock;
@@ -179,7 +179,10 @@ namespace Discord.Interactions
_autoServiceScopes = config.AutoServiceScopes;
_restResponseCallback = config.RestResponseCallback;

_genericTypeConverters = new ConcurrentDictionary<Type, Type>
_typeConverterMap = new TypeMap<TypeConverter>(this, new Dictionary<Type, TypeConverter>
{
[typeof(TimeSpan)] = new TimeSpanConverter()
}, new Dictionary<Type, Type>
{
[typeof(IChannel)] = typeof(DefaultChannelConverter<>),
[typeof(IRole)] = typeof(DefaultRoleConverter<>),
@@ -189,12 +192,9 @@ namespace Discord.Interactions
[typeof(IConvertible)] = typeof(DefaultValueConverter<>),
[typeof(Enum)] = typeof(EnumConverter<>),
[typeof(Nullable<>)] = typeof(NullableConverter<>),
};
});

_typeConverters = new ConcurrentDictionary<Type, TypeConverter>
{
[typeof(TimeSpan)] = new TimeSpanConverter()
};
_typeReaderMap = new TypeMap<TypeReader>(this);
}

/// <summary>
@@ -769,61 +769,24 @@ namespace Discord.Interactions
return await commandResult.Command.ExecuteAsync(context, services).ConfigureAwait(false);
}

private async Task<IResult> ExecuteModalCommandAsync(IInteractionContext context, string input, IServiceProvider services)
{
var result = _modalCommandMap.GetCommand(input);

if (!result.IsSuccess)
{
await _cmdLogger.DebugAsync($"Unknown custom interaction id, skipping execution ({input.ToUpper()})");

await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false);
return result;
}
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}"/>.
@@ -831,58 +794,55 @@ namespace Discord.Interactions
/// <typeparam name="T">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeConverter{T}"/>.</typeparam>
/// <param name="converterType">Type of the <see cref="TypeConverter{T}"/>.</param>

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

/// <summary>
/// Add a generic type <see cref="TypeConverter{T}"/>.
/// </summary>
/// <param name="targetType">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeConverter{T}"/>.</param>
/// <param name="converterType">Type of the <see cref="TypeConverter{T}"/>.</param>
public void AddGenericTypeConverter (Type targetType, Type converterType)
{
if (!converterType.IsGenericTypeDefinition)
throw new ArgumentException($"{converterType.FullName} is not generic.");

var genericArguments = converterType.GetGenericArguments();
public void AddGenericTypeConverter(Type targetType, Type converterType) =>
_typeConverterMap.AddGeneric(targetType, converterType);

if (genericArguments.Count() > 1)
throw new InvalidOperationException($"Valid generic {converterType.FullName}s cannot have more than 1 generic type parameter");
internal TypeReader GetTypeReader(Type type, IServiceProvider services = null)
=> _typeReaderMap.Get(type, services);

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}");
/// <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) =>
_typeReaderMap.AddConcrete<T>(reader);

/// <summary>
/// Serialize an object using a <see cref="TypeReader"/> into a <see cref="string"/> to be placed in a Component CustomId.
/// Add a concrete type <see cref="TypeReader"/>.
/// </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 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 = null) =>
_typeReaderMap.Get(typeof(T), services).SerializeAsync(obj);
/// <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 converter) =>
_typeReaderMap.AddConcrete(type, converter);

/// <summary>
/// Loads and caches an <see cref="ModalInfo"/> for the provided <see cref="IModal"/>.
/// Add a generic type <see cref="TypeReader{T}"/>.
/// </summary>
/// <typeparam name="T">Type of <see cref="IModal"/> to be loaded.</typeparam>
/// <returns>
/// The built <see cref="ModalInfo"/> instance.
/// </returns>
/// <exception cref="InvalidOperationException"></exception>
public ModalInfo AddModalInfo<T>() where T : class, IModal
{
var type = typeof(T);
/// <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>

if (_modalInfos.ContainsKey(type))
throw new InvalidOperationException($"Modal type {type.FullName} already exists.");
public void AddGenericTypeReader<T>(Type readerType) =>
_typeReaderMap.AddGeneric<T>(readerType);

return ModalUtils.GetOrAdd(type);
}
/// <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) =>
_typeConverterMap.AddGeneric(targetType, readerType);

public string SerializeWithTypeReader<T>(object obj, IServiceProvider services = null) =>
_typeReaderMap.Get(typeof(T), services)?.Serialize(obj);

internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null)
{
@@ -1043,21 +1003,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)


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

@@ -0,0 +1,91 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Discord.Interactions
{
internal class TypeMap<T> where T : class, ITypeHandler
{
private readonly ConcurrentDictionary<Type, T> _concretes;
private readonly ConcurrentDictionary<Type, Type> _generics;
private readonly InteractionService _interactionService;

public TypeMap(InteractionService interactionService, IDictionary<Type, T> 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 T Get(Type type, IServiceProvider services = null)
{
if (_concretes.TryGetValue(type, out var specific))
return specific;

else 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<T>.CreateObject(converterType.MakeGenericType(type).GetTypeInfo(), _interactionService, services);
_concretes[type] = converter;
return converter;
}

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

throw new ArgumentException($"No type {nameof(T)} is defined for this {type.FullName}", "type");
}

public void AddConcrete<TTarget>(T converter) =>
AddConcrete(typeof(TTarget), converter);

public void AddConcrete(Type type, T 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.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}");

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

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

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


+ 45
- 0
src/Discord.Net.Interactions/TypeReaders/ChannelTypeReader.cs View File

@@ -0,0 +1,45 @@
using System;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// A <see cref="TypeReader"/> for parsing objects implementing <see cref="IChannel"/>.
/// </summary>
/// <remarks>
/// This <see cref="TypeReader"/> is shipped with Discord.Net and is used by default to parse any
/// <see cref="IChannel"/> implemented object within a command. The TypeReader will attempt to first parse the
/// input by mention, then the snowflake identifier, then by name; the highest candidate will be chosen as the
/// final output; otherwise, an erroneous <see cref="TypeReaderResult"/> is returned.
/// </remarks>
/// <typeparam name="T">The type to be checked; must implement <see cref="IChannel"/>.</typeparam>
public class ChannelTypeReader<T> : TypeReader<T>
where T : class, IChannel
{
/// <inheritdoc />
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, object input, IServiceProvider services)
{
if (context.Guild is null)
{
var str = input as string;

if (ulong.TryParse(str, out var channelId))
return TypeConverterResult.FromSuccess(await context.Guild.GetChannelAsync(channelId).ConfigureAwait(false));

if (MentionUtils.TryParseChannel(str, out channelId))
return TypeConverterResult.FromSuccess(await context.Guild.GetChannelAsync(channelId).ConfigureAwait(false));

var channels = await context.Guild.GetChannelsAsync().ConfigureAwait(false);
var nameMatch = channels.FirstOrDefault(x => string.Equals(x.Name, str, StringComparison.OrdinalIgnoreCase));

if (nameMatch is not null)
return TypeConverterResult.FromSuccess(nameMatch);
}

return TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, "Channel not found.");
}

public override string Serialize(object value) => (value as IChannel)?.Id.ToString();
}
}

+ 19
- 0
src/Discord.Net.Interactions/TypeReaders/EnumTypeReader.cs View File

@@ -0,0 +1,19 @@
using System;
using System.Threading.Tasks;

namespace Discord.Interactions
{
internal class EnumTypeReader<T> : TypeReader<T> where T : struct, Enum
{
/// <inheritdoc />
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, string input, IServiceProvider services)
{
if (Enum.TryParse<T>(input, out var result))
return Task.FromResult(TypeConverterResult.FromSuccess(result));
else
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Value {input} cannot be converted to {nameof(T)}"));
}

public override string Serialize(object value) => value.ToString();
}
}

+ 27
- 0
src/Discord.Net.Interactions/TypeReaders/MessageTypeReader.cs View File

@@ -0,0 +1,27 @@
using System;
using System.Globalization;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// A <see cref="TypeReader"/> for parsing objects implementing <see cref="IMessage"/>.
/// </summary>
/// <typeparam name="T">The type to be checked; must implement <see cref="IMessage"/>.</typeparam>
public class MessageTypeReader<T> : TypeReader
where T : class, IMessage
{
/// <inheritdoc />
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{
//By Id (1.0)
if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out ulong id))
{
if (await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg)
return TypeReaderResult.FromSuccess(msg);
}

return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Message not found.");
}
}
}

+ 35
- 0
src/Discord.Net.Interactions/TypeReaders/NullableTypeReader.cs View File

@@ -0,0 +1,35 @@
using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

namespace Discord.Commands
{
internal static class NullableTypeReader
{
public static TypeReader Create(Type type, TypeReader reader)
{
var constructor = typeof(NullableTypeReader<>).MakeGenericType(type).GetTypeInfo().DeclaredConstructors.First();
return (TypeReader)constructor.Invoke(new object[] { reader });
}
}

internal class NullableTypeReader<T> : TypeReader
where T : struct
{
private readonly TypeReader _baseTypeReader;

public NullableTypeReader(TypeReader baseTypeReader)
{
_baseTypeReader = baseTypeReader;
}

/// <inheritdoc />
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{
if (string.Equals(input, "null", StringComparison.OrdinalIgnoreCase) || string.Equals(input, "nothing", StringComparison.OrdinalIgnoreCase))
return TypeReaderResult.FromSuccess(new T?());
return await _baseTypeReader.ReadAsync(context, input, services).ConfigureAwait(false);
}
}
}

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

@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.Commands
{
/// <summary>
/// A <see cref="TypeReader"/> for parsing objects implementing <see cref="IRole"/>.
/// </summary>
/// <typeparam name="T">The type to be checked; must implement <see cref="IRole"/>.</typeparam>
public class RoleTypeReader<T> : TypeReader
where T : class, IRole
{
/// <inheritdoc />
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{
if (context.Guild != null)
{
var results = new Dictionary<ulong, TypeReaderValue>();
var roles = context.Guild.Roles;

//By Mention (1.0)
if (MentionUtils.TryParseRole(input, out var id))
AddResult(results, context.Guild.GetRole(id) as T, 1.00f);

//By Id (0.9)
if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id))
AddResult(results, context.Guild.GetRole(id) as T, 0.90f);

//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 (results.Count > 0)
return Task.FromResult(TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection()));
}
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));
}
}
}

+ 55
- 0
src/Discord.Net.Interactions/TypeReaders/TimeSpanTypeReader.cs View File

@@ -0,0 +1,55 @@
using System;
using System.Globalization;
using System.Threading.Tasks;

namespace Discord.Commands
{
internal class TimeSpanTypeReader : TypeReader
{
/// <summary>
/// TimeSpan try parse formats.
/// </summary>
private static readonly string[] Formats =
{
"%d'd'%h'h'%m'm'%s's'", // 4d3h2m1s
"%d'd'%h'h'%m'm'", // 4d3h2m
"%d'd'%h'h'%s's'", // 4d3h 1s
"%d'd'%h'h'", // 4d3h
"%d'd'%m'm'%s's'", // 4d 2m1s
"%d'd'%m'm'", // 4d 2m
"%d'd'%s's'", // 4d 1s
"%d'd'", // 4d
"%h'h'%m'm'%s's'", // 3h2m1s
"%h'h'%m'm'", // 3h2m
"%h'h'%s's'", // 3h 1s
"%h'h'", // 3h
"%m'm'%s's'", // 2m1s
"%m'm'", // 2m
"%s's'", // 1s
};

/// <inheritdoc />
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{
if (string.IsNullOrEmpty(input))
throw new ArgumentException(message: $"{nameof(input)} must not be null or empty.", paramName: nameof(input));

var isNegative = input[0] == '-'; // Char for CultureInfo.InvariantCulture.NumberFormat.NegativeSign
if (isNegative)
{
input = input.Substring(1);
}

if (TimeSpan.TryParseExact(input.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out var timeSpan))
{
return isNegative
? Task.FromResult(TypeReaderResult.FromSuccess(-timeSpan))
: Task.FromResult(TypeReaderResult.FromSuccess(timeSpan));
}
else
{
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan"));
}
}
}
}

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

@@ -0,0 +1,43 @@
using System;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Base class for creating <see cref="TypeReader"/>s. <see cref="InteractionService"/> uses <see cref="TypeReader"/>s to parse string values into entities.
/// </summary>
/// <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.
/// </remarks>
public abstract class TypeReader : ITypeHandler
{
/// <summary>
/// Will be used to search for alternative TypeReaders whenever the Command Service encounters an unknown parameter type.
/// </summary>
/// <param name="type"></param>
/// <returns></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="input">Raw string input value.</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, object input, IServiceProvider services);

/// <summary>
/// Will be used to manipulate the outgoing command option, before the command gets registered to Discord.
/// </summary>
public virtual string Serialize(object value) => null;
}

/// <inheritdoc/>
public abstract class TypeReader<T> : TypeReader
{
/// <inheritdoc/>
public sealed override bool CanConvertTo(Type type) =>
typeof(T).IsAssignableFrom(type);
}
}

+ 95
- 0
src/Discord.Net.Interactions/TypeReaders/UserTypeReader.cs View File

@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.Commands
{
/// <summary>
/// A <see cref="TypeReader"/> for parsing objects implementing <see cref="IUser"/>.
/// </summary>
/// <typeparam name="T">The type to be checked; must implement <see cref="IUser"/>.</typeparam>
public class UserTypeReader<T> : TypeReader
where T : class, IUser
{
/// <inheritdoc />
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{
var results = new Dictionary<ulong, TypeReaderValue>();
IAsyncEnumerable<IUser> channelUsers = context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten(); // it's better
IReadOnlyCollection<IGuildUser> guildUsers = ImmutableArray.Create<IGuildUser>();

if (context.Guild != null)
guildUsers = await context.Guild.GetUsersAsync(CacheMode.CacheOnly).ConfigureAwait(false);

//By Mention (1.0)
if (MentionUtils.TryParseUser(input, out var id))
{
if (context.Guild != null)
AddResult(results, await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f);
else
AddResult(results, await context.Channel.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f);
}

//By Id (0.9)
if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id))
{
if (context.Guild != null)
AddResult(results, await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f);
else
AddResult(results, await context.Channel.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f);
}

//By Username + Discriminator (0.7-0.85)
int index = input.LastIndexOf('#');
if (index >= 0)
{
string username = input.Substring(0, index);
if (ushort.TryParse(input.Substring(index + 1), out ushort discriminator))
{
var channelUser = await channelUsers.FirstOrDefaultAsync(x => x.DiscriminatorValue == discriminator &&
string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)).ConfigureAwait(false);
AddResult(results, channelUser as T, channelUser?.Username == username ? 0.85f : 0.75f);

var guildUser = guildUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator &&
string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase));
AddResult(results, guildUser as T, guildUser?.Username == username ? 0.80f : 0.70f);
}
}

//By Username (0.5-0.6)
{
await channelUsers
.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))
.ForEachAsync(channelUser => AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f))
.ConfigureAwait(false);
foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)))
AddResult(results, guildUser as T, guildUser.Username == input ? 0.60f : 0.50f);
}

//By Nickname (0.5-0.6)
{
await channelUsers
.Where(x => string.Equals(input, (x as IGuildUser)?.Nickname, StringComparison.OrdinalIgnoreCase))
.ForEachAsync(channelUser => AddResult(results, channelUser as T, (channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f))
.ConfigureAwait(false);

foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Nickname, StringComparison.OrdinalIgnoreCase)))
AddResult(results, guildUser as T, guildUser.Nickname == input ? 0.60f : 0.50f);
}

if (results.Count > 0)
return TypeReaderResult.FromSuccess(results.Values.ToImmutableArray());
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));
}
}
}

Loading…
Cancel
Save