Browse Source

[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>
pull/2548/head
Misha133 GitHub 2 years ago
parent
commit
48fb1b5df4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 470 additions and 95 deletions
  1. +3
    -0
      docs/guides/int_framework/intro.md
  2. +93
    -21
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs
  3. +17
    -2
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs
  4. +22
    -2
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs
  5. +11
    -3
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs
  6. +2
    -2
      src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs
  7. +14
    -0
      src/Discord.Net.Core/Utils/ChannelTypeUtils.cs
  8. +8
    -0
      src/Discord.Net.Core/Utils/ComponentType.cs
  9. +41
    -10
      src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs
  10. +4
    -0
      src/Discord.Net.Rest/API/Common/ActionRowComponent.cs
  11. +4
    -0
      src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs
  12. +19
    -0
      src/Discord.Net.Rest/API/Common/MessageComponentInteractionDataResolved.cs
  13. +8
    -1
      src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs
  14. +1
    -1
      src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs
  15. +92
    -4
      src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs
  16. +1
    -1
      src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs
  17. +3
    -14
      src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs
  18. +10
    -8
      src/Discord.Net.Rest/Entities/Messages/RestMessage.cs
  19. +4
    -0
      src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs
  20. +2
    -1
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  21. +1
    -1
      src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs
  22. +101
    -17
      src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs
  23. +2
    -2
      src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs
  24. +3
    -3
      src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs
  25. +4
    -2
      src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs

+ 3
- 0
docs/guides/int_framework/intro.md View File

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


+ 93
- 21
src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs 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);
}
}



+ 17
- 2
src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs 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,
}
}

+ 22
- 2
src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs 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; }
}


+ 11
- 3
src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs 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>();
}
}
}

+ 2
- 2
src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs 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.


+ 14
- 0
src/Discord.Net.Core/Utils/ChannelTypeUtils.cs 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
};
}

+ 8
- 0
src/Discord.Net.Core/Utils/ComponentType.cs 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;
}

+ 41
- 10
src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs 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>();

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


+ 4
- 0
src/Discord.Net.Rest/API/Common/ActionRowComponent.cs 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
};


+ 4
- 0
src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs 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; }
}
}

+ 19
- 0
src/Discord.Net.Rest/API/Common/MessageComponentInteractionDataResolved.cs 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; }
}

+ 8
- 1
src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs 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();
}
}
}

+ 1
- 1
src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs 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)


+ 92
- 4
src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs 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;
}
}
}
}
}

+ 1
- 1
src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs 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)


+ 3
- 14
src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs 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();
}
}


+ 10
- 8
src/Discord.Net.Rest/Entities/Messages/RestMessage.cs 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;


+ 4
- 0
src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs 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:


+ 2
- 1
src/Discord.Net.WebSocket/DiscordSocketClient.cs 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);


+ 1
- 1
src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs 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)


+ 101
- 17
src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs 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;
}
}
}
}
}

+ 2
- 2
src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs 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)


+ 3
- 3
src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs 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();
}



+ 4
- 2
src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs 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:


Loading…
Cancel
Save