diff --git a/src/Discord.Net.Core/Discord.Net.Core.xml b/src/Discord.Net.Core/Discord.Net.Core.xml index f60639030..a4b16057d 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.xml +++ b/src/Discord.Net.Core/Discord.Net.Core.xml @@ -4132,6 +4132,11 @@ read-only property, always 1. + + + Represents an interface used to specify classes that they are a vaild dataype of a class. + + The response type for an . @@ -4277,6 +4282,16 @@ Represents a builder for creating a . + + + The max length of a . + + + + + The max length of a . + + The max amount of rows a message can have. @@ -4287,6 +4302,36 @@ Gets or sets the Action Rows for this Component Builder. + + + Adds a to the first row, if the first row cannot + accept the component then it will add it to a row that can + + The label of the menu. + The custom id of the menu. + The options of the menu. + The placeholder of the menu. + The min values of the placeholder. + The max values of the placeholder. + The row to add the menu to. + + + + + Adds a to the first row, if the first row cannot + accept the component then it will add it to a row that can + + The menu to add + The current builder. + + + + Adds a to the current builder at the specific row. + + The menu to add. + The row to attempt to add this component on. + The current builder. + Adds a button to the specified row. @@ -4336,14 +4381,14 @@ Gets or sets the components inside this row. - + Adds a list of components to the current row. The list of components to add. The current builder. - + Adds a component at the end of the current row. @@ -4363,16 +4408,6 @@ Represents a class used to build 's. - - - The max length of a . - - - - - The max length of a . - - Gets or sets the label of the current button. @@ -4493,6 +4528,226 @@ A button cannot contain a URL and a CustomId. A button must have an Emote or a label. + + + Represents a class used to build 's. + + + + + The max length of a . + + + + + The maximum number of values for the and properties. + + + + + The maximum number of options a can have. + + + + + Gets or sets the label of the current select menu. + + + + + Gets or sets the custom id of the current select menu. + + + + + Gets or sets the placeholder text of the current select menu. + + + + + Gets or sets the minimum values of the current select menu. + + + + + Gets or sets the maximum values of the current select menu. + + + + + Gets or sets a collection of for this current select menu. + + + + + Creates a new instance of a . + + + + + Creates a new instance of a . + + The custom id of this select menu. + The options for this select menu. + + + + Sets the field label. + + The value to set the field label to. + + The current builder. + + + + + Sets the field CustomId. + + The value to set the field CustomId to. + + The current builder. + + + + + Sets the field placeholder. + + The value to set the field placeholder to. + + The current builder. + + + + + Sets the field minValues. + + The value to set the field minValues to. + + The current builder. + + + + + Sets the field maxValues. + + The value to set the field maxValues to. + + The current builder. + + + + + Sets the field options. + + The value to set the field options to. + + The current builder. + + + + + Builds a + + The newly built + + + + Represents a class used to build 's. + + + + + The maximum length of a . + + + + + Gets or sets the label of the current select menu. + + + + + Gets or sets the custom id of the current select menu. + + + + + Gets or sets this menu options description. + + + + + Gets or sets the emote of this option. + + + + + Gets or sets the whether or not this option will render selected by default. + + + + + Creates a new instance of a . + + + + + Creates a new instance of a . + + The label for this option. + The value of this option. + + + + Sets the field label. + + The value to set the field label to. + + The current builder. + + + + + Sets the field value. + + The value to set the field value to. + + The current builder. + + + + + Sets the field description. + + The value to set the field description to. + + The current builder. + + + + + Sets the field emote. + + The value to set the field emote to. + + The current builder. + + + + + Sets the field default. + + The value to set the field default to. + + The current builder. + + + + + Builds a . + + The newly built . + Represents a type of a component @@ -4508,6 +4763,11 @@ A clickable button + + + A select menu for picking from choices + + The of this Message Component. @@ -4528,6 +4788,69 @@ Returns a empty . + + + Represents a select menu component defined at + + + + + + + + The custom id of this Select menu that will be sent with a . + + + + + The menus options to select from. + + + + + A custom placeholder text if nothing is selected, max 100 characters. + + + + + The minimum number of items that must be chosen; default 1, min 0, max 25 + + + + + The maximum number of items that can be chosen; default 1, max 25 + + + + + Represents a choice for a . + + + + + The user-facing name of the option, max 25 characters. + + + + + The dev-define value of the option, max 100 characters. + + + + + An additional description of the option, max 50 characters. + + + + + A that will be displayed with this menu option. + + + + + Will render this option as selected by default. + + A class used to build slash commands. diff --git a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs index 5c409e837..466bf3e91 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs @@ -28,7 +28,7 @@ namespace Discord /// /// Represents the data sent within this interaction. /// - object Data { get; } + IDiscordInteractionData Data { get; } /// /// A continuation token for responding to the interaction. diff --git a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteractionData.cs b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteractionData.cs new file mode 100644 index 000000000..7083810b7 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteractionData.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents an interface used to specify classes that they are a vaild dataype of a class. + /// + public interface IDiscordInteractionData { } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/Message Components/ActionRowComponent.cs b/src/Discord.Net.Core/Entities/Interactions/Message Components/ActionRowComponent.cs index c29ef44a2..c5bbc8d08 100644 --- a/src/Discord.Net.Core/Entities/Interactions/Message Components/ActionRowComponent.cs +++ b/src/Discord.Net.Core/Entities/Interactions/Message Components/ActionRowComponent.cs @@ -18,10 +18,10 @@ namespace Discord /// /// The child components in this row. /// - public IReadOnlyCollection Components { get; internal set; } + public IReadOnlyCollection Components { get; internal set; } internal ActionRowComponent() { } - internal ActionRowComponent(List components) + internal ActionRowComponent(List components) { this.Components = components; } diff --git a/src/Discord.Net.Core/Entities/Interactions/Message Components/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/Message Components/ComponentBuilder.cs index e8b0ce19c..afd1e34bd 100644 --- a/src/Discord.Net.Core/Entities/Interactions/Message Components/ComponentBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/Message Components/ComponentBuilder.cs @@ -11,6 +11,16 @@ namespace Discord /// public class ComponentBuilder { + /// + /// The max length of a . + /// + public const int MaxLabelLength = 80; + + /// + /// The max length of a . + /// + public const int MaxCustomIdLength = 100; + /// /// The max amount of rows a message can have. /// @@ -34,6 +44,84 @@ namespace Discord private List _actionRows { get; set; } + /// + /// Adds a to the first row, if the first row cannot + /// accept the component then it will add it to a row that can + /// + /// The label of the menu. + /// The custom id of the menu. + /// The options of the menu. + /// The placeholder of the menu. + /// The min values of the placeholder. + /// The max values of the placeholder. + /// The row to add the menu to. + /// + public ComponentBuilder WithSelectMenu(string label, string customId, List 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); + } + + /// + /// Adds a to the first row, if the first row cannot + /// accept the component then it will add it to a row that can + /// + /// The menu to add + /// The current builder. + public ComponentBuilder WithSelectMenu(SelectMenuBuilder menu) + => WithSelectMenu(menu, 0); + + /// + /// Adds a to the current builder at the specific row. + /// + /// The menu to add. + /// The row to attempt to add this component on. + /// The current builder. + public ComponentBuilder WithSelectMenu(SelectMenuBuilder menu, int row) + { + Preconditions.LessThan(row, 5, nameof(row)); + + var builtMenu = menu.Build(); + + if (_actionRows == null) + { + _actionRows = new List(); + _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; + } + /// /// Adds a button to the specified row. /// @@ -81,6 +169,8 @@ namespace Discord /// The current builder. 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 /// /// Gets or sets the components inside this row. /// - public List Components + public List Components { get => _components; set @@ -144,14 +243,14 @@ namespace Discord } } - private List _components { get; set; } + private List _components { get; set; } /// /// Adds a list of components to the current row. /// /// The list of components to add. /// The current builder. - public ActionRowBuilder WithComponents(List components) + public ActionRowBuilder WithComponents(List components) { this.Components = components; return this; @@ -162,10 +261,10 @@ namespace Discord /// /// The component to add. /// The current builder. - public ActionRowBuilder WithComponent(ButtonComponent component) + public ActionRowBuilder WithComponent(IMessageComponent component) { if (this.Components == null) - this.Components = new List(); + this.Components = new List(); 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; + } + } } /// @@ -195,16 +309,6 @@ namespace Discord /// public class ButtonBuilder { - /// - /// The max length of a . - /// - public const int MaxLabelLength = 80; - - /// - /// The max length of a . - /// - public const int MaxCustomIdLength = 100; - /// /// Gets or sets the label of the current button. /// @@ -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); } } + + /// + /// Represents a class used to build 's. + /// + public class SelectMenuBuilder + { + /// + /// The max length of a . + /// + public const int MaxPlaceholderLength = 100; + + /// + /// The maximum number of values for the and properties. + /// + public const int MaxValuesCount = 25; + + /// + /// The maximum number of options a can have. + /// + public const int MaxOptionCount = 25; + + /// + /// Gets or sets the label of the current select menu. + /// + 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; + } + } + + /// + /// Gets or sets the custom id of the current select menu. + /// + 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; + } + } + + /// + /// Gets or sets the placeholder text of the current select menu. + /// + 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; + } + } + + /// + /// Gets or sets the minimum values of the current select menu. + /// + public int MinValues + { + get => _minvalues; + set + { + Preconditions.LessThan(value, MaxValuesCount, nameof(MinValues)); + _minvalues = value; + } + } + + /// + /// Gets or sets the maximum values of the current select menu. + /// + public int MaxValues + { + get => _maxvalues; + set + { + Preconditions.LessThan(value, MaxValuesCount, nameof(MaxValues)); + _maxvalues = value; + } + } + + /// + /// Gets or sets a collection of for this current select menu. + /// + public List Options + { + get => _options; + set + { + if (value != null) + Preconditions.LessThan(value.Count, MaxOptionCount, nameof(Options)); + + _options = value; + } + } + + private List _options; + private int _minvalues = 1; + private int _maxvalues = 1; + private string _placeholder; + private string _label; + private string _customId; + + /// + /// Creates a new instance of a . + /// + public SelectMenuBuilder() { } + + /// + /// Creates a new instance of a . + /// + /// The custom id of this select menu. + /// The options for this select menu. + public SelectMenuBuilder(string customId, List options) + { + this.CustomId = customId; + this.Options = options; + } + + /// + /// Sets the field label. + /// + /// The value to set the field label to. + /// + /// The current builder. + /// + public SelectMenuBuilder WithLabel(string label) + { + this.Label = label; + return this; + } + + /// + /// Sets the field CustomId. + /// + /// The value to set the field CustomId to. + /// + /// The current builder. + /// + public SelectMenuBuilder WithCustomId(string customId) + { + this.CustomId = customId; + return this; + } + + /// + /// Sets the field placeholder. + /// + /// The value to set the field placeholder to. + /// + /// The current builder. + /// + public SelectMenuBuilder WithPlaceholder(string placeholder) + { + this.Placeholder = placeholder; + return this; + } + + /// + /// Sets the field minValues. + /// + /// The value to set the field minValues to. + /// + /// The current builder. + /// + public SelectMenuBuilder WithMinValues(int minValues) + { + this.MinValues = minValues; + return this; + } + + /// + /// Sets the field maxValues. + /// + /// The value to set the field maxValues to. + /// + /// The current builder. + /// + public SelectMenuBuilder WithMaxValues(int maxValues) + { + this.MaxValues = maxValues; + return this; + } + + /// + /// Sets the field options. + /// + /// The value to set the field options to. + /// + /// The current builder. + /// + public SelectMenuBuilder WithOptions(List options) + { + this.Options = options; + return this; + } + + /// + /// Builds a + /// + /// The newly built + public SelectMenu Build() + { + var opt = this.Options?.Select(x => x.Build()).ToList(); + + return new SelectMenu(this.CustomId, opt, this.Placeholder, this.MinValues, this.MaxValues); + } + } + + /// + /// Represents a class used to build 's. + /// + public class SelectMenuOptionBuilder + { + /// + /// The maximum length of a . + /// + public const int MaxDescriptionLength = 50; + + /// + /// Gets or sets the label of the current select menu. + /// + 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; + } + } + + /// + /// Gets or sets the custom id of the current select menu. + /// + 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; + } + } + + /// + /// Gets or sets this menu options description. + /// + public string Description + { + get => _description; + set + { + if (value != null) + Preconditions.LessThan(value.Length, MaxDescriptionLength, nameof(Description)); + + _description = value; + } + } + + /// + /// Gets or sets the emote of this option. + /// + public IEmote Emote { get; set; } + + /// + /// Gets or sets the whether or not this option will render selected by default. + /// + public bool? Default { get; set; } + + private string _label; + private string _value; + private string _description; + + /// + /// Creates a new instance of a . + /// + public SelectMenuOptionBuilder() { } + + /// + /// Creates a new instance of a . + /// + /// The label for this option. + /// The value of this option. + public SelectMenuOptionBuilder(string label, string value) + { + this.Label = label; + this.Value = value; + } + + /// + /// Sets the field label. + /// + /// The value to set the field label to. + /// + /// The current builder. + /// + public SelectMenuOptionBuilder WithLabel(string label) + { + this.Label = label; + return this; + } + + /// + /// Sets the field value. + /// + /// The value to set the field value to. + /// + /// The current builder. + /// + public SelectMenuOptionBuilder WithValue(string value) + { + this.Value = value; + return this; + } + + /// + /// Sets the field description. + /// + /// The value to set the field description to. + /// + /// The current builder. + /// + public SelectMenuOptionBuilder WithDescription(string description) + { + this.Description = description; + return this; + } + + /// + /// Sets the field emote. + /// + /// The value to set the field emote to. + /// + /// The current builder. + /// + public SelectMenuOptionBuilder WithEmote(IEmote emote) + { + this.Emote = emote; + return this; + } + + /// + /// Sets the field default. + /// + /// The value to set the field default to. + /// + /// The current builder. + /// + public SelectMenuOptionBuilder WithDefault(bool defaultValue) + { + this.Default = defaultValue; + return this; + } + + /// + /// Builds a . + /// + /// The newly built . + public SelectMenuOption Build() + { + return new SelectMenuOption(this.Label, this.Value, this.Description, this.Emote, this.Default); + } + } } diff --git a/src/Discord.Net.Core/Entities/Interactions/Message Components/ComponentType.cs b/src/Discord.Net.Core/Entities/Interactions/Message Components/ComponentType.cs index 9b75599c9..5fb4fc092 100644 --- a/src/Discord.Net.Core/Entities/Interactions/Message Components/ComponentType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/Message Components/ComponentType.cs @@ -19,6 +19,11 @@ namespace Discord /// /// A clickable button /// - Button = 2 + Button = 2, + + /// + /// A select menu for picking from choices + /// + SelectMenu = 3, } } diff --git a/src/Discord.Net.Core/Entities/Interactions/Message Components/SelectMenu.cs b/src/Discord.Net.Core/Entities/Interactions/Message Components/SelectMenu.cs new file mode 100644 index 000000000..70fd36398 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/Message Components/SelectMenu.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a select menu component defined at + /// + public class SelectMenu : IMessageComponent + { + /// + public ComponentType Type => ComponentType.SelectMenu; + + /// + /// The custom id of this Select menu that will be sent with a . + /// + public string CustomId { get; } + + /// + /// The menus options to select from. + /// + public IReadOnlyCollection Options { get; } + + /// + /// A custom placeholder text if nothing is selected, max 100 characters. + /// + public string Placeholder { get; } + + /// + /// The minimum number of items that must be chosen; default 1, min 0, max 25 + /// + public int MinValues { get; } + + /// + /// The maximum number of items that can be chosen; default 1, max 25 + /// + public int MaxValues { get; } + + internal SelectMenu(string customId, List options, string placeholder, int minValues, int maxValues) + { + this.CustomId = customId; + this.Options = options; + this.Placeholder = placeholder; + this.MinValues = minValues; + this.MaxValues = maxValues; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/Message Components/SelectMenuOption.cs b/src/Discord.Net.Core/Entities/Interactions/Message Components/SelectMenuOption.cs new file mode 100644 index 000000000..74da89cae --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/Message Components/SelectMenuOption.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a choice for a . + /// + public class SelectMenuOption + { + /// + /// The user-facing name of the option, max 25 characters. + /// + public string Label { get; } + + /// + /// The dev-define value of the option, max 100 characters. + /// + public string Value { get; } + + /// + /// An additional description of the option, max 50 characters. + /// + public string Description { get; } + + /// + /// A that will be displayed with this menu option. + /// + public IEmote Emote { get; } + + /// + /// Will render this option as selected by default. + /// + 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; + } + } +} diff --git a/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs b/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs index 7fddac1cf..4de9d6fe1 100644 --- a/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs +++ b/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs @@ -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 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(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(); } } } diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs index c72e8a686..b9647ba65 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs @@ -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; } diff --git a/src/Discord.Net.Rest/API/Common/ButtonComponent.cs b/src/Discord.Net.Rest/API/Common/ButtonComponent.cs index 775f78101..b9d1147a2 100644 --- a/src/Discord.Net.Rest/API/Common/ButtonComponent.cs +++ b/src/Discord.Net.Rest/API/Common/ButtonComponent.cs @@ -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; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/InteractionCreated.cs b/src/Discord.Net.Rest/API/Common/Interaction.cs similarity index 84% rename from src/Discord.Net.WebSocket/API/Gateway/InteractionCreated.cs rename to src/Discord.Net.Rest/API/Common/Interaction.cs index 6e9ebb4fb..ebbc5fc1b 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/InteractionCreated.cs +++ b/src/Discord.Net.Rest/API/Common/Interaction.cs @@ -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 Data { get; set; } + public Optional Data { get; set; } [JsonProperty("guild_id")] public Optional GuildId { get; set; } @@ -42,6 +42,5 @@ namespace Discord.API.Gateway [JsonProperty("message")] public Optional Message { get; set; } - } } diff --git a/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs b/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs index cdb4e7d5c..5dc81e61e 100644 --- a/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs +++ b/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs @@ -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 Values { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs b/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs new file mode 100644 index 000000000..e206ec11a --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs @@ -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 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; + } + } +} diff --git a/src/Discord.Net.Rest/API/Common/SelectMenuOption.cs b/src/Discord.Net.Rest/API/Common/SelectMenuOption.cs new file mode 100644 index 000000000..5c6f2816b --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/SelectMenuOption.cs @@ -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 Description { get; set; } + + [JsonProperty("emoji")] + public Optional Emoji { get; set; } + + [JsonProperty("default")] + public Optional 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.Unspecified; + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index 9f62c45be..4ce655fcb 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -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(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(); diff --git a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs index 931c0c4c9..5981a266e 100644 --- a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs @@ -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(); diff --git a/src/Discord.Net.Rest/Net/Converters/InteractionConverter.cs b/src/Discord.Net.Rest/Net/Converters/InteractionConverter.cs new file mode 100644 index 000000000..cda3b1e24 --- /dev/null +++ b/src/Discord.Net.Rest/Net/Converters/InteractionConverter.cs @@ -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.Unspecified; + + return interaction; + } + + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException(); + } +} diff --git a/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs b/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs new file mode 100644 index 000000000..2203008d6 --- /dev/null +++ b/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs @@ -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()) + { + 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; + } + } +} diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml index e08511d5d..c864a9f6b 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml @@ -3224,6 +3224,11 @@ The type of the component clicked + + + The value(s) of a interaction response. + + Represends a Websocket-based recieved over the gateway. diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 4fe1cfdfe..9f7a1e524 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1865,7 +1865,7 @@ namespace Discord.WebSocket { await _gatewayLogger.DebugAsync("Received Dispatch (INTERACTION_CREATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); SocketChannel channel = null; if(data.ChannelId.IsSpecified) diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponent.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponent.cs index beda6eb53..f42fb58ff 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponent.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponent.cs @@ -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)model.Data.Value : null; this.Data = new SocketMessageComponentData(dataModel); - - } new internal static SocketMessageComponent Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel) diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponentData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponentData.cs index 8477432c7..45e688266 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponentData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponentData.cs @@ -22,10 +22,16 @@ namespace Discord.WebSocket /// public ComponentType Type { get; } + /// + /// The value(s) of a interaction response. + /// + public IReadOnlyCollection Values { get; } + internal SocketMessageComponentData(Model model) { this.CustomId = model.CustomId; this.Type = model.ComponentType; + this.Values = model.Values.GetValueOrDefault(); } } } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommand.cs index 1b94f5c83..542e8efc5 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommand.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommand.cs @@ -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(client._serializer) + (DataModel)model.Data.Value : null; ulong? guildId = null; diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs index a31ecc692..0876d6eb6 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -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 /// /// The data sent with this interaction. /// - public object Data { get; private set; } + public IDiscordInteractionData Data { get; private set; } /// /// The version of this interaction. diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index d0b9443a4..8ec18d49e 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -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(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(); diff --git a/src/Discord.Net.Webhook/Discord.Net.Webhook.xml b/src/Discord.Net.Webhook/Discord.Net.Webhook.xml index e2c3e69a9..f629c2c29 100644 --- a/src/Discord.Net.Webhook/Discord.Net.Webhook.xml +++ b/src/Discord.Net.Webhook/Discord.Net.Webhook.xml @@ -35,6 +35,33 @@ Sends a message to the channel for this webhook. Returns the ID of the created message. + + + Modifies a message posted using this webhook. + + + This method can only modify messages that were sent using the same webhook. + + ID of the modified message. + A delegate containing the properties to modify the message with. + The options to be used when sending the request. + + A task that represents the asynchronous modification operation. + + + + + Deletes a message posted using this webhook. + + + This method can only delete messages that were sent using the same webhook. + + ID of the deleted message. + The options to be used when sending the request. + + A task that represents the asynchronous deletion operation. + + Sends a message to the channel for this webhook with an attachment. Returns the ID of the created message. @@ -49,6 +76,29 @@ Deletes this webhook from Discord and disposes the client. + + + Properties that are used to modify an Webhook message with the specified changes. + + + + + Gets or sets the content of the message. + + + This must be less than the constant defined by . + + + + + Gets or sets the embed array that the message should display. + + + + + Gets or sets the allowed mentions of the message. + + Could not find a webhook with the supplied credentials.