[Feature] Modal Select Components Support for IF (#3189)
* add interaction service attributes new modal input types * add new modal component type converters * relocate hide attribute outside of slash command enum converter for general purpose use * refactor base inputcomponentbuilder types to use modal component typeconverters * add builders for new modal input types * add component insertion methods to modal builder for new input types * refactor base inputcomponentinfo class to use modal component typeconverters * add component info classes for newly added modal input types * add build logic for new modal input metadata classes * add componet collection properties for new modal input types to modalinfo * implement convertion logic for new modal inputs to the respond with modal extension methods * implement modal input typeconverters into interaction service * add read logic to enum modal component typeconverter * add default entity modal component typeconverter * add write logic to default value modal component typeconverter * add write logic to the nullable modal component typeconverter * add description property to input label attribute * add inline docs to modal attributes * add modal file upload attribute * add inline docs to input component infos * add description property to input component builder * add inline docs to modal component builders * add modal file upload component info * add modal file upload component builder * rename select input attribute * refactor select input attribute and add description property in moduleClassBuilder * add inline docs to modalBuilder * add description to inputComponentInfo * file-scope namespace for commandBuilder and modal interfaces * update respondWithModal logic to include new components * add inline docs and file upload component to modalInfo * add attachment modal typeconverter * create base non-input modal component entities * update modal component typeconverter namespaces and remove unused * add default min/max values to select input attribute * create text display builder and info classes * add text display parsing logic * add text display attribute * add modal select menu option attribute * add docs to text display component info * fix text display parsing * add isRequired mapping to select menus * revert to block-scope to clear diff false-positives * fix inline doc annotations * fix build errors * add interaction parameter to modal component typeconverter write method * add null check to select menu option attribute * add null check to default value modalTypeConverter write method * make ctors of modal component base attributes internal * implement predicate to hide attribute and enum modalcomponent typeconverter * fix HideAttribute inline docs build errors * simplify naming of the component classes and normalize namespaces * fix build errors in module class builder * add inline docs to modalComponentTypeConverter TryGetModalInteractionData * add min/max values parameters to ModalChannelSelectAttribute * fix defaultArrayModalTypeConverter chanell type write logic * simplify addDefaultValue methods for channe, mentionable, role, and user selects * add emoji support to select menu options * add instance value parsing to enum modalComponentConverter
This commit is contained in:
25
src/Discord.Net.Interactions/Attributes/HideAttribute.cs
Normal file
25
src/Discord.Net.Interactions/Attributes/HideAttribute.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enum values tagged with this attribute will not be displayed as a parameter choice
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This attribute must be used along with the default <see cref="EnumConverter{T}"/> and <see cref="DefaultEntityTypeConverter{T}"/>.
|
||||||
|
/// </remarks>
|
||||||
|
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class HideAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Can be optionally implemented by inherited types to conditionally hide an enum value.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Only runs on prior to modal construction. For slash command parameters, this method is ignored.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="interaction">Interaction that <see cref="IDiscordInteractionExtentions.RespondWithModalAsync{T}(IDiscordInteraction, string, T, RequestOptions, Action{ModalBuilder})"/> is called on.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// <see langword="true"/> if the attribute should be active and hide the value.
|
||||||
|
/// </returns>
|
||||||
|
public virtual bool Predicate(IDiscordInteraction interaction) => true;
|
||||||
|
}
|
||||||
@@ -13,13 +13,20 @@ namespace Discord.Interactions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Label { get; }
|
public string Label { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the label description of the input.
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a custom label for an modal input.
|
/// Creates a custom label for an modal input.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="label">The label of the input.</param>
|
/// <param name="label">The label of the input.</param>
|
||||||
public InputLabelAttribute(string label)
|
/// <param name="description">The label description of the input.</param>
|
||||||
|
public InputLabelAttribute(string label, string description = null)
|
||||||
{
|
{
|
||||||
Label = label;
|
Label = label;
|
||||||
|
Description = description;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a <see cref="IModal"/> property as a channel select.
|
||||||
|
/// </summary>
|
||||||
|
public class ModalChannelSelectAttribute : ModalSelectComponentAttribute
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override ComponentType ComponentType => ComponentType.ChannelSelect;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="ModalChannelSelectAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="customId">Custom ID of the channel select component.</param>
|
||||||
|
public ModalChannelSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mark an <see cref="IModal"/> property as a modal component field.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
|
||||||
|
public abstract class ModalComponentAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the type of the component.
|
||||||
|
/// </summary>
|
||||||
|
public abstract ComponentType ComponentType { get; }
|
||||||
|
|
||||||
|
internal ModalComponentAttribute() { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a <see cref="IModal"/> property as a file upload input.
|
||||||
|
/// </summary>
|
||||||
|
public class ModalFileUploadAttribute : ModalInputAttribute
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override ComponentType ComponentType => ComponentType.FileUpload;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the minimum number of files that can be uploaded.
|
||||||
|
/// </summary>
|
||||||
|
public int MinValues { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the maximum number of files that can be uploaded.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxValues { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="ModalFileUploadAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="customId">Custom ID of the file upload component.</param>
|
||||||
|
/// <param name="minValues">Minimum number of files that can be uploaded.</param>
|
||||||
|
/// <param name="maxValues">Maximum number of files that can be uploaded.</param>
|
||||||
|
public ModalFileUploadAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId)
|
||||||
|
{
|
||||||
|
MinValues = minValues;
|
||||||
|
MaxValues = maxValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,23 +6,18 @@ namespace Discord.Interactions
|
|||||||
/// Mark an <see cref="IModal"/> property as a modal input field.
|
/// Mark an <see cref="IModal"/> property as a modal input field.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
|
||||||
public abstract class ModalInputAttribute : Attribute
|
public abstract class ModalInputAttribute : ModalComponentAttribute
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the custom id of the text input.
|
/// Gets the custom id of the text input.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string CustomId { get; }
|
public string CustomId { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the type of the component.
|
|
||||||
/// </summary>
|
|
||||||
public abstract ComponentType ComponentType { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new <see cref="ModalInputAttribute"/>.
|
/// Create a new <see cref="ModalInputAttribute"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="customId">The custom id of the input.</param>
|
/// <param name="customId">The custom id of the input.</param>
|
||||||
protected ModalInputAttribute(string customId)
|
internal ModalInputAttribute(string customId)
|
||||||
{
|
{
|
||||||
CustomId = customId;
|
CustomId = customId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a <see cref="IModal"/> property as a mentionable select input.
|
||||||
|
/// </summary>
|
||||||
|
public class ModalMentionableSelectAttribute : ModalSelectComponentAttribute
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override ComponentType ComponentType => ComponentType.MentionableSelect;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="ModalMentionableSelectAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="customId">Custom ID of the mentionable select component.</param>
|
||||||
|
/// <param name="minValues">Minimum number of values that can be selected.</param>
|
||||||
|
/// <param name="maxValues">Maximum number of values that can be selected</param>
|
||||||
|
public ModalMentionableSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a <see cref="IModal"/> property as a role select input.
|
||||||
|
/// </summary>
|
||||||
|
public class ModalRoleSelectAttribute : ModalSelectComponentAttribute
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override ComponentType ComponentType => ComponentType.RoleSelect;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="ModalRoleSelectAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="customId">Custom ID of the role select component.</param>
|
||||||
|
/// <param name="minValues">Minimum number of values that can be selected.</param>
|
||||||
|
/// <param name="maxValues">Maximum number of values that can be selected.</param>
|
||||||
|
public ModalRoleSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base attribute for select-menu, user, channel, role, and mentionable select inputs in modals.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class ModalSelectComponentAttribute : ModalInputAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the minimum number of values that can be selected.
|
||||||
|
/// </summary>
|
||||||
|
public int MinValues { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the maximum number of values that can be selected.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxValues { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the placeholder text.
|
||||||
|
/// </summary>
|
||||||
|
public string Placeholder { get; set; }
|
||||||
|
|
||||||
|
internal ModalSelectComponentAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId)
|
||||||
|
{
|
||||||
|
MinValues = minValues;
|
||||||
|
MaxValues = maxValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a <see cref="IModal"/> property as a select menu input.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ModalSelectMenuAttribute : ModalSelectComponentAttribute
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override ComponentType ComponentType => ComponentType.SelectMenu;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="ModalSelectMenuAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="customId">Custom ID of the select menu component.</param>
|
||||||
|
/// <param name="minValues">Minimum number of values that can be selected.</param>
|
||||||
|
/// <param name="maxValues">Maximum number of values that can be selected.</param>
|
||||||
|
public ModalSelectMenuAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a select menu option to the marked field.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// To add additional metadata to enum fields, use <see cref="SelectMenuOptionAttribute"/> instead.
|
||||||
|
/// </remarks>
|
||||||
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
|
||||||
|
public class ModalSelectMenuOptionAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the label of the option.
|
||||||
|
/// </summary>
|
||||||
|
public string Label { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the description of the option.
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the value of the option.
|
||||||
|
/// </summary>
|
||||||
|
public string Value { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the emote of the option.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Can be either an <see cref="Emoji"/> or an <see cref="Discord.Emote"/>
|
||||||
|
/// </remarks>
|
||||||
|
public string Emote { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether the option is selected by default.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDefault { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="ModalSelectComponentAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="label">Label of the option.</param>
|
||||||
|
/// <param name="value">Value of the option.</param>
|
||||||
|
/// <param name="description">Description of the option.</param>
|
||||||
|
/// <param name="emote">Emote of the option. Can be either an <see cref="Emoji"/> or an <see cref="Discord.Emote"/></param>
|
||||||
|
/// <param name="isDefault">Whether the option is selected by default</param>
|
||||||
|
public ModalSelectMenuOptionAttribute(string label, string value, string description = null, string emote = null, bool isDefault = false)
|
||||||
|
{
|
||||||
|
Label = label;
|
||||||
|
Value = value;
|
||||||
|
Description = description;
|
||||||
|
Emote = emote;
|
||||||
|
IsDefault = isDefault;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a <see cref="IModal"/> property as a text input.
|
||||||
|
/// </summary>
|
||||||
|
public class ModalTextDisplayAttribute : ModalComponentAttribute
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override ComponentType ComponentType => ComponentType.TextDisplay;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the content of the text display.
|
||||||
|
/// </summary>
|
||||||
|
public string Content { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="ModalTextInputAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">Content of the text display.</param>
|
||||||
|
public ModalTextDisplayAttribute(string content = null)
|
||||||
|
{
|
||||||
|
Content = content;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a <see cref="IModal"/> property as a user select input.
|
||||||
|
/// </summary>
|
||||||
|
public class ModalUserSelectAttribute : ModalSelectComponentAttribute
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override ComponentType ComponentType => ComponentType.UserSelect;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="ModalUserSelectAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="customId">Custom ID of the user select component.</param>
|
||||||
|
/// <param name="minValues">Minimum number of values that can be selected.</param>
|
||||||
|
/// <param name="maxValues">Maximum number of values that can be selected.</param>
|
||||||
|
public ModalUserSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Discord.Interactions.Builders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a builder for creating <see cref="ChannelSelectComponentInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class ChannelSelectComponentBuilder : SnowflakeSelectComponentBuilder<ChannelSelectComponentInfo, ChannelSelectComponentBuilder>
|
||||||
|
{
|
||||||
|
protected override ChannelSelectComponentBuilder Instance => this;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="ChannelSelectComponentBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modal">Parent modal of this component.</param>
|
||||||
|
public ChannelSelectComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.ChannelSelect) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a default value to <see cref="SnowflakeSelectComponentBuilder{TInfo, TBuilder}.DefaultValues"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="channelId">The channel ID to add as a default value.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public ChannelSelectComponentBuilder AddDefaulValue(ulong channelId)
|
||||||
|
{
|
||||||
|
_defaultValues.Add(new SelectMenuDefaultValue(channelId, SelectDefaultValueType.Channel));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds default values to <see cref="SnowflakeSelectComponentBuilder{TInfo, TBuilder}.DefaultValues"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="channels">The channels to add as a default value.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public ChannelSelectComponentBuilder AddDefaultValues(params IEnumerable<IChannel> channels)
|
||||||
|
{
|
||||||
|
_defaultValues.AddRange(channels.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Channel)));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override ChannelSelectComponentInfo Build(ModalInfo modal)
|
||||||
|
=> new(this, modal);
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
namespace Discord.Interactions.Builders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a builder for creating <see cref="FileUploadComponentInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class FileUploadComponentBuilder : InputComponentBuilder<FileUploadComponentInfo, FileUploadComponentBuilder>
|
||||||
|
{
|
||||||
|
protected override FileUploadComponentBuilder Instance => this;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets and sets the minimum number of files that can be uploaded.
|
||||||
|
/// </summary>
|
||||||
|
public int MinValues { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets and sets the maximum number of files that can be uploaded.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxValues { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="FileUploadComponentBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modal"></param>
|
||||||
|
public FileUploadComponentBuilder(ModalBuilder modal) : base(modal) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="MinValues"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="minValues">New value of the <see cref="MinValues"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public FileUploadComponentBuilder WithMinValues(int minValues)
|
||||||
|
{
|
||||||
|
MinValues = minValues;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="MinValues"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxValues">New value of the <see cref="MaxValues"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public FileUploadComponentBuilder WithMaxValues(int maxValues)
|
||||||
|
{
|
||||||
|
MaxValues = maxValues;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override FileUploadComponentInfo Build(ModalInfo modal)
|
||||||
|
=> new (this, modal);
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
namespace Discord.Interactions.Builders
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represent a builder for creating <see cref="InputComponentInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
public interface IInputComponentBuilder : IModalComponentBuilder
|
||||||
|
{
|
||||||
|
/// <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 the label description of this input component.
|
||||||
|
/// </summary>
|
||||||
|
string Description { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether this input component is required.
|
||||||
|
/// </summary>
|
||||||
|
bool IsRequired { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the <see cref="ModalComponentTypeConverter"/> assigned to this input.
|
||||||
|
/// </summary>
|
||||||
|
ModalComponentTypeConverter TypeConverter { 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="Description"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="description">New value of the <see cref="Description"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
IInputComponentBuilder WithDescription(string description);
|
||||||
|
|
||||||
|
/// <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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Discord.Interactions.Builders;
|
||||||
|
|
||||||
|
public interface IModalComponentBuilder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the parent modal of this input component.
|
||||||
|
/// </summary>
|
||||||
|
ModalBuilder Modal { 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>
|
||||||
|
/// Get the <see cref="PropertyInfo"/> of this component's property.
|
||||||
|
/// </summary>
|
||||||
|
PropertyInfo PropertyInfo { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default value of this input component property.
|
||||||
|
/// </summary>
|
||||||
|
object DefaultValue { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a collection of the attributes of this component.
|
||||||
|
/// </summary>
|
||||||
|
IReadOnlyCollection<Attribute> Attributes { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="Type"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">New value of the <see cref="Type"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
IModalComponentBuilder 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>
|
||||||
|
IModalComponentBuilder 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>
|
||||||
|
IModalComponentBuilder WithAttributes(params Attribute[] attributes);
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Discord.Interactions.Builders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represent a builder for creating <see cref="SnowflakeSelectComponentInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISnowflakeSelectComponentBuilder : IInputComponentBuilder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the minimum number of values that can be selected.
|
||||||
|
/// </summary>
|
||||||
|
int MinValues { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the maximum number of values that can be selected.
|
||||||
|
/// </summary>
|
||||||
|
int MaxValues { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the placeholder text for this select component.
|
||||||
|
/// </summary>
|
||||||
|
string Placeholder { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default value collection for this select component.
|
||||||
|
/// </summary>
|
||||||
|
IReadOnlyCollection<SelectMenuDefaultValue> DefaultValues { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default value type of this select component.
|
||||||
|
/// </summary>
|
||||||
|
SelectDefaultValueType? DefaultValuesType { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a default value to the <see cref="DefaultValues"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="defaultValue">Default value to be added.</param>
|
||||||
|
/// <returns>The builder instance.</returns>
|
||||||
|
ISnowflakeSelectComponentBuilder AddDefaultValue(SelectMenuDefaultValue defaultValue);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="MinValues"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="minValues">New value of the <see cref="MinValues"/></param>
|
||||||
|
/// <returns>The builder instance.</returns>
|
||||||
|
ISnowflakeSelectComponentBuilder WithMinValues(int minValues);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="MaxValues"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxValues">New value of the <see cref="MaxValues"/></param>
|
||||||
|
/// <returns>The builder instance.</returns>
|
||||||
|
ISnowflakeSelectComponentBuilder WithMaxValues(int maxValues);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="Placeholder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="placeholder">New value of the <see cref="Placeholder"/></param>
|
||||||
|
/// <returns>The builder instance.</returns>
|
||||||
|
ISnowflakeSelectComponentBuilder WithPlaceholder(string placeholder);
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace Discord.Interactions.Builders
|
namespace Discord.Interactions.Builders
|
||||||
{
|
{
|
||||||
@@ -9,15 +8,11 @@ namespace Discord.Interactions.Builders
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TInfo">The <see cref="InputComponentInfo"/> this builder yields when built.</typeparam>
|
/// <typeparam name="TInfo">The <see cref="InputComponentInfo"/> this builder yields when built.</typeparam>
|
||||||
/// <typeparam name="TBuilder">Inherited <see cref="InputComponentBuilder{TInfo, TBuilder}"/> type.</typeparam>
|
/// <typeparam name="TBuilder">Inherited <see cref="InputComponentBuilder{TInfo, TBuilder}"/> type.</typeparam>
|
||||||
public abstract class InputComponentBuilder<TInfo, TBuilder> : IInputComponentBuilder
|
public abstract class InputComponentBuilder<TInfo, TBuilder> : ModalComponentBuilder<TInfo, TBuilder>, IInputComponentBuilder
|
||||||
where TInfo : InputComponentInfo
|
where TInfo : InputComponentInfo
|
||||||
where TBuilder : InputComponentBuilder<TInfo, TBuilder>
|
where TBuilder : InputComponentBuilder<TInfo, TBuilder>
|
||||||
{
|
{
|
||||||
private readonly List<Attribute> _attributes;
|
private readonly List<Attribute> _attributes;
|
||||||
protected abstract TBuilder Instance { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public ModalBuilder Modal { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string CustomId { get; set; }
|
public string CustomId { get; set; }
|
||||||
@@ -25,34 +20,21 @@ namespace Discord.Interactions.Builders
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string Label { get; set; }
|
public string Label { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public bool IsRequired { get; set; } = true;
|
public bool IsRequired { get; set; } = true;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public ComponentType ComponentType { get; internal set; }
|
public ModalComponentTypeConverter TypeConverter { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public Type Type { get; private set; }
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public PropertyInfo PropertyInfo { get; internal set; }
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public ComponentTypeConverter TypeConverter { get; private set; }
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public object DefaultValue { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IReadOnlyCollection<Attribute> Attributes => _attributes;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an instance of <see cref="InputComponentBuilder{TInfo, TBuilder}"/>
|
/// Creates an instance of <see cref="InputComponentBuilder{TInfo, TBuilder}"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="modal">Parent modal of this input component.</param>
|
/// <param name="modal">Parent modal of this input component.</param>
|
||||||
public InputComponentBuilder(ModalBuilder modal)
|
internal InputComponentBuilder(ModalBuilder modal) : base(modal)
|
||||||
{
|
{
|
||||||
Modal = modal;
|
|
||||||
_attributes = new();
|
_attributes = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +64,19 @@ namespace Discord.Interactions.Builders
|
|||||||
return Instance;
|
return Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="Description"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="description">New value of the <see cref="Description"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public TBuilder WithDescription(string description)
|
||||||
|
{
|
||||||
|
Description = description;
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets <see cref="IsRequired"/>.
|
/// Sets <see cref="IsRequired"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -95,19 +90,6 @@ namespace Discord.Interactions.Builders
|
|||||||
return Instance;
|
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>
|
/// <summary>
|
||||||
/// Sets <see cref="Type"/>.
|
/// Sets <see cref="Type"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -115,56 +97,20 @@ namespace Discord.Interactions.Builders
|
|||||||
/// <returns>
|
/// <returns>
|
||||||
/// The builder instance.
|
/// The builder instance.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public TBuilder WithType(Type type)
|
public override TBuilder WithType(Type type)
|
||||||
{
|
{
|
||||||
Type = type;
|
TypeConverter = Modal._interactionService.GetModalInputTypeConverter(type);
|
||||||
TypeConverter = Modal._interactionService.GetComponentTypeConverter(type);
|
return base.WithType(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/>
|
/// <inheritdoc/>
|
||||||
IInputComponentBuilder IInputComponentBuilder.WithCustomId(string customId) => WithCustomId(customId);
|
IInputComponentBuilder IInputComponentBuilder.WithCustomId(string customId) => WithCustomId(customId);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
IInputComponentBuilder IInputComponentBuilder.WithLabel(string label) => WithCustomId(label);
|
IInputComponentBuilder IInputComponentBuilder.WithLabel(string label) => WithLabel(label);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
IInputComponentBuilder IInputComponentBuilder.WithType(Type type) => WithType(type);
|
IInputComponentBuilder IInputComponentBuilder.WithDescription(string description) => WithDescription(description);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
IInputComponentBuilder IInputComponentBuilder.SetDefaultValue(object value) => SetDefaultValue(value);
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
IInputComponentBuilder IInputComponentBuilder.WithAttributes(params Attribute[] attributes) => WithAttributes(attributes);
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
IInputComponentBuilder IInputComponentBuilder.SetIsRequired(bool isRequired) => SetIsRequired(isRequired);
|
IInputComponentBuilder IInputComponentBuilder.SetIsRequired(bool isRequired) => SetIsRequired(isRequired);
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Discord.Interactions.Builders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a builder for creating a <see cref="MentionableSelectComponentInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class MentionableSelectComponentBuilder : SnowflakeSelectComponentBuilder<MentionableSelectComponentInfo, MentionableSelectComponentBuilder>
|
||||||
|
{
|
||||||
|
protected override MentionableSelectComponentBuilder Instance => this;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize a new <see cref="MentionableSelectComponentBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modal">Parent modal of this input component.</param>
|
||||||
|
public MentionableSelectComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.MentionableSelect) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a snowflake ID as a default value to <see cref="SnowflakeSelectComponentBuilder{TInfo, TBuilder}.DefaultValues"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID to add as a default value.</param>
|
||||||
|
/// <param name="type">Enitity type of the snowflake ID.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public MentionableSelectComponentBuilder AddDefaultValue(ulong id, SelectDefaultValueType type)
|
||||||
|
{
|
||||||
|
_defaultValues.Add(new SelectMenuDefaultValue(id, type));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add users as a default value to <see cref="SnowflakeSelectComponentBuilder{TInfo, TBuilder}.DefaultValues"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="users">The users to add as a default value.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public MentionableSelectComponentBuilder AddDefaultValue(params IEnumerable<IUser> users)
|
||||||
|
{
|
||||||
|
_defaultValues.AddRange(users.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.User)));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds channels as a default value to <see cref="SnowflakeSelectComponentBuilder{TInfo, TBuilder}.DefaultValues"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="channels">The channel to add as a default value.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public MentionableSelectComponentBuilder AddDefaultValue(params IEnumerable<IChannel> channels)
|
||||||
|
{
|
||||||
|
_defaultValues.AddRange(channels.Select(x =>new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Channel)));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds roles as a default value to <see cref="SnowflakeSelectComponentBuilder{TInfo, TBuilder}.DefaultValues"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="roles">The role to add as a default value.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public MentionableSelectComponentBuilder AddDefaulValue(params IEnumerable<IRole> roles)
|
||||||
|
{
|
||||||
|
_defaultValues.AddRange(roles.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Role)));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override MentionableSelectComponentInfo Build(ModalInfo modal)
|
||||||
|
=> new(this, modal);
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Discord.Interactions.Builders;
|
||||||
|
|
||||||
|
public abstract class ModalComponentBuilder<TInfo, TBuilder> : IModalComponentBuilder
|
||||||
|
where TInfo : ModalComponentInfo
|
||||||
|
where TBuilder : ModalComponentBuilder<TInfo, TBuilder>
|
||||||
|
{
|
||||||
|
private readonly List<Attribute> _attributes;
|
||||||
|
protected abstract TBuilder Instance { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ModalBuilder Modal { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ComponentType ComponentType { get; internal set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public Type Type { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public PropertyInfo PropertyInfo { get; internal set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public object DefaultValue { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IReadOnlyCollection<Attribute> Attributes => _attributes;
|
||||||
|
|
||||||
|
internal ModalComponentBuilder(ModalBuilder modal)
|
||||||
|
{
|
||||||
|
Modal = modal;
|
||||||
|
_attributes = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="ComponentType"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="componentType">New value of the <see cref="ComponentType"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public virtual 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 virtual 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 virtual 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 virtual TBuilder WithAttributes(params Attribute[] attributes)
|
||||||
|
{
|
||||||
|
_attributes.AddRange(attributes);
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal abstract TInfo Build(ModalInfo modal);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
IModalComponentBuilder IModalComponentBuilder.WithType(Type type) => WithType(type);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
IModalComponentBuilder IModalComponentBuilder.SetDefaultValue(object value) => SetDefaultValue(value);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
IModalComponentBuilder IModalComponentBuilder.WithAttributes(params Attribute[] attributes) => WithAttributes(attributes);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Discord.Interactions.Builders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a builder for creating a <see cref="RoleSelectComponentInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class RoleSelectComponentBuilder : SnowflakeSelectComponentBuilder<RoleSelectComponentInfo, RoleSelectComponentBuilder>
|
||||||
|
{
|
||||||
|
protected override RoleSelectComponentBuilder Instance => this;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize a new <see cref="RoleSelectComponentBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modal">Parent modal of this input component.</param>
|
||||||
|
public RoleSelectComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.RoleSelect) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a default value to <see cref="SnowflakeSelectComponentBuilder{TInfo, TBuilder}.DefaultValues"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="roleId">The role ID to add as a default value.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public RoleSelectComponentBuilder AddDefaulValue(ulong roleId)
|
||||||
|
{
|
||||||
|
_defaultValues.Add(new SelectMenuDefaultValue(roleId, SelectDefaultValueType.Role));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds default values to <see cref="SnowflakeSelectComponentBuilder{TInfo, TBuilder}.DefaultValues"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="roles">The roles to add as a default value.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public RoleSelectComponentBuilder AddDefaultValues(params IEnumerable<IRole> roles)
|
||||||
|
{
|
||||||
|
_defaultValues.AddRange(roles.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Role)));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override RoleSelectComponentInfo Build(ModalInfo modal)
|
||||||
|
=> new(this, modal);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Discord.Interactions.Builders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a builder for creating <see cref="SelectMenuComponentInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class SelectMenuComponentBuilder : InputComponentBuilder<SelectMenuComponentInfo, SelectMenuComponentBuilder>
|
||||||
|
{
|
||||||
|
private readonly List<SelectMenuOptionBuilder> _options;
|
||||||
|
|
||||||
|
protected override SelectMenuComponentBuilder Instance => this;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets and sets the placeholder for the select menu iput.
|
||||||
|
/// </summary>
|
||||||
|
public string Placeholder { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets and sets the minimum number of values that can be selected.
|
||||||
|
/// </summary>
|
||||||
|
public int MinValues { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the maximum number of values that can be selected.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxValues { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the options of this select menu component.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<SelectMenuOptionBuilder> Options => _options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize a new <see cref="SelectMenuComponentBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modal">Parent modal of this component.</param>
|
||||||
|
public SelectMenuComponentBuilder(ModalBuilder modal) : base(modal)
|
||||||
|
{
|
||||||
|
_options = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an option to <see cref="Options"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="option">Option to be added to <see cref="Options"/>.</param>
|
||||||
|
/// <returns>The builder instance.</returns>
|
||||||
|
public SelectMenuComponentBuilder AddOption(SelectMenuOptionBuilder option)
|
||||||
|
{
|
||||||
|
_options.Add(option);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an option to <see cref="Options"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configure">Select menu option builder factory.</param>
|
||||||
|
/// <returns>The builder instance.</returns>
|
||||||
|
public SelectMenuComponentBuilder AddOption(Action<SelectMenuOptionBuilder> configure)
|
||||||
|
{
|
||||||
|
var builder = new SelectMenuOptionBuilder();
|
||||||
|
configure(builder);
|
||||||
|
_options.Add(builder);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override SelectMenuComponentInfo Build(ModalInfo modal)
|
||||||
|
=> new(this, modal);
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Discord.Interactions.Builders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a builder for creating <see cref="SnowflakeSelectComponentInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TInfo">The <see cref="SnowflakeSelectComponentInfo"/> this builder yields when built.</typeparam>
|
||||||
|
/// <typeparam name="TBuilder">Inherited <see cref="SnowflakeSelectComponentBuilder{TInfo, TBuilder}"/> type.</typeparam>
|
||||||
|
public abstract class SnowflakeSelectComponentBuilder<TInfo, TBuilder> : InputComponentBuilder<TInfo, TBuilder>, ISnowflakeSelectComponentBuilder
|
||||||
|
where TInfo : InputComponentInfo
|
||||||
|
where TBuilder : InputComponentBuilder<TInfo, TBuilder>, ISnowflakeSelectComponentBuilder
|
||||||
|
{
|
||||||
|
protected readonly List<SelectMenuDefaultValue> _defaultValues;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public int MinValues { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public int MaxValues { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Placeholder { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IReadOnlyCollection<SelectMenuDefaultValue> DefaultValues => _defaultValues.AsReadOnly();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public SelectDefaultValueType? DefaultValuesType
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return ComponentType switch
|
||||||
|
{
|
||||||
|
ComponentType.UserSelect => SelectDefaultValueType.User,
|
||||||
|
ComponentType.RoleSelect => SelectDefaultValueType.Role,
|
||||||
|
ComponentType.ChannelSelect => SelectDefaultValueType.Channel,
|
||||||
|
ComponentType.MentionableSelect => null,
|
||||||
|
_ => throw new InvalidOperationException("Component type must be a snowflake select type."),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize a new <see cref="SnowflakeSelectComponentBuilder{TInfo, TBuilder}"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modal">Parent modal of this input component.</param>
|
||||||
|
/// <param name="componentType">Type of this component.</param>
|
||||||
|
public SnowflakeSelectComponentBuilder(ModalBuilder modal, ComponentType componentType) : base(modal)
|
||||||
|
{
|
||||||
|
ValidateComponentType(componentType);
|
||||||
|
|
||||||
|
ComponentType = componentType;
|
||||||
|
_defaultValues = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public TBuilder AddDefaultValue(SelectMenuDefaultValue defaultValue)
|
||||||
|
{
|
||||||
|
if (DefaultValuesType.HasValue && defaultValue.Type != DefaultValuesType.Value)
|
||||||
|
throw new ArgumentException($"Only default values with {Enum.GetName(typeof(SelectDefaultValueType), DefaultValuesType.Value)} are support by {nameof(TInfo)} select type.", nameof(defaultValue));
|
||||||
|
|
||||||
|
_defaultValues.Add(defaultValue);
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override TBuilder WithComponentType(ComponentType componentType)
|
||||||
|
{
|
||||||
|
ValidateComponentType(componentType);
|
||||||
|
return base.WithComponentType(componentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public TBuilder WithMinValues(int minValues)
|
||||||
|
{
|
||||||
|
MinValues = minValues;
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public TBuilder WithMaxValues(int maxValues)
|
||||||
|
{
|
||||||
|
MaxValues = maxValues;
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public TBuilder WithPlaceholder(string placeholder)
|
||||||
|
{
|
||||||
|
Placeholder = placeholder;
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateComponentType(ComponentType componentType)
|
||||||
|
{
|
||||||
|
if (componentType is not (ComponentType.UserSelect or ComponentType.RoleSelect or ComponentType.MentionableSelect or ComponentType.ChannelSelect))
|
||||||
|
throw new ArgumentException("Component type must be a snowflake select type.", nameof(componentType));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
ISnowflakeSelectComponentBuilder ISnowflakeSelectComponentBuilder.AddDefaultValue(SelectMenuDefaultValue defaultValue) => AddDefaultValue(defaultValue);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
ISnowflakeSelectComponentBuilder ISnowflakeSelectComponentBuilder.WithMinValues(int minValues) => WithMinValues(minValues);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
ISnowflakeSelectComponentBuilder ISnowflakeSelectComponentBuilder.WithMaxValues(int maxValues) => WithMaxValues(maxValues);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
ISnowflakeSelectComponentBuilder ISnowflakeSelectComponentBuilder.WithPlaceholder(string placeholder) => WithPlaceholder(placeholder);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Interactions.Builders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a builder for creating <see cref="TextDisplayComponentInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class TextDisplayComponentBuilder : ModalComponentBuilder<TextDisplayComponentInfo, TextDisplayComponentBuilder>
|
||||||
|
{
|
||||||
|
protected override TextDisplayComponentBuilder Instance => this;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets and sets the content of the text display.
|
||||||
|
/// </summary>
|
||||||
|
public string Content { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize a new <see cref="TextDisplayComponentBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modal">Parent modal of this input component.</param>
|
||||||
|
public TextDisplayComponentBuilder(ModalBuilder modal) : base(modal)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="Content"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">New value of the <see cref="Content"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public TextDisplayComponentBuilder WithContent(string content)
|
||||||
|
{
|
||||||
|
Content = content;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override TextDisplayComponentBuilder WithType(Type type)
|
||||||
|
{
|
||||||
|
if(type != typeof(string))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Text display components can be only used with {typeof(string).Name} properties. {type.Name} provided instead.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.WithType(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override TextDisplayComponentInfo Build(ModalInfo modal)
|
||||||
|
=> new(this, modal);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Discord.Interactions.Builders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a builder for creating <see cref="UserSelectComponentInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class UserSelectComponentBuilder : SnowflakeSelectComponentBuilder<UserSelectComponentInfo, UserSelectComponentBuilder>
|
||||||
|
{
|
||||||
|
protected override UserSelectComponentBuilder Instance => this;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize a new <see cref="UserSelectComponentBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modal">Parent modal of this input component.</param>
|
||||||
|
public UserSelectComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.UserSelect) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a default value to <see cref="SnowflakeSelectComponentBuilder{TInfo, TBuilder}.DefaultValues"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user ID to add as a default value.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public UserSelectComponentBuilder AddDefaulValue(ulong userId)
|
||||||
|
{
|
||||||
|
_defaultValues.Add(new SelectMenuDefaultValue(userId, SelectDefaultValueType.User));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds default values to <see cref="SnowflakeSelectComponentBuilder{TInfo, TBuilder}.DefaultValues"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="users">The users to add as a default value.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public UserSelectComponentBuilder AddDefaultValues(params IEnumerable<IUser> users)
|
||||||
|
{
|
||||||
|
_defaultValues.AddRange(users.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.User)));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override UserSelectComponentInfo Build(ModalInfo modal)
|
||||||
|
=> new(this, modal);
|
||||||
|
}
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
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>
|
|
||||||
/// Get the <see cref="PropertyInfo"/> of this component's property.
|
|
||||||
/// </summary>
|
|
||||||
PropertyInfo PropertyInfo { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the <see cref="ComponentTypeConverter"/> assigned to this input.
|
|
||||||
/// </summary>
|
|
||||||
ComponentTypeConverter TypeConverter { 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Discord.Interactions.Builders
|
namespace Discord.Interactions.Builders
|
||||||
{
|
{
|
||||||
@@ -10,7 +9,7 @@ namespace Discord.Interactions.Builders
|
|||||||
public class ModalBuilder
|
public class ModalBuilder
|
||||||
{
|
{
|
||||||
internal readonly InteractionService _interactionService;
|
internal readonly InteractionService _interactionService;
|
||||||
internal readonly List<IInputComponentBuilder> _components;
|
internal readonly List<IModalComponentBuilder> _components;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the initialization delegate for this modal.
|
/// Gets the initialization delegate for this modal.
|
||||||
@@ -30,7 +29,7 @@ namespace Discord.Interactions.Builders
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a collection of the components of this modal.
|
/// Gets a collection of the components of this modal.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyCollection<IInputComponentBuilder> Components => _components;
|
public IReadOnlyCollection<IModalComponentBuilder> Components => _components.AsReadOnly();
|
||||||
|
|
||||||
internal ModalBuilder(Type type, InteractionService interactionService)
|
internal ModalBuilder(Type type, InteractionService interactionService)
|
||||||
{
|
{
|
||||||
@@ -72,7 +71,7 @@ namespace Discord.Interactions.Builders
|
|||||||
/// <returns>
|
/// <returns>
|
||||||
/// The builder instance.
|
/// The builder instance.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public ModalBuilder AddTextComponent(Action<TextInputComponentBuilder> configure)
|
public ModalBuilder AddTextInputComponent(Action<TextInputComponentBuilder> configure)
|
||||||
{
|
{
|
||||||
var builder = new TextInputComponentBuilder(this);
|
var builder = new TextInputComponentBuilder(this);
|
||||||
configure(builder);
|
configure(builder);
|
||||||
@@ -80,6 +79,111 @@ namespace Discord.Interactions.Builders
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a select menu component to <see cref="Components"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configure">Select menu component builder factory.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public ModalBuilder AddSelectMenuInputComponent(Action<SelectMenuComponentBuilder> configure)
|
||||||
|
{
|
||||||
|
var builder = new SelectMenuComponentBuilder(this);
|
||||||
|
configure(builder);
|
||||||
|
_components.Add(builder);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a user select component to <see cref="Components"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configure">User select component builder factory.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public ModalBuilder AddUserSelectInputComponent(Action<UserSelectComponentBuilder> configure)
|
||||||
|
{
|
||||||
|
var builder = new UserSelectComponentBuilder(this);
|
||||||
|
configure(builder);
|
||||||
|
_components.Add(builder);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a role select component to <see cref="Components"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configure">Role select component builder factory.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public ModalBuilder AddRoleSelectInputComponent(Action<RoleSelectComponentBuilder> configure)
|
||||||
|
{
|
||||||
|
var builder = new RoleSelectComponentBuilder(this);
|
||||||
|
configure(builder);
|
||||||
|
_components.Add(builder);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a mentionable select component to <see cref="Components"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configure">Mentionable select component builder factory.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public ModalBuilder AddMentionableSelectInputComponent(Action<MentionableSelectComponentBuilder> configure)
|
||||||
|
{
|
||||||
|
var builder = new MentionableSelectComponentBuilder(this);
|
||||||
|
configure(builder);
|
||||||
|
_components.Add(builder);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a channel select component to <see cref="Components"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configure">Channel select component builder factory.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public ModalBuilder AddChannelSelectInputComponent(Action<ChannelSelectComponentBuilder> configure)
|
||||||
|
{
|
||||||
|
var builder = new ChannelSelectComponentBuilder(this);
|
||||||
|
configure(builder);
|
||||||
|
_components.Add(builder);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a file upload component to <see cref="Components"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configure">File upload component builder factory.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public ModalBuilder AddFileUploadInputComponent(Action<FileUploadComponentBuilder> configure)
|
||||||
|
{
|
||||||
|
var builder = new FileUploadComponentBuilder(this);
|
||||||
|
configure(builder);
|
||||||
|
_components.Add(builder);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a text display component to <see cref="Components"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configure">Text display component builder factory.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public ModalBuilder AddTextDisplayComponent(Action<TextDisplayComponentBuilder> configure)
|
||||||
|
{
|
||||||
|
var builder = new TextDisplayComponentBuilder(this);
|
||||||
|
configure(builder);
|
||||||
|
_components.Add(builder);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
internal ModalInfo Build() => new(this);
|
internal ModalInfo Build() => new(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
@@ -95,7 +94,7 @@ namespace Discord.Interactions.Builders
|
|||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
case EnabledInDmAttribute enabledInDm:
|
case EnabledInDmAttribute enabledInDm:
|
||||||
{
|
{
|
||||||
builder.IsEnabledInDm = enabledInDm.IsEnabled;
|
builder.IsEnabledInDm = enabledInDm.IsEnabled;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -604,16 +603,37 @@ namespace Discord.Interactions.Builders
|
|||||||
Title = instance.Title
|
Title = instance.Title
|
||||||
};
|
};
|
||||||
|
|
||||||
var inputs = modalType.GetProperties().Where(IsValidModalInputDefinition);
|
var components = modalType.GetProperties().Where(IsValidModalComponentDefinition);
|
||||||
|
|
||||||
foreach (var prop in inputs)
|
foreach (var prop in components)
|
||||||
{
|
{
|
||||||
var componentType = prop.GetCustomAttribute<ModalInputAttribute>()?.ComponentType;
|
var componentType = prop.GetCustomAttribute<ModalComponentAttribute>()?.ComponentType;
|
||||||
|
|
||||||
switch (componentType)
|
switch (componentType)
|
||||||
{
|
{
|
||||||
case ComponentType.TextInput:
|
case ComponentType.TextInput:
|
||||||
builder.AddTextComponent(x => BuildTextInput(x, prop, prop.GetValue(instance)));
|
builder.AddTextInputComponent(x => BuildTextInputComponent(x, prop, prop.GetValue(instance)));
|
||||||
|
break;
|
||||||
|
case ComponentType.SelectMenu:
|
||||||
|
builder.AddSelectMenuInputComponent(x => BuildSelectMenuComponent(x, prop, prop.GetValue(instance)));
|
||||||
|
break;
|
||||||
|
case ComponentType.UserSelect:
|
||||||
|
builder.AddUserSelectInputComponent(x => BuildSnowflakeSelectComponent(x, prop, prop.GetValue(instance)));
|
||||||
|
break;
|
||||||
|
case ComponentType.RoleSelect:
|
||||||
|
builder.AddRoleSelectInputComponent(x => BuildSnowflakeSelectComponent(x, prop, prop.GetValue(instance)));
|
||||||
|
break;
|
||||||
|
case ComponentType.MentionableSelect:
|
||||||
|
builder.AddMentionableSelectInputComponent(x => BuildSnowflakeSelectComponent(x, prop, prop.GetValue(instance)));
|
||||||
|
break;
|
||||||
|
case ComponentType.ChannelSelect:
|
||||||
|
builder.AddChannelSelectInputComponent(x => BuildSnowflakeSelectComponent(x, prop, prop.GetValue(instance)));
|
||||||
|
break;
|
||||||
|
case ComponentType.FileUpload:
|
||||||
|
builder.AddFileUploadInputComponent(x => BuildFileUploadComponent(x, prop, prop.GetValue(instance)));
|
||||||
|
break;
|
||||||
|
case ComponentType.TextDisplay:
|
||||||
|
builder.AddTextDisplayComponent(x => BuildTextDisplayComponent(x, prop, prop.GetValue(instance)));
|
||||||
break;
|
break;
|
||||||
case null:
|
case null:
|
||||||
throw new InvalidOperationException($"{prop.Name} of {prop.DeclaringType.Name} isn't a valid modal input field.");
|
throw new InvalidOperationException($"{prop.Name} of {prop.DeclaringType.Name} isn't a valid modal input field.");
|
||||||
@@ -632,7 +652,7 @@ namespace Discord.Interactions.Builders
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void BuildTextInput(TextInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
|
private static void BuildTextInputComponent(TextInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
|
||||||
{
|
{
|
||||||
var attributes = propertyInfo.GetCustomAttributes();
|
var attributes = propertyInfo.GetCustomAttributes();
|
||||||
|
|
||||||
@@ -659,6 +679,149 @@ namespace Discord.Interactions.Builders
|
|||||||
break;
|
break;
|
||||||
case InputLabelAttribute inputLabel:
|
case InputLabelAttribute inputLabel:
|
||||||
builder.Label = inputLabel.Label;
|
builder.Label = inputLabel.Label;
|
||||||
|
builder.Description = inputLabel.Description;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
builder.WithAttributes(attribute);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildSelectMenuComponent(SelectMenuComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
|
||||||
|
{
|
||||||
|
var attributes = propertyInfo.GetCustomAttributes();
|
||||||
|
|
||||||
|
builder.Label = propertyInfo.Name;
|
||||||
|
builder.DefaultValue = defaultValue;
|
||||||
|
builder.WithType(propertyInfo.PropertyType);
|
||||||
|
builder.PropertyInfo = propertyInfo;
|
||||||
|
|
||||||
|
foreach (var attribute in attributes)
|
||||||
|
{
|
||||||
|
switch (attribute)
|
||||||
|
{
|
||||||
|
case ModalSelectMenuAttribute selectMenuInput:
|
||||||
|
builder.CustomId = selectMenuInput.CustomId;
|
||||||
|
builder.ComponentType = selectMenuInput.ComponentType;
|
||||||
|
builder.MinValues = selectMenuInput.MinValues;
|
||||||
|
builder.MaxValues = selectMenuInput.MaxValues;
|
||||||
|
builder.Placeholder = selectMenuInput.Placeholder;
|
||||||
|
break;
|
||||||
|
case RequiredInputAttribute requiredInput:
|
||||||
|
builder.IsRequired = requiredInput.IsRequired;
|
||||||
|
break;
|
||||||
|
case InputLabelAttribute inputLabel:
|
||||||
|
builder.Label = inputLabel.Label;
|
||||||
|
builder.Description = inputLabel.Description;
|
||||||
|
break;
|
||||||
|
case ModalSelectMenuOptionAttribute selectMenuOption:
|
||||||
|
Emoji emoji = null;
|
||||||
|
Emote emote = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(selectMenuOption?.Emote) && !(Emote.TryParse(selectMenuOption.Emote, out emote) || Emoji.TryParse(selectMenuOption.Emote, out emoji)))
|
||||||
|
throw new ArgumentException($"Unable to parse {selectMenuOption.Emote} of {propertyInfo.DeclaringType}.{propertyInfo.Name} into an {typeof(Emote).Name} or an {typeof(Emoji).Name}");
|
||||||
|
|
||||||
|
builder.AddOption(new SelectMenuOptionBuilder
|
||||||
|
{
|
||||||
|
Label = selectMenuOption.Label,
|
||||||
|
Description = selectMenuOption.Description,
|
||||||
|
Value = selectMenuOption.Value,
|
||||||
|
Emote = emote != null ? emote : emoji,
|
||||||
|
IsDefault = selectMenuOption.IsDefault
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
builder.WithAttributes(attribute);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildSnowflakeSelectComponent<TInfo, TBuilder>(SnowflakeSelectComponentBuilder<TInfo, TBuilder> builder, PropertyInfo propertyInfo, object defaultValue)
|
||||||
|
where TInfo : SnowflakeSelectComponentInfo
|
||||||
|
where TBuilder : SnowflakeSelectComponentBuilder<TInfo, TBuilder>
|
||||||
|
{
|
||||||
|
var attributes = propertyInfo.GetCustomAttributes();
|
||||||
|
|
||||||
|
builder.Label = propertyInfo.Name;
|
||||||
|
builder.DefaultValue = defaultValue;
|
||||||
|
builder.WithType(propertyInfo.PropertyType);
|
||||||
|
builder.PropertyInfo = propertyInfo;
|
||||||
|
|
||||||
|
foreach (var attribute in attributes)
|
||||||
|
{
|
||||||
|
switch (attribute)
|
||||||
|
{
|
||||||
|
case ModalSelectComponentAttribute selectInput:
|
||||||
|
builder.CustomId = selectInput.CustomId;
|
||||||
|
builder.ComponentType = selectInput.ComponentType;
|
||||||
|
builder.MinValues = selectInput.MinValues;
|
||||||
|
builder.MaxValues = selectInput.MaxValues;
|
||||||
|
builder.Placeholder = selectInput.Placeholder;
|
||||||
|
break;
|
||||||
|
case RequiredInputAttribute requiredInput:
|
||||||
|
builder.IsRequired = requiredInput.IsRequired;
|
||||||
|
break;
|
||||||
|
case InputLabelAttribute inputLabel:
|
||||||
|
builder.Label = inputLabel.Label;
|
||||||
|
builder.Description = inputLabel.Description;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
builder.WithAttributes(attribute);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildFileUploadComponent(FileUploadComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
|
||||||
|
{
|
||||||
|
var attributes = propertyInfo.GetCustomAttributes();
|
||||||
|
|
||||||
|
builder.Label = propertyInfo.Name;
|
||||||
|
builder.DefaultValue = defaultValue;
|
||||||
|
builder.WithType(propertyInfo.PropertyType);
|
||||||
|
builder.PropertyInfo = propertyInfo;
|
||||||
|
|
||||||
|
foreach (var attribute in attributes)
|
||||||
|
{
|
||||||
|
switch (attribute)
|
||||||
|
{
|
||||||
|
case ModalFileUploadAttribute fileUploadInput:
|
||||||
|
builder.CustomId = fileUploadInput.CustomId;
|
||||||
|
builder.ComponentType = fileUploadInput.ComponentType;
|
||||||
|
builder.MinValues = fileUploadInput.MinValues;
|
||||||
|
builder.MaxValues = fileUploadInput.MaxValues;
|
||||||
|
break;
|
||||||
|
case RequiredInputAttribute requiredInput:
|
||||||
|
builder.IsRequired = requiredInput.IsRequired;
|
||||||
|
break;
|
||||||
|
case InputLabelAttribute inputLabel:
|
||||||
|
builder.Label = inputLabel.Label;
|
||||||
|
builder.Description = inputLabel.Description;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
builder.WithAttributes(attribute);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildTextDisplayComponent(TextDisplayComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
|
||||||
|
{
|
||||||
|
var attributes = propertyInfo.GetCustomAttributes();
|
||||||
|
|
||||||
|
builder.DefaultValue = defaultValue;
|
||||||
|
builder.WithType(propertyInfo.PropertyType);
|
||||||
|
builder.PropertyInfo = propertyInfo;
|
||||||
|
|
||||||
|
foreach (var attribute in attributes)
|
||||||
|
{
|
||||||
|
switch (attribute)
|
||||||
|
{
|
||||||
|
case ModalTextDisplayAttribute textDisplay:
|
||||||
|
builder.ComponentType = textDisplay.ComponentType;
|
||||||
|
builder.Content = textDisplay.Content;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
builder.WithAttributes(attribute);
|
builder.WithAttributes(attribute);
|
||||||
@@ -717,11 +880,11 @@ namespace Discord.Interactions.Builders
|
|||||||
typeof(IModal).IsAssignableFrom(methodInfo.GetParameters().Last().ParameterType);
|
typeof(IModal).IsAssignableFrom(methodInfo.GetParameters().Last().ParameterType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsValidModalInputDefinition(PropertyInfo propertyInfo)
|
private static bool IsValidModalComponentDefinition(PropertyInfo propertyInfo)
|
||||||
{
|
{
|
||||||
return propertyInfo.SetMethod?.IsPublic == true &&
|
return propertyInfo.SetMethod?.IsPublic == true &&
|
||||||
propertyInfo.SetMethod?.IsStatic == false &&
|
propertyInfo.SetMethod?.IsStatic == false &&
|
||||||
propertyInfo.IsDefined(typeof(ModalInputAttribute));
|
propertyInfo.IsDefined(typeof(ModalComponentAttribute));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ConstructorInfo GetComplexParameterConstructor(TypeInfo typeInfo, ComplexParameterAttribute complexParameter)
|
private static ConstructorInfo GetComplexParameterConstructor(TypeInfo typeInfo, ComplexParameterAttribute complexParameter)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Discord.Interactions
|
namespace Discord.Interactions
|
||||||
@@ -20,7 +21,7 @@ namespace Discord.Interactions
|
|||||||
if (!ModalUtils.TryGet<T>(out var modalInfo))
|
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)}");
|
throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}");
|
||||||
|
|
||||||
return SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal);
|
return SendModalResponseAsync<T>(interaction, customId, modalInfo, null, options, modifyModal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -43,7 +44,7 @@ namespace Discord.Interactions
|
|||||||
{
|
{
|
||||||
var modalInfo = ModalUtils.GetOrAdd<T>(interactionService);
|
var modalInfo = ModalUtils.GetOrAdd<T>(interactionService);
|
||||||
|
|
||||||
return SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal);
|
return SendModalResponseAsync<T>(interaction, customId, modalInfo, null, options, modifyModal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -64,20 +65,78 @@ namespace Discord.Interactions
|
|||||||
if (!ModalUtils.TryGet<T>(out var modalInfo))
|
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)}");
|
throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}");
|
||||||
|
|
||||||
var builder = new ModalBuilder(modal.Title, customId);
|
return SendModalResponseAsync<T>(interaction, customId, modalInfo, modal, options, modifyModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SendModalResponseAsync<T>(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, T modalInstance = null, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
|
||||||
|
where T : class, IModal
|
||||||
|
{
|
||||||
|
if (!modalInfo.Type.IsAssignableFrom(typeof(T)))
|
||||||
|
throw new ArgumentException($"{modalInfo.Type.FullName} isn't assignable from {typeof(T).FullName}.");
|
||||||
|
|
||||||
|
var builder = new ModalBuilder(modalInstance.Title, customId);
|
||||||
|
|
||||||
foreach (var input in modalInfo.Components)
|
foreach (var input in modalInfo.Components)
|
||||||
switch (input)
|
switch (input)
|
||||||
{
|
{
|
||||||
case TextInputComponentInfo textComponent:
|
case TextInputComponentInfo textComponent:
|
||||||
{
|
{
|
||||||
var boxedValue = textComponent.Getter(modal);
|
var inputBuilder = new TextInputBuilder(textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null,
|
||||||
var value = textComponent.TypeOverridesToString
|
textComponent.MaxLength, textComponent.IsRequired);
|
||||||
? boxedValue?.ToString()
|
|
||||||
: boxedValue as string;
|
|
||||||
|
|
||||||
builder.AddTextInput(textComponent.Label, textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null,
|
if (modalInstance != null)
|
||||||
textComponent.MaxLength, textComponent.IsRequired, value);
|
{
|
||||||
|
await textComponent.TypeConverter.WriteAsync(inputBuilder, interaction, textComponent, textComponent.Getter(modalInstance));
|
||||||
|
}
|
||||||
|
|
||||||
|
var labelBuilder = new LabelBuilder(textComponent.Label, inputBuilder, textComponent.Description);
|
||||||
|
builder.AddLabel(labelBuilder);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SelectMenuComponentInfo selectMenuComponent:
|
||||||
|
{
|
||||||
|
var inputBuilder = new SelectMenuBuilder(selectMenuComponent.CustomId, selectMenuComponent.Options.Select(x => new SelectMenuOptionBuilder(x)).ToList(), selectMenuComponent.Placeholder, selectMenuComponent.MaxValues, selectMenuComponent.MinValues, false, isRequired: selectMenuComponent.IsRequired);
|
||||||
|
|
||||||
|
if (modalInstance != null)
|
||||||
|
{
|
||||||
|
await selectMenuComponent.TypeConverter.WriteAsync(inputBuilder, interaction, selectMenuComponent, selectMenuComponent.Getter(modalInstance));
|
||||||
|
}
|
||||||
|
|
||||||
|
var labelBuilder = new LabelBuilder(selectMenuComponent.Label, inputBuilder, selectMenuComponent.Description);
|
||||||
|
builder.AddLabel(labelBuilder);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SnowflakeSelectComponentInfo snowflakeSelectComponent:
|
||||||
|
{
|
||||||
|
var inputBuilder = new SelectMenuBuilder(snowflakeSelectComponent.CustomId, null, snowflakeSelectComponent.Placeholder, snowflakeSelectComponent.MaxValues, snowflakeSelectComponent.MinValues, false, snowflakeSelectComponent.ComponentType, null, snowflakeSelectComponent.DefaultValues.ToList(), null, snowflakeSelectComponent.IsRequired);
|
||||||
|
|
||||||
|
if (modalInstance != null)
|
||||||
|
{
|
||||||
|
await snowflakeSelectComponent.TypeConverter.WriteAsync(inputBuilder, interaction, snowflakeSelectComponent, snowflakeSelectComponent.Getter(modalInstance));
|
||||||
|
}
|
||||||
|
|
||||||
|
var labelBuilder = new LabelBuilder(snowflakeSelectComponent.Label, inputBuilder, snowflakeSelectComponent.Description);
|
||||||
|
builder.AddLabel(labelBuilder);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case FileUploadComponentInfo fileUploadComponent:
|
||||||
|
{
|
||||||
|
var inputBuilder = new FileUploadComponentBuilder(fileUploadComponent.CustomId, fileUploadComponent.MinValues, fileUploadComponent.MaxValues, fileUploadComponent.IsRequired);
|
||||||
|
|
||||||
|
if (modalInstance != null)
|
||||||
|
{
|
||||||
|
await fileUploadComponent.TypeConverter.WriteAsync(inputBuilder, interaction, fileUploadComponent, fileUploadComponent.Getter(modalInstance));
|
||||||
|
}
|
||||||
|
|
||||||
|
var labelBuilder = new LabelBuilder(fileUploadComponent.Label, inputBuilder, fileUploadComponent.Description);
|
||||||
|
builder.AddLabel(labelBuilder);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TextDisplayComponentInfo textDisplayComponent:
|
||||||
|
{
|
||||||
|
var content = textDisplayComponent.Getter(modalInstance).ToString() ?? textDisplayComponent.Content;
|
||||||
|
var componentBuilder = new TextDisplayBuilder(content);
|
||||||
|
builder.AddTextDisplay(componentBuilder);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -86,13 +145,7 @@ namespace Discord.Interactions
|
|||||||
|
|
||||||
modifyModal?.Invoke(builder);
|
modifyModal?.Invoke(builder);
|
||||||
|
|
||||||
return interaction.RespondWithModalAsync(builder.Build(), options);
|
await interaction.RespondWithModalAsync(builder.Build(), options);
|
||||||
}
|
|
||||||
|
|
||||||
private static Task SendModalResponseAsync(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
|
|
||||||
{
|
|
||||||
var modal = modalInfo.ToModal(customId, modifyModal);
|
|
||||||
return interaction.RespondWithModalAsync(modal, options);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the <see cref="InputComponentInfo"/> class for <see cref="ComponentType.ChannelSelect"/> type.
|
||||||
|
/// </summary>
|
||||||
|
public class ChannelSelectComponentInfo : SnowflakeSelectComponentInfo
|
||||||
|
{
|
||||||
|
internal ChannelSelectComponentInfo(Builders.ChannelSelectComponentBuilder builder, ModalInfo modal) : base(builder, modal) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the <see cref="InputComponentInfo"/> class for <see cref="ComponentType.FileUpload"/> type.
|
||||||
|
/// </summary>
|
||||||
|
public class FileUploadComponentInfo : InputComponentInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the minimum number of values that can be selected.
|
||||||
|
/// </summary>
|
||||||
|
public int MinValues { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the maximum number of values that can be selected.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxValues { get; }
|
||||||
|
|
||||||
|
internal FileUploadComponentInfo(Builders.FileUploadComponentBuilder builder, ModalInfo modal) : base(builder, modal)
|
||||||
|
{
|
||||||
|
MinValues = builder.MinValues;
|
||||||
|
MaxValues = builder.MaxValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
namespace Discord.Interactions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the base info class for <see cref="IModal"/> input components.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class InputComponentInfo : ModalComponentInfo
|
||||||
|
{
|
||||||
|
/// <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 the description of this component.
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether or not this component requires a user input.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRequired { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="ModalComponentTypeConverter"/> assigned to this component.
|
||||||
|
/// </summary>
|
||||||
|
public ModalComponentTypeConverter TypeConverter { get; }
|
||||||
|
|
||||||
|
internal InputComponentInfo(Builders.IInputComponentBuilder builder, ModalInfo modal)
|
||||||
|
: base(builder, modal)
|
||||||
|
{
|
||||||
|
CustomId = builder.CustomId;
|
||||||
|
Label = builder.Label;
|
||||||
|
Description = builder.Description;
|
||||||
|
IsRequired = builder.IsRequired;
|
||||||
|
TypeConverter = builder.TypeConverter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the <see cref="InputComponentInfo"/> class for <see cref="ComponentType.MentionableSelect"/> type.
|
||||||
|
/// </summary>
|
||||||
|
public class MentionableSelectComponentInfo : SnowflakeSelectComponentInfo
|
||||||
|
{
|
||||||
|
internal MentionableSelectComponentInfo(Builders.MentionableSelectComponentBuilder builder, ModalInfo modal) : base(builder, modal) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the base info class for <see cref="IModal"/> components.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class ModalComponentInfo
|
||||||
|
{
|
||||||
|
private Lazy<Func<object, object>> _getter;
|
||||||
|
internal Func<object, object> Getter => _getter.Value;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the parent modal of this component.
|
||||||
|
/// </summary>
|
||||||
|
public ModalInfo Modal { 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 property linked to this component.
|
||||||
|
/// </summary>
|
||||||
|
public PropertyInfo PropertyInfo { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default value of this component property.
|
||||||
|
/// </summary>
|
||||||
|
public object DefaultValue { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a collection of the attributes of this command.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<Attribute> Attributes { get; }
|
||||||
|
|
||||||
|
internal ModalComponentInfo(Builders.IModalComponentBuilder builder, ModalInfo modal)
|
||||||
|
{
|
||||||
|
Modal = modal;
|
||||||
|
ComponentType = builder.ComponentType;
|
||||||
|
Type = builder.Type;
|
||||||
|
PropertyInfo = builder.PropertyInfo;
|
||||||
|
DefaultValue = builder.DefaultValue;
|
||||||
|
Attributes = builder.Attributes.ToImmutableArray();
|
||||||
|
|
||||||
|
_getter = new(() => ReflectionUtils<object>.CreateLambdaPropertyGetter(Modal.Type, PropertyInfo));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the <see cref="InputComponentInfo"/> class for <see cref="ComponentType.RoleSelect"/> type.
|
||||||
|
/// </summary>
|
||||||
|
public class RoleSelectComponentInfo : SnowflakeSelectComponentInfo
|
||||||
|
{
|
||||||
|
internal RoleSelectComponentInfo(Builders.RoleSelectComponentBuilder builder, ModalInfo modal) : base(builder, modal) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the <see cref="InputComponentInfo"/> class for <see cref="ComponentType.SelectMenu"/> type.
|
||||||
|
/// </summary>
|
||||||
|
public class SelectMenuComponentInfo : InputComponentInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the placeholder of the select menu input.
|
||||||
|
/// </summary>
|
||||||
|
public string Placeholder { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the minimum number of values that can be selected.
|
||||||
|
/// </summary>
|
||||||
|
public int MinValues { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the maximum number of values that can be selected.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxValues { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the options of this select menu component.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<SelectMenuOption> Options { get; }
|
||||||
|
|
||||||
|
internal SelectMenuComponentInfo(Builders.SelectMenuComponentBuilder builder, ModalInfo modal) : base(builder, modal)
|
||||||
|
{
|
||||||
|
Placeholder = builder.Placeholder;
|
||||||
|
MinValues = builder.MinValues;
|
||||||
|
MaxValues = builder.MaxValues;
|
||||||
|
Options = builder.Options.Select(x => x.Build()).ToImmutableArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the base <see cref="InputComponentInfo"/> class for <see cref="ComponentType.UserSelect"/>, <see cref="ComponentType.ChannelSelect"/>, <see cref="ComponentType.RoleSelect"/>, <see cref="ComponentType.MentionableSelect"/> type.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class SnowflakeSelectComponentInfo : InputComponentInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the minimum number of values that can be selected.
|
||||||
|
/// </summary>
|
||||||
|
public int MinValues { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the maximum number of values that can be selected.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxValues { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the placeholder of this select input.
|
||||||
|
/// </summary>
|
||||||
|
public string Placeholder { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default values of this select input.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<SelectMenuDefaultValue> DefaultValues { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default value type of this select input.
|
||||||
|
/// </summary>
|
||||||
|
public SelectDefaultValueType? DefaultValueType { get; }
|
||||||
|
|
||||||
|
internal SnowflakeSelectComponentInfo(Builders.ISnowflakeSelectComponentBuilder builder, ModalInfo modal) : base(builder, modal)
|
||||||
|
{
|
||||||
|
MinValues = builder.MinValues;
|
||||||
|
MaxValues = builder.MaxValues;
|
||||||
|
Placeholder = builder.Placeholder;
|
||||||
|
DefaultValues = builder.DefaultValues.ToImmutableArray();
|
||||||
|
DefaultValueType = builder.DefaultValuesType;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using Discord.Interactions.Builders;
|
||||||
|
|
||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the <see cref="ModalComponentInfo"/> class for <see cref="ComponentType.TextDisplay"/> type.
|
||||||
|
/// </summary>
|
||||||
|
public class TextDisplayComponentInfo : ModalComponentInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the content of the text display.
|
||||||
|
/// </summary>
|
||||||
|
public string Content { get; }
|
||||||
|
|
||||||
|
internal TextDisplayComponentInfo(TextDisplayComponentBuilder builder, ModalInfo modal) : base(builder, modal)
|
||||||
|
{
|
||||||
|
Content = Content;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ namespace Discord.Interactions
|
|||||||
public class TextInputComponentInfo : InputComponentInfo
|
public class TextInputComponentInfo : InputComponentInfo
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <c>true</c> when <see cref="InputComponentInfo.Type"/> overrides <see cref="object.ToString"/>.
|
/// <c>true</c> when <see cref="ModalComponentInfo.Type"/> overrides <see cref="object.ToString"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal bool TypeOverridesToString => _typeOverridesToString.Value;
|
internal bool TypeOverridesToString => _typeOverridesToString.Value;
|
||||||
private readonly Lazy<bool> _typeOverridesToString;
|
private readonly Lazy<bool> _typeOverridesToString;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the <see cref="InputComponentInfo"/> class for <see cref="ComponentType.UserSelect"/> type.
|
||||||
|
/// </summary>
|
||||||
|
public class UserSelectComponentInfo : SnowflakeSelectComponentInfo
|
||||||
|
{
|
||||||
|
internal UserSelectComponentInfo(Builders.UserSelectComponentBuilder builder, ModalInfo modal) : base(builder, modal) { }
|
||||||
|
}
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace Discord.Interactions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the base info class for <see cref="IModal"/> input components.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class InputComponentInfo
|
|
||||||
{
|
|
||||||
private Lazy<Func<object, object>> _getter;
|
|
||||||
internal Func<object, object> Getter => _getter.Value;
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the parent modal of this component.
|
|
||||||
/// </summary>
|
|
||||||
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 property linked to this component.
|
|
||||||
/// </summary>
|
|
||||||
public PropertyInfo PropertyInfo { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the <see cref="ComponentTypeConverter"/> assigned to this component.
|
|
||||||
/// </summary>
|
|
||||||
public ComponentTypeConverter TypeConverter { 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;
|
|
||||||
PropertyInfo = builder.PropertyInfo;
|
|
||||||
TypeConverter = builder.TypeConverter;
|
|
||||||
DefaultValue = builder.DefaultValue;
|
|
||||||
Attributes = builder.Attributes.ToImmutableArray();
|
|
||||||
|
|
||||||
_getter = new(() => ReflectionUtils<object>.CreateLambdaPropertyGetter(Modal.Type, PropertyInfo));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Discord.Interactions.Builders;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
@@ -36,24 +37,80 @@ namespace Discord.Interactions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a collection of the components of this modal.
|
/// Gets a collection of the components of this modal.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyCollection<InputComponentInfo> Components { get; }
|
public IReadOnlyCollection<ModalComponentInfo> Components { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a collection of the input components of this modal.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<InputComponentInfo> InputComponents { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a collection of the text components of this modal.
|
/// Gets a collection of the text components of this modal.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyCollection<TextInputComponentInfo> TextComponents { get; }
|
public IReadOnlyCollection<TextInputComponentInfo> TextInputComponents { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a collection of the select menu components of this modal.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<SelectMenuComponentInfo> SelectMenuComponents { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a collection of the user select components of this modal.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<UserSelectComponentInfo> UserSelectComponents { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a collection of the role select components of this modal.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<RoleSelectComponentInfo> RoleSelectComponents { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a collection of the mentionable select components of this modal.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<MentionableSelectComponentInfo> MentionableSelectComponents { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a collection of the channel select components of this modal.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<ChannelSelectComponentInfo> ChannelSelectComponents { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a collection of the file upload components of this modal.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<FileUploadComponentInfo> FileUploadComponents { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a collection of the text display components of this modal.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<TextDisplayComponentInfo> TextDisplayComponents { get; }
|
||||||
|
|
||||||
internal ModalInfo(Builders.ModalBuilder builder)
|
internal ModalInfo(Builders.ModalBuilder builder)
|
||||||
{
|
{
|
||||||
Title = builder.Title;
|
Title = builder.Title;
|
||||||
Type = builder.Type;
|
Type = builder.Type;
|
||||||
Components = builder.Components.Select(x => x switch
|
Components = builder.Components.Select<IModalComponentBuilder, ModalComponentInfo>(x => x switch
|
||||||
{
|
{
|
||||||
Builders.TextInputComponentBuilder textComponent => textComponent.Build(this),
|
Builders.TextInputComponentBuilder textComponent => textComponent.Build(this),
|
||||||
|
Builders.SelectMenuComponentBuilder selectMenuComponent => selectMenuComponent.Build(this),
|
||||||
|
Builders.RoleSelectComponentBuilder roleSelectComponent => roleSelectComponent.Build(this),
|
||||||
|
Builders.ChannelSelectComponentBuilder channelSelectComponent => channelSelectComponent.Build(this),
|
||||||
|
Builders.UserSelectComponentBuilder userSelectComponent => userSelectComponent.Build(this),
|
||||||
|
Builders.MentionableSelectComponentBuilder mentionableSelectComponent => mentionableSelectComponent.Build(this),
|
||||||
|
Builders.FileUploadComponentBuilder fileUploadComponent => fileUploadComponent.Build(this),
|
||||||
|
Builders.TextDisplayComponentBuilder textDisplayComponent => textDisplayComponent.Build(this),
|
||||||
_ => throw new InvalidOperationException($"{x.GetType().FullName} isn't a supported modal input component builder type.")
|
_ => throw new InvalidOperationException($"{x.GetType().FullName} isn't a supported modal input component builder type.")
|
||||||
}).ToImmutableArray();
|
}).ToImmutableArray();
|
||||||
|
|
||||||
TextComponents = Components.OfType<TextInputComponentInfo>().ToImmutableArray();
|
InputComponents = Components.OfType<InputComponentInfo>().ToImmutableArray();
|
||||||
|
|
||||||
|
TextInputComponents = Components.OfType<TextInputComponentInfo>().ToImmutableArray();
|
||||||
|
SelectMenuComponents = Components.OfType<SelectMenuComponentInfo>().ToImmutableArray();
|
||||||
|
UserSelectComponents = Components.OfType<UserSelectComponentInfo>().ToImmutableArray();
|
||||||
|
RoleSelectComponents = Components.OfType<RoleSelectComponentInfo>().ToImmutableArray();
|
||||||
|
MentionableSelectComponents = Components.OfType<MentionableSelectComponentInfo>().ToImmutableArray();
|
||||||
|
ChannelSelectComponents = Components.OfType<ChannelSelectComponentInfo>().ToImmutableArray();
|
||||||
|
FileUploadComponents = Components.OfType<FileUploadComponentInfo>().ToImmutableArray();
|
||||||
|
TextDisplayComponents = Components.OfType<TextDisplayComponentInfo>().ToImmutableArray();
|
||||||
|
|
||||||
_interactionService = builder._interactionService;
|
_interactionService = builder._interactionService;
|
||||||
_initializer = builder.ModalInitializer;
|
_initializer = builder.ModalInitializer;
|
||||||
@@ -74,7 +131,7 @@ namespace Discord.Interactions
|
|||||||
|
|
||||||
for (var i = 0; i < Components.Count; i++)
|
for (var i = 0; i < Components.Count; i++)
|
||||||
{
|
{
|
||||||
var input = Components.ElementAt(i);
|
var input = InputComponents.ElementAt(i);
|
||||||
var component = components.Find(x => x.CustomId == input.CustomId);
|
var component = components.Find(x => x.CustomId == input.CustomId);
|
||||||
|
|
||||||
if (component is null)
|
if (component is null)
|
||||||
@@ -107,12 +164,12 @@ namespace Discord.Interactions
|
|||||||
|
|
||||||
services ??= EmptyServiceProvider.Instance;
|
services ??= EmptyServiceProvider.Instance;
|
||||||
|
|
||||||
var args = new object[Components.Count];
|
var args = new object[InputComponents.Count];
|
||||||
var components = modalInteraction.Data.Components.ToList();
|
var components = modalInteraction.Data.Components.ToList();
|
||||||
|
|
||||||
for (var i = 0; i < Components.Count; i++)
|
for (var i = 0; i < InputComponents.Count; i++)
|
||||||
{
|
{
|
||||||
var input = Components.ElementAt(i);
|
var input = InputComponents.ElementAt(i);
|
||||||
var component = components.Find(x => x.CustomId == input.CustomId);
|
var component = components.Find(x => x.CustomId == input.CustomId);
|
||||||
|
|
||||||
if (component is null)
|
if (component is null)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using Discord.Logging;
|
|||||||
using Discord.Rest;
|
using Discord.Rest;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -98,6 +97,7 @@ namespace Discord.Interactions
|
|||||||
private readonly TypeMap<TypeConverter, IApplicationCommandInteractionDataOption> _typeConverterMap;
|
private readonly TypeMap<TypeConverter, IApplicationCommandInteractionDataOption> _typeConverterMap;
|
||||||
private readonly TypeMap<ComponentTypeConverter, IComponentInteractionData> _compTypeConverterMap;
|
private readonly TypeMap<ComponentTypeConverter, IComponentInteractionData> _compTypeConverterMap;
|
||||||
private readonly TypeMap<TypeReader, string> _typeReaderMap;
|
private readonly TypeMap<TypeReader, string> _typeReaderMap;
|
||||||
|
private readonly TypeMap<ModalComponentTypeConverter, IComponentInteractionData> _modalInputTypeConverterMap;
|
||||||
private readonly ConcurrentDictionary<Type, IAutocompleteHandler> _autocompleteHandlers = new();
|
private readonly ConcurrentDictionary<Type, IAutocompleteHandler> _autocompleteHandlers = new();
|
||||||
private readonly ConcurrentDictionary<Type, ModalInfo> _modalInfos = new();
|
private readonly ConcurrentDictionary<Type, ModalInfo> _modalInfos = new();
|
||||||
private readonly SemaphoreSlim _lock;
|
private readonly SemaphoreSlim _lock;
|
||||||
@@ -228,6 +228,16 @@ namespace Discord.Interactions
|
|||||||
[typeof(Enum)] = typeof(EnumReader<>),
|
[typeof(Enum)] = typeof(EnumReader<>),
|
||||||
[typeof(Nullable<>)] = typeof(NullableReader<>)
|
[typeof(Nullable<>)] = typeof(NullableReader<>)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_modalInputTypeConverterMap = new TypeMap<ModalComponentTypeConverter, IComponentInteractionData>(this, new ConcurrentDictionary<Type, ModalComponentTypeConverter>
|
||||||
|
{
|
||||||
|
}, new ConcurrentDictionary<Type, Type>
|
||||||
|
{
|
||||||
|
[typeof(IConvertible)] = typeof(DefaultValueModalComponentConverter<>),
|
||||||
|
[typeof(Enum)] = typeof(EnumModalComponentConverter<>),
|
||||||
|
[typeof(Nullable<>)] = typeof(NullableComponentConverter<>),
|
||||||
|
[typeof(Array)] = typeof(DefaultArrayModalComponentConverter<>)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1064,6 +1074,94 @@ namespace Discord.Interactions
|
|||||||
public bool TryRemoveGenericTypeReader(Type type, out Type readerType)
|
public bool TryRemoveGenericTypeReader(Type type, out Type readerType)
|
||||||
=> _typeReaderMap.TryRemoveGeneric(type, out readerType);
|
=> _typeReaderMap.TryRemoveGeneric(type, out readerType);
|
||||||
|
|
||||||
|
internal ModalComponentTypeConverter GetModalInputTypeConverter(Type type, IServiceProvider services = null) =>
|
||||||
|
_modalInputTypeConverterMap.Get(type, services);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a concrete type <see cref="ModalComponentTypeConverter"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Primary target <see cref="Type"/> of the <see cref="ModalComponentTypeConverter"/>.</typeparam>
|
||||||
|
/// <param name="converter">The <see cref="ModalComponentTypeConverter"/> instance.</param>
|
||||||
|
public void AddModalComponentTypeConverter<T>(ModalComponentTypeConverter converter) =>
|
||||||
|
AddModalComponentTypeConverter(typeof(T), converter);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a concrete type <see cref="ModalComponentTypeConverter"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">Primary target <see cref="Type"/> of the <see cref="ModalComponentTypeConverter"/>.</param>
|
||||||
|
/// <param name="converter">The <see cref="ModalComponentTypeConverter"/> instance.</param>
|
||||||
|
public void AddModalComponentTypeConverter(Type type, ModalComponentTypeConverter converter) =>
|
||||||
|
_modalInputTypeConverterMap.AddConcrete(type, converter);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a generic type <see cref="ModalComponentTypeConverter{T}"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Generic Type constraint of the <see cref="Type"/> of the <see cref="ModalComponentTypeConverter{T}"/>.</typeparam>
|
||||||
|
/// <param name="converterType">Type of the <see cref="ModalComponentTypeConverter{T}"/>.</param>
|
||||||
|
public void AddGenericModalComponentTypeConverter<T>(Type converterType) =>
|
||||||
|
AddGenericModalComponentTypeConverter(typeof(T), converterType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a generic type <see cref="ModalComponentTypeConverter{T}"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="targetType">Generic Type constraint of the <see cref="Type"/> of the <see cref="ModalComponentTypeConverter{T}"/>.</param>
|
||||||
|
/// <param name="converterType">Type of the <see cref="ModalComponentTypeConverter{T}"/>.</param>
|
||||||
|
public void AddGenericModalComponentTypeConverter(Type targetType, Type converterType) =>
|
||||||
|
_modalInputTypeConverterMap.AddGeneric(targetType, converterType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a <see cref="ModalComponentTypeConverter"/> for the type <typeparamref name="T"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Removing a <see cref="ModalComponentTypeConverter"/> from the <see cref="InteractionService"/> will not dereference the <see cref="ModalComponentTypeConverter"/> from the loaded module/command instances.
|
||||||
|
/// You need to reload the modules for the changes to take effect.
|
||||||
|
/// </remarks>
|
||||||
|
/// <typeparam name="T">The type to remove the converter from.</typeparam>
|
||||||
|
/// <param name="converter">The converter if the resulting remove operation was successful.</param>
|
||||||
|
/// <returns><see langword="true"/> if the remove operation was successful; otherwise <see langword="false"/>.</returns>
|
||||||
|
public bool TryRemoveModalComponentTypeConverter<T>(out ModalComponentTypeConverter converter) =>
|
||||||
|
TryRemoveModalComponentTypeConverter(typeof(T), out converter);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a <see cref="ModalComponentTypeConverter"/> for the type <paramref name="type"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Removing a <see cref="ModalComponentTypeConverter"/> from the <see cref="InteractionService"/> will not dereference the <see cref="ModalComponentTypeConverter"/> from the loaded module/command instances.
|
||||||
|
/// You need to reload the modules for the changes to take effect.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="type">The type to remove the converter from.</param>
|
||||||
|
/// <param name="converter">The converter if the resulting remove operation was successful.</param>
|
||||||
|
/// <returns><see langword="true"/> if the remove operation was successful; otherwise <see langword="false"/>.</returns>
|
||||||
|
public bool TryRemoveModalComponentTypeConverter(Type type, out ModalComponentTypeConverter converter) =>
|
||||||
|
_modalInputTypeConverterMap.TryRemoveConcrete(type, out converter);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a generic <see cref="ModalComponentTypeConverter"/> for the type <typeparamref name="T"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Removing a <see cref="ModalComponentTypeConverter"/> from the <see cref="InteractionService"/> will not dereference the <see cref="ModalComponentTypeConverter"/> from the loaded module/command instances.
|
||||||
|
/// You need to reload the modules for the changes to take effect.
|
||||||
|
/// </remarks>
|
||||||
|
/// <typeparam name="T">The type to remove the converter from.</typeparam>
|
||||||
|
/// <param name="converterType">The converter if the resulting remove operation was successful.</param>
|
||||||
|
/// <returns><see langword="true"/> if the remove operation was successful; otherwise <see langword="false"/>.</returns>
|
||||||
|
public bool TryRemoveGenericModalComponentTypeConverter<T>(out Type converterType) =>
|
||||||
|
TryRemoveGenericModalComponentTypeConverter(typeof(T), out converterType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a generic <see cref="ModalComponentTypeConverter"/> for the type <paramref name="type"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Removing a <see cref="ModalComponentTypeConverter"/> from the <see cref="InteractionService"/> will not dereference the <see cref="ModalComponentTypeConverter"/> from the loaded module/command instances.
|
||||||
|
/// You need to reload the modules for the changes to take effect.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="type">The type to remove the converter from.</param>
|
||||||
|
/// <param name="converterType">The converter if the resulting remove operation was successful.</param>
|
||||||
|
/// <returns><see langword="true"/> if the remove operation was successful; otherwise <see langword="false"/>.</returns>
|
||||||
|
public bool TryRemoveGenericModalComponentTypeConverter(Type type, out Type converterType) =>
|
||||||
|
_modalInputTypeConverterMap.TryRemoveGeneric(type, out converterType);
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Serialize an object using a <see cref="TypeReader"/> into a <see cref="string"/> to be placed in a Component CustomId.
|
/// Serialize an object using a <see cref="TypeReader"/> into a <see cref="string"/> to be placed in a Component CustomId.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
using Discord.Utils;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
internal sealed class DefaultArrayModalComponentConverter<T> : ModalComponentTypeConverter<T>
|
||||||
|
{
|
||||||
|
private readonly Type _underlyingType;
|
||||||
|
private readonly TypeReader _typeReader;
|
||||||
|
private readonly ImmutableArray<ChannelType> _channelTypes;
|
||||||
|
|
||||||
|
public DefaultArrayModalComponentConverter(InteractionService interactionService)
|
||||||
|
{
|
||||||
|
var type = typeof(T);
|
||||||
|
|
||||||
|
if (!type.IsArray)
|
||||||
|
throw new InvalidOperationException($"{nameof(DefaultArrayComponentConverter<T>)} cannot be used to convert a non-array type.");
|
||||||
|
|
||||||
|
_underlyingType = typeof(T).GetElementType();
|
||||||
|
|
||||||
|
_typeReader = true switch
|
||||||
|
{
|
||||||
|
_ when typeof(IUser).IsAssignableFrom(_underlyingType)
|
||||||
|
|| typeof(IChannel).IsAssignableFrom(_underlyingType)
|
||||||
|
|| typeof(IMentionable).IsAssignableFrom(_underlyingType)
|
||||||
|
|| typeof(IRole).IsAssignableFrom(_underlyingType)
|
||||||
|
|| typeof(IAttachment).IsAssignableFrom(_underlyingType) => null,
|
||||||
|
_ => interactionService.GetTypeReader(_underlyingType)
|
||||||
|
};
|
||||||
|
|
||||||
|
_channelTypes = true switch
|
||||||
|
{
|
||||||
|
_ when typeof(IStageChannel).IsAssignableFrom(_underlyingType)
|
||||||
|
=> [ChannelType.Stage],
|
||||||
|
_ when typeof(IVoiceChannel).IsAssignableFrom(_underlyingType)
|
||||||
|
=> [ChannelType.Voice],
|
||||||
|
_ when typeof(IDMChannel).IsAssignableFrom(_underlyingType)
|
||||||
|
=> [ChannelType.DM],
|
||||||
|
_ when typeof(IGroupChannel).IsAssignableFrom(_underlyingType)
|
||||||
|
=> [ChannelType.Group],
|
||||||
|
_ when typeof(ICategoryChannel).IsAssignableFrom(_underlyingType)
|
||||||
|
=> [ChannelType.Category],
|
||||||
|
_ when typeof(INewsChannel).IsAssignableFrom(_underlyingType)
|
||||||
|
=> [ChannelType.News],
|
||||||
|
_ when typeof(IThreadChannel).IsAssignableFrom(_underlyingType)
|
||||||
|
=> [ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.NewsThread],
|
||||||
|
_ when typeof(ITextChannel).IsAssignableFrom(_underlyingType)
|
||||||
|
=> [ChannelType.Text],
|
||||||
|
_ when typeof(IMediaChannel).IsAssignableFrom(_underlyingType)
|
||||||
|
=> [ChannelType.Media],
|
||||||
|
_ when typeof(IForumChannel).IsAssignableFrom(_underlyingType)
|
||||||
|
=> [ChannelType.Forum],
|
||||||
|
_ => []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||||
|
{
|
||||||
|
var objs = new List<object>();
|
||||||
|
|
||||||
|
|
||||||
|
if (_typeReader is not null && option.Values.Count > 0)
|
||||||
|
foreach (var value in option.Values)
|
||||||
|
{
|
||||||
|
var result = await _typeReader.ReadAsync(context, value, services).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
objs.Add(result.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!TryGetModalInteractionData(context, out var modalData))
|
||||||
|
{
|
||||||
|
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{typeof(IModalInteractionData).Name} cannot be accessed from the provided {typeof(IInteractionContext).Name} type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolvedSnowflakes = new Dictionary<ulong, ISnowflakeEntity>();
|
||||||
|
|
||||||
|
if (modalData.Users is not null)
|
||||||
|
foreach (var user in modalData.Users)
|
||||||
|
resolvedSnowflakes[user.Id] = user;
|
||||||
|
|
||||||
|
if (modalData.Members is not null)
|
||||||
|
foreach (var member in modalData.Members)
|
||||||
|
resolvedSnowflakes[member.Id] = member;
|
||||||
|
|
||||||
|
if (modalData.Roles is not null)
|
||||||
|
foreach (var role in modalData.Roles)
|
||||||
|
resolvedSnowflakes[role.Id] = role;
|
||||||
|
|
||||||
|
if (modalData.Channels is not null)
|
||||||
|
foreach (var channel in modalData.Channels)
|
||||||
|
resolvedSnowflakes[channel.Id] = channel;
|
||||||
|
|
||||||
|
if (modalData.Attachments is not null)
|
||||||
|
foreach (var attachment in modalData.Attachments)
|
||||||
|
resolvedSnowflakes[attachment.Id] = attachment;
|
||||||
|
|
||||||
|
foreach (var value in option.Values)
|
||||||
|
{
|
||||||
|
if (!ulong.TryParse(value, out var id))
|
||||||
|
{
|
||||||
|
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{option.Type} contains invalid snowflake.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resolvedSnowflakes.TryGetValue(id, out var snowflakeEntity))
|
||||||
|
{
|
||||||
|
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Some snowflake entity references for the {option.Type} cannot be resolved.");
|
||||||
|
}
|
||||||
|
|
||||||
|
objs.Add(snowflakeEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var destination = Array.CreateInstance(_underlyingType, objs.Count);
|
||||||
|
|
||||||
|
for (var i = 0; i < objs.Count; i++)
|
||||||
|
destination.SetValue(objs[i], i);
|
||||||
|
|
||||||
|
return TypeConverterResult.FromSuccess(destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
||||||
|
{
|
||||||
|
if (builder is FileUploadComponentBuilder)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
if (builder is not SelectMenuBuilder selectMenu || !component.ComponentType.IsSelectType())
|
||||||
|
throw new InvalidOperationException($"Component type of the input {component.CustomId} of modal {component.Modal.Type.FullName} must be a select type.");
|
||||||
|
|
||||||
|
switch (value)
|
||||||
|
{
|
||||||
|
case IUser user:
|
||||||
|
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromUser(user));
|
||||||
|
break;
|
||||||
|
case IRole role:
|
||||||
|
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromRole(role));
|
||||||
|
break;
|
||||||
|
case IChannel channel:
|
||||||
|
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromChannel(channel));
|
||||||
|
break;
|
||||||
|
case IMentionable mentionable:
|
||||||
|
selectMenu.WithDefaultValues(mentionable switch
|
||||||
|
{
|
||||||
|
IUser user => SelectMenuDefaultValue.FromUser(user),
|
||||||
|
IRole role => SelectMenuDefaultValue.FromRole(role),
|
||||||
|
IChannel channel => SelectMenuDefaultValue.FromChannel(channel),
|
||||||
|
_ => throw new InvalidOperationException($"Mentionable select cannot be populated using an entity with type: {mentionable.GetType().FullName}")
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case IEnumerable<IUser> defaultUsers:
|
||||||
|
selectMenu.DefaultValues = defaultUsers.Select(SelectMenuDefaultValue.FromUser).ToList();
|
||||||
|
break;
|
||||||
|
case IEnumerable<IRole> defaultRoles:
|
||||||
|
selectMenu.DefaultValues = defaultRoles.Select(SelectMenuDefaultValue.FromRole).ToList();
|
||||||
|
break;
|
||||||
|
case IEnumerable<IChannel> defaultChannels:
|
||||||
|
selectMenu.DefaultValues = defaultChannels.Select(SelectMenuDefaultValue.FromChannel).ToList();
|
||||||
|
break;
|
||||||
|
case IEnumerable<IMentionable> defaultMentionables:
|
||||||
|
selectMenu.DefaultValues = defaultMentionables.Where(x => x is IUser or IRole or IChannel)
|
||||||
|
.Select(x =>
|
||||||
|
{
|
||||||
|
return x switch
|
||||||
|
{
|
||||||
|
IUser user => SelectMenuDefaultValue.FromUser(user),
|
||||||
|
IRole role => SelectMenuDefaultValue.FromRole(role),
|
||||||
|
IChannel channel => SelectMenuDefaultValue.FromChannel(channel),
|
||||||
|
_ => throw new InvalidOperationException($"Mentionable select cannot be populated using an entity with type: {x.GetType().FullName}")
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (component.ComponentType == ComponentType.ChannelSelect && _channelTypes.Length > 0)
|
||||||
|
selectMenu.WithChannelTypes(_channelTypes.ToList());
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
internal sealed class DefaultValueModalComponentConverter<T> : ModalComponentTypeConverter<T>
|
||||||
|
where T : IConvertible
|
||||||
|
{
|
||||||
|
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return option.Type switch
|
||||||
|
{
|
||||||
|
ComponentType.SelectMenu when option.Values.Count == 1 => Task.FromResult(TypeConverterResult.FromSuccess(Convert.ChangeType(option.Values.First(), typeof(T)))),
|
||||||
|
ComponentType.TextInput => Task.FromResult(TypeConverterResult.FromSuccess(Convert.ChangeType(option.Value, typeof(T)))),
|
||||||
|
_ => Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option.Type} doesn't have a convertible value."))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is FormatException or InvalidCastException)
|
||||||
|
{
|
||||||
|
return Task.FromResult(TypeConverterResult.FromError(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
||||||
|
{
|
||||||
|
var strValue = Convert.ToString(value);
|
||||||
|
|
||||||
|
if(string.IsNullOrEmpty(strValue))
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
switch (builder)
|
||||||
|
{
|
||||||
|
case TextInputBuilder textInput:
|
||||||
|
textInput.WithValue(strValue);
|
||||||
|
break;
|
||||||
|
case SelectMenuBuilder selectMenu when component.ComponentType is ComponentType.SelectMenu:
|
||||||
|
selectMenu.Options.FirstOrDefault(x => x.Value == strValue)?.IsDefault = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException($"{typeof(IConvertible).Name}s cannot be used to populate components other than SelectMenu and TextInput.");
|
||||||
|
}
|
||||||
|
;
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
internal sealed class EnumModalComponentConverter<T> : ModalComponentTypeConverter<T>
|
||||||
|
where T : struct, Enum
|
||||||
|
{
|
||||||
|
private record Option(SelectMenuOptionBuilder OptionBuilder, Predicate<IDiscordInteraction> Predicate, T Value);
|
||||||
|
|
||||||
|
private readonly bool _isFlags;
|
||||||
|
private readonly ImmutableArray<Option> _options;
|
||||||
|
|
||||||
|
public EnumModalComponentConverter()
|
||||||
|
{
|
||||||
|
var names = Enum.GetNames(typeof(T));
|
||||||
|
var members = names.SelectMany(x => typeof(T).GetMember(x));
|
||||||
|
|
||||||
|
_isFlags = typeof(T).IsDefined(typeof(FlagsAttribute));
|
||||||
|
|
||||||
|
_options = members.Select<MemberInfo, Option>(x =>
|
||||||
|
{
|
||||||
|
var selectMenuOptionAttr = x.GetCustomAttribute<SelectMenuOptionAttribute>();
|
||||||
|
|
||||||
|
Emoji emoji = null;
|
||||||
|
Emote emote = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(selectMenuOptionAttr?.Emote) && !(Emote.TryParse(selectMenuOptionAttr.Emote, out emote) || Emoji.TryParse(selectMenuOptionAttr.Emote, out emoji)))
|
||||||
|
throw new ArgumentException($"Unable to parse {selectMenuOptionAttr.Emote} of {x.DeclaringType.Name}.{x.Name} into an {typeof(Emote).Name} or an {typeof(Emoji).Name}");
|
||||||
|
|
||||||
|
var hideAttr = x.GetCustomAttribute<HideAttribute>();
|
||||||
|
Predicate<IDiscordInteraction> predicate = hideAttr != null ? hideAttr.Predicate : null;
|
||||||
|
|
||||||
|
var value = Enum.Parse<T>(x.Name);
|
||||||
|
var optionBuilder = new SelectMenuOptionBuilder(x.GetCustomAttribute<ChoiceDisplayAttribute>()?.Name ?? x.Name, x.Name, selectMenuOptionAttr?.Description, emote != null ? emote : emoji, selectMenuOptionAttr?.IsDefault);
|
||||||
|
|
||||||
|
return new(optionBuilder, predicate, value);
|
||||||
|
}).ToImmutableArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (option.Type is not ComponentType.SelectMenu or ComponentType.TextInput)
|
||||||
|
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option.Type} input type cannot be converted to {typeof(T).FullName}"));
|
||||||
|
|
||||||
|
var value = option.Type switch
|
||||||
|
{
|
||||||
|
ComponentType.SelectMenu => string.Join(",", option.Values),
|
||||||
|
ComponentType.TextInput => option.Value,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Enum.TryParse<T>(value, out var result))
|
||||||
|
return Task.FromResult(TypeConverterResult.FromSuccess(result));
|
||||||
|
|
||||||
|
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Value {option.Value} cannot be converted to {typeof(T).FullName}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
||||||
|
{
|
||||||
|
if (builder is not SelectMenuBuilder selectMenu || component.ComponentType is not ComponentType.SelectMenu)
|
||||||
|
throw new InvalidOperationException($"{nameof(EnumModalComponentConverter<T>)} can only write to select menu components.");
|
||||||
|
|
||||||
|
if (selectMenu.MaxValues > 1 && !_isFlags)
|
||||||
|
throw new InvalidOperationException($"Enum type {typeof(T).FullName} is not a [Flags] enum, so it cannot be used in a multi-select menu.");
|
||||||
|
|
||||||
|
var visibleOptions = _options.Where(x => !x.Predicate?.Invoke(interaction) ?? true);
|
||||||
|
|
||||||
|
if (value is T enumValue)
|
||||||
|
{
|
||||||
|
foreach(var option in visibleOptions)
|
||||||
|
{
|
||||||
|
option.OptionBuilder.IsDefault = _isFlags ? enumValue.HasFlag(option.Value) : enumValue.Equals(option.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectMenu.WithOptions([.. visibleOptions.Select(x => x.OptionBuilder)]);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds additional metadata to enum fields that are used for select-menus.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// To manually add select menu options to modal components, use <see cref="ModalSelectMenuOptionAttribute"/> instead.
|
||||||
|
/// </remarks>
|
||||||
|
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
|
||||||
|
public class SelectMenuOptionAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the desription of the option.
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether the option is selected by default.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDefault { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the emote of the option.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Can be either an <see cref="Emoji"/> or an <see cref="Discord.Emote"/>
|
||||||
|
/// </remarks>
|
||||||
|
public string Emote { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for creating ModalComponentTypeConverters. <see cref="InteractionService"/> uses ModalComponentTypeConverters to interface with Modal component parameters.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class ModalComponentTypeConverter : ITypeConverter<IComponentInteractionData>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Will be used to search for alternative ModalComponentTypeConverters whenever the Interaction Service encounters an unknown parameter type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">Type of the modal property.</param>
|
||||||
|
/// <returns>Whether this converter can be used to handle the given type.</returns>
|
||||||
|
public abstract bool CanConvertTo(Type type);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Will be used to read the incoming payload before building the modal instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">Command execution context.</param>
|
||||||
|
/// <param name="option">Received option payload.</param>
|
||||||
|
/// <param name="services">Service provider that will be used to initialize the command module.</param>
|
||||||
|
/// <returns>The result of the read process.</returns>
|
||||||
|
public abstract Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Will be used to manipulate the outgoing modal component, before the modal gets sent to Discord.
|
||||||
|
/// </summary>
|
||||||
|
public virtual Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
||||||
|
where TBuilder : class, IInteractableComponentBuilder
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to get the <see cref="IModalInteractionData"/> from the provided <see cref="IInteractionContext"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">Context containing the <see cref="IModalInteractionData"/>.</param>
|
||||||
|
/// <param name="modalData"><see cref="IModalInteractionData"/> found in the context if successful, <see langword="null"/> otherwise.</param>
|
||||||
|
/// <returns><see langword="true"/> when successful.</returns>
|
||||||
|
protected bool TryGetModalInteractionData(IInteractionContext context, out IModalInteractionData modalData)
|
||||||
|
{
|
||||||
|
if(context.Interaction is IModalInteraction modalInteraction)
|
||||||
|
{
|
||||||
|
modalData = modalInteraction.Data;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalData = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public abstract class ModalComponentTypeConverter<T> : ModalComponentTypeConverter
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public sealed override bool CanConvertTo(Type type) =>
|
||||||
|
typeof(T).IsAssignableFrom(type);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
internal class NullableModalComponentConverter<T> : ModalComponentTypeConverter<T>
|
||||||
|
{
|
||||||
|
private readonly ModalComponentTypeConverter _typeConverter;
|
||||||
|
|
||||||
|
public NullableModalComponentConverter(InteractionService interactionService, IServiceProvider services)
|
||||||
|
{
|
||||||
|
var type = Nullable.GetUnderlyingType(typeof(T));
|
||||||
|
|
||||||
|
if (type is null)
|
||||||
|
throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type");
|
||||||
|
|
||||||
|
_typeConverter = interactionService.GetModalInputTypeConverter(type, services);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||||
|
=> string.IsNullOrEmpty(option.Value) ? Task.FromResult(TypeConverterResult.FromSuccess(null)) : _typeConverter.ReadAsync(context, option, services);
|
||||||
|
|
||||||
|
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
||||||
|
=> _typeConverter.WriteAsync(builder, interaction, component, value);
|
||||||
|
}
|
||||||
@@ -43,13 +43,4 @@ namespace Discord.Interactions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Enum values tagged with this attribute will not be displayed as a parameter choice
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This attribute must be used along with the default <see cref="EnumConverter{T}"/>
|
|
||||||
/// </remarks>
|
|
||||||
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
|
|
||||||
public sealed class HideAttribute : Attribute { }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Interactions.TypeReaders;
|
||||||
|
|
||||||
|
internal class DateTimeTypeReader : TypeReader<DateTime>
|
||||||
|
{
|
||||||
|
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, string option, IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (DateTime.TryParse(option, out var dateTime))
|
||||||
|
return Task.FromResult(TypeConverterResult.FromSuccess(dateTime));
|
||||||
|
|
||||||
|
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option} is not a valid date time."));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user