diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ActionRowBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ActionRowBuilder.cs new file mode 100644 index 00000000..dbbe4e99 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ActionRowBuilder.cs @@ -0,0 +1,198 @@ +using Discord.Utils; +using System.Collections.Generic; +using System.Linq; +using System; + +namespace Discord; + +/// +/// Represents a class used to build Action rows. +/// +public class ActionRowBuilder +{ + /// + /// The max amount of child components this row can hold. + /// + public const int MaxChildCount = 5; + + /// + /// Gets or sets the components inside this row. + /// + /// cannot be null. + /// count exceeds . + public List Components + { + get => _components; + set + { + if (value == null) + throw new ArgumentNullException(nameof(value), $"{nameof(Components)} cannot be null."); + + _components = value.Count switch + { + 0 => throw new ArgumentOutOfRangeException(nameof(value), "There must be at least 1 component in a row."), + > MaxChildCount => throw new ArgumentOutOfRangeException(nameof(value), $"Action row can only contain {MaxChildCount} child components!"), + _ => value + }; + } + } + + private List _components = new List(); + + /// + /// Adds a list of components to the current row. + /// + /// The list of components to add. + /// + /// The current builder. + public ActionRowBuilder WithComponents(List components) + { + Components = components; + return this; + } + + /// + /// Adds a component at the end of the current row. + /// + /// The component to add. + /// Components count reached + /// The current builder. + public ActionRowBuilder AddComponent(IMessageComponent component) + { + if (Components.Count >= MaxChildCount) + throw new InvalidOperationException($"Components count reached {MaxChildCount}"); + + Components.Add(component); + return this; + } + + /// + /// Adds a to the . + /// + /// The custom id of the menu. + /// The options of the menu. + /// The placeholder of the menu. + /// The min values of the placeholder. + /// The max values of the placeholder. + /// Whether or not the menu is disabled. + /// The type of the select menu. + /// Menus valid channel types (only for ) + /// The current builder. + public ActionRowBuilder WithSelectMenu(string customId, List options = null, + string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, + ComponentType type = ComponentType.SelectMenu, ChannelType[] channelTypes = null) + { + return WithSelectMenu(new SelectMenuBuilder() + .WithCustomId(customId) + .WithOptions(options) + .WithPlaceholder(placeholder) + .WithMaxValues(maxValues) + .WithMinValues(minValues) + .WithDisabled(disabled) + .WithType(type) + .WithChannelTypes(channelTypes)); + } + + /// + /// Adds a to the . + /// + /// The menu to add. + /// A Select Menu cannot exist in a pre-occupied ActionRow. + /// The current builder. + public ActionRowBuilder WithSelectMenu(SelectMenuBuilder menu) + { + if (menu.Options is not null && menu.Options.Distinct().Count() != menu.Options.Count) + throw new InvalidOperationException("Please make sure that there is no duplicates values."); + + var builtMenu = menu.Build(); + + if (Components.Count != 0) + throw new InvalidOperationException($"A Select Menu cannot exist in a pre-occupied ActionRow."); + + AddComponent(builtMenu); + + return this; + } + + /// + /// Adds a with specified parameters to the . + /// + /// The label text for the newly added button. + /// The style of this newly added button. + /// A to be used with this button. + /// The custom id of the newly added button. + /// A URL to be used only if the is a Link. + /// Whether or not the newly created button is disabled. + /// The current builder. + public ActionRowBuilder WithButton( + string label = null, + string customId = null, + ButtonStyle style = ButtonStyle.Primary, + IEmote emote = null, + string url = null, + bool disabled = false) + { + var button = new ButtonBuilder() + .WithLabel(label) + .WithStyle(style) + .WithEmote(emote) + .WithCustomId(customId) + .WithUrl(url) + .WithDisabled(disabled); + + return WithButton(button); + } + + /// + /// Adds a to the . + /// + /// The button to add. + /// Components count reached . + /// A button cannot be added to a row with a SelectMenu. + /// The current builder. + public ActionRowBuilder WithButton(ButtonBuilder button) + { + var builtButton = button.Build(); + + if (Components.Count >= 5) + throw new InvalidOperationException($"Components count reached {MaxChildCount}"); + + if (Components.Any(x => x.Type.IsSelectType())) + throw new InvalidOperationException($"A button cannot be added to a row with a SelectMenu"); + + AddComponent(builtButton); + + return this; + } + + /// + /// Builds the current builder to a that can be used within a + /// + /// A that can be used within a + public ActionRowComponent Build() + { + return new ActionRowComponent(_components); + } + + internal bool CanTakeComponent(IMessageComponent component) + { + switch (component.Type) + { + case ComponentType.ActionRow: + return false; + case ComponentType.Button: + if (Components.Any(x => x.Type.IsSelectType())) + return false; + else + return Components.Count < 5; + case ComponentType.SelectMenu: + case ComponentType.ChannelSelect: + case ComponentType.MentionableSelect: + case ComponentType.RoleSelect: + case ComponentType.UserSelect: + return Components.Count == 0; + default: + return false; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ButtonBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ButtonBuilder.cs new file mode 100644 index 00000000..f1f44a3c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ButtonBuilder.cs @@ -0,0 +1,320 @@ +using Discord.Utils; + +using System; + +namespace Discord; + +/// +/// Represents a class used to build 's. +/// +public class ButtonBuilder +{ + /// + /// The max length of a . + /// + public const int MaxButtonLabelLength = 80; + + /// + /// Gets or sets the label of the current button. + /// + /// length exceeds . + /// length exceeds . + public string Label + { + get => _label; + set => _label = value?.Length switch + { + > MaxButtonLabelLength => throw new ArgumentOutOfRangeException(nameof(value), $"Label length must be less or equal to {MaxButtonLabelLength}."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Label length must be at least 1."), + _ => value + }; + } + + /// + /// Gets or sets the custom id of the current button. + /// + /// length exceeds + /// length subceeds 1. + public string CustomId + { + get => _customId; + set => _customId = value?.Length switch + { + > ComponentBuilder.MaxCustomIdLength => throw new ArgumentOutOfRangeException(nameof(value), $"Custom Id length must be less or equal to {ComponentBuilder.MaxCustomIdLength}."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Custom Id length must be at least 1."), + _ => value + }; + } + + /// + /// Gets or sets the of the current button. + /// + public ButtonStyle Style { get; set; } + + /// + /// Gets or sets the of the current button. + /// + public IEmote Emote { get; set; } + + /// + /// Gets or sets the url of the current button. + /// + public string Url { get; set; } + + /// + /// Gets or sets whether the current button is disabled. + /// + public bool IsDisabled { get; set; } + + /// + /// Gets or sets the id of the sku associated with the current button. + /// + /// + /// if the button is not of type . + /// + public ulong? SkuId { get; set; } + + private string _label; + private string _customId; + + /// + /// Creates a new instance of a . + /// + public ButtonBuilder() { } + + /// + /// Creates a new instance of a . + /// + /// The label to use on the newly created link button. + /// The url of this button. + /// The custom ID of this button. + /// The custom ID of this button. + /// The emote of this button. + /// Disabled this button or not. + /// The sku id of this button. + public ButtonBuilder(string label = null, string customId = null, ButtonStyle style = ButtonStyle.Primary, string url = null, IEmote emote = null, bool isDisabled = false, ulong? skuId = null) + { + CustomId = customId; + Style = style; + Url = url; + Label = label; + IsDisabled = isDisabled; + Emote = emote; + SkuId = skuId; + } + + /// + /// Creates a new instance of a from instance of a . + /// + public ButtonBuilder(ButtonComponent button) + { + CustomId = button.CustomId; + Style = button.Style; + Url = button.Url; + Label = button.Label; + IsDisabled = button.IsDisabled; + Emote = button.Emote; + SkuId = button.SkuId; + } + + /// + /// Creates a button with the style. + /// + /// The label for this link button. + /// The url for this link button to go to. + /// The emote for this link button. + /// A builder with the newly created button. + public static ButtonBuilder CreateLinkButton(string label, string url, IEmote emote = null) + => new (label, null, ButtonStyle.Link, url, emote: emote); + + /// + /// Creates a button with the style. + /// + /// The label for this danger button. + /// The custom id for this danger button. + /// The emote for this danger button. + /// A builder with the newly created button. + public static ButtonBuilder CreateDangerButton(string label, string customId, IEmote emote = null) + => new (label, customId, ButtonStyle.Danger, emote: emote); + + /// + /// Creates a button with the style. + /// + /// The label for this primary button. + /// The custom id for this primary button. + /// The emote for this primary button. + /// A builder with the newly created button. + public static ButtonBuilder CreatePrimaryButton(string label, string customId, IEmote emote = null) + => new (label, customId, emote: emote); + + /// + /// Creates a button with the style. + /// + /// The label for this secondary button. + /// The custom id for this secondary button. + /// The emote for this secondary button. + /// A builder with the newly created button. + public static ButtonBuilder CreateSecondaryButton(string label, string customId, IEmote emote = null) + => new (label, customId, ButtonStyle.Secondary, emote: emote); + + /// + /// Creates a button with the style. + /// + /// The label for this success button. + /// The custom id for this success button. + /// The emote for this success button. + /// A builder with the newly created button. + public static ButtonBuilder CreateSuccessButton(string label, string customId, IEmote emote = null) + => new (label, customId, ButtonStyle.Success, emote: emote); + + /// + /// Creates a button with the style. + /// + /// The label for this premium button. + /// The sku id for this premium button. + /// The emote for this premium button. + /// A builder with the newly created button. + public static ButtonBuilder CreatePremiumButton(string label, ulong skuId, IEmote emote = null) + => new (label, style: ButtonStyle.Success, emote: emote, skuId: skuId); + + /// + /// Sets the current buttons label to the specified text. + /// + /// The text for the label. + /// + /// The current builder. + public ButtonBuilder WithLabel(string label) + { + Label = label; + return this; + } + + /// + /// Sets the current buttons style. + /// + /// The style for this builders button. + /// The current builder. + public ButtonBuilder WithStyle(ButtonStyle style) + { + Style = style; + return this; + } + + /// + /// Sets the current buttons emote. + /// + /// The emote to use for the current button. + /// The current builder. + public ButtonBuilder WithEmote(IEmote emote) + { + Emote = emote; + return this; + } + + /// + /// Sets the current buttons url. + /// + /// The url to use for the current button. + /// The current builder. + public ButtonBuilder WithUrl(string url) + { + Url = url; + return this; + } + + /// + /// Sets the custom id of the current button. + /// + /// The id to use for the current button. + /// + /// The current builder. + public ButtonBuilder WithCustomId(string id) + { + CustomId = id; + return this; + } + + /// + /// Sets whether the current button is disabled. + /// + /// Whether the current button is disabled or not. + /// The current builder. + public ButtonBuilder WithDisabled(bool isDisabled) + { + IsDisabled = isDisabled; + return this; + } + + /// + /// Sets the sku id of the current button. + /// + /// The id of the sku + /// The current builder. + public ButtonBuilder WithSkuId(ulong? skuId) + { + SkuId = skuId; + return this; + } + + /// + /// Builds this builder into a to be used in a . + /// + /// A to be used in a . + /// A button must contain either a or a , but not both. + /// A button must have an or a . + /// A link button must contain a URL. + /// A URL must include a protocol (http or https). + /// A non-link button must contain a custom id + public ButtonComponent Build() + { + if (string.IsNullOrWhiteSpace(Label) && Emote is null) + throw new InvalidOperationException("A button must have an Emote or a label!"); + + var a = 0; + if (!string.IsNullOrWhiteSpace(Url)) + a++; + if (!string.IsNullOrWhiteSpace(CustomId)) + a++; + if (SkuId is not null) + a++; + + if (a is 0 or > 1) + throw new InvalidOperationException("A button must contain either a URL, CustomId or SkuId, but not multiple of them!"); + + switch (Style) + { + case 0: + { + throw new ArgumentException("A button must have a style.", nameof(Style)); + } + + case ButtonStyle.Primary: + case ButtonStyle.Secondary: + case ButtonStyle.Success: + case ButtonStyle.Danger: + { + if (string.IsNullOrWhiteSpace(CustomId)) + throw new InvalidOperationException("Non-link and non-premium buttons must have a custom id associated with them"); + + } + break; + + case ButtonStyle.Link: + { + if (string.IsNullOrWhiteSpace(Url)) + throw new InvalidOperationException("Link buttons must have a link associated with them"); + UrlValidation.ValidateButton(Url); + } + break; + + case ButtonStyle.Premium: + { + if (SkuId is null) + throw new InvalidOperationException("Premium buttons must have a sku id associated with them"); + } + break; + } + + return new ButtonComponent(Style, Label, Emote, CustomId, Url, IsDisabled, SkuId); + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilder.cs new file mode 100644 index 00000000..eeb7db85 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilder.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Discord; + +/// +/// Represents a builder for creating a . +/// +public class ComponentBuilder +{ + /// + /// The max length of a . + /// + public const int MaxCustomIdLength = 100; + + /// + /// The max amount of rows a message can have. + /// + public const int MaxActionRowCount = 5; + + /// + /// Gets or sets the Action Rows for this Component Builder. + /// + /// cannot be null. + /// count exceeds . + public List ActionRows + { + get => _actionRows; + set + { + if (value == null) + throw new ArgumentNullException(nameof(value), $"{nameof(ActionRows)} cannot be null."); + if (value.Count > MaxActionRowCount) + throw new ArgumentOutOfRangeException(nameof(value), $"Action row count must be less than or equal to {MaxActionRowCount}."); + _actionRows = value; + } + } + + private List _actionRows; + + /// + /// Creates a new builder from a message. + /// + /// The message to create the builder from. + /// The newly created builder. + public static ComponentBuilder FromMessage(IMessage message) + => FromComponents(message.Components); + + /// + /// Creates a new builder from the provided list of components. + /// + /// The components to create the builder from. + /// The newly created builder. + public static ComponentBuilder FromComponents(IReadOnlyCollection components) + { + var builder = new ComponentBuilder(); + for (int i = 0; i != components.Count; i++) + { + var component = components.ElementAt(i); + builder.AddComponent(component, i); + } + return builder; + } + + internal void AddComponent(IMessageComponent component, int row) + { + switch (component) + { + case ButtonComponent button: + WithButton(button.Label, button.CustomId, button.Style, button.Emote, button.Url, button.IsDisabled, row); + break; + case ActionRowComponent actionRow: + foreach (var cmp in actionRow.Components) + AddComponent(cmp, row); + break; + case SelectMenuComponent menu: + WithSelectMenu(menu.CustomId, menu.Options?.Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.IsDefault)).ToList(), menu.Placeholder, menu.MinValues, menu.MaxValues, menu.IsDisabled, row); + break; + } + } + + /// + /// Removes all components of the given type from the . + /// + /// The to remove. + /// The current builder. + public ComponentBuilder RemoveComponentsOfType(ComponentType t) + { + this.ActionRows.ForEach(ar => ar.Components.RemoveAll(c => c.Type == t)); + return this; + } + + /// + /// Removes a component from the . + /// + /// The custom id of the component. + /// The current builder. + public ComponentBuilder RemoveComponent(string customId) + { + this.ActionRows.ForEach(ar => ar.Components.RemoveAll(c => c.CustomId == customId)); + return this; + } + + /// + /// Removes a Link Button from the based on its URL. + /// + /// The URL of the Link Button. + /// The current builder. + public ComponentBuilder RemoveButtonByURL(string url) + { + this.ActionRows.ForEach(ar => ar.Components.RemoveAll(c => c is ButtonComponent b && b.Url == url)); + return this; + } + + /// + /// Adds a to the at the specific row. + /// If the row cannot accept the component then it will add it to a row that can. + /// + /// The custom id of the menu. + /// The options of the menu. + /// The placeholder of the menu. + /// The min values of the placeholder. + /// The max values of the placeholder. + /// Whether or not the menu is disabled. + /// The row to add the menu to. + /// The type of the select menu. + /// Menus valid channel types (only for ) + /// + public ComponentBuilder WithSelectMenu(string customId, List options = null, + string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0, ComponentType type = ComponentType.SelectMenu, + ChannelType[] channelTypes = null, SelectMenuDefaultValue[] defaultValues = null) + { + return WithSelectMenu(new SelectMenuBuilder() + .WithCustomId(customId) + .WithOptions(options) + .WithPlaceholder(placeholder) + .WithMaxValues(maxValues) + .WithMinValues(minValues) + .WithDisabled(disabled) + .WithType(type) + .WithChannelTypes(channelTypes) + .WithDefaultValues(defaultValues), + row); + } + + /// + /// Adds a to the at the specific row. + /// If the row cannot accept the component then it will add it to a row that can. + /// + /// The menu to add. + /// The row to attempt to add this component on. + /// There is no more row to add a menu. + /// must be less than . + /// The current builder. + public ComponentBuilder WithSelectMenu(SelectMenuBuilder menu, int row = 0) + { + Preconditions.LessThan(row, MaxActionRowCount, nameof(row)); + if (menu.Options is not null && menu.Options.Distinct().Count() != menu.Options.Count) + throw new InvalidOperationException("Please make sure that there is no duplicates values."); + + var builtMenu = menu.Build(); + + if (_actionRows == null) + { + _actionRows = new List + { + new ActionRowBuilder().AddComponent(builtMenu) + }; + } + else + { + if (_actionRows.Count == row) + _actionRows.Add(new ActionRowBuilder().AddComponent(builtMenu)); + else + { + ActionRowBuilder actionRow; + if (_actionRows.Count > row) + actionRow = _actionRows.ElementAt(row); + else + { + actionRow = new ActionRowBuilder(); + _actionRows.Add(actionRow); + } + + if (actionRow.CanTakeComponent(builtMenu)) + actionRow.AddComponent(builtMenu); + else if (row < MaxActionRowCount) + WithSelectMenu(menu, row + 1); + else + throw new InvalidOperationException($"There is no more row to add a {nameof(builtMenu)}"); + } + } + + return this; + } + + /// + /// Adds a with specified parameters to the at the specific row. + /// If the row cannot accept the component then it will add it to a row that can. + /// + /// The label text for the newly added button. + /// The style of this newly added button. + /// A to be used with this button. + /// The custom id of the newly added button. + /// A URL to be used only if the is a Link. + /// Whether or not the newly created button is disabled. + /// The row the button should be placed on. + /// The id of the sku associated with the current button. + /// The current builder. + public ComponentBuilder WithButton( + string label = null, + string customId = null, + ButtonStyle style = ButtonStyle.Primary, + IEmote emote = null, + string url = null, + bool disabled = false, + int row = 0, + ulong? skuId = null) + { + var button = new ButtonBuilder() + .WithLabel(label) + .WithStyle(style) + .WithEmote(emote) + .WithCustomId(customId) + .WithUrl(url) + .WithDisabled(disabled) + .WithSkuId(skuId); + + return WithButton(button, row); + } + + /// + /// Adds a to the at the specific row. + /// If the row cannot accept the component then it will add it to a row that can. + /// + /// The button to add. + /// The row to add the button. + /// There is no more row to add a button. + /// must be less than . + /// The current builder. + public ComponentBuilder WithButton(ButtonBuilder button, int row = 0) + { + Preconditions.LessThan(row, MaxActionRowCount, nameof(row)); + + var builtButton = button.Build(); + + if (_actionRows == null) + { + _actionRows = new List + { + new ActionRowBuilder().AddComponent(builtButton) + }; + } + else + { + if (_actionRows.Count == row) + _actionRows.Add(new ActionRowBuilder().AddComponent(builtButton)); + else + { + ActionRowBuilder actionRow; + if (_actionRows.Count > row) + actionRow = _actionRows.ElementAt(row); + else + { + actionRow = new ActionRowBuilder(); + _actionRows.Add(actionRow); + } + + if (actionRow.CanTakeComponent(builtButton)) + actionRow.AddComponent(builtButton); + else if (row < MaxActionRowCount) + WithButton(button, row + 1); + else + throw new InvalidOperationException($"There is no more row to add a {nameof(button)}"); + } + } + + return this; + } + + /// + /// Adds a row to this component builder. + /// + /// The row to add. + /// The component builder contains the max amount of rows defined as . + /// The current builder. + public ComponentBuilder AddRow(ActionRowBuilder row) + { + _actionRows ??= new(); + + if (_actionRows.Count >= MaxActionRowCount) + throw new IndexOutOfRangeException("The max amount of rows has been reached"); + + ActionRows.Add(row); + return this; + } + + /// + /// Sets the rows of this component builder to a specified collection. + /// + /// The rows to set. + /// The collection contains more rows then is allowed by discord. + /// The current builder. + public ComponentBuilder WithRows(IEnumerable rows) + { + if (rows.Count() > MaxActionRowCount) + throw new IndexOutOfRangeException($"Cannot have more than {MaxActionRowCount} rows"); + + _actionRows = new List(rows); + return this; + } + + /// + /// Builds this builder into a used to send your components. + /// + /// A that can be sent with . + public MessageComponent Build() + { + if (_actionRows?.SelectMany(x => x.Components)?.Any(x => x.Type == ComponentType.TextInput) ?? false) + throw new ArgumentException("TextInputComponents are not allowed in messages.", nameof(ActionRows)); + + if (_actionRows?.Count > 0) + for (int i = 0; i < _actionRows?.Count; i++) + if (_actionRows[i]?.Components?.Count == 0) + _actionRows.RemoveAt(i); + + return _actionRows != null + ? new MessageComponent(_actionRows.Select(x => x.Build()).ToList()) + : MessageComponent.Empty; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SelectMenuBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SelectMenuBuilder.cs new file mode 100644 index 00000000..000a1c62 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SelectMenuBuilder.cs @@ -0,0 +1,406 @@ +using Discord.Utils; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Discord; + +/// +/// Represents a class used to build 's. +/// +public class SelectMenuBuilder +{ + /// + /// The max length of a . + /// + public const int MaxPlaceholderLength = 100; + + /// + /// The maximum number of values for the and properties. + /// + public const int MaxValuesCount = 25; + + /// + /// The maximum number of options a can have. + /// + public const int MaxOptionCount = 25; + + /// + /// Gets or sets the custom id of the current select menu. + /// + /// length exceeds + /// length subceeds 1. + public string CustomId + { + get => _customId; + set => _customId = value?.Length switch + { + > ComponentBuilder.MaxCustomIdLength => throw new ArgumentOutOfRangeException(nameof(value), $"Custom Id length must be less or equal to {ComponentBuilder.MaxCustomIdLength}."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Custom Id length must be at least 1."), + _ => value + }; + } + + /// + /// Gets or sets the type of the current select menu. + /// + /// Type must be a select menu type. + public ComponentType Type + { + get => _type; + set => _type = value.IsSelectType() + ? value + : throw new ArgumentException("Type must be a select menu type.", nameof(value)); + } + + /// + /// Gets or sets the placeholder text of the current select menu. + /// + /// length exceeds . + /// length subceeds 1. + public string Placeholder + { + get => _placeholder; + set => _placeholder = value?.Length switch + { + > MaxPlaceholderLength => throw new ArgumentOutOfRangeException(nameof(value), $"Placeholder length must be less or equal to {MaxPlaceholderLength}."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Placeholder length must be at least 1."), + _ => value + }; + } + + /// + /// Gets or sets the minimum values of the current select menu. + /// + /// exceeds . + public int MinValues + { + get => _minValues; + set + { + Preconditions.AtMost(value, MaxValuesCount, nameof(MinValues)); + _minValues = value; + } + } + + /// + /// Gets or sets the maximum values of the current select menu. + /// + /// exceeds . + public int MaxValues + { + get => _maxValues; + set + { + Preconditions.AtMost(value, MaxValuesCount, nameof(MaxValues)); + _maxValues = value; + } + } + + /// + /// Gets or sets a collection of for this current select menu. + /// + /// count exceeds . + /// is null. + public List Options + { + get => _options; + set + { + if (value != null) + Preconditions.AtMost(value.Count, MaxOptionCount, nameof(Options)); + + _options = value; + } + } + + /// + /// Gets or sets whether the current menu is disabled. + /// + public bool IsDisabled { get; set; } + + /// + /// Gets or sets the menu's channel types (only valid on s). + /// + public List ChannelTypes { get; set; } + + public List DefaultValues + { + get => _defaultValues; + set + { + if (value != null) + Preconditions.AtMost(value.Count, MaxOptionCount, nameof(DefaultValues)); + + _defaultValues = value; + } + } + + private List _options = new List(); + private int _minValues = 1; + private int _maxValues = 1; + private string _placeholder; + private string _customId; + private ComponentType _type = ComponentType.SelectMenu; + private List _defaultValues = new(); + + /// + /// Creates a new instance of a . + /// + public SelectMenuBuilder() { } + + /// + /// Creates a new instance of a from instance of . + /// + public SelectMenuBuilder(SelectMenuComponent selectMenu) + { + Placeholder = selectMenu.Placeholder; + CustomId = selectMenu.CustomId; + MaxValues = selectMenu.MaxValues; + MinValues = selectMenu.MinValues; + IsDisabled = selectMenu.IsDisabled; + Options = selectMenu.Options? + .Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.IsDefault)) + .ToList(); + DefaultValues = selectMenu.DefaultValues?.ToList(); + } + + /// + /// Creates a new instance of a . + /// + /// The custom id of this select menu. + /// The options for this select menu. + /// The placeholder of this select menu. + /// The max values of this select menu. + /// The min values of this select menu. + /// Disabled this select menu or not. + /// The of this select menu. + /// The types of channels this menu can select (only valid on s) + public SelectMenuBuilder(string customId, List options = null, string placeholder = null, int maxValues = 1, int minValues = 1, + bool isDisabled = false, ComponentType type = ComponentType.SelectMenu, List channelTypes = null, List defaultValues = null) + { + CustomId = customId; + Options = options; + Placeholder = placeholder; + IsDisabled = isDisabled; + MaxValues = maxValues; + MinValues = minValues; + Type = type; + ChannelTypes = channelTypes ?? new(); + DefaultValues = defaultValues ?? new(); + } + + /// + /// Sets the field CustomId. + /// + /// The value to set the field CustomId to. + /// + /// + /// The current builder. + /// + public SelectMenuBuilder WithCustomId(string customId) + { + CustomId = customId; + return this; + } + + /// + /// Sets the field placeholder. + /// + /// The value to set the field placeholder to. + /// + /// + /// The current builder. + /// + public SelectMenuBuilder WithPlaceholder(string placeholder) + { + Placeholder = placeholder; + return this; + } + + /// + /// Sets the field minValues. + /// + /// The value to set the field minValues to. + /// + /// + /// The current builder. + /// + public SelectMenuBuilder WithMinValues(int minValues) + { + MinValues = minValues; + return this; + } + + /// + /// Sets the field maxValues. + /// + /// The value to set the field maxValues to. + /// + /// + /// The current builder. + /// + public SelectMenuBuilder WithMaxValues(int maxValues) + { + MaxValues = maxValues; + return this; + } + + /// + /// Sets the field options. + /// + /// The value to set the field options to. + /// + /// + /// The current builder. + /// + public SelectMenuBuilder WithOptions(List options) + { + Options = options; + return this; + } + + /// + /// Add one option to menu options. + /// + /// The option builder class containing the option properties. + /// Options count reached . + /// + /// The current builder. + /// + public SelectMenuBuilder AddOption(SelectMenuOptionBuilder option) + { + Options ??= new(); + + if (Options.Count >= MaxOptionCount) + throw new InvalidOperationException($"Options count reached {MaxOptionCount}."); + + Options.Add(option); + return this; + } + + /// + /// Add one option to menu options. + /// + /// The label for this option. + /// The value of this option. + /// The description of this option. + /// The emote of this option. + /// Render this option as selected by default or not. + /// Options count reached . + /// + /// The current builder. + /// + public SelectMenuBuilder AddOption(string label, string value, string description = null, IEmote emote = null, bool? isDefault = null) + { + AddOption(new SelectMenuOptionBuilder(label, value, description, emote, isDefault)); + return this; + } + + /// + /// Add one default value to menu options. + /// + /// The id of an entity to add. + /// The type of an entity to add. + /// Default values count reached . + /// + /// The current builder. + /// + public SelectMenuBuilder AddDefaultValue(ulong id, SelectDefaultValueType type) + => AddDefaultValue(new(id, type)); + + /// + /// Add one default value to menu options. + /// + /// The default value to add. + /// Default values count reached . + /// + /// The current builder. + /// + public SelectMenuBuilder AddDefaultValue(SelectMenuDefaultValue value) + { + if (DefaultValues.Count >= MaxOptionCount) + throw new InvalidOperationException($"Options count reached {MaxOptionCount}."); + + DefaultValues.Add(value); + return this; + } + + /// + /// Sets the field default values. + /// + /// The value to set the field default values to. + /// + /// The current builder. + /// + public SelectMenuBuilder WithDefaultValues(params SelectMenuDefaultValue[] defaultValues) + { + DefaultValues = defaultValues?.ToList(); + return this; + } + + /// + /// Sets whether the current menu is disabled. + /// + /// Whether the current menu is disabled or not. + /// + /// The current builder. + /// + public SelectMenuBuilder WithDisabled(bool isDisabled) + { + IsDisabled = isDisabled; + return this; + } + + /// + /// Sets the menu's current type. + /// + /// The type of the menu. + /// + /// The current builder. + /// + public SelectMenuBuilder WithType(ComponentType type) + { + Type = type; + return this; + } + + /// + /// Sets the menus valid channel types (only for s). + /// + /// The valid channel types of the menu. + /// + /// The current builder. + /// + public SelectMenuBuilder WithChannelTypes(List channelTypes) + { + ChannelTypes = channelTypes; + return this; + } + + /// + /// Sets the menus valid channel types (only for s). + /// + /// The valid channel types of the menu. + /// + /// The current builder. + /// + public SelectMenuBuilder WithChannelTypes(params ChannelType[] channelTypes) + { + ChannelTypes = channelTypes is null + ? ChannelTypeUtils.AllChannelTypes() + : channelTypes.ToList(); + return this; + } + + /// + /// Builds a + /// + /// The newly built + public SelectMenuComponent Build() + { + var options = Options?.Select(x => x.Build()).ToList(); + + return new SelectMenuComponent(CustomId, options, Placeholder, MinValues, MaxValues, IsDisabled, Type, ChannelTypes, DefaultValues); + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SelectMenuOptionBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SelectMenuOptionBuilder.cs new file mode 100644 index 00000000..6e5b87bf --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SelectMenuOptionBuilder.cs @@ -0,0 +1,207 @@ +using System; + +namespace Discord; + +/// +/// Represents a class used to build 's. +/// +public class SelectMenuOptionBuilder +{ + /// + /// The maximum length of a . + /// + public const int MaxSelectLabelLength = 100; + + /// + /// The maximum length of a . + /// + public const int MaxDescriptionLength = 100; + + /// + /// The maximum length of a . + /// + public const int MaxSelectValueLength = 100; + + /// + /// Gets or sets the label of the current select menu. + /// + /// length exceeds + /// length subceeds 1. + public string Label + { + get => _label; + set => _label = value?.Length switch + { + > MaxSelectLabelLength => throw new ArgumentOutOfRangeException(nameof(value), $"Label length must be less or equal to {MaxSelectLabelLength}."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Label length must be at least 1."), + _ => value + }; + } + + /// + /// Gets or sets the value of the current select menu. + /// + /// length exceeds . + /// length subceeds 1. + public string Value + { + get => _value; + set => _value = value?.Length switch + { + > MaxSelectValueLength => throw new ArgumentOutOfRangeException(nameof(value), $"Value length must be less or equal to {MaxSelectValueLength}."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Value length must be at least 1."), + _ => value + }; + } + + /// + /// Gets or sets this menu options description. + /// + /// length exceeds . + /// length subceeds 1. + public string Description + { + get => _description; + set => _description = value?.Length switch + { + > MaxDescriptionLength => throw new ArgumentOutOfRangeException(nameof(value), $"Description length must be less or equal to {MaxDescriptionLength}."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be at least 1."), + _ => value + }; + } + + /// + /// Gets or sets the emote of this option. + /// + public IEmote Emote { get; set; } + + /// + /// Gets or sets the whether or not this option will render selected by default. + /// + public bool? IsDefault { get; set; } + + private string _label; + private string _value; + private string _description; + + /// + /// Creates a new instance of a . + /// + public SelectMenuOptionBuilder() { } + + /// + /// Creates a new instance of a . + /// + /// The label for this option. + /// The value of this option. + /// The description of this option. + /// The emote of this option. + /// Render this option as selected by default or not. + public SelectMenuOptionBuilder(string label, string value, string description = null, IEmote emote = null, bool? isDefault = null) + { + Label = label; + Value = value; + Description = description; + Emote = emote; + IsDefault = isDefault; + } + + /// + /// Creates a new instance of a from instance of a . + /// + public SelectMenuOptionBuilder(SelectMenuOption option) + { + Label = option.Label; + Value = option.Value; + Description = option.Description; + Emote = option.Emote; + IsDefault = option.IsDefault; + } + + /// + /// Sets the field label. + /// + /// The value to set the field label to. + /// + /// + /// The current builder. + /// + public SelectMenuOptionBuilder WithLabel(string label) + { + Label = label; + return this; + } + + /// + /// Sets the field value. + /// + /// The value to set the field value to. + /// + /// + /// The current builder. + /// + public SelectMenuOptionBuilder WithValue(string value) + { + Value = value; + return this; + } + + /// + /// Sets the field description. + /// + /// The value to set the field description to. + /// + /// + /// The current builder. + /// + public SelectMenuOptionBuilder WithDescription(string description) + { + Description = description; + return this; + } + + /// + /// Sets the field emote. + /// + /// The value to set the field emote to. + /// + /// The current builder. + /// + public SelectMenuOptionBuilder WithEmote(IEmote emote) + { + Emote = emote; + return this; + } + + /// + /// Sets the field default. + /// + /// The value to set the field default to. + /// + /// The current builder. + /// + public SelectMenuOptionBuilder WithDefault(bool isDefault) + { + IsDefault = isDefault; + return this; + } + + /// + /// Builds a . + /// + /// The newly built . + public SelectMenuOption Build() + { + if (string.IsNullOrWhiteSpace(Label)) + throw new ArgumentNullException(nameof(Label), "Option must have a label."); + + Preconditions.AtMost(Label.Length, MaxSelectLabelLength, nameof(Label), $"Label length must be less or equal to {MaxSelectLabelLength}."); + + if (string.IsNullOrWhiteSpace(Value)) + throw new ArgumentNullException(nameof(Value), "Option must have a value."); + + Preconditions.AtMost(Value.Length, MaxSelectValueLength, nameof(Value), $"Value length must be less or equal to {MaxSelectValueLength}."); + + return new SelectMenuOption(Label, Value, Description, Emote, IsDefault); + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/TextInputBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/TextInputBuilder.cs new file mode 100644 index 00000000..b675980b --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/TextInputBuilder.cs @@ -0,0 +1,262 @@ +using System; +using System.Linq; + +namespace Discord; + +/// +/// Represents a builder for creating a . +/// +public class TextInputBuilder +{ + /// + /// The max length of a . + /// + public const int MaxPlaceholderLength = 100; + public const int LargestMaxLength = 4000; + + /// + /// Gets or sets the custom id of the current text input. + /// + /// length exceeds + /// length subceeds 1. + public string CustomId + { + get => _customId; + set => _customId = value?.Length switch + { + > ComponentBuilder.MaxCustomIdLength => throw new ArgumentOutOfRangeException(nameof(value), $"Custom Id length must be less or equal to {ComponentBuilder.MaxCustomIdLength}."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Custom Id length must be at least 1."), + _ => value + }; + } + + /// + /// Gets or sets the style of the current text input. + /// + public TextInputStyle Style { get; set; } = TextInputStyle.Short; + + /// + /// Gets or sets the label of the current text input. + /// + public string Label { get; set; } + + /// + /// Gets or sets the placeholder of the current text input. + /// + /// is longer than characters + public string Placeholder + { + get => _placeholder; + set => _placeholder = (value?.Length ?? 0) <= MaxPlaceholderLength + ? value + : throw new ArgumentException($"Placeholder cannot have more than {MaxPlaceholderLength} characters."); + } + + /// + /// Gets or sets the minimum length of the current text input. + /// + /// is less than 0. + /// is greater than . + /// is greater than . + public int? MinLength + { + get => _minLength; + set + { + if (value < 0) + throw new ArgumentOutOfRangeException(nameof(value), $"MinLength must not be less than 0"); + if (value > LargestMaxLength) + throw new ArgumentOutOfRangeException(nameof(value), $"MinLength must not be greater than {LargestMaxLength}"); + if (value > (MaxLength ?? LargestMaxLength)) + throw new ArgumentOutOfRangeException(nameof(value), $"MinLength must be less than MaxLength"); + _minLength = value; + } + } + + /// + /// Gets or sets the maximum length of the current text input. + /// + /// is less than 0. + /// is greater than . + /// is less than . + public int? MaxLength + { + get => _maxLength; + set + { + if (value < 0) + throw new ArgumentOutOfRangeException(nameof(value), $"MaxLength must not be less than 0"); + if (value > LargestMaxLength) + throw new ArgumentOutOfRangeException(nameof(value), $"MaxLength most not be greater than {LargestMaxLength}"); + if (value < (MinLength ?? -1)) + throw new ArgumentOutOfRangeException(nameof(value), $"MaxLength must be greater than MinLength ({MinLength})"); + _maxLength = value; + } + } + + /// + /// Gets or sets whether the user is required to input text. + /// + public bool? Required { get; set; } + + /// + /// Gets or sets the default value of the text input. + /// + /// .Length is less than 0. + /// + /// .Length is greater than or . + /// + /// + /// is and contains a new line character. + /// + public string Value + { + get => _value; + set + { + if (value?.Length > (MaxLength ?? LargestMaxLength)) + throw new ArgumentOutOfRangeException(nameof(value), $"Value must not be longer than {MaxLength ?? LargestMaxLength}."); + if (value?.Length < (MinLength ?? 0)) + throw new ArgumentOutOfRangeException(nameof(value), $"Value must not be shorter than {MinLength}"); + + _value = value; + } + } + + private string _customId; + private int? _maxLength; + private int? _minLength; + private string _placeholder; + private string _value; + + /// + /// Creates a new instance of a . + /// + /// The text input's label. + /// The text input's style. + /// The text input's custom id. + /// The text input's placeholder. + /// The text input's minimum length. + /// The text input's maximum length. + /// The text input's required value. + public TextInputBuilder(string label, string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, + int? minLength = null, int? maxLength = null, bool? required = null, string value = null) + { + Label = label; + Style = style; + CustomId = customId; + Placeholder = placeholder; + MinLength = minLength; + MaxLength = maxLength; + Required = required; + Value = value; + } + + /// + /// Creates a new instance of a . + /// + public TextInputBuilder() + { + + } + + /// + /// Sets the label of the current builder. + /// + /// The value to set. + /// The current builder. + public TextInputBuilder WithLabel(string label) + { + Label = label; + return this; + } + + /// + /// Sets the style of the current builder. + /// + /// The value to set. + /// The current builder. + public TextInputBuilder WithStyle(TextInputStyle style) + { + Style = style; + return this; + } + + /// + /// Sets the custom id of the current builder. + /// + /// The value to set. + /// The current builder. + public TextInputBuilder WithCustomId(string customId) + { + CustomId = customId; + return this; + } + + /// + /// Sets the placeholder of the current builder. + /// + /// The value to set. + /// The current builder. + public TextInputBuilder WithPlaceholder(string placeholder) + { + Placeholder = placeholder; + return this; + } + + /// + /// Sets the value of the current builder. + /// + /// The value to set + /// The current builder. + public TextInputBuilder WithValue(string value) + { + Value = value; + return this; + } + + /// + /// Sets the minimum length of the current builder. + /// + /// The value to set. + /// The current builder. + public TextInputBuilder WithMinLength(int minLength) + { + MinLength = minLength; + return this; + } + + /// + /// Sets the maximum length of the current builder. + /// + /// The value to set. + /// The current builder. + public TextInputBuilder WithMaxLength(int maxLength) + { + MaxLength = maxLength; + return this; + } + + /// + /// Sets the required value of the current builder. + /// + /// The value to set. + /// The current builder. + public TextInputBuilder WithRequired(bool required) + { + Required = required; + return this; + } + + public TextInputComponent Build() + { + if (string.IsNullOrEmpty(CustomId)) + throw new ArgumentException("TextInputComponents must have a custom id.", nameof(CustomId)); + if (string.IsNullOrWhiteSpace(Label)) + throw new ArgumentException("TextInputComponents must have a label.", nameof(Label)); + if (Style is TextInputStyle.Short && Value?.Any(x => x == '\n') is true) + throw new ArgumentException($"Value must not contain new line characters when style is {TextInputStyle.Short}.", nameof(Value)); + + return new TextInputComponent(CustomId, Label, Placeholder, MinLength, MaxLength, Style, Required, Value); + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ButtonComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ButtonComponent.cs index 4b9fa275..c387f9ad 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ButtonComponent.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ButtonComponent.cs @@ -1,61 +1,69 @@ -namespace Discord +namespace Discord; + +/// +/// Represents a Button. +/// +public class ButtonComponent : IMessageComponent { + /// + public ComponentType Type => ComponentType.Button; + /// - /// Represents a Button. + /// Gets the of this button, example buttons with each style can be found Here. /// - public class ButtonComponent : IMessageComponent + public ButtonStyle Style { get; } + + /// + /// Gets the label of the button, this is the text that is shown. + /// + public string Label { get; } + + /// + /// Gets the displayed with this button. + /// + public IEmote Emote { get; } + + /// + public string CustomId { get; } + + /// + /// Gets the URL for a button. + /// + /// + /// You cannot have a button with a URL and a CustomId. + /// + public string Url { get; } + + /// + /// Gets whether this button is disabled or not. + /// + public bool IsDisabled { get; } + + /// + /// Gets the id of the sku associated with the current button. + /// + /// + /// if the button is not of type . + /// + public ulong? SkuId { get; } + + /// + /// Turns this button into a button builder. + /// + /// + /// A newly created button builder with the same properties as this button. + /// + public ButtonBuilder ToBuilder() + => new (Label, CustomId, Style, Url, Emote, IsDisabled); + + internal ButtonComponent(ButtonStyle style, string label, IEmote emote, string customId, string url, bool isDisabled, ulong? skuId) { - /// - public ComponentType Type => ComponentType.Button; - - /// - /// Gets the of this button, example buttons with each style can be found Here. - /// - public ButtonStyle Style { get; } - - /// - /// Gets the label of the button, this is the text that is shown. - /// - public string Label { get; } - - /// - /// Gets the displayed with this button. - /// - public IEmote Emote { get; } - - /// - public string CustomId { get; } - - /// - /// Gets the URL for a button. - /// - /// - /// You cannot have a button with a URL and a CustomId. - /// - public string Url { get; } - - /// - /// Gets whether this button is disabled or not. - /// - public bool IsDisabled { get; } - - /// - /// Turns this button into a button builder. - /// - /// - /// A newly created button builder with the same properties as this button. - /// - public ButtonBuilder ToBuilder() - => new ButtonBuilder(Label, CustomId, Style, Url, Emote, IsDisabled); - - internal ButtonComponent(ButtonStyle style, string label, IEmote emote, string customId, string url, bool isDisabled) - { - Style = style; - Label = label; - Emote = emote; - CustomId = customId; - Url = url; - IsDisabled = isDisabled; - } + Style = style; + Label = label; + Emote = emote; + CustomId = customId; + Url = url; + IsDisabled = isDisabled; + SkuId = skuId; } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ButtonStyle.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ButtonStyle.cs index 92d48ab4..3b7a3158 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ButtonStyle.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ButtonStyle.cs @@ -1,33 +1,37 @@ -namespace Discord +namespace Discord; + +/// +/// Represents different styles to use with buttons. You can see an example of the different styles at +/// +public enum ButtonStyle { /// - /// Represents different styles to use with buttons. You can see an example of the different styles at + /// A Blurple button. /// - public enum ButtonStyle - { - /// - /// A Blurple button - /// - Primary = 1, + Primary = 1, - /// - /// A Grey (or gray) button - /// - Secondary = 2, + /// + /// A Grey (or gray) button. + /// + Secondary = 2, - /// - /// A Green button - /// - Success = 3, + /// + /// A Green button. + /// + Success = 3, - /// - /// A Red button - /// - Danger = 4, + /// + /// A Red button. + /// + Danger = 4, - /// - /// A button with a little popup box indicating that this button is a link. - /// - Link = 5 - } + /// + /// A button with a little popup box indicating that this button is a link. + /// + Link = 5, + + /// + /// A gradient button, opens a product's details modal. + /// + Premium = 6, } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs deleted file mode 100644 index e3c1623c..00000000 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs +++ /dev/null @@ -1,1636 +0,0 @@ -using Discord.Utils; - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Discord -{ - /// - /// Represents a builder for creating a . - /// - public class ComponentBuilder - { - /// - /// The max length of a . - /// - public const int MaxCustomIdLength = 100; - - /// - /// The max amount of rows a message can have. - /// - public const int MaxActionRowCount = 5; - - /// - /// Gets or sets the Action Rows for this Component Builder. - /// - /// cannot be null. - /// count exceeds . - public List ActionRows - { - get => _actionRows; - set - { - if (value == null) - throw new ArgumentNullException(nameof(value), $"{nameof(ActionRows)} cannot be null."); - if (value.Count > MaxActionRowCount) - throw new ArgumentOutOfRangeException(nameof(value), $"Action row count must be less than or equal to {MaxActionRowCount}."); - _actionRows = value; - } - } - - private List _actionRows; - - /// - /// Creates a new builder from a message. - /// - /// The message to create the builder from. - /// The newly created builder. - public static ComponentBuilder FromMessage(IMessage message) - => FromComponents(message.Components); - - /// - /// Creates a new builder from the provided list of components. - /// - /// The components to create the builder from. - /// The newly created builder. - public static ComponentBuilder FromComponents(IReadOnlyCollection components) - { - var builder = new ComponentBuilder(); - for (int i = 0; i != components.Count; i++) - { - var component = components.ElementAt(i); - builder.AddComponent(component, i); - } - return builder; - } - - internal void AddComponent(IMessageComponent component, int row) - { - switch (component) - { - case ButtonComponent button: - WithButton(button.Label, button.CustomId, button.Style, button.Emote, button.Url, button.IsDisabled, row); - break; - case ActionRowComponent actionRow: - foreach (var cmp in actionRow.Components) - AddComponent(cmp, row); - break; - case SelectMenuComponent menu: - WithSelectMenu(menu.CustomId, menu.Options?.Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.IsDefault)).ToList(), menu.Placeholder, menu.MinValues, menu.MaxValues, menu.IsDisabled, row); - break; - } - } - - /// - /// Removes all components of the given type from the . - /// - /// The to remove. - /// The current builder. - public ComponentBuilder RemoveComponentsOfType(ComponentType t) - { - this.ActionRows.ForEach(ar => ar.Components.RemoveAll(c => c.Type == t)); - return this; - } - - /// - /// Removes a component from the . - /// - /// The custom id of the component. - /// The current builder. - public ComponentBuilder RemoveComponent(string customId) - { - this.ActionRows.ForEach(ar => ar.Components.RemoveAll(c => c.CustomId == customId)); - return this; - } - - /// - /// Removes a Link Button from the based on its URL. - /// - /// The URL of the Link Button. - /// The current builder. - public ComponentBuilder RemoveButtonByURL(string url) - { - this.ActionRows.ForEach(ar => ar.Components.RemoveAll(c => c is ButtonComponent b && b.Url == url)); - return this; - } - - /// - /// Adds a to the at the specific row. - /// If the row cannot accept the component then it will add it to a row that can. - /// - /// The custom id of the menu. - /// The options of the menu. - /// The placeholder of the menu. - /// The min values of the placeholder. - /// The max values of the placeholder. - /// Whether or not the menu is disabled. - /// The row to add the menu to. - /// The type of the select menu. - /// Menus valid channel types (only for ) - /// - public ComponentBuilder WithSelectMenu(string customId, List options = null, - string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0, ComponentType type = ComponentType.SelectMenu, - ChannelType[] channelTypes = null, SelectMenuDefaultValue[] defaultValues = null) - { - return WithSelectMenu(new SelectMenuBuilder() - .WithCustomId(customId) - .WithOptions(options) - .WithPlaceholder(placeholder) - .WithMaxValues(maxValues) - .WithMinValues(minValues) - .WithDisabled(disabled) - .WithType(type) - .WithChannelTypes(channelTypes) - .WithDefaultValues(defaultValues), - row); - } - - /// - /// Adds a to the at the specific row. - /// If the row cannot accept the component then it will add it to a row that can. - /// - /// The menu to add. - /// The row to attempt to add this component on. - /// There is no more row to add a menu. - /// must be less than . - /// The current builder. - public ComponentBuilder WithSelectMenu(SelectMenuBuilder menu, int row = 0) - { - Preconditions.LessThan(row, MaxActionRowCount, nameof(row)); - if (menu.Options is not null && menu.Options.Distinct().Count() != menu.Options.Count) - throw new InvalidOperationException("Please make sure that there is no duplicates values."); - - var builtMenu = menu.Build(); - - if (_actionRows == null) - { - _actionRows = new List - { - new ActionRowBuilder().AddComponent(builtMenu) - }; - } - else - { - if (_actionRows.Count == row) - _actionRows.Add(new ActionRowBuilder().AddComponent(builtMenu)); - else - { - ActionRowBuilder actionRow; - if (_actionRows.Count > row) - actionRow = _actionRows.ElementAt(row); - else - { - actionRow = new ActionRowBuilder(); - _actionRows.Add(actionRow); - } - - if (actionRow.CanTakeComponent(builtMenu)) - actionRow.AddComponent(builtMenu); - else if (row < MaxActionRowCount) - WithSelectMenu(menu, row + 1); - else - throw new InvalidOperationException($"There is no more row to add a {nameof(builtMenu)}"); - } - } - - return this; - } - - /// - /// Adds a with specified parameters to the at the specific row. - /// If the row cannot accept the component then it will add it to a row that can. - /// - /// The label text for the newly added button. - /// The style of this newly added button. - /// A to be used with this button. - /// The custom id of the newly added button. - /// A URL to be used only if the is a Link. - /// Whether or not the newly created button is disabled. - /// The row the button should be placed on. - /// The current builder. - public ComponentBuilder WithButton( - string label = null, - string customId = null, - ButtonStyle style = ButtonStyle.Primary, - IEmote emote = null, - string url = null, - bool disabled = false, - int row = 0) - { - var button = new ButtonBuilder() - .WithLabel(label) - .WithStyle(style) - .WithEmote(emote) - .WithCustomId(customId) - .WithUrl(url) - .WithDisabled(disabled); - - return WithButton(button, row); - } - - /// - /// Adds a to the at the specific row. - /// If the row cannot accept the component then it will add it to a row that can. - /// - /// The button to add. - /// The row to add the button. - /// There is no more row to add a button. - /// must be less than . - /// The current builder. - public ComponentBuilder WithButton(ButtonBuilder button, int row = 0) - { - Preconditions.LessThan(row, MaxActionRowCount, nameof(row)); - - var builtButton = button.Build(); - - if (_actionRows == null) - { - _actionRows = new List - { - new ActionRowBuilder().AddComponent(builtButton) - }; - } - else - { - if (_actionRows.Count == row) - _actionRows.Add(new ActionRowBuilder().AddComponent(builtButton)); - else - { - ActionRowBuilder actionRow; - if (_actionRows.Count > row) - actionRow = _actionRows.ElementAt(row); - else - { - actionRow = new ActionRowBuilder(); - _actionRows.Add(actionRow); - } - - if (actionRow.CanTakeComponent(builtButton)) - actionRow.AddComponent(builtButton); - else if (row < MaxActionRowCount) - WithButton(button, row + 1); - else - throw new InvalidOperationException($"There is no more row to add a {nameof(button)}"); - } - } - - return this; - } - - /// - /// Adds a row to this component builder. - /// - /// The row to add. - /// The component builder contains the max amount of rows defined as . - /// The current builder. - public ComponentBuilder AddRow(ActionRowBuilder row) - { - _actionRows ??= new(); - - if (_actionRows.Count >= MaxActionRowCount) - throw new IndexOutOfRangeException("The max amount of rows has been reached"); - - ActionRows.Add(row); - return this; - } - - /// - /// Sets the rows of this component builder to a specified collection. - /// - /// The rows to set. - /// The collection contains more rows then is allowed by discord. - /// The current builder. - public ComponentBuilder WithRows(IEnumerable rows) - { - if (rows.Count() > MaxActionRowCount) - throw new IndexOutOfRangeException($"Cannot have more than {MaxActionRowCount} rows"); - - _actionRows = new List(rows); - return this; - } - - /// - /// Builds this builder into a used to send your components. - /// - /// A that can be sent with . - public MessageComponent Build() - { - if (_actionRows?.SelectMany(x => x.Components)?.Any(x => x.Type == ComponentType.TextInput) ?? false) - throw new ArgumentException("TextInputComponents are not allowed in messages.", nameof(ActionRows)); - - if (_actionRows?.Count > 0) - for (int i = 0; i < _actionRows?.Count; i++) - if (_actionRows[i]?.Components?.Count == 0) - _actionRows.RemoveAt(i); - - return _actionRows != null - ? new MessageComponent(_actionRows.Select(x => x.Build()).ToList()) - : MessageComponent.Empty; - } - } - - /// - /// Represents a class used to build Action rows. - /// - public class ActionRowBuilder - { - /// - /// The max amount of child components this row can hold. - /// - public const int MaxChildCount = 5; - - /// - /// Gets or sets the components inside this row. - /// - /// cannot be null. - /// count exceeds . - public List Components - { - get => _components; - set - { - if (value == null) - throw new ArgumentNullException(nameof(value), $"{nameof(Components)} cannot be null."); - - _components = value.Count switch - { - 0 => throw new ArgumentOutOfRangeException(nameof(value), "There must be at least 1 component in a row."), - > MaxChildCount => throw new ArgumentOutOfRangeException(nameof(value), $"Action row can only contain {MaxChildCount} child components!"), - _ => value - }; - } - } - - private List _components = new List(); - - /// - /// Adds a list of components to the current row. - /// - /// The list of components to add. - /// - /// The current builder. - public ActionRowBuilder WithComponents(List components) - { - Components = components; - return this; - } - - /// - /// Adds a component at the end of the current row. - /// - /// The component to add. - /// Components count reached - /// The current builder. - public ActionRowBuilder AddComponent(IMessageComponent component) - { - if (Components.Count >= MaxChildCount) - throw new InvalidOperationException($"Components count reached {MaxChildCount}"); - - Components.Add(component); - return this; - } - - /// - /// Adds a to the . - /// - /// The custom id of the menu. - /// The options of the menu. - /// The placeholder of the menu. - /// The min values of the placeholder. - /// The max values of the placeholder. - /// Whether or not the menu is disabled. - /// The type of the select menu. - /// Menus valid channel types (only for ) - /// The current builder. - public ActionRowBuilder WithSelectMenu(string customId, List options = null, - string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, - ComponentType type = ComponentType.SelectMenu, ChannelType[] channelTypes = null) - { - return WithSelectMenu(new SelectMenuBuilder() - .WithCustomId(customId) - .WithOptions(options) - .WithPlaceholder(placeholder) - .WithMaxValues(maxValues) - .WithMinValues(minValues) - .WithDisabled(disabled) - .WithType(type) - .WithChannelTypes(channelTypes)); - } - - /// - /// Adds a to the . - /// - /// The menu to add. - /// A Select Menu cannot exist in a pre-occupied ActionRow. - /// The current builder. - public ActionRowBuilder WithSelectMenu(SelectMenuBuilder menu) - { - if (menu.Options is not null && menu.Options.Distinct().Count() != menu.Options.Count) - throw new InvalidOperationException("Please make sure that there is no duplicates values."); - - var builtMenu = menu.Build(); - - if (Components.Count != 0) - throw new InvalidOperationException($"A Select Menu cannot exist in a pre-occupied ActionRow."); - - AddComponent(builtMenu); - - return this; - } - - /// - /// Adds a with specified parameters to the . - /// - /// The label text for the newly added button. - /// The style of this newly added button. - /// A to be used with this button. - /// The custom id of the newly added button. - /// A URL to be used only if the is a Link. - /// Whether or not the newly created button is disabled. - /// The current builder. - public ActionRowBuilder WithButton( - string label = null, - string customId = null, - ButtonStyle style = ButtonStyle.Primary, - IEmote emote = null, - string url = null, - bool disabled = false) - { - var button = new ButtonBuilder() - .WithLabel(label) - .WithStyle(style) - .WithEmote(emote) - .WithCustomId(customId) - .WithUrl(url) - .WithDisabled(disabled); - - return WithButton(button); - } - - /// - /// Adds a to the . - /// - /// The button to add. - /// Components count reached . - /// A button cannot be added to a row with a SelectMenu. - /// The current builder. - public ActionRowBuilder WithButton(ButtonBuilder button) - { - var builtButton = button.Build(); - - if (Components.Count >= 5) - throw new InvalidOperationException($"Components count reached {MaxChildCount}"); - - if (Components.Any(x => x.Type.IsSelectType())) - throw new InvalidOperationException($"A button cannot be added to a row with a SelectMenu"); - - AddComponent(builtButton); - - return this; - } - - /// - /// Builds the current builder to a that can be used within a - /// - /// A that can be used within a - public ActionRowComponent Build() - { - return new ActionRowComponent(_components); - } - - internal bool CanTakeComponent(IMessageComponent component) - { - switch (component.Type) - { - case ComponentType.ActionRow: - return false; - case ComponentType.Button: - if (Components.Any(x => x.Type.IsSelectType())) - return false; - else - return Components.Count < 5; - case ComponentType.SelectMenu: - case ComponentType.ChannelSelect: - case ComponentType.MentionableSelect: - case ComponentType.RoleSelect: - case ComponentType.UserSelect: - return Components.Count == 0; - default: - return false; - } - } - } - - /// - /// Represents a class used to build 's. - /// - public class ButtonBuilder - { - /// - /// The max length of a . - /// - public const int MaxButtonLabelLength = 80; - - /// - /// Gets or sets the label of the current button. - /// - /// length exceeds . - /// length exceeds . - public string Label - { - get => _label; - set => _label = value?.Length switch - { - > MaxButtonLabelLength => throw new ArgumentOutOfRangeException(nameof(value), $"Label length must be less or equal to {MaxButtonLabelLength}."), - 0 => throw new ArgumentOutOfRangeException(nameof(value), "Label length must be at least 1."), - _ => value - }; - } - - /// - /// Gets or sets the custom id of the current button. - /// - /// length exceeds - /// length subceeds 1. - public string CustomId - { - get => _customId; - set => _customId = value?.Length switch - { - > ComponentBuilder.MaxCustomIdLength => throw new ArgumentOutOfRangeException(nameof(value), $"Custom Id length must be less or equal to {ComponentBuilder.MaxCustomIdLength}."), - 0 => throw new ArgumentOutOfRangeException(nameof(value), "Custom Id length must be at least 1."), - _ => value - }; - } - - /// - /// Gets or sets the of the current button. - /// - public ButtonStyle Style { get; set; } - - /// - /// Gets or sets the of the current button. - /// - public IEmote Emote { get; set; } - - /// - /// Gets or sets the url of the current button. - /// - public string Url { get; set; } - - /// - /// Gets or sets whether the current button is disabled. - /// - public bool IsDisabled { get; set; } - - private string _label; - private string _customId; - - /// - /// Creates a new instance of a . - /// - public ButtonBuilder() { } - - /// - /// Creates a new instance of a . - /// - /// The label to use on the newly created link button. - /// The url of this button. - /// The custom ID of this button. - /// The custom ID of this button. - /// The emote of this button. - /// Disabled this button or not. - public ButtonBuilder(string label = null, string customId = null, ButtonStyle style = ButtonStyle.Primary, string url = null, IEmote emote = null, bool isDisabled = false) - { - CustomId = customId; - Style = style; - Url = url; - Label = label; - IsDisabled = isDisabled; - Emote = emote; - } - - /// - /// Creates a new instance of a from instance of a . - /// - public ButtonBuilder(ButtonComponent button) - { - CustomId = button.CustomId; - Style = button.Style; - Url = button.Url; - Label = button.Label; - IsDisabled = button.IsDisabled; - Emote = button.Emote; - } - - /// - /// Creates a button with the style. - /// - /// The label for this link button. - /// The url for this link button to go to. - /// The emote for this link button. - /// A builder with the newly created button. - public static ButtonBuilder CreateLinkButton(string label, string url, IEmote emote = null) - => new ButtonBuilder(label, null, ButtonStyle.Link, url, emote: emote); - - /// - /// Creates a button with the style. - /// - /// The label for this danger button. - /// The custom id for this danger button. - /// The emote for this danger button. - /// A builder with the newly created button. - public static ButtonBuilder CreateDangerButton(string label, string customId, IEmote emote = null) - => new ButtonBuilder(label, customId, ButtonStyle.Danger, emote: emote); - - /// - /// Creates a button with the style. - /// - /// The label for this primary button. - /// The custom id for this primary button. - /// The emote for this primary button. - /// A builder with the newly created button. - public static ButtonBuilder CreatePrimaryButton(string label, string customId, IEmote emote = null) - => new ButtonBuilder(label, customId, emote: emote); - - /// - /// Creates a button with the style. - /// - /// The label for this secondary button. - /// The custom id for this secondary button. - /// The emote for this secondary button. - /// A builder with the newly created button. - public static ButtonBuilder CreateSecondaryButton(string label, string customId, IEmote emote = null) - => new ButtonBuilder(label, customId, ButtonStyle.Secondary, emote: emote); - - /// - /// Creates a button with the style. - /// - /// The label for this success button. - /// The custom id for this success button. - /// The emote for this success button. - /// A builder with the newly created button. - public static ButtonBuilder CreateSuccessButton(string label, string customId, IEmote emote = null) - => new ButtonBuilder(label, customId, ButtonStyle.Success, emote: emote); - - /// - /// Sets the current buttons label to the specified text. - /// - /// The text for the label. - /// - /// The current builder. - public ButtonBuilder WithLabel(string label) - { - Label = label; - return this; - } - - /// - /// Sets the current buttons style. - /// - /// The style for this builders button. - /// The current builder. - public ButtonBuilder WithStyle(ButtonStyle style) - { - Style = style; - return this; - } - - /// - /// Sets the current buttons emote. - /// - /// The emote to use for the current button. - /// The current builder. - public ButtonBuilder WithEmote(IEmote emote) - { - Emote = emote; - return this; - } - - /// - /// Sets the current buttons url. - /// - /// The url to use for the current button. - /// The current builder. - public ButtonBuilder WithUrl(string url) - { - Url = url; - return this; - } - - /// - /// Sets the custom id of the current button. - /// - /// The id to use for the current button. - /// - /// The current builder. - public ButtonBuilder WithCustomId(string id) - { - CustomId = id; - return this; - } - - /// - /// Sets whether the current button is disabled. - /// - /// Whether the current button is disabled or not. - /// The current builder. - public ButtonBuilder WithDisabled(bool isDisabled) - { - IsDisabled = isDisabled; - return this; - } - - /// - /// Builds this builder into a to be used in a . - /// - /// A to be used in a . - /// A button must contain either a or a , but not both. - /// A button must have an or a . - /// A link button must contain a URL. - /// A URL must include a protocol (http or https). - /// A non-link button must contain a custom id - public ButtonComponent Build() - { - if (string.IsNullOrWhiteSpace(Label) && Emote == null) - throw new InvalidOperationException("A button must have an Emote or a label!"); - - if (!(string.IsNullOrWhiteSpace(Url) ^ string.IsNullOrWhiteSpace(CustomId))) - throw new InvalidOperationException("A button must contain either a URL or a CustomId, but not both!"); - - if (Style == 0) - throw new ArgumentException("A button must have a style.", nameof(Style)); - - if (Style == ButtonStyle.Link) - { - if (string.IsNullOrWhiteSpace(Url)) - throw new InvalidOperationException("Link buttons must have a link associated with them"); - UrlValidation.ValidateButton(Url); - } - else if (string.IsNullOrWhiteSpace(CustomId)) - throw new InvalidOperationException("Non-link buttons must have a custom id associated with them"); - - return new ButtonComponent(Style, Label, Emote, CustomId, Url, IsDisabled); - } - } - - /// - /// Represents a class used to build 's. - /// - public class SelectMenuBuilder - { - /// - /// The max length of a . - /// - public const int MaxPlaceholderLength = 100; - - /// - /// The maximum number of values for the and properties. - /// - public const int MaxValuesCount = 25; - - /// - /// The maximum number of options a can have. - /// - public const int MaxOptionCount = 25; - - /// - /// Gets or sets the custom id of the current select menu. - /// - /// length exceeds - /// length subceeds 1. - public string CustomId - { - get => _customId; - set => _customId = value?.Length switch - { - > ComponentBuilder.MaxCustomIdLength => throw new ArgumentOutOfRangeException(nameof(value), $"Custom Id length must be less or equal to {ComponentBuilder.MaxCustomIdLength}."), - 0 => throw new ArgumentOutOfRangeException(nameof(value), "Custom Id length must be at least 1."), - _ => value - }; - } - - /// - /// Gets or sets the type of the current select menu. - /// - /// Type must be a select menu type. - public ComponentType Type - { - get => _type; - set => _type = value.IsSelectType() - ? value - : throw new ArgumentException("Type must be a select menu type.", nameof(value)); - } - - /// - /// Gets or sets the placeholder text of the current select menu. - /// - /// length exceeds . - /// length subceeds 1. - public string Placeholder - { - get => _placeholder; - set => _placeholder = value?.Length switch - { - > MaxPlaceholderLength => throw new ArgumentOutOfRangeException(nameof(value), $"Placeholder length must be less or equal to {MaxPlaceholderLength}."), - 0 => throw new ArgumentOutOfRangeException(nameof(value), "Placeholder length must be at least 1."), - _ => value - }; - } - - /// - /// Gets or sets the minimum values of the current select menu. - /// - /// exceeds . - public int MinValues - { - get => _minValues; - set - { - Preconditions.AtMost(value, MaxValuesCount, nameof(MinValues)); - _minValues = value; - } - } - - /// - /// Gets or sets the maximum values of the current select menu. - /// - /// exceeds . - public int MaxValues - { - get => _maxValues; - set - { - Preconditions.AtMost(value, MaxValuesCount, nameof(MaxValues)); - _maxValues = value; - } - } - - /// - /// Gets or sets a collection of for this current select menu. - /// - /// count exceeds . - /// is null. - public List Options - { - get => _options; - set - { - if (value != null) - Preconditions.AtMost(value.Count, MaxOptionCount, nameof(Options)); - - _options = value; - } - } - - /// - /// Gets or sets whether the current menu is disabled. - /// - public bool IsDisabled { get; set; } - - /// - /// Gets or sets the menu's channel types (only valid on s). - /// - public List ChannelTypes { get; set; } - - public List DefaultValues - { - get => _defaultValues; - set - { - if (value != null) - Preconditions.AtMost(value.Count, MaxOptionCount, nameof(DefaultValues)); - - _defaultValues = value; - } - } - - private List _options = new List(); - private int _minValues = 1; - private int _maxValues = 1; - private string _placeholder; - private string _customId; - private ComponentType _type = ComponentType.SelectMenu; - private List _defaultValues = new(); - - /// - /// Creates a new instance of a . - /// - public SelectMenuBuilder() { } - - /// - /// Creates a new instance of a from instance of . - /// - public SelectMenuBuilder(SelectMenuComponent selectMenu) - { - Placeholder = selectMenu.Placeholder; - CustomId = selectMenu.CustomId; - MaxValues = selectMenu.MaxValues; - MinValues = selectMenu.MinValues; - IsDisabled = selectMenu.IsDisabled; - Options = selectMenu.Options? - .Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.IsDefault)) - .ToList(); - DefaultValues = selectMenu.DefaultValues?.ToList(); - } - - /// - /// Creates a new instance of a . - /// - /// The custom id of this select menu. - /// The options for this select menu. - /// The placeholder of this select menu. - /// The max values of this select menu. - /// The min values of this select menu. - /// Disabled this select menu or not. - /// The of this select menu. - /// The types of channels this menu can select (only valid on s) - public SelectMenuBuilder(string customId, List options = null, string placeholder = null, int maxValues = 1, int minValues = 1, - bool isDisabled = false, ComponentType type = ComponentType.SelectMenu, List channelTypes = null, List defaultValues = null) - { - CustomId = customId; - Options = options; - Placeholder = placeholder; - IsDisabled = isDisabled; - MaxValues = maxValues; - MinValues = minValues; - Type = type; - ChannelTypes = channelTypes ?? new(); - DefaultValues = defaultValues ?? new(); - } - - /// - /// Sets the field CustomId. - /// - /// The value to set the field CustomId to. - /// - /// - /// The current builder. - /// - public SelectMenuBuilder WithCustomId(string customId) - { - CustomId = customId; - return this; - } - - /// - /// Sets the field placeholder. - /// - /// The value to set the field placeholder to. - /// - /// - /// The current builder. - /// - public SelectMenuBuilder WithPlaceholder(string placeholder) - { - Placeholder = placeholder; - return this; - } - - /// - /// Sets the field minValues. - /// - /// The value to set the field minValues to. - /// - /// - /// The current builder. - /// - public SelectMenuBuilder WithMinValues(int minValues) - { - MinValues = minValues; - return this; - } - - /// - /// Sets the field maxValues. - /// - /// The value to set the field maxValues to. - /// - /// - /// The current builder. - /// - public SelectMenuBuilder WithMaxValues(int maxValues) - { - MaxValues = maxValues; - return this; - } - - /// - /// Sets the field options. - /// - /// The value to set the field options to. - /// - /// - /// The current builder. - /// - public SelectMenuBuilder WithOptions(List options) - { - Options = options; - return this; - } - - /// - /// Add one option to menu options. - /// - /// The option builder class containing the option properties. - /// Options count reached . - /// - /// The current builder. - /// - public SelectMenuBuilder AddOption(SelectMenuOptionBuilder option) - { - Options ??= new(); - - if (Options.Count >= MaxOptionCount) - throw new InvalidOperationException($"Options count reached {MaxOptionCount}."); - - Options.Add(option); - return this; - } - - /// - /// Add one option to menu options. - /// - /// The label for this option. - /// The value of this option. - /// The description of this option. - /// The emote of this option. - /// Render this option as selected by default or not. - /// Options count reached . - /// - /// The current builder. - /// - public SelectMenuBuilder AddOption(string label, string value, string description = null, IEmote emote = null, bool? isDefault = null) - { - AddOption(new SelectMenuOptionBuilder(label, value, description, emote, isDefault)); - return this; - } - - /// - /// Add one default value to menu options. - /// - /// The id of an entity to add. - /// The type of an entity to add. - /// Default values count reached . - /// - /// The current builder. - /// - public SelectMenuBuilder AddDefaultValue(ulong id, SelectDefaultValueType type) - => AddDefaultValue(new(id, type)); - - /// - /// Add one default value to menu options. - /// - /// The default value to add. - /// Default values count reached . - /// - /// The current builder. - /// - public SelectMenuBuilder AddDefaultValue(SelectMenuDefaultValue value) - { - if (DefaultValues.Count >= MaxOptionCount) - throw new InvalidOperationException($"Options count reached {MaxOptionCount}."); - - DefaultValues.Add(value); - return this; - } - - /// - /// Sets the field default values. - /// - /// The value to set the field default values to. - /// - /// The current builder. - /// - public SelectMenuBuilder WithDefaultValues(params SelectMenuDefaultValue[] defaultValues) - { - DefaultValues = defaultValues?.ToList(); - return this; - } - - /// - /// Sets whether the current menu is disabled. - /// - /// Whether the current menu is disabled or not. - /// - /// The current builder. - /// - public SelectMenuBuilder WithDisabled(bool isDisabled) - { - IsDisabled = isDisabled; - return this; - } - - /// - /// Sets the menu's current type. - /// - /// The type of the menu. - /// - /// The current builder. - /// - public SelectMenuBuilder WithType(ComponentType type) - { - Type = type; - return this; - } - - /// - /// Sets the menus valid channel types (only for s). - /// - /// The valid channel types of the menu. - /// - /// The current builder. - /// - public SelectMenuBuilder WithChannelTypes(List channelTypes) - { - ChannelTypes = channelTypes; - return this; - } - - /// - /// Sets the menus valid channel types (only for s). - /// - /// The valid channel types of the menu. - /// - /// The current builder. - /// - public SelectMenuBuilder WithChannelTypes(params ChannelType[] channelTypes) - { - ChannelTypes = channelTypes is null - ? ChannelTypeUtils.AllChannelTypes() - : channelTypes.ToList(); - return this; - } - - /// - /// Builds a - /// - /// The newly built - public SelectMenuComponent Build() - { - var options = Options?.Select(x => x.Build()).ToList(); - - return new SelectMenuComponent(CustomId, options, Placeholder, MinValues, MaxValues, IsDisabled, Type, ChannelTypes, DefaultValues); - } - } - - /// - /// Represents a class used to build 's. - /// - public class SelectMenuOptionBuilder - { - /// - /// The maximum length of a . - /// - public const int MaxSelectLabelLength = 100; - - /// - /// The maximum length of a . - /// - public const int MaxDescriptionLength = 100; - - /// - /// The maximum length of a . - /// - public const int MaxSelectValueLength = 100; - - /// - /// Gets or sets the label of the current select menu. - /// - /// length exceeds - /// length subceeds 1. - public string Label - { - get => _label; - set => _label = value?.Length switch - { - > MaxSelectLabelLength => throw new ArgumentOutOfRangeException(nameof(value), $"Label length must be less or equal to {MaxSelectLabelLength}."), - 0 => throw new ArgumentOutOfRangeException(nameof(value), "Label length must be at least 1."), - _ => value - }; - } - - /// - /// Gets or sets the value of the current select menu. - /// - /// length exceeds . - /// length subceeds 1. - public string Value - { - get => _value; - set => _value = value?.Length switch - { - > MaxSelectValueLength => throw new ArgumentOutOfRangeException(nameof(value), $"Value length must be less or equal to {MaxSelectValueLength}."), - 0 => throw new ArgumentOutOfRangeException(nameof(value), "Value length must be at least 1."), - _ => value - }; - } - - /// - /// Gets or sets this menu options description. - /// - /// length exceeds . - /// length subceeds 1. - public string Description - { - get => _description; - set => _description = value?.Length switch - { - > MaxDescriptionLength => throw new ArgumentOutOfRangeException(nameof(value), $"Description length must be less or equal to {MaxDescriptionLength}."), - 0 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be at least 1."), - _ => value - }; - } - - /// - /// Gets or sets the emote of this option. - /// - public IEmote Emote { get; set; } - - /// - /// Gets or sets the whether or not this option will render selected by default. - /// - public bool? IsDefault { get; set; } - - private string _label; - private string _value; - private string _description; - - /// - /// Creates a new instance of a . - /// - public SelectMenuOptionBuilder() { } - - /// - /// Creates a new instance of a . - /// - /// The label for this option. - /// The value of this option. - /// The description of this option. - /// The emote of this option. - /// Render this option as selected by default or not. - public SelectMenuOptionBuilder(string label, string value, string description = null, IEmote emote = null, bool? isDefault = null) - { - Label = label; - Value = value; - Description = description; - Emote = emote; - IsDefault = isDefault; - } - - /// - /// Creates a new instance of a from instance of a . - /// - public SelectMenuOptionBuilder(SelectMenuOption option) - { - Label = option.Label; - Value = option.Value; - Description = option.Description; - Emote = option.Emote; - IsDefault = option.IsDefault; - } - - /// - /// Sets the field label. - /// - /// The value to set the field label to. - /// - /// - /// The current builder. - /// - public SelectMenuOptionBuilder WithLabel(string label) - { - Label = label; - return this; - } - - /// - /// Sets the field value. - /// - /// The value to set the field value to. - /// - /// - /// The current builder. - /// - public SelectMenuOptionBuilder WithValue(string value) - { - Value = value; - return this; - } - - /// - /// Sets the field description. - /// - /// The value to set the field description to. - /// - /// - /// The current builder. - /// - public SelectMenuOptionBuilder WithDescription(string description) - { - Description = description; - return this; - } - - /// - /// Sets the field emote. - /// - /// The value to set the field emote to. - /// - /// The current builder. - /// - public SelectMenuOptionBuilder WithEmote(IEmote emote) - { - Emote = emote; - return this; - } - - /// - /// Sets the field default. - /// - /// The value to set the field default to. - /// - /// The current builder. - /// - public SelectMenuOptionBuilder WithDefault(bool isDefault) - { - IsDefault = isDefault; - return this; - } - - /// - /// Builds a . - /// - /// The newly built . - public SelectMenuOption Build() - { - if (string.IsNullOrWhiteSpace(Label)) - throw new ArgumentNullException(nameof(Label), "Option must have a label."); - - Preconditions.AtMost(Label.Length, MaxSelectLabelLength, nameof(Label), $"Label length must be less or equal to {MaxSelectLabelLength}."); - - if (string.IsNullOrWhiteSpace(Value)) - throw new ArgumentNullException(nameof(Value), "Option must have a value."); - - Preconditions.AtMost(Value.Length, MaxSelectValueLength, nameof(Value), $"Value length must be less or equal to {MaxSelectValueLength}."); - - return new SelectMenuOption(Label, Value, Description, Emote, IsDefault); - } - } - - public class TextInputBuilder - { - /// - /// The max length of a . - /// - public const int MaxPlaceholderLength = 100; - public const int LargestMaxLength = 4000; - - /// - /// Gets or sets the custom id of the current text input. - /// - /// length exceeds - /// length subceeds 1. - public string CustomId - { - get => _customId; - set => _customId = value?.Length switch - { - > ComponentBuilder.MaxCustomIdLength => throw new ArgumentOutOfRangeException(nameof(value), $"Custom Id length must be less or equal to {ComponentBuilder.MaxCustomIdLength}."), - 0 => throw new ArgumentOutOfRangeException(nameof(value), "Custom Id length must be at least 1."), - _ => value - }; - } - - /// - /// Gets or sets the style of the current text input. - /// - public TextInputStyle Style { get; set; } = TextInputStyle.Short; - - /// - /// Gets or sets the label of the current text input. - /// - public string Label { get; set; } - - /// - /// Gets or sets the placeholder of the current text input. - /// - /// is longer than characters - public string Placeholder - { - get => _placeholder; - set => _placeholder = (value?.Length ?? 0) <= MaxPlaceholderLength - ? value - : throw new ArgumentException($"Placeholder cannot have more than {MaxPlaceholderLength} characters."); - } - - /// - /// Gets or sets the minimum length of the current text input. - /// - /// is less than 0. - /// is greater than . - /// is greater than . - public int? MinLength - { - get => _minLength; - set - { - if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), $"MinLength must not be less than 0"); - if (value > LargestMaxLength) - throw new ArgumentOutOfRangeException(nameof(value), $"MinLength must not be greater than {LargestMaxLength}"); - if (value > (MaxLength ?? LargestMaxLength)) - throw new ArgumentOutOfRangeException(nameof(value), $"MinLength must be less than MaxLength"); - _minLength = value; - } - } - - /// - /// Gets or sets the maximum length of the current text input. - /// - /// is less than 0. - /// is greater than . - /// is less than . - public int? MaxLength - { - get => _maxLength; - set - { - if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), $"MaxLength must not be less than 0"); - if (value > LargestMaxLength) - throw new ArgumentOutOfRangeException(nameof(value), $"MaxLength most not be greater than {LargestMaxLength}"); - if (value < (MinLength ?? -1)) - throw new ArgumentOutOfRangeException(nameof(value), $"MaxLength must be greater than MinLength ({MinLength})"); - _maxLength = value; - } - } - - /// - /// Gets or sets whether the user is required to input text. - /// - public bool? Required { get; set; } - - /// - /// Gets or sets the default value of the text input. - /// - /// .Length is less than 0. - /// - /// .Length is greater than or . - /// - /// - /// is and contains a new line character. - /// - public string Value - { - get => _value; - set - { - if (value?.Length > (MaxLength ?? LargestMaxLength)) - throw new ArgumentOutOfRangeException(nameof(value), $"Value must not be longer than {MaxLength ?? LargestMaxLength}."); - if (value?.Length < (MinLength ?? 0)) - throw new ArgumentOutOfRangeException(nameof(value), $"Value must not be shorter than {MinLength}"); - - _value = value; - } - } - - private string _customId; - private int? _maxLength; - private int? _minLength; - private string _placeholder; - private string _value; - - /// - /// Creates a new instance of a . - /// - /// The text input's label. - /// The text input's style. - /// The text input's custom id. - /// The text input's placeholder. - /// The text input's minimum length. - /// The text input's maximum length. - /// The text input's required value. - public TextInputBuilder(string label, string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, - int? minLength = null, int? maxLength = null, bool? required = null, string value = null) - { - Label = label; - Style = style; - CustomId = customId; - Placeholder = placeholder; - MinLength = minLength; - MaxLength = maxLength; - Required = required; - Value = value; - } - - /// - /// Creates a new instance of a . - /// - public TextInputBuilder() - { - - } - - /// - /// Sets the label of the current builder. - /// - /// The value to set. - /// The current builder. - public TextInputBuilder WithLabel(string label) - { - Label = label; - return this; - } - - /// - /// Sets the style of the current builder. - /// - /// The value to set. - /// The current builder. - public TextInputBuilder WithStyle(TextInputStyle style) - { - Style = style; - return this; - } - - /// - /// Sets the custom id of the current builder. - /// - /// The value to set. - /// The current builder. - public TextInputBuilder WithCustomId(string customId) - { - CustomId = customId; - return this; - } - - /// - /// Sets the placeholder of the current builder. - /// - /// The value to set. - /// The current builder. - public TextInputBuilder WithPlaceholder(string placeholder) - { - Placeholder = placeholder; - return this; - } - - /// - /// Sets the value of the current builder. - /// - /// The value to set - /// The current builder. - public TextInputBuilder WithValue(string value) - { - Value = value; - return this; - } - - /// - /// Sets the minimum length of the current builder. - /// - /// The value to set. - /// The current builder. - public TextInputBuilder WithMinLength(int minLength) - { - MinLength = minLength; - return this; - } - - /// - /// Sets the maximum length of the current builder. - /// - /// The value to set. - /// The current builder. - public TextInputBuilder WithMaxLength(int maxLength) - { - MaxLength = maxLength; - return this; - } - - /// - /// Sets the required value of the current builder. - /// - /// The value to set. - /// The current builder. - public TextInputBuilder WithRequired(bool required) - { - Required = required; - return this; - } - - public TextInputComponent Build() - { - if (string.IsNullOrEmpty(CustomId)) - throw new ArgumentException("TextInputComponents must have a custom id.", nameof(CustomId)); - if (string.IsNullOrWhiteSpace(Label)) - throw new ArgumentException("TextInputComponents must have a label.", nameof(Label)); - if (Style == TextInputStyle.Short && Value?.Contains('\n') == true) - throw new ArgumentException($"Value must not contain new line characters when style is {TextInputStyle.Short}.", nameof(Value)); - - return new TextInputComponent(CustomId, Label, Placeholder, MinLength, MaxLength, Style, Required, Value); - } - } -} diff --git a/src/Discord.Net.Rest/API/Common/ButtonComponent.cs b/src/Discord.Net.Rest/API/Common/ButtonComponent.cs index 7f737d7a..f820fc02 100644 --- a/src/Discord.Net.Rest/API/Common/ButtonComponent.cs +++ b/src/Discord.Net.Rest/API/Common/ButtonComponent.cs @@ -25,6 +25,9 @@ namespace Discord.API [JsonProperty("disabled")] public Optional Disabled { get; set; } + [JsonProperty("sku_id")] + public Optional SkuId { get; set; } + public ButtonComponent() { } public ButtonComponent(Discord.ButtonComponent c) @@ -35,6 +38,7 @@ namespace Discord.API CustomId = c.CustomId; Url = c.Url; Disabled = c.IsDisabled; + SkuId = c.SkuId ?? Optional.Unspecified; if (c.Emote != null) { diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index fd9aa07c..c0da0228 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -181,7 +181,8 @@ namespace Discord.Rest : null, parsed.CustomId.GetValueOrDefault(), parsed.Url.GetValueOrDefault(), - parsed.Disabled.GetValueOrDefault()); + parsed.Disabled.GetValueOrDefault(), + parsed.SkuId.ToNullable()); } case ComponentType.SelectMenu or ComponentType.ChannelSelect or ComponentType.RoleSelect or ComponentType.MentionableSelect or ComponentType.UserSelect: { diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 930c6ca1..a9ada8de 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -215,7 +215,8 @@ namespace Discord.WebSocket : null, parsed.CustomId.GetValueOrDefault(), parsed.Url.GetValueOrDefault(), - parsed.Disabled.GetValueOrDefault()); + parsed.Disabled.GetValueOrDefault(), + parsed.SkuId.ToNullable()); } case ComponentType.SelectMenu: {