| @@ -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,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; | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| using System; | |||
| namespace Discord.Interactions | |||
| { | |||
| internal interface ITypeHandler | |||
| { | |||
| public bool CanConvertTo(Type type); | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| @@ -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) | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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. | |||
| @@ -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(); | |||
| } | |||
| } | |||
| @@ -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(); | |||
| } | |||
| } | |||
| @@ -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."); | |||
| } | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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)); | |||
| } | |||
| } | |||
| } | |||
| @@ -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")); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| @@ -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)); | |||
| } | |||
| } | |||
| } | |||