Compare commits
10 Commits
b386a0e22a
...
09e0fdb91f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09e0fdb91f | ||
|
|
c3a30dbf95 | ||
|
|
b169a116d2 | ||
|
|
e05e849584 | ||
|
|
e3cb507032 | ||
|
|
16ea091d20 | ||
|
|
0dea5bb4fd | ||
|
|
9c1db3f0f0 | ||
|
|
4fdebdc6ed | ||
|
|
86b885b24b |
37
CHANGELOG.md
37
CHANGELOG.md
@@ -1,5 +1,42 @@
|
||||
# Changelog
|
||||
|
||||
## [3.19.0-beta.1] - 2026-01-03
|
||||
### Added
|
||||
- #3172 Modal refactoring & select menu support (a476014)
|
||||
- #3178 Add `PinMessages` permission (5273f1d)
|
||||
- #3183 Update voice API to version 8 (927c905)
|
||||
- #3189 Modal Select Components Support for IF (e8c5436)
|
||||
- #3196 Add modify current guild member support (80fbbc2)
|
||||
- #3198 Get Archived thread calls on text channels (ca6c9bc)
|
||||
- #3199 Add select menu `IsRequired` property and fix usage of `DefaultValues` (0aff637)
|
||||
- #3200 Add net10.0 build target, update deps (f205bba)
|
||||
- #3193 Remove unsupported SDK targets (5ca29fd))
|
||||
- #3195 Add `GetRoleUserCounts` REST method (dade9b2)
|
||||
- #3207 Added UnknownComponent classes, and support to MessageComponentConverter and MessageComponentExtensions (ad8182f)
|
||||
- #3209 Modal Components v2 Single-select Typeconverters, Rest, and Patches (1e27c99)
|
||||
|
||||
### Fixed
|
||||
- #3167 Incorrect casts in the legacy component builder (958d286)
|
||||
- #3173 Remove voice gateway port stripping (0b078d7)
|
||||
- #3174 Fix duplicated flag value in ActivityProperties enum (ebc7db8)
|
||||
- #3186 Fix missing SelectMenu Type, ChannelTypes, DefaultValues in ComponentBuilder.AddComponent (8883596)
|
||||
- #3190 Fix voice receiving (a468e18)
|
||||
- #3192 fixed error on changing role icon/emoji to an image. (06510e1)
|
||||
- #3204 Fix user status update when speaking (11a56bc)
|
||||
- #3206 Fix AutocompleteResult.Value having no length limit (161a91e)
|
||||
- #3210 Fix NRE in Rest Interaction Guild User Resolution (fd6e3ad)
|
||||
- #3218 fix modal text display constructor initialization (86b885b)
|
||||
- #3197 Correct `IUserMessage.ModifyAsync` precondition expression (8668092)
|
||||
|
||||
### Misc
|
||||
- #3169 Make ParameterChoice constructor public (9cb6ffd)
|
||||
- #3171 Bump guild batch limit to 200 (fc0712d)
|
||||
- #3176 Add better json type exception (ae6e7d5)
|
||||
- #3177 Make Cacheable constructors public (e61eb51
|
||||
- #3216 Improve .NET 9.0+ locking performance (4e95dd7)
|
||||
- #3217 Switch lock backport package to #if defs instead (b386a0e)
|
||||
|
||||
|
||||
## [3.18.0] - 2025-07-19
|
||||
### Added
|
||||
- #3145 add `ApproximateUserAuthorizationCount` (6e1f9c1)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VersionPrefix>3.18.0</VersionPrefix>
|
||||
<VersionPrefix>3.19.0</VersionPrefix>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Authors>Discord.Net Contributors</Authors>
|
||||
<PackageTags>discord;discordapp</PackageTags>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"globalMetadata": {
|
||||
"_appTitle": "Discord.Net Documentation",
|
||||
"_appName": "Discord.Net",
|
||||
"_appFooter": "Discord.Net © 2015-2025 3.18.0",
|
||||
"_appFooter": "Discord.Net © 2015-2026 3.19.0",
|
||||
"_enableSearch": true,
|
||||
"_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg",
|
||||
"_appFaviconPath": "favicon.png"
|
||||
|
||||
@@ -64,6 +64,16 @@ public class ActionRowBuilder : IMessageComponentBuilder, IInteractableComponent
|
||||
Components = components?.ToList() ?? [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="ActionRowBuilder"/>.
|
||||
/// </summary>
|
||||
public ActionRowBuilder(IEnumerable<IMessageComponentBuilder> components, int? id)
|
||||
{
|
||||
Components = components?.ToList() ?? [];
|
||||
Id = id;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="ActionRowBuilder"/> from existing component.
|
||||
/// </summary>
|
||||
|
||||
@@ -62,6 +62,17 @@ public class ContainerBuilder : IMessageComponentBuilder, IStaticComponentContai
|
||||
{
|
||||
Components = components?.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="ContainerBuilder"/>.
|
||||
/// </summary>
|
||||
public ContainerBuilder(Color? accentColor = null, bool? isSpoiler = null, int? id = null, params IEnumerable<IMessageComponentBuilder> components)
|
||||
{
|
||||
Components = components?.ToList();
|
||||
AccentColor = accentColor;
|
||||
IsSpoiler = IsSpoiler;
|
||||
Id = id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="ContainerBuilder"/> from existing component.
|
||||
|
||||
@@ -32,6 +32,14 @@ public class MediaGalleryBuilder : IMessageComponentBuilder
|
||||
{
|
||||
Items = items?.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MediaGalleryBuilder"/>.
|
||||
/// </summary>
|
||||
public MediaGalleryBuilder(IEnumerable<MediaGalleryItemProperties> items, int? id) : this(items)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="MediaGalleryBuilder"/> from existing component.
|
||||
|
||||
@@ -59,6 +59,16 @@ public class SectionBuilder : IMessageComponentBuilder, IStaticComponentContaine
|
||||
Components = components?.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="SectionBuilder"/>.
|
||||
/// </summary>
|
||||
public SectionBuilder(IMessageComponentBuilder accessory, IEnumerable<IMessageComponentBuilder> components, int? id)
|
||||
{
|
||||
Accessory = accessory;
|
||||
Components = components?.ToList();
|
||||
Id = id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="SectionBuilder"/> from existing component.
|
||||
/// </summary>
|
||||
|
||||
@@ -21,10 +21,11 @@ public class SeparatorBuilder : IMessageComponentBuilder
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="SeparatorBuilder"/>.
|
||||
/// </summary>
|
||||
public SeparatorBuilder(bool isDivider = true, SeparatorSpacingSize spacing = SeparatorSpacingSize.Small)
|
||||
public SeparatorBuilder(bool isDivider = true, SeparatorSpacingSize spacing = SeparatorSpacingSize.Small, int? id = null)
|
||||
{
|
||||
IsDivider = isDivider;
|
||||
Spacing = spacing;
|
||||
Id = id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -38,11 +38,12 @@ public class ThumbnailBuilder : IMessageComponentBuilder
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ThumbnailBuilder"/>.
|
||||
/// </summary>
|
||||
public ThumbnailBuilder(UnfurledMediaItemProperties media, string description = null, bool isSpoiler = false)
|
||||
public ThumbnailBuilder(UnfurledMediaItemProperties media, string description = null, bool isSpoiler = false, int? id = null)
|
||||
{
|
||||
Media = media;
|
||||
Description = description;
|
||||
IsSpoiler = isSpoiler;
|
||||
Id = id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -212,5 +212,10 @@ namespace Discord
|
||||
/// Allows pinning and unpinning messages.
|
||||
/// </summary>
|
||||
PinMessages = 1L << 51,
|
||||
|
||||
/// <summary>
|
||||
/// Allows bypassing slowmode restrictions.
|
||||
/// </summary>
|
||||
BypassSlowmode = 1L << 52,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,17 +18,17 @@ namespace Discord
|
||||
/// <summary>
|
||||
/// Gets a <see cref="ChannelPermissions"/> that grants all permissions for text channels.
|
||||
/// </summary>
|
||||
public static readonly ChannelPermissions Text = new(0b1110_110001_001111_110010_110011_111101_111111_111101_010001);
|
||||
public static readonly ChannelPermissions Text = new(0b11110_110001_001111_110010_110011_111101_111111_111101_010001);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="ChannelPermissions"/> that grants all permissions for voice channels.
|
||||
/// </summary>
|
||||
public static readonly ChannelPermissions Voice = new(0b0111_110101_001010_001010_110011_111101_111111_111101_010001);
|
||||
public static readonly ChannelPermissions Voice = new(0b10111_110101_001010_001010_110011_111101_111111_111101_010001);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="ChannelPermissions"/> that grants all permissions for stage channels.
|
||||
/// </summary>
|
||||
public static readonly ChannelPermissions Stage = new(0b0110_110100_000010_001110_010001_010101_111111_111001_010001);
|
||||
public static readonly ChannelPermissions Stage = new(0b10110_110100_000010_001110_010001_010101_111111_111001_010001);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="ChannelPermissions"/> that grants all permissions for category channels.
|
||||
@@ -160,6 +160,8 @@ namespace Discord
|
||||
public bool UseExternalSounds => Permissions.GetValue(RawValue, ChannelPermission.UseExternalSounds);
|
||||
/// <summary> If <see langword="true"/>, a user can ping and unpin messages.</summary>
|
||||
public bool PinMessages => Permissions.GetValue(RawValue, ChannelPermission.PinMessages);
|
||||
/// <summary> If <see langword="true"/>, a user may bypass slowmode restrictions.</summary>
|
||||
public bool BypassSlowmode => Permissions.GetValue(RawValue, ChannelPermission.BypassSlowmode);
|
||||
|
||||
/// <summary> Creates a new <see cref="ChannelPermissions"/> with the provided packed value.</summary>
|
||||
public ChannelPermissions(ulong rawValue) { RawValue = rawValue; }
|
||||
@@ -203,7 +205,8 @@ namespace Discord
|
||||
bool? sendPolls = null,
|
||||
bool? useExternalApps = null,
|
||||
bool? useExternalSounds = null,
|
||||
bool? pinMessages = null)
|
||||
bool? pinMessages = null,
|
||||
bool? bypassSlowmode = null)
|
||||
{
|
||||
ulong value = initialValue;
|
||||
|
||||
@@ -246,6 +249,7 @@ namespace Discord
|
||||
Permissions.SetValue(ref value, useExternalApps, ChannelPermission.UseExternalApps);
|
||||
Permissions.SetValue(ref value, useExternalSounds, ChannelPermission.UseExternalSounds);
|
||||
Permissions.SetValue(ref value, pinMessages, ChannelPermission.PinMessages);
|
||||
Permissions.SetValue(ref value, bypassSlowmode, ChannelPermission.BypassSlowmode);
|
||||
|
||||
RawValue = value;
|
||||
}
|
||||
@@ -290,13 +294,14 @@ namespace Discord
|
||||
bool sendPolls = false,
|
||||
bool useExternalApps = false,
|
||||
bool useExternalSounds = false,
|
||||
bool pinMessages = false)
|
||||
bool pinMessages = false,
|
||||
bool bypassSlowmode = false)
|
||||
: this(0, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages,
|
||||
embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect,
|
||||
speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, prioritySpeaker, stream, manageRoles, manageWebhooks,
|
||||
useApplicationCommands, requestToSpeak, manageThreads, createPublicThreads, createPrivateThreads, useExternalStickers, sendMessagesInThreads,
|
||||
startEmbeddedActivities, useSoundboard, createEvents, sendVoiceMessages, useClydeAI, setVoiceChannelStatus, sendPolls, useExternalApps,
|
||||
useExternalSounds, pinMessages)
|
||||
useExternalSounds, pinMessages, bypassSlowmode)
|
||||
{ }
|
||||
|
||||
/// <summary> Creates a new <see cref="ChannelPermissions"/> from this one, changing the provided non-null permissions.</summary>
|
||||
@@ -339,7 +344,8 @@ namespace Discord
|
||||
bool? sendPolls = null,
|
||||
bool? useExternalApps = null,
|
||||
bool? useExternalSounds = null,
|
||||
bool? pinMessages = null)
|
||||
bool? pinMessages = null,
|
||||
bool? bypassSlowmode = null)
|
||||
=> new ChannelPermissions(RawValue,
|
||||
createInstantInvite,
|
||||
manageChannel,
|
||||
@@ -379,7 +385,8 @@ namespace Discord
|
||||
sendPolls,
|
||||
useExternalApps,
|
||||
useExternalSounds,
|
||||
pinMessages);
|
||||
pinMessages,
|
||||
bypassSlowmode);
|
||||
|
||||
public bool Has(ChannelPermission permission) => Permissions.GetValue(RawValue, permission);
|
||||
|
||||
|
||||
@@ -307,5 +307,10 @@ namespace Discord
|
||||
/// Allows pinning and unpinning messages.
|
||||
/// </summary>
|
||||
PinMessages = 1L << 51,
|
||||
|
||||
/// <summary>
|
||||
/// Allows bypassing slowmode restrictions.
|
||||
/// </summary>
|
||||
BypassSlowmode = 1L << 52,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,8 @@ namespace Discord
|
||||
public bool CreateEvents => Permissions.GetValue(RawValue, GuildPermission.CreateEvents);
|
||||
/// <summary> If <see langword="true"/>, a user can ping and unpin messages.</summary>
|
||||
public bool PinMessages => Permissions.GetValue(RawValue, GuildPermission.PinMessages);
|
||||
/// <summary> If <see langword="true"/>, a user may bypass slowmode restrictions. </summary>
|
||||
public bool BypassSlowmode => Permissions.GetValue(RawValue, GuildPermission.BypassSlowmode);
|
||||
|
||||
/// <summary> Creates a new <see cref="GuildPermissions"/> with the provided packed value. </summary>
|
||||
public GuildPermissions(ulong rawValue) { RawValue = rawValue; }
|
||||
@@ -185,7 +187,8 @@ namespace Discord
|
||||
bool? useExternalApps = null,
|
||||
bool? useExternalSounds = null,
|
||||
bool? createEvents = null,
|
||||
bool? pinMessages = null)
|
||||
bool? pinMessages = null,
|
||||
bool? bypassSlowmode = null)
|
||||
{
|
||||
ulong value = initialValue;
|
||||
|
||||
@@ -241,6 +244,7 @@ namespace Discord
|
||||
Permissions.SetValue(ref value, useExternalSounds, GuildPermission.UseExternalSounds);
|
||||
Permissions.SetValue(ref value, createEvents, GuildPermission.CreateEvents);
|
||||
Permissions.SetValue(ref value, pinMessages, GuildPermission.PinMessages);
|
||||
Permissions.SetValue(ref value, bypassSlowmode, GuildPermission.BypassSlowmode);
|
||||
|
||||
RawValue = value;
|
||||
}
|
||||
@@ -298,7 +302,8 @@ namespace Discord
|
||||
bool useExternalApps = false,
|
||||
bool useExternalSounds = false,
|
||||
bool createEvents = false,
|
||||
bool pinMessages = false)
|
||||
bool pinMessages = false,
|
||||
bool bypassSlowmode = false)
|
||||
: this(0,
|
||||
createInstantInvite: createInstantInvite,
|
||||
manageRoles: manageRoles,
|
||||
@@ -351,7 +356,8 @@ namespace Discord
|
||||
useExternalApps: useExternalApps,
|
||||
useExternalSounds: useExternalSounds,
|
||||
createEvents: createEvents,
|
||||
pinMessages: pinMessages)
|
||||
pinMessages: pinMessages,
|
||||
bypassSlowmode: bypassSlowmode)
|
||||
{ }
|
||||
|
||||
/// <summary> Creates a new <see cref="GuildPermissions"/> from this one, changing the provided non-null permissions. </summary>
|
||||
@@ -407,14 +413,15 @@ namespace Discord
|
||||
bool? useExternalApps = null,
|
||||
bool? useExternalSounds = null,
|
||||
bool? createEvents = null,
|
||||
bool? pinMessages = null)
|
||||
bool? pinMessages = null,
|
||||
bool? bypassSlowmode = null)
|
||||
=> new GuildPermissions(RawValue, createInstantInvite, kickMembers, banMembers, administrator, manageChannels, manageGuild, addReactions,
|
||||
viewAuditLog, viewGuildInsights, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles,
|
||||
readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers,
|
||||
useVoiceActivation, prioritySpeaker, stream, changeNickname, manageNicknames, manageRoles, manageWebhooks, manageEmojisAndStickers,
|
||||
useApplicationCommands, requestToSpeak, manageEvents, manageThreads, createPublicThreads, createPrivateThreads, useExternalStickers, sendMessagesInThreads,
|
||||
startEmbeddedActivities, moderateMembers, useSoundboard, viewMonetizationAnalytics, sendVoiceMessages, useClydeAI, createGuildExpressions, setVoiceChannelStatus,
|
||||
sendPolls, useExternalApps, useExternalSounds, createEvents, pinMessages);
|
||||
sendPolls, useExternalApps, useExternalSounds, createEvents, pinMessages, bypassSlowmode);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value that indicates if a specific <see cref="GuildPermission"/> is enabled
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Discord.Interactions
|
||||
/// <summary>
|
||||
/// Specify the target channel types for a <see cref="ApplicationCommandOptionType.Channel"/> option.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
|
||||
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||
public sealed class ChannelTypesAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -12,5 +12,9 @@ public class ModalChannelSelectAttribute : ModalSelectComponentAttribute
|
||||
/// Create a new <see cref="ModalChannelSelectAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="customId">Custom ID of the channel select component.</param>
|
||||
public ModalChannelSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
|
||||
/// <param name="minValues">The minimum number of values that can be selected.</param>
|
||||
/// <param name="maxValues">The maximum number of values that can be selected.</param>
|
||||
/// <param name="id">Optional identifier for the component.</param>
|
||||
public ModalChannelSelectAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
|
||||
: base(customId, minValues, maxValues, id) { }
|
||||
}
|
||||
|
||||
@@ -13,5 +13,16 @@ public abstract class ModalComponentAttribute : Attribute
|
||||
/// </summary>
|
||||
public abstract ComponentType ComponentType { get; }
|
||||
|
||||
internal ModalComponentAttribute() { }
|
||||
/// <summary>
|
||||
/// Gets the optional identifier for component.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sending components with an id of 0 is allowed but will be treated as empty and replaced by the API.
|
||||
/// </remarks>
|
||||
public int Id { get; set; }
|
||||
|
||||
internal ModalComponentAttribute(int id = 0)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ public class ModalFileUploadAttribute : ModalInputAttribute
|
||||
/// <param name="customId">Custom ID of the file upload component.</param>
|
||||
/// <param name="minValues">Minimum number of files that can be uploaded.</param>
|
||||
/// <param name="maxValues">Maximum number of files that can be uploaded.</param>
|
||||
public ModalFileUploadAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId)
|
||||
/// <param name="id">The optional identifier for the component.</param>
|
||||
public ModalFileUploadAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
|
||||
: base(customId, id)
|
||||
{
|
||||
MinValues = minValues;
|
||||
MaxValues = maxValues;
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace Discord.Interactions
|
||||
/// Create a new <see cref="ModalInputAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="customId">The custom id of the input.</param>
|
||||
internal ModalInputAttribute(string customId)
|
||||
/// <param name="id">Optional identifier for component.</param>
|
||||
internal ModalInputAttribute(string customId, int id) : base(id)
|
||||
{
|
||||
CustomId = customId;
|
||||
}
|
||||
|
||||
@@ -14,5 +14,7 @@ public class ModalMentionableSelectAttribute : ModalSelectComponentAttribute
|
||||
/// <param name="customId">Custom ID of the mentionable select component.</param>
|
||||
/// <param name="minValues">Minimum number of values that can be selected.</param>
|
||||
/// <param name="maxValues">Maximum number of values that can be selected</param>
|
||||
public ModalMentionableSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
|
||||
/// <param name="id">The optional identifier for the component.</param>
|
||||
public ModalMentionableSelectAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
|
||||
: base(customId, minValues, maxValues, id) { }
|
||||
}
|
||||
|
||||
@@ -14,5 +14,7 @@ public class ModalRoleSelectAttribute : ModalSelectComponentAttribute
|
||||
/// <param name="customId">Custom ID of the role select component.</param>
|
||||
/// <param name="minValues">Minimum number of values that can be selected.</param>
|
||||
/// <param name="maxValues">Maximum number of values that can be selected.</param>
|
||||
public ModalRoleSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
|
||||
/// <param name="id">The optional identifier for the component.</param>
|
||||
public ModalRoleSelectAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
|
||||
: base(customId, minValues, maxValues, id) { }
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ public abstract class ModalSelectComponentAttribute : ModalInputAttribute
|
||||
/// </summary>
|
||||
public string Placeholder { get; set; }
|
||||
|
||||
internal ModalSelectComponentAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId)
|
||||
internal ModalSelectComponentAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
|
||||
: base(customId, id)
|
||||
{
|
||||
MinValues = minValues;
|
||||
MaxValues = maxValues;
|
||||
|
||||
@@ -14,5 +14,7 @@ public sealed class ModalSelectMenuAttribute : ModalSelectComponentAttribute
|
||||
/// <param name="customId">Custom ID of the select menu component.</param>
|
||||
/// <param name="minValues">Minimum number of values that can be selected.</param>
|
||||
/// <param name="maxValues">Maximum number of values that can be selected.</param>
|
||||
public ModalSelectMenuAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
|
||||
/// <param name="id">The optional identifier for the component.</param>
|
||||
public ModalSelectMenuAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
|
||||
: base(customId, minValues, maxValues, id) { }
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ public class ModalTextDisplayAttribute : ModalComponentAttribute
|
||||
/// Create a new <see cref="ModalTextInputAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="content">Content of the text display.</param>
|
||||
public ModalTextDisplayAttribute(string content = null)
|
||||
/// <param name="id">Optional identifier for component.</param>
|
||||
public ModalTextDisplayAttribute(string content = null, int id = 0)
|
||||
: base(id)
|
||||
{
|
||||
Content = content;
|
||||
}
|
||||
|
||||
@@ -11,27 +11,27 @@ namespace Discord.Interactions
|
||||
/// <summary>
|
||||
/// Gets the style of the text input.
|
||||
/// </summary>
|
||||
public TextInputStyle Style { get; }
|
||||
public TextInputStyle Style { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the placeholder of the text input.
|
||||
/// </summary>
|
||||
public string Placeholder { get; }
|
||||
public string Placeholder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the minimum length of the text input.
|
||||
/// </summary>
|
||||
public int MinLength { get; }
|
||||
public int MinLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum length of the text input.
|
||||
/// </summary>
|
||||
public int MaxLength { get; }
|
||||
public int MaxLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the initial value to be displayed by this input.
|
||||
/// </summary>
|
||||
public string InitialValue { get; }
|
||||
public string InitialValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ModalTextInputAttribute"/>.
|
||||
@@ -42,8 +42,9 @@ namespace Discord.Interactions
|
||||
/// <param name="minLength">The minimum length of the text input's content.</param>
|
||||
/// <param name="maxLength">The maximum length of the text input's content.</param>
|
||||
/// <param name="initValue">The initial value to be displayed by this input.</param>
|
||||
public ModalTextInputAttribute(string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, int minLength = 1, int maxLength = 4000, string initValue = null)
|
||||
: base(customId)
|
||||
/// <param name="id">The optional identifier for the component.</param>
|
||||
public ModalTextInputAttribute(string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, int minLength = 1, int maxLength = 4000, string initValue = null, int id = 0)
|
||||
: base(customId, id)
|
||||
{
|
||||
Style = style;
|
||||
Placeholder = placeholder;
|
||||
|
||||
@@ -14,5 +14,7 @@ public class ModalUserSelectAttribute : ModalSelectComponentAttribute
|
||||
/// <param name="customId">Custom ID of the user select component.</param>
|
||||
/// <param name="minValues">Minimum number of values that can be selected.</param>
|
||||
/// <param name="maxValues">Maximum number of values that can be selected.</param>
|
||||
public ModalUserSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
|
||||
/// <param name="id">The optional identifier for the component.</param>
|
||||
public ModalUserSelectAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0)
|
||||
: base(customId, minValues, maxValues, id) { }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
|
||||
namespace Discord.Interactions;
|
||||
|
||||
/// <summary>
|
||||
/// Adds additional metadata to enum fields that are used for select-menus.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To manually add select menu options to modal components, use <see cref="ModalSelectMenuOptionAttribute"/> instead.
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
|
||||
public class SelectMenuOptionAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the desription of the option.
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the option is selected by default.
|
||||
/// </summary>
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the emote of the option.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Can be either an <see cref="Emoji"/> or an <see cref="Discord.Emote"/>
|
||||
/// </remarks>
|
||||
public string Emote { get; set; }
|
||||
}
|
||||
@@ -8,8 +8,15 @@ namespace Discord.Interactions.Builders;
|
||||
/// </summary>
|
||||
public class ChannelSelectComponentBuilder : SnowflakeSelectComponentBuilder<ChannelSelectComponentInfo, ChannelSelectComponentBuilder>
|
||||
{
|
||||
private readonly List<ChannelType> _channelTypes = new();
|
||||
|
||||
protected override ChannelSelectComponentBuilder Instance => this;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the presented channel types for this Channel Select.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<ChannelType> ChannelTypes => _channelTypes.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="ChannelSelectComponentBuilder"/>.
|
||||
/// </summary>
|
||||
@@ -42,6 +49,19 @@ public class ChannelSelectComponentBuilder : SnowflakeSelectComponentBuilder<Cha
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of <see cref="ChannelTypes"/>.
|
||||
/// </summary>
|
||||
/// <param name="channelTypes">the new value of <see cref="ChannelTypes"/>.</param>
|
||||
/// <returns>
|
||||
/// The builder instance.
|
||||
/// </returns>
|
||||
public ChannelSelectComponentBuilder WithChannelTypes(params IEnumerable<ChannelType> channelTypes)
|
||||
{
|
||||
_channelTypes.AddRange(channelTypes);
|
||||
return this;
|
||||
}
|
||||
|
||||
internal override ChannelSelectComponentInfo Build(ModalInfo modal)
|
||||
=> new(this, modal);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,14 @@ public interface IModalComponentBuilder
|
||||
/// </summary>
|
||||
object DefaultValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional identifier for component.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sending components with an id of 0 is allowed but will be treated as empty and replaced by the API.
|
||||
/// </remarks>
|
||||
int Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of the attributes of this component.
|
||||
/// </summary>
|
||||
@@ -62,4 +70,13 @@ public interface IModalComponentBuilder
|
||||
/// The builder instance.
|
||||
/// </returns>
|
||||
IModalComponentBuilder WithAttributes(params Attribute[] attributes);
|
||||
|
||||
/// <summary>
|
||||
/// Sets <see cref="Id"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">New value of the <see cref="Id"/>.</param>
|
||||
/// <returns>
|
||||
/// The builder instance.
|
||||
/// </returns>
|
||||
IModalComponentBuilder WithId(int id);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ public abstract class ModalComponentBuilder<TInfo, TBuilder> : IModalComponentBu
|
||||
/// <inheritdoc/>
|
||||
public object DefaultValue { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyCollection<Attribute> Attributes => _attributes;
|
||||
|
||||
@@ -87,6 +90,19 @@ public abstract class ModalComponentBuilder<TInfo, TBuilder> : IModalComponentBu
|
||||
return Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets <see cref="Id"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">New value of the <see cref="Id"/>.</param>
|
||||
/// <returns>
|
||||
/// The builder instance.
|
||||
/// </returns>
|
||||
public virtual TBuilder WithId(int id)
|
||||
{
|
||||
Id = id;
|
||||
return Instance;
|
||||
}
|
||||
|
||||
internal abstract TInfo Build(ModalInfo modal);
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -97,4 +113,7 @@ public abstract class ModalComponentBuilder<TInfo, TBuilder> : IModalComponentBu
|
||||
|
||||
/// <inheritdoc/>
|
||||
IModalComponentBuilder IModalComponentBuilder.WithAttributes(params Attribute[] attributes) => WithAttributes(attributes);
|
||||
|
||||
/// <inheritdoc/>
|
||||
IModalComponentBuilder IModalComponentBuilder.WithId(int id) => WithId(id);
|
||||
}
|
||||
|
||||
@@ -654,6 +654,8 @@ namespace Discord.Interactions.Builders
|
||||
|
||||
private static void BuildTextInputComponent(TextInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
|
||||
{
|
||||
EnsurePubliclySettable(propertyInfo);
|
||||
|
||||
var attributes = propertyInfo.GetCustomAttributes();
|
||||
|
||||
builder.Label = propertyInfo.Name;
|
||||
@@ -673,6 +675,7 @@ namespace Discord.Interactions.Builders
|
||||
builder.MaxLength = textInput.MaxLength;
|
||||
builder.MinLength = textInput.MinLength;
|
||||
builder.InitialValue = textInput.InitialValue;
|
||||
builder.Id = textInput.Id;
|
||||
break;
|
||||
case RequiredInputAttribute requiredInput:
|
||||
builder.IsRequired = requiredInput.IsRequired;
|
||||
@@ -690,6 +693,8 @@ namespace Discord.Interactions.Builders
|
||||
|
||||
private static void BuildSelectMenuComponent(SelectMenuComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
|
||||
{
|
||||
EnsurePubliclySettable(propertyInfo);
|
||||
|
||||
var attributes = propertyInfo.GetCustomAttributes();
|
||||
|
||||
builder.Label = propertyInfo.Name;
|
||||
@@ -707,6 +712,7 @@ namespace Discord.Interactions.Builders
|
||||
builder.MinValues = selectMenuInput.MinValues;
|
||||
builder.MaxValues = selectMenuInput.MaxValues;
|
||||
builder.Placeholder = selectMenuInput.Placeholder;
|
||||
builder.Id = selectMenuInput.Id;
|
||||
break;
|
||||
case RequiredInputAttribute requiredInput:
|
||||
builder.IsRequired = requiredInput.IsRequired;
|
||||
@@ -742,6 +748,8 @@ namespace Discord.Interactions.Builders
|
||||
where TInfo : SnowflakeSelectComponentInfo
|
||||
where TBuilder : SnowflakeSelectComponentBuilder<TInfo, TBuilder>
|
||||
{
|
||||
EnsurePubliclySettable(propertyInfo);
|
||||
|
||||
var attributes = propertyInfo.GetCustomAttributes();
|
||||
|
||||
builder.Label = propertyInfo.Name;
|
||||
@@ -759,6 +767,7 @@ namespace Discord.Interactions.Builders
|
||||
builder.MinValues = selectInput.MinValues;
|
||||
builder.MaxValues = selectInput.MaxValues;
|
||||
builder.Placeholder = selectInput.Placeholder;
|
||||
builder.Id = selectInput.Id;
|
||||
break;
|
||||
case RequiredInputAttribute requiredInput:
|
||||
builder.IsRequired = requiredInput.IsRequired;
|
||||
@@ -767,6 +776,9 @@ namespace Discord.Interactions.Builders
|
||||
builder.Label = inputLabel.Label;
|
||||
builder.Description = inputLabel.Description;
|
||||
break;
|
||||
case ChannelTypesAttribute channelTypes when builder is ChannelSelectComponentBuilder channelSelectBuilder:
|
||||
channelSelectBuilder.WithChannelTypes(channelTypes.ChannelTypes);
|
||||
break;
|
||||
default:
|
||||
builder.WithAttributes(attribute);
|
||||
break;
|
||||
@@ -776,6 +788,8 @@ namespace Discord.Interactions.Builders
|
||||
|
||||
private static void BuildFileUploadComponent(FileUploadComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
|
||||
{
|
||||
EnsurePubliclySettable(propertyInfo);
|
||||
|
||||
var attributes = propertyInfo.GetCustomAttributes();
|
||||
|
||||
builder.Label = propertyInfo.Name;
|
||||
@@ -792,6 +806,7 @@ namespace Discord.Interactions.Builders
|
||||
builder.ComponentType = fileUploadInput.ComponentType;
|
||||
builder.MinValues = fileUploadInput.MinValues;
|
||||
builder.MaxValues = fileUploadInput.MaxValues;
|
||||
builder.Id = fileUploadInput.Id;
|
||||
break;
|
||||
case RequiredInputAttribute requiredInput:
|
||||
builder.IsRequired = requiredInput.IsRequired;
|
||||
@@ -822,6 +837,7 @@ namespace Discord.Interactions.Builders
|
||||
case ModalTextDisplayAttribute textDisplay:
|
||||
builder.ComponentType = textDisplay.ComponentType;
|
||||
builder.Content = textDisplay.Content;
|
||||
builder.Id = textDisplay.Id;
|
||||
break;
|
||||
default:
|
||||
builder.WithAttributes(attribute);
|
||||
@@ -882,9 +898,18 @@ namespace Discord.Interactions.Builders
|
||||
|
||||
private static bool IsValidModalComponentDefinition(PropertyInfo propertyInfo)
|
||||
{
|
||||
return propertyInfo.SetMethod?.IsPublic == true &&
|
||||
propertyInfo.SetMethod?.IsStatic == false &&
|
||||
propertyInfo.IsDefined(typeof(ModalComponentAttribute));
|
||||
return propertyInfo.IsDefined(typeof(ModalComponentAttribute));
|
||||
}
|
||||
|
||||
private static bool IsPubliclySettable(PropertyInfo propertyInfo)
|
||||
{
|
||||
return propertyInfo.SetMethod is { IsPublic: true, IsStatic: false };
|
||||
}
|
||||
|
||||
private static void EnsurePubliclySettable(PropertyInfo propertyInfo)
|
||||
{
|
||||
if(!IsPubliclySettable(propertyInfo))
|
||||
throw new InvalidOperationException($"The property {propertyInfo.Name} must be publicly settable.");
|
||||
}
|
||||
|
||||
private static ConstructorInfo GetComplexParameterConstructor(TypeInfo typeInfo, ComplexParameterAttribute complexParameter)
|
||||
|
||||
@@ -82,12 +82,10 @@ namespace Discord.Interactions
|
||||
case TextInputComponentInfo textComponent:
|
||||
{
|
||||
var inputBuilder = new TextInputBuilder(textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null,
|
||||
textComponent.MaxLength, textComponent.IsRequired);
|
||||
textComponent.MaxLength, textComponent.IsRequired, id: textComponent.Id);
|
||||
|
||||
if (modalInstance != null)
|
||||
{
|
||||
await textComponent.TypeConverter.WriteAsync(inputBuilder, interaction, textComponent, textComponent.Getter(modalInstance));
|
||||
}
|
||||
var instanceValue = modalInstance is not null ? textComponent.Getter(modalInstance) : null;
|
||||
await textComponent.TypeConverter.WriteAsync(inputBuilder, interaction, textComponent, instanceValue);
|
||||
|
||||
var labelBuilder = new LabelBuilder(textComponent.Label, inputBuilder, textComponent.Description);
|
||||
builder.AddLabel(labelBuilder);
|
||||
@@ -95,12 +93,10 @@ namespace Discord.Interactions
|
||||
break;
|
||||
case SelectMenuComponentInfo selectMenuComponent:
|
||||
{
|
||||
var inputBuilder = new SelectMenuBuilder(selectMenuComponent.CustomId, selectMenuComponent.Options.Select(x => new SelectMenuOptionBuilder(x)).ToList(), selectMenuComponent.Placeholder, selectMenuComponent.MaxValues, selectMenuComponent.MinValues, false, isRequired: selectMenuComponent.IsRequired);
|
||||
var inputBuilder = new SelectMenuBuilder(selectMenuComponent.CustomId, selectMenuComponent.Options.Select(x => new SelectMenuOptionBuilder(x)).ToList(), selectMenuComponent.Placeholder, selectMenuComponent.MaxValues, selectMenuComponent.MinValues, false, isRequired: selectMenuComponent.IsRequired, id: selectMenuComponent.Id);
|
||||
|
||||
if (modalInstance != null)
|
||||
{
|
||||
await selectMenuComponent.TypeConverter.WriteAsync(inputBuilder, interaction, selectMenuComponent, selectMenuComponent.Getter(modalInstance));
|
||||
}
|
||||
var instanceValue = modalInstance is not null ? selectMenuComponent.Getter(modalInstance) : null;
|
||||
await selectMenuComponent.TypeConverter.WriteAsync(inputBuilder, interaction, selectMenuComponent, instanceValue);
|
||||
|
||||
var labelBuilder = new LabelBuilder(selectMenuComponent.Label, inputBuilder, selectMenuComponent.Description);
|
||||
builder.AddLabel(labelBuilder);
|
||||
@@ -108,12 +104,11 @@ namespace Discord.Interactions
|
||||
break;
|
||||
case SnowflakeSelectComponentInfo snowflakeSelectComponent:
|
||||
{
|
||||
var inputBuilder = new SelectMenuBuilder(snowflakeSelectComponent.CustomId, null, snowflakeSelectComponent.Placeholder, snowflakeSelectComponent.MaxValues, snowflakeSelectComponent.MinValues, false, snowflakeSelectComponent.ComponentType, null, snowflakeSelectComponent.DefaultValues.ToList(), null, snowflakeSelectComponent.IsRequired);
|
||||
var channelTypes = snowflakeSelectComponent is ChannelSelectComponentInfo channelSelectComponent ? channelSelectComponent.ChannelTypes : null;
|
||||
var inputBuilder = new SelectMenuBuilder(snowflakeSelectComponent.CustomId, null, snowflakeSelectComponent.Placeholder, snowflakeSelectComponent.MaxValues, snowflakeSelectComponent.MinValues, false, snowflakeSelectComponent.ComponentType, channelTypes?.ToList(), snowflakeSelectComponent.DefaultValues.ToList(), snowflakeSelectComponent.Id, snowflakeSelectComponent.IsRequired);
|
||||
|
||||
if (modalInstance != null)
|
||||
{
|
||||
await snowflakeSelectComponent.TypeConverter.WriteAsync(inputBuilder, interaction, snowflakeSelectComponent, snowflakeSelectComponent.Getter(modalInstance));
|
||||
}
|
||||
var instanceValue = modalInstance is not null ? snowflakeSelectComponent.Getter(modalInstance) : null;
|
||||
await snowflakeSelectComponent.TypeConverter.WriteAsync(inputBuilder, interaction, snowflakeSelectComponent, instanceValue);
|
||||
|
||||
var labelBuilder = new LabelBuilder(snowflakeSelectComponent.Label, inputBuilder, snowflakeSelectComponent.Description);
|
||||
builder.AddLabel(labelBuilder);
|
||||
@@ -121,12 +116,10 @@ namespace Discord.Interactions
|
||||
break;
|
||||
case FileUploadComponentInfo fileUploadComponent:
|
||||
{
|
||||
var inputBuilder = new FileUploadComponentBuilder(fileUploadComponent.CustomId, fileUploadComponent.MinValues, fileUploadComponent.MaxValues, fileUploadComponent.IsRequired);
|
||||
var inputBuilder = new FileUploadComponentBuilder(fileUploadComponent.CustomId, fileUploadComponent.MinValues, fileUploadComponent.MaxValues, fileUploadComponent.IsRequired, fileUploadComponent.Id);
|
||||
|
||||
if (modalInstance != null)
|
||||
{
|
||||
await fileUploadComponent.TypeConverter.WriteAsync(inputBuilder, interaction, fileUploadComponent, fileUploadComponent.Getter(modalInstance));
|
||||
}
|
||||
var instanceValue = modalInstance is not null ? fileUploadComponent.Getter(modalInstance) : null;
|
||||
await fileUploadComponent.TypeConverter.WriteAsync(inputBuilder, interaction, fileUploadComponent, instanceValue);
|
||||
|
||||
var labelBuilder = new LabelBuilder(fileUploadComponent.Label, inputBuilder, fileUploadComponent.Description);
|
||||
builder.AddLabel(labelBuilder);
|
||||
@@ -134,8 +127,10 @@ namespace Discord.Interactions
|
||||
break;
|
||||
case TextDisplayComponentInfo textDisplayComponent:
|
||||
{
|
||||
var content = modalInstance is not null ? textDisplayComponent.Getter(modalInstance).ToString() : (textDisplayComponent.DefaultValue as string) ?? textDisplayComponent.Content;
|
||||
var componentBuilder = new TextDisplayBuilder(content);
|
||||
var instanceValue = modalInstance is not null ? textDisplayComponent.Getter(modalInstance).ToString() : null;
|
||||
var content = instanceValue ?? (textDisplayComponent.DefaultValue as string) ?? textDisplayComponent.Content;
|
||||
|
||||
var componentBuilder = new TextDisplayBuilder(content, textDisplayComponent.Id);
|
||||
builder.AddTextDisplay(componentBuilder);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Discord.Interactions;
|
||||
|
||||
/// <summary>
|
||||
@@ -5,5 +8,14 @@ namespace Discord.Interactions;
|
||||
/// </summary>
|
||||
public class ChannelSelectComponentInfo : SnowflakeSelectComponentInfo
|
||||
{
|
||||
internal ChannelSelectComponentInfo(Builders.ChannelSelectComponentBuilder builder, ModalInfo modal) : base(builder, modal) { }
|
||||
/// <summary>
|
||||
/// Gets the presented channel types for this Channel Select.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<ChannelType> ChannelTypes { get; }
|
||||
|
||||
internal ChannelSelectComponentInfo(Builders.ChannelSelectComponentBuilder builder, ModalInfo modal)
|
||||
: base(builder, modal)
|
||||
{
|
||||
ChannelTypes = builder.ChannelTypes.ToImmutableArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,14 @@ public abstract class ModalComponentInfo
|
||||
/// </summary>
|
||||
public object DefaultValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional identifier for component.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sending components with an id of 0 is allowed but will be treated as empty and replaced by the API.
|
||||
/// </remarks>
|
||||
public int Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of the attributes of this command.
|
||||
/// </summary>
|
||||
@@ -51,6 +59,7 @@ public abstract class ModalComponentInfo
|
||||
Type = builder.Type;
|
||||
PropertyInfo = builder.PropertyInfo;
|
||||
DefaultValue = builder.DefaultValue;
|
||||
Id = builder.Id;
|
||||
Attributes = builder.Attributes.ToImmutableArray();
|
||||
|
||||
_getter = new(() => ReflectionUtils<object>.CreateLambdaPropertyGetter(Modal.Type, PropertyInfo));
|
||||
|
||||
@@ -14,6 +14,6 @@ public class TextDisplayComponentInfo : ModalComponentInfo
|
||||
|
||||
internal TextDisplayComponentInfo(TextDisplayComponentBuilder builder, ModalInfo modal) : base(builder, modal)
|
||||
{
|
||||
Content = Content;
|
||||
Content = builder.Content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1219,7 +1219,7 @@ namespace Discord.Interactions
|
||||
{
|
||||
var type = typeof(T);
|
||||
|
||||
if (_modalInfos.ContainsKey(type))
|
||||
if (ModalUtils.Contains(type))
|
||||
throw new InvalidOperationException($"Modal type {type.FullName} already exists.");
|
||||
|
||||
return ModalUtils.GetOrAdd(type, this);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Discord.Interactions.Utilities;
|
||||
using Discord.Utils;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
@@ -12,6 +14,7 @@ internal sealed class DefaultArrayModalComponentConverter<T> : ModalComponentTyp
|
||||
private readonly Type _underlyingType;
|
||||
private readonly TypeReader _typeReader;
|
||||
private readonly ImmutableArray<ChannelType> _channelTypes;
|
||||
private readonly ImmutableArray<EnumSelectMenuOption> _enumOptions;
|
||||
|
||||
public DefaultArrayModalComponentConverter(InteractionService interactionService)
|
||||
{
|
||||
@@ -56,13 +59,14 @@ internal sealed class DefaultArrayModalComponentConverter<T> : ModalComponentTyp
|
||||
=> [ChannelType.Forum],
|
||||
_ => []
|
||||
};
|
||||
|
||||
_enumOptions = _underlyingType!.IsEnum ? [..EnumUtils.BuildSelectMenuOptions(_underlyingType)] : ImmutableArray<EnumSelectMenuOption>.Empty;
|
||||
}
|
||||
|
||||
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||
{
|
||||
var objs = new List<object>();
|
||||
|
||||
|
||||
if (_typeReader is not null && option.Values.Count > 0)
|
||||
foreach (var value in option.Values)
|
||||
{
|
||||
@@ -77,7 +81,7 @@ internal sealed class DefaultArrayModalComponentConverter<T> : ModalComponentTyp
|
||||
{
|
||||
if (!TryGetModalInteractionData(context, out var modalData))
|
||||
{
|
||||
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{typeof(IModalInteractionData).Name} cannot be accessed from the provided {typeof(IInteractionContext).Name} type.");
|
||||
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{nameof(IModalInteractionData)} cannot be accessed from the provided {nameof(IInteractionContext)} type.");
|
||||
}
|
||||
|
||||
var resolvedSnowflakes = new Dictionary<ulong, ISnowflakeEntity>();
|
||||
@@ -134,49 +138,44 @@ internal sealed class DefaultArrayModalComponentConverter<T> : ModalComponentTyp
|
||||
if (builder is not SelectMenuBuilder selectMenu || !component.ComponentType.IsSelectType())
|
||||
throw new InvalidOperationException($"Component type of the input {component.CustomId} of modal {component.Modal.Type.FullName} must be a select type.");
|
||||
|
||||
switch (value)
|
||||
if (!_enumOptions.IsEmpty)
|
||||
{
|
||||
case IUser user:
|
||||
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromUser(user));
|
||||
break;
|
||||
case IRole role:
|
||||
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromRole(role));
|
||||
break;
|
||||
case IChannel channel:
|
||||
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromChannel(channel));
|
||||
break;
|
||||
case IMentionable mentionable:
|
||||
selectMenu.WithDefaultValues(mentionable switch
|
||||
var visibleOptions = _enumOptions.Where(x => !x.Predicate?.Invoke(interaction) ?? true);
|
||||
|
||||
var enumValues = value is IEnumerable valueArr ? valueArr.Cast<Enum>().ToArray() : null;
|
||||
|
||||
foreach (var option in visibleOptions)
|
||||
{
|
||||
var optionBuilder = new SelectMenuOptionBuilder(option.MenuOption);
|
||||
|
||||
if (enumValues is not null)
|
||||
optionBuilder.IsDefault = enumValues.Contains(option.Value);
|
||||
|
||||
selectMenu.AddOption(optionBuilder);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
selectMenu.DefaultValues = value switch
|
||||
{
|
||||
IEnumerable<IUser> defaultUsers => defaultUsers.Select(SelectMenuDefaultValue.FromUser).ToList(),
|
||||
IEnumerable<IRole> defaultRoles => defaultRoles.Select(SelectMenuDefaultValue.FromRole).ToList(),
|
||||
IEnumerable<IChannel> defaultChannels =>
|
||||
defaultChannels.Select(SelectMenuDefaultValue.FromChannel).ToList(),
|
||||
IEnumerable<IMentionable> defaultMentionables => defaultMentionables
|
||||
.Select(x =>
|
||||
{
|
||||
IUser user => SelectMenuDefaultValue.FromUser(user),
|
||||
IRole role => SelectMenuDefaultValue.FromRole(role),
|
||||
IChannel channel => SelectMenuDefaultValue.FromChannel(channel),
|
||||
_ => throw new InvalidOperationException($"Mentionable select cannot be populated using an entity with type: {mentionable.GetType().FullName}")
|
||||
});
|
||||
break;
|
||||
case IEnumerable<IUser> defaultUsers:
|
||||
selectMenu.DefaultValues = defaultUsers.Select(SelectMenuDefaultValue.FromUser).ToList();
|
||||
break;
|
||||
case IEnumerable<IRole> defaultRoles:
|
||||
selectMenu.DefaultValues = defaultRoles.Select(SelectMenuDefaultValue.FromRole).ToList();
|
||||
break;
|
||||
case IEnumerable<IChannel> defaultChannels:
|
||||
selectMenu.DefaultValues = defaultChannels.Select(SelectMenuDefaultValue.FromChannel).ToList();
|
||||
break;
|
||||
case IEnumerable<IMentionable> defaultMentionables:
|
||||
selectMenu.DefaultValues = defaultMentionables.Where(x => x is IUser or IRole or IChannel)
|
||||
.Select(x =>
|
||||
return x switch
|
||||
{
|
||||
return x switch
|
||||
{
|
||||
IUser user => SelectMenuDefaultValue.FromUser(user),
|
||||
IRole role => SelectMenuDefaultValue.FromRole(role),
|
||||
IChannel channel => SelectMenuDefaultValue.FromChannel(channel),
|
||||
_ => throw new InvalidOperationException($"Mentionable select cannot be populated using an entity with type: {x.GetType().FullName}")
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
break;
|
||||
IUser user => SelectMenuDefaultValue.FromUser(user),
|
||||
IRole role => SelectMenuDefaultValue.FromRole(role),
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Mentionable select cannot be populated using an entity with type: {x.GetType().FullName}")
|
||||
};
|
||||
})
|
||||
.ToList(),
|
||||
_ => selectMenu.DefaultValues
|
||||
};
|
||||
|
||||
if (component.ComponentType == ComponentType.ChannelSelect && _channelTypes.Length > 0)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -16,7 +17,7 @@ internal abstract class DefaultSnowflakeModalComponentConverter<T> : ModalCompon
|
||||
|
||||
if (!TryGetModalInteractionData(context, out modalData))
|
||||
{
|
||||
preemptiveResult = TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{typeof(IModalInteractionData).Name} cannot be accessed from the provided {typeof(IInteractionContext).Name} type.");
|
||||
preemptiveResult = TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{nameof(IModalInteractionData)} cannot be accessed from the provided {nameof(IInteractionContext)} type.");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -51,52 +52,47 @@ internal abstract class DefaultSnowflakeModalComponentConverter<T> : ModalCompon
|
||||
internal class DefaultAttachmentModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||
where T : class, IAttachment
|
||||
{
|
||||
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||
{
|
||||
if (TryGetPreemptiveResult(context, option, ComponentType.FileUpload, out var result, out var modalData, out var id))
|
||||
{
|
||||
return result;
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
var resolvedEntity = modalData.Attachments.FirstOrDefault(x => x.Id == id);
|
||||
|
||||
if (resolvedEntity is null)
|
||||
{
|
||||
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.");
|
||||
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved."));
|
||||
}
|
||||
|
||||
return TypeConverterResult.FromSuccess(resolvedEntity);
|
||||
return Task.FromResult(TypeConverterResult.FromSuccess(resolvedEntity));
|
||||
}
|
||||
}
|
||||
|
||||
internal class DefaultUserModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||
where T : class, IUser
|
||||
{
|
||||
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||
{
|
||||
if (TryGetPreemptiveResult(context, option, ComponentType.UserSelect, out var result, out var modalData, out var id))
|
||||
{
|
||||
return result;
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
var resolvedEntity = modalData.Members.UnionBy(modalData.Users, x => x.Id).FirstOrDefault(x => x.Id == id);
|
||||
|
||||
if (resolvedEntity is null)
|
||||
{
|
||||
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.");
|
||||
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved."));
|
||||
}
|
||||
|
||||
return TypeConverterResult.FromSuccess(resolvedEntity);
|
||||
return Task.FromResult(TypeConverterResult.FromSuccess(resolvedEntity));
|
||||
}
|
||||
|
||||
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.UserSelect)
|
||||
if (builder is not SelectMenuBuilder { Type: ComponentType.UserSelect } selectMenu)
|
||||
{
|
||||
throw new InvalidOperationException($"{typeof(DefaultUserModalComponentConverter<T>).Name} can only be used with User Select components.");
|
||||
}
|
||||
@@ -106,9 +102,14 @@ internal class DefaultUserModalComponentConverter<T> : DefaultSnowflakeModalComp
|
||||
throw new InvalidOperationException($"Multi-select User Select cannot be used with a single {typeof(T).Name} entity.");
|
||||
}
|
||||
|
||||
if(value is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (value is not IUser user)
|
||||
{
|
||||
throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default User Select values. Expected {typeof(IUser).Name}");
|
||||
throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default User Select values. Expected {nameof(IUser)}");
|
||||
}
|
||||
|
||||
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromUser(user));
|
||||
@@ -120,31 +121,26 @@ internal class DefaultUserModalComponentConverter<T> : DefaultSnowflakeModalComp
|
||||
internal class DefaultRoleModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||
where T : class, IRole
|
||||
{
|
||||
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||
{
|
||||
if (TryGetPreemptiveResult(context, option, ComponentType.RoleSelect, out var result, out var modalData, out var id))
|
||||
{
|
||||
return result;
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
var resolvedEntity = modalData.Roles.FirstOrDefault(x => x.Id == id);
|
||||
|
||||
if (resolvedEntity is null)
|
||||
{
|
||||
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.");
|
||||
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved."));
|
||||
}
|
||||
|
||||
return TypeConverterResult.FromSuccess(resolvedEntity);
|
||||
return Task.FromResult(TypeConverterResult.FromSuccess(resolvedEntity));
|
||||
}
|
||||
|
||||
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
||||
{
|
||||
if(value is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.RoleSelect)
|
||||
if (builder is not SelectMenuBuilder { Type: ComponentType.RoleSelect } selectMenu)
|
||||
{
|
||||
throw new InvalidOperationException($"{typeof(DefaultRoleModalComponentConverter<T>).Name} can only be used with Role Select components.");
|
||||
}
|
||||
@@ -154,9 +150,14 @@ internal class DefaultRoleModalComponentConverter<T> : DefaultSnowflakeModalComp
|
||||
throw new InvalidOperationException($"Multi-select Role Select cannot be used with a single {typeof(T).Name} entity.");
|
||||
}
|
||||
|
||||
if(value is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (value is not IRole role)
|
||||
{
|
||||
throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Role Select values. Expected {typeof(IRole).Name}");
|
||||
throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Role Select values. Expected {nameof(IRole)}");
|
||||
}
|
||||
|
||||
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromRole(role));
|
||||
@@ -168,43 +169,77 @@ internal class DefaultRoleModalComponentConverter<T> : DefaultSnowflakeModalComp
|
||||
internal class DefaultChannelModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||
where T : class, IChannel
|
||||
{
|
||||
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||
private readonly ImmutableArray<ChannelType> _channelTypes;
|
||||
|
||||
public DefaultChannelModalComponentConverter()
|
||||
{
|
||||
var type = typeof(T);
|
||||
|
||||
_channelTypes = true switch
|
||||
{
|
||||
_ when typeof(IStageChannel).IsAssignableFrom(type)
|
||||
=> [ChannelType.Stage],
|
||||
_ when typeof(IVoiceChannel).IsAssignableFrom(type)
|
||||
=> [ChannelType.Voice],
|
||||
_ when typeof(IDMChannel).IsAssignableFrom(type)
|
||||
=> [ChannelType.DM],
|
||||
_ when typeof(IGroupChannel).IsAssignableFrom(type)
|
||||
=> [ChannelType.Group],
|
||||
_ when typeof(ICategoryChannel).IsAssignableFrom(type)
|
||||
=> [ChannelType.Category],
|
||||
_ when typeof(INewsChannel).IsAssignableFrom(type)
|
||||
=> [ChannelType.News],
|
||||
_ when typeof(IThreadChannel).IsAssignableFrom(type)
|
||||
=> [ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.NewsThread],
|
||||
_ when typeof(ITextChannel).IsAssignableFrom(type)
|
||||
=> [ChannelType.Text],
|
||||
_ when typeof(IMediaChannel).IsAssignableFrom(type)
|
||||
=> [ChannelType.Media],
|
||||
_ when typeof(IForumChannel).IsAssignableFrom(type)
|
||||
=> [ChannelType.Forum],
|
||||
_ => []
|
||||
};
|
||||
}
|
||||
|
||||
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||
{
|
||||
if (TryGetPreemptiveResult(context, option, ComponentType.ChannelSelect, out var result, out var modalData, out var id))
|
||||
{
|
||||
return result;
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
var resolvedEntity = modalData.Channels.FirstOrDefault(x => x.Id == id);
|
||||
|
||||
if (resolvedEntity is null)
|
||||
{
|
||||
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.");
|
||||
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved."));
|
||||
}
|
||||
|
||||
return TypeConverterResult.FromSuccess(resolvedEntity);
|
||||
return Task.FromResult(TypeConverterResult.FromSuccess(resolvedEntity));
|
||||
}
|
||||
|
||||
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.ChannelSelect)
|
||||
if (builder is not SelectMenuBuilder { Type: ComponentType.ChannelSelect } selectMenu)
|
||||
{
|
||||
throw new InvalidOperationException($"{typeof(DefaultChannelModalComponentConverter<T>).Name} can only be used with Channel Select components.");
|
||||
}
|
||||
|
||||
selectMenu.WithChannelTypes(_channelTypes.ToList());
|
||||
|
||||
if(selectMenu.MaxValues > 1)
|
||||
{
|
||||
throw new InvalidOperationException($"Multi-select Channel Select cannot be used with a single {typeof(T).Name} entity.");
|
||||
}
|
||||
|
||||
if (value is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if(value is not IChannel channel)
|
||||
{
|
||||
throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Channel Select values. Expected {typeof(IChannel).Name}");
|
||||
throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Channel Select values. Expected {nameof(IChannel)}");
|
||||
}
|
||||
|
||||
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromChannel(channel));
|
||||
@@ -216,11 +251,11 @@ internal class DefaultChannelModalComponentConverter<T> : DefaultSnowflakeModalC
|
||||
internal class DefaultMentionableModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||
where T : class, IMentionable
|
||||
{
|
||||
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||
{
|
||||
if (TryGetPreemptiveResult(context, option, ComponentType.MentionableSelect, out var result, out var modalData, out var id))
|
||||
{
|
||||
return result;
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
var resolvedMentionables = new Dictionary<ulong, IMentionable>();
|
||||
@@ -236,20 +271,15 @@ internal class DefaultMentionableModalComponentConverter<T> : DefaultSnowflakeMo
|
||||
|
||||
if (resolvedMentionables.Count == 0 || !resolvedMentionables.TryGetValue(id, out var entity))
|
||||
{
|
||||
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.");
|
||||
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved."));
|
||||
}
|
||||
|
||||
return TypeConverterResult.FromSuccess(entity);
|
||||
return Task.FromResult(TypeConverterResult.FromSuccess(entity));
|
||||
}
|
||||
|
||||
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.MentionableSelect)
|
||||
if (builder is not SelectMenuBuilder { Type: ComponentType.MentionableSelect } selectMenu)
|
||||
{
|
||||
throw new InvalidOperationException($"{typeof(DefaultMentionableModalComponentConverter<T>).Name} can only be used with Mentionable Select components.");
|
||||
}
|
||||
@@ -259,11 +289,16 @@ internal class DefaultMentionableModalComponentConverter<T> : DefaultSnowflakeMo
|
||||
throw new InvalidOperationException($"Multi-select Mentionable Select cannot be used with a single {typeof(T).Name} entity.");
|
||||
}
|
||||
|
||||
if (value is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var defaultValue = value switch
|
||||
{
|
||||
IRole role => SelectMenuDefaultValue.FromRole(role),
|
||||
IUser user => SelectMenuDefaultValue.FromUser(user),
|
||||
_ => throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Mentionable Select values. Expected {typeof(IUser).Name} or {typeof(IRole).Name}")
|
||||
_ => throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Mentionable Select values. Expected {nameof(IUser)} or {nameof(IRole)}")
|
||||
};
|
||||
|
||||
selectMenu.WithDefaultValues(defaultValue);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Discord.Interactions.Utilities;
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
@@ -9,41 +10,18 @@ namespace Discord.Interactions;
|
||||
internal sealed class EnumModalComponentConverter<T> : ModalComponentTypeConverter<T>
|
||||
where T : struct, Enum
|
||||
{
|
||||
private record Option(SelectMenuOptionBuilder OptionBuilder, Predicate<IDiscordInteraction> Predicate, T Value);
|
||||
|
||||
private readonly bool _isFlags;
|
||||
private readonly ImmutableArray<Option> _options;
|
||||
private readonly ImmutableArray<EnumSelectMenuOption> _options;
|
||||
|
||||
public EnumModalComponentConverter()
|
||||
{
|
||||
var names = Enum.GetNames(typeof(T));
|
||||
var members = names.SelectMany(x => typeof(T).GetMember(x));
|
||||
|
||||
_isFlags = typeof(T).IsDefined(typeof(FlagsAttribute));
|
||||
|
||||
_options = members.Select<MemberInfo, Option>(x =>
|
||||
{
|
||||
var selectMenuOptionAttr = x.GetCustomAttribute<SelectMenuOptionAttribute>();
|
||||
|
||||
Emoji emoji = null;
|
||||
Emote emote = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(selectMenuOptionAttr?.Emote) && !(Emote.TryParse(selectMenuOptionAttr.Emote, out emote) || Emoji.TryParse(selectMenuOptionAttr.Emote, out emoji)))
|
||||
throw new ArgumentException($"Unable to parse {selectMenuOptionAttr.Emote} of {x.DeclaringType.Name}.{x.Name} into an {typeof(Emote).Name} or an {typeof(Emoji).Name}");
|
||||
|
||||
var hideAttr = x.GetCustomAttribute<HideAttribute>();
|
||||
Predicate<IDiscordInteraction> predicate = hideAttr != null ? hideAttr.Predicate : null;
|
||||
|
||||
var value = Enum.Parse<T>(x.Name);
|
||||
var optionBuilder = new SelectMenuOptionBuilder(x.GetCustomAttribute<ChoiceDisplayAttribute>()?.Name ?? x.Name, x.Name, selectMenuOptionAttr?.Description, emote != null ? emote : emoji, selectMenuOptionAttr?.IsDefault);
|
||||
|
||||
return new(optionBuilder, predicate, value);
|
||||
}).ToImmutableArray();
|
||||
_options = EnumUtils.BuildSelectMenuOptions(typeof(T)).ToImmutableArray();
|
||||
}
|
||||
|
||||
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||
{
|
||||
if (option.Type is not ComponentType.SelectMenu or ComponentType.TextInput)
|
||||
if (option.Type is not ComponentType.SelectMenu and not ComponentType.TextInput)
|
||||
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option.Type} input type cannot be converted to {typeof(T).FullName}"));
|
||||
|
||||
var value = option.Type switch
|
||||
@@ -69,44 +47,16 @@ internal sealed class EnumModalComponentConverter<T> : ModalComponentTypeConvert
|
||||
|
||||
var visibleOptions = _options.Where(x => !x.Predicate?.Invoke(interaction) ?? true);
|
||||
|
||||
if (value is T enumValue)
|
||||
foreach (var option in visibleOptions)
|
||||
{
|
||||
foreach(var option in visibleOptions)
|
||||
{
|
||||
option.OptionBuilder.IsDefault = _isFlags ? enumValue.HasFlag(option.Value) : enumValue.Equals(option.Value);
|
||||
}
|
||||
}
|
||||
var optionBuilder = new SelectMenuOptionBuilder(option.MenuOption);
|
||||
|
||||
selectMenu.WithOptions([.. visibleOptions.Select(x => x.OptionBuilder)]);
|
||||
if(value is T enumValue && option.Value is T optionValue)
|
||||
optionBuilder.IsDefault = _isFlags ? enumValue.HasFlag(optionValue) : enumValue.Equals(option.Value);
|
||||
|
||||
selectMenu.AddOption(optionBuilder);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds additional metadata to enum fields that are used for select-menus.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To manually add select menu options to modal components, use <see cref="ModalSelectMenuOptionAttribute"/> instead.
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
|
||||
public class SelectMenuOptionAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the desription of the option.
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the option is selected by default.
|
||||
/// </summary>
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the emote of the option.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Can be either an <see cref="Emoji"/> or an <see cref="Discord.Emote"/>
|
||||
/// </remarks>
|
||||
public string Emote { get; set; }
|
||||
}
|
||||
|
||||
43
src/Discord.Net.Interactions/Utilities/EnumUtils.cs
Normal file
43
src/Discord.Net.Interactions/Utilities/EnumUtils.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Discord.Interactions.Utilities;
|
||||
|
||||
internal record EnumSelectMenuOption(
|
||||
SelectMenuOption MenuOption,
|
||||
Predicate<IDiscordInteraction> Predicate,
|
||||
object Value);
|
||||
|
||||
internal class EnumUtils
|
||||
{
|
||||
public static IEnumerable<EnumSelectMenuOption> BuildSelectMenuOptions(Type enumType)
|
||||
{
|
||||
if(!enumType.IsEnum)
|
||||
throw new ArgumentException($"Type {enumType} is not an enum");
|
||||
|
||||
var names = Enum.GetNames(enumType);
|
||||
var members = names.SelectMany(x => enumType.GetMember(x));
|
||||
|
||||
foreach (var member in members)
|
||||
{
|
||||
var selectMenuOptionAttr = member.GetCustomAttribute<SelectMenuOptionAttribute>();
|
||||
|
||||
Emoji emoji = null;
|
||||
Emote emote = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(selectMenuOptionAttr?.Emote) && !(Emote.TryParse(selectMenuOptionAttr.Emote, out emote) || Emoji.TryParse(selectMenuOptionAttr.Emote, out emoji)))
|
||||
throw new ArgumentException($"Unable to parse {selectMenuOptionAttr.Emote} of {member.DeclaringType.Name}.{member.Name} into an {nameof(Emote)} or an {nameof(Emoji)}");
|
||||
|
||||
var hideAttr = member.GetCustomAttribute<HideAttribute>();
|
||||
Predicate<IDiscordInteraction> predicate = hideAttr != null ? hideAttr.Predicate : null;
|
||||
|
||||
var value = Enum.Parse(enumType, member.Name);
|
||||
var optionBuilder = new SelectMenuOptionBuilder(member.GetCustomAttribute<ChoiceDisplayAttribute>()?.Name ?? member.Name,
|
||||
member.Name, selectMenuOptionAttr?.Description, emote != null ? emote : emoji, selectMenuOptionAttr?.IsDefault);
|
||||
|
||||
yield return new EnumSelectMenuOption(optionBuilder.Build(), predicate, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,9 @@ namespace Discord.Interactions
|
||||
public static bool TryRemove<T>(out ModalInfo modalInfo) where T : class, IModal
|
||||
=> TryRemove(typeof(T), out modalInfo);
|
||||
|
||||
public static bool Contains(Type type)
|
||||
=> _modalInfos.ContainsKey(type);
|
||||
|
||||
public static void Clear() => _modalInfos.Clear();
|
||||
|
||||
public static int Count() => _modalInfos.Count;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>Discord.Net</id>
|
||||
<version>3.18.0$suffix$</version>
|
||||
<version>3.19.0$suffix$</version>
|
||||
<title>Discord.Net</title>
|
||||
<authors>Discord.Net Contributors</authors>
|
||||
<owners>foxbot</owners>
|
||||
@@ -15,28 +15,28 @@
|
||||
<readme>NUGET_README.md</readme>
|
||||
<dependencies>
|
||||
<group targetFramework="net10.0">
|
||||
<dependency id="Discord.Net.Core" version="3.18.0$suffix$" />
|
||||
<dependency id="Discord.Net.Rest" version="3.18.0$suffix$" />
|
||||
<dependency id="Discord.Net.WebSocket" version="3.18.0$suffix$" />
|
||||
<dependency id="Discord.Net.Commands" version="3.18.0$suffix$" />
|
||||
<dependency id="Discord.Net.Webhook" version="3.18.0$suffix$" />
|
||||
<dependency id="Discord.Net.Interactions" version="3.18.0$suffix$" />
|
||||
<dependency id="Discord.Net.Core" version="3.19.0$suffix$" />
|
||||
<dependency id="Discord.Net.Rest" version="3.19.0$suffix$" />
|
||||
<dependency id="Discord.Net.WebSocket" version="3.19.0$suffix$" />
|
||||
<dependency id="Discord.Net.Commands" version="3.19.0$suffix$" />
|
||||
<dependency id="Discord.Net.Webhook" version="3.19.0$suffix$" />
|
||||
<dependency id="Discord.Net.Interactions" version="3.19.0$suffix$" />
|
||||
</group>
|
||||
<group targetFramework="net9.0">
|
||||
<dependency id="Discord.Net.Core" version="3.18.0$suffix$" />
|
||||
<dependency id="Discord.Net.Rest" version="3.18.0$suffix$" />
|
||||
<dependency id="Discord.Net.WebSocket" version="3.18.0$suffix$" />
|
||||
<dependency id="Discord.Net.Commands" version="3.18.0$suffix$" />
|
||||
<dependency id="Discord.Net.Webhook" version="3.18.0$suffix$" />
|
||||
<dependency id="Discord.Net.Interactions" version="3.18.0$suffix$" />
|
||||
<dependency id="Discord.Net.Core" version="3.19.0$suffix$" />
|
||||
<dependency id="Discord.Net.Rest" version="3.19.0$suffix$" />
|
||||
<dependency id="Discord.Net.WebSocket" version="3.19.0$suffix$" />
|
||||
<dependency id="Discord.Net.Commands" version="3.19.0$suffix$" />
|
||||
<dependency id="Discord.Net.Webhook" version="3.19.0$suffix$" />
|
||||
<dependency id="Discord.Net.Interactions" version="3.19.0$suffix$" />
|
||||
</group>
|
||||
<group targetFramework="net8.0">
|
||||
<dependency id="Discord.Net.Core" version="3.18.0$suffix$" />
|
||||
<dependency id="Discord.Net.Rest" version="3.18.0$suffix$" />
|
||||
<dependency id="Discord.Net.WebSocket" version="3.18.0$suffix$" />
|
||||
<dependency id="Discord.Net.Commands" version="3.18.0$suffix$" />
|
||||
<dependency id="Discord.Net.Webhook" version="3.18.0$suffix$" />
|
||||
<dependency id="Discord.Net.Interactions" version="3.18.0$suffix$" />
|
||||
<dependency id="Discord.Net.Core" version="3.19.0$suffix$" />
|
||||
<dependency id="Discord.Net.Rest" version="3.19.0$suffix$" />
|
||||
<dependency id="Discord.Net.WebSocket" version="3.19.0$suffix$" />
|
||||
<dependency id="Discord.Net.Commands" version="3.19.0$suffix$" />
|
||||
<dependency id="Discord.Net.Webhook" version="3.19.0$suffix$" />
|
||||
<dependency id="Discord.Net.Interactions" version="3.19.0$suffix$" />
|
||||
</group>
|
||||
</dependencies>
|
||||
</metadata>
|
||||
|
||||
@@ -98,6 +98,7 @@ namespace Discord
|
||||
AssertFlag(() => new ChannelPermissions(sendPolls: true), ChannelPermission.SendPolls);
|
||||
AssertFlag(() => new ChannelPermissions(useExternalApps: true), ChannelPermission.UseExternalApps);
|
||||
AssertFlag(() => new ChannelPermissions(pinMessages: true), ChannelPermission.PinMessages);
|
||||
AssertFlag(() => new ChannelPermissions(bypassSlowmode: true), ChannelPermission.BypassSlowmode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -169,6 +170,7 @@ namespace Discord
|
||||
AssertUtil(ChannelPermission.UseExternalApps, x => x.UserExternalApps, (p, enable) => p.Modify(useExternalApps: enable));
|
||||
AssertUtil(ChannelPermission.UseExternalSounds, x => x.UseExternalSounds, (p, enable) => p.Modify(useExternalSounds: enable));
|
||||
AssertUtil(ChannelPermission.PinMessages, x => x.PinMessages, (p, enable) => p.Modify(pinMessages: enable));
|
||||
AssertUtil(ChannelPermission.BypassSlowmode, x => x.BypassSlowmode, (p, enable) => p.Modify(bypassSlowmode: enable));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -110,6 +110,7 @@ namespace Discord
|
||||
AssertFlag(() => new GuildPermissions(useExternalSounds: true), GuildPermission.UseExternalSounds);
|
||||
AssertFlag(() => new GuildPermissions(createEvents: true), GuildPermission.CreateEvents);
|
||||
AssertFlag(() => new GuildPermissions(pinMessages: true), GuildPermission.PinMessages);
|
||||
AssertFlag(() => new GuildPermissions(bypassSlowmode: true), GuildPermission.BypassSlowmode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -199,6 +200,7 @@ namespace Discord
|
||||
AssertUtil(GuildPermission.UseExternalSounds, x => x.UserExternalSounds, (p, enable) => p.Modify(useExternalSounds: enable));
|
||||
AssertUtil(GuildPermission.CreateEvents, x => x.CreateEvents, (p, enable) => p.Modify(createEvents: enable));
|
||||
AssertUtil(GuildPermission.PinMessages, x => x.PinMessages, (p, enable) => p.Modify(pinMessages: enable));
|
||||
AssertUtil(GuildPermission.BypassSlowmode, x => x.BypassSlowmode, (p, enable) => p.Modify(bypassSlowmode: enable));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user