diff --git a/DisCatSharp.Interactivity/Extensions/InteractionExtensions.cs b/DisCatSharp.Interactivity/Extensions/InteractionExtensions.cs
index baa44ddbf..73aa9e37b 100644
--- a/DisCatSharp.Interactivity/Extensions/InteractionExtensions.cs
+++ b/DisCatSharp.Interactivity/Extensions/InteractionExtensions.cs
@@ -1,120 +1,121 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.Interactivity.Enums;
using DisCatSharp.Interactivity.EventHandling;
namespace DisCatSharp.Interactivity.Extensions;
///
/// The interaction extensions.
///
public static class InteractionExtensions
{
///
/// Sends a paginated message in response to an interaction.
///
/// Pass the interaction directly. Interactivity will ACK it.
///
///
/// The interaction to create a response to.
+ /// Whether the interaction was deferred.
/// Whether the response should be ephemeral.
/// The user to listen for button presses from.
/// The pages to paginate.
/// Optional: custom buttons
/// Pagination behaviour.
/// Deletion behaviour
/// A custom cancellation token that can be cancelled at any point.
- public static Task SendPaginatedResponseAsync(this DiscordInteraction interaction, bool ephemeral, DiscordUser user, IEnumerable pages, PaginationButtons buttons = null, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default)
- => MessageExtensions.GetInteractivity(interaction.Message).SendPaginatedResponseAsync(interaction, ephemeral, user, pages, buttons, behaviour, deletion, token);
+ public static Task SendPaginatedResponseAsync(this DiscordInteraction interaction, bool deferred, bool ephemeral, DiscordUser user, IEnumerable pages, PaginationButtons buttons = null, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default)
+ => MessageExtensions.GetInteractivity(interaction.Message).SendPaginatedResponseAsync(interaction, deferred, ephemeral, user, pages, buttons, behaviour, deletion, token);
///
/// Sends multiple modals to the user with a prompt to open the next one.
///
/// The interaction to create a response to.
/// The modal pages.
/// A custom timeout. (Default: 15 minutes)
/// A read-only dictionary with the customid of the components as the key.
/// Is thrown when no modals are defined.
/// Is thrown when interactivity is not enabled for the client/shard.
public static async Task CreatePaginatedModalResponseAsync(this DiscordInteraction interaction, IReadOnlyList modals, TimeSpan? timeOutOverride = null)
{
if (modals is null || modals.Count == 0)
throw new ArgumentException("You have to set at least one page");
var client = (DiscordClient)interaction.Discord;
var interactivity = client.GetInteractivity() ?? throw new InvalidOperationException($"Interactivity is not enabled for this {(client.IsShard ? "shard" : "client")}.");
timeOutOverride ??= TimeSpan.FromMinutes(15);
Dictionary caughtResponses = new();
var previousInteraction = interaction;
foreach (var b in modals)
{
var modal = b.Modal.WithCustomId(Guid.NewGuid().ToString());
if (previousInteraction.Type is InteractionType.Ping or InteractionType.ModalSubmit)
{
await previousInteraction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, b.OpenMessage.AddComponents(b.OpenButton));
var originalResponse = await previousInteraction.GetOriginalResponseAsync();
var modalOpen = await interactivity.WaitForButtonAsync(originalResponse, new List { b.OpenButton }, timeOutOverride);
if (modalOpen.TimedOut)
{
_ = previousInteraction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent(b.OpenMessage.Content).AddComponents(b.OpenButton.Disable()));
return new PaginatedModalResponse { TimedOut = true };
}
await modalOpen.Result.Interaction.CreateInteractionModalResponseAsync(modal);
}
else
{
await previousInteraction.CreateInteractionModalResponseAsync(modal);
}
_ = previousInteraction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent(b.OpenMessage.Content).AddComponents(b.OpenButton.Disable()));
var modalResult = await interactivity.WaitForModalAsync(modal.CustomId, timeOutOverride);
if (modalResult.TimedOut)
return new PaginatedModalResponse { TimedOut = true };
foreach (var row in modalResult.Result.Interaction.Data.Components)
foreach (var submissions in row.Components)
caughtResponses.Add(submissions.CustomId, submissions.Value);
previousInteraction = modalResult.Result.Interaction;
}
await previousInteraction.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral());
return new PaginatedModalResponse { TimedOut = false, Responses = caughtResponses, Interaction = previousInteraction };
}
}
diff --git a/DisCatSharp.Interactivity/InteractivityExtension.cs b/DisCatSharp.Interactivity/InteractivityExtension.cs
index b0cf0a97d..42cfda735 100644
--- a/DisCatSharp.Interactivity/InteractivityExtension.cs
+++ b/DisCatSharp.Interactivity/InteractivityExtension.cs
@@ -1,958 +1,971 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Common.Utilities;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Interactivity.Enums;
using DisCatSharp.Interactivity.EventHandling;
namespace DisCatSharp.Interactivity;
///
/// Extension class for DisCatSharp.Interactivity
///
public class InteractivityExtension : BaseExtension
{
///
/// Gets the config.
///
internal InteractivityConfiguration Config { get; }
private EventWaiter _messageCreatedWaiter;
private EventWaiter _messageReactionAddWaiter;
private EventWaiter _typingStartWaiter;
private EventWaiter _modalInteractionWaiter;
private EventWaiter _componentInteractionWaiter;
private ComponentEventWaiter _componentEventWaiter;
private ModalEventWaiter _modalEventWaiter;
private ReactionCollector _reactionCollector;
private Poller _poller;
private Paginator _paginator;
private ComponentPaginator _compPaginator;
///
/// Initializes a new instance of the class.
///
/// The configuration.
internal InteractivityExtension(InteractivityConfiguration cfg)
{
this.Config = new InteractivityConfiguration(cfg);
}
///
/// Setups the Interactivity Extension.
///
/// Discord client.
protected internal override void Setup(DiscordClient client)
{
this.Client = client;
this._messageCreatedWaiter = new EventWaiter(this.Client);
this._messageReactionAddWaiter = new EventWaiter(this.Client);
this._componentInteractionWaiter = new EventWaiter(this.Client);
this._modalInteractionWaiter = new EventWaiter(this.Client);
this._typingStartWaiter = new EventWaiter(this.Client);
this._poller = new Poller(this.Client);
this._reactionCollector = new ReactionCollector(this.Client);
this._paginator = new Paginator(this.Client);
this._compPaginator = new ComponentPaginator(this.Client, this.Config);
this._componentEventWaiter = new ComponentEventWaiter(this.Client, this.Config);
this._modalEventWaiter = new ModalEventWaiter(this.Client, this.Config);
}
///
/// Makes a poll and returns poll results.
///
/// Message to create poll on.
/// Emojis to use for this poll.
/// What to do when the poll ends.
/// Override timeout period.
///
public async Task> DoPollAsync(DiscordMessage m, IEnumerable emojis, PollBehaviour? behaviour = default, TimeSpan? timeout = null)
{
if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No reaction intents are enabled.");
if (!emojis.Any())
throw new ArgumentException("You need to provide at least one emoji for a poll!");
foreach (var em in emojis)
await m.CreateReactionAsync(em).ConfigureAwait(false);
var res = await this._poller.DoPollAsync(new PollRequest(m, timeout ?? this.Config.Timeout, emojis)).ConfigureAwait(false);
var pollBehaviour = behaviour ?? this.Config.PollBehaviour;
var thisMember = await m.Channel.Guild.GetMemberAsync(this.Client.CurrentUser.Id).ConfigureAwait(false);
if (pollBehaviour == PollBehaviour.DeleteEmojis && m.Channel.PermissionsFor(thisMember).HasPermission(Permissions.ManageMessages))
await m.DeleteAllReactionsAsync().ConfigureAwait(false);
return new ReadOnlyCollection(res.ToList());
}
///
/// Waits for any button in the specified collection to be pressed.
///
/// The message to wait on.
/// A collection of buttons to listen for.
/// Override the timeout period in .
/// A with the result of button that was pressed, if any.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, buttons, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any button in the specified collection to be pressed.
///
/// The message to wait on.
/// A collection of buttons to listen for.
/// A custom cancellation token that can be cancelled at any point.
/// A with the result of button that was pressed, if any.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public async Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!buttons.Any())
throw new ArgumentException("You must specify at least one button to listen for.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Provided Message does not contain any button components.");
var res = await this._componentEventWaiter
.WaitForMatchAsync(new ComponentMatchRequest(message,
c =>
c.Interaction.Data.ComponentType == ComponentType.Button &&
buttons.Any(b => b.CustomId == c.Id), token)).ConfigureAwait(false);
return new InteractivityResult(res is null, res);
}
///
/// Waits for a user modal submit.
///
/// The custom id of the modal to wait for.
/// Override the timeout period specified in .
/// A with the result of the modal.
public Task> WaitForModalAsync(string customId, TimeSpan? timeoutOverride = null)
=> this.WaitForModalAsync(customId, this.GetCancellationToken(timeoutOverride));
///
/// Waits for a user modal submit.
///
/// The custom id of the modal to wait for.
/// A custom cancellation token that can be cancelled at any point.
/// A with the result of the modal.
public async Task> WaitForModalAsync(string customId, CancellationToken token)
{
var result =
await this
._modalEventWaiter
.WaitForModalMatchAsync(new ModalMatchRequest(customId, c => c.Interaction.Type == InteractionType.ModalSubmit, token))
.ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for any button on the specified message to be pressed.
///
/// The message to wait for the button on.
/// Override the timeout period specified in .
/// A with the result of button that was pressed, if any.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public Task> WaitForButtonAsync(DiscordMessage message, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any button on the specified message to be pressed.
///
/// The message to wait for the button on.
/// A custom cancellation token that can be cancelled at any point.
/// A with the result of button that was pressed, if any.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public async Task> WaitForButtonAsync(DiscordMessage message, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Message does not contain any button components.");
var ids = message.Components.SelectMany(m => m.Components).Select(c => c.CustomId);
var result =
await this
._componentEventWaiter
.WaitForMatchAsync(new ComponentMatchRequest(message, c => c.Interaction.Data.ComponentType == ComponentType.Button && ids.Contains(c.Id), token))
.ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for any button on the specified message to be pressed by the specified user.
///
/// The message to wait for the button on.
/// The user to wait for the button press from.
/// Override the timeout period specified in .
/// A with the result of button that was pressed, if any.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, user, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any button on the specified message to be pressed by the specified user.
///
/// The message to wait for the button on.
/// The user to wait for the button press from.
/// A custom cancellation token that can be cancelled at any point.
/// A with the result of button that was pressed, if any.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public async Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Message does not contain any button components.");
var result = await this
._componentEventWaiter
.WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.User == user, token))
.ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for a button with the specified Id to be pressed.
///
/// The message to wait for the button on.
/// The Id of the button to wait for.
/// Override the timeout period specified in .
/// A with the result of the operation.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public Task> WaitForButtonAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, id, this.GetCancellationToken(timeoutOverride));
///
/// Waits for a button with the specified Id to be pressed.
///
/// The message to wait for the button on.
/// The Id of the button to wait for.
/// Override the timeout period specified in .
/// A with the result of the operation.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public async Task> WaitForButtonAsync(DiscordMessage message, string id, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Message does not contain any button components.");
if (!message.Components.SelectMany(c => c.Components).OfType().Any(c => c.CustomId == id))
throw new ArgumentException($"Message does not contain button with Id of '{id}'.");
var result = await this
._componentEventWaiter
.WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.Id == id, token))
.ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for any button to be interacted with.
///
/// The message to wait on.
/// The predicate to filter interactions by.
/// Override the timeout specified in
public Task> WaitForButtonAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, predicate, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any button to be interacted with.
///
/// The message to wait on.
/// The predicate to filter interactions by.
/// A token to cancel interactivity with at any time. Pass to wait indefinitely.
public async Task> WaitForButtonAsync(DiscordMessage message, Func predicate, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Message does not contain any button components.");
var result = await this
._componentEventWaiter
.WaitForMatchAsync(new ComponentMatchRequest(message, c => c.Interaction.Data.ComponentType is ComponentType.Button && predicate(c), token))
.ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for any dropdown to be interacted with.
///
/// The message to wait for.
/// A filter predicate.
/// Override the timeout period specified in .
/// Thrown when the Provided message does not contain any dropdowns
public Task> WaitForSelectAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null)
=> this.WaitForSelectAsync(message, predicate, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any dropdown to be interacted with.
///
/// The message to wait for.
/// A filter predicate.
/// A token that can be used to cancel interactivity. Pass to wait indefinitely.
/// Thrown when the Provided message does not contain any dropdowns
public async Task> WaitForSelectAsync(DiscordMessage message, Func predicate, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select))
throw new ArgumentException("Message does not contain any select components.");
var result = await this
._componentEventWaiter
.WaitForMatchAsync(new ComponentMatchRequest(message, c => c.Interaction.Data.ComponentType is ComponentType.Select && predicate(c), token))
.ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for a dropdown to be interacted with.
///
/// This is here for backwards-compatibility and will internally create a cancellation token.
/// The message to wait on.
/// The Id of the dropdown to wait on.
/// Override the timeout period specified in .
/// Thrown when the message does not have any dropdowns or any dropdown with the specified Id.
public Task> WaitForSelectAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null)
=> this.WaitForSelectAsync(message, id, this.GetCancellationToken(timeoutOverride));
///
/// Waits for a dropdown to be interacted with.
///
/// The message to wait on.
/// The Id of the dropdown to wait on.
/// A custom cancellation token that can be cancelled at any point.
/// Thrown when the message does not have any dropdowns or any dropdown with the specified Id.
public async Task> WaitForSelectAsync(DiscordMessage message, string id, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select))
throw new ArgumentException("Message does not contain any select components.");
if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id))
throw new ArgumentException($"Message does not contain select component with Id of '{id}'.");
var result = await this
._componentEventWaiter
.WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Select && c.Id == id, token))
.ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for a dropdown to be interacted with by a specific user.
///
/// The message to wait on.
/// The user to wait on.
/// The Id of the dropdown to wait on.
/// Override the timeout period specified in .
/// Thrown when the message does not have any dropdowns or any dropdown with the specified Id.
public Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, TimeSpan? timeoutOverride = null)
=> this.WaitForSelectAsync(message, user, id, this.GetCancellationToken(timeoutOverride));
///
/// Waits for a dropdown to be interacted with by a specific user.
///
/// The message to wait on.
/// The user to wait on.
/// The Id of the dropdown to wait on.
/// A custom cancellation token that can be cancelled at any point.
/// Thrown when the message does not have any dropdowns or any dropdown with the specified Id.
public async Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select))
throw new ArgumentException("Message does not contain any select components.");
if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id))
throw new ArgumentException($"Message does not contain select with Id of '{id}'.");
var result = await this
._componentEventWaiter
.WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Id == id && c.User == user, token)).ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for a specific message.
///
/// Predicate to match.
/// Override timeout period.
public async Task> WaitForMessageAsync(Func predicate,
TimeSpan? timeoutOverride = null)
{
if (!Utilities.HasMessageIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No message intents are enabled.");
var timeout = timeoutOverride ?? this.Config.Timeout;
var returns = await this._messageCreatedWaiter.WaitForMatchAsync(new MatchRequest(x => predicate(x.Message), timeout)).ConfigureAwait(false);
return new InteractivityResult(returns == null, returns?.Message);
}
///
/// Wait for a specific reaction.
///
/// Predicate to match.
/// Override timeout period.
public async Task> WaitForReactionAsync(Func predicate,
TimeSpan? timeoutOverride = null)
{
if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No reaction intents are enabled.");
var timeout = timeoutOverride ?? this.Config.Timeout;
var returns = await this._messageReactionAddWaiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)).ConfigureAwait(false);
return new InteractivityResult(returns == null, returns);
}
///
/// Wait for a specific reaction.
/// For this Event you need the intent specified in
///
/// Message reaction was added to.
/// User that made the reaction.
/// Override timeout period.
public async Task> WaitForReactionAsync(DiscordMessage message, DiscordUser user,
TimeSpan? timeoutOverride = null)
=> await this.WaitForReactionAsync(x => x.User.Id == user.Id && x.Message.Id == message.Id, timeoutOverride).ConfigureAwait(false);
///
/// Waits for a specific reaction.
/// For this Event you need the intent specified in
///
/// Predicate to match.
/// Message reaction was added to.
/// User that made the reaction.
/// Override timeout period.
public async Task> WaitForReactionAsync(Func predicate,
DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null)
=> await this.WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id && x.Message.Id == message.Id, timeoutOverride).ConfigureAwait(false);
///
/// Waits for a specific reaction.
/// For this Event you need the intent specified in
///
/// predicate to match.
/// User that made the reaction.
/// Override timeout period.
public async Task> WaitForReactionAsync(Func predicate,
DiscordUser user, TimeSpan? timeoutOverride = null)
=> await this.WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id, timeoutOverride).ConfigureAwait(false);
///
/// Waits for a user to start typing.
///
/// User that starts typing.
/// Channel the user is typing in.
/// Override timeout period.
public async Task> WaitForUserTypingAsync(DiscordUser user,
DiscordChannel channel, TimeSpan? timeoutOverride = null)
{
if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No typing intents are enabled.");
var timeout = timeoutOverride ?? this.Config.Timeout;
var returns = await this._typingStartWaiter.WaitForMatchAsync(
new MatchRequest(x => x.User.Id == user.Id && x.Channel.Id == channel.Id, timeout))
.ConfigureAwait(false);
return new InteractivityResult(returns == null, returns);
}
///
/// Waits for a user to start typing.
///
/// User that starts typing.
/// Override timeout period.
public async Task> WaitForUserTypingAsync(DiscordUser user, TimeSpan? timeoutOverride = null)
{
if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No typing intents are enabled.");
var timeout = timeoutOverride ?? this.Config.Timeout;
var returns = await this._typingStartWaiter.WaitForMatchAsync(
new MatchRequest(x => x.User.Id == user.Id, timeout))
.ConfigureAwait(false);
return new InteractivityResult(returns == null, returns);
}
///
/// Waits for any user to start typing.
///
/// Channel to type in.
/// Override timeout period.
public async Task> WaitForTypingAsync(DiscordChannel channel, TimeSpan? timeoutOverride = null)
{
if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No typing intents are enabled.");
var timeout = timeoutOverride ?? this.Config.Timeout;
var returns = await this._typingStartWaiter.WaitForMatchAsync(
new MatchRequest(x => x.Channel.Id == channel.Id, timeout))
.ConfigureAwait(false);
return new InteractivityResult(returns == null, returns);
}
///
/// Collects reactions on a specific message.
///
/// Message to collect reactions on.
/// Override timeout period.
public async Task> CollectReactionsAsync(DiscordMessage m, TimeSpan? timeoutOverride = null)
{
if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No reaction intents are enabled.");
var timeout = timeoutOverride ?? this.Config.Timeout;
var collection = await this._reactionCollector.CollectAsync(new ReactionCollectRequest(m, timeout)).ConfigureAwait(false);
return collection;
}
///
/// Waits for specific event args to be received. Make sure the appropriate are registered, if needed.
///
///
/// The predicate.
/// Override timeout period.
public async Task> WaitForEventArgsAsync(Func predicate, TimeSpan? timeoutOverride = null) where T : AsyncEventArgs
{
var timeout = timeoutOverride ?? this.Config.Timeout;
using var waiter = new EventWaiter(this.Client);
var res = await waiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)).ConfigureAwait(false);
return new InteractivityResult(res == null, res);
}
///
/// Collects the event arguments.
///
/// The predicate.
/// Override timeout period.
public async Task> CollectEventArgsAsync(Func predicate, TimeSpan? timeoutOverride = null) where T : AsyncEventArgs
{
var timeout = timeoutOverride ?? this.Config.Timeout;
using var waiter = new EventWaiter(this.Client);
var res = await waiter.CollectMatchesAsync(new CollectRequest(predicate, timeout)).ConfigureAwait(false);
return res;
}
///
/// Sends a paginated message with buttons.
///
/// The channel to send it on.
/// User to give control.
/// The pages.
/// Pagination buttons (pass null to use buttons defined in ).
/// Pagination behaviour.
/// Deletion behaviour.
/// A custom cancellation token that can be cancelled at any point.
public async Task SendPaginatedMessageAsync(
DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons,
PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default)
{
var bhv = behaviour ?? this.Config.PaginationBehaviour;
var del = deletion ?? this.Config.ButtonBehavior;
var bts = buttons ?? this.Config.PaginationButtons;
bts = new PaginationButtons(bts);
if (bhv is PaginationBehaviour.Ignore)
{
bts.SkipLeft.Disable();
bts.Left.Disable();
}
var builder = new DiscordMessageBuilder()
.WithContent(pages.First().Content)
.WithEmbed(pages.First().Embed)
.AddComponents(bts.ButtonArray);
var message = await builder.SendAsync(channel).ConfigureAwait(false);
var req = new ButtonPaginationRequest(message, user, bhv, del, bts, pages, token == default ? this.GetCancellationToken() : token);
await this._compPaginator.DoPaginationAsync(req).ConfigureAwait(false);
}
///
/// Sends a paginated message with buttons.
///
/// The channel to send it on.
/// User to give control.
/// The pages.
/// Pagination buttons (pass null to use buttons defined in ).
/// Pagination behaviour.
/// Deletion behaviour.
/// Override timeout period.
public Task SendPaginatedMessageAsync(
DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, TimeSpan? timeoutOverride,
PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default)
=> this.SendPaginatedMessageAsync(channel, user, pages, buttons, behaviour, deletion, this.GetCancellationToken(timeoutOverride));
///
/// Sends the paginated message.
///
/// The channel.
/// The user.
/// The pages.
/// The behaviour.
/// The deletion.
/// The token.
/// A Task.
public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default)
=> this.SendPaginatedMessageAsync(channel, user, pages, default, behaviour, deletion, token);
///
/// Sends the paginated message.
///
/// The channel.
/// The user.
/// The pages.
/// Override timeout period.
/// The behaviour.
/// The deletion.
/// A Task.
public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, TimeSpan? timeoutOverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default)
=> this.SendPaginatedMessageAsync(channel, user, pages, timeoutOverride, behaviour, deletion);
///
/// Sends a paginated message.
/// For this Event you need the intent specified in
///
/// Channel to send paginated message in.
/// User to give control.
/// Pages.
/// Pagination emojis.
/// Pagination behaviour (when hitting max and min indices).
/// Deletion behaviour.
/// Override timeout period.
public async Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationEmojis emojis,
PaginationBehaviour? behaviour = default, PaginationDeletion? deletion = default, TimeSpan? timeoutOverride = null)
{
var builder = new DiscordMessageBuilder()
.WithContent(pages.First().Content)
.WithEmbed(pages.First().Embed);
var m = await builder.SendAsync(channel).ConfigureAwait(false);
var timeout = timeoutOverride ?? this.Config.Timeout;
var bhv = behaviour ?? this.Config.PaginationBehaviour;
var del = deletion ?? this.Config.PaginationDeletion;
var ems = emojis ?? this.Config.PaginationEmojis;
var pRequest = new PaginationRequest(m, user, bhv, del, ems, timeout, pages);
await this._paginator.DoPaginationAsync(pRequest).ConfigureAwait(false);
}
///
/// Sends a paginated message in response to an interaction.
///
/// Pass the interaction directly. Interactivity will ACK it.
///
///
/// The interaction to create a response to.
+ /// Whether the interaction was deferred.
/// Whether the response should be ephemeral.
/// The user to listen for button presses from.
/// The pages to paginate.
/// Optional: custom buttons.
/// Pagination behaviour.
/// Deletion behaviour.
/// A custom cancellation token that can be cancelled at any point.
- public async Task SendPaginatedResponseAsync(DiscordInteraction interaction, bool ephemeral, DiscordUser user, IEnumerable pages, PaginationButtons buttons = null, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default)
+ public async Task SendPaginatedResponseAsync(DiscordInteraction interaction, bool deferred, bool ephemeral, DiscordUser user, IEnumerable pages, PaginationButtons buttons = null, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default)
{
var bhv = behaviour ?? this.Config.PaginationBehaviour;
var del = deletion ?? this.Config.ButtonBehavior;
var bts = buttons ?? this.Config.PaginationButtons;
bts = new PaginationButtons(bts);
if (bhv is PaginationBehaviour.Ignore)
{
bts.SkipLeft.Disable();
bts.Left.Disable();
}
- var builder = new DiscordInteractionResponseBuilder()
+ DiscordMessage message;
+
+ if (deferred)
+ {
+ var builder = new DiscordWebhookBuilder()
+ .WithContent(pages.First().Content)
+ .AddEmbed(pages.First().Embed)
+ .AddComponents(bts.ButtonArray);
+ message = await interaction.EditOriginalResponseAsync(builder).ConfigureAwait(false);
+ }
+ else
+ {
+ var builder = new DiscordInteractionResponseBuilder()
.WithContent(pages.First().Content)
.AddEmbed(pages.First().Embed)
.AsEphemeral(ephemeral)
.AddComponents(bts.ButtonArray);
-
- await interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder).ConfigureAwait(false);
- var message = await interaction.GetOriginalResponseAsync().ConfigureAwait(false);
+ await interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder).ConfigureAwait(false);
+ message = await interaction.GetOriginalResponseAsync().ConfigureAwait(false);
+ }
var req = new InteractionPaginationRequest(interaction, message, user, bhv, del, bts, pages, token);
await this._compPaginator.DoPaginationAsync(req).ConfigureAwait(false);
}
///
/// Waits for a custom pagination request to finish.
/// This does NOT handle removing emojis after finishing for you.
///
///
///
public async Task WaitForCustomPaginationAsync(IPaginationRequest request) => await this._paginator.DoPaginationAsync(request).ConfigureAwait(false);
///
/// Waits for custom button-based pagination request to finish.
///
/// This does not invoke .
///
/// The request to wait for.
public async Task WaitForCustomComponentPaginationAsync(IPaginationRequest request) => await this._compPaginator.DoPaginationAsync(request).ConfigureAwait(false);
///
/// Generates pages from a string, and puts them in message content.
///
/// Input string.
/// How to split input string.
///
public IEnumerable GeneratePagesInContent(string input, SplitType splitType = SplitType.Character)
{
if (string.IsNullOrEmpty(input))
throw new ArgumentException("You must provide a string that is not null or empty!");
var result = new List();
List split;
switch (splitType)
{
default:
case SplitType.Character:
split = this.SplitString(input, 500).ToList();
break;
case SplitType.Line:
var subsplit = input.Split('\n');
split = new List();
var s = "";
for (var i = 0; i < subsplit.Length; i++)
{
s += subsplit[i];
if (i >= 15 && i % 15 == 0)
{
split.Add(s);
s = "";
}
}
if (split.All(x => x != s))
split.Add(s);
break;
}
var page = 1;
foreach (var s in split)
{
result.Add(new Page($"Page {page}:\n{s}"));
page++;
}
return result;
}
///
/// Generates pages from a string, and puts them in message embeds.
///
/// Input string.
/// How to split input string.
/// Base embed for output embeds.
///
public IEnumerable GeneratePagesInEmbed(string input, SplitType splitType = SplitType.Character, DiscordEmbedBuilder embedBase = null)
{
if (string.IsNullOrEmpty(input))
throw new ArgumentException("You must provide a string that is not null or empty!");
var embed = embedBase ?? new DiscordEmbedBuilder();
var result = new List();
List split;
switch (splitType)
{
default:
case SplitType.Character:
split = this.SplitString(input, 500).ToList();
break;
case SplitType.Line:
var subsplit = input.Split('\n');
split = new List();
var s = "";
for (var i = 0; i < subsplit.Length; i++)
{
s += $"{subsplit[i]}\n";
if (i % 15 == 0 && i != 0)
{
split.Add(s);
s = "";
}
}
if (!split.Any(x => x == s))
split.Add(s);
break;
}
var page = 1;
foreach (var s in split)
{
result.Add(new Page("", new DiscordEmbedBuilder(embed).WithDescription(s).WithFooter($"Page {page}/{split.Count}")));
page++;
}
return result;
}
///
/// Splits the string.
///
/// The string.
/// The chunk size.
private List SplitString(string str, int chunkSize)
{
var res = new List();
var len = str.Length;
var i = 0;
while (i < len)
{
var size = Math.Min(len - i, chunkSize);
res.Add(str.Substring(i, size));
i += size;
}
return res;
}
///
/// Gets the cancellation token.
///
/// The timeout.
private CancellationToken GetCancellationToken(TimeSpan? timeout = null) => new CancellationTokenSource(timeout ?? this.Config.Timeout).Token;
///
/// Handles an invalid interaction.
///
/// The interaction.
private async Task HandleInvalidInteraction(DiscordInteraction interaction)
{
var at = this.Config.ResponseBehavior switch
{
InteractionResponseBehavior.Ack => interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate),
InteractionResponseBehavior.Respond => interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder { Content = this.Config.ResponseMessage, IsEphemeral = true}),
InteractionResponseBehavior.Ignore => Task.CompletedTask,
_ => throw new ArgumentException("Unknown enum value.")
};
await at;
}
}