From 627eadf9487077c78fbcdb0d33577411968db3a9 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 17 Sep 2025 19:15:30 +0200 Subject: [PATCH 01/59] add interaction service attributes new modal input types --- .../Modals/ModalChannelSelectInputAttribute.cs | 10 ++++++++++ .../Modals/ModalMentionableSelectInputAttribute.cs | 10 ++++++++++ .../Modals/ModalRoleSelectInputAttribute.cs | 10 ++++++++++ .../Modals/ModalSelectMenuInputAttribute.cs | 11 +++++++++++ .../Modals/ModalUserSelectInputAttribute.cs | 10 ++++++++++ .../Attributes/Modals/SelectInputAttribute.cs | 14 ++++++++++++++ 6 files changed, 65 insertions(+) create mode 100644 src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectInputAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectInputAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectInputAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuInputAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectInputAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/Modals/SelectInputAttribute.cs diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectInputAttribute.cs new file mode 100644 index 0000000000..263518e04c --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectInputAttribute.cs @@ -0,0 +1,10 @@ +namespace Discord.Interactions.Attributes.Modals; + +public class ModalChannelSelectInputAttribute : SelectInputAttribute +{ + public override ComponentType ComponentType => ComponentType.ChannelSelect; + + public ModalChannelSelectInputAttribute(string customId) : base(customId) + { + } +} diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectInputAttribute.cs new file mode 100644 index 0000000000..f3088b53a6 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectInputAttribute.cs @@ -0,0 +1,10 @@ +namespace Discord.Interactions.Attributes.Modals; + +public class ModalMentionableSelectInputAttribute : SelectInputAttribute +{ + public override ComponentType ComponentType => ComponentType.MentionableSelect; + + public ModalMentionableSelectInputAttribute(string customId) : base(customId) + { + } +} diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectInputAttribute.cs new file mode 100644 index 0000000000..18e611b1f7 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectInputAttribute.cs @@ -0,0 +1,10 @@ +namespace Discord.Interactions.Attributes.Modals; + +public class ModalRoleSelectInputAttribute : SelectInputAttribute +{ + public override ComponentType ComponentType => ComponentType.RoleSelect; + + public ModalRoleSelectInputAttribute(string customId) : base(customId) + { + } +} diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuInputAttribute.cs new file mode 100644 index 0000000000..d5a17799b1 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuInputAttribute.cs @@ -0,0 +1,11 @@ +namespace Discord.Interactions.Attributes.Modals; + +public sealed class ModalSelectMenuInputAttribute : SelectInputAttribute +{ + public override ComponentType ComponentType => ComponentType.SelectMenu; + + public ModalSelectMenuInputAttribute(string customId) : base(customId) + { + + } +} diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectInputAttribute.cs new file mode 100644 index 0000000000..3299524cfc --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectInputAttribute.cs @@ -0,0 +1,10 @@ +namespace Discord.Interactions.Attributes.Modals; + +public class ModalUserSelectInputAttribute : SelectInputAttribute +{ + public override ComponentType ComponentType => ComponentType.UserSelect; + + public ModalUserSelectInputAttribute(string customId) : base(customId) + { + } +} diff --git a/src/Discord.Net.Interactions/Attributes/Modals/SelectInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/SelectInputAttribute.cs new file mode 100644 index 0000000000..7106125eb5 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Modals/SelectInputAttribute.cs @@ -0,0 +1,14 @@ +namespace Discord.Interactions.Attributes.Modals; + +public abstract class SelectInputAttribute : ModalInputAttribute +{ + public int MinValues { get; set; } = 1; + + public int MaxValues { get; set; } = 1; + + public string Placeholder { get; set; } + + public SelectInputAttribute(string customId) : base(customId) + { + } +} From 5d4d7fef40b1b993b6b4307e4d5065c8f294aa28 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 17 Sep 2025 19:17:53 +0200 Subject: [PATCH 02/59] add new modal component type converters --- .../DefaultArrayModalComponentConverter.cs | 170 ++++++++++++++++++ .../DefaultValueModalComponentConverter.cs | 26 +++ .../EnumModalComponentConverter.cs | 51 ++++++ .../ModalComponentTypeConverter.cs | 21 +++ .../NullableModalComponentConverter.cs | 23 +++ 5 files changed, 291 insertions(+) create mode 100644 src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs create mode 100644 src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs create mode 100644 src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs create mode 100644 src/Discord.Net.Interactions/TypeConverters/ModalComponents/ModalComponentTypeConverter.cs create mode 100644 src/Discord.Net.Interactions/TypeConverters/ModalComponents/NullableModalComponentConverter.cs diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs new file mode 100644 index 0000000000..5e8b2c02a3 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs @@ -0,0 +1,170 @@ +using Discord.Interactions.TypeConverters.ModalInputs; +using Discord.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Interactions.TypeConverters.ModalComponents; + +internal sealed class DefaultArrayModalComponentConverter : ModalComponentTypeConverter +{ + private readonly Type _underlyingType; + private readonly TypeReader _typeReader; + private readonly List _channelTypes; + + public DefaultArrayModalComponentConverter(InteractionService interactionService) + { + var type = typeof(T); + + if (!type.IsArray) + throw new InvalidOperationException($"{nameof(DefaultArrayComponentConverter)} cannot be used to convert a non-array type."); + + _underlyingType = typeof(T).GetElementType(); + + _typeReader = true switch + { + _ when typeof(IUser).IsAssignableFrom(_underlyingType) + || typeof(IChannel).IsAssignableFrom(_underlyingType) + || typeof(IMentionable).IsAssignableFrom(_underlyingType) + || typeof(IRole).IsAssignableFrom(_underlyingType) => null, + _ => interactionService.GetTypeReader(_underlyingType) + }; + + _channelTypes = true switch + { + _ when typeof(IStageChannel).IsAssignableFrom(type) + => new List { ChannelType.Stage }, + + _ when typeof(IVoiceChannel).IsAssignableFrom(type) + => new List { ChannelType.Voice }, + + _ when typeof(IDMChannel).IsAssignableFrom(type) + => new List { ChannelType.DM }, + + _ when typeof(IGroupChannel).IsAssignableFrom(type) + => new List { ChannelType.Group }, + + _ when typeof(ICategoryChannel).IsAssignableFrom(type) + => new List { ChannelType.Category }, + + _ when typeof(INewsChannel).IsAssignableFrom(type) + => new List { ChannelType.News }, + + _ when typeof(IThreadChannel).IsAssignableFrom(type) + => new List { ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.NewsThread }, + + _ when typeof(ITextChannel).IsAssignableFrom(type) + => new List { ChannelType.Text }, + + _ when typeof(IMediaChannel).IsAssignableFrom(type) + => new List { ChannelType.Media }, + + _ when typeof(IForumChannel).IsAssignableFrom(type) + => new List { ChannelType.Forum }, + + _ => null + }; + } + + public override async Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) + { + var objs = new List(); + + if (_typeReader is not null && option.Values.Count > 0) + foreach (var value in option.Values) + { + var result = await _typeReader.ReadAsync(context, value, services).ConfigureAwait(false); + + if (!result.IsSuccess) + return result; + + objs.Add(result.Value); + } + else + { + var users = new Dictionary(); + + if (option.Users is not null) + foreach (var user in option.Users) + users[user.Id] = user; + + if (option.Members is not null) + foreach (var member in option.Members) + users[member.Id] = member; + + objs.AddRange(users.Values); + + if (option.Roles is not null) + objs.AddRange(option.Roles); + + if (option.Channels is not null) + objs.AddRange(option.Channels); + } + + var destination = Array.CreateInstance(_underlyingType, objs.Count); + + for (var i = 0; i < objs.Count; i++) + destination.SetValue(objs[i], i); + + return TypeConverterResult.FromSuccess(destination); + } + + public override Task WriteAsync(TBuilder builder, InputComponentInfo component, object value) + { + 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) + { + 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 + { + 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 defaultUsers: + selectMenu.DefaultValues = defaultUsers.Select(x => SelectMenuDefaultValue.FromUser(x)).ToList(); + break; + case IEnumerable defaultRoles: + selectMenu.DefaultValues = defaultRoles.Select(x => SelectMenuDefaultValue.FromRole(x)).ToList(); + break; + case IEnumerable defaultChannels: + selectMenu.DefaultValues = defaultChannels.Select(x => SelectMenuDefaultValue.FromChannel(x)).ToList(); + break; + case IEnumerable defaultMentionables: + selectMenu.DefaultValues = defaultMentionables.Where(x => x is IUser or IRole or IChannel) + .Select(x => + { + 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; + }; + + + + if(component.ComponentType == ComponentType.ChannelSelect && _channelTypes is not null) + selectMenu.WithChannelTypes(_channelTypes); + + return Task.CompletedTask; + } +} diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs new file mode 100644 index 0000000000..8838102507 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs @@ -0,0 +1,26 @@ +using Discord.Interactions.TypeConverters.ModalInputs; +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions.TypeConverters.ModalComponents; + +internal sealed class DefaultValueModalComponentConverter : ModalComponentTypeConverter + where T : IConvertible +{ + public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) + { + try + { + return option.Type switch + { + ComponentType.SelectMenu => Task.FromResult(TypeConverterResult.FromSuccess(Convert.ChangeType(string.Join(",", option.Values), typeof(T)))), + ComponentType.TextInput => Task.FromResult(TypeConverterResult.FromSuccess(Convert.ChangeType(option.Value, typeof(T)))), + _ => Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option.Type} doesn't have a convertible value.")) + }; + } + catch (Exception ex) when (ex is FormatException or InvalidCastException) + { + return Task.FromResult(TypeConverterResult.FromError(ex)); + } + } +} diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs new file mode 100644 index 0000000000..06053cb853 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs @@ -0,0 +1,51 @@ +using Discord.Interactions.TypeConverters.ModalInputs; +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Discord.Interactions.TypeConverters.ModalComponents; + +internal sealed class EnumModalComponentConverter : ModalComponentTypeConverter + where T : struct, Enum +{ + private readonly ImmutableArray _options; + + public EnumModalComponentConverter() + { + var names = Enum.GetNames(typeof(T)); + var members = names.SelectMany(x => typeof(T).GetMember(x)).Where(x => !x.IsDefined(typeof(HideAttribute), true)); + + if (members.Count() > SelectMenuBuilder.MaxOptionCount) + throw new InvalidOperationException($"Enum type {typeof(T).FullName} has too many visible members to be used in a select menu. Maximum visible members is {SelectMenuBuilder.MaxOptionCount}, but {members.Count()} are visible."); + + _options = members.Select(x => + { + var selectMenuOptionAttr = x.GetCustomAttribute(); + return new SelectMenuOptionBuilder(x.GetCustomAttribute()?.Name ?? x.Name, x.Name, selectMenuOptionAttr?.Description, Emote.Parse(selectMenuOptionAttr?.Emote), selectMenuOptionAttr?.IsDefault); + }).ToImmutableArray(); + } + + public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) => throw new NotImplementedException(); + + public override Task WriteAsync(TBuilder builder, InputComponentInfo component, object value) + { + if (builder is not SelectMenuBuilder selectMenu || component.ComponentType is not ComponentType.SelectMenu) + throw new InvalidOperationException($"{nameof(EnumModalComponentConverter)} can only write to select menu components."); + + selectMenu.WithOptions(_options.ToList()); + + return Task.CompletedTask; + } +} + +[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] +public class SelectMenuOptionAttribute : Attribute +{ + public string Description { get; set; } + + public bool IsDefault { get; set; } + + public string Emote { get; set; } +} diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/ModalComponentTypeConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/ModalComponentTypeConverter.cs new file mode 100644 index 0000000000..485c7f5b10 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/ModalComponentTypeConverter.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions.TypeConverters.ModalInputs; +public abstract class ModalComponentTypeConverter : ITypeConverter +{ + public abstract bool CanConvertTo(Type type); + + public abstract Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services); + + public virtual Task WriteAsync(TBuilder builder, InputComponentInfo component, object value) + where TBuilder : class, IInteractableComponentBuilder + => Task.CompletedTask; +} + +public abstract class ModalComponentTypeConverter : ModalComponentTypeConverter +{ + /// + public sealed override bool CanConvertTo(Type type) => + typeof(T).IsAssignableFrom(type); +} diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/NullableModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/NullableModalComponentConverter.cs new file mode 100644 index 0000000000..64379f8ca3 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/NullableModalComponentConverter.cs @@ -0,0 +1,23 @@ +using Discord.Interactions.TypeConverters.ModalInputs; +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions.TypeConverters.ModalComponents; + +internal class NullableModalComponentConverter : ModalComponentTypeConverter +{ + private readonly ModalComponentTypeConverter _typeConverter; + + public NullableModalComponentConverter(InteractionService interactionService, IServiceProvider services) + { + var type = Nullable.GetUnderlyingType(typeof(T)); + + if (type is null) + throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type"); + + _typeConverter = interactionService.GetModalInputTypeConverter(type, services); + } + + public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) + => string.IsNullOrEmpty(option.Value) ? Task.FromResult(TypeConverterResult.FromSuccess(null)) : _typeConverter.ReadAsync(context, option, services); +} From 4e41461e7aea8a2f7884c56be0829d4fb7c02a95 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 17 Sep 2025 19:18:32 +0200 Subject: [PATCH 03/59] relocate hide attribute outside of slash command enum converter for general purpose use --- .../Attributes/HideAttribute.cs | 12 ++++++++++++ .../TypeConverters/SlashCommands/EnumConverter.cs | 9 --------- 2 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 src/Discord.Net.Interactions/Attributes/HideAttribute.cs diff --git a/src/Discord.Net.Interactions/Attributes/HideAttribute.cs b/src/Discord.Net.Interactions/Attributes/HideAttribute.cs new file mode 100644 index 0000000000..9e6f9a2835 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/HideAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace Discord.Interactions; + +/// +/// Enum values tagged with this attribute will not be displayed as a parameter choice +/// +/// +/// This attribute must be used along with the default +/// +[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)] +public sealed class HideAttribute : Attribute { } diff --git a/src/Discord.Net.Interactions/TypeConverters/SlashCommands/EnumConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/EnumConverter.cs index b95e859c03..3eb13b8321 100644 --- a/src/Discord.Net.Interactions/TypeConverters/SlashCommands/EnumConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/EnumConverter.cs @@ -43,13 +43,4 @@ public override void Write(ApplicationCommandOptionProperties properties, IParam } } } - - /// - /// Enum values tagged with this attribute will not be displayed as a parameter choice - /// - /// - /// This attribute must be used along with the default - /// - [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)] - public sealed class HideAttribute : Attribute { } } From 6e952003bbdb35632ada4d2efb33b1f5fcb518ef Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 17 Sep 2025 19:19:32 +0200 Subject: [PATCH 04/59] refactor base inputcomponentbuilder types to use modal component typeconverters --- .../Builders/Modals/Inputs/IInputComponentBuilder.cs | 7 ++++--- .../Builders/Modals/Inputs/InputComponentBuilder.cs | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs index 68c26fd037..e46d4ca1b3 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs @@ -1,3 +1,4 @@ +using Discord.Interactions.TypeConverters.ModalInputs; using System; using System.Collections.Generic; using System.Reflection; @@ -45,12 +46,12 @@ public interface IInputComponentBuilder PropertyInfo PropertyInfo { get; } /// - /// Get the assigned to this input. + /// Get the assigned to this input. /// - ComponentTypeConverter TypeConverter { get; } + ModalComponentTypeConverter TypeConverter { get; } /// - /// Gets the default value of this input component. + /// Gets the default value of this input component property. /// object DefaultValue { get; } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs index af0ab3a70e..8da3d07177 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs @@ -1,3 +1,4 @@ +using Discord.Interactions.TypeConverters.ModalInputs; using System; using System.Collections.Generic; using System.Reflection; @@ -38,7 +39,7 @@ public abstract class InputComponentBuilder : IInputComponentBu public PropertyInfo PropertyInfo { get; internal set; } /// - public ComponentTypeConverter TypeConverter { get; private set; } + public ModalComponentTypeConverter TypeConverter { get; private set; } /// public object DefaultValue { get; set; } @@ -102,7 +103,7 @@ public TBuilder SetIsRequired(bool isRequired) /// /// The builder instance. /// - public TBuilder WithComponentType(ComponentType componentType) + public virtual TBuilder WithComponentType(ComponentType componentType) { ComponentType = componentType; return Instance; @@ -118,7 +119,7 @@ public TBuilder WithComponentType(ComponentType componentType) public TBuilder WithType(Type type) { Type = type; - TypeConverter = Modal._interactionService.GetComponentTypeConverter(type); + TypeConverter = Modal._interactionService.GetModalInputTypeConverter(type); return Instance; } From 055fe777eb8ea1a19883ba8b78a67760dff089bb Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 17 Sep 2025 19:20:46 +0200 Subject: [PATCH 05/59] add builders for new modal input types --- .../ChannelSelectInputComponentBuilder.cs | 39 ++++++++ .../ISnowflakeSelectInputComponentBuilder.cs | 24 +++++ .../MentionableSelectInputComponentBuilder.cs | 37 ++++++++ .../Inputs/RoleSelectInputComponentBuilder.cs | 39 ++++++++ .../Inputs/SelectMenuInputComponentBuilder.cs | 42 +++++++++ .../SnowflakeSelectInputComponentBuilder.cs | 90 +++++++++++++++++++ .../Inputs/UserSelectInputComponentBuilder.cs | 39 ++++++++ 7 files changed, 310 insertions(+) create mode 100644 src/Discord.Net.Interactions/Builders/Modals/Inputs/ChannelSelectInputComponentBuilder.cs create mode 100644 src/Discord.Net.Interactions/Builders/Modals/Inputs/ISnowflakeSelectInputComponentBuilder.cs create mode 100644 src/Discord.Net.Interactions/Builders/Modals/Inputs/MentionableSelectInputComponentBuilder.cs create mode 100644 src/Discord.Net.Interactions/Builders/Modals/Inputs/RoleSelectInputComponentBuilder.cs create mode 100644 src/Discord.Net.Interactions/Builders/Modals/Inputs/SelectMenuInputComponentBuilder.cs create mode 100644 src/Discord.Net.Interactions/Builders/Modals/Inputs/SnowflakeSelectInputComponentBuilder.cs create mode 100644 src/Discord.Net.Interactions/Builders/Modals/Inputs/UserSelectInputComponentBuilder.cs diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/ChannelSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/ChannelSelectInputComponentBuilder.cs new file mode 100644 index 0000000000..3a5c52426d --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/ChannelSelectInputComponentBuilder.cs @@ -0,0 +1,39 @@ +using Discord.Interactions.Info.InputComponents; +using System.Collections.Generic; +using System.Linq; + +namespace Discord.Interactions.Builders.Modals.Inputs; + +public class ChannelSelectInputComponentBuilder : SnowflakeSelectInputComponentBuilder +{ + protected override ChannelSelectInputComponentBuilder Instance => this; + + public ChannelSelectInputComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.ChannelSelect) { } + + public ChannelSelectInputComponentBuilder AddDefaulValue(IChannel channel) + { + _defaultValues.Add(new SelectMenuDefaultValue(channel.Id, SelectDefaultValueType.Channel)); + return this; + } + + public ChannelSelectInputComponentBuilder AddDefaulValue(ulong channelId) + { + _defaultValues.Add(new SelectMenuDefaultValue(channelId, SelectDefaultValueType.Channel)); + return this; + } + + public ChannelSelectInputComponentBuilder AddDefaultValues(params IChannel[] channels) + { + _defaultValues.AddRange(channels.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Channel))); + return this; + } + + public ChannelSelectInputComponentBuilder AddDefaultValues(IEnumerable channels) + { + _defaultValues.AddRange(channels.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Channel))); + return this; + } + + internal override ChannelSelectInputComponentInfo Build(ModalInfo modal) + => new(this, modal); +} diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/ISnowflakeSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/ISnowflakeSelectInputComponentBuilder.cs new file mode 100644 index 0000000000..817415dc93 --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/ISnowflakeSelectInputComponentBuilder.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace Discord.Interactions.Builders.Modals.Inputs; + +public interface ISnowflakeSelectInputComponentBuilder : IInputComponentBuilder +{ + int MinValues { get; } + + int MaxValues { get; } + + string Placeholder { get; set; } + + IReadOnlyCollection DefaultValues { get; } + + SelectDefaultValueType? DefaultValuesType { get; } + + ISnowflakeSelectInputComponentBuilder AddDefaultValue(SelectMenuDefaultValue defaultValue); + + ISnowflakeSelectInputComponentBuilder WithMinValues(int minValues); + + ISnowflakeSelectInputComponentBuilder WithMaxValues(int maxValues); + + ISnowflakeSelectInputComponentBuilder WithPlaceholder(string placeholder); +} diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/MentionableSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/MentionableSelectInputComponentBuilder.cs new file mode 100644 index 0000000000..2122cef92b --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/MentionableSelectInputComponentBuilder.cs @@ -0,0 +1,37 @@ +using Discord.Interactions.Info.InputComponents; + +namespace Discord.Interactions.Builders.Modals.Inputs; + +public class MentionableSelectInputComponentBuilder : SnowflakeSelectInputComponentBuilder +{ + protected override MentionableSelectInputComponentBuilder Instance => this; + + public MentionableSelectInputComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.MentionableSelect) { } + + public MentionableSelectInputComponentBuilder AddDefaultValue(ulong id, SelectDefaultValueType type) + { + _defaultValues.Add(new SelectMenuDefaultValue(id, type)); + return this; + } + + public MentionableSelectInputComponentBuilder AddDefaultValue(IUser user) + { + _defaultValues.Add(new SelectMenuDefaultValue(user.Id, SelectDefaultValueType.User)); + return this; + } + + public MentionableSelectInputComponentBuilder AddDefaultValue(IChannel channel) + { + _defaultValues.Add(new SelectMenuDefaultValue(channel.Id, SelectDefaultValueType.Channel)); + return this; + } + + public MentionableSelectInputComponentBuilder AddDefaulValue(IRole role) + { + _defaultValues.Add(new SelectMenuDefaultValue(role.Id, SelectDefaultValueType.Role)); + return this; + } + + internal override MentionableSelectInputComponentInfo Build(ModalInfo modal) + => new(this, modal); +} diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/RoleSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/RoleSelectInputComponentBuilder.cs new file mode 100644 index 0000000000..e566ea3e6f --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/RoleSelectInputComponentBuilder.cs @@ -0,0 +1,39 @@ +using Discord.Interactions.Info.InputComponents; +using System.Collections.Generic; +using System.Linq; + +namespace Discord.Interactions.Builders.Modals.Inputs; + +public class RoleSelectInputComponentBuilder : SnowflakeSelectInputComponentBuilder +{ + protected override RoleSelectInputComponentBuilder Instance => this; + + public RoleSelectInputComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.RoleSelect) { } + + public RoleSelectInputComponentBuilder AddDefaulValue(IRole role) + { + _defaultValues.Add(new SelectMenuDefaultValue(role.Id, SelectDefaultValueType.Role)); + return this; + } + + public RoleSelectInputComponentBuilder AddDefaulValue(ulong roleId) + { + _defaultValues.Add(new SelectMenuDefaultValue(roleId, SelectDefaultValueType.Role)); + return this; + } + + public RoleSelectInputComponentBuilder AddDefaultValues(params IRole[] roles) + { + _defaultValues.AddRange(roles.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Role))); + return this; + } + + public RoleSelectInputComponentBuilder AddDefaultValues(IEnumerable roles) + { + _defaultValues.AddRange(roles.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Role))); + return this; + } + + internal override RoleSelectInputComponentInfo Build(ModalInfo modal) + => new(this, modal); +} diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/SelectMenuInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/SelectMenuInputComponentBuilder.cs new file mode 100644 index 0000000000..c75850b0fc --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/SelectMenuInputComponentBuilder.cs @@ -0,0 +1,42 @@ +using Discord.Interactions.Info.InputComponents; +using System; +using System.Collections.Generic; + +namespace Discord.Interactions.Builders; + +public class SelectMenuInputComponentBuilder : InputComponentBuilder +{ + private readonly List _options; + + protected override SelectMenuInputComponentBuilder Instance => this; + + public string Placeholder { get; set; } + + public int MinValues { get; set; } + + public int MaxValues { get; set; } + + public IReadOnlyCollection Options => _options; + + public SelectMenuInputComponentBuilder(ModalBuilder modal) : base(modal) + { + _options = new(); + } + + public SelectMenuInputComponentBuilder AddOption(SelectMenuOptionBuilder option) + { + _options.Add(option); + return this; + } + + public SelectMenuInputComponentBuilder AddOption(Action configure) + { + var builder = new SelectMenuOptionBuilder(); + configure(builder); + _options.Add(builder); + return this; + } + + internal override SelectMenuInputComponentInfo Build(ModalInfo modal) + => new(this, modal); +} diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/SnowflakeSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/SnowflakeSelectInputComponentBuilder.cs new file mode 100644 index 0000000000..e7c508f510 --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/SnowflakeSelectInputComponentBuilder.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; + +namespace Discord.Interactions.Builders.Modals.Inputs; + +public abstract class SnowflakeSelectInputComponentBuilder : InputComponentBuilder, ISnowflakeSelectInputComponentBuilder + where TInfo : InputComponentInfo + where TBuilder : InputComponentBuilder, ISnowflakeSelectInputComponentBuilder +{ + protected readonly List _defaultValues; + + public int MinValues { get; set; } = 1; + + public int MaxValues { get; set; } = 1; + + public string Placeholder { get; set; } + + public IReadOnlyCollection DefaultValues => _defaultValues.AsReadOnly(); + + public SelectDefaultValueType? DefaultValuesType + { + get + { + return ComponentType switch + { + ComponentType.UserSelect => SelectDefaultValueType.User, + ComponentType.RoleSelect => SelectDefaultValueType.Role, + ComponentType.ChannelSelect => SelectDefaultValueType.Channel, + ComponentType.MentionableSelect => null, + _ => throw new InvalidOperationException("Component type must be a snowflake select type."), + }; + } + } + + public SnowflakeSelectInputComponentBuilder(ModalBuilder modal, ComponentType componentType) : base(modal) + { + ValidateComponentType(componentType); + + ComponentType = componentType; + _defaultValues = new(); + } + + public TBuilder AddDefaultValue(SelectMenuDefaultValue defaultValue) + { + if (DefaultValuesType.HasValue && defaultValue.Type != DefaultValuesType.Value) + throw new ArgumentException($"Only default values with {Enum.GetName(typeof(SelectDefaultValueType), DefaultValuesType.Value)} are support by {nameof(TInfo)} select type.", nameof(defaultValue)); + + _defaultValues.Add(defaultValue); + return Instance; + } + + public override TBuilder WithComponentType(ComponentType componentType) + { + ValidateComponentType(componentType); + return base.WithComponentType(componentType); + } + + public TBuilder WithMinValues(int minValues) + { + MinValues = minValues; + return Instance; + } + + public TBuilder WithMaxValues(int maxValues) + { + MaxValues = maxValues; + return Instance; + } + + public TBuilder WithPlaceholder(string placeholder) + { + Placeholder = placeholder; + return Instance; + } + + private void ValidateComponentType(ComponentType componentType) + { + if (componentType is not ComponentType.UserSelect or ComponentType.RoleSelect or ComponentType.MentionableSelect or ComponentType.ChannelSelect) + throw new ArgumentException("Component type must be a snowflake select type.", nameof(componentType)); + + } + + ISnowflakeSelectInputComponentBuilder ISnowflakeSelectInputComponentBuilder.AddDefaultValue(SelectMenuDefaultValue defaultValue) => AddDefaultValue(defaultValue); + + ISnowflakeSelectInputComponentBuilder ISnowflakeSelectInputComponentBuilder.WithMinValues(int minValues) => WithMinValues(minValues); + + ISnowflakeSelectInputComponentBuilder ISnowflakeSelectInputComponentBuilder.WithMaxValues(int maxValues) => WithMaxValues(maxValues); + + ISnowflakeSelectInputComponentBuilder ISnowflakeSelectInputComponentBuilder.WithPlaceholder(string placeholder) => WithPlaceholder(placeholder); +} diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/UserSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/UserSelectInputComponentBuilder.cs new file mode 100644 index 0000000000..87180dbe53 --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/UserSelectInputComponentBuilder.cs @@ -0,0 +1,39 @@ +using Discord.Interactions.Info.InputComponents; +using System.Collections.Generic; +using System.Linq; + +namespace Discord.Interactions.Builders.Modals.Inputs; + +public class UserSelectInputComponentBuilder : SnowflakeSelectInputComponentBuilder +{ + protected override UserSelectInputComponentBuilder Instance => this; + + public UserSelectInputComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.UserSelect) { } + + public UserSelectInputComponentBuilder AddDefaulValue(IUser user) + { + _defaultValues.Add(new SelectMenuDefaultValue(user.Id, SelectDefaultValueType.User)); + return this; + } + + public UserSelectInputComponentBuilder AddDefaulValue(ulong userId) + { + _defaultValues.Add(new SelectMenuDefaultValue(userId, SelectDefaultValueType.User)); + return this; + } + + public UserSelectInputComponentBuilder AddDefaultValues(params IUser[] users) + { + _defaultValues.AddRange(users.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.User))); + return this; + } + + public UserSelectInputComponentBuilder AddDefaultValues(IEnumerable users) + { + _defaultValues.AddRange(users.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.User))); + return this; + } + + internal override UserSelectInputComponentInfo Build(ModalInfo modal) + => new(this, modal); +} From 297c3277b7678b371887a85089db3ade07a92a4a Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 17 Sep 2025 19:21:26 +0200 Subject: [PATCH 06/59] add component insertion methods to modal builder for new input types --- .../Builders/Modals/ModalBuilder.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs index 66aeadf75b..1d90134915 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs @@ -1,3 +1,4 @@ +using Discord.Interactions.Builders.Modals.Inputs; using System; using System.Collections.Generic; using System.Linq; @@ -80,6 +81,53 @@ public ModalBuilder AddTextComponent(Action configure return this; } + /// + /// Adds a select menu component to . + /// + /// Select menu component builder factory. + /// + /// The builder instance. + /// + public ModalBuilder AddSelectMenuComponent(Action configure) + { + var builder = new SelectMenuInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + + public ModalBuilder AddUserSelectComponent(Action configure) + { + var builder = new UserSelectInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + + public ModalBuilder AddRoleSelectComponent(Action configure) + { + var builder = new RoleSelectInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + + public ModalBuilder AddMentionableSelectComponent(Action configure) + { + var builder = new MentionableSelectInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + + public ModalBuilder AddChannelSelectComponent(Action configure) + { + var builder = new ChannelSelectInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + internal ModalInfo Build() => new(this); } } From 1d971c84c6557294e07682e84057af006d691c67 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 17 Sep 2025 19:22:04 +0200 Subject: [PATCH 07/59] refactor base inputcomponentinfo class to use modal component typeconverters --- .../Info/InputComponents/InputComponentInfo.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs index 23a0db8447..dc7fa02e08 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs @@ -1,3 +1,4 @@ +using Discord.Interactions.TypeConverters.ModalInputs; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -50,12 +51,12 @@ public abstract class InputComponentInfo public PropertyInfo PropertyInfo { get; } /// - /// Gets the assigned to this component. + /// Gets the assigned to this component. /// - public ComponentTypeConverter TypeConverter { get; } + public ModalComponentTypeConverter TypeConverter { get; } /// - /// Gets the default value of this component. + /// Gets the default value of this component property. /// public object DefaultValue { get; } From d773b4e2a8e254827a58637db77052a48f51bb83 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 17 Sep 2025 19:22:33 +0200 Subject: [PATCH 08/59] add component info classes for newly added modal input types --- .../ChannelSelectInputComponentInfo.cs | 8 ++++++ .../MentionableSelectInputComponentInfo.cs | 8 ++++++ .../RoleSelectInputComponentInfo.cs | 8 ++++++ .../SelectMenuInputComponentInfo.cs | 24 +++++++++++++++++ .../SnowflakeSelectInputComponentInfo.cs | 27 +++++++++++++++++++ .../UserSelectInputComponentInfo.cs | 8 ++++++ 6 files changed, 83 insertions(+) create mode 100644 src/Discord.Net.Interactions/Info/InputComponents/ChannelSelectInputComponentInfo.cs create mode 100644 src/Discord.Net.Interactions/Info/InputComponents/MentionableSelectInputComponentInfo.cs create mode 100644 src/Discord.Net.Interactions/Info/InputComponents/RoleSelectInputComponentInfo.cs create mode 100644 src/Discord.Net.Interactions/Info/InputComponents/SelectMenuInputComponentInfo.cs create mode 100644 src/Discord.Net.Interactions/Info/InputComponents/SnowflakeSelectInputComponentInfo.cs create mode 100644 src/Discord.Net.Interactions/Info/InputComponents/UserSelectInputComponentInfo.cs diff --git a/src/Discord.Net.Interactions/Info/InputComponents/ChannelSelectInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/ChannelSelectInputComponentInfo.cs new file mode 100644 index 0000000000..032a85f46f --- /dev/null +++ b/src/Discord.Net.Interactions/Info/InputComponents/ChannelSelectInputComponentInfo.cs @@ -0,0 +1,8 @@ +using Discord.Interactions.Builders.Modals.Inputs; + +namespace Discord.Interactions.Info.InputComponents; + +public class ChannelSelectInputComponentInfo : SnowflakeSelectInputComponentInfo +{ + public ChannelSelectInputComponentInfo(ChannelSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } +} diff --git a/src/Discord.Net.Interactions/Info/InputComponents/MentionableSelectInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/MentionableSelectInputComponentInfo.cs new file mode 100644 index 0000000000..3924384b73 --- /dev/null +++ b/src/Discord.Net.Interactions/Info/InputComponents/MentionableSelectInputComponentInfo.cs @@ -0,0 +1,8 @@ +using Discord.Interactions.Builders.Modals.Inputs; + +namespace Discord.Interactions.Info.InputComponents; + +public class MentionableSelectInputComponentInfo : SnowflakeSelectInputComponentInfo +{ + public MentionableSelectInputComponentInfo(MentionableSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } +} diff --git a/src/Discord.Net.Interactions/Info/InputComponents/RoleSelectInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/RoleSelectInputComponentInfo.cs new file mode 100644 index 0000000000..676d3ee01b --- /dev/null +++ b/src/Discord.Net.Interactions/Info/InputComponents/RoleSelectInputComponentInfo.cs @@ -0,0 +1,8 @@ +using Discord.Interactions.Builders.Modals.Inputs; + +namespace Discord.Interactions.Info.InputComponents; + +public class RoleSelectInputComponentInfo : SnowflakeSelectInputComponentInfo +{ + public RoleSelectInputComponentInfo(RoleSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } +} diff --git a/src/Discord.Net.Interactions/Info/InputComponents/SelectMenuInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/SelectMenuInputComponentInfo.cs new file mode 100644 index 0000000000..12bacf3ac2 --- /dev/null +++ b/src/Discord.Net.Interactions/Info/InputComponents/SelectMenuInputComponentInfo.cs @@ -0,0 +1,24 @@ +using Discord.Interactions.Builders; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Discord.Interactions.Info.InputComponents; +public class SelectMenuInputComponentInfo : InputComponentInfo +{ + public string Placeholder { get; set; } + + public int MinValues { get; set; } + + public int MaxValues { get; set; } + + public IReadOnlyCollection Options { get; } + + internal SelectMenuInputComponentInfo(SelectMenuInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) + { + Placeholder = builder.Placeholder; + MinValues = builder.MinValues; + MaxValues = builder.MaxValues; + Options = builder.Options.Select(x => x.Build()).ToImmutableArray(); + } +} diff --git a/src/Discord.Net.Interactions/Info/InputComponents/SnowflakeSelectInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/SnowflakeSelectInputComponentInfo.cs new file mode 100644 index 0000000000..175e013f34 --- /dev/null +++ b/src/Discord.Net.Interactions/Info/InputComponents/SnowflakeSelectInputComponentInfo.cs @@ -0,0 +1,27 @@ +using Discord.Interactions.Builders.Modals.Inputs; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Discord.Interactions.Info.InputComponents; + +public abstract class SnowflakeSelectInputComponentInfo : InputComponentInfo +{ + public int MinValues { get; } + + public int MaxValues { get; } + + public string Placeholder { get; } + + public IReadOnlyCollection DefaultValues { get; } + + public SelectDefaultValueType? DefaultValueType { get; } + + internal SnowflakeSelectInputComponentInfo(ISnowflakeSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) + { + MinValues = builder.MinValues; + MaxValues = builder.MaxValues; + Placeholder = builder.Placeholder; + DefaultValues = builder.DefaultValues.ToImmutableArray(); + DefaultValueType = builder.DefaultValuesType; + } +} diff --git a/src/Discord.Net.Interactions/Info/InputComponents/UserSelectInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/UserSelectInputComponentInfo.cs new file mode 100644 index 0000000000..dd189084bb --- /dev/null +++ b/src/Discord.Net.Interactions/Info/InputComponents/UserSelectInputComponentInfo.cs @@ -0,0 +1,8 @@ +using Discord.Interactions.Builders.Modals.Inputs; + +namespace Discord.Interactions.Info.InputComponents; + +public class UserSelectInputComponentInfo : SnowflakeSelectInputComponentInfo +{ + public UserSelectInputComponentInfo(UserSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } +} From 3c920d435442577d9c3c04e4975c7622b06f26a5 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 17 Sep 2025 19:24:10 +0200 Subject: [PATCH 09/59] add build logic for new modal input metadata classes --- .../Builders/ModuleClassBuilder.cs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index 87aefa8f24..2c541ad1be 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -1,3 +1,6 @@ +using Discord.Interactions.Attributes.Modals; +using Discord.Interactions.Builders.Modals.Inputs; +using Discord.Interactions.Info.InputComponents; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -615,6 +618,21 @@ public static ModalInfo BuildModalInfo(Type modalType, InteractionService intera case ComponentType.TextInput: builder.AddTextComponent(x => BuildTextInput(x, prop, prop.GetValue(instance))); break; + case ComponentType.SelectMenu: + builder.AddSelectMenuComponent(x => BuildSelectMenuInput(x, prop, prop.GetValue(instance))); + break; + case ComponentType.UserSelect: + builder.AddUserSelectComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + break; + case ComponentType.RoleSelect: + builder.AddRoleSelectComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + break; + case ComponentType.MentionableSelect: + builder.AddMentionableSelectComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + break; + case ComponentType.ChannelSelect: + builder.AddChannelSelectComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + break; case null: throw new InvalidOperationException($"{prop.Name} of {prop.DeclaringType.Name} isn't a valid modal input field."); default: @@ -666,6 +684,74 @@ private static void BuildTextInput(TextInputComponentBuilder builder, PropertyIn } } } + + private static void BuildSelectMenuInput(SelectMenuInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) + { + var attributes = propertyInfo.GetCustomAttributes(); + + builder.Label = propertyInfo.Name; + builder.DefaultValue = defaultValue; + builder.WithType(propertyInfo.PropertyType); + builder.PropertyInfo = propertyInfo; + + foreach (var attribute in attributes) + { + switch (attribute) + { + case ModalSelectMenuInputAttribute selectMenuInput: + builder.CustomId = selectMenuInput.CustomId; + builder.ComponentType = selectMenuInput.ComponentType; + builder.MinValues = selectMenuInput.MinValues; + builder.MaxValues = selectMenuInput.MaxValues; + builder.Placeholder = selectMenuInput.Placeholder; + break; + case RequiredInputAttribute requiredInput: + builder.IsRequired = requiredInput.IsRequired; + break; + case InputLabelAttribute inputLabel: + builder.Label = inputLabel.Label; + break; + default: + builder.WithAttributes(attribute); + break; + } + } + } + + private static void BuildSnowflakeSelectInput(SnowflakeSelectInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) + where TInfo: SnowflakeSelectInputComponentInfo + where TBuilder: SnowflakeSelectInputComponentBuilder + { + var attributes = propertyInfo.GetCustomAttributes(); + + builder.Label = propertyInfo.Name; + builder.DefaultValue = defaultValue; + builder.WithType(propertyInfo.PropertyType); + builder.PropertyInfo = propertyInfo; + + foreach (var attribute in attributes) + { + switch (attribute) + { + case SelectInputAttribute selectInput: + builder.CustomId = selectInput.CustomId; + builder.ComponentType = selectInput.ComponentType; + builder.MinValues = selectInput.MinValues; + builder.MaxValues = selectInput.MaxValues; + builder.Placeholder = selectInput.Placeholder; + break; + case RequiredInputAttribute requiredInput: + builder.IsRequired = requiredInput.IsRequired; + break; + case InputLabelAttribute inputLabel: + builder.Label = inputLabel.Label; + break; + default: + builder.WithAttributes(attribute); + break; + } + } + } #endregion internal static bool IsValidModuleDefinition(TypeInfo typeInfo) From 73004bb35a62bcd2cbbbeac951718f9bdd56abb4 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 17 Sep 2025 19:25:15 +0200 Subject: [PATCH 10/59] add componet collection properties for new modal input types to modalinfo --- .../Info/ModalInfo.cs | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Interactions/Info/ModalInfo.cs b/src/Discord.Net.Interactions/Info/ModalInfo.cs index bef789ac9d..93e6c14449 100644 --- a/src/Discord.Net.Interactions/Info/ModalInfo.cs +++ b/src/Discord.Net.Interactions/Info/ModalInfo.cs @@ -1,3 +1,6 @@ +using Discord.Interactions.Builders; +using Discord.Interactions.Builders.Modals.Inputs; +using Discord.Interactions.Info.InputComponents; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -43,17 +46,40 @@ public class ModalInfo /// public IReadOnlyCollection TextComponents { get; } + /// + /// Get a collection of the select menu components of this modal. + /// + public IReadOnlyCollection SelectMenuComponents { get; } + + public IReadOnlyCollection UserSelectComponents { get; } + + public IReadOnlyCollection RoleSelectComponents { get; } + + public IReadOnlyCollection MentionableSelectComponents { get; } + + public IReadOnlyCollection ChannelSelectComponents { get; } + internal ModalInfo(Builders.ModalBuilder builder) { Title = builder.Title; Type = builder.Type; - Components = builder.Components.Select(x => x switch + Components = builder.Components.Select(x => x switch { - Builders.TextInputComponentBuilder textComponent => textComponent.Build(this), + TextInputComponentBuilder textComponent => textComponent.Build(this), + SelectMenuInputComponentBuilder selectMenuComponent => selectMenuComponent.Build(this), + RoleSelectInputComponentBuilder roleSelectComponent => roleSelectComponent.Build(this), + ChannelSelectInputComponentBuilder channelSelectComponent => channelSelectComponent.Build(this), + UserSelectInputComponentBuilder userSelectComponent => userSelectComponent.Build(this), + MentionableSelectInputComponentBuilder mentionableSelectComponent => mentionableSelectComponent.Build(this), _ => throw new InvalidOperationException($"{x.GetType().FullName} isn't a supported modal input component builder type.") }).ToImmutableArray(); TextComponents = Components.OfType().ToImmutableArray(); + SelectMenuComponents = Components.OfType().ToImmutableArray(); + UserSelectComponents = Components.OfType().ToImmutableArray(); + RoleSelectComponents = Components.OfType().ToImmutableArray(); + MentionableSelectComponents = Components.OfType().ToImmutableArray(); + ChannelSelectComponents = Components.OfType().ToImmutableArray(); _interactionService = builder._interactionService; _initializer = builder.ModalInitializer; From f6256e5be30efa45baaa0bb5f2b364ab4a3e8175 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 17 Sep 2025 19:26:23 +0200 Subject: [PATCH 11/59] implement convertion logic for new modal inputs to the respond with modal extension methods --- .../IDiscordInteractionExtensions.cs | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs index 85f53af3f5..a2e9ca669c 100644 --- a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs +++ b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs @@ -1,4 +1,6 @@ +using Discord.Interactions.Info.InputComponents; using System; +using System.Linq; using System.Threading.Tasks; namespace Discord.Interactions @@ -54,7 +56,7 @@ public static Task RespondWithModalAsync(this IDiscordInteraction interaction /// The request options for this request. /// Delegate that can be used to modify the modal. /// - public static Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, T modal, RequestOptions options = null, + public static async Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, T modal, RequestOptions options = null, Action modifyModal = null) where T : class, IModal { @@ -68,13 +70,30 @@ public static Task RespondWithModalAsync(this IDiscordInteraction interaction { case TextInputComponentInfo textComponent: { - var boxedValue = textComponent.Getter(modal); - var value = textComponent.TypeOverridesToString - ? boxedValue?.ToString() - : boxedValue as string; + var inputBuilder = new TextInputBuilder(textComponent.Label, textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null, + textComponent.MaxLength, textComponent.IsRequired); - builder.AddTextInput(textComponent.Label, textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null, - textComponent.MaxLength, textComponent.IsRequired, value); + await textComponent.TypeConverter.WriteAsync(inputBuilder, textComponent, textComponent.Getter(modal)); + + builder.AddTextInput(inputBuilder); + } + break; + case SelectMenuInputComponentInfo selectMenuComponent: + { + var inputBuilder = new SelectMenuBuilder(selectMenuComponent.CustomId, selectMenuComponent.Options.Select(x => new SelectMenuOptionBuilder(x)).ToList(), selectMenuComponent.Placeholder, selectMenuComponent.MaxValues, selectMenuComponent.MinValues, false); + + await selectMenuComponent.TypeConverter.WriteAsync(inputBuilder, selectMenuComponent, selectMenuComponent.Getter(modal)); + + //todo: add to builder + } + break; + case SnowflakeSelectInputComponentInfo snowflakeSelectComponent: + { + var inputBuilder = new SelectMenuBuilder(snowflakeSelectComponent.CustomId, null, snowflakeSelectComponent.Placeholder, snowflakeSelectComponent.MaxValues, snowflakeSelectComponent.MinValues, false, snowflakeSelectComponent.ComponentType, null, snowflakeSelectComponent.DefaultValues.ToList()); + + await snowflakeSelectComponent.TypeConverter.WriteAsync(inputBuilder, snowflakeSelectComponent, snowflakeSelectComponent.Getter(modal)); + + //todo: add to builder } break; default: @@ -84,7 +103,7 @@ public static Task RespondWithModalAsync(this IDiscordInteraction interaction if (modifyModal is not null) modifyModal(builder); - return interaction.RespondWithModalAsync(builder.Build(), options); + await interaction.RespondWithModalAsync(builder.Build(), options); } private static Task SendModalResponseAsync(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, RequestOptions options = null, Action modifyModal = null) From 651329a379cc949c492e88a0a840ea16e032f12e Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 17 Sep 2025 19:27:06 +0200 Subject: [PATCH 12/59] implement modal input typeconverters into interaction service --- .../InteractionService.cs | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index 3089aa5846..c12cbe486a 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -1,7 +1,10 @@ using Discord.Interactions.Builders; +using Discord.Interactions.TypeConverters.ModalComponents; +using Discord.Interactions.TypeConverters.ModalInputs; using Discord.Logging; using Discord.Rest; using Discord.WebSocket; +using Newtonsoft.Json.Bson; using System; using System.Collections; using System.Collections.Concurrent; @@ -98,6 +101,7 @@ public event Func InteractionE private readonly TypeMap _typeConverterMap; private readonly TypeMap _compTypeConverterMap; private readonly TypeMap _typeReaderMap; + private readonly TypeMap _modalInputTypeConverterMap; private readonly ConcurrentDictionary _autocompleteHandlers = new(); private readonly ConcurrentDictionary _modalInfos = new(); private readonly SemaphoreSlim _lock; @@ -228,6 +232,16 @@ private InteractionService(Func getRestClient, InteractionSer [typeof(Enum)] = typeof(EnumReader<>), [typeof(Nullable<>)] = typeof(NullableReader<>) }); + + _modalInputTypeConverterMap = new TypeMap(this, new ConcurrentDictionary + { + }, new ConcurrentDictionary + { + [typeof(IConvertible)] = typeof(DefaultValueModalComponentConverter<>), + [typeof(Enum)] = typeof(EnumModalComponentConverter<>), + [typeof(Nullable<>)] = typeof(NullableComponentConverter<>), + [typeof(Array)] = typeof(DefaultArrayModalComponentConverter<>) + }); } /// @@ -1064,6 +1078,94 @@ public bool TryRemoveGenericTypeReader(out Type readerType) public bool TryRemoveGenericTypeReader(Type type, out Type readerType) => _typeReaderMap.TryRemoveGeneric(type, out readerType); + internal ModalComponentTypeConverter GetModalInputTypeConverter(Type type, IServiceProvider services = null) => + _modalInputTypeConverterMap.Get(type, services); + + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddModalComponentTypeConverter(ModalComponentTypeConverter converter) => + AddModalComponentTypeConverter(typeof(T), converter); + + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddModalComponentTypeConverter(Type type, ModalComponentTypeConverter converter) => + _modalInputTypeConverterMap.AddConcrete(type, converter); + + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericModalComponentTypeConverter(Type converterType) => + AddGenericModalComponentTypeConverter(typeof(T), converterType); + + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericModalComponentTypeConverter(Type targetType, Type converterType) => + _modalInputTypeConverterMap.AddGeneric(targetType, converterType); + + /// + /// Removes a for the type . + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the converter from. + /// The converter if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveModalComponentTypeConverter(out ModalComponentTypeConverter converter) => + TryRemoveModalComponentTypeConverter(typeof(T), out converter); + + /// + /// Removes a for the type . + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the converter from. + /// The converter if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveModalComponentTypeConverter(Type type, out ModalComponentTypeConverter converter) => + _modalInputTypeConverterMap.TryRemoveConcrete(type, out converter); + + /// + /// Removes a generic for the type . + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the converter from. + /// The converter if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveGenericModalComponentTypeConverter(out Type converterType) => + TryRemoveGenericModalComponentTypeConverter(typeof(T), out converterType); + + /// + /// Removes a generic for the type . + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the converter from. + /// The converter if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveGenericModalComponentTypeConverter(Type type, out Type converterType) => + _modalInputTypeConverterMap.TryRemoveGeneric(type, out converterType); + + /// /// Serialize an object using a into a to be placed in a Component CustomId. /// From 1de62c073157009def5c9503145ca2f1d3f4afee Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Thu, 18 Sep 2025 00:58:52 +0200 Subject: [PATCH 13/59] add read logic to enum modal component typeconverter --- .../EnumModalComponentConverter.cs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs index 06053cb853..1bf53e8a2b 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs @@ -10,6 +10,7 @@ namespace Discord.Interactions.TypeConverters.ModalComponents; internal sealed class EnumModalComponentConverter : ModalComponentTypeConverter where T : struct, Enum { + private readonly bool _isFlags; private readonly ImmutableArray _options; public EnumModalComponentConverter() @@ -20,6 +21,8 @@ public EnumModalComponentConverter() if (members.Count() > SelectMenuBuilder.MaxOptionCount) throw new InvalidOperationException($"Enum type {typeof(T).FullName} has too many visible members to be used in a select menu. Maximum visible members is {SelectMenuBuilder.MaxOptionCount}, but {members.Count()} are visible."); + _isFlags = typeof(T).GetCustomAttribute() is not null; + _options = members.Select(x => { var selectMenuOptionAttr = x.GetCustomAttribute(); @@ -27,13 +30,32 @@ public EnumModalComponentConverter() }).ToImmutableArray(); } - public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) => throw new NotImplementedException(); + public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) + { + if(option.Type is not ComponentType.SelectMenu or 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 + { + ComponentType.SelectMenu => string.Join(",", option.Values), + ComponentType.TextInput => option.Value, + _ => null + }; + + if(Enum.TryParse(value, out var result)) + return Task.FromResult(TypeConverterResult.FromSuccess(result)); + + return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Value {option.Value} cannot be converted to {typeof(T).FullName}")); + } public override Task WriteAsync(TBuilder builder, InputComponentInfo component, object value) { if (builder is not SelectMenuBuilder selectMenu || component.ComponentType is not ComponentType.SelectMenu) throw new InvalidOperationException($"{nameof(EnumModalComponentConverter)} can only write to select menu components."); + if(selectMenu.MaxValues > 1 && !_isFlags) + throw new InvalidOperationException($"Enum type {typeof(T).FullName} is not a [Flags] enum, so it cannot be used in a multi-select menu."); + selectMenu.WithOptions(_options.ToList()); return Task.CompletedTask; From 9a59117636ef6f5fdb81c62ef10435808fbfe3a8 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Thu, 18 Sep 2025 00:59:13 +0200 Subject: [PATCH 14/59] add default entity modal component typeconverter --- .../DefaultEntityModalComponentConverter.cs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultEntityModalComponentConverter.cs diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultEntityModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultEntityModalComponentConverter.cs new file mode 100644 index 0000000000..952f43e6a7 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultEntityModalComponentConverter.cs @@ -0,0 +1,64 @@ +using Discord.Interactions.TypeConverters.ModalInputs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Interactions.TypeConverters.ModalComponents; + +internal sealed class DefaultEntityModalComponentConverter : ModalComponentTypeConverter + where T : class, ISnowflakeEntity +{ + public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) + { + var objs = new List(); + + var users = new Dictionary(); + + if (option.Users is not null) + foreach (var user in option.Users) + users[user.Id] = user; + + if (option.Members is not null) + foreach (var member in option.Members) + users[member.Id] = member; + + objs.AddRange(users.Values); + + if (option.Roles is not null) + objs.AddRange(option.Roles); + + if (option.Channels is not null) + objs.AddRange(option.Channels); + + if (objs.Count > 1) + return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Component input returned multiple entities, but {typeof(T).FullName} is not an array type.")); + + return Task.FromResult(TypeConverterResult.FromSuccess(objs.FirstOrDefault() as T)); + } + + public override Task WriteAsync(TBuilder builder, InputComponentInfo component, object value) + { + (ISnowflakeEntity Snowflake, SelectDefaultValueType Type) defaultValue = value switch + { + IUser user => (user, SelectDefaultValueType.User), + IRole role => (role, SelectDefaultValueType.Role), + IChannel channel => (channel, SelectDefaultValueType.Channel), + _ => throw new InvalidOperationException($"Only snowflake entities can be used to populate components using {nameof(DefaultEntityModalComponentConverter<>)}") + }; + + switch (builder) + { + case TextInputBuilder textInput: + textInput.WithValue(defaultValue.Snowflake.Id.ToString()); + break; + case SelectMenuBuilder selectMenu: + selectMenu.WithDefaultValues(new SelectMenuDefaultValue(defaultValue.Snowflake.Id, defaultValue.Type)); + break; + default: + throw new InvalidOperationException($"{builder.GetType().FullName} is not supported by {nameof(DefaultEntityModalComponentConverter<>)}"); + } + + return Task.CompletedTask; + } +} From fc95bb4c53171775c40d56e648d645d482761813 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Thu, 18 Sep 2025 00:59:41 +0200 Subject: [PATCH 15/59] add write logic to default value modal component typeconverter --- .../DefaultValueModalComponentConverter.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs index 8838102507..f655d794bf 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs @@ -1,5 +1,6 @@ using Discord.Interactions.TypeConverters.ModalInputs; using System; +using System.Linq; using System.Threading.Tasks; namespace Discord.Interactions.TypeConverters.ModalComponents; @@ -13,7 +14,7 @@ public override Task ReadAsync(IInteractionContext context, { return option.Type switch { - ComponentType.SelectMenu => Task.FromResult(TypeConverterResult.FromSuccess(Convert.ChangeType(string.Join(",", option.Values), typeof(T)))), + ComponentType.SelectMenu when option.Values.Count == 1 => Task.FromResult(TypeConverterResult.FromSuccess(Convert.ChangeType(option.Values.First(), typeof(T)))), ComponentType.TextInput => Task.FromResult(TypeConverterResult.FromSuccess(Convert.ChangeType(option.Value, typeof(T)))), _ => Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option.Type} doesn't have a convertible value.")) }; @@ -23,4 +24,23 @@ public override Task ReadAsync(IInteractionContext context, return Task.FromResult(TypeConverterResult.FromError(ex)); } } + + public override Task WriteAsync(TBuilder builder, InputComponentInfo component, object value) + { + var strValue = Convert.ToString(value); + + switch (builder) + { + case TextInputBuilder textInput: + textInput.WithValue(strValue); + break; + case SelectMenuBuilder selectMenu when component.ComponentType is ComponentType.SelectMenu: + selectMenu.Options.FirstOrDefault(x => x.Value == strValue)?.IsDefault = true; + break; + default: + throw new InvalidOperationException($"{nameof(IConvertible)}s cannot be used to populate components other than SelectMenu and TextInput."); + }; + + return Task.CompletedTask; + } } From 6c50876c39af2fcc9e2f7e4831a4fe06e34f9d84 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Thu, 18 Sep 2025 01:05:42 +0200 Subject: [PATCH 16/59] add write logic to the nullable modal component typeconverter --- .../ModalComponents/NullableModalComponentConverter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/NullableModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/NullableModalComponentConverter.cs index 64379f8ca3..b567316b69 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/NullableModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/NullableModalComponentConverter.cs @@ -20,4 +20,7 @@ public NullableModalComponentConverter(InteractionService interactionService, IS public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) => string.IsNullOrEmpty(option.Value) ? Task.FromResult(TypeConverterResult.FromSuccess(null)) : _typeConverter.ReadAsync(context, option, services); + + public override Task WriteAsync(TBuilder builder, InputComponentInfo component, object value) + => _typeConverter.WriteAsync(builder, component, value); } From 741aa51debd60cb12d44e89d1e88c2310524e47e Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Mon, 10 Nov 2025 09:58:22 +0100 Subject: [PATCH 17/59] add description property to input label attribute --- .../Attributes/Modals/InputLabelAttribute.cs | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/Discord.Net.Interactions/Attributes/Modals/InputLabelAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/InputLabelAttribute.cs index fdeb8c4144..ecbb44fd7d 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/InputLabelAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/InputLabelAttribute.cs @@ -1,25 +1,31 @@ using System; -namespace Discord.Interactions +namespace Discord.Interactions; + +/// +/// Creates a custom label for an modal input. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +public class InputLabelAttribute : Attribute { + /// + /// Gets the label of the input. + /// + public string Label { get; } + + /// + /// Gets the label description of the input. + /// + public string Description { get; set; } + /// /// Creates a custom label for an modal input. /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class InputLabelAttribute : Attribute + /// The label of the input. + /// The label description of the input. + public InputLabelAttribute(string label, string description = null) { - /// - /// Gets the label of the input. - /// - public string Label { get; } - - /// - /// Creates a custom label for an modal input. - /// - /// The label of the input. - public InputLabelAttribute(string label) - { - Label = label; - } + Label = label; + Description = description; } } From 85c85832bc1e79f9aefe8a4da8834db3f2f42f93 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Mon, 10 Nov 2025 10:00:01 +0100 Subject: [PATCH 18/59] add inline docs to modal attributes --- .../ModalChannelSelectInputAttribute.cs | 10 ++- .../Attributes/Modals/ModalInputAttribute.cs | 41 +++++---- .../ModalMentionableSelectInputAttribute.cs | 16 +++- .../Modals/ModalRoleSelectInputAttribute.cs | 14 ++- .../Modals/ModalSelectMenuInputAttribute.cs | 15 +++- .../Modals/ModalTextInputAttribute.cs | 89 +++++++++---------- .../Modals/ModalUserSelectInputAttribute.cs | 14 ++- .../Modals/RequiredInputAttribute.cs | 31 ++++--- 8 files changed, 137 insertions(+), 93 deletions(-) diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectInputAttribute.cs index 263518e04c..798ddf051f 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectInputAttribute.cs @@ -1,9 +1,17 @@ namespace Discord.Interactions.Attributes.Modals; -public class ModalChannelSelectInputAttribute : SelectInputAttribute +/// +/// Marks a property as a channel select. +/// +public class ModalChannelSelectInputAttribute : ModalSelectInputAttribute { + /// public override ComponentType ComponentType => ComponentType.ChannelSelect; + /// + /// Create a new . + /// + /// Custom ID of the channel select component. public ModalChannelSelectInputAttribute(string customId) : base(customId) { } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs index e9b877268a..3e37fa1592 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs @@ -1,30 +1,29 @@ using System; -namespace Discord.Interactions +namespace Discord.Interactions; + +/// +/// Mark an property as a modal input field. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] +public abstract class ModalInputAttribute : Attribute { /// - /// Mark an property as a modal input field. + /// Gets the custom id of the text input. /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] - public abstract class ModalInputAttribute : Attribute - { - /// - /// Gets the custom id of the text input. - /// - public string CustomId { get; } + public string CustomId { get; } - /// - /// Gets the type of the component. - /// - public abstract ComponentType ComponentType { get; } + /// + /// Gets the type of the component. + /// + public abstract ComponentType ComponentType { get; } - /// - /// Create a new . - /// - /// The custom id of the input. - protected ModalInputAttribute(string customId) - { - CustomId = customId; - } + /// + /// Create a new . + /// + /// The custom id of the input. + protected ModalInputAttribute(string customId) + { + CustomId = customId; } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectInputAttribute.cs index f3088b53a6..44a6501480 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectInputAttribute.cs @@ -1,10 +1,20 @@ -namespace Discord.Interactions.Attributes.Modals; +namespace Discord.Interactions; -public class ModalMentionableSelectInputAttribute : SelectInputAttribute +/// +/// Marks a property as a mentionable select input. +/// +public class ModalMentionableSelectInputAttribute : ModalSelectInputAttribute { + /// public override ComponentType ComponentType => ComponentType.MentionableSelect; - public ModalMentionableSelectInputAttribute(string customId) : base(customId) + /// + /// Create a new . + /// + /// Custom ID of the mentionable select component. + /// Minimum number of values that can be selected. + /// Maximum number of values that can be selected + public ModalMentionableSelectInputAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectInputAttribute.cs index 18e611b1f7..dd0e6622a3 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectInputAttribute.cs @@ -1,10 +1,20 @@ namespace Discord.Interactions.Attributes.Modals; -public class ModalRoleSelectInputAttribute : SelectInputAttribute +/// +/// Marks a property as a role select input. +/// +public class ModalRoleSelectInputAttribute : ModalSelectInputAttribute { + /// public override ComponentType ComponentType => ComponentType.RoleSelect; - public ModalRoleSelectInputAttribute(string customId) : base(customId) + /// + /// Create a new . + /// + /// Custom ID of the role select component. + /// Minimum number of values that can be selected. + /// Maximum number of values that can be selected. + public ModalRoleSelectInputAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuInputAttribute.cs index d5a17799b1..9769ba00a1 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuInputAttribute.cs @@ -1,11 +1,20 @@ namespace Discord.Interactions.Attributes.Modals; -public sealed class ModalSelectMenuInputAttribute : SelectInputAttribute +/// +/// Marks a property as a select menu input. +/// +public sealed class ModalSelectMenuInputAttribute : ModalSelectInputAttribute { + /// public override ComponentType ComponentType => ComponentType.SelectMenu; - public ModalSelectMenuInputAttribute(string customId) : base(customId) + /// + /// Create a new . + /// + /// Custom ID of the select menu component. + /// Minimum number of values that can be selected. + /// Maximum number of values that can be selected. + public ModalSelectMenuInputAttribute(string customId, int minValues, int maxValues) : base(customId) { - } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs index 4439e1d845..37bec8bc83 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs @@ -1,55 +1,54 @@ -namespace Discord.Interactions +namespace Discord.Interactions; + +/// +/// Marks a property as a text input. +/// +public sealed class ModalTextInputAttribute : ModalInputAttribute { + /// + public override ComponentType ComponentType => ComponentType.TextInput; + /// - /// Marks a property as a text input. + /// Gets the style of the text input. /// - public sealed class ModalTextInputAttribute : ModalInputAttribute - { - /// - public override ComponentType ComponentType => ComponentType.TextInput; - - /// - /// Gets the style of the text input. - /// - public TextInputStyle Style { get; } + public TextInputStyle Style { get; } - /// - /// Gets the placeholder of the text input. - /// - public string Placeholder { get; } + /// + /// Gets the placeholder of the text input. + /// + public string Placeholder { get; } - /// - /// Gets the minimum length of the text input. - /// - public int MinLength { get; } + /// + /// Gets the minimum length of the text input. + /// + public int MinLength { get; } - /// - /// Gets the maximum length of the text input. - /// - public int MaxLength { get; } + /// + /// Gets the maximum length of the text input. + /// + public int MaxLength { get; } - /// - /// Gets the initial value to be displayed by this input. - /// - public string InitialValue { get; } + /// + /// Gets the initial value to be displayed by this input. + /// + public string InitialValue { get; } - /// - /// Create a new . - /// - /// The custom id of the text input.> - /// The style of the text input. - /// The placeholder of the text input. - /// The minimum length of the text input's content. - /// The maximum length of the text input's content. - /// The initial value to be displayed by this input. - public ModalTextInputAttribute(string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, int minLength = 1, int maxLength = 4000, string initValue = null) - : base(customId) - { - Style = style; - Placeholder = placeholder; - MinLength = minLength; - MaxLength = maxLength; - InitialValue = initValue; - } + /// + /// Create a new . + /// + /// The custom id of the text input.> + /// The style of the text input. + /// The placeholder of the text input. + /// The minimum length of the text input's content. + /// The maximum length of the text input's content. + /// The initial value to be displayed by this input. + public ModalTextInputAttribute(string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, int minLength = 1, int maxLength = 4000, string initValue = null) + : base(customId) + { + Style = style; + Placeholder = placeholder; + MinLength = minLength; + MaxLength = maxLength; + InitialValue = initValue; } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectInputAttribute.cs index 3299524cfc..79784e0898 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectInputAttribute.cs @@ -1,10 +1,20 @@ namespace Discord.Interactions.Attributes.Modals; -public class ModalUserSelectInputAttribute : SelectInputAttribute +/// +/// Marks a property as a user select input. +/// +public class ModalUserSelectInputAttribute : ModalSelectInputAttribute { + /// public override ComponentType ComponentType => ComponentType.UserSelect; - public ModalUserSelectInputAttribute(string customId) : base(customId) + /// + /// Create a new . + /// + /// Custom ID of the user select component. + /// Minimum number of values that can be selected. + /// Maximum number of values that can be selected. + public ModalUserSelectInputAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/RequiredInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/RequiredInputAttribute.cs index 1f580ff000..112a25a58a 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/RequiredInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/RequiredInputAttribute.cs @@ -1,25 +1,24 @@ using System; -namespace Discord.Interactions +namespace Discord.Interactions; + +/// +/// Sets the input as required or optional. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +public class RequiredInputAttribute : Attribute { + /// + /// Gets whether or not user input is required for this input. + /// + public bool IsRequired { get; } + /// /// Sets the input as required or optional. /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class RequiredInputAttribute : Attribute + /// Whether or not user input is required for this input. + public RequiredInputAttribute(bool isRequired = true) { - /// - /// Gets whether or not user input is required for this input. - /// - public bool IsRequired { get; } - - /// - /// Sets the input as required or optional. - /// - /// Whether or not user input is required for this input. - public RequiredInputAttribute(bool isRequired = true) - { - IsRequired = isRequired; - } + IsRequired = isRequired; } } From 375fcfe6851e0d2eecbe752e963e9add43f2be04 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Mon, 10 Nov 2025 10:00:34 +0100 Subject: [PATCH 19/59] add modal file upload attribute --- .../Modals/ModalFileUploadInputAttribute.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadInputAttribute.cs diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadInputAttribute.cs new file mode 100644 index 0000000000..1a6e141041 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadInputAttribute.cs @@ -0,0 +1,32 @@ +namespace Discord.Interactions; + +/// +/// Marks a property as a file upload input. +/// +public class ModalFileUploadInputAttribute : ModalInputAttribute +{ + /// + public override ComponentType ComponentType => ComponentType.FileUpload; + + /// + /// Get the minimum number of files that can be uploaded. + /// + public int MinValues { get; set; } = 1; + + /// + /// Get the maximum number of files that can be uploaded. + /// + public int MaxValues { get; set; } = 1; + + /// + /// Create a new . + /// + /// Custom ID of the file upload component. + /// Minimum number of files that can be uploaded. + /// Maximum number of files that can be uploaded. + public ModalFileUploadInputAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId) + { + MinValues = minValues; + MaxValues = maxValues; + } +} From eca76f305d2afb567cb4d322865d697ec4c1426f Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Mon, 10 Nov 2025 10:02:07 +0100 Subject: [PATCH 20/59] add inline docs to input component infos --- .../ChannelSelectInputComponentInfo.cs | 9 +- .../MentionableSelectInputComponentInfo.cs | 9 +- .../RoleSelectInputComponentInfo.cs | 9 +- .../SelectMenuInputComponentInfo.cs | 27 ++++-- .../SnowflakeSelectInputComponentInfo.cs | 23 ++++- .../InputComponents/TextInputComponentInfo.cs | 87 +++++++++---------- .../UserSelectInputComponentInfo.cs | 9 +- 7 files changed, 104 insertions(+), 69 deletions(-) diff --git a/src/Discord.Net.Interactions/Info/InputComponents/ChannelSelectInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/ChannelSelectInputComponentInfo.cs index 032a85f46f..e16be578df 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/ChannelSelectInputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/InputComponents/ChannelSelectInputComponentInfo.cs @@ -1,8 +1,9 @@ -using Discord.Interactions.Builders.Modals.Inputs; - -namespace Discord.Interactions.Info.InputComponents; +namespace Discord.Interactions; +/// +/// Represents the class for type. +/// public class ChannelSelectInputComponentInfo : SnowflakeSelectInputComponentInfo { - public ChannelSelectInputComponentInfo(ChannelSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } + internal ChannelSelectInputComponentInfo(Builders.ChannelSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } } diff --git a/src/Discord.Net.Interactions/Info/InputComponents/MentionableSelectInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/MentionableSelectInputComponentInfo.cs index 3924384b73..31b2447bc2 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/MentionableSelectInputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/InputComponents/MentionableSelectInputComponentInfo.cs @@ -1,8 +1,9 @@ -using Discord.Interactions.Builders.Modals.Inputs; - -namespace Discord.Interactions.Info.InputComponents; +namespace Discord.Interactions; +/// +/// Represents the class for type. +/// public class MentionableSelectInputComponentInfo : SnowflakeSelectInputComponentInfo { - public MentionableSelectInputComponentInfo(MentionableSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } + internal MentionableSelectInputComponentInfo(Builders.MentionableSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } } diff --git a/src/Discord.Net.Interactions/Info/InputComponents/RoleSelectInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/RoleSelectInputComponentInfo.cs index 676d3ee01b..0afee9f06b 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/RoleSelectInputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/InputComponents/RoleSelectInputComponentInfo.cs @@ -1,8 +1,9 @@ -using Discord.Interactions.Builders.Modals.Inputs; - -namespace Discord.Interactions.Info.InputComponents; +namespace Discord.Interactions; +/// +/// Represents the class for type. +/// public class RoleSelectInputComponentInfo : SnowflakeSelectInputComponentInfo { - public RoleSelectInputComponentInfo(RoleSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } + internal RoleSelectInputComponentInfo(Builders.RoleSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } } diff --git a/src/Discord.Net.Interactions/Info/InputComponents/SelectMenuInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/SelectMenuInputComponentInfo.cs index 12bacf3ac2..8724960266 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/SelectMenuInputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/InputComponents/SelectMenuInputComponentInfo.cs @@ -1,20 +1,35 @@ -using Discord.Interactions.Builders; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -namespace Discord.Interactions.Info.InputComponents; +namespace Discord.Interactions; + +/// +/// Represents the class for type. +/// public class SelectMenuInputComponentInfo : InputComponentInfo { - public string Placeholder { get; set; } + /// + /// Gets the placeholder of the select menu input. + /// + public string Placeholder { get; } - public int MinValues { get; set; } + /// + /// Gets the minimum number of values that can be selected. + /// + public int MinValues { get; } - public int MaxValues { get; set; } + /// + /// Gets the maximum number of values that can be selected. + /// + public int MaxValues { get; } + /// + /// Gets the options of this select menu component. + /// public IReadOnlyCollection Options { get; } - internal SelectMenuInputComponentInfo(SelectMenuInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) + internal SelectMenuInputComponentInfo(Builders.SelectMenuInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { Placeholder = builder.Placeholder; MinValues = builder.MinValues; diff --git a/src/Discord.Net.Interactions/Info/InputComponents/SnowflakeSelectInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/SnowflakeSelectInputComponentInfo.cs index 175e013f34..b6b29d46d0 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/SnowflakeSelectInputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/InputComponents/SnowflakeSelectInputComponentInfo.cs @@ -1,22 +1,39 @@ -using Discord.Interactions.Builders.Modals.Inputs; using System.Collections.Generic; using System.Collections.Immutable; -namespace Discord.Interactions.Info.InputComponents; +namespace Discord.Interactions; +/// +/// Represents the base class for , , , type. +/// public abstract class SnowflakeSelectInputComponentInfo : InputComponentInfo { + /// + /// Gets the minimum number of values that can be selected. + /// public int MinValues { get; } + /// + /// Gets the maximum number of values that can be selected. + /// public int MaxValues { get; } + /// + /// Gets the placeholder of this select input. + /// public string Placeholder { get; } + /// + /// Gets the default values of this select input. + /// public IReadOnlyCollection DefaultValues { get; } + /// + /// Gets the default value type of this select input. + /// public SelectDefaultValueType? DefaultValueType { get; } - internal SnowflakeSelectInputComponentInfo(ISnowflakeSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) + internal SnowflakeSelectInputComponentInfo(Builders.ISnowflakeSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { MinValues = builder.MinValues; MaxValues = builder.MaxValues; diff --git a/src/Discord.Net.Interactions/Info/InputComponents/TextInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/TextInputComponentInfo.cs index 6831c7953d..94165c429f 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/TextInputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/InputComponents/TextInputComponentInfo.cs @@ -1,52 +1,51 @@ using System; -namespace Discord.Interactions +namespace Discord.Interactions; + +/// +/// Represents the class for type. +/// +public class TextInputComponentInfo : InputComponentInfo { /// - /// Represents the class for type. + /// true when overrides . + /// + internal bool TypeOverridesToString => _typeOverridesToString.Value; + private readonly Lazy _typeOverridesToString; + + /// + /// Gets the style of the text input. + /// + public TextInputStyle Style { get; } + + /// + /// Gets the placeholder of the text input. + /// + public string Placeholder { get; } + + /// + /// Gets the minimum length of the text input. /// - public class TextInputComponentInfo : InputComponentInfo + public int MinLength { get; } + + /// + /// Gets the maximum length of the text input. + /// + public int MaxLength { get; } + + /// + /// Gets the initial value to be displayed by this input. + /// + public string InitialValue { get; } + + internal TextInputComponentInfo(Builders.TextInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { - /// - /// true when overrides . - /// - internal bool TypeOverridesToString => _typeOverridesToString.Value; - private readonly Lazy _typeOverridesToString; - - /// - /// Gets the style of the text input. - /// - public TextInputStyle Style { get; } - - /// - /// Gets the placeholder of the text input. - /// - public string Placeholder { get; } - - /// - /// Gets the minimum length of the text input. - /// - public int MinLength { get; } - - /// - /// Gets the maximum length of the text input. - /// - public int MaxLength { get; } - - /// - /// Gets the initial value to be displayed by this input. - /// - public string InitialValue { get; } - - internal TextInputComponentInfo(Builders.TextInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) - { - Style = builder.Style; - Placeholder = builder.Placeholder; - MinLength = builder.MinLength; - MaxLength = builder.MaxLength; - InitialValue = builder.InitialValue; - - _typeOverridesToString = new(() => ReflectionUtils.OverridesToString(Type)); - } + Style = builder.Style; + Placeholder = builder.Placeholder; + MinLength = builder.MinLength; + MaxLength = builder.MaxLength; + InitialValue = builder.InitialValue; + + _typeOverridesToString = new(() => ReflectionUtils.OverridesToString(Type)); } } diff --git a/src/Discord.Net.Interactions/Info/InputComponents/UserSelectInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/UserSelectInputComponentInfo.cs index dd189084bb..4423b180fc 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/UserSelectInputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/InputComponents/UserSelectInputComponentInfo.cs @@ -1,8 +1,9 @@ -using Discord.Interactions.Builders.Modals.Inputs; - -namespace Discord.Interactions.Info.InputComponents; +namespace Discord.Interactions; +/// +/// Represents the class for type. +/// public class UserSelectInputComponentInfo : SnowflakeSelectInputComponentInfo { - public UserSelectInputComponentInfo(UserSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } + internal UserSelectInputComponentInfo(Builders.UserSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } } From b24a11527b96e022d72e012d14bcc35d5cd1ab5b Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Mon, 10 Nov 2025 10:05:06 +0100 Subject: [PATCH 21/59] add description property to input component builder --- .../Modals/Inputs/IInputComponentBuilder.cs | 231 ++++++------ .../Modals/Inputs/InputComponentBuilder.cs | 341 +++++++++--------- 2 files changed, 301 insertions(+), 271 deletions(-) diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs index e46d4ca1b3..3b9892ff3f 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs @@ -3,115 +3,128 @@ using System.Collections.Generic; using System.Reflection; -namespace Discord.Interactions.Builders +namespace Discord.Interactions.Builders; + +/// +/// Represent a builder for creating . +/// +public interface IInputComponentBuilder { /// - /// Represent a builder for creating . - /// - public interface IInputComponentBuilder - { - /// - /// Gets the parent modal of this input component. - /// - ModalBuilder Modal { get; } - - /// - /// Gets the custom id of this input component. - /// - string CustomId { get; } - - /// - /// Gets the label of this input component. - /// - string Label { get; } - - /// - /// Gets whether this input component is required. - /// - bool IsRequired { get; } - - /// - /// Gets the component type of this input component. - /// - ComponentType ComponentType { get; } - - /// - /// Get the reference type of this input component. - /// - Type Type { get; } - - /// - /// Get the of this component's property. - /// - PropertyInfo PropertyInfo { get; } - - /// - /// Get the assigned to this input. - /// - ModalComponentTypeConverter TypeConverter { get; } - - /// - /// Gets the default value of this input component property. - /// - object DefaultValue { get; } - - /// - /// Gets a collection of the attributes of this component. - /// - IReadOnlyCollection Attributes { get; } - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - IInputComponentBuilder WithCustomId(string customId); - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - IInputComponentBuilder WithLabel(string label); - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - IInputComponentBuilder SetIsRequired(bool isRequired); - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - IInputComponentBuilder WithType(Type type); - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - IInputComponentBuilder SetDefaultValue(object value); - - /// - /// Adds attributes to . - /// - /// New attributes to be added to . - /// - /// The builder instance. - /// - IInputComponentBuilder WithAttributes(params Attribute[] attributes); - } + /// Gets the parent modal of this input component. + /// + ModalBuilder Modal { get; } + + /// + /// Gets the custom id of this input component. + /// + string CustomId { get; } + + /// + /// Gets the label of this input component. + /// + string Label { get; } + + /// + /// Gets the label description of this input component. + /// + string Description { get; } + + /// + /// Gets whether this input component is required. + /// + bool IsRequired { get; } + + /// + /// Gets the component type of this input component. + /// + ComponentType ComponentType { get; } + + /// + /// Get the reference type of this input component. + /// + Type Type { get; } + + /// + /// Get the of this component's property. + /// + PropertyInfo PropertyInfo { get; } + + /// + /// Get the assigned to this input. + /// + ModalComponentTypeConverter TypeConverter { get; } + + /// + /// Gets the default value of this input component property. + /// + object DefaultValue { get; } + + /// + /// Gets a collection of the attributes of this component. + /// + IReadOnlyCollection Attributes { get; } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + IInputComponentBuilder WithCustomId(string customId); + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + IInputComponentBuilder WithLabel(string label); + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + IInputComponentBuilder WithDescription(string description); + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + IInputComponentBuilder SetIsRequired(bool isRequired); + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + IInputComponentBuilder WithType(Type type); + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + IInputComponentBuilder SetDefaultValue(object value); + + /// + /// Adds attributes to . + /// + /// New attributes to be added to . + /// + /// The builder instance. + /// + IInputComponentBuilder WithAttributes(params Attribute[] attributes); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs index 8da3d07177..ce9a262017 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs @@ -3,171 +3,188 @@ using System.Collections.Generic; using System.Reflection; -namespace Discord.Interactions.Builders +namespace Discord.Interactions.Builders; + +/// +/// Represents the base builder class for creating . +/// +/// The this builder yields when built. +/// Inherited type. +public abstract class InputComponentBuilder : IInputComponentBuilder + where TInfo : InputComponentInfo + where TBuilder : InputComponentBuilder { + private readonly List _attributes; + protected abstract TBuilder Instance { get; } + + /// + public ModalBuilder Modal { get; } + + /// + public string CustomId { get; set; } + + /// + public string Label { get; set; } + + /// + public string Description { get; set; } + + /// + public bool IsRequired { get; set; } = true; + + /// + public ComponentType ComponentType { get; internal set; } + + /// + public Type Type { get; private set; } + + /// + public PropertyInfo PropertyInfo { get; internal set; } + + /// + public ModalComponentTypeConverter TypeConverter { get; private set; } + + /// + public object DefaultValue { get; set; } + + /// + public IReadOnlyCollection Attributes => _attributes; + + /// + /// Creates an instance of + /// + /// Parent modal of this input component. + public InputComponentBuilder(ModalBuilder modal) + { + Modal = modal; + _attributes = new(); + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TBuilder WithCustomId(string customId) + { + CustomId = customId; + return Instance; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TBuilder WithLabel(string label) + { + Label = label; + return Instance; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TBuilder WithDescription(string description) + { + Description = description; + return Instance; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TBuilder SetIsRequired(bool isRequired) + { + IsRequired = isRequired; + return Instance; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public virtual TBuilder WithComponentType(ComponentType componentType) + { + ComponentType = componentType; + return Instance; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TBuilder WithType(Type type) + { + Type = type; + TypeConverter = Modal._interactionService.GetModalInputTypeConverter(type); + return Instance; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TBuilder SetDefaultValue(object value) + { + DefaultValue = value; + return Instance; + } + /// - /// Represents the base builder class for creating . + /// Adds attributes to . /// - /// The this builder yields when built. - /// Inherited type. - public abstract class InputComponentBuilder : IInputComponentBuilder - where TInfo : InputComponentInfo - where TBuilder : InputComponentBuilder + /// New attributes to be added to . + /// + /// The builder instance. + /// + public TBuilder WithAttributes(params Attribute[] attributes) { - private readonly List _attributes; - protected abstract TBuilder Instance { get; } - - /// - public ModalBuilder Modal { get; } - - /// - public string CustomId { get; set; } - - /// - public string Label { get; set; } - - /// - public bool IsRequired { get; set; } = true; - - /// - public ComponentType ComponentType { get; internal set; } - - /// - public Type Type { get; private set; } - - /// - public PropertyInfo PropertyInfo { get; internal set; } - - /// - public ModalComponentTypeConverter TypeConverter { get; private set; } - - /// - public object DefaultValue { get; set; } - - /// - public IReadOnlyCollection Attributes => _attributes; - - /// - /// Creates an instance of - /// - /// Parent modal of this input component. - public InputComponentBuilder(ModalBuilder modal) - { - Modal = modal; - _attributes = new(); - } - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TBuilder WithCustomId(string customId) - { - CustomId = customId; - return Instance; - } - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TBuilder WithLabel(string label) - { - Label = label; - return Instance; - } - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TBuilder SetIsRequired(bool isRequired) - { - IsRequired = isRequired; - return Instance; - } - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public virtual TBuilder WithComponentType(ComponentType componentType) - { - ComponentType = componentType; - return Instance; - } - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TBuilder WithType(Type type) - { - Type = type; - TypeConverter = Modal._interactionService.GetModalInputTypeConverter(type); - return Instance; - } - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TBuilder SetDefaultValue(object value) - { - DefaultValue = value; - return Instance; - } - - /// - /// Adds attributes to . - /// - /// New attributes to be added to . - /// - /// The builder instance. - /// - public TBuilder WithAttributes(params Attribute[] attributes) - { - _attributes.AddRange(attributes); - return Instance; - } - - internal abstract TInfo Build(ModalInfo modal); - - //IInputComponentBuilder - /// - IInputComponentBuilder IInputComponentBuilder.WithCustomId(string customId) => WithCustomId(customId); - - /// - IInputComponentBuilder IInputComponentBuilder.WithLabel(string label) => WithCustomId(label); - - /// - IInputComponentBuilder IInputComponentBuilder.WithType(Type type) => WithType(type); - - /// - IInputComponentBuilder IInputComponentBuilder.SetDefaultValue(object value) => SetDefaultValue(value); - - /// - IInputComponentBuilder IInputComponentBuilder.WithAttributes(params Attribute[] attributes) => WithAttributes(attributes); - - /// - IInputComponentBuilder IInputComponentBuilder.SetIsRequired(bool isRequired) => SetIsRequired(isRequired); + _attributes.AddRange(attributes); + return Instance; } + + internal abstract TInfo Build(ModalInfo modal); + + //IInputComponentBuilder + /// + IInputComponentBuilder IInputComponentBuilder.WithCustomId(string customId) => WithCustomId(customId); + + /// + IInputComponentBuilder IInputComponentBuilder.WithLabel(string label) => WithCustomId(label); + + /// + IInputComponentBuilder IInputComponentBuilder.WithType(Type type) => WithType(type); + + /// + IInputComponentBuilder IInputComponentBuilder.SetDefaultValue(object value) => SetDefaultValue(value); + + /// + IInputComponentBuilder IInputComponentBuilder.WithAttributes(params Attribute[] attributes) => WithAttributes(attributes); + + /// + IInputComponentBuilder IInputComponentBuilder.SetIsRequired(bool isRequired) => SetIsRequired(isRequired); + + IInputComponentBuilder IInputComponentBuilder.WithDescription(string description) => WithDescription(description); } From 2eb8672f1dcc3a761f940007d8b3da353c30c423 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Mon, 10 Nov 2025 10:05:59 +0100 Subject: [PATCH 22/59] add inline docs to modal component builders --- .../ChannelSelectInputComponentBuilder.cs | 38 +++- .../ISnowflakeSelectInputComponentBuilder.cs | 40 +++- .../MentionableSelectInputComponentBuilder.cs | 40 +++- .../Inputs/RoleSelectInputComponentBuilder.cs | 38 +++- .../Inputs/SelectMenuInputComponentBuilder.cs | 30 ++- .../SnowflakeSelectInputComponentBuilder.cs | 28 ++- .../Inputs/TextInputComponentBuilder.cs | 187 +++++++++--------- .../Inputs/UserSelectInputComponentBuilder.cs | 38 +++- 8 files changed, 332 insertions(+), 107 deletions(-) diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/ChannelSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/ChannelSelectInputComponentBuilder.cs index 3a5c52426d..945fcbd022 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/ChannelSelectInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/ChannelSelectInputComponentBuilder.cs @@ -1,33 +1,67 @@ -using Discord.Interactions.Info.InputComponents; using System.Collections.Generic; using System.Linq; -namespace Discord.Interactions.Builders.Modals.Inputs; +namespace Discord.Interactions.Builders; +/// +/// Represents a builder for creating . +/// public class ChannelSelectInputComponentBuilder : SnowflakeSelectInputComponentBuilder { protected override ChannelSelectInputComponentBuilder Instance => this; + /// + /// Initializes a new . + /// + /// Parent modal of this component. public ChannelSelectInputComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.ChannelSelect) { } + /// + /// Adds a default value to . + /// + /// The channel to add as a default value. + /// + /// The builder instance. + /// public ChannelSelectInputComponentBuilder AddDefaulValue(IChannel channel) { _defaultValues.Add(new SelectMenuDefaultValue(channel.Id, SelectDefaultValueType.Channel)); return this; } + /// + /// Adds a default value to . + /// + /// The channel ID to add as a default value. + /// + /// The builder instance. + /// public ChannelSelectInputComponentBuilder AddDefaulValue(ulong channelId) { _defaultValues.Add(new SelectMenuDefaultValue(channelId, SelectDefaultValueType.Channel)); return this; } + /// + /// Adds default values to . + /// + /// The channels to add as a default value. + /// + /// The builder instance. + /// public ChannelSelectInputComponentBuilder AddDefaultValues(params IChannel[] channels) { _defaultValues.AddRange(channels.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Channel))); return this; } + /// + /// Adds default values to . + /// + /// The channels to add as a default value. + /// + /// The builder instance. + /// public ChannelSelectInputComponentBuilder AddDefaultValues(IEnumerable channels) { _defaultValues.AddRange(channels.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Channel))); diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/ISnowflakeSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/ISnowflakeSelectInputComponentBuilder.cs index 817415dc93..fd5b363c30 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/ISnowflakeSelectInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/ISnowflakeSelectInputComponentBuilder.cs @@ -1,24 +1,62 @@ using System.Collections.Generic; -namespace Discord.Interactions.Builders.Modals.Inputs; +namespace Discord.Interactions.Builders; +/// +/// Represent a builder for creating . +/// public interface ISnowflakeSelectInputComponentBuilder : IInputComponentBuilder { + /// + /// Gets the minimum number of values that can be selected. + /// int MinValues { get; } + /// + /// Gets the maximum number of values that can be selected. + /// int MaxValues { get; } + /// + /// Gets the placeholder text for this select component. + /// string Placeholder { get; set; } + /// + /// Gets the default value collection for this select component. + /// IReadOnlyCollection DefaultValues { get; } + /// + /// Gets the default value type of this select component. + /// SelectDefaultValueType? DefaultValuesType { get; } + /// + /// Adds a default value to the . + /// + /// Default value to be added. + /// The builder instance. ISnowflakeSelectInputComponentBuilder AddDefaultValue(SelectMenuDefaultValue defaultValue); + /// + /// Sets . + /// + /// New value of the + /// The builder instance. ISnowflakeSelectInputComponentBuilder WithMinValues(int minValues); + /// + /// Sets . + /// + /// New value of the + /// The builder instance. ISnowflakeSelectInputComponentBuilder WithMaxValues(int maxValues); + /// + /// Sets . + /// + /// New value of the + /// The builder instance. ISnowflakeSelectInputComponentBuilder WithPlaceholder(string placeholder); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/MentionableSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/MentionableSelectInputComponentBuilder.cs index 2122cef92b..1d16f3585e 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/MentionableSelectInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/MentionableSelectInputComponentBuilder.cs @@ -1,31 +1,65 @@ -using Discord.Interactions.Info.InputComponents; - -namespace Discord.Interactions.Builders.Modals.Inputs; +namespace Discord.Interactions.Builders; +/// +/// Represents a builder for creating a . +/// public class MentionableSelectInputComponentBuilder : SnowflakeSelectInputComponentBuilder { protected override MentionableSelectInputComponentBuilder Instance => this; + /// + /// Initialize a new . + /// + /// Parent modal of this input component. public MentionableSelectInputComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.MentionableSelect) { } + /// + /// Adds a snowflake ID as a default value to . + /// + /// The ID to add as a default value. + /// Enitity type of the snowflake ID. + /// + /// The builder instance. + /// public MentionableSelectInputComponentBuilder AddDefaultValue(ulong id, SelectDefaultValueType type) { _defaultValues.Add(new SelectMenuDefaultValue(id, type)); return this; } + /// + /// Adds a user as a default value to . + /// + /// The user to add as a default value. + /// + /// The builder instance. + /// public MentionableSelectInputComponentBuilder AddDefaultValue(IUser user) { _defaultValues.Add(new SelectMenuDefaultValue(user.Id, SelectDefaultValueType.User)); return this; } + /// + /// Adds a channel as a default value to . + /// + /// The channel to add as a default value. + /// + /// The builder instance. + /// public MentionableSelectInputComponentBuilder AddDefaultValue(IChannel channel) { _defaultValues.Add(new SelectMenuDefaultValue(channel.Id, SelectDefaultValueType.Channel)); return this; } + /// + /// Adds a role as a default value to . + /// + /// The role to add as a default value. + /// + /// The builder instance. + /// public MentionableSelectInputComponentBuilder AddDefaulValue(IRole role) { _defaultValues.Add(new SelectMenuDefaultValue(role.Id, SelectDefaultValueType.Role)); diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/RoleSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/RoleSelectInputComponentBuilder.cs index e566ea3e6f..658972b00b 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/RoleSelectInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/RoleSelectInputComponentBuilder.cs @@ -1,33 +1,67 @@ -using Discord.Interactions.Info.InputComponents; using System.Collections.Generic; using System.Linq; -namespace Discord.Interactions.Builders.Modals.Inputs; +namespace Discord.Interactions.Builders; +/// +/// Represents a builder for creating a . +/// public class RoleSelectInputComponentBuilder : SnowflakeSelectInputComponentBuilder { protected override RoleSelectInputComponentBuilder Instance => this; + /// + /// Initialize a new . + /// + /// Parent modal of this input component. public RoleSelectInputComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.RoleSelect) { } + /// + /// Adds a default value to . + /// + /// The role to add as a default value. + /// + /// The builder instance. + /// public RoleSelectInputComponentBuilder AddDefaulValue(IRole role) { _defaultValues.Add(new SelectMenuDefaultValue(role.Id, SelectDefaultValueType.Role)); return this; } + /// + /// Adds a default value to . + /// + /// The role ID to add as a default value. + /// + /// The builder instance. + /// public RoleSelectInputComponentBuilder AddDefaulValue(ulong roleId) { _defaultValues.Add(new SelectMenuDefaultValue(roleId, SelectDefaultValueType.Role)); return this; } + /// + /// Adds default values to . + /// + /// The roles to add as a default value. + /// + /// The builder instance. + /// public RoleSelectInputComponentBuilder AddDefaultValues(params IRole[] roles) { _defaultValues.AddRange(roles.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Role))); return this; } + /// + /// Adds default values to . + /// + /// The roles to add as a default value. + /// + /// The builder instance. + /// public RoleSelectInputComponentBuilder AddDefaultValues(IEnumerable roles) { _defaultValues.AddRange(roles.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Role))); diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/SelectMenuInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/SelectMenuInputComponentBuilder.cs index c75850b0fc..6bef2ccf55 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/SelectMenuInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/SelectMenuInputComponentBuilder.cs @@ -1,34 +1,62 @@ -using Discord.Interactions.Info.InputComponents; using System; using System.Collections.Generic; namespace Discord.Interactions.Builders; +/// +/// Represents a builder for creating . +/// public class SelectMenuInputComponentBuilder : InputComponentBuilder { private readonly List _options; protected override SelectMenuInputComponentBuilder Instance => this; + /// + /// Gets and sets the placeholder for the select menu iput. + /// public string Placeholder { get; set; } + /// + /// Gets and sets the minimum number of values that can be selected. + /// public int MinValues { get; set; } + /// + /// Gets or sets the maximum number of values that can be selected. + /// public int MaxValues { get; set; } + /// + /// Gets the options of this select menu component. + /// public IReadOnlyCollection Options => _options; + /// + /// Initialize a new . + /// + /// Parent modal of this component. public SelectMenuInputComponentBuilder(ModalBuilder modal) : base(modal) { _options = new(); } + /// + /// Adds an option to . + /// + /// Option to be added to . + /// The builder instance. public SelectMenuInputComponentBuilder AddOption(SelectMenuOptionBuilder option) { _options.Add(option); return this; } + /// + /// Adds an option to . + /// + /// Select menu option builder factory. + /// The builder instance. public SelectMenuInputComponentBuilder AddOption(Action configure) { var builder = new SelectMenuOptionBuilder(); diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/SnowflakeSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/SnowflakeSelectInputComponentBuilder.cs index e7c508f510..60e9c4c61b 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/SnowflakeSelectInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/SnowflakeSelectInputComponentBuilder.cs @@ -1,22 +1,32 @@ using System; using System.Collections.Generic; -namespace Discord.Interactions.Builders.Modals.Inputs; +namespace Discord.Interactions.Builders; +/// +/// Represents a builder for creating . +/// +/// The this builder yields when built. +/// Inherited type. public abstract class SnowflakeSelectInputComponentBuilder : InputComponentBuilder, ISnowflakeSelectInputComponentBuilder where TInfo : InputComponentInfo where TBuilder : InputComponentBuilder, ISnowflakeSelectInputComponentBuilder { protected readonly List _defaultValues; + /// public int MinValues { get; set; } = 1; + /// public int MaxValues { get; set; } = 1; + /// public string Placeholder { get; set; } + /// public IReadOnlyCollection DefaultValues => _defaultValues.AsReadOnly(); + /// public SelectDefaultValueType? DefaultValuesType { get @@ -32,6 +42,11 @@ public SelectDefaultValueType? DefaultValuesType } } + /// + /// Initialize a new . + /// + /// Parent modal of this input component. + /// Type of this component. public SnowflakeSelectInputComponentBuilder(ModalBuilder modal, ComponentType componentType) : base(modal) { ValidateComponentType(componentType); @@ -40,6 +55,7 @@ public SnowflakeSelectInputComponentBuilder(ModalBuilder modal, ComponentType co _defaultValues = new(); } + /// public TBuilder AddDefaultValue(SelectMenuDefaultValue defaultValue) { if (DefaultValuesType.HasValue && defaultValue.Type != DefaultValuesType.Value) @@ -49,24 +65,28 @@ public TBuilder AddDefaultValue(SelectMenuDefaultValue defaultValue) return Instance; } + /// public override TBuilder WithComponentType(ComponentType componentType) { ValidateComponentType(componentType); return base.WithComponentType(componentType); } + /// public TBuilder WithMinValues(int minValues) { MinValues = minValues; return Instance; } + /// public TBuilder WithMaxValues(int maxValues) { MaxValues = maxValues; return Instance; } + /// public TBuilder WithPlaceholder(string placeholder) { Placeholder = placeholder; @@ -75,16 +95,20 @@ public TBuilder WithPlaceholder(string placeholder) private void ValidateComponentType(ComponentType componentType) { - if (componentType is not ComponentType.UserSelect or ComponentType.RoleSelect or ComponentType.MentionableSelect or ComponentType.ChannelSelect) + if (componentType is not (ComponentType.UserSelect or ComponentType.RoleSelect or ComponentType.MentionableSelect or ComponentType.ChannelSelect)) throw new ArgumentException("Component type must be a snowflake select type.", nameof(componentType)); } + /// ISnowflakeSelectInputComponentBuilder ISnowflakeSelectInputComponentBuilder.AddDefaultValue(SelectMenuDefaultValue defaultValue) => AddDefaultValue(defaultValue); + /// ISnowflakeSelectInputComponentBuilder ISnowflakeSelectInputComponentBuilder.WithMinValues(int minValues) => WithMinValues(minValues); + /// ISnowflakeSelectInputComponentBuilder ISnowflakeSelectInputComponentBuilder.WithMaxValues(int maxValues) => WithMaxValues(maxValues); + /// ISnowflakeSelectInputComponentBuilder ISnowflakeSelectInputComponentBuilder.WithPlaceholder(string placeholder) => WithPlaceholder(placeholder); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs index 728b97a7ac..844e2e4b50 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs @@ -1,109 +1,108 @@ -namespace Discord.Interactions.Builders +namespace Discord.Interactions.Builders; + +/// +/// Represents a builder for creating . +/// +public class TextInputComponentBuilder : InputComponentBuilder { + protected override TextInputComponentBuilder Instance => this; + /// - /// Represents a builder for creating . + /// Gets and sets the style of the text input. /// - public class TextInputComponentBuilder : InputComponentBuilder - { - protected override TextInputComponentBuilder Instance => this; - - /// - /// Gets and sets the style of the text input. - /// - public TextInputStyle Style { get; set; } - - /// - /// Gets and sets the placeholder of the text input. - /// - public string Placeholder { get; set; } + public TextInputStyle Style { get; set; } - /// - /// Gets and sets the minimum length of the text input. - /// - public int MinLength { get; set; } + /// + /// Gets and sets the placeholder of the text input. + /// + public string Placeholder { get; set; } - /// - /// Gets and sets the maximum length of the text input. - /// - public int MaxLength { get; set; } + /// + /// Gets and sets the minimum length of the text input. + /// + public int MinLength { get; set; } - /// - /// Gets and sets the initial value to be displayed by this input. - /// - public string InitialValue { get; set; } + /// + /// Gets and sets the maximum length of the text input. + /// + public int MaxLength { get; set; } - /// - /// Initializes a new . - /// - /// Parent modal of this component. - public TextInputComponentBuilder(ModalBuilder modal) : base(modal) { } + /// + /// Gets and sets the initial value to be displayed by this input. + /// + public string InitialValue { get; set; } - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TextInputComponentBuilder WithStyle(TextInputStyle style) - { - Style = style; - return this; - } + /// + /// Initializes a new . + /// + /// Parent modal of this component. + public TextInputComponentBuilder(ModalBuilder modal) : base(modal) { } - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TextInputComponentBuilder WithPlaceholder(string placeholder) - { - Placeholder = placeholder; - return this; - } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TextInputComponentBuilder WithStyle(TextInputStyle style) + { + Style = style; + return this; + } - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TextInputComponentBuilder WithMinLength(int minLength) - { - MinLength = minLength; - return this; - } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TextInputComponentBuilder WithPlaceholder(string placeholder) + { + Placeholder = placeholder; + return this; + } - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TextInputComponentBuilder WithMaxLength(int maxLength) - { - MaxLength = maxLength; - return this; - } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TextInputComponentBuilder WithMinLength(int minLength) + { + MinLength = minLength; + return this; + } - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TextInputComponentBuilder WithInitialValue(string value) - { - InitialValue = value; - return this; - } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TextInputComponentBuilder WithMaxLength(int maxLength) + { + MaxLength = maxLength; + return this; + } - internal override TextInputComponentInfo Build(ModalInfo modal) => - new(this, modal); + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TextInputComponentBuilder WithInitialValue(string value) + { + InitialValue = value; + return this; } + + internal override TextInputComponentInfo Build(ModalInfo modal) => + new(this, modal); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/UserSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/UserSelectInputComponentBuilder.cs index 87180dbe53..d48273a2f3 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/UserSelectInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/UserSelectInputComponentBuilder.cs @@ -1,33 +1,67 @@ -using Discord.Interactions.Info.InputComponents; using System.Collections.Generic; using System.Linq; -namespace Discord.Interactions.Builders.Modals.Inputs; +namespace Discord.Interactions.Builders; +/// +/// Represents a builder for creating . +/// public class UserSelectInputComponentBuilder : SnowflakeSelectInputComponentBuilder { protected override UserSelectInputComponentBuilder Instance => this; + /// + /// Initialize a new . + /// + /// Parent modal of this input component. public UserSelectInputComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.UserSelect) { } + /// + /// Adds a default value to . + /// + /// The user to add as a default value. + /// + /// The builder instance. + /// public UserSelectInputComponentBuilder AddDefaulValue(IUser user) { _defaultValues.Add(new SelectMenuDefaultValue(user.Id, SelectDefaultValueType.User)); return this; } + /// + /// Adds a default value to . + /// + /// The user ID to add as a default value. + /// + /// The builder instance. + /// public UserSelectInputComponentBuilder AddDefaulValue(ulong userId) { _defaultValues.Add(new SelectMenuDefaultValue(userId, SelectDefaultValueType.User)); return this; } + /// + /// Adds default values to . + /// + /// The users to add as a default value. + /// + /// The builder instance. + /// public UserSelectInputComponentBuilder AddDefaultValues(params IUser[] users) { _defaultValues.AddRange(users.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.User))); return this; } + /// + /// Adds default values to . + /// + /// The users to add as a default value. + /// + /// The builder instance. + /// public UserSelectInputComponentBuilder AddDefaultValues(IEnumerable users) { _defaultValues.AddRange(users.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.User))); From 81581ee98db999f1af90d7d74db5cefeb8214ec9 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Mon, 10 Nov 2025 10:06:20 +0100 Subject: [PATCH 23/59] add modal file upload component info --- .../FileUploadInputComponentInfo.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/Discord.Net.Interactions/Info/InputComponents/FileUploadInputComponentInfo.cs diff --git a/src/Discord.Net.Interactions/Info/InputComponents/FileUploadInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/FileUploadInputComponentInfo.cs new file mode 100644 index 0000000000..290bdc7940 --- /dev/null +++ b/src/Discord.Net.Interactions/Info/InputComponents/FileUploadInputComponentInfo.cs @@ -0,0 +1,23 @@ +namespace Discord.Interactions; + +/// +/// Represents the class for type. +/// +public class FileUploadInputComponentInfo : InputComponentInfo +{ + /// + /// Gets the minimum number of values that can be selected. + /// + public int MinValues { get; } + + /// + /// Gets the maximum number of values that can be selected. + /// + public int MaxValues { get; } + + internal FileUploadInputComponentInfo(Builders.FileUploadInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) + { + MinValues = builder.MinValues; + MaxValues = builder.MaxValues; + } +} From 7af0c485c0703df3b0070669eb4efb869751ee4a Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Mon, 10 Nov 2025 10:07:44 +0100 Subject: [PATCH 24/59] add modal file upload component builder --- .../Inputs/FileUploadInputComponentBuilder.cs | 54 +++++++++++++++++++ .../Builders/Modals/ModalBuilder.cs | 15 ++++++ .../Builders/ModuleClassBuilder.cs | 44 +++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 src/Discord.Net.Interactions/Builders/Modals/Inputs/FileUploadInputComponentBuilder.cs diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/FileUploadInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/FileUploadInputComponentBuilder.cs new file mode 100644 index 0000000000..c65f639186 --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/FileUploadInputComponentBuilder.cs @@ -0,0 +1,54 @@ +namespace Discord.Interactions.Builders; + +/// +/// Represents a builder for creating . +/// +public class FileUploadInputComponentBuilder : InputComponentBuilder +{ + protected override FileUploadInputComponentBuilder Instance => this; + + /// + /// Gets and sets the minimum number of files that can be uploaded. + /// + public int MinValues { get; set; } = 1; + + /// + /// Gets and sets the maximum number of files that can be uploaded. + /// + public int MaxValues { get; set; } = 1; + + /// + /// Initializes a new . + /// + /// + public FileUploadInputComponentBuilder(ModalBuilder modal) : base(modal) { } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public FileUploadInputComponentBuilder WithMinValues(int minValues) + { + MinValues = minValues; + return this; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public FileUploadInputComponentBuilder WithMaxValues(int maxValues) + { + MaxValues = maxValues; + return this; + } + + internal override FileUploadInputComponentInfo Build(ModalInfo modal) + => new (this, modal); +} diff --git a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs index 1d90134915..115c044ad9 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs @@ -128,6 +128,21 @@ public ModalBuilder AddChannelSelectComponent(Action + /// Adds a file upload component to . + /// + /// File upload component builder factory. + /// + /// The builder instance. + /// + public ModalBuilder AddFileUploadComponent(Action configure) + { + var builder = new FileUploadInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + internal ModalInfo Build() => new(this); } } diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index 2c541ad1be..0084570ef8 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -633,6 +633,11 @@ public static ModalInfo BuildModalInfo(Type modalType, InteractionService intera case ComponentType.ChannelSelect: builder.AddChannelSelectComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); break; + case ComponentType.FileUpload: + builder.AddFileUploadComponent(x => BuildFileUploadInput(x, prop, prop.GetValue(instance))); + break; + case ComponentType.TextDisplay: + throw new NotImplementedException(); case null: throw new InvalidOperationException($"{prop.Name} of {prop.DeclaringType.Name} isn't a valid modal input field."); default: @@ -752,6 +757,45 @@ private static void BuildSnowflakeSelectInput(SnowflakeSelectIn } } } + + private static void BuildFileUploadInput(FileUploadInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) + { + var attributes = propertyInfo.GetCustomAttributes(); + + builder.Label = propertyInfo.Name; + builder.DefaultValue = defaultValue; + builder.WithType(propertyInfo.PropertyType); + builder.PropertyInfo = propertyInfo; + + foreach(var attribute in attributes) + { + switch (attribute) + { + case ModalFileUploadInputAttribute fileUploadInput: + builder.CustomId = fileUploadInput.CustomId; + builder.ComponentType = fileUploadInput.ComponentType; + builder.MinValues = fileUploadInput.MinValues; + builder.MaxValues = fileUploadInput.MaxValues; + break; + case RequiredInputAttribute requiredInput: + builder.IsRequired = requiredInput.IsRequired; + break; + case InputLabelAttribute inputLabel: + builder.Label = inputLabel.Label; + builder.Description = inputLabel.Description; + break; + default: + builder.WithAttributes(attribute); + break; + } + } + + } + + private static void BuildTextDisplayComponent() + { + + } #endregion internal static bool IsValidModuleDefinition(TypeInfo typeInfo) From 0ae42e0acca47a8424fbe8b118a23cfbb28b8a31 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Mon, 10 Nov 2025 10:08:07 +0100 Subject: [PATCH 25/59] rename select input attribute --- .../Modals/ModalSelectInputAttribute.cs | 28 +++++++++++++++++++ .../Attributes/Modals/SelectInputAttribute.cs | 14 ---------- 2 files changed, 28 insertions(+), 14 deletions(-) create mode 100644 src/Discord.Net.Interactions/Attributes/Modals/ModalSelectInputAttribute.cs delete mode 100644 src/Discord.Net.Interactions/Attributes/Modals/SelectInputAttribute.cs diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectInputAttribute.cs new file mode 100644 index 0000000000..66b43f7639 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectInputAttribute.cs @@ -0,0 +1,28 @@ +namespace Discord.Interactions; + +/// +/// Base attribute for select-menu, user, channel, role, and mentionable select inputs in modals. +/// +public abstract class ModalSelectInputAttribute : ModalInputAttribute +{ + /// + /// Gets or sets the minimum number of values that can be selected. + /// + public int MinValues { get; set; } = 1; + + /// + /// Gets or sets the maximum number of values that can be selected. + /// + public int MaxValues { get; set; } = 1; + + /// + /// Gets or sets the placeholder text. + /// + public string Placeholder { get; set; } + + internal ModalSelectInputAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId) + { + MinValues = minValues; + MaxValues = maxValues; + } +} diff --git a/src/Discord.Net.Interactions/Attributes/Modals/SelectInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/SelectInputAttribute.cs deleted file mode 100644 index 7106125eb5..0000000000 --- a/src/Discord.Net.Interactions/Attributes/Modals/SelectInputAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Discord.Interactions.Attributes.Modals; - -public abstract class SelectInputAttribute : ModalInputAttribute -{ - public int MinValues { get; set; } = 1; - - public int MaxValues { get; set; } = 1; - - public string Placeholder { get; set; } - - public SelectInputAttribute(string customId) : base(customId) - { - } -} From 7e878a82007cc4d88246cc5b3be7937a14dde617 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Mon, 10 Nov 2025 10:10:13 +0100 Subject: [PATCH 26/59] refactor select input attribute and add description property in moduleClassBuilder --- .../Builders/ModuleClassBuilder.cs | 1351 ++++++++--------- 1 file changed, 675 insertions(+), 676 deletions(-) diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index 0084570ef8..bed6417a7f 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -1,762 +1,762 @@ using Discord.Interactions.Attributes.Modals; -using Discord.Interactions.Builders.Modals.Inputs; -using Discord.Interactions.Info.InputComponents; using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace Discord.Interactions.Builders +namespace Discord.Interactions.Builders; + +internal static class ModuleClassBuilder { - internal static class ModuleClassBuilder - { - private static readonly TypeInfo ModuleTypeInfo = typeof(IInteractionModuleBase).GetTypeInfo(); + private static readonly TypeInfo ModuleTypeInfo = typeof(IInteractionModuleBase).GetTypeInfo(); - public const int MaxCommandDepth = 3; + public const int MaxCommandDepth = 3; - public static async Task> SearchAsync(Assembly assembly, InteractionService commandService) + public static async Task> SearchAsync(Assembly assembly, InteractionService commandService) + { + static bool IsLoadableModule(TypeInfo info) => + !info.IsAbstract && + info.DeclaredMethods + .SelectMany(x => x.GetCustomAttributes()) + .Any(x => x is SlashCommandAttribute + or ComponentInteractionAttribute + or ContextCommandAttribute + or AutocompleteCommandAttribute + or ModalInteractionAttribute); + + var result = new List(); + + foreach (var type in assembly.DefinedTypes) { - static bool IsLoadableModule(TypeInfo info) => - !info.IsAbstract && - info.DeclaredMethods - .SelectMany(x => x.GetCustomAttributes()) - .Any(x => x is SlashCommandAttribute - or ComponentInteractionAttribute - or ContextCommandAttribute - or AutocompleteCommandAttribute - or ModalInteractionAttribute); - - var result = new List(); - - foreach (var type in assembly.DefinedTypes) + if ((type.IsPublic || type.IsNestedPublic) && IsValidModuleDefinition(type)) { - if ((type.IsPublic || type.IsNestedPublic) && IsValidModuleDefinition(type)) - { - result.Add(type); - } - else if (IsLoadableModule(type)) - { - await commandService._cmdLogger.WarningAsync($"Class {type.FullName} is not public and cannot be loaded.").ConfigureAwait(false); - } + result.Add(type); + } + else if (IsLoadableModule(type)) + { + await commandService._cmdLogger.WarningAsync($"Class {type.FullName} is not public and cannot be loaded.").ConfigureAwait(false); } - return result; } + return result; + } - public static async Task> BuildAsync(IEnumerable validTypes, InteractionService commandService, - IServiceProvider services) - { - var topLevelGroups = validTypes.Where(x => x.DeclaringType == null || !IsValidModuleDefinition(x.DeclaringType.GetTypeInfo())); - var built = new List(); + public static async Task> BuildAsync(IEnumerable validTypes, InteractionService commandService, + IServiceProvider services) + { + var topLevelGroups = validTypes.Where(x => x.DeclaringType == null || !IsValidModuleDefinition(x.DeclaringType.GetTypeInfo())); + var built = new List(); - var result = new Dictionary(); + var result = new Dictionary(); - foreach (var type in topLevelGroups) - { - var builder = new ModuleBuilder(commandService); + foreach (var type in topLevelGroups) + { + var builder = new ModuleBuilder(commandService); - BuildModule(builder, type, commandService, services); - BuildSubModules(builder, type.DeclaredNestedTypes, built, commandService, services); - built.Add(type); + BuildModule(builder, type, commandService, services); + BuildSubModules(builder, type.DeclaredNestedTypes, built, commandService, services); + built.Add(type); - var moduleInfo = builder.Build(commandService, services); + var moduleInfo = builder.Build(commandService, services); - result.Add(type.AsType(), moduleInfo); - } + result.Add(type.AsType(), moduleInfo); + } - await commandService._cmdLogger.DebugAsync($"Successfully built {built.Count} interaction modules.").ConfigureAwait(false); + await commandService._cmdLogger.DebugAsync($"Successfully built {built.Count} interaction modules.").ConfigureAwait(false); - return result; - } + return result; + } - private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, InteractionService commandService, - IServiceProvider services) - { - var attributes = typeInfo.GetCustomAttributes(); + private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, InteractionService commandService, + IServiceProvider services) + { + var attributes = typeInfo.GetCustomAttributes(); - builder.Name = typeInfo.Name; - builder.TypeInfo = typeInfo; + builder.Name = typeInfo.Name; + builder.TypeInfo = typeInfo; - foreach (var attribute in attributes) + foreach (var attribute in attributes) + { + switch (attribute) { - switch (attribute) - { - case GroupAttribute group: - { - builder.SlashGroupName = group.Name; - builder.Description = group.Description; - } - break; + case GroupAttribute group: + { + builder.SlashGroupName = group.Name; + builder.Description = group.Description; + } + break; #pragma warning disable CS0618 // Type or member is obsolete - case DefaultPermissionAttribute defPermission: - { - builder.DefaultPermission = defPermission.IsDefaultPermission; - } - break; + case DefaultPermissionAttribute defPermission: + { + builder.DefaultPermission = defPermission.IsDefaultPermission; + } + break; #pragma warning restore CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete - case EnabledInDmAttribute enabledInDm: + case EnabledInDmAttribute enabledInDm: { - builder.IsEnabledInDm = enabledInDm.IsEnabled; - } - break; + builder.IsEnabledInDm = enabledInDm.IsEnabled; + } + break; #pragma warning restore CS0618 // Type or member is obsolete - case DefaultMemberPermissionsAttribute memberPermission: - { - builder.DefaultMemberPermissions = memberPermission.Permissions; - } - break; - case PreconditionAttribute precondition: - builder.AddPreconditions(precondition); - break; - case DontAutoRegisterAttribute dontAutoRegister: - builder.DontAutoRegister = true; - break; - case NsfwCommandAttribute nsfwCommand: - builder.SetNsfw(nsfwCommand.IsNsfw); - break; - case CommandContextTypeAttribute contextType: - builder.WithContextTypes(contextType.ContextTypes?.ToArray()); - break; - case IntegrationTypeAttribute integrationType: - builder.WithIntegrationTypes(integrationType.IntegrationTypes?.ToArray()); - break; - default: - builder.AddAttributes(attribute); - break; - } + case DefaultMemberPermissionsAttribute memberPermission: + { + builder.DefaultMemberPermissions = memberPermission.Permissions; + } + break; + case PreconditionAttribute precondition: + builder.AddPreconditions(precondition); + break; + case DontAutoRegisterAttribute dontAutoRegister: + builder.DontAutoRegister = true; + break; + case NsfwCommandAttribute nsfwCommand: + builder.SetNsfw(nsfwCommand.IsNsfw); + break; + case CommandContextTypeAttribute contextType: + builder.WithContextTypes(contextType.ContextTypes?.ToArray()); + break; + case IntegrationTypeAttribute integrationType: + builder.WithIntegrationTypes(integrationType.IntegrationTypes?.ToArray()); + break; + default: + builder.AddAttributes(attribute); + break; } + } - var methods = typeInfo.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + var methods = typeInfo.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - var validSlashCommands = methods.Where(IsValidSlashCommandDefinition); - var validContextCommands = methods.Where(IsValidContextCommandDefinition); - var validInteractions = methods.Where(IsValidComponentCommandDefinition); - var validAutocompleteCommands = methods.Where(IsValidAutocompleteCommandDefinition); - var validModalCommands = methods.Where(IsValidModalCommandDefinition); + var validSlashCommands = methods.Where(IsValidSlashCommandDefinition); + var validContextCommands = methods.Where(IsValidContextCommandDefinition); + var validInteractions = methods.Where(IsValidComponentCommandDefinition); + var validAutocompleteCommands = methods.Where(IsValidAutocompleteCommandDefinition); + var validModalCommands = methods.Where(IsValidModalCommandDefinition); - Func createInstance = commandService._useCompiledLambda ? - ReflectionUtils.CreateLambdaBuilder(typeInfo, commandService) : ReflectionUtils.CreateBuilder(typeInfo, commandService); + Func createInstance = commandService._useCompiledLambda ? + ReflectionUtils.CreateLambdaBuilder(typeInfo, commandService) : ReflectionUtils.CreateBuilder(typeInfo, commandService); - foreach (var method in validSlashCommands) - builder.AddSlashCommand(x => BuildSlashCommand(x, createInstance, method, commandService, services)); + foreach (var method in validSlashCommands) + builder.AddSlashCommand(x => BuildSlashCommand(x, createInstance, method, commandService, services)); - foreach (var method in validContextCommands) - builder.AddContextCommand(x => BuildContextCommand(x, createInstance, method, commandService, services)); + foreach (var method in validContextCommands) + builder.AddContextCommand(x => BuildContextCommand(x, createInstance, method, commandService, services)); - foreach (var method in validInteractions) - builder.AddComponentCommand(x => BuildComponentCommand(x, createInstance, method, commandService, services)); + foreach (var method in validInteractions) + builder.AddComponentCommand(x => BuildComponentCommand(x, createInstance, method, commandService, services)); - foreach (var method in validAutocompleteCommands) - builder.AddAutocompleteCommand(x => BuildAutocompleteCommand(x, createInstance, method, commandService, services)); + foreach (var method in validAutocompleteCommands) + builder.AddAutocompleteCommand(x => BuildAutocompleteCommand(x, createInstance, method, commandService, services)); - foreach (var method in validModalCommands) - builder.AddModalCommand(x => BuildModalCommand(x, createInstance, method, commandService, services)); - } + foreach (var method in validModalCommands) + builder.AddModalCommand(x => BuildModalCommand(x, createInstance, method, commandService, services)); + } - private static void BuildSubModules(ModuleBuilder parent, IEnumerable subModules, IList builtTypes, InteractionService commandService, - IServiceProvider services, int slashGroupDepth = 0) + private static void BuildSubModules(ModuleBuilder parent, IEnumerable subModules, IList builtTypes, InteractionService commandService, + IServiceProvider services, int slashGroupDepth = 0) + { + foreach (var submodule in subModules.Where(IsValidModuleDefinition)) { - foreach (var submodule in subModules.Where(IsValidModuleDefinition)) - { - if (builtTypes.Contains(submodule)) - continue; + if (builtTypes.Contains(submodule)) + continue; - parent.AddModule((builder) => - { - BuildModule(builder, submodule, commandService, services); + parent.AddModule((builder) => + { + BuildModule(builder, submodule, commandService, services); - if (slashGroupDepth >= MaxCommandDepth - 1) - throw new InvalidOperationException($"Slash Commands only support {MaxCommandDepth - 1} command prefixes for sub-commands"); + if (slashGroupDepth >= MaxCommandDepth - 1) + throw new InvalidOperationException($"Slash Commands only support {MaxCommandDepth - 1} command prefixes for sub-commands"); - BuildSubModules(builder, submodule.DeclaredNestedTypes, builtTypes, commandService, services, builder.IsSlashGroup ? slashGroupDepth + 1 : slashGroupDepth); - }); - builtTypes.Add(submodule); - } + BuildSubModules(builder, submodule.DeclaredNestedTypes, builtTypes, commandService, services, builder.IsSlashGroup ? slashGroupDepth + 1 : slashGroupDepth); + }); + builtTypes.Add(submodule); } + } - private static void BuildSlashCommand(SlashCommandBuilder builder, Func createInstance, MethodInfo methodInfo, - InteractionService commandService, IServiceProvider services) - { - var attributes = methodInfo.GetCustomAttributes(); + private static void BuildSlashCommand(SlashCommandBuilder builder, Func createInstance, MethodInfo methodInfo, + InteractionService commandService, IServiceProvider services) + { + var attributes = methodInfo.GetCustomAttributes(); - builder.MethodName = methodInfo.Name; + builder.MethodName = methodInfo.Name; - foreach (var attribute in attributes) + foreach (var attribute in attributes) + { + switch (attribute) { - switch (attribute) - { - case SlashCommandAttribute command: - { - builder.Name = command.Name; - builder.Description = command.Description; - builder.IgnoreGroupNames = command.IgnoreGroupNames; - builder.RunMode = command.RunMode; - } - break; + case SlashCommandAttribute command: + { + builder.Name = command.Name; + builder.Description = command.Description; + builder.IgnoreGroupNames = command.IgnoreGroupNames; + builder.RunMode = command.RunMode; + } + break; #pragma warning disable CS0618 // Type or member is obsolete - case DefaultPermissionAttribute defaultPermission: - { - builder.DefaultPermission = defaultPermission.IsDefaultPermission; - } - break; - case EnabledInDmAttribute enabledInDm: - { - builder.IsEnabledInDm = enabledInDm.IsEnabled; - } - break; + case DefaultPermissionAttribute defaultPermission: + { + builder.DefaultPermission = defaultPermission.IsDefaultPermission; + } + break; + case EnabledInDmAttribute enabledInDm: + { + builder.IsEnabledInDm = enabledInDm.IsEnabled; + } + break; #pragma warning restore CS0618 // Type or member is obsolete - case DefaultMemberPermissionsAttribute memberPermission: - { - builder.DefaultMemberPermissions = memberPermission.Permissions; - } - break; - case PreconditionAttribute precondition: - builder.WithPreconditions(precondition); - break; - case NsfwCommandAttribute nsfwCommand: - builder.SetNsfw(nsfwCommand.IsNsfw); - break; - case CommandContextTypeAttribute contextType: - builder.WithContextTypes(contextType.ContextTypes.ToArray()); - break; - case IntegrationTypeAttribute integrationType: - builder.WithIntegrationTypes(integrationType.IntegrationTypes.ToArray()); - break; - default: - builder.WithAttributes(attribute); - break; - } + case DefaultMemberPermissionsAttribute memberPermission: + { + builder.DefaultMemberPermissions = memberPermission.Permissions; + } + break; + case PreconditionAttribute precondition: + builder.WithPreconditions(precondition); + break; + case NsfwCommandAttribute nsfwCommand: + builder.SetNsfw(nsfwCommand.IsNsfw); + break; + case CommandContextTypeAttribute contextType: + builder.WithContextTypes(contextType.ContextTypes.ToArray()); + break; + case IntegrationTypeAttribute integrationType: + builder.WithIntegrationTypes(integrationType.IntegrationTypes.ToArray()); + break; + default: + builder.WithAttributes(attribute); + break; } + } - var parameters = methodInfo.GetParameters(); + var parameters = methodInfo.GetParameters(); - foreach (var parameter in parameters) - builder.AddParameter(x => BuildSlashParameter(x, parameter, services)); + foreach (var parameter in parameters) + builder.AddParameter(x => BuildSlashParameter(x, parameter, services)); - builder.Callback = CreateCallback(createInstance, methodInfo, commandService); - } + builder.Callback = CreateCallback(createInstance, methodInfo, commandService); + } - private static void BuildContextCommand(ContextCommandBuilder builder, Func createInstance, MethodInfo methodInfo, - InteractionService commandService, IServiceProvider services) - { - var attributes = methodInfo.GetCustomAttributes(); + private static void BuildContextCommand(ContextCommandBuilder builder, Func createInstance, MethodInfo methodInfo, + InteractionService commandService, IServiceProvider services) + { + var attributes = methodInfo.GetCustomAttributes(); - builder.MethodName = methodInfo.Name; + builder.MethodName = methodInfo.Name; - foreach (var attribute in attributes) + foreach (var attribute in attributes) + { + switch (attribute) { - switch (attribute) - { - case ContextCommandAttribute command: - { - builder.Name = command.Name; - builder.CommandType = command.CommandType; - builder.RunMode = command.RunMode; + case ContextCommandAttribute command: + { + builder.Name = command.Name; + builder.CommandType = command.CommandType; + builder.RunMode = command.RunMode; - command.CheckMethodDefinition(methodInfo); - } - break; + command.CheckMethodDefinition(methodInfo); + } + break; #pragma warning disable CS0618 // Type or member is obsolete - case DefaultPermissionAttribute defaultPermission: - { - builder.DefaultPermission = defaultPermission.IsDefaultPermission; - } - break; - case EnabledInDmAttribute enabledInDm: - { - builder.IsEnabledInDm = enabledInDm.IsEnabled; - } - break; + case DefaultPermissionAttribute defaultPermission: + { + builder.DefaultPermission = defaultPermission.IsDefaultPermission; + } + break; + case EnabledInDmAttribute enabledInDm: + { + builder.IsEnabledInDm = enabledInDm.IsEnabled; + } + break; #pragma warning restore CS0618 // Type or member is obsolete - case DefaultMemberPermissionsAttribute memberPermission: - { - builder.DefaultMemberPermissions = memberPermission.Permissions; - } - break; - case PreconditionAttribute precondition: - builder.WithPreconditions(precondition); - break; - case NsfwCommandAttribute nsfwCommand: - builder.SetNsfw(nsfwCommand.IsNsfw); - break; - case CommandContextTypeAttribute contextType: - builder.WithContextTypes(contextType.ContextTypes.ToArray()); - break; - case IntegrationTypeAttribute integrationType: - builder.WithIntegrationTypes(integrationType.IntegrationTypes.ToArray()); - break; - default: - builder.WithAttributes(attribute); - break; - } + case DefaultMemberPermissionsAttribute memberPermission: + { + builder.DefaultMemberPermissions = memberPermission.Permissions; + } + break; + case PreconditionAttribute precondition: + builder.WithPreconditions(precondition); + break; + case NsfwCommandAttribute nsfwCommand: + builder.SetNsfw(nsfwCommand.IsNsfw); + break; + case CommandContextTypeAttribute contextType: + builder.WithContextTypes(contextType.ContextTypes.ToArray()); + break; + case IntegrationTypeAttribute integrationType: + builder.WithIntegrationTypes(integrationType.IntegrationTypes.ToArray()); + break; + default: + builder.WithAttributes(attribute); + break; } + } - var parameters = methodInfo.GetParameters(); + var parameters = methodInfo.GetParameters(); - foreach (var parameter in parameters) - builder.AddParameter(x => BuildParameter(x, parameter)); + foreach (var parameter in parameters) + builder.AddParameter(x => BuildParameter(x, parameter)); - builder.Callback = CreateCallback(createInstance, methodInfo, commandService); - } + builder.Callback = CreateCallback(createInstance, methodInfo, commandService); + } - private static void BuildComponentCommand(ComponentCommandBuilder builder, Func createInstance, MethodInfo methodInfo, - InteractionService commandService, IServiceProvider services) - { - var attributes = methodInfo.GetCustomAttributes(); + private static void BuildComponentCommand(ComponentCommandBuilder builder, Func createInstance, MethodInfo methodInfo, + InteractionService commandService, IServiceProvider services) + { + var attributes = methodInfo.GetCustomAttributes(); - builder.MethodName = methodInfo.Name; + builder.MethodName = methodInfo.Name; - foreach (var attribute in attributes) + foreach (var attribute in attributes) + { + switch (attribute) { - switch (attribute) - { - case ComponentInteractionAttribute interaction: - { - builder.Name = interaction.CustomId; - builder.RunMode = interaction.RunMode; - builder.IgnoreGroupNames = interaction.IgnoreGroupNames; - builder.TreatNameAsRegex = interaction.TreatAsRegex; - } - break; - case PreconditionAttribute precondition: - builder.WithPreconditions(precondition); - break; - default: - builder.WithAttributes(attribute); - break; - } + case ComponentInteractionAttribute interaction: + { + builder.Name = interaction.CustomId; + builder.RunMode = interaction.RunMode; + builder.IgnoreGroupNames = interaction.IgnoreGroupNames; + builder.TreatNameAsRegex = interaction.TreatAsRegex; + } + break; + case PreconditionAttribute precondition: + builder.WithPreconditions(precondition); + break; + default: + builder.WithAttributes(attribute); + break; } + } - var parameters = methodInfo.GetParameters(); + var parameters = methodInfo.GetParameters(); - var wildCardCount = RegexUtils.GetWildCardCount(builder.Name, commandService._wildCardExp); + var wildCardCount = RegexUtils.GetWildCardCount(builder.Name, commandService._wildCardExp); - foreach (var parameter in parameters) - builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount)); + foreach (var parameter in parameters) + builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount)); - builder.Callback = CreateCallback(createInstance, methodInfo, commandService); - } + builder.Callback = CreateCallback(createInstance, methodInfo, commandService); + } - private static void BuildAutocompleteCommand(AutocompleteCommandBuilder builder, Func createInstance, MethodInfo methodInfo, - InteractionService commandService, IServiceProvider services) - { - var attributes = methodInfo.GetCustomAttributes(); + private static void BuildAutocompleteCommand(AutocompleteCommandBuilder builder, Func createInstance, MethodInfo methodInfo, + InteractionService commandService, IServiceProvider services) + { + var attributes = methodInfo.GetCustomAttributes(); - builder.MethodName = methodInfo.Name; + builder.MethodName = methodInfo.Name; - foreach (var attribute in attributes) + foreach (var attribute in attributes) + { + switch (attribute) { - switch (attribute) - { - case AutocompleteCommandAttribute autocomplete: - { - builder.ParameterName = autocomplete.ParameterName; - builder.CommandName = autocomplete.CommandName; - builder.Name = autocomplete.CommandName + " " + autocomplete.ParameterName; - builder.RunMode = autocomplete.RunMode; - } - break; - case PreconditionAttribute precondition: - builder.WithPreconditions(precondition); - break; - default: - builder.WithAttributes(attribute); - break; - } + case AutocompleteCommandAttribute autocomplete: + { + builder.ParameterName = autocomplete.ParameterName; + builder.CommandName = autocomplete.CommandName; + builder.Name = autocomplete.CommandName + " " + autocomplete.ParameterName; + builder.RunMode = autocomplete.RunMode; + } + break; + case PreconditionAttribute precondition: + builder.WithPreconditions(precondition); + break; + default: + builder.WithAttributes(attribute); + break; } + } - var parameters = methodInfo.GetParameters(); + var parameters = methodInfo.GetParameters(); - foreach (var parameter in parameters) - builder.AddParameter(x => BuildParameter(x, parameter)); + foreach (var parameter in parameters) + builder.AddParameter(x => BuildParameter(x, parameter)); - builder.Callback = CreateCallback(createInstance, methodInfo, commandService); - } + builder.Callback = CreateCallback(createInstance, methodInfo, commandService); + } - private static void BuildModalCommand(ModalCommandBuilder builder, Func createInstance, MethodInfo methodInfo, - InteractionService commandService, IServiceProvider services) - { - var parameters = methodInfo.GetParameters(); + private static void BuildModalCommand(ModalCommandBuilder builder, Func createInstance, MethodInfo methodInfo, + InteractionService commandService, IServiceProvider services) + { + var parameters = methodInfo.GetParameters(); - if (parameters.Count(x => typeof(IModal).IsAssignableFrom(x.ParameterType)) > 1) - throw new InvalidOperationException($"A modal command can only have one {nameof(IModal)} parameter."); + if (parameters.Count(x => typeof(IModal).IsAssignableFrom(x.ParameterType)) > 1) + throw new InvalidOperationException($"A modal command can only have one {nameof(IModal)} parameter."); - if (!typeof(IModal).IsAssignableFrom(parameters.Last().ParameterType)) - throw new InvalidOperationException($"Last parameter of a modal command must be an implementation of {nameof(IModal)}"); + if (!typeof(IModal).IsAssignableFrom(parameters.Last().ParameterType)) + throw new InvalidOperationException($"Last parameter of a modal command must be an implementation of {nameof(IModal)}"); - var attributes = methodInfo.GetCustomAttributes(); + var attributes = methodInfo.GetCustomAttributes(); - builder.MethodName = methodInfo.Name; + builder.MethodName = methodInfo.Name; - foreach (var attribute in attributes) + foreach (var attribute in attributes) + { + switch (attribute) { - switch (attribute) - { - case ModalInteractionAttribute modal: - { - builder.Name = modal.CustomId; - builder.RunMode = modal.RunMode; - builder.IgnoreGroupNames = modal.IgnoreGroupNames; - builder.TreatNameAsRegex = modal.TreatAsRegex; - } - break; - case PreconditionAttribute precondition: - builder.WithPreconditions(precondition); - break; - default: - builder.WithAttributes(attribute); - break; - } + case ModalInteractionAttribute modal: + { + builder.Name = modal.CustomId; + builder.RunMode = modal.RunMode; + builder.IgnoreGroupNames = modal.IgnoreGroupNames; + builder.TreatNameAsRegex = modal.TreatAsRegex; + } + break; + case PreconditionAttribute precondition: + builder.WithPreconditions(precondition); + break; + default: + builder.WithAttributes(attribute); + break; } + } - foreach (var parameter in parameters) - builder.AddParameter(x => BuildParameter(x, parameter)); + foreach (var parameter in parameters) + builder.AddParameter(x => BuildParameter(x, parameter)); - builder.Callback = CreateCallback(createInstance, methodInfo, commandService); - } + builder.Callback = CreateCallback(createInstance, methodInfo, commandService); + } + + private static ExecuteCallback CreateCallback(Func createInstance, + MethodInfo methodInfo, InteractionService commandService) + { + Func commandInvoker = commandService._useCompiledLambda ? + ReflectionUtils.CreateMethodInvoker(methodInfo) : (module, args) => methodInfo.Invoke(module, args) as Task; - private static ExecuteCallback CreateCallback(Func createInstance, - MethodInfo methodInfo, InteractionService commandService) + async Task ExecuteCallback(IInteractionContext context, object[] args, IServiceProvider serviceProvider, ICommandInfo commandInfo) { - Func commandInvoker = commandService._useCompiledLambda ? - ReflectionUtils.CreateMethodInvoker(methodInfo) : (module, args) => methodInfo.Invoke(module, args) as Task; + var instance = createInstance(serviceProvider); + instance.SetContext(context); - async Task ExecuteCallback(IInteractionContext context, object[] args, IServiceProvider serviceProvider, ICommandInfo commandInfo) + try { - var instance = createInstance(serviceProvider); - instance.SetContext(context); - - try - { - await instance.BeforeExecuteAsync(commandInfo).ConfigureAwait(false); - instance.BeforeExecute(commandInfo); - var task = commandInvoker(instance, args) ?? Task.CompletedTask; + await instance.BeforeExecuteAsync(commandInfo).ConfigureAwait(false); + instance.BeforeExecute(commandInfo); + var task = commandInvoker(instance, args) ?? Task.CompletedTask; - if (task is Task runtimeTask) - { - return await runtimeTask.ConfigureAwait(false); - } - else - { - await task.ConfigureAwait(false); - return ExecuteResult.FromSuccess(); - - } - } - catch (Exception ex) + if (task is Task runtimeTask) { - var interactionException = new InteractionException(commandInfo, context, ex); - await commandService._cmdLogger.ErrorAsync(interactionException).ConfigureAwait(false); - return ExecuteResult.FromError(interactionException); + return await runtimeTask.ConfigureAwait(false); } - finally + else { - await instance.AfterExecuteAsync(commandInfo).ConfigureAwait(false); - instance.AfterExecute(commandInfo); - (instance as IDisposable)?.Dispose(); + await task.ConfigureAwait(false); + return ExecuteResult.FromSuccess(); + } } - - return ExecuteCallback; + catch (Exception ex) + { + var interactionException = new InteractionException(commandInfo, context, ex); + await commandService._cmdLogger.ErrorAsync(interactionException).ConfigureAwait(false); + return ExecuteResult.FromError(interactionException); + } + finally + { + await instance.AfterExecuteAsync(commandInfo).ConfigureAwait(false); + instance.AfterExecute(commandInfo); + (instance as IDisposable)?.Dispose(); + } } - #region Parameters - private static void BuildSlashParameter(SlashCommandParameterBuilder builder, ParameterInfo paramInfo, IServiceProvider services) - { - var attributes = paramInfo.GetCustomAttributes(); - var paramType = paramInfo.ParameterType; + return ExecuteCallback; + } - builder.Name = paramInfo.Name; - builder.Description = paramInfo.Name; - builder.IsRequired = !paramInfo.IsOptional; - builder.DefaultValue = paramInfo.DefaultValue; + #region Parameters + private static void BuildSlashParameter(SlashCommandParameterBuilder builder, ParameterInfo paramInfo, IServiceProvider services) + { + var attributes = paramInfo.GetCustomAttributes(); + var paramType = paramInfo.ParameterType; - var supportedNumericalRange = paramInfo.GetSupportedNumericalRange(); - builder.MinValue = supportedNumericalRange.Min; - builder.MaxValue = supportedNumericalRange.Max; + builder.Name = paramInfo.Name; + builder.Description = paramInfo.Name; + builder.IsRequired = !paramInfo.IsOptional; + builder.DefaultValue = paramInfo.DefaultValue; - foreach (var attribute in attributes) - { - switch (attribute) - { - case SummaryAttribute description: - { - if (!string.IsNullOrEmpty(description.Name)) - builder.Name = description.Name; + var supportedNumericalRange = paramInfo.GetSupportedNumericalRange(); + builder.MinValue = supportedNumericalRange.Min; + builder.MaxValue = supportedNumericalRange.Max; - if (!string.IsNullOrEmpty(description.Description)) - builder.Description = description.Description; - } - break; - case ChoiceAttribute choice: - builder.WithChoices(new ParameterChoice(choice.Name, choice.Value)); - break; - case ParamArrayAttribute _: - builder.IsParameterArray = true; - break; - case ParameterPreconditionAttribute precondition: - builder.AddPreconditions(precondition); - break; - case ChannelTypesAttribute channelTypes: - builder.WithChannelTypes(channelTypes.ChannelTypes); - break; - case AutocompleteAttribute autocomplete: - builder.Autocomplete = true; - if (autocomplete.AutocompleteHandlerType is not null) - builder.WithAutocompleteHandler(autocomplete.AutocompleteHandlerType, services); - break; - case MaxValueAttribute maxValue: - if (maxValue.Value > supportedNumericalRange.Max) - throw new ArgumentOutOfRangeException($"{nameof(maxValue)} cannot be greater than {supportedNumericalRange.Max}."); + foreach (var attribute in attributes) + { + switch (attribute) + { + case SummaryAttribute description: + { + if (!string.IsNullOrEmpty(description.Name)) + builder.Name = description.Name; - builder.MaxValue = maxValue.Value; - break; - case MinValueAttribute minValue: - if (minValue.Value < supportedNumericalRange.Min) - throw new ArgumentOutOfRangeException($"{nameof(minValue)} cannot be less than {supportedNumericalRange.Min}."); + if (!string.IsNullOrEmpty(description.Description)) + builder.Description = description.Description; + } + break; + case ChoiceAttribute choice: + builder.WithChoices(new ParameterChoice(choice.Name, choice.Value)); + break; + case ParamArrayAttribute _: + builder.IsParameterArray = true; + break; + case ParameterPreconditionAttribute precondition: + builder.AddPreconditions(precondition); + break; + case ChannelTypesAttribute channelTypes: + builder.WithChannelTypes(channelTypes.ChannelTypes); + break; + case AutocompleteAttribute autocomplete: + builder.Autocomplete = true; + if (autocomplete.AutocompleteHandlerType is not null) + builder.WithAutocompleteHandler(autocomplete.AutocompleteHandlerType, services); + break; + case MaxValueAttribute maxValue: + if (maxValue.Value > supportedNumericalRange.Max) + throw new ArgumentOutOfRangeException($"{nameof(maxValue)} cannot be greater than {supportedNumericalRange.Max}."); - builder.MinValue = minValue.Value; - break; - case MinLengthAttribute minLength: - builder.MinLength = minLength.Length; - break; - case MaxLengthAttribute maxLength: - builder.MaxLength = maxLength.Length; - break; - case ComplexParameterAttribute complexParameter: - { - builder.IsComplexParameter = true; - ConstructorInfo ctor = GetComplexParameterConstructor(paramInfo.ParameterType.GetTypeInfo(), complexParameter); + builder.MaxValue = maxValue.Value; + break; + case MinValueAttribute minValue: + if (minValue.Value < supportedNumericalRange.Min) + throw new ArgumentOutOfRangeException($"{nameof(minValue)} cannot be less than {supportedNumericalRange.Min}."); - foreach (var parameter in ctor.GetParameters()) - { - if (parameter.IsDefined(typeof(ComplexParameterAttribute))) - throw new InvalidOperationException("You cannot create nested complex parameters."); + builder.MinValue = minValue.Value; + break; + case MinLengthAttribute minLength: + builder.MinLength = minLength.Length; + break; + case MaxLengthAttribute maxLength: + builder.MaxLength = maxLength.Length; + break; + case ComplexParameterAttribute complexParameter: + { + builder.IsComplexParameter = true; + ConstructorInfo ctor = GetComplexParameterConstructor(paramInfo.ParameterType.GetTypeInfo(), complexParameter); - builder.AddComplexParameterField(fieldBuilder => BuildSlashParameter(fieldBuilder, parameter, services)); - } + foreach (var parameter in ctor.GetParameters()) + { + if (parameter.IsDefined(typeof(ComplexParameterAttribute))) + throw new InvalidOperationException("You cannot create nested complex parameters."); - var initializer = builder.Command.Module.InteractionService._useCompiledLambda ? - ReflectionUtils.CreateLambdaConstructorInvoker(paramInfo.ParameterType.GetTypeInfo()) : ctor.Invoke; - builder.ComplexParameterInitializer = args => initializer(args); + builder.AddComplexParameterField(fieldBuilder => BuildSlashParameter(fieldBuilder, parameter, services)); } - break; - default: - builder.AddAttributes(attribute); - break; - } + + var initializer = builder.Command.Module.InteractionService._useCompiledLambda ? + ReflectionUtils.CreateLambdaConstructorInvoker(paramInfo.ParameterType.GetTypeInfo()) : ctor.Invoke; + builder.ComplexParameterInitializer = args => initializer(args); + } + break; + default: + builder.AddAttributes(attribute); + break; } + } - builder.SetParameterType(paramType, services); + builder.SetParameterType(paramType, services); - // Replace pascal casings with '-' - builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower(); - } + // Replace pascal casings with '-' + builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower(); + } - private static void BuildComponentParameter(ComponentCommandParameterBuilder builder, ParameterInfo paramInfo, bool isComponentParam) - { - builder.SetIsRouteSegment(!isComponentParam); - BuildParameter(builder, paramInfo); - } + private static void BuildComponentParameter(ComponentCommandParameterBuilder builder, ParameterInfo paramInfo, bool isComponentParam) + { + builder.SetIsRouteSegment(!isComponentParam); + BuildParameter(builder, paramInfo); + } - private static void BuildParameter(ParameterBuilder builder, ParameterInfo paramInfo) - where TInfo : class, IParameterInfo - where TBuilder : ParameterBuilder - { - var attributes = paramInfo.GetCustomAttributes(); - var paramType = paramInfo.ParameterType; + private static void BuildParameter(ParameterBuilder builder, ParameterInfo paramInfo) + where TInfo : class, IParameterInfo + where TBuilder : ParameterBuilder + { + var attributes = paramInfo.GetCustomAttributes(); + var paramType = paramInfo.ParameterType; - builder.Name = paramInfo.Name; - builder.IsRequired = !paramInfo.IsOptional; - builder.DefaultValue = paramInfo.DefaultValue; - builder.SetParameterType(paramType); + builder.Name = paramInfo.Name; + builder.IsRequired = !paramInfo.IsOptional; + builder.DefaultValue = paramInfo.DefaultValue; + builder.SetParameterType(paramType); - foreach (var attribute in attributes) + foreach (var attribute in attributes) + { + switch (attribute) { - switch (attribute) - { - case ParameterPreconditionAttribute precondition: - builder.AddPreconditions(precondition); - break; - case ParamArrayAttribute _: - builder.IsParameterArray = true; - break; - default: - builder.AddAttributes(attribute); - break; - } + case ParameterPreconditionAttribute precondition: + builder.AddPreconditions(precondition); + break; + case ParamArrayAttribute _: + builder.IsParameterArray = true; + break; + default: + builder.AddAttributes(attribute); + break; } } - #endregion + } + #endregion - #region Modals - public static ModalInfo BuildModalInfo(Type modalType, InteractionService interactionService) - { - if (!typeof(IModal).IsAssignableFrom(modalType)) - throw new InvalidOperationException($"{modalType.FullName} isn't an implementation of {typeof(IModal).FullName}"); + #region Modals + public static ModalInfo BuildModalInfo(Type modalType, InteractionService interactionService) + { + if (!typeof(IModal).IsAssignableFrom(modalType)) + throw new InvalidOperationException($"{modalType.FullName} isn't an implementation of {typeof(IModal).FullName}"); - var instance = Activator.CreateInstance(modalType, false) as IModal; + var instance = Activator.CreateInstance(modalType, false) as IModal; - try + try + { + var builder = new ModalBuilder(modalType, interactionService) { - var builder = new ModalBuilder(modalType, interactionService) - { - Title = instance.Title - }; + Title = instance.Title + }; - var inputs = modalType.GetProperties().Where(IsValidModalInputDefinition); + var inputs = modalType.GetProperties().Where(IsValidModalInputDefinition); - foreach (var prop in inputs) - { - var componentType = prop.GetCustomAttribute()?.ComponentType; + foreach (var prop in inputs) + { + var componentType = prop.GetCustomAttribute()?.ComponentType; - switch (componentType) - { - case ComponentType.TextInput: - builder.AddTextComponent(x => BuildTextInput(x, prop, prop.GetValue(instance))); - break; - case ComponentType.SelectMenu: - builder.AddSelectMenuComponent(x => BuildSelectMenuInput(x, prop, prop.GetValue(instance))); - break; - case ComponentType.UserSelect: - builder.AddUserSelectComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); - break; - case ComponentType.RoleSelect: - builder.AddRoleSelectComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); - break; - case ComponentType.MentionableSelect: - builder.AddMentionableSelectComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); - break; - case ComponentType.ChannelSelect: - builder.AddChannelSelectComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); - break; + switch (componentType) + { + case ComponentType.TextInput: + builder.AddTextComponent(x => BuildTextInput(x, prop, prop.GetValue(instance))); + break; + case ComponentType.SelectMenu: + builder.AddSelectMenuComponent(x => BuildSelectMenuInput(x, prop, prop.GetValue(instance))); + break; + case ComponentType.UserSelect: + builder.AddUserSelectComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + break; + case ComponentType.RoleSelect: + builder.AddRoleSelectComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + break; + case ComponentType.MentionableSelect: + builder.AddMentionableSelectComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + break; + case ComponentType.ChannelSelect: + builder.AddChannelSelectComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + break; case ComponentType.FileUpload: builder.AddFileUploadComponent(x => BuildFileUploadInput(x, prop, prop.GetValue(instance))); break; case ComponentType.TextDisplay: throw new NotImplementedException(); - case null: - throw new InvalidOperationException($"{prop.Name} of {prop.DeclaringType.Name} isn't a valid modal input field."); - default: - throw new InvalidOperationException($"Component type {componentType} cannot be used in modals."); - } + case null: + throw new InvalidOperationException($"{prop.Name} of {prop.DeclaringType.Name} isn't a valid modal input field."); + default: + throw new InvalidOperationException($"Component type {componentType} cannot be used in modals."); } - - var memberInit = ReflectionUtils.CreateLambdaMemberInit(modalType.GetTypeInfo(), modalType.GetConstructor(Type.EmptyTypes), x => x.IsDefined(typeof(ModalInputAttribute))); - builder.ModalInitializer = (args) => memberInit(Array.Empty(), args); - return builder.Build(); - } - finally - { - (instance as IDisposable)?.Dispose(); } - } - private static void BuildTextInput(TextInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) + var memberInit = ReflectionUtils.CreateLambdaMemberInit(modalType.GetTypeInfo(), modalType.GetConstructor(Type.EmptyTypes), x => x.IsDefined(typeof(ModalInputAttribute))); + builder.ModalInitializer = (args) => memberInit(Array.Empty(), args); + return builder.Build(); + } + finally { - var attributes = propertyInfo.GetCustomAttributes(); + (instance as IDisposable)?.Dispose(); + } + } + + private static void BuildTextInput(TextInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) + { + var attributes = propertyInfo.GetCustomAttributes(); - builder.Label = propertyInfo.Name; - builder.DefaultValue = defaultValue; - builder.WithType(propertyInfo.PropertyType); - builder.PropertyInfo = propertyInfo; + builder.Label = propertyInfo.Name; + builder.DefaultValue = defaultValue; + builder.WithType(propertyInfo.PropertyType); + builder.PropertyInfo = propertyInfo; - foreach (var attribute in attributes) + foreach (var attribute in attributes) + { + switch (attribute) { - switch (attribute) - { - case ModalTextInputAttribute textInput: - builder.CustomId = textInput.CustomId; - builder.ComponentType = textInput.ComponentType; - builder.Style = textInput.Style; - builder.Placeholder = textInput.Placeholder; - builder.MaxLength = textInput.MaxLength; - builder.MinLength = textInput.MinLength; - builder.InitialValue = textInput.InitialValue; - break; - case RequiredInputAttribute requiredInput: - builder.IsRequired = requiredInput.IsRequired; - break; - case InputLabelAttribute inputLabel: - builder.Label = inputLabel.Label; - break; - default: - builder.WithAttributes(attribute); - break; - } + case ModalTextInputAttribute textInput: + builder.CustomId = textInput.CustomId; + builder.ComponentType = textInput.ComponentType; + builder.Style = textInput.Style; + builder.Placeholder = textInput.Placeholder; + builder.MaxLength = textInput.MaxLength; + builder.MinLength = textInput.MinLength; + builder.InitialValue = textInput.InitialValue; + break; + case RequiredInputAttribute requiredInput: + builder.IsRequired = requiredInput.IsRequired; + break; + case InputLabelAttribute inputLabel: + builder.Label = inputLabel.Label; + builder.Description = inputLabel.Description; + break; + default: + builder.WithAttributes(attribute); + break; } } + } - private static void BuildSelectMenuInput(SelectMenuInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) - { - var attributes = propertyInfo.GetCustomAttributes(); + private static void BuildSelectMenuInput(SelectMenuInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) + { + var attributes = propertyInfo.GetCustomAttributes(); - builder.Label = propertyInfo.Name; - builder.DefaultValue = defaultValue; - builder.WithType(propertyInfo.PropertyType); - builder.PropertyInfo = propertyInfo; + builder.Label = propertyInfo.Name; + builder.DefaultValue = defaultValue; + builder.WithType(propertyInfo.PropertyType); + builder.PropertyInfo = propertyInfo; - foreach (var attribute in attributes) + foreach (var attribute in attributes) + { + switch (attribute) { - switch (attribute) - { - case ModalSelectMenuInputAttribute selectMenuInput: - builder.CustomId = selectMenuInput.CustomId; - builder.ComponentType = selectMenuInput.ComponentType; - builder.MinValues = selectMenuInput.MinValues; - builder.MaxValues = selectMenuInput.MaxValues; - builder.Placeholder = selectMenuInput.Placeholder; - break; - case RequiredInputAttribute requiredInput: - builder.IsRequired = requiredInput.IsRequired; - break; - case InputLabelAttribute inputLabel: - builder.Label = inputLabel.Label; - break; - default: - builder.WithAttributes(attribute); - break; - } + case ModalSelectMenuInputAttribute selectMenuInput: + builder.CustomId = selectMenuInput.CustomId; + builder.ComponentType = selectMenuInput.ComponentType; + builder.MinValues = selectMenuInput.MinValues; + builder.MaxValues = selectMenuInput.MaxValues; + builder.Placeholder = selectMenuInput.Placeholder; + break; + case RequiredInputAttribute requiredInput: + builder.IsRequired = requiredInput.IsRequired; + break; + case InputLabelAttribute inputLabel: + builder.Label = inputLabel.Label; + builder.Description = inputLabel.Description; + break; + default: + builder.WithAttributes(attribute); + break; } } + } - private static void BuildSnowflakeSelectInput(SnowflakeSelectInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) - where TInfo: SnowflakeSelectInputComponentInfo - where TBuilder: SnowflakeSelectInputComponentBuilder - { - var attributes = propertyInfo.GetCustomAttributes(); + private static void BuildSnowflakeSelectInput(SnowflakeSelectInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) + where TInfo : SnowflakeSelectInputComponentInfo + where TBuilder : SnowflakeSelectInputComponentBuilder + { + var attributes = propertyInfo.GetCustomAttributes(); - builder.Label = propertyInfo.Name; - builder.DefaultValue = defaultValue; - builder.WithType(propertyInfo.PropertyType); - builder.PropertyInfo = propertyInfo; + builder.Label = propertyInfo.Name; + builder.DefaultValue = defaultValue; + builder.WithType(propertyInfo.PropertyType); + builder.PropertyInfo = propertyInfo; - foreach (var attribute in attributes) + foreach (var attribute in attributes) + { + switch (attribute) { - switch (attribute) - { - case SelectInputAttribute selectInput: - builder.CustomId = selectInput.CustomId; - builder.ComponentType = selectInput.ComponentType; - builder.MinValues = selectInput.MinValues; - builder.MaxValues = selectInput.MaxValues; - builder.Placeholder = selectInput.Placeholder; - break; - case RequiredInputAttribute requiredInput: - builder.IsRequired = requiredInput.IsRequired; - break; - case InputLabelAttribute inputLabel: - builder.Label = inputLabel.Label; - break; - default: - builder.WithAttributes(attribute); - break; - } + case ModalSelectInputAttribute selectInput: + builder.CustomId = selectInput.CustomId; + builder.ComponentType = selectInput.ComponentType; + builder.MinValues = selectInput.MinValues; + builder.MaxValues = selectInput.MaxValues; + builder.Placeholder = selectInput.Placeholder; + break; + case RequiredInputAttribute requiredInput: + builder.IsRequired = requiredInput.IsRequired; + break; + case InputLabelAttribute inputLabel: + builder.Label = inputLabel.Label; + builder.Description = inputLabel.Description; + break; + default: + builder.WithAttributes(attribute); + break; } } + } private static void BuildFileUploadInput(FileUploadInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) { @@ -796,98 +796,97 @@ private static void BuildTextDisplayComponent() { } - #endregion + #endregion - internal static bool IsValidModuleDefinition(TypeInfo typeInfo) - { - return ModuleTypeInfo.IsAssignableFrom(typeInfo) && - !typeInfo.IsAbstract && - !typeInfo.ContainsGenericParameters; - } + internal static bool IsValidModuleDefinition(TypeInfo typeInfo) + { + return ModuleTypeInfo.IsAssignableFrom(typeInfo) && + !typeInfo.IsAbstract && + !typeInfo.ContainsGenericParameters; + } - private static bool IsValidSlashCommandDefinition(MethodInfo methodInfo) - { - return methodInfo.IsDefined(typeof(SlashCommandAttribute)) && - (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && - !methodInfo.IsStatic && - !methodInfo.IsGenericMethod; - } + private static bool IsValidSlashCommandDefinition(MethodInfo methodInfo) + { + return methodInfo.IsDefined(typeof(SlashCommandAttribute)) && + (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && + !methodInfo.IsStatic && + !methodInfo.IsGenericMethod; + } - private static bool IsValidContextCommandDefinition(MethodInfo methodInfo) - { - return methodInfo.IsDefined(typeof(ContextCommandAttribute)) && - (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && - !methodInfo.IsStatic && - !methodInfo.IsGenericMethod; - } + private static bool IsValidContextCommandDefinition(MethodInfo methodInfo) + { + return methodInfo.IsDefined(typeof(ContextCommandAttribute)) && + (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && + !methodInfo.IsStatic && + !methodInfo.IsGenericMethod; + } - private static bool IsValidComponentCommandDefinition(MethodInfo methodInfo) - { - return methodInfo.IsDefined(typeof(ComponentInteractionAttribute)) && - (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && - !methodInfo.IsStatic && - !methodInfo.IsGenericMethod; - } + private static bool IsValidComponentCommandDefinition(MethodInfo methodInfo) + { + return methodInfo.IsDefined(typeof(ComponentInteractionAttribute)) && + (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && + !methodInfo.IsStatic && + !methodInfo.IsGenericMethod; + } - private static bool IsValidAutocompleteCommandDefinition(MethodInfo methodInfo) - { - return methodInfo.IsDefined(typeof(AutocompleteCommandAttribute)) && - (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && - !methodInfo.IsStatic && - !methodInfo.IsGenericMethod && - methodInfo.GetParameters().Length == 0; - } + private static bool IsValidAutocompleteCommandDefinition(MethodInfo methodInfo) + { + return methodInfo.IsDefined(typeof(AutocompleteCommandAttribute)) && + (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && + !methodInfo.IsStatic && + !methodInfo.IsGenericMethod && + methodInfo.GetParameters().Length == 0; + } - private static bool IsValidModalCommandDefinition(MethodInfo methodInfo) - { - return methodInfo.IsDefined(typeof(ModalInteractionAttribute)) && - (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && - !methodInfo.IsStatic && - !methodInfo.IsGenericMethod && - typeof(IModal).IsAssignableFrom(methodInfo.GetParameters().Last().ParameterType); - } + private static bool IsValidModalCommandDefinition(MethodInfo methodInfo) + { + return methodInfo.IsDefined(typeof(ModalInteractionAttribute)) && + (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && + !methodInfo.IsStatic && + !methodInfo.IsGenericMethod && + typeof(IModal).IsAssignableFrom(methodInfo.GetParameters().Last().ParameterType); + } - private static bool IsValidModalInputDefinition(PropertyInfo propertyInfo) - { - return propertyInfo.SetMethod?.IsPublic == true && - propertyInfo.SetMethod?.IsStatic == false && - propertyInfo.IsDefined(typeof(ModalInputAttribute)); - } + private static bool IsValidModalInputDefinition(PropertyInfo propertyInfo) + { + return propertyInfo.SetMethod?.IsPublic == true && + propertyInfo.SetMethod?.IsStatic == false && + propertyInfo.IsDefined(typeof(ModalInputAttribute)); + } - private static ConstructorInfo GetComplexParameterConstructor(TypeInfo typeInfo, ComplexParameterAttribute complexParameter) - { - var ctors = typeInfo.GetConstructors(); + private static ConstructorInfo GetComplexParameterConstructor(TypeInfo typeInfo, ComplexParameterAttribute complexParameter) + { + var ctors = typeInfo.GetConstructors(); - if (ctors.Length == 0) - throw new InvalidOperationException($"No constructor found for \"{typeInfo.FullName}\"."); + if (ctors.Length == 0) + throw new InvalidOperationException($"No constructor found for \"{typeInfo.FullName}\"."); - if (complexParameter.PrioritizedCtorSignature is not null) - { - var ctor = typeInfo.GetConstructor(complexParameter.PrioritizedCtorSignature); + if (complexParameter.PrioritizedCtorSignature is not null) + { + var ctor = typeInfo.GetConstructor(complexParameter.PrioritizedCtorSignature); - if (ctor is null) - throw new InvalidOperationException($"No constructor was found with the signature: {string.Join(",", complexParameter.PrioritizedCtorSignature.Select(x => x.Name))}"); + if (ctor is null) + throw new InvalidOperationException($"No constructor was found with the signature: {string.Join(",", complexParameter.PrioritizedCtorSignature.Select(x => x.Name))}"); - return ctor; - } + return ctor; + } - var prioritizedCtors = ctors.Where(x => x.IsDefined(typeof(ComplexParameterCtorAttribute), true)); + var prioritizedCtors = ctors.Where(x => x.IsDefined(typeof(ComplexParameterCtorAttribute), true)); - switch (prioritizedCtors.Count()) - { - case > 1: - throw new InvalidOperationException($"{nameof(ComplexParameterCtorAttribute)} can only be used once in a type."); - case 1: - return prioritizedCtors.First(); - } + switch (prioritizedCtors.Count()) + { + case > 1: + throw new InvalidOperationException($"{nameof(ComplexParameterCtorAttribute)} can only be used once in a type."); + case 1: + return prioritizedCtors.First(); + } - switch (ctors.Length) - { - case > 1: - throw new InvalidOperationException($"Multiple constructors found for \"{typeInfo.FullName}\"."); - default: - return ctors.First(); - } + switch (ctors.Length) + { + case > 1: + throw new InvalidOperationException($"Multiple constructors found for \"{typeInfo.FullName}\"."); + default: + return ctors.First(); } } } From 120ab0472e449b6085dcabad184cb1082d513681 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Mon, 10 Nov 2025 10:10:40 +0100 Subject: [PATCH 27/59] add inline docs to modalBuilder --- .../Builders/Modals/ModalBuilder.cs | 273 ++++++++++-------- 1 file changed, 149 insertions(+), 124 deletions(-) diff --git a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs index 115c044ad9..dd12634f10 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs @@ -1,132 +1,158 @@ -using Discord.Interactions.Builders.Modals.Inputs; using System; using System.Collections.Generic; -using System.Linq; -namespace Discord.Interactions.Builders +namespace Discord.Interactions.Builders; + +/// +/// Represents a builder for creating . +/// +public class ModalBuilder { + internal readonly InteractionService _interactionService; + internal readonly List _components; + + /// + /// Gets the initialization delegate for this modal. + /// + public ModalInitializer ModalInitializer { get; internal set; } + + /// + /// Gets the title of this modal. + /// + public string Title { get; set; } + + /// + /// Gets the implementation used to initialize this object. + /// + public Type Type { get; } + + /// + /// Gets a collection of the components of this modal. + /// + public IReadOnlyCollection Components => _components; + + internal ModalBuilder(Type type, InteractionService interactionService) + { + if (!typeof(IModal).IsAssignableFrom(type)) + throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); + + Type = type; + + _interactionService = interactionService; + _components = new(); + } + + /// + /// Initializes a new + /// + /// The initialization delegate for this modal. + public ModalBuilder(Type type, ModalInitializer modalInitializer, InteractionService interactionService) : this(type, interactionService) + { + ModalInitializer = modalInitializer; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public ModalBuilder WithTitle(string title) + { + Title = title; + return this; + } + + /// + /// Adds text components to . + /// + /// Text Component builder factory. + /// + /// The builder instance. + /// + public ModalBuilder AddTextComponent(Action configure) + { + var builder = new TextInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + + /// + /// Adds a select menu component to . + /// + /// Select menu component builder factory. + /// + /// The builder instance. + /// + public ModalBuilder AddSelectMenuComponent(Action configure) + { + var builder = new SelectMenuInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + + /// + /// Adds a user select component to . + /// + /// User select component builder factory. + /// + /// The builder instance. + /// + public ModalBuilder AddUserSelectComponent(Action configure) + { + var builder = new UserSelectInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + + /// + /// Adds a role select component to . + /// + /// Role select component builder factory. + /// + /// The builder instance. + /// + public ModalBuilder AddRoleSelectComponent(Action configure) + { + var builder = new RoleSelectInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + + /// + /// Adds a mentionable select component to . + /// + /// Mentionable select component builder factory. + /// + /// The builder instance. + /// + public ModalBuilder AddMentionableSelectComponent(Action configure) + { + var builder = new MentionableSelectInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + /// - /// Represents a builder for creating . + /// Adds a channel select component to . /// - public class ModalBuilder + /// Channel select component builder factory. + /// + /// The builder instance. + /// + public ModalBuilder AddChannelSelectComponent(Action configure) { - internal readonly InteractionService _interactionService; - internal readonly List _components; - - /// - /// Gets the initialization delegate for this modal. - /// - public ModalInitializer ModalInitializer { get; internal set; } - - /// - /// Gets the title of this modal. - /// - public string Title { get; set; } - - /// - /// Gets the implementation used to initialize this object. - /// - public Type Type { get; } - - /// - /// Gets a collection of the components of this modal. - /// - public IReadOnlyCollection Components => _components; - - internal ModalBuilder(Type type, InteractionService interactionService) - { - if (!typeof(IModal).IsAssignableFrom(type)) - throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); - - Type = type; - - _interactionService = interactionService; - _components = new(); - } - - /// - /// Initializes a new - /// - /// The initialization delegate for this modal. - public ModalBuilder(Type type, ModalInitializer modalInitializer, InteractionService interactionService) : this(type, interactionService) - { - ModalInitializer = modalInitializer; - } - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public ModalBuilder WithTitle(string title) - { - Title = title; - return this; - } - - /// - /// Adds text components to . - /// - /// Text Component builder factory. - /// - /// The builder instance. - /// - public ModalBuilder AddTextComponent(Action configure) - { - var builder = new TextInputComponentBuilder(this); - configure(builder); - _components.Add(builder); - return this; - } - - /// - /// Adds a select menu component to . - /// - /// Select menu component builder factory. - /// - /// The builder instance. - /// - public ModalBuilder AddSelectMenuComponent(Action configure) - { - var builder = new SelectMenuInputComponentBuilder(this); - configure(builder); - _components.Add(builder); - return this; - } - - public ModalBuilder AddUserSelectComponent(Action configure) - { - var builder = new UserSelectInputComponentBuilder(this); - configure(builder); - _components.Add(builder); - return this; - } - - public ModalBuilder AddRoleSelectComponent(Action configure) - { - var builder = new RoleSelectInputComponentBuilder(this); - configure(builder); - _components.Add(builder); - return this; - } - - public ModalBuilder AddMentionableSelectComponent(Action configure) - { - var builder = new MentionableSelectInputComponentBuilder(this); - configure(builder); - _components.Add(builder); - return this; - } - - public ModalBuilder AddChannelSelectComponent(Action configure) - { - var builder = new ChannelSelectInputComponentBuilder(this); - configure(builder); - _components.Add(builder); - return this; - } + var builder = new ChannelSelectInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } /// /// Adds a file upload component to . @@ -143,6 +169,5 @@ public ModalBuilder AddFileUploadComponent(Action new(this); - } + internal ModalInfo Build() => new(this); } From 6633091bdcfea54f1da41457f2f04a3bcf53ad0a Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Mon, 10 Nov 2025 10:11:03 +0100 Subject: [PATCH 28/59] add description to inputComponentInfo --- .../InputComponents/InputComponentInfo.cs | 149 +++++++++--------- 1 file changed, 77 insertions(+), 72 deletions(-) diff --git a/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs index dc7fa02e08..f541c30481 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs @@ -4,81 +4,86 @@ using System.Collections.Immutable; using System.Reflection; -namespace Discord.Interactions +namespace Discord.Interactions; + +/// +/// Represents the base info class for input components. +/// +public abstract class InputComponentInfo { + private Lazy> _getter; + internal Func Getter => _getter.Value; + + + /// + /// Gets the parent modal of this component. + /// + public ModalInfo Modal { get; } + + /// + /// Gets the custom id of this component. + /// + public string CustomId { get; } + + /// + /// Gets the label of this component. + /// + public string Label { get; } + + /// + /// Gets the description of this component. + /// + public string Description { get; } + + /// + /// Gets whether or not this component requires a user input. + /// + public bool IsRequired { get; } + /// - /// Represents the base info class for input components. + /// Gets the type of this component. /// - public abstract class InputComponentInfo + public ComponentType ComponentType { get; } + + /// + /// Gets the reference type of this component. + /// + public Type Type { get; } + + /// + /// Gets the property linked to this component. + /// + public PropertyInfo PropertyInfo { get; } + + /// + /// Gets the assigned to this component. + /// + public ModalComponentTypeConverter TypeConverter { get; } + + /// + /// Gets the default value of this component property. + /// + public object DefaultValue { get; } + + /// + /// Gets a collection of the attributes of this command. + /// + public IReadOnlyCollection Attributes { get; } + + protected InputComponentInfo(Builders.IInputComponentBuilder builder, ModalInfo modal) { - private Lazy> _getter; - internal Func Getter => _getter.Value; - - - /// - /// Gets the parent modal of this component. - /// - public ModalInfo Modal { get; } - - /// - /// Gets the custom id of this component. - /// - public string CustomId { get; } - - /// - /// Gets the label of this component. - /// - public string Label { get; } - - /// - /// Gets whether or not this component requires a user input. - /// - public bool IsRequired { get; } - - /// - /// Gets the type of this component. - /// - public ComponentType ComponentType { get; } - - /// - /// Gets the reference type of this component. - /// - public Type Type { get; } - - /// - /// Gets the property linked to this component. - /// - public PropertyInfo PropertyInfo { get; } - - /// - /// Gets the assigned to this component. - /// - public ModalComponentTypeConverter TypeConverter { get; } - - /// - /// Gets the default value of this component property. - /// - public object DefaultValue { get; } - - /// - /// Gets a collection of the attributes of this command. - /// - public IReadOnlyCollection Attributes { get; } - - protected InputComponentInfo(Builders.IInputComponentBuilder builder, ModalInfo modal) - { - Modal = modal; - CustomId = builder.CustomId; - Label = builder.Label; - IsRequired = builder.IsRequired; - ComponentType = builder.ComponentType; - Type = builder.Type; - PropertyInfo = builder.PropertyInfo; - TypeConverter = builder.TypeConverter; - DefaultValue = builder.DefaultValue; - Attributes = builder.Attributes.ToImmutableArray(); - - _getter = new(() => ReflectionUtils.CreateLambdaPropertyGetter(Modal.Type, PropertyInfo)); - } + Modal = modal; + CustomId = builder.CustomId; + Label = builder.Label; + Description = builder.Description; + IsRequired = builder.IsRequired; + ComponentType = builder.ComponentType; + Type = builder.Type; + PropertyInfo = builder.PropertyInfo; + TypeConverter = builder.TypeConverter; + DefaultValue = builder.DefaultValue; + Attributes = builder.Attributes.ToImmutableArray(); + + _getter = new(() => ReflectionUtils.CreateLambdaPropertyGetter(Modal.Type, PropertyInfo)); } } From 33bc29dadedeec7be32fc657fc09c8e893e0fb14 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Mon, 10 Nov 2025 10:11:51 +0100 Subject: [PATCH 29/59] file-scope namespace for commandBuilder and modal interfaces --- .../Builders/Commands/ICommandBuilder.cs | 237 +++++++++--------- .../Entities/IModal.cs | 17 +- 2 files changed, 126 insertions(+), 128 deletions(-) diff --git a/src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs index a8036e6043..e2943b455b 100644 --- a/src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs @@ -1,125 +1,124 @@ using System; using System.Collections.Generic; -namespace Discord.Interactions.Builders +namespace Discord.Interactions.Builders; + +/// +/// Represent a command builder for creating . +/// +public interface ICommandBuilder { /// - /// Represent a command builder for creating . - /// - public interface ICommandBuilder - { - /// - /// Gets the execution delegate of this command. - /// - ExecuteCallback Callback { get; } - - /// - /// Gets the parent module of this command. - /// - ModuleBuilder Module { get; } - - /// - /// Gets the name of this command. - /// - string Name { get; } - - /// - /// Gets or sets the method name of this command. - /// - string MethodName { get; set; } - - /// - /// Gets or sets if this command will be registered and executed as a standalone command, unaffected by the s of - /// of the commands parents. - /// - bool IgnoreGroupNames { get; set; } - - /// - /// Gets or sets whether the should be directly used as a Regex pattern. - /// - bool TreatNameAsRegex { get; set; } - - /// - /// Gets or sets the run mode this command gets executed with. - /// - RunMode RunMode { get; set; } - - /// - /// Gets a collection of the attributes of this command. - /// - IReadOnlyList Attributes { get; } - - /// - /// Gets a collection of the parameters of this command. - /// - IReadOnlyList Parameters { get; } - - /// - /// Gets a collection of the preconditions of this command. - /// - IReadOnlyList Preconditions { get; } - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - ICommandBuilder WithName(string name); - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - ICommandBuilder WithMethodName(string name); - - /// - /// Adds attributes to . - /// - /// New attributes to be added to . - /// - /// The builder instance. - /// - ICommandBuilder WithAttributes(params Attribute[] attributes); - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - ICommandBuilder SetRunMode(RunMode runMode); - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - ICommandBuilder WithNameAsRegex(bool value); - - /// - /// Adds parameter builders to . - /// - /// New parameter builders to be added to . - /// - /// The builder instance. - /// - ICommandBuilder AddParameters(params IParameterBuilder[] parameters); - - /// - /// Adds preconditions to . - /// - /// New preconditions to be added to . - /// - /// The builder instance. - /// - ICommandBuilder WithPreconditions(params PreconditionAttribute[] preconditions); - } + /// Gets the execution delegate of this command. + /// + ExecuteCallback Callback { get; } + + /// + /// Gets the parent module of this command. + /// + ModuleBuilder Module { get; } + + /// + /// Gets the name of this command. + /// + string Name { get; } + + /// + /// Gets or sets the method name of this command. + /// + string MethodName { get; set; } + + /// + /// Gets or sets if this command will be registered and executed as a standalone command, unaffected by the s of + /// of the commands parents. + /// + bool IgnoreGroupNames { get; set; } + + /// + /// Gets or sets whether the should be directly used as a Regex pattern. + /// + bool TreatNameAsRegex { get; set; } + + /// + /// Gets or sets the run mode this command gets executed with. + /// + RunMode RunMode { get; set; } + + /// + /// Gets a collection of the attributes of this command. + /// + IReadOnlyList Attributes { get; } + + /// + /// Gets a collection of the parameters of this command. + /// + IReadOnlyList Parameters { get; } + + /// + /// Gets a collection of the preconditions of this command. + /// + IReadOnlyList Preconditions { get; } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + ICommandBuilder WithName(string name); + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + ICommandBuilder WithMethodName(string name); + + /// + /// Adds attributes to . + /// + /// New attributes to be added to . + /// + /// The builder instance. + /// + ICommandBuilder WithAttributes(params Attribute[] attributes); + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + ICommandBuilder SetRunMode(RunMode runMode); + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + ICommandBuilder WithNameAsRegex(bool value); + + /// + /// Adds parameter builders to . + /// + /// New parameter builders to be added to . + /// + /// The builder instance. + /// + ICommandBuilder AddParameters(params IParameterBuilder[] parameters); + + /// + /// Adds preconditions to . + /// + /// New preconditions to be added to . + /// + /// The builder instance. + /// + ICommandBuilder WithPreconditions(params PreconditionAttribute[] preconditions); } diff --git a/src/Discord.Net.Interactions/Entities/IModal.cs b/src/Discord.Net.Interactions/Entities/IModal.cs index 572a88033e..1516b458e2 100644 --- a/src/Discord.Net.Interactions/Entities/IModal.cs +++ b/src/Discord.Net.Interactions/Entities/IModal.cs @@ -1,13 +1,12 @@ -namespace Discord.Interactions +namespace Discord.Interactions; + +/// +/// Represents a generic for use with the interaction service. +/// +public interface IModal { /// - /// Represents a generic for use with the interaction service. + /// Gets the modal's title. /// - public interface IModal - { - /// - /// Gets the modal's title. - /// - string Title { get; } - } + string Title { get; } } From 8bd22d871de784d6619c75dcda5558fc9f2b3af9 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Mon, 10 Nov 2025 10:13:02 +0100 Subject: [PATCH 30/59] update respondWithModal logic to include new components --- .../IDiscordInteractionExtensions.cs | 219 ++++++++++-------- 1 file changed, 123 insertions(+), 96 deletions(-) diff --git a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs index 0491ca6e11..e03f0a2873 100644 --- a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs +++ b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs @@ -1,117 +1,144 @@ -using Discord.Interactions.Info.InputComponents; using System; using System.Linq; +using System.Reflection; using System.Threading.Tasks; -namespace Discord.Interactions +namespace Discord.Interactions; + +public static class IDiscordInteractionExtentions { - public static class IDiscordInteractionExtentions + /// + /// Respond to an interaction with a . + /// + /// Type of the implementation. + /// The interaction to respond to. + /// The custom id of the modal. + /// Delegate that can be used to modify the modal. + /// The request options for this request. + /// A task that represents the asynchronous operation of responding to the interaction. + public static Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, RequestOptions options = null, Action modifyModal = null) + where T : class, IModal { - /// - /// Respond to an interaction with a . - /// - /// Type of the implementation. - /// The interaction to respond to. - /// The custom id of the modal. - /// Delegate that can be used to modify the modal. - /// The request options for this request. - /// A task that represents the asynchronous operation of responding to the interaction. - public static Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, RequestOptions options = null, Action modifyModal = null) - where T : class, IModal - { - if (!ModalUtils.TryGet(out var modalInfo)) - throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}"); - - return SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal); - } - - /// - /// Respond to an interaction with a . - /// - /// - /// This method overload uses the parameter to create a new - /// if there isn't a built one already in cache. - /// - /// Type of the implementation. - /// The interaction to respond to. - /// The custom id of the modal. - /// Interaction service instance that should be used to build s. - /// The request options for this request. - /// Delegate that can be used to modify the modal. - /// A task that represents the asynchronous operation of responding to the interaction. - public static Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, InteractionService interactionService, - RequestOptions options = null, Action modifyModal = null) - where T : class, IModal - { - var modalInfo = ModalUtils.GetOrAdd(interactionService); - - return SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal); - } - - /// - /// Respond to an interaction with an and fills the value fields of the modal using the property values of the provided - /// instance. - /// - /// Type of the implementation. - /// The interaction to respond to. - /// The custom id of the modal. - /// The instance to get field values from. - /// The request options for this request. - /// Delegate that can be used to modify the modal. - /// A task that represents the asynchronous operation of responding to the interaction. - public static async Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, T modal, RequestOptions options = null, - Action modifyModal = null) - where T : class, IModal - { - if (!ModalUtils.TryGet(out var modalInfo)) - throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}"); - - var builder = new ModalBuilder(modal.Title, customId); - - foreach (var input in modalInfo.Components) - switch (input) - { - case TextInputComponentInfo textComponent: - { - var inputBuilder = new TextInputBuilder(textComponent.Label, textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null, - textComponent.MaxLength, textComponent.IsRequired); + if (!ModalUtils.TryGet(out var modalInfo)) + throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}"); + + return SendModalResponseAsync(interaction, customId, modalInfo, null, options, modifyModal); + } - await textComponent.TypeConverter.WriteAsync(inputBuilder, textComponent, textComponent.Getter(modal)); + /// + /// Respond to an interaction with a . + /// + /// + /// This method overload uses the parameter to create a new + /// if there isn't a built one already in cache. + /// + /// Type of the implementation. + /// The interaction to respond to. + /// The custom id of the modal. + /// Interaction service instance that should be used to build s. + /// The request options for this request. + /// Delegate that can be used to modify the modal. + /// A task that represents the asynchronous operation of responding to the interaction. + public static Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, InteractionService interactionService, + RequestOptions options = null, Action modifyModal = null) + where T : class, IModal + { + var modalInfo = ModalUtils.GetOrAdd(interactionService); - builder.AddTextInput(inputBuilder); - } - break; - case SelectMenuInputComponentInfo selectMenuComponent: + return SendModalResponseAsync(interaction, customId, modalInfo, null, options, modifyModal); + } + + /// + /// Respond to an interaction with an and fills the value fields of the modal using the property values of the provided + /// instance. + /// + /// Type of the implementation. + /// The interaction to respond to. + /// The custom id of the modal. + /// The instance to get field values from. + /// The request options for this request. + /// Delegate that can be used to modify the modal. + /// A task that represents the asynchronous operation of responding to the interaction. + public static Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, T modal, RequestOptions options = null, + Action modifyModal = null) + where T : class, IModal + { + if (!ModalUtils.TryGet(out var modalInfo)) + throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}"); + + return SendModalResponseAsync(interaction, customId, modalInfo, modal, options, modifyModal); + } + + private static async Task SendModalResponseAsync(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, T modalInstance = null, RequestOptions options = null, Action modifyModal = null) + where T : class, IModal + { + if (!modalInfo.Type.IsAssignableFrom(typeof(T))) + throw new ArgumentException($"{modalInfo.Type.FullName} isn't assignable from {typeof(T).FullName}."); + + var builder = new ModalBuilder(modalInstance.Title, customId); + + foreach (var input in modalInfo.Components) + switch (input) + { + case TextInputComponentInfo textComponent: + { + var inputBuilder = new TextInputBuilder(textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null, + textComponent.MaxLength, textComponent.IsRequired); + + if(modalInstance != null) { - var inputBuilder = new SelectMenuBuilder(selectMenuComponent.CustomId, selectMenuComponent.Options.Select(x => new SelectMenuOptionBuilder(x)).ToList(), selectMenuComponent.Placeholder, selectMenuComponent.MaxValues, selectMenuComponent.MinValues, false); + await textComponent.TypeConverter.WriteAsync(inputBuilder, textComponent, textComponent.Getter(modalInstance)); + } - await selectMenuComponent.TypeConverter.WriteAsync(inputBuilder, selectMenuComponent, selectMenuComponent.Getter(modal)); + var labelBuilder = new LabelBuilder(textComponent.Label, inputBuilder, textComponent.Description); + builder.AddLabel(labelBuilder); + } + break; + case SelectMenuInputComponentInfo selectMenuComponent: + { + var inputBuilder = new SelectMenuBuilder(selectMenuComponent.CustomId, selectMenuComponent.Options.Select(x => new SelectMenuOptionBuilder(x)).ToList(), selectMenuComponent.Placeholder, selectMenuComponent.MaxValues, selectMenuComponent.MinValues, false); - //todo: add to builder + if(modalInstance != null) + { + await selectMenuComponent.TypeConverter.WriteAsync(inputBuilder, selectMenuComponent, selectMenuComponent.Getter(modalInstance)); } - break; - case SnowflakeSelectInputComponentInfo snowflakeSelectComponent: + + var labelBuilder = new LabelBuilder(selectMenuComponent.Label, inputBuilder, selectMenuComponent.Description); + builder.AddLabel(labelBuilder); + } + break; + case SnowflakeSelectInputComponentInfo snowflakeSelectComponent: + { + var inputBuilder = new SelectMenuBuilder(snowflakeSelectComponent.CustomId, null, snowflakeSelectComponent.Placeholder, snowflakeSelectComponent.MaxValues, snowflakeSelectComponent.MinValues, false, snowflakeSelectComponent.ComponentType, null, snowflakeSelectComponent.DefaultValues.ToList()); + + if(modalInstance != null) { - var inputBuilder = new SelectMenuBuilder(snowflakeSelectComponent.CustomId, null, snowflakeSelectComponent.Placeholder, snowflakeSelectComponent.MaxValues, snowflakeSelectComponent.MinValues, false, snowflakeSelectComponent.ComponentType, null, snowflakeSelectComponent.DefaultValues.ToList()); + await snowflakeSelectComponent.TypeConverter.WriteAsync(inputBuilder, snowflakeSelectComponent, snowflakeSelectComponent.Getter(modalInstance)); + } - await snowflakeSelectComponent.TypeConverter.WriteAsync(inputBuilder, snowflakeSelectComponent, snowflakeSelectComponent.Getter(modal)); + var labelBuilder = new LabelBuilder(snowflakeSelectComponent.Label, inputBuilder, snowflakeSelectComponent.Description); + builder.AddLabel(labelBuilder); + } + break; + case FileUploadInputComponentInfo fileUploadComponent: + { + var inputBuilder = new FileUploadComponentBuilder(fileUploadComponent.CustomId, fileUploadComponent.MinValues, fileUploadComponent.MaxValues, fileUploadComponent.IsRequired); - //todo: add to builder + if(modalInstance != null) + { + await fileUploadComponent.TypeConverter.WriteAsync(inputBuilder, fileUploadComponent, fileUploadComponent.Getter(modalInstance)); } - break; - default: - throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class"); - } - modifyModal?.Invoke(builder); + var labelBuilder = new LabelBuilder(fileUploadComponent.Label, inputBuilder, fileUploadComponent.Description); + builder.AddLabel(labelBuilder); + } + break; + default: + throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class"); + } - await interaction.RespondWithModalAsync(builder.Build(), options); - } + modifyModal?.Invoke(builder); - private static Task SendModalResponseAsync(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, RequestOptions options = null, Action modifyModal = null) - { - var modal = modalInfo.ToModal(customId, modifyModal); - return interaction.RespondWithModalAsync(modal, options); - } + await interaction.RespondWithModalAsync(builder.Build(), options); } } From 2d9327a6a5b234183a65ca72e174afcc6f73cef8 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Mon, 10 Nov 2025 10:13:29 +0100 Subject: [PATCH 31/59] add inline docs and file upload component to modalInfo --- .../Info/ModalInfo.cs | 262 ++++++++++-------- 1 file changed, 139 insertions(+), 123 deletions(-) diff --git a/src/Discord.Net.Interactions/Info/ModalInfo.cs b/src/Discord.Net.Interactions/Info/ModalInfo.cs index 93e6c14449..39e61ad18f 100644 --- a/src/Discord.Net.Interactions/Info/ModalInfo.cs +++ b/src/Discord.Net.Interactions/Info/ModalInfo.cs @@ -1,165 +1,181 @@ using Discord.Interactions.Builders; -using Discord.Interactions.Builders.Modals.Inputs; -using Discord.Interactions.Info.InputComponents; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; -namespace Discord.Interactions +namespace Discord.Interactions; + +/// +/// Represents a cached object initialization delegate. +/// +/// Property arguments array. +/// +/// Returns the constructed object. +/// +public delegate IModal ModalInitializer(object[] args); + +/// +/// Represents the info class of an form. +/// +public class ModalInfo { + internal readonly InteractionService _interactionService; + internal readonly ModalInitializer _initializer; + /// - /// Represents a cached object initialization delegate. + /// Gets the title of this modal. /// - /// Property arguments array. - /// - /// Returns the constructed object. - /// - public delegate IModal ModalInitializer(object[] args); + public string Title { get; } /// - /// Represents the info class of an form. + /// Gets the implementation used to initialize this object. /// - public class ModalInfo - { - internal readonly InteractionService _interactionService; - internal readonly ModalInitializer _initializer; - - /// - /// Gets the title of this modal. - /// - public string Title { get; } + public Type Type { get; } - /// - /// Gets the implementation used to initialize this object. - /// - public Type Type { get; } + /// + /// Gets a collection of the components of this modal. + /// + public IReadOnlyCollection Components { get; } - /// - /// Gets a collection of the components of this modal. - /// - public IReadOnlyCollection Components { get; } + /// + /// Gets a collection of the text components of this modal. + /// + public IReadOnlyCollection TextComponents { get; } - /// - /// Gets a collection of the text components of this modal. - /// - public IReadOnlyCollection TextComponents { get; } + /// + /// Get a collection of the select menu components of this modal. + /// + public IReadOnlyCollection SelectMenuComponents { get; } - /// - /// Get a collection of the select menu components of this modal. - /// - public IReadOnlyCollection SelectMenuComponents { get; } + /// + /// Get a collection of the user select components of this modal. + /// + public IReadOnlyCollection UserSelectComponents { get; } - public IReadOnlyCollection UserSelectComponents { get; } + /// + /// Get a collection of the role select components of this modal. + /// + public IReadOnlyCollection RoleSelectComponents { get; } - public IReadOnlyCollection RoleSelectComponents { get; } + /// + /// Get a collection of the mentionable select components of this modal. + /// + public IReadOnlyCollection MentionableSelectComponents { get; } - public IReadOnlyCollection MentionableSelectComponents { get; } + /// + /// Get a collection of the channel select components of this modal. + /// + public IReadOnlyCollection ChannelSelectComponents { get; } - public IReadOnlyCollection ChannelSelectComponents { get; } + /// + /// Get a collection of the file upload components of this modal. + /// + public IReadOnlyCollection FileUploadComponents { get; } - internal ModalInfo(Builders.ModalBuilder builder) + internal ModalInfo(Builders.ModalBuilder builder) + { + Title = builder.Title; + Type = builder.Type; + Components = builder.Components.Select(x => x switch { - Title = builder.Title; - Type = builder.Type; - Components = builder.Components.Select(x => x switch - { - TextInputComponentBuilder textComponent => textComponent.Build(this), - SelectMenuInputComponentBuilder selectMenuComponent => selectMenuComponent.Build(this), - RoleSelectInputComponentBuilder roleSelectComponent => roleSelectComponent.Build(this), - ChannelSelectInputComponentBuilder channelSelectComponent => channelSelectComponent.Build(this), - UserSelectInputComponentBuilder userSelectComponent => userSelectComponent.Build(this), - MentionableSelectInputComponentBuilder mentionableSelectComponent => mentionableSelectComponent.Build(this), - _ => throw new InvalidOperationException($"{x.GetType().FullName} isn't a supported modal input component builder type.") - }).ToImmutableArray(); - - TextComponents = Components.OfType().ToImmutableArray(); - SelectMenuComponents = Components.OfType().ToImmutableArray(); - UserSelectComponents = Components.OfType().ToImmutableArray(); - RoleSelectComponents = Components.OfType().ToImmutableArray(); - MentionableSelectComponents = Components.OfType().ToImmutableArray(); - ChannelSelectComponents = Components.OfType().ToImmutableArray(); - - _interactionService = builder._interactionService; - _initializer = builder.ModalInitializer; - } + TextInputComponentBuilder textComponent => textComponent.Build(this), + SelectMenuInputComponentBuilder selectMenuComponent => selectMenuComponent.Build(this), + RoleSelectInputComponentBuilder roleSelectComponent => roleSelectComponent.Build(this), + ChannelSelectInputComponentBuilder channelSelectComponent => channelSelectComponent.Build(this), + UserSelectInputComponentBuilder userSelectComponent => userSelectComponent.Build(this), + MentionableSelectInputComponentBuilder mentionableSelectComponent => mentionableSelectComponent.Build(this), + FileUploadInputComponentBuilder fileUploadComponent => fileUploadComponent.Build(this), + _ => throw new InvalidOperationException($"{x.GetType().FullName} isn't a supported modal input component builder type.") + }).ToImmutableArray(); + + TextComponents = Components.OfType().ToImmutableArray(); + SelectMenuComponents = Components.OfType().ToImmutableArray(); + UserSelectComponents = Components.OfType().ToImmutableArray(); + RoleSelectComponents = Components.OfType().ToImmutableArray(); + MentionableSelectComponents = Components.OfType().ToImmutableArray(); + ChannelSelectComponents = Components.OfType().ToImmutableArray(); + FileUploadComponents = Components.OfType().ToImmutableArray(); + + _interactionService = builder._interactionService; + _initializer = builder.ModalInitializer; + } + + /// + /// Creates an and fills it with provided message components. + /// + /// that will be injected into the modal. + /// + /// A filled with the provided components. + /// + [Obsolete("This method is no longer supported with the introduction of Component TypeConverters, please use the CreateModalAsync method.")] + public IModal CreateModal(IModalInteraction modalInteraction, bool throwOnMissingField = false) + { + var args = new object[Components.Count]; + var components = modalInteraction.Data.Components.ToList(); - /// - /// Creates an and fills it with provided message components. - /// - /// that will be injected into the modal. - /// - /// A filled with the provided components. - /// - [Obsolete("This method is no longer supported with the introduction of Component TypeConverters, please use the CreateModalAsync method.")] - public IModal CreateModal(IModalInteraction modalInteraction, bool throwOnMissingField = false) + for (var i = 0; i < Components.Count; i++) { - var args = new object[Components.Count]; - var components = modalInteraction.Data.Components.ToList(); + var input = Components.ElementAt(i); + var component = components.Find(x => x.CustomId == input.CustomId); - for (var i = 0; i < Components.Count; i++) + if (component is null) { - var input = Components.ElementAt(i); - var component = components.Find(x => x.CustomId == input.CustomId); - - if (component is null) - { - if (!throwOnMissingField) - args[i] = input.DefaultValue; - else - throw new InvalidOperationException($"Modal interaction is missing the required field: {input.CustomId}"); - } + if (!throwOnMissingField) + args[i] = input.DefaultValue; else - args[i] = component.Value; + throw new InvalidOperationException($"Modal interaction is missing the required field: {input.CustomId}"); } - - return _initializer(args); + else + args[i] = component.Value; } - /// - /// Creates an and fills it with provided message components. - /// - /// Context of the that will be injected into the modal. - /// Services to be passed onto the s of the modal fields. - /// Whether or not this method should exit on encountering a missing modal field. - /// - /// A if a type conversion has failed, else a . - /// - public async Task CreateModalAsync(IInteractionContext context, IServiceProvider services = null, bool throwOnMissingField = false) - { - if (context.Interaction is not IModalInteraction modalInteraction) - return TypeConverterResult.FromError(InteractionCommandError.Unsuccessful, "Provided context doesn't belong to a Modal Interaction."); + return _initializer(args); + } - services ??= EmptyServiceProvider.Instance; + /// + /// Creates an and fills it with provided message components. + /// + /// Context of the that will be injected into the modal. + /// Services to be passed onto the s of the modal fields. + /// Whether or not this method should exit on encountering a missing modal field. + /// + /// A if a type conversion has failed, else a . + /// + public async Task CreateModalAsync(IInteractionContext context, IServiceProvider services = null, bool throwOnMissingField = false) + { + if (context.Interaction is not IModalInteraction modalInteraction) + return TypeConverterResult.FromError(InteractionCommandError.Unsuccessful, "Provided context doesn't belong to a Modal Interaction."); + + services ??= EmptyServiceProvider.Instance; - var args = new object[Components.Count]; - var components = modalInteraction.Data.Components.ToList(); + var args = new object[Components.Count]; + var components = modalInteraction.Data.Components.ToList(); - for (var i = 0; i < Components.Count; i++) + for (var i = 0; i < Components.Count; i++) + { + var input = Components.ElementAt(i); + var component = components.Find(x => x.CustomId == input.CustomId); + + if (component is null) { - var input = Components.ElementAt(i); - var component = components.Find(x => x.CustomId == input.CustomId); - - if (component is null) - { - if (!throwOnMissingField) - args[i] = input.DefaultValue; - else - return TypeConverterResult.FromError(InteractionCommandError.BadArgs, $"Modal interaction is missing the required field: {input.CustomId}"); - } + if (!throwOnMissingField) + args[i] = input.DefaultValue; else - { - var readResult = await input.TypeConverter.ReadAsync(context, component, services).ConfigureAwait(false); + return TypeConverterResult.FromError(InteractionCommandError.BadArgs, $"Modal interaction is missing the required field: {input.CustomId}"); + } + else + { + var readResult = await input.TypeConverter.ReadAsync(context, component, services).ConfigureAwait(false); - if (!readResult.IsSuccess) - return readResult; + if (!readResult.IsSuccess) + return readResult; - args[i] = readResult.Value; - } + args[i] = readResult.Value; } - - return TypeConverterResult.FromSuccess(_initializer(args)); } + + return TypeConverterResult.FromSuccess(_initializer(args)); } } From 8fbca81c334e9ae8294cb7ed3cd43cfb931802f1 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Mon, 10 Nov 2025 10:14:10 +0100 Subject: [PATCH 32/59] add attachment modal typeconverter --- .../InteractionService.cs | 2496 ++++++++--------- .../AttachmentModalComponentConverter.cs | 14 + .../EnumModalComponentConverter.cs | 2 +- 3 files changed, 1262 insertions(+), 1250 deletions(-) create mode 100644 src/Discord.Net.Interactions/TypeConverters/ModalComponents/AttachmentModalComponentConverter.cs diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index c12cbe486a..fce5f8d428 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -4,9 +4,7 @@ using Discord.Logging; using Discord.Rest; using Discord.WebSocket; -using Newtonsoft.Json.Bson; using System; -using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -14,1442 +12,1442 @@ using System.Threading; using System.Threading.Tasks; -namespace Discord.Interactions +namespace Discord.Interactions; + +/// +/// Provides the framework for building and registering Discord Application Commands. +/// +public class InteractionService : IDisposable { /// - /// Provides the framework for building and registering Discord Application Commands. + /// Occurs when a Slash Command related information is received. + /// + public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } + internal readonly AsyncEvent> _logEvent = new(); + + /// + /// Occurs when any type of interaction is executed. /// - public class InteractionService : IDisposable + public event Func InteractionExecuted { - /// - /// Occurs when a Slash Command related information is received. - /// - public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } - internal readonly AsyncEvent> _logEvent = new(); - - /// - /// Occurs when any type of interaction is executed. - /// - public event Func InteractionExecuted + add { - add - { - SlashCommandExecuted += value; - ContextCommandExecuted += value; - ComponentCommandExecuted += value; - AutocompleteCommandExecuted += value; - ModalCommandExecuted += value; - } - remove - { - SlashCommandExecuted -= value; - ContextCommandExecuted -= value; - ComponentCommandExecuted -= value; - AutocompleteCommandExecuted -= value; - ModalCommandExecuted -= value; - } + SlashCommandExecuted += value; + ContextCommandExecuted += value; + ComponentCommandExecuted += value; + AutocompleteCommandExecuted += value; + ModalCommandExecuted += value; + } + remove + { + SlashCommandExecuted -= value; + ContextCommandExecuted -= value; + ComponentCommandExecuted -= value; + AutocompleteCommandExecuted -= value; + ModalCommandExecuted -= value; } + } + + /// + /// Occurs when a Slash Command is executed. + /// + public event Func SlashCommandExecuted { add { _slashCommandExecutedEvent.Add(value); } remove { _slashCommandExecutedEvent.Remove(value); } } + internal readonly AsyncEvent> _slashCommandExecutedEvent = new(); - /// - /// Occurs when a Slash Command is executed. - /// - public event Func SlashCommandExecuted { add { _slashCommandExecutedEvent.Add(value); } remove { _slashCommandExecutedEvent.Remove(value); } } - internal readonly AsyncEvent> _slashCommandExecutedEvent = new(); - - /// - /// Occurs when a Context Command is executed. - /// - public event Func ContextCommandExecuted { add { _contextCommandExecutedEvent.Add(value); } remove { _contextCommandExecutedEvent.Remove(value); } } - internal readonly AsyncEvent> _contextCommandExecutedEvent = new(); - - /// - /// Occurs when a Message Component command is executed. - /// - public event Func ComponentCommandExecuted { add { _componentCommandExecutedEvent.Add(value); } remove { _componentCommandExecutedEvent.Remove(value); } } - internal readonly AsyncEvent> _componentCommandExecutedEvent = new(); - - /// - /// Occurs when a Autocomplete command is executed. - /// - public event Func AutocompleteCommandExecuted { add { _autocompleteCommandExecutedEvent.Add(value); } remove { _autocompleteCommandExecutedEvent.Remove(value); } } - internal readonly AsyncEvent> _autocompleteCommandExecutedEvent = new(); - - /// - /// Occurs when a AutocompleteHandler is executed. - /// - public event Func AutocompleteHandlerExecuted { add { _autocompleteHandlerExecutedEvent.Add(value); } remove { _autocompleteHandlerExecutedEvent.Remove(value); } } - internal readonly AsyncEvent> _autocompleteHandlerExecutedEvent = new(); - - /// - /// Occurs when a Modal command is executed. - /// - public event Func ModalCommandExecuted { add { _modalCommandExecutedEvent.Add(value); } remove { _modalCommandExecutedEvent.Remove(value); } } - internal readonly AsyncEvent> _modalCommandExecutedEvent = new(); - - /// - /// Get the used by this Interaction Service instance to localize strings. - /// - public ILocalizationManager LocalizationManager { get; set; } - - private readonly ConcurrentDictionary _typedModuleDefs; - private readonly CommandMap _slashCommandMap; - private readonly ConcurrentDictionary> _contextCommandMaps; - private readonly CommandMap _componentCommandMap; - private readonly CommandMap _autocompleteCommandMap; - private readonly CommandMap _modalCommandMap; - private readonly HashSet _moduleDefs; - private readonly TypeMap _typeConverterMap; - private readonly TypeMap _compTypeConverterMap; - private readonly TypeMap _typeReaderMap; - private readonly TypeMap _modalInputTypeConverterMap; - private readonly ConcurrentDictionary _autocompleteHandlers = new(); - private readonly ConcurrentDictionary _modalInfos = new(); - private readonly SemaphoreSlim _lock; - internal readonly Logger _cmdLogger; - internal readonly LogManager _logManager; - internal readonly Func _getRestClient; - - internal readonly bool _throwOnError, _useCompiledLambda, _enableAutocompleteHandlers, _autoServiceScopes, _exitOnMissingModalField; - internal readonly string _wildCardExp; - internal readonly RunMode _runMode; - internal readonly RestResponseCallback _restResponseCallback; - - /// - /// Rest client to be used to register application commands. - /// - public DiscordRestClient RestClient { get => _getRestClient(); } - - /// - /// Represents all modules loaded within . - /// - public IReadOnlyList Modules => _moduleDefs.ToList(); - - /// - /// Represents all Slash Commands loaded within . - /// - public IReadOnlyList SlashCommands => _moduleDefs.SelectMany(x => x.SlashCommands).ToList(); - - /// - /// Represents all Context Commands loaded within . - /// - public IReadOnlyList ContextCommands => _moduleDefs.SelectMany(x => x.ContextCommands).ToList(); - - /// - /// Represents all Component Commands loaded within . - /// - public IReadOnlyCollection ComponentCommands => _moduleDefs.SelectMany(x => x.ComponentCommands).ToList(); - - /// - /// Represents all Modal Commands loaded within . - /// - public IReadOnlyCollection ModalCommands => _moduleDefs.SelectMany(x => x.ModalCommands).ToList(); - - /// - /// Gets a collection of the cached classes that are referenced in registered s. - /// - public IReadOnlyCollection Modals => ModalUtils.Modals; - - /// - /// Initialize a with provided configurations. - /// - /// The discord client. - /// The configuration class. - public InteractionService(DiscordRestClient discord, InteractionServiceConfig config = null) - : this(() => discord, config ?? new InteractionServiceConfig()) { } - - /// - /// Initialize a with provided configurations. - /// - /// The discord client provider. - /// The configuration class. - public InteractionService(IRestClientProvider discordProvider, InteractionServiceConfig config = null) - : this(() => discordProvider.RestClient, config ?? new InteractionServiceConfig()) { } - - private InteractionService(Func getRestClient, InteractionServiceConfig config = null) + /// + /// Occurs when a Context Command is executed. + /// + public event Func ContextCommandExecuted { add { _contextCommandExecutedEvent.Add(value); } remove { _contextCommandExecutedEvent.Remove(value); } } + internal readonly AsyncEvent> _contextCommandExecutedEvent = new(); + + /// + /// Occurs when a Message Component command is executed. + /// + public event Func ComponentCommandExecuted { add { _componentCommandExecutedEvent.Add(value); } remove { _componentCommandExecutedEvent.Remove(value); } } + internal readonly AsyncEvent> _componentCommandExecutedEvent = new(); + + /// + /// Occurs when a Autocomplete command is executed. + /// + public event Func AutocompleteCommandExecuted { add { _autocompleteCommandExecutedEvent.Add(value); } remove { _autocompleteCommandExecutedEvent.Remove(value); } } + internal readonly AsyncEvent> _autocompleteCommandExecutedEvent = new(); + + /// + /// Occurs when a AutocompleteHandler is executed. + /// + public event Func AutocompleteHandlerExecuted { add { _autocompleteHandlerExecutedEvent.Add(value); } remove { _autocompleteHandlerExecutedEvent.Remove(value); } } + internal readonly AsyncEvent> _autocompleteHandlerExecutedEvent = new(); + + /// + /// Occurs when a Modal command is executed. + /// + public event Func ModalCommandExecuted { add { _modalCommandExecutedEvent.Add(value); } remove { _modalCommandExecutedEvent.Remove(value); } } + internal readonly AsyncEvent> _modalCommandExecutedEvent = new(); + + /// + /// Get the used by this Interaction Service instance to localize strings. + /// + public ILocalizationManager LocalizationManager { get; set; } + + private readonly ConcurrentDictionary _typedModuleDefs; + private readonly CommandMap _slashCommandMap; + private readonly ConcurrentDictionary> _contextCommandMaps; + private readonly CommandMap _componentCommandMap; + private readonly CommandMap _autocompleteCommandMap; + private readonly CommandMap _modalCommandMap; + private readonly HashSet _moduleDefs; + private readonly TypeMap _typeConverterMap; + private readonly TypeMap _compTypeConverterMap; + private readonly TypeMap _typeReaderMap; + private readonly TypeMap _modalInputTypeConverterMap; + private readonly ConcurrentDictionary _autocompleteHandlers = new(); + private readonly ConcurrentDictionary _modalInfos = new(); + private readonly SemaphoreSlim _lock; + internal readonly Logger _cmdLogger; + internal readonly LogManager _logManager; + internal readonly Func _getRestClient; + + internal readonly bool _throwOnError, _useCompiledLambda, _enableAutocompleteHandlers, _autoServiceScopes, _exitOnMissingModalField; + internal readonly string _wildCardExp; + internal readonly RunMode _runMode; + internal readonly RestResponseCallback _restResponseCallback; + + /// + /// Rest client to be used to register application commands. + /// + public DiscordRestClient RestClient { get => _getRestClient(); } + + /// + /// Represents all modules loaded within . + /// + public IReadOnlyList Modules => _moduleDefs.ToList(); + + /// + /// Represents all Slash Commands loaded within . + /// + public IReadOnlyList SlashCommands => _moduleDefs.SelectMany(x => x.SlashCommands).ToList(); + + /// + /// Represents all Context Commands loaded within . + /// + public IReadOnlyList ContextCommands => _moduleDefs.SelectMany(x => x.ContextCommands).ToList(); + + /// + /// Represents all Component Commands loaded within . + /// + public IReadOnlyCollection ComponentCommands => _moduleDefs.SelectMany(x => x.ComponentCommands).ToList(); + + /// + /// Represents all Modal Commands loaded within . + /// + public IReadOnlyCollection ModalCommands => _moduleDefs.SelectMany(x => x.ModalCommands).ToList(); + + /// + /// Gets a collection of the cached classes that are referenced in registered s. + /// + public IReadOnlyCollection Modals => ModalUtils.Modals; + + /// + /// Initialize a with provided configurations. + /// + /// The discord client. + /// The configuration class. + public InteractionService(DiscordRestClient discord, InteractionServiceConfig config = null) + : this(() => discord, config ?? new InteractionServiceConfig()) { } + + /// + /// Initialize a with provided configurations. + /// + /// The discord client provider. + /// The configuration class. + public InteractionService(IRestClientProvider discordProvider, InteractionServiceConfig config = null) + : this(() => discordProvider.RestClient, config ?? new InteractionServiceConfig()) { } + + private InteractionService(Func getRestClient, InteractionServiceConfig config = null) + { + config ??= new InteractionServiceConfig(); + + _lock = new SemaphoreSlim(1, 1); + _typedModuleDefs = new ConcurrentDictionary(); + _moduleDefs = new HashSet(); + + _logManager = new LogManager(config.LogLevel); + _logManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); + _cmdLogger = _logManager.CreateLogger("App Commands"); + + _slashCommandMap = new CommandMap(this); + _contextCommandMaps = new ConcurrentDictionary>(); + _componentCommandMap = new CommandMap(this, config.InteractionCustomIdDelimiters); + _autocompleteCommandMap = new CommandMap(this); + _modalCommandMap = new CommandMap(this, config.InteractionCustomIdDelimiters); + + _getRestClient = getRestClient; + + _runMode = config.DefaultRunMode; + if (_runMode == RunMode.Default) + throw new InvalidOperationException($"RunMode cannot be set to {RunMode.Default}"); + + _throwOnError = config.ThrowOnError; + _wildCardExp = config.WildCardExpression; + _useCompiledLambda = config.UseCompiledLambda; + _exitOnMissingModalField = config.ExitOnMissingModalField; + _enableAutocompleteHandlers = config.EnableAutocompleteHandlers; + _autoServiceScopes = config.AutoServiceScopes; + _restResponseCallback = config.RestResponseCallback; + LocalizationManager = config.LocalizationManager; + + _typeConverterMap = new TypeMap(this, new ConcurrentDictionary { - config ??= new InteractionServiceConfig(); - - _lock = new SemaphoreSlim(1, 1); - _typedModuleDefs = new ConcurrentDictionary(); - _moduleDefs = new HashSet(); - - _logManager = new LogManager(config.LogLevel); - _logManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); - _cmdLogger = _logManager.CreateLogger("App Commands"); - - _slashCommandMap = new CommandMap(this); - _contextCommandMaps = new ConcurrentDictionary>(); - _componentCommandMap = new CommandMap(this, config.InteractionCustomIdDelimiters); - _autocompleteCommandMap = new CommandMap(this); - _modalCommandMap = new CommandMap(this, config.InteractionCustomIdDelimiters); - - _getRestClient = getRestClient; - - _runMode = config.DefaultRunMode; - if (_runMode == RunMode.Default) - throw new InvalidOperationException($"RunMode cannot be set to {RunMode.Default}"); - - _throwOnError = config.ThrowOnError; - _wildCardExp = config.WildCardExpression; - _useCompiledLambda = config.UseCompiledLambda; - _exitOnMissingModalField = config.ExitOnMissingModalField; - _enableAutocompleteHandlers = config.EnableAutocompleteHandlers; - _autoServiceScopes = config.AutoServiceScopes; - _restResponseCallback = config.RestResponseCallback; - LocalizationManager = config.LocalizationManager; - - _typeConverterMap = new TypeMap(this, new ConcurrentDictionary - { - [typeof(TimeSpan)] = new TimeSpanConverter() - }, new ConcurrentDictionary + [typeof(TimeSpan)] = new TimeSpanConverter() + }, new ConcurrentDictionary + { + [typeof(IChannel)] = typeof(DefaultChannelConverter<>), + [typeof(IRole)] = typeof(DefaultRoleConverter<>), + [typeof(IAttachment)] = typeof(DefaultAttachmentConverter<>), + [typeof(IUser)] = typeof(DefaultUserConverter<>), + [typeof(IMentionable)] = typeof(DefaultMentionableConverter<>), + [typeof(IConvertible)] = typeof(DefaultValueConverter<>), + [typeof(Enum)] = typeof(EnumConverter<>), + [typeof(Nullable<>)] = typeof(NullableConverter<>) + }); + + _compTypeConverterMap = new TypeMap(this, new ConcurrentDictionary(), + new ConcurrentDictionary { - [typeof(IChannel)] = typeof(DefaultChannelConverter<>), - [typeof(IRole)] = typeof(DefaultRoleConverter<>), - [typeof(IAttachment)] = typeof(DefaultAttachmentConverter<>), - [typeof(IUser)] = typeof(DefaultUserConverter<>), - [typeof(IMentionable)] = typeof(DefaultMentionableConverter<>), - [typeof(IConvertible)] = typeof(DefaultValueConverter<>), - [typeof(Enum)] = typeof(EnumConverter<>), - [typeof(Nullable<>)] = typeof(NullableConverter<>) + [typeof(Array)] = typeof(DefaultArrayComponentConverter<>), + [typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>), + [typeof(Nullable<>)] = typeof(NullableComponentConverter<>) }); - _compTypeConverterMap = new TypeMap(this, new ConcurrentDictionary(), - new ConcurrentDictionary - { - [typeof(Array)] = typeof(DefaultArrayComponentConverter<>), - [typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>), - [typeof(Nullable<>)] = typeof(NullableComponentConverter<>) - }); - - _typeReaderMap = new TypeMap(this, new ConcurrentDictionary(), - new ConcurrentDictionary - { - [typeof(IChannel)] = typeof(DefaultChannelReader<>), - [typeof(IRole)] = typeof(DefaultRoleReader<>), - [typeof(IUser)] = typeof(DefaultUserReader<>), - [typeof(IMessage)] = typeof(DefaultMessageReader<>), - [typeof(IConvertible)] = typeof(DefaultValueReader<>), - [typeof(Enum)] = typeof(EnumReader<>), - [typeof(Nullable<>)] = typeof(NullableReader<>) - }); - - _modalInputTypeConverterMap = new TypeMap(this, new ConcurrentDictionary + _typeReaderMap = new TypeMap(this, new ConcurrentDictionary(), + new ConcurrentDictionary { - }, new ConcurrentDictionary - { - [typeof(IConvertible)] = typeof(DefaultValueModalComponentConverter<>), - [typeof(Enum)] = typeof(EnumModalComponentConverter<>), - [typeof(Nullable<>)] = typeof(NullableComponentConverter<>), - [typeof(Array)] = typeof(DefaultArrayModalComponentConverter<>) + [typeof(IChannel)] = typeof(DefaultChannelReader<>), + [typeof(IRole)] = typeof(DefaultRoleReader<>), + [typeof(IUser)] = typeof(DefaultUserReader<>), + [typeof(IMessage)] = typeof(DefaultMessageReader<>), + [typeof(IConvertible)] = typeof(DefaultValueReader<>), + [typeof(Enum)] = typeof(EnumReader<>), + [typeof(Nullable<>)] = typeof(NullableReader<>) }); - } - /// - /// Create and loads a using a builder factory. - /// - /// Name of the module. - /// The for your dependency injection solution if using one; otherwise, pass . - /// Module builder factory. - /// - /// A task representing the operation for adding modules. The task result contains the built module instance. - /// - public async Task CreateModuleAsync(string name, IServiceProvider services, Action buildFunc) + _modalInputTypeConverterMap = new TypeMap(this, new ConcurrentDictionary { - services ??= EmptyServiceProvider.Instance; + }, new ConcurrentDictionary + { + [typeof(IConvertible)] = typeof(DefaultValueModalComponentConverter<>), + [typeof(Enum)] = typeof(EnumModalComponentConverter<>), + [typeof(Nullable<>)] = typeof(NullableComponentConverter<>), + [typeof(Array)] = typeof(DefaultArrayModalComponentConverter<>), + [typeof(IAttachment)] = typeof(AttachmentModalComponentConverter<>) + }); + } - await _lock.WaitAsync().ConfigureAwait(false); - try - { - var builder = new ModuleBuilder(this, name); - buildFunc(builder); + /// + /// Create and loads a using a builder factory. + /// + /// Name of the module. + /// The for your dependency injection solution if using one; otherwise, pass . + /// Module builder factory. + /// + /// A task representing the operation for adding modules. The task result contains the built module instance. + /// + public async Task CreateModuleAsync(string name, IServiceProvider services, Action buildFunc) + { + services ??= EmptyServiceProvider.Instance; - var moduleInfo = builder.Build(this, services); - LoadModuleInternal(moduleInfo); + await _lock.WaitAsync().ConfigureAwait(false); + try + { + var builder = new ModuleBuilder(this, name); + buildFunc(builder); - return moduleInfo; - } - finally - { - _lock.Release(); - } - } + var moduleInfo = builder.Build(this, services); + LoadModuleInternal(moduleInfo); - /// - /// Discover and load command modules from an . - /// - /// the command modules are defined in. - /// The for your dependency injection solution if using one; otherwise, pass . - /// - /// A task representing the operation for adding modules. The task result contains a collection of the modules added. - /// - public async Task> AddModulesAsync(Assembly assembly, IServiceProvider services) + return moduleInfo; + } + finally { - services ??= EmptyServiceProvider.Instance; + _lock.Release(); + } + } - await _lock.WaitAsync().ConfigureAwait(false); + /// + /// Discover and load command modules from an . + /// + /// the command modules are defined in. + /// The for your dependency injection solution if using one; otherwise, pass . + /// + /// A task representing the operation for adding modules. The task result contains a collection of the modules added. + /// + public async Task> AddModulesAsync(Assembly assembly, IServiceProvider services) + { + services ??= EmptyServiceProvider.Instance; - try - { - var types = await ModuleClassBuilder.SearchAsync(assembly, this); - var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this, services); - - foreach (var info in moduleDefs) - { - _typedModuleDefs[info.Key] = info.Value; - LoadModuleInternal(info.Value); - } - return moduleDefs.Values; - } - finally + await _lock.WaitAsync().ConfigureAwait(false); + + try + { + var types = await ModuleClassBuilder.SearchAsync(assembly, this); + var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this, services); + + foreach (var info in moduleDefs) { - _lock.Release(); + _typedModuleDefs[info.Key] = info.Value; + LoadModuleInternal(info.Value); } + return moduleDefs.Values; } - - /// - /// Add a command module from a . - /// - /// Type of the module. - /// The for your dependency injection solution if using one; otherwise, pass . - /// - /// A task representing the operation for adding the module. The task result contains the built module. - /// - /// - /// Thrown if this module has already been added. - /// - /// - /// Thrown when the is not a valid module definition. - /// - public Task AddModuleAsync(IServiceProvider services) where T : class => - AddModuleAsync(typeof(T), services); - - /// - /// Add a command module from a . - /// - /// Type of the module. - /// The for your dependency injection solution if using one; otherwise, pass . - /// - /// A task representing the operation for adding the module. The task result contains the built module. - /// - /// - /// Thrown if this module has already been added. - /// - /// - /// Thrown when the is not a valid module definition. - /// - public async Task AddModuleAsync(Type type, IServiceProvider services) + finally { - if (!typeof(IInteractionModuleBase).IsAssignableFrom(type)) - throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(type)); + _lock.Release(); + } + } - services ??= EmptyServiceProvider.Instance; + /// + /// Add a command module from a . + /// + /// Type of the module. + /// The for your dependency injection solution if using one; otherwise, pass . + /// + /// A task representing the operation for adding the module. The task result contains the built module. + /// + /// + /// Thrown if this module has already been added. + /// + /// + /// Thrown when the is not a valid module definition. + /// + public Task AddModuleAsync(IServiceProvider services) where T : class => + AddModuleAsync(typeof(T), services); - await _lock.WaitAsync().ConfigureAwait(false); + /// + /// Add a command module from a . + /// + /// Type of the module. + /// The for your dependency injection solution if using one; otherwise, pass . + /// + /// A task representing the operation for adding the module. The task result contains the built module. + /// + /// + /// Thrown if this module has already been added. + /// + /// + /// Thrown when the is not a valid module definition. + /// + public async Task AddModuleAsync(Type type, IServiceProvider services) + { + if (!typeof(IInteractionModuleBase).IsAssignableFrom(type)) + throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(type)); - try - { - var typeInfo = type.GetTypeInfo(); + services ??= EmptyServiceProvider.Instance; - if (_typedModuleDefs.ContainsKey(typeInfo)) - throw new ArgumentException("Module definition for this type already exists."); + await _lock.WaitAsync().ConfigureAwait(false); - var moduleDef = (await ModuleClassBuilder.BuildAsync(new List { typeInfo }, this, services).ConfigureAwait(false)).FirstOrDefault(); + try + { + var typeInfo = type.GetTypeInfo(); - if (moduleDef.Value == default) - throw new InvalidOperationException($"Could not build the module {typeInfo.FullName}, did you pass an invalid type?"); + if (_typedModuleDefs.ContainsKey(typeInfo)) + throw new ArgumentException("Module definition for this type already exists."); - if (!_typedModuleDefs.TryAdd(type, moduleDef.Value)) - throw new ArgumentException("Module definition for this type already exists."); + var moduleDef = (await ModuleClassBuilder.BuildAsync(new List { typeInfo }, this, services).ConfigureAwait(false)).FirstOrDefault(); - _typedModuleDefs[moduleDef.Key] = moduleDef.Value; - LoadModuleInternal(moduleDef.Value); + if (moduleDef.Value == default) + throw new InvalidOperationException($"Could not build the module {typeInfo.FullName}, did you pass an invalid type?"); - return moduleDef.Value; - } - finally - { - _lock.Release(); - } - } + if (!_typedModuleDefs.TryAdd(type, moduleDef.Value)) + throw new ArgumentException("Module definition for this type already exists."); - /// - /// Register Application Commands from and to a guild. - /// - /// Id of the target guild. - /// If , this operation will not delete the commands that are missing from . - /// - /// A task representing the command registration process. The task result contains the active application commands of the target guild. - /// - public async Task> RegisterCommandsToGuildAsync(ulong guildId, bool deleteMissing = true) + _typedModuleDefs[moduleDef.Key] = moduleDef.Value; + LoadModuleInternal(moduleDef.Value); + + return moduleDef.Value; + } + finally { - EnsureClientReady(); + _lock.Release(); + } + } - var topLevelModules = _moduleDefs.Where(x => !x.IsSubModule); - var props = topLevelModules.SelectMany(x => x.ToApplicationCommandProps()).ToList(); + /// + /// Register Application Commands from and to a guild. + /// + /// Id of the target guild. + /// If , this operation will not delete the commands that are missing from . + /// + /// A task representing the command registration process. The task result contains the active application commands of the target guild. + /// + public async Task> RegisterCommandsToGuildAsync(ulong guildId, bool deleteMissing = true) + { + EnsureClientReady(); - if (!deleteMissing) - { + var topLevelModules = _moduleDefs.Where(x => !x.IsSubModule); + var props = topLevelModules.SelectMany(x => x.ToApplicationCommandProps()).ToList(); - var existing = await RestClient.GetGuildApplicationCommands(guildId, true).ConfigureAwait(false); - var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); - props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); - } + if (!deleteMissing) + { - return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); + var existing = await RestClient.GetGuildApplicationCommands(guildId, true).ConfigureAwait(false); + var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); + props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); } - /// - /// Register Application Commands from and to Discord on in global scope. - /// - /// If , this operation will not delete the commands that are missing from . - /// - /// A task representing the command registration process. The task result contains the active global application commands of bot. - /// - public async Task> RegisterCommandsGloballyAsync(bool deleteMissing = true) + return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); + } + + /// + /// Register Application Commands from and to Discord on in global scope. + /// + /// If , this operation will not delete the commands that are missing from . + /// + /// A task representing the command registration process. The task result contains the active global application commands of bot. + /// + public async Task> RegisterCommandsGloballyAsync(bool deleteMissing = true) + { + EnsureClientReady(); + + var topLevelModules = _moduleDefs.Where(x => !x.IsSubModule); + var props = topLevelModules.SelectMany(x => x.ToApplicationCommandProps()).ToList(); + + if (!deleteMissing) { - EnsureClientReady(); + var existing = await RestClient.GetGlobalApplicationCommands(true).ConfigureAwait(false); + var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); + props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); + } + + return await RestClient.BulkOverwriteGlobalCommands(props.ToArray()).ConfigureAwait(false); + } + + /// + /// Register Application Commands from to a guild. + /// + /// + /// Commands will be registered as standalone commands, if you want the to take effect, + /// use . Registering a commands without group names might cause the command traversal to fail. + /// + /// The target guild. + /// If , this operation will not delete the commands that are missing from . + /// Commands to be registered to Discord. + /// + /// A task representing the command registration process. The task result contains the active application commands of the target guild. + /// + public Task> AddCommandsToGuildAsync(IGuild guild, bool deleteMissing = false, params ICommandInfo[] commands) + { + if (guild is null) + throw new ArgumentNullException(nameof(guild)); + + return AddCommandsToGuildAsync(guild.Id, deleteMissing, commands); + } + + /// + /// Register Application Commands from to a guild. + /// + /// + /// Commands will be registered as standalone commands, if you want the to take effect, + /// use . Registering a commands without group names might cause the command traversal to fail. + /// + /// The target guild ID. + /// If , this operation will not delete the commands that are missing from . + /// Commands to be registered to Discord. + /// + /// A task representing the command registration process. The task result contains the active application commands of the target guild. + /// + public async Task> AddCommandsToGuildAsync(ulong guildId, bool deleteMissing = false, params ICommandInfo[] commands) + { + EnsureClientReady(); - var topLevelModules = _moduleDefs.Where(x => !x.IsSubModule); - var props = topLevelModules.SelectMany(x => x.ToApplicationCommandProps()).ToList(); + var props = new List(); - if (!deleteMissing) + foreach (var command in commands) + { + switch (command) { - var existing = await RestClient.GetGlobalApplicationCommands(true).ConfigureAwait(false); - var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); - props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); + case SlashCommandInfo slashCommand: + props.Add(slashCommand.ToApplicationCommandProps()); + break; + case ContextCommandInfo contextCommand: + props.Add(contextCommand.ToApplicationCommandProps()); + break; + default: + throw new InvalidOperationException($"Command type {command.GetType().FullName} isn't supported yet"); } - - return await RestClient.BulkOverwriteGlobalCommands(props.ToArray()).ConfigureAwait(false); } - /// - /// Register Application Commands from to a guild. - /// - /// - /// Commands will be registered as standalone commands, if you want the to take effect, - /// use . Registering a commands without group names might cause the command traversal to fail. - /// - /// The target guild. - /// If , this operation will not delete the commands that are missing from . - /// Commands to be registered to Discord. - /// - /// A task representing the command registration process. The task result contains the active application commands of the target guild. - /// - public Task> AddCommandsToGuildAsync(IGuild guild, bool deleteMissing = false, params ICommandInfo[] commands) + if (!deleteMissing) { - if (guild is null) - throw new ArgumentNullException(nameof(guild)); + var existing = await RestClient.GetGuildApplicationCommands(guildId, true).ConfigureAwait(false); + var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); + props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); + } - return AddCommandsToGuildAsync(guild.Id, deleteMissing, commands); + return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); + } + + /// + /// Register Application Commands from modules provided in to a guild. + /// + /// The target guild. + /// If , this operation will not delete the commands that are missing from . + /// Modules to be registered to Discord. + /// + /// A task representing the command registration process. The task result contains the active application commands of the target guild. + /// + public Task> AddModulesToGuildAsync(IGuild guild, bool deleteMissing = false, params ModuleInfo[] modules) + { + if (guild is null) + throw new ArgumentNullException(nameof(guild)); + + return AddModulesToGuildAsync(guild.Id, deleteMissing, modules); + } + + /// + /// Register Application Commands from modules provided in to a guild. + /// + /// The target guild ID. + /// If , this operation will not delete the commands that are missing from . + /// Modules to be registered to Discord. + /// + /// A task representing the command registration process. The task result contains the active application commands of the target guild. + /// + public async Task> AddModulesToGuildAsync(ulong guildId, bool deleteMissing = false, params ModuleInfo[] modules) + { + EnsureClientReady(); + + var props = modules.SelectMany(x => x.ToApplicationCommandProps(true)).Distinct().ToList(); + + if (!deleteMissing) + { + var existing = await RestClient.GetGuildApplicationCommands(guildId, true).ConfigureAwait(false); + var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); + props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); } - /// - /// Register Application Commands from to a guild. - /// - /// - /// Commands will be registered as standalone commands, if you want the to take effect, - /// use . Registering a commands without group names might cause the command traversal to fail. - /// - /// The target guild ID. - /// If , this operation will not delete the commands that are missing from . - /// Commands to be registered to Discord. - /// - /// A task representing the command registration process. The task result contains the active application commands of the target guild. - /// - public async Task> AddCommandsToGuildAsync(ulong guildId, bool deleteMissing = false, params ICommandInfo[] commands) + return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); + } + + /// + /// Register Application Commands from modules provided in as global commands. + /// + /// If , this operation will not delete the commands that are missing from . + /// Modules to be registered to Discord. + /// + /// A task representing the command registration process. The task result contains the active application commands of the target guild. + /// + public async Task> AddModulesGloballyAsync(bool deleteMissing = false, params ModuleInfo[] modules) + { + EnsureClientReady(); + + var props = modules.SelectMany(x => x.ToApplicationCommandProps(true)).Distinct().ToList(); + + if (!deleteMissing) { - EnsureClientReady(); + var existing = await RestClient.GetGlobalApplicationCommands(true).ConfigureAwait(false); + var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); + props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); + } - var props = new List(); + return await RestClient.BulkOverwriteGlobalCommands(props.ToArray()).ConfigureAwait(false); + } - foreach (var command in commands) - { - switch (command) - { - case SlashCommandInfo slashCommand: - props.Add(slashCommand.ToApplicationCommandProps()); - break; - case ContextCommandInfo contextCommand: - props.Add(contextCommand.ToApplicationCommandProps()); - break; - default: - throw new InvalidOperationException($"Command type {command.GetType().FullName} isn't supported yet"); - } - } + /// + /// Register Application Commands from as global commands. + /// + /// + /// Commands will be registered as standalone commands, if you want the to take effect, + /// use . Registering a commands without group names might cause the command traversal to fail. + /// + /// If , this operation will not delete the commands that are missing from . + /// Commands to be registered to Discord. + /// + /// A task representing the command registration process. The task result contains the active application commands of the target guild. + /// + public async Task> AddCommandsGloballyAsync(bool deleteMissing = false, params IApplicationCommandInfo[] commands) + { + EnsureClientReady(); - if (!deleteMissing) + var props = new List(); + + foreach (var command in commands) + { + switch (command) { - var existing = await RestClient.GetGuildApplicationCommands(guildId, true).ConfigureAwait(false); - var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); - props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); + case SlashCommandInfo slashCommand: + props.Add(slashCommand.ToApplicationCommandProps()); + break; + case ContextCommandInfo contextCommand: + props.Add(contextCommand.ToApplicationCommandProps()); + break; + default: + throw new InvalidOperationException($"Command type {command.GetType().FullName} isn't supported yet"); } + } - return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); + if (!deleteMissing) + { + var existing = await RestClient.GetGlobalApplicationCommands(true).ConfigureAwait(false); + var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); + props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); } - /// - /// Register Application Commands from modules provided in to a guild. - /// - /// The target guild. - /// If , this operation will not delete the commands that are missing from . - /// Modules to be registered to Discord. - /// - /// A task representing the command registration process. The task result contains the active application commands of the target guild. - /// - public Task> AddModulesToGuildAsync(IGuild guild, bool deleteMissing = false, params ModuleInfo[] modules) + return await RestClient.BulkOverwriteGlobalCommands(props.ToArray()).ConfigureAwait(false); + } + + private void LoadModuleInternal(ModuleInfo module) + { + _moduleDefs.Add(module); + + foreach (var command in module.SlashCommands) + _slashCommandMap.AddCommand(command, command.IgnoreGroupNames); + + foreach (var command in module.ContextCommands) + _contextCommandMaps.GetOrAdd(command.CommandType, new CommandMap(this)).AddCommand(command, command.IgnoreGroupNames); + + foreach (var interaction in module.ComponentCommands) + _componentCommandMap.AddCommand(interaction, interaction.IgnoreGroupNames); + + foreach (var command in module.AutocompleteCommands) + _autocompleteCommandMap.AddCommand(command.GetCommandKeywords(), command); + + foreach (var command in module.ModalCommands) + _modalCommandMap.AddCommand(command, command.IgnoreGroupNames); + + foreach (var subModule in module.SubModules) + LoadModuleInternal(subModule); + } + + /// + /// Remove a command module. + /// + /// The of the module. + /// + /// A task that represents the asynchronous removal operation. The task result contains a value that + /// indicates whether the module is successfully removed. + /// + public Task RemoveModuleAsync() => + RemoveModuleAsync(typeof(T)); + + /// + /// Remove a command module. + /// + /// The of the module. + /// + /// A task that represents the asynchronous removal operation. The task result contains a value that + /// indicates whether the module is successfully removed. + /// + public async Task RemoveModuleAsync(Type type) + { + await _lock.WaitAsync().ConfigureAwait(false); + + try { - if (guild is null) - throw new ArgumentNullException(nameof(guild)); + if (!_typedModuleDefs.TryRemove(type, out var module)) + return false; - return AddModulesToGuildAsync(guild.Id, deleteMissing, modules); + return RemoveModuleInternal(module); } - - /// - /// Register Application Commands from modules provided in to a guild. - /// - /// The target guild ID. - /// If , this operation will not delete the commands that are missing from . - /// Modules to be registered to Discord. - /// - /// A task representing the command registration process. The task result contains the active application commands of the target guild. - /// - public async Task> AddModulesToGuildAsync(ulong guildId, bool deleteMissing = false, params ModuleInfo[] modules) + finally { - EnsureClientReady(); + _lock.Release(); + } + } - var props = modules.SelectMany(x => x.ToApplicationCommandProps(true)).Distinct().ToList(); + /// + /// Remove a command module. + /// + /// The to be removed from the service. + /// + /// A task that represents the asynchronous removal operation. The task result contains a value that + /// indicates whether the is successfully removed. + /// + public async Task RemoveModuleAsync(ModuleInfo module) + { + await _lock.WaitAsync().ConfigureAwait(false); - if (!deleteMissing) - { - var existing = await RestClient.GetGuildApplicationCommands(guildId, true).ConfigureAwait(false); - var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); - props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); - } + try + { + var typeModulePair = _typedModuleDefs.FirstOrDefault(x => x.Value.Equals(module)); - return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); - } + if (!typeModulePair.Equals(default(KeyValuePair))) + _typedModuleDefs.TryRemove(typeModulePair.Key, out var _); - /// - /// Register Application Commands from modules provided in as global commands. - /// - /// If , this operation will not delete the commands that are missing from . - /// Modules to be registered to Discord. - /// - /// A task representing the command registration process. The task result contains the active application commands of the target guild. - /// - public async Task> AddModulesGloballyAsync(bool deleteMissing = false, params ModuleInfo[] modules) + return RemoveModuleInternal(module); + } + finally { - EnsureClientReady(); + _lock.Release(); + } + } - var props = modules.SelectMany(x => x.ToApplicationCommandProps(true)).Distinct().ToList(); + /// + /// Unregister Application Commands from modules provided in from a guild. + /// + /// The target guild. + /// Modules to be deregistered from Discord. + /// + /// A task representing the command de-registration process. The task result contains the active application commands of the target guild. + /// + public Task> RemoveModulesFromGuildAsync(IGuild guild, params ModuleInfo[] modules) + { + if (guild is null) + throw new ArgumentNullException(nameof(guild)); - if (!deleteMissing) - { - var existing = await RestClient.GetGlobalApplicationCommands(true).ConfigureAwait(false); - var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); - props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); - } + return RemoveModulesFromGuildAsync(guild.Id, modules); + } - return await RestClient.BulkOverwriteGlobalCommands(props.ToArray()).ConfigureAwait(false); - } + /// + /// Unregister Application Commands from modules provided in from a guild. + /// + /// The target guild ID. + /// Modules to be deregistered from Discord. + /// + /// A task representing the command de-registration process. The task result contains the active application commands of the target guild. + /// + public async Task> RemoveModulesFromGuildAsync(ulong guildId, params ModuleInfo[] modules) + { + EnsureClientReady(); - /// - /// Register Application Commands from as global commands. - /// - /// - /// Commands will be registered as standalone commands, if you want the to take effect, - /// use . Registering a commands without group names might cause the command traversal to fail. - /// - /// If , this operation will not delete the commands that are missing from . - /// Commands to be registered to Discord. - /// - /// A task representing the command registration process. The task result contains the active application commands of the target guild. - /// - public async Task> AddCommandsGloballyAsync(bool deleteMissing = false, params IApplicationCommandInfo[] commands) - { - EnsureClientReady(); + var exclude = modules.SelectMany(x => x.ToApplicationCommandProps(true)).ToList(); + var existing = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); - var props = new List(); + var props = existing.Where(x => !exclude.Any(y => y.Name.IsSpecified && x.Name == y.Name.Value)).Select(x => x.ToApplicationCommandProps()); - foreach (var command in commands) - { - switch (command) - { - case SlashCommandInfo slashCommand: - props.Add(slashCommand.ToApplicationCommandProps()); - break; - case ContextCommandInfo contextCommand: - props.Add(contextCommand.ToApplicationCommandProps()); - break; - default: - throw new InvalidOperationException($"Command type {command.GetType().FullName} isn't supported yet"); - } - } + return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); + } - if (!deleteMissing) - { - var existing = await RestClient.GetGlobalApplicationCommands(true).ConfigureAwait(false); - var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); - props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); - } + private bool RemoveModuleInternal(ModuleInfo moduleInfo) + { + if (!_moduleDefs.Remove(moduleInfo)) + return false; - return await RestClient.BulkOverwriteGlobalCommands(props.ToArray()).ConfigureAwait(false); + foreach (var command in moduleInfo.SlashCommands) + { + _slashCommandMap.RemoveCommand(command); } - private void LoadModuleInternal(ModuleInfo module) - { - _moduleDefs.Add(module); + return true; + } - foreach (var command in module.SlashCommands) - _slashCommandMap.AddCommand(command, command.IgnoreGroupNames); + /// + /// Search the registered slash commands using a . + /// + /// Interaction entity to perform the search with. + /// + /// The search result. When successful, result contains the found . + /// + public SearchResult SearchSlashCommand(ISlashCommandInteraction slashCommandInteraction) + => _slashCommandMap.GetCommand(slashCommandInteraction.Data.GetCommandKeywords()); - foreach (var command in module.ContextCommands) - _contextCommandMaps.GetOrAdd(command.CommandType, new CommandMap(this)).AddCommand(command, command.IgnoreGroupNames); + /// + /// Search the registered slash commands using a . + /// + /// Interaction entity to perform the search with. + /// + /// The search result. When successful, result contains the found . + /// + public SearchResult SearchComponentCommand(IComponentInteraction componentInteraction) + => _componentCommandMap.GetCommand(componentInteraction.Data.CustomId); - foreach (var interaction in module.ComponentCommands) - _componentCommandMap.AddCommand(interaction, interaction.IgnoreGroupNames); + /// + /// Search the registered slash commands using a . + /// + /// Interaction entity to perform the search with. + /// + /// The search result. When successful, result contains the found . + /// + public SearchResult SearchUserCommand(IUserCommandInteraction userCommandInteraction) + => _contextCommandMaps[ApplicationCommandType.User].GetCommand(userCommandInteraction.Data.Name); - foreach (var command in module.AutocompleteCommands) - _autocompleteCommandMap.AddCommand(command.GetCommandKeywords(), command); + /// + /// Search the registered slash commands using a . + /// + /// Interaction entity to perform the search with. + /// + /// The search result. When successful, result contains the found . + /// + public SearchResult SearchMessageCommand(IMessageCommandInteraction messageCommandInteraction) + => _contextCommandMaps[ApplicationCommandType.Message].GetCommand(messageCommandInteraction.Data.Name); - foreach (var command in module.ModalCommands) - _modalCommandMap.AddCommand(command, command.IgnoreGroupNames); + /// + /// Search the registered slash commands using a . + /// + /// Interaction entity to perform the search with. + /// + /// The search result. When successful, result contains the found . + /// + public SearchResult SearchAutocompleteCommand(IAutocompleteInteraction autocompleteInteraction) + { + var keywords = autocompleteInteraction.Data.GetCommandKeywords(); + keywords.Add(autocompleteInteraction.Data.Current.Name); + return _autocompleteCommandMap.GetCommand(keywords); + } - foreach (var subModule in module.SubModules) - LoadModuleInternal(subModule); - } + /// + /// Execute a Command from a given . + /// + /// Name context of the command. + /// The service to be used in the command's dependency injection. + /// + /// A task representing the command execution process. The task result contains the result of the execution. + /// + public async Task ExecuteCommandAsync(IInteractionContext context, IServiceProvider services) + { + var interaction = context.Interaction; - /// - /// Remove a command module. - /// - /// The of the module. - /// - /// A task that represents the asynchronous removal operation. The task result contains a value that - /// indicates whether the module is successfully removed. - /// - public Task RemoveModuleAsync() => - RemoveModuleAsync(typeof(T)); - - /// - /// Remove a command module. - /// - /// The of the module. - /// - /// A task that represents the asynchronous removal operation. The task result contains a value that - /// indicates whether the module is successfully removed. - /// - public async Task RemoveModuleAsync(Type type) + return interaction switch { - await _lock.WaitAsync().ConfigureAwait(false); + ISlashCommandInteraction slashCommand => await ExecuteSlashCommandAsync(context, slashCommand, services).ConfigureAwait(false), + IComponentInteraction messageComponent => await ExecuteComponentCommandAsync(context, messageComponent.Data.CustomId, services).ConfigureAwait(false), + IUserCommandInteraction userCommand => await ExecuteContextCommandAsync(context, userCommand.Data.Name, ApplicationCommandType.User, services).ConfigureAwait(false), + IMessageCommandInteraction messageCommand => await ExecuteContextCommandAsync(context, messageCommand.Data.Name, ApplicationCommandType.Message, services).ConfigureAwait(false), + IAutocompleteInteraction autocomplete => await ExecuteAutocompleteAsync(context, autocomplete, services).ConfigureAwait(false), + IModalInteraction modalCommand => await ExecuteModalCommandAsync(context, modalCommand.Data.CustomId, services).ConfigureAwait(false), + _ => throw new InvalidOperationException($"{interaction.Type} interaction type cannot be executed by the Interaction service"), + }; + } - try - { - if (!_typedModuleDefs.TryRemove(type, out var module)) - return false; + private async Task ExecuteSlashCommandAsync(IInteractionContext context, ISlashCommandInteraction interaction, IServiceProvider services) + { + var keywords = interaction.Data.GetCommandKeywords(); - return RemoveModuleInternal(module); - } - finally - { - _lock.Release(); - } - } + var result = _slashCommandMap.GetCommand(keywords); - /// - /// Remove a command module. - /// - /// The to be removed from the service. - /// - /// A task that represents the asynchronous removal operation. The task result contains a value that - /// indicates whether the is successfully removed. - /// - public async Task RemoveModuleAsync(ModuleInfo module) + if (!result.IsSuccess) { - await _lock.WaitAsync().ConfigureAwait(false); + await _cmdLogger.DebugAsync($"Unknown slash command, skipping execution ({string.Join(" ", keywords).ToUpper()})"); - try - { - var typeModulePair = _typedModuleDefs.FirstOrDefault(x => x.Value.Equals(module)); + await _slashCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); + return result; + } + return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); + } - if (!typeModulePair.Equals(default(KeyValuePair))) - _typedModuleDefs.TryRemove(typeModulePair.Key, out var _); + private async Task ExecuteContextCommandAsync(IInteractionContext context, string input, ApplicationCommandType commandType, IServiceProvider services) + { + if (!_contextCommandMaps.TryGetValue(commandType, out var map)) + return SearchResult.FromError(input, InteractionCommandError.UnknownCommand, $"No {commandType} command found."); - return RemoveModuleInternal(module); - } - finally - { - _lock.Release(); - } - } + var result = map.GetCommand(input); - /// - /// Unregister Application Commands from modules provided in from a guild. - /// - /// The target guild. - /// Modules to be deregistered from Discord. - /// - /// A task representing the command de-registration process. The task result contains the active application commands of the target guild. - /// - public Task> RemoveModulesFromGuildAsync(IGuild guild, params ModuleInfo[] modules) + if (!result.IsSuccess) { - if (guild is null) - throw new ArgumentNullException(nameof(guild)); + await _cmdLogger.DebugAsync($"Unknown context command, skipping execution ({result.Text.ToUpper()})"); - return RemoveModulesFromGuildAsync(guild.Id, modules); + await _contextCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); + return result; } + return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); + } - /// - /// Unregister Application Commands from modules provided in from a guild. - /// - /// The target guild ID. - /// Modules to be deregistered from Discord. - /// - /// A task representing the command de-registration process. The task result contains the active application commands of the target guild. - /// - public async Task> RemoveModulesFromGuildAsync(ulong guildId, params ModuleInfo[] modules) + private async Task ExecuteComponentCommandAsync(IInteractionContext context, string input, IServiceProvider services) + { + var result = _componentCommandMap.GetCommand(input); + + if (!result.IsSuccess) { - EnsureClientReady(); + await _cmdLogger.DebugAsync($"Unknown custom interaction id, skipping execution ({input.ToUpper()})"); - var exclude = modules.SelectMany(x => x.ToApplicationCommandProps(true)).ToList(); - var existing = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); + await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); + return result; + } - var props = existing.Where(x => !exclude.Any(y => y.Name.IsSpecified && x.Name == y.Name.Value)).Select(x => x.ToApplicationCommandProps()); + SetMatchesIfApplicable(context, result); - return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); - } + return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); + } + + private async Task ExecuteAutocompleteAsync(IInteractionContext context, IAutocompleteInteraction interaction, IServiceProvider services) + { + var keywords = interaction.Data.GetCommandKeywords(); - private bool RemoveModuleInternal(ModuleInfo moduleInfo) + if (_enableAutocompleteHandlers) { - if (!_moduleDefs.Remove(moduleInfo)) - return false; + var autocompleteHandlerResult = _slashCommandMap.GetCommand(keywords); - foreach (var command in moduleInfo.SlashCommands) + if (autocompleteHandlerResult.IsSuccess) { - _slashCommandMap.RemoveCommand(command); + if (autocompleteHandlerResult.Command._flattenedParameterDictionary.TryGetValue(interaction.Data.Current.Name, out var parameter) && parameter?.AutocompleteHandler is not null) + return await parameter.AutocompleteHandler.ExecuteAsync(context, interaction, parameter, services).ConfigureAwait(false); } - - return true; } - /// - /// Search the registered slash commands using a . - /// - /// Interaction entity to perform the search with. - /// - /// The search result. When successful, result contains the found . - /// - public SearchResult SearchSlashCommand(ISlashCommandInteraction slashCommandInteraction) - => _slashCommandMap.GetCommand(slashCommandInteraction.Data.GetCommandKeywords()); - - /// - /// Search the registered slash commands using a . - /// - /// Interaction entity to perform the search with. - /// - /// The search result. When successful, result contains the found . - /// - public SearchResult SearchComponentCommand(IComponentInteraction componentInteraction) - => _componentCommandMap.GetCommand(componentInteraction.Data.CustomId); - - /// - /// Search the registered slash commands using a . - /// - /// Interaction entity to perform the search with. - /// - /// The search result. When successful, result contains the found . - /// - public SearchResult SearchUserCommand(IUserCommandInteraction userCommandInteraction) - => _contextCommandMaps[ApplicationCommandType.User].GetCommand(userCommandInteraction.Data.Name); - - /// - /// Search the registered slash commands using a . - /// - /// Interaction entity to perform the search with. - /// - /// The search result. When successful, result contains the found . - /// - public SearchResult SearchMessageCommand(IMessageCommandInteraction messageCommandInteraction) - => _contextCommandMaps[ApplicationCommandType.Message].GetCommand(messageCommandInteraction.Data.Name); - - /// - /// Search the registered slash commands using a . - /// - /// Interaction entity to perform the search with. - /// - /// The search result. When successful, result contains the found . - /// - public SearchResult SearchAutocompleteCommand(IAutocompleteInteraction autocompleteInteraction) + keywords.Add(interaction.Data.Current.Name); + + var commandResult = _autocompleteCommandMap.GetCommand(keywords); + + if (!commandResult.IsSuccess) { - var keywords = autocompleteInteraction.Data.GetCommandKeywords(); - keywords.Add(autocompleteInteraction.Data.Current.Name); - return _autocompleteCommandMap.GetCommand(keywords); + await _cmdLogger.DebugAsync($"Unknown command name, skipping autocomplete process ({interaction.Data.CommandName.ToUpper()})"); + + await _autocompleteCommandExecutedEvent.InvokeAsync(null, context, commandResult).ConfigureAwait(false); + return commandResult; } - /// - /// Execute a Command from a given . - /// - /// Name context of the command. - /// The service to be used in the command's dependency injection. - /// - /// A task representing the command execution process. The task result contains the result of the execution. - /// - public async Task ExecuteCommandAsync(IInteractionContext context, IServiceProvider services) + return await commandResult.Command.ExecuteAsync(context, services).ConfigureAwait(false); + } + + private async Task ExecuteModalCommandAsync(IInteractionContext context, string input, IServiceProvider services) + { + var result = _modalCommandMap.GetCommand(input); + + if (!result.IsSuccess) { - var interaction = context.Interaction; + await _cmdLogger.DebugAsync($"Unknown custom interaction id, skipping execution ({input.ToUpper()})"); - return interaction switch - { - ISlashCommandInteraction slashCommand => await ExecuteSlashCommandAsync(context, slashCommand, services).ConfigureAwait(false), - IComponentInteraction messageComponent => await ExecuteComponentCommandAsync(context, messageComponent.Data.CustomId, services).ConfigureAwait(false), - IUserCommandInteraction userCommand => await ExecuteContextCommandAsync(context, userCommand.Data.Name, ApplicationCommandType.User, services).ConfigureAwait(false), - IMessageCommandInteraction messageCommand => await ExecuteContextCommandAsync(context, messageCommand.Data.Name, ApplicationCommandType.Message, services).ConfigureAwait(false), - IAutocompleteInteraction autocomplete => await ExecuteAutocompleteAsync(context, autocomplete, services).ConfigureAwait(false), - IModalInteraction modalCommand => await ExecuteModalCommandAsync(context, modalCommand.Data.CustomId, services).ConfigureAwait(false), - _ => throw new InvalidOperationException($"{interaction.Type} interaction type cannot be executed by the Interaction service"), - }; + await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); + return result; } - private async Task ExecuteSlashCommandAsync(IInteractionContext context, ISlashCommandInteraction interaction, IServiceProvider services) - { - var keywords = interaction.Data.GetCommandKeywords(); + SetMatchesIfApplicable(context, result); - var result = _slashCommandMap.GetCommand(keywords); + return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); + } - if (!result.IsSuccess) - { - await _cmdLogger.DebugAsync($"Unknown slash command, skipping execution ({string.Join(" ", keywords).ToUpper()})"); + private static void SetMatchesIfApplicable(IInteractionContext context, SearchResult searchResult) + where T : class, ICommandInfo + { + if (!searchResult.Command.SupportsWildCards || context is not IRouteMatchContainer matchContainer) + return; - await _slashCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); - return result; - } - return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); + if (searchResult.RegexCaptureGroups?.Length > 0) + { + var matches = new RouteSegmentMatch[searchResult.RegexCaptureGroups.Length]; + for (var i = 0; i < searchResult.RegexCaptureGroups.Length; i++) + matches[i] = new RouteSegmentMatch(searchResult.RegexCaptureGroups[i]); + + matchContainer.SetSegmentMatches(matches); } + else + matchContainer.SetSegmentMatches(Array.Empty()); + } - private async Task ExecuteContextCommandAsync(IInteractionContext context, string input, ApplicationCommandType commandType, IServiceProvider services) - { - if (!_contextCommandMaps.TryGetValue(commandType, out var map)) - return SearchResult.FromError(input, InteractionCommandError.UnknownCommand, $"No {commandType} command found."); + internal TypeConverter GetTypeConverter(Type type, IServiceProvider services = null) + => _typeConverterMap.Get(type, services); - var result = map.GetCommand(input); + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddTypeConverter(TypeConverter converter) => + _typeConverterMap.AddConcrete(converter); - if (!result.IsSuccess) - { - await _cmdLogger.DebugAsync($"Unknown context command, skipping execution ({result.Text.ToUpper()})"); + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddTypeConverter(Type type, TypeConverter converter) => + _typeConverterMap.AddConcrete(type, converter); - await _contextCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); - return result; - } - return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); - } + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . - private async Task ExecuteComponentCommandAsync(IInteractionContext context, string input, IServiceProvider services) - { - var result = _componentCommandMap.GetCommand(input); + public void AddGenericTypeConverter(Type converterType) => + _typeConverterMap.AddGeneric(converterType); - if (!result.IsSuccess) - { - await _cmdLogger.DebugAsync($"Unknown custom interaction id, skipping execution ({input.ToUpper()})"); + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericTypeConverter(Type targetType, Type converterType) => + _typeConverterMap.AddGeneric(targetType, converterType); - await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); - return result; - } + internal ComponentTypeConverter GetComponentTypeConverter(Type type, IServiceProvider services = null) => + _compTypeConverterMap.Get(type, services); - SetMatchesIfApplicable(context, result); + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddComponentTypeConverter(ComponentTypeConverter converter) => + AddComponentTypeConverter(typeof(T), converter); - return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); - } + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddComponentTypeConverter(Type type, ComponentTypeConverter converter) => + _compTypeConverterMap.AddConcrete(type, converter); - private async Task ExecuteAutocompleteAsync(IInteractionContext context, IAutocompleteInteraction interaction, IServiceProvider services) - { - var keywords = interaction.Data.GetCommandKeywords(); + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericComponentTypeConverter(Type converterType) => + AddGenericComponentTypeConverter(typeof(T), converterType); - if (_enableAutocompleteHandlers) - { - var autocompleteHandlerResult = _slashCommandMap.GetCommand(keywords); + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericComponentTypeConverter(Type targetType, Type converterType) => + _compTypeConverterMap.AddGeneric(targetType, converterType); - if (autocompleteHandlerResult.IsSuccess) - { - if (autocompleteHandlerResult.Command._flattenedParameterDictionary.TryGetValue(interaction.Data.Current.Name, out var parameter) && parameter?.AutocompleteHandler is not null) - return await parameter.AutocompleteHandler.ExecuteAsync(context, interaction, parameter, services).ConfigureAwait(false); - } - } + internal TypeReader GetTypeReader(Type type, IServiceProvider services = null) => + _typeReaderMap.Get(type, services); - keywords.Add(interaction.Data.Current.Name); + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddTypeReader(TypeReader reader) => + AddTypeReader(typeof(T), reader); - var commandResult = _autocompleteCommandMap.GetCommand(keywords); + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddTypeReader(Type type, TypeReader reader) => + _typeReaderMap.AddConcrete(type, reader); - if (!commandResult.IsSuccess) - { - await _cmdLogger.DebugAsync($"Unknown command name, skipping autocomplete process ({interaction.Data.CommandName.ToUpper()})"); + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericTypeReader(Type readerType) => + AddGenericTypeReader(typeof(T), readerType); - await _autocompleteCommandExecutedEvent.InvokeAsync(null, context, commandResult).ConfigureAwait(false); - return commandResult; - } + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericTypeReader(Type targetType, Type readerType) => + _typeReaderMap.AddGeneric(targetType, readerType); - return await commandResult.Command.ExecuteAsync(context, services).ConfigureAwait(false); - } + /// + /// Removes a type reader for the type . + /// + /// The type to remove the readers from. + /// The reader if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveTypeReader(out TypeReader reader) + => TryRemoveTypeReader(typeof(T), out reader); - private async Task ExecuteModalCommandAsync(IInteractionContext context, string input, IServiceProvider services) - { - var result = _modalCommandMap.GetCommand(input); + /// + /// Removes a type reader for the given type. + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the reader from. + /// The reader if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveTypeReader(Type type, out TypeReader reader) + => _typeReaderMap.TryRemoveConcrete(type, out reader); - if (!result.IsSuccess) - { - await _cmdLogger.DebugAsync($"Unknown custom interaction id, skipping execution ({input.ToUpper()})"); + /// + /// Removes a generic type reader from the type . + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the readers from. + /// The removed readers type. + /// if the remove operation was successful; otherwise . + public bool TryRemoveGenericTypeReader(out Type readerType) + => TryRemoveGenericTypeReader(typeof(T), out readerType); - await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); - return result; - } + /// + /// Removes a generic type reader from the given type. + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the reader from. + /// The readers type if the remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveGenericTypeReader(Type type, out Type readerType) + => _typeReaderMap.TryRemoveGeneric(type, out readerType); + + internal ModalComponentTypeConverter GetModalInputTypeConverter(Type type, IServiceProvider services = null) => + _modalInputTypeConverterMap.Get(type, services); - SetMatchesIfApplicable(context, result); + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddModalComponentTypeConverter(ModalComponentTypeConverter converter) => + AddModalComponentTypeConverter(typeof(T), converter); - return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); - } + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddModalComponentTypeConverter(Type type, ModalComponentTypeConverter converter) => + _modalInputTypeConverterMap.AddConcrete(type, converter); - private static void SetMatchesIfApplicable(IInteractionContext context, SearchResult searchResult) - where T : class, ICommandInfo - { - if (!searchResult.Command.SupportsWildCards || context is not IRouteMatchContainer matchContainer) - return; + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericModalComponentTypeConverter(Type converterType) => + AddGenericModalComponentTypeConverter(typeof(T), converterType); - if (searchResult.RegexCaptureGroups?.Length > 0) - { - var matches = new RouteSegmentMatch[searchResult.RegexCaptureGroups.Length]; - for (var i = 0; i < searchResult.RegexCaptureGroups.Length; i++) - matches[i] = new RouteSegmentMatch(searchResult.RegexCaptureGroups[i]); + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericModalComponentTypeConverter(Type targetType, Type converterType) => + _modalInputTypeConverterMap.AddGeneric(targetType, converterType); - matchContainer.SetSegmentMatches(matches); - } - else - matchContainer.SetSegmentMatches(Array.Empty()); - } + /// + /// Removes a for the type . + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the converter from. + /// The converter if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveModalComponentTypeConverter(out ModalComponentTypeConverter converter) => + TryRemoveModalComponentTypeConverter(typeof(T), out converter); - internal TypeConverter GetTypeConverter(Type type, IServiceProvider services = null) - => _typeConverterMap.Get(type, services); - - /// - /// Add a concrete type . - /// - /// Primary target of the . - /// The instance. - public void AddTypeConverter(TypeConverter converter) => - _typeConverterMap.AddConcrete(converter); - - /// - /// Add a concrete type . - /// - /// Primary target of the . - /// The instance. - public void AddTypeConverter(Type type, TypeConverter converter) => - _typeConverterMap.AddConcrete(type, converter); - - /// - /// Add a generic type . - /// - /// Generic Type constraint of the of the . - /// Type of the . - - public void AddGenericTypeConverter(Type converterType) => - _typeConverterMap.AddGeneric(converterType); - - /// - /// Add a generic type . - /// - /// Generic Type constraint of the of the . - /// Type of the . - public void AddGenericTypeConverter(Type targetType, Type converterType) => - _typeConverterMap.AddGeneric(targetType, converterType); - - internal ComponentTypeConverter GetComponentTypeConverter(Type type, IServiceProvider services = null) => - _compTypeConverterMap.Get(type, services); - - /// - /// Add a concrete type . - /// - /// Primary target of the . - /// The instance. - public void AddComponentTypeConverter(ComponentTypeConverter converter) => - AddComponentTypeConverter(typeof(T), converter); - - /// - /// Add a concrete type . - /// - /// Primary target of the . - /// The instance. - public void AddComponentTypeConverter(Type type, ComponentTypeConverter converter) => - _compTypeConverterMap.AddConcrete(type, converter); - - /// - /// Add a generic type . - /// - /// Generic Type constraint of the of the . - /// Type of the . - public void AddGenericComponentTypeConverter(Type converterType) => - AddGenericComponentTypeConverter(typeof(T), converterType); - - /// - /// Add a generic type . - /// - /// Generic Type constraint of the of the . - /// Type of the . - public void AddGenericComponentTypeConverter(Type targetType, Type converterType) => - _compTypeConverterMap.AddGeneric(targetType, converterType); - - internal TypeReader GetTypeReader(Type type, IServiceProvider services = null) => - _typeReaderMap.Get(type, services); - - /// - /// Add a concrete type . - /// - /// Primary target of the . - /// The instance. - public void AddTypeReader(TypeReader reader) => - AddTypeReader(typeof(T), reader); - - /// - /// Add a concrete type . - /// - /// Primary target of the . - /// The instance. - public void AddTypeReader(Type type, TypeReader reader) => - _typeReaderMap.AddConcrete(type, reader); - - /// - /// Add a generic type . - /// - /// Generic Type constraint of the of the . - /// Type of the . - public void AddGenericTypeReader(Type readerType) => - AddGenericTypeReader(typeof(T), readerType); - - /// - /// Add a generic type . - /// - /// Generic Type constraint of the of the . - /// Type of the . - public void AddGenericTypeReader(Type targetType, Type readerType) => - _typeReaderMap.AddGeneric(targetType, readerType); - - /// - /// Removes a type reader for the type . - /// - /// The type to remove the readers from. - /// The reader if the resulting remove operation was successful. - /// if the remove operation was successful; otherwise . - public bool TryRemoveTypeReader(out TypeReader reader) - => TryRemoveTypeReader(typeof(T), out reader); - - /// - /// Removes a type reader for the given type. - /// - /// - /// Removing a from the will not dereference the from the loaded module/command instances. - /// You need to reload the modules for the changes to take effect. - /// - /// The type to remove the reader from. - /// The reader if the resulting remove operation was successful. - /// if the remove operation was successful; otherwise . - public bool TryRemoveTypeReader(Type type, out TypeReader reader) - => _typeReaderMap.TryRemoveConcrete(type, out reader); - - /// - /// Removes a generic type reader from the type . - /// - /// - /// Removing a from the will not dereference the from the loaded module/command instances. - /// You need to reload the modules for the changes to take effect. - /// - /// The type to remove the readers from. - /// The removed readers type. - /// if the remove operation was successful; otherwise . - public bool TryRemoveGenericTypeReader(out Type readerType) - => TryRemoveGenericTypeReader(typeof(T), out readerType); - - /// - /// Removes a generic type reader from the given type. - /// - /// - /// Removing a from the will not dereference the from the loaded module/command instances. - /// You need to reload the modules for the changes to take effect. - /// - /// The type to remove the reader from. - /// The readers type if the remove operation was successful. - /// if the remove operation was successful; otherwise . - public bool TryRemoveGenericTypeReader(Type type, out Type readerType) - => _typeReaderMap.TryRemoveGeneric(type, out readerType); - - internal ModalComponentTypeConverter GetModalInputTypeConverter(Type type, IServiceProvider services = null) => - _modalInputTypeConverterMap.Get(type, services); - - /// - /// Add a concrete type . - /// - /// Primary target of the . - /// The instance. - public void AddModalComponentTypeConverter(ModalComponentTypeConverter converter) => - AddModalComponentTypeConverter(typeof(T), converter); - - /// - /// Add a concrete type . - /// - /// Primary target of the . - /// The instance. - public void AddModalComponentTypeConverter(Type type, ModalComponentTypeConverter converter) => - _modalInputTypeConverterMap.AddConcrete(type, converter); - - /// - /// Add a generic type . - /// - /// Generic Type constraint of the of the . - /// Type of the . - public void AddGenericModalComponentTypeConverter(Type converterType) => - AddGenericModalComponentTypeConverter(typeof(T), converterType); - - /// - /// Add a generic type . - /// - /// Generic Type constraint of the of the . - /// Type of the . - public void AddGenericModalComponentTypeConverter(Type targetType, Type converterType) => - _modalInputTypeConverterMap.AddGeneric(targetType, converterType); - - /// - /// Removes a for the type . - /// - /// - /// Removing a from the will not dereference the from the loaded module/command instances. - /// You need to reload the modules for the changes to take effect. - /// - /// The type to remove the converter from. - /// The converter if the resulting remove operation was successful. - /// if the remove operation was successful; otherwise . - public bool TryRemoveModalComponentTypeConverter(out ModalComponentTypeConverter converter) => - TryRemoveModalComponentTypeConverter(typeof(T), out converter); - - /// - /// Removes a for the type . - /// - /// - /// Removing a from the will not dereference the from the loaded module/command instances. - /// You need to reload the modules for the changes to take effect. - /// - /// The type to remove the converter from. - /// The converter if the resulting remove operation was successful. - /// if the remove operation was successful; otherwise . - public bool TryRemoveModalComponentTypeConverter(Type type, out ModalComponentTypeConverter converter) => - _modalInputTypeConverterMap.TryRemoveConcrete(type, out converter); - - /// - /// Removes a generic for the type . - /// - /// - /// Removing a from the will not dereference the from the loaded module/command instances. - /// You need to reload the modules for the changes to take effect. - /// - /// The type to remove the converter from. - /// The converter if the resulting remove operation was successful. - /// if the remove operation was successful; otherwise . - public bool TryRemoveGenericModalComponentTypeConverter(out Type converterType) => - TryRemoveGenericModalComponentTypeConverter(typeof(T), out converterType); - - /// - /// Removes a generic for the type . - /// - /// - /// Removing a from the will not dereference the from the loaded module/command instances. - /// You need to reload the modules for the changes to take effect. - /// - /// The type to remove the converter from. - /// The converter if the resulting remove operation was successful. - /// if the remove operation was successful; otherwise . - public bool TryRemoveGenericModalComponentTypeConverter(Type type, out Type converterType) => - _modalInputTypeConverterMap.TryRemoveGeneric(type, out converterType); - - - /// - /// Serialize an object using a into a to be placed in a Component CustomId. - /// - /// - /// Removing a from the will not dereference the from the loaded module/command instances. - /// You need to reload the modules for the changes to take effect. - /// - /// Type of the object to be serialized. - /// Object to be serialized. - /// Services that will be passed on to the . - /// - /// A task representing the conversion process. The task result contains the result of the conversion. - /// - public Task SerializeValueAsync(T obj, IServiceProvider services) => - _typeReaderMap.Get(typeof(T), services).SerializeAsync(obj, services); - - /// - /// Serialize and format multiple objects into a Custom Id string. - /// - /// A composite format string. - /// >Services that will be passed on to the s. - /// Objects to be serialized. - /// - /// A task representing the conversion process. The task result contains the result of the conversion. - /// - public async Task GenerateCustomIdStringAsync(string format, IServiceProvider services, params object[] args) - { - var serializedValues = new string[args.Length]; + /// + /// Removes a for the type . + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the converter from. + /// The converter if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveModalComponentTypeConverter(Type type, out ModalComponentTypeConverter converter) => + _modalInputTypeConverterMap.TryRemoveConcrete(type, out converter); - for (var i = 0; i < args.Length; i++) - { - var arg = args[i]; - var typeReader = _typeReaderMap.Get(arg.GetType(), null); - var result = await typeReader.SerializeAsync(arg, services).ConfigureAwait(false); - serializedValues[i] = result; - } + /// + /// Removes a generic for the type . + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the converter from. + /// The converter if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveGenericModalComponentTypeConverter(out Type converterType) => + TryRemoveGenericModalComponentTypeConverter(typeof(T), out converterType); - return string.Format(format, serializedValues); - } + /// + /// Removes a generic for the type . + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the converter from. + /// The converter if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveGenericModalComponentTypeConverter(Type type, out Type converterType) => + _modalInputTypeConverterMap.TryRemoveGeneric(type, out converterType); - /// - /// Loads and caches an for the provided . - /// - /// Type of to be loaded. - /// - /// The built instance. - /// - /// - public ModalInfo AddModalInfo() where T : class, IModal - { - var type = typeof(T); - if (_modalInfos.ContainsKey(type)) - throw new InvalidOperationException($"Modal type {type.FullName} already exists."); + /// + /// Serialize an object using a into a to be placed in a Component CustomId. + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// Type of the object to be serialized. + /// Object to be serialized. + /// Services that will be passed on to the . + /// + /// A task representing the conversion process. The task result contains the result of the conversion. + /// + public Task SerializeValueAsync(T obj, IServiceProvider services) => + _typeReaderMap.Get(typeof(T), services).SerializeAsync(obj, services); - return ModalUtils.GetOrAdd(type, this); - } + /// + /// Serialize and format multiple objects into a Custom Id string. + /// + /// A composite format string. + /// >Services that will be passed on to the s. + /// Objects to be serialized. + /// + /// A task representing the conversion process. The task result contains the result of the conversion. + /// + public async Task GenerateCustomIdStringAsync(string format, IServiceProvider services, params object[] args) + { + var serializedValues = new string[args.Length]; - internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null) + for (var i = 0; i < args.Length; i++) { - services ??= EmptyServiceProvider.Instance; + var arg = args[i]; + var typeReader = _typeReaderMap.Get(arg.GetType(), null); + var result = await typeReader.SerializeAsync(arg, services).ConfigureAwait(false); + serializedValues[i] = result; + } - if (!_enableAutocompleteHandlers) - throw new InvalidOperationException($"{nameof(IAutocompleteHandler)}s are not enabled. To use this feature set {nameof(InteractionServiceConfig.EnableAutocompleteHandlers)} to TRUE"); + return string.Format(format, serializedValues); + } - if (_autocompleteHandlers.TryGetValue(autocompleteHandlerType, out var autocompleteHandler)) - return autocompleteHandler; - else - { - autocompleteHandler = ReflectionUtils.CreateObject(autocompleteHandlerType.GetTypeInfo(), this, services); - _autocompleteHandlers[autocompleteHandlerType] = autocompleteHandler; - return autocompleteHandler; - } - } + /// + /// Loads and caches an for the provided . + /// + /// Type of to be loaded. + /// + /// The built instance. + /// + /// + public ModalInfo AddModalInfo() where T : class, IModal + { + var type = typeof(T); - /// - /// Modify the command permissions of the matching Discord Slash Command. - /// - /// Module representing the top level Slash Command. - /// Target guild. - /// New permission values. - /// - /// The active command permissions after the modification. - /// - public Task ModifySlashCommandPermissionsAsync(ModuleInfo module, IGuild guild, - params ApplicationCommandPermission[] permissions) - { - if (module is null) - throw new ArgumentNullException(nameof(module)); + if (_modalInfos.ContainsKey(type)) + throw new InvalidOperationException($"Modal type {type.FullName} already exists."); - if (guild is null) - throw new ArgumentNullException(nameof(guild)); + return ModalUtils.GetOrAdd(type, this); + } - return ModifySlashCommandPermissionsAsync(module, guild.Id, permissions); - } + internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null) + { + services ??= EmptyServiceProvider.Instance; + + if (!_enableAutocompleteHandlers) + throw new InvalidOperationException($"{nameof(IAutocompleteHandler)}s are not enabled. To use this feature set {nameof(InteractionServiceConfig.EnableAutocompleteHandlers)} to TRUE"); - /// - /// Modify the command permissions of the matching Discord Slash Command. - /// - /// Module representing the top level Slash Command. - /// Target guild ID. - /// New permission values. - /// - /// The active command permissions after the modification. - /// - public async Task ModifySlashCommandPermissionsAsync(ModuleInfo module, ulong guildId, - params ApplicationCommandPermission[] permissions) + if (_autocompleteHandlers.TryGetValue(autocompleteHandlerType, out var autocompleteHandler)) + return autocompleteHandler; + else { - if (module is null) - throw new ArgumentNullException(nameof(module)); + autocompleteHandler = ReflectionUtils.CreateObject(autocompleteHandlerType.GetTypeInfo(), this, services); + _autocompleteHandlers[autocompleteHandlerType] = autocompleteHandler; + return autocompleteHandler; + } + } - if (!module.IsSlashGroup) - throw new InvalidOperationException($"This module does not have a {nameof(GroupAttribute)} and does not represent an Application Command"); + /// + /// Modify the command permissions of the matching Discord Slash Command. + /// + /// Module representing the top level Slash Command. + /// Target guild. + /// New permission values. + /// + /// The active command permissions after the modification. + /// + public Task ModifySlashCommandPermissionsAsync(ModuleInfo module, IGuild guild, + params ApplicationCommandPermission[] permissions) + { + if (module is null) + throw new ArgumentNullException(nameof(module)); - if (!module.IsTopLevelGroup) - throw new InvalidOperationException("This module is not a top level application command. You cannot change its permissions"); + if (guild is null) + throw new ArgumentNullException(nameof(guild)); - var commands = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); - var appCommand = commands.First(x => x.Name == module.SlashGroupName); + return ModifySlashCommandPermissionsAsync(module, guild.Id, permissions); + } - return await appCommand.ModifyCommandPermissions(permissions).ConfigureAwait(false); - } + /// + /// Modify the command permissions of the matching Discord Slash Command. + /// + /// Module representing the top level Slash Command. + /// Target guild ID. + /// New permission values. + /// + /// The active command permissions after the modification. + /// + public async Task ModifySlashCommandPermissionsAsync(ModuleInfo module, ulong guildId, + params ApplicationCommandPermission[] permissions) + { + if (module is null) + throw new ArgumentNullException(nameof(module)); - /// - /// Modify the command permissions of the matching Discord Slash Command. - /// - /// The Slash Command. - /// Target guild. - /// New permission values. - /// - /// The active command permissions after the modification. - /// - public Task ModifySlashCommandPermissionsAsync(SlashCommandInfo command, IGuild guild, - params ApplicationCommandPermission[] permissions) - { - if (command is null) - throw new ArgumentNullException(nameof(command)); + if (!module.IsSlashGroup) + throw new InvalidOperationException($"This module does not have a {nameof(GroupAttribute)} and does not represent an Application Command"); - if (guild is null) - throw new ArgumentNullException(nameof(guild)); + if (!module.IsTopLevelGroup) + throw new InvalidOperationException("This module is not a top level application command. You cannot change its permissions"); - return ModifyApplicationCommandPermissionsAsync(command, guild.Id, permissions); - } + var commands = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); + var appCommand = commands.First(x => x.Name == module.SlashGroupName); - /// - /// Modify the command permissions of the matching Discord Slash Command. - /// - /// The Slash Command. - /// Target guild ID. - /// New permission values. - /// - /// The active command permissions after the modification. - /// - public Task ModifySlashCommandPermissionsAsync(SlashCommandInfo command, ulong guildId, - params ApplicationCommandPermission[] permissions) - => ModifyApplicationCommandPermissionsAsync(command, guildId, permissions); - - /// - /// Modify the command permissions of the matching Discord Slash Command. - /// - /// The Context Command. - /// Target guild. - /// New permission values. - /// - /// The active command permissions after the modification. - /// - public Task ModifyContextCommandPermissionsAsync(ContextCommandInfo command, IGuild guild, - params ApplicationCommandPermission[] permissions) - { - if (command is null) - throw new ArgumentNullException(nameof(command)); + return await appCommand.ModifyCommandPermissions(permissions).ConfigureAwait(false); + } - if (guild is null) - throw new ArgumentNullException(nameof(guild)); + /// + /// Modify the command permissions of the matching Discord Slash Command. + /// + /// The Slash Command. + /// Target guild. + /// New permission values. + /// + /// The active command permissions after the modification. + /// + public Task ModifySlashCommandPermissionsAsync(SlashCommandInfo command, IGuild guild, + params ApplicationCommandPermission[] permissions) + { + if (command is null) + throw new ArgumentNullException(nameof(command)); - return ModifyApplicationCommandPermissionsAsync(command, guild.Id, permissions); - } + if (guild is null) + throw new ArgumentNullException(nameof(guild)); - /// - /// Modify the command permissions of the matching Discord Slash Command. - /// - /// The Context Command. - /// Target guild ID. - /// New permission values. - /// - /// The active command permissions after the modification. - /// - public Task ModifyContextCommandPermissionsAsync(ContextCommandInfo command, ulong guildId, - params ApplicationCommandPermission[] permissions) - => ModifyApplicationCommandPermissionsAsync(command, guildId, permissions); - - private async Task ModifyApplicationCommandPermissionsAsync(T command, ulong guildId, - params ApplicationCommandPermission[] permissions) where T : class, IApplicationCommandInfo, ICommandInfo - { - if (command is null) - throw new ArgumentNullException(nameof(command)); + return ModifyApplicationCommandPermissionsAsync(command, guild.Id, permissions); + } - if (!command.IsTopLevelCommand) - throw new InvalidOperationException("This command is not a top level application command. You cannot change its permissions"); + /// + /// Modify the command permissions of the matching Discord Slash Command. + /// + /// The Slash Command. + /// Target guild ID. + /// New permission values. + /// + /// The active command permissions after the modification. + /// + public Task ModifySlashCommandPermissionsAsync(SlashCommandInfo command, ulong guildId, + params ApplicationCommandPermission[] permissions) + => ModifyApplicationCommandPermissionsAsync(command, guildId, permissions); - var commands = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); - var appCommand = commands.First(x => x.Name == (command as IApplicationCommandInfo).Name); + /// + /// Modify the command permissions of the matching Discord Slash Command. + /// + /// The Context Command. + /// Target guild. + /// New permission values. + /// + /// The active command permissions after the modification. + /// + public Task ModifyContextCommandPermissionsAsync(ContextCommandInfo command, IGuild guild, + params ApplicationCommandPermission[] permissions) + { + if (command is null) + throw new ArgumentNullException(nameof(command)); - return await appCommand.ModifyCommandPermissions(permissions).ConfigureAwait(false); - } + if (guild is null) + throw new ArgumentNullException(nameof(guild)); - /// - /// Gets a . - /// - /// Declaring module type of this command, must be a type of . - /// Method name of the handler, use of is recommended. - /// - /// instance for this command. - /// - /// Module or Slash Command couldn't be found. - public SlashCommandInfo GetSlashCommandInfo(string methodName) where TModule : class - { - var module = GetModuleInfo(); + return ModifyApplicationCommandPermissionsAsync(command, guild.Id, permissions); + } - return module.SlashCommands.First(x => x.MethodName == methodName); - } + /// + /// Modify the command permissions of the matching Discord Slash Command. + /// + /// The Context Command. + /// Target guild ID. + /// New permission values. + /// + /// The active command permissions after the modification. + /// + public Task ModifyContextCommandPermissionsAsync(ContextCommandInfo command, ulong guildId, + params ApplicationCommandPermission[] permissions) + => ModifyApplicationCommandPermissionsAsync(command, guildId, permissions); + + private async Task ModifyApplicationCommandPermissionsAsync(T command, ulong guildId, + params ApplicationCommandPermission[] permissions) where T : class, IApplicationCommandInfo, ICommandInfo + { + if (command is null) + throw new ArgumentNullException(nameof(command)); - /// - /// Gets a . - /// - /// Declaring module type of this command, must be a type of . - /// Method name of the handler, use of is recommended. - /// - /// instance for this command. - /// - /// Module or Context Command couldn't be found. - public ContextCommandInfo GetContextCommandInfo(string methodName) where TModule : class - { - var module = GetModuleInfo(); + if (!command.IsTopLevelCommand) + throw new InvalidOperationException("This command is not a top level application command. You cannot change its permissions"); - return module.ContextCommands.First(x => x.MethodName == methodName); - } + var commands = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); + var appCommand = commands.First(x => x.Name == (command as IApplicationCommandInfo).Name); - /// - /// Gets a . - /// - /// Declaring module type of this command, must be a type of . - /// Method name of the handler, use of is recommended. - /// - /// instance for this command. - /// - /// Module or Component Command couldn't be found. - public ComponentCommandInfo GetComponentCommandInfo(string methodName) where TModule : class - { - var module = GetModuleInfo(); + return await appCommand.ModifyCommandPermissions(permissions).ConfigureAwait(false); + } - return module.ComponentCommands.First(x => x.MethodName == methodName); - } + /// + /// Gets a . + /// + /// Declaring module type of this command, must be a type of . + /// Method name of the handler, use of is recommended. + /// + /// instance for this command. + /// + /// Module or Slash Command couldn't be found. + public SlashCommandInfo GetSlashCommandInfo(string methodName) where TModule : class + { + var module = GetModuleInfo(); - /// - /// Gets a built . - /// - /// Type of the module, must be a type of . - /// - /// instance for this module. - /// - public ModuleInfo GetModuleInfo() where TModule : class - { - if (!typeof(IInteractionModuleBase).IsAssignableFrom(typeof(TModule))) - throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(TModule)); + return module.SlashCommands.First(x => x.MethodName == methodName); + } - var module = _typedModuleDefs[typeof(TModule)]; + /// + /// Gets a . + /// + /// Declaring module type of this command, must be a type of . + /// Method name of the handler, use of is recommended. + /// + /// instance for this command. + /// + /// Module or Context Command couldn't be found. + public ContextCommandInfo GetContextCommandInfo(string methodName) where TModule : class + { + var module = GetModuleInfo(); - if (module is null) - throw new InvalidOperationException($"{typeof(TModule).FullName} is not loaded to the Slash Command Service"); + return module.ContextCommands.First(x => x.MethodName == methodName); + } - return module; - } + /// + /// Gets a . + /// + /// Declaring module type of this command, must be a type of . + /// Method name of the handler, use of is recommended. + /// + /// instance for this command. + /// + /// Module or Component Command couldn't be found. + public ComponentCommandInfo GetComponentCommandInfo(string methodName) where TModule : class + { + var module = GetModuleInfo(); - /// - public void Dispose() - { - _lock.Dispose(); - } + return module.ComponentCommands.First(x => x.MethodName == methodName); + } - private void EnsureClientReady() - { - if (RestClient?.CurrentUser is null || RestClient?.CurrentUser?.Id == 0) - throw new InvalidOperationException($"Provided client is not ready to execute this operation, invoke this operation after a `Client Ready` event"); - } + /// + /// Gets a built . + /// + /// Type of the module, must be a type of . + /// + /// instance for this module. + /// + public ModuleInfo GetModuleInfo() where TModule : class + { + if (!typeof(IInteractionModuleBase).IsAssignableFrom(typeof(TModule))) + throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(TModule)); + + var module = _typedModuleDefs[typeof(TModule)]; + + if (module is null) + throw new InvalidOperationException($"{typeof(TModule).FullName} is not loaded to the Slash Command Service"); + + return module; + } + + /// + public void Dispose() + { + _lock.Dispose(); + } + + private void EnsureClientReady() + { + if (RestClient?.CurrentUser is null || RestClient?.CurrentUser?.Id == 0) + throw new InvalidOperationException($"Provided client is not ready to execute this operation, invoke this operation after a `Client Ready` event"); } } diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/AttachmentModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/AttachmentModalComponentConverter.cs new file mode 100644 index 0000000000..a8600409ae --- /dev/null +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/AttachmentModalComponentConverter.cs @@ -0,0 +1,14 @@ +using Discord.Interactions.TypeConverters.ModalInputs; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Interactions.TypeConverters.ModalComponents; + +internal class AttachmentModalComponentConverter : ModalComponentTypeConverter where T : class, IAttachment +{ + public override async Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) + { + return TypeConverterResult.FromSuccess(option.Values.FirstOrDefault()); + } +} diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs index 1bf53e8a2b..9b0390041f 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs @@ -26,7 +26,7 @@ public EnumModalComponentConverter() _options = members.Select(x => { var selectMenuOptionAttr = x.GetCustomAttribute(); - return new SelectMenuOptionBuilder(x.GetCustomAttribute()?.Name ?? x.Name, x.Name, selectMenuOptionAttr?.Description, Emote.Parse(selectMenuOptionAttr?.Emote), selectMenuOptionAttr?.IsDefault); + return new SelectMenuOptionBuilder(x.GetCustomAttribute()?.Name ?? x.Name, x.Name, selectMenuOptionAttr?.Description, selectMenuOptionAttr?.Emote != null ? Emote.Parse(selectMenuOptionAttr?.Emote) : null, selectMenuOptionAttr?.IsDefault); }).ToImmutableArray(); } From bf635144b252e4bdb692cca4bab3f113b7664f8f Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 12 Nov 2025 01:05:07 +0100 Subject: [PATCH 33/59] create base non-input modal component entities --- .../Modals/ModalComponentAttribute.cs | 15 +++ .../Attributes/Modals/ModalInputAttribute.cs | 8 +- .../Builders/Modals/IModalComponentBuilder.cs | 65 ++++++++++++ .../Modals/Inputs/IInputComponentBuilder.cs | 65 +----------- .../Modals/Inputs/InputComponentBuilder.cs | 85 ++------------- .../Builders/Modals/ModalBuilder.cs | 41 ++++--- .../Builders/Modals/ModalComponentBuilder.cs | 100 ++++++++++++++++++ .../Builders/ModuleClassBuilder.cs | 14 +-- .../InputComponents/InputComponentInfo.cs | 53 +--------- .../Info/ModalComponentInfo.cs | 58 ++++++++++ .../Info/ModalInfo.cs | 53 ++++++---- 11 files changed, 315 insertions(+), 242 deletions(-) create mode 100644 src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs create mode 100644 src/Discord.Net.Interactions/Builders/Modals/IModalComponentBuilder.cs create mode 100644 src/Discord.Net.Interactions/Builders/Modals/ModalComponentBuilder.cs create mode 100644 src/Discord.Net.Interactions/Info/ModalComponentInfo.cs diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs new file mode 100644 index 0000000000..6ac33f3dea --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace Discord.Interactions.Attributes.Modals; + +/// +/// Mark an property as a modal component field. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] +public abstract class ModalComponentAttribute : Attribute +{ + /// + /// Gets the type of the component. + /// + public abstract ComponentType ComponentType { get; } +} diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs index 3e37fa1592..054f495486 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs @@ -1,3 +1,4 @@ +using Discord.Interactions.Attributes.Modals; using System; namespace Discord.Interactions; @@ -6,18 +7,13 @@ namespace Discord.Interactions; /// Mark an property as a modal input field. /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] -public abstract class ModalInputAttribute : Attribute +public abstract class ModalInputAttribute : ModalComponentAttribute { /// /// Gets the custom id of the text input. /// public string CustomId { get; } - /// - /// Gets the type of the component. - /// - public abstract ComponentType ComponentType { get; } - /// /// Create a new . /// diff --git a/src/Discord.Net.Interactions/Builders/Modals/IModalComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/IModalComponentBuilder.cs new file mode 100644 index 0000000000..9d869e4c57 --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Modals/IModalComponentBuilder.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Discord.Interactions.Builders; + +public interface IModalComponentBuilder +{ + /// + /// Gets the parent modal of this input component. + /// + ModalBuilder Modal { get; } + + /// + /// Gets the component type of this input component. + /// + ComponentType ComponentType { get; } + + /// + /// Get the reference type of this input component. + /// + Type Type { get; } + + /// + /// Get the of this component's property. + /// + PropertyInfo PropertyInfo { get; } + + /// + /// Gets the default value of this input component property. + /// + object DefaultValue { get; } + + /// + /// Gets a collection of the attributes of this component. + /// + IReadOnlyCollection Attributes { get; } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + IModalComponentBuilder WithType(Type type); + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + IModalComponentBuilder SetDefaultValue(object value); + + /// + /// Adds attributes to . + /// + /// New attributes to be added to . + /// + /// The builder instance. + /// + IModalComponentBuilder WithAttributes(params Attribute[] attributes); +} diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs index 3b9892ff3f..3660210687 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs @@ -1,20 +1,10 @@ -using Discord.Interactions.TypeConverters.ModalInputs; -using System; -using System.Collections.Generic; -using System.Reflection; - namespace Discord.Interactions.Builders; /// /// Represent a builder for creating . /// -public interface IInputComponentBuilder +public interface IInputComponentBuilder : IModalComponentBuilder { - /// - /// Gets the parent modal of this input component. - /// - ModalBuilder Modal { get; } - /// /// Gets the custom id of this input component. /// @@ -35,36 +25,10 @@ public interface IInputComponentBuilder /// bool IsRequired { get; } - /// - /// Gets the component type of this input component. - /// - ComponentType ComponentType { get; } - - /// - /// Get the reference type of this input component. - /// - Type Type { get; } - - /// - /// Get the of this component's property. - /// - PropertyInfo PropertyInfo { get; } - /// /// Get the assigned to this input. /// ModalComponentTypeConverter TypeConverter { get; } - - /// - /// Gets the default value of this input component property. - /// - object DefaultValue { get; } - - /// - /// Gets a collection of the attributes of this component. - /// - IReadOnlyCollection Attributes { get; } - /// /// Sets . /// @@ -100,31 +64,4 @@ public interface IInputComponentBuilder /// The builder instance. /// IInputComponentBuilder SetIsRequired(bool isRequired); - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - IInputComponentBuilder WithType(Type type); - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - IInputComponentBuilder SetDefaultValue(object value); - - /// - /// Adds attributes to . - /// - /// New attributes to be added to . - /// - /// The builder instance. - /// - IInputComponentBuilder WithAttributes(params Attribute[] attributes); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs index ce9a262017..ac50487499 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs @@ -1,7 +1,5 @@ -using Discord.Interactions.TypeConverters.ModalInputs; using System; using System.Collections.Generic; -using System.Reflection; namespace Discord.Interactions.Builders; @@ -10,15 +8,11 @@ namespace Discord.Interactions.Builders; /// /// The this builder yields when built. /// Inherited type. -public abstract class InputComponentBuilder : IInputComponentBuilder +public abstract class InputComponentBuilder : ModalComponentBuilder, IInputComponentBuilder where TInfo : InputComponentInfo where TBuilder : InputComponentBuilder { private readonly List _attributes; - protected abstract TBuilder Instance { get; } - - /// - public ModalBuilder Modal { get; } /// public string CustomId { get; set; } @@ -32,31 +26,15 @@ public abstract class InputComponentBuilder : IInputComponentBu /// public bool IsRequired { get; set; } = true; - /// - public ComponentType ComponentType { get; internal set; } - - /// - public Type Type { get; private set; } - - /// - public PropertyInfo PropertyInfo { get; internal set; } - /// public ModalComponentTypeConverter TypeConverter { get; private set; } - /// - public object DefaultValue { get; set; } - - /// - public IReadOnlyCollection Attributes => _attributes; - /// /// Creates an instance of /// /// Parent modal of this input component. - public InputComponentBuilder(ModalBuilder modal) + internal InputComponentBuilder(ModalBuilder modal) : base(modal) { - Modal = modal; _attributes = new(); } @@ -112,19 +90,6 @@ public TBuilder SetIsRequired(bool isRequired) return Instance; } - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public virtual TBuilder WithComponentType(ComponentType componentType) - { - ComponentType = componentType; - return Instance; - } - /// /// Sets . /// @@ -132,59 +97,21 @@ public virtual TBuilder WithComponentType(ComponentType componentType) /// /// The builder instance. /// - public TBuilder WithType(Type type) + public override TBuilder WithType(Type type) { - Type = type; TypeConverter = Modal._interactionService.GetModalInputTypeConverter(type); - return Instance; - } - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TBuilder SetDefaultValue(object value) - { - DefaultValue = value; - return Instance; - } - - /// - /// Adds attributes to . - /// - /// New attributes to be added to . - /// - /// The builder instance. - /// - public TBuilder WithAttributes(params Attribute[] attributes) - { - _attributes.AddRange(attributes); - return Instance; + return base.WithType(type); } - internal abstract TInfo Build(ModalInfo modal); - - //IInputComponentBuilder /// IInputComponentBuilder IInputComponentBuilder.WithCustomId(string customId) => WithCustomId(customId); /// - IInputComponentBuilder IInputComponentBuilder.WithLabel(string label) => WithCustomId(label); - - /// - IInputComponentBuilder IInputComponentBuilder.WithType(Type type) => WithType(type); - - /// - IInputComponentBuilder IInputComponentBuilder.SetDefaultValue(object value) => SetDefaultValue(value); + IInputComponentBuilder IInputComponentBuilder.WithLabel(string label) => WithLabel(label); /// - IInputComponentBuilder IInputComponentBuilder.WithAttributes(params Attribute[] attributes) => WithAttributes(attributes); + IInputComponentBuilder IInputComponentBuilder.WithDescription(string description) => WithDescription(description); /// IInputComponentBuilder IInputComponentBuilder.SetIsRequired(bool isRequired) => SetIsRequired(isRequired); - - IInputComponentBuilder IInputComponentBuilder.WithDescription(string description) => WithDescription(description); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs index dd12634f10..73ceadc6d7 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Discord.Interactions.Builders; @@ -9,7 +10,7 @@ namespace Discord.Interactions.Builders; public class ModalBuilder { internal readonly InteractionService _interactionService; - internal readonly List _components; + internal readonly List _components; /// /// Gets the initialization delegate for this modal. @@ -29,7 +30,7 @@ public class ModalBuilder /// /// Gets a collection of the components of this modal. /// - public IReadOnlyCollection Components => _components; + public IReadOnlyCollection Components => _components.AsReadOnly(); internal ModalBuilder(Type type, InteractionService interactionService) { @@ -65,13 +66,13 @@ public ModalBuilder WithTitle(string title) } /// - /// Adds text components to . + /// Adds text components to . /// /// Text Component builder factory. /// /// The builder instance. /// - public ModalBuilder AddTextComponent(Action configure) + public ModalBuilder AddTextInputComponent(Action configure) { var builder = new TextInputComponentBuilder(this); configure(builder); @@ -80,13 +81,13 @@ public ModalBuilder AddTextComponent(Action configure } /// - /// Adds a select menu component to . + /// Adds a select menu component to . /// /// Select menu component builder factory. /// /// The builder instance. /// - public ModalBuilder AddSelectMenuComponent(Action configure) + public ModalBuilder AddSelectMenuInputComponent(Action configure) { var builder = new SelectMenuInputComponentBuilder(this); configure(builder); @@ -95,13 +96,13 @@ public ModalBuilder AddSelectMenuComponent(Action - /// Adds a user select component to . + /// Adds a user select component to . /// /// User select component builder factory. /// /// The builder instance. /// - public ModalBuilder AddUserSelectComponent(Action configure) + public ModalBuilder AddUserSelectInputComponent(Action configure) { var builder = new UserSelectInputComponentBuilder(this); configure(builder); @@ -110,13 +111,13 @@ public ModalBuilder AddUserSelectComponent(Action - /// Adds a role select component to . + /// Adds a role select component to . /// /// Role select component builder factory. /// /// The builder instance. /// - public ModalBuilder AddRoleSelectComponent(Action configure) + public ModalBuilder AddRoleSelectInputComponent(Action configure) { var builder = new RoleSelectInputComponentBuilder(this); configure(builder); @@ -125,13 +126,13 @@ public ModalBuilder AddRoleSelectComponent(Action - /// Adds a mentionable select component to . + /// Adds a mentionable select component to . /// /// Mentionable select component builder factory. /// /// The builder instance. /// - public ModalBuilder AddMentionableSelectComponent(Action configure) + public ModalBuilder AddMentionableSelectInputComponent(Action configure) { var builder = new MentionableSelectInputComponentBuilder(this); configure(builder); @@ -140,13 +141,13 @@ public ModalBuilder AddMentionableSelectComponent(Action - /// Adds a channel select component to . + /// Adds a channel select component to . /// /// Channel select component builder factory. /// /// The builder instance. /// - public ModalBuilder AddChannelSelectComponent(Action configure) + public ModalBuilder AddChannelSelectInputComponent(Action configure) { var builder = new ChannelSelectInputComponentBuilder(this); configure(builder); @@ -155,13 +156,13 @@ public ModalBuilder AddChannelSelectComponent(Action - /// Adds a file upload component to . + /// Adds a file upload component to . /// /// File upload component builder factory. /// /// The builder instance. /// - public ModalBuilder AddFileUploadComponent(Action configure) + public ModalBuilder AddFileUploadInputComponent(Action configure) { var builder = new FileUploadInputComponentBuilder(this); configure(builder); @@ -169,5 +170,13 @@ public ModalBuilder AddFileUploadComponent(Action configure) + { + var builder = new TextDisplayComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + internal ModalInfo Build() => new(this); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/ModalComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/ModalComponentBuilder.cs new file mode 100644 index 0000000000..f7c41b3f82 --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Modals/ModalComponentBuilder.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Discord.Interactions.Builders; + +public abstract class ModalComponentBuilder : IModalComponentBuilder + where TInfo : ModalComponentInfo + where TBuilder : ModalComponentBuilder +{ + private readonly List _attributes; + protected abstract TBuilder Instance { get; } + + /// + public ModalBuilder Modal { get; } + + /// + public ComponentType ComponentType { get; internal set; } + + /// + public Type Type { get; private set; } + + /// + public PropertyInfo PropertyInfo { get; internal set; } + + /// + public object DefaultValue { get; set; } + + /// + public IReadOnlyCollection Attributes => _attributes; + + internal ModalComponentBuilder(ModalBuilder modal) + { + Modal = modal; + _attributes = new(); + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public virtual TBuilder WithComponentType(ComponentType componentType) + { + ComponentType = componentType; + return Instance; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public virtual TBuilder WithType(Type type) + { + Type = type; + return Instance; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public virtual TBuilder SetDefaultValue(object value) + { + DefaultValue = value; + return Instance; + } + + /// + /// Adds attributes to . + /// + /// New attributes to be added to . + /// + /// The builder instance. + /// + public virtual TBuilder WithAttributes(params Attribute[] attributes) + { + _attributes.AddRange(attributes); + return Instance; + } + + internal abstract TInfo Build(ModalInfo modal); + + /// + IModalComponentBuilder IModalComponentBuilder.WithType(Type type) => WithType(type); + + /// + IModalComponentBuilder IModalComponentBuilder.SetDefaultValue(object value) => SetDefaultValue(value); + + /// + IModalComponentBuilder IModalComponentBuilder.WithAttributes(params Attribute[] attributes) => WithAttributes(attributes); +} diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index bed6417a7f..9723d3bdee 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -613,25 +613,25 @@ public static ModalInfo BuildModalInfo(Type modalType, InteractionService intera switch (componentType) { case ComponentType.TextInput: - builder.AddTextComponent(x => BuildTextInput(x, prop, prop.GetValue(instance))); + builder.AddTextInputComponent(x => BuildTextInput(x, prop, prop.GetValue(instance))); break; case ComponentType.SelectMenu: - builder.AddSelectMenuComponent(x => BuildSelectMenuInput(x, prop, prop.GetValue(instance))); + builder.AddSelectMenuInputComponent(x => BuildSelectMenuInput(x, prop, prop.GetValue(instance))); break; case ComponentType.UserSelect: - builder.AddUserSelectComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + builder.AddUserSelectInputComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); break; case ComponentType.RoleSelect: - builder.AddRoleSelectComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + builder.AddRoleSelectInputComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); break; case ComponentType.MentionableSelect: - builder.AddMentionableSelectComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + builder.AddMentionableSelectInputComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); break; case ComponentType.ChannelSelect: - builder.AddChannelSelectComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + builder.AddChannelSelectInputComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); break; case ComponentType.FileUpload: - builder.AddFileUploadComponent(x => BuildFileUploadInput(x, prop, prop.GetValue(instance))); + builder.AddFileUploadInputComponent(x => BuildFileUploadInput(x, prop, prop.GetValue(instance))); break; case ComponentType.TextDisplay: throw new NotImplementedException(); diff --git a/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs index f541c30481..5269b9c900 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs @@ -1,25 +1,10 @@ -using Discord.Interactions.TypeConverters.ModalInputs; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Reflection; - namespace Discord.Interactions; /// /// Represents the base info class for input components. /// -public abstract class InputComponentInfo +public abstract class InputComponentInfo : ModalComponentInfo { - private Lazy> _getter; - internal Func Getter => _getter.Value; - - - /// - /// Gets the parent modal of this component. - /// - public ModalInfo Modal { get; } - /// /// Gets the custom id of this component. /// @@ -40,50 +25,18 @@ public abstract class InputComponentInfo /// public bool IsRequired { get; } - /// - /// Gets the type of this component. - /// - public ComponentType ComponentType { get; } - - /// - /// Gets the reference type of this component. - /// - public Type Type { get; } - - /// - /// Gets the property linked to this component. - /// - public PropertyInfo PropertyInfo { get; } - /// /// Gets the assigned to this component. /// public ModalComponentTypeConverter TypeConverter { get; } - /// - /// Gets the default value of this component property. - /// - public object DefaultValue { get; } - - /// - /// Gets a collection of the attributes of this command. - /// - public IReadOnlyCollection Attributes { get; } - - protected InputComponentInfo(Builders.IInputComponentBuilder builder, ModalInfo modal) + internal InputComponentInfo(Builders.IInputComponentBuilder builder, ModalInfo modal) + : base(builder, modal) { - Modal = modal; CustomId = builder.CustomId; Label = builder.Label; Description = builder.Description; IsRequired = builder.IsRequired; - ComponentType = builder.ComponentType; - Type = builder.Type; - PropertyInfo = builder.PropertyInfo; TypeConverter = builder.TypeConverter; - DefaultValue = builder.DefaultValue; - Attributes = builder.Attributes.ToImmutableArray(); - - _getter = new(() => ReflectionUtils.CreateLambdaPropertyGetter(Modal.Type, PropertyInfo)); } } diff --git a/src/Discord.Net.Interactions/Info/ModalComponentInfo.cs b/src/Discord.Net.Interactions/Info/ModalComponentInfo.cs new file mode 100644 index 0000000000..b92455892d --- /dev/null +++ b/src/Discord.Net.Interactions/Info/ModalComponentInfo.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Reflection; + +namespace Discord.Interactions; + +/// +/// Represents the base info class for components. +/// +public abstract class ModalComponentInfo +{ + private Lazy> _getter; + internal Func Getter => _getter.Value; + + + /// + /// Gets the parent modal of this component. + /// + public ModalInfo Modal { get; } + + /// + /// Gets the type of this component. + /// + public ComponentType ComponentType { get; } + + /// + /// Gets the reference type of this component. + /// + public Type Type { get; } + + /// + /// Gets the property linked to this component. + /// + public PropertyInfo PropertyInfo { get; } + + /// + /// Gets the default value of this component property. + /// + public object DefaultValue { get; } + + /// + /// Gets a collection of the attributes of this command. + /// + public IReadOnlyCollection Attributes { get; } + + internal ModalComponentInfo(Builders.IModalComponentBuilder builder, ModalInfo modal) + { + Modal = modal; + ComponentType = builder.ComponentType; + Type = builder.Type; + PropertyInfo = builder.PropertyInfo; + DefaultValue = builder.DefaultValue; + Attributes = builder.Attributes.ToImmutableArray(); + + _getter = new(() => ReflectionUtils.CreateLambdaPropertyGetter(Modal.Type, PropertyInfo)); + } +} diff --git a/src/Discord.Net.Interactions/Info/ModalInfo.cs b/src/Discord.Net.Interactions/Info/ModalInfo.cs index 39e61ad18f..20e4e8f18b 100644 --- a/src/Discord.Net.Interactions/Info/ModalInfo.cs +++ b/src/Discord.Net.Interactions/Info/ModalInfo.cs @@ -37,48 +37,58 @@ public class ModalInfo /// /// Gets a collection of the components of this modal. /// - public IReadOnlyCollection Components { get; } + public IReadOnlyCollection Components { get; } + + /// + /// Gets a collection of the input components of this modal. + /// + public IReadOnlyCollection InputComponents { get; } /// /// Gets a collection of the text components of this modal. /// - public IReadOnlyCollection TextComponents { get; } + public IReadOnlyCollection TextInputComponents { get; } /// /// Get a collection of the select menu components of this modal. /// - public IReadOnlyCollection SelectMenuComponents { get; } + public IReadOnlyCollection SelectMenuInputComponents { get; } /// /// Get a collection of the user select components of this modal. /// - public IReadOnlyCollection UserSelectComponents { get; } + public IReadOnlyCollection UserSelectInputComponents { get; } /// /// Get a collection of the role select components of this modal. /// - public IReadOnlyCollection RoleSelectComponents { get; } + public IReadOnlyCollection RoleSelectInputComponents { get; } /// /// Get a collection of the mentionable select components of this modal. /// - public IReadOnlyCollection MentionableSelectComponents { get; } + public IReadOnlyCollection MentionableSelectInputComponents { get; } /// /// Get a collection of the channel select components of this modal. /// - public IReadOnlyCollection ChannelSelectComponents { get; } + public IReadOnlyCollection ChannelSelectInputComponents { get; } /// /// Get a collection of the file upload components of this modal. /// - public IReadOnlyCollection FileUploadComponents { get; } + public IReadOnlyCollection FileUploadInputComponents { get; } + + /// + /// Gets a collection of the text display components of this modal. + /// + public IReadOnlyCollection TextDisplayComponents { get; } internal ModalInfo(Builders.ModalBuilder builder) { Title = builder.Title; Type = builder.Type; - Components = builder.Components.Select(x => x switch + Components = builder.Components.Select(x => x switch { TextInputComponentBuilder textComponent => textComponent.Build(this), SelectMenuInputComponentBuilder selectMenuComponent => selectMenuComponent.Build(this), @@ -87,16 +97,19 @@ internal ModalInfo(Builders.ModalBuilder builder) UserSelectInputComponentBuilder userSelectComponent => userSelectComponent.Build(this), MentionableSelectInputComponentBuilder mentionableSelectComponent => mentionableSelectComponent.Build(this), FileUploadInputComponentBuilder fileUploadComponent => fileUploadComponent.Build(this), + TextDisplayComponentBuilder textDisplayComponent => textDisplayComponent.Build(this), _ => throw new InvalidOperationException($"{x.GetType().FullName} isn't a supported modal input component builder type.") }).ToImmutableArray(); - TextComponents = Components.OfType().ToImmutableArray(); - SelectMenuComponents = Components.OfType().ToImmutableArray(); - UserSelectComponents = Components.OfType().ToImmutableArray(); - RoleSelectComponents = Components.OfType().ToImmutableArray(); - MentionableSelectComponents = Components.OfType().ToImmutableArray(); - ChannelSelectComponents = Components.OfType().ToImmutableArray(); - FileUploadComponents = Components.OfType().ToImmutableArray(); + InputComponents = Components.OfType().ToImmutableArray(); + + TextInputComponents = Components.OfType().ToImmutableArray(); + SelectMenuInputComponents = Components.OfType().ToImmutableArray(); + UserSelectInputComponents = Components.OfType().ToImmutableArray(); + RoleSelectInputComponents = Components.OfType().ToImmutableArray(); + MentionableSelectInputComponents = Components.OfType().ToImmutableArray(); + ChannelSelectInputComponents = Components.OfType().ToImmutableArray(); + FileUploadInputComponents = Components.OfType().ToImmutableArray(); _interactionService = builder._interactionService; _initializer = builder.ModalInitializer; @@ -117,7 +130,7 @@ public IModal CreateModal(IModalInteraction modalInteraction, bool throwOnMissin for (var i = 0; i < Components.Count; i++) { - var input = Components.ElementAt(i); + var input = InputComponents.ElementAt(i); var component = components.Find(x => x.CustomId == input.CustomId); if (component is null) @@ -150,12 +163,12 @@ public async Task CreateModalAsync(IInteractionContext context, IServic services ??= EmptyServiceProvider.Instance; - var args = new object[Components.Count]; + var args = new object[InputComponents.Count]; var components = modalInteraction.Data.Components.ToList(); - for (var i = 0; i < Components.Count; i++) + for (var i = 0; i < InputComponents.Count; i++) { - var input = Components.ElementAt(i); + var input = InputComponents.ElementAt(i); var component = components.Find(x => x.CustomId == input.CustomId); if (component is null) From 77d10625183f339a88639ccdc5e26a95e63a49c6 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 12 Nov 2025 01:06:15 +0100 Subject: [PATCH 34/59] update modal component typeconverter namespaces and remove unused --- .../InteractionService.cs | 5 +- .../AttachmentModalComponentConverter.cs | 14 ---- .../DefaultArrayModalComponentConverter.cs | 68 +++++++++++++------ .../DefaultEntityModalComponentConverter.cs | 64 ----------------- .../DefaultValueModalComponentConverter.cs | 8 +-- .../EnumModalComponentConverter.cs | 9 ++- .../ModalComponentTypeConverter.cs | 34 +++++++++- .../NullableModalComponentConverter.cs | 3 +- 8 files changed, 90 insertions(+), 115 deletions(-) delete mode 100644 src/Discord.Net.Interactions/TypeConverters/ModalComponents/AttachmentModalComponentConverter.cs delete mode 100644 src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultEntityModalComponentConverter.cs diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index fce5f8d428..b4700170b7 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -1,6 +1,4 @@ using Discord.Interactions.Builders; -using Discord.Interactions.TypeConverters.ModalComponents; -using Discord.Interactions.TypeConverters.ModalInputs; using Discord.Logging; using Discord.Rest; using Discord.WebSocket; @@ -238,8 +236,7 @@ private InteractionService(Func getRestClient, InteractionSer [typeof(IConvertible)] = typeof(DefaultValueModalComponentConverter<>), [typeof(Enum)] = typeof(EnumModalComponentConverter<>), [typeof(Nullable<>)] = typeof(NullableComponentConverter<>), - [typeof(Array)] = typeof(DefaultArrayModalComponentConverter<>), - [typeof(IAttachment)] = typeof(AttachmentModalComponentConverter<>) + [typeof(Array)] = typeof(DefaultArrayModalComponentConverter<>) }); } diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/AttachmentModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/AttachmentModalComponentConverter.cs deleted file mode 100644 index a8600409ae..0000000000 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/AttachmentModalComponentConverter.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Discord.Interactions.TypeConverters.ModalInputs; -using System; -using System.Linq; -using System.Threading.Tasks; - -namespace Discord.Interactions.TypeConverters.ModalComponents; - -internal class AttachmentModalComponentConverter : ModalComponentTypeConverter where T : class, IAttachment -{ - public override async Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) - { - return TypeConverterResult.FromSuccess(option.Values.FirstOrDefault()); - } -} diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs index 5e8b2c02a3..3cde935c8c 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs @@ -1,11 +1,10 @@ -using Discord.Interactions.TypeConverters.ModalInputs; using Discord.Utils; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -namespace Discord.Interactions.TypeConverters.ModalComponents; +namespace Discord.Interactions; internal sealed class DefaultArrayModalComponentConverter : ModalComponentTypeConverter { @@ -27,7 +26,8 @@ public DefaultArrayModalComponentConverter(InteractionService interactionService _ when typeof(IUser).IsAssignableFrom(_underlyingType) || typeof(IChannel).IsAssignableFrom(_underlyingType) || typeof(IMentionable).IsAssignableFrom(_underlyingType) - || typeof(IRole).IsAssignableFrom(_underlyingType) => null, + || typeof(IRole).IsAssignableFrom(_underlyingType) + || typeof(IAttachment).IsAssignableFrom(_underlyingType) => null, _ => interactionService.GetTypeReader(_underlyingType) }; @@ -71,6 +71,7 @@ public override async Task ReadAsync(IInteractionContext co { var objs = new List(); + if (_typeReader is not null && option.Values.Count > 0) foreach (var value in option.Values) { @@ -83,23 +84,47 @@ public override async Task ReadAsync(IInteractionContext co } else { - var users = new Dictionary(); + if (!TryGetModalInteractionData(context, out var modalData)) + { + return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{typeof(IModalInteractionData).Name} cannot be accessed from the provided {typeof(IInteractionContext).Name} type."); + } + + var resolvedSnowflakes = new Dictionary(); + + if (modalData.Users is not null) + foreach (var user in modalData.Users) + resolvedSnowflakes[user.Id] = user; + + if (modalData.Members is not null) + foreach (var member in modalData.Members) + resolvedSnowflakes[member.Id] = member; + + if (modalData.Roles is not null) + foreach (var role in modalData.Roles) + resolvedSnowflakes[role.Id] = role; - if (option.Users is not null) - foreach (var user in option.Users) - users[user.Id] = user; + if (modalData.Channels is not null) + foreach (var channel in modalData.Channels) + resolvedSnowflakes[channel.Id] = channel; - if (option.Members is not null) - foreach (var member in option.Members) - users[member.Id] = member; + if (modalData.Attachments is not null) + foreach (var attachment in modalData.Attachments) + resolvedSnowflakes[attachment.Id] = attachment; - objs.AddRange(users.Values); + foreach (var value in option.Values) + { + if (!ulong.TryParse(value, out var id)) + { + return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{option.Type} contains invalid snowflake."); + } - if (option.Roles is not null) - objs.AddRange(option.Roles); + if (!resolvedSnowflakes.TryGetValue(id, out var snowflakeEntity)) + { + return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Some snowflake entity references for the {option.Type} cannot be resolved."); + } - if (option.Channels is not null) - objs.AddRange(option.Channels); + objs.Add(snowflakeEntity); + } } var destination = Array.CreateInstance(_underlyingType, objs.Count); @@ -112,7 +137,7 @@ public override async Task ReadAsync(IInteractionContext co public override Task WriteAsync(TBuilder builder, InputComponentInfo component, object value) { - if(builder is not SelectMenuBuilder selectMenu || !component.ComponentType.IsSelectType()) + 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) @@ -136,13 +161,13 @@ public override Task WriteAsync(TBuilder builder, InputComponentInfo c }); break; case IEnumerable defaultUsers: - selectMenu.DefaultValues = defaultUsers.Select(x => SelectMenuDefaultValue.FromUser(x)).ToList(); + selectMenu.DefaultValues = defaultUsers.Select(SelectMenuDefaultValue.FromUser).ToList(); break; case IEnumerable defaultRoles: - selectMenu.DefaultValues = defaultRoles.Select(x => SelectMenuDefaultValue.FromRole(x)).ToList(); + selectMenu.DefaultValues = defaultRoles.Select(SelectMenuDefaultValue.FromRole).ToList(); break; case IEnumerable defaultChannels: - selectMenu.DefaultValues = defaultChannels.Select(x => SelectMenuDefaultValue.FromChannel(x)).ToList(); + selectMenu.DefaultValues = defaultChannels.Select(SelectMenuDefaultValue.FromChannel).ToList(); break; case IEnumerable defaultMentionables: selectMenu.DefaultValues = defaultMentionables.Where(x => x is IUser or IRole or IChannel) @@ -158,11 +183,12 @@ public override Task WriteAsync(TBuilder builder, InputComponentInfo c }) .ToList(); break; - }; + } + ; - if(component.ComponentType == ComponentType.ChannelSelect && _channelTypes is not null) + if (component.ComponentType == ComponentType.ChannelSelect && _channelTypes is not null) selectMenu.WithChannelTypes(_channelTypes); return Task.CompletedTask; diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultEntityModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultEntityModalComponentConverter.cs deleted file mode 100644 index 952f43e6a7..0000000000 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultEntityModalComponentConverter.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Discord.Interactions.TypeConverters.ModalInputs; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Discord.Interactions.TypeConverters.ModalComponents; - -internal sealed class DefaultEntityModalComponentConverter : ModalComponentTypeConverter - where T : class, ISnowflakeEntity -{ - public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) - { - var objs = new List(); - - var users = new Dictionary(); - - if (option.Users is not null) - foreach (var user in option.Users) - users[user.Id] = user; - - if (option.Members is not null) - foreach (var member in option.Members) - users[member.Id] = member; - - objs.AddRange(users.Values); - - if (option.Roles is not null) - objs.AddRange(option.Roles); - - if (option.Channels is not null) - objs.AddRange(option.Channels); - - if (objs.Count > 1) - return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Component input returned multiple entities, but {typeof(T).FullName} is not an array type.")); - - return Task.FromResult(TypeConverterResult.FromSuccess(objs.FirstOrDefault() as T)); - } - - public override Task WriteAsync(TBuilder builder, InputComponentInfo component, object value) - { - (ISnowflakeEntity Snowflake, SelectDefaultValueType Type) defaultValue = value switch - { - IUser user => (user, SelectDefaultValueType.User), - IRole role => (role, SelectDefaultValueType.Role), - IChannel channel => (channel, SelectDefaultValueType.Channel), - _ => throw new InvalidOperationException($"Only snowflake entities can be used to populate components using {nameof(DefaultEntityModalComponentConverter<>)}") - }; - - switch (builder) - { - case TextInputBuilder textInput: - textInput.WithValue(defaultValue.Snowflake.Id.ToString()); - break; - case SelectMenuBuilder selectMenu: - selectMenu.WithDefaultValues(new SelectMenuDefaultValue(defaultValue.Snowflake.Id, defaultValue.Type)); - break; - default: - throw new InvalidOperationException($"{builder.GetType().FullName} is not supported by {nameof(DefaultEntityModalComponentConverter<>)}"); - } - - return Task.CompletedTask; - } -} diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs index f655d794bf..33a40e01c2 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs @@ -1,9 +1,8 @@ -using Discord.Interactions.TypeConverters.ModalInputs; using System; using System.Linq; using System.Threading.Tasks; -namespace Discord.Interactions.TypeConverters.ModalComponents; +namespace Discord.Interactions; internal sealed class DefaultValueModalComponentConverter : ModalComponentTypeConverter where T : IConvertible @@ -38,8 +37,9 @@ public override Task WriteAsync(TBuilder builder, InputComponentInfo c selectMenu.Options.FirstOrDefault(x => x.Value == strValue)?.IsDefault = true; break; default: - throw new InvalidOperationException($"{nameof(IConvertible)}s cannot be used to populate components other than SelectMenu and TextInput."); - }; + throw new InvalidOperationException($"{typeof(IConvertible).Name}s cannot be used to populate components other than SelectMenu and TextInput."); + } + ; return Task.CompletedTask; } diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs index 9b0390041f..535cfa2b0e 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs @@ -1,11 +1,10 @@ -using Discord.Interactions.TypeConverters.ModalInputs; using System; using System.Collections.Immutable; using System.Linq; using System.Reflection; using System.Threading.Tasks; -namespace Discord.Interactions.TypeConverters.ModalComponents; +namespace Discord.Interactions; internal sealed class EnumModalComponentConverter : ModalComponentTypeConverter where T : struct, Enum @@ -32,7 +31,7 @@ public EnumModalComponentConverter() public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) { - if(option.Type is not ComponentType.SelectMenu or ComponentType.TextInput) + if (option.Type is not ComponentType.SelectMenu or 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 @@ -42,7 +41,7 @@ public override Task ReadAsync(IInteractionContext context, _ => null }; - if(Enum.TryParse(value, out var result)) + if (Enum.TryParse(value, out var result)) return Task.FromResult(TypeConverterResult.FromSuccess(result)); return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Value {option.Value} cannot be converted to {typeof(T).FullName}")); @@ -53,7 +52,7 @@ public override Task WriteAsync(TBuilder builder, InputComponentInfo c if (builder is not SelectMenuBuilder selectMenu || component.ComponentType is not ComponentType.SelectMenu) throw new InvalidOperationException($"{nameof(EnumModalComponentConverter)} can only write to select menu components."); - if(selectMenu.MaxValues > 1 && !_isFlags) + if (selectMenu.MaxValues > 1 && !_isFlags) throw new InvalidOperationException($"Enum type {typeof(T).FullName} is not a [Flags] enum, so it cannot be used in a multi-select menu."); selectMenu.WithOptions(_options.ToList()); diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/ModalComponentTypeConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/ModalComponentTypeConverter.cs index 485c7f5b10..8b9ef3d137 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/ModalComponentTypeConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/ModalComponentTypeConverter.cs @@ -1,18 +1,50 @@ using System; using System.Threading.Tasks; -namespace Discord.Interactions.TypeConverters.ModalInputs; +namespace Discord.Interactions; + +/// +/// Base class for creating ModalComponentTypeConverters. uses ModalComponentTypeConverters to interface with Modal component parameters. +/// public abstract class ModalComponentTypeConverter : ITypeConverter { + /// + /// Will be used to search for alternative ModalComponentTypeConverters whenever the Interaction Service encounters an unknown parameter type. + /// + /// Type of the modal property. + /// Whether this converter can be used to handle the given type. public abstract bool CanConvertTo(Type type); + /// + /// Will be used to read the incoming payload before building the modal instance. + /// + /// Command execution context. + /// Received option payload. + /// Service provider that will be used to initialize the command module. + /// The result of the read process. public abstract Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services); + /// + /// Will be used to manipulate the outgoing modal component, before the modal gets sent to Discord. + /// public virtual Task WriteAsync(TBuilder builder, InputComponentInfo component, object value) where TBuilder : class, IInteractableComponentBuilder => Task.CompletedTask; + + protected bool TryGetModalInteractionData(IInteractionContext context, out IModalInteractionData modalData) + { + if(context.Interaction is IModalInteraction modalInteraction) + { + modalData = modalInteraction.Data; + return true; + } + + modalData = null; + return false; + } } +/// public abstract class ModalComponentTypeConverter : ModalComponentTypeConverter { /// diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/NullableModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/NullableModalComponentConverter.cs index b567316b69..16156f5c26 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/NullableModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/NullableModalComponentConverter.cs @@ -1,8 +1,7 @@ -using Discord.Interactions.TypeConverters.ModalInputs; using System; using System.Threading.Tasks; -namespace Discord.Interactions.TypeConverters.ModalComponents; +namespace Discord.Interactions; internal class NullableModalComponentConverter : ModalComponentTypeConverter { From a4518f1ff4c6ea5f78b13f55250f82f5c87d204f Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 12 Nov 2025 01:06:33 +0100 Subject: [PATCH 35/59] add default min/max values to select input attribute --- .../Attributes/Modals/ModalSelectMenuInputAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuInputAttribute.cs index 9769ba00a1..60f42b4cb0 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuInputAttribute.cs @@ -14,7 +14,7 @@ public sealed class ModalSelectMenuInputAttribute : ModalSelectInputAttribute /// Custom ID of the select menu component. /// Minimum number of values that can be selected. /// Maximum number of values that can be selected. - public ModalSelectMenuInputAttribute(string customId, int minValues, int maxValues) : base(customId) + public ModalSelectMenuInputAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { } } From eedf53ebc974db716658a1c6a3e2262c1f47b4aa Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 12 Nov 2025 01:06:58 +0100 Subject: [PATCH 36/59] create text display builder and info classes --- .../Modals/TextDisplayComponentBuilder.cs | 50 +++++++++++++++++++ .../Components/TextDisplayComponentInfo.cs | 13 +++++ 2 files changed, 63 insertions(+) create mode 100644 src/Discord.Net.Interactions/Builders/Modals/TextDisplayComponentBuilder.cs create mode 100644 src/Discord.Net.Interactions/Info/Commands/Components/TextDisplayComponentInfo.cs diff --git a/src/Discord.Net.Interactions/Builders/Modals/TextDisplayComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/TextDisplayComponentBuilder.cs new file mode 100644 index 0000000000..6bf4553d7b --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Modals/TextDisplayComponentBuilder.cs @@ -0,0 +1,50 @@ +using System; + +namespace Discord.Interactions.Builders; + +/// +/// Represents a builder for creating . +/// +public class TextDisplayComponentBuilder : ModalComponentBuilder +{ + protected override TextDisplayComponentBuilder Instance => throw new NotImplementedException(); + + /// + /// Gets and sets the content of the text display. + /// + public string Content { get; set; } + + /// + /// Initialize a new . + /// + /// Parent modal of this input component. + public TextDisplayComponentBuilder(ModalBuilder modal) : base(modal) + { + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TextDisplayComponentBuilder WithContent(string content) + { + Content = content; + return this; + } + + public override TextDisplayComponentBuilder WithType(Type type) + { + if(type != typeof(string)) + { + throw new ArgumentException($"Text display components can be only used with {typeof(string).Name} properties. {type.Name} provided instead."); + } + + return base.WithType(type); + } + + internal override TextDisplayComponentInfo Build(ModalInfo modal) + => new(this, modal); +} diff --git a/src/Discord.Net.Interactions/Info/Commands/Components/TextDisplayComponentInfo.cs b/src/Discord.Net.Interactions/Info/Commands/Components/TextDisplayComponentInfo.cs new file mode 100644 index 0000000000..69c85d536c --- /dev/null +++ b/src/Discord.Net.Interactions/Info/Commands/Components/TextDisplayComponentInfo.cs @@ -0,0 +1,13 @@ +using Discord.Interactions.Builders; + +namespace Discord.Interactions; + +public class TextDisplayComponentInfo : ModalComponentInfo +{ + public string Content { get; } + + public TextDisplayComponentInfo(TextDisplayComponentBuilder builder, ModalInfo modal) : base(builder, modal) + { + Content = Content; + } +} From 54266b8049004699ef2d9ff556038d7d6a3911db Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 12 Nov 2025 01:12:46 +0100 Subject: [PATCH 37/59] add text display parsing logic --- .../Builders/ModuleClassBuilder.cs | 34 +++++++++++++++++-- .../IDiscordInteractionExtensions.cs | 17 ++++++---- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index 9723d3bdee..f1e7dd4afd 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -634,7 +634,8 @@ public static ModalInfo BuildModalInfo(Type modalType, InteractionService intera builder.AddFileUploadInputComponent(x => BuildFileUploadInput(x, prop, prop.GetValue(instance))); break; case ComponentType.TextDisplay: - throw new NotImplementedException(); + builder.AddTextDisplayComponent(x => BuildTextDisplayComponent(x, prop, prop.GetValue(instance))); + break; case null: throw new InvalidOperationException($"{prop.Name} of {prop.DeclaringType.Name} isn't a valid modal input field."); default: @@ -715,6 +716,16 @@ private static void BuildSelectMenuInput(SelectMenuInputComponentBuilder builder builder.Label = inputLabel.Label; builder.Description = inputLabel.Description; break; + case ModalSelectMenuOptionAttribute selectMenuOption: + builder.AddOption(new SelectMenuOptionBuilder + { + Label = selectMenuOption.Label, + Description = selectMenuOption.Description, + Value = selectMenuOption.Value, + Emote = Emote.Parse(selectMenuOption.Emote), + IsDefault = selectMenuOption.IsDefault + }); + break; default: builder.WithAttributes(attribute); break; @@ -789,12 +800,29 @@ private static void BuildFileUploadInput(FileUploadInputComponentBuilder builder break; } } - } - private static void BuildTextDisplayComponent() + private static void BuildTextDisplayComponent(TextDisplayComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) { + var attributes = propertyInfo.GetCustomAttributes(); + builder.DefaultValue = defaultValue; + builder.WithType(propertyInfo.PropertyType); + builder.PropertyInfo = propertyInfo; + + foreach (var attribute in attributes) + { + switch (attribute) + { + case ModalTextDisplayAttribute textDisplay: + builder.ComponentType = textDisplay.ComponentType; + builder.Content = textDisplay.Content; + break; + default: + builder.WithAttributes(attribute); + break; + } + } } #endregion diff --git a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs index e03f0a2873..74e024a5f0 100644 --- a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs +++ b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using System.Reflection; using System.Threading.Tasks; namespace Discord.Interactions; @@ -66,7 +65,7 @@ public static Task RespondWithModalAsync(this IDiscordInteraction interaction if (!ModalUtils.TryGet(out var modalInfo)) throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}"); - return SendModalResponseAsync(interaction, customId, modalInfo, modal, options, modifyModal); + return SendModalResponseAsync(interaction, customId, modalInfo, modal, options, modifyModal); } private static async Task SendModalResponseAsync(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, T modalInstance = null, RequestOptions options = null, Action modifyModal = null) @@ -85,7 +84,7 @@ private static async Task SendModalResponseAsync(IDiscordInteraction interact var inputBuilder = new TextInputBuilder(textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null, textComponent.MaxLength, textComponent.IsRequired); - if(modalInstance != null) + if (modalInstance != null) { await textComponent.TypeConverter.WriteAsync(inputBuilder, textComponent, textComponent.Getter(modalInstance)); } @@ -98,7 +97,7 @@ private static async Task SendModalResponseAsync(IDiscordInteraction interact { var inputBuilder = new SelectMenuBuilder(selectMenuComponent.CustomId, selectMenuComponent.Options.Select(x => new SelectMenuOptionBuilder(x)).ToList(), selectMenuComponent.Placeholder, selectMenuComponent.MaxValues, selectMenuComponent.MinValues, false); - if(modalInstance != null) + if (modalInstance != null) { await selectMenuComponent.TypeConverter.WriteAsync(inputBuilder, selectMenuComponent, selectMenuComponent.Getter(modalInstance)); } @@ -111,7 +110,7 @@ private static async Task SendModalResponseAsync(IDiscordInteraction interact { var inputBuilder = new SelectMenuBuilder(snowflakeSelectComponent.CustomId, null, snowflakeSelectComponent.Placeholder, snowflakeSelectComponent.MaxValues, snowflakeSelectComponent.MinValues, false, snowflakeSelectComponent.ComponentType, null, snowflakeSelectComponent.DefaultValues.ToList()); - if(modalInstance != null) + if (modalInstance != null) { await snowflakeSelectComponent.TypeConverter.WriteAsync(inputBuilder, snowflakeSelectComponent, snowflakeSelectComponent.Getter(modalInstance)); } @@ -124,7 +123,7 @@ private static async Task SendModalResponseAsync(IDiscordInteraction interact { var inputBuilder = new FileUploadComponentBuilder(fileUploadComponent.CustomId, fileUploadComponent.MinValues, fileUploadComponent.MaxValues, fileUploadComponent.IsRequired); - if(modalInstance != null) + if (modalInstance != null) { await fileUploadComponent.TypeConverter.WriteAsync(inputBuilder, fileUploadComponent, fileUploadComponent.Getter(modalInstance)); } @@ -133,6 +132,12 @@ private static async Task SendModalResponseAsync(IDiscordInteraction interact builder.AddLabel(labelBuilder); } break; + case TextDisplayComponentInfo textDisplayComponent: + { + var componentBuilder = new TextDisplayBuilder(textDisplayComponent.Content); + builder.AddTextDisplay(componentBuilder); + } + break; default: throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class"); } From 3e5e1962d4cf0a60a7f23be016843dc11145ac7c Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 12 Nov 2025 01:12:58 +0100 Subject: [PATCH 38/59] add text display attribute --- .../Modals/ModalTextDisplayAttribute.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs new file mode 100644 index 0000000000..29b3db2ada --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs @@ -0,0 +1,26 @@ +using Discord.Interactions.Attributes.Modals; + +namespace Discord.Interactions; + +/// +/// Marks a property as a text input. +/// +public class ModalTextDisplayAttribute : ModalComponentAttribute +{ + /// + public override ComponentType ComponentType => ComponentType.TextDisplay; + + /// + /// Gets the content of the text display. + /// + public string Content { get; } + + /// + /// Create a new . + /// + /// Content of the text display. + public ModalTextDisplayAttribute(string content = null) + { + Content = content; + } +} From 751a71dc331ea9c5bc129fd34bf2dc9a38881b78 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 12 Nov 2025 01:13:08 +0100 Subject: [PATCH 39/59] add modal select menu option attribute --- .../Modals/ModalSelectMenuOptionAttribute.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuOptionAttribute.cs diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuOptionAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuOptionAttribute.cs new file mode 100644 index 0000000000..dfe46ddd18 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuOptionAttribute.cs @@ -0,0 +1,52 @@ +using System; + +namespace Discord.Interactions.Attributes.Modals; + +/// +/// Adds a select menu option to the marked field. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] +public class ModalSelectMenuOptionAttribute : Attribute +{ + /// + /// Gets the label of the option. + /// + public string Label { get; } + + /// + /// Gets or sets the description of the option. + /// + public string Description { get; set; } + + /// + /// Gets the value of the option. + /// + public string Value { get; } + + /// + /// Gets or sets the emote of the option. + /// + public string Emote { get; set; } + + /// + /// Gets or sets whether the option is selected by default. + /// + public bool IsDefault { get; set; } + + /// + /// Create a new . + /// + /// Label of the option. + /// Value of the option. + /// Description of the option. + /// Emote of the option. + /// Whether the option is selected by default + public ModalSelectMenuOptionAttribute(string label, string value, string description = null, string emote = null, bool isDefault = false) + { + Label = label; + Value = value; + Description = description; + Emote = emote; + IsDefault = isDefault; + } +} From 02c75ea44a335940f95831baac645e0c004bf8ae Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 12 Nov 2025 01:16:40 +0100 Subject: [PATCH 40/59] add docs to text display component info --- .../Components/TextDisplayComponentInfo.cs | 13 ------------- .../Components/TextDisplayComponentInfo.cs | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 13 deletions(-) delete mode 100644 src/Discord.Net.Interactions/Info/Commands/Components/TextDisplayComponentInfo.cs create mode 100644 src/Discord.Net.Interactions/Info/Components/TextDisplayComponentInfo.cs diff --git a/src/Discord.Net.Interactions/Info/Commands/Components/TextDisplayComponentInfo.cs b/src/Discord.Net.Interactions/Info/Commands/Components/TextDisplayComponentInfo.cs deleted file mode 100644 index 69c85d536c..0000000000 --- a/src/Discord.Net.Interactions/Info/Commands/Components/TextDisplayComponentInfo.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Discord.Interactions.Builders; - -namespace Discord.Interactions; - -public class TextDisplayComponentInfo : ModalComponentInfo -{ - public string Content { get; } - - public TextDisplayComponentInfo(TextDisplayComponentBuilder builder, ModalInfo modal) : base(builder, modal) - { - Content = Content; - } -} diff --git a/src/Discord.Net.Interactions/Info/Components/TextDisplayComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/TextDisplayComponentInfo.cs new file mode 100644 index 0000000000..4869280620 --- /dev/null +++ b/src/Discord.Net.Interactions/Info/Components/TextDisplayComponentInfo.cs @@ -0,0 +1,19 @@ +using Discord.Interactions.Builders; + +namespace Discord.Interactions; + +/// +/// Represents the class for type. +/// +public class TextDisplayComponentInfo : ModalComponentInfo +{ + /// + /// Gets the content of the text display. + /// + public string Content { get; } + + internal TextDisplayComponentInfo(TextDisplayComponentBuilder builder, ModalInfo modal) : base(builder, modal) + { + Content = Content; + } +} From 54d0379ab13e800d96a840b9c5be4d14129980eb Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 12 Nov 2025 15:23:00 +0100 Subject: [PATCH 41/59] fix text display parsing --- .../Builders/Modals/ModalBuilder.cs | 21 ++++++++++++------- .../Modals/TextDisplayComponentBuilder.cs | 2 +- .../Builders/ModuleClassBuilder.cs | 10 ++++----- .../IDiscordInteractionExtensions.cs | 3 ++- .../Info/ModalInfo.cs | 1 + .../DefaultArrayModalComponentConverter.cs | 3 +++ 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs index 73ceadc6d7..7271ef9d89 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs @@ -66,7 +66,7 @@ public ModalBuilder WithTitle(string title) } /// - /// Adds text components to . + /// Adds text components to . /// /// Text Component builder factory. /// @@ -81,7 +81,7 @@ public ModalBuilder AddTextInputComponent(Action conf } /// - /// Adds a select menu component to . + /// Adds a select menu component to . /// /// Select menu component builder factory. /// @@ -96,7 +96,7 @@ public ModalBuilder AddSelectMenuInputComponent(Action - /// Adds a user select component to . + /// Adds a user select component to . /// /// User select component builder factory. /// @@ -111,7 +111,7 @@ public ModalBuilder AddUserSelectInputComponent(Action - /// Adds a role select component to . + /// Adds a role select component to . /// /// Role select component builder factory. /// @@ -126,7 +126,7 @@ public ModalBuilder AddRoleSelectInputComponent(Action - /// Adds a mentionable select component to . + /// Adds a mentionable select component to . /// /// Mentionable select component builder factory. /// @@ -141,7 +141,7 @@ public ModalBuilder AddMentionableSelectInputComponent(Action - /// Adds a channel select component to . + /// Adds a channel select component to . /// /// Channel select component builder factory. /// @@ -156,7 +156,7 @@ public ModalBuilder AddChannelSelectInputComponent(Action - /// Adds a file upload component to . + /// Adds a file upload component to . /// /// File upload component builder factory. /// @@ -170,6 +170,13 @@ public ModalBuilder AddFileUploadInputComponent(Action + /// Adds a text display component to . + /// + /// Text display component builder factory. + /// + /// The builder instance. + /// public ModalBuilder AddTextDisplayComponent(Action configure) { var builder = new TextDisplayComponentBuilder(this); diff --git a/src/Discord.Net.Interactions/Builders/Modals/TextDisplayComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/TextDisplayComponentBuilder.cs index 6bf4553d7b..7d6d1e3107 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/TextDisplayComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/TextDisplayComponentBuilder.cs @@ -7,7 +7,7 @@ namespace Discord.Interactions.Builders; /// public class TextDisplayComponentBuilder : ModalComponentBuilder { - protected override TextDisplayComponentBuilder Instance => throw new NotImplementedException(); + protected override TextDisplayComponentBuilder Instance => this; /// /// Gets and sets the content of the text display. diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index f1e7dd4afd..38d926bb29 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -604,11 +604,11 @@ public static ModalInfo BuildModalInfo(Type modalType, InteractionService intera Title = instance.Title }; - var inputs = modalType.GetProperties().Where(IsValidModalInputDefinition); + var components = modalType.GetProperties().Where(IsValidModalComponentDefinition); - foreach (var prop in inputs) + foreach (var prop in components) { - var componentType = prop.GetCustomAttribute()?.ComponentType; + var componentType = prop.GetCustomAttribute()?.ComponentType; switch (componentType) { @@ -875,11 +875,11 @@ private static bool IsValidModalCommandDefinition(MethodInfo methodInfo) typeof(IModal).IsAssignableFrom(methodInfo.GetParameters().Last().ParameterType); } - private static bool IsValidModalInputDefinition(PropertyInfo propertyInfo) + private static bool IsValidModalComponentDefinition(PropertyInfo propertyInfo) { return propertyInfo.SetMethod?.IsPublic == true && propertyInfo.SetMethod?.IsStatic == false && - propertyInfo.IsDefined(typeof(ModalInputAttribute)); + propertyInfo.IsDefined(typeof(ModalComponentAttribute)); } private static ConstructorInfo GetComplexParameterConstructor(TypeInfo typeInfo, ComplexParameterAttribute complexParameter) diff --git a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs index 74e024a5f0..886d9b9572 100644 --- a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs +++ b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs @@ -134,7 +134,8 @@ private static async Task SendModalResponseAsync(IDiscordInteraction interact break; case TextDisplayComponentInfo textDisplayComponent: { - var componentBuilder = new TextDisplayBuilder(textDisplayComponent.Content); + var content = textDisplayComponent.Getter(modalInstance).ToString() ?? textDisplayComponent.Content; + var componentBuilder = new TextDisplayBuilder(content); builder.AddTextDisplay(componentBuilder); } break; diff --git a/src/Discord.Net.Interactions/Info/ModalInfo.cs b/src/Discord.Net.Interactions/Info/ModalInfo.cs index 20e4e8f18b..37ad926417 100644 --- a/src/Discord.Net.Interactions/Info/ModalInfo.cs +++ b/src/Discord.Net.Interactions/Info/ModalInfo.cs @@ -110,6 +110,7 @@ internal ModalInfo(Builders.ModalBuilder builder) MentionableSelectInputComponents = Components.OfType().ToImmutableArray(); ChannelSelectInputComponents = Components.OfType().ToImmutableArray(); FileUploadInputComponents = Components.OfType().ToImmutableArray(); + TextDisplayComponents = Components.OfType().ToImmutableArray(); _interactionService = builder._interactionService; _initializer = builder.ModalInitializer; diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs index 3cde935c8c..98ed224fe7 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs @@ -137,6 +137,9 @@ public override async Task ReadAsync(IInteractionContext co public override Task WriteAsync(TBuilder builder, InputComponentInfo component, object value) { + if (builder is FileUploadComponentBuilder) + return Task.CompletedTask; + 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."); From f9e11bd143eded4a4c4ac791740e200db88845d3 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 12 Nov 2025 15:38:10 +0100 Subject: [PATCH 42/59] add isRequired mapping to select menus --- .../Extensions/IDiscordInteractionExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs index 886d9b9572..5893af2de3 100644 --- a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs +++ b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs @@ -95,7 +95,7 @@ private static async Task SendModalResponseAsync(IDiscordInteraction interact break; case SelectMenuInputComponentInfo selectMenuComponent: { - var inputBuilder = new SelectMenuBuilder(selectMenuComponent.CustomId, selectMenuComponent.Options.Select(x => new SelectMenuOptionBuilder(x)).ToList(), selectMenuComponent.Placeholder, selectMenuComponent.MaxValues, selectMenuComponent.MinValues, false); + var inputBuilder = new SelectMenuBuilder(selectMenuComponent.CustomId, selectMenuComponent.Options.Select(x => new SelectMenuOptionBuilder(x)).ToList(), selectMenuComponent.Placeholder, selectMenuComponent.MaxValues, selectMenuComponent.MinValues, false, isRequired: selectMenuComponent.IsRequired); if (modalInstance != null) { @@ -108,7 +108,7 @@ private static async Task SendModalResponseAsync(IDiscordInteraction interact break; case SnowflakeSelectInputComponentInfo snowflakeSelectComponent: { - var inputBuilder = new SelectMenuBuilder(snowflakeSelectComponent.CustomId, null, snowflakeSelectComponent.Placeholder, snowflakeSelectComponent.MaxValues, snowflakeSelectComponent.MinValues, false, snowflakeSelectComponent.ComponentType, null, snowflakeSelectComponent.DefaultValues.ToList()); + var inputBuilder = new SelectMenuBuilder(snowflakeSelectComponent.CustomId, null, snowflakeSelectComponent.Placeholder, snowflakeSelectComponent.MaxValues, snowflakeSelectComponent.MinValues, false, snowflakeSelectComponent.ComponentType, null, snowflakeSelectComponent.DefaultValues.ToList(), null, snowflakeSelectComponent.IsRequired); if (modalInstance != null) { From 1e61ea384afe80b35e882d84528f457f3a762431 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 12 Nov 2025 16:27:44 +0100 Subject: [PATCH 43/59] revert to block-scope to clear diff false-positives --- .../Attributes/Modals/InputLabelAttribute.cs | 45 +- .../Attributes/Modals/ModalInputAttribute.cs | 33 +- .../Modals/ModalTextInputAttribute.cs | 89 +- .../Modals/RequiredInputAttribute.cs | 31 +- .../Builders/Commands/ICommandBuilder.cs | 237 +- .../Modals/Inputs/IInputComponentBuilder.cs | 113 +- .../Modals/Inputs/InputComponentBuilder.cs | 217 +- .../Inputs/TextInputComponentBuilder.cs | 187 +- .../Builders/Modals/ModalBuilder.cs | 360 +-- .../Builders/ModuleClassBuilder.cs | 1469 +++++----- .../Entities/IModal.cs | 17 +- .../Entities/ISelectMenuModel.cs | 11 + .../IDiscordInteractionExtensions.cs | 257 +- .../InputComponents/InputComponentInfo.cs | 65 +- .../InputComponents/TextInputComponentInfo.cs | 87 +- .../Info/ModalInfo.cs | 323 +-- .../InteractionService.cs | 2493 +++++++++-------- .../TypeReaders/DateTimeTypeReader.cs | 15 + 18 files changed, 3045 insertions(+), 3004 deletions(-) create mode 100644 src/Discord.Net.Interactions/Entities/ISelectMenuModel.cs create mode 100644 src/Discord.Net.Interactions/TypeReaders/DateTimeTypeReader.cs diff --git a/src/Discord.Net.Interactions/Attributes/Modals/InputLabelAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/InputLabelAttribute.cs index ecbb44fd7d..0e018f745f 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/InputLabelAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/InputLabelAttribute.cs @@ -1,31 +1,32 @@ using System; -namespace Discord.Interactions; - -/// -/// Creates a custom label for an modal input. -/// -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public class InputLabelAttribute : Attribute +namespace Discord.Interactions { - /// - /// Gets the label of the input. - /// - public string Label { get; } - - /// - /// Gets the label description of the input. - /// - public string Description { get; set; } - /// /// Creates a custom label for an modal input. /// - /// The label of the input. - /// The label description of the input. - public InputLabelAttribute(string label, string description = null) + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class InputLabelAttribute : Attribute { - Label = label; - Description = description; + /// + /// Gets the label of the input. + /// + public string Label { get; } + + /// + /// Gets the label description of the input. + /// + public string Description { get; set; } + + /// + /// Creates a custom label for an modal input. + /// + /// The label of the input. + /// The label description of the input. + public InputLabelAttribute(string label, string description = null) + { + Label = label; + Description = description; + } } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs index 054f495486..960fc75d49 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs @@ -1,25 +1,26 @@ using Discord.Interactions.Attributes.Modals; using System; -namespace Discord.Interactions; - -/// -/// Mark an property as a modal input field. -/// -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] -public abstract class ModalInputAttribute : ModalComponentAttribute +namespace Discord.Interactions { /// - /// Gets the custom id of the text input. + /// Mark an property as a modal input field. /// - public string CustomId { get; } - - /// - /// Create a new . - /// - /// The custom id of the input. - protected ModalInputAttribute(string customId) + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + public abstract class ModalInputAttribute : ModalComponentAttribute { - CustomId = customId; + /// + /// Gets the custom id of the text input. + /// + public string CustomId { get; } + + /// + /// Create a new . + /// + /// The custom id of the input. + protected ModalInputAttribute(string customId) + { + CustomId = customId; + } } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs index 37bec8bc83..4439e1d845 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs @@ -1,54 +1,55 @@ -namespace Discord.Interactions; - -/// -/// Marks a property as a text input. -/// -public sealed class ModalTextInputAttribute : ModalInputAttribute +namespace Discord.Interactions { - /// - public override ComponentType ComponentType => ComponentType.TextInput; - /// - /// Gets the style of the text input. + /// Marks a property as a text input. /// - public TextInputStyle Style { get; } + public sealed class ModalTextInputAttribute : ModalInputAttribute + { + /// + public override ComponentType ComponentType => ComponentType.TextInput; - /// - /// Gets the placeholder of the text input. - /// - public string Placeholder { get; } + /// + /// Gets the style of the text input. + /// + public TextInputStyle Style { get; } - /// - /// Gets the minimum length of the text input. - /// - public int MinLength { get; } + /// + /// Gets the placeholder of the text input. + /// + public string Placeholder { get; } - /// - /// Gets the maximum length of the text input. - /// - public int MaxLength { get; } + /// + /// Gets the minimum length of the text input. + /// + public int MinLength { get; } - /// - /// Gets the initial value to be displayed by this input. - /// - public string InitialValue { get; } + /// + /// Gets the maximum length of the text input. + /// + public int MaxLength { get; } - /// - /// Create a new . - /// - /// The custom id of the text input.> - /// The style of the text input. - /// The placeholder of the text input. - /// The minimum length of the text input's content. - /// The maximum length of the text input's content. - /// The initial value to be displayed by this input. - public ModalTextInputAttribute(string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, int minLength = 1, int maxLength = 4000, string initValue = null) - : base(customId) - { - Style = style; - Placeholder = placeholder; - MinLength = minLength; - MaxLength = maxLength; - InitialValue = initValue; + /// + /// Gets the initial value to be displayed by this input. + /// + public string InitialValue { get; } + + /// + /// Create a new . + /// + /// The custom id of the text input.> + /// The style of the text input. + /// The placeholder of the text input. + /// The minimum length of the text input's content. + /// The maximum length of the text input's content. + /// The initial value to be displayed by this input. + public ModalTextInputAttribute(string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, int minLength = 1, int maxLength = 4000, string initValue = null) + : base(customId) + { + Style = style; + Placeholder = placeholder; + MinLength = minLength; + MaxLength = maxLength; + InitialValue = initValue; + } } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/RequiredInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/RequiredInputAttribute.cs index 112a25a58a..1f580ff000 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/RequiredInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/RequiredInputAttribute.cs @@ -1,24 +1,25 @@ using System; -namespace Discord.Interactions; - -/// -/// Sets the input as required or optional. -/// -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public class RequiredInputAttribute : Attribute +namespace Discord.Interactions { - /// - /// Gets whether or not user input is required for this input. - /// - public bool IsRequired { get; } - /// /// Sets the input as required or optional. /// - /// Whether or not user input is required for this input. - public RequiredInputAttribute(bool isRequired = true) + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class RequiredInputAttribute : Attribute { - IsRequired = isRequired; + /// + /// Gets whether or not user input is required for this input. + /// + public bool IsRequired { get; } + + /// + /// Sets the input as required or optional. + /// + /// Whether or not user input is required for this input. + public RequiredInputAttribute(bool isRequired = true) + { + IsRequired = isRequired; + } } } diff --git a/src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs index e2943b455b..a8036e6043 100644 --- a/src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs @@ -1,124 +1,125 @@ using System; using System.Collections.Generic; -namespace Discord.Interactions.Builders; - -/// -/// Represent a command builder for creating . -/// -public interface ICommandBuilder +namespace Discord.Interactions.Builders { /// - /// Gets the execution delegate of this command. - /// - ExecuteCallback Callback { get; } - - /// - /// Gets the parent module of this command. - /// - ModuleBuilder Module { get; } - - /// - /// Gets the name of this command. - /// - string Name { get; } - - /// - /// Gets or sets the method name of this command. - /// - string MethodName { get; set; } - - /// - /// Gets or sets if this command will be registered and executed as a standalone command, unaffected by the s of - /// of the commands parents. - /// - bool IgnoreGroupNames { get; set; } - - /// - /// Gets or sets whether the should be directly used as a Regex pattern. - /// - bool TreatNameAsRegex { get; set; } - - /// - /// Gets or sets the run mode this command gets executed with. - /// - RunMode RunMode { get; set; } - - /// - /// Gets a collection of the attributes of this command. - /// - IReadOnlyList Attributes { get; } - - /// - /// Gets a collection of the parameters of this command. - /// - IReadOnlyList Parameters { get; } - - /// - /// Gets a collection of the preconditions of this command. - /// - IReadOnlyList Preconditions { get; } - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - ICommandBuilder WithName(string name); - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - ICommandBuilder WithMethodName(string name); - - /// - /// Adds attributes to . - /// - /// New attributes to be added to . - /// - /// The builder instance. - /// - ICommandBuilder WithAttributes(params Attribute[] attributes); - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - ICommandBuilder SetRunMode(RunMode runMode); - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - ICommandBuilder WithNameAsRegex(bool value); - - /// - /// Adds parameter builders to . - /// - /// New parameter builders to be added to . - /// - /// The builder instance. - /// - ICommandBuilder AddParameters(params IParameterBuilder[] parameters); - - /// - /// Adds preconditions to . - /// - /// New preconditions to be added to . - /// - /// The builder instance. - /// - ICommandBuilder WithPreconditions(params PreconditionAttribute[] preconditions); + /// Represent a command builder for creating . + /// + public interface ICommandBuilder + { + /// + /// Gets the execution delegate of this command. + /// + ExecuteCallback Callback { get; } + + /// + /// Gets the parent module of this command. + /// + ModuleBuilder Module { get; } + + /// + /// Gets the name of this command. + /// + string Name { get; } + + /// + /// Gets or sets the method name of this command. + /// + string MethodName { get; set; } + + /// + /// Gets or sets if this command will be registered and executed as a standalone command, unaffected by the s of + /// of the commands parents. + /// + bool IgnoreGroupNames { get; set; } + + /// + /// Gets or sets whether the should be directly used as a Regex pattern. + /// + bool TreatNameAsRegex { get; set; } + + /// + /// Gets or sets the run mode this command gets executed with. + /// + RunMode RunMode { get; set; } + + /// + /// Gets a collection of the attributes of this command. + /// + IReadOnlyList Attributes { get; } + + /// + /// Gets a collection of the parameters of this command. + /// + IReadOnlyList Parameters { get; } + + /// + /// Gets a collection of the preconditions of this command. + /// + IReadOnlyList Preconditions { get; } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + ICommandBuilder WithName(string name); + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + ICommandBuilder WithMethodName(string name); + + /// + /// Adds attributes to . + /// + /// New attributes to be added to . + /// + /// The builder instance. + /// + ICommandBuilder WithAttributes(params Attribute[] attributes); + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + ICommandBuilder SetRunMode(RunMode runMode); + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + ICommandBuilder WithNameAsRegex(bool value); + + /// + /// Adds parameter builders to . + /// + /// New parameter builders to be added to . + /// + /// The builder instance. + /// + ICommandBuilder AddParameters(params IParameterBuilder[] parameters); + + /// + /// Adds preconditions to . + /// + /// New preconditions to be added to . + /// + /// The builder instance. + /// + ICommandBuilder WithPreconditions(params PreconditionAttribute[] preconditions); + } } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs index 3660210687..4ab2578390 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs @@ -1,67 +1,68 @@ -namespace Discord.Interactions.Builders; - -/// -/// Represent a builder for creating . -/// -public interface IInputComponentBuilder : IModalComponentBuilder +namespace Discord.Interactions.Builders { /// - /// Gets the custom id of this input component. + /// Represent a builder for creating . /// - string CustomId { get; } + public interface IInputComponentBuilder : IModalComponentBuilder + { + /// + /// Gets the custom id of this input component. + /// + string CustomId { get; } - /// - /// Gets the label of this input component. - /// - string Label { get; } + /// + /// Gets the label of this input component. + /// + string Label { get; } - /// - /// Gets the label description of this input component. - /// - string Description { get; } + /// + /// Gets the label description of this input component. + /// + string Description { get; } - /// - /// Gets whether this input component is required. - /// - bool IsRequired { get; } + /// + /// Gets whether this input component is required. + /// + bool IsRequired { get; } - /// - /// Get the assigned to this input. - /// - ModalComponentTypeConverter TypeConverter { get; } - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - IInputComponentBuilder WithCustomId(string customId); + /// + /// Get the assigned to this input. + /// + ModalComponentTypeConverter TypeConverter { get; } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + IInputComponentBuilder WithCustomId(string customId); - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - IInputComponentBuilder WithLabel(string label); + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + IInputComponentBuilder WithLabel(string label); - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - IInputComponentBuilder WithDescription(string description); + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + IInputComponentBuilder WithDescription(string description); - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - IInputComponentBuilder SetIsRequired(bool isRequired); + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + IInputComponentBuilder SetIsRequired(bool isRequired); + } } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs index ac50487499..000df98ce5 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs @@ -1,117 +1,118 @@ using System; using System.Collections.Generic; -namespace Discord.Interactions.Builders; - -/// -/// Represents the base builder class for creating . -/// -/// The this builder yields when built. -/// Inherited type. -public abstract class InputComponentBuilder : ModalComponentBuilder, IInputComponentBuilder - where TInfo : InputComponentInfo - where TBuilder : InputComponentBuilder +namespace Discord.Interactions.Builders { - private readonly List _attributes; - - /// - public string CustomId { get; set; } - - /// - public string Label { get; set; } - - /// - public string Description { get; set; } - - /// - public bool IsRequired { get; set; } = true; - - /// - public ModalComponentTypeConverter TypeConverter { get; private set; } - - /// - /// Creates an instance of - /// - /// Parent modal of this input component. - internal InputComponentBuilder(ModalBuilder modal) : base(modal) - { - _attributes = new(); - } - /// - /// Sets . + /// Represents the base builder class for creating . /// - /// New value of the . - /// - /// The builder instance. - /// - public TBuilder WithCustomId(string customId) + /// The this builder yields when built. + /// Inherited type. + public abstract class InputComponentBuilder : ModalComponentBuilder, IInputComponentBuilder + where TInfo : InputComponentInfo + where TBuilder : InputComponentBuilder { - CustomId = customId; - return Instance; + private readonly List _attributes; + + /// + public string CustomId { get; set; } + + /// + public string Label { get; set; } + + /// + public string Description { get; set; } + + /// + public bool IsRequired { get; set; } = true; + + /// + public ModalComponentTypeConverter TypeConverter { get; private set; } + + /// + /// Creates an instance of + /// + /// Parent modal of this input component. + internal InputComponentBuilder(ModalBuilder modal) : base(modal) + { + _attributes = new(); + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TBuilder WithCustomId(string customId) + { + CustomId = customId; + return Instance; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TBuilder WithLabel(string label) + { + Label = label; + return Instance; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TBuilder WithDescription(string description) + { + Description = description; + return Instance; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TBuilder SetIsRequired(bool isRequired) + { + IsRequired = isRequired; + return Instance; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public override TBuilder WithType(Type type) + { + TypeConverter = Modal._interactionService.GetModalInputTypeConverter(type); + return base.WithType(type); + } + + /// + IInputComponentBuilder IInputComponentBuilder.WithCustomId(string customId) => WithCustomId(customId); + + /// + IInputComponentBuilder IInputComponentBuilder.WithLabel(string label) => WithLabel(label); + + /// + IInputComponentBuilder IInputComponentBuilder.WithDescription(string description) => WithDescription(description); + + /// + IInputComponentBuilder IInputComponentBuilder.SetIsRequired(bool isRequired) => SetIsRequired(isRequired); } - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TBuilder WithLabel(string label) - { - Label = label; - return Instance; - } - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TBuilder WithDescription(string description) - { - Description = description; - return Instance; - } - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TBuilder SetIsRequired(bool isRequired) - { - IsRequired = isRequired; - return Instance; - } - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public override TBuilder WithType(Type type) - { - TypeConverter = Modal._interactionService.GetModalInputTypeConverter(type); - return base.WithType(type); - } - - /// - IInputComponentBuilder IInputComponentBuilder.WithCustomId(string customId) => WithCustomId(customId); - - /// - IInputComponentBuilder IInputComponentBuilder.WithLabel(string label) => WithLabel(label); - - /// - IInputComponentBuilder IInputComponentBuilder.WithDescription(string description) => WithDescription(description); - - /// - IInputComponentBuilder IInputComponentBuilder.SetIsRequired(bool isRequired) => SetIsRequired(isRequired); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs index 844e2e4b50..728b97a7ac 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs @@ -1,108 +1,109 @@ -namespace Discord.Interactions.Builders; - -/// -/// Represents a builder for creating . -/// -public class TextInputComponentBuilder : InputComponentBuilder +namespace Discord.Interactions.Builders { - protected override TextInputComponentBuilder Instance => this; - /// - /// Gets and sets the style of the text input. + /// Represents a builder for creating . /// - public TextInputStyle Style { get; set; } + public class TextInputComponentBuilder : InputComponentBuilder + { + protected override TextInputComponentBuilder Instance => this; - /// - /// Gets and sets the placeholder of the text input. - /// - public string Placeholder { get; set; } + /// + /// Gets and sets the style of the text input. + /// + public TextInputStyle Style { get; set; } - /// - /// Gets and sets the minimum length of the text input. - /// - public int MinLength { get; set; } + /// + /// Gets and sets the placeholder of the text input. + /// + public string Placeholder { get; set; } - /// - /// Gets and sets the maximum length of the text input. - /// - public int MaxLength { get; set; } + /// + /// Gets and sets the minimum length of the text input. + /// + public int MinLength { get; set; } - /// - /// Gets and sets the initial value to be displayed by this input. - /// - public string InitialValue { get; set; } + /// + /// Gets and sets the maximum length of the text input. + /// + public int MaxLength { get; set; } - /// - /// Initializes a new . - /// - /// Parent modal of this component. - public TextInputComponentBuilder(ModalBuilder modal) : base(modal) { } + /// + /// Gets and sets the initial value to be displayed by this input. + /// + public string InitialValue { get; set; } - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TextInputComponentBuilder WithStyle(TextInputStyle style) - { - Style = style; - return this; - } + /// + /// Initializes a new . + /// + /// Parent modal of this component. + public TextInputComponentBuilder(ModalBuilder modal) : base(modal) { } - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TextInputComponentBuilder WithPlaceholder(string placeholder) - { - Placeholder = placeholder; - return this; - } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TextInputComponentBuilder WithStyle(TextInputStyle style) + { + Style = style; + return this; + } - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TextInputComponentBuilder WithMinLength(int minLength) - { - MinLength = minLength; - return this; - } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TextInputComponentBuilder WithPlaceholder(string placeholder) + { + Placeholder = placeholder; + return this; + } - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TextInputComponentBuilder WithMaxLength(int maxLength) - { - MaxLength = maxLength; - return this; - } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TextInputComponentBuilder WithMinLength(int minLength) + { + MinLength = minLength; + return this; + } - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public TextInputComponentBuilder WithInitialValue(string value) - { - InitialValue = value; - return this; - } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TextInputComponentBuilder WithMaxLength(int maxLength) + { + MaxLength = maxLength; + return this; + } - internal override TextInputComponentInfo Build(ModalInfo modal) => - new(this, modal); + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TextInputComponentBuilder WithInitialValue(string value) + { + InitialValue = value; + return this; + } + + internal override TextInputComponentInfo Build(ModalInfo modal) => + new(this, modal); + } } diff --git a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs index 7271ef9d89..a6f8599264 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs @@ -1,189 +1,189 @@ using System; using System.Collections.Generic; -using System.Linq; -namespace Discord.Interactions.Builders; - -/// -/// Represents a builder for creating . -/// -public class ModalBuilder +namespace Discord.Interactions.Builders { - internal readonly InteractionService _interactionService; - internal readonly List _components; - - /// - /// Gets the initialization delegate for this modal. - /// - public ModalInitializer ModalInitializer { get; internal set; } - /// - /// Gets the title of this modal. + /// Represents a builder for creating . /// - public string Title { get; set; } - - /// - /// Gets the implementation used to initialize this object. - /// - public Type Type { get; } - - /// - /// Gets a collection of the components of this modal. - /// - public IReadOnlyCollection Components => _components.AsReadOnly(); - - internal ModalBuilder(Type type, InteractionService interactionService) + public class ModalBuilder { - if (!typeof(IModal).IsAssignableFrom(type)) - throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); - - Type = type; - - _interactionService = interactionService; - _components = new(); + internal readonly InteractionService _interactionService; + internal readonly List _components; + + /// + /// Gets the initialization delegate for this modal. + /// + public ModalInitializer ModalInitializer { get; internal set; } + + /// + /// Gets the title of this modal. + /// + public string Title { get; set; } + + /// + /// Gets the implementation used to initialize this object. + /// + public Type Type { get; } + + /// + /// Gets a collection of the components of this modal. + /// + public IReadOnlyCollection Components => _components.AsReadOnly(); + + internal ModalBuilder(Type type, InteractionService interactionService) + { + if (!typeof(IModal).IsAssignableFrom(type)) + throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); + + Type = type; + + _interactionService = interactionService; + _components = new(); + } + + /// + /// Initializes a new + /// + /// The initialization delegate for this modal. + public ModalBuilder(Type type, ModalInitializer modalInitializer, InteractionService interactionService) : this(type, interactionService) + { + ModalInitializer = modalInitializer; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public ModalBuilder WithTitle(string title) + { + Title = title; + return this; + } + + /// + /// Adds text components to . + /// + /// Text Component builder factory. + /// + /// The builder instance. + /// + public ModalBuilder AddTextInputComponent(Action configure) + { + var builder = new TextInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + + /// + /// Adds a select menu component to . + /// + /// Select menu component builder factory. + /// + /// The builder instance. + /// + public ModalBuilder AddSelectMenuInputComponent(Action configure) + { + var builder = new SelectMenuInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + + /// + /// Adds a user select component to . + /// + /// User select component builder factory. + /// + /// The builder instance. + /// + public ModalBuilder AddUserSelectInputComponent(Action configure) + { + var builder = new UserSelectInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + + /// + /// Adds a role select component to . + /// + /// Role select component builder factory. + /// + /// The builder instance. + /// + public ModalBuilder AddRoleSelectInputComponent(Action configure) + { + var builder = new RoleSelectInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + + /// + /// Adds a mentionable select component to . + /// + /// Mentionable select component builder factory. + /// + /// The builder instance. + /// + public ModalBuilder AddMentionableSelectInputComponent(Action configure) + { + var builder = new MentionableSelectInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + + /// + /// Adds a channel select component to . + /// + /// Channel select component builder factory. + /// + /// The builder instance. + /// + public ModalBuilder AddChannelSelectInputComponent(Action configure) + { + var builder = new ChannelSelectInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + + /// + /// Adds a file upload component to . + /// + /// File upload component builder factory. + /// + /// The builder instance. + /// + public ModalBuilder AddFileUploadInputComponent(Action configure) + { + var builder = new FileUploadInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + + /// + /// Adds a text display component to . + /// + /// Text display component builder factory. + /// + /// The builder instance. + /// + public ModalBuilder AddTextDisplayComponent(Action configure) + { + var builder = new TextDisplayComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + + internal ModalInfo Build() => new(this); } - - /// - /// Initializes a new - /// - /// The initialization delegate for this modal. - public ModalBuilder(Type type, ModalInitializer modalInitializer, InteractionService interactionService) : this(type, interactionService) - { - ModalInitializer = modalInitializer; - } - - /// - /// Sets . - /// - /// New value of the . - /// - /// The builder instance. - /// - public ModalBuilder WithTitle(string title) - { - Title = title; - return this; - } - - /// - /// Adds text components to . - /// - /// Text Component builder factory. - /// - /// The builder instance. - /// - public ModalBuilder AddTextInputComponent(Action configure) - { - var builder = new TextInputComponentBuilder(this); - configure(builder); - _components.Add(builder); - return this; - } - - /// - /// Adds a select menu component to . - /// - /// Select menu component builder factory. - /// - /// The builder instance. - /// - public ModalBuilder AddSelectMenuInputComponent(Action configure) - { - var builder = new SelectMenuInputComponentBuilder(this); - configure(builder); - _components.Add(builder); - return this; - } - - /// - /// Adds a user select component to . - /// - /// User select component builder factory. - /// - /// The builder instance. - /// - public ModalBuilder AddUserSelectInputComponent(Action configure) - { - var builder = new UserSelectInputComponentBuilder(this); - configure(builder); - _components.Add(builder); - return this; - } - - /// - /// Adds a role select component to . - /// - /// Role select component builder factory. - /// - /// The builder instance. - /// - public ModalBuilder AddRoleSelectInputComponent(Action configure) - { - var builder = new RoleSelectInputComponentBuilder(this); - configure(builder); - _components.Add(builder); - return this; - } - - /// - /// Adds a mentionable select component to . - /// - /// Mentionable select component builder factory. - /// - /// The builder instance. - /// - public ModalBuilder AddMentionableSelectInputComponent(Action configure) - { - var builder = new MentionableSelectInputComponentBuilder(this); - configure(builder); - _components.Add(builder); - return this; - } - - /// - /// Adds a channel select component to . - /// - /// Channel select component builder factory. - /// - /// The builder instance. - /// - public ModalBuilder AddChannelSelectInputComponent(Action configure) - { - var builder = new ChannelSelectInputComponentBuilder(this); - configure(builder); - _components.Add(builder); - return this; - } - - /// - /// Adds a file upload component to . - /// - /// File upload component builder factory. - /// - /// The builder instance. - /// - public ModalBuilder AddFileUploadInputComponent(Action configure) - { - var builder = new FileUploadInputComponentBuilder(this); - configure(builder); - _components.Add(builder); - return this; - } - - /// - /// Adds a text display component to . - /// - /// Text display component builder factory. - /// - /// The builder instance. - /// - public ModalBuilder AddTextDisplayComponent(Action configure) - { - var builder = new TextDisplayComponentBuilder(this); - configure(builder); - _components.Add(builder); - return this; - } - - internal ModalInfo Build() => new(this); } diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index 38d926bb29..483977f00f 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -6,915 +6,916 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace Discord.Interactions.Builders; - -internal static class ModuleClassBuilder +namespace Discord.Interactions.Builders { - private static readonly TypeInfo ModuleTypeInfo = typeof(IInteractionModuleBase).GetTypeInfo(); + internal static class ModuleClassBuilder + { + private static readonly TypeInfo ModuleTypeInfo = typeof(IInteractionModuleBase).GetTypeInfo(); - public const int MaxCommandDepth = 3; + public const int MaxCommandDepth = 3; - public static async Task> SearchAsync(Assembly assembly, InteractionService commandService) - { - static bool IsLoadableModule(TypeInfo info) => - !info.IsAbstract && - info.DeclaredMethods - .SelectMany(x => x.GetCustomAttributes()) - .Any(x => x is SlashCommandAttribute - or ComponentInteractionAttribute - or ContextCommandAttribute - or AutocompleteCommandAttribute - or ModalInteractionAttribute); - - var result = new List(); - - foreach (var type in assembly.DefinedTypes) + public static async Task> SearchAsync(Assembly assembly, InteractionService commandService) { - if ((type.IsPublic || type.IsNestedPublic) && IsValidModuleDefinition(type)) + static bool IsLoadableModule(TypeInfo info) => + !info.IsAbstract && + info.DeclaredMethods + .SelectMany(x => x.GetCustomAttributes()) + .Any(x => x is SlashCommandAttribute + or ComponentInteractionAttribute + or ContextCommandAttribute + or AutocompleteCommandAttribute + or ModalInteractionAttribute); + + var result = new List(); + + foreach (var type in assembly.DefinedTypes) { - result.Add(type); - } - else if (IsLoadableModule(type)) - { - await commandService._cmdLogger.WarningAsync($"Class {type.FullName} is not public and cannot be loaded.").ConfigureAwait(false); + if ((type.IsPublic || type.IsNestedPublic) && IsValidModuleDefinition(type)) + { + result.Add(type); + } + else if (IsLoadableModule(type)) + { + await commandService._cmdLogger.WarningAsync($"Class {type.FullName} is not public and cannot be loaded.").ConfigureAwait(false); + } } + return result; } - return result; - } - - public static async Task> BuildAsync(IEnumerable validTypes, InteractionService commandService, - IServiceProvider services) - { - var topLevelGroups = validTypes.Where(x => x.DeclaringType == null || !IsValidModuleDefinition(x.DeclaringType.GetTypeInfo())); - var built = new List(); - var result = new Dictionary(); - - foreach (var type in topLevelGroups) + public static async Task> BuildAsync(IEnumerable validTypes, InteractionService commandService, + IServiceProvider services) { - var builder = new ModuleBuilder(commandService); + var topLevelGroups = validTypes.Where(x => x.DeclaringType == null || !IsValidModuleDefinition(x.DeclaringType.GetTypeInfo())); + var built = new List(); - BuildModule(builder, type, commandService, services); - BuildSubModules(builder, type.DeclaredNestedTypes, built, commandService, services); - built.Add(type); + var result = new Dictionary(); - var moduleInfo = builder.Build(commandService, services); + foreach (var type in topLevelGroups) + { + var builder = new ModuleBuilder(commandService); - result.Add(type.AsType(), moduleInfo); - } + BuildModule(builder, type, commandService, services); + BuildSubModules(builder, type.DeclaredNestedTypes, built, commandService, services); + built.Add(type); - await commandService._cmdLogger.DebugAsync($"Successfully built {built.Count} interaction modules.").ConfigureAwait(false); + var moduleInfo = builder.Build(commandService, services); - return result; - } + result.Add(type.AsType(), moduleInfo); + } - private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, InteractionService commandService, - IServiceProvider services) - { - var attributes = typeInfo.GetCustomAttributes(); + await commandService._cmdLogger.DebugAsync($"Successfully built {built.Count} interaction modules.").ConfigureAwait(false); - builder.Name = typeInfo.Name; - builder.TypeInfo = typeInfo; + return result; + } - foreach (var attribute in attributes) + private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, InteractionService commandService, + IServiceProvider services) { - switch (attribute) + var attributes = typeInfo.GetCustomAttributes(); + + builder.Name = typeInfo.Name; + builder.TypeInfo = typeInfo; + + foreach (var attribute in attributes) { - case GroupAttribute group: - { - builder.SlashGroupName = group.Name; - builder.Description = group.Description; - } - break; + switch (attribute) + { + case GroupAttribute group: + { + builder.SlashGroupName = group.Name; + builder.Description = group.Description; + } + break; #pragma warning disable CS0618 // Type or member is obsolete - case DefaultPermissionAttribute defPermission: - { - builder.DefaultPermission = defPermission.IsDefaultPermission; - } - break; + case DefaultPermissionAttribute defPermission: + { + builder.DefaultPermission = defPermission.IsDefaultPermission; + } + break; #pragma warning restore CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete - case EnabledInDmAttribute enabledInDm: - { - builder.IsEnabledInDm = enabledInDm.IsEnabled; - } - break; + case EnabledInDmAttribute enabledInDm: + { + builder.IsEnabledInDm = enabledInDm.IsEnabled; + } + break; #pragma warning restore CS0618 // Type or member is obsolete - case DefaultMemberPermissionsAttribute memberPermission: - { - builder.DefaultMemberPermissions = memberPermission.Permissions; - } - break; - case PreconditionAttribute precondition: - builder.AddPreconditions(precondition); - break; - case DontAutoRegisterAttribute dontAutoRegister: - builder.DontAutoRegister = true; - break; - case NsfwCommandAttribute nsfwCommand: - builder.SetNsfw(nsfwCommand.IsNsfw); - break; - case CommandContextTypeAttribute contextType: - builder.WithContextTypes(contextType.ContextTypes?.ToArray()); - break; - case IntegrationTypeAttribute integrationType: - builder.WithIntegrationTypes(integrationType.IntegrationTypes?.ToArray()); - break; - default: - builder.AddAttributes(attribute); - break; + case DefaultMemberPermissionsAttribute memberPermission: + { + builder.DefaultMemberPermissions = memberPermission.Permissions; + } + break; + case PreconditionAttribute precondition: + builder.AddPreconditions(precondition); + break; + case DontAutoRegisterAttribute dontAutoRegister: + builder.DontAutoRegister = true; + break; + case NsfwCommandAttribute nsfwCommand: + builder.SetNsfw(nsfwCommand.IsNsfw); + break; + case CommandContextTypeAttribute contextType: + builder.WithContextTypes(contextType.ContextTypes?.ToArray()); + break; + case IntegrationTypeAttribute integrationType: + builder.WithIntegrationTypes(integrationType.IntegrationTypes?.ToArray()); + break; + default: + builder.AddAttributes(attribute); + break; + } } - } - var methods = typeInfo.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + var methods = typeInfo.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - var validSlashCommands = methods.Where(IsValidSlashCommandDefinition); - var validContextCommands = methods.Where(IsValidContextCommandDefinition); - var validInteractions = methods.Where(IsValidComponentCommandDefinition); - var validAutocompleteCommands = methods.Where(IsValidAutocompleteCommandDefinition); - var validModalCommands = methods.Where(IsValidModalCommandDefinition); + var validSlashCommands = methods.Where(IsValidSlashCommandDefinition); + var validContextCommands = methods.Where(IsValidContextCommandDefinition); + var validInteractions = methods.Where(IsValidComponentCommandDefinition); + var validAutocompleteCommands = methods.Where(IsValidAutocompleteCommandDefinition); + var validModalCommands = methods.Where(IsValidModalCommandDefinition); - Func createInstance = commandService._useCompiledLambda ? - ReflectionUtils.CreateLambdaBuilder(typeInfo, commandService) : ReflectionUtils.CreateBuilder(typeInfo, commandService); + Func createInstance = commandService._useCompiledLambda ? + ReflectionUtils.CreateLambdaBuilder(typeInfo, commandService) : ReflectionUtils.CreateBuilder(typeInfo, commandService); - foreach (var method in validSlashCommands) - builder.AddSlashCommand(x => BuildSlashCommand(x, createInstance, method, commandService, services)); + foreach (var method in validSlashCommands) + builder.AddSlashCommand(x => BuildSlashCommand(x, createInstance, method, commandService, services)); - foreach (var method in validContextCommands) - builder.AddContextCommand(x => BuildContextCommand(x, createInstance, method, commandService, services)); + foreach (var method in validContextCommands) + builder.AddContextCommand(x => BuildContextCommand(x, createInstance, method, commandService, services)); - foreach (var method in validInteractions) - builder.AddComponentCommand(x => BuildComponentCommand(x, createInstance, method, commandService, services)); + foreach (var method in validInteractions) + builder.AddComponentCommand(x => BuildComponentCommand(x, createInstance, method, commandService, services)); - foreach (var method in validAutocompleteCommands) - builder.AddAutocompleteCommand(x => BuildAutocompleteCommand(x, createInstance, method, commandService, services)); + foreach (var method in validAutocompleteCommands) + builder.AddAutocompleteCommand(x => BuildAutocompleteCommand(x, createInstance, method, commandService, services)); - foreach (var method in validModalCommands) - builder.AddModalCommand(x => BuildModalCommand(x, createInstance, method, commandService, services)); - } + foreach (var method in validModalCommands) + builder.AddModalCommand(x => BuildModalCommand(x, createInstance, method, commandService, services)); + } - private static void BuildSubModules(ModuleBuilder parent, IEnumerable subModules, IList builtTypes, InteractionService commandService, - IServiceProvider services, int slashGroupDepth = 0) - { - foreach (var submodule in subModules.Where(IsValidModuleDefinition)) + private static void BuildSubModules(ModuleBuilder parent, IEnumerable subModules, IList builtTypes, InteractionService commandService, + IServiceProvider services, int slashGroupDepth = 0) { - if (builtTypes.Contains(submodule)) - continue; - - parent.AddModule((builder) => + foreach (var submodule in subModules.Where(IsValidModuleDefinition)) { - BuildModule(builder, submodule, commandService, services); + if (builtTypes.Contains(submodule)) + continue; - if (slashGroupDepth >= MaxCommandDepth - 1) - throw new InvalidOperationException($"Slash Commands only support {MaxCommandDepth - 1} command prefixes for sub-commands"); + parent.AddModule((builder) => + { + BuildModule(builder, submodule, commandService, services); + + if (slashGroupDepth >= MaxCommandDepth - 1) + throw new InvalidOperationException($"Slash Commands only support {MaxCommandDepth - 1} command prefixes for sub-commands"); - BuildSubModules(builder, submodule.DeclaredNestedTypes, builtTypes, commandService, services, builder.IsSlashGroup ? slashGroupDepth + 1 : slashGroupDepth); - }); - builtTypes.Add(submodule); + BuildSubModules(builder, submodule.DeclaredNestedTypes, builtTypes, commandService, services, builder.IsSlashGroup ? slashGroupDepth + 1 : slashGroupDepth); + }); + builtTypes.Add(submodule); + } } - } - private static void BuildSlashCommand(SlashCommandBuilder builder, Func createInstance, MethodInfo methodInfo, - InteractionService commandService, IServiceProvider services) - { - var attributes = methodInfo.GetCustomAttributes(); + private static void BuildSlashCommand(SlashCommandBuilder builder, Func createInstance, MethodInfo methodInfo, + InteractionService commandService, IServiceProvider services) + { + var attributes = methodInfo.GetCustomAttributes(); - builder.MethodName = methodInfo.Name; + builder.MethodName = methodInfo.Name; - foreach (var attribute in attributes) - { - switch (attribute) + foreach (var attribute in attributes) { - case SlashCommandAttribute command: - { - builder.Name = command.Name; - builder.Description = command.Description; - builder.IgnoreGroupNames = command.IgnoreGroupNames; - builder.RunMode = command.RunMode; - } - break; + switch (attribute) + { + case SlashCommandAttribute command: + { + builder.Name = command.Name; + builder.Description = command.Description; + builder.IgnoreGroupNames = command.IgnoreGroupNames; + builder.RunMode = command.RunMode; + } + break; #pragma warning disable CS0618 // Type or member is obsolete - case DefaultPermissionAttribute defaultPermission: - { - builder.DefaultPermission = defaultPermission.IsDefaultPermission; - } - break; - case EnabledInDmAttribute enabledInDm: - { - builder.IsEnabledInDm = enabledInDm.IsEnabled; - } - break; + case DefaultPermissionAttribute defaultPermission: + { + builder.DefaultPermission = defaultPermission.IsDefaultPermission; + } + break; + case EnabledInDmAttribute enabledInDm: + { + builder.IsEnabledInDm = enabledInDm.IsEnabled; + } + break; #pragma warning restore CS0618 // Type or member is obsolete - case DefaultMemberPermissionsAttribute memberPermission: - { - builder.DefaultMemberPermissions = memberPermission.Permissions; - } - break; - case PreconditionAttribute precondition: - builder.WithPreconditions(precondition); - break; - case NsfwCommandAttribute nsfwCommand: - builder.SetNsfw(nsfwCommand.IsNsfw); - break; - case CommandContextTypeAttribute contextType: - builder.WithContextTypes(contextType.ContextTypes.ToArray()); - break; - case IntegrationTypeAttribute integrationType: - builder.WithIntegrationTypes(integrationType.IntegrationTypes.ToArray()); - break; - default: - builder.WithAttributes(attribute); - break; + case DefaultMemberPermissionsAttribute memberPermission: + { + builder.DefaultMemberPermissions = memberPermission.Permissions; + } + break; + case PreconditionAttribute precondition: + builder.WithPreconditions(precondition); + break; + case NsfwCommandAttribute nsfwCommand: + builder.SetNsfw(nsfwCommand.IsNsfw); + break; + case CommandContextTypeAttribute contextType: + builder.WithContextTypes(contextType.ContextTypes.ToArray()); + break; + case IntegrationTypeAttribute integrationType: + builder.WithIntegrationTypes(integrationType.IntegrationTypes.ToArray()); + break; + default: + builder.WithAttributes(attribute); + break; + } } - } - var parameters = methodInfo.GetParameters(); + var parameters = methodInfo.GetParameters(); - foreach (var parameter in parameters) - builder.AddParameter(x => BuildSlashParameter(x, parameter, services)); + foreach (var parameter in parameters) + builder.AddParameter(x => BuildSlashParameter(x, parameter, services)); - builder.Callback = CreateCallback(createInstance, methodInfo, commandService); - } + builder.Callback = CreateCallback(createInstance, methodInfo, commandService); + } - private static void BuildContextCommand(ContextCommandBuilder builder, Func createInstance, MethodInfo methodInfo, - InteractionService commandService, IServiceProvider services) - { - var attributes = methodInfo.GetCustomAttributes(); + private static void BuildContextCommand(ContextCommandBuilder builder, Func createInstance, MethodInfo methodInfo, + InteractionService commandService, IServiceProvider services) + { + var attributes = methodInfo.GetCustomAttributes(); - builder.MethodName = methodInfo.Name; + builder.MethodName = methodInfo.Name; - foreach (var attribute in attributes) - { - switch (attribute) + foreach (var attribute in attributes) { - case ContextCommandAttribute command: - { - builder.Name = command.Name; - builder.CommandType = command.CommandType; - builder.RunMode = command.RunMode; + switch (attribute) + { + case ContextCommandAttribute command: + { + builder.Name = command.Name; + builder.CommandType = command.CommandType; + builder.RunMode = command.RunMode; - command.CheckMethodDefinition(methodInfo); - } - break; + command.CheckMethodDefinition(methodInfo); + } + break; #pragma warning disable CS0618 // Type or member is obsolete - case DefaultPermissionAttribute defaultPermission: - { - builder.DefaultPermission = defaultPermission.IsDefaultPermission; - } - break; - case EnabledInDmAttribute enabledInDm: - { - builder.IsEnabledInDm = enabledInDm.IsEnabled; - } - break; + case DefaultPermissionAttribute defaultPermission: + { + builder.DefaultPermission = defaultPermission.IsDefaultPermission; + } + break; + case EnabledInDmAttribute enabledInDm: + { + builder.IsEnabledInDm = enabledInDm.IsEnabled; + } + break; #pragma warning restore CS0618 // Type or member is obsolete - case DefaultMemberPermissionsAttribute memberPermission: - { - builder.DefaultMemberPermissions = memberPermission.Permissions; - } - break; - case PreconditionAttribute precondition: - builder.WithPreconditions(precondition); - break; - case NsfwCommandAttribute nsfwCommand: - builder.SetNsfw(nsfwCommand.IsNsfw); - break; - case CommandContextTypeAttribute contextType: - builder.WithContextTypes(contextType.ContextTypes.ToArray()); - break; - case IntegrationTypeAttribute integrationType: - builder.WithIntegrationTypes(integrationType.IntegrationTypes.ToArray()); - break; - default: - builder.WithAttributes(attribute); - break; + case DefaultMemberPermissionsAttribute memberPermission: + { + builder.DefaultMemberPermissions = memberPermission.Permissions; + } + break; + case PreconditionAttribute precondition: + builder.WithPreconditions(precondition); + break; + case NsfwCommandAttribute nsfwCommand: + builder.SetNsfw(nsfwCommand.IsNsfw); + break; + case CommandContextTypeAttribute contextType: + builder.WithContextTypes(contextType.ContextTypes.ToArray()); + break; + case IntegrationTypeAttribute integrationType: + builder.WithIntegrationTypes(integrationType.IntegrationTypes.ToArray()); + break; + default: + builder.WithAttributes(attribute); + break; + } } - } - var parameters = methodInfo.GetParameters(); + var parameters = methodInfo.GetParameters(); - foreach (var parameter in parameters) - builder.AddParameter(x => BuildParameter(x, parameter)); + foreach (var parameter in parameters) + builder.AddParameter(x => BuildParameter(x, parameter)); - builder.Callback = CreateCallback(createInstance, methodInfo, commandService); - } + builder.Callback = CreateCallback(createInstance, methodInfo, commandService); + } - private static void BuildComponentCommand(ComponentCommandBuilder builder, Func createInstance, MethodInfo methodInfo, - InteractionService commandService, IServiceProvider services) - { - var attributes = methodInfo.GetCustomAttributes(); + private static void BuildComponentCommand(ComponentCommandBuilder builder, Func createInstance, MethodInfo methodInfo, + InteractionService commandService, IServiceProvider services) + { + var attributes = methodInfo.GetCustomAttributes(); - builder.MethodName = methodInfo.Name; + builder.MethodName = methodInfo.Name; - foreach (var attribute in attributes) - { - switch (attribute) + foreach (var attribute in attributes) { - case ComponentInteractionAttribute interaction: - { - builder.Name = interaction.CustomId; - builder.RunMode = interaction.RunMode; - builder.IgnoreGroupNames = interaction.IgnoreGroupNames; - builder.TreatNameAsRegex = interaction.TreatAsRegex; - } - break; - case PreconditionAttribute precondition: - builder.WithPreconditions(precondition); - break; - default: - builder.WithAttributes(attribute); - break; + switch (attribute) + { + case ComponentInteractionAttribute interaction: + { + builder.Name = interaction.CustomId; + builder.RunMode = interaction.RunMode; + builder.IgnoreGroupNames = interaction.IgnoreGroupNames; + builder.TreatNameAsRegex = interaction.TreatAsRegex; + } + break; + case PreconditionAttribute precondition: + builder.WithPreconditions(precondition); + break; + default: + builder.WithAttributes(attribute); + break; + } } - } - var parameters = methodInfo.GetParameters(); + var parameters = methodInfo.GetParameters(); - var wildCardCount = RegexUtils.GetWildCardCount(builder.Name, commandService._wildCardExp); + var wildCardCount = RegexUtils.GetWildCardCount(builder.Name, commandService._wildCardExp); - foreach (var parameter in parameters) - builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount)); + foreach (var parameter in parameters) + builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount)); - builder.Callback = CreateCallback(createInstance, methodInfo, commandService); - } + builder.Callback = CreateCallback(createInstance, methodInfo, commandService); + } - private static void BuildAutocompleteCommand(AutocompleteCommandBuilder builder, Func createInstance, MethodInfo methodInfo, - InteractionService commandService, IServiceProvider services) - { - var attributes = methodInfo.GetCustomAttributes(); + private static void BuildAutocompleteCommand(AutocompleteCommandBuilder builder, Func createInstance, MethodInfo methodInfo, + InteractionService commandService, IServiceProvider services) + { + var attributes = methodInfo.GetCustomAttributes(); - builder.MethodName = methodInfo.Name; + builder.MethodName = methodInfo.Name; - foreach (var attribute in attributes) - { - switch (attribute) + foreach (var attribute in attributes) { - case AutocompleteCommandAttribute autocomplete: - { - builder.ParameterName = autocomplete.ParameterName; - builder.CommandName = autocomplete.CommandName; - builder.Name = autocomplete.CommandName + " " + autocomplete.ParameterName; - builder.RunMode = autocomplete.RunMode; - } - break; - case PreconditionAttribute precondition: - builder.WithPreconditions(precondition); - break; - default: - builder.WithAttributes(attribute); - break; + switch (attribute) + { + case AutocompleteCommandAttribute autocomplete: + { + builder.ParameterName = autocomplete.ParameterName; + builder.CommandName = autocomplete.CommandName; + builder.Name = autocomplete.CommandName + " " + autocomplete.ParameterName; + builder.RunMode = autocomplete.RunMode; + } + break; + case PreconditionAttribute precondition: + builder.WithPreconditions(precondition); + break; + default: + builder.WithAttributes(attribute); + break; + } } - } - var parameters = methodInfo.GetParameters(); + var parameters = methodInfo.GetParameters(); - foreach (var parameter in parameters) - builder.AddParameter(x => BuildParameter(x, parameter)); + foreach (var parameter in parameters) + builder.AddParameter(x => BuildParameter(x, parameter)); - builder.Callback = CreateCallback(createInstance, methodInfo, commandService); - } + builder.Callback = CreateCallback(createInstance, methodInfo, commandService); + } - private static void BuildModalCommand(ModalCommandBuilder builder, Func createInstance, MethodInfo methodInfo, - InteractionService commandService, IServiceProvider services) - { - var parameters = methodInfo.GetParameters(); + private static void BuildModalCommand(ModalCommandBuilder builder, Func createInstance, MethodInfo methodInfo, + InteractionService commandService, IServiceProvider services) + { + var parameters = methodInfo.GetParameters(); - if (parameters.Count(x => typeof(IModal).IsAssignableFrom(x.ParameterType)) > 1) - throw new InvalidOperationException($"A modal command can only have one {nameof(IModal)} parameter."); + if (parameters.Count(x => typeof(IModal).IsAssignableFrom(x.ParameterType)) > 1) + throw new InvalidOperationException($"A modal command can only have one {nameof(IModal)} parameter."); - if (!typeof(IModal).IsAssignableFrom(parameters.Last().ParameterType)) - throw new InvalidOperationException($"Last parameter of a modal command must be an implementation of {nameof(IModal)}"); + if (!typeof(IModal).IsAssignableFrom(parameters.Last().ParameterType)) + throw new InvalidOperationException($"Last parameter of a modal command must be an implementation of {nameof(IModal)}"); - var attributes = methodInfo.GetCustomAttributes(); + var attributes = methodInfo.GetCustomAttributes(); - builder.MethodName = methodInfo.Name; + builder.MethodName = methodInfo.Name; - foreach (var attribute in attributes) - { - switch (attribute) + foreach (var attribute in attributes) { - case ModalInteractionAttribute modal: - { - builder.Name = modal.CustomId; - builder.RunMode = modal.RunMode; - builder.IgnoreGroupNames = modal.IgnoreGroupNames; - builder.TreatNameAsRegex = modal.TreatAsRegex; - } - break; - case PreconditionAttribute precondition: - builder.WithPreconditions(precondition); - break; - default: - builder.WithAttributes(attribute); - break; + switch (attribute) + { + case ModalInteractionAttribute modal: + { + builder.Name = modal.CustomId; + builder.RunMode = modal.RunMode; + builder.IgnoreGroupNames = modal.IgnoreGroupNames; + builder.TreatNameAsRegex = modal.TreatAsRegex; + } + break; + case PreconditionAttribute precondition: + builder.WithPreconditions(precondition); + break; + default: + builder.WithAttributes(attribute); + break; + } } - } - foreach (var parameter in parameters) - builder.AddParameter(x => BuildParameter(x, parameter)); + foreach (var parameter in parameters) + builder.AddParameter(x => BuildParameter(x, parameter)); - builder.Callback = CreateCallback(createInstance, methodInfo, commandService); - } - - private static ExecuteCallback CreateCallback(Func createInstance, - MethodInfo methodInfo, InteractionService commandService) - { - Func commandInvoker = commandService._useCompiledLambda ? - ReflectionUtils.CreateMethodInvoker(methodInfo) : (module, args) => methodInfo.Invoke(module, args) as Task; + builder.Callback = CreateCallback(createInstance, methodInfo, commandService); + } - async Task ExecuteCallback(IInteractionContext context, object[] args, IServiceProvider serviceProvider, ICommandInfo commandInfo) + private static ExecuteCallback CreateCallback(Func createInstance, + MethodInfo methodInfo, InteractionService commandService) { - var instance = createInstance(serviceProvider); - instance.SetContext(context); + Func commandInvoker = commandService._useCompiledLambda ? + ReflectionUtils.CreateMethodInvoker(methodInfo) : (module, args) => methodInfo.Invoke(module, args) as Task; - try + async Task ExecuteCallback(IInteractionContext context, object[] args, IServiceProvider serviceProvider, ICommandInfo commandInfo) { - await instance.BeforeExecuteAsync(commandInfo).ConfigureAwait(false); - instance.BeforeExecute(commandInfo); - var task = commandInvoker(instance, args) ?? Task.CompletedTask; + var instance = createInstance(serviceProvider); + instance.SetContext(context); + + try + { + await instance.BeforeExecuteAsync(commandInfo).ConfigureAwait(false); + instance.BeforeExecute(commandInfo); + var task = commandInvoker(instance, args) ?? Task.CompletedTask; + + if (task is Task runtimeTask) + { + return await runtimeTask.ConfigureAwait(false); + } + else + { + await task.ConfigureAwait(false); + return ExecuteResult.FromSuccess(); - if (task is Task runtimeTask) + } + } + catch (Exception ex) { - return await runtimeTask.ConfigureAwait(false); + var interactionException = new InteractionException(commandInfo, context, ex); + await commandService._cmdLogger.ErrorAsync(interactionException).ConfigureAwait(false); + return ExecuteResult.FromError(interactionException); } - else + finally { - await task.ConfigureAwait(false); - return ExecuteResult.FromSuccess(); - + await instance.AfterExecuteAsync(commandInfo).ConfigureAwait(false); + instance.AfterExecute(commandInfo); + (instance as IDisposable)?.Dispose(); } } - catch (Exception ex) - { - var interactionException = new InteractionException(commandInfo, context, ex); - await commandService._cmdLogger.ErrorAsync(interactionException).ConfigureAwait(false); - return ExecuteResult.FromError(interactionException); - } - finally - { - await instance.AfterExecuteAsync(commandInfo).ConfigureAwait(false); - instance.AfterExecute(commandInfo); - (instance as IDisposable)?.Dispose(); - } - } - return ExecuteCallback; - } + return ExecuteCallback; + } - #region Parameters - private static void BuildSlashParameter(SlashCommandParameterBuilder builder, ParameterInfo paramInfo, IServiceProvider services) - { - var attributes = paramInfo.GetCustomAttributes(); - var paramType = paramInfo.ParameterType; + #region Parameters + private static void BuildSlashParameter(SlashCommandParameterBuilder builder, ParameterInfo paramInfo, IServiceProvider services) + { + var attributes = paramInfo.GetCustomAttributes(); + var paramType = paramInfo.ParameterType; - builder.Name = paramInfo.Name; - builder.Description = paramInfo.Name; - builder.IsRequired = !paramInfo.IsOptional; - builder.DefaultValue = paramInfo.DefaultValue; + builder.Name = paramInfo.Name; + builder.Description = paramInfo.Name; + builder.IsRequired = !paramInfo.IsOptional; + builder.DefaultValue = paramInfo.DefaultValue; - var supportedNumericalRange = paramInfo.GetSupportedNumericalRange(); - builder.MinValue = supportedNumericalRange.Min; - builder.MaxValue = supportedNumericalRange.Max; + var supportedNumericalRange = paramInfo.GetSupportedNumericalRange(); + builder.MinValue = supportedNumericalRange.Min; + builder.MaxValue = supportedNumericalRange.Max; - foreach (var attribute in attributes) - { - switch (attribute) + foreach (var attribute in attributes) { - case SummaryAttribute description: - { - if (!string.IsNullOrEmpty(description.Name)) - builder.Name = description.Name; + switch (attribute) + { + case SummaryAttribute description: + { + if (!string.IsNullOrEmpty(description.Name)) + builder.Name = description.Name; - if (!string.IsNullOrEmpty(description.Description)) - builder.Description = description.Description; - } - break; - case ChoiceAttribute choice: - builder.WithChoices(new ParameterChoice(choice.Name, choice.Value)); - break; - case ParamArrayAttribute _: - builder.IsParameterArray = true; - break; - case ParameterPreconditionAttribute precondition: - builder.AddPreconditions(precondition); - break; - case ChannelTypesAttribute channelTypes: - builder.WithChannelTypes(channelTypes.ChannelTypes); - break; - case AutocompleteAttribute autocomplete: - builder.Autocomplete = true; - if (autocomplete.AutocompleteHandlerType is not null) - builder.WithAutocompleteHandler(autocomplete.AutocompleteHandlerType, services); - break; - case MaxValueAttribute maxValue: - if (maxValue.Value > supportedNumericalRange.Max) - throw new ArgumentOutOfRangeException($"{nameof(maxValue)} cannot be greater than {supportedNumericalRange.Max}."); - - builder.MaxValue = maxValue.Value; - break; - case MinValueAttribute minValue: - if (minValue.Value < supportedNumericalRange.Min) - throw new ArgumentOutOfRangeException($"{nameof(minValue)} cannot be less than {supportedNumericalRange.Min}."); - - builder.MinValue = minValue.Value; - break; - case MinLengthAttribute minLength: - builder.MinLength = minLength.Length; - break; - case MaxLengthAttribute maxLength: - builder.MaxLength = maxLength.Length; - break; - case ComplexParameterAttribute complexParameter: - { - builder.IsComplexParameter = true; - ConstructorInfo ctor = GetComplexParameterConstructor(paramInfo.ParameterType.GetTypeInfo(), complexParameter); + if (!string.IsNullOrEmpty(description.Description)) + builder.Description = description.Description; + } + break; + case ChoiceAttribute choice: + builder.WithChoices(new ParameterChoice(choice.Name, choice.Value)); + break; + case ParamArrayAttribute _: + builder.IsParameterArray = true; + break; + case ParameterPreconditionAttribute precondition: + builder.AddPreconditions(precondition); + break; + case ChannelTypesAttribute channelTypes: + builder.WithChannelTypes(channelTypes.ChannelTypes); + break; + case AutocompleteAttribute autocomplete: + builder.Autocomplete = true; + if (autocomplete.AutocompleteHandlerType is not null) + builder.WithAutocompleteHandler(autocomplete.AutocompleteHandlerType, services); + break; + case MaxValueAttribute maxValue: + if (maxValue.Value > supportedNumericalRange.Max) + throw new ArgumentOutOfRangeException($"{nameof(maxValue)} cannot be greater than {supportedNumericalRange.Max}."); - foreach (var parameter in ctor.GetParameters()) + builder.MaxValue = maxValue.Value; + break; + case MinValueAttribute minValue: + if (minValue.Value < supportedNumericalRange.Min) + throw new ArgumentOutOfRangeException($"{nameof(minValue)} cannot be less than {supportedNumericalRange.Min}."); + + builder.MinValue = minValue.Value; + break; + case MinLengthAttribute minLength: + builder.MinLength = minLength.Length; + break; + case MaxLengthAttribute maxLength: + builder.MaxLength = maxLength.Length; + break; + case ComplexParameterAttribute complexParameter: { - if (parameter.IsDefined(typeof(ComplexParameterAttribute))) - throw new InvalidOperationException("You cannot create nested complex parameters."); + builder.IsComplexParameter = true; + ConstructorInfo ctor = GetComplexParameterConstructor(paramInfo.ParameterType.GetTypeInfo(), complexParameter); - builder.AddComplexParameterField(fieldBuilder => BuildSlashParameter(fieldBuilder, parameter, services)); - } + foreach (var parameter in ctor.GetParameters()) + { + if (parameter.IsDefined(typeof(ComplexParameterAttribute))) + throw new InvalidOperationException("You cannot create nested complex parameters."); - var initializer = builder.Command.Module.InteractionService._useCompiledLambda ? - ReflectionUtils.CreateLambdaConstructorInvoker(paramInfo.ParameterType.GetTypeInfo()) : ctor.Invoke; - builder.ComplexParameterInitializer = args => initializer(args); - } - break; - default: - builder.AddAttributes(attribute); - break; + builder.AddComplexParameterField(fieldBuilder => BuildSlashParameter(fieldBuilder, parameter, services)); + } + + var initializer = builder.Command.Module.InteractionService._useCompiledLambda ? + ReflectionUtils.CreateLambdaConstructorInvoker(paramInfo.ParameterType.GetTypeInfo()) : ctor.Invoke; + builder.ComplexParameterInitializer = args => initializer(args); + } + break; + default: + builder.AddAttributes(attribute); + break; + } } - } - builder.SetParameterType(paramType, services); + builder.SetParameterType(paramType, services); - // Replace pascal casings with '-' - builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower(); - } + // Replace pascal casings with '-' + builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower(); + } - private static void BuildComponentParameter(ComponentCommandParameterBuilder builder, ParameterInfo paramInfo, bool isComponentParam) - { - builder.SetIsRouteSegment(!isComponentParam); - BuildParameter(builder, paramInfo); - } + private static void BuildComponentParameter(ComponentCommandParameterBuilder builder, ParameterInfo paramInfo, bool isComponentParam) + { + builder.SetIsRouteSegment(!isComponentParam); + BuildParameter(builder, paramInfo); + } - private static void BuildParameter(ParameterBuilder builder, ParameterInfo paramInfo) - where TInfo : class, IParameterInfo - where TBuilder : ParameterBuilder - { - var attributes = paramInfo.GetCustomAttributes(); - var paramType = paramInfo.ParameterType; + private static void BuildParameter(ParameterBuilder builder, ParameterInfo paramInfo) + where TInfo : class, IParameterInfo + where TBuilder : ParameterBuilder + { + var attributes = paramInfo.GetCustomAttributes(); + var paramType = paramInfo.ParameterType; - builder.Name = paramInfo.Name; - builder.IsRequired = !paramInfo.IsOptional; - builder.DefaultValue = paramInfo.DefaultValue; - builder.SetParameterType(paramType); + builder.Name = paramInfo.Name; + builder.IsRequired = !paramInfo.IsOptional; + builder.DefaultValue = paramInfo.DefaultValue; + builder.SetParameterType(paramType); - foreach (var attribute in attributes) - { - switch (attribute) + foreach (var attribute in attributes) { - case ParameterPreconditionAttribute precondition: - builder.AddPreconditions(precondition); - break; - case ParamArrayAttribute _: - builder.IsParameterArray = true; - break; - default: - builder.AddAttributes(attribute); - break; + switch (attribute) + { + case ParameterPreconditionAttribute precondition: + builder.AddPreconditions(precondition); + break; + case ParamArrayAttribute _: + builder.IsParameterArray = true; + break; + default: + builder.AddAttributes(attribute); + break; + } } } - } - #endregion + #endregion - #region Modals - public static ModalInfo BuildModalInfo(Type modalType, InteractionService interactionService) - { - if (!typeof(IModal).IsAssignableFrom(modalType)) - throw new InvalidOperationException($"{modalType.FullName} isn't an implementation of {typeof(IModal).FullName}"); + #region Modals + public static ModalInfo BuildModalInfo(Type modalType, InteractionService interactionService) + { + if (!typeof(IModal).IsAssignableFrom(modalType)) + throw new InvalidOperationException($"{modalType.FullName} isn't an implementation of {typeof(IModal).FullName}"); - var instance = Activator.CreateInstance(modalType, false) as IModal; + var instance = Activator.CreateInstance(modalType, false) as IModal; - try - { - var builder = new ModalBuilder(modalType, interactionService) + try { - Title = instance.Title - }; + var builder = new ModalBuilder(modalType, interactionService) + { + Title = instance.Title + }; + + var components = modalType.GetProperties().Where(IsValidModalComponentDefinition); - var components = modalType.GetProperties().Where(IsValidModalComponentDefinition); + foreach (var prop in components) + { + var componentType = prop.GetCustomAttribute()?.ComponentType; - foreach (var prop in components) + switch (componentType) + { + case ComponentType.TextInput: + builder.AddTextInputComponent(x => BuildTextInput(x, prop, prop.GetValue(instance))); + break; + case ComponentType.SelectMenu: + builder.AddSelectMenuInputComponent(x => BuildSelectMenuInput(x, prop, prop.GetValue(instance))); + break; + case ComponentType.UserSelect: + builder.AddUserSelectInputComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + break; + case ComponentType.RoleSelect: + builder.AddRoleSelectInputComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + break; + case ComponentType.MentionableSelect: + builder.AddMentionableSelectInputComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + break; + case ComponentType.ChannelSelect: + builder.AddChannelSelectInputComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + break; + case ComponentType.FileUpload: + builder.AddFileUploadInputComponent(x => BuildFileUploadInput(x, prop, prop.GetValue(instance))); + break; + case ComponentType.TextDisplay: + builder.AddTextDisplayComponent(x => BuildTextDisplayComponent(x, prop, prop.GetValue(instance))); + break; + case null: + throw new InvalidOperationException($"{prop.Name} of {prop.DeclaringType.Name} isn't a valid modal input field."); + default: + throw new InvalidOperationException($"Component type {componentType} cannot be used in modals."); + } + } + + var memberInit = ReflectionUtils.CreateLambdaMemberInit(modalType.GetTypeInfo(), modalType.GetConstructor(Type.EmptyTypes), x => x.IsDefined(typeof(ModalInputAttribute))); + builder.ModalInitializer = (args) => memberInit(Array.Empty(), args); + return builder.Build(); + } + finally { - var componentType = prop.GetCustomAttribute()?.ComponentType; + (instance as IDisposable)?.Dispose(); + } + } + + private static void BuildTextInput(TextInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) + { + var attributes = propertyInfo.GetCustomAttributes(); - switch (componentType) + builder.Label = propertyInfo.Name; + builder.DefaultValue = defaultValue; + builder.WithType(propertyInfo.PropertyType); + builder.PropertyInfo = propertyInfo; + + foreach (var attribute in attributes) + { + switch (attribute) { - case ComponentType.TextInput: - builder.AddTextInputComponent(x => BuildTextInput(x, prop, prop.GetValue(instance))); + case ModalTextInputAttribute textInput: + builder.CustomId = textInput.CustomId; + builder.ComponentType = textInput.ComponentType; + builder.Style = textInput.Style; + builder.Placeholder = textInput.Placeholder; + builder.MaxLength = textInput.MaxLength; + builder.MinLength = textInput.MinLength; + builder.InitialValue = textInput.InitialValue; break; - case ComponentType.SelectMenu: - builder.AddSelectMenuInputComponent(x => BuildSelectMenuInput(x, prop, prop.GetValue(instance))); + case RequiredInputAttribute requiredInput: + builder.IsRequired = requiredInput.IsRequired; break; - case ComponentType.UserSelect: - builder.AddUserSelectInputComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + case InputLabelAttribute inputLabel: + builder.Label = inputLabel.Label; + builder.Description = inputLabel.Description; break; - case ComponentType.RoleSelect: - builder.AddRoleSelectInputComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + default: + builder.WithAttributes(attribute); break; - case ComponentType.MentionableSelect: - builder.AddMentionableSelectInputComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + } + } + } + + private static void BuildSelectMenuInput(SelectMenuInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) + { + var attributes = propertyInfo.GetCustomAttributes(); + + builder.Label = propertyInfo.Name; + builder.DefaultValue = defaultValue; + builder.WithType(propertyInfo.PropertyType); + builder.PropertyInfo = propertyInfo; + + foreach (var attribute in attributes) + { + switch (attribute) + { + case ModalSelectMenuInputAttribute selectMenuInput: + builder.CustomId = selectMenuInput.CustomId; + builder.ComponentType = selectMenuInput.ComponentType; + builder.MinValues = selectMenuInput.MinValues; + builder.MaxValues = selectMenuInput.MaxValues; + builder.Placeholder = selectMenuInput.Placeholder; break; - case ComponentType.ChannelSelect: - builder.AddChannelSelectInputComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + case RequiredInputAttribute requiredInput: + builder.IsRequired = requiredInput.IsRequired; break; - case ComponentType.FileUpload: - builder.AddFileUploadInputComponent(x => BuildFileUploadInput(x, prop, prop.GetValue(instance))); + case InputLabelAttribute inputLabel: + builder.Label = inputLabel.Label; + builder.Description = inputLabel.Description; break; - case ComponentType.TextDisplay: - builder.AddTextDisplayComponent(x => BuildTextDisplayComponent(x, prop, prop.GetValue(instance))); + case ModalSelectMenuOptionAttribute selectMenuOption: + builder.AddOption(new SelectMenuOptionBuilder + { + Label = selectMenuOption.Label, + Description = selectMenuOption.Description, + Value = selectMenuOption.Value, + Emote = Emote.Parse(selectMenuOption.Emote), + IsDefault = selectMenuOption.IsDefault + }); break; - case null: - throw new InvalidOperationException($"{prop.Name} of {prop.DeclaringType.Name} isn't a valid modal input field."); default: - throw new InvalidOperationException($"Component type {componentType} cannot be used in modals."); + builder.WithAttributes(attribute); + break; } } - - var memberInit = ReflectionUtils.CreateLambdaMemberInit(modalType.GetTypeInfo(), modalType.GetConstructor(Type.EmptyTypes), x => x.IsDefined(typeof(ModalInputAttribute))); - builder.ModalInitializer = (args) => memberInit(Array.Empty(), args); - return builder.Build(); - } - finally - { - (instance as IDisposable)?.Dispose(); } - } - private static void BuildTextInput(TextInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) - { - var attributes = propertyInfo.GetCustomAttributes(); + private static void BuildSnowflakeSelectInput(SnowflakeSelectInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) + where TInfo : SnowflakeSelectInputComponentInfo + where TBuilder : SnowflakeSelectInputComponentBuilder + { + var attributes = propertyInfo.GetCustomAttributes(); - builder.Label = propertyInfo.Name; - builder.DefaultValue = defaultValue; - builder.WithType(propertyInfo.PropertyType); - builder.PropertyInfo = propertyInfo; + builder.Label = propertyInfo.Name; + builder.DefaultValue = defaultValue; + builder.WithType(propertyInfo.PropertyType); + builder.PropertyInfo = propertyInfo; - foreach (var attribute in attributes) - { - switch (attribute) + foreach (var attribute in attributes) { - case ModalTextInputAttribute textInput: - builder.CustomId = textInput.CustomId; - builder.ComponentType = textInput.ComponentType; - builder.Style = textInput.Style; - builder.Placeholder = textInput.Placeholder; - builder.MaxLength = textInput.MaxLength; - builder.MinLength = textInput.MinLength; - builder.InitialValue = textInput.InitialValue; - break; - case RequiredInputAttribute requiredInput: - builder.IsRequired = requiredInput.IsRequired; - break; - case InputLabelAttribute inputLabel: - builder.Label = inputLabel.Label; - builder.Description = inputLabel.Description; - break; - default: - builder.WithAttributes(attribute); - break; + switch (attribute) + { + case ModalSelectInputAttribute selectInput: + builder.CustomId = selectInput.CustomId; + builder.ComponentType = selectInput.ComponentType; + builder.MinValues = selectInput.MinValues; + builder.MaxValues = selectInput.MaxValues; + builder.Placeholder = selectInput.Placeholder; + break; + case RequiredInputAttribute requiredInput: + builder.IsRequired = requiredInput.IsRequired; + break; + case InputLabelAttribute inputLabel: + builder.Label = inputLabel.Label; + builder.Description = inputLabel.Description; + break; + default: + builder.WithAttributes(attribute); + break; + } } } - } - private static void BuildSelectMenuInput(SelectMenuInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) - { - var attributes = propertyInfo.GetCustomAttributes(); + private static void BuildFileUploadInput(FileUploadInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) + { + var attributes = propertyInfo.GetCustomAttributes(); - builder.Label = propertyInfo.Name; - builder.DefaultValue = defaultValue; - builder.WithType(propertyInfo.PropertyType); - builder.PropertyInfo = propertyInfo; + builder.Label = propertyInfo.Name; + builder.DefaultValue = defaultValue; + builder.WithType(propertyInfo.PropertyType); + builder.PropertyInfo = propertyInfo; - foreach (var attribute in attributes) - { - switch (attribute) + foreach(var attribute in attributes) { - case ModalSelectMenuInputAttribute selectMenuInput: - builder.CustomId = selectMenuInput.CustomId; - builder.ComponentType = selectMenuInput.ComponentType; - builder.MinValues = selectMenuInput.MinValues; - builder.MaxValues = selectMenuInput.MaxValues; - builder.Placeholder = selectMenuInput.Placeholder; - break; - case RequiredInputAttribute requiredInput: - builder.IsRequired = requiredInput.IsRequired; - break; - case InputLabelAttribute inputLabel: - builder.Label = inputLabel.Label; - builder.Description = inputLabel.Description; - break; - case ModalSelectMenuOptionAttribute selectMenuOption: - builder.AddOption(new SelectMenuOptionBuilder - { - Label = selectMenuOption.Label, - Description = selectMenuOption.Description, - Value = selectMenuOption.Value, - Emote = Emote.Parse(selectMenuOption.Emote), - IsDefault = selectMenuOption.IsDefault - }); - break; - default: - builder.WithAttributes(attribute); - break; + switch (attribute) + { + case ModalFileUploadInputAttribute fileUploadInput: + builder.CustomId = fileUploadInput.CustomId; + builder.ComponentType = fileUploadInput.ComponentType; + builder.MinValues = fileUploadInput.MinValues; + builder.MaxValues = fileUploadInput.MaxValues; + break; + case RequiredInputAttribute requiredInput: + builder.IsRequired = requiredInput.IsRequired; + break; + case InputLabelAttribute inputLabel: + builder.Label = inputLabel.Label; + builder.Description = inputLabel.Description; + break; + default: + builder.WithAttributes(attribute); + break; + } } } - } - private static void BuildSnowflakeSelectInput(SnowflakeSelectInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) - where TInfo : SnowflakeSelectInputComponentInfo - where TBuilder : SnowflakeSelectInputComponentBuilder - { - var attributes = propertyInfo.GetCustomAttributes(); + private static void BuildTextDisplayComponent(TextDisplayComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) + { + var attributes = propertyInfo.GetCustomAttributes(); - builder.Label = propertyInfo.Name; - builder.DefaultValue = defaultValue; - builder.WithType(propertyInfo.PropertyType); - builder.PropertyInfo = propertyInfo; + builder.DefaultValue = defaultValue; + builder.WithType(propertyInfo.PropertyType); + builder.PropertyInfo = propertyInfo; - foreach (var attribute in attributes) - { - switch (attribute) + foreach (var attribute in attributes) { - case ModalSelectInputAttribute selectInput: - builder.CustomId = selectInput.CustomId; - builder.ComponentType = selectInput.ComponentType; - builder.MinValues = selectInput.MinValues; - builder.MaxValues = selectInput.MaxValues; - builder.Placeholder = selectInput.Placeholder; - break; - case RequiredInputAttribute requiredInput: - builder.IsRequired = requiredInput.IsRequired; - break; - case InputLabelAttribute inputLabel: - builder.Label = inputLabel.Label; - builder.Description = inputLabel.Description; - break; - default: - builder.WithAttributes(attribute); - break; + switch (attribute) + { + case ModalTextDisplayAttribute textDisplay: + builder.ComponentType = textDisplay.ComponentType; + builder.Content = textDisplay.Content; + break; + default: + builder.WithAttributes(attribute); + break; + } } } - } - - private static void BuildFileUploadInput(FileUploadInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) - { - var attributes = propertyInfo.GetCustomAttributes(); - - builder.Label = propertyInfo.Name; - builder.DefaultValue = defaultValue; - builder.WithType(propertyInfo.PropertyType); - builder.PropertyInfo = propertyInfo; + #endregion - foreach(var attribute in attributes) + internal static bool IsValidModuleDefinition(TypeInfo typeInfo) { - switch (attribute) - { - case ModalFileUploadInputAttribute fileUploadInput: - builder.CustomId = fileUploadInput.CustomId; - builder.ComponentType = fileUploadInput.ComponentType; - builder.MinValues = fileUploadInput.MinValues; - builder.MaxValues = fileUploadInput.MaxValues; - break; - case RequiredInputAttribute requiredInput: - builder.IsRequired = requiredInput.IsRequired; - break; - case InputLabelAttribute inputLabel: - builder.Label = inputLabel.Label; - builder.Description = inputLabel.Description; - break; - default: - builder.WithAttributes(attribute); - break; - } + return ModuleTypeInfo.IsAssignableFrom(typeInfo) && + !typeInfo.IsAbstract && + !typeInfo.ContainsGenericParameters; } - } - private static void BuildTextDisplayComponent(TextDisplayComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) - { - var attributes = propertyInfo.GetCustomAttributes(); - - builder.DefaultValue = defaultValue; - builder.WithType(propertyInfo.PropertyType); - builder.PropertyInfo = propertyInfo; - - foreach (var attribute in attributes) + private static bool IsValidSlashCommandDefinition(MethodInfo methodInfo) { - switch (attribute) - { - case ModalTextDisplayAttribute textDisplay: - builder.ComponentType = textDisplay.ComponentType; - builder.Content = textDisplay.Content; - break; - default: - builder.WithAttributes(attribute); - break; - } + return methodInfo.IsDefined(typeof(SlashCommandAttribute)) && + (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && + !methodInfo.IsStatic && + !methodInfo.IsGenericMethod; } - } - #endregion - - internal static bool IsValidModuleDefinition(TypeInfo typeInfo) - { - return ModuleTypeInfo.IsAssignableFrom(typeInfo) && - !typeInfo.IsAbstract && - !typeInfo.ContainsGenericParameters; - } - - private static bool IsValidSlashCommandDefinition(MethodInfo methodInfo) - { - return methodInfo.IsDefined(typeof(SlashCommandAttribute)) && - (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && - !methodInfo.IsStatic && - !methodInfo.IsGenericMethod; - } - private static bool IsValidContextCommandDefinition(MethodInfo methodInfo) - { - return methodInfo.IsDefined(typeof(ContextCommandAttribute)) && - (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && - !methodInfo.IsStatic && - !methodInfo.IsGenericMethod; - } + private static bool IsValidContextCommandDefinition(MethodInfo methodInfo) + { + return methodInfo.IsDefined(typeof(ContextCommandAttribute)) && + (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && + !methodInfo.IsStatic && + !methodInfo.IsGenericMethod; + } - private static bool IsValidComponentCommandDefinition(MethodInfo methodInfo) - { - return methodInfo.IsDefined(typeof(ComponentInteractionAttribute)) && - (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && - !methodInfo.IsStatic && - !methodInfo.IsGenericMethod; - } + private static bool IsValidComponentCommandDefinition(MethodInfo methodInfo) + { + return methodInfo.IsDefined(typeof(ComponentInteractionAttribute)) && + (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && + !methodInfo.IsStatic && + !methodInfo.IsGenericMethod; + } - private static bool IsValidAutocompleteCommandDefinition(MethodInfo methodInfo) - { - return methodInfo.IsDefined(typeof(AutocompleteCommandAttribute)) && - (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && - !methodInfo.IsStatic && - !methodInfo.IsGenericMethod && - methodInfo.GetParameters().Length == 0; - } + private static bool IsValidAutocompleteCommandDefinition(MethodInfo methodInfo) + { + return methodInfo.IsDefined(typeof(AutocompleteCommandAttribute)) && + (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && + !methodInfo.IsStatic && + !methodInfo.IsGenericMethod && + methodInfo.GetParameters().Length == 0; + } - private static bool IsValidModalCommandDefinition(MethodInfo methodInfo) - { - return methodInfo.IsDefined(typeof(ModalInteractionAttribute)) && - (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && - !methodInfo.IsStatic && - !methodInfo.IsGenericMethod && - typeof(IModal).IsAssignableFrom(methodInfo.GetParameters().Last().ParameterType); - } + private static bool IsValidModalCommandDefinition(MethodInfo methodInfo) + { + return methodInfo.IsDefined(typeof(ModalInteractionAttribute)) && + (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && + !methodInfo.IsStatic && + !methodInfo.IsGenericMethod && + typeof(IModal).IsAssignableFrom(methodInfo.GetParameters().Last().ParameterType); + } - private static bool IsValidModalComponentDefinition(PropertyInfo propertyInfo) - { - return propertyInfo.SetMethod?.IsPublic == true && - propertyInfo.SetMethod?.IsStatic == false && - propertyInfo.IsDefined(typeof(ModalComponentAttribute)); - } + private static bool IsValidModalComponentDefinition(PropertyInfo propertyInfo) + { + return propertyInfo.SetMethod?.IsPublic == true && + propertyInfo.SetMethod?.IsStatic == false && + propertyInfo.IsDefined(typeof(ModalComponentAttribute)); + } - private static ConstructorInfo GetComplexParameterConstructor(TypeInfo typeInfo, ComplexParameterAttribute complexParameter) - { - var ctors = typeInfo.GetConstructors(); + private static ConstructorInfo GetComplexParameterConstructor(TypeInfo typeInfo, ComplexParameterAttribute complexParameter) + { + var ctors = typeInfo.GetConstructors(); - if (ctors.Length == 0) - throw new InvalidOperationException($"No constructor found for \"{typeInfo.FullName}\"."); + if (ctors.Length == 0) + throw new InvalidOperationException($"No constructor found for \"{typeInfo.FullName}\"."); - if (complexParameter.PrioritizedCtorSignature is not null) - { - var ctor = typeInfo.GetConstructor(complexParameter.PrioritizedCtorSignature); + if (complexParameter.PrioritizedCtorSignature is not null) + { + var ctor = typeInfo.GetConstructor(complexParameter.PrioritizedCtorSignature); - if (ctor is null) - throw new InvalidOperationException($"No constructor was found with the signature: {string.Join(",", complexParameter.PrioritizedCtorSignature.Select(x => x.Name))}"); + if (ctor is null) + throw new InvalidOperationException($"No constructor was found with the signature: {string.Join(",", complexParameter.PrioritizedCtorSignature.Select(x => x.Name))}"); - return ctor; - } + return ctor; + } - var prioritizedCtors = ctors.Where(x => x.IsDefined(typeof(ComplexParameterCtorAttribute), true)); + var prioritizedCtors = ctors.Where(x => x.IsDefined(typeof(ComplexParameterCtorAttribute), true)); - switch (prioritizedCtors.Count()) - { - case > 1: - throw new InvalidOperationException($"{nameof(ComplexParameterCtorAttribute)} can only be used once in a type."); - case 1: - return prioritizedCtors.First(); - } + switch (prioritizedCtors.Count()) + { + case > 1: + throw new InvalidOperationException($"{nameof(ComplexParameterCtorAttribute)} can only be used once in a type."); + case 1: + return prioritizedCtors.First(); + } - switch (ctors.Length) - { - case > 1: - throw new InvalidOperationException($"Multiple constructors found for \"{typeInfo.FullName}\"."); - default: - return ctors.First(); + switch (ctors.Length) + { + case > 1: + throw new InvalidOperationException($"Multiple constructors found for \"{typeInfo.FullName}\"."); + default: + return ctors.First(); + } } } } diff --git a/src/Discord.Net.Interactions/Entities/IModal.cs b/src/Discord.Net.Interactions/Entities/IModal.cs index 1516b458e2..572a88033e 100644 --- a/src/Discord.Net.Interactions/Entities/IModal.cs +++ b/src/Discord.Net.Interactions/Entities/IModal.cs @@ -1,12 +1,13 @@ -namespace Discord.Interactions; - -/// -/// Represents a generic for use with the interaction service. -/// -public interface IModal +namespace Discord.Interactions { /// - /// Gets the modal's title. + /// Represents a generic for use with the interaction service. /// - string Title { get; } + public interface IModal + { + /// + /// Gets the modal's title. + /// + string Title { get; } + } } diff --git a/src/Discord.Net.Interactions/Entities/ISelectMenuModel.cs b/src/Discord.Net.Interactions/Entities/ISelectMenuModel.cs new file mode 100644 index 0000000000..b2ceb57de8 --- /dev/null +++ b/src/Discord.Net.Interactions/Entities/ISelectMenuModel.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace Discord.Interactions.Entities; + +public interface ISelectMenuModel +{ + IList Values { get; } + + IEnumerable Options(IModal modal, IInteractionContext context, IServiceProvider services); +} diff --git a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs index 5893af2de3..7922e7606d 100644 --- a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs +++ b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs @@ -2,149 +2,150 @@ using System.Linq; using System.Threading.Tasks; -namespace Discord.Interactions; - -public static class IDiscordInteractionExtentions +namespace Discord.Interactions { - /// - /// Respond to an interaction with a . - /// - /// Type of the implementation. - /// The interaction to respond to. - /// The custom id of the modal. - /// Delegate that can be used to modify the modal. - /// The request options for this request. - /// A task that represents the asynchronous operation of responding to the interaction. - public static Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, RequestOptions options = null, Action modifyModal = null) - where T : class, IModal - { - if (!ModalUtils.TryGet(out var modalInfo)) - throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}"); - - return SendModalResponseAsync(interaction, customId, modalInfo, null, options, modifyModal); - } - - /// - /// Respond to an interaction with a . - /// - /// - /// This method overload uses the parameter to create a new - /// if there isn't a built one already in cache. - /// - /// Type of the implementation. - /// The interaction to respond to. - /// The custom id of the modal. - /// Interaction service instance that should be used to build s. - /// The request options for this request. - /// Delegate that can be used to modify the modal. - /// A task that represents the asynchronous operation of responding to the interaction. - public static Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, InteractionService interactionService, - RequestOptions options = null, Action modifyModal = null) - where T : class, IModal - { - var modalInfo = ModalUtils.GetOrAdd(interactionService); - - return SendModalResponseAsync(interaction, customId, modalInfo, null, options, modifyModal); - } - - /// - /// Respond to an interaction with an and fills the value fields of the modal using the property values of the provided - /// instance. - /// - /// Type of the implementation. - /// The interaction to respond to. - /// The custom id of the modal. - /// The instance to get field values from. - /// The request options for this request. - /// Delegate that can be used to modify the modal. - /// A task that represents the asynchronous operation of responding to the interaction. - public static Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, T modal, RequestOptions options = null, - Action modifyModal = null) - where T : class, IModal + public static class IDiscordInteractionExtentions { - if (!ModalUtils.TryGet(out var modalInfo)) - throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}"); - - return SendModalResponseAsync(interaction, customId, modalInfo, modal, options, modifyModal); - } - - private static async Task SendModalResponseAsync(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, T modalInstance = null, RequestOptions options = null, Action modifyModal = null) - where T : class, IModal - { - if (!modalInfo.Type.IsAssignableFrom(typeof(T))) - throw new ArgumentException($"{modalInfo.Type.FullName} isn't assignable from {typeof(T).FullName}."); - - var builder = new ModalBuilder(modalInstance.Title, customId); + /// + /// Respond to an interaction with a . + /// + /// Type of the implementation. + /// The interaction to respond to. + /// The custom id of the modal. + /// Delegate that can be used to modify the modal. + /// The request options for this request. + /// A task that represents the asynchronous operation of responding to the interaction. + public static Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, RequestOptions options = null, Action modifyModal = null) + where T : class, IModal + { + if (!ModalUtils.TryGet(out var modalInfo)) + throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}"); + + return SendModalResponseAsync(interaction, customId, modalInfo, null, options, modifyModal); + } + + /// + /// Respond to an interaction with a . + /// + /// + /// This method overload uses the parameter to create a new + /// if there isn't a built one already in cache. + /// + /// Type of the implementation. + /// The interaction to respond to. + /// The custom id of the modal. + /// Interaction service instance that should be used to build s. + /// The request options for this request. + /// Delegate that can be used to modify the modal. + /// A task that represents the asynchronous operation of responding to the interaction. + public static Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, InteractionService interactionService, + RequestOptions options = null, Action modifyModal = null) + where T : class, IModal + { + var modalInfo = ModalUtils.GetOrAdd(interactionService); + + return SendModalResponseAsync(interaction, customId, modalInfo, null, options, modifyModal); + } + + /// + /// Respond to an interaction with an and fills the value fields of the modal using the property values of the provided + /// instance. + /// + /// Type of the implementation. + /// The interaction to respond to. + /// The custom id of the modal. + /// The instance to get field values from. + /// The request options for this request. + /// Delegate that can be used to modify the modal. + /// A task that represents the asynchronous operation of responding to the interaction. + public static Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, T modal, RequestOptions options = null, + Action modifyModal = null) + where T : class, IModal + { + if (!ModalUtils.TryGet(out var modalInfo)) + throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}"); + + return SendModalResponseAsync(interaction, customId, modalInfo, modal, options, modifyModal); + } + + private static async Task SendModalResponseAsync(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, T modalInstance = null, RequestOptions options = null, Action modifyModal = null) + where T : class, IModal + { + if (!modalInfo.Type.IsAssignableFrom(typeof(T))) + throw new ArgumentException($"{modalInfo.Type.FullName} isn't assignable from {typeof(T).FullName}."); + + var builder = new ModalBuilder(modalInstance.Title, customId); + + foreach (var input in modalInfo.Components) + switch (input) + { + case TextInputComponentInfo textComponent: + { + var inputBuilder = new TextInputBuilder(textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null, + textComponent.MaxLength, textComponent.IsRequired); - foreach (var input in modalInfo.Components) - switch (input) - { - case TextInputComponentInfo textComponent: - { - var inputBuilder = new TextInputBuilder(textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null, - textComponent.MaxLength, textComponent.IsRequired); + if (modalInstance != null) + { + await textComponent.TypeConverter.WriteAsync(inputBuilder, textComponent, textComponent.Getter(modalInstance)); + } - if (modalInstance != null) - { - await textComponent.TypeConverter.WriteAsync(inputBuilder, textComponent, textComponent.Getter(modalInstance)); + var labelBuilder = new LabelBuilder(textComponent.Label, inputBuilder, textComponent.Description); + builder.AddLabel(labelBuilder); } + break; + case SelectMenuInputComponentInfo 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 labelBuilder = new LabelBuilder(textComponent.Label, inputBuilder, textComponent.Description); - builder.AddLabel(labelBuilder); - } - break; - case SelectMenuInputComponentInfo 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); + if (modalInstance != null) + { + await selectMenuComponent.TypeConverter.WriteAsync(inputBuilder, selectMenuComponent, selectMenuComponent.Getter(modalInstance)); + } - if (modalInstance != null) - { - await selectMenuComponent.TypeConverter.WriteAsync(inputBuilder, selectMenuComponent, selectMenuComponent.Getter(modalInstance)); + var labelBuilder = new LabelBuilder(selectMenuComponent.Label, inputBuilder, selectMenuComponent.Description); + builder.AddLabel(labelBuilder); } + break; + case SnowflakeSelectInputComponentInfo 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 labelBuilder = new LabelBuilder(selectMenuComponent.Label, inputBuilder, selectMenuComponent.Description); - builder.AddLabel(labelBuilder); - } - break; - case SnowflakeSelectInputComponentInfo snowflakeSelectComponent: - { - var inputBuilder = new SelectMenuBuilder(snowflakeSelectComponent.CustomId, null, snowflakeSelectComponent.Placeholder, snowflakeSelectComponent.MaxValues, snowflakeSelectComponent.MinValues, false, snowflakeSelectComponent.ComponentType, null, snowflakeSelectComponent.DefaultValues.ToList(), null, snowflakeSelectComponent.IsRequired); + if (modalInstance != null) + { + await snowflakeSelectComponent.TypeConverter.WriteAsync(inputBuilder, snowflakeSelectComponent, snowflakeSelectComponent.Getter(modalInstance)); + } - if (modalInstance != null) - { - await snowflakeSelectComponent.TypeConverter.WriteAsync(inputBuilder, snowflakeSelectComponent, snowflakeSelectComponent.Getter(modalInstance)); + var labelBuilder = new LabelBuilder(snowflakeSelectComponent.Label, inputBuilder, snowflakeSelectComponent.Description); + builder.AddLabel(labelBuilder); } + break; + case FileUploadInputComponentInfo fileUploadComponent: + { + var inputBuilder = new FileUploadComponentBuilder(fileUploadComponent.CustomId, fileUploadComponent.MinValues, fileUploadComponent.MaxValues, fileUploadComponent.IsRequired); - var labelBuilder = new LabelBuilder(snowflakeSelectComponent.Label, inputBuilder, snowflakeSelectComponent.Description); - builder.AddLabel(labelBuilder); - } - break; - case FileUploadInputComponentInfo fileUploadComponent: - { - var inputBuilder = new FileUploadComponentBuilder(fileUploadComponent.CustomId, fileUploadComponent.MinValues, fileUploadComponent.MaxValues, fileUploadComponent.IsRequired); + if (modalInstance != null) + { + await fileUploadComponent.TypeConverter.WriteAsync(inputBuilder, fileUploadComponent, fileUploadComponent.Getter(modalInstance)); + } - if (modalInstance != null) + var labelBuilder = new LabelBuilder(fileUploadComponent.Label, inputBuilder, fileUploadComponent.Description); + builder.AddLabel(labelBuilder); + } + break; + case TextDisplayComponentInfo textDisplayComponent: { - await fileUploadComponent.TypeConverter.WriteAsync(inputBuilder, fileUploadComponent, fileUploadComponent.Getter(modalInstance)); + var content = textDisplayComponent.Getter(modalInstance).ToString() ?? textDisplayComponent.Content; + var componentBuilder = new TextDisplayBuilder(content); + builder.AddTextDisplay(componentBuilder); } + break; + default: + throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class"); + } + + modifyModal?.Invoke(builder); - var labelBuilder = new LabelBuilder(fileUploadComponent.Label, inputBuilder, fileUploadComponent.Description); - builder.AddLabel(labelBuilder); - } - break; - case TextDisplayComponentInfo textDisplayComponent: - { - var content = textDisplayComponent.Getter(modalInstance).ToString() ?? textDisplayComponent.Content; - var componentBuilder = new TextDisplayBuilder(content); - builder.AddTextDisplay(componentBuilder); - } - break; - default: - throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class"); - } - - modifyModal?.Invoke(builder); - - await interaction.RespondWithModalAsync(builder.Build(), options); + await interaction.RespondWithModalAsync(builder.Build(), options); + } } } diff --git a/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs index 5269b9c900..7a5aa67d4b 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs @@ -1,42 +1,43 @@ -namespace Discord.Interactions; - -/// -/// Represents the base info class for input components. -/// -public abstract class InputComponentInfo : ModalComponentInfo +namespace Discord.Interactions { /// - /// Gets the custom id of this component. + /// Represents the base info class for input components. /// - public string CustomId { get; } + public abstract class InputComponentInfo : ModalComponentInfo + { + /// + /// Gets the custom id of this component. + /// + public string CustomId { get; } - /// - /// Gets the label of this component. - /// - public string Label { get; } + /// + /// Gets the label of this component. + /// + public string Label { get; } - /// - /// Gets the description of this component. - /// - public string Description { get; } + /// + /// Gets the description of this component. + /// + public string Description { get; } - /// - /// Gets whether or not this component requires a user input. - /// - public bool IsRequired { get; } + /// + /// Gets whether or not this component requires a user input. + /// + public bool IsRequired { get; } - /// - /// Gets the assigned to this component. - /// - public ModalComponentTypeConverter TypeConverter { get; } + /// + /// Gets the assigned to this component. + /// + public ModalComponentTypeConverter TypeConverter { get; } - internal InputComponentInfo(Builders.IInputComponentBuilder builder, ModalInfo modal) - : base(builder, modal) - { - CustomId = builder.CustomId; - Label = builder.Label; - Description = builder.Description; - IsRequired = builder.IsRequired; - TypeConverter = builder.TypeConverter; + internal InputComponentInfo(Builders.IInputComponentBuilder builder, ModalInfo modal) + : base(builder, modal) + { + CustomId = builder.CustomId; + Label = builder.Label; + Description = builder.Description; + IsRequired = builder.IsRequired; + TypeConverter = builder.TypeConverter; + } } } diff --git a/src/Discord.Net.Interactions/Info/InputComponents/TextInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/TextInputComponentInfo.cs index 94165c429f..6831c7953d 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/TextInputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/InputComponents/TextInputComponentInfo.cs @@ -1,51 +1,52 @@ using System; -namespace Discord.Interactions; - -/// -/// Represents the class for type. -/// -public class TextInputComponentInfo : InputComponentInfo +namespace Discord.Interactions { /// - /// true when overrides . - /// - internal bool TypeOverridesToString => _typeOverridesToString.Value; - private readonly Lazy _typeOverridesToString; - - /// - /// Gets the style of the text input. - /// - public TextInputStyle Style { get; } - - /// - /// Gets the placeholder of the text input. - /// - public string Placeholder { get; } - - /// - /// Gets the minimum length of the text input. + /// Represents the class for type. /// - public int MinLength { get; } - - /// - /// Gets the maximum length of the text input. - /// - public int MaxLength { get; } - - /// - /// Gets the initial value to be displayed by this input. - /// - public string InitialValue { get; } - - internal TextInputComponentInfo(Builders.TextInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) + public class TextInputComponentInfo : InputComponentInfo { - Style = builder.Style; - Placeholder = builder.Placeholder; - MinLength = builder.MinLength; - MaxLength = builder.MaxLength; - InitialValue = builder.InitialValue; - - _typeOverridesToString = new(() => ReflectionUtils.OverridesToString(Type)); + /// + /// true when overrides . + /// + internal bool TypeOverridesToString => _typeOverridesToString.Value; + private readonly Lazy _typeOverridesToString; + + /// + /// Gets the style of the text input. + /// + public TextInputStyle Style { get; } + + /// + /// Gets the placeholder of the text input. + /// + public string Placeholder { get; } + + /// + /// Gets the minimum length of the text input. + /// + public int MinLength { get; } + + /// + /// Gets the maximum length of the text input. + /// + public int MaxLength { get; } + + /// + /// Gets the initial value to be displayed by this input. + /// + public string InitialValue { get; } + + internal TextInputComponentInfo(Builders.TextInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) + { + Style = builder.Style; + Placeholder = builder.Placeholder; + MinLength = builder.MinLength; + MaxLength = builder.MaxLength; + InitialValue = builder.InitialValue; + + _typeOverridesToString = new(() => ReflectionUtils.OverridesToString(Type)); + } } } diff --git a/src/Discord.Net.Interactions/Info/ModalInfo.cs b/src/Discord.Net.Interactions/Info/ModalInfo.cs index 37ad926417..c0e5a7f0c0 100644 --- a/src/Discord.Net.Interactions/Info/ModalInfo.cs +++ b/src/Discord.Net.Interactions/Info/ModalInfo.cs @@ -5,191 +5,192 @@ using System.Linq; using System.Threading.Tasks; -namespace Discord.Interactions; - -/// -/// Represents a cached object initialization delegate. -/// -/// Property arguments array. -/// -/// Returns the constructed object. -/// -public delegate IModal ModalInitializer(object[] args); - -/// -/// Represents the info class of an form. -/// -public class ModalInfo +namespace Discord.Interactions { - internal readonly InteractionService _interactionService; - internal readonly ModalInitializer _initializer; - - /// - /// Gets the title of this modal. - /// - public string Title { get; } - - /// - /// Gets the implementation used to initialize this object. - /// - public Type Type { get; } - - /// - /// Gets a collection of the components of this modal. - /// - public IReadOnlyCollection Components { get; } - - /// - /// Gets a collection of the input components of this modal. - /// - public IReadOnlyCollection InputComponents { get; } - - /// - /// Gets a collection of the text components of this modal. - /// - public IReadOnlyCollection TextInputComponents { get; } - - /// - /// Get a collection of the select menu components of this modal. - /// - public IReadOnlyCollection SelectMenuInputComponents { get; } - - /// - /// Get a collection of the user select components of this modal. - /// - public IReadOnlyCollection UserSelectInputComponents { get; } - - /// - /// Get a collection of the role select components of this modal. - /// - public IReadOnlyCollection RoleSelectInputComponents { get; } - - /// - /// Get a collection of the mentionable select components of this modal. - /// - public IReadOnlyCollection MentionableSelectInputComponents { get; } - - /// - /// Get a collection of the channel select components of this modal. - /// - public IReadOnlyCollection ChannelSelectInputComponents { get; } - /// - /// Get a collection of the file upload components of this modal. + /// Represents a cached object initialization delegate. /// - public IReadOnlyCollection FileUploadInputComponents { get; } + /// Property arguments array. + /// + /// Returns the constructed object. + /// + public delegate IModal ModalInitializer(object[] args); /// - /// Gets a collection of the text display components of this modal. + /// Represents the info class of an form. /// - public IReadOnlyCollection TextDisplayComponents { get; } - - internal ModalInfo(Builders.ModalBuilder builder) + public class ModalInfo { - Title = builder.Title; - Type = builder.Type; - Components = builder.Components.Select(x => x switch + internal readonly InteractionService _interactionService; + internal readonly ModalInitializer _initializer; + + /// + /// Gets the title of this modal. + /// + public string Title { get; } + + /// + /// Gets the implementation used to initialize this object. + /// + public Type Type { get; } + + /// + /// Gets a collection of the components of this modal. + /// + public IReadOnlyCollection Components { get; } + + /// + /// Gets a collection of the input components of this modal. + /// + public IReadOnlyCollection InputComponents { get; } + + /// + /// Gets a collection of the text components of this modal. + /// + public IReadOnlyCollection TextInputComponents { get; } + + /// + /// Get a collection of the select menu components of this modal. + /// + public IReadOnlyCollection SelectMenuInputComponents { get; } + + /// + /// Get a collection of the user select components of this modal. + /// + public IReadOnlyCollection UserSelectInputComponents { get; } + + /// + /// Get a collection of the role select components of this modal. + /// + public IReadOnlyCollection RoleSelectInputComponents { get; } + + /// + /// Get a collection of the mentionable select components of this modal. + /// + public IReadOnlyCollection MentionableSelectInputComponents { get; } + + /// + /// Get a collection of the channel select components of this modal. + /// + public IReadOnlyCollection ChannelSelectInputComponents { get; } + + /// + /// Get a collection of the file upload components of this modal. + /// + public IReadOnlyCollection FileUploadInputComponents { get; } + + /// + /// Gets a collection of the text display components of this modal. + /// + public IReadOnlyCollection TextDisplayComponents { get; } + + internal ModalInfo(Builders.ModalBuilder builder) { - TextInputComponentBuilder textComponent => textComponent.Build(this), - SelectMenuInputComponentBuilder selectMenuComponent => selectMenuComponent.Build(this), - RoleSelectInputComponentBuilder roleSelectComponent => roleSelectComponent.Build(this), - ChannelSelectInputComponentBuilder channelSelectComponent => channelSelectComponent.Build(this), - UserSelectInputComponentBuilder userSelectComponent => userSelectComponent.Build(this), - MentionableSelectInputComponentBuilder mentionableSelectComponent => mentionableSelectComponent.Build(this), - FileUploadInputComponentBuilder fileUploadComponent => fileUploadComponent.Build(this), - TextDisplayComponentBuilder textDisplayComponent => textDisplayComponent.Build(this), - _ => throw new InvalidOperationException($"{x.GetType().FullName} isn't a supported modal input component builder type.") - }).ToImmutableArray(); - - InputComponents = Components.OfType().ToImmutableArray(); - - TextInputComponents = Components.OfType().ToImmutableArray(); - SelectMenuInputComponents = Components.OfType().ToImmutableArray(); - UserSelectInputComponents = Components.OfType().ToImmutableArray(); - RoleSelectInputComponents = Components.OfType().ToImmutableArray(); - MentionableSelectInputComponents = Components.OfType().ToImmutableArray(); - ChannelSelectInputComponents = Components.OfType().ToImmutableArray(); - FileUploadInputComponents = Components.OfType().ToImmutableArray(); - TextDisplayComponents = Components.OfType().ToImmutableArray(); - - _interactionService = builder._interactionService; - _initializer = builder.ModalInitializer; - } - - /// - /// Creates an and fills it with provided message components. - /// - /// that will be injected into the modal. - /// - /// A filled with the provided components. - /// - [Obsolete("This method is no longer supported with the introduction of Component TypeConverters, please use the CreateModalAsync method.")] - public IModal CreateModal(IModalInteraction modalInteraction, bool throwOnMissingField = false) - { - var args = new object[Components.Count]; - var components = modalInteraction.Data.Components.ToList(); + Title = builder.Title; + Type = builder.Type; + Components = builder.Components.Select(x => x switch + { + TextInputComponentBuilder textComponent => textComponent.Build(this), + SelectMenuInputComponentBuilder selectMenuComponent => selectMenuComponent.Build(this), + RoleSelectInputComponentBuilder roleSelectComponent => roleSelectComponent.Build(this), + ChannelSelectInputComponentBuilder channelSelectComponent => channelSelectComponent.Build(this), + UserSelectInputComponentBuilder userSelectComponent => userSelectComponent.Build(this), + MentionableSelectInputComponentBuilder mentionableSelectComponent => mentionableSelectComponent.Build(this), + FileUploadInputComponentBuilder fileUploadComponent => fileUploadComponent.Build(this), + TextDisplayComponentBuilder textDisplayComponent => textDisplayComponent.Build(this), + _ => throw new InvalidOperationException($"{x.GetType().FullName} isn't a supported modal input component builder type.") + }).ToImmutableArray(); + + InputComponents = Components.OfType().ToImmutableArray(); + + TextInputComponents = Components.OfType().ToImmutableArray(); + SelectMenuInputComponents = Components.OfType().ToImmutableArray(); + UserSelectInputComponents = Components.OfType().ToImmutableArray(); + RoleSelectInputComponents = Components.OfType().ToImmutableArray(); + MentionableSelectInputComponents = Components.OfType().ToImmutableArray(); + ChannelSelectInputComponents = Components.OfType().ToImmutableArray(); + FileUploadInputComponents = Components.OfType().ToImmutableArray(); + TextDisplayComponents = Components.OfType().ToImmutableArray(); + + _interactionService = builder._interactionService; + _initializer = builder.ModalInitializer; + } - for (var i = 0; i < Components.Count; i++) + /// + /// Creates an and fills it with provided message components. + /// + /// that will be injected into the modal. + /// + /// A filled with the provided components. + /// + [Obsolete("This method is no longer supported with the introduction of Component TypeConverters, please use the CreateModalAsync method.")] + public IModal CreateModal(IModalInteraction modalInteraction, bool throwOnMissingField = false) { - var input = InputComponents.ElementAt(i); - var component = components.Find(x => x.CustomId == input.CustomId); + var args = new object[Components.Count]; + var components = modalInteraction.Data.Components.ToList(); - if (component is null) + for (var i = 0; i < Components.Count; i++) { - if (!throwOnMissingField) - args[i] = input.DefaultValue; + var input = InputComponents.ElementAt(i); + var component = components.Find(x => x.CustomId == input.CustomId); + + if (component is null) + { + if (!throwOnMissingField) + args[i] = input.DefaultValue; + else + throw new InvalidOperationException($"Modal interaction is missing the required field: {input.CustomId}"); + } else - throw new InvalidOperationException($"Modal interaction is missing the required field: {input.CustomId}"); + args[i] = component.Value; } - else - args[i] = component.Value; - } - return _initializer(args); - } - - /// - /// Creates an and fills it with provided message components. - /// - /// Context of the that will be injected into the modal. - /// Services to be passed onto the s of the modal fields. - /// Whether or not this method should exit on encountering a missing modal field. - /// - /// A if a type conversion has failed, else a . - /// - public async Task CreateModalAsync(IInteractionContext context, IServiceProvider services = null, bool throwOnMissingField = false) - { - if (context.Interaction is not IModalInteraction modalInteraction) - return TypeConverterResult.FromError(InteractionCommandError.Unsuccessful, "Provided context doesn't belong to a Modal Interaction."); + return _initializer(args); + } - services ??= EmptyServiceProvider.Instance; + /// + /// Creates an and fills it with provided message components. + /// + /// Context of the that will be injected into the modal. + /// Services to be passed onto the s of the modal fields. + /// Whether or not this method should exit on encountering a missing modal field. + /// + /// A if a type conversion has failed, else a . + /// + public async Task CreateModalAsync(IInteractionContext context, IServiceProvider services = null, bool throwOnMissingField = false) + { + if (context.Interaction is not IModalInteraction modalInteraction) + return TypeConverterResult.FromError(InteractionCommandError.Unsuccessful, "Provided context doesn't belong to a Modal Interaction."); - var args = new object[InputComponents.Count]; - var components = modalInteraction.Data.Components.ToList(); + services ??= EmptyServiceProvider.Instance; - for (var i = 0; i < InputComponents.Count; i++) - { - var input = InputComponents.ElementAt(i); - var component = components.Find(x => x.CustomId == input.CustomId); + var args = new object[InputComponents.Count]; + var components = modalInteraction.Data.Components.ToList(); - if (component is null) + for (var i = 0; i < InputComponents.Count; i++) { - if (!throwOnMissingField) - args[i] = input.DefaultValue; + var input = InputComponents.ElementAt(i); + var component = components.Find(x => x.CustomId == input.CustomId); + + if (component is null) + { + if (!throwOnMissingField) + args[i] = input.DefaultValue; + else + return TypeConverterResult.FromError(InteractionCommandError.BadArgs, $"Modal interaction is missing the required field: {input.CustomId}"); + } else - return TypeConverterResult.FromError(InteractionCommandError.BadArgs, $"Modal interaction is missing the required field: {input.CustomId}"); - } - else - { - var readResult = await input.TypeConverter.ReadAsync(context, component, services).ConfigureAwait(false); + { + var readResult = await input.TypeConverter.ReadAsync(context, component, services).ConfigureAwait(false); - if (!readResult.IsSuccess) - return readResult; + if (!readResult.IsSuccess) + return readResult; - args[i] = readResult.Value; + args[i] = readResult.Value; + } } - } - return TypeConverterResult.FromSuccess(_initializer(args)); + return TypeConverterResult.FromSuccess(_initializer(args)); + } } } diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index b4700170b7..8fad4d48c5 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -10,1441 +10,1442 @@ using System.Threading; using System.Threading.Tasks; -namespace Discord.Interactions; - -/// -/// Provides the framework for building and registering Discord Application Commands. -/// -public class InteractionService : IDisposable +namespace Discord.Interactions { /// - /// Occurs when a Slash Command related information is received. - /// - public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } - internal readonly AsyncEvent> _logEvent = new(); - - /// - /// Occurs when any type of interaction is executed. + /// Provides the framework for building and registering Discord Application Commands. /// - public event Func InteractionExecuted + public class InteractionService : IDisposable { - add + /// + /// Occurs when a Slash Command related information is received. + /// + public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } + internal readonly AsyncEvent> _logEvent = new(); + + /// + /// Occurs when any type of interaction is executed. + /// + public event Func InteractionExecuted { - SlashCommandExecuted += value; - ContextCommandExecuted += value; - ComponentCommandExecuted += value; - AutocompleteCommandExecuted += value; - ModalCommandExecuted += value; - } - remove - { - SlashCommandExecuted -= value; - ContextCommandExecuted -= value; - ComponentCommandExecuted -= value; - AutocompleteCommandExecuted -= value; - ModalCommandExecuted -= value; + add + { + SlashCommandExecuted += value; + ContextCommandExecuted += value; + ComponentCommandExecuted += value; + AutocompleteCommandExecuted += value; + ModalCommandExecuted += value; + } + remove + { + SlashCommandExecuted -= value; + ContextCommandExecuted -= value; + ComponentCommandExecuted -= value; + AutocompleteCommandExecuted -= value; + ModalCommandExecuted -= value; + } } - } - - /// - /// Occurs when a Slash Command is executed. - /// - public event Func SlashCommandExecuted { add { _slashCommandExecutedEvent.Add(value); } remove { _slashCommandExecutedEvent.Remove(value); } } - internal readonly AsyncEvent> _slashCommandExecutedEvent = new(); - /// - /// Occurs when a Context Command is executed. - /// - public event Func ContextCommandExecuted { add { _contextCommandExecutedEvent.Add(value); } remove { _contextCommandExecutedEvent.Remove(value); } } - internal readonly AsyncEvent> _contextCommandExecutedEvent = new(); - - /// - /// Occurs when a Message Component command is executed. - /// - public event Func ComponentCommandExecuted { add { _componentCommandExecutedEvent.Add(value); } remove { _componentCommandExecutedEvent.Remove(value); } } - internal readonly AsyncEvent> _componentCommandExecutedEvent = new(); - - /// - /// Occurs when a Autocomplete command is executed. - /// - public event Func AutocompleteCommandExecuted { add { _autocompleteCommandExecutedEvent.Add(value); } remove { _autocompleteCommandExecutedEvent.Remove(value); } } - internal readonly AsyncEvent> _autocompleteCommandExecutedEvent = new(); - - /// - /// Occurs when a AutocompleteHandler is executed. - /// - public event Func AutocompleteHandlerExecuted { add { _autocompleteHandlerExecutedEvent.Add(value); } remove { _autocompleteHandlerExecutedEvent.Remove(value); } } - internal readonly AsyncEvent> _autocompleteHandlerExecutedEvent = new(); - - /// - /// Occurs when a Modal command is executed. - /// - public event Func ModalCommandExecuted { add { _modalCommandExecutedEvent.Add(value); } remove { _modalCommandExecutedEvent.Remove(value); } } - internal readonly AsyncEvent> _modalCommandExecutedEvent = new(); - - /// - /// Get the used by this Interaction Service instance to localize strings. - /// - public ILocalizationManager LocalizationManager { get; set; } - - private readonly ConcurrentDictionary _typedModuleDefs; - private readonly CommandMap _slashCommandMap; - private readonly ConcurrentDictionary> _contextCommandMaps; - private readonly CommandMap _componentCommandMap; - private readonly CommandMap _autocompleteCommandMap; - private readonly CommandMap _modalCommandMap; - private readonly HashSet _moduleDefs; - private readonly TypeMap _typeConverterMap; - private readonly TypeMap _compTypeConverterMap; - private readonly TypeMap _typeReaderMap; - private readonly TypeMap _modalInputTypeConverterMap; - private readonly ConcurrentDictionary _autocompleteHandlers = new(); - private readonly ConcurrentDictionary _modalInfos = new(); - private readonly SemaphoreSlim _lock; - internal readonly Logger _cmdLogger; - internal readonly LogManager _logManager; - internal readonly Func _getRestClient; - - internal readonly bool _throwOnError, _useCompiledLambda, _enableAutocompleteHandlers, _autoServiceScopes, _exitOnMissingModalField; - internal readonly string _wildCardExp; - internal readonly RunMode _runMode; - internal readonly RestResponseCallback _restResponseCallback; - - /// - /// Rest client to be used to register application commands. - /// - public DiscordRestClient RestClient { get => _getRestClient(); } - - /// - /// Represents all modules loaded within . - /// - public IReadOnlyList Modules => _moduleDefs.ToList(); - - /// - /// Represents all Slash Commands loaded within . - /// - public IReadOnlyList SlashCommands => _moduleDefs.SelectMany(x => x.SlashCommands).ToList(); - - /// - /// Represents all Context Commands loaded within . - /// - public IReadOnlyList ContextCommands => _moduleDefs.SelectMany(x => x.ContextCommands).ToList(); - - /// - /// Represents all Component Commands loaded within . - /// - public IReadOnlyCollection ComponentCommands => _moduleDefs.SelectMany(x => x.ComponentCommands).ToList(); - - /// - /// Represents all Modal Commands loaded within . - /// - public IReadOnlyCollection ModalCommands => _moduleDefs.SelectMany(x => x.ModalCommands).ToList(); - - /// - /// Gets a collection of the cached classes that are referenced in registered s. - /// - public IReadOnlyCollection Modals => ModalUtils.Modals; - - /// - /// Initialize a with provided configurations. - /// - /// The discord client. - /// The configuration class. - public InteractionService(DiscordRestClient discord, InteractionServiceConfig config = null) - : this(() => discord, config ?? new InteractionServiceConfig()) { } - - /// - /// Initialize a with provided configurations. - /// - /// The discord client provider. - /// The configuration class. - public InteractionService(IRestClientProvider discordProvider, InteractionServiceConfig config = null) - : this(() => discordProvider.RestClient, config ?? new InteractionServiceConfig()) { } - - private InteractionService(Func getRestClient, InteractionServiceConfig config = null) - { - config ??= new InteractionServiceConfig(); - - _lock = new SemaphoreSlim(1, 1); - _typedModuleDefs = new ConcurrentDictionary(); - _moduleDefs = new HashSet(); - - _logManager = new LogManager(config.LogLevel); - _logManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); - _cmdLogger = _logManager.CreateLogger("App Commands"); - - _slashCommandMap = new CommandMap(this); - _contextCommandMaps = new ConcurrentDictionary>(); - _componentCommandMap = new CommandMap(this, config.InteractionCustomIdDelimiters); - _autocompleteCommandMap = new CommandMap(this); - _modalCommandMap = new CommandMap(this, config.InteractionCustomIdDelimiters); - - _getRestClient = getRestClient; - - _runMode = config.DefaultRunMode; - if (_runMode == RunMode.Default) - throw new InvalidOperationException($"RunMode cannot be set to {RunMode.Default}"); - - _throwOnError = config.ThrowOnError; - _wildCardExp = config.WildCardExpression; - _useCompiledLambda = config.UseCompiledLambda; - _exitOnMissingModalField = config.ExitOnMissingModalField; - _enableAutocompleteHandlers = config.EnableAutocompleteHandlers; - _autoServiceScopes = config.AutoServiceScopes; - _restResponseCallback = config.RestResponseCallback; - LocalizationManager = config.LocalizationManager; - - _typeConverterMap = new TypeMap(this, new ConcurrentDictionary - { - [typeof(TimeSpan)] = new TimeSpanConverter() - }, new ConcurrentDictionary + /// + /// Occurs when a Slash Command is executed. + /// + public event Func SlashCommandExecuted { add { _slashCommandExecutedEvent.Add(value); } remove { _slashCommandExecutedEvent.Remove(value); } } + internal readonly AsyncEvent> _slashCommandExecutedEvent = new(); + + /// + /// Occurs when a Context Command is executed. + /// + public event Func ContextCommandExecuted { add { _contextCommandExecutedEvent.Add(value); } remove { _contextCommandExecutedEvent.Remove(value); } } + internal readonly AsyncEvent> _contextCommandExecutedEvent = new(); + + /// + /// Occurs when a Message Component command is executed. + /// + public event Func ComponentCommandExecuted { add { _componentCommandExecutedEvent.Add(value); } remove { _componentCommandExecutedEvent.Remove(value); } } + internal readonly AsyncEvent> _componentCommandExecutedEvent = new(); + + /// + /// Occurs when a Autocomplete command is executed. + /// + public event Func AutocompleteCommandExecuted { add { _autocompleteCommandExecutedEvent.Add(value); } remove { _autocompleteCommandExecutedEvent.Remove(value); } } + internal readonly AsyncEvent> _autocompleteCommandExecutedEvent = new(); + + /// + /// Occurs when a AutocompleteHandler is executed. + /// + public event Func AutocompleteHandlerExecuted { add { _autocompleteHandlerExecutedEvent.Add(value); } remove { _autocompleteHandlerExecutedEvent.Remove(value); } } + internal readonly AsyncEvent> _autocompleteHandlerExecutedEvent = new(); + + /// + /// Occurs when a Modal command is executed. + /// + public event Func ModalCommandExecuted { add { _modalCommandExecutedEvent.Add(value); } remove { _modalCommandExecutedEvent.Remove(value); } } + internal readonly AsyncEvent> _modalCommandExecutedEvent = new(); + + /// + /// Get the used by this Interaction Service instance to localize strings. + /// + public ILocalizationManager LocalizationManager { get; set; } + + private readonly ConcurrentDictionary _typedModuleDefs; + private readonly CommandMap _slashCommandMap; + private readonly ConcurrentDictionary> _contextCommandMaps; + private readonly CommandMap _componentCommandMap; + private readonly CommandMap _autocompleteCommandMap; + private readonly CommandMap _modalCommandMap; + private readonly HashSet _moduleDefs; + private readonly TypeMap _typeConverterMap; + private readonly TypeMap _compTypeConverterMap; + private readonly TypeMap _typeReaderMap; + private readonly TypeMap _modalInputTypeConverterMap; + private readonly ConcurrentDictionary _autocompleteHandlers = new(); + private readonly ConcurrentDictionary _modalInfos = new(); + private readonly SemaphoreSlim _lock; + internal readonly Logger _cmdLogger; + internal readonly LogManager _logManager; + internal readonly Func _getRestClient; + + internal readonly bool _throwOnError, _useCompiledLambda, _enableAutocompleteHandlers, _autoServiceScopes, _exitOnMissingModalField; + internal readonly string _wildCardExp; + internal readonly RunMode _runMode; + internal readonly RestResponseCallback _restResponseCallback; + + /// + /// Rest client to be used to register application commands. + /// + public DiscordRestClient RestClient { get => _getRestClient(); } + + /// + /// Represents all modules loaded within . + /// + public IReadOnlyList Modules => _moduleDefs.ToList(); + + /// + /// Represents all Slash Commands loaded within . + /// + public IReadOnlyList SlashCommands => _moduleDefs.SelectMany(x => x.SlashCommands).ToList(); + + /// + /// Represents all Context Commands loaded within . + /// + public IReadOnlyList ContextCommands => _moduleDefs.SelectMany(x => x.ContextCommands).ToList(); + + /// + /// Represents all Component Commands loaded within . + /// + public IReadOnlyCollection ComponentCommands => _moduleDefs.SelectMany(x => x.ComponentCommands).ToList(); + + /// + /// Represents all Modal Commands loaded within . + /// + public IReadOnlyCollection ModalCommands => _moduleDefs.SelectMany(x => x.ModalCommands).ToList(); + + /// + /// Gets a collection of the cached classes that are referenced in registered s. + /// + public IReadOnlyCollection Modals => ModalUtils.Modals; + + /// + /// Initialize a with provided configurations. + /// + /// The discord client. + /// The configuration class. + public InteractionService(DiscordRestClient discord, InteractionServiceConfig config = null) + : this(() => discord, config ?? new InteractionServiceConfig()) { } + + /// + /// Initialize a with provided configurations. + /// + /// The discord client provider. + /// The configuration class. + public InteractionService(IRestClientProvider discordProvider, InteractionServiceConfig config = null) + : this(() => discordProvider.RestClient, config ?? new InteractionServiceConfig()) { } + + private InteractionService(Func getRestClient, InteractionServiceConfig config = null) { - [typeof(IChannel)] = typeof(DefaultChannelConverter<>), - [typeof(IRole)] = typeof(DefaultRoleConverter<>), - [typeof(IAttachment)] = typeof(DefaultAttachmentConverter<>), - [typeof(IUser)] = typeof(DefaultUserConverter<>), - [typeof(IMentionable)] = typeof(DefaultMentionableConverter<>), - [typeof(IConvertible)] = typeof(DefaultValueConverter<>), - [typeof(Enum)] = typeof(EnumConverter<>), - [typeof(Nullable<>)] = typeof(NullableConverter<>) - }); - - _compTypeConverterMap = new TypeMap(this, new ConcurrentDictionary(), - new ConcurrentDictionary + config ??= new InteractionServiceConfig(); + + _lock = new SemaphoreSlim(1, 1); + _typedModuleDefs = new ConcurrentDictionary(); + _moduleDefs = new HashSet(); + + _logManager = new LogManager(config.LogLevel); + _logManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); + _cmdLogger = _logManager.CreateLogger("App Commands"); + + _slashCommandMap = new CommandMap(this); + _contextCommandMaps = new ConcurrentDictionary>(); + _componentCommandMap = new CommandMap(this, config.InteractionCustomIdDelimiters); + _autocompleteCommandMap = new CommandMap(this); + _modalCommandMap = new CommandMap(this, config.InteractionCustomIdDelimiters); + + _getRestClient = getRestClient; + + _runMode = config.DefaultRunMode; + if (_runMode == RunMode.Default) + throw new InvalidOperationException($"RunMode cannot be set to {RunMode.Default}"); + + _throwOnError = config.ThrowOnError; + _wildCardExp = config.WildCardExpression; + _useCompiledLambda = config.UseCompiledLambda; + _exitOnMissingModalField = config.ExitOnMissingModalField; + _enableAutocompleteHandlers = config.EnableAutocompleteHandlers; + _autoServiceScopes = config.AutoServiceScopes; + _restResponseCallback = config.RestResponseCallback; + LocalizationManager = config.LocalizationManager; + + _typeConverterMap = new TypeMap(this, new ConcurrentDictionary + { + [typeof(TimeSpan)] = new TimeSpanConverter() + }, new ConcurrentDictionary { - [typeof(Array)] = typeof(DefaultArrayComponentConverter<>), - [typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>), - [typeof(Nullable<>)] = typeof(NullableComponentConverter<>) + [typeof(IChannel)] = typeof(DefaultChannelConverter<>), + [typeof(IRole)] = typeof(DefaultRoleConverter<>), + [typeof(IAttachment)] = typeof(DefaultAttachmentConverter<>), + [typeof(IUser)] = typeof(DefaultUserConverter<>), + [typeof(IMentionable)] = typeof(DefaultMentionableConverter<>), + [typeof(IConvertible)] = typeof(DefaultValueConverter<>), + [typeof(Enum)] = typeof(EnumConverter<>), + [typeof(Nullable<>)] = typeof(NullableConverter<>) }); - _typeReaderMap = new TypeMap(this, new ConcurrentDictionary(), - new ConcurrentDictionary + _compTypeConverterMap = new TypeMap(this, new ConcurrentDictionary(), + new ConcurrentDictionary + { + [typeof(Array)] = typeof(DefaultArrayComponentConverter<>), + [typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>), + [typeof(Nullable<>)] = typeof(NullableComponentConverter<>) + }); + + _typeReaderMap = new TypeMap(this, new ConcurrentDictionary(), + new ConcurrentDictionary + { + [typeof(IChannel)] = typeof(DefaultChannelReader<>), + [typeof(IRole)] = typeof(DefaultRoleReader<>), + [typeof(IUser)] = typeof(DefaultUserReader<>), + [typeof(IMessage)] = typeof(DefaultMessageReader<>), + [typeof(IConvertible)] = typeof(DefaultValueReader<>), + [typeof(Enum)] = typeof(EnumReader<>), + [typeof(Nullable<>)] = typeof(NullableReader<>) + }); + + _modalInputTypeConverterMap = new TypeMap(this, new ConcurrentDictionary { - [typeof(IChannel)] = typeof(DefaultChannelReader<>), - [typeof(IRole)] = typeof(DefaultRoleReader<>), - [typeof(IUser)] = typeof(DefaultUserReader<>), - [typeof(IMessage)] = typeof(DefaultMessageReader<>), - [typeof(IConvertible)] = typeof(DefaultValueReader<>), - [typeof(Enum)] = typeof(EnumReader<>), - [typeof(Nullable<>)] = typeof(NullableReader<>) + }, new ConcurrentDictionary + { + [typeof(IConvertible)] = typeof(DefaultValueModalComponentConverter<>), + [typeof(Enum)] = typeof(EnumModalComponentConverter<>), + [typeof(Nullable<>)] = typeof(NullableComponentConverter<>), + [typeof(Array)] = typeof(DefaultArrayModalComponentConverter<>) }); - - _modalInputTypeConverterMap = new TypeMap(this, new ConcurrentDictionary - { - }, new ConcurrentDictionary - { - [typeof(IConvertible)] = typeof(DefaultValueModalComponentConverter<>), - [typeof(Enum)] = typeof(EnumModalComponentConverter<>), - [typeof(Nullable<>)] = typeof(NullableComponentConverter<>), - [typeof(Array)] = typeof(DefaultArrayModalComponentConverter<>) - }); - } - - /// - /// Create and loads a using a builder factory. - /// - /// Name of the module. - /// The for your dependency injection solution if using one; otherwise, pass . - /// Module builder factory. - /// - /// A task representing the operation for adding modules. The task result contains the built module instance. - /// - public async Task CreateModuleAsync(string name, IServiceProvider services, Action buildFunc) - { - services ??= EmptyServiceProvider.Instance; - - await _lock.WaitAsync().ConfigureAwait(false); - try - { - var builder = new ModuleBuilder(this, name); - buildFunc(builder); - - var moduleInfo = builder.Build(this, services); - LoadModuleInternal(moduleInfo); - - return moduleInfo; } - finally - { - _lock.Release(); - } - } - /// - /// Discover and load command modules from an . - /// - /// the command modules are defined in. - /// The for your dependency injection solution if using one; otherwise, pass . - /// - /// A task representing the operation for adding modules. The task result contains a collection of the modules added. - /// - public async Task> AddModulesAsync(Assembly assembly, IServiceProvider services) - { - services ??= EmptyServiceProvider.Instance; + /// + /// Create and loads a using a builder factory. + /// + /// Name of the module. + /// The for your dependency injection solution if using one; otherwise, pass . + /// Module builder factory. + /// + /// A task representing the operation for adding modules. The task result contains the built module instance. + /// + public async Task CreateModuleAsync(string name, IServiceProvider services, Action buildFunc) + { + services ??= EmptyServiceProvider.Instance; - await _lock.WaitAsync().ConfigureAwait(false); + await _lock.WaitAsync().ConfigureAwait(false); + try + { + var builder = new ModuleBuilder(this, name); + buildFunc(builder); - try - { - var types = await ModuleClassBuilder.SearchAsync(assembly, this); - var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this, services); + var moduleInfo = builder.Build(this, services); + LoadModuleInternal(moduleInfo); - foreach (var info in moduleDefs) + return moduleInfo; + } + finally { - _typedModuleDefs[info.Key] = info.Value; - LoadModuleInternal(info.Value); + _lock.Release(); } - return moduleDefs.Values; } - finally - { - _lock.Release(); - } - } - - /// - /// Add a command module from a . - /// - /// Type of the module. - /// The for your dependency injection solution if using one; otherwise, pass . - /// - /// A task representing the operation for adding the module. The task result contains the built module. - /// - /// - /// Thrown if this module has already been added. - /// - /// - /// Thrown when the is not a valid module definition. - /// - public Task AddModuleAsync(IServiceProvider services) where T : class => - AddModuleAsync(typeof(T), services); - - /// - /// Add a command module from a . - /// - /// Type of the module. - /// The for your dependency injection solution if using one; otherwise, pass . - /// - /// A task representing the operation for adding the module. The task result contains the built module. - /// - /// - /// Thrown if this module has already been added. - /// - /// - /// Thrown when the is not a valid module definition. - /// - public async Task AddModuleAsync(Type type, IServiceProvider services) - { - if (!typeof(IInteractionModuleBase).IsAssignableFrom(type)) - throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(type)); - - services ??= EmptyServiceProvider.Instance; - await _lock.WaitAsync().ConfigureAwait(false); - - try + /// + /// Discover and load command modules from an . + /// + /// the command modules are defined in. + /// The for your dependency injection solution if using one; otherwise, pass . + /// + /// A task representing the operation for adding modules. The task result contains a collection of the modules added. + /// + public async Task> AddModulesAsync(Assembly assembly, IServiceProvider services) { - var typeInfo = type.GetTypeInfo(); - - if (_typedModuleDefs.ContainsKey(typeInfo)) - throw new ArgumentException("Module definition for this type already exists."); - - var moduleDef = (await ModuleClassBuilder.BuildAsync(new List { typeInfo }, this, services).ConfigureAwait(false)).FirstOrDefault(); - - if (moduleDef.Value == default) - throw new InvalidOperationException($"Could not build the module {typeInfo.FullName}, did you pass an invalid type?"); + services ??= EmptyServiceProvider.Instance; - if (!_typedModuleDefs.TryAdd(type, moduleDef.Value)) - throw new ArgumentException("Module definition for this type already exists."); + await _lock.WaitAsync().ConfigureAwait(false); - _typedModuleDefs[moduleDef.Key] = moduleDef.Value; - LoadModuleInternal(moduleDef.Value); - - return moduleDef.Value; - } - finally - { - _lock.Release(); + try + { + var types = await ModuleClassBuilder.SearchAsync(assembly, this); + var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this, services); + + foreach (var info in moduleDefs) + { + _typedModuleDefs[info.Key] = info.Value; + LoadModuleInternal(info.Value); + } + return moduleDefs.Values; + } + finally + { + _lock.Release(); + } } - } - - /// - /// Register Application Commands from and to a guild. - /// - /// Id of the target guild. - /// If , this operation will not delete the commands that are missing from . - /// - /// A task representing the command registration process. The task result contains the active application commands of the target guild. - /// - public async Task> RegisterCommandsToGuildAsync(ulong guildId, bool deleteMissing = true) - { - EnsureClientReady(); - - var topLevelModules = _moduleDefs.Where(x => !x.IsSubModule); - var props = topLevelModules.SelectMany(x => x.ToApplicationCommandProps()).ToList(); - if (!deleteMissing) + /// + /// Add a command module from a . + /// + /// Type of the module. + /// The for your dependency injection solution if using one; otherwise, pass . + /// + /// A task representing the operation for adding the module. The task result contains the built module. + /// + /// + /// Thrown if this module has already been added. + /// + /// + /// Thrown when the is not a valid module definition. + /// + public Task AddModuleAsync(IServiceProvider services) where T : class => + AddModuleAsync(typeof(T), services); + + /// + /// Add a command module from a . + /// + /// Type of the module. + /// The for your dependency injection solution if using one; otherwise, pass . + /// + /// A task representing the operation for adding the module. The task result contains the built module. + /// + /// + /// Thrown if this module has already been added. + /// + /// + /// Thrown when the is not a valid module definition. + /// + public async Task AddModuleAsync(Type type, IServiceProvider services) { + if (!typeof(IInteractionModuleBase).IsAssignableFrom(type)) + throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(type)); - var existing = await RestClient.GetGuildApplicationCommands(guildId, true).ConfigureAwait(false); - var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); - props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); - } - - return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); - } - - /// - /// Register Application Commands from and to Discord on in global scope. - /// - /// If , this operation will not delete the commands that are missing from . - /// - /// A task representing the command registration process. The task result contains the active global application commands of bot. - /// - public async Task> RegisterCommandsGloballyAsync(bool deleteMissing = true) - { - EnsureClientReady(); + services ??= EmptyServiceProvider.Instance; - var topLevelModules = _moduleDefs.Where(x => !x.IsSubModule); - var props = topLevelModules.SelectMany(x => x.ToApplicationCommandProps()).ToList(); + await _lock.WaitAsync().ConfigureAwait(false); - if (!deleteMissing) - { - var existing = await RestClient.GetGlobalApplicationCommands(true).ConfigureAwait(false); - var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); - props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); - } + try + { + var typeInfo = type.GetTypeInfo(); - return await RestClient.BulkOverwriteGlobalCommands(props.ToArray()).ConfigureAwait(false); - } + if (_typedModuleDefs.ContainsKey(typeInfo)) + throw new ArgumentException("Module definition for this type already exists."); - /// - /// Register Application Commands from to a guild. - /// - /// - /// Commands will be registered as standalone commands, if you want the to take effect, - /// use . Registering a commands without group names might cause the command traversal to fail. - /// - /// The target guild. - /// If , this operation will not delete the commands that are missing from . - /// Commands to be registered to Discord. - /// - /// A task representing the command registration process. The task result contains the active application commands of the target guild. - /// - public Task> AddCommandsToGuildAsync(IGuild guild, bool deleteMissing = false, params ICommandInfo[] commands) - { - if (guild is null) - throw new ArgumentNullException(nameof(guild)); + var moduleDef = (await ModuleClassBuilder.BuildAsync(new List { typeInfo }, this, services).ConfigureAwait(false)).FirstOrDefault(); - return AddCommandsToGuildAsync(guild.Id, deleteMissing, commands); - } + if (moduleDef.Value == default) + throw new InvalidOperationException($"Could not build the module {typeInfo.FullName}, did you pass an invalid type?"); - /// - /// Register Application Commands from to a guild. - /// - /// - /// Commands will be registered as standalone commands, if you want the to take effect, - /// use . Registering a commands without group names might cause the command traversal to fail. - /// - /// The target guild ID. - /// If , this operation will not delete the commands that are missing from . - /// Commands to be registered to Discord. - /// - /// A task representing the command registration process. The task result contains the active application commands of the target guild. - /// - public async Task> AddCommandsToGuildAsync(ulong guildId, bool deleteMissing = false, params ICommandInfo[] commands) - { - EnsureClientReady(); + if (!_typedModuleDefs.TryAdd(type, moduleDef.Value)) + throw new ArgumentException("Module definition for this type already exists."); - var props = new List(); + _typedModuleDefs[moduleDef.Key] = moduleDef.Value; + LoadModuleInternal(moduleDef.Value); - foreach (var command in commands) - { - switch (command) + return moduleDef.Value; + } + finally { - case SlashCommandInfo slashCommand: - props.Add(slashCommand.ToApplicationCommandProps()); - break; - case ContextCommandInfo contextCommand: - props.Add(contextCommand.ToApplicationCommandProps()); - break; - default: - throw new InvalidOperationException($"Command type {command.GetType().FullName} isn't supported yet"); + _lock.Release(); } } - if (!deleteMissing) + /// + /// Register Application Commands from and to a guild. + /// + /// Id of the target guild. + /// If , this operation will not delete the commands that are missing from . + /// + /// A task representing the command registration process. The task result contains the active application commands of the target guild. + /// + public async Task> RegisterCommandsToGuildAsync(ulong guildId, bool deleteMissing = true) { - var existing = await RestClient.GetGuildApplicationCommands(guildId, true).ConfigureAwait(false); - var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); - props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); - } + EnsureClientReady(); - return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); - } + var topLevelModules = _moduleDefs.Where(x => !x.IsSubModule); + var props = topLevelModules.SelectMany(x => x.ToApplicationCommandProps()).ToList(); - /// - /// Register Application Commands from modules provided in to a guild. - /// - /// The target guild. - /// If , this operation will not delete the commands that are missing from . - /// Modules to be registered to Discord. - /// - /// A task representing the command registration process. The task result contains the active application commands of the target guild. - /// - public Task> AddModulesToGuildAsync(IGuild guild, bool deleteMissing = false, params ModuleInfo[] modules) - { - if (guild is null) - throw new ArgumentNullException(nameof(guild)); - - return AddModulesToGuildAsync(guild.Id, deleteMissing, modules); - } - - /// - /// Register Application Commands from modules provided in to a guild. - /// - /// The target guild ID. - /// If , this operation will not delete the commands that are missing from . - /// Modules to be registered to Discord. - /// - /// A task representing the command registration process. The task result contains the active application commands of the target guild. - /// - public async Task> AddModulesToGuildAsync(ulong guildId, bool deleteMissing = false, params ModuleInfo[] modules) - { - EnsureClientReady(); + if (!deleteMissing) + { - var props = modules.SelectMany(x => x.ToApplicationCommandProps(true)).Distinct().ToList(); + var existing = await RestClient.GetGuildApplicationCommands(guildId, true).ConfigureAwait(false); + var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); + props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); + } - if (!deleteMissing) - { - var existing = await RestClient.GetGuildApplicationCommands(guildId, true).ConfigureAwait(false); - var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); - props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); + return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); } - return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); - } - - /// - /// Register Application Commands from modules provided in as global commands. - /// - /// If , this operation will not delete the commands that are missing from . - /// Modules to be registered to Discord. - /// - /// A task representing the command registration process. The task result contains the active application commands of the target guild. - /// - public async Task> AddModulesGloballyAsync(bool deleteMissing = false, params ModuleInfo[] modules) - { - EnsureClientReady(); - - var props = modules.SelectMany(x => x.ToApplicationCommandProps(true)).Distinct().ToList(); - - if (!deleteMissing) + /// + /// Register Application Commands from and to Discord on in global scope. + /// + /// If , this operation will not delete the commands that are missing from . + /// + /// A task representing the command registration process. The task result contains the active global application commands of bot. + /// + public async Task> RegisterCommandsGloballyAsync(bool deleteMissing = true) { - var existing = await RestClient.GetGlobalApplicationCommands(true).ConfigureAwait(false); - var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); - props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); - } + EnsureClientReady(); - return await RestClient.BulkOverwriteGlobalCommands(props.ToArray()).ConfigureAwait(false); - } + var topLevelModules = _moduleDefs.Where(x => !x.IsSubModule); + var props = topLevelModules.SelectMany(x => x.ToApplicationCommandProps()).ToList(); - /// - /// Register Application Commands from as global commands. - /// - /// - /// Commands will be registered as standalone commands, if you want the to take effect, - /// use . Registering a commands without group names might cause the command traversal to fail. - /// - /// If , this operation will not delete the commands that are missing from . - /// Commands to be registered to Discord. - /// - /// A task representing the command registration process. The task result contains the active application commands of the target guild. - /// - public async Task> AddCommandsGloballyAsync(bool deleteMissing = false, params IApplicationCommandInfo[] commands) - { - EnsureClientReady(); - - var props = new List(); - - foreach (var command in commands) - { - switch (command) + if (!deleteMissing) { - case SlashCommandInfo slashCommand: - props.Add(slashCommand.ToApplicationCommandProps()); - break; - case ContextCommandInfo contextCommand: - props.Add(contextCommand.ToApplicationCommandProps()); - break; - default: - throw new InvalidOperationException($"Command type {command.GetType().FullName} isn't supported yet"); + var existing = await RestClient.GetGlobalApplicationCommands(true).ConfigureAwait(false); + var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); + props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); } - } - if (!deleteMissing) - { - var existing = await RestClient.GetGlobalApplicationCommands(true).ConfigureAwait(false); - var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); - props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); + return await RestClient.BulkOverwriteGlobalCommands(props.ToArray()).ConfigureAwait(false); } - return await RestClient.BulkOverwriteGlobalCommands(props.ToArray()).ConfigureAwait(false); - } - - private void LoadModuleInternal(ModuleInfo module) - { - _moduleDefs.Add(module); - - foreach (var command in module.SlashCommands) - _slashCommandMap.AddCommand(command, command.IgnoreGroupNames); - - foreach (var command in module.ContextCommands) - _contextCommandMaps.GetOrAdd(command.CommandType, new CommandMap(this)).AddCommand(command, command.IgnoreGroupNames); + /// + /// Register Application Commands from to a guild. + /// + /// + /// Commands will be registered as standalone commands, if you want the to take effect, + /// use . Registering a commands without group names might cause the command traversal to fail. + /// + /// The target guild. + /// If , this operation will not delete the commands that are missing from . + /// Commands to be registered to Discord. + /// + /// A task representing the command registration process. The task result contains the active application commands of the target guild. + /// + public Task> AddCommandsToGuildAsync(IGuild guild, bool deleteMissing = false, params ICommandInfo[] commands) + { + if (guild is null) + throw new ArgumentNullException(nameof(guild)); - foreach (var interaction in module.ComponentCommands) - _componentCommandMap.AddCommand(interaction, interaction.IgnoreGroupNames); + return AddCommandsToGuildAsync(guild.Id, deleteMissing, commands); + } - foreach (var command in module.AutocompleteCommands) - _autocompleteCommandMap.AddCommand(command.GetCommandKeywords(), command); + /// + /// Register Application Commands from to a guild. + /// + /// + /// Commands will be registered as standalone commands, if you want the to take effect, + /// use . Registering a commands without group names might cause the command traversal to fail. + /// + /// The target guild ID. + /// If , this operation will not delete the commands that are missing from . + /// Commands to be registered to Discord. + /// + /// A task representing the command registration process. The task result contains the active application commands of the target guild. + /// + public async Task> AddCommandsToGuildAsync(ulong guildId, bool deleteMissing = false, params ICommandInfo[] commands) + { + EnsureClientReady(); - foreach (var command in module.ModalCommands) - _modalCommandMap.AddCommand(command, command.IgnoreGroupNames); + var props = new List(); - foreach (var subModule in module.SubModules) - LoadModuleInternal(subModule); - } + foreach (var command in commands) + { + switch (command) + { + case SlashCommandInfo slashCommand: + props.Add(slashCommand.ToApplicationCommandProps()); + break; + case ContextCommandInfo contextCommand: + props.Add(contextCommand.ToApplicationCommandProps()); + break; + default: + throw new InvalidOperationException($"Command type {command.GetType().FullName} isn't supported yet"); + } + } - /// - /// Remove a command module. - /// - /// The of the module. - /// - /// A task that represents the asynchronous removal operation. The task result contains a value that - /// indicates whether the module is successfully removed. - /// - public Task RemoveModuleAsync() => - RemoveModuleAsync(typeof(T)); + if (!deleteMissing) + { + var existing = await RestClient.GetGuildApplicationCommands(guildId, true).ConfigureAwait(false); + var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); + props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); + } - /// - /// Remove a command module. - /// - /// The of the module. - /// - /// A task that represents the asynchronous removal operation. The task result contains a value that - /// indicates whether the module is successfully removed. - /// - public async Task RemoveModuleAsync(Type type) - { - await _lock.WaitAsync().ConfigureAwait(false); + return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); + } - try + /// + /// Register Application Commands from modules provided in to a guild. + /// + /// The target guild. + /// If , this operation will not delete the commands that are missing from . + /// Modules to be registered to Discord. + /// + /// A task representing the command registration process. The task result contains the active application commands of the target guild. + /// + public Task> AddModulesToGuildAsync(IGuild guild, bool deleteMissing = false, params ModuleInfo[] modules) { - if (!_typedModuleDefs.TryRemove(type, out var module)) - return false; + if (guild is null) + throw new ArgumentNullException(nameof(guild)); - return RemoveModuleInternal(module); + return AddModulesToGuildAsync(guild.Id, deleteMissing, modules); } - finally - { - _lock.Release(); - } - } - - /// - /// Remove a command module. - /// - /// The to be removed from the service. - /// - /// A task that represents the asynchronous removal operation. The task result contains a value that - /// indicates whether the is successfully removed. - /// - public async Task RemoveModuleAsync(ModuleInfo module) - { - await _lock.WaitAsync().ConfigureAwait(false); - try + /// + /// Register Application Commands from modules provided in to a guild. + /// + /// The target guild ID. + /// If , this operation will not delete the commands that are missing from . + /// Modules to be registered to Discord. + /// + /// A task representing the command registration process. The task result contains the active application commands of the target guild. + /// + public async Task> AddModulesToGuildAsync(ulong guildId, bool deleteMissing = false, params ModuleInfo[] modules) { - var typeModulePair = _typedModuleDefs.FirstOrDefault(x => x.Value.Equals(module)); + EnsureClientReady(); - if (!typeModulePair.Equals(default(KeyValuePair))) - _typedModuleDefs.TryRemove(typeModulePair.Key, out var _); + var props = modules.SelectMany(x => x.ToApplicationCommandProps(true)).Distinct().ToList(); - return RemoveModuleInternal(module); + if (!deleteMissing) + { + var existing = await RestClient.GetGuildApplicationCommands(guildId, true).ConfigureAwait(false); + var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); + props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); + } + + return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); } - finally + + /// + /// Register Application Commands from modules provided in as global commands. + /// + /// If , this operation will not delete the commands that are missing from . + /// Modules to be registered to Discord. + /// + /// A task representing the command registration process. The task result contains the active application commands of the target guild. + /// + public async Task> AddModulesGloballyAsync(bool deleteMissing = false, params ModuleInfo[] modules) { - _lock.Release(); - } - } + EnsureClientReady(); - /// - /// Unregister Application Commands from modules provided in from a guild. - /// - /// The target guild. - /// Modules to be deregistered from Discord. - /// - /// A task representing the command de-registration process. The task result contains the active application commands of the target guild. - /// - public Task> RemoveModulesFromGuildAsync(IGuild guild, params ModuleInfo[] modules) - { - if (guild is null) - throw new ArgumentNullException(nameof(guild)); + var props = modules.SelectMany(x => x.ToApplicationCommandProps(true)).Distinct().ToList(); - return RemoveModulesFromGuildAsync(guild.Id, modules); - } + if (!deleteMissing) + { + var existing = await RestClient.GetGlobalApplicationCommands(true).ConfigureAwait(false); + var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); + props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); + } - /// - /// Unregister Application Commands from modules provided in from a guild. - /// - /// The target guild ID. - /// Modules to be deregistered from Discord. - /// - /// A task representing the command de-registration process. The task result contains the active application commands of the target guild. - /// - public async Task> RemoveModulesFromGuildAsync(ulong guildId, params ModuleInfo[] modules) - { - EnsureClientReady(); + return await RestClient.BulkOverwriteGlobalCommands(props.ToArray()).ConfigureAwait(false); + } - var exclude = modules.SelectMany(x => x.ToApplicationCommandProps(true)).ToList(); - var existing = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); + /// + /// Register Application Commands from as global commands. + /// + /// + /// Commands will be registered as standalone commands, if you want the to take effect, + /// use . Registering a commands without group names might cause the command traversal to fail. + /// + /// If , this operation will not delete the commands that are missing from . + /// Commands to be registered to Discord. + /// + /// A task representing the command registration process. The task result contains the active application commands of the target guild. + /// + public async Task> AddCommandsGloballyAsync(bool deleteMissing = false, params IApplicationCommandInfo[] commands) + { + EnsureClientReady(); - var props = existing.Where(x => !exclude.Any(y => y.Name.IsSpecified && x.Name == y.Name.Value)).Select(x => x.ToApplicationCommandProps()); + var props = new List(); - return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); - } + foreach (var command in commands) + { + switch (command) + { + case SlashCommandInfo slashCommand: + props.Add(slashCommand.ToApplicationCommandProps()); + break; + case ContextCommandInfo contextCommand: + props.Add(contextCommand.ToApplicationCommandProps()); + break; + default: + throw new InvalidOperationException($"Command type {command.GetType().FullName} isn't supported yet"); + } + } - private bool RemoveModuleInternal(ModuleInfo moduleInfo) - { - if (!_moduleDefs.Remove(moduleInfo)) - return false; + if (!deleteMissing) + { + var existing = await RestClient.GetGlobalApplicationCommands(true).ConfigureAwait(false); + var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); + props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); + } - foreach (var command in moduleInfo.SlashCommands) - { - _slashCommandMap.RemoveCommand(command); + return await RestClient.BulkOverwriteGlobalCommands(props.ToArray()).ConfigureAwait(false); } - return true; - } + private void LoadModuleInternal(ModuleInfo module) + { + _moduleDefs.Add(module); - /// - /// Search the registered slash commands using a . - /// - /// Interaction entity to perform the search with. - /// - /// The search result. When successful, result contains the found . - /// - public SearchResult SearchSlashCommand(ISlashCommandInteraction slashCommandInteraction) - => _slashCommandMap.GetCommand(slashCommandInteraction.Data.GetCommandKeywords()); + foreach (var command in module.SlashCommands) + _slashCommandMap.AddCommand(command, command.IgnoreGroupNames); - /// - /// Search the registered slash commands using a . - /// - /// Interaction entity to perform the search with. - /// - /// The search result. When successful, result contains the found . - /// - public SearchResult SearchComponentCommand(IComponentInteraction componentInteraction) - => _componentCommandMap.GetCommand(componentInteraction.Data.CustomId); + foreach (var command in module.ContextCommands) + _contextCommandMaps.GetOrAdd(command.CommandType, new CommandMap(this)).AddCommand(command, command.IgnoreGroupNames); - /// - /// Search the registered slash commands using a . - /// - /// Interaction entity to perform the search with. - /// - /// The search result. When successful, result contains the found . - /// - public SearchResult SearchUserCommand(IUserCommandInteraction userCommandInteraction) - => _contextCommandMaps[ApplicationCommandType.User].GetCommand(userCommandInteraction.Data.Name); + foreach (var interaction in module.ComponentCommands) + _componentCommandMap.AddCommand(interaction, interaction.IgnoreGroupNames); - /// - /// Search the registered slash commands using a . - /// - /// Interaction entity to perform the search with. - /// - /// The search result. When successful, result contains the found . - /// - public SearchResult SearchMessageCommand(IMessageCommandInteraction messageCommandInteraction) - => _contextCommandMaps[ApplicationCommandType.Message].GetCommand(messageCommandInteraction.Data.Name); + foreach (var command in module.AutocompleteCommands) + _autocompleteCommandMap.AddCommand(command.GetCommandKeywords(), command); - /// - /// Search the registered slash commands using a . - /// - /// Interaction entity to perform the search with. - /// - /// The search result. When successful, result contains the found . - /// - public SearchResult SearchAutocompleteCommand(IAutocompleteInteraction autocompleteInteraction) - { - var keywords = autocompleteInteraction.Data.GetCommandKeywords(); - keywords.Add(autocompleteInteraction.Data.Current.Name); - return _autocompleteCommandMap.GetCommand(keywords); - } + foreach (var command in module.ModalCommands) + _modalCommandMap.AddCommand(command, command.IgnoreGroupNames); - /// - /// Execute a Command from a given . - /// - /// Name context of the command. - /// The service to be used in the command's dependency injection. - /// - /// A task representing the command execution process. The task result contains the result of the execution. - /// - public async Task ExecuteCommandAsync(IInteractionContext context, IServiceProvider services) - { - var interaction = context.Interaction; + foreach (var subModule in module.SubModules) + LoadModuleInternal(subModule); + } - return interaction switch + /// + /// Remove a command module. + /// + /// The of the module. + /// + /// A task that represents the asynchronous removal operation. The task result contains a value that + /// indicates whether the module is successfully removed. + /// + public Task RemoveModuleAsync() => + RemoveModuleAsync(typeof(T)); + + /// + /// Remove a command module. + /// + /// The of the module. + /// + /// A task that represents the asynchronous removal operation. The task result contains a value that + /// indicates whether the module is successfully removed. + /// + public async Task RemoveModuleAsync(Type type) { - ISlashCommandInteraction slashCommand => await ExecuteSlashCommandAsync(context, slashCommand, services).ConfigureAwait(false), - IComponentInteraction messageComponent => await ExecuteComponentCommandAsync(context, messageComponent.Data.CustomId, services).ConfigureAwait(false), - IUserCommandInteraction userCommand => await ExecuteContextCommandAsync(context, userCommand.Data.Name, ApplicationCommandType.User, services).ConfigureAwait(false), - IMessageCommandInteraction messageCommand => await ExecuteContextCommandAsync(context, messageCommand.Data.Name, ApplicationCommandType.Message, services).ConfigureAwait(false), - IAutocompleteInteraction autocomplete => await ExecuteAutocompleteAsync(context, autocomplete, services).ConfigureAwait(false), - IModalInteraction modalCommand => await ExecuteModalCommandAsync(context, modalCommand.Data.CustomId, services).ConfigureAwait(false), - _ => throw new InvalidOperationException($"{interaction.Type} interaction type cannot be executed by the Interaction service"), - }; - } + await _lock.WaitAsync().ConfigureAwait(false); - private async Task ExecuteSlashCommandAsync(IInteractionContext context, ISlashCommandInteraction interaction, IServiceProvider services) - { - var keywords = interaction.Data.GetCommandKeywords(); + try + { + if (!_typedModuleDefs.TryRemove(type, out var module)) + return false; - var result = _slashCommandMap.GetCommand(keywords); + return RemoveModuleInternal(module); + } + finally + { + _lock.Release(); + } + } - if (!result.IsSuccess) + /// + /// Remove a command module. + /// + /// The to be removed from the service. + /// + /// A task that represents the asynchronous removal operation. The task result contains a value that + /// indicates whether the is successfully removed. + /// + public async Task RemoveModuleAsync(ModuleInfo module) { - await _cmdLogger.DebugAsync($"Unknown slash command, skipping execution ({string.Join(" ", keywords).ToUpper()})"); + await _lock.WaitAsync().ConfigureAwait(false); - await _slashCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); - return result; - } - return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); - } + try + { + var typeModulePair = _typedModuleDefs.FirstOrDefault(x => x.Value.Equals(module)); - private async Task ExecuteContextCommandAsync(IInteractionContext context, string input, ApplicationCommandType commandType, IServiceProvider services) - { - if (!_contextCommandMaps.TryGetValue(commandType, out var map)) - return SearchResult.FromError(input, InteractionCommandError.UnknownCommand, $"No {commandType} command found."); + if (!typeModulePair.Equals(default(KeyValuePair))) + _typedModuleDefs.TryRemove(typeModulePair.Key, out var _); - var result = map.GetCommand(input); + return RemoveModuleInternal(module); + } + finally + { + _lock.Release(); + } + } - if (!result.IsSuccess) + /// + /// Unregister Application Commands from modules provided in from a guild. + /// + /// The target guild. + /// Modules to be deregistered from Discord. + /// + /// A task representing the command de-registration process. The task result contains the active application commands of the target guild. + /// + public Task> RemoveModulesFromGuildAsync(IGuild guild, params ModuleInfo[] modules) { - await _cmdLogger.DebugAsync($"Unknown context command, skipping execution ({result.Text.ToUpper()})"); + if (guild is null) + throw new ArgumentNullException(nameof(guild)); - await _contextCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); - return result; + return RemoveModulesFromGuildAsync(guild.Id, modules); } - return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); - } - private async Task ExecuteComponentCommandAsync(IInteractionContext context, string input, IServiceProvider services) - { - var result = _componentCommandMap.GetCommand(input); - - if (!result.IsSuccess) + /// + /// Unregister Application Commands from modules provided in from a guild. + /// + /// The target guild ID. + /// Modules to be deregistered from Discord. + /// + /// A task representing the command de-registration process. The task result contains the active application commands of the target guild. + /// + public async Task> RemoveModulesFromGuildAsync(ulong guildId, params ModuleInfo[] modules) { - await _cmdLogger.DebugAsync($"Unknown custom interaction id, skipping execution ({input.ToUpper()})"); - - await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); - return result; - } + EnsureClientReady(); - SetMatchesIfApplicable(context, result); + var exclude = modules.SelectMany(x => x.ToApplicationCommandProps(true)).ToList(); + var existing = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); - return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); - } + var props = existing.Where(x => !exclude.Any(y => y.Name.IsSpecified && x.Name == y.Name.Value)).Select(x => x.ToApplicationCommandProps()); - private async Task ExecuteAutocompleteAsync(IInteractionContext context, IAutocompleteInteraction interaction, IServiceProvider services) - { - var keywords = interaction.Data.GetCommandKeywords(); + return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); + } - if (_enableAutocompleteHandlers) + private bool RemoveModuleInternal(ModuleInfo moduleInfo) { - var autocompleteHandlerResult = _slashCommandMap.GetCommand(keywords); + if (!_moduleDefs.Remove(moduleInfo)) + return false; - if (autocompleteHandlerResult.IsSuccess) + foreach (var command in moduleInfo.SlashCommands) { - if (autocompleteHandlerResult.Command._flattenedParameterDictionary.TryGetValue(interaction.Data.Current.Name, out var parameter) && parameter?.AutocompleteHandler is not null) - return await parameter.AutocompleteHandler.ExecuteAsync(context, interaction, parameter, services).ConfigureAwait(false); + _slashCommandMap.RemoveCommand(command); } - } - - keywords.Add(interaction.Data.Current.Name); - - var commandResult = _autocompleteCommandMap.GetCommand(keywords); - if (!commandResult.IsSuccess) - { - await _cmdLogger.DebugAsync($"Unknown command name, skipping autocomplete process ({interaction.Data.CommandName.ToUpper()})"); - - await _autocompleteCommandExecutedEvent.InvokeAsync(null, context, commandResult).ConfigureAwait(false); - return commandResult; + return true; } - return await commandResult.Command.ExecuteAsync(context, services).ConfigureAwait(false); - } - - private async Task ExecuteModalCommandAsync(IInteractionContext context, string input, IServiceProvider services) - { - var result = _modalCommandMap.GetCommand(input); - - if (!result.IsSuccess) + /// + /// Search the registered slash commands using a . + /// + /// Interaction entity to perform the search with. + /// + /// The search result. When successful, result contains the found . + /// + public SearchResult SearchSlashCommand(ISlashCommandInteraction slashCommandInteraction) + => _slashCommandMap.GetCommand(slashCommandInteraction.Data.GetCommandKeywords()); + + /// + /// Search the registered slash commands using a . + /// + /// Interaction entity to perform the search with. + /// + /// The search result. When successful, result contains the found . + /// + public SearchResult SearchComponentCommand(IComponentInteraction componentInteraction) + => _componentCommandMap.GetCommand(componentInteraction.Data.CustomId); + + /// + /// Search the registered slash commands using a . + /// + /// Interaction entity to perform the search with. + /// + /// The search result. When successful, result contains the found . + /// + public SearchResult SearchUserCommand(IUserCommandInteraction userCommandInteraction) + => _contextCommandMaps[ApplicationCommandType.User].GetCommand(userCommandInteraction.Data.Name); + + /// + /// Search the registered slash commands using a . + /// + /// Interaction entity to perform the search with. + /// + /// The search result. When successful, result contains the found . + /// + public SearchResult SearchMessageCommand(IMessageCommandInteraction messageCommandInteraction) + => _contextCommandMaps[ApplicationCommandType.Message].GetCommand(messageCommandInteraction.Data.Name); + + /// + /// Search the registered slash commands using a . + /// + /// Interaction entity to perform the search with. + /// + /// The search result. When successful, result contains the found . + /// + public SearchResult SearchAutocompleteCommand(IAutocompleteInteraction autocompleteInteraction) { - await _cmdLogger.DebugAsync($"Unknown custom interaction id, skipping execution ({input.ToUpper()})"); - - await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); - return result; + var keywords = autocompleteInteraction.Data.GetCommandKeywords(); + keywords.Add(autocompleteInteraction.Data.Current.Name); + return _autocompleteCommandMap.GetCommand(keywords); } - SetMatchesIfApplicable(context, result); - - return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); - } - - private static void SetMatchesIfApplicable(IInteractionContext context, SearchResult searchResult) - where T : class, ICommandInfo - { - if (!searchResult.Command.SupportsWildCards || context is not IRouteMatchContainer matchContainer) - return; - - if (searchResult.RegexCaptureGroups?.Length > 0) + /// + /// Execute a Command from a given . + /// + /// Name context of the command. + /// The service to be used in the command's dependency injection. + /// + /// A task representing the command execution process. The task result contains the result of the execution. + /// + public async Task ExecuteCommandAsync(IInteractionContext context, IServiceProvider services) { - var matches = new RouteSegmentMatch[searchResult.RegexCaptureGroups.Length]; - for (var i = 0; i < searchResult.RegexCaptureGroups.Length; i++) - matches[i] = new RouteSegmentMatch(searchResult.RegexCaptureGroups[i]); + var interaction = context.Interaction; - matchContainer.SetSegmentMatches(matches); + return interaction switch + { + ISlashCommandInteraction slashCommand => await ExecuteSlashCommandAsync(context, slashCommand, services).ConfigureAwait(false), + IComponentInteraction messageComponent => await ExecuteComponentCommandAsync(context, messageComponent.Data.CustomId, services).ConfigureAwait(false), + IUserCommandInteraction userCommand => await ExecuteContextCommandAsync(context, userCommand.Data.Name, ApplicationCommandType.User, services).ConfigureAwait(false), + IMessageCommandInteraction messageCommand => await ExecuteContextCommandAsync(context, messageCommand.Data.Name, ApplicationCommandType.Message, services).ConfigureAwait(false), + IAutocompleteInteraction autocomplete => await ExecuteAutocompleteAsync(context, autocomplete, services).ConfigureAwait(false), + IModalInteraction modalCommand => await ExecuteModalCommandAsync(context, modalCommand.Data.CustomId, services).ConfigureAwait(false), + _ => throw new InvalidOperationException($"{interaction.Type} interaction type cannot be executed by the Interaction service"), + }; } - else - matchContainer.SetSegmentMatches(Array.Empty()); - } - internal TypeConverter GetTypeConverter(Type type, IServiceProvider services = null) - => _typeConverterMap.Get(type, services); + private async Task ExecuteSlashCommandAsync(IInteractionContext context, ISlashCommandInteraction interaction, IServiceProvider services) + { + var keywords = interaction.Data.GetCommandKeywords(); - /// - /// Add a concrete type . - /// - /// Primary target of the . - /// The instance. - public void AddTypeConverter(TypeConverter converter) => - _typeConverterMap.AddConcrete(converter); + var result = _slashCommandMap.GetCommand(keywords); - /// - /// Add a concrete type . - /// - /// Primary target of the . - /// The instance. - public void AddTypeConverter(Type type, TypeConverter converter) => - _typeConverterMap.AddConcrete(type, converter); + if (!result.IsSuccess) + { + await _cmdLogger.DebugAsync($"Unknown slash command, skipping execution ({string.Join(" ", keywords).ToUpper()})"); - /// - /// Add a generic type . - /// - /// Generic Type constraint of the of the . - /// Type of the . + await _slashCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); + return result; + } + return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); + } - public void AddGenericTypeConverter(Type converterType) => - _typeConverterMap.AddGeneric(converterType); + private async Task ExecuteContextCommandAsync(IInteractionContext context, string input, ApplicationCommandType commandType, IServiceProvider services) + { + if (!_contextCommandMaps.TryGetValue(commandType, out var map)) + return SearchResult.FromError(input, InteractionCommandError.UnknownCommand, $"No {commandType} command found."); - /// - /// Add a generic type . - /// - /// Generic Type constraint of the of the . - /// Type of the . - public void AddGenericTypeConverter(Type targetType, Type converterType) => - _typeConverterMap.AddGeneric(targetType, converterType); + var result = map.GetCommand(input); - internal ComponentTypeConverter GetComponentTypeConverter(Type type, IServiceProvider services = null) => - _compTypeConverterMap.Get(type, services); + if (!result.IsSuccess) + { + await _cmdLogger.DebugAsync($"Unknown context command, skipping execution ({result.Text.ToUpper()})"); - /// - /// Add a concrete type . - /// - /// Primary target of the . - /// The instance. - public void AddComponentTypeConverter(ComponentTypeConverter converter) => - AddComponentTypeConverter(typeof(T), converter); + await _contextCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); + return result; + } + return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); + } - /// - /// Add a concrete type . - /// - /// Primary target of the . - /// The instance. - public void AddComponentTypeConverter(Type type, ComponentTypeConverter converter) => - _compTypeConverterMap.AddConcrete(type, converter); + private async Task ExecuteComponentCommandAsync(IInteractionContext context, string input, IServiceProvider services) + { + var result = _componentCommandMap.GetCommand(input); - /// - /// Add a generic type . - /// - /// Generic Type constraint of the of the . - /// Type of the . - public void AddGenericComponentTypeConverter(Type converterType) => - AddGenericComponentTypeConverter(typeof(T), converterType); + if (!result.IsSuccess) + { + await _cmdLogger.DebugAsync($"Unknown custom interaction id, skipping execution ({input.ToUpper()})"); - /// - /// Add a generic type . - /// - /// Generic Type constraint of the of the . - /// Type of the . - public void AddGenericComponentTypeConverter(Type targetType, Type converterType) => - _compTypeConverterMap.AddGeneric(targetType, converterType); + await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); + return result; + } - internal TypeReader GetTypeReader(Type type, IServiceProvider services = null) => - _typeReaderMap.Get(type, services); + SetMatchesIfApplicable(context, result); - /// - /// Add a concrete type . - /// - /// Primary target of the . - /// The instance. - public void AddTypeReader(TypeReader reader) => - AddTypeReader(typeof(T), reader); + return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); + } - /// - /// Add a concrete type . - /// - /// Primary target of the . - /// The instance. - public void AddTypeReader(Type type, TypeReader reader) => - _typeReaderMap.AddConcrete(type, reader); + private async Task ExecuteAutocompleteAsync(IInteractionContext context, IAutocompleteInteraction interaction, IServiceProvider services) + { + var keywords = interaction.Data.GetCommandKeywords(); - /// - /// Add a generic type . - /// - /// Generic Type constraint of the of the . - /// Type of the . - public void AddGenericTypeReader(Type readerType) => - AddGenericTypeReader(typeof(T), readerType); + if (_enableAutocompleteHandlers) + { + var autocompleteHandlerResult = _slashCommandMap.GetCommand(keywords); - /// - /// Add a generic type . - /// - /// Generic Type constraint of the of the . - /// Type of the . - public void AddGenericTypeReader(Type targetType, Type readerType) => - _typeReaderMap.AddGeneric(targetType, readerType); + if (autocompleteHandlerResult.IsSuccess) + { + if (autocompleteHandlerResult.Command._flattenedParameterDictionary.TryGetValue(interaction.Data.Current.Name, out var parameter) && parameter?.AutocompleteHandler is not null) + return await parameter.AutocompleteHandler.ExecuteAsync(context, interaction, parameter, services).ConfigureAwait(false); + } + } - /// - /// Removes a type reader for the type . - /// - /// The type to remove the readers from. - /// The reader if the resulting remove operation was successful. - /// if the remove operation was successful; otherwise . - public bool TryRemoveTypeReader(out TypeReader reader) - => TryRemoveTypeReader(typeof(T), out reader); + keywords.Add(interaction.Data.Current.Name); - /// - /// Removes a type reader for the given type. - /// - /// - /// Removing a from the will not dereference the from the loaded module/command instances. - /// You need to reload the modules for the changes to take effect. - /// - /// The type to remove the reader from. - /// The reader if the resulting remove operation was successful. - /// if the remove operation was successful; otherwise . - public bool TryRemoveTypeReader(Type type, out TypeReader reader) - => _typeReaderMap.TryRemoveConcrete(type, out reader); + var commandResult = _autocompleteCommandMap.GetCommand(keywords); - /// - /// Removes a generic type reader from the type . - /// - /// - /// Removing a from the will not dereference the from the loaded module/command instances. - /// You need to reload the modules for the changes to take effect. - /// - /// The type to remove the readers from. - /// The removed readers type. - /// if the remove operation was successful; otherwise . - public bool TryRemoveGenericTypeReader(out Type readerType) - => TryRemoveGenericTypeReader(typeof(T), out readerType); + if (!commandResult.IsSuccess) + { + await _cmdLogger.DebugAsync($"Unknown command name, skipping autocomplete process ({interaction.Data.CommandName.ToUpper()})"); - /// - /// Removes a generic type reader from the given type. - /// - /// - /// Removing a from the will not dereference the from the loaded module/command instances. - /// You need to reload the modules for the changes to take effect. - /// - /// The type to remove the reader from. - /// The readers type if the remove operation was successful. - /// if the remove operation was successful; otherwise . - public bool TryRemoveGenericTypeReader(Type type, out Type readerType) - => _typeReaderMap.TryRemoveGeneric(type, out readerType); - - internal ModalComponentTypeConverter GetModalInputTypeConverter(Type type, IServiceProvider services = null) => - _modalInputTypeConverterMap.Get(type, services); + await _autocompleteCommandExecutedEvent.InvokeAsync(null, context, commandResult).ConfigureAwait(false); + return commandResult; + } - /// - /// Add a concrete type . - /// - /// Primary target of the . - /// The instance. - public void AddModalComponentTypeConverter(ModalComponentTypeConverter converter) => - AddModalComponentTypeConverter(typeof(T), converter); + return await commandResult.Command.ExecuteAsync(context, services).ConfigureAwait(false); + } - /// - /// Add a concrete type . - /// - /// Primary target of the . - /// The instance. - public void AddModalComponentTypeConverter(Type type, ModalComponentTypeConverter converter) => - _modalInputTypeConverterMap.AddConcrete(type, converter); + private async Task ExecuteModalCommandAsync(IInteractionContext context, string input, IServiceProvider services) + { + var result = _modalCommandMap.GetCommand(input); - /// - /// Add a generic type . - /// - /// Generic Type constraint of the of the . - /// Type of the . - public void AddGenericModalComponentTypeConverter(Type converterType) => - AddGenericModalComponentTypeConverter(typeof(T), converterType); + if (!result.IsSuccess) + { + await _cmdLogger.DebugAsync($"Unknown custom interaction id, skipping execution ({input.ToUpper()})"); - /// - /// Add a generic type . - /// - /// Generic Type constraint of the of the . - /// Type of the . - public void AddGenericModalComponentTypeConverter(Type targetType, Type converterType) => - _modalInputTypeConverterMap.AddGeneric(targetType, converterType); + await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); + return result; + } - /// - /// Removes a for the type . - /// - /// - /// Removing a from the will not dereference the from the loaded module/command instances. - /// You need to reload the modules for the changes to take effect. - /// - /// The type to remove the converter from. - /// The converter if the resulting remove operation was successful. - /// if the remove operation was successful; otherwise . - public bool TryRemoveModalComponentTypeConverter(out ModalComponentTypeConverter converter) => - TryRemoveModalComponentTypeConverter(typeof(T), out converter); + SetMatchesIfApplicable(context, result); - /// - /// Removes a for the type . - /// - /// - /// Removing a from the will not dereference the from the loaded module/command instances. - /// You need to reload the modules for the changes to take effect. - /// - /// The type to remove the converter from. - /// The converter if the resulting remove operation was successful. - /// if the remove operation was successful; otherwise . - public bool TryRemoveModalComponentTypeConverter(Type type, out ModalComponentTypeConverter converter) => - _modalInputTypeConverterMap.TryRemoveConcrete(type, out converter); + return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); + } - /// - /// Removes a generic for the type . - /// - /// - /// Removing a from the will not dereference the from the loaded module/command instances. - /// You need to reload the modules for the changes to take effect. - /// - /// The type to remove the converter from. - /// The converter if the resulting remove operation was successful. - /// if the remove operation was successful; otherwise . - public bool TryRemoveGenericModalComponentTypeConverter(out Type converterType) => - TryRemoveGenericModalComponentTypeConverter(typeof(T), out converterType); + private static void SetMatchesIfApplicable(IInteractionContext context, SearchResult searchResult) + where T : class, ICommandInfo + { + if (!searchResult.Command.SupportsWildCards || context is not IRouteMatchContainer matchContainer) + return; - /// - /// Removes a generic for the type . - /// - /// - /// Removing a from the will not dereference the from the loaded module/command instances. - /// You need to reload the modules for the changes to take effect. - /// - /// The type to remove the converter from. - /// The converter if the resulting remove operation was successful. - /// if the remove operation was successful; otherwise . - public bool TryRemoveGenericModalComponentTypeConverter(Type type, out Type converterType) => - _modalInputTypeConverterMap.TryRemoveGeneric(type, out converterType); + if (searchResult.RegexCaptureGroups?.Length > 0) + { + var matches = new RouteSegmentMatch[searchResult.RegexCaptureGroups.Length]; + for (var i = 0; i < searchResult.RegexCaptureGroups.Length; i++) + matches[i] = new RouteSegmentMatch(searchResult.RegexCaptureGroups[i]); + matchContainer.SetSegmentMatches(matches); + } + else + matchContainer.SetSegmentMatches(Array.Empty()); + } - /// - /// Serialize an object using a into a to be placed in a Component CustomId. - /// - /// - /// Removing a from the will not dereference the from the loaded module/command instances. - /// You need to reload the modules for the changes to take effect. - /// - /// Type of the object to be serialized. - /// Object to be serialized. - /// Services that will be passed on to the . - /// - /// A task representing the conversion process. The task result contains the result of the conversion. - /// - public Task SerializeValueAsync(T obj, IServiceProvider services) => - _typeReaderMap.Get(typeof(T), services).SerializeAsync(obj, services); + internal TypeConverter GetTypeConverter(Type type, IServiceProvider services = null) + => _typeConverterMap.Get(type, services); + + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddTypeConverter(TypeConverter converter) => + _typeConverterMap.AddConcrete(converter); + + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddTypeConverter(Type type, TypeConverter converter) => + _typeConverterMap.AddConcrete(type, converter); + + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + + public void AddGenericTypeConverter(Type converterType) => + _typeConverterMap.AddGeneric(converterType); + + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericTypeConverter(Type targetType, Type converterType) => + _typeConverterMap.AddGeneric(targetType, converterType); + + internal ComponentTypeConverter GetComponentTypeConverter(Type type, IServiceProvider services = null) => + _compTypeConverterMap.Get(type, services); + + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddComponentTypeConverter(ComponentTypeConverter converter) => + AddComponentTypeConverter(typeof(T), converter); + + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddComponentTypeConverter(Type type, ComponentTypeConverter converter) => + _compTypeConverterMap.AddConcrete(type, converter); + + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericComponentTypeConverter(Type converterType) => + AddGenericComponentTypeConverter(typeof(T), converterType); + + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericComponentTypeConverter(Type targetType, Type converterType) => + _compTypeConverterMap.AddGeneric(targetType, converterType); + + internal TypeReader GetTypeReader(Type type, IServiceProvider services = null) => + _typeReaderMap.Get(type, services); + + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddTypeReader(TypeReader reader) => + AddTypeReader(typeof(T), reader); + + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddTypeReader(Type type, TypeReader reader) => + _typeReaderMap.AddConcrete(type, reader); + + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericTypeReader(Type readerType) => + AddGenericTypeReader(typeof(T), readerType); + + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericTypeReader(Type targetType, Type readerType) => + _typeReaderMap.AddGeneric(targetType, readerType); + + /// + /// Removes a type reader for the type . + /// + /// The type to remove the readers from. + /// The reader if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveTypeReader(out TypeReader reader) + => TryRemoveTypeReader(typeof(T), out reader); + + /// + /// Removes a type reader for the given type. + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the reader from. + /// The reader if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveTypeReader(Type type, out TypeReader reader) + => _typeReaderMap.TryRemoveConcrete(type, out reader); + + /// + /// Removes a generic type reader from the type . + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the readers from. + /// The removed readers type. + /// if the remove operation was successful; otherwise . + public bool TryRemoveGenericTypeReader(out Type readerType) + => TryRemoveGenericTypeReader(typeof(T), out readerType); + + /// + /// Removes a generic type reader from the given type. + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the reader from. + /// The readers type if the remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveGenericTypeReader(Type type, out Type readerType) + => _typeReaderMap.TryRemoveGeneric(type, out readerType); + + internal ModalComponentTypeConverter GetModalInputTypeConverter(Type type, IServiceProvider services = null) => + _modalInputTypeConverterMap.Get(type, services); + + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddModalComponentTypeConverter(ModalComponentTypeConverter converter) => + AddModalComponentTypeConverter(typeof(T), converter); + + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddModalComponentTypeConverter(Type type, ModalComponentTypeConverter converter) => + _modalInputTypeConverterMap.AddConcrete(type, converter); + + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericModalComponentTypeConverter(Type converterType) => + AddGenericModalComponentTypeConverter(typeof(T), converterType); + + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericModalComponentTypeConverter(Type targetType, Type converterType) => + _modalInputTypeConverterMap.AddGeneric(targetType, converterType); + + /// + /// Removes a for the type . + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the converter from. + /// The converter if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveModalComponentTypeConverter(out ModalComponentTypeConverter converter) => + TryRemoveModalComponentTypeConverter(typeof(T), out converter); + + /// + /// Removes a for the type . + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the converter from. + /// The converter if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveModalComponentTypeConverter(Type type, out ModalComponentTypeConverter converter) => + _modalInputTypeConverterMap.TryRemoveConcrete(type, out converter); + + /// + /// Removes a generic for the type . + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the converter from. + /// The converter if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveGenericModalComponentTypeConverter(out Type converterType) => + TryRemoveGenericModalComponentTypeConverter(typeof(T), out converterType); + + /// + /// Removes a generic for the type . + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the converter from. + /// The converter if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveGenericModalComponentTypeConverter(Type type, out Type converterType) => + _modalInputTypeConverterMap.TryRemoveGeneric(type, out converterType); + + + /// + /// Serialize an object using a into a to be placed in a Component CustomId. + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// Type of the object to be serialized. + /// Object to be serialized. + /// Services that will be passed on to the . + /// + /// A task representing the conversion process. The task result contains the result of the conversion. + /// + public Task SerializeValueAsync(T obj, IServiceProvider services) => + _typeReaderMap.Get(typeof(T), services).SerializeAsync(obj, services); + + /// + /// Serialize and format multiple objects into a Custom Id string. + /// + /// A composite format string. + /// >Services that will be passed on to the s. + /// Objects to be serialized. + /// + /// A task representing the conversion process. The task result contains the result of the conversion. + /// + public async Task GenerateCustomIdStringAsync(string format, IServiceProvider services, params object[] args) + { + var serializedValues = new string[args.Length]; - /// - /// Serialize and format multiple objects into a Custom Id string. - /// - /// A composite format string. - /// >Services that will be passed on to the s. - /// Objects to be serialized. - /// - /// A task representing the conversion process. The task result contains the result of the conversion. - /// - public async Task GenerateCustomIdStringAsync(string format, IServiceProvider services, params object[] args) - { - var serializedValues = new string[args.Length]; + for (var i = 0; i < args.Length; i++) + { + var arg = args[i]; + var typeReader = _typeReaderMap.Get(arg.GetType(), null); + var result = await typeReader.SerializeAsync(arg, services).ConfigureAwait(false); + serializedValues[i] = result; + } - for (var i = 0; i < args.Length; i++) - { - var arg = args[i]; - var typeReader = _typeReaderMap.Get(arg.GetType(), null); - var result = await typeReader.SerializeAsync(arg, services).ConfigureAwait(false); - serializedValues[i] = result; + return string.Format(format, serializedValues); } - return string.Format(format, serializedValues); - } - - /// - /// Loads and caches an for the provided . - /// - /// Type of to be loaded. - /// - /// The built instance. - /// - /// - public ModalInfo AddModalInfo() where T : class, IModal - { - var type = typeof(T); + /// + /// Loads and caches an for the provided . + /// + /// Type of to be loaded. + /// + /// The built instance. + /// + /// + public ModalInfo AddModalInfo() where T : class, IModal + { + var type = typeof(T); - if (_modalInfos.ContainsKey(type)) - throw new InvalidOperationException($"Modal type {type.FullName} already exists."); + if (_modalInfos.ContainsKey(type)) + throw new InvalidOperationException($"Modal type {type.FullName} already exists."); - return ModalUtils.GetOrAdd(type, this); - } + return ModalUtils.GetOrAdd(type, this); + } - internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null) - { - services ??= EmptyServiceProvider.Instance; + internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null) + { + services ??= EmptyServiceProvider.Instance; - if (!_enableAutocompleteHandlers) - throw new InvalidOperationException($"{nameof(IAutocompleteHandler)}s are not enabled. To use this feature set {nameof(InteractionServiceConfig.EnableAutocompleteHandlers)} to TRUE"); + if (!_enableAutocompleteHandlers) + throw new InvalidOperationException($"{nameof(IAutocompleteHandler)}s are not enabled. To use this feature set {nameof(InteractionServiceConfig.EnableAutocompleteHandlers)} to TRUE"); - if (_autocompleteHandlers.TryGetValue(autocompleteHandlerType, out var autocompleteHandler)) - return autocompleteHandler; - else - { - autocompleteHandler = ReflectionUtils.CreateObject(autocompleteHandlerType.GetTypeInfo(), this, services); - _autocompleteHandlers[autocompleteHandlerType] = autocompleteHandler; - return autocompleteHandler; + if (_autocompleteHandlers.TryGetValue(autocompleteHandlerType, out var autocompleteHandler)) + return autocompleteHandler; + else + { + autocompleteHandler = ReflectionUtils.CreateObject(autocompleteHandlerType.GetTypeInfo(), this, services); + _autocompleteHandlers[autocompleteHandlerType] = autocompleteHandler; + return autocompleteHandler; + } } - } - - /// - /// Modify the command permissions of the matching Discord Slash Command. - /// - /// Module representing the top level Slash Command. - /// Target guild. - /// New permission values. - /// - /// The active command permissions after the modification. - /// - public Task ModifySlashCommandPermissionsAsync(ModuleInfo module, IGuild guild, - params ApplicationCommandPermission[] permissions) - { - if (module is null) - throw new ArgumentNullException(nameof(module)); - if (guild is null) - throw new ArgumentNullException(nameof(guild)); + /// + /// Modify the command permissions of the matching Discord Slash Command. + /// + /// Module representing the top level Slash Command. + /// Target guild. + /// New permission values. + /// + /// The active command permissions after the modification. + /// + public Task ModifySlashCommandPermissionsAsync(ModuleInfo module, IGuild guild, + params ApplicationCommandPermission[] permissions) + { + if (module is null) + throw new ArgumentNullException(nameof(module)); - return ModifySlashCommandPermissionsAsync(module, guild.Id, permissions); - } + if (guild is null) + throw new ArgumentNullException(nameof(guild)); - /// - /// Modify the command permissions of the matching Discord Slash Command. - /// - /// Module representing the top level Slash Command. - /// Target guild ID. - /// New permission values. - /// - /// The active command permissions after the modification. - /// - public async Task ModifySlashCommandPermissionsAsync(ModuleInfo module, ulong guildId, - params ApplicationCommandPermission[] permissions) - { - if (module is null) - throw new ArgumentNullException(nameof(module)); + return ModifySlashCommandPermissionsAsync(module, guild.Id, permissions); + } - if (!module.IsSlashGroup) - throw new InvalidOperationException($"This module does not have a {nameof(GroupAttribute)} and does not represent an Application Command"); + /// + /// Modify the command permissions of the matching Discord Slash Command. + /// + /// Module representing the top level Slash Command. + /// Target guild ID. + /// New permission values. + /// + /// The active command permissions after the modification. + /// + public async Task ModifySlashCommandPermissionsAsync(ModuleInfo module, ulong guildId, + params ApplicationCommandPermission[] permissions) + { + if (module is null) + throw new ArgumentNullException(nameof(module)); - if (!module.IsTopLevelGroup) - throw new InvalidOperationException("This module is not a top level application command. You cannot change its permissions"); + if (!module.IsSlashGroup) + throw new InvalidOperationException($"This module does not have a {nameof(GroupAttribute)} and does not represent an Application Command"); - var commands = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); - var appCommand = commands.First(x => x.Name == module.SlashGroupName); + if (!module.IsTopLevelGroup) + throw new InvalidOperationException("This module is not a top level application command. You cannot change its permissions"); - return await appCommand.ModifyCommandPermissions(permissions).ConfigureAwait(false); - } + var commands = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); + var appCommand = commands.First(x => x.Name == module.SlashGroupName); - /// - /// Modify the command permissions of the matching Discord Slash Command. - /// - /// The Slash Command. - /// Target guild. - /// New permission values. - /// - /// The active command permissions after the modification. - /// - public Task ModifySlashCommandPermissionsAsync(SlashCommandInfo command, IGuild guild, - params ApplicationCommandPermission[] permissions) - { - if (command is null) - throw new ArgumentNullException(nameof(command)); + return await appCommand.ModifyCommandPermissions(permissions).ConfigureAwait(false); + } - if (guild is null) - throw new ArgumentNullException(nameof(guild)); + /// + /// Modify the command permissions of the matching Discord Slash Command. + /// + /// The Slash Command. + /// Target guild. + /// New permission values. + /// + /// The active command permissions after the modification. + /// + public Task ModifySlashCommandPermissionsAsync(SlashCommandInfo command, IGuild guild, + params ApplicationCommandPermission[] permissions) + { + if (command is null) + throw new ArgumentNullException(nameof(command)); - return ModifyApplicationCommandPermissionsAsync(command, guild.Id, permissions); - } + if (guild is null) + throw new ArgumentNullException(nameof(guild)); - /// - /// Modify the command permissions of the matching Discord Slash Command. - /// - /// The Slash Command. - /// Target guild ID. - /// New permission values. - /// - /// The active command permissions after the modification. - /// - public Task ModifySlashCommandPermissionsAsync(SlashCommandInfo command, ulong guildId, - params ApplicationCommandPermission[] permissions) - => ModifyApplicationCommandPermissionsAsync(command, guildId, permissions); + return ModifyApplicationCommandPermissionsAsync(command, guild.Id, permissions); + } - /// - /// Modify the command permissions of the matching Discord Slash Command. - /// - /// The Context Command. - /// Target guild. - /// New permission values. - /// - /// The active command permissions after the modification. - /// - public Task ModifyContextCommandPermissionsAsync(ContextCommandInfo command, IGuild guild, - params ApplicationCommandPermission[] permissions) - { - if (command is null) - throw new ArgumentNullException(nameof(command)); + /// + /// Modify the command permissions of the matching Discord Slash Command. + /// + /// The Slash Command. + /// Target guild ID. + /// New permission values. + /// + /// The active command permissions after the modification. + /// + public Task ModifySlashCommandPermissionsAsync(SlashCommandInfo command, ulong guildId, + params ApplicationCommandPermission[] permissions) + => ModifyApplicationCommandPermissionsAsync(command, guildId, permissions); + + /// + /// Modify the command permissions of the matching Discord Slash Command. + /// + /// The Context Command. + /// Target guild. + /// New permission values. + /// + /// The active command permissions after the modification. + /// + public Task ModifyContextCommandPermissionsAsync(ContextCommandInfo command, IGuild guild, + params ApplicationCommandPermission[] permissions) + { + if (command is null) + throw new ArgumentNullException(nameof(command)); - if (guild is null) - throw new ArgumentNullException(nameof(guild)); + if (guild is null) + throw new ArgumentNullException(nameof(guild)); - return ModifyApplicationCommandPermissionsAsync(command, guild.Id, permissions); - } + return ModifyApplicationCommandPermissionsAsync(command, guild.Id, permissions); + } - /// - /// Modify the command permissions of the matching Discord Slash Command. - /// - /// The Context Command. - /// Target guild ID. - /// New permission values. - /// - /// The active command permissions after the modification. - /// - public Task ModifyContextCommandPermissionsAsync(ContextCommandInfo command, ulong guildId, - params ApplicationCommandPermission[] permissions) - => ModifyApplicationCommandPermissionsAsync(command, guildId, permissions); - - private async Task ModifyApplicationCommandPermissionsAsync(T command, ulong guildId, - params ApplicationCommandPermission[] permissions) where T : class, IApplicationCommandInfo, ICommandInfo - { - if (command is null) - throw new ArgumentNullException(nameof(command)); + /// + /// Modify the command permissions of the matching Discord Slash Command. + /// + /// The Context Command. + /// Target guild ID. + /// New permission values. + /// + /// The active command permissions after the modification. + /// + public Task ModifyContextCommandPermissionsAsync(ContextCommandInfo command, ulong guildId, + params ApplicationCommandPermission[] permissions) + => ModifyApplicationCommandPermissionsAsync(command, guildId, permissions); + + private async Task ModifyApplicationCommandPermissionsAsync(T command, ulong guildId, + params ApplicationCommandPermission[] permissions) where T : class, IApplicationCommandInfo, ICommandInfo + { + if (command is null) + throw new ArgumentNullException(nameof(command)); - if (!command.IsTopLevelCommand) - throw new InvalidOperationException("This command is not a top level application command. You cannot change its permissions"); + if (!command.IsTopLevelCommand) + throw new InvalidOperationException("This command is not a top level application command. You cannot change its permissions"); - var commands = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); - var appCommand = commands.First(x => x.Name == (command as IApplicationCommandInfo).Name); + var commands = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); + var appCommand = commands.First(x => x.Name == (command as IApplicationCommandInfo).Name); - return await appCommand.ModifyCommandPermissions(permissions).ConfigureAwait(false); - } + return await appCommand.ModifyCommandPermissions(permissions).ConfigureAwait(false); + } - /// - /// Gets a . - /// - /// Declaring module type of this command, must be a type of . - /// Method name of the handler, use of is recommended. - /// - /// instance for this command. - /// - /// Module or Slash Command couldn't be found. - public SlashCommandInfo GetSlashCommandInfo(string methodName) where TModule : class - { - var module = GetModuleInfo(); + /// + /// Gets a . + /// + /// Declaring module type of this command, must be a type of . + /// Method name of the handler, use of is recommended. + /// + /// instance for this command. + /// + /// Module or Slash Command couldn't be found. + public SlashCommandInfo GetSlashCommandInfo(string methodName) where TModule : class + { + var module = GetModuleInfo(); - return module.SlashCommands.First(x => x.MethodName == methodName); - } + return module.SlashCommands.First(x => x.MethodName == methodName); + } - /// - /// Gets a . - /// - /// Declaring module type of this command, must be a type of . - /// Method name of the handler, use of is recommended. - /// - /// instance for this command. - /// - /// Module or Context Command couldn't be found. - public ContextCommandInfo GetContextCommandInfo(string methodName) where TModule : class - { - var module = GetModuleInfo(); + /// + /// Gets a . + /// + /// Declaring module type of this command, must be a type of . + /// Method name of the handler, use of is recommended. + /// + /// instance for this command. + /// + /// Module or Context Command couldn't be found. + public ContextCommandInfo GetContextCommandInfo(string methodName) where TModule : class + { + var module = GetModuleInfo(); - return module.ContextCommands.First(x => x.MethodName == methodName); - } + return module.ContextCommands.First(x => x.MethodName == methodName); + } - /// - /// Gets a . - /// - /// Declaring module type of this command, must be a type of . - /// Method name of the handler, use of is recommended. - /// - /// instance for this command. - /// - /// Module or Component Command couldn't be found. - public ComponentCommandInfo GetComponentCommandInfo(string methodName) where TModule : class - { - var module = GetModuleInfo(); + /// + /// Gets a . + /// + /// Declaring module type of this command, must be a type of . + /// Method name of the handler, use of is recommended. + /// + /// instance for this command. + /// + /// Module or Component Command couldn't be found. + public ComponentCommandInfo GetComponentCommandInfo(string methodName) where TModule : class + { + var module = GetModuleInfo(); - return module.ComponentCommands.First(x => x.MethodName == methodName); - } + return module.ComponentCommands.First(x => x.MethodName == methodName); + } - /// - /// Gets a built . - /// - /// Type of the module, must be a type of . - /// - /// instance for this module. - /// - public ModuleInfo GetModuleInfo() where TModule : class - { - if (!typeof(IInteractionModuleBase).IsAssignableFrom(typeof(TModule))) - throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(TModule)); + /// + /// Gets a built . + /// + /// Type of the module, must be a type of . + /// + /// instance for this module. + /// + public ModuleInfo GetModuleInfo() where TModule : class + { + if (!typeof(IInteractionModuleBase).IsAssignableFrom(typeof(TModule))) + throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(TModule)); - var module = _typedModuleDefs[typeof(TModule)]; + var module = _typedModuleDefs[typeof(TModule)]; - if (module is null) - throw new InvalidOperationException($"{typeof(TModule).FullName} is not loaded to the Slash Command Service"); + if (module is null) + throw new InvalidOperationException($"{typeof(TModule).FullName} is not loaded to the Slash Command Service"); - return module; - } + return module; + } - /// - public void Dispose() - { - _lock.Dispose(); - } + /// + public void Dispose() + { + _lock.Dispose(); + } - private void EnsureClientReady() - { - if (RestClient?.CurrentUser is null || RestClient?.CurrentUser?.Id == 0) - throw new InvalidOperationException($"Provided client is not ready to execute this operation, invoke this operation after a `Client Ready` event"); + private void EnsureClientReady() + { + if (RestClient?.CurrentUser is null || RestClient?.CurrentUser?.Id == 0) + throw new InvalidOperationException($"Provided client is not ready to execute this operation, invoke this operation after a `Client Ready` event"); + } } } diff --git a/src/Discord.Net.Interactions/TypeReaders/DateTimeTypeReader.cs b/src/Discord.Net.Interactions/TypeReaders/DateTimeTypeReader.cs new file mode 100644 index 0000000000..d4d1bce541 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeReaders/DateTimeTypeReader.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions.TypeReaders; + +internal class DateTimeTypeReader : TypeReader +{ + public override Task ReadAsync(IInteractionContext context, string option, IServiceProvider services) + { + if (DateTime.TryParse(option, out var dateTime)) + return Task.FromResult(TypeConverterResult.FromSuccess(dateTime)); + + return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option} is not a valid date time.")); + } +} From 625e7c283b40b28f3c55eb99e19a2c0a461697d9 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 12 Nov 2025 23:20:56 +0100 Subject: [PATCH 44/59] fix inline doc annotations --- src/Discord.Net.Interactions/InteractionService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index 8fad4d48c5..3d2ec71326 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -1155,7 +1155,7 @@ public bool TryRemoveGenericModalComponentTypeConverter(out Type converterTyp /// Removing a from the will not dereference the from the loaded module/command instances. /// You need to reload the modules for the changes to take effect. /// - /// The type to remove the converter from. + /// The type to remove the converter from. /// The converter if the resulting remove operation was successful. /// if the remove operation was successful; otherwise . public bool TryRemoveGenericModalComponentTypeConverter(Type type, out Type converterType) => From 18e67aace0266b5400aac9e81d64062a6953ecaf Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 12 Nov 2025 23:28:44 +0100 Subject: [PATCH 45/59] fix build errors --- .../Builders/Modals/Inputs/FileUploadInputComponentBuilder.cs | 2 +- .../Builders/Modals/Inputs/RoleSelectInputComponentBuilder.cs | 4 ++-- .../Info/InputComponents/TextInputComponentInfo.cs | 2 +- src/Discord.Net.Interactions/InteractionService.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/FileUploadInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/FileUploadInputComponentBuilder.cs index c65f639186..699d133344 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/FileUploadInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/FileUploadInputComponentBuilder.cs @@ -39,7 +39,7 @@ public FileUploadInputComponentBuilder WithMinValues(int minValues) /// /// Sets . /// - /// New value of the . + /// New value of the . /// /// The builder instance. /// diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/RoleSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/RoleSelectInputComponentBuilder.cs index 658972b00b..af30feefe5 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/RoleSelectInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/RoleSelectInputComponentBuilder.cs @@ -45,7 +45,7 @@ public RoleSelectInputComponentBuilder AddDefaulValue(ulong roleId) /// /// Adds default values to . /// - /// The roles to add as a default value. + /// The roles to add as a default value. /// /// The builder instance. /// @@ -58,7 +58,7 @@ public RoleSelectInputComponentBuilder AddDefaultValues(params IRole[] roles) /// /// Adds default values to . /// - /// The roles to add as a default value. + /// The roles to add as a default value. /// /// The builder instance. /// diff --git a/src/Discord.Net.Interactions/Info/InputComponents/TextInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/TextInputComponentInfo.cs index 6831c7953d..f0c481c945 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/TextInputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/InputComponents/TextInputComponentInfo.cs @@ -8,7 +8,7 @@ namespace Discord.Interactions public class TextInputComponentInfo : InputComponentInfo { /// - /// true when overrides . + /// true when overrides . /// internal bool TypeOverridesToString => _typeOverridesToString.Value; private readonly Lazy _typeOverridesToString; diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index 3d2ec71326..82a7b4e7bb 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -1149,7 +1149,7 @@ public bool TryRemoveGenericModalComponentTypeConverter(out Type converterTyp TryRemoveGenericModalComponentTypeConverter(typeof(T), out converterType); /// - /// Removes a generic for the type . + /// Removes a generic for the type . /// /// /// Removing a from the will not dereference the from the loaded module/command instances. From 7d039f4dd0ba4ee4466c1d8e49f3c8a360f7f533 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Wed, 12 Nov 2025 23:47:07 +0100 Subject: [PATCH 46/59] add interaction parameter to modal component typeconverter write method --- .../Extensions/IDiscordInteractionExtensions.cs | 8 ++++---- .../DefaultArrayModalComponentConverter.cs | 2 +- .../DefaultValueModalComponentConverter.cs | 2 +- .../ModalComponents/EnumModalComponentConverter.cs | 2 +- .../ModalComponents/ModalComponentTypeConverter.cs | 2 +- .../ModalComponents/NullableModalComponentConverter.cs | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs index 7922e7606d..43d07d9058 100644 --- a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs +++ b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs @@ -86,7 +86,7 @@ private static async Task SendModalResponseAsync(IDiscordInteraction interact if (modalInstance != null) { - await textComponent.TypeConverter.WriteAsync(inputBuilder, textComponent, textComponent.Getter(modalInstance)); + await textComponent.TypeConverter.WriteAsync(inputBuilder, interaction, textComponent, textComponent.Getter(modalInstance)); } var labelBuilder = new LabelBuilder(textComponent.Label, inputBuilder, textComponent.Description); @@ -99,7 +99,7 @@ private static async Task SendModalResponseAsync(IDiscordInteraction interact if (modalInstance != null) { - await selectMenuComponent.TypeConverter.WriteAsync(inputBuilder, selectMenuComponent, selectMenuComponent.Getter(modalInstance)); + await selectMenuComponent.TypeConverter.WriteAsync(inputBuilder, interaction, selectMenuComponent, selectMenuComponent.Getter(modalInstance)); } var labelBuilder = new LabelBuilder(selectMenuComponent.Label, inputBuilder, selectMenuComponent.Description); @@ -112,7 +112,7 @@ private static async Task SendModalResponseAsync(IDiscordInteraction interact if (modalInstance != null) { - await snowflakeSelectComponent.TypeConverter.WriteAsync(inputBuilder, snowflakeSelectComponent, snowflakeSelectComponent.Getter(modalInstance)); + await snowflakeSelectComponent.TypeConverter.WriteAsync(inputBuilder, interaction, snowflakeSelectComponent, snowflakeSelectComponent.Getter(modalInstance)); } var labelBuilder = new LabelBuilder(snowflakeSelectComponent.Label, inputBuilder, snowflakeSelectComponent.Description); @@ -125,7 +125,7 @@ private static async Task SendModalResponseAsync(IDiscordInteraction interact if (modalInstance != null) { - await fileUploadComponent.TypeConverter.WriteAsync(inputBuilder, fileUploadComponent, fileUploadComponent.Getter(modalInstance)); + await fileUploadComponent.TypeConverter.WriteAsync(inputBuilder, interaction, fileUploadComponent, fileUploadComponent.Getter(modalInstance)); } var labelBuilder = new LabelBuilder(fileUploadComponent.Label, inputBuilder, fileUploadComponent.Description); diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs index 98ed224fe7..21cf1dbfc4 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs @@ -135,7 +135,7 @@ public override async Task ReadAsync(IInteractionContext co return TypeConverterResult.FromSuccess(destination); } - public override Task WriteAsync(TBuilder builder, InputComponentInfo component, object value) + public override Task WriteAsync(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value) { if (builder is FileUploadComponentBuilder) return Task.CompletedTask; diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs index 33a40e01c2..8f05838d31 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs @@ -24,7 +24,7 @@ public override Task ReadAsync(IInteractionContext context, } } - public override Task WriteAsync(TBuilder builder, InputComponentInfo component, object value) + public override Task WriteAsync(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value) { var strValue = Convert.ToString(value); diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs index 535cfa2b0e..f695cbd42d 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs @@ -47,7 +47,7 @@ public override Task ReadAsync(IInteractionContext context, return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Value {option.Value} cannot be converted to {typeof(T).FullName}")); } - public override Task WriteAsync(TBuilder builder, InputComponentInfo component, object value) + public override Task WriteAsync(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value) { if (builder is not SelectMenuBuilder selectMenu || component.ComponentType is not ComponentType.SelectMenu) throw new InvalidOperationException($"{nameof(EnumModalComponentConverter)} can only write to select menu components."); diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/ModalComponentTypeConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/ModalComponentTypeConverter.cs index 8b9ef3d137..56454b8262 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/ModalComponentTypeConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/ModalComponentTypeConverter.cs @@ -27,7 +27,7 @@ public abstract class ModalComponentTypeConverter : ITypeConverter /// Will be used to manipulate the outgoing modal component, before the modal gets sent to Discord. /// - public virtual Task WriteAsync(TBuilder builder, InputComponentInfo component, object value) + public virtual Task WriteAsync(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value) where TBuilder : class, IInteractableComponentBuilder => Task.CompletedTask; diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/NullableModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/NullableModalComponentConverter.cs index 16156f5c26..df6e47aff3 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/NullableModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/NullableModalComponentConverter.cs @@ -20,6 +20,6 @@ public NullableModalComponentConverter(InteractionService interactionService, IS public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) => string.IsNullOrEmpty(option.Value) ? Task.FromResult(TypeConverterResult.FromSuccess(null)) : _typeConverter.ReadAsync(context, option, services); - public override Task WriteAsync(TBuilder builder, InputComponentInfo component, object value) - => _typeConverter.WriteAsync(builder, component, value); + public override Task WriteAsync(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value) + => _typeConverter.WriteAsync(builder, interaction, component, value); } From d4ff359243cd3fa54bd60508991cb6a142f3ad3b Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Thu, 13 Nov 2025 00:23:28 +0100 Subject: [PATCH 47/59] add null check to select menu option attribute --- src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index 483977f00f..ac9c2b618a 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -717,12 +717,16 @@ private static void BuildSelectMenuInput(SelectMenuInputComponentBuilder builder builder.Description = inputLabel.Description; break; case ModalSelectMenuOptionAttribute selectMenuOption: + + if (!Emote.TryParse(selectMenuOption.Emote, out var emote) && !string.IsNullOrEmpty(selectMenuOption.Emote)) + throw new ArgumentException($"Invalid emote format on {propertyInfo.DeclaringType} modal, {propertyInfo.Name} property"); + builder.AddOption(new SelectMenuOptionBuilder { Label = selectMenuOption.Label, Description = selectMenuOption.Description, Value = selectMenuOption.Value, - Emote = Emote.Parse(selectMenuOption.Emote), + Emote = emote, IsDefault = selectMenuOption.IsDefault }); break; From cb21cbeb469ae57b16b985e1357e24b63d2c89e3 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Thu, 13 Nov 2025 00:23:53 +0100 Subject: [PATCH 48/59] add null check to default value modalTypeConverter write method --- .../ModalComponents/DefaultValueModalComponentConverter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs index 8f05838d31..d702a6e012 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultValueModalComponentConverter.cs @@ -28,6 +28,9 @@ public override Task WriteAsync(TBuilder builder, IDiscordInteraction { var strValue = Convert.ToString(value); + if(string.IsNullOrEmpty(strValue)) + return Task.CompletedTask; + switch (builder) { case TextInputBuilder textInput: From fa4d7a1ff209515adea9f999fa8a0af58341860e Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Thu, 13 Nov 2025 11:24:53 +0100 Subject: [PATCH 49/59] make ctors of modal component base attributes internal --- .../Attributes/Modals/ModalComponentAttribute.cs | 2 ++ .../Attributes/Modals/ModalInputAttribute.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs index 6ac33f3dea..11317f9a17 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs @@ -12,4 +12,6 @@ public abstract class ModalComponentAttribute : Attribute /// Gets the type of the component. /// public abstract ComponentType ComponentType { get; } + + internal ModalComponentAttribute() { } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs index 960fc75d49..311ec188a8 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs @@ -18,7 +18,7 @@ public abstract class ModalInputAttribute : ModalComponentAttribute /// Create a new . /// /// The custom id of the input. - protected ModalInputAttribute(string customId) + internal ModalInputAttribute(string customId) { CustomId = customId; } From d2b66cab73187984686f1a21de6d1e44d3169be4 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Thu, 13 Nov 2025 11:25:36 +0100 Subject: [PATCH 50/59] implement predicate to hide attribute and enum modalcomponent typeconverter --- .../Attributes/HideAttribute.cs | 17 +++++++++++++++-- .../EnumModalComponentConverter.cs | 13 ++++++------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/Discord.Net.Interactions/Attributes/HideAttribute.cs b/src/Discord.Net.Interactions/Attributes/HideAttribute.cs index 9e6f9a2835..8347799353 100644 --- a/src/Discord.Net.Interactions/Attributes/HideAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/HideAttribute.cs @@ -6,7 +6,20 @@ namespace Discord.Interactions; /// Enum values tagged with this attribute will not be displayed as a parameter choice /// /// -/// This attribute must be used along with the default +/// This attribute must be used along with the default and . /// [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)] -public sealed class HideAttribute : Attribute { } +public class HideAttribute : Attribute +{ + /// + /// Can be optionally implemented by inherited types to conditionally hide an enum value. + /// + /// + /// Only runs on prior to modal construction. For slash command parameters, this method is ignored. + /// + /// Interaction that is called on. + /// + /// if the attribute should be active and hide the value. + /// + public virtual bool Predicate(IDiscordInteraction interaction) => true; +} diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs index f695cbd42d..9af73e0246 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs @@ -10,22 +10,21 @@ internal sealed class EnumModalComponentConverter : ModalComponentTypeConvert where T : struct, Enum { private readonly bool _isFlags; - private readonly ImmutableArray _options; + private readonly ImmutableArray<(SelectMenuOptionBuilder Option, Predicate Predicate)> _options; public EnumModalComponentConverter() { var names = Enum.GetNames(typeof(T)); - var members = names.SelectMany(x => typeof(T).GetMember(x)).Where(x => !x.IsDefined(typeof(HideAttribute), true)); - - if (members.Count() > SelectMenuBuilder.MaxOptionCount) - throw new InvalidOperationException($"Enum type {typeof(T).FullName} has too many visible members to be used in a select menu. Maximum visible members is {SelectMenuBuilder.MaxOptionCount}, but {members.Count()} are visible."); + var members = names.SelectMany(x => typeof(T).GetMember(x)); _isFlags = typeof(T).GetCustomAttribute() is not null; _options = members.Select(x => { var selectMenuOptionAttr = x.GetCustomAttribute(); - return new SelectMenuOptionBuilder(x.GetCustomAttribute()?.Name ?? x.Name, x.Name, selectMenuOptionAttr?.Description, selectMenuOptionAttr?.Emote != null ? Emote.Parse(selectMenuOptionAttr?.Emote) : null, selectMenuOptionAttr?.IsDefault); + var hideAttr = x.GetCustomAttribute(); + Predicate predicate = hideAttr != null ? hideAttr.Predicate : null; + return (new SelectMenuOptionBuilder(x.GetCustomAttribute()?.Name ?? x.Name, x.Name, selectMenuOptionAttr?.Description, selectMenuOptionAttr?.Emote != null ? Emote.Parse(selectMenuOptionAttr?.Emote) : null, selectMenuOptionAttr?.IsDefault), predicate); }).ToImmutableArray(); } @@ -55,7 +54,7 @@ public override Task WriteAsync(TBuilder builder, IDiscordInteraction if (selectMenu.MaxValues > 1 && !_isFlags) throw new InvalidOperationException($"Enum type {typeof(T).FullName} is not a [Flags] enum, so it cannot be used in a multi-select menu."); - selectMenu.WithOptions(_options.ToList()); + selectMenu.WithOptions([.. _options.Where(x => !x.Predicate?.Invoke(interaction) ?? true).Select(x => x.Option)]); return Task.CompletedTask; } From a5acfb337b641ffd01f812d0fc25da004357b7de Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Thu, 13 Nov 2025 11:32:34 +0100 Subject: [PATCH 51/59] fix HideAttribute inline docs build errors --- src/Discord.Net.Interactions/Attributes/HideAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Interactions/Attributes/HideAttribute.cs b/src/Discord.Net.Interactions/Attributes/HideAttribute.cs index 8347799353..4dcec6f5c0 100644 --- a/src/Discord.Net.Interactions/Attributes/HideAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/HideAttribute.cs @@ -19,7 +19,7 @@ public class HideAttribute : Attribute /// /// Interaction that is called on. /// - /// if the attribute should be active and hide the value. + /// if the attribute should be active and hide the value. /// public virtual bool Predicate(IDiscordInteraction interaction) => true; } From 56ba54f85b3ffdbb6e11fe4cb66521d2db27025b Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Thu, 13 Nov 2025 12:24:06 +0100 Subject: [PATCH 52/59] simplify naming of the component classes and normalize namespaces --- ...bute.cs => ModalChannelSelectAttribute.cs} | 10 ++--- .../Modals/ModalComponentAttribute.cs | 2 +- ...tribute.cs => ModalFileUploadAttribute.cs} | 6 +-- .../Attributes/Modals/ModalInputAttribute.cs | 1 - ....cs => ModalMentionableSelectAttribute.cs} | 8 ++-- ...tribute.cs => ModalRoleSelectAttribute.cs} | 10 ++--- ...te.cs => ModalSelectComponentAttribute.cs} | 4 +- ...tribute.cs => ModalSelectMenuAttribute.cs} | 10 ++--- .../Modals/ModalSelectMenuOptionAttribute.cs | 7 +++- .../Modals/ModalTextDisplayAttribute.cs | 2 - ...tribute.cs => ModalUserSelectAttribute.cs} | 10 ++--- .../ChannelSelectComponentBuilder.cs} | 28 ++++++------- .../FileUploadComponentBuilder.cs} | 16 ++++---- .../IInputComponentBuilder.cs | 0 .../IModalComponentBuilder.cs | 0 .../ISnowflakeSelectComponentBuilder.cs} | 12 +++--- .../InputComponentBuilder.cs | 0 .../MentionableSelectComponentBuilder.cs} | 28 ++++++------- .../{ => Components}/ModalComponentBuilder.cs | 0 .../RoleSelectComponentBuilder.cs} | 28 ++++++------- .../SelectMenuComponentBuilder.cs} | 16 ++++---- .../SnowflakeSelectComponentBuilder.cs} | 22 +++++----- .../TextDisplayComponentBuilder.cs | 0 .../TextInputComponentBuilder.cs | 0 .../UserSelectComponentBuilder.cs} | 28 ++++++------- .../Builders/Modals/ModalBuilder.cs | 24 +++++------ .../Builders/ModuleClassBuilder.cs | 32 +++++++-------- .../Entities/ISelectMenuModel.cs | 11 ----- .../IDiscordInteractionExtensions.cs | 6 +-- .../Components/ChannelSelectComponentInfo.cs | 9 +++++ .../FileUploadComponentInfo.cs} | 4 +- .../InputComponentInfo.cs | 0 .../MentionableSelectComponentInfo.cs | 9 +++++ .../{ => Components}/ModalComponentInfo.cs | 0 .../Components/RoleSelectComponentInfo.cs | 9 +++++ .../SelectMenuComponentInfo.cs} | 4 +- .../SnowflakeSelectComponentInfo.cs} | 4 +- .../TextInputComponentInfo.cs | 0 .../Components/UserSelectComponentInfo.cs | 9 +++++ .../ChannelSelectInputComponentInfo.cs | 9 ----- .../MentionableSelectInputComponentInfo.cs | 9 ----- .../RoleSelectInputComponentInfo.cs | 9 ----- .../UserSelectInputComponentInfo.cs | 9 ----- .../Info/ModalInfo.cs | 40 +++++++++---------- .../EnumModalComponentConverter.cs | 15 +++++++ 45 files changed, 227 insertions(+), 233 deletions(-) rename src/Discord.Net.Interactions/Attributes/Modals/{ModalChannelSelectInputAttribute.cs => ModalChannelSelectAttribute.cs} (52%) rename src/Discord.Net.Interactions/Attributes/Modals/{ModalFileUploadInputAttribute.cs => ModalFileUploadAttribute.cs} (78%) rename src/Discord.Net.Interactions/Attributes/Modals/{ModalMentionableSelectInputAttribute.cs => ModalMentionableSelectAttribute.cs} (63%) rename src/Discord.Net.Interactions/Attributes/Modals/{ModalRoleSelectInputAttribute.cs => ModalRoleSelectAttribute.cs} (59%) rename src/Discord.Net.Interactions/Attributes/Modals/{ModalSelectInputAttribute.cs => ModalSelectComponentAttribute.cs} (78%) rename src/Discord.Net.Interactions/Attributes/Modals/{ModalSelectMenuInputAttribute.cs => ModalSelectMenuAttribute.cs} (58%) rename src/Discord.Net.Interactions/Attributes/Modals/{ModalUserSelectInputAttribute.cs => ModalUserSelectAttribute.cs} (59%) rename src/Discord.Net.Interactions/Builders/Modals/{Inputs/ChannelSelectInputComponentBuilder.cs => Components/ChannelSelectComponentBuilder.cs} (55%) rename src/Discord.Net.Interactions/Builders/Modals/{Inputs/FileUploadInputComponentBuilder.cs => Components/FileUploadComponentBuilder.cs} (62%) rename src/Discord.Net.Interactions/Builders/Modals/{Inputs => Components}/IInputComponentBuilder.cs (100%) rename src/Discord.Net.Interactions/Builders/Modals/{ => Components}/IModalComponentBuilder.cs (100%) rename src/Discord.Net.Interactions/Builders/Modals/{Inputs/ISnowflakeSelectInputComponentBuilder.cs => Components/ISnowflakeSelectComponentBuilder.cs} (80%) rename src/Discord.Net.Interactions/Builders/Modals/{Inputs => Components}/InputComponentBuilder.cs (100%) rename src/Discord.Net.Interactions/Builders/Modals/{Inputs/MentionableSelectInputComponentBuilder.cs => Components/MentionableSelectComponentBuilder.cs} (60%) rename src/Discord.Net.Interactions/Builders/Modals/{ => Components}/ModalComponentBuilder.cs (100%) rename src/Discord.Net.Interactions/Builders/Modals/{Inputs/RoleSelectInputComponentBuilder.cs => Components/RoleSelectComponentBuilder.cs} (55%) rename src/Discord.Net.Interactions/Builders/Modals/{Inputs/SelectMenuInputComponentBuilder.cs => Components/SelectMenuComponentBuilder.cs} (70%) rename src/Discord.Net.Interactions/Builders/Modals/{Inputs/SnowflakeSelectInputComponentBuilder.cs => Components/SnowflakeSelectComponentBuilder.cs} (72%) rename src/Discord.Net.Interactions/Builders/Modals/{ => Components}/TextDisplayComponentBuilder.cs (100%) rename src/Discord.Net.Interactions/Builders/Modals/{Inputs => Components}/TextInputComponentBuilder.cs (100%) rename src/Discord.Net.Interactions/Builders/Modals/{Inputs/UserSelectInputComponentBuilder.cs => Components/UserSelectComponentBuilder.cs} (53%) delete mode 100644 src/Discord.Net.Interactions/Entities/ISelectMenuModel.cs create mode 100644 src/Discord.Net.Interactions/Info/Components/ChannelSelectComponentInfo.cs rename src/Discord.Net.Interactions/Info/{InputComponents/FileUploadInputComponentInfo.cs => Components/FileUploadComponentInfo.cs} (73%) rename src/Discord.Net.Interactions/Info/{InputComponents => Components}/InputComponentInfo.cs (100%) create mode 100644 src/Discord.Net.Interactions/Info/Components/MentionableSelectComponentInfo.cs rename src/Discord.Net.Interactions/Info/{ => Components}/ModalComponentInfo.cs (100%) create mode 100644 src/Discord.Net.Interactions/Info/Components/RoleSelectComponentInfo.cs rename src/Discord.Net.Interactions/Info/{InputComponents/SelectMenuInputComponentInfo.cs => Components/SelectMenuComponentInfo.cs} (84%) rename src/Discord.Net.Interactions/Info/{InputComponents/SnowflakeSelectInputComponentInfo.cs => Components/SnowflakeSelectComponentInfo.cs} (86%) rename src/Discord.Net.Interactions/Info/{InputComponents => Components}/TextInputComponentInfo.cs (100%) create mode 100644 src/Discord.Net.Interactions/Info/Components/UserSelectComponentInfo.cs delete mode 100644 src/Discord.Net.Interactions/Info/InputComponents/ChannelSelectInputComponentInfo.cs delete mode 100644 src/Discord.Net.Interactions/Info/InputComponents/MentionableSelectInputComponentInfo.cs delete mode 100644 src/Discord.Net.Interactions/Info/InputComponents/RoleSelectInputComponentInfo.cs delete mode 100644 src/Discord.Net.Interactions/Info/InputComponents/UserSelectInputComponentInfo.cs diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectAttribute.cs similarity index 52% rename from src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectInputAttribute.cs rename to src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectAttribute.cs index 798ddf051f..552f641e6d 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectAttribute.cs @@ -1,18 +1,16 @@ -namespace Discord.Interactions.Attributes.Modals; +namespace Discord.Interactions; /// /// Marks a property as a channel select. /// -public class ModalChannelSelectInputAttribute : ModalSelectInputAttribute +public class ModalChannelSelectAttribute : ModalSelectComponentAttribute { /// public override ComponentType ComponentType => ComponentType.ChannelSelect; /// - /// Create a new . + /// Create a new . /// /// Custom ID of the channel select component. - public ModalChannelSelectInputAttribute(string customId) : base(customId) - { - } + public ModalChannelSelectAttribute(string customId) : base(customId) { } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs index 11317f9a17..7b6ce3314d 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace Discord.Interactions.Attributes.Modals; +namespace Discord.Interactions; /// /// Mark an property as a modal component field. diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadAttribute.cs similarity index 78% rename from src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadInputAttribute.cs rename to src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadAttribute.cs index 1a6e141041..5271b64a24 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadAttribute.cs @@ -3,7 +3,7 @@ namespace Discord.Interactions; /// /// Marks a property as a file upload input. /// -public class ModalFileUploadInputAttribute : ModalInputAttribute +public class ModalFileUploadAttribute : ModalInputAttribute { /// public override ComponentType ComponentType => ComponentType.FileUpload; @@ -19,12 +19,12 @@ public class ModalFileUploadInputAttribute : ModalInputAttribute public int MaxValues { get; set; } = 1; /// - /// Create a new . + /// Create a new . /// /// Custom ID of the file upload component. /// Minimum number of files that can be uploaded. /// Maximum number of files that can be uploaded. - public ModalFileUploadInputAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId) + public ModalFileUploadAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId) { MinValues = minValues; MaxValues = maxValues; diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs index 311ec188a8..5829b363f8 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs @@ -1,4 +1,3 @@ -using Discord.Interactions.Attributes.Modals; using System; namespace Discord.Interactions diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectAttribute.cs similarity index 63% rename from src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectInputAttribute.cs rename to src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectAttribute.cs index 44a6501480..57a0b1b991 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectAttribute.cs @@ -3,18 +3,16 @@ namespace Discord.Interactions; /// /// Marks a property as a mentionable select input. /// -public class ModalMentionableSelectInputAttribute : ModalSelectInputAttribute +public class ModalMentionableSelectAttribute : ModalSelectComponentAttribute { /// public override ComponentType ComponentType => ComponentType.MentionableSelect; /// - /// Create a new . + /// Create a new . /// /// Custom ID of the mentionable select component. /// Minimum number of values that can be selected. /// Maximum number of values that can be selected - public ModalMentionableSelectInputAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) - { - } + public ModalMentionableSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectAttribute.cs similarity index 59% rename from src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectInputAttribute.cs rename to src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectAttribute.cs index dd0e6622a3..3808be32c5 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectAttribute.cs @@ -1,20 +1,18 @@ -namespace Discord.Interactions.Attributes.Modals; +namespace Discord.Interactions; /// /// Marks a property as a role select input. /// -public class ModalRoleSelectInputAttribute : ModalSelectInputAttribute +public class ModalRoleSelectAttribute : ModalSelectComponentAttribute { /// public override ComponentType ComponentType => ComponentType.RoleSelect; /// - /// Create a new . + /// Create a new . /// /// Custom ID of the role select component. /// Minimum number of values that can be selected. /// Maximum number of values that can be selected. - public ModalRoleSelectInputAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) - { - } + public ModalRoleSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectComponentAttribute.cs similarity index 78% rename from src/Discord.Net.Interactions/Attributes/Modals/ModalSelectInputAttribute.cs rename to src/Discord.Net.Interactions/Attributes/Modals/ModalSelectComponentAttribute.cs index 66b43f7639..ccbc5891b8 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectComponentAttribute.cs @@ -3,7 +3,7 @@ namespace Discord.Interactions; /// /// Base attribute for select-menu, user, channel, role, and mentionable select inputs in modals. /// -public abstract class ModalSelectInputAttribute : ModalInputAttribute +public abstract class ModalSelectComponentAttribute : ModalInputAttribute { /// /// Gets or sets the minimum number of values that can be selected. @@ -20,7 +20,7 @@ public abstract class ModalSelectInputAttribute : ModalInputAttribute /// public string Placeholder { get; set; } - internal ModalSelectInputAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId) + internal ModalSelectComponentAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId) { MinValues = minValues; MaxValues = maxValues; diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuAttribute.cs similarity index 58% rename from src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuInputAttribute.cs rename to src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuAttribute.cs index 60f42b4cb0..5d70eb0c8b 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuAttribute.cs @@ -1,20 +1,18 @@ -namespace Discord.Interactions.Attributes.Modals; +namespace Discord.Interactions; /// /// Marks a property as a select menu input. /// -public sealed class ModalSelectMenuInputAttribute : ModalSelectInputAttribute +public sealed class ModalSelectMenuAttribute : ModalSelectComponentAttribute { /// public override ComponentType ComponentType => ComponentType.SelectMenu; /// - /// Create a new . + /// Create a new . /// /// Custom ID of the select menu component. /// Minimum number of values that can be selected. /// Maximum number of values that can be selected. - public ModalSelectMenuInputAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) - { - } + public ModalSelectMenuAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuOptionAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuOptionAttribute.cs index dfe46ddd18..7790119ab4 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuOptionAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuOptionAttribute.cs @@ -1,10 +1,13 @@ using System; -namespace Discord.Interactions.Attributes.Modals; +namespace Discord.Interactions; /// /// Adds a select menu option to the marked field. /// +/// +/// To add additional metadata to enum fields, use instead. +/// [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] public class ModalSelectMenuOptionAttribute : Attribute { @@ -34,7 +37,7 @@ public class ModalSelectMenuOptionAttribute : Attribute public bool IsDefault { get; set; } /// - /// Create a new . + /// Create a new . /// /// Label of the option. /// Value of the option. diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs index 29b3db2ada..300db32b24 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs @@ -1,5 +1,3 @@ -using Discord.Interactions.Attributes.Modals; - namespace Discord.Interactions; /// diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectAttribute.cs similarity index 59% rename from src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectInputAttribute.cs rename to src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectAttribute.cs index 79784e0898..cc3cf28802 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectAttribute.cs @@ -1,20 +1,18 @@ -namespace Discord.Interactions.Attributes.Modals; +namespace Discord.Interactions; /// /// Marks a property as a user select input. /// -public class ModalUserSelectInputAttribute : ModalSelectInputAttribute +public class ModalUserSelectAttribute : ModalSelectComponentAttribute { /// public override ComponentType ComponentType => ComponentType.UserSelect; /// - /// Create a new . + /// Create a new . /// /// Custom ID of the user select component. /// Minimum number of values that can be selected. /// Maximum number of values that can be selected. - public ModalUserSelectInputAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) - { - } + public ModalUserSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { } } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/ChannelSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/ChannelSelectComponentBuilder.cs similarity index 55% rename from src/Discord.Net.Interactions/Builders/Modals/Inputs/ChannelSelectInputComponentBuilder.cs rename to src/Discord.Net.Interactions/Builders/Modals/Components/ChannelSelectComponentBuilder.cs index 945fcbd022..d91075027d 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/ChannelSelectInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Components/ChannelSelectComponentBuilder.cs @@ -4,70 +4,70 @@ namespace Discord.Interactions.Builders; /// -/// Represents a builder for creating . +/// Represents a builder for creating . /// -public class ChannelSelectInputComponentBuilder : SnowflakeSelectInputComponentBuilder +public class ChannelSelectComponentBuilder : SnowflakeSelectComponentBuilder { - protected override ChannelSelectInputComponentBuilder Instance => this; + protected override ChannelSelectComponentBuilder Instance => this; /// - /// Initializes a new . + /// Initializes a new . /// /// Parent modal of this component. - public ChannelSelectInputComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.ChannelSelect) { } + public ChannelSelectComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.ChannelSelect) { } /// - /// Adds a default value to . + /// Adds a default value to . /// /// The channel to add as a default value. /// /// The builder instance. /// - public ChannelSelectInputComponentBuilder AddDefaulValue(IChannel channel) + public ChannelSelectComponentBuilder AddDefaulValue(IChannel channel) { _defaultValues.Add(new SelectMenuDefaultValue(channel.Id, SelectDefaultValueType.Channel)); return this; } /// - /// Adds a default value to . + /// Adds a default value to . /// /// The channel ID to add as a default value. /// /// The builder instance. /// - public ChannelSelectInputComponentBuilder AddDefaulValue(ulong channelId) + public ChannelSelectComponentBuilder AddDefaulValue(ulong channelId) { _defaultValues.Add(new SelectMenuDefaultValue(channelId, SelectDefaultValueType.Channel)); return this; } /// - /// Adds default values to . + /// Adds default values to . /// /// The channels to add as a default value. /// /// The builder instance. /// - public ChannelSelectInputComponentBuilder AddDefaultValues(params IChannel[] channels) + public ChannelSelectComponentBuilder AddDefaultValues(params IChannel[] channels) { _defaultValues.AddRange(channels.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Channel))); return this; } /// - /// Adds default values to . + /// Adds default values to . /// /// The channels to add as a default value. /// /// The builder instance. /// - public ChannelSelectInputComponentBuilder AddDefaultValues(IEnumerable channels) + public ChannelSelectComponentBuilder AddDefaultValues(IEnumerable channels) { _defaultValues.AddRange(channels.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Channel))); return this; } - internal override ChannelSelectInputComponentInfo Build(ModalInfo modal) + internal override ChannelSelectComponentInfo Build(ModalInfo modal) => new(this, modal); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/FileUploadInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/FileUploadComponentBuilder.cs similarity index 62% rename from src/Discord.Net.Interactions/Builders/Modals/Inputs/FileUploadInputComponentBuilder.cs rename to src/Discord.Net.Interactions/Builders/Modals/Components/FileUploadComponentBuilder.cs index 699d133344..e004f34f5c 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/FileUploadInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Components/FileUploadComponentBuilder.cs @@ -1,11 +1,11 @@ namespace Discord.Interactions.Builders; /// -/// Represents a builder for creating . +/// Represents a builder for creating . /// -public class FileUploadInputComponentBuilder : InputComponentBuilder +public class FileUploadComponentBuilder : InputComponentBuilder { - protected override FileUploadInputComponentBuilder Instance => this; + protected override FileUploadComponentBuilder Instance => this; /// /// Gets and sets the minimum number of files that can be uploaded. @@ -18,10 +18,10 @@ public class FileUploadInputComponentBuilder : InputComponentBuilder - /// Initializes a new . + /// Initializes a new . /// /// - public FileUploadInputComponentBuilder(ModalBuilder modal) : base(modal) { } + public FileUploadComponentBuilder(ModalBuilder modal) : base(modal) { } /// /// Sets . @@ -30,7 +30,7 @@ public FileUploadInputComponentBuilder(ModalBuilder modal) : base(modal) { } /// /// The builder instance. /// - public FileUploadInputComponentBuilder WithMinValues(int minValues) + public FileUploadComponentBuilder WithMinValues(int minValues) { MinValues = minValues; return this; @@ -43,12 +43,12 @@ public FileUploadInputComponentBuilder WithMinValues(int minValues) /// /// The builder instance. /// - public FileUploadInputComponentBuilder WithMaxValues(int maxValues) + public FileUploadComponentBuilder WithMaxValues(int maxValues) { MaxValues = maxValues; return this; } - internal override FileUploadInputComponentInfo Build(ModalInfo modal) + internal override FileUploadComponentInfo Build(ModalInfo modal) => new (this, modal); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/IInputComponentBuilder.cs similarity index 100% rename from src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs rename to src/Discord.Net.Interactions/Builders/Modals/Components/IInputComponentBuilder.cs diff --git a/src/Discord.Net.Interactions/Builders/Modals/IModalComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/IModalComponentBuilder.cs similarity index 100% rename from src/Discord.Net.Interactions/Builders/Modals/IModalComponentBuilder.cs rename to src/Discord.Net.Interactions/Builders/Modals/Components/IModalComponentBuilder.cs diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/ISnowflakeSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/ISnowflakeSelectComponentBuilder.cs similarity index 80% rename from src/Discord.Net.Interactions/Builders/Modals/Inputs/ISnowflakeSelectInputComponentBuilder.cs rename to src/Discord.Net.Interactions/Builders/Modals/Components/ISnowflakeSelectComponentBuilder.cs index fd5b363c30..f310e4bb71 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/ISnowflakeSelectInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Components/ISnowflakeSelectComponentBuilder.cs @@ -3,9 +3,9 @@ namespace Discord.Interactions.Builders; /// -/// Represent a builder for creating . +/// Represent a builder for creating . /// -public interface ISnowflakeSelectInputComponentBuilder : IInputComponentBuilder +public interface ISnowflakeSelectComponentBuilder : IInputComponentBuilder { /// /// Gets the minimum number of values that can be selected. @@ -37,26 +37,26 @@ public interface ISnowflakeSelectInputComponentBuilder : IInputComponentBuilder /// /// Default value to be added. /// The builder instance. - ISnowflakeSelectInputComponentBuilder AddDefaultValue(SelectMenuDefaultValue defaultValue); + ISnowflakeSelectComponentBuilder AddDefaultValue(SelectMenuDefaultValue defaultValue); /// /// Sets . /// /// New value of the /// The builder instance. - ISnowflakeSelectInputComponentBuilder WithMinValues(int minValues); + ISnowflakeSelectComponentBuilder WithMinValues(int minValues); /// /// Sets . /// /// New value of the /// The builder instance. - ISnowflakeSelectInputComponentBuilder WithMaxValues(int maxValues); + ISnowflakeSelectComponentBuilder WithMaxValues(int maxValues); /// /// Sets . /// /// New value of the /// The builder instance. - ISnowflakeSelectInputComponentBuilder WithPlaceholder(string placeholder); + ISnowflakeSelectComponentBuilder WithPlaceholder(string placeholder); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/InputComponentBuilder.cs similarity index 100% rename from src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs rename to src/Discord.Net.Interactions/Builders/Modals/Components/InputComponentBuilder.cs diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/MentionableSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/MentionableSelectComponentBuilder.cs similarity index 60% rename from src/Discord.Net.Interactions/Builders/Modals/Inputs/MentionableSelectInputComponentBuilder.cs rename to src/Discord.Net.Interactions/Builders/Modals/Components/MentionableSelectComponentBuilder.cs index 1d16f3585e..41444eb3ef 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/MentionableSelectInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Components/MentionableSelectComponentBuilder.cs @@ -1,71 +1,71 @@ namespace Discord.Interactions.Builders; /// -/// Represents a builder for creating a . +/// Represents a builder for creating a . /// -public class MentionableSelectInputComponentBuilder : SnowflakeSelectInputComponentBuilder +public class MentionableSelectComponentBuilder : SnowflakeSelectComponentBuilder { - protected override MentionableSelectInputComponentBuilder Instance => this; + protected override MentionableSelectComponentBuilder Instance => this; /// - /// Initialize a new . + /// Initialize a new . /// /// Parent modal of this input component. - public MentionableSelectInputComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.MentionableSelect) { } + public MentionableSelectComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.MentionableSelect) { } /// - /// Adds a snowflake ID as a default value to . + /// Adds a snowflake ID as a default value to . /// /// The ID to add as a default value. /// Enitity type of the snowflake ID. /// /// The builder instance. /// - public MentionableSelectInputComponentBuilder AddDefaultValue(ulong id, SelectDefaultValueType type) + public MentionableSelectComponentBuilder AddDefaultValue(ulong id, SelectDefaultValueType type) { _defaultValues.Add(new SelectMenuDefaultValue(id, type)); return this; } /// - /// Adds a user as a default value to . + /// Adds a user as a default value to . /// /// The user to add as a default value. /// /// The builder instance. /// - public MentionableSelectInputComponentBuilder AddDefaultValue(IUser user) + public MentionableSelectComponentBuilder AddDefaultValue(IUser user) { _defaultValues.Add(new SelectMenuDefaultValue(user.Id, SelectDefaultValueType.User)); return this; } /// - /// Adds a channel as a default value to . + /// Adds a channel as a default value to . /// /// The channel to add as a default value. /// /// The builder instance. /// - public MentionableSelectInputComponentBuilder AddDefaultValue(IChannel channel) + public MentionableSelectComponentBuilder AddDefaultValue(IChannel channel) { _defaultValues.Add(new SelectMenuDefaultValue(channel.Id, SelectDefaultValueType.Channel)); return this; } /// - /// Adds a role as a default value to . + /// Adds a role as a default value to . /// /// The role to add as a default value. /// /// The builder instance. /// - public MentionableSelectInputComponentBuilder AddDefaulValue(IRole role) + public MentionableSelectComponentBuilder AddDefaulValue(IRole role) { _defaultValues.Add(new SelectMenuDefaultValue(role.Id, SelectDefaultValueType.Role)); return this; } - internal override MentionableSelectInputComponentInfo Build(ModalInfo modal) + internal override MentionableSelectComponentInfo Build(ModalInfo modal) => new(this, modal); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/ModalComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/ModalComponentBuilder.cs similarity index 100% rename from src/Discord.Net.Interactions/Builders/Modals/ModalComponentBuilder.cs rename to src/Discord.Net.Interactions/Builders/Modals/Components/ModalComponentBuilder.cs diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/RoleSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/RoleSelectComponentBuilder.cs similarity index 55% rename from src/Discord.Net.Interactions/Builders/Modals/Inputs/RoleSelectInputComponentBuilder.cs rename to src/Discord.Net.Interactions/Builders/Modals/Components/RoleSelectComponentBuilder.cs index af30feefe5..17b1f72f63 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/RoleSelectInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Components/RoleSelectComponentBuilder.cs @@ -4,70 +4,70 @@ namespace Discord.Interactions.Builders; /// -/// Represents a builder for creating a . +/// Represents a builder for creating a . /// -public class RoleSelectInputComponentBuilder : SnowflakeSelectInputComponentBuilder +public class RoleSelectComponentBuilder : SnowflakeSelectComponentBuilder { - protected override RoleSelectInputComponentBuilder Instance => this; + protected override RoleSelectComponentBuilder Instance => this; /// - /// Initialize a new . + /// Initialize a new . /// /// Parent modal of this input component. - public RoleSelectInputComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.RoleSelect) { } + public RoleSelectComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.RoleSelect) { } /// - /// Adds a default value to . + /// Adds a default value to . /// /// The role to add as a default value. /// /// The builder instance. /// - public RoleSelectInputComponentBuilder AddDefaulValue(IRole role) + public RoleSelectComponentBuilder AddDefaulValue(IRole role) { _defaultValues.Add(new SelectMenuDefaultValue(role.Id, SelectDefaultValueType.Role)); return this; } /// - /// Adds a default value to . + /// Adds a default value to . /// /// The role ID to add as a default value. /// /// The builder instance. /// - public RoleSelectInputComponentBuilder AddDefaulValue(ulong roleId) + public RoleSelectComponentBuilder AddDefaulValue(ulong roleId) { _defaultValues.Add(new SelectMenuDefaultValue(roleId, SelectDefaultValueType.Role)); return this; } /// - /// Adds default values to . + /// Adds default values to . /// /// The roles to add as a default value. /// /// The builder instance. /// - public RoleSelectInputComponentBuilder AddDefaultValues(params IRole[] roles) + public RoleSelectComponentBuilder AddDefaultValues(params IRole[] roles) { _defaultValues.AddRange(roles.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Role))); return this; } /// - /// Adds default values to . + /// Adds default values to . /// /// The roles to add as a default value. /// /// The builder instance. /// - public RoleSelectInputComponentBuilder AddDefaultValues(IEnumerable roles) + public RoleSelectComponentBuilder AddDefaultValues(IEnumerable roles) { _defaultValues.AddRange(roles.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Role))); return this; } - internal override RoleSelectInputComponentInfo Build(ModalInfo modal) + internal override RoleSelectComponentInfo Build(ModalInfo modal) => new(this, modal); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/SelectMenuInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/SelectMenuComponentBuilder.cs similarity index 70% rename from src/Discord.Net.Interactions/Builders/Modals/Inputs/SelectMenuInputComponentBuilder.cs rename to src/Discord.Net.Interactions/Builders/Modals/Components/SelectMenuComponentBuilder.cs index 6bef2ccf55..1826dec756 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/SelectMenuInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Components/SelectMenuComponentBuilder.cs @@ -4,13 +4,13 @@ namespace Discord.Interactions.Builders; /// -/// Represents a builder for creating . +/// Represents a builder for creating . /// -public class SelectMenuInputComponentBuilder : InputComponentBuilder +public class SelectMenuComponentBuilder : InputComponentBuilder { private readonly List _options; - protected override SelectMenuInputComponentBuilder Instance => this; + protected override SelectMenuComponentBuilder Instance => this; /// /// Gets and sets the placeholder for the select menu iput. @@ -33,10 +33,10 @@ public class SelectMenuInputComponentBuilder : InputComponentBuilder Options => _options; /// - /// Initialize a new . + /// Initialize a new . /// /// Parent modal of this component. - public SelectMenuInputComponentBuilder(ModalBuilder modal) : base(modal) + public SelectMenuComponentBuilder(ModalBuilder modal) : base(modal) { _options = new(); } @@ -46,7 +46,7 @@ public SelectMenuInputComponentBuilder(ModalBuilder modal) : base(modal) /// /// Option to be added to . /// The builder instance. - public SelectMenuInputComponentBuilder AddOption(SelectMenuOptionBuilder option) + public SelectMenuComponentBuilder AddOption(SelectMenuOptionBuilder option) { _options.Add(option); return this; @@ -57,7 +57,7 @@ public SelectMenuInputComponentBuilder AddOption(SelectMenuOptionBuilder option) /// /// Select menu option builder factory. /// The builder instance. - public SelectMenuInputComponentBuilder AddOption(Action configure) + public SelectMenuComponentBuilder AddOption(Action configure) { var builder = new SelectMenuOptionBuilder(); configure(builder); @@ -65,6 +65,6 @@ public SelectMenuInputComponentBuilder AddOption(Action return this; } - internal override SelectMenuInputComponentInfo Build(ModalInfo modal) + internal override SelectMenuComponentInfo Build(ModalInfo modal) => new(this, modal); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/SnowflakeSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/SnowflakeSelectComponentBuilder.cs similarity index 72% rename from src/Discord.Net.Interactions/Builders/Modals/Inputs/SnowflakeSelectInputComponentBuilder.cs rename to src/Discord.Net.Interactions/Builders/Modals/Components/SnowflakeSelectComponentBuilder.cs index 60e9c4c61b..8de18ad051 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/SnowflakeSelectInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Components/SnowflakeSelectComponentBuilder.cs @@ -4,13 +4,13 @@ namespace Discord.Interactions.Builders; /// -/// Represents a builder for creating . +/// Represents a builder for creating . /// -/// The this builder yields when built. -/// Inherited type. -public abstract class SnowflakeSelectInputComponentBuilder : InputComponentBuilder, ISnowflakeSelectInputComponentBuilder +/// The this builder yields when built. +/// Inherited type. +public abstract class SnowflakeSelectComponentBuilder : InputComponentBuilder, ISnowflakeSelectComponentBuilder where TInfo : InputComponentInfo - where TBuilder : InputComponentBuilder, ISnowflakeSelectInputComponentBuilder + where TBuilder : InputComponentBuilder, ISnowflakeSelectComponentBuilder { protected readonly List _defaultValues; @@ -43,11 +43,11 @@ public SelectDefaultValueType? DefaultValuesType } /// - /// Initialize a new . + /// Initialize a new . /// /// Parent modal of this input component. /// Type of this component. - public SnowflakeSelectInputComponentBuilder(ModalBuilder modal, ComponentType componentType) : base(modal) + public SnowflakeSelectComponentBuilder(ModalBuilder modal, ComponentType componentType) : base(modal) { ValidateComponentType(componentType); @@ -101,14 +101,14 @@ private void ValidateComponentType(ComponentType componentType) } /// - ISnowflakeSelectInputComponentBuilder ISnowflakeSelectInputComponentBuilder.AddDefaultValue(SelectMenuDefaultValue defaultValue) => AddDefaultValue(defaultValue); + ISnowflakeSelectComponentBuilder ISnowflakeSelectComponentBuilder.AddDefaultValue(SelectMenuDefaultValue defaultValue) => AddDefaultValue(defaultValue); /// - ISnowflakeSelectInputComponentBuilder ISnowflakeSelectInputComponentBuilder.WithMinValues(int minValues) => WithMinValues(minValues); + ISnowflakeSelectComponentBuilder ISnowflakeSelectComponentBuilder.WithMinValues(int minValues) => WithMinValues(minValues); /// - ISnowflakeSelectInputComponentBuilder ISnowflakeSelectInputComponentBuilder.WithMaxValues(int maxValues) => WithMaxValues(maxValues); + ISnowflakeSelectComponentBuilder ISnowflakeSelectComponentBuilder.WithMaxValues(int maxValues) => WithMaxValues(maxValues); /// - ISnowflakeSelectInputComponentBuilder ISnowflakeSelectInputComponentBuilder.WithPlaceholder(string placeholder) => WithPlaceholder(placeholder); + ISnowflakeSelectComponentBuilder ISnowflakeSelectComponentBuilder.WithPlaceholder(string placeholder) => WithPlaceholder(placeholder); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/TextDisplayComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/TextDisplayComponentBuilder.cs similarity index 100% rename from src/Discord.Net.Interactions/Builders/Modals/TextDisplayComponentBuilder.cs rename to src/Discord.Net.Interactions/Builders/Modals/Components/TextDisplayComponentBuilder.cs diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/TextInputComponentBuilder.cs similarity index 100% rename from src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs rename to src/Discord.Net.Interactions/Builders/Modals/Components/TextInputComponentBuilder.cs diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/UserSelectInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/UserSelectComponentBuilder.cs similarity index 53% rename from src/Discord.Net.Interactions/Builders/Modals/Inputs/UserSelectInputComponentBuilder.cs rename to src/Discord.Net.Interactions/Builders/Modals/Components/UserSelectComponentBuilder.cs index d48273a2f3..7c7876b0ce 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/UserSelectInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Components/UserSelectComponentBuilder.cs @@ -4,70 +4,70 @@ namespace Discord.Interactions.Builders; /// -/// Represents a builder for creating . +/// Represents a builder for creating . /// -public class UserSelectInputComponentBuilder : SnowflakeSelectInputComponentBuilder +public class UserSelectComponentBuilder : SnowflakeSelectComponentBuilder { - protected override UserSelectInputComponentBuilder Instance => this; + protected override UserSelectComponentBuilder Instance => this; /// - /// Initialize a new . + /// Initialize a new . /// /// Parent modal of this input component. - public UserSelectInputComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.UserSelect) { } + public UserSelectComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.UserSelect) { } /// - /// Adds a default value to . + /// Adds a default value to . /// /// The user to add as a default value. /// /// The builder instance. /// - public UserSelectInputComponentBuilder AddDefaulValue(IUser user) + public UserSelectComponentBuilder AddDefaulValue(IUser user) { _defaultValues.Add(new SelectMenuDefaultValue(user.Id, SelectDefaultValueType.User)); return this; } /// - /// Adds a default value to . + /// Adds a default value to . /// /// The user ID to add as a default value. /// /// The builder instance. /// - public UserSelectInputComponentBuilder AddDefaulValue(ulong userId) + public UserSelectComponentBuilder AddDefaulValue(ulong userId) { _defaultValues.Add(new SelectMenuDefaultValue(userId, SelectDefaultValueType.User)); return this; } /// - /// Adds default values to . + /// Adds default values to . /// /// The users to add as a default value. /// /// The builder instance. /// - public UserSelectInputComponentBuilder AddDefaultValues(params IUser[] users) + public UserSelectComponentBuilder AddDefaultValues(params IUser[] users) { _defaultValues.AddRange(users.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.User))); return this; } /// - /// Adds default values to . + /// Adds default values to . /// /// The users to add as a default value. /// /// The builder instance. /// - public UserSelectInputComponentBuilder AddDefaultValues(IEnumerable users) + public UserSelectComponentBuilder AddDefaultValues(IEnumerable users) { _defaultValues.AddRange(users.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.User))); return this; } - internal override UserSelectInputComponentInfo Build(ModalInfo modal) + internal override UserSelectComponentInfo Build(ModalInfo modal) => new(this, modal); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs index a6f8599264..419ceadb36 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs @@ -86,9 +86,9 @@ public ModalBuilder AddTextInputComponent(Action conf /// /// The builder instance. /// - public ModalBuilder AddSelectMenuInputComponent(Action configure) + public ModalBuilder AddSelectMenuInputComponent(Action configure) { - var builder = new SelectMenuInputComponentBuilder(this); + var builder = new SelectMenuComponentBuilder(this); configure(builder); _components.Add(builder); return this; @@ -101,9 +101,9 @@ public ModalBuilder AddSelectMenuInputComponent(Action /// The builder instance. /// - public ModalBuilder AddUserSelectInputComponent(Action configure) + public ModalBuilder AddUserSelectInputComponent(Action configure) { - var builder = new UserSelectInputComponentBuilder(this); + var builder = new UserSelectComponentBuilder(this); configure(builder); _components.Add(builder); return this; @@ -116,9 +116,9 @@ public ModalBuilder AddUserSelectInputComponent(Action /// The builder instance. /// - public ModalBuilder AddRoleSelectInputComponent(Action configure) + public ModalBuilder AddRoleSelectInputComponent(Action configure) { - var builder = new RoleSelectInputComponentBuilder(this); + var builder = new RoleSelectComponentBuilder(this); configure(builder); _components.Add(builder); return this; @@ -131,9 +131,9 @@ public ModalBuilder AddRoleSelectInputComponent(Action /// The builder instance. /// - public ModalBuilder AddMentionableSelectInputComponent(Action configure) + public ModalBuilder AddMentionableSelectInputComponent(Action configure) { - var builder = new MentionableSelectInputComponentBuilder(this); + var builder = new MentionableSelectComponentBuilder(this); configure(builder); _components.Add(builder); return this; @@ -146,9 +146,9 @@ public ModalBuilder AddMentionableSelectInputComponent(Action /// The builder instance. /// - public ModalBuilder AddChannelSelectInputComponent(Action configure) + public ModalBuilder AddChannelSelectInputComponent(Action configure) { - var builder = new ChannelSelectInputComponentBuilder(this); + var builder = new ChannelSelectComponentBuilder(this); configure(builder); _components.Add(builder); return this; @@ -161,9 +161,9 @@ public ModalBuilder AddChannelSelectInputComponent(Action /// The builder instance. /// - public ModalBuilder AddFileUploadInputComponent(Action configure) + public ModalBuilder AddFileUploadInputComponent(Action configure) { - var builder = new FileUploadInputComponentBuilder(this); + var builder = new FileUploadComponentBuilder(this); configure(builder); _components.Add(builder); return this; diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index ac9c2b618a..fd36f0f330 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -613,25 +613,25 @@ public static ModalInfo BuildModalInfo(Type modalType, InteractionService intera switch (componentType) { case ComponentType.TextInput: - builder.AddTextInputComponent(x => BuildTextInput(x, prop, prop.GetValue(instance))); + builder.AddTextInputComponent(x => BuildTextInputComponent(x, prop, prop.GetValue(instance))); break; case ComponentType.SelectMenu: - builder.AddSelectMenuInputComponent(x => BuildSelectMenuInput(x, prop, prop.GetValue(instance))); + builder.AddSelectMenuInputComponent(x => BuildSelectMenuComponent(x, prop, prop.GetValue(instance))); break; case ComponentType.UserSelect: - builder.AddUserSelectInputComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + builder.AddUserSelectInputComponent(x => BuildSnowflakeSelectComponent(x, prop, prop.GetValue(instance))); break; case ComponentType.RoleSelect: - builder.AddRoleSelectInputComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + builder.AddRoleSelectInputComponent(x => BuildSnowflakeSelectComponent(x, prop, prop.GetValue(instance))); break; case ComponentType.MentionableSelect: - builder.AddMentionableSelectInputComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + builder.AddMentionableSelectInputComponent(x => BuildSnowflakeSelectComponent(x, prop, prop.GetValue(instance))); break; case ComponentType.ChannelSelect: - builder.AddChannelSelectInputComponent(x => BuildSnowflakeSelectInput(x, prop, prop.GetValue(instance))); + builder.AddChannelSelectInputComponent(x => BuildSnowflakeSelectComponent(x, prop, prop.GetValue(instance))); break; case ComponentType.FileUpload: - builder.AddFileUploadInputComponent(x => BuildFileUploadInput(x, prop, prop.GetValue(instance))); + builder.AddFileUploadInputComponent(x => BuildFileUploadComponent(x, prop, prop.GetValue(instance))); break; case ComponentType.TextDisplay: builder.AddTextDisplayComponent(x => BuildTextDisplayComponent(x, prop, prop.GetValue(instance))); @@ -653,7 +653,7 @@ public static ModalInfo BuildModalInfo(Type modalType, InteractionService intera } } - private static void BuildTextInput(TextInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) + private static void BuildTextInputComponent(TextInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) { var attributes = propertyInfo.GetCustomAttributes(); @@ -689,7 +689,7 @@ private static void BuildTextInput(TextInputComponentBuilder builder, PropertyIn } } - private static void BuildSelectMenuInput(SelectMenuInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) + private static void BuildSelectMenuComponent(SelectMenuComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) { var attributes = propertyInfo.GetCustomAttributes(); @@ -702,7 +702,7 @@ private static void BuildSelectMenuInput(SelectMenuInputComponentBuilder builder { switch (attribute) { - case ModalSelectMenuInputAttribute selectMenuInput: + case ModalSelectMenuAttribute selectMenuInput: builder.CustomId = selectMenuInput.CustomId; builder.ComponentType = selectMenuInput.ComponentType; builder.MinValues = selectMenuInput.MinValues; @@ -737,9 +737,9 @@ private static void BuildSelectMenuInput(SelectMenuInputComponentBuilder builder } } - private static void BuildSnowflakeSelectInput(SnowflakeSelectInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) - where TInfo : SnowflakeSelectInputComponentInfo - where TBuilder : SnowflakeSelectInputComponentBuilder + private static void BuildSnowflakeSelectComponent(SnowflakeSelectComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) + where TInfo : SnowflakeSelectComponentInfo + where TBuilder : SnowflakeSelectComponentBuilder { var attributes = propertyInfo.GetCustomAttributes(); @@ -752,7 +752,7 @@ private static void BuildSnowflakeSelectInput(SnowflakeSelectIn { switch (attribute) { - case ModalSelectInputAttribute selectInput: + case ModalSelectComponentAttribute selectInput: builder.CustomId = selectInput.CustomId; builder.ComponentType = selectInput.ComponentType; builder.MinValues = selectInput.MinValues; @@ -773,7 +773,7 @@ private static void BuildSnowflakeSelectInput(SnowflakeSelectIn } } - private static void BuildFileUploadInput(FileUploadInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) + private static void BuildFileUploadComponent(FileUploadComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) { var attributes = propertyInfo.GetCustomAttributes(); @@ -786,7 +786,7 @@ private static void BuildFileUploadInput(FileUploadInputComponentBuilder builder { switch (attribute) { - case ModalFileUploadInputAttribute fileUploadInput: + case ModalFileUploadAttribute fileUploadInput: builder.CustomId = fileUploadInput.CustomId; builder.ComponentType = fileUploadInput.ComponentType; builder.MinValues = fileUploadInput.MinValues; diff --git a/src/Discord.Net.Interactions/Entities/ISelectMenuModel.cs b/src/Discord.Net.Interactions/Entities/ISelectMenuModel.cs deleted file mode 100644 index b2ceb57de8..0000000000 --- a/src/Discord.Net.Interactions/Entities/ISelectMenuModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Discord.Interactions.Entities; - -public interface ISelectMenuModel -{ - IList Values { get; } - - IEnumerable Options(IModal modal, IInteractionContext context, IServiceProvider services); -} diff --git a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs index 43d07d9058..5d2a0af335 100644 --- a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs +++ b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs @@ -93,7 +93,7 @@ private static async Task SendModalResponseAsync(IDiscordInteraction interact builder.AddLabel(labelBuilder); } break; - case SelectMenuInputComponentInfo selectMenuComponent: + 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); @@ -106,7 +106,7 @@ private static async Task SendModalResponseAsync(IDiscordInteraction interact builder.AddLabel(labelBuilder); } break; - case SnowflakeSelectInputComponentInfo snowflakeSelectComponent: + 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); @@ -119,7 +119,7 @@ private static async Task SendModalResponseAsync(IDiscordInteraction interact builder.AddLabel(labelBuilder); } break; - case FileUploadInputComponentInfo fileUploadComponent: + case FileUploadComponentInfo fileUploadComponent: { var inputBuilder = new FileUploadComponentBuilder(fileUploadComponent.CustomId, fileUploadComponent.MinValues, fileUploadComponent.MaxValues, fileUploadComponent.IsRequired); diff --git a/src/Discord.Net.Interactions/Info/Components/ChannelSelectComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/ChannelSelectComponentInfo.cs new file mode 100644 index 0000000000..6161a3c845 --- /dev/null +++ b/src/Discord.Net.Interactions/Info/Components/ChannelSelectComponentInfo.cs @@ -0,0 +1,9 @@ +namespace Discord.Interactions; + +/// +/// Represents the class for type. +/// +public class ChannelSelectComponentInfo : SnowflakeSelectComponentInfo +{ + internal ChannelSelectComponentInfo(Builders.ChannelSelectComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } +} diff --git a/src/Discord.Net.Interactions/Info/InputComponents/FileUploadInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/FileUploadComponentInfo.cs similarity index 73% rename from src/Discord.Net.Interactions/Info/InputComponents/FileUploadInputComponentInfo.cs rename to src/Discord.Net.Interactions/Info/Components/FileUploadComponentInfo.cs index 290bdc7940..41713e0c88 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/FileUploadInputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/Components/FileUploadComponentInfo.cs @@ -3,7 +3,7 @@ namespace Discord.Interactions; /// /// Represents the class for type. /// -public class FileUploadInputComponentInfo : InputComponentInfo +public class FileUploadComponentInfo : InputComponentInfo { /// /// Gets the minimum number of values that can be selected. @@ -15,7 +15,7 @@ public class FileUploadInputComponentInfo : InputComponentInfo /// public int MaxValues { get; } - internal FileUploadInputComponentInfo(Builders.FileUploadInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) + internal FileUploadComponentInfo(Builders.FileUploadComponentBuilder builder, ModalInfo modal) : base(builder, modal) { MinValues = builder.MinValues; MaxValues = builder.MaxValues; diff --git a/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/InputComponentInfo.cs similarity index 100% rename from src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs rename to src/Discord.Net.Interactions/Info/Components/InputComponentInfo.cs diff --git a/src/Discord.Net.Interactions/Info/Components/MentionableSelectComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/MentionableSelectComponentInfo.cs new file mode 100644 index 0000000000..498c500c0a --- /dev/null +++ b/src/Discord.Net.Interactions/Info/Components/MentionableSelectComponentInfo.cs @@ -0,0 +1,9 @@ +namespace Discord.Interactions; + +/// +/// Represents the class for type. +/// +public class MentionableSelectComponentInfo : SnowflakeSelectComponentInfo +{ + internal MentionableSelectComponentInfo(Builders.MentionableSelectComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } +} diff --git a/src/Discord.Net.Interactions/Info/ModalComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/ModalComponentInfo.cs similarity index 100% rename from src/Discord.Net.Interactions/Info/ModalComponentInfo.cs rename to src/Discord.Net.Interactions/Info/Components/ModalComponentInfo.cs diff --git a/src/Discord.Net.Interactions/Info/Components/RoleSelectComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/RoleSelectComponentInfo.cs new file mode 100644 index 0000000000..1f08b03768 --- /dev/null +++ b/src/Discord.Net.Interactions/Info/Components/RoleSelectComponentInfo.cs @@ -0,0 +1,9 @@ +namespace Discord.Interactions; + +/// +/// Represents the class for type. +/// +public class RoleSelectComponentInfo : SnowflakeSelectComponentInfo +{ + internal RoleSelectComponentInfo(Builders.RoleSelectComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } +} diff --git a/src/Discord.Net.Interactions/Info/InputComponents/SelectMenuInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/SelectMenuComponentInfo.cs similarity index 84% rename from src/Discord.Net.Interactions/Info/InputComponents/SelectMenuInputComponentInfo.cs rename to src/Discord.Net.Interactions/Info/Components/SelectMenuComponentInfo.cs index 8724960266..4ca3432300 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/SelectMenuInputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/Components/SelectMenuComponentInfo.cs @@ -7,7 +7,7 @@ namespace Discord.Interactions; /// /// Represents the class for type. /// -public class SelectMenuInputComponentInfo : InputComponentInfo +public class SelectMenuComponentInfo : InputComponentInfo { /// /// Gets the placeholder of the select menu input. @@ -29,7 +29,7 @@ public class SelectMenuInputComponentInfo : InputComponentInfo /// public IReadOnlyCollection Options { get; } - internal SelectMenuInputComponentInfo(Builders.SelectMenuInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) + internal SelectMenuComponentInfo(Builders.SelectMenuComponentBuilder builder, ModalInfo modal) : base(builder, modal) { Placeholder = builder.Placeholder; MinValues = builder.MinValues; diff --git a/src/Discord.Net.Interactions/Info/InputComponents/SnowflakeSelectInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/SnowflakeSelectComponentInfo.cs similarity index 86% rename from src/Discord.Net.Interactions/Info/InputComponents/SnowflakeSelectInputComponentInfo.cs rename to src/Discord.Net.Interactions/Info/Components/SnowflakeSelectComponentInfo.cs index b6b29d46d0..f5150954e2 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/SnowflakeSelectInputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/Components/SnowflakeSelectComponentInfo.cs @@ -6,7 +6,7 @@ namespace Discord.Interactions; /// /// Represents the base class for , , , type. /// -public abstract class SnowflakeSelectInputComponentInfo : InputComponentInfo +public abstract class SnowflakeSelectComponentInfo : InputComponentInfo { /// /// Gets the minimum number of values that can be selected. @@ -33,7 +33,7 @@ public abstract class SnowflakeSelectInputComponentInfo : InputComponentInfo /// public SelectDefaultValueType? DefaultValueType { get; } - internal SnowflakeSelectInputComponentInfo(Builders.ISnowflakeSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) + internal SnowflakeSelectComponentInfo(Builders.ISnowflakeSelectComponentBuilder builder, ModalInfo modal) : base(builder, modal) { MinValues = builder.MinValues; MaxValues = builder.MaxValues; diff --git a/src/Discord.Net.Interactions/Info/InputComponents/TextInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/TextInputComponentInfo.cs similarity index 100% rename from src/Discord.Net.Interactions/Info/InputComponents/TextInputComponentInfo.cs rename to src/Discord.Net.Interactions/Info/Components/TextInputComponentInfo.cs diff --git a/src/Discord.Net.Interactions/Info/Components/UserSelectComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/UserSelectComponentInfo.cs new file mode 100644 index 0000000000..a1b49ec34d --- /dev/null +++ b/src/Discord.Net.Interactions/Info/Components/UserSelectComponentInfo.cs @@ -0,0 +1,9 @@ +namespace Discord.Interactions; + +/// +/// Represents the class for type. +/// +public class UserSelectComponentInfo : SnowflakeSelectComponentInfo +{ + internal UserSelectComponentInfo(Builders.UserSelectComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } +} diff --git a/src/Discord.Net.Interactions/Info/InputComponents/ChannelSelectInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/ChannelSelectInputComponentInfo.cs deleted file mode 100644 index e16be578df..0000000000 --- a/src/Discord.Net.Interactions/Info/InputComponents/ChannelSelectInputComponentInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord.Interactions; - -/// -/// Represents the class for type. -/// -public class ChannelSelectInputComponentInfo : SnowflakeSelectInputComponentInfo -{ - internal ChannelSelectInputComponentInfo(Builders.ChannelSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } -} diff --git a/src/Discord.Net.Interactions/Info/InputComponents/MentionableSelectInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/MentionableSelectInputComponentInfo.cs deleted file mode 100644 index 31b2447bc2..0000000000 --- a/src/Discord.Net.Interactions/Info/InputComponents/MentionableSelectInputComponentInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord.Interactions; - -/// -/// Represents the class for type. -/// -public class MentionableSelectInputComponentInfo : SnowflakeSelectInputComponentInfo -{ - internal MentionableSelectInputComponentInfo(Builders.MentionableSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } -} diff --git a/src/Discord.Net.Interactions/Info/InputComponents/RoleSelectInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/RoleSelectInputComponentInfo.cs deleted file mode 100644 index 0afee9f06b..0000000000 --- a/src/Discord.Net.Interactions/Info/InputComponents/RoleSelectInputComponentInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord.Interactions; - -/// -/// Represents the class for type. -/// -public class RoleSelectInputComponentInfo : SnowflakeSelectInputComponentInfo -{ - internal RoleSelectInputComponentInfo(Builders.RoleSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } -} diff --git a/src/Discord.Net.Interactions/Info/InputComponents/UserSelectInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/UserSelectInputComponentInfo.cs deleted file mode 100644 index 4423b180fc..0000000000 --- a/src/Discord.Net.Interactions/Info/InputComponents/UserSelectInputComponentInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord.Interactions; - -/// -/// Represents the class for type. -/// -public class UserSelectInputComponentInfo : SnowflakeSelectInputComponentInfo -{ - internal UserSelectInputComponentInfo(Builders.UserSelectInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } -} diff --git a/src/Discord.Net.Interactions/Info/ModalInfo.cs b/src/Discord.Net.Interactions/Info/ModalInfo.cs index c0e5a7f0c0..2f9581b1b4 100644 --- a/src/Discord.Net.Interactions/Info/ModalInfo.cs +++ b/src/Discord.Net.Interactions/Info/ModalInfo.cs @@ -52,32 +52,32 @@ public class ModalInfo /// /// Get a collection of the select menu components of this modal. /// - public IReadOnlyCollection SelectMenuInputComponents { get; } + public IReadOnlyCollection SelectMenuComponents { get; } /// /// Get a collection of the user select components of this modal. /// - public IReadOnlyCollection UserSelectInputComponents { get; } + public IReadOnlyCollection UserSelectComponents { get; } /// /// Get a collection of the role select components of this modal. /// - public IReadOnlyCollection RoleSelectInputComponents { get; } + public IReadOnlyCollection RoleSelectComponents { get; } /// /// Get a collection of the mentionable select components of this modal. /// - public IReadOnlyCollection MentionableSelectInputComponents { get; } + public IReadOnlyCollection MentionableSelectComponents { get; } /// /// Get a collection of the channel select components of this modal. /// - public IReadOnlyCollection ChannelSelectInputComponents { get; } + public IReadOnlyCollection ChannelSelectComponents { get; } /// /// Get a collection of the file upload components of this modal. /// - public IReadOnlyCollection FileUploadInputComponents { get; } + public IReadOnlyCollection FileUploadComponents { get; } /// /// Gets a collection of the text display components of this modal. @@ -90,26 +90,26 @@ internal ModalInfo(Builders.ModalBuilder builder) Type = builder.Type; Components = builder.Components.Select(x => x switch { - TextInputComponentBuilder textComponent => textComponent.Build(this), - SelectMenuInputComponentBuilder selectMenuComponent => selectMenuComponent.Build(this), - RoleSelectInputComponentBuilder roleSelectComponent => roleSelectComponent.Build(this), - ChannelSelectInputComponentBuilder channelSelectComponent => channelSelectComponent.Build(this), - UserSelectInputComponentBuilder userSelectComponent => userSelectComponent.Build(this), - MentionableSelectInputComponentBuilder mentionableSelectComponent => mentionableSelectComponent.Build(this), - FileUploadInputComponentBuilder fileUploadComponent => fileUploadComponent.Build(this), - TextDisplayComponentBuilder textDisplayComponent => textDisplayComponent.Build(this), + Builders.TextInputComponentBuilder textComponent => textComponent.Build(this), + Builders.SelectMenuComponentBuilder selectMenuComponent => selectMenuComponent.Build(this), + Builders.RoleSelectComponentBuilder roleSelectComponent => roleSelectComponent.Build(this), + Builders.ChannelSelectComponentBuilder channelSelectComponent => channelSelectComponent.Build(this), + Builders.UserSelectComponentBuilder userSelectComponent => userSelectComponent.Build(this), + Builders.MentionableSelectComponentBuilder mentionableSelectComponent => mentionableSelectComponent.Build(this), + Builders.FileUploadComponentBuilder fileUploadComponent => fileUploadComponent.Build(this), + Builders.TextDisplayComponentBuilder textDisplayComponent => textDisplayComponent.Build(this), _ => throw new InvalidOperationException($"{x.GetType().FullName} isn't a supported modal input component builder type.") }).ToImmutableArray(); InputComponents = Components.OfType().ToImmutableArray(); TextInputComponents = Components.OfType().ToImmutableArray(); - SelectMenuInputComponents = Components.OfType().ToImmutableArray(); - UserSelectInputComponents = Components.OfType().ToImmutableArray(); - RoleSelectInputComponents = Components.OfType().ToImmutableArray(); - MentionableSelectInputComponents = Components.OfType().ToImmutableArray(); - ChannelSelectInputComponents = Components.OfType().ToImmutableArray(); - FileUploadInputComponents = Components.OfType().ToImmutableArray(); + SelectMenuComponents = Components.OfType().ToImmutableArray(); + UserSelectComponents = Components.OfType().ToImmutableArray(); + RoleSelectComponents = Components.OfType().ToImmutableArray(); + MentionableSelectComponents = Components.OfType().ToImmutableArray(); + ChannelSelectComponents = Components.OfType().ToImmutableArray(); + FileUploadComponents = Components.OfType().ToImmutableArray(); TextDisplayComponents = Components.OfType().ToImmutableArray(); _interactionService = builder._interactionService; diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs index 9af73e0246..18d459be4e 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs @@ -60,12 +60,27 @@ public override Task WriteAsync(TBuilder builder, IDiscordInteraction } } +/// +/// Adds additional metadata to enum fields that are used for select-menus. +/// +/// +/// To manually add select menu options to modal components, use instead. +/// [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] public class SelectMenuOptionAttribute : Attribute { + /// + /// Gets or sets the desription of the option. + /// public string Description { get; set; } + /// + /// Gets or sets whether the option is selected by default. + /// public bool IsDefault { get; set; } + /// + /// Gets or sets the emote of the option. + /// public string Emote { get; set; } } From 138134f3b8e608af15d4e2efbb64d1bd46e2ded5 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Thu, 13 Nov 2025 12:34:36 +0100 Subject: [PATCH 53/59] fix build errors in module class builder --- src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index fd36f0f330..37ffa5591d 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -1,4 +1,3 @@ -using Discord.Interactions.Attributes.Modals; using System; using System.Collections.Generic; using System.Linq; @@ -782,7 +781,7 @@ private static void BuildFileUploadComponent(FileUploadComponentBuilder builder, builder.WithType(propertyInfo.PropertyType); builder.PropertyInfo = propertyInfo; - foreach(var attribute in attributes) + foreach (var attribute in attributes) { switch (attribute) { From cef3aa0e1ee3f5d8ebb222c0efa28d03f9ba8e43 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Thu, 13 Nov 2025 17:50:50 +0100 Subject: [PATCH 54/59] add inline docs to modalComponentTypeConverter TryGetModalInteractionData --- .../ModalComponents/ModalComponentTypeConverter.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/ModalComponentTypeConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/ModalComponentTypeConverter.cs index 56454b8262..6c91b2942e 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/ModalComponentTypeConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/ModalComponentTypeConverter.cs @@ -31,6 +31,12 @@ public virtual Task WriteAsync(TBuilder builder, IDiscordInteraction i where TBuilder : class, IInteractableComponentBuilder => Task.CompletedTask; + /// + /// Tries to get the from the provided . + /// + /// Context containing the . + /// found in the context if successful, otherwise. + /// when successful. protected bool TryGetModalInteractionData(IInteractionContext context, out IModalInteractionData modalData) { if(context.Interaction is IModalInteraction modalInteraction) From 21fb8ee2f56fe6e18349781eae3537f8939e0524 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Thu, 13 Nov 2025 18:09:25 +0100 Subject: [PATCH 55/59] add min/max values parameters to ModalChannelSelectAttribute --- .../Attributes/Modals/ModalChannelSelectAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectAttribute.cs index 552f641e6d..5e445f231e 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectAttribute.cs @@ -12,5 +12,5 @@ public class ModalChannelSelectAttribute : ModalSelectComponentAttribute /// Create a new . /// /// Custom ID of the channel select component. - public ModalChannelSelectAttribute(string customId) : base(customId) { } + public ModalChannelSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { } } From c404da43873c943789a6aa5be45f9aa4fdf02252 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Thu, 13 Nov 2025 18:26:48 +0100 Subject: [PATCH 56/59] fix defaultArrayModalTypeConverter chanell type write logic --- .../DefaultArrayModalComponentConverter.cs | 64 ++++++++----------- 1 file changed, 26 insertions(+), 38 deletions(-) diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs index 21cf1dbfc4..3fc145926d 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs @@ -1,6 +1,7 @@ using Discord.Utils; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; @@ -10,7 +11,7 @@ internal sealed class DefaultArrayModalComponentConverter : ModalComponentTyp { private readonly Type _underlyingType; private readonly TypeReader _typeReader; - private readonly List _channelTypes; + private readonly ImmutableArray _channelTypes; public DefaultArrayModalComponentConverter(InteractionService interactionService) { @@ -33,37 +34,27 @@ _ when typeof(IUser).IsAssignableFrom(_underlyingType) _channelTypes = true switch { - _ when typeof(IStageChannel).IsAssignableFrom(type) - => new List { ChannelType.Stage }, - - _ when typeof(IVoiceChannel).IsAssignableFrom(type) - => new List { ChannelType.Voice }, - - _ when typeof(IDMChannel).IsAssignableFrom(type) - => new List { ChannelType.DM }, - - _ when typeof(IGroupChannel).IsAssignableFrom(type) - => new List { ChannelType.Group }, - - _ when typeof(ICategoryChannel).IsAssignableFrom(type) - => new List { ChannelType.Category }, - - _ when typeof(INewsChannel).IsAssignableFrom(type) - => new List { ChannelType.News }, - - _ when typeof(IThreadChannel).IsAssignableFrom(type) - => new List { ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.NewsThread }, - - _ when typeof(ITextChannel).IsAssignableFrom(type) - => new List { ChannelType.Text }, - - _ when typeof(IMediaChannel).IsAssignableFrom(type) - => new List { ChannelType.Media }, - - _ when typeof(IForumChannel).IsAssignableFrom(type) - => new List { ChannelType.Forum }, - - _ => null + _ when typeof(IStageChannel).IsAssignableFrom(_underlyingType) + => [ChannelType.Stage], + _ when typeof(IVoiceChannel).IsAssignableFrom(_underlyingType) + => [ChannelType.Voice], + _ when typeof(IDMChannel).IsAssignableFrom(_underlyingType) + => [ChannelType.DM], + _ when typeof(IGroupChannel).IsAssignableFrom(_underlyingType) + => [ChannelType.Group], + _ when typeof(ICategoryChannel).IsAssignableFrom(_underlyingType) + => [ChannelType.Category], + _ when typeof(INewsChannel).IsAssignableFrom(_underlyingType) + => [ChannelType.News], + _ when typeof(IThreadChannel).IsAssignableFrom(_underlyingType) + => [ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.NewsThread], + _ when typeof(ITextChannel).IsAssignableFrom(_underlyingType) + => [ChannelType.Text], + _ when typeof(IMediaChannel).IsAssignableFrom(_underlyingType) + => [ChannelType.Media], + _ when typeof(IForumChannel).IsAssignableFrom(_underlyingType) + => [ChannelType.Forum], + _ => [] }; } @@ -186,13 +177,10 @@ public override Task WriteAsync(TBuilder builder, IDiscordInteraction }) .ToList(); break; - } - ; - - + }; - if (component.ComponentType == ComponentType.ChannelSelect && _channelTypes is not null) - selectMenu.WithChannelTypes(_channelTypes); + if (component.ComponentType == ComponentType.ChannelSelect && _channelTypes.Length > 0) + selectMenu.WithChannelTypes(_channelTypes.ToList()); return Task.CompletedTask; } From 2e0b57fe65e458c3a22503ecabf3a36efef5fbf8 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Sat, 15 Nov 2025 19:57:38 +0100 Subject: [PATCH 57/59] simplify addDefaultValue methods for channe, mentionable, role, and user selects --- .../ChannelSelectComponentBuilder.cs | 28 +------------------ .../MentionableSelectComponentBuilder.cs | 27 ++++++++++-------- .../Components/RoleSelectComponentBuilder.cs | 28 +------------------ .../Components/UserSelectComponentBuilder.cs | 28 +------------------ 4 files changed, 18 insertions(+), 93 deletions(-) diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/ChannelSelectComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/ChannelSelectComponentBuilder.cs index d91075027d..0164245871 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Components/ChannelSelectComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Components/ChannelSelectComponentBuilder.cs @@ -16,19 +16,6 @@ public class ChannelSelectComponentBuilder : SnowflakeSelectComponentBuilderParent modal of this component. public ChannelSelectComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.ChannelSelect) { } - /// - /// Adds a default value to . - /// - /// The channel to add as a default value. - /// - /// The builder instance. - /// - public ChannelSelectComponentBuilder AddDefaulValue(IChannel channel) - { - _defaultValues.Add(new SelectMenuDefaultValue(channel.Id, SelectDefaultValueType.Channel)); - return this; - } - /// /// Adds a default value to . /// @@ -49,20 +36,7 @@ public ChannelSelectComponentBuilder AddDefaulValue(ulong channelId) /// /// The builder instance. /// - public ChannelSelectComponentBuilder AddDefaultValues(params IChannel[] channels) - { - _defaultValues.AddRange(channels.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Channel))); - return this; - } - - /// - /// Adds default values to . - /// - /// The channels to add as a default value. - /// - /// The builder instance. - /// - public ChannelSelectComponentBuilder AddDefaultValues(IEnumerable channels) + public ChannelSelectComponentBuilder AddDefaultValues(params IEnumerable channels) { _defaultValues.AddRange(channels.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Channel))); return this; diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/MentionableSelectComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/MentionableSelectComponentBuilder.cs index 41444eb3ef..86e1662063 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Components/MentionableSelectComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Components/MentionableSelectComponentBuilder.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using System.Linq; + namespace Discord.Interactions.Builders; /// @@ -28,41 +31,41 @@ public MentionableSelectComponentBuilder AddDefaultValue(ulong id, SelectDefault } /// - /// Adds a user as a default value to . + /// Add users as a default value to . /// - /// The user to add as a default value. + /// The users to add as a default value. /// /// The builder instance. /// - public MentionableSelectComponentBuilder AddDefaultValue(IUser user) + public MentionableSelectComponentBuilder AddDefaultValue(params IEnumerable users) { - _defaultValues.Add(new SelectMenuDefaultValue(user.Id, SelectDefaultValueType.User)); + _defaultValues.AddRange(users.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.User))); return this; } /// - /// Adds a channel as a default value to . + /// Adds channels as a default value to . /// - /// The channel to add as a default value. + /// The channel to add as a default value. /// /// The builder instance. /// - public MentionableSelectComponentBuilder AddDefaultValue(IChannel channel) + public MentionableSelectComponentBuilder AddDefaultValue(params IEnumerable channels) { - _defaultValues.Add(new SelectMenuDefaultValue(channel.Id, SelectDefaultValueType.Channel)); + _defaultValues.AddRange(channels.Select(x =>new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Channel))); return this; } /// - /// Adds a role as a default value to . + /// Adds roles as a default value to . /// - /// The role to add as a default value. + /// The role to add as a default value. /// /// The builder instance. /// - public MentionableSelectComponentBuilder AddDefaulValue(IRole role) + public MentionableSelectComponentBuilder AddDefaulValue(params IEnumerable roles) { - _defaultValues.Add(new SelectMenuDefaultValue(role.Id, SelectDefaultValueType.Role)); + _defaultValues.AddRange(roles.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Role))); return this; } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/RoleSelectComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/RoleSelectComponentBuilder.cs index 17b1f72f63..49e1a6dd0f 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Components/RoleSelectComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Components/RoleSelectComponentBuilder.cs @@ -16,19 +16,6 @@ public class RoleSelectComponentBuilder : SnowflakeSelectComponentBuilderParent modal of this input component. public RoleSelectComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.RoleSelect) { } - /// - /// Adds a default value to . - /// - /// The role to add as a default value. - /// - /// The builder instance. - /// - public RoleSelectComponentBuilder AddDefaulValue(IRole role) - { - _defaultValues.Add(new SelectMenuDefaultValue(role.Id, SelectDefaultValueType.Role)); - return this; - } - /// /// Adds a default value to . /// @@ -49,20 +36,7 @@ public RoleSelectComponentBuilder AddDefaulValue(ulong roleId) /// /// The builder instance. /// - public RoleSelectComponentBuilder AddDefaultValues(params IRole[] roles) - { - _defaultValues.AddRange(roles.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Role))); - return this; - } - - /// - /// Adds default values to . - /// - /// The roles to add as a default value. - /// - /// The builder instance. - /// - public RoleSelectComponentBuilder AddDefaultValues(IEnumerable roles) + public RoleSelectComponentBuilder AddDefaultValues(params IEnumerable roles) { _defaultValues.AddRange(roles.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Role))); return this; diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/UserSelectComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/UserSelectComponentBuilder.cs index 7c7876b0ce..6189b1cfd4 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Components/UserSelectComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Components/UserSelectComponentBuilder.cs @@ -16,19 +16,6 @@ public class UserSelectComponentBuilder : SnowflakeSelectComponentBuilderParent modal of this input component. public UserSelectComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.UserSelect) { } - /// - /// Adds a default value to . - /// - /// The user to add as a default value. - /// - /// The builder instance. - /// - public UserSelectComponentBuilder AddDefaulValue(IUser user) - { - _defaultValues.Add(new SelectMenuDefaultValue(user.Id, SelectDefaultValueType.User)); - return this; - } - /// /// Adds a default value to . /// @@ -49,20 +36,7 @@ public UserSelectComponentBuilder AddDefaulValue(ulong userId) /// /// The builder instance. /// - public UserSelectComponentBuilder AddDefaultValues(params IUser[] users) - { - _defaultValues.AddRange(users.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.User))); - return this; - } - - /// - /// Adds default values to . - /// - /// The users to add as a default value. - /// - /// The builder instance. - /// - public UserSelectComponentBuilder AddDefaultValues(IEnumerable users) + public UserSelectComponentBuilder AddDefaultValues(params IEnumerable users) { _defaultValues.AddRange(users.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.User))); return this; From 51e32397652f4bd1b0e006ec82bab4e483c576f4 Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Sat, 15 Nov 2025 21:44:53 +0100 Subject: [PATCH 58/59] add emoji support to select menu options --- .../Modals/ModalSelectMenuOptionAttribute.cs | 5 ++++- .../Builders/ModuleClassBuilder.cs | 8 +++++--- .../ModalComponents/EnumModalComponentConverter.cs | 13 ++++++++++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuOptionAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuOptionAttribute.cs index 7790119ab4..2cf81fb659 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuOptionAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuOptionAttribute.cs @@ -29,6 +29,9 @@ public class ModalSelectMenuOptionAttribute : Attribute /// /// Gets or sets the emote of the option. /// + /// + /// Can be either an or an + /// public string Emote { get; set; } /// @@ -42,7 +45,7 @@ public class ModalSelectMenuOptionAttribute : Attribute /// Label of the option. /// Value of the option. /// Description of the option. - /// Emote of the option. + /// Emote of the option. Can be either an or an /// Whether the option is selected by default public ModalSelectMenuOptionAttribute(string label, string value, string description = null, string emote = null, bool isDefault = false) { diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index 37ffa5591d..c76552ef9c 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -716,16 +716,18 @@ private static void BuildSelectMenuComponent(SelectMenuComponentBuilder builder, builder.Description = inputLabel.Description; break; case ModalSelectMenuOptionAttribute selectMenuOption: + Emoji emoji = null; + Emote emote = null; - if (!Emote.TryParse(selectMenuOption.Emote, out var emote) && !string.IsNullOrEmpty(selectMenuOption.Emote)) - throw new ArgumentException($"Invalid emote format on {propertyInfo.DeclaringType} modal, {propertyInfo.Name} property"); + if (!string.IsNullOrEmpty(selectMenuOption?.Emote) && !(Emote.TryParse(selectMenuOption.Emote, out emote) || Emoji.TryParse(selectMenuOption.Emote, out emoji))) + throw new ArgumentException($"Unable to parse {selectMenuOption.Emote} of {propertyInfo.DeclaringType}.{propertyInfo.Name} into an {typeof(Emote).Name} or an {typeof(Emoji).Name}"); builder.AddOption(new SelectMenuOptionBuilder { Label = selectMenuOption.Label, Description = selectMenuOption.Description, Value = selectMenuOption.Value, - Emote = emote, + Emote = emote != null ? emote : emoji, IsDefault = selectMenuOption.IsDefault }); break; diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs index 18d459be4e..0598cf0b53 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs @@ -22,9 +22,17 @@ public EnumModalComponentConverter() _options = members.Select(x => { var selectMenuOptionAttr = x.GetCustomAttribute(); + + 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(); Predicate predicate = hideAttr != null ? hideAttr.Predicate : null; - return (new SelectMenuOptionBuilder(x.GetCustomAttribute()?.Name ?? x.Name, x.Name, selectMenuOptionAttr?.Description, selectMenuOptionAttr?.Emote != null ? Emote.Parse(selectMenuOptionAttr?.Emote) : null, selectMenuOptionAttr?.IsDefault), predicate); + return (new SelectMenuOptionBuilder(x.GetCustomAttribute()?.Name ?? x.Name, x.Name, selectMenuOptionAttr?.Description, emote != null ? emote : emoji, selectMenuOptionAttr?.IsDefault), predicate); }).ToImmutableArray(); } @@ -82,5 +90,8 @@ public class SelectMenuOptionAttribute : Attribute /// /// Gets or sets the emote of the option. /// + /// + /// Can be either an or an + /// public string Emote { get; set; } } From 2f6d7c74cd58ce53e43a0d00a8b4cb99e17ce0dd Mon Sep 17 00:00:00 2001 From: Cenk Ergen Date: Sun, 16 Nov 2025 03:20:02 +0100 Subject: [PATCH 59/59] add instance value parsing to enum modalComponentConverter --- .../EnumModalComponentConverter.cs | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs index 0598cf0b53..75a1c1fd35 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs @@ -9,17 +9,19 @@ namespace Discord.Interactions; internal sealed class EnumModalComponentConverter : ModalComponentTypeConverter where T : struct, Enum { + private record Option(SelectMenuOptionBuilder OptionBuilder, Predicate Predicate, T Value); + private readonly bool _isFlags; - private readonly ImmutableArray<(SelectMenuOptionBuilder Option, Predicate Predicate)> _options; + private readonly ImmutableArray