| @@ -1,7 +1,7 @@ | |||
| using Discord.Utils; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using Discord.Utils; | |||
| namespace Discord | |||
| { | |||
| @@ -278,9 +278,7 @@ namespace Discord | |||
| { | |||
| if (_actionRows?.SelectMany(x => x.Components)?.Any(x => x.Type == ComponentType.TextInput) ?? false) | |||
| throw new ArgumentException("TextInputComponents are not allowed in messages.", nameof(ActionRows)); | |||
| if (_actionRows?.SelectMany(x => x.Components)?.Any(x => x.Type == ComponentType.ModalSubmit) ?? false) | |||
| throw new ArgumentException("ModalSubmit components are not allowed in messages.", nameof(ActionRows)); | |||
| return _actionRows != null | |||
| ? new MessageComponent(_actionRows.Select(x => x.Build()).ToList()) | |||
| : MessageComponent.Empty; | |||
| @@ -357,7 +355,7 @@ namespace Discord | |||
| /// <param name="minValues">The min values of the placeholder.</param> | |||
| /// <param name="maxValues">The max values of the placeholder.</param> | |||
| /// <param name="disabled">Whether or not the menu is disabled.</param> | |||
| /// <returns>The current builder.</returns> | |||
| /// <returns>The current builder.</returns> | |||
| public ActionRowBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> options, | |||
| string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false) | |||
| { | |||
| @@ -431,10 +429,10 @@ namespace Discord | |||
| { | |||
| var builtButton = button.Build(); | |||
| if(Components.Count >= 5) | |||
| if (Components.Count >= 5) | |||
| throw new InvalidOperationException($"Components count reached {MaxChildCount}"); | |||
| if (Components.Any(x => x.Type == ComponentType.SelectMenu)) | |||
| if (Components.Any(x => x.Type.IsSelectType())) | |||
| throw new InvalidOperationException($"A button cannot be added to a row with a SelectMenu"); | |||
| AddComponent(builtButton); | |||
| @@ -458,11 +456,15 @@ namespace Discord | |||
| case ComponentType.ActionRow: | |||
| return false; | |||
| case ComponentType.Button: | |||
| if (Components.Any(x => x.Type == ComponentType.SelectMenu)) | |||
| if (Components.Any(x => x.Type.IsSelectType())) | |||
| return false; | |||
| else | |||
| return Components.Count < 5; | |||
| case ComponentType.SelectMenu: | |||
| case ComponentType.ChannelSelect: | |||
| case ComponentType.MentionableSelect: | |||
| case ComponentType.RoleSelect: | |||
| case ComponentType.UserSelect: | |||
| return Components.Count == 0; | |||
| default: | |||
| return false; | |||
| @@ -759,6 +761,18 @@ namespace Discord | |||
| }; | |||
| } | |||
| /// <summary> | |||
| /// Gets or sets the type of the current select menu. | |||
| /// </summary> | |||
| /// <exception cref="ArgumentException">Type must be a select menu type.</exception> | |||
| public ComponentType Type | |||
| { | |||
| get => _type; | |||
| set => _type = value.IsSelectType() | |||
| ? value | |||
| : throw new ArgumentException("Type must be a select menu type.", nameof(value)); | |||
| } | |||
| /// <summary> | |||
| /// Gets or sets the placeholder text of the current select menu. | |||
| /// </summary> | |||
| @@ -827,11 +841,17 @@ namespace Discord | |||
| /// </summary> | |||
| public bool IsDisabled { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the menu's channel types (only valid on <see cref="ComponentType.ChannelSelect"/>s). | |||
| /// </summary> | |||
| public List<ChannelType> ChannelTypes { get; set; } | |||
| private List<SelectMenuOptionBuilder> _options = new List<SelectMenuOptionBuilder>(); | |||
| private int _minValues = 1; | |||
| private int _maxValues = 1; | |||
| private string _placeholder; | |||
| private string _customId; | |||
| private ComponentType _type = ComponentType.SelectMenu; | |||
| /// <summary> | |||
| /// Creates a new instance of a <see cref="SelectMenuBuilder"/>. | |||
| @@ -862,7 +882,9 @@ namespace Discord | |||
| /// <param name="maxValues">The max values of this select menu.</param> | |||
| /// <param name="minValues">The min values of this select menu.</param> | |||
| /// <param name="isDisabled">Disabled this select menu or not.</param> | |||
| public SelectMenuBuilder(string customId, List<SelectMenuOptionBuilder> options, string placeholder = null, int maxValues = 1, int minValues = 1, bool isDisabled = false) | |||
| /// <param name="type">The <see cref="ComponentType"/> of this select menu.</param> | |||
| /// <param name="channelTypes">The types of channels this menu can select (only valid on <see cref="ComponentType.ChannelSelect"/>s)</param> | |||
| public SelectMenuBuilder(string customId, List<SelectMenuOptionBuilder> options = null, string placeholder = null, int maxValues = 1, int minValues = 1, bool isDisabled = false, ComponentType type = ComponentType.SelectMenu, List<ChannelType> channelTypes = null) | |||
| { | |||
| CustomId = customId; | |||
| Options = options; | |||
| @@ -870,6 +892,8 @@ namespace Discord | |||
| IsDisabled = isDisabled; | |||
| MaxValues = maxValues; | |||
| MinValues = minValues; | |||
| Type = type; | |||
| ChannelTypes = channelTypes ?? new(); | |||
| } | |||
| /// <summary> | |||
| @@ -990,6 +1014,45 @@ namespace Discord | |||
| return this; | |||
| } | |||
| /// <summary> | |||
| /// Sets the menu's current type. | |||
| /// </summary> | |||
| /// <param name="type">The type of the menu.</param> | |||
| /// <returns> | |||
| /// The current builder. | |||
| /// </returns> | |||
| public SelectMenuBuilder WithType(ComponentType type) | |||
| { | |||
| Type = type; | |||
| return this; | |||
| } | |||
| /// <summary> | |||
| /// Sets the menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>s). | |||
| /// </summary> | |||
| /// <param name="channelTypes">The valid channel types of the menu.</param> | |||
| /// <returns> | |||
| /// The current builder. | |||
| /// </returns> | |||
| public SelectMenuBuilder WithChannelTypes(List<ChannelType> channelTypes) | |||
| { | |||
| ChannelTypes = channelTypes; | |||
| return this; | |||
| } | |||
| /// <summary> | |||
| /// Sets the menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>s). | |||
| /// </summary> | |||
| /// <param name="channelTypes">The valid channel types of the menu.</param> | |||
| /// <returns> | |||
| /// The current builder. | |||
| /// </returns> | |||
| public SelectMenuBuilder WithChannelTypes(params ChannelType[] channelTypes) | |||
| { | |||
| ChannelTypes = channelTypes.ToList(); | |||
| return this; | |||
| } | |||
| /// <summary> | |||
| /// Builds a <see cref="SelectMenuComponent"/> | |||
| /// </summary> | |||
| @@ -998,7 +1061,7 @@ namespace Discord | |||
| { | |||
| var options = Options?.Select(x => x.Build()).ToList(); | |||
| return new SelectMenuComponent(CustomId, options, Placeholder, MinValues, MaxValues, IsDisabled); | |||
| return new SelectMenuComponent(CustomId, options, Placeholder, MinValues, MaxValues, IsDisabled, Type, ChannelTypes); | |||
| } | |||
| } | |||
| @@ -26,8 +26,23 @@ namespace Discord | |||
| TextInput = 4, | |||
| /// <summary> | |||
| /// An interaction sent when a model is submitted. | |||
| /// A select menu for picking from users. | |||
| /// </summary> | |||
| ModalSubmit = 5, | |||
| UserSelect = 5, | |||
| /// <summary> | |||
| /// A select menu for picking from roles. | |||
| /// </summary> | |||
| RoleSelect = 6, | |||
| /// <summary> | |||
| /// A select menu for picking from roles and users. | |||
| /// </summary> | |||
| MentionableSelect = 7, | |||
| /// <summary> | |||
| /// A select menu for picking from channels. | |||
| /// </summary> | |||
| ChannelSelect = 8, | |||
| } | |||
| } | |||
| @@ -18,12 +18,27 @@ namespace Discord | |||
| ComponentType Type { get; } | |||
| /// <summary> | |||
| /// Gets the value(s) of a <see cref="SelectMenuComponent"/> interaction response. | |||
| /// Gets the value(s) of a <see cref="ComponentType.SelectMenu"/> interaction response. | |||
| /// </summary> | |||
| IReadOnlyCollection<string> Values { get; } | |||
| /// <summary> | |||
| /// Gets the value of a <see cref="TextInputComponent"/> interaction response. | |||
| /// Gets the channels(s) of a <see cref="ComponentType.ChannelSelect"/> interaction response. | |||
| /// </summary> | |||
| IReadOnlyCollection<IChannel> Channels { get; } | |||
| /// <summary> | |||
| /// Gets the user(s) of a <see cref="ComponentType.UserSelect"/> or <see cref="ComponentType.MentionableSelect"/> interaction response. | |||
| /// </summary> | |||
| IReadOnlyCollection<IUser> Users { get; } | |||
| /// <summary> | |||
| /// Gets the roles(s) of a <see cref="ComponentType.RoleSelect"/> or <see cref="ComponentType.MentionableSelect"/> interaction response. | |||
| /// </summary> | |||
| IReadOnlyCollection<IRole> Roles { get; } | |||
| /// <summary> | |||
| /// Gets the value of a <see cref="ComponentType.TextInput"/> interaction response. | |||
| /// </summary> | |||
| public string Value { get; } | |||
| } | |||
| @@ -1,3 +1,4 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| @@ -9,7 +10,7 @@ namespace Discord | |||
| public class SelectMenuComponent : IMessageComponent | |||
| { | |||
| /// <inheritdoc/> | |||
| public ComponentType Type => ComponentType.SelectMenu; | |||
| public ComponentType Type { get; } | |||
| /// <inheritdoc/> | |||
| public string CustomId { get; } | |||
| @@ -39,6 +40,11 @@ namespace Discord | |||
| /// </summary> | |||
| public bool IsDisabled { get; } | |||
| /// <summary> | |||
| /// Gets the allowed channel types for this modal | |||
| /// </summary> | |||
| public IReadOnlyCollection<ChannelType> ChannelTypes { get; } | |||
| /// <summary> | |||
| /// Turns this select menu into a builder. | |||
| /// </summary> | |||
| @@ -52,9 +58,9 @@ namespace Discord | |||
| Placeholder, | |||
| MaxValues, | |||
| MinValues, | |||
| IsDisabled); | |||
| IsDisabled, Type, ChannelTypes.ToList()); | |||
| internal SelectMenuComponent(string customId, List<SelectMenuOption> options, string placeholder, int minValues, int maxValues, bool disabled) | |||
| internal SelectMenuComponent(string customId, List<SelectMenuOption> options, string placeholder, int minValues, int maxValues, bool disabled, ComponentType type, IEnumerable<ChannelType> channelTypes = null) | |||
| { | |||
| CustomId = customId; | |||
| Options = options; | |||
| @@ -62,6 +68,8 @@ namespace Discord | |||
| MinValues = minValues; | |||
| MaxValues = maxValues; | |||
| IsDisabled = disabled; | |||
| Type = type; | |||
| ChannelTypes = channelTypes?.ToArray() ?? Array.Empty<ChannelType>(); | |||
| } | |||
| } | |||
| } | |||
| @@ -7,12 +7,12 @@ using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| /// <summary> | |||
| /// Represents a modal interaction. | |||
| /// Represents a modal interaction. | |||
| /// </summary> | |||
| public class Modal : IMessageComponent | |||
| { | |||
| /// <inheritdoc/> | |||
| public ComponentType Type => ComponentType.ModalSubmit; | |||
| public ComponentType Type => throw new NotSupportedException("Modals do not have a component type."); | |||
| /// <summary> | |||
| /// Gets the title of the modal. | |||
| @@ -0,0 +1,8 @@ | |||
| namespace Discord.Utils; | |||
| public static class ComponentTypeUtils | |||
| { | |||
| public static bool IsSelectType(this ComponentType type) => type is ComponentType.ChannelSelect | |||
| or ComponentType.SelectMenu or ComponentType.RoleSelect or ComponentType.UserSelect | |||
| or ComponentType.MentionableSelect; | |||
| } | |||
| @@ -21,6 +21,10 @@ namespace Discord.API | |||
| { | |||
| ComponentType.Button => new ButtonComponent(x as Discord.ButtonComponent), | |||
| ComponentType.SelectMenu => new SelectMenuComponent(x as Discord.SelectMenuComponent), | |||
| ComponentType.ChannelSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent), | |||
| ComponentType.UserSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent), | |||
| ComponentType.RoleSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent), | |||
| ComponentType.MentionableSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent), | |||
| ComponentType.TextInput => new TextInputComponent(x as Discord.TextInputComponent), | |||
| _ => null | |||
| }; | |||
| @@ -1,4 +1,5 @@ | |||
| using Newtonsoft.Json; | |||
| using System.Collections.Generic; | |||
| namespace Discord.API | |||
| { | |||
| @@ -15,5 +16,8 @@ namespace Discord.API | |||
| [JsonProperty("value")] | |||
| public Optional<string> Value { get; set; } | |||
| [JsonProperty("resolved")] | |||
| public Optional<MessageComponentInteractionDataResolved> Resolved { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,19 @@ | |||
| using Newtonsoft.Json; | |||
| using System.Collections.Generic; | |||
| namespace Discord.API; | |||
| internal class MessageComponentInteractionDataResolved | |||
| { | |||
| [JsonProperty("users")] | |||
| public Optional<Dictionary<string, User>> Users { get; set; } | |||
| [JsonProperty("members")] | |||
| public Optional<Dictionary<string, GuildMember>> Members { get; set; } | |||
| [JsonProperty("channels")] | |||
| public Optional<Dictionary<string, Channel>> Channels { get; set; } | |||
| [JsonProperty("roles")] | |||
| public Optional<Dictionary<string, Role>> Roles { get; set; } | |||
| } | |||
| @@ -26,6 +26,12 @@ namespace Discord.API | |||
| [JsonProperty("disabled")] | |||
| public bool Disabled { get; set; } | |||
| [JsonProperty("channel_types")] | |||
| public Optional<ChannelType[]> ChannelTypes { get; set; } | |||
| [JsonProperty("resolved")] | |||
| public Optional<MessageComponentInteractionDataResolved> Resolved { get; set; } | |||
| [JsonProperty("values")] | |||
| public Optional<string[]> Values { get; set; } | |||
| public SelectMenuComponent() { } | |||
| @@ -39,6 +45,7 @@ namespace Discord.API | |||
| MinValues = component.MinValues; | |||
| MaxValues = component.MaxValues; | |||
| Disabled = component.IsDisabled; | |||
| ChannelTypes = component.ChannelTypes.ToArray(); | |||
| } | |||
| } | |||
| } | |||
| @@ -21,6 +21,26 @@ namespace Discord.Rest | |||
| /// <inheritdoc/> | |||
| public IReadOnlyCollection<string> Values { get; } | |||
| /// <inheritdoc cref="IComponentInteractionData.Channels"/>/> | |||
| public IReadOnlyCollection<RestChannel> Channels { get; } | |||
| /// <inheritdoc cref="IComponentInteractionData.Users"/>/> | |||
| public IReadOnlyCollection<RestUser> Users { get; } | |||
| /// <inheritdoc cref="IComponentInteractionData.Roles"/>/> | |||
| public IReadOnlyCollection<RestRole> Roles { get; } | |||
| #region IComponentInteractionData | |||
| /// <inheritdoc/> | |||
| IReadOnlyCollection<IChannel> IComponentInteractionData.Channels => Channels; | |||
| /// <inheritdoc/> | |||
| IReadOnlyCollection<IUser> IComponentInteractionData.Users => Users; | |||
| /// <inheritdoc/> | |||
| IReadOnlyCollection<IRole> IComponentInteractionData.Roles => Roles; | |||
| #endregion | |||
| /// <inheritdoc/> | |||
| public string Value { get; } | |||
| @@ -21,12 +21,24 @@ namespace Discord.Rest | |||
| public IReadOnlyCollection<RestMessageComponentData> Components { get; } | |||
| /// <inheritdoc/> | |||
| public ComponentType Type => ComponentType.ModalSubmit; | |||
| public ComponentType Type => throw new NotSupportedException("Modals do not have a component type."); | |||
| /// <inheritdoc/> | |||
| public IReadOnlyCollection<string> Values | |||
| => throw new NotSupportedException("Modal interactions do not have values!"); | |||
| /// <inheritdoc/> | |||
| public IReadOnlyCollection<IChannel> Channels | |||
| => throw new NotSupportedException("Modal interactions do not have channels!"); | |||
| /// <inheritdoc/> | |||
| public IReadOnlyCollection<IUser> Users | |||
| => throw new NotSupportedException("Modal interactions do not have users!"); | |||
| /// <inheritdoc/> | |||
| public IReadOnlyCollection<IRole> Roles | |||
| => throw new NotSupportedException("Modal interactions do not have roles!"); | |||
| /// <inheritdoc/> | |||
| public string Value | |||
| => throw new NotSupportedException("Modal interactions do not have value!"); | |||
| @@ -170,26 +170,28 @@ namespace Discord.Rest | |||
| parsed.Url.GetValueOrDefault(), | |||
| parsed.Disabled.GetValueOrDefault()); | |||
| } | |||
| case ComponentType.SelectMenu: | |||
| case ComponentType.SelectMenu or ComponentType.ChannelSelect or ComponentType.RoleSelect or ComponentType.MentionableSelect or ComponentType.UserSelect: | |||
| { | |||
| var parsed = (API.SelectMenuComponent)y; | |||
| return new SelectMenuComponent( | |||
| parsed.CustomId, | |||
| parsed.Options.Select(z => new SelectMenuOption( | |||
| 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.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, | |||
| parsed.Disabled | |||
| ); | |||
| parsed.Disabled, | |||
| parsed.Type, | |||
| parsed.ChannelTypes.GetValueOrDefault() | |||
| ); | |||
| } | |||
| default: | |||
| return null; | |||
| @@ -30,6 +30,10 @@ namespace Discord.Net.Converters | |||
| messageComponent = new API.ButtonComponent(); | |||
| break; | |||
| case ComponentType.SelectMenu: | |||
| case ComponentType.ChannelSelect: | |||
| case ComponentType.MentionableSelect: | |||
| case ComponentType.RoleSelect: | |||
| case ComponentType.UserSelect: | |||
| messageComponent = new API.SelectMenuComponent(); | |||
| break; | |||
| case ComponentType.TextInput: | |||
| @@ -1,3 +1,5 @@ | |||
| #define DEBUG_PACKETS | |||
| using Discord.API.Gateway; | |||
| using Discord.Net.Queue; | |||
| using Discord.Net.Rest; | |||
| @@ -5,6 +5,7 @@ using Discord.Net.Converters; | |||
| using Discord.Net.Udp; | |||
| using Discord.Net.WebSockets; | |||
| using Discord.Rest; | |||
| using Discord.Utils; | |||
| using Newtonsoft.Json; | |||
| using Newtonsoft.Json.Linq; | |||
| using System; | |||
| @@ -2364,7 +2365,7 @@ namespace Discord.WebSocket | |||
| await TimedInvokeAsync(_slashCommandExecuted, nameof(SlashCommandExecuted), slashCommand).ConfigureAwait(false); | |||
| break; | |||
| case SocketMessageComponent messageComponent: | |||
| if (messageComponent.Data.Type == ComponentType.SelectMenu) | |||
| if (messageComponent.Data.Type.IsSelectType()) | |||
| await TimedInvokeAsync(_selectMenuExecuted, nameof(SelectMenuExecuted), messageComponent).ConfigureAwait(false); | |||
| if (messageComponent.Data.Type == ComponentType.Button) | |||
| await TimedInvokeAsync(_buttonExecuted, nameof(ButtonExecuted), messageComponent).ConfigureAwait(false); | |||
| @@ -1,3 +1,5 @@ | |||
| using Discord.Utils; | |||
| using System.Linq; | |||
| using System.Collections.Generic; | |||
| using Model = Discord.API.MessageComponentInteractionData; | |||
| @@ -8,24 +10,25 @@ namespace Discord.WebSocket | |||
| /// </summary> | |||
| public class SocketMessageComponentData : IComponentInteractionData | |||
| { | |||
| /// <summary> | |||
| /// Gets the components Custom Id that was clicked. | |||
| /// </summary> | |||
| /// <inheritdoc /> | |||
| public string CustomId { get; } | |||
| /// <summary> | |||
| /// Gets the type of the component clicked. | |||
| /// </summary> | |||
| /// <inheritdoc /> | |||
| public ComponentType Type { get; } | |||
| /// <summary> | |||
| /// Gets the value(s) of a <see cref="SelectMenuComponent"/> interaction response. | |||
| /// </summary> | |||
| /// <inheritdoc /> | |||
| public IReadOnlyCollection<string> Values { get; } | |||
| /// <summary> | |||
| /// Gets the value of a <see cref="TextInputComponent"/> interaction response. | |||
| /// </summary> | |||
| /// <inheritdoc /> | |||
| public IReadOnlyCollection<IChannel> Channels { get; } | |||
| /// <inheritdoc /> | |||
| public IReadOnlyCollection<IUser> Users { get; } | |||
| /// <inheritdoc /> | |||
| public IReadOnlyCollection<IRole> Roles { get; } | |||
| /// <inheritdoc /> | |||
| public string Value { get; } | |||
| internal SocketMessageComponentData(Model model) | |||
| @@ -45,7 +48,7 @@ namespace Discord.WebSocket | |||
| ? (component as API.TextInputComponent).Value.Value | |||
| : null; | |||
| Values = component.Type == ComponentType.SelectMenu | |||
| Values = component.Type.IsSelectType() | |||
| ? (component as API.SelectMenuComponent).Values.Value | |||
| : null; | |||
| } | |||
| @@ -118,7 +118,7 @@ namespace Discord.WebSocket | |||
| /// <returns> | |||
| /// Collection of WebSocket-based users. | |||
| /// </returns> | |||
| public IReadOnlyCollection<SocketUser> MentionedUsers => _userMentions; | |||
| public IReadOnlyCollection<SocketUser> MentionedUsers => _userMentions; | |||
| /// <inheritdoc /> | |||
| public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); | |||
| @@ -226,7 +226,9 @@ namespace Discord.WebSocket | |||
| parsed.Placeholder.GetValueOrDefault(), | |||
| parsed.MinValues, | |||
| parsed.MaxValues, | |||
| parsed.Disabled | |||
| parsed.Disabled, | |||
| parsed.Type, | |||
| parsed.ChannelTypes.GetValueOrDefault() | |||
| ); | |||
| } | |||
| default: | |||