PluralKit/PluralKit.Bot/Utils/ContextUtils.cs

276 lines
13 KiB
C#
Raw Normal View History

2019-04-29 15:42:09 +00:00
using System;
using System.Collections.Generic;
using System.Linq;
2020-05-05 14:03:46 +00:00
using System.Threading;
2019-04-29 15:42:09 +00:00
using System.Threading.Tasks;
2020-05-05 14:03:46 +00:00
using Autofac;
2020-12-24 13:52:44 +00:00
using Myriad.Builders;
using Myriad.Gateway;
using Myriad.Rest.Exceptions;
using Myriad.Rest.Types;
using Myriad.Rest.Types.Requests;
using Myriad.Types;
2020-05-05 14:03:46 +00:00
using NodaTime;
using PluralKit.Core;
2019-10-05 05:41:00 +00:00
2019-04-29 15:42:09 +00:00
namespace PluralKit.Bot {
public static class ContextUtils {
public static async Task<bool> ConfirmClear(this Context ctx, string toClear)
{
if (!(await ctx.PromptYesNo($"{Emojis.Warn} Are you sure you want to clear {toClear}?"))) throw Errors.GenericCancelled();
else return true;
}
2020-12-24 13:52:44 +00:00
public static async Task<bool> PromptYesNo(this Context ctx, string msgString, User user = null, Duration? timeout = null, AllowedMentions mentions = null, bool matchFlag = true)
2020-05-05 14:03:46 +00:00
{
2020-12-24 13:52:44 +00:00
Message message;
if (matchFlag && ctx.MatchFlag("y", "yes")) return true;
2020-07-21 00:10:26 +00:00
else message = await ctx.Reply(msgString, mentions: mentions);
2020-05-05 14:03:46 +00:00
var cts = new CancellationTokenSource();
2021-01-31 15:16:52 +00:00
if (user == null) user = ctx.Author;
2020-05-05 14:03:46 +00:00
if (timeout == null) timeout = Duration.FromMinutes(5);
// "Fork" the task adding the reactions off so we don't have to wait for them to be finished to start listening for presses
2021-01-31 15:16:52 +00:00
await ctx.Rest.CreateReactionsBulk(message, new[] {Emojis.Success, Emojis.Error});
2020-05-05 14:03:46 +00:00
2020-12-24 13:52:44 +00:00
bool ReactionPredicate(MessageReactionAddEvent e)
2020-05-05 14:03:46 +00:00
{
2020-12-24 13:52:44 +00:00
if (e.ChannelId != message.ChannelId || e.MessageId != message.Id) return false;
if (e.UserId != user.Id) return false;
2020-05-05 14:03:46 +00:00
return true;
}
2020-12-24 13:52:44 +00:00
bool MessagePredicate(MessageCreateEvent e)
2020-05-05 14:03:46 +00:00
{
2020-12-24 13:52:44 +00:00
if (e.ChannelId != message.ChannelId) return false;
2020-05-05 14:03:46 +00:00
if (e.Author.Id != user.Id) return false;
var strings = new [] {"y", "yes", "n", "no"};
2020-12-24 13:52:44 +00:00
return strings.Any(str => string.Equals(e.Content, str, StringComparison.InvariantCultureIgnoreCase));
2020-05-05 14:03:46 +00:00
}
2020-12-24 13:52:44 +00:00
var messageTask = ctx.Services.Resolve<HandlerQueue<MessageCreateEvent>>().WaitFor(MessagePredicate, timeout, cts.Token);
var reactionTask = ctx.Services.Resolve<HandlerQueue<MessageReactionAddEvent>>().WaitFor(ReactionPredicate, timeout, cts.Token);
2020-05-05 14:03:46 +00:00
var theTask = await Task.WhenAny(messageTask, reactionTask);
cts.Cancel();
if (theTask == messageTask)
{
2020-12-24 13:52:44 +00:00
var responseMsg = (await messageTask);
2020-05-05 14:03:46 +00:00
var positives = new[] {"y", "yes"};
2020-12-24 13:52:44 +00:00
return positives.Any(p => string.Equals(responseMsg.Content, p, StringComparison.InvariantCultureIgnoreCase));
2020-05-05 14:03:46 +00:00
}
if (theTask == reactionTask)
return (await reactionTask).Emoji.Name == Emojis.Success;
return false;
2019-04-29 15:42:09 +00:00
}
2020-12-24 13:52:44 +00:00
public static async Task<MessageReactionAddEvent> AwaitReaction(this Context ctx, Message message, User user = null, Func<MessageReactionAddEvent, bool> predicate = null, Duration? timeout = null)
{
bool ReactionPredicate(MessageReactionAddEvent evt)
{
if (message.Id != evt.MessageId) return false; // Ignore reactions for different messages
if (user != null && user.Id != evt.UserId) return false; // Ignore messages from other users if a user was defined
if (predicate != null && !predicate.Invoke(evt)) return false; // Check predicate
return true;
2019-04-29 15:42:09 +00:00
}
2020-12-24 13:52:44 +00:00
return await ctx.Services.Resolve<HandlerQueue<MessageReactionAddEvent>>().WaitFor(ReactionPredicate, timeout);
2019-04-29 15:42:09 +00:00
}
2019-10-05 05:41:00 +00:00
public static async Task<bool> ConfirmWithReply(this Context ctx, string expectedReply)
2019-05-13 21:08:44 +00:00
{
2020-12-24 13:52:44 +00:00
bool Predicate(MessageCreateEvent e) =>
2021-01-31 15:16:52 +00:00
e.Author.Id == ctx.Author.Id && e.ChannelId == ctx.Channel.Id;
2020-05-05 14:03:46 +00:00
2020-12-24 13:52:44 +00:00
var msg = await ctx.Services.Resolve<HandlerQueue<MessageCreateEvent>>()
2020-05-05 14:03:46 +00:00
.WaitFor(Predicate, Duration.FromMinutes(1));
2020-12-24 13:52:44 +00:00
return string.Equals(msg.Content, expectedReply, StringComparison.InvariantCultureIgnoreCase);
2019-05-13 21:08:44 +00:00
}
2019-04-29 15:42:09 +00:00
2020-12-24 13:52:44 +00:00
public static async Task Paginate<T>(this Context ctx, IAsyncEnumerable<T> items, int totalCount, int itemsPerPage, string title, Func<EmbedBuilder, IEnumerable<T>, Task> renderer) {
2019-06-13 21:42:39 +00:00
// TODO: make this generic enough we can use it in Choose<T> below
var buffer = new List<T>();
await using var enumerator = items.GetAsyncEnumerator();
var pageCount = (int) Math.Ceiling(totalCount / (double) itemsPerPage);
2020-12-24 13:52:44 +00:00
async Task<Embed> MakeEmbedForPage(int page)
{
var bufferedItemsNeeded = (page + 1) * itemsPerPage;
while (buffer.Count < bufferedItemsNeeded && await enumerator.MoveNextAsync())
buffer.Add(enumerator.Current);
2020-12-24 13:52:44 +00:00
var eb = new EmbedBuilder();
eb.Title(pageCount > 1 ? $"[{page+1}/{pageCount}] {title}" : title);
await renderer(eb, buffer.Skip(page*itemsPerPage).Take(itemsPerPage));
2019-04-29 15:42:09 +00:00
return eb.Build();
}
try
{
var msg = await ctx.Reply(embed: await MakeEmbedForPage(0));
if (pageCount <= 1) return; // If we only have one (or no) page, don't bother with the reaction/pagination logic, lol
string[] botEmojis = { "\u23EA", "\u2B05", "\u27A1", "\u23E9", Emojis.Error };
2020-12-24 13:52:44 +00:00
2021-01-31 15:16:52 +00:00
var _ = ctx.Rest.CreateReactionsBulk(msg, botEmojis); // Again, "fork"
2019-04-29 15:42:09 +00:00
try {
var currentPage = 0;
while (true) {
2021-01-31 15:16:52 +00:00
var reaction = await ctx.AwaitReaction(msg, ctx.Author, timeout: Duration.FromMinutes(5));
2019-04-29 15:42:09 +00:00
// Increment/decrement page counter based on which reaction was clicked
if (reaction.Emoji.Name == "\u23EA") currentPage = 0; // <<
if (reaction.Emoji.Name == "\u2B05") currentPage = (currentPage - 1) % pageCount; // <
if (reaction.Emoji.Name == "\u27A1") currentPage = (currentPage + 1) % pageCount; // >
if (reaction.Emoji.Name == "\u23E9") currentPage = pageCount - 1; // >>
if (reaction.Emoji.Name == Emojis.Error) break; // X
// C#'s % operator is dumb and wrong, so we fix negative numbers
if (currentPage < 0) currentPage += pageCount;
// If we can, remove the user's reaction (so they can press again quickly)
if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages))
2021-01-31 15:16:52 +00:00
await ctx.Rest.DeleteUserReaction(msg.ChannelId, msg.Id, reaction.Emoji, reaction.UserId);
// Edit the embed with the new page
var embed = await MakeEmbedForPage(currentPage);
2021-01-31 15:16:52 +00:00
await ctx.Rest.EditMessage(msg.ChannelId, msg.Id, new MessageEditRequest {Embed = embed});
}
} catch (TimeoutException) {
// "escape hatch", clean up as if we hit X
2019-04-29 15:42:09 +00:00
}
if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages))
2021-01-31 15:16:52 +00:00
await ctx.Rest.DeleteAllReactions(msg.ChannelId, msg.Id);
}
// If we get a "NotFound" error, the message has been deleted and thus not our problem
catch (NotFoundException) { }
2019-04-29 15:42:09 +00:00
}
2019-06-13 21:42:39 +00:00
2019-10-05 05:41:00 +00:00
public static async Task<T> Choose<T>(this Context ctx, string description, IList<T> items, Func<T, string> display = null)
2019-06-13 21:42:39 +00:00
{
// Generate a list of :regional_indicator_?: emoji surrogate pairs (starting at codepoint 0x1F1E6)
// We just do 7 (ABCDEFG), this amount is arbitrary (although sending a lot of emojis takes a while)
var pageSize = 7;
var indicators = new string[pageSize];
for (var i = 0; i < pageSize; i++) indicators[i] = char.ConvertFromUtf32(0x1F1E6 + i);
// Default to x.ToString()
if (display == null) display = x => x.ToString();
string MakeOptionList(int page)
{
var makeOptionList = string.Join("\n", items
.Skip(page * pageSize)
.Take(pageSize)
.Select((x, i) => $"{indicators[i]} {display(x)}"));
return makeOptionList;
}
// If we have more items than the page size, we paginate as appropriate
if (items.Count > pageSize)
{
var currPage = 0;
var pageCount = (items.Count-1) / pageSize + 1;
// Send the original message
var msg = await ctx.Reply($"**[Page {currPage + 1}/{pageCount}]**\n{description}\n{MakeOptionList(currPage)}");
2019-06-13 21:42:39 +00:00
// Add back/forward reactions and the actual indicator emojis
async Task AddEmojis()
{
2021-01-31 15:16:52 +00:00
await ctx.Rest.CreateReaction(msg.ChannelId, msg.Id, new() { Name = "\u2B05" });
await ctx.Rest.CreateReaction(msg.ChannelId, msg.Id, new() { Name = "\u27A1" });
2020-12-24 13:52:44 +00:00
for (int i = 0; i < items.Count; i++)
2021-01-31 15:16:52 +00:00
await ctx.Rest.CreateReaction(msg.ChannelId, msg.Id, new() { Name = indicators[i] });
2019-06-13 21:42:39 +00:00
}
var _ = AddEmojis(); // Not concerned about awaiting
2019-06-13 21:42:39 +00:00
while (true)
{
// Wait for a reaction
2021-01-31 15:16:52 +00:00
var reaction = await ctx.AwaitReaction(msg, ctx.Author);
2019-06-13 21:42:39 +00:00
// If it's a movement reaction, inc/dec the page index
if (reaction.Emoji.Name == "\u2B05") currPage -= 1; // <
if (reaction.Emoji.Name == "\u27A1") currPage += 1; // >
2019-06-13 21:42:39 +00:00
if (currPage < 0) currPage += pageCount;
if (currPage >= pageCount) currPage -= pageCount;
// If it's an indicator emoji, return the relevant item
if (indicators.Contains(reaction.Emoji.Name))
2019-06-13 21:42:39 +00:00
{
var idx = Array.IndexOf(indicators, reaction.Emoji.Name) + pageSize * currPage;
2019-06-13 21:42:39 +00:00
// only if it's in bounds, though
// eg. 8 items, we're on page 2, and I hit D (3 + 1*7 = index 10 on an 8-long list) = boom
if (idx < items.Count) return items[idx];
}
2021-01-31 15:16:52 +00:00
var __ = ctx.Rest.DeleteUserReaction(msg.ChannelId, msg.Id, reaction.Emoji, ctx.Author.Id);
await ctx.Rest.EditMessage(msg.ChannelId, msg.Id,
2020-12-24 13:52:44 +00:00
new()
{
Content =
$"**[Page {currPage + 1}/{pageCount}]**\n{description}\n{MakeOptionList(currPage)}"
});
2019-06-13 21:42:39 +00:00
}
}
else
{
var msg = await ctx.Reply($"{description}\n{MakeOptionList(0)}");
2019-06-13 21:42:39 +00:00
// Add the relevant reactions (we don't care too much about awaiting)
async Task AddEmojis()
{
2020-12-24 13:52:44 +00:00
for (int i = 0; i < items.Count; i++)
2021-01-31 15:16:52 +00:00
await ctx.Rest.CreateReaction(msg.ChannelId, msg.Id, new() {Name = indicators[i]});
2019-06-13 21:42:39 +00:00
}
var _ = AddEmojis();
2019-06-13 21:42:39 +00:00
// Then wait for a reaction and return whichever one we found
2021-01-31 15:16:52 +00:00
var reaction = await ctx.AwaitReaction(msg, ctx.Author,rx => indicators.Contains(rx.Emoji.Name));
return items[Array.IndexOf(indicators, reaction.Emoji.Name)];
2019-06-13 21:42:39 +00:00
}
}
2019-10-05 05:41:00 +00:00
public static async Task BusyIndicator(this Context ctx, Func<Task> f, string emoji = "\u23f3" /* hourglass */)
{
await ctx.BusyIndicator<object>(async () =>
{
await f();
return null;
}, emoji);
}
2019-10-05 05:41:00 +00:00
public static async Task<T> BusyIndicator<T>(this Context ctx, Func<Task<T>> f, string emoji = "\u23f3" /* hourglass */)
{
var task = f();
// If we don't have permission to add reactions, don't bother, and just await the task normally.
var neededPermissions = PermissionSet.AddReactions | PermissionSet.ReadMessageHistory;
if ((ctx.BotPermissions & neededPermissions) != neededPermissions) return await task;
try
{
2021-01-31 15:16:52 +00:00
await Task.WhenAll(ctx.Rest.CreateReaction(ctx.Message.ChannelId, ctx.Message.Id, new() {Name = emoji}), task);
return await task;
}
finally
{
2021-01-31 15:16:52 +00:00
var _ = ctx.Rest.DeleteOwnReaction(ctx.Message.ChannelId, ctx.Message.Id, new() { Name = emoji });
}
2019-10-05 05:41:00 +00:00
}
2019-04-29 15:42:09 +00:00
}
}