From 800a23430d64db59c45009e43e5fb1beee71565b Mon Sep 17 00:00:00 2001
From: Mihail Gribkov <61027276+Misha-133@users.noreply.github.com>
Date: Tue, 1 Jul 2025 22:11:27 +0300
Subject: [PATCH] [CV2] QoL & fixes (#3153)
---
.../Builders/ActionRowBuilder.cs | 20 ++-
.../Builders/ComponentBuilderV2.cs | 30 ++--
.../Builders/ComponentContainerExtensions.cs | 136 ++++++++++++++++++
.../Builders/ContainerBuilder.cs | 33 +++--
.../Builders/IComponentContainer.cs | 11 ++
.../Builders/SectionBuilder.cs | 29 ++--
6 files changed, 226 insertions(+), 33 deletions(-)
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ActionRowBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ActionRowBuilder.cs
index ad27476a..bf19c813 100644
--- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ActionRowBuilder.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ActionRowBuilder.cs
@@ -2,6 +2,7 @@ using Discord.Utils;
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
@@ -13,6 +14,18 @@ namespace Discord;
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class ActionRowBuilder : IMessageComponentBuilder, IInteractableComponentContainer
{
+ ///
+ public ImmutableArray SupportedComponentTypes { get; } =
+ [
+ ComponentType.Button,
+ ComponentType.SelectMenu,
+ ComponentType.UserSelect,
+ ComponentType.RoleSelect,
+ ComponentType.ChannelSelect,
+ ComponentType.MentionableSelect,
+ ComponentType.TextInput
+ ];
+
///
public ComponentType Type => ComponentType.ActionRow;
@@ -207,7 +220,10 @@ public class ActionRowBuilder : IMessageComponentBuilder, IInteractableComponent
{
Preconditions.AtLeast(Components.Count, 1, nameof(Components), "There must be at least 1 component in a row.");
Preconditions.AtMost(Components.Count, MaxChildCount, nameof(Components), $"Action row can only contain {MaxChildCount} child components!");
-
+
+ if (Components.Any(x => !SupportedComponentTypes.Contains(x.Type)))
+ throw new InvalidOperationException($"This component container only supports components of types: {string.Join(", ", SupportedComponentTypes)}");
+
return new ActionRowComponent(_components.Select(x => x.Build()).ToList(), Id);
}
IMessageComponent IMessageComponentBuilder.Build() => Build();
@@ -242,6 +258,8 @@ public class ActionRowBuilder : IMessageComponentBuilder, IInteractableComponent
///
IComponentContainer IComponentContainer.WithComponents(IEnumerable components) => WithComponents(components);
+ ///
+ int IComponentContainer.MaxChildCount => MaxChildCount;
private string DebuggerDisplay => $"{nameof(ActionRowBuilder)}: {this.ComponentCount()} child components.";
}
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilderV2.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilderV2.cs
index 66824e3a..20d1ab45 100644
--- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilderV2.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilderV2.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
@@ -9,10 +10,22 @@ namespace Discord;
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class ComponentBuilderV2 : IStaticComponentContainer
{
+ ///
+ public ImmutableArray SupportedComponentTypes { get; } =
+ [
+ ComponentType.ActionRow,
+ ComponentType.Section,
+ ComponentType.MediaGallery,
+ ComponentType.Separator,
+ ComponentType.Container,
+ ComponentType.File,
+ ComponentType.TextDisplay
+ ];
+
///
/// Gets the maximum number of components that can be added to a message.
///
- public const int MaxComponents = 40;
+ public const int MaxChildCount = 40;
private List _components = new();
@@ -74,21 +87,14 @@ public class ComponentBuilderV2 : IStaticComponentContainer
{
Preconditions.NotNull(Components, nameof(Components));
Preconditions.AtLeast(Components.Count, 1, nameof(Components.Count), "At least 1 component must be added to this container.");
- Preconditions.AtMost(this.ComponentCount(), MaxComponents, nameof(Components.Count), $"A message must contain {MaxComponents} components or less.");
+ Preconditions.AtMost(this.ComponentCount(), MaxChildCount, nameof(Components.Count), $"A message must contain {MaxChildCount} components or less.");
var ids = this.GetComponentIds().ToList();
if (ids.Count != ids.Distinct().Count())
throw new InvalidOperationException("Components must have unique ids.");
- if (_components.Any(x =>
- x is not ActionRowBuilder
- and not SectionBuilder
- and not TextDisplayBuilder
- and not MediaGalleryBuilder
- and not FileComponentBuilder
- and not SeparatorBuilder
- and not ContainerBuilder))
- throw new InvalidOperationException($"Only the following components can be at the top level: {nameof(ActionRowBuilder)}, {nameof(TextDisplayBuilder)}, {nameof(SectionBuilder)}, {nameof(MediaGalleryBuilder)}, {nameof(SeparatorBuilder)}, or {nameof(FileComponentBuilder)} components.");
+ if (Components.Any(x => !SupportedComponentTypes.Contains(x.Type)))
+ throw new InvalidOperationException($"This component container only supports components of types: {string.Join(", ", SupportedComponentTypes)}");
return new MessageComponent(Components.Select(x => x.Build()).ToList());
}
@@ -101,6 +107,8 @@ public class ComponentBuilderV2 : IStaticComponentContainer
///
IComponentContainer IComponentContainer.WithComponents(IEnumerable components) => WithComponents(components);
+ ///
+ int IComponentContainer.MaxChildCount => MaxChildCount;
private string DebuggerDisplay => $"{nameof(ComponentBuilderV2)}: {this.ComponentCount()} child components.";
}
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentContainerExtensions.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentContainerExtensions.cs
index a7c5c58f..5d57d396 100644
--- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentContainerExtensions.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentContainerExtensions.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Linq;
@@ -52,6 +53,21 @@ public static class ComponentContainerExtensions
.WithContent(content)
.WithId(id));
+ ///
+ /// Adds a to the container.
+ ///
+ ///
+ /// The current container.
+ ///
+ public static BuilderT WithTextDisplay(this BuilderT container,
+ Action options)
+ where BuilderT : class, IStaticComponentContainer
+ {
+ var comp = new TextDisplayBuilder();
+ options(comp);
+ return container.WithTextDisplay(comp);
+ }
+
///
/// Adds a to the container.
///
@@ -65,6 +81,21 @@ public static class ComponentContainerExtensions
return container;
}
+ ///
+ /// Adds a to the container.
+ ///
+ ///
+ /// The current container.
+ ///
+ public static BuilderT WithSection(this BuilderT container,
+ Action options)
+ where BuilderT : class, IStaticComponentContainer
+ {
+ var comp = new SectionBuilder();
+ options(comp);
+ return container.WithSection(comp);
+ }
+
///
/// Adds a to the container.
///
@@ -122,6 +153,21 @@ public static class ComponentContainerExtensions
.WithItems(urls.Select(x => new MediaGalleryItemProperties(new UnfurledMediaItemProperties(x))))
.WithId(id));
+ ///
+ /// Adds a to the container.
+ ///
+ ///
+ /// The current container.
+ ///
+ public static BuilderT WithMediaGallery(this BuilderT container,
+ Action options)
+ where BuilderT : class, IStaticComponentContainer
+ {
+ var comp = new MediaGalleryBuilder();
+ options(comp);
+ return container.WithMediaGallery(comp);
+ }
+
///
/// Adds a to the container.
///
@@ -151,6 +197,21 @@ public static class ComponentContainerExtensions
.WithIsDivider(isDivider)
.WithId(id));
+ ///
+ /// Adds a to the container.
+ ///
+ ///
+ /// The current container.
+ ///
+ public static BuilderT WithSeparator(this BuilderT container,
+ Action options)
+ where BuilderT : class, IStaticComponentContainer
+ {
+ var comp = new SeparatorBuilder();
+ options(comp);
+ return container.WithSeparator(comp);
+ }
+
///
/// Adds a to the container.
///
@@ -180,6 +241,21 @@ public static class ComponentContainerExtensions
.WithIsSpoiler(isSpoiler)
.WithId(id));
+ ///
+ /// Adds a to the container.
+ ///
+ ///
+ /// The current container.
+ ///
+ public static BuilderT WithFile(this BuilderT container,
+ Action options)
+ where BuilderT : class, IStaticComponentContainer
+ {
+ var comp = new FileComponentBuilder();
+ options(comp);
+ return container.WithFile(comp);
+ }
+
///
/// Adds a to the container.
///
@@ -223,6 +299,21 @@ public static class ComponentContainerExtensions
=> container.WithContainer(new ContainerBuilder()
.WithComponents(components));
+ ///
+ /// Adds a to the container.
+ ///
+ ///
+ /// The current container.
+ ///
+ public static BuilderT WithContainer(this BuilderT container,
+ Action options)
+ where BuilderT : class, IStaticComponentContainer
+ {
+ var comp = new ContainerBuilder();
+ options(comp);
+ return container.WithContainer(comp);
+ }
+
///
/// Adds a to the container.
///
@@ -262,6 +353,21 @@ public static class ComponentContainerExtensions
.WithSkuId(skuId)
.WithId(id));
+ ///
+ /// Adds a to the container.
+ ///
+ ///
+ /// The current container.
+ ///
+ public static BuilderT WithButton(this BuilderT container,
+ Action options)
+ where BuilderT : class, IInteractableComponentContainer
+ {
+ var comp = new ButtonBuilder();
+ options(comp);
+ return container.WithButton(comp);
+ }
+
///
/// Adds a to the container.
///
@@ -306,6 +412,21 @@ public static class ComponentContainerExtensions
.WithDefaultValues(defaultValues)
.WithId(id));
+ ///
+ /// Adds a to the container.
+ ///
+ ///
+ /// The current container.
+ ///
+ public static BuilderT WithSelectMenu(this BuilderT container,
+ Action options)
+ where BuilderT : class, IInteractableComponentContainer
+ {
+ var comp = new SelectMenuBuilder();
+ options(comp);
+ return container.WithSelectMenu(comp);
+ }
+
///
/// Adds a to the container.
///
@@ -333,6 +454,21 @@ public static class ComponentContainerExtensions
.WithComponents(components)
.WithId(id));
+ ///
+ /// Adds a to the container.
+ ///
+ ///
+ /// The current container.
+ ///
+ public static BuilderT WithActionRow(this BuilderT container,
+ Action options)
+ where BuilderT : class, IStaticComponentContainer
+ {
+ var cont = new ActionRowBuilder();
+ options(cont);
+ return container.WithActionRow(cont);
+ }
+
///
/// Finds the first in the
/// or any of its child s with matching id.
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ContainerBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ContainerBuilder.cs
index 645316bb..c826966e 100644
--- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ContainerBuilder.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ContainerBuilder.cs
@@ -9,9 +9,25 @@ namespace Discord;
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class ContainerBuilder : IMessageComponentBuilder, IStaticComponentContainer
{
+ ///
+ public ImmutableArray SupportedComponentTypes { get; } =
+ [
+ ComponentType.ActionRow,
+ ComponentType.Section,
+ ComponentType.Button,
+ ComponentType.MediaGallery,
+ ComponentType.Separator,
+ ComponentType.File,
+ ComponentType.SelectMenu,
+ ComponentType.TextDisplay
+ ];
+
///
public ComponentType Type => ComponentType.Container;
+ ///
+ public const int MaxChildCount = 39;
+
///
public int? Id { get; set; }
@@ -46,7 +62,7 @@ public class ContainerBuilder : IMessageComponentBuilder, IStaticComponentContai
{
Components = components?.ToList();
}
-
+
///
/// Initializes a new from existing component.
///
@@ -107,14 +123,11 @@ public class ContainerBuilder : IMessageComponentBuilder, IStaticComponentContai
///
public ContainerComponent Build()
{
- if (_components.Any(x => x
- is not ActionRowBuilder
- and not TextDisplayBuilder
- and not SectionBuilder
- and not MediaGalleryBuilder
- and not SeparatorBuilder
- and not FileComponentBuilder))
- throw new InvalidOperationException($"A container can only contain {nameof(ActionRowBuilder)}, {nameof(TextDisplayBuilder)}, {nameof(SectionBuilder)}, {nameof(MediaGalleryBuilder)}, {nameof(SeparatorBuilder)}, or {nameof(FileComponentBuilder)} components.");
+ Preconditions.NotNull(Components, nameof(Components));
+ Preconditions.AtLeast(Components.Count, 1, nameof(Components.Count), "At least 1 component must be added to this container.");
+
+ if (Components.Any(x => !SupportedComponentTypes.Contains(x.Type)))
+ throw new InvalidOperationException($"This component container only supports components of types: {string.Join(", ", SupportedComponentTypes)}");
return new(Components.ConvertAll(x => x.Build()).ToImmutableArray(), AccentColor, IsSpoiler, Id);
}
@@ -127,6 +140,8 @@ public class ContainerBuilder : IMessageComponentBuilder, IStaticComponentContai
IComponentContainer IComponentContainer.AddComponents(params IMessageComponentBuilder[] components) => AddComponents(components);
///
IComponentContainer IComponentContainer.WithComponents(IEnumerable components) => WithComponents(components);
+ ///
+ int IComponentContainer.MaxChildCount => MaxChildCount;
private string DebuggerDisplay => $"{nameof(ContainerBuilder)}: {this.ComponentCount()} child components.";
}
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IComponentContainer.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IComponentContainer.cs
index 2c2734fb..58f0b080 100644
--- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IComponentContainer.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IComponentContainer.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Collections.Immutable;
namespace Discord;
@@ -7,6 +8,16 @@ namespace Discord;
///
public interface IComponentContainer
{
+ ///
+ /// Gets the types of child components supported by this container.
+ ///
+ ImmutableArray SupportedComponentTypes { get; }
+
+ ///
+ /// Gets the maximum number of components allowed in the container.
+ ///
+ int MaxChildCount { get; }
+
///
/// Gets the components in the container.
///
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SectionBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SectionBuilder.cs
index 1cd799ff..14df1877 100644
--- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SectionBuilder.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SectionBuilder.cs
@@ -10,10 +10,14 @@ namespace Discord;
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SectionBuilder : IMessageComponentBuilder, IStaticComponentContainer
{
- ///
- /// Gets the maximum number of components allowed in this container.
- ///
- public const int MaxComponents = 3;
+ ///
+ public ImmutableArray SupportedComponentTypes { get; } =
+ [
+ ComponentType.TextDisplay
+ ];
+
+ ///
+ public const int MaxChildCount = 3;
///
public ComponentType Type => ComponentType.Section;
@@ -45,7 +49,7 @@ public class SectionBuilder : IMessageComponentBuilder, IStaticComponentContaine
/// Initializes a new .
///
public SectionBuilder() { }
-
+
///
/// Initializes a new .
///
@@ -108,17 +112,17 @@ public class SectionBuilder : IMessageComponentBuilder, IStaticComponentContaine
///
public SectionComponent Build()
{
- if (_components.Count is 0 or > MaxComponents)
- throw new InvalidOperationException($"Section component can only contain {MaxComponents} child components!");
+ if (_components.Count is 0 or > MaxChildCount)
+ throw new InvalidOperationException($"Section component can only contain {MaxChildCount} child components.");
- if (_components.Any(x => x is not TextDisplayBuilder))
- throw new InvalidOperationException($"Section component can only contain {nameof(TextDisplayBuilder)}!");
+ if (Components.Any(x => !SupportedComponentTypes.Contains(x.Type)))
+ throw new InvalidOperationException($"This component container only supports components of types: {string.Join(", ", SupportedComponentTypes)}");
if (Accessory is null)
- throw new ArgumentNullException(nameof(Accessory), "A section must have an accessory");
+ throw new ArgumentNullException(nameof(Accessory), "A section must have an accessory.");
if (Accessory is not ButtonBuilder and not ThumbnailBuilder)
- throw new InvalidOperationException($"Accessory component can only be {nameof(ButtonBuilder)} or {nameof(ThumbnailBuilder)}!");
+ throw new InvalidOperationException($"Accessory component can only be {nameof(ButtonBuilder)} or {nameof(ThumbnailBuilder)}.");
return new(Id, Components.Select(x => x.Build()).ToImmutableArray(), Accessory?.Build());
}
@@ -131,6 +135,7 @@ public class SectionBuilder : IMessageComponentBuilder, IStaticComponentContaine
IComponentContainer IComponentContainer.AddComponents(params IMessageComponentBuilder[] components) => AddComponents(components);
///
IComponentContainer IComponentContainer.WithComponents(IEnumerable components) => WithComponents(components.ToList());
-
+ ///
+ int IComponentContainer.MaxChildCount => MaxChildCount;
private string DebuggerDisplay => $"{nameof(SectionBuilder)}: {this.ComponentCount()} child components.";
}