Browse Source

Added select menus

This comit addds support for select menu message components defined at https://discord.com/developers/docs/interactions/message-components#select-menus.

Added 2 new converters: InteractionConverter and MessageComponentConverter. These converters are responsible for resolving the API type given some condition.

Added new core interface for representing Interaction Data, this is used in part with the InteractionConverter to parse the data to the respective types.

Updated the way gateway converting is done with interactions. The Interaction payload was moved from the API.Gateway namespace to API, as the interaction object is not websocket specific. The socket entities no longer try to parse the interaction models data to a JToken to desterilize since the data is now parsed to the correct model by the contract resolver.
pull/1923/head
quin lynch 4 years ago
parent
commit
3f86e61047
27 changed files with 1359 additions and 78 deletions
  1. +335
    -12
      src/Discord.Net.Core/Discord.Net.Core.xml
  2. +1
    -1
      src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs
  3. +13
    -0
      src/Discord.Net.Core/Entities/Interactions/IDiscordInteractionData.cs
  4. +2
    -2
      src/Discord.Net.Core/Entities/Interactions/Message Components/ActionRowComponent.cs
  5. +507
    -22
      src/Discord.Net.Core/Entities/Interactions/Message Components/ComponentBuilder.cs
  6. +6
    -1
      src/Discord.Net.Core/Entities/Interactions/Message Components/ComponentType.cs
  7. +51
    -0
      src/Discord.Net.Core/Entities/Interactions/Message Components/SelectMenu.cs
  8. +48
    -0
      src/Discord.Net.Core/Entities/Interactions/Message Components/SelectMenuOption.cs
  9. +15
    -3
      src/Discord.Net.Rest/API/Common/ActionRowComponent.cs
  10. +1
    -1
      src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs
  11. +1
    -1
      src/Discord.Net.Rest/API/Common/ButtonComponent.cs
  12. +4
    -5
      src/Discord.Net.Rest/API/Common/Interaction.cs
  13. +4
    -1
      src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs
  14. +42
    -0
      src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs
  15. +58
    -0
      src/Discord.Net.Rest/API/Common/SelectMenuOption.cs
  16. +44
    -10
      src/Discord.Net.Rest/Entities/Messages/RestMessage.cs
  17. +4
    -0
      src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs
  18. +67
    -0
      src/Discord.Net.Rest/Net/Converters/InteractionConverter.cs
  19. +44
    -0
      src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs
  20. +5
    -0
      src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml
  21. +1
    -1
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  22. +2
    -4
      src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponent.cs
  23. +6
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponentData.cs
  24. +2
    -2
      src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommand.cs
  25. +2
    -2
      src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs
  26. +44
    -10
      src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs
  27. +50
    -0
      src/Discord.Net.Webhook/Discord.Net.Webhook.xml

+ 335
- 12
src/Discord.Net.Core/Discord.Net.Core.xml View File

@@ -4132,6 +4132,11 @@
read-only property, always 1.
</summary>
</member>
<member name="T:Discord.IDiscordInteractionData">
<summary>
Represents an interface used to specify classes that they are a vaild dataype of a <see cref="T:Discord.IDiscordInteraction"/> class.
</summary>
</member>
<member name="T:Discord.InteractionResponseType">
<summary>
The response type for an <see cref="T:Discord.IDiscordInteraction"/>.
@@ -4277,6 +4282,16 @@
Represents a builder for creating a <see cref="T:Discord.MessageComponent"/>.
</summary>
</member>
<member name="F:Discord.ComponentBuilder.MaxLabelLength">
<summary>
The max length of a <see cref="P:Discord.ButtonComponent.Label"/>.
</summary>
</member>
<member name="F:Discord.ComponentBuilder.MaxCustomIdLength">
<summary>
The max length of a <see cref="P:Discord.ButtonComponent.CustomId"/>.
</summary>
</member>
<member name="F:Discord.ComponentBuilder.MaxActionRowCount">
<summary>
The max amount of rows a message can have.
@@ -4287,6 +4302,36 @@
Gets or sets the Action Rows for this Component Builder.
</summary>
</member>
<member name="M:Discord.ComponentBuilder.WithSelectMenu(System.String,System.String,System.Collections.Generic.List{Discord.SelectMenuOptionBuilder},System.String,System.Int32,System.Int32,System.Int32)">
<summary>
Adds a <see cref="T:Discord.SelectMenuBuilder"/> to the first row, if the first row cannot
accept the component then it will add it to a row that can
</summary>
<param name="label">The label of the menu.</param>
<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="row">The row to add the menu to.</param>
<returns></returns>
</member>
<member name="M:Discord.ComponentBuilder.WithSelectMenu(Discord.SelectMenuBuilder)">
<summary>
Adds a <see cref="T:Discord.SelectMenuBuilder"/> to the first row, if the first row cannot
accept the component then it will add it to a row that can
</summary>
<param name="menu">The menu to add</param>
<returns>The current builder.</returns>
</member>
<member name="M:Discord.ComponentBuilder.WithSelectMenu(Discord.SelectMenuBuilder,System.Int32)">
<summary>
Adds a <see cref="T:Discord.SelectMenuBuilder"/> to the current builder at the specific row.
</summary>
<param name="menu">The menu to add.</param>
<param name="row">The row to attempt to add this component on.</param>
<returns>The current builder.</returns>
</member>
<member name="M:Discord.ComponentBuilder.WithButton(System.String,System.String,Discord.ButtonStyle,Discord.IEmote,System.String,System.Boolean,System.Int32)">
<summary>
Adds a button to the specified row.
@@ -4336,14 +4381,14 @@
Gets or sets the components inside this row.
</summary>
</member>
<member name="M:Discord.ActionRowBuilder.WithComponents(System.Collections.Generic.List{Discord.ButtonComponent})">
<member name="M:Discord.ActionRowBuilder.WithComponents(System.Collections.Generic.List{Discord.IMessageComponent})">
<summary>
Adds a list of components to the current row.
</summary>
<param name="components">The list of components to add.</param>
<returns>The current builder.</returns>
</member>
<member name="M:Discord.ActionRowBuilder.WithComponent(Discord.ButtonComponent)">
<member name="M:Discord.ActionRowBuilder.WithComponent(Discord.IMessageComponent)">
<summary>
Adds a component at the end of the current row.
</summary>
@@ -4363,16 +4408,6 @@
Represents a class used to build <see cref="T:Discord.ButtonComponent"/>'s.
</summary>
</member>
<member name="F:Discord.ButtonBuilder.MaxLabelLength">
<summary>
The max length of a <see cref="P:Discord.ButtonComponent.Label"/>.
</summary>
</member>
<member name="F:Discord.ButtonBuilder.MaxCustomIdLength">
<summary>
The max length of a <see cref="P:Discord.ButtonComponent.CustomId"/>.
</summary>
</member>
<member name="P:Discord.ButtonBuilder.Label">
<summary>
Gets or sets the label of the current button.
@@ -4493,6 +4528,226 @@
<exception cref="T:System.InvalidOperationException">A button cannot contain a URL and a CustomId.</exception>
<exception cref="T:System.ArgumentException">A button must have an Emote or a label.</exception>
</member>
<member name="T:Discord.SelectMenuBuilder">
<summary>
Represents a class used to build <see cref="T:Discord.SelectMenu"/>'s.
</summary>
</member>
<member name="F:Discord.SelectMenuBuilder.MaxPlaceholderLength">
<summary>
The max length of a <see cref="P:Discord.SelectMenu.Placeholder"/>.
</summary>
</member>
<member name="F:Discord.SelectMenuBuilder.MaxValuesCount">
<summary>
The maximum number of values for the <see cref="P:Discord.SelectMenu.MinValues"/> and <see cref="P:Discord.SelectMenu.MaxValues"/> properties.
</summary>
</member>
<member name="F:Discord.SelectMenuBuilder.MaxOptionCount">
<summary>
The maximum number of options a <see cref="T:Discord.SelectMenu"/> can have.
</summary>
</member>
<member name="P:Discord.SelectMenuBuilder.Label">
<summary>
Gets or sets the label of the current select menu.
</summary>
</member>
<member name="P:Discord.SelectMenuBuilder.CustomId">
<summary>
Gets or sets the custom id of the current select menu.
</summary>
</member>
<member name="P:Discord.SelectMenuBuilder.Placeholder">
<summary>
Gets or sets the placeholder text of the current select menu.
</summary>
</member>
<member name="P:Discord.SelectMenuBuilder.MinValues">
<summary>
Gets or sets the minimum values of the current select menu.
</summary>
</member>
<member name="P:Discord.SelectMenuBuilder.MaxValues">
<summary>
Gets or sets the maximum values of the current select menu.
</summary>
</member>
<member name="P:Discord.SelectMenuBuilder.Options">
<summary>
Gets or sets a collection of <see cref="T:Discord.SelectMenuOptionBuilder"/> for this current select menu.
</summary>
</member>
<member name="M:Discord.SelectMenuBuilder.#ctor">
<summary>
Creates a new instance of a <see cref="T:Discord.SelectMenuBuilder"/>.
</summary>
</member>
<member name="M:Discord.SelectMenuBuilder.#ctor(System.String,System.Collections.Generic.List{Discord.SelectMenuOptionBuilder})">
<summary>
Creates a new instance of a <see cref="T:Discord.SelectMenuBuilder"/>.
</summary>
<param name="customId">The custom id of this select menu.</param>
<param name="options">The options for this select menu.</param>
</member>
<member name="M:Discord.SelectMenuBuilder.WithLabel(System.String)">
<summary>
Sets the field label.
</summary>
<param name="label">The value to set the field label to.</param>
<returns>
The current builder.
</returns>
</member>
<member name="M:Discord.SelectMenuBuilder.WithCustomId(System.String)">
<summary>
Sets the field CustomId.
</summary>
<param name="customId">The value to set the field CustomId to.</param>
<returns>
The current builder.
</returns>
</member>
<member name="M:Discord.SelectMenuBuilder.WithPlaceholder(System.String)">
<summary>
Sets the field placeholder.
</summary>
<param name="placeholder">The value to set the field placeholder to.</param>
<returns>
The current builder.
</returns>
</member>
<member name="M:Discord.SelectMenuBuilder.WithMinValues(System.Int32)">
<summary>
Sets the field minValues.
</summary>
<param name="minValues">The value to set the field minValues to.</param>
<returns>
The current builder.
</returns>
</member>
<member name="M:Discord.SelectMenuBuilder.WithMaxValues(System.Int32)">
<summary>
Sets the field maxValues.
</summary>
<param name="maxValues">The value to set the field maxValues to.</param>
<returns>
The current builder.
</returns>
</member>
<member name="M:Discord.SelectMenuBuilder.WithOptions(System.Collections.Generic.List{Discord.SelectMenuOptionBuilder})">
<summary>
Sets the field options.
</summary>
<param name="options">The value to set the field options to.</param>
<returns>
The current builder.
</returns>
</member>
<member name="M:Discord.SelectMenuBuilder.Build">
<summary>
Builds a <see cref="T:Discord.SelectMenu"/>
</summary>
<returns>The newly built <see cref="T:Discord.SelectMenu"/></returns>
</member>
<member name="T:Discord.SelectMenuOptionBuilder">
<summary>
Represents a class used to build <see cref="T:Discord.SelectMenuOption"/>'s.
</summary>
</member>
<member name="F:Discord.SelectMenuOptionBuilder.MaxDescriptionLength">
<summary>
The maximum length of a <see cref="P:Discord.SelectMenuOption.Description"/>.
</summary>
</member>
<member name="P:Discord.SelectMenuOptionBuilder.Label">
<summary>
Gets or sets the label of the current select menu.
</summary>
</member>
<member name="P:Discord.SelectMenuOptionBuilder.Value">
<summary>
Gets or sets the custom id of the current select menu.
</summary>
</member>
<member name="P:Discord.SelectMenuOptionBuilder.Description">
<summary>
Gets or sets this menu options description.
</summary>
</member>
<member name="P:Discord.SelectMenuOptionBuilder.Emote">
<summary>
Gets or sets the emote of this option.
</summary>
</member>
<member name="P:Discord.SelectMenuOptionBuilder.Default">
<summary>
Gets or sets the whether or not this option will render selected by default.
</summary>
</member>
<member name="M:Discord.SelectMenuOptionBuilder.#ctor">
<summary>
Creates a new instance of a <see cref="T:Discord.SelectMenuOptionBuilder"/>.
</summary>
</member>
<member name="M:Discord.SelectMenuOptionBuilder.#ctor(System.String,System.String)">
<summary>
Creates a new instance of a <see cref="T:Discord.SelectMenuOptionBuilder"/>.
</summary>
<param name="label">The label for this option.</param>
<param name="value">The value of this option.</param>
</member>
<member name="M:Discord.SelectMenuOptionBuilder.WithLabel(System.String)">
<summary>
Sets the field label.
</summary>
<param name="label">The value to set the field label to.</param>
<returns>
The current builder.
</returns>
</member>
<member name="M:Discord.SelectMenuOptionBuilder.WithValue(System.String)">
<summary>
Sets the field value.
</summary>
<param name="value">The value to set the field value to.</param>
<returns>
The current builder.
</returns>
</member>
<member name="M:Discord.SelectMenuOptionBuilder.WithDescription(System.String)">
<summary>
Sets the field description.
</summary>
<param name="description">The value to set the field description to.</param>
<returns>
The current builder.
</returns>
</member>
<member name="M:Discord.SelectMenuOptionBuilder.WithEmote(Discord.IEmote)">
<summary>
Sets the field emote.
</summary>
<param name="emote">The value to set the field emote to.</param>
<returns>
The current builder.
</returns>
</member>
<member name="M:Discord.SelectMenuOptionBuilder.WithDefault(System.Boolean)">
<summary>
Sets the field default.
</summary>
<param name="defaultValue">The value to set the field default to.</param>
<returns>
The current builder.
</returns>
</member>
<member name="M:Discord.SelectMenuOptionBuilder.Build">
<summary>
Builds a <see cref="T:Discord.SelectMenuOption"/>.
</summary>
<returns>The newly built <see cref="T:Discord.SelectMenuOption"/>.</returns>
</member>
<member name="T:Discord.ComponentType">
<summary>
Represents a type of a component
@@ -4508,6 +4763,11 @@
A clickable button
</summary>
</member>
<member name="F:Discord.ComponentType.SelectMenu">
<summary>
A select menu for picking from choices
</summary>
</member>
<member name="P:Discord.IMessageComponent.Type">
<summary>
The <see cref="T:Discord.ComponentType"/> of this Message Component.
@@ -4528,6 +4788,69 @@
Returns a empty <see cref="T:Discord.MessageComponent"/>.
</summary>
</member>
<member name="T:Discord.SelectMenu">
<summary>
Represents a select menu component defined at <see href="https://discord.com/developers/docs/interactions/message-components#select-menu-object"/>
</summary>
</member>
<member name="P:Discord.SelectMenu.Type">
<inheritdoc/>
</member>
<member name="P:Discord.SelectMenu.CustomId">
<summary>
The custom id of this Select menu that will be sent with a <see cref="T:Discord.IDiscordInteraction"/>.
</summary>
</member>
<member name="P:Discord.SelectMenu.Options">
<summary>
The menus options to select from.
</summary>
</member>
<member name="P:Discord.SelectMenu.Placeholder">
<summary>
A custom placeholder text if nothing is selected, max 100 characters.
</summary>
</member>
<member name="P:Discord.SelectMenu.MinValues">
<summary>
The minimum number of items that must be chosen; default 1, min 0, max 25
</summary>
</member>
<member name="P:Discord.SelectMenu.MaxValues">
<summary>
The maximum number of items that can be chosen; default 1, max 25
</summary>
</member>
<member name="T:Discord.SelectMenuOption">
<summary>
Represents a choice for a <see cref="T:Discord.SelectMenu"/>.
</summary>
</member>
<member name="P:Discord.SelectMenuOption.Label">
<summary>
The user-facing name of the option, max 25 characters.
</summary>
</member>
<member name="P:Discord.SelectMenuOption.Value">
<summary>
The dev-define value of the option, max 100 characters.
</summary>
</member>
<member name="P:Discord.SelectMenuOption.Description">
<summary>
An additional description of the option, max 50 characters.
</summary>
</member>
<member name="P:Discord.SelectMenuOption.Emote">
<summary>
A <see cref="T:Discord.IEmote"/> that will be displayed with this menu option.
</summary>
</member>
<member name="P:Discord.SelectMenuOption.Default">
<summary>
Will render this option as selected by default.
</summary>
</member>
<member name="T:Discord.SlashCommandBuilder">
<summary>
A class used to build slash commands.


+ 1
- 1
src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs View File

@@ -28,7 +28,7 @@ namespace Discord
/// <summary>
/// Represents the data sent within this interaction.
/// </summary>
object Data { get; }
IDiscordInteractionData Data { get; }

/// <summary>
/// A continuation token for responding to the interaction.


+ 13
- 0
src/Discord.Net.Core/Entities/Interactions/IDiscordInteractionData.cs View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents an interface used to specify classes that they are a vaild dataype of a <see cref="IDiscordInteraction"/> class.
/// </summary>
public interface IDiscordInteractionData { }
}

+ 2
- 2
src/Discord.Net.Core/Entities/Interactions/Message Components/ActionRowComponent.cs View File

@@ -18,10 +18,10 @@ namespace Discord
/// <summary>
/// The child components in this row.
/// </summary>
public IReadOnlyCollection<ButtonComponent> Components { get; internal set; }
public IReadOnlyCollection<IMessageComponent> Components { get; internal set; }

internal ActionRowComponent() { }
internal ActionRowComponent(List<ButtonComponent> components)
internal ActionRowComponent(List<IMessageComponent> components)
{
this.Components = components;
}


+ 507
- 22
src/Discord.Net.Core/Entities/Interactions/Message Components/ComponentBuilder.cs View File

@@ -11,6 +11,16 @@ namespace Discord
/// </summary>
public class ComponentBuilder
{
/// <summary>
/// The max length of a <see cref="ButtonComponent.Label"/>.
/// </summary>
public const int MaxLabelLength = 80;

/// <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>
@@ -34,6 +44,84 @@ namespace Discord

private List<ActionRowBuilder> _actionRows { get; set; }

/// <summary>
/// Adds a <see cref="SelectMenuBuilder"/> to the first row, if the first row cannot
/// accept the component then it will add it to a row that can
/// </summary>
/// <param name="label">The label of the menu.</param>
/// <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="row">The row to add the menu to.</param>
/// <returns></returns>
public ComponentBuilder WithSelectMenu(string label, string customId, List<SelectMenuOptionBuilder> options,
string placeholder = null, int minValues = 1, int maxValues = 1, int row = 0)
{
return WithSelectMenu(new SelectMenuBuilder()
.WithLabel(label)
.WithCustomId(customId)
.WithOptions(options)
.WithPlaceholder(placeholder)
.WithMaxValues(maxValues)
.WithMinValues(minValues),
row);
}

/// <summary>
/// Adds a <see cref="SelectMenuBuilder"/> to the first row, if the first row cannot
/// accept the component then it will add it to a row that can
/// </summary>
/// <param name="menu">The menu to add</param>
/// <returns>The current builder.</returns>
public ComponentBuilder WithSelectMenu(SelectMenuBuilder menu)
=> WithSelectMenu(menu, 0);

/// <summary>
/// Adds a <see cref="SelectMenuBuilder"/> to the current builder at the specific row.
/// </summary>
/// <param name="menu">The menu to add.</param>
/// <param name="row">The row to attempt to add this component on.</param>
/// <returns>The current builder.</returns>
public ComponentBuilder WithSelectMenu(SelectMenuBuilder menu, int row)
{
Preconditions.LessThan(row, 5, nameof(row));

var builtMenu = menu.Build();

if (_actionRows == null)
{
_actionRows = new List<ActionRowBuilder>();
_actionRows.Add(new ActionRowBuilder().WithComponent(builtMenu));
}
else
{
if (_actionRows.Count == row)
_actionRows.Add(new ActionRowBuilder().WithComponent(builtMenu));
else
{
ActionRowBuilder actionRow = null;
if (_actionRows.Count < row)
actionRow = _actionRows.ElementAt(row);
else
{
actionRow = new ActionRowBuilder();
_actionRows.Add(actionRow);
}

if (actionRow.CanTakeComponent(builtMenu))
actionRow.WithComponent(builtMenu);
else if (row < 5)
WithSelectMenu(menu, row + 1);
else
throw new ArgumentOutOfRangeException($"There is no more room to add a {nameof(builtMenu)}");
}
}

return this;
}

/// <summary>
/// Adds a button to the specified row.
/// </summary>
@@ -81,6 +169,8 @@ namespace Discord
/// <returns>The current builder.</returns>
public ComponentBuilder WithButton(ButtonBuilder button, int row)
{
Preconditions.LessThan(row, 5, nameof(row));

var builtButton = button.Build();

if (_actionRows == null)
@@ -94,12 +184,21 @@ namespace Discord
_actionRows.Add(new ActionRowBuilder().WithComponent(builtButton));
else
{
if (_actionRows.Count > row)
_actionRows[row].WithComponent(builtButton);
ActionRowBuilder actionRow = null;
if(_actionRows.Count < row)
actionRow = _actionRows.ElementAt(row);
else
{
_actionRows.First().WithComponent(builtButton);
actionRow = new ActionRowBuilder();
_actionRows.Add(actionRow);
}

if (actionRow.CanTakeComponent(builtButton))
actionRow.WithComponent(builtButton);
else if (row < 5)
WithButton(button, row + 1);
else
throw new ArgumentOutOfRangeException($"There is no more room to add a {nameof(button)}");
}
}

@@ -132,7 +231,7 @@ namespace Discord
/// <summary>
/// Gets or sets the components inside this row.
/// </summary>
public List<ButtonComponent> Components
public List<IMessageComponent> Components
{
get => _components;
set
@@ -144,14 +243,14 @@ namespace Discord
}
}

private List<ButtonComponent> _components { get; set; }
private List<IMessageComponent> _components { get; set; }

/// <summary>
/// Adds a list of components to the current row.
/// </summary>
/// <param name="components">The list of components to add.</param>
/// <returns>The current builder.</returns>
public ActionRowBuilder WithComponents(List<ButtonComponent> components)
public ActionRowBuilder WithComponents(List<IMessageComponent> components)
{
this.Components = components;
return this;
@@ -162,10 +261,10 @@ namespace Discord
/// </summary>
/// <param name="component">The component to add.</param>
/// <returns>The current builder.</returns>
public ActionRowBuilder WithComponent(ButtonComponent component)
public ActionRowBuilder WithComponent(IMessageComponent component)
{
if (this.Components == null)
this.Components = new List<ButtonComponent>();
this.Components = new List<IMessageComponent>();

this.Components.Add(component);

@@ -188,6 +287,21 @@ namespace Discord

return new ActionRowComponent(this._components);
}

internal bool CanTakeComponent(IMessageComponent component)
{
switch (component.Type)
{
case ComponentType.ActionRow:
return false;
case ComponentType.Button:
return this.Components.Count < 5;
case ComponentType.SelectMenu:
return this.Components.Count == 0;
default:
return false;
}
}
}

/// <summary>
@@ -195,16 +309,6 @@ namespace Discord
/// </summary>
public class ButtonBuilder
{
/// <summary>
/// The max length of a <see cref="ButtonComponent.Label"/>.
/// </summary>
public const int MaxLabelLength = 80;

/// <summary>
/// The max length of a <see cref="ButtonComponent.CustomId"/>.
/// </summary>
public const int MaxCustomIdLength = 100;

/// <summary>
/// Gets or sets the label of the current button.
/// </summary>
@@ -214,8 +318,8 @@ namespace Discord
set
{
if (value != null)
if (value.Length > MaxLabelLength)
throw new ArgumentException(message: $"Button label must be {MaxLabelLength} characters or less!", paramName: nameof(Label));
if (value.Length > ComponentBuilder.MaxLabelLength)
throw new ArgumentException(message: $"Button label must be {ComponentBuilder.MaxLabelLength} characters or less!", paramName: nameof(Label));

_label = value;
}
@@ -230,8 +334,8 @@ namespace Discord
set
{
if (value != null)
if (value.Length > MaxCustomIdLength)
throw new ArgumentException(message: $"Custom Id must be {MaxCustomIdLength} characters or less!", paramName: nameof(CustomId));
if (value.Length > ComponentBuilder.MaxCustomIdLength)
throw new ArgumentException(message: $"Custom Id must be {ComponentBuilder.MaxCustomIdLength} characters or less!", paramName: nameof(CustomId));
_customId = value;
}
}
@@ -429,4 +533,385 @@ namespace Discord
return new ButtonComponent(this.Style, this.Label, this.Emote, this.CustomId, this.Url, this.Disabled);
}
}

/// <summary>
/// Represents a class used to build <see cref="SelectMenu"/>'s.
/// </summary>
public class SelectMenuBuilder
{
/// <summary>
/// The max length of a <see cref="SelectMenu.Placeholder"/>.
/// </summary>
public const int MaxPlaceholderLength = 100;

/// <summary>
/// The maximum number of values for the <see cref="SelectMenu.MinValues"/> and <see cref="SelectMenu.MaxValues"/> properties.
/// </summary>
public const int MaxValuesCount = 25;

/// <summary>
/// The maximum number of options a <see cref="SelectMenu"/> can have.
/// </summary>
public const int MaxOptionCount = 25;

/// <summary>
/// Gets or sets the label of the current select menu.
/// </summary>
public string Label
{
get => _label;
set
{
if (value != null)
if (value.Length > ComponentBuilder.MaxLabelLength)
throw new ArgumentException(message: $"Button label must be {ComponentBuilder.MaxLabelLength} characters or less!", paramName: nameof(Label));

_label = value;
}
}

/// <summary>
/// Gets or sets the custom id of the current select menu.
/// </summary>
public string CustomId
{
get => _customId;
set
{
if (value != null)
if (value.Length > ComponentBuilder.MaxCustomIdLength)
throw new ArgumentException(message: $"Custom Id must be {ComponentBuilder.MaxCustomIdLength} characters or less!", paramName: nameof(CustomId));
_customId = value;
}
}

/// <summary>
/// Gets or sets the placeholder text of the current select menu.
/// </summary>
public string Placeholder
{
get => _placeholder;
set
{
if (value?.Length > MaxPlaceholderLength)
throw new ArgumentException(message: $"Placeholder must be {MaxPlaceholderLength} characters or less!", paramName: nameof(Placeholder));

_placeholder = value;
}
}

/// <summary>
/// Gets or sets the minimum values of the current select menu.
/// </summary>
public int MinValues
{
get => _minvalues;
set
{
Preconditions.LessThan(value, MaxValuesCount, nameof(MinValues));
_minvalues = value;
}
}

/// <summary>
/// Gets or sets the maximum values of the current select menu.
/// </summary>
public int MaxValues
{
get => _maxvalues;
set
{
Preconditions.LessThan(value, MaxValuesCount, nameof(MaxValues));
_maxvalues = value;
}
}

/// <summary>
/// Gets or sets a collection of <see cref="SelectMenuOptionBuilder"/> for this current select menu.
/// </summary>
public List<SelectMenuOptionBuilder> Options
{
get => _options;
set
{
if (value != null)
Preconditions.LessThan(value.Count, MaxOptionCount, nameof(Options));

_options = value;
}
}

private List<SelectMenuOptionBuilder> _options;
private int _minvalues = 1;
private int _maxvalues = 1;
private string _placeholder;
private string _label;
private string _customId;

/// <summary>
/// Creates a new instance of a <see cref="SelectMenuBuilder"/>.
/// </summary>
public SelectMenuBuilder() { }

/// <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>
public SelectMenuBuilder(string customId, List<SelectMenuOptionBuilder> options)
{
this.CustomId = customId;
this.Options = options;
}

/// <summary>
/// Sets the field label.
/// </summary>
/// <param name="label">The value to set the field label to.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithLabel(string label)
{
this.Label = label;
return this;
}

/// <summary>
/// Sets the field CustomId.
/// </summary>
/// <param name="customId">The value to set the field CustomId to.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithCustomId(string customId)
{
this.CustomId = customId;
return this;
}

/// <summary>
/// Sets the field placeholder.
/// </summary>
/// <param name="placeholder">The value to set the field placeholder to.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithPlaceholder(string placeholder)
{
this.Placeholder = placeholder;
return this;
}

/// <summary>
/// Sets the field minValues.
/// </summary>
/// <param name="minValues">The value to set the field minValues to.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithMinValues(int minValues)
{
this.MinValues = minValues;
return this;
}

/// <summary>
/// Sets the field maxValues.
/// </summary>
/// <param name="maxValues">The value to set the field maxValues to.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithMaxValues(int maxValues)
{
this.MaxValues = maxValues;
return this;
}

/// <summary>
/// Sets the field options.
/// </summary>
/// <param name="options">The value to set the field options to.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithOptions(List<SelectMenuOptionBuilder> options)
{
this.Options = options;
return this;
}

/// <summary>
/// Builds a <see cref="SelectMenu"/>
/// </summary>
/// <returns>The newly built <see cref="SelectMenu"/></returns>
public SelectMenu Build()
{
var opt = this.Options?.Select(x => x.Build()).ToList();

return new SelectMenu(this.CustomId, opt, this.Placeholder, this.MinValues, this.MaxValues);
}
}

/// <summary>
/// Represents a class used to build <see cref="SelectMenuOption"/>'s.
/// </summary>
public class SelectMenuOptionBuilder
{
/// <summary>
/// The maximum length of a <see cref="SelectMenuOption.Description"/>.
/// </summary>
public const int MaxDescriptionLength = 50;

/// <summary>
/// Gets or sets the label of the current select menu.
/// </summary>
public string Label
{
get => _label;
set
{
if (value != null)
if (value.Length > ComponentBuilder.MaxLabelLength)
throw new ArgumentException(message: $"Button label must be {ComponentBuilder.MaxLabelLength} characters or less!", paramName: nameof(Label));

_label = value;
}
}

/// <summary>
/// Gets or sets the custom id of the current select menu.
/// </summary>
public string Value
{
get => _value;
set
{
if (value != null)
if (value.Length > ComponentBuilder.MaxCustomIdLength)
throw new ArgumentException(message: $"Value must be {ComponentBuilder.MaxCustomIdLength} characters or less!", paramName: nameof(Value));
_value = value;
}
}

/// <summary>
/// Gets or sets this menu options description.
/// </summary>
public string Description
{
get => _description;
set
{
if (value != null)
Preconditions.LessThan(value.Length, MaxDescriptionLength, nameof(Description));

_description = 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? Default { 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>
public SelectMenuOptionBuilder(string label, string value)
{
this.Label = label;
this.Value = value;
}

/// <summary>
/// Sets the field label.
/// </summary>
/// <param name="label">The value to set the field label to.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuOptionBuilder WithLabel(string label)
{
this.Label = label;
return this;
}

/// <summary>
/// Sets the field value.
/// </summary>
/// <param name="value">The value to set the field value to.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuOptionBuilder WithValue(string value)
{
this.Value = value;
return this;
}

/// <summary>
/// Sets the field description.
/// </summary>
/// <param name="description">The value to set the field description to.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuOptionBuilder WithDescription(string description)
{
this.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)
{
this.Emote = emote;
return this;
}

/// <summary>
/// Sets the field default.
/// </summary>
/// <param name="defaultValue">The value to set the field default to.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuOptionBuilder WithDefault(bool defaultValue)
{
this.Default = defaultValue;
return this;
}

/// <summary>
/// Builds a <see cref="SelectMenuOption"/>.
/// </summary>
/// <returns>The newly built <see cref="SelectMenuOption"/>.</returns>
public SelectMenuOption Build()
{
return new SelectMenuOption(this.Label, this.Value, this.Description, this.Emote, this.Default);
}
}
}

+ 6
- 1
src/Discord.Net.Core/Entities/Interactions/Message Components/ComponentType.cs View File

@@ -19,6 +19,11 @@ namespace Discord
/// <summary>
/// A clickable button
/// </summary>
Button = 2
Button = 2,

/// <summary>
/// A select menu for picking from choices
/// </summary>
SelectMenu = 3,
}
}

+ 51
- 0
src/Discord.Net.Core/Entities/Interactions/Message Components/SelectMenu.cs View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents a select menu component defined at <see href="https://discord.com/developers/docs/interactions/message-components#select-menu-object"/>
/// </summary>
public class SelectMenu : IMessageComponent
{
/// <inheritdoc/>
public ComponentType Type => ComponentType.SelectMenu;

/// <summary>
/// The custom id of this Select menu that will be sent with a <see cref="IDiscordInteraction"/>.
/// </summary>
public string CustomId { get; }

/// <summary>
/// The menus options to select from.
/// </summary>
public IReadOnlyCollection<SelectMenuOption> Options { get; }

/// <summary>
/// A custom placeholder text if nothing is selected, max 100 characters.
/// </summary>
public string Placeholder { get; }

/// <summary>
/// The minimum number of items that must be chosen; default 1, min 0, max 25
/// </summary>
public int MinValues { get; }

/// <summary>
/// The maximum number of items that can be chosen; default 1, max 25
/// </summary>
public int MaxValues { get; }

internal SelectMenu(string customId, List<SelectMenuOption> options, string placeholder, int minValues, int maxValues)
{
this.CustomId = customId;
this.Options = options;
this.Placeholder = placeholder;
this.MinValues = minValues;
this.MaxValues = maxValues;
}
}
}

+ 48
- 0
src/Discord.Net.Core/Entities/Interactions/Message Components/SelectMenuOption.cs View File

@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents a choice for a <see cref="SelectMenu"/>.
/// </summary>
public class SelectMenuOption
{
/// <summary>
/// The user-facing name of the option, max 25 characters.
/// </summary>
public string Label { get; }

/// <summary>
/// The dev-define value of the option, max 100 characters.
/// </summary>
public string Value { get; }

/// <summary>
/// An additional description of the option, max 50 characters.
/// </summary>
public string Description { get; }

/// <summary>
/// A <see cref="IEmote"/> that will be displayed with this menu option.
/// </summary>
public IEmote Emote { get; }

/// <summary>
/// Will render this option as selected by default.
/// </summary>
public bool? Default { get; }

internal SelectMenuOption(string label, string value, string description, IEmote emote, bool? defaultValue)
{
this.Label = label;
this.Value = value;
this.Description = description;
this.Emote = emote;
this.Default = defaultValue;
}
}
}

+ 15
- 3
src/Discord.Net.Rest/API/Common/ActionRowComponent.cs View File

@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -7,19 +8,30 @@ using System.Threading.Tasks;

namespace Discord.API
{
internal class ActionRowComponent
internal class ActionRowComponent : IMessageComponent
{
[JsonProperty("type")]
public ComponentType Type { get; set; }

[JsonProperty("components")]
public List<ButtonComponent> Components { get; set; }
public IMessageComponent[] Components { get; set; }

internal ActionRowComponent() { }
internal ActionRowComponent(Discord.ActionRowComponent c)
{
this.Type = c.Type;
this.Components = c.Components?.Select(x => new ButtonComponent(x)).ToList();
this.Components = c.Components?.Select<IMessageComponent, IMessageComponent>(x =>
{
switch (x.Type)
{
case ComponentType.Button:
return new ButtonComponent(x as Discord.ButtonComponent);
case ComponentType.SelectMenu:
return new SelectMenuComponent(x as Discord.SelectMenu);
default: return null;

}
}).ToArray();
}
}
}

+ 1
- 1
src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;

namespace Discord.API
{
internal class ApplicationCommandInteractionData
internal class ApplicationCommandInteractionData : IDiscordInteractionData
{
[JsonProperty("id")]
public ulong Id { get; set; }


+ 1
- 1
src/Discord.Net.Rest/API/Common/ButtonComponent.cs View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;

namespace Discord.API
{
internal class ButtonComponent
internal class ButtonComponent : IMessageComponent
{
[JsonProperty("type")]
public ComponentType Type { get; set; }


src/Discord.Net.WebSocket/API/Gateway/InteractionCreated.cs → src/Discord.Net.Rest/API/Common/Interaction.cs View File

@@ -1,4 +1,3 @@
using Discord.API;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
@@ -6,9 +5,10 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.API.Gateway
namespace Discord.API
{
internal class InteractionCreated
[JsonConverter(typeof(Net.Converters.InteractionConverter))]
internal class Interaction
{
[JsonProperty("id")]
public ulong Id { get; set; }
@@ -20,7 +20,7 @@ namespace Discord.API.Gateway
public InteractionType Type { get; set; }

[JsonProperty("data")]
public Optional<object> Data { get; set; }
public Optional<IDiscordInteractionData> Data { get; set; }

[JsonProperty("guild_id")]
public Optional<ulong> GuildId { get; set; }
@@ -42,6 +42,5 @@ namespace Discord.API.Gateway

[JsonProperty("message")]
public Optional<Message> Message { get; set; }

}
}

+ 4
- 1
src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs View File

@@ -7,12 +7,15 @@ using System.Threading.Tasks;

namespace Discord.API
{
internal class MessageComponentInteractionData
internal class MessageComponentInteractionData : IDiscordInteractionData
{
[JsonProperty("custom_id")]
public string CustomId { get; set; }

[JsonProperty("component_type")]
public ComponentType ComponentType { get; set; }

[JsonProperty("values")]
public Optional<string[]> Values { get; set; }
}
}

+ 42
- 0
src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs View File

@@ -0,0 +1,42 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.API
{
internal class SelectMenuComponent : IMessageComponent
{
[JsonProperty("type")]
public ComponentType Type { get; set; }

[JsonProperty("custom_id")]
public string CustomId { get; set; }

[JsonProperty("options")]
public SelectMenuOption[] Options { get; set; }

[JsonProperty("placeholder")]
public Optional<string> Placeholder { get; set; }

[JsonProperty("min_values")]
public int MinValues { get; set; }

[JsonProperty("max_values")]
public int MaxValues { get; set; }

public SelectMenuComponent() { }

public SelectMenuComponent(Discord.SelectMenu component)
{
this.Type = component.Type;
this.CustomId = component.CustomId;
this.Options = component.Options.Select(x => new SelectMenuOption(x)).ToArray();
this.Placeholder = component.Placeholder;
this.MinValues = component.MinValues;
this.MaxValues = component.MaxValues;
}
}
}

+ 58
- 0
src/Discord.Net.Rest/API/Common/SelectMenuOption.cs View File

@@ -0,0 +1,58 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.API
{
internal class SelectMenuOption
{
[JsonProperty("label")]
public string Label { get; set; }

[JsonProperty("value")]
public string Value { get; set; }

[JsonProperty("description")]
public Optional<string> Description { get; set; }

[JsonProperty("emoji")]
public Optional<Emoji> Emoji { get; set; }

[JsonProperty("default")]
public Optional<bool> Default { get; set; }

public SelectMenuOption() { }

public SelectMenuOption(Discord.SelectMenuOption option)
{
this.Label = option.Label;
this.Value = option.Value;
this.Description = option.Description;

if (option.Emote != null)
{
if (option.Emote is Emote e)
{
this.Emoji = new Emoji()
{
Name = e.Name,
Animated = e.Animated,
Id = e.Id,
};
}
else
{
this.Emoji = new Emoji()
{
Name = option.Emote.Name
};
}
}

this.Default = option.Default.HasValue ? option.Default.Value : Optional<bool>.Unspecified;
}
}
}

+ 44
- 10
src/Discord.Net.Rest/Entities/Messages/RestMessage.cs View File

@@ -1,3 +1,4 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -133,16 +134,49 @@ namespace Discord.Rest

if (model.Components.IsSpecified)
{
Components = model.Components.Value.Select(x => new ActionRowComponent(x.Components.Select(x =>
new ButtonComponent(
x.Style,
x.Label.GetValueOrDefault(),
x.Emote.IsSpecified ? x.Emote.Value.Id.HasValue ? new Emote(x.Emote.Value.Id.Value, x.Emote.Value.Name, x.Emote.Value.Animated.GetValueOrDefault()) : new Emoji(x.Emote.Value.Name) : null,
x.CustomId.GetValueOrDefault(),
x.Url.GetValueOrDefault(),
x.Disabled.GetValueOrDefault())
).ToList()
)).ToList();
Components = model.Components.Value.Select(x => new ActionRowComponent(x.Components.Select<IMessageComponent, IMessageComponent>(y =>
{
switch (y.Type)
{
case ComponentType.Button:
{
var parsed = (API.ButtonComponent)y;
return new Discord.ButtonComponent(
parsed.Style,
parsed.Label.GetValueOrDefault(),
parsed.Emote.IsSpecified
? parsed.Emote.Value.Id.HasValue
? new Emote(parsed.Emote.Value.Id.Value, parsed.Emote.Value.Name, parsed.Emote.Value.Animated.GetValueOrDefault())
: new Emoji(parsed.Emote.Value.Name)
: null,
parsed.CustomId.GetValueOrDefault(),
parsed.Url.GetValueOrDefault(),
parsed.Disabled.GetValueOrDefault());
}
case ComponentType.SelectMenu:
{
var parsed = (API.SelectMenuComponent)y;
return new SelectMenu(
parsed.CustomId,
parsed.Options.Select(z => new SelectMenuOption(
z.Label,
z.Value,
z.Description.GetValueOrDefault(),
z.Emoji.IsSpecified
? z.Emoji.Value.Id.HasValue
? new Emote(z.Emoji.Value.Id.Value, z.Emoji.Value.Name, z.Emoji.Value.Animated.GetValueOrDefault())
: new Emoji(z.Emoji.Value.Name)
: null,
z.Default.ToNullable())).ToList(),
parsed.Placeholder.GetValueOrDefault(),
parsed.MinValues,
parsed.MaxValues
);
}
default:
return null;
}
}).ToList())).ToImmutableArray();
}
else
Components = new List<ActionRowComponent>();


+ 4
- 0
src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs View File

@@ -81,6 +81,10 @@ namespace Discord.Net.Converters
//Special
if (type == typeof(API.Image))
return ImageConverter.Instance;
if (typeof(IMessageComponent).IsAssignableFrom(type))
return MessageComponentConverter.Instance;
if (type == typeof(API.Interaction))
return InteractionConverter.Instance;

//Entities
var typeInfo = type.GetTypeInfo();


+ 67
- 0
src/Discord.Net.Rest/Net/Converters/InteractionConverter.cs View File

@@ -0,0 +1,67 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.Net.Converters
{
internal class InteractionConverter : JsonConverter
{
public static InteractionConverter Instance => new InteractionConverter();

public override bool CanRead => true;
public override bool CanWrite => false;
public override bool CanConvert(Type objectType) => true;
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;

var obj = JObject.Load(reader);
var interaction = new API.Interaction();


// Remove the data property for manual deserialization
var result = obj.GetValue("data", StringComparison.OrdinalIgnoreCase);
result.Parent.Remove();

// Populate the remaining properties.
using (var subReader = obj.CreateReader())
{
serializer.Populate(subReader, interaction);
}

// Process the Result property
if (result != null)
{
switch (interaction.Type)
{
case InteractionType.ApplicationCommand:
{
var appCommandData = new API.ApplicationCommandInteractionData();
serializer.Populate(result.CreateReader(), appCommandData);
interaction.Data = appCommandData;
}
break;
case InteractionType.MessageComponent:
{
var messageComponent = new API.MessageComponentInteractionData();
serializer.Populate(result.CreateReader(), messageComponent);
interaction.Data = messageComponent;
}
break;
}
}
else
interaction.Data = Optional<IDiscordInteractionData>.Unspecified;

return interaction;
}


public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
}
}

+ 44
- 0
src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs View File

@@ -0,0 +1,44 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.Net.Converters
{
internal class MessageComponentConverter : JsonConverter
{
public static MessageComponentConverter Instance => new MessageComponentConverter();

public override bool CanRead => true;
public override bool CanWrite => false;
public override bool CanConvert(Type objectType) => true;
public override void WriteJson(JsonWriter writer,
object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jsonObject = JObject.Load(reader);
var messageComponent = default(IMessageComponent);
switch ((ComponentType)jsonObject["type"].Value<int>())
{
case ComponentType.ActionRow:
messageComponent = new API.ActionRowComponent();
break;
case ComponentType.Button:
messageComponent = new API.ButtonComponent();
break;
case ComponentType.SelectMenu:
messageComponent = new API.SelectMenuComponent();
break;
}
serializer.Populate(jsonObject.CreateReader(), messageComponent);
return messageComponent;
}
}
}

+ 5
- 0
src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml View File

@@ -3224,6 +3224,11 @@
The type of the component clicked
</summary>
</member>
<member name="P:Discord.WebSocket.SocketMessageComponentData.Values">
<summary>
The value(s) of a <see cref="T:Discord.SelectMenu"/> interaction response.
</summary>
</member>
<member name="T:Discord.WebSocket.SocketApplicationCommand">
<summary>
Represends a Websocket-based <see cref="T:Discord.IApplicationCommand"/> recieved over the gateway.


+ 1
- 1
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -1865,7 +1865,7 @@ namespace Discord.WebSocket
{
await _gatewayLogger.DebugAsync("Received Dispatch (INTERACTION_CREATE)").ConfigureAwait(false);

var data = (payload as JToken).ToObject<API.Gateway.InteractionCreated>(_serializer);
var data = (payload as JToken).ToObject<API.Interaction>(_serializer);

SocketChannel channel = null;
if(data.ChannelId.IsSpecified)


+ 2
- 4
src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponent.cs View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.Gateway.InteractionCreated;
using Model = Discord.API.Interaction;
using DataModel = Discord.API.MessageComponentInteractionData;
using Newtonsoft.Json.Linq;
using Discord.Rest;
@@ -29,12 +29,10 @@ namespace Discord.WebSocket
: base(client, model.Id, channel)
{
var dataModel = model.Data.IsSpecified ?
(model.Data.Value as JToken).ToObject<DataModel>()
(DataModel)model.Data.Value
: null;

this.Data = new SocketMessageComponentData(dataModel);


}

new internal static SocketMessageComponent Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel)


+ 6
- 0
src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponentData.cs View File

@@ -22,10 +22,16 @@ namespace Discord.WebSocket
/// </summary>
public ComponentType Type { get; }

/// <summary>
/// The value(s) of a <see cref="SelectMenu"/> interaction response.
/// </summary>
public IReadOnlyCollection<string> Values { get; }

internal SocketMessageComponentData(Model model)
{
this.CustomId = model.CustomId;
this.Type = model.ComponentType;
this.Values = model.Values.GetValueOrDefault();
}
}
}

+ 2
- 2
src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommand.cs View File

@@ -4,7 +4,7 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using DataModel = Discord.API.ApplicationCommandInteractionData;
using Model = Discord.API.Gateway.InteractionCreated;
using Model = Discord.API.Interaction;

namespace Discord.WebSocket
{
@@ -22,7 +22,7 @@ namespace Discord.WebSocket
: base(client, model.Id, channel)
{
var dataModel = model.Data.IsSpecified ?
(model.Data.Value as JToken).ToObject<DataModel>(client._serializer)
(DataModel)model.Data.Value
: null;

ulong? guildId = null;


+ 2
- 2
src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs View File

@@ -4,7 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.Gateway.InteractionCreated;
using Model = Discord.API.Interaction;

namespace Discord.WebSocket
{
@@ -36,7 +36,7 @@ namespace Discord.WebSocket
/// <summary>
/// The data sent with this interaction.
/// </summary>
public object Data { get; private set; }
public IDiscordInteractionData Data { get; private set; }

/// <summary>
/// The version of this interaction.


+ 44
- 10
src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs View File

@@ -1,4 +1,5 @@
using Discord.Rest;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -167,16 +168,49 @@ namespace Discord.WebSocket

if (model.Components.IsSpecified)
{
Components = model.Components.Value.Select(x => new ActionRowComponent(x.Components.Select(x =>
new ButtonComponent(
x.Style,
x.Label.GetValueOrDefault(),
x.Emote.IsSpecified ? x.Emote.Value.Id.HasValue ? new Emote(x.Emote.Value.Id.Value, x.Emote.Value.Name, x.Emote.Value.Animated.GetValueOrDefault()) : new Emoji(x.Emote.Value.Name) : null,
x.CustomId.GetValueOrDefault(),
x.Url.GetValueOrDefault(),
x.Disabled.GetValueOrDefault())
).ToList()
)).ToList();
Components = model.Components.Value.Select(x => new ActionRowComponent(x.Components.Select<IMessageComponent, IMessageComponent>(y =>
{
switch (y.Type)
{
case ComponentType.Button:
{
var parsed = (API.ButtonComponent)y;
return new Discord.ButtonComponent(
parsed.Style,
parsed.Label.GetValueOrDefault(),
parsed.Emote.IsSpecified
? parsed.Emote.Value.Id.HasValue
? new Emote(parsed.Emote.Value.Id.Value, parsed.Emote.Value.Name, parsed.Emote.Value.Animated.GetValueOrDefault())
: new Emoji(parsed.Emote.Value.Name)
: null,
parsed.CustomId.GetValueOrDefault(),
parsed.Url.GetValueOrDefault(),
parsed.Disabled.GetValueOrDefault());
}
case ComponentType.SelectMenu:
{
var parsed = (API.SelectMenuComponent)y;
return new SelectMenu(
parsed.CustomId,
parsed.Options.Select(z => new SelectMenuOption(
z.Label,
z.Value,
z.Description.GetValueOrDefault(),
z.Emoji.IsSpecified
? z.Emoji.Value.Id.HasValue
? new Emote(z.Emoji.Value.Id.Value, z.Emoji.Value.Name, z.Emoji.Value.Animated.GetValueOrDefault())
: new Emoji(z.Emoji.Value.Name)
: null,
z.Default.ToNullable())).ToList(),
parsed.Placeholder.GetValueOrDefault(),
parsed.MinValues,
parsed.MaxValues
);
}
default:
return null;
}
}).ToList())).ToImmutableArray();
}
else
Components = new List<ActionRowComponent>();


+ 50
- 0
src/Discord.Net.Webhook/Discord.Net.Webhook.xml View File

@@ -35,6 +35,33 @@
<summary> Sends a message to the channel for this webhook. </summary>
<returns> Returns the ID of the created message. </returns>
</member>
<member name="M:Discord.Webhook.DiscordWebhookClient.ModifyMessageAsync(System.UInt64,System.Action{Discord.Webhook.WebhookMessageProperties},Discord.RequestOptions)">
<summary>
Modifies a message posted using this webhook.
</summary>
<remarks>
This method can only modify messages that were sent using the same webhook.
</remarks>
<param name="messageId">ID of the modified message.</param>
<param name="func">A delegate containing the properties to modify the message with.</param>
<param name="options">The options to be used when sending the request.</param>
<returns>
A task that represents the asynchronous modification operation.
</returns>
</member>
<member name="M:Discord.Webhook.DiscordWebhookClient.DeleteMessageAsync(System.UInt64,Discord.RequestOptions)">
<summary>
Deletes a message posted using this webhook.
</summary>
<remarks>
This method can only delete messages that were sent using the same webhook.
</remarks>
<param name="messageId">ID of the deleted message.</param>
<param name="options">The options to be used when sending the request.</param>
<returns>
A task that represents the asynchronous deletion operation.
</returns>
</member>
<member name="M:Discord.Webhook.DiscordWebhookClient.SendFileAsync(System.String,System.String,System.Boolean,System.Collections.Generic.IEnumerable{Discord.Embed},System.String,System.String,Discord.RequestOptions,System.Boolean,Discord.AllowedMentions)">
<summary> Sends a message to the channel for this webhook with an attachment. </summary>
<returns> Returns the ID of the created message. </returns>
@@ -49,6 +76,29 @@
<member name="M:Discord.Webhook.DiscordWebhookClient.DeleteWebhookAsync(Discord.RequestOptions)">
<summary> Deletes this webhook from Discord and disposes the client. </summary>
</member>
<member name="T:Discord.Webhook.WebhookMessageProperties">
<summary>
Properties that are used to modify an Webhook message with the specified changes.
</summary>
</member>
<member name="P:Discord.Webhook.WebhookMessageProperties.Content">
<summary>
Gets or sets the content of the message.
</summary>
<remarks>
This must be less than the constant defined by <see cref="F:Discord.DiscordConfig.MaxMessageSize"/>.
</remarks>
</member>
<member name="P:Discord.Webhook.WebhookMessageProperties.Embeds">
<summary>
Gets or sets the embed array that the message should display.
</summary>
</member>
<member name="P:Discord.Webhook.WebhookMessageProperties.AllowedMentions">
<summary>
Gets or sets the allowed mentions of the message.
</summary>
</member>
<member name="M:Discord.Webhook.WebhookClientHelper.GetWebhookAsync(Discord.Webhook.DiscordWebhookClient,System.UInt64)">
<exception cref="T:System.InvalidOperationException">Could not find a webhook with the supplied credentials.</exception>
</member>


Loading…
Cancel
Save