[Fix] Modal Write invocation without instance and missing ChannelTypes (#3221)
* add channel types to channel select builder and info * add channelTypes to select builder from IModal * remove public setter requirement from modal component definition and add guard clause to inputs * refactor modal building to run typeConverter writes even without modal instance * add inline docs to channelTypes props and method * add property as a target for ChannelTypesAttribute * move enum option building logic out of enum typeConverter * add channel type constraint mapping to channel single-select typeConverter * move SelectMenuOptionAttribute to its own file * add null forgiving operator to channel type mapping * remove list initialization from enum modal typeConverter * disallow channel default value assignment to mentionable selects * add id property to modal components * add component id assignment from attributes * update component attribute ctor signatures and inline docs * Update src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs Co-authored-by: Mihail Gribkov <61027276+Misha-133@users.noreply.github.com> * replace default values of component ids with 0 --------- Co-authored-by: Mihail Gribkov <61027276+Misha-133@users.noreply.github.com>
This commit is contained in:
@@ -7,7 +7,7 @@ namespace Discord.Interactions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Specify the target channel types for a <see cref="ApplicationCommandOptionType.Channel"/> option.
|
/// Specify the target channel types for a <see cref="ApplicationCommandOptionType.Channel"/> option.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
|
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||||
public sealed class ChannelTypesAttribute : Attribute
|
public sealed class ChannelTypesAttribute : Attribute
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -12,5 +12,9 @@ public class ModalChannelSelectAttribute : ModalSelectComponentAttribute
|
|||||||
/// Create a new <see cref="ModalChannelSelectAttribute"/>.
|
/// Create a new <see cref="ModalChannelSelectAttribute"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="customId">Custom ID of the channel select component.</param>
|
/// <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) { }
|
/// <param name="minValues">The minimum number of values that can be selected.</param>
|
||||||
|
/// <param name="maxValues">The maximum number of values that can be selected.</param>
|
||||||
|
/// <param name="id">Optional identifier for the component.</param>
|
||||||
|
public ModalChannelSelectAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
|
||||||
|
: base(customId, minValues, maxValues, id) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,16 @@ public abstract class ModalComponentAttribute : Attribute
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract ComponentType ComponentType { get; }
|
public abstract ComponentType ComponentType { get; }
|
||||||
|
|
||||||
internal ModalComponentAttribute() { }
|
/// <summary>
|
||||||
|
/// Gets the optional identifier for component.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Sending components with an id of 0 is allowed but will be treated as empty and replaced by the API.
|
||||||
|
/// </remarks>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
internal ModalComponentAttribute(int id = 0)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ public class ModalFileUploadAttribute : ModalInputAttribute
|
|||||||
/// <param name="customId">Custom ID of the file upload component.</param>
|
/// <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="minValues">Minimum number of files that can be uploaded.</param>
|
||||||
/// <param name="maxValues">Maximum 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)
|
/// <param name="id">The optional identifier for the component.</param>
|
||||||
|
public ModalFileUploadAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
|
||||||
|
: base(customId, id)
|
||||||
{
|
{
|
||||||
MinValues = minValues;
|
MinValues = minValues;
|
||||||
MaxValues = maxValues;
|
MaxValues = maxValues;
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ namespace Discord.Interactions
|
|||||||
/// Create a new <see cref="ModalInputAttribute"/>.
|
/// Create a new <see cref="ModalInputAttribute"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="customId">The custom id of the input.</param>
|
/// <param name="customId">The custom id of the input.</param>
|
||||||
internal ModalInputAttribute(string customId)
|
/// <param name="id">Optional identifier for component.</param>
|
||||||
|
internal ModalInputAttribute(string customId, int id) : base(id)
|
||||||
{
|
{
|
||||||
CustomId = customId;
|
CustomId = customId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,7 @@ public class ModalMentionableSelectAttribute : ModalSelectComponentAttribute
|
|||||||
/// <param name="customId">Custom ID of the mentionable select component.</param>
|
/// <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="minValues">Minimum number of values that can be selected.</param>
|
||||||
/// <param name="maxValues">Maximum 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) { }
|
/// <param name="id">The optional identifier for the component.</param>
|
||||||
|
public ModalMentionableSelectAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
|
||||||
|
: base(customId, minValues, maxValues, id) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,7 @@ public class ModalRoleSelectAttribute : ModalSelectComponentAttribute
|
|||||||
/// <param name="customId">Custom ID of the role select component.</param>
|
/// <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="minValues">Minimum number of values that can be selected.</param>
|
||||||
/// <param name="maxValues">Maximum 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) { }
|
/// <param name="id">The optional identifier for the component.</param>
|
||||||
|
public ModalRoleSelectAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
|
||||||
|
: base(customId, minValues, maxValues, id) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ public abstract class ModalSelectComponentAttribute : ModalInputAttribute
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Placeholder { get; set; }
|
public string Placeholder { get; set; }
|
||||||
|
|
||||||
internal ModalSelectComponentAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId)
|
internal ModalSelectComponentAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
|
||||||
|
: base(customId, id)
|
||||||
{
|
{
|
||||||
MinValues = minValues;
|
MinValues = minValues;
|
||||||
MaxValues = maxValues;
|
MaxValues = maxValues;
|
||||||
|
|||||||
@@ -14,5 +14,7 @@ public sealed class ModalSelectMenuAttribute : ModalSelectComponentAttribute
|
|||||||
/// <param name="customId">Custom ID of the select menu component.</param>
|
/// <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="minValues">Minimum number of values that can be selected.</param>
|
||||||
/// <param name="maxValues">Maximum 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) { }
|
/// <param name="id">The optional identifier for the component.</param>
|
||||||
|
public ModalSelectMenuAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
|
||||||
|
: base(customId, minValues, maxValues, id) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ public class ModalTextDisplayAttribute : ModalComponentAttribute
|
|||||||
/// Create a new <see cref="ModalTextInputAttribute"/>.
|
/// Create a new <see cref="ModalTextInputAttribute"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="content">Content of the text display.</param>
|
/// <param name="content">Content of the text display.</param>
|
||||||
public ModalTextDisplayAttribute(string content = null)
|
/// <param name="id">Optional identifier for component.</param>
|
||||||
|
public ModalTextDisplayAttribute(string content = null, int id = 0)
|
||||||
|
: base(id)
|
||||||
{
|
{
|
||||||
Content = content;
|
Content = content;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,27 +11,27 @@ namespace Discord.Interactions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the style of the text input.
|
/// Gets the style of the text input.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TextInputStyle Style { get; }
|
public TextInputStyle Style { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the placeholder of the text input.
|
/// Gets the placeholder of the text input.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Placeholder { get; }
|
public string Placeholder { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the minimum length of the text input.
|
/// Gets the minimum length of the text input.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int MinLength { get; }
|
public int MinLength { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the maximum length of the text input.
|
/// Gets the maximum length of the text input.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int MaxLength { get; }
|
public int MaxLength { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the initial value to be displayed by this input.
|
/// Gets the initial value to be displayed by this input.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string InitialValue { get; }
|
public string InitialValue { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new <see cref="ModalTextInputAttribute"/>.
|
/// Create a new <see cref="ModalTextInputAttribute"/>.
|
||||||
@@ -42,8 +42,9 @@ namespace Discord.Interactions
|
|||||||
/// <param name="minLength">The minimum length of the text input's content.</param>
|
/// <param name="minLength">The minimum length of the text input's content.</param>
|
||||||
/// <param name="maxLength">The maximum length of the text input's content.</param>
|
/// <param name="maxLength">The maximum length of the text input's content.</param>
|
||||||
/// <param name="initValue">The initial value to be displayed by this input.</param>
|
/// <param name="initValue">The initial value to be displayed by this input.</param>
|
||||||
public ModalTextInputAttribute(string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, int minLength = 1, int maxLength = 4000, string initValue = null)
|
/// <param name="id">The optional identifier for the component.</param>
|
||||||
: base(customId)
|
public ModalTextInputAttribute(string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, int minLength = 1, int maxLength = 4000, string initValue = null, int id = 0)
|
||||||
|
: base(customId, id)
|
||||||
{
|
{
|
||||||
Style = style;
|
Style = style;
|
||||||
Placeholder = placeholder;
|
Placeholder = placeholder;
|
||||||
|
|||||||
@@ -14,5 +14,7 @@ public class ModalUserSelectAttribute : ModalSelectComponentAttribute
|
|||||||
/// <param name="customId">Custom ID of the user select component.</param>
|
/// <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="minValues">Minimum number of values that can be selected.</param>
|
||||||
/// <param name="maxValues">Maximum 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) { }
|
/// <param name="id">The optional identifier for the component.</param>
|
||||||
|
public ModalUserSelectAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
|
||||||
|
: base(customId, minValues, maxValues, id) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
/// <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; }
|
||||||
|
}
|
||||||
@@ -8,8 +8,15 @@ namespace Discord.Interactions.Builders;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ChannelSelectComponentBuilder : SnowflakeSelectComponentBuilder<ChannelSelectComponentInfo, ChannelSelectComponentBuilder>
|
public class ChannelSelectComponentBuilder : SnowflakeSelectComponentBuilder<ChannelSelectComponentInfo, ChannelSelectComponentBuilder>
|
||||||
{
|
{
|
||||||
|
private readonly List<ChannelType> _channelTypes = new();
|
||||||
|
|
||||||
protected override ChannelSelectComponentBuilder Instance => this;
|
protected override ChannelSelectComponentBuilder Instance => this;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the presented channel types for this Channel Select.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<ChannelType> ChannelTypes => _channelTypes.AsReadOnly();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new <see cref="ChannelSelectComponentBuilder"/>.
|
/// Initializes a new <see cref="ChannelSelectComponentBuilder"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -42,6 +49,19 @@ public class ChannelSelectComponentBuilder : SnowflakeSelectComponentBuilder<Cha
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the value of <see cref="ChannelTypes"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="channelTypes">the new value of <see cref="ChannelTypes"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public ChannelSelectComponentBuilder WithChannelTypes(params IEnumerable<ChannelType> channelTypes)
|
||||||
|
{
|
||||||
|
_channelTypes.AddRange(channelTypes);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
internal override ChannelSelectComponentInfo Build(ModalInfo modal)
|
internal override ChannelSelectComponentInfo Build(ModalInfo modal)
|
||||||
=> new(this, modal);
|
=> new(this, modal);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ public interface IModalComponentBuilder
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
object DefaultValue { get; }
|
object DefaultValue { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the optional identifier for component.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Sending components with an id of 0 is allowed but will be treated as empty and replaced by the API.
|
||||||
|
/// </remarks>
|
||||||
|
int Id { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a collection of the attributes of this component.
|
/// Gets a collection of the attributes of this component.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -62,4 +70,13 @@ public interface IModalComponentBuilder
|
|||||||
/// The builder instance.
|
/// The builder instance.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
IModalComponentBuilder WithAttributes(params Attribute[] attributes);
|
IModalComponentBuilder WithAttributes(params Attribute[] attributes);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="Id"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">New value of the <see cref="Id"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
IModalComponentBuilder WithId(int id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ public abstract class ModalComponentBuilder<TInfo, TBuilder> : IModalComponentBu
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public object DefaultValue { get; set; }
|
public object DefaultValue { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IReadOnlyCollection<Attribute> Attributes => _attributes;
|
public IReadOnlyCollection<Attribute> Attributes => _attributes;
|
||||||
|
|
||||||
@@ -87,6 +90,19 @@ public abstract class ModalComponentBuilder<TInfo, TBuilder> : IModalComponentBu
|
|||||||
return Instance;
|
return Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="Id"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">New value of the <see cref="Id"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public virtual TBuilder WithId(int id)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
internal abstract TInfo Build(ModalInfo modal);
|
internal abstract TInfo Build(ModalInfo modal);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -97,4 +113,7 @@ public abstract class ModalComponentBuilder<TInfo, TBuilder> : IModalComponentBu
|
|||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
IModalComponentBuilder IModalComponentBuilder.WithAttributes(params Attribute[] attributes) => WithAttributes(attributes);
|
IModalComponentBuilder IModalComponentBuilder.WithAttributes(params Attribute[] attributes) => WithAttributes(attributes);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
IModalComponentBuilder IModalComponentBuilder.WithId(int id) => WithId(id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -654,6 +654,8 @@ namespace Discord.Interactions.Builders
|
|||||||
|
|
||||||
private static void BuildTextInputComponent(TextInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
|
private static void BuildTextInputComponent(TextInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
|
||||||
{
|
{
|
||||||
|
EnsurePubliclySettable(propertyInfo);
|
||||||
|
|
||||||
var attributes = propertyInfo.GetCustomAttributes();
|
var attributes = propertyInfo.GetCustomAttributes();
|
||||||
|
|
||||||
builder.Label = propertyInfo.Name;
|
builder.Label = propertyInfo.Name;
|
||||||
@@ -673,6 +675,7 @@ namespace Discord.Interactions.Builders
|
|||||||
builder.MaxLength = textInput.MaxLength;
|
builder.MaxLength = textInput.MaxLength;
|
||||||
builder.MinLength = textInput.MinLength;
|
builder.MinLength = textInput.MinLength;
|
||||||
builder.InitialValue = textInput.InitialValue;
|
builder.InitialValue = textInput.InitialValue;
|
||||||
|
builder.Id = textInput.Id;
|
||||||
break;
|
break;
|
||||||
case RequiredInputAttribute requiredInput:
|
case RequiredInputAttribute requiredInput:
|
||||||
builder.IsRequired = requiredInput.IsRequired;
|
builder.IsRequired = requiredInput.IsRequired;
|
||||||
@@ -690,6 +693,8 @@ namespace Discord.Interactions.Builders
|
|||||||
|
|
||||||
private static void BuildSelectMenuComponent(SelectMenuComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
|
private static void BuildSelectMenuComponent(SelectMenuComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
|
||||||
{
|
{
|
||||||
|
EnsurePubliclySettable(propertyInfo);
|
||||||
|
|
||||||
var attributes = propertyInfo.GetCustomAttributes();
|
var attributes = propertyInfo.GetCustomAttributes();
|
||||||
|
|
||||||
builder.Label = propertyInfo.Name;
|
builder.Label = propertyInfo.Name;
|
||||||
@@ -707,6 +712,7 @@ namespace Discord.Interactions.Builders
|
|||||||
builder.MinValues = selectMenuInput.MinValues;
|
builder.MinValues = selectMenuInput.MinValues;
|
||||||
builder.MaxValues = selectMenuInput.MaxValues;
|
builder.MaxValues = selectMenuInput.MaxValues;
|
||||||
builder.Placeholder = selectMenuInput.Placeholder;
|
builder.Placeholder = selectMenuInput.Placeholder;
|
||||||
|
builder.Id = selectMenuInput.Id;
|
||||||
break;
|
break;
|
||||||
case RequiredInputAttribute requiredInput:
|
case RequiredInputAttribute requiredInput:
|
||||||
builder.IsRequired = requiredInput.IsRequired;
|
builder.IsRequired = requiredInput.IsRequired;
|
||||||
@@ -742,6 +748,8 @@ namespace Discord.Interactions.Builders
|
|||||||
where TInfo : SnowflakeSelectComponentInfo
|
where TInfo : SnowflakeSelectComponentInfo
|
||||||
where TBuilder : SnowflakeSelectComponentBuilder<TInfo, TBuilder>
|
where TBuilder : SnowflakeSelectComponentBuilder<TInfo, TBuilder>
|
||||||
{
|
{
|
||||||
|
EnsurePubliclySettable(propertyInfo);
|
||||||
|
|
||||||
var attributes = propertyInfo.GetCustomAttributes();
|
var attributes = propertyInfo.GetCustomAttributes();
|
||||||
|
|
||||||
builder.Label = propertyInfo.Name;
|
builder.Label = propertyInfo.Name;
|
||||||
@@ -759,6 +767,7 @@ namespace Discord.Interactions.Builders
|
|||||||
builder.MinValues = selectInput.MinValues;
|
builder.MinValues = selectInput.MinValues;
|
||||||
builder.MaxValues = selectInput.MaxValues;
|
builder.MaxValues = selectInput.MaxValues;
|
||||||
builder.Placeholder = selectInput.Placeholder;
|
builder.Placeholder = selectInput.Placeholder;
|
||||||
|
builder.Id = selectInput.Id;
|
||||||
break;
|
break;
|
||||||
case RequiredInputAttribute requiredInput:
|
case RequiredInputAttribute requiredInput:
|
||||||
builder.IsRequired = requiredInput.IsRequired;
|
builder.IsRequired = requiredInput.IsRequired;
|
||||||
@@ -767,6 +776,9 @@ namespace Discord.Interactions.Builders
|
|||||||
builder.Label = inputLabel.Label;
|
builder.Label = inputLabel.Label;
|
||||||
builder.Description = inputLabel.Description;
|
builder.Description = inputLabel.Description;
|
||||||
break;
|
break;
|
||||||
|
case ChannelTypesAttribute channelTypes when builder is ChannelSelectComponentBuilder channelSelectBuilder:
|
||||||
|
channelSelectBuilder.WithChannelTypes(channelTypes.ChannelTypes);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
builder.WithAttributes(attribute);
|
builder.WithAttributes(attribute);
|
||||||
break;
|
break;
|
||||||
@@ -776,6 +788,8 @@ namespace Discord.Interactions.Builders
|
|||||||
|
|
||||||
private static void BuildFileUploadComponent(FileUploadComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
|
private static void BuildFileUploadComponent(FileUploadComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
|
||||||
{
|
{
|
||||||
|
EnsurePubliclySettable(propertyInfo);
|
||||||
|
|
||||||
var attributes = propertyInfo.GetCustomAttributes();
|
var attributes = propertyInfo.GetCustomAttributes();
|
||||||
|
|
||||||
builder.Label = propertyInfo.Name;
|
builder.Label = propertyInfo.Name;
|
||||||
@@ -792,6 +806,7 @@ namespace Discord.Interactions.Builders
|
|||||||
builder.ComponentType = fileUploadInput.ComponentType;
|
builder.ComponentType = fileUploadInput.ComponentType;
|
||||||
builder.MinValues = fileUploadInput.MinValues;
|
builder.MinValues = fileUploadInput.MinValues;
|
||||||
builder.MaxValues = fileUploadInput.MaxValues;
|
builder.MaxValues = fileUploadInput.MaxValues;
|
||||||
|
builder.Id = fileUploadInput.Id;
|
||||||
break;
|
break;
|
||||||
case RequiredInputAttribute requiredInput:
|
case RequiredInputAttribute requiredInput:
|
||||||
builder.IsRequired = requiredInput.IsRequired;
|
builder.IsRequired = requiredInput.IsRequired;
|
||||||
@@ -822,6 +837,7 @@ namespace Discord.Interactions.Builders
|
|||||||
case ModalTextDisplayAttribute textDisplay:
|
case ModalTextDisplayAttribute textDisplay:
|
||||||
builder.ComponentType = textDisplay.ComponentType;
|
builder.ComponentType = textDisplay.ComponentType;
|
||||||
builder.Content = textDisplay.Content;
|
builder.Content = textDisplay.Content;
|
||||||
|
builder.Id = textDisplay.Id;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
builder.WithAttributes(attribute);
|
builder.WithAttributes(attribute);
|
||||||
@@ -882,9 +898,18 @@ namespace Discord.Interactions.Builders
|
|||||||
|
|
||||||
private static bool IsValidModalComponentDefinition(PropertyInfo propertyInfo)
|
private static bool IsValidModalComponentDefinition(PropertyInfo propertyInfo)
|
||||||
{
|
{
|
||||||
return propertyInfo.SetMethod?.IsPublic == true &&
|
return propertyInfo.IsDefined(typeof(ModalComponentAttribute));
|
||||||
propertyInfo.SetMethod?.IsStatic == false &&
|
}
|
||||||
propertyInfo.IsDefined(typeof(ModalComponentAttribute));
|
|
||||||
|
private static bool IsPubliclySettable(PropertyInfo propertyInfo)
|
||||||
|
{
|
||||||
|
return propertyInfo.SetMethod is { IsPublic: true, IsStatic: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsurePubliclySettable(PropertyInfo propertyInfo)
|
||||||
|
{
|
||||||
|
if(!IsPubliclySettable(propertyInfo))
|
||||||
|
throw new InvalidOperationException($"The property {propertyInfo.Name} must be publicly settable.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ConstructorInfo GetComplexParameterConstructor(TypeInfo typeInfo, ComplexParameterAttribute complexParameter)
|
private static ConstructorInfo GetComplexParameterConstructor(TypeInfo typeInfo, ComplexParameterAttribute complexParameter)
|
||||||
|
|||||||
@@ -82,12 +82,10 @@ namespace Discord.Interactions
|
|||||||
case TextInputComponentInfo textComponent:
|
case TextInputComponentInfo textComponent:
|
||||||
{
|
{
|
||||||
var inputBuilder = new TextInputBuilder(textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null,
|
var inputBuilder = new TextInputBuilder(textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null,
|
||||||
textComponent.MaxLength, textComponent.IsRequired);
|
textComponent.MaxLength, textComponent.IsRequired, id: textComponent.Id);
|
||||||
|
|
||||||
if (modalInstance != null)
|
var instanceValue = modalInstance is not null ? textComponent.Getter(modalInstance) : null;
|
||||||
{
|
await textComponent.TypeConverter.WriteAsync(inputBuilder, interaction, textComponent, instanceValue);
|
||||||
await textComponent.TypeConverter.WriteAsync(inputBuilder, interaction, textComponent, textComponent.Getter(modalInstance));
|
|
||||||
}
|
|
||||||
|
|
||||||
var labelBuilder = new LabelBuilder(textComponent.Label, inputBuilder, textComponent.Description);
|
var labelBuilder = new LabelBuilder(textComponent.Label, inputBuilder, textComponent.Description);
|
||||||
builder.AddLabel(labelBuilder);
|
builder.AddLabel(labelBuilder);
|
||||||
@@ -95,12 +93,10 @@ namespace Discord.Interactions
|
|||||||
break;
|
break;
|
||||||
case SelectMenuComponentInfo selectMenuComponent:
|
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);
|
var inputBuilder = new SelectMenuBuilder(selectMenuComponent.CustomId, selectMenuComponent.Options.Select(x => new SelectMenuOptionBuilder(x)).ToList(), selectMenuComponent.Placeholder, selectMenuComponent.MaxValues, selectMenuComponent.MinValues, false, isRequired: selectMenuComponent.IsRequired, id: selectMenuComponent.Id);
|
||||||
|
|
||||||
if (modalInstance != null)
|
var instanceValue = modalInstance is not null ? selectMenuComponent.Getter(modalInstance) : null;
|
||||||
{
|
await selectMenuComponent.TypeConverter.WriteAsync(inputBuilder, interaction, selectMenuComponent, instanceValue);
|
||||||
await selectMenuComponent.TypeConverter.WriteAsync(inputBuilder, interaction, selectMenuComponent, selectMenuComponent.Getter(modalInstance));
|
|
||||||
}
|
|
||||||
|
|
||||||
var labelBuilder = new LabelBuilder(selectMenuComponent.Label, inputBuilder, selectMenuComponent.Description);
|
var labelBuilder = new LabelBuilder(selectMenuComponent.Label, inputBuilder, selectMenuComponent.Description);
|
||||||
builder.AddLabel(labelBuilder);
|
builder.AddLabel(labelBuilder);
|
||||||
@@ -108,12 +104,11 @@ namespace Discord.Interactions
|
|||||||
break;
|
break;
|
||||||
case SnowflakeSelectComponentInfo snowflakeSelectComponent:
|
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);
|
var channelTypes = snowflakeSelectComponent is ChannelSelectComponentInfo channelSelectComponent ? channelSelectComponent.ChannelTypes : null;
|
||||||
|
var inputBuilder = new SelectMenuBuilder(snowflakeSelectComponent.CustomId, null, snowflakeSelectComponent.Placeholder, snowflakeSelectComponent.MaxValues, snowflakeSelectComponent.MinValues, false, snowflakeSelectComponent.ComponentType, channelTypes?.ToList(), snowflakeSelectComponent.DefaultValues.ToList(), snowflakeSelectComponent.Id, snowflakeSelectComponent.IsRequired);
|
||||||
|
|
||||||
if (modalInstance != null)
|
var instanceValue = modalInstance is not null ? snowflakeSelectComponent.Getter(modalInstance) : null;
|
||||||
{
|
await snowflakeSelectComponent.TypeConverter.WriteAsync(inputBuilder, interaction, snowflakeSelectComponent, instanceValue);
|
||||||
await snowflakeSelectComponent.TypeConverter.WriteAsync(inputBuilder, interaction, snowflakeSelectComponent, snowflakeSelectComponent.Getter(modalInstance));
|
|
||||||
}
|
|
||||||
|
|
||||||
var labelBuilder = new LabelBuilder(snowflakeSelectComponent.Label, inputBuilder, snowflakeSelectComponent.Description);
|
var labelBuilder = new LabelBuilder(snowflakeSelectComponent.Label, inputBuilder, snowflakeSelectComponent.Description);
|
||||||
builder.AddLabel(labelBuilder);
|
builder.AddLabel(labelBuilder);
|
||||||
@@ -121,12 +116,10 @@ namespace Discord.Interactions
|
|||||||
break;
|
break;
|
||||||
case FileUploadComponentInfo fileUploadComponent:
|
case FileUploadComponentInfo fileUploadComponent:
|
||||||
{
|
{
|
||||||
var inputBuilder = new FileUploadComponentBuilder(fileUploadComponent.CustomId, fileUploadComponent.MinValues, fileUploadComponent.MaxValues, fileUploadComponent.IsRequired);
|
var inputBuilder = new FileUploadComponentBuilder(fileUploadComponent.CustomId, fileUploadComponent.MinValues, fileUploadComponent.MaxValues, fileUploadComponent.IsRequired, fileUploadComponent.Id);
|
||||||
|
|
||||||
if (modalInstance != null)
|
var instanceValue = modalInstance is not null ? fileUploadComponent.Getter(modalInstance) : null;
|
||||||
{
|
await fileUploadComponent.TypeConverter.WriteAsync(inputBuilder, interaction, fileUploadComponent, instanceValue);
|
||||||
await fileUploadComponent.TypeConverter.WriteAsync(inputBuilder, interaction, fileUploadComponent, fileUploadComponent.Getter(modalInstance));
|
|
||||||
}
|
|
||||||
|
|
||||||
var labelBuilder = new LabelBuilder(fileUploadComponent.Label, inputBuilder, fileUploadComponent.Description);
|
var labelBuilder = new LabelBuilder(fileUploadComponent.Label, inputBuilder, fileUploadComponent.Description);
|
||||||
builder.AddLabel(labelBuilder);
|
builder.AddLabel(labelBuilder);
|
||||||
@@ -136,7 +129,8 @@ namespace Discord.Interactions
|
|||||||
{
|
{
|
||||||
var instanceValue = modalInstance is not null ? textDisplayComponent.Getter(modalInstance).ToString() : null;
|
var instanceValue = modalInstance is not null ? textDisplayComponent.Getter(modalInstance).ToString() : null;
|
||||||
var content = instanceValue ?? (textDisplayComponent.DefaultValue as string) ?? textDisplayComponent.Content;
|
var content = instanceValue ?? (textDisplayComponent.DefaultValue as string) ?? textDisplayComponent.Content;
|
||||||
var componentBuilder = new TextDisplayBuilder(content);
|
|
||||||
|
var componentBuilder = new TextDisplayBuilder(content, textDisplayComponent.Id);
|
||||||
builder.AddTextDisplay(componentBuilder);
|
builder.AddTextDisplay(componentBuilder);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
namespace Discord.Interactions;
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -5,5 +8,14 @@ namespace Discord.Interactions;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ChannelSelectComponentInfo : SnowflakeSelectComponentInfo
|
public class ChannelSelectComponentInfo : SnowflakeSelectComponentInfo
|
||||||
{
|
{
|
||||||
internal ChannelSelectComponentInfo(Builders.ChannelSelectComponentBuilder builder, ModalInfo modal) : base(builder, modal) { }
|
/// <summary>
|
||||||
|
/// Gets the presented channel types for this Channel Select.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<ChannelType> ChannelTypes { get; }
|
||||||
|
|
||||||
|
internal ChannelSelectComponentInfo(Builders.ChannelSelectComponentBuilder builder, ModalInfo modal)
|
||||||
|
: base(builder, modal)
|
||||||
|
{
|
||||||
|
ChannelTypes = builder.ChannelTypes.ToImmutableArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ public abstract class ModalComponentInfo
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public object DefaultValue { get; }
|
public object DefaultValue { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the optional identifier for component.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Sending components with an id of 0 is allowed but will be treated as empty and replaced by the API.
|
||||||
|
/// </remarks>
|
||||||
|
public int Id { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a collection of the attributes of this command.
|
/// Gets a collection of the attributes of this command.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -51,6 +59,7 @@ public abstract class ModalComponentInfo
|
|||||||
Type = builder.Type;
|
Type = builder.Type;
|
||||||
PropertyInfo = builder.PropertyInfo;
|
PropertyInfo = builder.PropertyInfo;
|
||||||
DefaultValue = builder.DefaultValue;
|
DefaultValue = builder.DefaultValue;
|
||||||
|
Id = builder.Id;
|
||||||
Attributes = builder.Attributes.ToImmutableArray();
|
Attributes = builder.Attributes.ToImmutableArray();
|
||||||
|
|
||||||
_getter = new(() => ReflectionUtils<object>.CreateLambdaPropertyGetter(Modal.Type, PropertyInfo));
|
_getter = new(() => ReflectionUtils<object>.CreateLambdaPropertyGetter(Modal.Type, PropertyInfo));
|
||||||
|
|||||||
@@ -1219,7 +1219,7 @@ namespace Discord.Interactions
|
|||||||
{
|
{
|
||||||
var type = typeof(T);
|
var type = typeof(T);
|
||||||
|
|
||||||
if (_modalInfos.ContainsKey(type))
|
if (ModalUtils.Contains(type))
|
||||||
throw new InvalidOperationException($"Modal type {type.FullName} already exists.");
|
throw new InvalidOperationException($"Modal type {type.FullName} already exists.");
|
||||||
|
|
||||||
return ModalUtils.GetOrAdd(type, this);
|
return ModalUtils.GetOrAdd(type, this);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using Discord.Interactions.Utilities;
|
||||||
using Discord.Utils;
|
using Discord.Utils;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -12,6 +14,7 @@ internal sealed class DefaultArrayModalComponentConverter<T> : ModalComponentTyp
|
|||||||
private readonly Type _underlyingType;
|
private readonly Type _underlyingType;
|
||||||
private readonly TypeReader _typeReader;
|
private readonly TypeReader _typeReader;
|
||||||
private readonly ImmutableArray<ChannelType> _channelTypes;
|
private readonly ImmutableArray<ChannelType> _channelTypes;
|
||||||
|
private readonly ImmutableArray<EnumSelectMenuOption> _enumOptions;
|
||||||
|
|
||||||
public DefaultArrayModalComponentConverter(InteractionService interactionService)
|
public DefaultArrayModalComponentConverter(InteractionService interactionService)
|
||||||
{
|
{
|
||||||
@@ -56,13 +59,14 @@ internal sealed class DefaultArrayModalComponentConverter<T> : ModalComponentTyp
|
|||||||
=> [ChannelType.Forum],
|
=> [ChannelType.Forum],
|
||||||
_ => []
|
_ => []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_enumOptions = _underlyingType!.IsEnum ? [..EnumUtils.BuildSelectMenuOptions(_underlyingType)] : ImmutableArray<EnumSelectMenuOption>.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||||
{
|
{
|
||||||
var objs = new List<object>();
|
var objs = new List<object>();
|
||||||
|
|
||||||
|
|
||||||
if (_typeReader is not null && option.Values.Count > 0)
|
if (_typeReader is not null && option.Values.Count > 0)
|
||||||
foreach (var value in option.Values)
|
foreach (var value in option.Values)
|
||||||
{
|
{
|
||||||
@@ -77,7 +81,7 @@ internal sealed class DefaultArrayModalComponentConverter<T> : ModalComponentTyp
|
|||||||
{
|
{
|
||||||
if (!TryGetModalInteractionData(context, out var modalData))
|
if (!TryGetModalInteractionData(context, out var modalData))
|
||||||
{
|
{
|
||||||
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{typeof(IModalInteractionData).Name} cannot be accessed from the provided {typeof(IInteractionContext).Name} type.");
|
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{nameof(IModalInteractionData)} cannot be accessed from the provided {nameof(IInteractionContext)} type.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var resolvedSnowflakes = new Dictionary<ulong, ISnowflakeEntity>();
|
var resolvedSnowflakes = new Dictionary<ulong, ISnowflakeEntity>();
|
||||||
@@ -134,49 +138,44 @@ internal sealed class DefaultArrayModalComponentConverter<T> : ModalComponentTyp
|
|||||||
if (builder is not SelectMenuBuilder selectMenu || !component.ComponentType.IsSelectType())
|
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.");
|
throw new InvalidOperationException($"Component type of the input {component.CustomId} of modal {component.Modal.Type.FullName} must be a select type.");
|
||||||
|
|
||||||
switch (value)
|
if (!_enumOptions.IsEmpty)
|
||||||
{
|
{
|
||||||
case IUser user:
|
var visibleOptions = _enumOptions.Where(x => !x.Predicate?.Invoke(interaction) ?? true);
|
||||||
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromUser(user));
|
|
||||||
break;
|
var enumValues = value is IEnumerable valueArr ? valueArr.Cast<Enum>().ToArray() : null;
|
||||||
case IRole role:
|
|
||||||
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromRole(role));
|
foreach (var option in visibleOptions)
|
||||||
break;
|
|
||||||
case IChannel channel:
|
|
||||||
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromChannel(channel));
|
|
||||||
break;
|
|
||||||
case IMentionable mentionable:
|
|
||||||
selectMenu.WithDefaultValues(mentionable switch
|
|
||||||
{
|
{
|
||||||
IUser user => SelectMenuDefaultValue.FromUser(user),
|
var optionBuilder = new SelectMenuOptionBuilder(option.MenuOption);
|
||||||
IRole role => SelectMenuDefaultValue.FromRole(role),
|
|
||||||
IChannel channel => SelectMenuDefaultValue.FromChannel(channel),
|
if (enumValues is not null)
|
||||||
_ => throw new InvalidOperationException($"Mentionable select cannot be populated using an entity with type: {mentionable.GetType().FullName}")
|
optionBuilder.IsDefault = enumValues.Contains(option.Value);
|
||||||
});
|
|
||||||
break;
|
selectMenu.AddOption(optionBuilder);
|
||||||
case IEnumerable<IUser> defaultUsers:
|
}
|
||||||
selectMenu.DefaultValues = defaultUsers.Select(SelectMenuDefaultValue.FromUser).ToList();
|
|
||||||
break;
|
return Task.CompletedTask;
|
||||||
case IEnumerable<IRole> defaultRoles:
|
}
|
||||||
selectMenu.DefaultValues = defaultRoles.Select(SelectMenuDefaultValue.FromRole).ToList();
|
|
||||||
break;
|
selectMenu.DefaultValues = value switch
|
||||||
case IEnumerable<IChannel> defaultChannels:
|
{
|
||||||
selectMenu.DefaultValues = defaultChannels.Select(SelectMenuDefaultValue.FromChannel).ToList();
|
IEnumerable<IUser> defaultUsers => defaultUsers.Select(SelectMenuDefaultValue.FromUser).ToList(),
|
||||||
break;
|
IEnumerable<IRole> defaultRoles => defaultRoles.Select(SelectMenuDefaultValue.FromRole).ToList(),
|
||||||
case IEnumerable<IMentionable> defaultMentionables:
|
IEnumerable<IChannel> defaultChannels =>
|
||||||
selectMenu.DefaultValues = defaultMentionables.Where(x => x is IUser or IRole or IChannel)
|
defaultChannels.Select(SelectMenuDefaultValue.FromChannel).ToList(),
|
||||||
|
IEnumerable<IMentionable> defaultMentionables => defaultMentionables
|
||||||
.Select(x =>
|
.Select(x =>
|
||||||
{
|
{
|
||||||
return x switch
|
return x switch
|
||||||
{
|
{
|
||||||
IUser user => SelectMenuDefaultValue.FromUser(user),
|
IUser user => SelectMenuDefaultValue.FromUser(user),
|
||||||
IRole role => SelectMenuDefaultValue.FromRole(role),
|
IRole role => SelectMenuDefaultValue.FromRole(role),
|
||||||
IChannel channel => SelectMenuDefaultValue.FromChannel(channel),
|
_ => throw new InvalidOperationException(
|
||||||
_ => throw new InvalidOperationException($"Mentionable select cannot be populated using an entity with type: {x.GetType().FullName}")
|
$"Mentionable select cannot be populated using an entity with type: {x.GetType().FullName}")
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList(),
|
||||||
break;
|
_ => selectMenu.DefaultValues
|
||||||
};
|
};
|
||||||
|
|
||||||
if (component.ComponentType == ComponentType.ChannelSelect && _channelTypes.Length > 0)
|
if (component.ComponentType == ComponentType.ChannelSelect && _channelTypes.Length > 0)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ internal abstract class DefaultSnowflakeModalComponentConverter<T> : ModalCompon
|
|||||||
|
|
||||||
if (!TryGetModalInteractionData(context, out modalData))
|
if (!TryGetModalInteractionData(context, out modalData))
|
||||||
{
|
{
|
||||||
preemptiveResult = TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{typeof(IModalInteractionData).Name} cannot be accessed from the provided {typeof(IInteractionContext).Name} type.");
|
preemptiveResult = TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{nameof(IModalInteractionData)} cannot be accessed from the provided {nameof(IInteractionContext)} type.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,52 +52,47 @@ internal abstract class DefaultSnowflakeModalComponentConverter<T> : ModalCompon
|
|||||||
internal class DefaultAttachmentModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
internal class DefaultAttachmentModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||||
where T : class, IAttachment
|
where T : class, IAttachment
|
||||||
{
|
{
|
||||||
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||||
{
|
{
|
||||||
if (TryGetPreemptiveResult(context, option, ComponentType.FileUpload, out var result, out var modalData, out var id))
|
if (TryGetPreemptiveResult(context, option, ComponentType.FileUpload, out var result, out var modalData, out var id))
|
||||||
{
|
{
|
||||||
return result;
|
return Task.FromResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
var resolvedEntity = modalData.Attachments.FirstOrDefault(x => x.Id == id);
|
var resolvedEntity = modalData.Attachments.FirstOrDefault(x => x.Id == id);
|
||||||
|
|
||||||
if (resolvedEntity is null)
|
if (resolvedEntity is null)
|
||||||
{
|
{
|
||||||
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.");
|
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved."));
|
||||||
}
|
}
|
||||||
|
|
||||||
return TypeConverterResult.FromSuccess(resolvedEntity);
|
return Task.FromResult(TypeConverterResult.FromSuccess(resolvedEntity));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class DefaultUserModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
internal class DefaultUserModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||||
where T : class, IUser
|
where T : class, IUser
|
||||||
{
|
{
|
||||||
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||||
{
|
{
|
||||||
if (TryGetPreemptiveResult(context, option, ComponentType.UserSelect, out var result, out var modalData, out var id))
|
if (TryGetPreemptiveResult(context, option, ComponentType.UserSelect, out var result, out var modalData, out var id))
|
||||||
{
|
{
|
||||||
return result;
|
return Task.FromResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
var resolvedEntity = modalData.Members.UnionBy(modalData.Users, x => x.Id).FirstOrDefault(x => x.Id == id);
|
var resolvedEntity = modalData.Members.UnionBy(modalData.Users, x => x.Id).FirstOrDefault(x => x.Id == id);
|
||||||
|
|
||||||
if (resolvedEntity is null)
|
if (resolvedEntity is null)
|
||||||
{
|
{
|
||||||
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.");
|
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved."));
|
||||||
}
|
}
|
||||||
|
|
||||||
return TypeConverterResult.FromSuccess(resolvedEntity);
|
return Task.FromResult(TypeConverterResult.FromSuccess(resolvedEntity));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
||||||
{
|
{
|
||||||
if (value is null)
|
if (builder is not SelectMenuBuilder { Type: ComponentType.UserSelect } selectMenu)
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.UserSelect)
|
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"{typeof(DefaultUserModalComponentConverter<T>).Name} can only be used with User Select components.");
|
throw new InvalidOperationException($"{typeof(DefaultUserModalComponentConverter<T>).Name} can only be used with User Select components.");
|
||||||
}
|
}
|
||||||
@@ -106,9 +102,14 @@ internal class DefaultUserModalComponentConverter<T> : DefaultSnowflakeModalComp
|
|||||||
throw new InvalidOperationException($"Multi-select User Select cannot be used with a single {typeof(T).Name} entity.");
|
throw new InvalidOperationException($"Multi-select User Select cannot be used with a single {typeof(T).Name} entity.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(value is null)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
if (value is not IUser user)
|
if (value is not IUser user)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default User Select values. Expected {typeof(IUser).Name}");
|
throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default User Select values. Expected {nameof(IUser)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromUser(user));
|
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromUser(user));
|
||||||
@@ -120,31 +121,26 @@ internal class DefaultUserModalComponentConverter<T> : DefaultSnowflakeModalComp
|
|||||||
internal class DefaultRoleModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
internal class DefaultRoleModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||||
where T : class, IRole
|
where T : class, IRole
|
||||||
{
|
{
|
||||||
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||||
{
|
{
|
||||||
if (TryGetPreemptiveResult(context, option, ComponentType.RoleSelect, out var result, out var modalData, out var id))
|
if (TryGetPreemptiveResult(context, option, ComponentType.RoleSelect, out var result, out var modalData, out var id))
|
||||||
{
|
{
|
||||||
return result;
|
return Task.FromResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
var resolvedEntity = modalData.Roles.FirstOrDefault(x => x.Id == id);
|
var resolvedEntity = modalData.Roles.FirstOrDefault(x => x.Id == id);
|
||||||
|
|
||||||
if (resolvedEntity is null)
|
if (resolvedEntity is null)
|
||||||
{
|
{
|
||||||
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.");
|
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved."));
|
||||||
}
|
}
|
||||||
|
|
||||||
return TypeConverterResult.FromSuccess(resolvedEntity);
|
return Task.FromResult(TypeConverterResult.FromSuccess(resolvedEntity));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
||||||
{
|
{
|
||||||
if(value is null)
|
if (builder is not SelectMenuBuilder { Type: ComponentType.RoleSelect } selectMenu)
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.RoleSelect)
|
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"{typeof(DefaultRoleModalComponentConverter<T>).Name} can only be used with Role Select components.");
|
throw new InvalidOperationException($"{typeof(DefaultRoleModalComponentConverter<T>).Name} can only be used with Role Select components.");
|
||||||
}
|
}
|
||||||
@@ -154,9 +150,14 @@ internal class DefaultRoleModalComponentConverter<T> : DefaultSnowflakeModalComp
|
|||||||
throw new InvalidOperationException($"Multi-select Role Select cannot be used with a single {typeof(T).Name} entity.");
|
throw new InvalidOperationException($"Multi-select Role Select cannot be used with a single {typeof(T).Name} entity.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(value is null)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
if (value is not IRole role)
|
if (value is not IRole role)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Role Select values. Expected {typeof(IRole).Name}");
|
throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Role Select values. Expected {nameof(IRole)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromRole(role));
|
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromRole(role));
|
||||||
@@ -168,43 +169,77 @@ internal class DefaultRoleModalComponentConverter<T> : DefaultSnowflakeModalComp
|
|||||||
internal class DefaultChannelModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
internal class DefaultChannelModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||||
where T : class, IChannel
|
where T : class, IChannel
|
||||||
{
|
{
|
||||||
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
private readonly ImmutableArray<ChannelType> _channelTypes;
|
||||||
|
|
||||||
|
public DefaultChannelModalComponentConverter()
|
||||||
|
{
|
||||||
|
var type = typeof(T);
|
||||||
|
|
||||||
|
_channelTypes = true switch
|
||||||
|
{
|
||||||
|
_ when typeof(IStageChannel).IsAssignableFrom(type)
|
||||||
|
=> [ChannelType.Stage],
|
||||||
|
_ when typeof(IVoiceChannel).IsAssignableFrom(type)
|
||||||
|
=> [ChannelType.Voice],
|
||||||
|
_ when typeof(IDMChannel).IsAssignableFrom(type)
|
||||||
|
=> [ChannelType.DM],
|
||||||
|
_ when typeof(IGroupChannel).IsAssignableFrom(type)
|
||||||
|
=> [ChannelType.Group],
|
||||||
|
_ when typeof(ICategoryChannel).IsAssignableFrom(type)
|
||||||
|
=> [ChannelType.Category],
|
||||||
|
_ when typeof(INewsChannel).IsAssignableFrom(type)
|
||||||
|
=> [ChannelType.News],
|
||||||
|
_ when typeof(IThreadChannel).IsAssignableFrom(type)
|
||||||
|
=> [ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.NewsThread],
|
||||||
|
_ when typeof(ITextChannel).IsAssignableFrom(type)
|
||||||
|
=> [ChannelType.Text],
|
||||||
|
_ when typeof(IMediaChannel).IsAssignableFrom(type)
|
||||||
|
=> [ChannelType.Media],
|
||||||
|
_ when typeof(IForumChannel).IsAssignableFrom(type)
|
||||||
|
=> [ChannelType.Forum],
|
||||||
|
_ => []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||||
{
|
{
|
||||||
if (TryGetPreemptiveResult(context, option, ComponentType.ChannelSelect, out var result, out var modalData, out var id))
|
if (TryGetPreemptiveResult(context, option, ComponentType.ChannelSelect, out var result, out var modalData, out var id))
|
||||||
{
|
{
|
||||||
return result;
|
return Task.FromResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
var resolvedEntity = modalData.Channels.FirstOrDefault(x => x.Id == id);
|
var resolvedEntity = modalData.Channels.FirstOrDefault(x => x.Id == id);
|
||||||
|
|
||||||
if (resolvedEntity is null)
|
if (resolvedEntity is null)
|
||||||
{
|
{
|
||||||
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.");
|
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved."));
|
||||||
}
|
}
|
||||||
|
|
||||||
return TypeConverterResult.FromSuccess(resolvedEntity);
|
return Task.FromResult(TypeConverterResult.FromSuccess(resolvedEntity));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
||||||
{
|
{
|
||||||
if (value is null)
|
if (builder is not SelectMenuBuilder { Type: ComponentType.ChannelSelect } selectMenu)
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.ChannelSelect)
|
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"{typeof(DefaultChannelModalComponentConverter<T>).Name} can only be used with Channel Select components.");
|
throw new InvalidOperationException($"{typeof(DefaultChannelModalComponentConverter<T>).Name} can only be used with Channel Select components.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectMenu.WithChannelTypes(_channelTypes.ToList());
|
||||||
|
|
||||||
if(selectMenu.MaxValues > 1)
|
if(selectMenu.MaxValues > 1)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Multi-select Channel Select cannot be used with a single {typeof(T).Name} entity.");
|
throw new InvalidOperationException($"Multi-select Channel Select cannot be used with a single {typeof(T).Name} entity.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
if(value is not IChannel channel)
|
if(value is not IChannel channel)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Channel Select values. Expected {typeof(IChannel).Name}");
|
throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Channel Select values. Expected {nameof(IChannel)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromChannel(channel));
|
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromChannel(channel));
|
||||||
@@ -216,11 +251,11 @@ internal class DefaultChannelModalComponentConverter<T> : DefaultSnowflakeModalC
|
|||||||
internal class DefaultMentionableModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
internal class DefaultMentionableModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||||
where T : class, IMentionable
|
where T : class, IMentionable
|
||||||
{
|
{
|
||||||
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||||
{
|
{
|
||||||
if (TryGetPreemptiveResult(context, option, ComponentType.MentionableSelect, out var result, out var modalData, out var id))
|
if (TryGetPreemptiveResult(context, option, ComponentType.MentionableSelect, out var result, out var modalData, out var id))
|
||||||
{
|
{
|
||||||
return result;
|
return Task.FromResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
var resolvedMentionables = new Dictionary<ulong, IMentionable>();
|
var resolvedMentionables = new Dictionary<ulong, IMentionable>();
|
||||||
@@ -236,20 +271,15 @@ internal class DefaultMentionableModalComponentConverter<T> : DefaultSnowflakeMo
|
|||||||
|
|
||||||
if (resolvedMentionables.Count == 0 || !resolvedMentionables.TryGetValue(id, out var entity))
|
if (resolvedMentionables.Count == 0 || !resolvedMentionables.TryGetValue(id, out var entity))
|
||||||
{
|
{
|
||||||
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.");
|
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved."));
|
||||||
}
|
}
|
||||||
|
|
||||||
return TypeConverterResult.FromSuccess(entity);
|
return Task.FromResult(TypeConverterResult.FromSuccess(entity));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
||||||
{
|
{
|
||||||
if (value is null)
|
if (builder is not SelectMenuBuilder { Type: ComponentType.MentionableSelect } selectMenu)
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.MentionableSelect)
|
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"{typeof(DefaultMentionableModalComponentConverter<T>).Name} can only be used with Mentionable Select components.");
|
throw new InvalidOperationException($"{typeof(DefaultMentionableModalComponentConverter<T>).Name} can only be used with Mentionable Select components.");
|
||||||
}
|
}
|
||||||
@@ -259,11 +289,16 @@ internal class DefaultMentionableModalComponentConverter<T> : DefaultSnowflakeMo
|
|||||||
throw new InvalidOperationException($"Multi-select Mentionable Select cannot be used with a single {typeof(T).Name} entity.");
|
throw new InvalidOperationException($"Multi-select Mentionable Select cannot be used with a single {typeof(T).Name} entity.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
var defaultValue = value switch
|
var defaultValue = value switch
|
||||||
{
|
{
|
||||||
IRole role => SelectMenuDefaultValue.FromRole(role),
|
IRole role => SelectMenuDefaultValue.FromRole(role),
|
||||||
IUser user => SelectMenuDefaultValue.FromUser(user),
|
IUser user => SelectMenuDefaultValue.FromUser(user),
|
||||||
_ => throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Mentionable Select values. Expected {typeof(IUser).Name} or {typeof(IRole).Name}")
|
_ => throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Mentionable Select values. Expected {nameof(IUser)} or {nameof(IRole)}")
|
||||||
};
|
};
|
||||||
|
|
||||||
selectMenu.WithDefaultValues(defaultValue);
|
selectMenu.WithDefaultValues(defaultValue);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Discord.Interactions.Utilities;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -9,41 +10,18 @@ namespace Discord.Interactions;
|
|||||||
internal sealed class EnumModalComponentConverter<T> : ModalComponentTypeConverter<T>
|
internal sealed class EnumModalComponentConverter<T> : ModalComponentTypeConverter<T>
|
||||||
where T : struct, Enum
|
where T : struct, Enum
|
||||||
{
|
{
|
||||||
private record Option(SelectMenuOptionBuilder OptionBuilder, Predicate<IDiscordInteraction> Predicate, T Value);
|
|
||||||
|
|
||||||
private readonly bool _isFlags;
|
private readonly bool _isFlags;
|
||||||
private readonly ImmutableArray<Option> _options;
|
private readonly ImmutableArray<EnumSelectMenuOption> _options;
|
||||||
|
|
||||||
public EnumModalComponentConverter()
|
public EnumModalComponentConverter()
|
||||||
{
|
{
|
||||||
var names = Enum.GetNames(typeof(T));
|
|
||||||
var members = names.SelectMany(x => typeof(T).GetMember(x));
|
|
||||||
|
|
||||||
_isFlags = typeof(T).IsDefined(typeof(FlagsAttribute));
|
_isFlags = typeof(T).IsDefined(typeof(FlagsAttribute));
|
||||||
|
_options = EnumUtils.BuildSelectMenuOptions(typeof(T)).ToImmutableArray();
|
||||||
_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)
|
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||||
{
|
{
|
||||||
if (option.Type is not ComponentType.SelectMenu or ComponentType.TextInput)
|
if (option.Type is not ComponentType.SelectMenu and not ComponentType.TextInput)
|
||||||
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option.Type} input type cannot be converted to {typeof(T).FullName}"));
|
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option.Type} input type cannot be converted to {typeof(T).FullName}"));
|
||||||
|
|
||||||
var value = option.Type switch
|
var value = option.Type switch
|
||||||
@@ -69,44 +47,16 @@ internal sealed class EnumModalComponentConverter<T> : ModalComponentTypeConvert
|
|||||||
|
|
||||||
var visibleOptions = _options.Where(x => !x.Predicate?.Invoke(interaction) ?? true);
|
var visibleOptions = _options.Where(x => !x.Predicate?.Invoke(interaction) ?? true);
|
||||||
|
|
||||||
if (value is T enumValue)
|
foreach (var option in visibleOptions)
|
||||||
{
|
{
|
||||||
foreach(var option in visibleOptions)
|
var optionBuilder = new SelectMenuOptionBuilder(option.MenuOption);
|
||||||
{
|
|
||||||
option.OptionBuilder.IsDefault = _isFlags ? enumValue.HasFlag(option.Value) : enumValue.Equals(option.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectMenu.WithOptions([.. visibleOptions.Select(x => x.OptionBuilder)]);
|
if(value is T enumValue && option.Value is T optionValue)
|
||||||
|
optionBuilder.IsDefault = _isFlags ? enumValue.HasFlag(optionValue) : enumValue.Equals(option.Value);
|
||||||
|
|
||||||
|
selectMenu.AddOption(optionBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
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; }
|
|
||||||
}
|
|
||||||
|
|||||||
43
src/Discord.Net.Interactions/Utilities/EnumUtils.cs
Normal file
43
src/Discord.Net.Interactions/Utilities/EnumUtils.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Discord.Interactions.Utilities;
|
||||||
|
|
||||||
|
internal record EnumSelectMenuOption(
|
||||||
|
SelectMenuOption MenuOption,
|
||||||
|
Predicate<IDiscordInteraction> Predicate,
|
||||||
|
object Value);
|
||||||
|
|
||||||
|
internal class EnumUtils
|
||||||
|
{
|
||||||
|
public static IEnumerable<EnumSelectMenuOption> BuildSelectMenuOptions(Type enumType)
|
||||||
|
{
|
||||||
|
if(!enumType.IsEnum)
|
||||||
|
throw new ArgumentException($"Type {enumType} is not an enum");
|
||||||
|
|
||||||
|
var names = Enum.GetNames(enumType);
|
||||||
|
var members = names.SelectMany(x => enumType.GetMember(x));
|
||||||
|
|
||||||
|
foreach (var member in members)
|
||||||
|
{
|
||||||
|
var selectMenuOptionAttr = member.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 {member.DeclaringType.Name}.{member.Name} into an {nameof(Emote)} or an {nameof(Emoji)}");
|
||||||
|
|
||||||
|
var hideAttr = member.GetCustomAttribute<HideAttribute>();
|
||||||
|
Predicate<IDiscordInteraction> predicate = hideAttr != null ? hideAttr.Predicate : null;
|
||||||
|
|
||||||
|
var value = Enum.Parse(enumType, member.Name);
|
||||||
|
var optionBuilder = new SelectMenuOptionBuilder(member.GetCustomAttribute<ChoiceDisplayAttribute>()?.Name ?? member.Name,
|
||||||
|
member.Name, selectMenuOptionAttr?.Description, emote != null ? emote : emoji, selectMenuOptionAttr?.IsDefault);
|
||||||
|
|
||||||
|
yield return new EnumSelectMenuOption(optionBuilder.Build(), predicate, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,9 @@ namespace Discord.Interactions
|
|||||||
public static bool TryRemove<T>(out ModalInfo modalInfo) where T : class, IModal
|
public static bool TryRemove<T>(out ModalInfo modalInfo) where T : class, IModal
|
||||||
=> TryRemove(typeof(T), out modalInfo);
|
=> TryRemove(typeof(T), out modalInfo);
|
||||||
|
|
||||||
|
public static bool Contains(Type type)
|
||||||
|
=> _modalInfos.ContainsKey(type);
|
||||||
|
|
||||||
public static void Clear() => _modalInfos.Clear();
|
public static void Clear() => _modalInfos.Clear();
|
||||||
|
|
||||||
public static int Count() => _modalInfos.Count;
|
public static int Count() => _modalInfos.Count;
|
||||||
|
|||||||
Reference in New Issue
Block a user