Feature: Implement modals (#2087)

* Implement Modals (#428)

* Socket Modal Support

* fix shareded client support

* Properly use `HasResponded` instead of `_hasResponded`

* `ModalBuilder` and `TextInputBuilder` validation.

* make orginisation more consistant.

* Rest Modals.

* Docs + add missing methods

* fix message signatures and missing abstract members

* modal changes

* um?????

* update modal docs

* update docs - again for some reason

* cleanup

* fix message signatures

* add modal commands support to interaction service

* Fix _hasResponded

* update to new unsupported standard.

* Sending modals with Interaction service.

* fix spelling in ComponentBuilder

* sending IModals when responding to interactions

* interaction service modals

* fix rest modals

* spelling and minor improvements.

* improve interaction service modal proformance

* use precompiled lambda for interaction service modals

* respect user compiled lambda choice

* changes to modals in the interaction service (more)

* support compiled lambdas in modal properties.

* modal interactions tweaks

* fix inline doc

* more modal docs

* configure responce to faild modal component

* init

* solve runtime errors

* solve build errors

* add default value parsing

* make modal info caching static

* make ModalUtils static

* add inline docs

* fix build errors

* code cleanup

* Introduce Required and Label properties as seperate attributes.

* replace internal dictionary of ModalInfo with a list

* change input building logic of modals

* update RespondWithModalAsync method

* add initial value parameter back to ModalTextInput and fix optional modal field

* add missing inline docs

* dispose the reference modal instance after building

* code cleanup on modalcommandbuilder

* Update docs/guides/int_basics/message-components/text-input.md

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update docs/guides/int_basics/message-components/text-input.md

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update docs/guides/int_basics/modals/intro.md

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update docs/guides/int_basics/modals/intro.md

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update docs/guides/int_basics/modals/intro.md

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update docs/guides/int_basics/modals/intro.md

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update docs/guides/int_basics/modals/intro.md

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update docs/guides/int_basics/modals/intro.md

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update docs/guides/int_basics/modals/intro.md

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update docs/guides/int_framework/intro.md

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update docs/guides/int_framework/intro.md

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update docs/guides/int_framework/samples/intro/modal.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputComponent.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update src/Discord.Net.Core/Entities/Interactions/Modals/IModalInteraction.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update src/Discord.Net.Interactions/Attributes/Modals/RequiredInputAttribute.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update src/Discord.Net.Interactions/InteractionServiceConfig.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* update interaction service modal docs

* implements ExitOnMissingmModalField config option and adds Type field to modal info

* Add WithValue to text input builders

* Fix rare NRE on component enumeration

* Fix RequestOptions being required in some methods

* Use 'OfType' instead of 'Where'

* Remove android unsported warning

* Change publicity of properties in IInputComponeontBuilder.cs

Co-authored-by: Cenk Ergen <57065323+Cenngo@users.noreply.github.com>
Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Remove complex parameter ref

Co-authored-by: CottageDwellingCat <80918250+CottageDwellingCat@users.noreply.github.com>
Co-authored-by: Cenk Ergen <57065323+Cenngo@users.noreply.github.com>
Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>
This commit is contained in:
Quin Lynch
2022-02-09 00:17:56 -04:00
committed by GitHub
parent 33efd8981d
commit c8f175e11a
80 changed files with 3502 additions and 25 deletions

View File

@@ -0,0 +1,44 @@
using System;
namespace Discord.Interactions.Builders
{
/// <summary>
/// Represents a builder for creating a <see cref="ModalCommandInfo"/>.
/// </summary>
public class ModalCommandBuilder : CommandBuilder<ModalCommandInfo, ModalCommandBuilder, ModalCommandParameterBuilder>
{
protected override ModalCommandBuilder Instance => this;
/// <summary>
/// Initializes a new <see cref="ModalCommandBuilder"/>.
/// </summary>
/// <param name="module">Parent module of this modal.</param>
public ModalCommandBuilder(ModuleBuilder module) : base(module) { }
/// <summary>
/// Initializes a new <see cref="ModalCommandBuilder"/>.
/// </summary>
/// <param name="module">Parent module of this modal.</param>
/// <param name="name">Name of this modal.</param>
/// <param name="callback">Execution callback of this modal.</param>
public ModalCommandBuilder(ModuleBuilder module, string name, ExecuteCallback callback) : base(module, name, callback) { }
/// <summary>
/// Adds a modal parameter to the parameters collection.
/// </summary>
/// <param name="configure"><see cref="ModalCommandParameterBuilder"/> factory.</param>
/// <returns>
/// The builder instance.
/// </returns>
public override ModalCommandBuilder AddParameter(Action<ModalCommandParameterBuilder> configure)
{
var parameter = new ModalCommandParameterBuilder(this);
configure(parameter);
AddParameters(parameter);
return this;
}
internal override ModalCommandInfo Build(ModuleInfo module, InteractionService commandService) =>
new(this, module, commandService);
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
namespace Discord.Interactions.Builders
{
/// <summary>
/// Represent a builder for creating <see cref="InputComponentInfo"/>.
/// </summary>
public interface IInputComponentBuilder
{
/// <summary>
/// Gets the parent modal of this input component.
/// </summary>
ModalBuilder Modal { get; }
/// <summary>
/// Gets the custom id of this input component.
/// </summary>
string CustomId { get; }
/// <summary>
/// Gets the label of this input component.
/// </summary>
string Label { get; }
/// <summary>
/// Gets whether this input component is required.
/// </summary>
bool IsRequired { get; }
/// <summary>
/// Gets the component type of this input component.
/// </summary>
ComponentType ComponentType { get; }
/// <summary>
/// Get the reference type of this input component.
/// </summary>
Type Type { get; }
/// <summary>
/// Gets the default value of this input component.
/// </summary>
object DefaultValue { get; }
/// <summary>
/// Gets a collection of the attributes of this component.
/// </summary>
IReadOnlyCollection<Attribute> Attributes { get; }
/// <summary>
/// Sets <see cref="CustomId"/>.
/// </summary>
/// <param name="customId">New value of the <see cref="CustomId"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
IInputComponentBuilder WithCustomId(string customId);
/// <summary>
/// Sets <see cref="Label"/>.
/// </summary>
/// <param name="label">New value of the <see cref="Label"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
IInputComponentBuilder WithLabel(string label);
/// <summary>
/// Sets <see cref="IsRequired"/>.
/// </summary>
/// <param name="isRequired">New value of the <see cref="IsRequired"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
IInputComponentBuilder SetIsRequired(bool isRequired);
/// <summary>
/// Sets <see cref="Type"/>.
/// </summary>
/// <param name="type">New value of the <see cref="Type"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
IInputComponentBuilder WithType(Type type);
/// <summary>
/// Sets <see cref="DefaultValue"/>.
/// </summary>
/// <param name="value">New value of the <see cref="DefaultValue"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
IInputComponentBuilder SetDefaultValue(object value);
/// <summary>
/// Adds attributes to <see cref="Attributes"/>.
/// </summary>
/// <param name="attributes">New attributes to be added to <see cref="Attributes"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
IInputComponentBuilder WithAttributes(params Attribute[] attributes);
}
}

View File

@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
namespace Discord.Interactions.Builders
{
/// <summary>
/// Represents the base builder class for creating <see cref="InputComponentInfo"/>.
/// </summary>
/// <typeparam name="TInfo">The <see cref="InputComponentInfo"/> this builder yields when built.</typeparam>
/// <typeparam name="TBuilder">Inherited <see cref="InputComponentBuilder{TInfo, TBuilder}"/> type.</typeparam>
public abstract class InputComponentBuilder<TInfo, TBuilder> : IInputComponentBuilder
where TInfo : InputComponentInfo
where TBuilder : InputComponentBuilder<TInfo, TBuilder>
{
private readonly List<Attribute> _attributes;
protected abstract TBuilder Instance { get; }
/// <inheritdoc/>
public ModalBuilder Modal { get; }
/// <inheritdoc/>
public string CustomId { get; set; }
/// <inheritdoc/>
public string Label { get; set; }
/// <inheritdoc/>
public bool IsRequired { get; set; } = true;
/// <inheritdoc/>
public ComponentType ComponentType { get; internal set; }
/// <inheritdoc/>
public Type Type { get; private set; }
/// <inheritdoc/>
public object DefaultValue { get; set; }
/// <inheritdoc/>
public IReadOnlyCollection<Attribute> Attributes => _attributes;
/// <summary>
/// Creates an instance of <see cref="InputComponentBuilder{TInfo, TBuilder}"/>
/// </summary>
/// <param name="modal">Parent modal of this input component.</param>
public InputComponentBuilder(ModalBuilder modal)
{
Modal = modal;
_attributes = new();
}
/// <summary>
/// Sets <see cref="CustomId"/>.
/// </summary>
/// <param name="customId">New value of the <see cref="CustomId"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TBuilder WithCustomId(string customId)
{
CustomId = customId;
return Instance;
}
/// <summary>
/// Sets <see cref="Label"/>.
/// </summary>
/// <param name="label">New value of the <see cref="Label"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TBuilder WithLabel(string label)
{
Label = label;
return Instance;
}
/// <summary>
/// Sets <see cref="IsRequired"/>.
/// </summary>
/// <param name="isRequired">New value of the <see cref="IsRequired"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TBuilder SetIsRequired(bool isRequired)
{
IsRequired = isRequired;
return Instance;
}
/// <summary>
/// Sets <see cref="ComponentType"/>.
/// </summary>
/// <param name="componentType">New value of the <see cref="ComponentType"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TBuilder WithComponentType(ComponentType componentType)
{
ComponentType = componentType;
return Instance;
}
/// <summary>
/// Sets <see cref="Type"/>.
/// </summary>
/// <param name="type">New value of the <see cref="Type"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TBuilder WithType(Type type)
{
Type = type;
return Instance;
}
/// <summary>
/// Sets <see cref="DefaultValue"/>.
/// </summary>
/// <param name="value">New value of the <see cref="DefaultValue"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TBuilder SetDefaultValue(object value)
{
DefaultValue = value;
return Instance;
}
/// <summary>
/// Adds attributes to <see cref="Attributes"/>.
/// </summary>
/// <param name="attributes">New attributes to be added to <see cref="Attributes"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TBuilder WithAttributes(params Attribute[] attributes)
{
_attributes.AddRange(attributes);
return Instance;
}
internal abstract TInfo Build(ModalInfo modal);
//IInputComponentBuilder
/// <inheritdoc/>
IInputComponentBuilder IInputComponentBuilder.WithCustomId(string customId) => WithCustomId(customId);
/// <inheritdoc/>
IInputComponentBuilder IInputComponentBuilder.WithLabel(string label) => WithCustomId(label);
/// <inheritdoc/>
IInputComponentBuilder IInputComponentBuilder.WithType(Type type) => WithType(type);
/// <inheritdoc/>
IInputComponentBuilder IInputComponentBuilder.SetDefaultValue(object value) => SetDefaultValue(value);
/// <inheritdoc/>
IInputComponentBuilder IInputComponentBuilder.WithAttributes(params Attribute[] attributes) => WithAttributes(attributes);
/// <inheritdoc/>
IInputComponentBuilder IInputComponentBuilder.SetIsRequired(bool isRequired) => SetIsRequired(isRequired);
}
}

View File

@@ -0,0 +1,109 @@
namespace Discord.Interactions.Builders
{
/// <summary>
/// Represents a builder for creating <see cref="TextInputComponentInfo"/>.
/// </summary>
public class TextInputComponentBuilder : InputComponentBuilder<TextInputComponentInfo, TextInputComponentBuilder>
{
protected override TextInputComponentBuilder Instance => this;
/// <summary>
/// Gets and sets the style of the text input.
/// </summary>
public TextInputStyle Style { get; set; }
/// <summary>
/// Gets and sets the placeholder of the text input.
/// </summary>
public string Placeholder { get; set; }
/// <summary>
/// Gets and sets the minimum length of the text input.
/// </summary>
public int MinLength { get; set; }
/// <summary>
/// Gets and sets the maximum length of the text input.
/// </summary>
public int MaxLength { get; set; }
/// <summary>
/// Gets and sets the initial value to be displayed by this input.
/// </summary>
public string InitialValue { get; set; }
/// <summary>
/// Initializes a new <see cref="TextInputComponentBuilder"/>.
/// </summary>
/// <param name="modal">Parent modal of this component.</param>
public TextInputComponentBuilder(ModalBuilder modal) : base(modal) { }
/// <summary>
/// Sets <see cref="Style"/>.
/// </summary>
/// <param name="style">New value of the <see cref="SetValue(string)"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TextInputComponentBuilder WithStyle(TextInputStyle style)
{
Style = style;
return this;
}
/// <summary>
/// Sets <see cref="Placeholder"/>.
/// </summary>
/// <param name="placeholder">New value of the <see cref="Placeholder"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TextInputComponentBuilder WithPlaceholder(string placeholder)
{
Placeholder = placeholder;
return this;
}
/// <summary>
/// Sets <see cref="MinLength"/>.
/// </summary>
/// <param name="minLenght">New value of the <see cref="MinLength"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TextInputComponentBuilder WithMinLenght(int minLenght)
{
MinLength = minLenght;
return this;
}
/// <summary>
/// Sets <see cref="MaxLength"/>.
/// </summary>
/// <param name="maxLenght">New value of the <see cref="MaxLength"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TextInputComponentBuilder WithMaxLenght(int maxLenght)
{
MaxLength = maxLenght;
return this;
}
/// <summary>
/// Sets <see cref="InitialValue"/>.
/// </summary>
/// <param name="value">New value of the <see cref="InitialValue"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TextInputComponentBuilder WithInitialValue(string value)
{
InitialValue = value;
return this;
}
internal override TextInputComponentInfo Build(ModalInfo modal) =>
new(this, modal);
}
}

View File

@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Discord.Interactions.Builders
{
/// <summary>
/// Represents a builder for creating <see cref="ModalInfo"/>.
/// </summary>
public class ModalBuilder
{
internal readonly List<IInputComponentBuilder> _components;
/// <summary>
/// Gets the initialization delegate for this modal.
/// </summary>
public ModalInitializer ModalInitializer { get; internal set; }
/// <summary>
/// Gets the title of this modal.
/// </summary>
public string Title { get; set; }
/// <summary>
/// Gets the <see cref="IModal"/> implementation used to initialize this object.
/// </summary>
public Type Type { get; }
/// <summary>
/// Gets a collection of the components of this modal.
/// </summary>
public IReadOnlyCollection<IInputComponentBuilder> Components => _components;
internal ModalBuilder(Type type)
{
if (!typeof(IModal).IsAssignableFrom(type))
throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type));
_components = new();
}
/// <summary>
/// Initializes a new <see cref="ModalBuilder"/>
/// </summary>
/// <param name="modalInitializer">The initialization delegate for this modal.</param>
public ModalBuilder(Type type, ModalInitializer modalInitializer) : this(type)
{
ModalInitializer = modalInitializer;
}
/// <summary>
/// Sets <see cref="Title"/>.
/// </summary>
/// <param name="title">New value of the <see cref="Title"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ModalBuilder WithTitle(string title)
{
Title = title;
return this;
}
/// <summary>
/// Adds text components to <see cref="TextComponents"/>.
/// </summary>
/// <param name="configure">Text Component builder factory.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ModalBuilder AddTextComponent(Action<TextInputComponentBuilder> configure)
{
var builder = new TextInputComponentBuilder(this);
configure(builder);
_components.Add(builder);
return this;
}
internal ModalInfo Build() => new(this);
}
}

View File

@@ -16,6 +16,7 @@ namespace Discord.Interactions.Builders
private readonly List<ContextCommandBuilder> _contextCommands;
private readonly List<ComponentCommandBuilder> _componentCommands;
private readonly List<AutocompleteCommandBuilder> _autocompleteCommands;
private readonly List<ModalCommandBuilder> _modalCommands;
/// <summary>
/// Gets the underlying Interaction Service.
@@ -92,6 +93,11 @@ namespace Discord.Interactions.Builders
/// </summary>
public IReadOnlyList<AutocompleteCommandBuilder> AutocompleteCommands => _autocompleteCommands;
/// <summary>
/// Gets a collection of the Modal Commands of this module.
/// </summary>
public IReadOnlyList<ModalCommandBuilder> ModalCommands => _modalCommands;
internal TypeInfo TypeInfo { get; set; }
internal ModuleBuilder (InteractionService interactionService, ModuleBuilder parent = null)
@@ -105,6 +111,7 @@ namespace Discord.Interactions.Builders
_contextCommands = new List<ContextCommandBuilder>();
_componentCommands = new List<ComponentCommandBuilder>();
_autocompleteCommands = new List<AutocompleteCommandBuilder>();
_modalCommands = new List<ModalCommandBuilder> ();
_preconditions = new List<PreconditionAttribute>();
}
@@ -152,7 +159,7 @@ namespace Discord.Interactions.Builders
/// <returns>
/// The builder instance.
/// </returns>
public ModuleBuilder WithDefaultPermision (bool permission)
public ModuleBuilder WithDefaultPermission (bool permission)
{
DefaultPermission = permission;
return this;
@@ -310,6 +317,21 @@ namespace Discord.Interactions.Builders
configure(command);
_autocompleteCommands.Add(command);
return this;
}
/// Adds a modal command builder to <see cref="ModalCommands"/>.
/// </summary>
/// <param name="configure"><see cref="ModalCommands"/> factory.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ModuleBuilder AddModalCommand(Action<ModalCommandBuilder> configure)
{
var command = new ModalCommandBuilder(this);
configure(command);
_modalCommands.Add(command);
return this;
}
/// <summary>

View File

@@ -103,6 +103,7 @@ namespace Discord.Interactions.Builders
var validContextCommands = methods.Where(IsValidContextCommandDefinition);
var validInteractions = methods.Where(IsValidComponentCommandDefinition);
var validAutocompleteCommands = methods.Where(IsValidAutocompleteCommandDefinition);
var validModalCommands = methods.Where(IsValidModalCommanDefinition);
Func<IServiceProvider, IInteractionModuleBase> createInstance = commandService._useCompiledLambda ?
ReflectionUtils<IInteractionModuleBase>.CreateLambdaBuilder(typeInfo, commandService) : ReflectionUtils<IInteractionModuleBase>.CreateBuilder(typeInfo, commandService);
@@ -118,6 +119,9 @@ namespace Discord.Interactions.Builders
foreach(var method in validAutocompleteCommands)
builder.AddAutocompleteCommand(x => BuildAutocompleteCommand(x, createInstance, method, commandService, services));
foreach(var method in validModalCommands)
builder.AddModalCommand(x => BuildModalCommand(x, createInstance, method, commandService, services));
}
private static void BuildSubModules (ModuleBuilder parent, IEnumerable<TypeInfo> subModules, IList<TypeInfo> builtTypes, InteractionService commandService,
@@ -298,6 +302,47 @@ namespace Discord.Interactions.Builders
builder.Callback = CreateCallback(createInstance, methodInfo, commandService);
}
private static void BuildModalCommand(ModalCommandBuilder builder, Func<IServiceProvider, IInteractionModuleBase> createInstance, MethodInfo methodInfo,
InteractionService commandService, IServiceProvider services)
{
var parameters = methodInfo.GetParameters();
if (parameters.Count(x => typeof(IModal).IsAssignableFrom(x.ParameterType)) > 1)
throw new InvalidOperationException($"A modal command can only have one {nameof(IModal)} parameter.");
if (!parameters.All(x => x.ParameterType == typeof(string) || typeof(IModal).IsAssignableFrom(x.ParameterType)))
throw new InvalidOperationException($"All parameters of a modal command must be either a string or an implementation of {nameof(IModal)}");
var attributes = methodInfo.GetCustomAttributes();
builder.MethodName = methodInfo.Name;
foreach (var attribute in attributes)
{
switch (attribute)
{
case ModalInteractionAttribute modal:
{
builder.Name = modal.CustomId;
builder.RunMode = modal.RunMode;
builder.IgnoreGroupNames = modal.IgnoreGroupNames;
}
break;
case PreconditionAttribute precondition:
builder.WithPreconditions(precondition);
break;
default:
builder.WithAttributes(attribute);
break;
}
}
foreach (var parameter in parameters)
builder.AddParameter(x => BuildParameter(x, parameter));
builder.Callback = CreateCallback(createInstance, methodInfo, commandService);
}
private static ExecuteCallback CreateCallback (Func<IServiceProvider, IInteractionModuleBase> createInstance,
MethodInfo methodInfo, InteractionService commandService)
{
@@ -400,7 +445,9 @@ namespace Discord.Interactions.Builders
builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower();
}
private static void BuildParameter (CommandParameterBuilder builder, ParameterInfo paramInfo)
private static void BuildParameter<TInfo, TBuilder> (ParameterBuilder<TInfo, TBuilder> builder, ParameterInfo paramInfo)
where TInfo : class, IParameterInfo
where TBuilder : ParameterBuilder<TInfo, TBuilder>
{
var attributes = paramInfo.GetCustomAttributes();
var paramType = paramInfo.ParameterType;
@@ -428,6 +475,84 @@ namespace Discord.Interactions.Builders
}
#endregion
#region Modals
public static ModalInfo BuildModalInfo(Type modalType)
{
if (!typeof(IModal).IsAssignableFrom(modalType))
throw new InvalidOperationException($"{modalType.FullName} isn't an implementation of {typeof(IModal).FullName}");
var instance = Activator.CreateInstance(modalType, false) as IModal;
try
{
var builder = new ModalBuilder(modalType)
{
Title = instance.Title
};
var inputs = modalType.GetProperties().Where(IsValidModalInputDefinition);
foreach (var prop in inputs)
{
var componentType = prop.GetCustomAttribute<ModalInputAttribute>()?.ComponentType;
switch (componentType)
{
case ComponentType.TextInput:
builder.AddTextComponent(x => BuildTextInput(x, prop, prop.GetValue(instance)));
break;
case null:
throw new InvalidOperationException($"{prop.Name} of {prop.DeclaringType.Name} isn't a valid modal input field.");
default:
throw new InvalidOperationException($"Component type {componentType} cannot be used in modals.");
}
}
var memberInit = ReflectionUtils<IModal>.CreateLambdaMemberInit(modalType.GetTypeInfo(), modalType.GetConstructor(Type.EmptyTypes), x => x.IsDefined(typeof(ModalInputAttribute)));
builder.ModalInitializer = (args) => memberInit(Array.Empty<object>(), args);
return builder.Build();
}
finally
{
(instance as IDisposable)?.Dispose();
}
}
private static void BuildTextInput(TextInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
{
var attributes = propertyInfo.GetCustomAttributes();
builder.Label = propertyInfo.Name;
builder.DefaultValue = defaultValue;
builder.WithType(propertyInfo.PropertyType);
foreach(var attribute in attributes)
{
switch (attribute)
{
case ModalTextInputAttribute textInput:
builder.CustomId = textInput.CustomId;
builder.ComponentType = textInput.ComponentType;
builder.Style = textInput.Style;
builder.Placeholder = textInput.Placeholder;
builder.MaxLength = textInput.MaxLength;
builder.MinLength = textInput.MinLength;
builder.InitialValue = textInput.InitialValue;
break;
case RequiredInputAttribute requiredInput:
builder.IsRequired = requiredInput.IsRequired;
break;
case InputLabelAttribute inputLabel:
builder.Label = inputLabel.Label;
break;
default:
builder.WithAttributes(attribute);
break;
}
}
}
#endregion
internal static bool IsValidModuleDefinition (TypeInfo typeInfo)
{
return ModuleTypeInfo.IsAssignableFrom(typeInfo) &&
@@ -467,5 +592,21 @@ namespace Discord.Interactions.Builders
!methodInfo.IsGenericMethod &&
methodInfo.GetParameters().Length == 0;
}
private static bool IsValidModalCommanDefinition(MethodInfo methodInfo)
{
return methodInfo.IsDefined(typeof(ModalInteractionAttribute)) &&
(methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task<RuntimeResult>)) &&
!methodInfo.IsStatic &&
!methodInfo.IsGenericMethod &&
typeof(IModal).IsAssignableFrom(methodInfo.GetParameters().Last().ParameterType);
}
private static bool IsValidModalInputDefinition(PropertyInfo propertyInfo)
{
return propertyInfo.SetMethod?.IsPublic == true &&
propertyInfo.SetMethod?.IsStatic == false &&
propertyInfo.IsDefined(typeof(ModalInputAttribute));
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
namespace Discord.Interactions.Builders
{
/// <summary>
/// Represents a builder for creating <see cref="ModalCommandBuilder"/>.
/// </summary>
public class ModalCommandParameterBuilder : ParameterBuilder<ModalCommandParameterInfo, ModalCommandParameterBuilder>
{
protected override ModalCommandParameterBuilder Instance => this;
/// <summary>
/// Gets the built <see cref="ModalInfo"/> class for this parameter, if <see cref="IsModalParameter"/> is <see langword="true"/>.
/// </summary>
public ModalInfo Modal { get; private set; }
/// <summary>
/// Gets whether or not this parameter is an <see cref="IModal"/>.
/// </summary>
public bool IsModalParameter => Modal is not null;
internal ModalCommandParameterBuilder(ICommandBuilder command) : base(command) { }
/// <summary>
/// Initializes a new <see cref="ModalCommandParameterBuilder"/>.
/// </summary>
/// <param name="command">Parent command of this parameter.</param>
/// <param name="name">Name of this command.</param>
/// <param name="type">Type of this parameter.</param>
public ModalCommandParameterBuilder(ICommandBuilder command, string name, Type type) : base(command, name, type) { }
/// <inheritdoc/>
public override ModalCommandParameterBuilder SetParameterType(Type type)
{
if (typeof(IModal).IsAssignableFrom(type))
Modal = ModalUtils.GetOrAdd(type);
return base.SetParameterType(type);
}
internal override ModalCommandParameterInfo Build(ICommandInfo command) =>
new(this, command);
}
}