Feature: Implement modals (#2087)
* Implement Modals (#428) * Socket Modal Support * fix shareded client support * Properly use `HasResponded` instead of `_hasResponded` * `ModalBuilder` and `TextInputBuilder` validation. * make orginisation more consistant. * Rest Modals. * Docs + add missing methods * fix message signatures and missing abstract members * modal changes * um????? * update modal docs * update docs - again for some reason * cleanup * fix message signatures * add modal commands support to interaction service * Fix _hasResponded * update to new unsupported standard. * Sending modals with Interaction service. * fix spelling in ComponentBuilder * sending IModals when responding to interactions * interaction service modals * fix rest modals * spelling and minor improvements. * improve interaction service modal proformance * use precompiled lambda for interaction service modals * respect user compiled lambda choice * changes to modals in the interaction service (more) * support compiled lambdas in modal properties. * modal interactions tweaks * fix inline doc * more modal docs * configure responce to faild modal component * init * solve runtime errors * solve build errors * add default value parsing * make modal info caching static * make ModalUtils static * add inline docs * fix build errors * code cleanup * Introduce Required and Label properties as seperate attributes. * replace internal dictionary of ModalInfo with a list * change input building logic of modals * update RespondWithModalAsync method * add initial value parameter back to ModalTextInput and fix optional modal field * add missing inline docs * dispose the reference modal instance after building * code cleanup on modalcommandbuilder * Update docs/guides/int_basics/message-components/text-input.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_basics/message-components/text-input.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_basics/modals/intro.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_basics/modals/intro.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_basics/modals/intro.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_basics/modals/intro.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_basics/modals/intro.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_basics/modals/intro.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_basics/modals/intro.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_framework/intro.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_framework/intro.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_framework/samples/intro/modal.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputComponent.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Interactions/Modals/IModalInteraction.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Interactions/Attributes/Modals/RequiredInputAttribute.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Interactions/InteractionServiceConfig.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * update interaction service modal docs * implements ExitOnMissingmModalField config option and adds Type field to modal info * Add WithValue to text input builders * Fix rare NRE on component enumeration * Fix RequestOptions being required in some methods * Use 'OfType' instead of 'Where' * Remove android unsported warning * Change publicity of properties in IInputComponeontBuilder.cs Co-authored-by: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Remove complex parameter ref Co-authored-by: CottageDwellingCat <80918250+CottageDwellingCat@users.noreply.github.com> Co-authored-by: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>
This commit is contained in:
@@ -103,6 +103,7 @@ namespace Discord.Interactions.Builders
|
||||
var validContextCommands = methods.Where(IsValidContextCommandDefinition);
|
||||
var validInteractions = methods.Where(IsValidComponentCommandDefinition);
|
||||
var validAutocompleteCommands = methods.Where(IsValidAutocompleteCommandDefinition);
|
||||
var validModalCommands = methods.Where(IsValidModalCommanDefinition);
|
||||
|
||||
Func<IServiceProvider, IInteractionModuleBase> createInstance = commandService._useCompiledLambda ?
|
||||
ReflectionUtils<IInteractionModuleBase>.CreateLambdaBuilder(typeInfo, commandService) : ReflectionUtils<IInteractionModuleBase>.CreateBuilder(typeInfo, commandService);
|
||||
@@ -118,6 +119,9 @@ namespace Discord.Interactions.Builders
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
private static void BuildSubModules (ModuleBuilder parent, IEnumerable<TypeInfo> subModules, IList<TypeInfo> builtTypes, InteractionService commandService,
|
||||
@@ -298,6 +302,47 @@ namespace Discord.Interactions.Builders
|
||||
builder.Callback = CreateCallback(createInstance, methodInfo, commandService);
|
||||
}
|
||||
|
||||
private static void BuildModalCommand(ModalCommandBuilder builder, Func<IServiceProvider, IInteractionModuleBase> 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.All(x => x.ParameterType == typeof(string) || typeof(IModal).IsAssignableFrom(x.ParameterType)))
|
||||
throw new InvalidOperationException($"All parameters of a modal command must be either a string or an implementation of {nameof(IModal)}");
|
||||
|
||||
var attributes = methodInfo.GetCustomAttributes();
|
||||
|
||||
builder.MethodName = methodInfo.Name;
|
||||
|
||||
foreach (var attribute in attributes)
|
||||
{
|
||||
switch (attribute)
|
||||
{
|
||||
case ModalInteractionAttribute modal:
|
||||
{
|
||||
builder.Name = modal.CustomId;
|
||||
builder.RunMode = modal.RunMode;
|
||||
builder.IgnoreGroupNames = modal.IgnoreGroupNames;
|
||||
}
|
||||
break;
|
||||
case PreconditionAttribute precondition:
|
||||
builder.WithPreconditions(precondition);
|
||||
break;
|
||||
default:
|
||||
builder.WithAttributes(attribute);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var parameter in parameters)
|
||||
builder.AddParameter(x => BuildParameter(x, parameter));
|
||||
|
||||
builder.Callback = CreateCallback(createInstance, methodInfo, commandService);
|
||||
}
|
||||
|
||||
private static ExecuteCallback CreateCallback (Func<IServiceProvider, IInteractionModuleBase> createInstance,
|
||||
MethodInfo methodInfo, InteractionService commandService)
|
||||
{
|
||||
@@ -400,7 +445,9 @@ namespace Discord.Interactions.Builders
|
||||
builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower();
|
||||
}
|
||||
|
||||
private static void BuildParameter (CommandParameterBuilder builder, ParameterInfo paramInfo)
|
||||
private static void BuildParameter<TInfo, TBuilder> (ParameterBuilder<TInfo, TBuilder> builder, ParameterInfo paramInfo)
|
||||
where TInfo : class, IParameterInfo
|
||||
where TBuilder : ParameterBuilder<TInfo, TBuilder>
|
||||
{
|
||||
var attributes = paramInfo.GetCustomAttributes();
|
||||
var paramType = paramInfo.ParameterType;
|
||||
@@ -428,6 +475,84 @@ namespace Discord.Interactions.Builders
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Modals
|
||||
public static ModalInfo BuildModalInfo(Type modalType)
|
||||
{
|
||||
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;
|
||||
|
||||
try
|
||||
{
|
||||
var builder = new ModalBuilder(modalType)
|
||||
{
|
||||
Title = instance.Title
|
||||
};
|
||||
|
||||
var inputs = modalType.GetProperties().Where(IsValidModalInputDefinition);
|
||||
|
||||
foreach (var prop in inputs)
|
||||
{
|
||||
var componentType = prop.GetCustomAttribute<ModalInputAttribute>()?.ComponentType;
|
||||
|
||||
switch (componentType)
|
||||
{
|
||||
case ComponentType.TextInput:
|
||||
builder.AddTextComponent(x => BuildTextInput(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<IModal>.CreateLambdaMemberInit(modalType.GetTypeInfo(), modalType.GetConstructor(Type.EmptyTypes), x => x.IsDefined(typeof(ModalInputAttribute)));
|
||||
builder.ModalInitializer = (args) => memberInit(Array.Empty<object>(), args);
|
||||
return builder.Build();
|
||||
}
|
||||
finally
|
||||
{
|
||||
(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);
|
||||
|
||||
foreach(var attribute in attributes)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
internal static bool IsValidModuleDefinition (TypeInfo typeInfo)
|
||||
{
|
||||
return ModuleTypeInfo.IsAssignableFrom(typeInfo) &&
|
||||
@@ -467,5 +592,21 @@ namespace Discord.Interactions.Builders
|
||||
!methodInfo.IsGenericMethod &&
|
||||
methodInfo.GetParameters().Length == 0;
|
||||
}
|
||||
|
||||
private static bool IsValidModalCommanDefinition(MethodInfo methodInfo)
|
||||
{
|
||||
return methodInfo.IsDefined(typeof(ModalInteractionAttribute)) &&
|
||||
(methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task<RuntimeResult>)) &&
|
||||
!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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user