[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:
Misha133
2022-12-25 15:41:15 +03:00
committed by GitHub
parent c67642acfa
commit 48fb1b5df4
25 changed files with 470 additions and 95 deletions

View File

@@ -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;
@@ -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);
}
}

View File

@@ -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,
}
}

View File

@@ -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; }
}

View File

@@ -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>();
}
}
}

View File

@@ -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.

View 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
};
}

View 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;
}

View File

@@ -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>();
foreach (var value in option.Values)
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;
objs.Add(result.Value);
}
else
{
var result = await _typeReader.ReadAsync(context, value, services).ConfigureAwait(false);
var users = new Dictionary<ulong, IUser>();
if (!result.IsSuccess)
return result;
if (option.Users is not null)
foreach (var user in option.Users)
users[user.Id] = user;
results.Add(result);
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);
}

View File

@@ -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
};

View File

@@ -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; }
}
}

View File

@@ -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; }
}

View File

@@ -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();
}
}
}

View File

@@ -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)

View File

@@ -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;
}
}
}
}
}

View File

@@ -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)

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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:

View File

@@ -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);

View File

@@ -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)

View File

@@ -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;
}
}
}
}
}

View File

@@ -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)

View File

@@ -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();
}

View File

@@ -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: