From 9c1db3f0f05fa2ca2cac8df712b02f3ff97fc06c Mon Sep 17 00:00:00 2001
From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com>
Date: Thu, 22 Jan 2026 15:50:27 +0100
Subject: [PATCH] [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>
---
.../Attributes/ChannelTypesAttribute.cs | 2 +-
.../Modals/ModalChannelSelectAttribute.cs | 6 +-
.../Modals/ModalComponentAttribute.cs | 13 +-
.../Modals/ModalFileUploadAttribute.cs | 4 +-
.../Attributes/Modals/ModalInputAttribute.cs | 3 +-
.../Modals/ModalMentionableSelectAttribute.cs | 4 +-
.../Modals/ModalRoleSelectAttribute.cs | 4 +-
.../Modals/ModalSelectComponentAttribute.cs | 3 +-
.../Modals/ModalSelectMenuAttribute.cs | 4 +-
.../Modals/ModalTextDisplayAttribute.cs | 4 +-
.../Modals/ModalTextInputAttribute.cs | 15 +-
.../Modals/ModalUserSelectAttribute.cs | 4 +-
.../Modals/SelectMenuOptionAttribute.cs | 31 ++++
.../ChannelSelectComponentBuilder.cs | 20 +++
.../Components/IModalComponentBuilder.cs | 17 +++
.../Components/ModalComponentBuilder.cs | 19 +++
.../Builders/ModuleClassBuilder.cs | 31 +++-
.../IDiscordInteractionExtensions.cs | 44 +++---
.../Components/ChannelSelectComponentInfo.cs | 14 +-
.../Info/Components/ModalComponentInfo.cs | 9 ++
.../InteractionService.cs | 2 +-
.../DefaultArrayModalComponentConverter.cs | 83 ++++++-----
...DefaultSnowflakeModalComponentConverter.cs | 133 +++++++++++-------
.../EnumModalComponentConverter.cs | 72 ++--------
.../Utilities/EnumUtils.cs | 43 ++++++
.../Utilities/ModalUtils.cs | 3 +
26 files changed, 387 insertions(+), 200 deletions(-)
create mode 100644 src/Discord.Net.Interactions/Attributes/Modals/SelectMenuOptionAttribute.cs
create mode 100644 src/Discord.Net.Interactions/Utilities/EnumUtils.cs
diff --git a/src/Discord.Net.Interactions/Attributes/ChannelTypesAttribute.cs b/src/Discord.Net.Interactions/Attributes/ChannelTypesAttribute.cs
index 1f498e34..37015161 100644
--- a/src/Discord.Net.Interactions/Attributes/ChannelTypesAttribute.cs
+++ b/src/Discord.Net.Interactions/Attributes/ChannelTypesAttribute.cs
@@ -7,7 +7,7 @@ namespace Discord.Interactions
///
/// Specify the target channel types for a option.
///
- [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
+ [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class ChannelTypesAttribute : Attribute
{
///
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectAttribute.cs
index 5e445f23..4cf1a6b5 100644
--- a/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectAttribute.cs
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectAttribute.cs
@@ -12,5 +12,9 @@ public class ModalChannelSelectAttribute : ModalSelectComponentAttribute
/// Create a new .
///
/// Custom ID of the channel select component.
- public ModalChannelSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
+ /// The minimum number of values that can be selected.
+ /// The maximum number of values that can be selected.
+ /// Optional identifier for the component.
+ public ModalChannelSelectAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
+ : base(customId, minValues, maxValues, id) { }
}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs
index 7b6ce331..b8d67d6d 100644
--- a/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs
@@ -13,5 +13,16 @@ public abstract class ModalComponentAttribute : Attribute
///
public abstract ComponentType ComponentType { get; }
- internal ModalComponentAttribute() { }
+ ///
+ /// Gets the optional identifier for component.
+ ///
+ ///
+ /// Sending components with an id of 0 is allowed but will be treated as empty and replaced by the API.
+ ///
+ public int Id { get; set; }
+
+ internal ModalComponentAttribute(int id = 0)
+ {
+ Id = id;
+ }
}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadAttribute.cs
index 5271b64a..bb22c9e2 100644
--- a/src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadAttribute.cs
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadAttribute.cs
@@ -24,7 +24,9 @@ public class ModalFileUploadAttribute : ModalInputAttribute
/// Custom ID of the file upload component.
/// Minimum number of files that can be uploaded.
/// Maximum number of files that can be uploaded.
- public ModalFileUploadAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId)
+ /// The optional identifier for the component.
+ public ModalFileUploadAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
+ : base(customId, id)
{
MinValues = minValues;
MaxValues = maxValues;
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs
index 5829b363..5ba3a447 100644
--- a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs
@@ -17,7 +17,8 @@ namespace Discord.Interactions
/// Create a new .
///
/// The custom id of the input.
- internal ModalInputAttribute(string customId)
+ /// Optional identifier for component.
+ internal ModalInputAttribute(string customId, int id) : base(id)
{
CustomId = customId;
}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectAttribute.cs
index 57a0b1b9..f76a3345 100644
--- a/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectAttribute.cs
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectAttribute.cs
@@ -14,5 +14,7 @@ public class ModalMentionableSelectAttribute : ModalSelectComponentAttribute
/// Custom ID of the mentionable select component.
/// Minimum number of values that can be selected.
/// Maximum number of values that can be selected
- public ModalMentionableSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
+ /// The optional identifier for the component.
+ public ModalMentionableSelectAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
+ : base(customId, minValues, maxValues, id) { }
}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectAttribute.cs
index 3808be32..ca9b7b47 100644
--- a/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectAttribute.cs
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectAttribute.cs
@@ -14,5 +14,7 @@ public class ModalRoleSelectAttribute : ModalSelectComponentAttribute
/// Custom ID of the role select component.
/// Minimum number of values that can be selected.
/// Maximum number of values that can be selected.
- public ModalRoleSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
+ /// The optional identifier for the component.
+ public ModalRoleSelectAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
+ : base(customId, minValues, maxValues, id) { }
}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectComponentAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectComponentAttribute.cs
index ccbc5891..b7cb710f 100644
--- a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectComponentAttribute.cs
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectComponentAttribute.cs
@@ -20,7 +20,8 @@ public abstract class ModalSelectComponentAttribute : ModalInputAttribute
///
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;
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuAttribute.cs
index 5d70eb0c..3e3eca95 100644
--- a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuAttribute.cs
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuAttribute.cs
@@ -14,5 +14,7 @@ public sealed class ModalSelectMenuAttribute : ModalSelectComponentAttribute
/// Custom ID of the select menu component.
/// Minimum number of values that can be selected.
/// Maximum number of values that can be selected.
- public ModalSelectMenuAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
+ /// The optional identifier for the component.
+ public ModalSelectMenuAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
+ : base(customId, minValues, maxValues, id) { }
}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs
index 300db32b..eecb71ec 100644
--- a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs
@@ -17,7 +17,9 @@ public class ModalTextDisplayAttribute : ModalComponentAttribute
/// Create a new .
///
/// Content of the text display.
- public ModalTextDisplayAttribute(string content = null)
+ /// Optional identifier for component.
+ public ModalTextDisplayAttribute(string content = null, int id = 0)
+ : base(id)
{
Content = content;
}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs
index 4439e1d8..e9972b97 100644
--- a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs
@@ -11,27 +11,27 @@ namespace Discord.Interactions
///
/// Gets the style of the text input.
///
- public TextInputStyle Style { get; }
+ public TextInputStyle Style { get; set; }
///
/// Gets the placeholder of the text input.
///
- public string Placeholder { get; }
+ public string Placeholder { get; set; }
///
/// Gets the minimum length of the text input.
///
- public int MinLength { get; }
+ public int MinLength { get; set; }
///
/// Gets the maximum length of the text input.
///
- public int MaxLength { get; }
+ public int MaxLength { get; set; }
///
/// Gets the initial value to be displayed by this input.
///
- public string InitialValue { get; }
+ public string InitialValue { get; set; }
///
/// Create a new .
@@ -42,8 +42,9 @@ namespace Discord.Interactions
/// The minimum length of the text input's content.
/// The maximum length of the text input's content.
/// The initial value to be displayed by this input.
- public ModalTextInputAttribute(string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, int minLength = 1, int maxLength = 4000, string initValue = null)
- : base(customId)
+ /// The optional identifier for the component.
+ 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;
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectAttribute.cs
index cc3cf288..630e9b1b 100644
--- a/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectAttribute.cs
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectAttribute.cs
@@ -14,5 +14,7 @@ public class ModalUserSelectAttribute : ModalSelectComponentAttribute
/// Custom ID of the user select component.
/// Minimum number of values that can be selected.
/// Maximum number of values that can be selected.
- public ModalUserSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
+ /// The optional identifier for the component.
+ public ModalUserSelectAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
+ : base(customId, minValues, maxValues, id) { }
}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/SelectMenuOptionAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/SelectMenuOptionAttribute.cs
new file mode 100644
index 00000000..c84e4c97
--- /dev/null
+++ b/src/Discord.Net.Interactions/Attributes/Modals/SelectMenuOptionAttribute.cs
@@ -0,0 +1,31 @@
+using System;
+
+namespace Discord.Interactions;
+
+///
+/// Adds additional metadata to enum fields that are used for select-menus.
+///
+///
+/// To manually add select menu options to modal components, use instead.
+///
+[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
+public class SelectMenuOptionAttribute : Attribute
+{
+ ///
+ /// Gets or sets the desription of the option.
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// Gets or sets whether the option is selected by default.
+ ///
+ public bool IsDefault { get; set; }
+
+ ///
+ /// Gets or sets the emote of the option.
+ ///
+ ///
+ /// Can be either an or an
+ ///
+ public string Emote { get; set; }
+}
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/ChannelSelectComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/ChannelSelectComponentBuilder.cs
index 01642458..338e7c72 100644
--- a/src/Discord.Net.Interactions/Builders/Modals/Components/ChannelSelectComponentBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/Modals/Components/ChannelSelectComponentBuilder.cs
@@ -8,8 +8,15 @@ namespace Discord.Interactions.Builders;
///
public class ChannelSelectComponentBuilder : SnowflakeSelectComponentBuilder
{
+ private readonly List _channelTypes = new();
+
protected override ChannelSelectComponentBuilder Instance => this;
+ ///
+ /// Gets the presented channel types for this Channel Select.
+ ///
+ public IReadOnlyCollection ChannelTypes => _channelTypes.AsReadOnly();
+
///
/// Initializes a new .
///
@@ -42,6 +49,19 @@ public class ChannelSelectComponentBuilder : SnowflakeSelectComponentBuilder
+ /// Sets the value of .
+ ///
+ /// the new value of .
+ ///
+ /// The builder instance.
+ ///
+ public ChannelSelectComponentBuilder WithChannelTypes(params IEnumerable channelTypes)
+ {
+ _channelTypes.AddRange(channelTypes);
+ return this;
+ }
+
internal override ChannelSelectComponentInfo Build(ModalInfo modal)
=> new(this, modal);
}
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/IModalComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/IModalComponentBuilder.cs
index 9d869e4c..e7f05261 100644
--- a/src/Discord.Net.Interactions/Builders/Modals/Components/IModalComponentBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/Modals/Components/IModalComponentBuilder.cs
@@ -31,6 +31,14 @@ public interface IModalComponentBuilder
///
object DefaultValue { get; }
+ ///
+ /// Gets the optional identifier for component.
+ ///
+ ///
+ /// Sending components with an id of 0 is allowed but will be treated as empty and replaced by the API.
+ ///
+ int Id { get; }
+
///
/// Gets a collection of the attributes of this component.
///
@@ -62,4 +70,13 @@ public interface IModalComponentBuilder
/// The builder instance.
///
IModalComponentBuilder WithAttributes(params Attribute[] attributes);
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ IModalComponentBuilder WithId(int id);
}
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/ModalComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/ModalComponentBuilder.cs
index f7c41b3f..70b7f706 100644
--- a/src/Discord.Net.Interactions/Builders/Modals/Components/ModalComponentBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/Modals/Components/ModalComponentBuilder.cs
@@ -26,6 +26,9 @@ public abstract class ModalComponentBuilder : IModalComponentBu
///
public object DefaultValue { get; set; }
+ ///
+ public int Id { get; set; }
+
///
public IReadOnlyCollection Attributes => _attributes;
@@ -87,6 +90,19 @@ public abstract class ModalComponentBuilder : IModalComponentBu
return Instance;
}
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ public virtual TBuilder WithId(int id)
+ {
+ Id = id;
+ return Instance;
+ }
+
internal abstract TInfo Build(ModalInfo modal);
///
@@ -97,4 +113,7 @@ public abstract class ModalComponentBuilder : IModalComponentBu
///
IModalComponentBuilder IModalComponentBuilder.WithAttributes(params Attribute[] attributes) => WithAttributes(attributes);
+
+ ///
+ IModalComponentBuilder IModalComponentBuilder.WithId(int id) => WithId(id);
}
diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
index c76552ef..f3567f5e 100644
--- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
@@ -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
{
+ 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)
diff --git a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs
index 56549da8..cb8babd8 100644
--- a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs
+++ b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs
@@ -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:
diff --git a/src/Discord.Net.Interactions/Info/Components/ChannelSelectComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/ChannelSelectComponentInfo.cs
index 6161a3c8..0a218a67 100644
--- a/src/Discord.Net.Interactions/Info/Components/ChannelSelectComponentInfo.cs
+++ b/src/Discord.Net.Interactions/Info/Components/ChannelSelectComponentInfo.cs
@@ -1,3 +1,6 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
+
namespace Discord.Interactions;
///
@@ -5,5 +8,14 @@ namespace Discord.Interactions;
///
public class ChannelSelectComponentInfo : SnowflakeSelectComponentInfo
{
- internal ChannelSelectComponentInfo(Builders.ChannelSelectComponentBuilder builder, ModalInfo modal) : base(builder, modal) { }
+ ///
+ /// Gets the presented channel types for this Channel Select.
+ ///
+ public IReadOnlyCollection ChannelTypes { get; }
+
+ internal ChannelSelectComponentInfo(Builders.ChannelSelectComponentBuilder builder, ModalInfo modal)
+ : base(builder, modal)
+ {
+ ChannelTypes = builder.ChannelTypes.ToImmutableArray();
+ }
}
diff --git a/src/Discord.Net.Interactions/Info/Components/ModalComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/ModalComponentInfo.cs
index b9245589..782821c9 100644
--- a/src/Discord.Net.Interactions/Info/Components/ModalComponentInfo.cs
+++ b/src/Discord.Net.Interactions/Info/Components/ModalComponentInfo.cs
@@ -39,6 +39,14 @@ public abstract class ModalComponentInfo
///
public object DefaultValue { get; }
+ ///
+ /// Gets the optional identifier for component.
+ ///
+ ///
+ /// Sending components with an id of 0 is allowed but will be treated as empty and replaced by the API.
+ ///
+ public int Id { get; }
+
///
/// Gets a collection of the attributes of this command.
///
@@ -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