* 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>fix/if-docs
@@ -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] | |||
@@ -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 | |||
/// <param name="maxValues">The max values of the placeholder.</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="type">The type of the select menu.</param> | |||
/// <param name="channelTypes">Menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>)</param> | |||
/// <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() | |||
.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 | |||
/// <param name="placeholder">The placeholder of the menu.</param> | |||
/// <param name="minValues">The min values of the placeholder.</param> | |||
/// <param name="maxValues">The max values of the placeholder.</param> | |||
/// <param name="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() | |||
.WithCustomId(customId) | |||
@@ -367,7 +372,9 @@ namespace Discord | |||
.WithPlaceholder(placeholder) | |||
.WithMaxValues(maxValues) | |||
.WithMinValues(minValues) | |||
.WithDisabled(disabled)); | |||
.WithDisabled(disabled) | |||
.WithType(type) | |||
.WithChannelTypes(channelTypes)); | |||
} | |||
/// <summary> | |||
@@ -378,7 +385,7 @@ namespace Discord | |||
/// <returns>The current builder.</returns> | |||
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; | |||
@@ -760,6 +771,18 @@ namespace Discord | |||
} | |||
/// <summary> | |||
/// Gets or sets the type of the current select menu. | |||
/// </summary> | |||
/// <exception cref="ArgumentException">Type must be a select menu type.</exception> | |||
public ComponentType Type | |||
{ | |||
get => _type; | |||
set => _type = value.IsSelectType() | |||
? value | |||
: throw new ArgumentException("Type must be a select menu type.", nameof(value)); | |||
} | |||
/// <summary> | |||
/// Gets or sets the placeholder text of the current select menu. | |||
/// </summary> | |||
/// <exception cref="ArgumentException" accessor="set"><see cref="Placeholder"/> length exceeds <see cref="MaxPlaceholderLength"/>.</exception> | |||
@@ -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 | |||
/// </summary> | |||
public bool IsDisabled { get; set; } | |||
/// <summary> | |||
/// Gets or sets the menu's channel types (only valid on <see cref="ComponentType.ChannelSelect"/>s). | |||
/// </summary> | |||
public List<ChannelType> ChannelTypes { get; set; } | |||
private List<SelectMenuOptionBuilder> _options = new List<SelectMenuOptionBuilder>(); | |||
private int _minValues = 1; | |||
private int _maxValues = 1; | |||
private string _placeholder; | |||
private string _customId; | |||
private ComponentType _type = ComponentType.SelectMenu; | |||
/// <summary> | |||
/// Creates a new instance of a <see cref="SelectMenuBuilder"/>. | |||
@@ -862,7 +889,9 @@ namespace Discord | |||
/// <param name="maxValues">The max values of this select menu.</param> | |||
/// <param name="minValues">The min values of this select menu.</param> | |||
/// <param name="isDisabled">Disabled this select menu or not.</param> | |||
public SelectMenuBuilder(string customId, List<SelectMenuOptionBuilder> options, string placeholder = null, int maxValues = 1, int minValues = 1, bool isDisabled = false) | |||
/// <param name="type">The <see cref="ComponentType"/> of this select menu.</param> | |||
/// <param name="channelTypes">The types of channels this menu can select (only valid on <see cref="ComponentType.ChannelSelect"/>s)</param> | |||
public SelectMenuBuilder(string customId, List<SelectMenuOptionBuilder> options = null, string placeholder = null, int maxValues = 1, int minValues = 1, bool isDisabled = false, ComponentType type = ComponentType.SelectMenu, List<ChannelType> channelTypes = null) | |||
{ | |||
CustomId = customId; | |||
Options = options; | |||
@@ -870,6 +899,8 @@ namespace Discord | |||
IsDisabled = isDisabled; | |||
MaxValues = maxValues; | |||
MinValues = minValues; | |||
Type = type; | |||
ChannelTypes = channelTypes ?? new(); | |||
} | |||
/// <summary> | |||
@@ -991,6 +1022,47 @@ namespace Discord | |||
} | |||
/// <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> | |||
/// Builds a <see cref="SelectMenuComponent"/> | |||
/// </summary> | |||
/// <returns>The newly built <see cref="SelectMenuComponent"/></returns> | |||
@@ -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); | |||
} | |||
} | |||
@@ -26,8 +26,23 @@ namespace Discord | |||
TextInput = 4, | |||
/// <summary> | |||
/// An interaction sent when a model is submitted. | |||
/// A select menu for picking from users. | |||
/// </summary> | |||
ModalSubmit = 5, | |||
UserSelect = 5, | |||
/// <summary> | |||
/// A select menu for picking from roles. | |||
/// </summary> | |||
RoleSelect = 6, | |||
/// <summary> | |||
/// A select menu for picking from roles and users. | |||
/// </summary> | |||
MentionableSelect = 7, | |||
/// <summary> | |||
/// A select menu for picking from channels. | |||
/// </summary> | |||
ChannelSelect = 8, | |||
} | |||
} |
@@ -18,12 +18,32 @@ namespace Discord | |||
ComponentType Type { get; } | |||
/// <summary> | |||
/// Gets the value(s) of a <see cref="SelectMenuComponent"/> interaction response. | |||
/// Gets the value(s) of a <see cref="ComponentType.SelectMenu"/> interaction response. <see langword="null"/> if select type is different. | |||
/// </summary> | |||
IReadOnlyCollection<string> Values { get; } | |||
/// <summary> | |||
/// Gets the value of a <see cref="TextInputComponent"/> interaction response. | |||
/// Gets the channels(s) of a <see cref="ComponentType.ChannelSelect"/> interaction response. <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> | |||
public string Value { get; } | |||
} | |||
@@ -1,3 +1,4 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
@@ -9,7 +10,7 @@ namespace Discord | |||
public class SelectMenuComponent : IMessageComponent | |||
{ | |||
/// <inheritdoc/> | |||
public ComponentType Type => ComponentType.SelectMenu; | |||
public ComponentType Type { get; } | |||
/// <inheritdoc/> | |||
public string CustomId { get; } | |||
@@ -40,6 +41,11 @@ namespace Discord | |||
public bool IsDisabled { get; } | |||
/// <summary> | |||
/// Gets the allowed channel types for this modal | |||
/// </summary> | |||
public IReadOnlyCollection<ChannelType> ChannelTypes { get; } | |||
/// <summary> | |||
/// Turns this select menu into a builder. | |||
/// </summary> | |||
/// <returns> | |||
@@ -52,9 +58,9 @@ namespace Discord | |||
Placeholder, | |||
MaxValues, | |||
MinValues, | |||
IsDisabled); | |||
IsDisabled, Type, ChannelTypes.ToList()); | |||
internal SelectMenuComponent(string customId, List<SelectMenuOption> options, string placeholder, int minValues, int maxValues, bool disabled) | |||
internal SelectMenuComponent(string customId, List<SelectMenuOption> options, string placeholder, int minValues, int maxValues, bool disabled, ComponentType type, IEnumerable<ChannelType> channelTypes = null) | |||
{ | |||
CustomId = customId; | |||
Options = options; | |||
@@ -62,6 +68,8 @@ namespace Discord | |||
MinValues = minValues; | |||
MaxValues = maxValues; | |||
IsDisabled = disabled; | |||
Type = type; | |||
ChannelTypes = channelTypes?.ToArray() ?? Array.Empty<ChannelType>(); | |||
} | |||
} | |||
} |
@@ -7,12 +7,12 @@ using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
/// <summary> | |||
/// Represents a modal interaction. | |||
/// Represents a modal interaction. | |||
/// </summary> | |||
public class Modal : IMessageComponent | |||
{ | |||
/// <inheritdoc/> | |||
public ComponentType Type => ComponentType.ModalSubmit; | |||
public ComponentType Type => throw new NotSupportedException("Modals do not have a component type."); | |||
/// <summary> | |||
/// Gets the title of the modal. | |||
@@ -0,0 +1,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.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<T>)} 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<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); | |||
} | |||
@@ -21,6 +21,10 @@ namespace Discord.API | |||
{ | |||
ComponentType.Button => new ButtonComponent(x as Discord.ButtonComponent), | |||
ComponentType.SelectMenu => new SelectMenuComponent(x as Discord.SelectMenuComponent), | |||
ComponentType.ChannelSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent), | |||
ComponentType.UserSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent), | |||
ComponentType.RoleSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent), | |||
ComponentType.MentionableSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent), | |||
ComponentType.TextInput => new TextInputComponent(x as Discord.TextInputComponent), | |||
_ => null | |||
}; | |||
@@ -1,4 +1,5 @@ | |||
using Newtonsoft.Json; | |||
using System.Collections.Generic; | |||
namespace Discord.API | |||
{ | |||
@@ -15,5 +16,8 @@ namespace Discord.API | |||
[JsonProperty("value")] | |||
public Optional<string> Value { get; set; } | |||
[JsonProperty("resolved")] | |||
public Optional<MessageComponentInteractionDataResolved> Resolved { get; set; } | |||
} | |||
} |
@@ -0,0 +1,19 @@ | |||
using Newtonsoft.Json; | |||
using System.Collections.Generic; | |||
namespace Discord.API; | |||
internal class MessageComponentInteractionDataResolved | |||
{ | |||
[JsonProperty("users")] | |||
public Optional<Dictionary<string, User>> Users { get; set; } | |||
[JsonProperty("members")] | |||
public Optional<Dictionary<string, GuildMember>> Members { get; set; } | |||
[JsonProperty("channels")] | |||
public Optional<Dictionary<string, Channel>> Channels { get; set; } | |||
[JsonProperty("roles")] | |||
public Optional<Dictionary<string, Role>> Roles { get; set; } | |||
} |
@@ -26,6 +26,12 @@ namespace Discord.API | |||
[JsonProperty("disabled")] | |||
public bool Disabled { get; set; } | |||
[JsonProperty("channel_types")] | |||
public Optional<ChannelType[]> ChannelTypes { get; set; } | |||
[JsonProperty("resolved")] | |||
public Optional<MessageComponentInteractionDataResolved> Resolved { get; set; } | |||
[JsonProperty("values")] | |||
public Optional<string[]> Values { get; set; } | |||
public SelectMenuComponent() { } | |||
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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<RestMessageComponent> CreateAsync(DiscordRestClient client, Model model, bool doApiCall) | |||
@@ -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 | |||
/// <summary> | |||
/// Represents data for a <see cref="RestMessageComponent"/>. | |||
/// </summary> | |||
public class RestMessageComponentData : IComponentInteractionData, IDiscordInteractionData | |||
public class RestMessageComponentData : IComponentInteractionData | |||
{ | |||
/// <inheritdoc/> | |||
public string CustomId { get; } | |||
@@ -21,17 +25,75 @@ namespace Discord.Rest | |||
/// <inheritdoc/> | |||
public IReadOnlyCollection<string> Values { get; } | |||
/// <inheritdoc cref="IComponentInteractionData.Channels"/> | |||
public IReadOnlyCollection<RestChannel> Channels { get; } | |||
/// <inheritdoc cref="IComponentInteractionData.Users"/> | |||
public IReadOnlyCollection<RestUser> Users { get; } | |||
/// <inheritdoc cref="IComponentInteractionData.Roles"/> | |||
public IReadOnlyCollection<RestRole> Roles { get; } | |||
/// <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/> | |||
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<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; | |||
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; | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -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<RestModal> CreateAsync(DiscordRestClient client, ModelBase model, bool doApiCall) | |||
@@ -10,7 +10,7 @@ namespace Discord.Rest | |||
/// <summary> | |||
/// Represents data sent from a <see cref="InteractionType.ModalSubmit"/> Interaction. | |||
/// </summary> | |||
public class RestModalData : IComponentInteractionData, IModalInteractionData | |||
public class RestModalData : IModalInteractionData | |||
{ | |||
/// <inheritdoc/> | |||
public string CustomId { get; } | |||
@@ -20,25 +20,14 @@ namespace Discord.Rest | |||
/// </summary> | |||
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; | |||
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(); | |||
} | |||
} | |||
@@ -170,26 +170,28 @@ namespace Discord.Rest | |||
parsed.Url.GetValueOrDefault(), | |||
parsed.Disabled.GetValueOrDefault()); | |||
} | |||
case ComponentType.SelectMenu: | |||
case ComponentType.SelectMenu or ComponentType.ChannelSelect or ComponentType.RoleSelect or ComponentType.MentionableSelect or ComponentType.UserSelect: | |||
{ | |||
var parsed = (API.SelectMenuComponent)y; | |||
return new SelectMenuComponent( | |||
parsed.CustomId, | |||
parsed.Options.Select(z => new SelectMenuOption( | |||
parsed.Options?.Select(z => new SelectMenuOption( | |||
z.Label, | |||
z.Value, | |||
z.Description.GetValueOrDefault(), | |||
z.Emoji.IsSpecified | |||
? z.Emoji.Value.Id.HasValue | |||
? new Emote(z.Emoji.Value.Id.Value, z.Emoji.Value.Name, z.Emoji.Value.Animated.GetValueOrDefault()) | |||
: new Emoji(z.Emoji.Value.Name) | |||
: null, | |||
? z.Emoji.Value.Id.HasValue | |||
? new Emote(z.Emoji.Value.Id.Value, z.Emoji.Value.Name, z.Emoji.Value.Animated.GetValueOrDefault()) | |||
: new Emoji(z.Emoji.Value.Name) | |||
: null, | |||
z.Default.ToNullable())).ToList(), | |||
parsed.Placeholder.GetValueOrDefault(), | |||
parsed.MinValues, | |||
parsed.MaxValues, | |||
parsed.Disabled | |||
); | |||
parsed.Disabled, | |||
parsed.Type, | |||
parsed.ChannelTypes.GetValueOrDefault() | |||
); | |||
} | |||
default: | |||
return null; | |||
@@ -30,6 +30,10 @@ namespace Discord.Net.Converters | |||
messageComponent = new API.ButtonComponent(); | |||
break; | |||
case ComponentType.SelectMenu: | |||
case ComponentType.ChannelSelect: | |||
case ComponentType.MentionableSelect: | |||
case ComponentType.RoleSelect: | |||
case ComponentType.UserSelect: | |||
messageComponent = new API.SelectMenuComponent(); | |||
break; | |||
case ComponentType.TextInput: | |||
@@ -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); | |||
@@ -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) | |||
@@ -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 | |||
/// </summary> | |||
public class SocketMessageComponentData : IComponentInteractionData | |||
{ | |||
/// <summary> | |||
/// Gets the components Custom Id that was clicked. | |||
/// </summary> | |||
/// <inheritdoc /> | |||
public string CustomId { get; } | |||
/// <summary> | |||
/// Gets the type of the component clicked. | |||
/// </summary> | |||
/// <inheritdoc /> | |||
public ComponentType Type { get; } | |||
/// <summary> | |||
/// Gets the value(s) of a <see cref="SelectMenuComponent"/> interaction response. | |||
/// </summary> | |||
/// <inheritdoc /> | |||
public IReadOnlyCollection<string> Values { get; } | |||
/// <summary> | |||
/// Gets the value of a <see cref="TextInputComponent"/> interaction response. | |||
/// </summary> | |||
/// <inheritdoc 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; } | |||
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; | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -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) | |||
@@ -10,7 +10,7 @@ namespace Discord.WebSocket | |||
/// <summary> | |||
/// Represents data sent from a <see cref="InteractionType.ModalSubmit"/>. | |||
/// </summary> | |||
public class SocketModalData : IDiscordInteractionData, IModalInteractionData | |||
public class SocketModalData : IModalInteractionData | |||
{ | |||
/// <summary> | |||
/// Gets the <see cref="Modal"/>'s Custom Id. | |||
@@ -22,12 +22,12 @@ namespace Discord.WebSocket | |||
/// </summary> | |||
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; | |||
Components = model.Components | |||
.SelectMany(x => x.Components) | |||
.Select(x => new SocketMessageComponentData(x)) | |||
.Select(x => new SocketMessageComponentData(x, discord, state, guild, dmUser)) | |||
.ToArray(); | |||
} | |||
@@ -118,7 +118,7 @@ namespace Discord.WebSocket | |||
/// <returns> | |||
/// Collection of WebSocket-based users. | |||
/// </returns> | |||
public IReadOnlyCollection<SocketUser> MentionedUsers => _userMentions; | |||
public IReadOnlyCollection<SocketUser> MentionedUsers => _userMentions; | |||
/// <inheritdoc /> | |||
public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); | |||
@@ -226,7 +226,9 @@ namespace Discord.WebSocket | |||
parsed.Placeholder.GetValueOrDefault(), | |||
parsed.MinValues, | |||
parsed.MaxValues, | |||
parsed.Disabled | |||
parsed.Disabled, | |||
parsed.Type, | |||
parsed.ChannelTypes.GetValueOrDefault() | |||
); | |||
} | |||
default: | |||