[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>
|
||||
/// Specify the target channel types for a <see cref="ApplicationCommandOptionType.Channel"/> option.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
|
||||
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||
public sealed class ChannelTypesAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -12,5 +12,9 @@ public class ModalChannelSelectAttribute : ModalSelectComponentAttribute
|
||||
/// 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) { }
|
||||
/// <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>
|
||||
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="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)
|
||||
/// <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;
|
||||
MaxValues = maxValues;
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace Discord.Interactions
|
||||
/// Create a new <see cref="ModalInputAttribute"/>.
|
||||
/// </summary>
|
||||
/// <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;
|
||||
}
|
||||
|
||||
@@ -14,5 +14,7 @@ public class ModalMentionableSelectAttribute : ModalSelectComponentAttribute
|
||||
/// <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) { }
|
||||
/// <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="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) { }
|
||||
/// <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>
|
||||
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;
|
||||
MaxValues = maxValues;
|
||||
|
||||
@@ -14,5 +14,7 @@ public sealed class ModalSelectMenuAttribute : ModalSelectComponentAttribute
|
||||
/// <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) { }
|
||||
/// <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"/>.
|
||||
/// </summary>
|
||||
/// <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;
|
||||
}
|
||||
|
||||
@@ -11,27 +11,27 @@ namespace Discord.Interactions
|
||||
/// <summary>
|
||||
/// Gets the style of the text input.
|
||||
/// </summary>
|
||||
public TextInputStyle Style { get; }
|
||||
public TextInputStyle Style { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the placeholder of the text input.
|
||||
/// </summary>
|
||||
public string Placeholder { get; }
|
||||
public string Placeholder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the minimum length of the text input.
|
||||
/// </summary>
|
||||
public int MinLength { get; }
|
||||
public int MinLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum length of the text input.
|
||||
/// </summary>
|
||||
public int MaxLength { get; }
|
||||
public int MaxLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the initial value to be displayed by this input.
|
||||
/// </summary>
|
||||
public string InitialValue { get; }
|
||||
public string InitialValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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="maxLength">The maximum length of the text input's content.</param>
|
||||
/// <param name="initValue">The initial value to be displayed by this input.</param>
|
||||
public ModalTextInputAttribute(string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, int minLength = 1, int maxLength = 4000, string initValue = null)
|
||||
: base(customId)
|
||||
/// <param name="id">The optional identifier for the component.</param>
|
||||
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;
|
||||
Placeholder = placeholder;
|
||||
|
||||
@@ -14,5 +14,7 @@ public class ModalUserSelectAttribute : ModalSelectComponentAttribute
|
||||
/// <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) { }
|
||||
/// <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>
|
||||
public class ChannelSelectComponentBuilder : SnowflakeSelectComponentBuilder<ChannelSelectComponentInfo, ChannelSelectComponentBuilder>
|
||||
{
|
||||
private readonly List<ChannelType> _channelTypes = new();
|
||||
|
||||
protected override ChannelSelectComponentBuilder Instance => this;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the presented channel types for this Channel Select.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<ChannelType> ChannelTypes => _channelTypes.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="ChannelSelectComponentBuilder"/>.
|
||||
/// </summary>
|
||||
@@ -42,6 +49,19 @@ public class ChannelSelectComponentBuilder : SnowflakeSelectComponentBuilder<Cha
|
||||
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)
|
||||
=> new(this, modal);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,14 @@ public interface IModalComponentBuilder
|
||||
/// </summary>
|
||||
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>
|
||||
/// Gets a collection of the attributes of this component.
|
||||
/// </summary>
|
||||
@@ -62,4 +70,13 @@ public interface IModalComponentBuilder
|
||||
/// The builder instance.
|
||||
/// </returns>
|
||||
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/>
|
||||
public object DefaultValue { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyCollection<Attribute> Attributes => _attributes;
|
||||
|
||||
@@ -87,6 +90,19 @@ public abstract class ModalComponentBuilder<TInfo, TBuilder> : IModalComponentBu
|
||||
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);
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -97,4 +113,7 @@ public abstract class ModalComponentBuilder<TInfo, TBuilder> : IModalComponentBu
|
||||
|
||||
/// <inheritdoc/>
|
||||
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)
|
||||
{
|
||||
EnsurePubliclySettable(propertyInfo);
|
||||
|
||||
var attributes = propertyInfo.GetCustomAttributes();
|
||||
|
||||
builder.Label = propertyInfo.Name;
|
||||
@@ -673,6 +675,7 @@ namespace Discord.Interactions.Builders
|
||||
builder.MaxLength = textInput.MaxLength;
|
||||
builder.MinLength = textInput.MinLength;
|
||||
builder.InitialValue = textInput.InitialValue;
|
||||
builder.Id = textInput.Id;
|
||||
break;
|
||||
case RequiredInputAttribute requiredInput:
|
||||
builder.IsRequired = requiredInput.IsRequired;
|
||||
@@ -690,6 +693,8 @@ namespace Discord.Interactions.Builders
|
||||
|
||||
private static void BuildSelectMenuComponent(SelectMenuComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
|
||||
{
|
||||
EnsurePubliclySettable(propertyInfo);
|
||||
|
||||
var attributes = propertyInfo.GetCustomAttributes();
|
||||
|
||||
builder.Label = propertyInfo.Name;
|
||||
@@ -707,6 +712,7 @@ namespace Discord.Interactions.Builders
|
||||
builder.MinValues = selectMenuInput.MinValues;
|
||||
builder.MaxValues = selectMenuInput.MaxValues;
|
||||
builder.Placeholder = selectMenuInput.Placeholder;
|
||||
builder.Id = selectMenuInput.Id;
|
||||
break;
|
||||
case RequiredInputAttribute requiredInput:
|
||||
builder.IsRequired = requiredInput.IsRequired;
|
||||
@@ -742,6 +748,8 @@ namespace Discord.Interactions.Builders
|
||||
where TInfo : SnowflakeSelectComponentInfo
|
||||
where TBuilder : SnowflakeSelectComponentBuilder<TInfo, TBuilder>
|
||||
{
|
||||
EnsurePubliclySettable(propertyInfo);
|
||||
|
||||
var attributes = propertyInfo.GetCustomAttributes();
|
||||
|
||||
builder.Label = propertyInfo.Name;
|
||||
@@ -759,6 +767,7 @@ namespace Discord.Interactions.Builders
|
||||
builder.MinValues = selectInput.MinValues;
|
||||
builder.MaxValues = selectInput.MaxValues;
|
||||
builder.Placeholder = selectInput.Placeholder;
|
||||
builder.Id = selectInput.Id;
|
||||
break;
|
||||
case RequiredInputAttribute requiredInput:
|
||||
builder.IsRequired = requiredInput.IsRequired;
|
||||
@@ -767,6 +776,9 @@ namespace Discord.Interactions.Builders
|
||||
builder.Label = inputLabel.Label;
|
||||
builder.Description = inputLabel.Description;
|
||||
break;
|
||||
case ChannelTypesAttribute channelTypes when builder is ChannelSelectComponentBuilder channelSelectBuilder:
|
||||
channelSelectBuilder.WithChannelTypes(channelTypes.ChannelTypes);
|
||||
break;
|
||||
default:
|
||||
builder.WithAttributes(attribute);
|
||||
break;
|
||||
@@ -776,6 +788,8 @@ namespace Discord.Interactions.Builders
|
||||
|
||||
private static void BuildFileUploadComponent(FileUploadComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
|
||||
{
|
||||
EnsurePubliclySettable(propertyInfo);
|
||||
|
||||
var attributes = propertyInfo.GetCustomAttributes();
|
||||
|
||||
builder.Label = propertyInfo.Name;
|
||||
@@ -792,6 +806,7 @@ namespace Discord.Interactions.Builders
|
||||
builder.ComponentType = fileUploadInput.ComponentType;
|
||||
builder.MinValues = fileUploadInput.MinValues;
|
||||
builder.MaxValues = fileUploadInput.MaxValues;
|
||||
builder.Id = fileUploadInput.Id;
|
||||
break;
|
||||
case RequiredInputAttribute requiredInput:
|
||||
builder.IsRequired = requiredInput.IsRequired;
|
||||
@@ -822,6 +837,7 @@ namespace Discord.Interactions.Builders
|
||||
case ModalTextDisplayAttribute textDisplay:
|
||||
builder.ComponentType = textDisplay.ComponentType;
|
||||
builder.Content = textDisplay.Content;
|
||||
builder.Id = textDisplay.Id;
|
||||
break;
|
||||
default:
|
||||
builder.WithAttributes(attribute);
|
||||
@@ -882,9 +898,18 @@ namespace Discord.Interactions.Builders
|
||||
|
||||
private static bool IsValidModalComponentDefinition(PropertyInfo propertyInfo)
|
||||
{
|
||||
return propertyInfo.SetMethod?.IsPublic == true &&
|
||||
propertyInfo.SetMethod?.IsStatic == false &&
|
||||
propertyInfo.IsDefined(typeof(ModalComponentAttribute));
|
||||
return 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)
|
||||
|
||||
@@ -82,12 +82,10 @@ namespace Discord.Interactions
|
||||
case TextInputComponentInfo textComponent:
|
||||
{
|
||||
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)
|
||||
{
|
||||
await textComponent.TypeConverter.WriteAsync(inputBuilder, interaction, textComponent, textComponent.Getter(modalInstance));
|
||||
}
|
||||
var instanceValue = modalInstance is not null ? textComponent.Getter(modalInstance) : null;
|
||||
await textComponent.TypeConverter.WriteAsync(inputBuilder, interaction, textComponent, instanceValue);
|
||||
|
||||
var labelBuilder = new LabelBuilder(textComponent.Label, inputBuilder, textComponent.Description);
|
||||
builder.AddLabel(labelBuilder);
|
||||
@@ -95,12 +93,10 @@ namespace Discord.Interactions
|
||||
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);
|
||||
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)
|
||||
{
|
||||
await selectMenuComponent.TypeConverter.WriteAsync(inputBuilder, interaction, selectMenuComponent, selectMenuComponent.Getter(modalInstance));
|
||||
}
|
||||
var instanceValue = modalInstance is not null ? selectMenuComponent.Getter(modalInstance) : null;
|
||||
await selectMenuComponent.TypeConverter.WriteAsync(inputBuilder, interaction, selectMenuComponent, instanceValue);
|
||||
|
||||
var labelBuilder = new LabelBuilder(selectMenuComponent.Label, inputBuilder, selectMenuComponent.Description);
|
||||
builder.AddLabel(labelBuilder);
|
||||
@@ -108,12 +104,11 @@ namespace Discord.Interactions
|
||||
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);
|
||||
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)
|
||||
{
|
||||
await snowflakeSelectComponent.TypeConverter.WriteAsync(inputBuilder, interaction, snowflakeSelectComponent, snowflakeSelectComponent.Getter(modalInstance));
|
||||
}
|
||||
var instanceValue = modalInstance is not null ? snowflakeSelectComponent.Getter(modalInstance) : null;
|
||||
await snowflakeSelectComponent.TypeConverter.WriteAsync(inputBuilder, interaction, snowflakeSelectComponent, instanceValue);
|
||||
|
||||
var labelBuilder = new LabelBuilder(snowflakeSelectComponent.Label, inputBuilder, snowflakeSelectComponent.Description);
|
||||
builder.AddLabel(labelBuilder);
|
||||
@@ -121,23 +116,22 @@ namespace Discord.Interactions
|
||||
break;
|
||||
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)
|
||||
{
|
||||
await fileUploadComponent.TypeConverter.WriteAsync(inputBuilder, interaction, fileUploadComponent, fileUploadComponent.Getter(modalInstance));
|
||||
}
|
||||
var instanceValue = modalInstance is not null ? fileUploadComponent.Getter(modalInstance) : null;
|
||||
await fileUploadComponent.TypeConverter.WriteAsync(inputBuilder, interaction, fileUploadComponent, instanceValue);
|
||||
|
||||
var labelBuilder = new LabelBuilder(fileUploadComponent.Label, inputBuilder, fileUploadComponent.Description);
|
||||
builder.AddLabel(labelBuilder);
|
||||
}
|
||||
break;
|
||||
case TextDisplayComponentInfo textDisplayComponent:
|
||||
{
|
||||
var instanceValue = modalInstance is not null ? textDisplayComponent.Getter(modalInstance).ToString() : null;
|
||||
var content = instanceValue ?? (textDisplayComponent.DefaultValue as string) ?? textDisplayComponent.Content;
|
||||
var componentBuilder = new TextDisplayBuilder(content);
|
||||
builder.AddTextDisplay(componentBuilder);
|
||||
{
|
||||
var instanceValue = modalInstance is not null ? textDisplayComponent.Getter(modalInstance).ToString() : null;
|
||||
var content = instanceValue ?? (textDisplayComponent.DefaultValue as string) ?? textDisplayComponent.Content;
|
||||
|
||||
var componentBuilder = new TextDisplayBuilder(content, textDisplayComponent.Id);
|
||||
builder.AddTextDisplay(componentBuilder);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Discord.Interactions;
|
||||
|
||||
/// <summary>
|
||||
@@ -5,5 +8,14 @@ namespace Discord.Interactions;
|
||||
/// </summary>
|
||||
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>
|
||||
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>
|
||||
/// Gets a collection of the attributes of this command.
|
||||
/// </summary>
|
||||
@@ -51,6 +59,7 @@ public abstract class ModalComponentInfo
|
||||
Type = builder.Type;
|
||||
PropertyInfo = builder.PropertyInfo;
|
||||
DefaultValue = builder.DefaultValue;
|
||||
Id = builder.Id;
|
||||
Attributes = builder.Attributes.ToImmutableArray();
|
||||
|
||||
_getter = new(() => ReflectionUtils<object>.CreateLambdaPropertyGetter(Modal.Type, PropertyInfo));
|
||||
|
||||
@@ -1219,7 +1219,7 @@ namespace Discord.Interactions
|
||||
{
|
||||
var type = typeof(T);
|
||||
|
||||
if (_modalInfos.ContainsKey(type))
|
||||
if (ModalUtils.Contains(type))
|
||||
throw new InvalidOperationException($"Modal type {type.FullName} already exists.");
|
||||
|
||||
return ModalUtils.GetOrAdd(type, this);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Discord.Interactions.Utilities;
|
||||
using Discord.Utils;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
@@ -12,6 +14,7 @@ internal sealed class DefaultArrayModalComponentConverter<T> : ModalComponentTyp
|
||||
private readonly Type _underlyingType;
|
||||
private readonly TypeReader _typeReader;
|
||||
private readonly ImmutableArray<ChannelType> _channelTypes;
|
||||
private readonly ImmutableArray<EnumSelectMenuOption> _enumOptions;
|
||||
|
||||
public DefaultArrayModalComponentConverter(InteractionService interactionService)
|
||||
{
|
||||
@@ -56,13 +59,14 @@ internal sealed class DefaultArrayModalComponentConverter<T> : ModalComponentTyp
|
||||
=> [ChannelType.Forum],
|
||||
_ => []
|
||||
};
|
||||
|
||||
_enumOptions = _underlyingType!.IsEnum ? [..EnumUtils.BuildSelectMenuOptions(_underlyingType)] : ImmutableArray<EnumSelectMenuOption>.Empty;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -77,7 +81,7 @@ internal sealed class DefaultArrayModalComponentConverter<T> : ModalComponentTyp
|
||||
{
|
||||
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>();
|
||||
@@ -134,49 +138,44 @@ internal sealed class DefaultArrayModalComponentConverter<T> : ModalComponentTyp
|
||||
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)
|
||||
if (!_enumOptions.IsEmpty)
|
||||
{
|
||||
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
|
||||
var visibleOptions = _enumOptions.Where(x => !x.Predicate?.Invoke(interaction) ?? true);
|
||||
|
||||
var enumValues = value is IEnumerable valueArr ? valueArr.Cast<Enum>().ToArray() : null;
|
||||
|
||||
foreach (var option in visibleOptions)
|
||||
{
|
||||
var optionBuilder = new SelectMenuOptionBuilder(option.MenuOption);
|
||||
|
||||
if (enumValues is not null)
|
||||
optionBuilder.IsDefault = enumValues.Contains(option.Value);
|
||||
|
||||
selectMenu.AddOption(optionBuilder);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
selectMenu.DefaultValues = value switch
|
||||
{
|
||||
IEnumerable<IUser> defaultUsers => defaultUsers.Select(SelectMenuDefaultValue.FromUser).ToList(),
|
||||
IEnumerable<IRole> defaultRoles => defaultRoles.Select(SelectMenuDefaultValue.FromRole).ToList(),
|
||||
IEnumerable<IChannel> defaultChannels =>
|
||||
defaultChannels.Select(SelectMenuDefaultValue.FromChannel).ToList(),
|
||||
IEnumerable<IMentionable> defaultMentionables => defaultMentionables
|
||||
.Select(x =>
|
||||
{
|
||||
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
|
||||
{
|
||||
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;
|
||||
IUser user => SelectMenuDefaultValue.FromUser(user),
|
||||
IRole role => SelectMenuDefaultValue.FromRole(role),
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Mentionable select cannot be populated using an entity with type: {x.GetType().FullName}")
|
||||
};
|
||||
})
|
||||
.ToList(),
|
||||
_ => selectMenu.DefaultValues
|
||||
};
|
||||
|
||||
if (component.ComponentType == ComponentType.ChannelSelect && _channelTypes.Length > 0)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -16,7 +17,7 @@ internal abstract class DefaultSnowflakeModalComponentConverter<T> : ModalCompon
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -51,52 +52,47 @@ internal abstract class DefaultSnowflakeModalComponentConverter<T> : ModalCompon
|
||||
internal class DefaultAttachmentModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||
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))
|
||||
{
|
||||
return result;
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
var resolvedEntity = modalData.Attachments.FirstOrDefault(x => x.Id == id);
|
||||
|
||||
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>
|
||||
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))
|
||||
{
|
||||
return result;
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
var resolvedEntity = modalData.Members.UnionBy(modalData.Users, x => x.Id).FirstOrDefault(x => x.Id == id);
|
||||
|
||||
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)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.UserSelect)
|
||||
if (builder is not SelectMenuBuilder { Type: ComponentType.UserSelect } selectMenu)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
if(value is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
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));
|
||||
@@ -120,31 +121,26 @@ internal class DefaultUserModalComponentConverter<T> : DefaultSnowflakeModalComp
|
||||
internal class DefaultRoleModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||
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))
|
||||
{
|
||||
return result;
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
var resolvedEntity = modalData.Roles.FirstOrDefault(x => x.Id == id);
|
||||
|
||||
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)
|
||||
{
|
||||
if(value is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.RoleSelect)
|
||||
if (builder is not SelectMenuBuilder { Type: ComponentType.RoleSelect } selectMenu)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
if(value is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
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));
|
||||
@@ -168,43 +169,77 @@ internal class DefaultRoleModalComponentConverter<T> : DefaultSnowflakeModalComp
|
||||
internal class DefaultChannelModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||
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))
|
||||
{
|
||||
return result;
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
var resolvedEntity = modalData.Channels.FirstOrDefault(x => x.Id == id);
|
||||
|
||||
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)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.ChannelSelect)
|
||||
if (builder is not SelectMenuBuilder { Type: ComponentType.ChannelSelect } selectMenu)
|
||||
{
|
||||
throw new InvalidOperationException($"{typeof(DefaultChannelModalComponentConverter<T>).Name} can only be used with Channel Select components.");
|
||||
}
|
||||
|
||||
selectMenu.WithChannelTypes(_channelTypes.ToList());
|
||||
|
||||
if(selectMenu.MaxValues > 1)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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));
|
||||
@@ -216,11 +251,11 @@ internal class DefaultChannelModalComponentConverter<T> : DefaultSnowflakeModalC
|
||||
internal class DefaultMentionableModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||
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))
|
||||
{
|
||||
return result;
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.MentionableSelect)
|
||||
if (builder is not SelectMenuBuilder { Type: ComponentType.MentionableSelect } selectMenu)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
if (value is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var defaultValue = value switch
|
||||
{
|
||||
IRole role => SelectMenuDefaultValue.FromRole(role),
|
||||
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);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Discord.Interactions.Utilities;
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
@@ -9,41 +10,18 @@ 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;
|
||||
private readonly ImmutableArray<EnumSelectMenuOption> _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();
|
||||
_options = EnumUtils.BuildSelectMenuOptions(typeof(T)).ToImmutableArray();
|
||||
}
|
||||
|
||||
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}"));
|
||||
|
||||
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);
|
||||
|
||||
if (value is T enumValue)
|
||||
foreach (var option in visibleOptions)
|
||||
{
|
||||
foreach(var option in visibleOptions)
|
||||
{
|
||||
option.OptionBuilder.IsDefault = _isFlags ? enumValue.HasFlag(option.Value) : enumValue.Equals(option.Value);
|
||||
}
|
||||
}
|
||||
var optionBuilder = new SelectMenuOptionBuilder(option.MenuOption);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
=> TryRemove(typeof(T), out modalInfo);
|
||||
|
||||
public static bool Contains(Type type)
|
||||
=> _modalInfos.ContainsKey(type);
|
||||
|
||||
public static void Clear() => _modalInfos.Clear();
|
||||
|
||||
public static int Count() => _modalInfos.Count;
|
||||
|
||||
Reference in New Issue
Block a user