[Feature] Select menu default values (#2776)

* initial commit

* docs & readonly
This commit is contained in:
Mihail Gribkov
2023-11-18 23:44:16 +03:00
committed by GitHub
parent 8060dcf4ae
commit ac274d4b76
15 changed files with 302 additions and 8 deletions

View File

@@ -1,4 +1,5 @@
using Discord.Utils; using Discord.Utils;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -129,7 +130,8 @@ namespace Discord
/// <param name="channelTypes">Menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>)</param> /// <param name="channelTypes">Menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>)</param>
/// <returns></returns> /// <returns></returns>
public ComponentBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> options = null, 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) string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0, ComponentType type = ComponentType.SelectMenu,
ChannelType[] channelTypes = null, SelectMenuDefaultValue[] defaultValues = null)
{ {
return WithSelectMenu(new SelectMenuBuilder() return WithSelectMenu(new SelectMenuBuilder()
.WithCustomId(customId) .WithCustomId(customId)
@@ -139,7 +141,8 @@ namespace Discord
.WithMinValues(minValues) .WithMinValues(minValues)
.WithDisabled(disabled) .WithDisabled(disabled)
.WithType(type) .WithType(type)
.WithChannelTypes(channelTypes), .WithChannelTypes(channelTypes)
.WithDefaultValues(defaultValues),
row); row);
} }
@@ -891,12 +894,25 @@ namespace Discord
/// </summary> /// </summary>
public List<ChannelType> ChannelTypes { get; set; } public List<ChannelType> ChannelTypes { get; set; }
public List<SelectMenuDefaultValue> DefaultValues
{
get => _defaultValues;
set
{
if (value != null)
Preconditions.AtMost(value.Count, MaxOptionCount, nameof(DefaultValues));
_defaultValues = value;
}
}
private List<SelectMenuOptionBuilder> _options = new List<SelectMenuOptionBuilder>(); private List<SelectMenuOptionBuilder> _options = new List<SelectMenuOptionBuilder>();
private int _minValues = 1; private int _minValues = 1;
private int _maxValues = 1; private int _maxValues = 1;
private string _placeholder; private string _placeholder;
private string _customId; private string _customId;
private ComponentType _type = ComponentType.SelectMenu; private ComponentType _type = ComponentType.SelectMenu;
private List<SelectMenuDefaultValue> _defaultValues = new();
/// <summary> /// <summary>
/// Creates a new instance of a <see cref="SelectMenuBuilder"/>. /// Creates a new instance of a <see cref="SelectMenuBuilder"/>.
@@ -916,6 +932,7 @@ namespace Discord
Options = selectMenu.Options? Options = selectMenu.Options?
.Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.IsDefault)) .Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.IsDefault))
.ToList(); .ToList();
DefaultValues = selectMenu.DefaultValues?.ToList();
} }
/// <summary> /// <summary>
@@ -929,7 +946,8 @@ namespace Discord
/// <param name="isDisabled">Disabled this select menu or not.</param> /// <param name="isDisabled">Disabled this select menu or not.</param>
/// <param name="type">The <see cref="ComponentType"/> of this select menu.</param> /// <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> /// <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) 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, List<SelectMenuDefaultValue> defaultValues = null)
{ {
CustomId = customId; CustomId = customId;
Options = options; Options = options;
@@ -939,6 +957,7 @@ namespace Discord
MinValues = minValues; MinValues = minValues;
Type = type; Type = type;
ChannelTypes = channelTypes ?? new(); ChannelTypes = channelTypes ?? new();
DefaultValues = defaultValues ?? new();
} }
/// <summary> /// <summary>
@@ -1046,6 +1065,48 @@ namespace Discord
return this; return this;
} }
/// <summary>
/// Add one default value to menu options.
/// </summary>
/// <param name="id">The id of an entity to add.</param>
/// <param name="type">The type of an entity to add.</param>
/// <exception cref="InvalidOperationException">Default values count reached <see cref="MaxOptionCount"/>.</exception>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder AddDefaultValue(ulong id, SelectDefaultValueType type)
=> AddDefaultValue(new(id, type));
/// <summary>
/// Add one default value to menu options.
/// </summary>
/// <param name="value">The default value to add.</param>
/// <exception cref="InvalidOperationException">Default values count reached <see cref="MaxOptionCount"/>.</exception>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder AddDefaultValue(SelectMenuDefaultValue value)
{
if (DefaultValues.Count >= MaxOptionCount)
throw new InvalidOperationException($"Options count reached {MaxOptionCount}.");
DefaultValues.Add(value);
return this;
}
/// <summary>
/// Sets the field default values.
/// </summary>
/// <param name="defaultValues">The value to set the field default values to.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithDefaultValues(params SelectMenuDefaultValue[] defaultValues)
{
DefaultValues = defaultValues.ToList();
return this;
}
/// <summary> /// <summary>
/// Sets whether the current menu is disabled. /// Sets whether the current menu is disabled.
/// </summary> /// </summary>
@@ -1108,7 +1169,7 @@ namespace Discord
{ {
var options = Options?.Select(x => x.Build()).ToList(); var options = Options?.Select(x => x.Build()).ToList();
return new SelectMenuComponent(CustomId, options, Placeholder, MinValues, MaxValues, IsDisabled, Type, ChannelTypes); return new SelectMenuComponent(CustomId, options, Placeholder, MinValues, MaxValues, IsDisabled, Type, ChannelTypes, DefaultValues);
} }
} }

View File

@@ -0,0 +1,22 @@
namespace Discord;
/// <summary>
/// Type of a <see cref="SelectDefaultValueType" />.
/// </summary>
public enum SelectDefaultValueType
{
/// <summary>
/// The select menu default value is a user.
/// </summary>
User = 0,
/// <summary>
/// The select menu default value is a role.
/// </summary>
Role = 1,
/// <summary>
/// The select menu default value is a channel.
/// </summary>
Channel = 2
}

View File

@@ -45,6 +45,11 @@ namespace Discord
/// </summary> /// </summary>
public IReadOnlyCollection<ChannelType> ChannelTypes { get; } public IReadOnlyCollection<ChannelType> ChannelTypes { get; }
/// <summary>
/// Gets default values for auto-populated select menu components.
/// </summary>
public IReadOnlyCollection<SelectMenuDefaultValue> DefaultValues { get; }
/// <summary> /// <summary>
/// Turns this select menu into a builder. /// Turns this select menu into a builder.
/// </summary> /// </summary>
@@ -58,9 +63,11 @@ namespace Discord
Placeholder, Placeholder,
MaxValues, MaxValues,
MinValues, MinValues,
IsDisabled, Type, ChannelTypes.ToList()); IsDisabled, Type, ChannelTypes.ToList(),
DefaultValues.ToList());
internal SelectMenuComponent(string customId, List<SelectMenuOption> options, string placeholder, int minValues, int maxValues, bool disabled, ComponentType type, IEnumerable<ChannelType> channelTypes = null) internal SelectMenuComponent(string customId, List<SelectMenuOption> options, string placeholder, int minValues, int maxValues,
bool disabled, ComponentType type, IEnumerable<ChannelType> channelTypes = null, IEnumerable<SelectMenuDefaultValue> defaultValues = null)
{ {
CustomId = customId; CustomId = customId;
Options = options; Options = options;
@@ -70,6 +77,7 @@ namespace Discord
IsDisabled = disabled; IsDisabled = disabled;
Type = type; Type = type;
ChannelTypes = channelTypes?.ToArray() ?? Array.Empty<ChannelType>(); ChannelTypes = channelTypes?.ToArray() ?? Array.Empty<ChannelType>();
DefaultValues = defaultValues?.ToArray() ?? Array.Empty<SelectMenuDefaultValue>();
} }
} }
} }

View File

@@ -0,0 +1,46 @@
namespace Discord;
/// <summary>
/// Represents a default value of an auto-populated select menu.
/// </summary>
public readonly struct SelectMenuDefaultValue
{
/// <summary>
/// Gets the id of entity this default value refers to.
/// </summary>
public ulong Id { get; }
/// <summary>
/// Gets the type of this default value.
/// </summary>
public SelectDefaultValueType Type { get; }
/// <summary>
/// Creates a new default value.
/// </summary>
/// <param name="id">Id of the target object.</param>
/// <param name="type">Type of the target entity.</param>
public SelectMenuDefaultValue(ulong id, SelectDefaultValueType type)
{
Id = id;
Type = type;
}
/// <summary>
/// Creates a new default value from a <see cref="IChannel"/>.
/// </summary>
public static SelectMenuDefaultValue FromChannel(IChannel channel)
=> new(channel.Id, SelectDefaultValueType.Channel);
/// <summary>
/// Creates a new default value from a <see cref="IRole"/>.
/// </summary>
public static SelectMenuDefaultValue FromRole(IRole role)
=> new(role.Id, SelectDefaultValueType.Role);
/// <summary>
/// Creates a new default value from a <see cref="IUser"/>.
/// </summary>
public static SelectMenuDefaultValue FromUser(IUser user)
=> new(user.Id, SelectDefaultValueType.User);
}

View File

@@ -8,6 +8,11 @@ namespace Discord
/// </summary> /// </summary>
public interface IUserMessage : IMessage public interface IUserMessage : IMessage
{ {
/// <summary>
/// Gets the resolved data if the message has components. <see langword="null"/> otherwise.
/// </summary>
MessageResolvedData ResolvedData { get; }
/// <summary> /// <summary>
/// Gets the referenced message if it is a crosspost, channel follow add, pin, or reply message. /// Gets the referenced message if it is a crosspost, channel follow add, pin, or reply message.
/// </summary> /// </summary>

View File

@@ -0,0 +1,34 @@
using System.Collections.Generic;
namespace Discord;
public class MessageResolvedData
{
/// <summary>
/// Gets a collection of <see cref="IUser"/> resolved in the message.
/// </summary>
public IReadOnlyCollection<IUser> Users { get; }
/// <summary>
/// Gets a collection of <see cref="IGuildUser"/> resolved in the message.
/// </summary>
public IReadOnlyCollection<IGuildUser> Members { get; }
/// <summary>
/// Gets a collection of <see cref="IRole"/> resolved in the message.
/// </summary>
public IReadOnlyCollection<IRole> Roles { get; }
/// <summary>
/// Gets a collection of <see cref="IChannel"/> resolved in the message.
/// </summary>
public IReadOnlyCollection<IChannel> Channels { get; }
internal MessageResolvedData(IReadOnlyCollection<IUser> users, IReadOnlyCollection<IGuildUser> members, IReadOnlyCollection<IRole> roles, IReadOnlyCollection<IChannel> channels)
{
Users = users;
Members = members;
Roles = roles;
Channels = channels;
}
}

View File

@@ -67,5 +67,8 @@ namespace Discord.API
[JsonProperty("thread")] [JsonProperty("thread")]
public Optional<Channel> Thread { get; set; } public Optional<Channel> Thread { get; set; }
[JsonProperty("resolved")]
public Optional<MessageComponentInteractionDataResolved> Resolved { get; set; }
} }
} }

View File

@@ -34,6 +34,10 @@ namespace Discord.API
[JsonProperty("values")] [JsonProperty("values")]
public Optional<string[]> Values { get; set; } public Optional<string[]> Values { get; set; }
[JsonProperty("default_values")]
public Optional<SelectMenuDefaultValue[]> DefaultValues { get; set; }
public SelectMenuComponent() { } public SelectMenuComponent() { }
public SelectMenuComponent(Discord.SelectMenuComponent component) public SelectMenuComponent(Discord.SelectMenuComponent component)
@@ -46,6 +50,7 @@ namespace Discord.API
MaxValues = component.MaxValues; MaxValues = component.MaxValues;
Disabled = component.IsDisabled; Disabled = component.IsDisabled;
ChannelTypes = component.ChannelTypes.ToArray(); ChannelTypes = component.ChannelTypes.ToArray();
DefaultValues = component.DefaultValues.Select(x => new SelectMenuDefaultValue {Id = x.Id, Type = x.Type}).ToArray();
} }
} }
} }

View File

@@ -0,0 +1,12 @@
using Newtonsoft.Json;
namespace Discord.API;
internal class SelectMenuDefaultValue
{
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("type")]
public SelectDefaultValueType Type { get; set; }
}

View File

@@ -203,7 +203,10 @@ namespace Discord.Rest
parsed.MaxValues, parsed.MaxValues,
parsed.Disabled, parsed.Disabled,
parsed.Type, parsed.Type,
parsed.ChannelTypes.GetValueOrDefault() parsed.ChannelTypes.GetValueOrDefault(),
parsed.DefaultValues.IsSpecified
? parsed.DefaultValues.Value.Select(x => new SelectMenuDefaultValue(x.Id, x.Type))
: Array.Empty<SelectMenuDefaultValue>()
); );
} }
default: default:

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Model = Discord.API.Message; using Model = Discord.API.Message;
@@ -47,6 +48,9 @@ namespace Discord.Rest
/// <inheritdoc /> /// <inheritdoc />
public IUserMessage ReferencedMessage => _referencedMessage; public IUserMessage ReferencedMessage => _referencedMessage;
/// <inheritdoc />
public MessageResolvedData ResolvedData { get; internal set; }
internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source)
: base(discord, id, channel, author, source) : base(discord, id, channel, author, source)
{ {
@@ -130,6 +134,34 @@ namespace Discord.Rest
else else
_stickers = ImmutableArray.Create<StickerItem>(); _stickers = ImmutableArray.Create<StickerItem>();
} }
if (model.Resolved.IsSpecified)
{
var users = model.Resolved.Value.Users.IsSpecified
? model.Resolved.Value.Users.Value.Select(x => RestUser.Create(Discord, x.Value)).ToImmutableArray()
: ImmutableArray<RestUser>.Empty;
var members = model.Resolved.Value.Members.IsSpecified
? model.Resolved.Value.Members.Value.Select(x =>
{
x.Value.User = model.Resolved.Value.Users.Value.TryGetValue(x.Key, out var user)
? user
: null;
return RestGuildUser.Create(Discord, guild, x.Value, guildId);
}).ToImmutableArray()
: ImmutableArray<RestGuildUser>.Empty;
var roles = model.Resolved.Value.Roles.IsSpecified
? model.Resolved.Value.Roles.Value.Select(x => RestRole.Create(Discord, guild, x.Value)).ToImmutableArray()
: ImmutableArray<RestRole>.Empty;
var channels = model.Resolved.Value.Channels.IsSpecified
? model.Resolved.Value.Channels.Value.Select(x => RestChannel.Create(Discord, x.Value, guild)).ToImmutableArray()
: ImmutableArray<RestChannel>.Empty;
ResolvedData = new MessageResolvedData(users, members, roles, channels);
}
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@@ -79,6 +79,8 @@ namespace Discord.Net.Converters
return UserStatusConverter.Instance; return UserStatusConverter.Instance;
if (type == typeof(EmbedType)) if (type == typeof(EmbedType))
return EmbedTypeConverter.Instance; return EmbedTypeConverter.Instance;
if (type == typeof(SelectDefaultValueType))
return SelectMenuDefaultValueTypeConverter.Instance;
//Special //Special
if (type == typeof(API.Image)) if (type == typeof(API.Image))

View File

@@ -0,0 +1,27 @@
using Newtonsoft.Json;
using System;
using System.Globalization;
namespace Discord.Net.Converters;
internal class SelectMenuDefaultValueTypeConverter : JsonConverter
{
public static readonly SelectMenuDefaultValueTypeConverter Instance = new ();
public override bool CanConvert(Type objectType) => true;
public override bool CanRead => true;
public override bool CanWrite => true;
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return Enum.TryParse<SelectDefaultValueType>((string)reader.Value, true, out var result)
? result
: null;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(((SelectDefaultValueType)value).ToString().ToLower(CultureInfo.InvariantCulture));
}
}

View File

@@ -237,7 +237,10 @@ namespace Discord.WebSocket
parsed.MaxValues, parsed.MaxValues,
parsed.Disabled, parsed.Disabled,
parsed.Type, parsed.Type,
parsed.ChannelTypes.GetValueOrDefault() parsed.ChannelTypes.GetValueOrDefault(),
parsed.DefaultValues.IsSpecified
? parsed.DefaultValues.Value.Select(x => new SelectMenuDefaultValue(x.Id, x.Type))
: Array.Empty<SelectMenuDefaultValue>()
); );
} }
default: default:

View File

@@ -49,6 +49,9 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
public IUserMessage ReferencedMessage => _referencedMessage; public IUserMessage ReferencedMessage => _referencedMessage;
/// <inheritdoc />
public MessageResolvedData ResolvedData { get; internal set; }
internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source)
: base(discord, id, channel, author, source) : base(discord, id, channel, author, source)
{ {
@@ -172,6 +175,34 @@ namespace Discord.WebSocket
else else
_stickers = ImmutableArray.Create<SocketSticker>(); _stickers = ImmutableArray.Create<SocketSticker>();
} }
if (model.Resolved.IsSpecified)
{
var users = model.Resolved.Value.Users.IsSpecified
? model.Resolved.Value.Users.Value.Select(x => RestUser.Create(Discord, x.Value)).ToImmutableArray()
: ImmutableArray<RestUser>.Empty;
var members = model.Resolved.Value.Members.IsSpecified
? model.Resolved.Value.Members.Value.Select(x =>
{
x.Value.User = model.Resolved.Value.Users.Value.TryGetValue(x.Key, out var user)
? user
: null;
return RestGuildUser.Create(Discord, guild, x.Value);
}).ToImmutableArray()
: ImmutableArray<RestGuildUser>.Empty;
var roles = model.Resolved.Value.Roles.IsSpecified
? model.Resolved.Value.Roles.Value.Select(x => RestRole.Create(Discord, guild, x.Value)).ToImmutableArray()
: ImmutableArray<RestRole>.Empty;
var channels = model.Resolved.Value.Channels.IsSpecified
? model.Resolved.Value.Channels.Value.Select(x => RestChannel.Create(Discord, x.Value, guild)).ToImmutableArray()
: ImmutableArray<RestChannel>.Empty;
ResolvedData = new MessageResolvedData(users, members, roles, channels);
}
} }
/// <inheritdoc /> /// <inheritdoc />