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:
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
|
||||
namespace Discord.Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a Modal interaction handler. CustomId represents
|
||||
/// the CustomId of the Modal that will be handled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="GroupAttribute"/>s will add prefixes to this command if <see cref="IgnoreGroupNames"/> is set to <see langword="false"/>
|
||||
/// CustomID supports a Wild Card pattern where you can use the <see cref="InteractionServiceConfig.WildCardExpression"/> to match a set of CustomIDs.
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||
public sealed class ModalInteractionAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the string to compare the Modal CustomIDs with.
|
||||
/// </summary>
|
||||
public string CustomId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets <see langword="true"/> if <see cref="GroupAttribute"/>s will be ignored while creating this command and this method will be treated as a top level command.
|
||||
/// </summary>
|
||||
public bool IgnoreGroupNames { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the run mode this command gets executed with.
|
||||
/// </summary>
|
||||
public RunMode RunMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a command for modal interaction handling.
|
||||
/// </summary>
|
||||
/// <param name="customId">String to compare the modal CustomIDs with.</param>
|
||||
/// <param name="ignoreGroupNames">If <see langword="true"/> <see cref="GroupAttribute"/>s will be ignored while creating this command and this method will be treated as a top level command.</param>
|
||||
/// <param name="runMode">Set the run mode of the command.</param>
|
||||
public ModalInteractionAttribute(string customId, bool ignoreGroupNames = false, RunMode runMode = RunMode.Default)
|
||||
{
|
||||
CustomId = customId;
|
||||
IgnoreGroupNames = ignoreGroupNames;
|
||||
RunMode = runMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace Discord.Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a custom label for an modal input.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||
public class InputLabelAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the label of the input.
|
||||
/// </summary>
|
||||
public string Label { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a custom label for an modal input.
|
||||
/// </summary>
|
||||
/// <param name="label">The label of the input.</param>
|
||||
public InputLabelAttribute(string label)
|
||||
{
|
||||
Label = label;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
|
||||
namespace Discord.Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Mark an <see cref="IModal"/> property as a modal input field.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
|
||||
public abstract class ModalInputAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the custom id of the text input.
|
||||
/// </summary>
|
||||
public string CustomId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the component.
|
||||
/// </summary>
|
||||
public abstract ComponentType ComponentType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ModalInputAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="label">The label of the input.</param>
|
||||
/// <param name="customId">The custom id of the input.</param>
|
||||
/// <param name="required">Whether the user is required to input a value.></param>
|
||||
protected ModalInputAttribute(string customId)
|
||||
{
|
||||
CustomId = customId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
namespace Discord.Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks a <see cref="IModal"/> property as a text input.
|
||||
/// </summary>
|
||||
public sealed class ModalTextInputAttribute : ModalInputAttribute
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ComponentType ComponentType => ComponentType.TextInput;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the style of the text input.
|
||||
/// </summary>
|
||||
public TextInputStyle Style { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the placeholder of the text input.
|
||||
/// </summary>
|
||||
public string Placeholder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the minimum length of the text input.
|
||||
/// </summary>
|
||||
public int MinLength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum length of the text input.
|
||||
/// </summary>
|
||||
public int MaxLength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the initial value to be displayed by this input.
|
||||
/// </summary>
|
||||
public string InitialValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ModalTextInputAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="customId"The custom id of the text input.></param>
|
||||
/// <param name="style">The style of the text input.</param>
|
||||
/// <param name="placeholder">The placeholder of the text input.</param>
|
||||
/// <param name="minLength">The minimum length of the text input's content.</param>
|
||||
/// <param name="maxLength">The maximum length of the text input's content.</param>
|
||||
/// <param name="initValue">The initial value to be displayed by this input.</param>
|
||||
public ModalTextInputAttribute(string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, int minLength = 1, int maxLength = 4000, string initValue = null)
|
||||
: base(customId)
|
||||
{
|
||||
Style = style;
|
||||
Placeholder = placeholder;
|
||||
MinLength = minLength;
|
||||
MaxLength = maxLength;
|
||||
InitialValue = initValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace Discord.Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the input as required or optional.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||
public class RequiredInputAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether or not user input is required for this input.
|
||||
/// </summary>
|
||||
public bool IsRequired { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the input as required or optinal.
|
||||
/// </summary>
|
||||
/// <param name="isRequired">Whether or not user input is required for this input.</param>
|
||||
public RequiredInputAttribute(bool isRequired = true)
|
||||
{
|
||||
IsRequired = isRequired;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
81
src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs
Normal file
81
src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
13
src/Discord.Net.Interactions/Entities/IModal.cs
Normal file
13
src/Discord.Net.Interactions/Entities/IModal.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Discord.Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a generic <see cref="Modal"/> for use with the interaction service.
|
||||
/// </summary>
|
||||
public interface IModal
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the modal's title.
|
||||
/// </summary>
|
||||
string Title { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord.Interactions
|
||||
{
|
||||
public static class IDiscordInteractionExtentions
|
||||
{
|
||||
/// <summary>
|
||||
/// Respond to an interaction with a <see cref="IModal"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam>
|
||||
/// <param name="interaction">The interaction to respond to.</param>
|
||||
/// <param name="options">The request options for this <see langword="async"/> request.</param>
|
||||
/// <returns>A task that represents the asynchronous operation of responding to the interaction.</returns>
|
||||
public static async Task RespondWithModalAsync<T>(this IDiscordInteraction interaction, string customId, RequestOptions options = 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(modalInfo.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.InitialValue);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class");
|
||||
}
|
||||
|
||||
await interaction.RespondWithModalAsync(builder.Build(), options).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ namespace Discord.Interactions
|
||||
/// <param name="services">Services that will be used while initializing the <see cref="InteractionModuleBase{T}"/>.</param>
|
||||
/// <param name="additionalArgs">Provide additional string parameters to the method along with the auto generated parameters.</param>
|
||||
/// <returns>
|
||||
/// A task representing the asyncronous command execution process.
|
||||
/// A task representing the asynchronous command execution process.
|
||||
/// </returns>
|
||||
public async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services, params string[] additionalArgs)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
namespace Discord.Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the info class of an attribute based method for handling Modal Interaction events.
|
||||
/// </summary>
|
||||
public class ModalCommandInfo : CommandInfo<ModalCommandParameterInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ModalInfo"/> class for this commands <see cref="IModal"/> parameter.
|
||||
/// </summary>
|
||||
public ModalInfo Modal { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool SupportsWildCards => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyCollection<ModalCommandParameterInfo> Parameters { get; }
|
||||
|
||||
internal ModalCommandInfo(Builders.ModalCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService)
|
||||
{
|
||||
Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray();
|
||||
Modal = Parameters.Last().Modal;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services)
|
||||
=> await ExecuteAsync(context, services, null).ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Execute this command using dependency injection.
|
||||
/// </summary>
|
||||
/// <param name="context">Context that will be injected to the <see cref="InteractionModuleBase{T}"/>.</param>
|
||||
/// <param name="services">Services that will be used while initializing the <see cref="InteractionModuleBase{T}"/>.</param>
|
||||
/// <param name="additionalArgs">Provide additional string parameters to the method along with the auto generated parameters.</param>
|
||||
/// <returns>
|
||||
/// A task representing the asynchronous command execution process.
|
||||
/// </returns>
|
||||
public async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services, params string[] additionalArgs)
|
||||
{
|
||||
if (context.Interaction is not IModalInteraction modalInteraction)
|
||||
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Modal Interaction.");
|
||||
|
||||
try
|
||||
{
|
||||
var args = new List<object>();
|
||||
|
||||
if (additionalArgs is not null)
|
||||
args.AddRange(additionalArgs);
|
||||
|
||||
var modal = Modal.CreateModal(modalInteraction, Module.CommandService._exitOnMissingModalField);
|
||||
args.Add(modal);
|
||||
|
||||
return await RunAsync(context, args.ToArray(), services);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var result = ExecuteResult.FromError(ex);
|
||||
await InvokeModuleEvent(context, result).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Task InvokeModuleEvent(IInteractionContext context, IResult result)
|
||||
=> CommandService._modalCommandExecutedEvent.InvokeAsync(this, context, result);
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override string GetLogString(IInteractionContext context)
|
||||
{
|
||||
if (context.Guild != null)
|
||||
return $"Modal Command: \"{base.ToString()}\" for {context.User} in {context.Guild}/{context.Channel}";
|
||||
else
|
||||
return $"Modal Command: \"{base.ToString()}\" for {context.User} in {context.Channel}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Discord.Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the base info class for <see cref="IModal"/> input components.
|
||||
/// </summary>
|
||||
public abstract class InputComponentInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the parent modal of this component.
|
||||
/// </summary>
|
||||
public ModalInfo Modal { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the custom id of this component.
|
||||
/// </summary>
|
||||
public string CustomId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the label of this component.
|
||||
/// </summary>
|
||||
public string Label { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether or not this component requires a user input.
|
||||
/// </summary>
|
||||
public bool IsRequired { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of this component.
|
||||
/// </summary>
|
||||
public ComponentType ComponentType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reference type of this component.
|
||||
/// </summary>
|
||||
public Type Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default value of this component.
|
||||
/// </summary>
|
||||
public object DefaultValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of the attributes of this command.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<Attribute> Attributes { get; }
|
||||
|
||||
protected InputComponentInfo(Builders.IInputComponentBuilder builder, ModalInfo modal)
|
||||
{
|
||||
Modal = modal;
|
||||
CustomId = builder.CustomId;
|
||||
Label = builder.Label;
|
||||
IsRequired = builder.IsRequired;
|
||||
ComponentType = builder.ComponentType;
|
||||
Type = builder.Type;
|
||||
DefaultValue = builder.DefaultValue;
|
||||
Attributes = builder.Attributes.ToImmutableArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace Discord.Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the <see cref="InputComponentInfo"/> class for <see cref="ComponentType.TextInput"/> type.
|
||||
/// </summary>
|
||||
public class TextInputComponentInfo : InputComponentInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the style of the text input.
|
||||
/// </summary>
|
||||
public TextInputStyle Style { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the placeholder of the text input.
|
||||
/// </summary>
|
||||
public string Placeholder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the minimum length of the text input.
|
||||
/// </summary>
|
||||
public int MinLength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum length of the text input.
|
||||
/// </summary>
|
||||
public int MaxLength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the initial value to be displayed by this input.
|
||||
/// </summary>
|
||||
public string InitialValue { get; }
|
||||
|
||||
internal TextInputComponentInfo(Builders.TextInputComponentBuilder builder, ModalInfo modal) : base(builder, modal)
|
||||
{
|
||||
Style = builder.Style;
|
||||
Placeholder = builder.Placeholder;
|
||||
MinLength = builder.MinLength;
|
||||
MaxLength = builder.MaxLength;
|
||||
InitialValue = builder.InitialValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/Discord.Net.Interactions/Info/ModalInfo.cs
Normal file
90
src/Discord.Net.Interactions/Info/ModalInfo.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Discord.Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a cached object initialization delegate.
|
||||
/// </summary>
|
||||
/// <param name="args">Property arguments array.</param>
|
||||
/// <returns>
|
||||
/// Returns the constructed object.
|
||||
/// </returns>
|
||||
public delegate IModal ModalInitializer(object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// Represents the info class of an <see cref="IModal"/> form.
|
||||
/// </summary>
|
||||
public class ModalInfo
|
||||
{
|
||||
internal readonly ModalInitializer _initializer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the title of this modal.
|
||||
/// </summary>
|
||||
public string Title { get; }
|
||||
|
||||
/// <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<InputComponentInfo> Components { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of the text components of this modal.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<TextInputComponentInfo> TextComponents { get; }
|
||||
|
||||
internal ModalInfo(Builders.ModalBuilder builder)
|
||||
{
|
||||
Title = builder.Title;
|
||||
Type = builder.Type;
|
||||
Components = builder.Components.Select(x => x switch
|
||||
{
|
||||
Builders.TextInputComponentBuilder textComponent => textComponent.Build(this),
|
||||
_ => throw new InvalidOperationException($"{x.GetType().FullName} isn't a supported modal input component builder type.")
|
||||
}).ToImmutableArray();
|
||||
|
||||
TextComponents = Components.OfType<TextInputComponentInfo>().ToImmutableArray();
|
||||
|
||||
_initializer = builder.ModalInitializer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="IModal"/> and fills it with provided message components.
|
||||
/// </summary>
|
||||
/// <param name="components"><see cref="IModalInteraction"/> that will be injected into the modal.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="IModal"/> filled with the provided components.
|
||||
/// </returns>
|
||||
public IModal CreateModal(IModalInteraction modalInteraction, bool throwOnMissingField = false)
|
||||
{
|
||||
var args = new object[Components.Count];
|
||||
var components = modalInteraction.Data.Components.ToList();
|
||||
|
||||
for (var i = 0; i < Components.Count; i++)
|
||||
{
|
||||
var input = Components.ElementAt(i);
|
||||
var component = components.Find(x => x.CustomId == input.CustomId);
|
||||
|
||||
if (component is null)
|
||||
{
|
||||
if (!throwOnMissingField)
|
||||
args[i] = input.DefaultValue;
|
||||
else
|
||||
throw new InvalidOperationException($"Modal interaction is missing the required field: {input.CustomId}");
|
||||
}
|
||||
else
|
||||
args[i] = component.Value;
|
||||
}
|
||||
|
||||
return _initializer(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,8 @@ namespace Discord.Interactions
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<AutocompleteCommandInfo> AutocompleteCommands { get; }
|
||||
|
||||
public IReadOnlyCollection<ModalCommandInfo> ModalCommands { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the declaring type of this module, if <see cref="IsSubModule"/> is <see langword="true"/>.
|
||||
/// </summary>
|
||||
@@ -112,6 +114,7 @@ namespace Discord.Interactions
|
||||
ContextCommands = BuildContextCommands(builder).ToImmutableArray();
|
||||
ComponentCommands = BuildComponentCommands(builder).ToImmutableArray();
|
||||
AutocompleteCommands = BuildAutocompleteCommands(builder).ToImmutableArray();
|
||||
ModalCommands = BuildModalCommands(builder).ToImmutableArray();
|
||||
SubModules = BuildSubModules(builder, commandService, services).ToImmutableArray();
|
||||
Attributes = BuildAttributes(builder).ToImmutableArray();
|
||||
Preconditions = BuildPreconditions(builder).ToImmutableArray();
|
||||
@@ -171,6 +174,16 @@ namespace Discord.Interactions
|
||||
return result;
|
||||
}
|
||||
|
||||
private IEnumerable<ModalCommandInfo> BuildModalCommands(ModuleBuilder builder)
|
||||
{
|
||||
var result = new List<ModalCommandInfo>();
|
||||
|
||||
foreach (var commandBuilder in builder.ModalCommands)
|
||||
result.Add(commandBuilder.Build(this, CommandService));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private IEnumerable<Attribute> BuildAttributes (ModuleBuilder builder)
|
||||
{
|
||||
var result = new List<Attribute>();
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using Discord.Interactions.Builders;
|
||||
|
||||
namespace Discord.Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the base parameter info class for <see cref="InteractionService"/> modals.
|
||||
/// </summary>
|
||||
public class ModalCommandParameterInfo : CommandParameterInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ModalInfo"/> class for this parameter if <see cref="IsModalParameter"/> is true.
|
||||
/// </summary>
|
||||
public ModalInfo Modal { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this parameter is an <see cref="IModal"/>
|
||||
/// </summary>
|
||||
public bool IsModalParameter => Modal is not null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public new ModalCommandInfo Command => base.Command as ModalCommandInfo;
|
||||
|
||||
internal ModalCommandParameterInfo(ModalCommandParameterBuilder builder, ICommandInfo command) : base(builder, command)
|
||||
{
|
||||
Modal = builder.Modal;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,13 @@ namespace Discord.Interactions
|
||||
var response = await Context.Interaction.GetOriginalResponseAsync().ConfigureAwait(false);
|
||||
await response.DeleteAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IDiscordInteraction.RespondWithModalAsync(Modal, RequestOptions)"/>
|
||||
protected virtual async Task RespondWithModalAsync(Modal modal, RequestOptions options = null) => await Context.Interaction.RespondWithModalAsync(modal);
|
||||
|
||||
/// <inheritdoc cref="IDiscordInteractionExtentions.RespondWithModalAsync(IDiscordInteraction, IModal, RequestOptions)"/>
|
||||
protected virtual async Task RespondWithModalAsync<T>(string customId, RequestOptions options = null) where T : class, IModal
|
||||
=> await Context.Interaction.RespondWithModalAsync<T>(customId, options);
|
||||
|
||||
//IInteractionModuleBase
|
||||
|
||||
|
||||
@@ -53,21 +53,29 @@ namespace Discord.Interactions
|
||||
public event Func<IAutocompleteHandler, IInteractionContext, IResult, Task> AutocompleteHandlerExecuted { add { _autocompleteHandlerExecutedEvent.Add(value); } remove { _autocompleteHandlerExecutedEvent.Remove(value); } }
|
||||
internal readonly AsyncEvent<Func<IAutocompleteHandler, IInteractionContext, IResult, Task>> _autocompleteHandlerExecutedEvent = new();
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a Modal command is executed.
|
||||
/// </summary>
|
||||
public event Func<ModalCommandInfo, IInteractionContext, IResult, Task> ModalCommandExecuted { add { _modalCommandExecutedEvent.Add(value); } remove { _modalCommandExecutedEvent.Remove(value); } }
|
||||
internal readonly AsyncEvent<Func<ModalCommandInfo, IInteractionContext, IResult, Task>> _modalCommandExecutedEvent = new();
|
||||
|
||||
private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs;
|
||||
private readonly CommandMap<SlashCommandInfo> _slashCommandMap;
|
||||
private readonly ConcurrentDictionary<ApplicationCommandType, CommandMap<ContextCommandInfo>> _contextCommandMaps;
|
||||
private readonly CommandMap<ComponentCommandInfo> _componentCommandMap;
|
||||
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 ConcurrentDictionary<Type, IAutocompleteHandler> _autocompleteHandlers = new();
|
||||
private readonly ConcurrentDictionary<Type, ModalInfo> _modalInfos = new();
|
||||
private readonly SemaphoreSlim _lock;
|
||||
internal readonly Logger _cmdLogger;
|
||||
internal readonly LogManager _logManager;
|
||||
internal readonly Func<DiscordRestClient> _getRestClient;
|
||||
|
||||
internal readonly bool _throwOnError, _useCompiledLambda, _enableAutocompleteHandlers, _autoServiceScopes;
|
||||
internal readonly bool _throwOnError, _useCompiledLambda, _enableAutocompleteHandlers, _autoServiceScopes, _exitOnMissingModalField;
|
||||
internal readonly string _wildCardExp;
|
||||
internal readonly RunMode _runMode;
|
||||
internal readonly RestResponseCallback _restResponseCallback;
|
||||
@@ -97,6 +105,16 @@ namespace Discord.Interactions
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<ComponentCommandInfo> ComponentCommands => _moduleDefs.SelectMany(x => x.ComponentCommands).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Represents all Modal Commands loaded within <see cref="InteractionService"/>.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<ModalCommandInfo> ModalCommands => _moduleDefs.SelectMany(x => x.ModalCommands).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of the cached <see cref="ModalInfo"/> classes that are referenced in registered <see cref="ModalCommandInfo"/>s.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<ModalInfo> Modals => ModalUtils.Modals;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a <see cref="InteractionService"/> with provided configurations.
|
||||
/// </summary>
|
||||
@@ -145,6 +163,7 @@ namespace Discord.Interactions
|
||||
_contextCommandMaps = new ConcurrentDictionary<ApplicationCommandType, CommandMap<ContextCommandInfo>>();
|
||||
_componentCommandMap = new CommandMap<ComponentCommandInfo>(this, config.InteractionCustomIdDelimiters);
|
||||
_autocompleteCommandMap = new CommandMap<AutocompleteCommandInfo>(this);
|
||||
_modalCommandMap = new CommandMap<ModalCommandInfo>(this, config.InteractionCustomIdDelimiters);
|
||||
|
||||
_getRestClient = getRestClient;
|
||||
|
||||
@@ -155,6 +174,7 @@ namespace Discord.Interactions
|
||||
_throwOnError = config.ThrowOnError;
|
||||
_wildCardExp = config.WildCardExpression;
|
||||
_useCompiledLambda = config.UseCompiledLambda;
|
||||
_exitOnMissingModalField = config.ExitOnMissingModalField;
|
||||
_enableAutocompleteHandlers = config.EnableAutocompleteHandlers;
|
||||
_autoServiceScopes = config.AutoServiceScopes;
|
||||
_restResponseCallback = config.RestResponseCallback;
|
||||
@@ -509,6 +529,9 @@ namespace Discord.Interactions
|
||||
foreach (var command in module.AutocompleteCommands)
|
||||
_autocompleteCommandMap.AddCommand(command.GetCommandKeywords(), command);
|
||||
|
||||
foreach (var command in module.ModalCommands)
|
||||
_modalCommandMap.AddCommand(command, command.IgnoreGroupNames);
|
||||
|
||||
foreach (var subModule in module.SubModules)
|
||||
LoadModuleInternal(subModule);
|
||||
}
|
||||
@@ -654,7 +677,7 @@ namespace Discord.Interactions
|
||||
public async Task<IResult> ExecuteCommandAsync (IInteractionContext context, IServiceProvider services)
|
||||
{
|
||||
var interaction = context.Interaction;
|
||||
|
||||
|
||||
return interaction switch
|
||||
{
|
||||
ISlashCommandInteraction slashCommand => await ExecuteSlashCommandAsync(context, slashCommand, services).ConfigureAwait(false),
|
||||
@@ -662,6 +685,7 @@ namespace Discord.Interactions
|
||||
IUserCommandInteraction userCommand => await ExecuteContextCommandAsync(context, userCommand.Data.Name, ApplicationCommandType.User, services).ConfigureAwait(false),
|
||||
IMessageCommandInteraction messageCommand => await ExecuteContextCommandAsync(context, messageCommand.Data.Name, ApplicationCommandType.Message, services).ConfigureAwait(false),
|
||||
IAutocompleteInteraction autocomplete => await ExecuteAutocompleteAsync(context, autocomplete, services).ConfigureAwait(false),
|
||||
IModalInteraction modalCommand => await ExecuteModalCommandAsync(context, modalCommand.Data.CustomId, services).ConfigureAwait(false),
|
||||
_ => throw new InvalidOperationException($"{interaction.Type} interaction type cannot be executed by the Interaction service"),
|
||||
};
|
||||
}
|
||||
@@ -745,6 +769,20 @@ 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))
|
||||
@@ -819,6 +857,24 @@ namespace Discord.Interactions
|
||||
_genericTypeConverters[targetType] = converterType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads and caches an <see cref="ModalInfo"/> for the provided <see cref="IModal"/>.
|
||||
/// </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);
|
||||
|
||||
if (_modalInfos.ContainsKey(type))
|
||||
throw new InvalidOperationException($"Modal type {type.FullName} already exists.");
|
||||
|
||||
return ModalUtils.GetOrAdd(type);
|
||||
}
|
||||
|
||||
internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null)
|
||||
{
|
||||
services ??= EmptyServiceProvider.Instance;
|
||||
|
||||
@@ -36,6 +36,9 @@ namespace Discord.Interactions
|
||||
/// <summary>
|
||||
/// Gets or sets the option to use compiled lambda expressions to create module instances and execute commands. This method improves performance at the cost of memory.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For performance reasons, if you frequently use <see cref="Modal"/>s with the service, it is highly recommended that you enable compiled lambdas.
|
||||
/// </remarks>
|
||||
public bool UseCompiledLambda { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
@@ -56,6 +59,11 @@ namespace Discord.Interactions
|
||||
/// Gets or sets delegate to be used by the <see cref="InteractionService"/> when responding to a Rest based interaction.
|
||||
/// </summary>
|
||||
public RestResponseCallback RestResponseCallback { get; set; } = (ctx, str) => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether a command execution should exit when a modal command encounters a missing modal component value.
|
||||
/// </summary>
|
||||
public bool ExitOnMissingModalField { get; set; } = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
51
src/Discord.Net.Interactions/Utilities/ModalUtils.cs
Normal file
51
src/Discord.Net.Interactions/Utilities/ModalUtils.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Discord.Interactions.Builders;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Discord.Interactions
|
||||
{
|
||||
internal static class ModalUtils
|
||||
{
|
||||
private static ConcurrentDictionary<Type, ModalInfo> _modalInfos = new();
|
||||
|
||||
public static IReadOnlyCollection<ModalInfo> Modals => _modalInfos.Values.ToReadOnlyCollection();
|
||||
|
||||
public static ModalInfo GetOrAdd(Type type)
|
||||
{
|
||||
if (!typeof(IModal).IsAssignableFrom(type))
|
||||
throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type));
|
||||
|
||||
return _modalInfos.GetOrAdd(type, ModuleClassBuilder.BuildModalInfo(type));
|
||||
}
|
||||
|
||||
public static ModalInfo GetOrAdd<T>() where T : class, IModal
|
||||
=> GetOrAdd(typeof(T));
|
||||
|
||||
public static bool TryGet(Type type, out ModalInfo modalInfo)
|
||||
{
|
||||
if (!typeof(IModal).IsAssignableFrom(type))
|
||||
throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type));
|
||||
|
||||
return _modalInfos.TryGetValue(type, out modalInfo);
|
||||
}
|
||||
|
||||
public static bool TryGet<T>(out ModalInfo modalInfo) where T : class, IModal
|
||||
=> TryGet(typeof(T), out modalInfo);
|
||||
|
||||
public static bool TryRemove(Type type, out ModalInfo modalInfo)
|
||||
{
|
||||
if (!typeof(IModal).IsAssignableFrom(type))
|
||||
throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type));
|
||||
|
||||
return _modalInfos.TryRemove(type, out modalInfo);
|
||||
}
|
||||
|
||||
public static bool TryRemove<T>(out ModalInfo modalInfo) where T : class, IModal
|
||||
=> TryRemove(typeof(T), out modalInfo);
|
||||
|
||||
public static void Clear() => _modalInfos.Clear();
|
||||
|
||||
public static int Count() => _modalInfos.Count;
|
||||
}
|
||||
}
|
||||
@@ -112,6 +112,67 @@ namespace Discord.Interactions
|
||||
var parameters = constructor.GetParameters();
|
||||
var properties = GetProperties(typeInfo);
|
||||
|
||||
var lambda = CreateLambdaMemberInit(typeInfo, constructor);
|
||||
|
||||
return (services) =>
|
||||
{
|
||||
var args = new object[parameters.Length];
|
||||
var props = new object[properties.Length];
|
||||
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
args[i] = GetMember(commandService, services, parameters[i].ParameterType, typeInfo);
|
||||
|
||||
for (int i = 0; i < properties.Length; i++)
|
||||
props[i] = GetMember(commandService, services, properties[i].PropertyType, typeInfo);
|
||||
|
||||
var instance = lambda(args, props);
|
||||
|
||||
return instance;
|
||||
};
|
||||
}
|
||||
|
||||
internal static Func<object[], T> CreateLambdaConstructorInvoker(TypeInfo typeInfo)
|
||||
{
|
||||
var constructor = GetConstructor(typeInfo);
|
||||
var parameters = constructor.GetParameters();
|
||||
|
||||
var argsExp = Expression.Parameter(typeof(object[]), "args");
|
||||
|
||||
var parameterExps = new Expression[parameters.Length];
|
||||
|
||||
for (var i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
var indexExp = Expression.Constant(i);
|
||||
var accessExp = Expression.ArrayIndex(argsExp, indexExp);
|
||||
parameterExps[i] = Expression.Convert(accessExp, parameters[i].ParameterType);
|
||||
}
|
||||
|
||||
var newExp = Expression.New(constructor, parameterExps);
|
||||
|
||||
return Expression.Lambda<Func<object[], T>>(newExp, argsExp).Compile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a compiled lambda property setter.
|
||||
/// </summary>
|
||||
internal static Action<T, object> CreateLambdaPropertySetter(PropertyInfo propertyInfo)
|
||||
{
|
||||
var instanceParam = Expression.Parameter(typeof(T), "instance");
|
||||
var valueParam = Expression.Parameter(typeof(object), "value");
|
||||
|
||||
var prop = Expression.Property(instanceParam, propertyInfo);
|
||||
var assign = Expression.Assign(prop, Expression.Convert(valueParam, propertyInfo.PropertyType));
|
||||
|
||||
return Expression.Lambda<Action<T, object>>(assign, instanceParam, valueParam).Compile();
|
||||
}
|
||||
|
||||
internal static Func<object[], object[], T> CreateLambdaMemberInit(TypeInfo typeInfo, ConstructorInfo constructor, Predicate<PropertyInfo> propertySelect = null)
|
||||
{
|
||||
propertySelect ??= x => true;
|
||||
|
||||
var parameters = constructor.GetParameters();
|
||||
var properties = GetProperties(typeInfo).Where(x => propertySelect(x)).ToArray();
|
||||
|
||||
var argsExp = Expression.Parameter(typeof(object[]), "args");
|
||||
var propsExp = Expression.Parameter(typeof(object[]), "props");
|
||||
|
||||
@@ -137,17 +198,8 @@ namespace Discord.Interactions
|
||||
var memberInit = Expression.MemberInit(newExp, memberExps);
|
||||
var lambda = Expression.Lambda<Func<object[], object[], T>>(memberInit, argsExp, propsExp).Compile();
|
||||
|
||||
return (services) =>
|
||||
return (args, props) =>
|
||||
{
|
||||
var args = new object[parameters.Length];
|
||||
var props = new object[properties.Length];
|
||||
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
args[i] = GetMember(commandService, services, parameters[i].ParameterType, typeInfo);
|
||||
|
||||
for (int i = 0; i < properties.Length; i++)
|
||||
props[i] = GetMember(commandService, services, properties[i].PropertyType, typeInfo);
|
||||
|
||||
var instance = lambda(args, props);
|
||||
|
||||
return instance;
|
||||
|
||||
Reference in New Issue
Block a user