import { PartialEmoji , GuildTextableChannel , Message , MessageContent , User , Client } from 'eris' ; import ECH from 'eris-command-handler' ; import ms from 'ms' ; type ReactionHandler = ( message : Message < GuildTextableChannel > , user : User ) => void | Promise < void > ; export interface ReactionMenuState { reactions : EmojiMap ; message : MessageContent | ( ( ) => MessageContent | Promise < MessageContent > ) ; } export class EmojiMap extends Map < PartialEmoji , ReactionHandler > { constructor ( entries ? ) { super ( entries ) ; } get ( key ) { let reactions = Array . from ( this . entries ( ) ) ; let value = reactions . find ( r => r [ 0 ] . id === key . id && r [ 0 ] . name === key . name ) ; return value ? value [ 1 ] : undefined ; } has ( key ) { let reactions = Array . from ( this . keys ( ) ) ; let value = reactions . find ( r => r . id === key . id && r . name === key . name ) ; return ! ! value ; } delete ( key ) { let reactions = Array . from ( this . keys ( ) ) ; let origKey = reactions . find ( r => r . id === key . id && r . name === key . name ) ; if ( ! origKey ) return false ; return super . delete ( origKey ) ; } } name description export default class ReactionMenu { private isDeleted : boolean = false ; private client : ECH . CommandClient ; private channel : GuildTextableChannel ; private originalMessage : Message < GuildTextableChannel > ; private reactors : string [ ] = [ ] ; private state : string = 'default' ; private ready : boolean = false ; private states : Map < string , ReactionMenuState > = new Map ( ) ; private message : Message < GuildTextableChannel > ; private timeout : NodeJS . Timeout ; private timeoutDur : number ; private reactionHandlerInstance : ( msg : Message < GuildTextableChannel > , emoji : PartialEmoji , user : User ) => void ; param { Client } client param { Message < GuildTextableChannel > } msg param { ReactionMenuState } defaultState param { string | number } timeout constructor ( client , msg , defaultState , timeout ? ) { this . client = client ; this . originalMessage = msg ; this . channel = this . originalMessage . channel ; this . timeoutDur = timeout !== undefined ? typeof timeout === 'number' ? timeout : typeof timeout === 'string' ? ms ( timeout ) : ms ( '1m' ) : ms ( '1m' ) ; this . reactors . push ( msg . author . id ) ; if ( ! this . channel . permissionsOf ( this . client . user . id ) . has ( 'manageMessages' ) ) { this . channel . createMessage ( "Well, that's embarrassing, but it looks like I don't have the permission to manage messages in this channel, please check the permissions of the channel and make sure I can manage messages! (Note: I only need to be able to manage messages to: delete the message that initiated this command, bulk remove reactions on the menu, and remove other users' reactions.)" ) ; throw new Error ( 'Missing MANAGE_MESSAGES' ) ; } this . states . set ( 'default' , defaultState ) ; this . sendMenuMessage ( ) . then ( ( ) => { this . reactionHandlerInstance = this . reactionAddListener . bind ( this ) ; this . client . on ( 'messageReactionAdd' , this . reactionHandlerInstance ) ; } , err => { throw err ; } ) ; } private async sendMenuMessage ( ) { let msg = this . states . get ( 'default' ) . message ; this . message = await this . channel . createMessage ( typeof msg === 'function' ? await msg ( ) : msg ) ; try { await this . addReactions ( Array . from ( this . states . get ( this . state ) . reactions . keys ( ) ) ) ; } catch ( err ) { throw err ; } this . restartInactivityTimer ( ) ; this . ready = true ; } private reactionAddListener ( msg , emoji , user ) { try { if ( msg . id === this . message . id && user . id !== this . client . user . id ) this . message . removeReaction ( emoji . id ? `${ emoji . name }:${ emoji . id }` : emoji . name , user . id ) ; if ( msg . id === this . message . id && this . reactors . includes ( user . id ) && this . ready ) this . handleReaction ( emoji , user ) ; } catch ( error ) { console . error ( error ) ; console . log ( this . message , this . originalMessage ) ; } } private async handleReaction ( emoji , user ) { this . restartInactivityTimer ( ) ; if ( ! this . states . has ( this . state ) ) throw new Error ( `State '${ this . state }' does not exist` ) ; if ( this . states . get ( this . state ) . reactions . has ( emoji ) ) await this . states . get ( this . state ) . reactions . get ( emoji ) ( this . message , user ) ; if ( this . isDeleted ) return ; let msg = this . states . get ( this . state ) . message ; if ( typeof msg === 'function' ) { await this . message . edit ( await msg ( ) ) ; } } returns { void } public restartInactivityTimer ( ) { clearTimeout ( this . timeout ) ; this . timeout = setTimeout ( this . endMenu . bind ( this , [ true ] ) , this . timeoutDur ) ; } param { boolean } fromTimeout public async endMenu ( fromTimeout = false ) { this . isDeleted = true ; try { await this . originalMessage . delete ( ) ; } catch ( err ) { } try { await this . message . delete ( ) ; } catch ( err ) { } this . client . off ( 'messageReactionAdd' , this . reactionHandlerInstance ) ; clearTimeout ( this . timeout ) ; if ( fromTimeout ) { let timeoutMessage = await this . channel . createMessage ( `${ this . originalMessage . author . mention } menu timed-out due to inactivity!` ) ; setTimeout ( ( ) => timeoutMessage . delete ( ) , ms ( '5s' ) ) ; } } private async addReactions ( reactions ) { if ( reactions . length < 1 ) return ; let ind = 0 ; let addReaction = async ( ) : Promise < void > => { try { await this . message . addReaction ( reactions [ ind ] . id ? `${ reactions [ ind ] . name }:${ reactions [ ind ] . id }` : reactions [ ind ] . name ) ; } catch ( error ) { throw error ; } if ( ! reactions [ ++ ind ] ) return ; else return await addReaction ( ) ; } ; return await addReaction ( ) ; } returns { Promise < void > } public async resendMessage ( ) { await this . message . delete ( ) ; await this . sendMenuMessage ( ) ; return ; } param { string } state returns { Promise < void > } throws public async setState ( state ) { if ( ! this . states . has ( state ) ) throw new Error ( `State "${ state }" does not exist.` ) ; this . ready = false ; this . state = state ; await this . message . removeReactions ( ) ; let stateObj = this . states . get ( this . state ) ; if ( typeof stateObj . message === 'function' ) this . message . edit ( await stateObj . message ( ) ) ; else this . message . edit ( stateObj . message ) ; await this . addReactions ( Array . from ( stateObj . reactions . keys ( ) ) ) ; this . ready = true ; return ; } param state param reactionMenuState returns { ReactionMenu } throws public addState ( state , reactionMenuState ) { if ( this . states . has ( state ) ) throw new Error ( `State "${ state }" already exists.` ) ; this . states . set ( state , reactionMenuState ) ; return this ; } param state returns { ReactionMenu } throws public removeState ( state ) { if ( this . state === state ) throw new Error ( "This is the ReactionMenu's current state." ) ; if ( ! this . states . has ( state ) ) throw new Error ( `State "${ state }" does not exist.` ) ; this . states . delete ( state ) ; return this ; } param state param reaction param handler returns { ReactionMenu } throws public addReaction ( state , reaction , handler ) { if ( ! this . states . has ( state ) ) throw new Error ( `State "${ state }" does not exist.` ) ; if ( this . states . get ( state ) . reactions . has ( reaction ) ) throw new Error ( `This reaction already exists on state "${ state }"` ) ; this . states . get ( state ) . reactions . set ( reaction , handler ) ; if ( this . state === state ) { this . ready = false ; this . addReactions ( [ reaction ] ) . then ( ( ) => { this . ready = true ; } ) ; } return this ; } param state param reaction returns { ReactionMenu } throws public removeReaction ( state , reaction ) { if ( ! this . states . has ( state ) ) throw new Error ( `State "${ state }" does not exist.` ) ; if ( ! this . states . get ( state ) . reactions . has ( reaction ) ) throw new Error ( `This reaction does not exist on state "${ state }"` ) ; this . states . get ( state ) . reactions . delete ( reaction ) ; if ( this . state === state ) { this . ready = false ; this . message . removeReactionEmoji ( reaction . id ? `${ reaction . name }:${ reaction . id }` : reaction . name ) . then ( ( ) => { this . ready = true ; } ) ; } return this ; } }