2021-03-30 10:25:07 +00:00
// Library for Discord-specific functions
2021-03-30 12:16:31 +00:00
import {
Message ,
Guild ,
GuildMember ,
Permissions ,
TextChannel ,
DMChannel ,
NewsChannel ,
MessageOptions
} from "discord.js" ;
2021-04-01 10:44:44 +00:00
import { unreactEventListeners , replyEventListeners } from "./eventListeners" ;
2021-03-30 10:25:07 +00:00
2021-04-01 10:44:44 +00:00
export type SingleMessageOptions = MessageOptions & { split? : false } ;
2021-03-30 10:25:07 +00:00
2021-03-31 02:56:25 +00:00
export function botHasPermission ( guild : Guild | null , permission : number ) : boolean {
return ! ! guild ? . me ? . hasPermission ( permission ) ;
2021-03-30 10:25:07 +00:00
}
// Maybe promisify this section to reduce the potential for creating callback hell? Especially if multiple questions in a row are being asked.
// Pagination function that allows for customization via a callback.
// Define your own pages outside the function because this only manages the actual turning of pages.
export async function paginate (
2021-03-30 12:16:31 +00:00
channel : TextChannel | DMChannel | NewsChannel ,
2021-03-30 10:25:07 +00:00
senderID : string ,
total : number ,
2021-04-01 10:44:44 +00:00
callback : ( page : number , hasMultiplePages : boolean ) = > SingleMessageOptions ,
2021-03-30 10:25:07 +00:00
duration = 60000
) {
2021-03-30 12:16:31 +00:00
const hasMultiplePages = total > 1 ;
const message = await channel . send ( callback ( 0 , hasMultiplePages ) ) ;
2021-03-30 10:25:07 +00:00
2021-03-30 12:16:31 +00:00
if ( hasMultiplePages ) {
let page = 0 ;
const turn = ( amount : number ) = > {
page += amount ;
2021-03-30 10:25:07 +00:00
2021-03-30 12:16:31 +00:00
if ( page < 0 ) page += total ;
else if ( page >= total ) page -= total ;
2021-03-30 10:25:07 +00:00
2021-03-30 12:16:31 +00:00
message . edit ( callback ( page , true ) ) ;
} ;
const BACKWARDS_EMOJI = "⬅️" ;
const FORWARDS_EMOJI = "➡️" ;
const handle = ( emote : string , reacterID : string ) = > {
if ( senderID === reacterID ) {
switch ( emote ) {
case BACKWARDS_EMOJI :
turn ( - 1 ) ;
break ;
case FORWARDS_EMOJI :
turn ( 1 ) ;
break ;
}
2021-03-30 10:25:07 +00:00
}
2021-03-30 12:16:31 +00:00
} ;
// Listen for reactions and call the handler.
let backwardsReaction = await message . react ( BACKWARDS_EMOJI ) ;
let forwardsReaction = await message . react ( FORWARDS_EMOJI ) ;
2021-04-01 10:44:44 +00:00
unreactEventListeners . set ( message . id , handle ) ;
2021-03-30 12:16:31 +00:00
const collector = message . createReactionCollector (
( reaction , user ) = > {
if ( user . id === senderID ) {
// The reason this is inside the call is because it's possible to switch a user's permissions halfway and suddenly throw an error.
// This will dynamically adjust for that, switching modes depending on whether it currently has the "Manage Messages" permission.
const canDeleteEmotes = botHasPermission ( message . guild , Permissions . FLAGS . MANAGE_MESSAGES ) ;
handle ( reaction . emoji . name , user . id ) ;
if ( canDeleteEmotes ) reaction . users . remove ( user ) ;
collector . resetTimer ( ) ;
}
2021-03-30 10:25:07 +00:00
2021-03-30 12:16:31 +00:00
return false ;
} ,
// Apparently, regardless of whether you put "time" or "idle", it won't matter to the collector.
// In order to actually reset the timer, you have to do it manually via collector.resetTimer().
{ time : duration }
) ;
// When time's up, remove the bot's own reactions.
collector . on ( "end" , ( ) = > {
2021-04-01 10:44:44 +00:00
unreactEventListeners . delete ( message . id ) ;
2021-03-30 12:16:31 +00:00
backwardsReaction . users . remove ( message . author ) ;
forwardsReaction . users . remove ( message . author ) ;
} ) ;
}
2021-03-30 10:25:07 +00:00
}
// Waits for the sender to either confirm an action or let it pass (and delete the message).
// This should probably be renamed to "confirm" now that I think of it, "prompt" is better used elsewhere.
// Append "\n*(This message will automatically be deleted after 10 seconds.)*" in the future?
export async function prompt ( message : Message , senderID : string , onConfirm : ( ) = > void , duration = 10000 ) {
let isDeleted = false ;
message . react ( "✅" ) ;
await message . awaitReactions (
( reaction , user ) = > {
if ( user . id === senderID ) {
if ( reaction . emoji . name === "✅" ) {
onConfirm ( ) ;
isDeleted = true ;
message . delete ( ) ;
}
}
// CollectorFilter requires a boolean to be returned.
// My guess is that the return value of awaitReactions can be altered by making a boolean filter.
// However, because that's not my concern with this command, I don't have to worry about it.
// May as well just set it to false because I'm not concerned with collecting any reactions.
return false ;
} ,
{ time : duration }
) ;
if ( ! isDeleted ) message . delete ( ) ;
}
// Asks the user for some input using the inline reply feature. The message here is a message you send beforehand.
// If the reply is rejected, reply with an error message (when stable support comes from discord.js).
// Append "\n*(Note: Make sure to use Discord's inline reply feature or this won't work!)*" in the future? And also the "you can now reply to this message" edit.
export function ask (
message : Message ,
senderID : string ,
condition : ( reply : string ) = > boolean ,
onSuccess : ( ) = > void ,
onReject : ( ) = > string ,
timeout = 60000
) {
const referenceID = ` ${ message . channel . id } - ${ message . id } ` ;
replyEventListeners . set ( referenceID , ( reply ) = > {
if ( reply . author . id === senderID ) {
if ( condition ( reply . content ) ) {
onSuccess ( ) ;
replyEventListeners . delete ( referenceID ) ;
} else {
reply . reply ( onReject ( ) ) ;
}
}
} ) ;
setTimeout ( ( ) = > {
replyEventListeners . set ( referenceID , ( reply ) = > {
reply . reply ( "that action timed out, try using the command again" ) ;
replyEventListeners . delete ( referenceID ) ;
} ) ;
} , timeout ) ;
}
export function askYesOrNo ( message : Message , senderID : string , timeout = 30000 ) : Promise < boolean > {
return new Promise ( async ( resolve , reject ) = > {
let isDeleted = false ;
await message . react ( "✅" ) ;
message . react ( "❌" ) ;
await message . awaitReactions (
( reaction , user ) = > {
if ( user . id === senderID ) {
const isCheckReacted = reaction . emoji . name === "✅" ;
if ( isCheckReacted || reaction . emoji . name === "❌" ) {
resolve ( isCheckReacted ) ;
isDeleted = true ;
message . delete ( ) ;
}
}
return false ;
} ,
{ time : timeout }
) ;
if ( ! isDeleted ) {
message . delete ( ) ;
reject ( "Prompt timed out." ) ;
}
} ) ;
}
// This MUST be split into an array. These emojis are made up of several characters each, adding up to 29 in length.
const multiNumbers = [ "1️ ⃣" , "2️ ⃣" , "3️ ⃣" , "4️ ⃣" , "5️ ⃣" , "6️ ⃣" , "7️ ⃣" , "8️ ⃣" , "9️ ⃣" , "🔟" ] ;
// This will bring up an option to let the user choose between one option out of many.
// This definitely needs a single callback alternative, because using the numerical version isn't actually that uncommon of a pattern.
export async function askMultipleChoice (
message : Message ,
senderID : string ,
callbackStack : ( ( ) = > void ) [ ] ,
timeout = 90000
) {
if ( callbackStack . length > multiNumbers . length ) {
message . channel . send (
` \` ERROR: The amount of callbacks in "askMultipleChoice" must not exceed the total amount of allowed options ( ${ multiNumbers . length } )! \` `
) ;
return ;
}
let isDeleted = false ;
for ( let i = 0 ; i < callbackStack . length ; i ++ ) {
await message . react ( multiNumbers [ i ] ) ;
}
await message . awaitReactions (
( reaction , user ) = > {
if ( user . id === senderID ) {
const index = multiNumbers . indexOf ( reaction . emoji . name ) ;
if ( index !== - 1 ) {
callbackStack [ index ] ( ) ;
isDeleted = true ;
message . delete ( ) ;
}
}
return false ;
} ,
{ time : timeout }
) ;
if ( ! isDeleted ) message . delete ( ) ;
}
export async function getMemberByUsername ( guild : Guild , username : string ) {
return (
await guild . members . fetch ( {
query : username ,
limit : 1
} )
) . first ( ) ;
}
/** Convenience function to handle false cases automatically. */
export async function callMemberByUsername (
message : Message ,
username : string ,
onSuccess : ( member : GuildMember ) = > void
) {
const guild = message . guild ;
const send = message . channel . send ;
if ( guild ) {
const member = await getMemberByUsername ( guild , username ) ;
if ( member ) onSuccess ( member ) ;
else send ( ` Couldn't find a user by the name of \` ${ username } \` ! ` ) ;
} else send ( "You must execute this command in a server!" ) ;
}