Browse Source

[Feature] RespondWithModal() which accepts an IModal instance as template (#2564)

* introduce overload for responding to an interaction with an instatiated IModal obj

* add inline docs to ModalInfo.PropertyInfo

* Apply suggestions from code review

Co-authored-by: Casmir <68127614+csmir@users.noreply.github.com>

---------

Co-authored-by: Casmir <68127614+csmir@users.noreply.github.com>
pull/2585/head
Cenk Ergen GitHub 2 years ago
parent
commit
e7bda0f8a5
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 79 additions and 1 deletions
  1. +6
    -0
      src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs
  2. +4
    -0
      src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs
  3. +2
    -0
      src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs
  4. +1
    -0
      src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
  5. +38
    -0
      src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs
  6. +13
    -0
      src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs
  7. +15
    -1
      src/Discord.Net.Interactions/Utilities/ReflectionUtils.cs

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

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reflection;

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

/// <summary>
/// Get the <see cref="PropertyInfo"/> of this component's property.
/// </summary>
PropertyInfo PropertyInfo { get; }

/// <summary>
/// Get the <see cref="ComponentTypeConverter"/> assigned to this input.
/// </summary>


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

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reflection;

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

/// <inheritdoc/>
public PropertyInfo PropertyInfo { get; internal set; }

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



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

@@ -37,6 +37,8 @@ namespace Discord.Interactions.Builders
if (!typeof(IModal).IsAssignableFrom(type))
throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type));

Type = type;

_interactionService = interactionService;
_components = new();
}


+ 1
- 0
src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs View File

@@ -596,6 +596,7 @@ namespace Discord.Interactions.Builders
builder.Label = propertyInfo.Name;
builder.DefaultValue = defaultValue;
builder.WithType(propertyInfo.PropertyType);
builder.PropertyInfo = propertyInfo;

foreach(var attribute in attributes)
{


+ 38
- 0
src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs View File

@@ -44,6 +44,44 @@ namespace Discord.Interactions
await SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal);
}

/// <summary>
/// Respond to an interaction with an <see cref="IModal"/> and fills the value fields of the modal using the property values of the provided
/// instance.
/// </summary>
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam>
/// <param name="interaction">The interaction to respond to.</param>
/// <param name="modal">The <see cref="IModal"/> instance to get field values from.</param>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <param name="modifyModal">Delegate that can be used to modify the modal.</param>
/// <returns></returns>
public static async Task RespondWithModalAsync<T>(this IDiscordInteraction interaction, string customId, T modal, RequestOptions options = null,
Action<ModalBuilder> modifyModal = null)
where T : class, IModal
{
if (!ModalUtils.TryGet<T>(out var modalInfo))
throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}");

var builder = new ModalBuilder(modal.Title, customId);

foreach (var input in modalInfo.Components)
switch (input)
{
case TextInputComponentInfo textComponent:
{
builder.AddTextInput(textComponent.Label, textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null,
textComponent.MaxLength, textComponent.IsRequired, textComponent.Getter(modal) as string);
}
break;
default:
throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class");
}

if (modifyModal is not null)
modifyModal(builder);

await interaction.RespondWithModalAsync(builder.Build(), options).ConfigureAwait(false);
}

private static async Task SendModalResponseAsync(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
{
var builder = new ModalBuilder(modalInfo.Title, customId);


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

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Reflection;

namespace Discord.Interactions
{
@@ -9,6 +10,10 @@ namespace Discord.Interactions
/// </summary>
public abstract class InputComponentInfo
{
private Lazy<Func<object, object>> _getter;
internal Func<object, object> Getter => _getter.Value;


/// <summary>
/// Gets the parent modal of this component.
/// </summary>
@@ -39,6 +44,11 @@ namespace Discord.Interactions
/// </summary>
public Type Type { get; }

/// <summary>
/// Gets the property linked to this component.
/// </summary>
public PropertyInfo PropertyInfo { get; }

/// <summary>
/// Gets the <see cref="ComponentTypeConverter"/> assigned to this component.
/// </summary>
@@ -62,9 +72,12 @@ namespace Discord.Interactions
IsRequired = builder.IsRequired;
ComponentType = builder.ComponentType;
Type = builder.Type;
PropertyInfo = builder.PropertyInfo;
TypeConverter = builder.TypeConverter;
DefaultValue = builder.DefaultValue;
Attributes = builder.Attributes.ToImmutableArray();

_getter = new(() => ReflectionUtils<object>.CreateLambdaPropertyGetter(Modal.Type, PropertyInfo));
}
}
}

+ 15
- 1
src/Discord.Net.Interactions/Utilities/ReflectionUtils.cs View File

@@ -10,7 +10,6 @@ namespace Discord.Interactions
internal static class ReflectionUtils<T>
{
private static readonly TypeInfo ObjectTypeInfo = typeof(object).GetTypeInfo();

internal static T CreateObject (TypeInfo typeInfo, InteractionService commandService, IServiceProvider services = null) =>
CreateBuilder(typeInfo, commandService)(services);

@@ -166,6 +165,21 @@ namespace Discord.Interactions
return Expression.Lambda<Action<T, object>>(assign, instanceParam, valueParam).Compile();
}

internal static Func<T, object> CreateLambdaPropertyGetter(PropertyInfo propertyInfo)
{
var instanceParam = Expression.Parameter(typeof(T), "instance");
var prop = Expression.Property(instanceParam, propertyInfo);
return Expression.Lambda<Func<T, object>>(prop, instanceParam).Compile();
}

internal static Func<T, object> CreateLambdaPropertyGetter(Type type, PropertyInfo propertyInfo)
{
var instanceParam = Expression.Parameter(typeof(T), "instance");
var instanceAccess = Expression.Convert(instanceParam, type);
var prop = Expression.Property(instanceAccess, propertyInfo);
return Expression.Lambda<Func<T, object>>(prop, instanceParam).Compile();
}

internal static Func<object[], object[], T> CreateLambdaMemberInit(TypeInfo typeInfo, ConstructorInfo constructor, Predicate<PropertyInfo> propertySelect = null)
{
propertySelect ??= x => true;


Loading…
Cancel
Save