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-04-17 21:10:01 +00:00
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 ;
2020-12-22 15:55:13 +00:00
using Myriad.Types ;
2020-05-05 14:03:46 +00:00
using NodaTime ;
2020-02-12 14:16:19 +00:00
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 {
2020-10-04 08:53:07 +00:00
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 ;
2020-10-20 11:33:35 +00:00
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 ) ;
2021-04-01 20:58:48 +00:00
if ( ! DiscordUtils . HasReactionPermissions ( ctx ) )
await ctx . Reply ( $"{Emojis.Note} PluralKit does not have permissions to add reactions in this channel. \nPlease reply with 'yes' to confirm, or 'no' to cancel." ) ;
else
2019-12-26 19:27:22 +00:00
// "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
2021-03-28 17:22:31 +00:00
public static async Task Paginate < T > ( this Context ctx , IAsyncEnumerable < T > items , int totalCount , int itemsPerPage , string title , string color , 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
2020-01-17 23:58:35 +00:00
var buffer = new List < T > ( ) ;
await using var enumerator = items . GetAsyncEnumerator ( ) ;
2020-06-15 11:00:28 +00:00
var pageCount = ( int ) Math . Ceiling ( totalCount / ( double ) itemsPerPage ) ;
2020-12-24 13:52:44 +00:00
async Task < Embed > MakeEmbedForPage ( int page )
2020-01-17 23:58:35 +00:00
{
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 ) ;
2021-03-28 17:22:31 +00:00
if ( color ! = null )
eb . Color ( color . ToDiscordColor ( ) ) ;
2020-01-17 23:58:35 +00:00
await renderer ( eb , buffer . Skip ( page * itemsPerPage ) . Take ( itemsPerPage ) ) ;
2019-04-29 15:42:09 +00:00
return eb . Build ( ) ;
}
2020-01-03 13:02:25 +00:00
try
{
2020-02-24 08:57:16 +00:00
var msg = await ctx . Reply ( embed : await MakeEmbedForPage ( 0 ) ) ;
2020-06-18 15:40:51 +00:00
if ( pageCount < = 1 ) return ; // If we only have one (or no) page, don't bother with the reaction/pagination logic, lol
2020-04-17 21:10:01 +00:00
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
2020-01-03 13:02:25 +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
2020-01-03 13:02:25 +00:00
// Increment/decrement page counter based on which reaction was clicked
2020-04-17 21:10:01 +00:00
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
2020-01-03 13:02:25 +00:00
// 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)
2020-12-22 15:55:13 +00:00
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 ) ;
2020-01-03 13:02:25 +00:00
// Edit the embed with the new page
2020-01-17 23:58:35 +00:00
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 } ) ;
2020-01-03 13:02:25 +00:00
}
} catch ( TimeoutException ) {
// "escape hatch", clean up as if we hit X
2019-04-29 15:42:09 +00:00
}
2020-12-22 15:55:13 +00:00
if ( ctx . BotPermissions . HasFlag ( PermissionSet . ManageMessages ) )
2021-01-31 15:16:52 +00:00
await ctx . Rest . DeleteAllReactions ( msg . ChannelId , msg . Id ) ;
2020-01-03 13:02:25 +00:00
}
// If we get a "NotFound" error, the message has been deleted and thus not our problem
2020-04-17 21:10:01 +00:00
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
2020-02-24 08:57:16 +00:00
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
}
2019-07-21 14:43:28 +00:00
var _ = AddEmojis ( ) ; // Not concerned about awaiting
2020-04-17 21:10:01 +00:00
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
2020-04-17 21:10:01 +00:00
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
2020-04-17 21:10:01 +00:00
if ( indicators . Contains ( reaction . Emoji . Name ) )
2019-06-13 21:42:39 +00:00
{
2020-04-17 21:10:01 +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
{
2020-02-24 08:57:16 +00:00
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
}
2019-07-21 14:43:28 +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 ) ) ;
2020-04-17 21:10:01 +00:00
return items [ Array . IndexOf ( indicators , reaction . Emoji . Name ) ] ;
2019-06-13 21:42:39 +00:00
}
}
2020-12-22 15:55:13 +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 */ )
2019-05-16 23:23:09 +00:00
{
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 */ )
2019-05-16 23:23:09 +00:00
{
var task = f ( ) ;
2019-08-12 16:07:29 +00:00
// If we don't have permission to add reactions, don't bother, and just await the task normally.
2021-04-01 20:58:48 +00:00
if ( ! DiscordUtils . HasReactionPermissions ( ctx ) ) return await task ;
2019-08-12 16:07:29 +00:00
2019-05-16 23:23:09 +00:00
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 ) ;
2019-05-16 23:23:09 +00:00
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-05-16 23:23:09 +00:00
}
2019-10-05 05:41:00 +00:00
}
2019-04-29 15:42:09 +00:00
}
}