diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs index 549913dc..a10a58c3 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs @@ -1,4 +1,5 @@ using Discord.Utils; + using System; using System.Collections.Generic; using System.Linq; @@ -129,7 +130,8 @@ namespace Discord /// Menus valid channel types (only for ) /// public ComponentBuilder WithSelectMenu(string customId, List 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() .WithCustomId(customId) @@ -139,7 +141,8 @@ namespace Discord .WithMinValues(minValues) .WithDisabled(disabled) .WithType(type) - .WithChannelTypes(channelTypes), + .WithChannelTypes(channelTypes) + .WithDefaultValues(defaultValues), row); } @@ -891,12 +894,25 @@ namespace Discord /// public List ChannelTypes { get; set; } + public List DefaultValues + { + get => _defaultValues; + set + { + if (value != null) + Preconditions.AtMost(value.Count, MaxOptionCount, nameof(DefaultValues)); + + _defaultValues = value; + } + } + private List _options = new List(); private int _minValues = 1; private int _maxValues = 1; private string _placeholder; private string _customId; private ComponentType _type = ComponentType.SelectMenu; + private List _defaultValues = new(); /// /// Creates a new instance of a . @@ -916,6 +932,7 @@ namespace Discord Options = selectMenu.Options? .Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.IsDefault)) .ToList(); + DefaultValues = selectMenu.DefaultValues?.ToList(); } /// @@ -929,7 +946,8 @@ namespace Discord /// Disabled this select menu or not. /// The of this select menu. /// The types of channels this menu can select (only valid on s) - public SelectMenuBuilder(string customId, List options = null, string placeholder = null, int maxValues = 1, int minValues = 1, bool isDisabled = false, ComponentType type = ComponentType.SelectMenu, List channelTypes = null) + public SelectMenuBuilder(string customId, List options = null, string placeholder = null, int maxValues = 1, int minValues = 1, + bool isDisabled = false, ComponentType type = ComponentType.SelectMenu, List channelTypes = null, List defaultValues = null) { CustomId = customId; Options = options; @@ -939,6 +957,7 @@ namespace Discord MinValues = minValues; Type = type; ChannelTypes = channelTypes ?? new(); + DefaultValues = defaultValues ?? new(); } /// @@ -1046,6 +1065,48 @@ namespace Discord return this; } + /// + /// Add one default value to menu options. + /// + /// The id of an entity to add. + /// The type of an entity to add. + /// Default values count reached . + /// + /// The current builder. + /// + public SelectMenuBuilder AddDefaultValue(ulong id, SelectDefaultValueType type) + => AddDefaultValue(new(id, type)); + + /// + /// Add one default value to menu options. + /// + /// The default value to add. + /// Default values count reached . + /// + /// The current builder. + /// + public SelectMenuBuilder AddDefaultValue(SelectMenuDefaultValue value) + { + if (DefaultValues.Count >= MaxOptionCount) + throw new InvalidOperationException($"Options count reached {MaxOptionCount}."); + + DefaultValues.Add(value); + return this; + } + + /// + /// Sets the field default values. + /// + /// The value to set the field default values to. + /// + /// The current builder. + /// + public SelectMenuBuilder WithDefaultValues(params SelectMenuDefaultValue[] defaultValues) + { + DefaultValues = defaultValues.ToList(); + return this; + } + /// /// Sets whether the current menu is disabled. /// @@ -1108,7 +1169,7 @@ namespace Discord { 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); } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectDefaultValueType.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectDefaultValueType.cs new file mode 100644 index 00000000..207a14f7 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectDefaultValueType.cs @@ -0,0 +1,22 @@ +namespace Discord; + +/// +/// Type of a . +/// +public enum SelectDefaultValueType +{ + /// + /// The select menu default value is a user. + /// + User = 0, + + /// + /// The select menu default value is a role. + /// + Role = 1, + + /// + /// The select menu default value is a channel. + /// + Channel = 2 +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs index eccdd18c..39631dee 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs @@ -45,6 +45,11 @@ namespace Discord /// public IReadOnlyCollection ChannelTypes { get; } + /// + /// Gets default values for auto-populated select menu components. + /// + public IReadOnlyCollection DefaultValues { get; } + /// /// Turns this select menu into a builder. /// @@ -58,9 +63,11 @@ namespace Discord Placeholder, MaxValues, MinValues, - IsDisabled, Type, ChannelTypes.ToList()); + IsDisabled, Type, ChannelTypes.ToList(), + DefaultValues.ToList()); - internal SelectMenuComponent(string customId, List options, string placeholder, int minValues, int maxValues, bool disabled, ComponentType type, IEnumerable channelTypes = null) + internal SelectMenuComponent(string customId, List options, string placeholder, int minValues, int maxValues, + bool disabled, ComponentType type, IEnumerable channelTypes = null, IEnumerable defaultValues = null) { CustomId = customId; Options = options; @@ -70,6 +77,7 @@ namespace Discord IsDisabled = disabled; Type = type; ChannelTypes = channelTypes?.ToArray() ?? Array.Empty(); + DefaultValues = defaultValues?.ToArray() ?? Array.Empty(); } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuDefaultValue.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuDefaultValue.cs new file mode 100644 index 00000000..f2718de1 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuDefaultValue.cs @@ -0,0 +1,46 @@ +namespace Discord; + +/// +/// Represents a default value of an auto-populated select menu. +/// +public readonly struct SelectMenuDefaultValue +{ + /// + /// Gets the id of entity this default value refers to. + /// + public ulong Id { get; } + + /// + /// Gets the type of this default value. + /// + public SelectDefaultValueType Type { get; } + + /// + /// Creates a new default value. + /// + /// Id of the target object. + /// Type of the target entity. + public SelectMenuDefaultValue(ulong id, SelectDefaultValueType type) + { + Id = id; + Type = type; + } + + /// + /// Creates a new default value from a . + /// + public static SelectMenuDefaultValue FromChannel(IChannel channel) + => new(channel.Id, SelectDefaultValueType.Channel); + + /// + /// Creates a new default value from a . + /// + public static SelectMenuDefaultValue FromRole(IRole role) + => new(role.Id, SelectDefaultValueType.Role); + + /// + /// Creates a new default value from a . + /// + public static SelectMenuDefaultValue FromUser(IUser user) + => new(user.Id, SelectDefaultValueType.User); +} diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs index c2d0e13b..72770899 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -8,6 +8,11 @@ namespace Discord /// public interface IUserMessage : IMessage { + /// + /// Gets the resolved data if the message has components. otherwise. + /// + MessageResolvedData ResolvedData { get; } + /// /// Gets the referenced message if it is a crosspost, channel follow add, pin, or reply message. /// diff --git a/src/Discord.Net.Core/Entities/Messages/MessageResolvedData.cs b/src/Discord.Net.Core/Entities/Messages/MessageResolvedData.cs new file mode 100644 index 00000000..2f6216a2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/MessageResolvedData.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace Discord; + +public class MessageResolvedData +{ + /// + /// Gets a collection of resolved in the message. + /// + public IReadOnlyCollection Users { get; } + + /// + /// Gets a collection of resolved in the message. + /// + public IReadOnlyCollection Members { get; } + + /// + /// Gets a collection of resolved in the message. + /// + public IReadOnlyCollection Roles { get; } + + /// + /// Gets a collection of resolved in the message. + /// + public IReadOnlyCollection Channels { get; } + + internal MessageResolvedData(IReadOnlyCollection users, IReadOnlyCollection members, IReadOnlyCollection roles, IReadOnlyCollection channels) + { + Users = users; + Members = members; + Roles = roles; + Channels = channels; + } +} diff --git a/src/Discord.Net.Rest/API/Common/Message.cs b/src/Discord.Net.Rest/API/Common/Message.cs index 2b845bee..8cde11c8 100644 --- a/src/Discord.Net.Rest/API/Common/Message.cs +++ b/src/Discord.Net.Rest/API/Common/Message.cs @@ -67,5 +67,8 @@ namespace Discord.API [JsonProperty("thread")] public Optional Thread { get; set; } + + [JsonProperty("resolved")] + public Optional Resolved { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs b/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs index 3975a8c1..c7a69568 100644 --- a/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs +++ b/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs @@ -34,6 +34,10 @@ namespace Discord.API [JsonProperty("values")] public Optional Values { get; set; } + + [JsonProperty("default_values")] + public Optional DefaultValues { get; set; } + public SelectMenuComponent() { } public SelectMenuComponent(Discord.SelectMenuComponent component) @@ -46,6 +50,7 @@ namespace Discord.API MaxValues = component.MaxValues; Disabled = component.IsDisabled; ChannelTypes = component.ChannelTypes.ToArray(); + DefaultValues = component.DefaultValues.Select(x => new SelectMenuDefaultValue {Id = x.Id, Type = x.Type}).ToArray(); } } } diff --git a/src/Discord.Net.Rest/API/Common/SelectMenuDefaultValue.cs b/src/Discord.Net.Rest/API/Common/SelectMenuDefaultValue.cs new file mode 100644 index 00000000..ebaed146 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/SelectMenuDefaultValue.cs @@ -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; } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index f928e362..0f4dd291 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -203,7 +203,10 @@ namespace Discord.Rest parsed.MaxValues, parsed.Disabled, parsed.Type, - parsed.ChannelTypes.GetValueOrDefault() + parsed.ChannelTypes.GetValueOrDefault(), + parsed.DefaultValues.IsSpecified + ? parsed.DefaultValues.Value.Select(x => new SelectMenuDefaultValue(x.Id, x.Type)) + : Array.Empty() ); } default: diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index 083a8e72..f807c9e1 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Message; @@ -47,6 +48,9 @@ namespace Discord.Rest /// public IUserMessage ReferencedMessage => _referencedMessage; + /// + public MessageResolvedData ResolvedData { get; internal set; } + internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) : base(discord, id, channel, author, source) { @@ -130,6 +134,34 @@ namespace Discord.Rest else _stickers = ImmutableArray.Create(); } + + 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.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.Empty; + + var roles = model.Resolved.Value.Roles.IsSpecified + ? model.Resolved.Value.Roles.Value.Select(x => RestRole.Create(Discord, guild, x.Value)).ToImmutableArray() + : ImmutableArray.Empty; + + var channels = model.Resolved.Value.Channels.IsSpecified + ? model.Resolved.Value.Channels.Value.Select(x => RestChannel.Create(Discord, x.Value, guild)).ToImmutableArray() + : ImmutableArray.Empty; + + ResolvedData = new MessageResolvedData(users, members, roles, channels); + } } /// diff --git a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs index ae16f19c..62c0e580 100644 --- a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs @@ -79,6 +79,8 @@ namespace Discord.Net.Converters return UserStatusConverter.Instance; if (type == typeof(EmbedType)) return EmbedTypeConverter.Instance; + if (type == typeof(SelectDefaultValueType)) + return SelectMenuDefaultValueTypeConverter.Instance; //Special if (type == typeof(API.Image)) diff --git a/src/Discord.Net.Rest/Net/Converters/SelectMenuDefaultValueTypeConverter.cs b/src/Discord.Net.Rest/Net/Converters/SelectMenuDefaultValueTypeConverter.cs new file mode 100644 index 00000000..32212feb --- /dev/null +++ b/src/Discord.Net.Rest/Net/Converters/SelectMenuDefaultValueTypeConverter.cs @@ -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((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)); + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index e7b09c33..24c9dce2 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -237,7 +237,10 @@ namespace Discord.WebSocket parsed.MaxValues, parsed.Disabled, parsed.Type, - parsed.ChannelTypes.GetValueOrDefault() + parsed.ChannelTypes.GetValueOrDefault(), + parsed.DefaultValues.IsSpecified + ? parsed.DefaultValues.Value.Select(x => new SelectMenuDefaultValue(x.Id, x.Type)) + : Array.Empty() ); } default: diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index f5abb2c4..4f061b7e 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -49,6 +49,9 @@ namespace Discord.WebSocket /// public IUserMessage ReferencedMessage => _referencedMessage; + /// + public MessageResolvedData ResolvedData { get; internal set; } + internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) : base(discord, id, channel, author, source) { @@ -172,6 +175,34 @@ namespace Discord.WebSocket else _stickers = ImmutableArray.Create(); } + + 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.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.Empty; + + var roles = model.Resolved.Value.Roles.IsSpecified + ? model.Resolved.Value.Roles.Value.Select(x => RestRole.Create(Discord, guild, x.Value)).ToImmutableArray() + : ImmutableArray.Empty; + + var channels = model.Resolved.Value.Channels.IsSpecified + ? model.Resolved.Value.Channels.Value.Select(x => RestChannel.Create(Discord, x.Value, guild)).ToImmutableArray() + : ImmutableArray.Empty; + + ResolvedData = new MessageResolvedData(users, members, roles, channels); + } } ///