diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index 21ea365de..4bee07e94 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -208,6 +208,9 @@ You may use as many wild card characters as you want. Unlike button interactions, select menu interactions also contain the values of the selected menu items. In this case, you should structure your method to accept a string array. +> [!NOTE] +> Use arrays of `IUser`, `IChannel`, `IRole`, `IMentionable` or their implementations to get data from a select menu with respective type. + [!code-csharp[Dropdown](samples/intro/dropdown.cs)] > [!NOTE] diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs index fd8798ed3..29ff80cf2 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs @@ -1,7 +1,7 @@ +using Discord.Utils; using System; using System.Collections.Generic; using System.Linq; -using Discord.Utils; namespace Discord { @@ -92,9 +92,11 @@ namespace Discord /// The max values of the placeholder. /// Whether or not the menu is disabled. /// The row to add the menu to. + /// The type of the select menu. + /// Menus valid channel types (only for ) /// - public ComponentBuilder WithSelectMenu(string customId, List options, - string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0) + public ComponentBuilder WithSelectMenu(string customId, List options = null, + string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0, ComponentType type = ComponentType.SelectMenu, ChannelType[] channelTypes = null) { return WithSelectMenu(new SelectMenuBuilder() .WithCustomId(customId) @@ -102,7 +104,9 @@ namespace Discord .WithPlaceholder(placeholder) .WithMaxValues(maxValues) .WithMinValues(minValues) - .WithDisabled(disabled), + .WithDisabled(disabled) + .WithType(type) + .WithChannelTypes(channelTypes), row); } @@ -118,7 +122,7 @@ namespace Discord public ComponentBuilder WithSelectMenu(SelectMenuBuilder menu, int row = 0) { Preconditions.LessThan(row, MaxActionRowCount, nameof(row)); - if (menu.Options.Distinct().Count() != menu.Options.Count) + if (menu.Options is not null && menu.Options.Distinct().Count() != menu.Options.Count) throw new InvalidOperationException("Please make sure that there is no duplicates values."); var builtMenu = menu.Build(); @@ -278,9 +282,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; @@ -356,10 +358,13 @@ namespace Discord /// The placeholder of the menu. /// The min values of the placeholder. /// The max values of the placeholder. - /// Whether or not the menu is disabled. - /// The current builder. - public ActionRowBuilder WithSelectMenu(string customId, List options, - string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false) + /// Whether or not the menu is disabled. + /// The type of the select menu. + /// Menus valid channel types (only for ) + /// The current builder. + public ActionRowBuilder WithSelectMenu(string customId, List options = null, + string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, + ComponentType type = ComponentType.SelectMenu, ChannelType[] channelTypes = null) { return WithSelectMenu(new SelectMenuBuilder() .WithCustomId(customId) @@ -367,7 +372,9 @@ namespace Discord .WithPlaceholder(placeholder) .WithMaxValues(maxValues) .WithMinValues(minValues) - .WithDisabled(disabled)); + .WithDisabled(disabled) + .WithType(type) + .WithChannelTypes(channelTypes)); } /// @@ -378,7 +385,7 @@ namespace Discord /// The current builder. public ActionRowBuilder WithSelectMenu(SelectMenuBuilder menu) { - if (menu.Options.Distinct().Count() != menu.Options.Count) + if (menu.Options is not null && menu.Options.Distinct().Count() != menu.Options.Count) throw new InvalidOperationException("Please make sure that there is no duplicates values."); var builtMenu = menu.Build(); @@ -431,10 +438,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 +465,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 +770,18 @@ namespace Discord }; } + /// + /// Gets or sets the type of the current select menu. + /// + /// Type must be a select menu type. + public ComponentType Type + { + get => _type; + set => _type = value.IsSelectType() + ? value + : throw new ArgumentException("Type must be a select menu type.", nameof(value)); + } + /// /// Gets or sets the placeholder text of the current select menu. /// @@ -815,8 +838,6 @@ namespace Discord { if (value != null) Preconditions.AtMost(value.Count, MaxOptionCount, nameof(Options)); - else - throw new ArgumentNullException(nameof(value), $"{nameof(Options)} cannot be null."); _options = value; } @@ -827,11 +848,17 @@ namespace Discord /// public bool IsDisabled { get; set; } + /// + /// Gets or sets the menu's channel types (only valid on s). + /// + public List ChannelTypes { get; set; } + private List _options = new List(); private int _minValues = 1; private int _maxValues = 1; private string _placeholder; private string _customId; + private ComponentType _type = ComponentType.SelectMenu; /// /// Creates a new instance of a . @@ -862,7 +889,9 @@ namespace Discord /// The max values of this select menu. /// The min values of this select menu. /// Disabled this select menu or not. - public SelectMenuBuilder(string customId, List options, string placeholder = null, int maxValues = 1, int minValues = 1, bool isDisabled = false) + /// The of this select menu. + /// The types of channels this menu can select (only valid on s) + public SelectMenuBuilder(string customId, List options = null, string placeholder = null, int maxValues = 1, int minValues = 1, bool isDisabled = false, ComponentType type = ComponentType.SelectMenu, List channelTypes = null) { CustomId = customId; Options = options; @@ -870,6 +899,8 @@ namespace Discord IsDisabled = isDisabled; MaxValues = maxValues; MinValues = minValues; + Type = type; + ChannelTypes = channelTypes ?? new(); } /// @@ -990,6 +1021,47 @@ namespace Discord return this; } + /// + /// Sets the menu's current type. + /// + /// The type of the menu. + /// + /// The current builder. + /// + public SelectMenuBuilder WithType(ComponentType type) + { + Type = type; + return this; + } + + /// + /// Sets the menus valid channel types (only for s). + /// + /// The valid channel types of the menu. + /// + /// The current builder. + /// + public SelectMenuBuilder WithChannelTypes(List channelTypes) + { + ChannelTypes = channelTypes; + return this; + } + + /// + /// Sets the menus valid channel types (only for s). + /// + /// The valid channel types of the menu. + /// + /// The current builder. + /// + public SelectMenuBuilder WithChannelTypes(params ChannelType[] channelTypes) + { + ChannelTypes = channelTypes is null + ? ChannelTypeUtils.AllChannelTypes() + : channelTypes.ToList(); + return this; + } + /// /// Builds a /// @@ -998,7 +1070,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); } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs index 1d63ee829..0ad3f741a 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs @@ -26,8 +26,23 @@ namespace Discord TextInput = 4, /// - /// An interaction sent when a model is submitted. + /// A select menu for picking from users. /// - ModalSubmit = 5, + UserSelect = 5, + + /// + /// A select menu for picking from roles. + /// + RoleSelect = 6, + + /// + /// A select menu for picking from roles and users. + /// + MentionableSelect = 7, + + /// + /// A select menu for picking from channels. + /// + ChannelSelect = 8, } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs index 039b6b41f..3a6526ee2 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs @@ -18,12 +18,32 @@ namespace Discord ComponentType Type { get; } /// - /// Gets the value(s) of a interaction response. + /// Gets the value(s) of a interaction response. if select type is different. /// IReadOnlyCollection Values { get; } /// - /// Gets the value of a interaction response. + /// Gets the channels(s) of a interaction response. if select type is different. + /// + IReadOnlyCollection Channels { get; } + + /// + /// Gets the user(s) of a or interaction response. if select type is different. + /// + IReadOnlyCollection Users { get; } + + /// + /// Gets the roles(s) of a or interaction response. if select type is different. + /// + IReadOnlyCollection Roles { get; } + + /// + /// Gets the guild member(s) of a or interaction response. if type select is different. + /// + IReadOnlyCollection Members { get; } + + /// + /// Gets the value of a interaction response. /// public string Value { get; } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs index 229c1e148..eccdd18c6 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; @@ -9,7 +10,7 @@ namespace Discord public class SelectMenuComponent : IMessageComponent { /// - public ComponentType Type => ComponentType.SelectMenu; + public ComponentType Type { get; } /// public string CustomId { get; } @@ -39,6 +40,11 @@ namespace Discord /// public bool IsDisabled { get; } + /// + /// Gets the allowed channel types for this modal + /// + public IReadOnlyCollection ChannelTypes { get; } + /// /// Turns this select menu into a builder. /// @@ -52,9 +58,9 @@ namespace Discord Placeholder, MaxValues, MinValues, - IsDisabled); + IsDisabled, Type, ChannelTypes.ToList()); - internal SelectMenuComponent(string customId, List options, string placeholder, int minValues, int maxValues, bool disabled) + internal SelectMenuComponent(string customId, List options, string placeholder, int minValues, int maxValues, bool disabled, ComponentType type, IEnumerable 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(); } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs b/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs index a0fde5ea3..a435d33ef 100644 --- a/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs +++ b/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs @@ -7,12 +7,12 @@ using System.Threading.Tasks; namespace Discord { /// - /// Represents a modal interaction. + /// Represents a modal interaction. /// public class Modal : IMessageComponent { /// - public ComponentType Type => ComponentType.ModalSubmit; + public ComponentType Type => throw new NotSupportedException("Modals do not have a component type."); /// /// Gets the title of the modal. diff --git a/src/Discord.Net.Core/Utils/ChannelTypeUtils.cs b/src/Discord.Net.Core/Utils/ChannelTypeUtils.cs new file mode 100644 index 000000000..4dd764508 --- /dev/null +++ b/src/Discord.Net.Core/Utils/ChannelTypeUtils.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Discord.Utils; + +public static class ChannelTypeUtils +{ + public static List AllChannelTypes() + => new List() + { + ChannelType.Forum, ChannelType.Category, ChannelType.DM, ChannelType.Group, ChannelType.GuildDirectory, + ChannelType.News, ChannelType.NewsThread, ChannelType.PrivateThread, ChannelType.PublicThread, + ChannelType.Stage, ChannelType.Store, ChannelType.Text, ChannelType.Voice + }; +} diff --git a/src/Discord.Net.Core/Utils/ComponentType.cs b/src/Discord.Net.Core/Utils/ComponentType.cs new file mode 100644 index 000000000..c7d42c512 --- /dev/null +++ b/src/Discord.Net.Core/Utils/ComponentType.cs @@ -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; +} diff --git a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs index 87fc431c5..5efdd537a 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Threading.Tasks; namespace Discord.Interactions @@ -17,27 +19,56 @@ namespace Discord.Interactions throw new InvalidOperationException($"{nameof(DefaultArrayComponentConverter)} cannot be used to convert a non-array type."); _underlyingType = typeof(T).GetElementType(); - _typeReader = interactionService.GetTypeReader(_underlyingType); + + _typeReader = true switch + { + _ when typeof(IUser).IsAssignableFrom(_underlyingType) + || typeof(IChannel).IsAssignableFrom(_underlyingType) + || typeof(IMentionable).IsAssignableFrom(_underlyingType) + || typeof(IRole).IsAssignableFrom(_underlyingType) => null, + _ => interactionService.GetTypeReader(_underlyingType) + }; } public override async Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) { - var results = new List(); + var objs = new List(); + + if(_typeReader is not null && option.Values.Count > 0) + foreach (var value in option.Values) + { + var result = await _typeReader.ReadAsync(context, value, services).ConfigureAwait(false); + + if (!result.IsSuccess) + return result; - foreach (var value in option.Values) + objs.Add(result.Value); + } + else { - var result = await _typeReader.ReadAsync(context, value, services).ConfigureAwait(false); + var users = new Dictionary(); + + if (option.Users is not null) + foreach (var user in option.Users) + users[user.Id] = user; + + if(option.Members is not null) + foreach(var member in option.Members) + users[member.Id] = member; + + objs.AddRange(users.Values); - if (!result.IsSuccess) - return result; + if(option.Roles is not null) + objs.AddRange(option.Roles); - results.Add(result); + if (option.Channels is not null) + objs.AddRange(option.Channels); } - var destination = Array.CreateInstance(_underlyingType, results.Count); + var destination = Array.CreateInstance(_underlyingType, objs.Count); - for (var i = 0; i < results.Count; i++) - destination.SetValue(results[i].Value, i); + for (var i = 0; i < objs.Count; i++) + destination.SetValue(objs[i], i); return TypeConverterResult.FromSuccess(destination); } diff --git a/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs b/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs index 9a7eb80dd..e97ca71d6 100644 --- a/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs +++ b/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs @@ -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 }; diff --git a/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs b/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs index 4633fc25a..1bc45d21b 100644 --- a/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs +++ b/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs @@ -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 Value { get; set; } + + [JsonProperty("resolved")] + public Optional Resolved { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/MessageComponentInteractionDataResolved.cs b/src/Discord.Net.Rest/API/Common/MessageComponentInteractionDataResolved.cs new file mode 100644 index 000000000..04f97cdfd --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/MessageComponentInteractionDataResolved.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Discord.API; + +internal class MessageComponentInteractionDataResolved +{ + [JsonProperty("users")] + public Optional> Users { get; set; } + + [JsonProperty("members")] + public Optional> Members { get; set; } + + [JsonProperty("channels")] + public Optional> Channels { get; set; } + + [JsonProperty("roles")] + public Optional> Roles { get; set; } +} diff --git a/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs b/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs index 25ac476c5..3975a8c1e 100644 --- a/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs +++ b/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs @@ -26,6 +26,12 @@ namespace Discord.API [JsonProperty("disabled")] public bool Disabled { get; set; } + [JsonProperty("channel_types")] + public Optional ChannelTypes { get; set; } + + [JsonProperty("resolved")] + public Optional Resolved { get; set; } + [JsonProperty("values")] public Optional Values { get; set; } public SelectMenuComponent() { } @@ -34,11 +40,12 @@ namespace Discord.API { Type = component.Type; CustomId = component.CustomId; - Options = component.Options.Select(x => new SelectMenuOption(x)).ToArray(); + Options = component.Options?.Select(x => new SelectMenuOption(x)).ToArray(); Placeholder = component.Placeholder; MinValues = component.MinValues; MaxValues = component.MaxValues; Disabled = component.IsDisabled; + ChannelTypes = component.ChannelTypes.ToArray(); } } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs index e0eab6051..b400852d6 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs @@ -34,7 +34,7 @@ namespace Discord.Rest ? (DataModel)model.Data.Value : null; - Data = new RestMessageComponentData(dataModel); + Data = new RestMessageComponentData(dataModel, client, Guild); } internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) diff --git a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs index b0efe418c..bc44d0df5 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs @@ -1,8 +1,12 @@ +using Discord.API; + using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text; using System.Threading.Tasks; + using Model = Discord.API.MessageComponentInteractionData; namespace Discord.Rest @@ -10,7 +14,7 @@ namespace Discord.Rest /// /// Represents data for a . /// - public class RestMessageComponentData : IComponentInteractionData, IDiscordInteractionData + public class RestMessageComponentData : IComponentInteractionData { /// public string CustomId { get; } @@ -21,17 +25,75 @@ namespace Discord.Rest /// public IReadOnlyCollection Values { get; } + /// + public IReadOnlyCollection Channels { get; } + + /// + public IReadOnlyCollection Users { get; } + + /// + public IReadOnlyCollection Roles { get; } + + /// + public IReadOnlyCollection Members { get; } + + #region IComponentInteractionData + + /// + IReadOnlyCollection IComponentInteractionData.Channels => Channels; + + /// + IReadOnlyCollection IComponentInteractionData.Users => Users; + + /// + IReadOnlyCollection IComponentInteractionData.Roles => Roles; + + /// + IReadOnlyCollection IComponentInteractionData.Members => Members; + + #endregion + /// public string Value { get; } - internal RestMessageComponentData(Model model) + internal RestMessageComponentData(Model model, BaseDiscordClient discord, IGuild guild) { CustomId = model.CustomId; Type = model.ComponentType; Values = model.Values.GetValueOrDefault(); + Value = model.Value.GetValueOrDefault(); + + if (model.Resolved.IsSpecified) + { + Users = model.Resolved.Value.Users.IsSpecified + ? model.Resolved.Value.Users.Value.Select(user => RestUser.Create(discord, user.Value)).ToImmutableArray() + : Array.Empty(); + + Members = model.Resolved.Value.Members.IsSpecified + ? model.Resolved.Value.Members.Value.Select(member => + { + member.Value.User = model.Resolved.Value.Users.Value.First(u => u.Key == member.Key).Value; + + return RestGuildUser.Create(discord, guild, member.Value); + }).ToImmutableArray() + : null; + + Channels = model.Resolved.Value.Channels.IsSpecified + ? model.Resolved.Value.Channels.Value.Select(channel => + { + if (channel.Value.Type is ChannelType.DM) + return RestDMChannel.Create(discord, channel.Value); + return RestChannel.Create(discord, channel.Value); + }).ToImmutableArray() + : Array.Empty(); + + Roles = model.Resolved.Value.Roles.IsSpecified + ? model.Resolved.Value.Roles.Value.Select(role => RestRole.Create(discord, guild, role.Value)).ToImmutableArray() + : Array.Empty(); + } } - internal RestMessageComponentData(IMessageComponent component) + internal RestMessageComponentData(IMessageComponent component, BaseDiscordClient discord, IGuild guild) { CustomId = component.CustomId; Type = component.Type; @@ -40,7 +102,33 @@ namespace Discord.Rest Value = textInput.Value.Value; if (component is API.SelectMenuComponent select) - Values = select.Values.Value; + { + Values = select.Values.GetValueOrDefault(null); + + if (select.Resolved.IsSpecified) + { + Users = select.Resolved.Value.Users.IsSpecified + ? select.Resolved.Value.Users.Value.Select(user => RestUser.Create(discord, user.Value)).ToImmutableArray() + : null; + + Members = select.Resolved.Value.Members.IsSpecified + ? select.Resolved.Value.Members.Value.Select(member => + { + member.Value.User = select.Resolved.Value.Users.Value.First(u => u.Key == member.Key).Value; + + return RestGuildUser.Create(discord, guild, member.Value); + }).ToImmutableArray() + : null; + + Channels = select.Resolved.Value.Channels.IsSpecified + ? select.Resolved.Value.Channels.Value.Select(channel => RestChannel.Create(discord, channel.Value)).ToImmutableArray() + : null; + + Roles = select.Resolved.Value.Roles.IsSpecified + ? select.Resolved.Value.Roles.Value.Select(role => RestRole.Create(discord, guild, role.Value)).ToImmutableArray() + : null; + } + } } } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs index 9229b63b5..ef16d7b25 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs @@ -23,7 +23,7 @@ namespace Discord.Rest ? (DataModel)model.Data.Value : null; - Data = new RestModalData(dataModel); + Data = new RestModalData(dataModel, client, Guild); } internal new static async Task CreateAsync(DiscordRestClient client, ModelBase model, bool doApiCall) diff --git a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs index 22460ae51..7ecf804a1 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs @@ -10,7 +10,7 @@ namespace Discord.Rest /// /// Represents data sent from a Interaction. /// - public class RestModalData : IComponentInteractionData, IModalInteractionData + public class RestModalData : IModalInteractionData { /// public string CustomId { get; } @@ -20,25 +20,14 @@ namespace Discord.Rest /// public IReadOnlyCollection Components { get; } - /// - public ComponentType Type => ComponentType.ModalSubmit; - - /// - public IReadOnlyCollection Values - => throw new NotSupportedException("Modal interactions do not have values!"); - - /// - public string Value - => throw new NotSupportedException("Modal interactions do not have value!"); - IReadOnlyCollection IModalInteractionData.Components => Components; - internal RestModalData(Model model) + internal RestModalData(Model model, BaseDiscordClient discord, IGuild guild) { CustomId = model.CustomId; Components = model.Components .SelectMany(x => x.Components) - .Select(x => new RestMessageComponentData(x)) + .Select(x => new RestMessageComponentData(x, discord, guild)) .ToArray(); } } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index 69e038fd2..8b6b44e39 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -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; diff --git a/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs b/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs index 36542d83b..7888219bc 100644 --- a/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs @@ -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: diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index f2239010f..cb982889c 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -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; @@ -2394,7 +2395,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); diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs index 2a1a67d04..286629ceb 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs @@ -35,7 +35,7 @@ namespace Discord.WebSocket ? (DataModel)model.Data.Value : null; - Data = new SocketMessageComponentData(dataModel); + Data = new SocketMessageComponentData(dataModel, client, client.State, client.Guilds.FirstOrDefault(x => x.Id == model.GuildId.GetValueOrDefault()), model.User.GetValueOrDefault()); } internal new static SocketMessageComponent Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel, SocketUser user) diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs index c7f6c5106..0099ec77c 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs @@ -1,4 +1,9 @@ +using Discord.Rest; +using Discord.Utils; +using System; +using System.Linq; using System.Collections.Generic; +using System.Collections.Immutable; using Model = Discord.API.MessageComponentInteractionData; namespace Discord.WebSocket @@ -8,35 +13,84 @@ namespace Discord.WebSocket /// public class SocketMessageComponentData : IComponentInteractionData { - /// - /// Gets the components Custom Id that was clicked. - /// + /// public string CustomId { get; } - /// - /// Gets the type of the component clicked. - /// + /// public ComponentType Type { get; } - /// - /// Gets the value(s) of a interaction response. - /// + /// public IReadOnlyCollection Values { get; } - /// - /// Gets the value of a interaction response. - /// + /// + public IReadOnlyCollection Channels { get; } + + /// + /// Returns if user is cached, otherwise. + public IReadOnlyCollection Users { get; } + + /// + public IReadOnlyCollection Roles { get; } + + /// + public IReadOnlyCollection Members { get; } + + #region IComponentInteractionData + + /// + IReadOnlyCollection IComponentInteractionData.Channels => Channels; + + /// + IReadOnlyCollection IComponentInteractionData.Users => Users; + + /// + IReadOnlyCollection IComponentInteractionData.Roles => Roles; + + /// + IReadOnlyCollection IComponentInteractionData.Members => Members; + + #endregion + /// public string Value { get; } - internal SocketMessageComponentData(Model model) + internal SocketMessageComponentData(Model model, DiscordSocketClient discord, ClientState state, SocketGuild guild, API.User dmUser) { CustomId = model.CustomId; Type = model.ComponentType; Values = model.Values.GetValueOrDefault(); Value = model.Value.GetValueOrDefault(); + + if (model.Resolved.IsSpecified) + { + Users = model.Resolved.Value.Users.IsSpecified + ? model.Resolved.Value.Users.Value.Select(user => (IUser)state.GetUser(user.Value.Id) ?? RestUser.Create(discord, user.Value)).ToImmutableArray() + : null; + + Members = model.Resolved.Value.Members.IsSpecified + ? model.Resolved.Value.Members.Value.Select(member => + { + member.Value.User = model.Resolved.Value.Users.Value.First(u => u.Key == member.Key).Value; + return SocketGuildUser.Create(guild, state, member.Value); + }).ToImmutableArray() + : null; + + Channels = model.Resolved.Value.Channels.IsSpecified + ? model.Resolved.Value.Channels.Value.Select( + channel => + { + if (channel.Value.Type is ChannelType.DM) + return SocketDMChannel.Create(discord, state, channel.Value.Id, dmUser); + return (SocketChannel)SocketGuildChannel.Create(guild, state, channel.Value); + }).ToImmutableArray() + : null; + + Roles = model.Resolved.Value.Roles.IsSpecified + ? model.Resolved.Value.Roles.Value.Select(role => SocketRole.Create(guild, state, role.Value)).ToImmutableArray() + : null; + } } - internal SocketMessageComponentData(IMessageComponent component) + internal SocketMessageComponentData(IMessageComponent component, DiscordSocketClient discord, ClientState state, SocketGuild guild, API.User dmUser) { CustomId = component.CustomId; Type = component.Type; @@ -45,9 +99,39 @@ namespace Discord.WebSocket ? (component as API.TextInputComponent).Value.Value : null; - Values = component.Type == ComponentType.SelectMenu - ? (component as API.SelectMenuComponent).Values.Value - : null; + if (component is API.SelectMenuComponent select) + { + Values = select.Values.GetValueOrDefault(null); + + if (select.Resolved.IsSpecified) + { + Users = select.Resolved.Value.Users.IsSpecified + ? select.Resolved.Value.Users.Value.Select(user => (IUser)state.GetUser(user.Value.Id) ?? RestUser.Create(discord, user.Value)).ToImmutableArray() + : null; + + Members = select.Resolved.Value.Members.IsSpecified + ? select.Resolved.Value.Members.Value.Select(member => + { + member.Value.User = select.Resolved.Value.Users.Value.First(u => u.Key == member.Key).Value; + return SocketGuildUser.Create(guild, state, member.Value); + }).ToImmutableArray() + : null; + + Channels = select.Resolved.Value.Channels.IsSpecified + ? select.Resolved.Value.Channels.Value.Select( + channel => + { + if (channel.Value.Type is ChannelType.DM) + return SocketDMChannel.Create(discord, state, channel.Value.Id, dmUser); + return (SocketChannel)SocketGuildChannel.Create(guild, state, channel.Value); + }).ToImmutableArray() + : null; + + Roles = select.Resolved.Value.Roles.IsSpecified + ? select.Resolved.Value.Roles.Value.Select(role => SocketRole.Create(guild, state, role.Value)).ToImmutableArray() + : null; + } + } } } } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs index 647544b48..7b4466af3 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs @@ -27,8 +27,8 @@ namespace Discord.WebSocket var dataModel = model.Data.IsSpecified ? (DataModel)model.Data.Value : null; - - Data = new SocketModalData(dataModel); + + Data = new SocketModalData(dataModel, client, client.State, client.State.GetGuild(model.GuildId.GetValueOrDefault()), model.User.GetValueOrDefault()); } internal new static SocketModal Create(DiscordSocketClient client, ModelBase model, ISocketMessageChannel channel, SocketUser user) diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs index df8be2fe8..ec1fe9622 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs @@ -10,7 +10,7 @@ namespace Discord.WebSocket /// /// Represents data sent from a . /// - public class SocketModalData : IDiscordInteractionData, IModalInteractionData + public class SocketModalData : IModalInteractionData { /// /// Gets the 's Custom Id. @@ -22,12 +22,12 @@ namespace Discord.WebSocket /// public IReadOnlyCollection Components { get; } - internal SocketModalData(Model model) + internal SocketModalData(Model model, DiscordSocketClient discord, ClientState state, SocketGuild guild, API.User dmUser) { CustomId = model.CustomId; Components = model.Components .SelectMany(x => x.Components) - .Select(x => new SocketMessageComponentData(x)) + .Select(x => new SocketMessageComponentData(x, discord, state, guild, dmUser)) .ToArray(); } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 3cd67beb5..40a645afb 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -118,7 +118,7 @@ namespace Discord.WebSocket /// /// Collection of WebSocket-based users. /// - public IReadOnlyCollection MentionedUsers => _userMentions; + public IReadOnlyCollection MentionedUsers => _userMentions; /// 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: