[Feature] Selects v2 support (#2507)
* 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>
This commit is contained in:
@@ -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,8 +282,6 @@ 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())
|
||||
@@ -357,9 +359,12 @@ namespace Discord
|
||||
/// <param name="minValues">The min values of the placeholder.</param>
|
||||
/// <param name="maxValues">The max values of the placeholder.</param>
|
||||
/// <param name="disabled">Whether or not the menu is disabled.</param>
|
||||
/// <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,
|
||||
string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false)
|
||||
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;
|
||||
@@ -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>
|
||||
/// Gets or sets the placeholder text of the current select menu.
|
||||
/// </summary>
|
||||
@@ -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>
|
||||
@@ -990,6 +1021,47 @@ namespace Discord
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the menu's current type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the menu.</param>
|
||||
/// <returns>
|
||||
/// The current builder.
|
||||
/// </returns>
|
||||
public SelectMenuBuilder WithType(ComponentType type)
|
||||
{
|
||||
Type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>s).
|
||||
/// </summary>
|
||||
/// <param name="channelTypes">The valid channel types of the menu.</param>
|
||||
/// <returns>
|
||||
/// The current builder.
|
||||
/// </returns>
|
||||
public SelectMenuBuilder WithChannelTypes(List<ChannelType> channelTypes)
|
||||
{
|
||||
ChannelTypes = channelTypes;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>s).
|
||||
/// </summary>
|
||||
/// <param name="channelTypes">The valid channel types of the menu.</param>
|
||||
/// <returns>
|
||||
/// The current builder.
|
||||
/// </returns>
|
||||
public SelectMenuBuilder WithChannelTypes(params ChannelType[] channelTypes)
|
||||
{
|
||||
ChannelTypes = channelTypes is null
|
||||
? ChannelTypeUtils.AllChannelTypes()
|
||||
: channelTypes.ToList();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a <see cref="SelectMenuComponent"/>
|
||||
/// </summary>
|
||||
@@ -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; }
|
||||
@@ -39,6 +40,11 @@ namespace Discord
|
||||
/// </summary>
|
||||
public bool IsDisabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the allowed channel types for this modal
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<ChannelType> ChannelTypes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Turns this select menu into a builder.
|
||||
/// </summary>
|
||||
@@ -52,9 +58,9 @@ namespace Discord
|
||||
Placeholder,
|
||||
MaxValues,
|
||||
MinValues,
|
||||
IsDisabled);
|
||||
IsDisabled, Type, ChannelTypes.ToList());
|
||||
|
||||
internal SelectMenuComponent(string customId, List<SelectMenuOption> options, string placeholder, int minValues, int maxValues, bool disabled)
|
||||
internal SelectMenuComponent(string customId, List<SelectMenuOption> options, string placeholder, int minValues, int maxValues, bool disabled, ComponentType type, IEnumerable<ChannelType> channelTypes = null)
|
||||
{
|
||||
CustomId = customId;
|
||||
Options = options;
|
||||
@@ -62,6 +68,8 @@ namespace Discord
|
||||
MinValues = minValues;
|
||||
MaxValues = maxValues;
|
||||
IsDisabled = disabled;
|
||||
Type = type;
|
||||
ChannelTypes = channelTypes?.ToArray() ?? Array.Empty<ChannelType>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Discord
|
||||
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.
|
||||
|
||||
14
src/Discord.Net.Core/Utils/ChannelTypeUtils.cs
Normal file
14
src/Discord.Net.Core/Utils/ChannelTypeUtils.cs
Normal file
@@ -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
|
||||
};
|
||||
}
|
||||
8
src/Discord.Net.Core/Utils/ComponentType.cs
Normal file
8
src/Discord.Net.Core/Utils/ComponentType.cs
Normal file
@@ -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,13 +19,22 @@ 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);
|
||||
@@ -31,13 +42,33 @@ namespace Discord.Interactions
|
||||
if (!result.IsSuccess)
|
||||
return result;
|
||||
|
||||
results.Add(result);
|
||||
objs.Add(result.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
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(option.Roles is not null)
|
||||
objs.AddRange(option.Roles);
|
||||
|
||||
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,12 +170,12 @@ 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(),
|
||||
@@ -188,7 +188,9 @@ namespace Discord.Rest
|
||||
parsed.Placeholder.GetValueOrDefault(),
|
||||
parsed.MinValues,
|
||||
parsed.MaxValues,
|
||||
parsed.Disabled
|
||||
parsed.Disabled,
|
||||
parsed.Type,
|
||||
parsed.ChannelTypes.GetValueOrDefault()
|
||||
);
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -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
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Discord.WebSocket
|
||||
? (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();
|
||||
}
|
||||
|
||||
|
||||
@@ -226,7 +226,9 @@ namespace Discord.WebSocket
|
||||
parsed.Placeholder.GetValueOrDefault(),
|
||||
parsed.MinValues,
|
||||
parsed.MaxValues,
|
||||
parsed.Disabled
|
||||
parsed.Disabled,
|
||||
parsed.Type,
|
||||
parsed.ChannelTypes.GetValueOrDefault()
|
||||
);
|
||||
}
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user