diff --git a/DisCatSharp.Interactivity/InteractivityExtension.cs b/DisCatSharp.Interactivity/InteractivityExtension.cs
index c5ca85171..37e5bdc4b 100644
--- a/DisCatSharp.Interactivity/InteractivityExtension.cs
+++ b/DisCatSharp.Interactivity/InteractivityExtension.cs
@@ -1,927 +1,930 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 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.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.EventArgs;
using DisCatSharp.Interactivity.Enums;
using DisCatSharp.Interactivity.EventHandling;
using DisCatSharp.Common.Utilities;
using DisCatSharp.Enums;
using System.Threading;
using System.Globalization;
namespace DisCatSharp.Interactivity
{
///
/// Extension class for DSharpPlus.Interactivity
///
public class InteractivityExtension : BaseExtension
{
#pragma warning disable IDE1006 // Naming Styles
///
/// Gets the config.
///
internal InteractivityConfiguration Config { get; }
private EventWaiter MessageCreatedWaiter;
private EventWaiter MessageReactionAddWaiter;
private EventWaiter TypingStartWaiter;
private EventWaiter ComponentInteractionWaiter;
private ComponentEventWaiter ComponentEventWaiter;
private ReactionCollector ReactionCollector;
private Poller Poller;
private Paginator Paginator;
private ComponentPaginator _compPaginator;
#pragma warning restore IDE1006 // Naming Styles
///
/// 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.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(this.Client, this.Config);
this.ComponentEventWaiter = new(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(message,
c =>
c.Interaction.Data.ComponentType == ComponentType.Button &&
buttons.Any(b => b.CustomId == c.Id), token)).ConfigureAwait(false);
return new(res is null, res);
}
///
/// 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(message, c => c.Interaction.Data.ComponentType == ComponentType.Button && ids.Contains(c.Id), token))
.ConfigureAwait(false);
return new(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(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.User == user, token))
.ConfigureAwait(false);
return new(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(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.Id == id, token))
.ConfigureAwait(false);
return new(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(message, c => c.Interaction.Data.ComponentType is ComponentType.Button && predicate(c), token))
.ConfigureAwait(false);
return new(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(message, c => c.Interaction.Data.ComponentType is ComponentType.Select && predicate(c), token))
.ConfigureAwait(false);
return new(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().Any(c => c.CustomId == id))
throw new ArgumentException($"Message does not contain select component with Id of '{id}'.");
var result = await this
.ComponentEventWaiter
.WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Select && c.Id == id, token))
.ConfigureAwait(false);
return new(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().Any(c => c.CustomId == id))
throw new ArgumentException($"Message does not contain select with Id of '{id}'.");
var result = await this
.ComponentEventWaiter
.WaitForMatchAsync(new(message, (c) => c.Id == id && c.User == user, token)).ConfigureAwait(false);
return new(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(bts);
- if (this.Config.PaginationBehaviour is PaginationBehaviour.Ignore)
+ 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.ToArray(), 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.
/// The timeoutoverride.
/// 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.ToArray());
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 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)
{
var bhv = behaviour ?? this.Config.PaginationBehaviour;
var del = deletion ?? this.Config.ButtonBehavior;
var bts = buttons ?? this.Config.PaginationButtons;
bts = new(bts);
- bts.SkipLeft.Disable();
- bts.Left.Disable();
+ if (bhv is PaginationBehaviour.Ignore)
+ {
+ bts.SkipLeft.Disable();
+ bts.Left.Disable();
+ }
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);
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() { Content = this.Config.ResponseMessage, IsEphemeral = true}),
InteractionResponseBehavior.Ignore => Task.CompletedTask,
_ => throw new ArgumentException("Unknown enum value.")
};
await at;
}
}
}