[Feature] premium buttonz (#2933)

This commit is contained in:
Mihail Gribkov
2024-06-14 00:43:11 +03:00
committed by GitHub
parent f7f29d5cc8
commit 531b5eb3b7
12 changed files with 1825 additions and 1718 deletions

View File

@@ -0,0 +1,198 @@
using Discord.Utils;
using System.Collections.Generic;
using System.Linq;
using System;
namespace Discord;
/// <summary>
/// Represents a class used to build Action rows.
/// </summary>
public class ActionRowBuilder
{
/// <summary>
/// The max amount of child components this row can hold.
/// </summary>
public const int MaxChildCount = 5;
/// <summary>
/// Gets or sets the components inside this row.
/// </summary>
/// <exception cref="ArgumentNullException" accessor="set"><see cref="Components"/> cannot be null.</exception>
/// <exception cref="ArgumentException" accessor="set"><see cref="Components"/> count exceeds <see cref="MaxChildCount"/>.</exception>
public List<IMessageComponent> 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<IMessageComponent> _components = new List<IMessageComponent>();
/// <summary>
/// Adds a list of components to the current row.
/// </summary>
/// <param name="components">The list of components to add.</param>
/// <inheritdoc cref="Components"/>
/// <returns>The current builder.</returns>
public ActionRowBuilder WithComponents(List<IMessageComponent> components)
{
Components = components;
return this;
}
/// <summary>
/// Adds a component at the end of the current row.
/// </summary>
/// <param name="component">The component to add.</param>
/// <exception cref="InvalidOperationException">Components count reached <see cref="MaxChildCount"/></exception>
/// <returns>The current builder.</returns>
public ActionRowBuilder AddComponent(IMessageComponent component)
{
if (Components.Count >= MaxChildCount)
throw new InvalidOperationException($"Components count reached {MaxChildCount}");
Components.Add(component);
return this;
}
/// <summary>
/// Adds a <see cref="SelectMenuBuilder"/> to the <see cref="ActionRowBuilder"/>.
/// </summary>
/// <param name="customId">The custom id of the menu.</param>
/// <param name="options">The options of the menu.</param>
/// <param name="placeholder">The placeholder of the menu.</param>
/// <param name="minValues">The min values of the placeholder.</param>
/// <param name="maxValues">The max values of the placeholder.</param>
/// <param name="disabled">Whether or not the menu is disabled.</param>
/// <param name="type">The type of the select menu.</param>
/// <param name="channelTypes">Menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>)</param>
/// <returns>The current builder.</returns>
public ActionRowBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> 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));
}
/// <summary>
/// Adds a <see cref="SelectMenuBuilder"/> to the <see cref="ActionRowBuilder"/>.
/// </summary>
/// <param name="menu">The menu to add.</param>
/// <exception cref="InvalidOperationException">A Select Menu cannot exist in a pre-occupied ActionRow.</exception>
/// <returns>The current builder.</returns>
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;
}
/// <summary>
/// Adds a <see cref="ButtonBuilder"/> with specified parameters to the <see cref="ActionRowBuilder"/>.
/// </summary>
/// <param name="label">The label text for the newly added button.</param>
/// <param name="style">The style of this newly added button.</param>
/// <param name="emote">A <see cref="IEmote"/> to be used with this button.</param>
/// <param name="customId">The custom id of the newly added button.</param>
/// <param name="url">A URL to be used only if the <see cref="ButtonStyle"/> is a Link.</param>
/// <param name="disabled">Whether or not the newly created button is disabled.</param>
/// <returns>The current builder.</returns>
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);
}
/// <summary>
/// Adds a <see cref="ButtonBuilder"/> to the <see cref="ActionRowBuilder"/>.
/// </summary>
/// <param name="button">The button to add.</param>
/// <exception cref="InvalidOperationException">Components count reached <see cref="MaxChildCount"/>.</exception>
/// <exception cref="InvalidOperationException">A button cannot be added to a row with a SelectMenu.</exception>
/// <returns>The current builder.</returns>
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;
}
/// <summary>
/// Builds the current builder to a <see cref="ActionRowComponent"/> that can be used within a <see cref="ComponentBuilder"/>
/// </summary>
/// <returns>A <see cref="ActionRowComponent"/> that can be used within a <see cref="ComponentBuilder"/></returns>
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;
}
}
}

View File

@@ -0,0 +1,320 @@
using Discord.Utils;
using System;
namespace Discord;
/// <summary>
/// Represents a class used to build <see cref="ButtonComponent"/>'s.
/// </summary>
public class ButtonBuilder
{
/// <summary>
/// The max length of a <see cref="ButtonComponent.Label"/>.
/// </summary>
public const int MaxButtonLabelLength = 80;
/// <summary>
/// Gets or sets the label of the current button.
/// </summary>
/// <exception cref="ArgumentException" accessor="set"><see cref="Label"/> length exceeds <see cref="MaxButtonLabelLength"/>.</exception>
/// <exception cref="ArgumentException" accessor="set"><see cref="Label"/> length exceeds <see cref="MaxButtonLabelLength"/>.</exception>
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
};
}
/// <summary>
/// Gets or sets the custom id of the current button.
/// </summary>
/// <exception cref="ArgumentException" accessor="set"><see cref="CustomId"/> length exceeds <see cref="ComponentBuilder.MaxCustomIdLength"/></exception>
/// <exception cref="ArgumentException" accessor="set"><see cref="CustomId"/> length subceeds 1.</exception>
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
};
}
/// <summary>
/// Gets or sets the <see cref="ButtonStyle"/> of the current button.
/// </summary>
public ButtonStyle Style { get; set; }
/// <summary>
/// Gets or sets the <see cref="IEmote"/> of the current button.
/// </summary>
public IEmote Emote { get; set; }
/// <summary>
/// Gets or sets the url of the current button.
/// </summary>
public string Url { get; set; }
/// <summary>
/// Gets or sets whether the current button is disabled.
/// </summary>
public bool IsDisabled { get; set; }
/// <summary>
/// Gets or sets the id of the sku associated with the current button.
/// </summary>
/// <remarks>
/// <see langword="null"/> if the button is not of type <see cref="ButtonStyle.Premium"/>.
/// </remarks>
public ulong? SkuId { get; set; }
private string _label;
private string _customId;
/// <summary>
/// Creates a new instance of a <see cref="ButtonBuilder"/>.
/// </summary>
public ButtonBuilder() { }
/// <summary>
/// Creates a new instance of a <see cref="ButtonBuilder"/>.
/// </summary>
/// <param name="label">The label to use on the newly created link button.</param>
/// <param name="url">The url of this button.</param>
/// <param name="customId">The custom ID of this button.</param>
/// <param name="style">The custom ID of this button.</param>
/// <param name="emote">The emote of this button.</param>
/// <param name="isDisabled">Disabled this button or not.</param>
/// <param name="skuId">The sku id of this button.</param>
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;
}
/// <summary>
/// Creates a new instance of a <see cref="ButtonBuilder"/> from instance of a <see cref="ButtonComponent"/>.
/// </summary>
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;
}
/// <summary>
/// Creates a button with the <see cref="ButtonStyle.Link"/> style.
/// </summary>
/// <param name="label">The label for this link button.</param>
/// <param name="url">The url for this link button to go to.</param>
/// <param name="emote">The emote for this link button.</param>
/// <returns>A builder with the newly created button.</returns>
public static ButtonBuilder CreateLinkButton(string label, string url, IEmote emote = null)
=> new (label, null, ButtonStyle.Link, url, emote: emote);
/// <summary>
/// Creates a button with the <see cref="ButtonStyle.Danger"/> style.
/// </summary>
/// <param name="label">The label for this danger button.</param>
/// <param name="customId">The custom id for this danger button.</param>
/// <param name="emote">The emote for this danger button.</param>
/// <returns>A builder with the newly created button.</returns>
public static ButtonBuilder CreateDangerButton(string label, string customId, IEmote emote = null)
=> new (label, customId, ButtonStyle.Danger, emote: emote);
/// <summary>
/// Creates a button with the <see cref="ButtonStyle.Primary"/> style.
/// </summary>
/// <param name="label">The label for this primary button.</param>
/// <param name="customId">The custom id for this primary button.</param>
/// <param name="emote">The emote for this primary button.</param>
/// <returns>A builder with the newly created button.</returns>
public static ButtonBuilder CreatePrimaryButton(string label, string customId, IEmote emote = null)
=> new (label, customId, emote: emote);
/// <summary>
/// Creates a button with the <see cref="ButtonStyle.Secondary"/> style.
/// </summary>
/// <param name="label">The label for this secondary button.</param>
/// <param name="customId">The custom id for this secondary button.</param>
/// <param name="emote">The emote for this secondary button.</param>
/// <returns>A builder with the newly created button.</returns>
public static ButtonBuilder CreateSecondaryButton(string label, string customId, IEmote emote = null)
=> new (label, customId, ButtonStyle.Secondary, emote: emote);
/// <summary>
/// Creates a button with the <see cref="ButtonStyle.Success"/> style.
/// </summary>
/// <param name="label">The label for this success button.</param>
/// <param name="customId">The custom id for this success button.</param>
/// <param name="emote">The emote for this success button.</param>
/// <returns>A builder with the newly created button.</returns>
public static ButtonBuilder CreateSuccessButton(string label, string customId, IEmote emote = null)
=> new (label, customId, ButtonStyle.Success, emote: emote);
/// <summary>
/// Creates a button with the <see cref="ButtonStyle.Premium"/> style.
/// </summary>
/// <param name="label">The label for this premium button.</param>
/// <param name="skuId">The sku id for this premium button.</param>
/// <param name="emote">The emote for this premium button.</param>
/// <returns>A builder with the newly created button.</returns>
public static ButtonBuilder CreatePremiumButton(string label, ulong skuId, IEmote emote = null)
=> new (label, style: ButtonStyle.Success, emote: emote, skuId: skuId);
/// <summary>
/// Sets the current buttons label to the specified text.
/// </summary>
/// <param name="label">The text for the label.</param>
/// <inheritdoc cref="Label"/>
/// <returns>The current builder.</returns>
public ButtonBuilder WithLabel(string label)
{
Label = label;
return this;
}
/// <summary>
/// Sets the current buttons style.
/// </summary>
/// <param name="style">The style for this builders button.</param>
/// <returns>The current builder.</returns>
public ButtonBuilder WithStyle(ButtonStyle style)
{
Style = style;
return this;
}
/// <summary>
/// Sets the current buttons emote.
/// </summary>
/// <param name="emote">The emote to use for the current button.</param>
/// <returns>The current builder.</returns>
public ButtonBuilder WithEmote(IEmote emote)
{
Emote = emote;
return this;
}
/// <summary>
/// Sets the current buttons url.
/// </summary>
/// <param name="url">The url to use for the current button.</param>
/// <returns>The current builder.</returns>
public ButtonBuilder WithUrl(string url)
{
Url = url;
return this;
}
/// <summary>
/// Sets the custom id of the current button.
/// </summary>
/// <param name="id">The id to use for the current button.</param>
/// <inheritdoc cref="CustomId"/>
/// <returns>The current builder.</returns>
public ButtonBuilder WithCustomId(string id)
{
CustomId = id;
return this;
}
/// <summary>
/// Sets whether the current button is disabled.
/// </summary>
/// <param name="isDisabled">Whether the current button is disabled or not.</param>
/// <returns>The current builder.</returns>
public ButtonBuilder WithDisabled(bool isDisabled)
{
IsDisabled = isDisabled;
return this;
}
/// <summary>
/// Sets the sku id of the current button.
/// </summary>
/// <param name="skuId">The id of the sku</param>
/// <returns>The current builder.</returns>
public ButtonBuilder WithSkuId(ulong? skuId)
{
SkuId = skuId;
return this;
}
/// <summary>
/// Builds this builder into a <see cref="ButtonComponent"/> to be used in a <see cref="ComponentBuilder"/>.
/// </summary>
/// <returns>A <see cref="ButtonComponent"/> to be used in a <see cref="ComponentBuilder"/>.</returns>
/// <exception cref="InvalidOperationException">A button must contain either a <see cref="Url"/> or a <see cref="CustomId"/>, but not both.</exception>
/// <exception cref="InvalidOperationException">A button must have an <see cref="Emote"/> or a <see cref="Label"/>.</exception>
/// <exception cref="InvalidOperationException">A link button must contain a URL.</exception>
/// <exception cref="InvalidOperationException">A URL must include a protocol (http or https).</exception>
/// <exception cref="InvalidOperationException">A non-link button must contain a custom id</exception>
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);
}
}

View File

@@ -0,0 +1,332 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Discord;
/// <summary>
/// Represents a builder for creating a <see cref="MessageComponent"/>.
/// </summary>
public class ComponentBuilder
{
/// <summary>
/// The max length of a <see cref="ButtonComponent.CustomId"/>.
/// </summary>
public const int MaxCustomIdLength = 100;
/// <summary>
/// The max amount of rows a message can have.
/// </summary>
public const int MaxActionRowCount = 5;
/// <summary>
/// Gets or sets the Action Rows for this Component Builder.
/// </summary>
/// <exception cref="ArgumentNullException" accessor="set"><see cref="ActionRows"/> cannot be null.</exception>
/// <exception cref="ArgumentException" accessor="set"><see cref="ActionRows"/> count exceeds <see cref="MaxActionRowCount"/>.</exception>
public List<ActionRowBuilder> 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<ActionRowBuilder> _actionRows;
/// <summary>
/// Creates a new builder from a message.
/// </summary>
/// <param name="message">The message to create the builder from.</param>
/// <returns>The newly created builder.</returns>
public static ComponentBuilder FromMessage(IMessage message)
=> FromComponents(message.Components);
/// <summary>
/// Creates a new builder from the provided list of components.
/// </summary>
/// <param name="components">The components to create the builder from.</param>
/// <returns>The newly created builder.</returns>
public static ComponentBuilder FromComponents(IReadOnlyCollection<IMessageComponent> 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;
}
}
/// <summary>
/// Removes all components of the given type from the <see cref="ComponentBuilder"/>.
/// </summary>
/// <param name="t">The <see cref="ComponentType"/> to remove.</param>
/// <returns>The current builder.</returns>
public ComponentBuilder RemoveComponentsOfType(ComponentType t)
{
this.ActionRows.ForEach(ar => ar.Components.RemoveAll(c => c.Type == t));
return this;
}
/// <summary>
/// Removes a component from the <see cref="ComponentBuilder"/>.
/// </summary>
/// <param name="customId">The custom id of the component.</param>
/// <returns>The current builder.</returns>
public ComponentBuilder RemoveComponent(string customId)
{
this.ActionRows.ForEach(ar => ar.Components.RemoveAll(c => c.CustomId == customId));
return this;
}
/// <summary>
/// Removes a Link Button from the <see cref="ComponentBuilder"/> based on its URL.
/// </summary>
/// <param name="url">The URL of the Link Button.</param>
/// <returns>The current builder.</returns>
public ComponentBuilder RemoveButtonByURL(string url)
{
this.ActionRows.ForEach(ar => ar.Components.RemoveAll(c => c is ButtonComponent b && b.Url == url));
return this;
}
/// <summary>
/// Adds a <see cref="SelectMenuBuilder"/> to the <see cref="ComponentBuilder"/> at the specific row.
/// If the row cannot accept the component then it will add it to a row that can.
/// </summary>
/// <param name="customId">The custom id of the menu.</param>
/// <param name="options">The options of the menu.</param>
/// <param name="placeholder">The placeholder of the menu.</param>
/// <param name="minValues">The min values of the placeholder.</param>
/// <param name="maxValues">The max values of the placeholder.</param>
/// <param name="disabled">Whether or not the menu is disabled.</param>
/// <param name="row">The row to add the menu to.</param>
/// <param name="type">The type of the select menu.</param>
/// <param name="channelTypes">Menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>)</param>
/// <returns></returns>
public ComponentBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> 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);
}
/// <summary>
/// Adds a <see cref="SelectMenuBuilder"/> to the <see cref="ComponentBuilder"/> at the specific row.
/// If the row cannot accept the component then it will add it to a row that can.
/// </summary>
/// <param name="menu">The menu to add.</param>
/// <param name="row">The row to attempt to add this component on.</param>
/// <exception cref="InvalidOperationException">There is no more row to add a menu.</exception>
/// <exception cref="ArgumentException"><paramref name="row"/> must be less than <see cref="MaxActionRowCount"/>.</exception>
/// <returns>The current builder.</returns>
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<ActionRowBuilder>
{
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;
}
/// <summary>
/// Adds a <see cref="ButtonBuilder"/> with specified parameters to the <see cref="ComponentBuilder"/> at the specific row.
/// If the row cannot accept the component then it will add it to a row that can.
/// </summary>
/// <param name="label">The label text for the newly added button.</param>
/// <param name="style">The style of this newly added button.</param>
/// <param name="emote">A <see cref="IEmote"/> to be used with this button.</param>
/// <param name="customId">The custom id of the newly added button.</param>
/// <param name="url">A URL to be used only if the <see cref="ButtonStyle"/> is a Link.</param>
/// <param name="disabled">Whether or not the newly created button is disabled.</param>
/// <param name="row">The row the button should be placed on.</param>
/// <param name="skuId">The id of the sku associated with the current button.</param>
/// <returns>The current builder.</returns>
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);
}
/// <summary>
/// Adds a <see cref="ButtonBuilder"/> to the <see cref="ComponentBuilder"/> at the specific row.
/// If the row cannot accept the component then it will add it to a row that can.
/// </summary>
/// <param name="button">The button to add.</param>
/// <param name="row">The row to add the button.</param>
/// <exception cref="InvalidOperationException">There is no more row to add a button.</exception>
/// <exception cref="ArgumentException"><paramref name="row"/> must be less than <see cref="MaxActionRowCount"/>.</exception>
/// <returns>The current builder.</returns>
public ComponentBuilder WithButton(ButtonBuilder button, int row = 0)
{
Preconditions.LessThan(row, MaxActionRowCount, nameof(row));
var builtButton = button.Build();
if (_actionRows == null)
{
_actionRows = new List<ActionRowBuilder>
{
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;
}
/// <summary>
/// Adds a row to this component builder.
/// </summary>
/// <param name="row">The row to add.</param>
/// <exception cref="IndexOutOfRangeException">The component builder contains the max amount of rows defined as <see cref="MaxActionRowCount"/>.</exception>
/// <returns>The current builder.</returns>
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;
}
/// <summary>
/// Sets the rows of this component builder to a specified collection.
/// </summary>
/// <param name="rows">The rows to set.</param>
/// <exception cref="IndexOutOfRangeException">The collection contains more rows then is allowed by discord.</exception>
/// <returns>The current builder.</returns>
public ComponentBuilder WithRows(IEnumerable<ActionRowBuilder> rows)
{
if (rows.Count() > MaxActionRowCount)
throw new IndexOutOfRangeException($"Cannot have more than {MaxActionRowCount} rows");
_actionRows = new List<ActionRowBuilder>(rows);
return this;
}
/// <summary>
/// Builds this builder into a <see cref="MessageComponent"/> used to send your components.
/// </summary>
/// <returns>A <see cref="MessageComponent"/> that can be sent with <see cref="IMessageChannel.SendMessageAsync"/>.</returns>
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;
}
}

View File

@@ -0,0 +1,406 @@
using Discord.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Discord;
/// <summary>
/// Represents a class used to build <see cref="SelectMenuComponent"/>'s.
/// </summary>
public class SelectMenuBuilder
{
/// <summary>
/// The max length of a <see cref="SelectMenuComponent.Placeholder"/>.
/// </summary>
public const int MaxPlaceholderLength = 100;
/// <summary>
/// The maximum number of values for the <see cref="SelectMenuComponent.MinValues"/> and <see cref="SelectMenuComponent.MaxValues"/> properties.
/// </summary>
public const int MaxValuesCount = 25;
/// <summary>
/// The maximum number of options a <see cref="SelectMenuComponent"/> can have.
/// </summary>
public const int MaxOptionCount = 25;
/// <summary>
/// Gets or sets the custom id of the current select menu.
/// </summary>
/// <exception cref="ArgumentException" accessor="set"><see cref="CustomId"/> length exceeds <see cref="ComponentBuilder.MaxCustomIdLength"/></exception>
/// <exception cref="ArgumentException" accessor="set"><see cref="CustomId"/> length subceeds 1.</exception>
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
};
}
/// <summary>
/// Gets or sets the type of the current select menu.
/// </summary>
/// <exception cref="ArgumentException">Type must be a select menu type.</exception>
public ComponentType Type
{
get => _type;
set => _type = value.IsSelectType()
? value
: throw new ArgumentException("Type must be a select menu type.", nameof(value));
}
/// <summary>
/// Gets or sets the placeholder text of the current select menu.
/// </summary>
/// <exception cref="ArgumentException" accessor="set"><see cref="Placeholder"/> length exceeds <see cref="MaxPlaceholderLength"/>.</exception>
/// <exception cref="ArgumentException" accessor="set"><see cref="Placeholder"/> length subceeds 1.</exception>
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
};
}
/// <summary>
/// Gets or sets the minimum values of the current select menu.
/// </summary>
/// <exception cref="ArgumentException" accessor="set"><see cref="MinValues"/> exceeds <see cref="MaxValuesCount"/>.</exception>
public int MinValues
{
get => _minValues;
set
{
Preconditions.AtMost(value, MaxValuesCount, nameof(MinValues));
_minValues = value;
}
}
/// <summary>
/// Gets or sets the maximum values of the current select menu.
/// </summary>
/// <exception cref="ArgumentException" accessor="set"><see cref="MaxValues"/> exceeds <see cref="MaxValuesCount"/>.</exception>
public int MaxValues
{
get => _maxValues;
set
{
Preconditions.AtMost(value, MaxValuesCount, nameof(MaxValues));
_maxValues = value;
}
}
/// <summary>
/// Gets or sets a collection of <see cref="SelectMenuOptionBuilder"/> for this current select menu.
/// </summary>
/// <exception cref="ArgumentException" accessor="set"><see cref="Options"/> count exceeds <see cref="MaxOptionCount"/>.</exception>
/// <exception cref="ArgumentNullException" accessor="set"><see cref="Options"/> is null.</exception>
public List<SelectMenuOptionBuilder> Options
{
get => _options;
set
{
if (value != null)
Preconditions.AtMost(value.Count, MaxOptionCount, nameof(Options));
_options = value;
}
}
/// <summary>
/// Gets or sets whether the current menu is disabled.
/// </summary>
public bool IsDisabled { get; set; }
/// <summary>
/// Gets or sets the menu's channel types (only valid on <see cref="ComponentType.ChannelSelect"/>s).
/// </summary>
public List<ChannelType> ChannelTypes { get; set; }
public List<SelectMenuDefaultValue> DefaultValues
{
get => _defaultValues;
set
{
if (value != null)
Preconditions.AtMost(value.Count, MaxOptionCount, nameof(DefaultValues));
_defaultValues = value;
}
}
private List<SelectMenuOptionBuilder> _options = new List<SelectMenuOptionBuilder>();
private int _minValues = 1;
private int _maxValues = 1;
private string _placeholder;
private string _customId;
private ComponentType _type = ComponentType.SelectMenu;
private List<SelectMenuDefaultValue> _defaultValues = new();
/// <summary>
/// Creates a new instance of a <see cref="SelectMenuBuilder"/>.
/// </summary>
public SelectMenuBuilder() { }
/// <summary>
/// Creates a new instance of a <see cref="SelectMenuBuilder"/> from instance of <see cref="SelectMenuComponent"/>.
/// </summary>
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();
}
/// <summary>
/// Creates a new instance of a <see cref="SelectMenuBuilder"/>.
/// </summary>
/// <param name="customId">The custom id of this select menu.</param>
/// <param name="options">The options for this select menu.</param>
/// <param name="placeholder">The placeholder of this select menu.</param>
/// <param name="maxValues">The max values of this select menu.</param>
/// <param name="minValues">The min values of this select menu.</param>
/// <param name="isDisabled">Disabled this select menu or not.</param>
/// <param name="type">The <see cref="ComponentType"/> of this select menu.</param>
/// <param name="channelTypes">The types of channels this menu can select (only valid on <see cref="ComponentType.ChannelSelect"/>s)</param>
public SelectMenuBuilder(string customId, List<SelectMenuOptionBuilder> options = null, string placeholder = null, int maxValues = 1, int minValues = 1,
bool isDisabled = false, ComponentType type = ComponentType.SelectMenu, List<ChannelType> channelTypes = null, List<SelectMenuDefaultValue> defaultValues = null)
{
CustomId = customId;
Options = options;
Placeholder = placeholder;
IsDisabled = isDisabled;
MaxValues = maxValues;
MinValues = minValues;
Type = type;
ChannelTypes = channelTypes ?? new();
DefaultValues = defaultValues ?? new();
}
/// <summary>
/// Sets the field CustomId.
/// </summary>
/// <param name="customId">The value to set the field CustomId to.</param>
/// <inheritdoc cref="CustomId"/>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithCustomId(string customId)
{
CustomId = customId;
return this;
}
/// <summary>
/// Sets the field placeholder.
/// </summary>
/// <param name="placeholder">The value to set the field placeholder to.</param>
/// <inheritdoc cref="Placeholder"/>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithPlaceholder(string placeholder)
{
Placeholder = placeholder;
return this;
}
/// <summary>
/// Sets the field minValues.
/// </summary>
/// <param name="minValues">The value to set the field minValues to.</param>
/// <inheritdoc cref="MinValues"/>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithMinValues(int minValues)
{
MinValues = minValues;
return this;
}
/// <summary>
/// Sets the field maxValues.
/// </summary>
/// <param name="maxValues">The value to set the field maxValues to.</param>
/// <inheritdoc cref="MaxValues"/>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithMaxValues(int maxValues)
{
MaxValues = maxValues;
return this;
}
/// <summary>
/// Sets the field options.
/// </summary>
/// <param name="options">The value to set the field options to.</param>
/// <inheritdoc cref="Options"/>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithOptions(List<SelectMenuOptionBuilder> options)
{
Options = options;
return this;
}
/// <summary>
/// Add one option to menu options.
/// </summary>
/// <param name="option">The option builder class containing the option properties.</param>
/// <exception cref="InvalidOperationException">Options count reached <see cref="MaxOptionCount"/>.</exception>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder AddOption(SelectMenuOptionBuilder option)
{
Options ??= new();
if (Options.Count >= MaxOptionCount)
throw new InvalidOperationException($"Options count reached {MaxOptionCount}.");
Options.Add(option);
return this;
}
/// <summary>
/// Add one option to menu options.
/// </summary>
/// <param name="label">The label for this option.</param>
/// <param name="value">The value of this option.</param>
/// <param name="description">The description of this option.</param>
/// <param name="emote">The emote of this option.</param>
/// <param name="isDefault">Render this option as selected by default or not.</param>
/// <exception cref="InvalidOperationException">Options count reached <see cref="MaxOptionCount"/>.</exception>
/// <returns>
/// The current builder.
/// </returns>
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;
}
/// <summary>
/// Add one default value to menu options.
/// </summary>
/// <param name="id">The id of an entity to add.</param>
/// <param name="type">The type of an entity to add.</param>
/// <exception cref="InvalidOperationException">Default values count reached <see cref="MaxOptionCount"/>.</exception>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder AddDefaultValue(ulong id, SelectDefaultValueType type)
=> AddDefaultValue(new(id, type));
/// <summary>
/// Add one default value to menu options.
/// </summary>
/// <param name="value">The default value to add.</param>
/// <exception cref="InvalidOperationException">Default values count reached <see cref="MaxOptionCount"/>.</exception>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder AddDefaultValue(SelectMenuDefaultValue value)
{
if (DefaultValues.Count >= MaxOptionCount)
throw new InvalidOperationException($"Options count reached {MaxOptionCount}.");
DefaultValues.Add(value);
return this;
}
/// <summary>
/// Sets the field default values.
/// </summary>
/// <param name="defaultValues">The value to set the field default values to.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithDefaultValues(params SelectMenuDefaultValue[] defaultValues)
{
DefaultValues = defaultValues?.ToList();
return this;
}
/// <summary>
/// Sets whether the current menu is disabled.
/// </summary>
/// <param name="isDisabled">Whether the current menu is disabled or not.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithDisabled(bool isDisabled)
{
IsDisabled = isDisabled;
return this;
}
/// <summary>
/// Sets the menu's current type.
/// </summary>
/// <param name="type">The type of the menu.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithType(ComponentType type)
{
Type = type;
return this;
}
/// <summary>
/// Sets the menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>s).
/// </summary>
/// <param name="channelTypes">The valid channel types of the menu.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithChannelTypes(List<ChannelType> channelTypes)
{
ChannelTypes = channelTypes;
return this;
}
/// <summary>
/// Sets the menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>s).
/// </summary>
/// <param name="channelTypes">The valid channel types of the menu.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithChannelTypes(params ChannelType[] channelTypes)
{
ChannelTypes = channelTypes is null
? ChannelTypeUtils.AllChannelTypes()
: channelTypes.ToList();
return this;
}
/// <summary>
/// Builds a <see cref="SelectMenuComponent"/>
/// </summary>
/// <returns>The newly built <see cref="SelectMenuComponent"/></returns>
public SelectMenuComponent Build()
{
var options = Options?.Select(x => x.Build()).ToList();
return new SelectMenuComponent(CustomId, options, Placeholder, MinValues, MaxValues, IsDisabled, Type, ChannelTypes, DefaultValues);
}
}

View File

@@ -0,0 +1,207 @@
using System;
namespace Discord;
/// <summary>
/// Represents a class used to build <see cref="SelectMenuOption"/>'s.
/// </summary>
public class SelectMenuOptionBuilder
{
/// <summary>
/// The maximum length of a <see cref="SelectMenuOption.Label"/>.
/// </summary>
public const int MaxSelectLabelLength = 100;
/// <summary>
/// The maximum length of a <see cref="SelectMenuOption.Description"/>.
/// </summary>
public const int MaxDescriptionLength = 100;
/// <summary>
/// The maximum length of a <see cref="SelectMenuOption.Value"/>.
/// </summary>
public const int MaxSelectValueLength = 100;
/// <summary>
/// Gets or sets the label of the current select menu.
/// </summary>
/// <exception cref="ArgumentException" accessor="set"><see cref="Label"/> length exceeds <see cref="MaxSelectLabelLength"/></exception>
/// <exception cref="ArgumentException" accessor="set"><see cref="Label"/> length subceeds 1.</exception>
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
};
}
/// <summary>
/// Gets or sets the value of the current select menu.
/// </summary>
/// <exception cref="ArgumentException" accessor="set"><see cref="Value"/> length exceeds <see cref="MaxSelectValueLength"/>.</exception>
/// <exception cref="ArgumentException" accessor="set"><see cref="Value"/> length subceeds 1.</exception>
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
};
}
/// <summary>
/// Gets or sets this menu options description.
/// </summary>
/// <exception cref="ArgumentException" accessor="set"><see cref="Description"/> length exceeds <see cref="MaxDescriptionLength"/>.</exception>
/// <exception cref="ArgumentException" accessor="set"><see cref="Description"/> length subceeds 1.</exception>
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
};
}
/// <summary>
/// Gets or sets the emote of this option.
/// </summary>
public IEmote Emote { get; set; }
/// <summary>
/// Gets or sets the whether or not this option will render selected by default.
/// </summary>
public bool? IsDefault { get; set; }
private string _label;
private string _value;
private string _description;
/// <summary>
/// Creates a new instance of a <see cref="SelectMenuOptionBuilder"/>.
/// </summary>
public SelectMenuOptionBuilder() { }
/// <summary>
/// Creates a new instance of a <see cref="SelectMenuOptionBuilder"/>.
/// </summary>
/// <param name="label">The label for this option.</param>
/// <param name="value">The value of this option.</param>
/// <param name="description">The description of this option.</param>
/// <param name="emote">The emote of this option.</param>
/// <param name="isDefault">Render this option as selected by default or not.</param>
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;
}
/// <summary>
/// Creates a new instance of a <see cref="SelectMenuOptionBuilder"/> from instance of a <see cref="SelectMenuOption"/>.
/// </summary>
public SelectMenuOptionBuilder(SelectMenuOption option)
{
Label = option.Label;
Value = option.Value;
Description = option.Description;
Emote = option.Emote;
IsDefault = option.IsDefault;
}
/// <summary>
/// Sets the field label.
/// </summary>
/// <param name="label">The value to set the field label to.</param>
/// <inheritdoc cref="Label"/>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuOptionBuilder WithLabel(string label)
{
Label = label;
return this;
}
/// <summary>
/// Sets the field value.
/// </summary>
/// <param name="value">The value to set the field value to.</param>
/// <inheritdoc cref="Value"/>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuOptionBuilder WithValue(string value)
{
Value = value;
return this;
}
/// <summary>
/// Sets the field description.
/// </summary>
/// <param name="description">The value to set the field description to.</param>
/// <inheritdoc cref="Description"/>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuOptionBuilder WithDescription(string description)
{
Description = description;
return this;
}
/// <summary>
/// Sets the field emote.
/// </summary>
/// <param name="emote">The value to set the field emote to.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuOptionBuilder WithEmote(IEmote emote)
{
Emote = emote;
return this;
}
/// <summary>
/// Sets the field default.
/// </summary>
/// <param name="isDefault">The value to set the field default to.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuOptionBuilder WithDefault(bool isDefault)
{
IsDefault = isDefault;
return this;
}
/// <summary>
/// Builds a <see cref="SelectMenuOption"/>.
/// </summary>
/// <returns>The newly built <see cref="SelectMenuOption"/>.</returns>
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);
}
}

View File

@@ -0,0 +1,262 @@
using System;
using System.Linq;
namespace Discord;
/// <summary>
/// Represents a builder for creating a <see cref="TextInputComponent"/>.
/// </summary>
public class TextInputBuilder
{
/// <summary>
/// The max length of a <see cref="TextInputComponent.Placeholder"/>.
/// </summary>
public const int MaxPlaceholderLength = 100;
public const int LargestMaxLength = 4000;
/// <summary>
/// Gets or sets the custom id of the current text input.
/// </summary>
/// <exception cref="ArgumentException" accessor="set"><see cref="CustomId"/> length exceeds <see cref="ComponentBuilder.MaxCustomIdLength"/></exception>
/// <exception cref="ArgumentException" accessor="set"><see cref="CustomId"/> length subceeds 1.</exception>
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
};
}
/// <summary>
/// Gets or sets the style of the current text input.
/// </summary>
public TextInputStyle Style { get; set; } = TextInputStyle.Short;
/// <summary>
/// Gets or sets the label of the current text input.
/// </summary>
public string Label { get; set; }
/// <summary>
/// Gets or sets the placeholder of the current text input.
/// </summary>
/// <exception cref="ArgumentException"><see cref="Placeholder"/> is longer than <see cref="MaxPlaceholderLength"/> characters</exception>
public string Placeholder
{
get => _placeholder;
set => _placeholder = (value?.Length ?? 0) <= MaxPlaceholderLength
? value
: throw new ArgumentException($"Placeholder cannot have more than {MaxPlaceholderLength} characters.");
}
/// <summary>
/// Gets or sets the minimum length of the current text input.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException"><see cref="MinLength"/> is less than 0.</exception>
/// <exception cref="ArgumentOutOfRangeException"><see cref="MinLength"/> is greater than <see cref="LargestMaxLength"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException"><see cref="MinLength"/> is greater than <see cref="MaxLength"/>.</exception>
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;
}
}
/// <summary>
/// Gets or sets the maximum length of the current text input.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException"><see cref="MaxLength"/> is less than 0.</exception>
/// <exception cref="ArgumentOutOfRangeException"><see cref="MaxLength"/> is greater than <see cref="LargestMaxLength"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException"><see cref="MaxLength"/> is less than <see cref="MinLength"/>.</exception>
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;
}
}
/// <summary>
/// Gets or sets whether the user is required to input text.
/// </summary>
public bool? Required { get; set; }
/// <summary>
/// Gets or sets the default value of the text input.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException"><see cref="Value"/>.Length is less than 0.</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <see cref="Value"/>.Length is greater than <see cref="LargestMaxLength"/> or <see cref="MaxLength"/>.
/// </exception>
/// <exception cref="ArgumentException">
/// <see cref="Style"/> is <see cref="TextInputStyle.Short"/> and <see cref="Value"/> contains a new line character.
/// </exception>
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;
/// <summary>
/// Creates a new instance of a <see cref="TextInputBuilder"/>.
/// </summary>
/// <param name="label">The text input's label.</param>
/// <param name="style">The text input's style.</param>
/// <param name="customId">The text input's custom id.</param>
/// <param name="placeholder">The text input's placeholder.</param>
/// <param name="minLength">The text input's minimum length.</param>
/// <param name="maxLength">The text input's maximum length.</param>
/// <param name="required">The text input's required value.</param>
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;
}
/// <summary>
/// Creates a new instance of a <see cref="TextInputBuilder"/>.
/// </summary>
public TextInputBuilder()
{
}
/// <summary>
/// Sets the label of the current builder.
/// </summary>
/// <param name="label">The value to set.</param>
/// <returns>The current builder. </returns>
public TextInputBuilder WithLabel(string label)
{
Label = label;
return this;
}
/// <summary>
/// Sets the style of the current builder.
/// </summary>
/// <param name="style">The value to set.</param>
/// <returns>The current builder. </returns>
public TextInputBuilder WithStyle(TextInputStyle style)
{
Style = style;
return this;
}
/// <summary>
/// Sets the custom id of the current builder.
/// </summary>
/// <param name="customId">The value to set.</param>
/// <returns>The current builder. </returns>
public TextInputBuilder WithCustomId(string customId)
{
CustomId = customId;
return this;
}
/// <summary>
/// Sets the placeholder of the current builder.
/// </summary>
/// <param name="placeholder">The value to set.</param>
/// <returns>The current builder. </returns>
public TextInputBuilder WithPlaceholder(string placeholder)
{
Placeholder = placeholder;
return this;
}
/// <summary>
/// Sets the value of the current builder.
/// </summary>
/// <param name="value">The value to set</param>
/// <returns>The current builder.</returns>
public TextInputBuilder WithValue(string value)
{
Value = value;
return this;
}
/// <summary>
/// Sets the minimum length of the current builder.
/// </summary>
/// <param name="minLength">The value to set.</param>
/// <returns>The current builder. </returns>
public TextInputBuilder WithMinLength(int minLength)
{
MinLength = minLength;
return this;
}
/// <summary>
/// Sets the maximum length of the current builder.
/// </summary>
/// <param name="maxLength">The value to set.</param>
/// <returns>The current builder. </returns>
public TextInputBuilder WithMaxLength(int maxLength)
{
MaxLength = maxLength;
return this;
}
/// <summary>
/// Sets the required value of the current builder.
/// </summary>
/// <param name="required">The value to set.</param>
/// <returns>The current builder. </returns>
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);
}
}

View File

@@ -1,10 +1,10 @@
namespace Discord namespace Discord;
/// <summary>
/// Represents a <see cref="IMessageComponent"/> Button.
/// </summary>
public class ButtonComponent : IMessageComponent
{ {
/// <summary>
/// Represents a <see cref="IMessageComponent"/> Button.
/// </summary>
public class ButtonComponent : IMessageComponent
{
/// <inheritdoc/> /// <inheritdoc/>
public ComponentType Type => ComponentType.Button; public ComponentType Type => ComponentType.Button;
@@ -39,6 +39,14 @@ namespace Discord
/// </summary> /// </summary>
public bool IsDisabled { get; } public bool IsDisabled { get; }
/// <summary>
/// Gets the id of the sku associated with the current button.
/// </summary>
/// <remarks>
/// <see langword="null"/> if the button is not of type <see cref="ButtonStyle.Premium"/>.
/// </remarks>
public ulong? SkuId { get; }
/// <summary> /// <summary>
/// Turns this button into a button builder. /// Turns this button into a button builder.
/// </summary> /// </summary>
@@ -46,9 +54,9 @@ namespace Discord
/// A newly created button builder with the same properties as this button. /// A newly created button builder with the same properties as this button.
/// </returns> /// </returns>
public ButtonBuilder ToBuilder() public ButtonBuilder ToBuilder()
=> new ButtonBuilder(Label, CustomId, Style, Url, Emote, IsDisabled); => new (Label, CustomId, Style, Url, Emote, IsDisabled);
internal ButtonComponent(ButtonStyle style, string label, IEmote emote, string customId, string url, bool isDisabled) internal ButtonComponent(ButtonStyle style, string label, IEmote emote, string customId, string url, bool isDisabled, ulong? skuId)
{ {
Style = style; Style = style;
Label = label; Label = label;
@@ -56,6 +64,6 @@ namespace Discord
CustomId = customId; CustomId = customId;
Url = url; Url = url;
IsDisabled = isDisabled; IsDisabled = isDisabled;
} SkuId = skuId;
} }
} }

View File

@@ -1,33 +1,37 @@
namespace Discord namespace Discord;
/// <summary>
/// Represents different styles to use with buttons. You can see an example of the different styles at <see href="https://discord.com/developers/docs/interactions/message-components#buttons-button-styles"/>
/// </summary>
public enum ButtonStyle
{ {
/// <summary> /// <summary>
/// Represents different styles to use with buttons. You can see an example of the different styles at <see href="https://discord.com/developers/docs/interactions/message-components#buttons-button-styles"/> /// A Blurple button.
/// </summary>
public enum ButtonStyle
{
/// <summary>
/// A Blurple button
/// </summary> /// </summary>
Primary = 1, Primary = 1,
/// <summary> /// <summary>
/// A Grey (or gray) button /// A Grey (or gray) button.
/// </summary> /// </summary>
Secondary = 2, Secondary = 2,
/// <summary> /// <summary>
/// A Green button /// A Green button.
/// </summary> /// </summary>
Success = 3, Success = 3,
/// <summary> /// <summary>
/// A Red button /// A Red button.
/// </summary> /// </summary>
Danger = 4, Danger = 4,
/// <summary> /// <summary>
/// A <see cref="Secondary"/> button with a little popup box indicating that this button is a link. /// A <see cref="Secondary"/> button with a little popup box indicating that this button is a link.
/// </summary> /// </summary>
Link = 5 Link = 5,
}
/// <summary>
/// A gradient button, opens a product's details modal.
/// </summary>
Premium = 6,
} }

View File

@@ -25,6 +25,9 @@ namespace Discord.API
[JsonProperty("disabled")] [JsonProperty("disabled")]
public Optional<bool> Disabled { get; set; } public Optional<bool> Disabled { get; set; }
[JsonProperty("sku_id")]
public Optional<ulong> SkuId { get; set; }
public ButtonComponent() { } public ButtonComponent() { }
public ButtonComponent(Discord.ButtonComponent c) public ButtonComponent(Discord.ButtonComponent c)
@@ -35,6 +38,7 @@ namespace Discord.API
CustomId = c.CustomId; CustomId = c.CustomId;
Url = c.Url; Url = c.Url;
Disabled = c.IsDisabled; Disabled = c.IsDisabled;
SkuId = c.SkuId ?? Optional<ulong>.Unspecified;
if (c.Emote != null) if (c.Emote != null)
{ {

View File

@@ -181,7 +181,8 @@ namespace Discord.Rest
: null, : null,
parsed.CustomId.GetValueOrDefault(), parsed.CustomId.GetValueOrDefault(),
parsed.Url.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: case ComponentType.SelectMenu or ComponentType.ChannelSelect or ComponentType.RoleSelect or ComponentType.MentionableSelect or ComponentType.UserSelect:
{ {

View File

@@ -215,7 +215,8 @@ namespace Discord.WebSocket
: null, : null,
parsed.CustomId.GetValueOrDefault(), parsed.CustomId.GetValueOrDefault(),
parsed.Url.GetValueOrDefault(), parsed.Url.GetValueOrDefault(),
parsed.Disabled.GetValueOrDefault()); parsed.Disabled.GetValueOrDefault(),
parsed.SkuId.ToNullable());
} }
case ComponentType.SelectMenu: case ComponentType.SelectMenu:
{ {