* Initial support for new select types * Merge branch 'dev' of https://github.com/discord-net/Discord.Net into dev * some component&action row builder additions * remove redundant code * changes1 * maybe working rest part? * working-ish state? * fix some xml docs & small rework * typos * fix `ActionRowBuilder` * update DefaultArrayComponentConverter to accomodate new select-v2 types * now supports dm channels in channel selects * add a note to IF docs * add notes about nullable properties * <see langword="null"/> * update Modal.cs Co-authored-by: cat <lumitydev@gmail.com> Co-authored-by: Cenngo <cenk.ergen1@gmail.com>pull/2548/head
| @@ -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. | 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. | 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)] | [!code-csharp[Dropdown](samples/intro/dropdown.cs)] | ||||
| > [!NOTE] | > [!NOTE] | ||||
| @@ -1,7 +1,7 @@ | |||||
| using Discord.Utils; | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | using System.Linq; | ||||
| using Discord.Utils; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -92,9 +92,11 @@ namespace Discord | |||||
| /// <param name="maxValues">The max values of the placeholder.</param> | /// <param name="maxValues">The max values of the placeholder.</param> | ||||
| /// <param name="disabled">Whether or not the menu is disabled.</param> | /// <param name="disabled">Whether or not the menu is disabled.</param> | ||||
| /// <param name="row">The row to add the menu to.</param> | /// <param name="row">The row to add the menu to.</param> | ||||
| /// <param name="type">The type of the select menu.</param> | |||||
| /// <param name="channelTypes">Menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>)</param> | |||||
| /// <returns></returns> | /// <returns></returns> | ||||
| public ComponentBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> options, | |||||
| string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0) | |||||
| public ComponentBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> options = null, | |||||
| string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0, ComponentType type = ComponentType.SelectMenu, ChannelType[] channelTypes = null) | |||||
| { | { | ||||
| return WithSelectMenu(new SelectMenuBuilder() | return WithSelectMenu(new SelectMenuBuilder() | ||||
| .WithCustomId(customId) | .WithCustomId(customId) | ||||
| @@ -102,7 +104,9 @@ namespace Discord | |||||
| .WithPlaceholder(placeholder) | .WithPlaceholder(placeholder) | ||||
| .WithMaxValues(maxValues) | .WithMaxValues(maxValues) | ||||
| .WithMinValues(minValues) | .WithMinValues(minValues) | ||||
| .WithDisabled(disabled), | |||||
| .WithDisabled(disabled) | |||||
| .WithType(type) | |||||
| .WithChannelTypes(channelTypes), | |||||
| row); | row); | ||||
| } | } | ||||
| @@ -118,7 +122,7 @@ namespace Discord | |||||
| public ComponentBuilder WithSelectMenu(SelectMenuBuilder menu, int row = 0) | public ComponentBuilder WithSelectMenu(SelectMenuBuilder menu, int row = 0) | ||||
| { | { | ||||
| Preconditions.LessThan(row, MaxActionRowCount, nameof(row)); | 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."); | throw new InvalidOperationException("Please make sure that there is no duplicates values."); | ||||
| var builtMenu = menu.Build(); | var builtMenu = menu.Build(); | ||||
| @@ -278,9 +282,7 @@ namespace Discord | |||||
| { | { | ||||
| if (_actionRows?.SelectMany(x => x.Components)?.Any(x => x.Type == ComponentType.TextInput) ?? false) | if (_actionRows?.SelectMany(x => x.Components)?.Any(x => x.Type == ComponentType.TextInput) ?? false) | ||||
| throw new ArgumentException("TextInputComponents are not allowed in messages.", nameof(ActionRows)); | 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 | return _actionRows != null | ||||
| ? new MessageComponent(_actionRows.Select(x => x.Build()).ToList()) | ? new MessageComponent(_actionRows.Select(x => x.Build()).ToList()) | ||||
| : MessageComponent.Empty; | : MessageComponent.Empty; | ||||
| @@ -356,10 +358,13 @@ namespace Discord | |||||
| /// <param name="placeholder">The placeholder of the menu.</param> | /// <param name="placeholder">The placeholder of the menu.</param> | ||||
| /// <param name="minValues">The min values of the placeholder.</param> | /// <param name="minValues">The min values of the placeholder.</param> | ||||
| /// <param name="maxValues">The max 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> | |||||
| public ActionRowBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> options, | |||||
| string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false) | |||||
| /// <param name="disabled">Whether or not the menu is disabled.</param> | |||||
| /// <param name="type">The type of the select menu.</param> | |||||
| /// <param name="channelTypes">Menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>)</param> | |||||
| /// <returns>The current builder.</returns> | |||||
| public ActionRowBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> options = null, | |||||
| string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, | |||||
| ComponentType type = ComponentType.SelectMenu, ChannelType[] channelTypes = null) | |||||
| { | { | ||||
| return WithSelectMenu(new SelectMenuBuilder() | return WithSelectMenu(new SelectMenuBuilder() | ||||
| .WithCustomId(customId) | .WithCustomId(customId) | ||||
| @@ -367,7 +372,9 @@ namespace Discord | |||||
| .WithPlaceholder(placeholder) | .WithPlaceholder(placeholder) | ||||
| .WithMaxValues(maxValues) | .WithMaxValues(maxValues) | ||||
| .WithMinValues(minValues) | .WithMinValues(minValues) | ||||
| .WithDisabled(disabled)); | |||||
| .WithDisabled(disabled) | |||||
| .WithType(type) | |||||
| .WithChannelTypes(channelTypes)); | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -378,7 +385,7 @@ namespace Discord | |||||
| /// <returns>The current builder.</returns> | /// <returns>The current builder.</returns> | ||||
| public ActionRowBuilder WithSelectMenu(SelectMenuBuilder menu) | 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."); | throw new InvalidOperationException("Please make sure that there is no duplicates values."); | ||||
| var builtMenu = menu.Build(); | var builtMenu = menu.Build(); | ||||
| @@ -431,10 +438,10 @@ namespace Discord | |||||
| { | { | ||||
| var builtButton = button.Build(); | var builtButton = button.Build(); | ||||
| if(Components.Count >= 5) | |||||
| if (Components.Count >= 5) | |||||
| throw new InvalidOperationException($"Components count reached {MaxChildCount}"); | 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"); | throw new InvalidOperationException($"A button cannot be added to a row with a SelectMenu"); | ||||
| AddComponent(builtButton); | AddComponent(builtButton); | ||||
| @@ -458,11 +465,15 @@ namespace Discord | |||||
| case ComponentType.ActionRow: | case ComponentType.ActionRow: | ||||
| return false; | return false; | ||||
| case ComponentType.Button: | case ComponentType.Button: | ||||
| if (Components.Any(x => x.Type == ComponentType.SelectMenu)) | |||||
| if (Components.Any(x => x.Type.IsSelectType())) | |||||
| return false; | return false; | ||||
| else | else | ||||
| return Components.Count < 5; | return Components.Count < 5; | ||||
| case ComponentType.SelectMenu: | case ComponentType.SelectMenu: | ||||
| case ComponentType.ChannelSelect: | |||||
| case ComponentType.MentionableSelect: | |||||
| case ComponentType.RoleSelect: | |||||
| case ComponentType.UserSelect: | |||||
| return Components.Count == 0; | return Components.Count == 0; | ||||
| default: | default: | ||||
| return false; | return false; | ||||
| @@ -759,6 +770,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> | /// <summary> | ||||
| /// Gets or sets the placeholder text of the current select menu. | /// Gets or sets the placeholder text of the current select menu. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -815,8 +838,6 @@ namespace Discord | |||||
| { | { | ||||
| if (value != null) | if (value != null) | ||||
| Preconditions.AtMost(value.Count, MaxOptionCount, nameof(Options)); | Preconditions.AtMost(value.Count, MaxOptionCount, nameof(Options)); | ||||
| else | |||||
| throw new ArgumentNullException(nameof(value), $"{nameof(Options)} cannot be null."); | |||||
| _options = value; | _options = value; | ||||
| } | } | ||||
| @@ -827,11 +848,17 @@ namespace Discord | |||||
| /// </summary> | /// </summary> | ||||
| public bool IsDisabled { get; set; } | 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 List<SelectMenuOptionBuilder> _options = new List<SelectMenuOptionBuilder>(); | ||||
| private int _minValues = 1; | private int _minValues = 1; | ||||
| private int _maxValues = 1; | private int _maxValues = 1; | ||||
| private string _placeholder; | private string _placeholder; | ||||
| private string _customId; | private string _customId; | ||||
| private ComponentType _type = ComponentType.SelectMenu; | |||||
| /// <summary> | /// <summary> | ||||
| /// Creates a new instance of a <see cref="SelectMenuBuilder"/>. | /// Creates a new instance of a <see cref="SelectMenuBuilder"/>. | ||||
| @@ -862,7 +889,9 @@ namespace Discord | |||||
| /// <param name="maxValues">The max values of this select menu.</param> | /// <param name="maxValues">The max values of this select menu.</param> | ||||
| /// <param name="minValues">The min values of this select menu.</param> | /// <param name="minValues">The min values of this select menu.</param> | ||||
| /// <param name="isDisabled">Disabled this select menu or not.</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; | CustomId = customId; | ||||
| Options = options; | Options = options; | ||||
| @@ -870,6 +899,8 @@ namespace Discord | |||||
| IsDisabled = isDisabled; | IsDisabled = isDisabled; | ||||
| MaxValues = maxValues; | MaxValues = maxValues; | ||||
| MinValues = minValues; | MinValues = minValues; | ||||
| Type = type; | |||||
| ChannelTypes = channelTypes ?? new(); | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -990,6 +1021,47 @@ namespace Discord | |||||
| return this; | return this; | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Sets the menu's current type. | |||||
| /// </summary> | |||||
| /// <param name="type">The type of the menu.</param> | |||||
| /// <returns> | |||||
| /// The current builder. | |||||
| /// </returns> | |||||
| public SelectMenuBuilder WithType(ComponentType type) | |||||
| { | |||||
| Type = type; | |||||
| return this; | |||||
| } | |||||
| /// <summary> | |||||
| /// Sets the menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>s). | |||||
| /// </summary> | |||||
| /// <param name="channelTypes">The valid channel types of the menu.</param> | |||||
| /// <returns> | |||||
| /// The current builder. | |||||
| /// </returns> | |||||
| public SelectMenuBuilder WithChannelTypes(List<ChannelType> channelTypes) | |||||
| { | |||||
| ChannelTypes = channelTypes; | |||||
| return this; | |||||
| } | |||||
| /// <summary> | |||||
| /// Sets the menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>s). | |||||
| /// </summary> | |||||
| /// <param name="channelTypes">The valid channel types of the menu.</param> | |||||
| /// <returns> | |||||
| /// The current builder. | |||||
| /// </returns> | |||||
| public SelectMenuBuilder WithChannelTypes(params ChannelType[] channelTypes) | |||||
| { | |||||
| ChannelTypes = channelTypes is null | |||||
| ? ChannelTypeUtils.AllChannelTypes() | |||||
| : channelTypes.ToList(); | |||||
| return this; | |||||
| } | |||||
| /// <summary> | /// <summary> | ||||
| /// Builds a <see cref="SelectMenuComponent"/> | /// Builds a <see cref="SelectMenuComponent"/> | ||||
| /// </summary> | /// </summary> | ||||
| @@ -998,7 +1070,7 @@ namespace Discord | |||||
| { | { | ||||
| var options = Options?.Select(x => x.Build()).ToList(); | 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, | TextInput = 4, | ||||
| /// <summary> | /// <summary> | ||||
| /// An interaction sent when a model is submitted. | |||||
| /// A select menu for picking from users. | |||||
| /// </summary> | /// </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,32 @@ namespace Discord | |||||
| ComponentType Type { get; } | ComponentType Type { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets the value(s) of a <see cref="SelectMenuComponent"/> interaction response. | |||||
| /// Gets the value(s) of a <see cref="ComponentType.SelectMenu"/> interaction response. <see langword="null"/> if select type is different. | |||||
| /// </summary> | /// </summary> | ||||
| IReadOnlyCollection<string> Values { get; } | IReadOnlyCollection<string> Values { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets the value of a <see cref="TextInputComponent"/> interaction response. | |||||
| /// Gets the channels(s) of a <see cref="ComponentType.ChannelSelect"/> interaction response. <see langword="null"/> if select type is different. | |||||
| /// </summary> | |||||
| IReadOnlyCollection<IChannel> Channels { get; } | |||||
| /// <summary> | |||||
| /// Gets the user(s) of a <see cref="ComponentType.UserSelect"/> or <see cref="ComponentType.MentionableSelect"/> interaction response. <see langword="null"/> if select type is different. | |||||
| /// </summary> | |||||
| IReadOnlyCollection<IUser> Users { get; } | |||||
| /// <summary> | |||||
| /// Gets the roles(s) of a <see cref="ComponentType.RoleSelect"/> or <see cref="ComponentType.MentionableSelect"/> interaction response. <see langword="null"/> if select type is different. | |||||
| /// </summary> | |||||
| IReadOnlyCollection<IRole> Roles { get; } | |||||
| /// <summary> | |||||
| /// Gets the guild member(s) of a <see cref="ComponentType.UserSelect"/> or <see cref="ComponentType.MentionableSelect"/> interaction response. <see langword="null"/> if type select is different. | |||||
| /// </summary> | |||||
| IReadOnlyCollection<IGuildUser> Members { get; } | |||||
| /// <summary> | |||||
| /// Gets the value of a <see cref="ComponentType.TextInput"/> interaction response. | |||||
| /// </summary> | /// </summary> | ||||
| public string Value { get; } | public string Value { get; } | ||||
| } | } | ||||
| @@ -1,3 +1,4 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | using System.Linq; | ||||
| @@ -9,7 +10,7 @@ namespace Discord | |||||
| public class SelectMenuComponent : IMessageComponent | public class SelectMenuComponent : IMessageComponent | ||||
| { | { | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public ComponentType Type => ComponentType.SelectMenu; | |||||
| public ComponentType Type { get; } | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string CustomId { get; } | public string CustomId { get; } | ||||
| @@ -39,6 +40,11 @@ namespace Discord | |||||
| /// </summary> | /// </summary> | ||||
| public bool IsDisabled { get; } | public bool IsDisabled { get; } | ||||
| /// <summary> | |||||
| /// Gets the allowed channel types for this modal | |||||
| /// </summary> | |||||
| public IReadOnlyCollection<ChannelType> ChannelTypes { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Turns this select menu into a builder. | /// Turns this select menu into a builder. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -52,9 +58,9 @@ namespace Discord | |||||
| Placeholder, | Placeholder, | ||||
| MaxValues, | MaxValues, | ||||
| MinValues, | 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; | CustomId = customId; | ||||
| Options = options; | Options = options; | ||||
| @@ -62,6 +68,8 @@ namespace Discord | |||||
| MinValues = minValues; | MinValues = minValues; | ||||
| MaxValues = maxValues; | MaxValues = maxValues; | ||||
| IsDisabled = disabled; | IsDisabled = disabled; | ||||
| Type = type; | |||||
| ChannelTypes = channelTypes?.ToArray() ?? Array.Empty<ChannelType>(); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -7,12 +7,12 @@ using System.Threading.Tasks; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// Represents a modal interaction. | |||||
| /// Represents a modal interaction. | |||||
| /// </summary> | /// </summary> | ||||
| public class Modal : IMessageComponent | public class Modal : IMessageComponent | ||||
| { | { | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public ComponentType Type => ComponentType.ModalSubmit; | |||||
| public ComponentType Type => throw new NotSupportedException("Modals do not have a component type."); | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the title of the modal. | /// Gets the title of the modal. | ||||
| @@ -0,0 +1,14 @@ | |||||
| using System.Collections.Generic; | |||||
| namespace Discord.Utils; | |||||
| public static class ChannelTypeUtils | |||||
| { | |||||
| public static List<ChannelType> AllChannelTypes() | |||||
| => new List<ChannelType>() | |||||
| { | |||||
| 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 | |||||
| }; | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| @@ -1,5 +1,7 @@ | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Diagnostics.CodeAnalysis; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord.Interactions | namespace Discord.Interactions | ||||
| @@ -17,27 +19,56 @@ namespace Discord.Interactions | |||||
| throw new InvalidOperationException($"{nameof(DefaultArrayComponentConverter<T>)} cannot be used to convert a non-array type."); | throw new InvalidOperationException($"{nameof(DefaultArrayComponentConverter<T>)} cannot be used to convert a non-array type."); | ||||
| _underlyingType = typeof(T).GetElementType(); | _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<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) | public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) | ||||
| { | { | ||||
| var results = new List<TypeConverterResult>(); | |||||
| var objs = new List<object>(); | |||||
| 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<ulong, IUser>(); | |||||
| 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); | return TypeConverterResult.FromSuccess(destination); | ||||
| } | } | ||||
| @@ -21,6 +21,10 @@ namespace Discord.API | |||||
| { | { | ||||
| ComponentType.Button => new ButtonComponent(x as Discord.ButtonComponent), | ComponentType.Button => new ButtonComponent(x as Discord.ButtonComponent), | ||||
| ComponentType.SelectMenu => new SelectMenuComponent(x as Discord.SelectMenuComponent), | 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), | ComponentType.TextInput => new TextInputComponent(x as Discord.TextInputComponent), | ||||
| _ => null | _ => null | ||||
| }; | }; | ||||
| @@ -1,4 +1,5 @@ | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using System.Collections.Generic; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| @@ -15,5 +16,8 @@ namespace Discord.API | |||||
| [JsonProperty("value")] | [JsonProperty("value")] | ||||
| public Optional<string> Value { get; set; } | 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")] | [JsonProperty("disabled")] | ||||
| public bool Disabled { get; set; } | public bool Disabled { get; set; } | ||||
| [JsonProperty("channel_types")] | |||||
| public Optional<ChannelType[]> ChannelTypes { get; set; } | |||||
| [JsonProperty("resolved")] | |||||
| public Optional<MessageComponentInteractionDataResolved> Resolved { get; set; } | |||||
| [JsonProperty("values")] | [JsonProperty("values")] | ||||
| public Optional<string[]> Values { get; set; } | public Optional<string[]> Values { get; set; } | ||||
| public SelectMenuComponent() { } | public SelectMenuComponent() { } | ||||
| @@ -34,11 +40,12 @@ namespace Discord.API | |||||
| { | { | ||||
| Type = component.Type; | Type = component.Type; | ||||
| CustomId = component.CustomId; | CustomId = component.CustomId; | ||||
| Options = component.Options.Select(x => new SelectMenuOption(x)).ToArray(); | |||||
| Options = component.Options?.Select(x => new SelectMenuOption(x)).ToArray(); | |||||
| Placeholder = component.Placeholder; | Placeholder = component.Placeholder; | ||||
| MinValues = component.MinValues; | MinValues = component.MinValues; | ||||
| MaxValues = component.MaxValues; | MaxValues = component.MaxValues; | ||||
| Disabled = component.IsDisabled; | Disabled = component.IsDisabled; | ||||
| ChannelTypes = component.ChannelTypes.ToArray(); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -34,7 +34,7 @@ namespace Discord.Rest | |||||
| ? (DataModel)model.Data.Value | ? (DataModel)model.Data.Value | ||||
| : null; | : null; | ||||
| Data = new RestMessageComponentData(dataModel); | |||||
| Data = new RestMessageComponentData(dataModel, client, Guild); | |||||
| } | } | ||||
| internal new static async Task<RestMessageComponent> CreateAsync(DiscordRestClient client, Model model, bool doApiCall) | internal new static async Task<RestMessageComponent> CreateAsync(DiscordRestClient client, Model model, bool doApiCall) | ||||
| @@ -1,8 +1,12 @@ | |||||
| using Discord.API; | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | |||||
| using System.Linq; | using System.Linq; | ||||
| using System.Text; | using System.Text; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.MessageComponentInteractionData; | using Model = Discord.API.MessageComponentInteractionData; | ||||
| namespace Discord.Rest | namespace Discord.Rest | ||||
| @@ -10,7 +14,7 @@ namespace Discord.Rest | |||||
| /// <summary> | /// <summary> | ||||
| /// Represents data for a <see cref="RestMessageComponent"/>. | /// Represents data for a <see cref="RestMessageComponent"/>. | ||||
| /// </summary> | /// </summary> | ||||
| public class RestMessageComponentData : IComponentInteractionData, IDiscordInteractionData | |||||
| public class RestMessageComponentData : IComponentInteractionData | |||||
| { | { | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string CustomId { get; } | public string CustomId { get; } | ||||
| @@ -21,17 +25,75 @@ namespace Discord.Rest | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public IReadOnlyCollection<string> Values { get; } | 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; } | |||||
| /// <inheritdoc cref="IComponentInteractionData.Members"/> | |||||
| public IReadOnlyCollection<RestGuildUser> Members { get; } | |||||
| #region IComponentInteractionData | |||||
| /// <inheritdoc/> | |||||
| IReadOnlyCollection<IChannel> IComponentInteractionData.Channels => Channels; | |||||
| /// <inheritdoc/> | |||||
| IReadOnlyCollection<IUser> IComponentInteractionData.Users => Users; | |||||
| /// <inheritdoc/> | |||||
| IReadOnlyCollection<IRole> IComponentInteractionData.Roles => Roles; | |||||
| /// <inheritdoc/> | |||||
| IReadOnlyCollection<IGuildUser> IComponentInteractionData.Members => Members; | |||||
| #endregion | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string Value { get; } | public string Value { get; } | ||||
| internal RestMessageComponentData(Model model) | |||||
| internal RestMessageComponentData(Model model, BaseDiscordClient discord, IGuild guild) | |||||
| { | { | ||||
| CustomId = model.CustomId; | CustomId = model.CustomId; | ||||
| Type = model.ComponentType; | Type = model.ComponentType; | ||||
| Values = model.Values.GetValueOrDefault(); | 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<RestUser>(); | |||||
| 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<RestChannel>(); | |||||
| Roles = model.Resolved.Value.Roles.IsSpecified | |||||
| ? model.Resolved.Value.Roles.Value.Select(role => RestRole.Create(discord, guild, role.Value)).ToImmutableArray() | |||||
| : Array.Empty<RestRole>(); | |||||
| } | |||||
| } | } | ||||
| internal RestMessageComponentData(IMessageComponent component) | |||||
| internal RestMessageComponentData(IMessageComponent component, BaseDiscordClient discord, IGuild guild) | |||||
| { | { | ||||
| CustomId = component.CustomId; | CustomId = component.CustomId; | ||||
| Type = component.Type; | Type = component.Type; | ||||
| @@ -40,7 +102,33 @@ namespace Discord.Rest | |||||
| Value = textInput.Value.Value; | Value = textInput.Value.Value; | ||||
| if (component is API.SelectMenuComponent select) | 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; | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -23,7 +23,7 @@ namespace Discord.Rest | |||||
| ? (DataModel)model.Data.Value | ? (DataModel)model.Data.Value | ||||
| : null; | : null; | ||||
| Data = new RestModalData(dataModel); | |||||
| Data = new RestModalData(dataModel, client, Guild); | |||||
| } | } | ||||
| internal new static async Task<RestModal> CreateAsync(DiscordRestClient client, ModelBase model, bool doApiCall) | internal new static async Task<RestModal> CreateAsync(DiscordRestClient client, ModelBase model, bool doApiCall) | ||||
| @@ -10,7 +10,7 @@ namespace Discord.Rest | |||||
| /// <summary> | /// <summary> | ||||
| /// Represents data sent from a <see cref="InteractionType.ModalSubmit"/> Interaction. | /// Represents data sent from a <see cref="InteractionType.ModalSubmit"/> Interaction. | ||||
| /// </summary> | /// </summary> | ||||
| public class RestModalData : IComponentInteractionData, IModalInteractionData | |||||
| public class RestModalData : IModalInteractionData | |||||
| { | { | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string CustomId { get; } | public string CustomId { get; } | ||||
| @@ -20,25 +20,14 @@ namespace Discord.Rest | |||||
| /// </summary> | /// </summary> | ||||
| public IReadOnlyCollection<RestMessageComponentData> Components { get; } | public IReadOnlyCollection<RestMessageComponentData> Components { get; } | ||||
| /// <inheritdoc/> | |||||
| public ComponentType Type => ComponentType.ModalSubmit; | |||||
| /// <inheritdoc/> | |||||
| public IReadOnlyCollection<string> Values | |||||
| => throw new NotSupportedException("Modal interactions do not have values!"); | |||||
| /// <inheritdoc/> | |||||
| public string Value | |||||
| => throw new NotSupportedException("Modal interactions do not have value!"); | |||||
| IReadOnlyCollection<IComponentInteractionData> IModalInteractionData.Components => Components; | IReadOnlyCollection<IComponentInteractionData> IModalInteractionData.Components => Components; | ||||
| internal RestModalData(Model model) | |||||
| internal RestModalData(Model model, BaseDiscordClient discord, IGuild guild) | |||||
| { | { | ||||
| CustomId = model.CustomId; | CustomId = model.CustomId; | ||||
| Components = model.Components | Components = model.Components | ||||
| .SelectMany(x => x.Components) | .SelectMany(x => x.Components) | ||||
| .Select(x => new RestMessageComponentData(x)) | |||||
| .Select(x => new RestMessageComponentData(x, discord, guild)) | |||||
| .ToArray(); | .ToArray(); | ||||
| } | } | ||||
| } | } | ||||
| @@ -170,26 +170,28 @@ namespace Discord.Rest | |||||
| parsed.Url.GetValueOrDefault(), | parsed.Url.GetValueOrDefault(), | ||||
| parsed.Disabled.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; | var parsed = (API.SelectMenuComponent)y; | ||||
| return new SelectMenuComponent( | return new SelectMenuComponent( | ||||
| parsed.CustomId, | parsed.CustomId, | ||||
| parsed.Options.Select(z => new SelectMenuOption( | |||||
| parsed.Options?.Select(z => new SelectMenuOption( | |||||
| z.Label, | z.Label, | ||||
| z.Value, | z.Value, | ||||
| z.Description.GetValueOrDefault(), | z.Description.GetValueOrDefault(), | ||||
| z.Emoji.IsSpecified | 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(), | z.Default.ToNullable())).ToList(), | ||||
| parsed.Placeholder.GetValueOrDefault(), | parsed.Placeholder.GetValueOrDefault(), | ||||
| parsed.MinValues, | parsed.MinValues, | ||||
| parsed.MaxValues, | parsed.MaxValues, | ||||
| parsed.Disabled | |||||
| ); | |||||
| parsed.Disabled, | |||||
| parsed.Type, | |||||
| parsed.ChannelTypes.GetValueOrDefault() | |||||
| ); | |||||
| } | } | ||||
| default: | default: | ||||
| return null; | return null; | ||||
| @@ -30,6 +30,10 @@ namespace Discord.Net.Converters | |||||
| messageComponent = new API.ButtonComponent(); | messageComponent = new API.ButtonComponent(); | ||||
| break; | break; | ||||
| case ComponentType.SelectMenu: | case ComponentType.SelectMenu: | ||||
| case ComponentType.ChannelSelect: | |||||
| case ComponentType.MentionableSelect: | |||||
| case ComponentType.RoleSelect: | |||||
| case ComponentType.UserSelect: | |||||
| messageComponent = new API.SelectMenuComponent(); | messageComponent = new API.SelectMenuComponent(); | ||||
| break; | break; | ||||
| case ComponentType.TextInput: | case ComponentType.TextInput: | ||||
| @@ -5,6 +5,7 @@ using Discord.Net.Converters; | |||||
| using Discord.Net.Udp; | using Discord.Net.Udp; | ||||
| using Discord.Net.WebSockets; | using Discord.Net.WebSockets; | ||||
| using Discord.Rest; | using Discord.Rest; | ||||
| using Discord.Utils; | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using Newtonsoft.Json.Linq; | using Newtonsoft.Json.Linq; | ||||
| using System; | using System; | ||||
| @@ -2394,7 +2395,7 @@ namespace Discord.WebSocket | |||||
| await TimedInvokeAsync(_slashCommandExecuted, nameof(SlashCommandExecuted), slashCommand).ConfigureAwait(false); | await TimedInvokeAsync(_slashCommandExecuted, nameof(SlashCommandExecuted), slashCommand).ConfigureAwait(false); | ||||
| break; | break; | ||||
| case SocketMessageComponent messageComponent: | case SocketMessageComponent messageComponent: | ||||
| if (messageComponent.Data.Type == ComponentType.SelectMenu) | |||||
| if (messageComponent.Data.Type.IsSelectType()) | |||||
| await TimedInvokeAsync(_selectMenuExecuted, nameof(SelectMenuExecuted), messageComponent).ConfigureAwait(false); | await TimedInvokeAsync(_selectMenuExecuted, nameof(SelectMenuExecuted), messageComponent).ConfigureAwait(false); | ||||
| if (messageComponent.Data.Type == ComponentType.Button) | if (messageComponent.Data.Type == ComponentType.Button) | ||||
| await TimedInvokeAsync(_buttonExecuted, nameof(ButtonExecuted), messageComponent).ConfigureAwait(false); | await TimedInvokeAsync(_buttonExecuted, nameof(ButtonExecuted), messageComponent).ConfigureAwait(false); | ||||
| @@ -35,7 +35,7 @@ namespace Discord.WebSocket | |||||
| ? (DataModel)model.Data.Value | ? (DataModel)model.Data.Value | ||||
| : null; | : 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) | internal new static SocketMessageComponent Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel, SocketUser user) | ||||
| @@ -1,4 +1,9 @@ | |||||
| using Discord.Rest; | |||||
| using Discord.Utils; | |||||
| using System; | |||||
| using System.Linq; | |||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | |||||
| using Model = Discord.API.MessageComponentInteractionData; | using Model = Discord.API.MessageComponentInteractionData; | ||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| @@ -8,35 +13,84 @@ namespace Discord.WebSocket | |||||
| /// </summary> | /// </summary> | ||||
| public class SocketMessageComponentData : IComponentInteractionData | public class SocketMessageComponentData : IComponentInteractionData | ||||
| { | { | ||||
| /// <summary> | |||||
| /// Gets the components Custom Id that was clicked. | |||||
| /// </summary> | |||||
| /// <inheritdoc /> | |||||
| public string CustomId { get; } | public string CustomId { get; } | ||||
| /// <summary> | |||||
| /// Gets the type of the component clicked. | |||||
| /// </summary> | |||||
| /// <inheritdoc /> | |||||
| public ComponentType Type { get; } | public ComponentType Type { get; } | ||||
| /// <summary> | |||||
| /// Gets the value(s) of a <see cref="SelectMenuComponent"/> interaction response. | |||||
| /// </summary> | |||||
| /// <inheritdoc /> | |||||
| public IReadOnlyCollection<string> Values { get; } | public IReadOnlyCollection<string> Values { get; } | ||||
| /// <summary> | |||||
| /// Gets the value of a <see cref="TextInputComponent"/> interaction response. | |||||
| /// </summary> | |||||
| /// <inheritdoc cref="IComponentInteractionData.Channels"/> | |||||
| public IReadOnlyCollection<SocketChannel> Channels { get; } | |||||
| /// <inheritdoc cref="IComponentInteractionData.Users"/> | |||||
| /// <remarks>Returns <see cref="SocketUser"/> if user is cached, <see cref="RestUser"/> otherwise.</remarks> | |||||
| public IReadOnlyCollection<IUser> Users { get; } | |||||
| /// <inheritdoc cref="IComponentInteractionData.Roles"/> | |||||
| public IReadOnlyCollection<SocketRole> Roles { get; } | |||||
| /// <inheritdoc cref="IComponentInteractionData.Members"/> | |||||
| public IReadOnlyCollection<SocketGuildUser> Members { get; } | |||||
| #region IComponentInteractionData | |||||
| /// <inheritdoc /> | |||||
| IReadOnlyCollection<IChannel> IComponentInteractionData.Channels => Channels; | |||||
| /// <inheritdoc /> | |||||
| IReadOnlyCollection<IUser> IComponentInteractionData.Users => Users; | |||||
| /// <inheritdoc /> | |||||
| IReadOnlyCollection<IRole> IComponentInteractionData.Roles => Roles; | |||||
| /// <inheritdoc /> | |||||
| IReadOnlyCollection<IGuildUser> IComponentInteractionData.Members => Members; | |||||
| #endregion | |||||
| /// <inheritdoc /> | |||||
| public string Value { get; } | public string Value { get; } | ||||
| internal SocketMessageComponentData(Model model) | |||||
| internal SocketMessageComponentData(Model model, DiscordSocketClient discord, ClientState state, SocketGuild guild, API.User dmUser) | |||||
| { | { | ||||
| CustomId = model.CustomId; | CustomId = model.CustomId; | ||||
| Type = model.ComponentType; | Type = model.ComponentType; | ||||
| Values = model.Values.GetValueOrDefault(); | Values = model.Values.GetValueOrDefault(); | ||||
| Value = model.Value.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; | CustomId = component.CustomId; | ||||
| Type = component.Type; | Type = component.Type; | ||||
| @@ -45,9 +99,39 @@ namespace Discord.WebSocket | |||||
| ? (component as API.TextInputComponent).Value.Value | ? (component as API.TextInputComponent).Value.Value | ||||
| : null; | : 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; | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -27,8 +27,8 @@ namespace Discord.WebSocket | |||||
| var dataModel = model.Data.IsSpecified | var dataModel = model.Data.IsSpecified | ||||
| ? (DataModel)model.Data.Value | ? (DataModel)model.Data.Value | ||||
| : null; | : 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) | internal new static SocketModal Create(DiscordSocketClient client, ModelBase model, ISocketMessageChannel channel, SocketUser user) | ||||
| @@ -10,7 +10,7 @@ namespace Discord.WebSocket | |||||
| /// <summary> | /// <summary> | ||||
| /// Represents data sent from a <see cref="InteractionType.ModalSubmit"/>. | /// Represents data sent from a <see cref="InteractionType.ModalSubmit"/>. | ||||
| /// </summary> | /// </summary> | ||||
| public class SocketModalData : IDiscordInteractionData, IModalInteractionData | |||||
| public class SocketModalData : IModalInteractionData | |||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets the <see cref="Modal"/>'s Custom Id. | /// Gets the <see cref="Modal"/>'s Custom Id. | ||||
| @@ -22,12 +22,12 @@ namespace Discord.WebSocket | |||||
| /// </summary> | /// </summary> | ||||
| public IReadOnlyCollection<SocketMessageComponentData> Components { get; } | public IReadOnlyCollection<SocketMessageComponentData> Components { get; } | ||||
| internal SocketModalData(Model model) | |||||
| internal SocketModalData(Model model, DiscordSocketClient discord, ClientState state, SocketGuild guild, API.User dmUser) | |||||
| { | { | ||||
| CustomId = model.CustomId; | CustomId = model.CustomId; | ||||
| Components = model.Components | Components = model.Components | ||||
| .SelectMany(x => x.Components) | .SelectMany(x => x.Components) | ||||
| .Select(x => new SocketMessageComponentData(x)) | |||||
| .Select(x => new SocketMessageComponentData(x, discord, state, guild, dmUser)) | |||||
| .ToArray(); | .ToArray(); | ||||
| } | } | ||||
| @@ -118,7 +118,7 @@ namespace Discord.WebSocket | |||||
| /// <returns> | /// <returns> | ||||
| /// Collection of WebSocket-based users. | /// Collection of WebSocket-based users. | ||||
| /// </returns> | /// </returns> | ||||
| public IReadOnlyCollection<SocketUser> MentionedUsers => _userMentions; | |||||
| public IReadOnlyCollection<SocketUser> MentionedUsers => _userMentions; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); | public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); | ||||
| @@ -226,7 +226,9 @@ namespace Discord.WebSocket | |||||
| parsed.Placeholder.GetValueOrDefault(), | parsed.Placeholder.GetValueOrDefault(), | ||||
| parsed.MinValues, | parsed.MinValues, | ||||
| parsed.MaxValues, | parsed.MaxValues, | ||||
| parsed.Disabled | |||||
| parsed.Disabled, | |||||
| parsed.Type, | |||||
| parsed.ChannelTypes.GetValueOrDefault() | |||||
| ); | ); | ||||
| } | } | ||||
| default: | default: | ||||