[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>
|
||||
public string Label { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the label description of the input.
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a custom label for an modal input.
|
||||
/// </summary>
|
||||
/// <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;
|
||||
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.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
|
||||
public abstract class ModalInputAttribute : Attribute
|
||||
public abstract class ModalInputAttribute : ModalComponentAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the custom id of the text input.
|
||||
/// </summary>
|
||||
public string CustomId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the component.
|
||||
/// </summary>
|
||||
public abstract ComponentType ComponentType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ModalInputAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="customId">The custom id of the input.</param>
|
||||
protected ModalInputAttribute(string customId)
|
||||
internal ModalInputAttribute(string 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.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Discord.Interactions.Builders
|
||||
{
|
||||
@@ -9,15 +8,11 @@ namespace Discord.Interactions.Builders
|
||||
/// </summary>
|
||||
/// <typeparam name="TInfo">The <see cref="InputComponentInfo"/> this builder yields when built.</typeparam>
|
||||
/// <typeparam name="TBuilder">Inherited <see cref="InputComponentBuilder{TInfo, TBuilder}"/> type.</typeparam>
|
||||
public abstract class InputComponentBuilder<TInfo, TBuilder> : IInputComponentBuilder
|
||||
public abstract class InputComponentBuilder<TInfo, TBuilder> : ModalComponentBuilder<TInfo, TBuilder>, IInputComponentBuilder
|
||||
where TInfo : InputComponentInfo
|
||||
where TBuilder : InputComponentBuilder<TInfo, TBuilder>
|
||||
{
|
||||
private readonly List<Attribute> _attributes;
|
||||
protected abstract TBuilder Instance { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ModalBuilder Modal { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string CustomId { get; set; }
|
||||
@@ -25,34 +20,21 @@ namespace Discord.Interactions.Builders
|
||||
/// <inheritdoc/>
|
||||
public string Label { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsRequired { get; set; } = true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ComponentType ComponentType { get; internal set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Type Type { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public PropertyInfo PropertyInfo { get; internal set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ComponentTypeConverter TypeConverter { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object DefaultValue { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyCollection<Attribute> Attributes => _attributes;
|
||||
public ModalComponentTypeConverter TypeConverter { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of <see cref="InputComponentBuilder{TInfo, TBuilder}"/>
|
||||
/// </summary>
|
||||
/// <param name="modal">Parent modal of this input component.</param>
|
||||
public InputComponentBuilder(ModalBuilder modal)
|
||||
internal InputComponentBuilder(ModalBuilder modal) : base(modal)
|
||||
{
|
||||
Modal = modal;
|
||||
_attributes = new();
|
||||
}
|
||||
|
||||
@@ -82,6 +64,19 @@ namespace Discord.Interactions.Builders
|
||||
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>
|
||||
/// Sets <see cref="IsRequired"/>.
|
||||
/// </summary>
|
||||
@@ -95,19 +90,6 @@ namespace Discord.Interactions.Builders
|
||||
return Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets <see cref="ComponentType"/>.
|
||||
/// </summary>
|
||||
/// <param name="componentType">New value of the <see cref="ComponentType"/>.</param>
|
||||
/// <returns>
|
||||
/// The builder instance.
|
||||
/// </returns>
|
||||
public TBuilder WithComponentType(ComponentType componentType)
|
||||
{
|
||||
ComponentType = componentType;
|
||||
return Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets <see cref="Type"/>.
|
||||
/// </summary>
|
||||
@@ -115,56 +97,20 @@ namespace Discord.Interactions.Builders
|
||||
/// <returns>
|
||||
/// The builder instance.
|
||||
/// </returns>
|
||||
public TBuilder WithType(Type type)
|
||||
public override TBuilder WithType(Type type)
|
||||
{
|
||||
Type = type;
|
||||
TypeConverter = Modal._interactionService.GetComponentTypeConverter(type);
|
||||
return Instance;
|
||||
TypeConverter = Modal._interactionService.GetModalInputTypeConverter(type);
|
||||
return base.WithType(type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets <see cref="DefaultValue"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">New value of the <see cref="DefaultValue"/>.</param>
|
||||
/// <returns>
|
||||
/// The builder instance.
|
||||
/// </returns>
|
||||
public TBuilder SetDefaultValue(object value)
|
||||
{
|
||||
DefaultValue = value;
|
||||
return Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds attributes to <see cref="Attributes"/>.
|
||||
/// </summary>
|
||||
/// <param name="attributes">New attributes to be added to <see cref="Attributes"/>.</param>
|
||||
/// <returns>
|
||||
/// The builder instance.
|
||||
/// </returns>
|
||||
public TBuilder WithAttributes(params Attribute[] attributes)
|
||||
{
|
||||
_attributes.AddRange(attributes);
|
||||
return Instance;
|
||||
}
|
||||
|
||||
internal abstract TInfo Build(ModalInfo modal);
|
||||
|
||||
//IInputComponentBuilder
|
||||
/// <inheritdoc/>
|
||||
IInputComponentBuilder IInputComponentBuilder.WithCustomId(string customId) => WithCustomId(customId);
|
||||
|
||||
/// <inheritdoc/>
|
||||
IInputComponentBuilder IInputComponentBuilder.WithLabel(string label) => WithCustomId(label);
|
||||
IInputComponentBuilder IInputComponentBuilder.WithLabel(string label) => WithLabel(label);
|
||||
|
||||
/// <inheritdoc/>
|
||||
IInputComponentBuilder IInputComponentBuilder.WithType(Type type) => WithType(type);
|
||||
|
||||
/// <inheritdoc/>
|
||||
IInputComponentBuilder IInputComponentBuilder.SetDefaultValue(object value) => SetDefaultValue(value);
|
||||
|
||||
/// <inheritdoc/>
|
||||
IInputComponentBuilder IInputComponentBuilder.WithAttributes(params Attribute[] attributes) => WithAttributes(attributes);
|
||||
IInputComponentBuilder IInputComponentBuilder.WithDescription(string description) => WithDescription(description);
|
||||
|
||||
/// <inheritdoc/>
|
||||
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.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Discord.Interactions.Builders
|
||||
{
|
||||
@@ -10,7 +9,7 @@ namespace Discord.Interactions.Builders
|
||||
public class ModalBuilder
|
||||
{
|
||||
internal readonly InteractionService _interactionService;
|
||||
internal readonly List<IInputComponentBuilder> _components;
|
||||
internal readonly List<IModalComponentBuilder> _components;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the initialization delegate for this modal.
|
||||
@@ -30,7 +29,7 @@ namespace Discord.Interactions.Builders
|
||||
/// <summary>
|
||||
/// Gets a collection of the components of this modal.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<IInputComponentBuilder> Components => _components;
|
||||
public IReadOnlyCollection<IModalComponentBuilder> Components => _components.AsReadOnly();
|
||||
|
||||
internal ModalBuilder(Type type, InteractionService interactionService)
|
||||
{
|
||||
@@ -72,7 +71,7 @@ namespace Discord.Interactions.Builders
|
||||
/// <returns>
|
||||
/// The builder instance.
|
||||
/// </returns>
|
||||
public ModalBuilder AddTextComponent(Action<TextInputComponentBuilder> configure)
|
||||
public ModalBuilder AddTextInputComponent(Action<TextInputComponentBuilder> configure)
|
||||
{
|
||||
var builder = new TextInputComponentBuilder(this);
|
||||
configure(builder);
|
||||
@@ -80,6 +79,111 @@ namespace Discord.Interactions.Builders
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -95,7 +94,7 @@ namespace Discord.Interactions.Builders
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
case EnabledInDmAttribute enabledInDm:
|
||||
{
|
||||
{
|
||||
builder.IsEnabledInDm = enabledInDm.IsEnabled;
|
||||
}
|
||||
break;
|
||||
@@ -604,16 +603,37 @@ namespace Discord.Interactions.Builders
|
||||
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)
|
||||
{
|
||||
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;
|
||||
case null:
|
||||
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();
|
||||
|
||||
@@ -659,6 +679,149 @@ namespace Discord.Interactions.Builders
|
||||
break;
|
||||
case InputLabelAttribute inputLabel:
|
||||
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;
|
||||
default:
|
||||
builder.WithAttributes(attribute);
|
||||
@@ -717,11 +880,11 @@ namespace Discord.Interactions.Builders
|
||||
typeof(IModal).IsAssignableFrom(methodInfo.GetParameters().Last().ParameterType);
|
||||
}
|
||||
|
||||
private static bool IsValidModalInputDefinition(PropertyInfo propertyInfo)
|
||||
private static bool IsValidModalComponentDefinition(PropertyInfo propertyInfo)
|
||||
{
|
||||
return propertyInfo.SetMethod?.IsPublic == true &&
|
||||
propertyInfo.SetMethod?.IsStatic == false &&
|
||||
propertyInfo.IsDefined(typeof(ModalInputAttribute));
|
||||
propertyInfo.IsDefined(typeof(ModalComponentAttribute));
|
||||
}
|
||||
|
||||
private static ConstructorInfo GetComplexParameterConstructor(TypeInfo typeInfo, ComplexParameterAttribute complexParameter)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord.Interactions
|
||||
@@ -20,7 +21,7 @@ namespace Discord.Interactions
|
||||
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)}");
|
||||
|
||||
return SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal);
|
||||
return SendModalResponseAsync<T>(interaction, customId, modalInfo, null, options, modifyModal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -43,7 +44,7 @@ namespace Discord.Interactions
|
||||
{
|
||||
var modalInfo = ModalUtils.GetOrAdd<T>(interactionService);
|
||||
|
||||
return SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal);
|
||||
return SendModalResponseAsync<T>(interaction, customId, modalInfo, null, options, modifyModal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -64,20 +65,78 @@ namespace Discord.Interactions
|
||||
if (!ModalUtils.TryGet<T>(out var modalInfo))
|
||||
throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}");
|
||||
|
||||
var builder = new ModalBuilder(modal.Title, customId);
|
||||
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)
|
||||
switch (input)
|
||||
{
|
||||
case TextInputComponentInfo textComponent:
|
||||
{
|
||||
var boxedValue = textComponent.Getter(modal);
|
||||
var value = textComponent.TypeOverridesToString
|
||||
? boxedValue?.ToString()
|
||||
: boxedValue as string;
|
||||
var inputBuilder = new TextInputBuilder(textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null,
|
||||
textComponent.MaxLength, textComponent.IsRequired);
|
||||
|
||||
builder.AddTextInput(textComponent.Label, textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null,
|
||||
textComponent.MaxLength, textComponent.IsRequired, value);
|
||||
if (modalInstance != null)
|
||||
{
|
||||
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;
|
||||
default:
|
||||
@@ -86,13 +145,7 @@ namespace Discord.Interactions
|
||||
|
||||
modifyModal?.Invoke(builder);
|
||||
|
||||
return 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);
|
||||
await interaction.RespondWithModalAsync(builder.Build(), 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
|
||||
{
|
||||
/// <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>
|
||||
internal bool TypeOverridesToString => _typeOverridesToString.Value;
|
||||
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.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
@@ -36,24 +37,80 @@ namespace Discord.Interactions
|
||||
/// <summary>
|
||||
/// Gets a collection of the components of this modal.
|
||||
/// </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>
|
||||
/// Gets a collection of the text components of this modal.
|
||||
/// </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)
|
||||
{
|
||||
Title = builder.Title;
|
||||
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.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.")
|
||||
}).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;
|
||||
_initializer = builder.ModalInitializer;
|
||||
@@ -74,7 +131,7 @@ namespace Discord.Interactions
|
||||
|
||||
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);
|
||||
|
||||
if (component is null)
|
||||
@@ -107,12 +164,12 @@ namespace Discord.Interactions
|
||||
|
||||
services ??= EmptyServiceProvider.Instance;
|
||||
|
||||
var args = new object[Components.Count];
|
||||
var args = new object[InputComponents.Count];
|
||||
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);
|
||||
|
||||
if (component is null)
|
||||
|
||||
@@ -3,7 +3,6 @@ using Discord.Logging;
|
||||
using Discord.Rest;
|
||||
using Discord.WebSocket;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -98,6 +97,7 @@ namespace Discord.Interactions
|
||||
private readonly TypeMap<TypeConverter, IApplicationCommandInteractionDataOption> _typeConverterMap;
|
||||
private readonly TypeMap<ComponentTypeConverter, IComponentInteractionData> _compTypeConverterMap;
|
||||
private readonly TypeMap<TypeReader, string> _typeReaderMap;
|
||||
private readonly TypeMap<ModalComponentTypeConverter, IComponentInteractionData> _modalInputTypeConverterMap;
|
||||
private readonly ConcurrentDictionary<Type, IAutocompleteHandler> _autocompleteHandlers = new();
|
||||
private readonly ConcurrentDictionary<Type, ModalInfo> _modalInfos = new();
|
||||
private readonly SemaphoreSlim _lock;
|
||||
@@ -228,6 +228,16 @@ namespace Discord.Interactions
|
||||
[typeof(Enum)] = typeof(EnumReader<>),
|
||||
[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>
|
||||
@@ -1064,6 +1074,94 @@ namespace Discord.Interactions
|
||||
public bool TryRemoveGenericTypeReader(Type type, out Type 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>
|
||||
/// Serialize an object using a <see cref="TypeReader"/> into a <see cref="string"/> to be placed in a Component CustomId.
|
||||
/// </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