Modal Components v2 Single-select Typeconverters, Rest, and Patches (#3209)
* implement modal v2 components to rest interaction extensions * update rest extensions inline docs * rename respondwithmodal to respondwithmodalasync * add type converters for single user, role, channel, mentionable selects and file-uploads * update rest interaction module base with new async methods * code cleanup and bug fixes in default snowflake modal typeconverter * add default snowflake typeconverters to interaction service * fix nre in respondwithmodal extension method
This commit is contained in:
@@ -65,16 +65,16 @@ namespace Discord.Interactions
|
|||||||
if (!ModalUtils.TryGet<T>(out var modalInfo))
|
if (!ModalUtils.TryGet<T>(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)}");
|
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<T>(interaction, customId, modalInfo, modal, options, modifyModal);
|
return SendModalResponseAsync(interaction, customId, modalInfo, modal, options, modifyModal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task SendModalResponseAsync<T>(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, T modalInstance = null, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
|
public static async Task<Modal> ToModalAsync<T>(this IDiscordInteraction interaction, string customId, ModalInfo modalInfo, T modalInstance = null, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
|
||||||
where T : class, IModal
|
where T : class, IModal
|
||||||
{
|
{
|
||||||
if (!modalInfo.Type.IsAssignableFrom(typeof(T)))
|
if (!modalInfo.Type.IsAssignableFrom(typeof(T)))
|
||||||
throw new ArgumentException($"{modalInfo.Type.FullName} isn't assignable from {typeof(T).FullName}.");
|
throw new ArgumentException($"{modalInfo.Type.FullName} isn't assignable from {typeof(T).FullName}.");
|
||||||
|
|
||||||
var builder = new ModalBuilder(modalInstance.Title, customId);
|
var builder = new ModalBuilder(modalInstance?.Title ?? modalInfo.Title, customId);
|
||||||
|
|
||||||
foreach (var input in modalInfo.Components)
|
foreach (var input in modalInfo.Components)
|
||||||
switch (input)
|
switch (input)
|
||||||
@@ -134,7 +134,7 @@ namespace Discord.Interactions
|
|||||||
break;
|
break;
|
||||||
case TextDisplayComponentInfo textDisplayComponent:
|
case TextDisplayComponentInfo textDisplayComponent:
|
||||||
{
|
{
|
||||||
var content = textDisplayComponent.Getter(modalInstance).ToString() ?? textDisplayComponent.Content;
|
var content = modalInstance is not null ? textDisplayComponent.Getter(modalInstance).ToString() : (textDisplayComponent.DefaultValue as string) ?? textDisplayComponent.Content;
|
||||||
var componentBuilder = new TextDisplayBuilder(content);
|
var componentBuilder = new TextDisplayBuilder(content);
|
||||||
builder.AddTextDisplay(componentBuilder);
|
builder.AddTextDisplay(componentBuilder);
|
||||||
}
|
}
|
||||||
@@ -145,7 +145,15 @@ namespace Discord.Interactions
|
|||||||
|
|
||||||
modifyModal?.Invoke(builder);
|
modifyModal?.Invoke(builder);
|
||||||
|
|
||||||
await interaction.RespondWithModalAsync(builder.Build(), options);
|
return builder.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SendModalResponseAsync<T>(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, T modalInstance = null, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
|
||||||
|
where T : class, IModal
|
||||||
|
{
|
||||||
|
var modal = await interaction.ToModalAsync(customId, modalInfo, modalInstance, options, modifyModal);
|
||||||
|
|
||||||
|
await interaction.RespondWithModalAsync(modal, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Discord.Interactions;
|
using Discord.Interactions;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Discord.Rest
|
namespace Discord.Rest
|
||||||
{
|
{
|
||||||
@@ -11,14 +12,16 @@ namespace Discord.Rest
|
|||||||
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam>
|
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam>
|
||||||
/// <param name="interaction">The interaction to respond to.</param>
|
/// <param name="interaction">The interaction to respond to.</param>
|
||||||
/// <param name="options">The request options for this <see langword="async"/> request.</param>
|
/// <param name="options">The request options for this <see langword="async"/> request.</param>
|
||||||
/// <returns>Serialized payload to be used to create a HTTP response.</returns>
|
/// <returns>
|
||||||
public static string RespondWithModal<T>(this RestInteraction interaction, string customId, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
|
/// Task representing the asynchronous modal creation operation. The task result contains the serialized payload to be used to create a HTTP response.
|
||||||
|
/// </returns>
|
||||||
|
public static async Task<string> RespondWithModalAsync<T>(this RestInteraction interaction, string customId, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
|
||||||
where T : class, IModal
|
where T : class, IModal
|
||||||
{
|
{
|
||||||
if (!ModalUtils.TryGet<T>(out var modalInfo))
|
if (!ModalUtils.TryGet<T>(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)}");
|
throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}");
|
||||||
|
|
||||||
var modal = modalInfo.ToModal(customId, modifyModal);
|
var modal = await interaction.ToModalAsync<T>(customId, modalInfo, null, options, modifyModal);
|
||||||
return interaction.RespondWithModal(modal, options);
|
return interaction.RespondWithModal(modal, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,35 +30,20 @@ namespace Discord.Rest
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam>
|
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam>
|
||||||
/// <param name="interaction">The interaction to respond to.</param>
|
/// <param name="interaction">The interaction to respond to.</param>
|
||||||
/// <param name="modal">The <see cref="IModal"/> instance to get field values from.</param>
|
/// <param name="modalInstance">The <see cref="IModal"/> instance to get field values from.</param>
|
||||||
/// <param name="options">The request options for this <see langword="async"/> request.</param>
|
/// <param name="options">The request options for this <see langword="async"/> request.</param>
|
||||||
/// <param name="modifyModal">Delegate that can be used to modify the modal.</param>
|
/// <param name="modifyModal">Delegate that can be used to modify the modal.</param>
|
||||||
/// <returns>Serialized payload to be used to create a HTTP response.</returns>
|
/// <returns>
|
||||||
public static string RespondWithModal<T>(this RestInteraction interaction, string customId, T modal, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
|
/// Task representing the asynchronous modal creation operation. The task result contains the serialized payload to be used to create a HTTP response.
|
||||||
|
/// </returns>
|
||||||
|
public static async Task<string> RespondWithModalAsync<T>(this RestInteraction interaction, string customId, T modalInstance, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
|
||||||
where T : class, IModal
|
where T : class, IModal
|
||||||
{
|
{
|
||||||
if (!ModalUtils.TryGet<T>(out var modalInfo))
|
if (!ModalUtils.TryGet<T>(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)}");
|
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);
|
var modal = await interaction.ToModalAsync<T>(customId, modalInfo, null, options, modifyModal);
|
||||||
|
return interaction.RespondWithModal(modal, options);
|
||||||
foreach (var input in modalInfo.Components)
|
|
||||||
switch (input)
|
|
||||||
{
|
|
||||||
case TextInputComponentInfo textComponent:
|
|
||||||
{
|
|
||||||
builder.AddTextInput(textComponent.Label, textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null,
|
|
||||||
textComponent.MaxLength, textComponent.IsRequired, textComponent.Getter(modal) as string);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modifyModal is not null)
|
|
||||||
modifyModal(builder);
|
|
||||||
|
|
||||||
return interaction.RespondWithModal(builder.Build(), options);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,7 +236,12 @@ namespace Discord.Interactions
|
|||||||
[typeof(IConvertible)] = typeof(DefaultValueModalComponentConverter<>),
|
[typeof(IConvertible)] = typeof(DefaultValueModalComponentConverter<>),
|
||||||
[typeof(Enum)] = typeof(EnumModalComponentConverter<>),
|
[typeof(Enum)] = typeof(EnumModalComponentConverter<>),
|
||||||
[typeof(Nullable<>)] = typeof(NullableComponentConverter<>),
|
[typeof(Nullable<>)] = typeof(NullableComponentConverter<>),
|
||||||
[typeof(Array)] = typeof(DefaultArrayModalComponentConverter<>)
|
[typeof(Array)] = typeof(DefaultArrayModalComponentConverter<>),
|
||||||
|
[typeof(IChannel)] = typeof(DefaultChannelModalComponentConverter<>),
|
||||||
|
[typeof(IUser)] = typeof(DefaultUserModalComponentConverter<>),
|
||||||
|
[typeof(IRole)] = typeof(DefaultRoleModalComponentConverter<>),
|
||||||
|
[typeof(IMentionable)] = typeof(DefaultMentionableModalComponentConverter<>),
|
||||||
|
[typeof(IAttachment)] = typeof(DefaultAttachmentModalComponentConverter<>)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ namespace Discord.Interactions
|
|||||||
/// </returns>
|
/// </returns>
|
||||||
/// <exception cref="InvalidOperationException">Thrown if the interaction isn't a type of <see cref="RestInteraction"/>.</exception>
|
/// <exception cref="InvalidOperationException">Thrown if the interaction isn't a type of <see cref="RestInteraction"/>.</exception>
|
||||||
protected override async Task RespondWithModalAsync<TModal>(string customId, TModal modal, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
|
protected override async Task RespondWithModalAsync<TModal>(string customId, TModal modal, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
|
||||||
=> await HandleInteractionAsync(x => x.RespondWithModal(customId, modal, options, modifyModal));
|
=> await HandleInteractionAsync(x => RestExtensions.RespondWithModalAsync(x, customId, modal, options, modifyModal));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Responds to the interaction with a modal.
|
/// Responds to the interaction with a modal.
|
||||||
@@ -85,7 +85,7 @@ namespace Discord.Interactions
|
|||||||
/// </returns>
|
/// </returns>
|
||||||
/// <exception cref="InvalidOperationException">Thrown if the interaction isn't a type of <see cref="RestInteraction"/>.</exception>
|
/// <exception cref="InvalidOperationException">Thrown if the interaction isn't a type of <see cref="RestInteraction"/>.</exception>
|
||||||
protected override Task RespondWithModalAsync<TModal>(string customId, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
|
protected override Task RespondWithModalAsync<TModal>(string customId, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
|
||||||
=> HandleInteractionAsync(x => x.RespondWithModal<TModal>(customId, options, modifyModal));
|
=> HandleInteractionAsync(x => RestExtensions.RespondWithModalAsync<TModal>(x, customId, options, modifyModal));
|
||||||
|
|
||||||
private Task HandleInteractionAsync(Func<RestInteraction, string> action)
|
private Task HandleInteractionAsync(Func<RestInteraction, string> action)
|
||||||
{
|
{
|
||||||
@@ -99,5 +99,18 @@ namespace Discord.Interactions
|
|||||||
else
|
else
|
||||||
return InteractionService._restResponseCallback(Context, payload);
|
return InteractionService._restResponseCallback(Context, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task HandleInteractionAsync(Func<RestInteraction, Task<string>> action)
|
||||||
|
{
|
||||||
|
if (Context.Interaction is not RestInteraction restInteraction)
|
||||||
|
throw new InvalidOperationException($"Interaction must be a type of {nameof(RestInteraction)} in order to execute this method.");
|
||||||
|
|
||||||
|
var payload = await action(restInteraction);
|
||||||
|
|
||||||
|
if (Context is IRestInteractionContext restContext && restContext.InteractionResponseCallback != null)
|
||||||
|
await restContext.InteractionResponseCallback.Invoke(payload);
|
||||||
|
else
|
||||||
|
await InteractionService._restResponseCallback(Context, payload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Interactions;
|
||||||
|
|
||||||
|
internal abstract class DefaultSnowflakeModalComponentConverter<T> : ModalComponentTypeConverter<T>
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
protected bool TryGetPreemptiveResult(IInteractionContext context, IComponentInteractionData option, ComponentType componentType, out TypeConverterResult preemptiveResult, out IModalInteractionData modalData, out ulong id)
|
||||||
|
{
|
||||||
|
preemptiveResult = default;
|
||||||
|
modalData = null;
|
||||||
|
id = 0;
|
||||||
|
|
||||||
|
if (!TryGetModalInteractionData(context, out modalData))
|
||||||
|
{
|
||||||
|
preemptiveResult = TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{typeof(IModalInteractionData).Name} cannot be accessed from the provided {typeof(IInteractionContext).Name} type.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.Type != componentType)
|
||||||
|
{
|
||||||
|
preemptiveResult = TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{typeof(DefaultSnowflakeModalComponentConverter<T>).Name} cannot be used to convert component result other than {componentType} to {typeof(T).Name}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.Values.Count > 1)
|
||||||
|
{
|
||||||
|
preemptiveResult = TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Multiple values were provided for a single {option.Type} component.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.Values.Count == 0)
|
||||||
|
{
|
||||||
|
preemptiveResult = TypeConverterResult.FromSuccess(null);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ulong.TryParse(option.Values.First(), out id))
|
||||||
|
{
|
||||||
|
preemptiveResult = TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{option.Type} contains invalid snowflake.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultAttachmentModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||||
|
where T : class, IAttachment
|
||||||
|
{
|
||||||
|
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (TryGetPreemptiveResult(context, option, ComponentType.FileUpload, out var result, out var modalData, out var id))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolvedEntity = modalData.Attachments.FirstOrDefault(x => x.Id == id);
|
||||||
|
|
||||||
|
if (resolvedEntity is null)
|
||||||
|
{
|
||||||
|
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypeConverterResult.FromSuccess(resolvedEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultUserModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||||
|
where T : class, IUser
|
||||||
|
{
|
||||||
|
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (TryGetPreemptiveResult(context, option, ComponentType.UserSelect, out var result, out var modalData, out var id))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolvedEntity = modalData.Members.UnionBy(modalData.Users, x => x.Id).FirstOrDefault(x => x.Id == id);
|
||||||
|
|
||||||
|
if (resolvedEntity is null)
|
||||||
|
{
|
||||||
|
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypeConverterResult.FromSuccess(resolvedEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.UserSelect)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{typeof(DefaultUserModalComponentConverter<T>).Name} can only be used with User Select components.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectMenu.MaxValues > 1)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Multi-select User Select cannot be used with a single {typeof(T).Name} entity.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is not IUser user)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default User Select values. Expected {typeof(IUser).Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromUser(user));
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultRoleModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||||
|
where T : class, IRole
|
||||||
|
{
|
||||||
|
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (TryGetPreemptiveResult(context, option, ComponentType.RoleSelect, out var result, out var modalData, out var id))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolvedEntity = modalData.Roles.FirstOrDefault(x => x.Id == id);
|
||||||
|
|
||||||
|
if (resolvedEntity is null)
|
||||||
|
{
|
||||||
|
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypeConverterResult.FromSuccess(resolvedEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
||||||
|
{
|
||||||
|
if(value is null)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.RoleSelect)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{typeof(DefaultRoleModalComponentConverter<T>).Name} can only be used with Role Select components.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectMenu.MaxValues > 1)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Multi-select Role Select cannot be used with a single {typeof(T).Name} entity.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is not IRole role)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Role Select values. Expected {typeof(IRole).Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromRole(role));
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultChannelModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||||
|
where T : class, IChannel
|
||||||
|
{
|
||||||
|
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (TryGetPreemptiveResult(context, option, ComponentType.ChannelSelect, out var result, out var modalData, out var id))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolvedEntity = modalData.Channels.FirstOrDefault(x => x.Id == id);
|
||||||
|
|
||||||
|
if (resolvedEntity is null)
|
||||||
|
{
|
||||||
|
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypeConverterResult.FromSuccess(resolvedEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.ChannelSelect)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{typeof(DefaultChannelModalComponentConverter<T>).Name} can only be used with Channel Select components.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(selectMenu.MaxValues > 1)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Multi-select Channel Select cannot be used with a single {typeof(T).Name} entity.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(value is not IChannel channel)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Channel Select values. Expected {typeof(IChannel).Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromChannel(channel));
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultMentionableModalComponentConverter<T> : DefaultSnowflakeModalComponentConverter<T>
|
||||||
|
where T : class, IMentionable
|
||||||
|
{
|
||||||
|
public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (TryGetPreemptiveResult(context, option, ComponentType.MentionableSelect, out var result, out var modalData, out var id))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolvedMentionables = new Dictionary<ulong, IMentionable>();
|
||||||
|
|
||||||
|
foreach (var user in modalData.Users) // should never be null in mentionable select
|
||||||
|
resolvedMentionables[user.Id] = user;
|
||||||
|
|
||||||
|
foreach (var member in modalData.Members)
|
||||||
|
resolvedMentionables[member.Id] = member;
|
||||||
|
|
||||||
|
foreach (var role in modalData.Roles)
|
||||||
|
resolvedMentionables[role.Id] = role;
|
||||||
|
|
||||||
|
if (resolvedMentionables.Count == 0 || !resolvedMentionables.TryGetValue(id, out var entity))
|
||||||
|
{
|
||||||
|
return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypeConverterResult.FromSuccess(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task WriteAsync<TBuilder>(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value)
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.MentionableSelect)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{typeof(DefaultMentionableModalComponentConverter<T>).Name} can only be used with Mentionable Select components.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectMenu.MaxValues > 1)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Multi-select Mentionable Select cannot be used with a single {typeof(T).Name} entity.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultValue = value switch
|
||||||
|
{
|
||||||
|
IRole role => SelectMenuDefaultValue.FromRole(role),
|
||||||
|
IUser user => SelectMenuDefaultValue.FromUser(user),
|
||||||
|
_ => throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Mentionable Select values. Expected {typeof(IUser).Name} or {typeof(IRole).Name}")
|
||||||
|
};
|
||||||
|
|
||||||
|
selectMenu.WithDefaultValues(defaultValue);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Discord.Interactions
|
namespace Discord.Interactions
|
||||||
{
|
{
|
||||||
@@ -337,26 +338,6 @@ namespace Discord.Interactions
|
|||||||
MinLength = commandOption.MinLength,
|
MinLength = commandOption.MinLength,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static Modal ToModal(this ModalInfo modalInfo, string customId, Action<ModalBuilder> modifyModal = null)
|
|
||||||
{
|
|
||||||
var builder = new ModalBuilder(modalInfo.Title, customId);
|
|
||||||
|
|
||||||
foreach (var input in modalInfo.Components)
|
|
||||||
switch (input)
|
|
||||||
{
|
|
||||||
case TextInputComponentInfo textComponent:
|
|
||||||
builder.AddTextInput(textComponent.Label, textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null,
|
|
||||||
textComponent.MaxLength, textComponent.IsRequired, textComponent.InitialValue);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class");
|
|
||||||
}
|
|
||||||
|
|
||||||
modifyModal?.Invoke(builder);
|
|
||||||
|
|
||||||
return builder.Build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static GuildPermission? SanitizeGuildPermissions(this GuildPermission permissions) =>
|
public static GuildPermission? SanitizeGuildPermissions(this GuildPermission permissions) =>
|
||||||
permissions == 0 ? null : permissions;
|
permissions == 0 ? null : permissions;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user