Browse Source

Initial support for new select types

pull/2507/head
cat Misha133 2 years ago
parent
commit
d871e9c1dc
18 changed files with 234 additions and 45 deletions
  1. +73
    -10
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs
  2. +17
    -2
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs
  3. +17
    -2
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs
  4. +11
    -3
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs
  5. +2
    -2
      src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs
  6. +8
    -0
      src/Discord.Net.Core/Utils/ComponentType.cs
  7. +4
    -0
      src/Discord.Net.Rest/API/Common/ActionRowComponent.cs
  8. +4
    -0
      src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs
  9. +19
    -0
      src/Discord.Net.Rest/API/Common/MessageComponentInteractionDataResolved.cs
  10. +7
    -0
      src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs
  11. +20
    -0
      src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs
  12. +14
    -2
      src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs
  13. +10
    -8
      src/Discord.Net.Rest/Entities/Messages/RestMessage.cs
  14. +4
    -0
      src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs
  15. +2
    -0
      src/Discord.Net.WebSocket/DiscordSocketApiClient.cs
  16. +2
    -1
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  17. +16
    -13
      src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs
  18. +4
    -2
      src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs

+ 73
- 10
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
{
@@ -278,9 +278,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;
@@ -357,7 +355,7 @@ 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>
/// <returns>The current builder.</returns>
/// <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)
{
@@ -431,10 +429,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 +456,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 +761,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>
@@ -827,11 +841,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 +882,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 +892,8 @@ namespace Discord
IsDisabled = isDisabled;
MaxValues = maxValues;
MinValues = minValues;
Type = type;
ChannelTypes = channelTypes ?? new();
}

/// <summary>
@@ -990,6 +1014,45 @@ 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.ToList();
return this;
}

/// <summary>
/// Builds a <see cref="SelectMenuComponent"/>
/// </summary>
@@ -998,7 +1061,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,
}
}

+ 17
- 2
src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs View File

@@ -18,12 +18,27 @@ 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.
/// </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.
/// </summary>
IReadOnlyCollection<IChannel> Channels { get; }

/// <summary>
/// Gets the user(s) of a <see cref="ComponentType.UserSelect"/> or <see cref="ComponentType.MentionableSelect"/> interaction response.
/// </summary>
IReadOnlyCollection<IUser> Users { get; }

/// <summary>
/// Gets the roles(s) of a <see cref="ComponentType.RoleSelect"/> or <see cref="ComponentType.MentionableSelect"/> interaction response.
/// </summary>
IReadOnlyCollection<IRole> Roles { 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.


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

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

+ 7
- 0
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() { }
@@ -39,6 +45,7 @@ namespace Discord.API
MinValues = component.MinValues;
MaxValues = component.MaxValues;
Disabled = component.IsDisabled;
ChannelTypes = component.ChannelTypes.ToArray();
}
}
}

+ 20
- 0
src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs View File

@@ -21,6 +21,26 @@ 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; }

#region IComponentInteractionData

/// <inheritdoc/>
IReadOnlyCollection<IChannel> IComponentInteractionData.Channels => Channels;

/// <inheritdoc/>
IReadOnlyCollection<IUser> IComponentInteractionData.Users => Users;

/// <inheritdoc/>
IReadOnlyCollection<IRole> IComponentInteractionData.Roles => Roles;

#endregion

/// <inheritdoc/>
public string Value { get; }



+ 14
- 2
src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs View File

@@ -21,12 +21,24 @@ namespace Discord.Rest
public IReadOnlyCollection<RestMessageComponentData> Components { get; }

/// <inheritdoc/>
public ComponentType Type => ComponentType.ModalSubmit;
public ComponentType Type => throw new NotSupportedException("Modals do not have a component type.");

/// <inheritdoc/>
public IReadOnlyCollection<string> Values
=> throw new NotSupportedException("Modal interactions do not have values!");

/// <inheritdoc/>
public IReadOnlyCollection<IChannel> Channels
=> throw new NotSupportedException("Modal interactions do not have channels!");

/// <inheritdoc/>
public IReadOnlyCollection<IUser> Users
=> throw new NotSupportedException("Modal interactions do not have users!");

/// <inheritdoc/>
public IReadOnlyCollection<IRole> Roles
=> throw new NotSupportedException("Modal interactions do not have roles!");

/// <inheritdoc/>
public string Value
=> throw new NotSupportedException("Modal interactions do not have value!");


+ 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
- 0
src/Discord.Net.WebSocket/DiscordSocketApiClient.cs View File

@@ -1,3 +1,5 @@
#define DEBUG_PACKETS

using Discord.API.Gateway;
using Discord.Net.Queue;
using Discord.Net.Rest;


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


+ 16
- 13
src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs View File

@@ -1,3 +1,5 @@
using Discord.Utils;
using System.Linq;
using System.Collections.Generic;
using Model = Discord.API.MessageComponentInteractionData;

@@ -8,24 +10,25 @@ 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 />
public IReadOnlyCollection<IChannel> Channels { get; }

/// <inheritdoc />
public IReadOnlyCollection<IUser> Users { get; }

/// <inheritdoc />
public IReadOnlyCollection<IRole> Roles { get; }

/// <inheritdoc />
public string Value { get; }

internal SocketMessageComponentData(Model model)
@@ -45,7 +48,7 @@ namespace Discord.WebSocket
? (component as API.TextInputComponent).Value.Value
: null;

Values = component.Type == ComponentType.SelectMenu
Values = component.Type.IsSelectType()
? (component as API.SelectMenuComponent).Values.Value
: null;
}


+ 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