import _ from 'lodash' ; import { CommandArgument , CommandDescriptor , CommandName , Commands } from 'constants/command' ; import Notice from 'libs/Notice' ; import { SerializedRoomState } from 'libs/RoomState' ; import Twitch from 'libs/Twitch' ; import { Log } from 'store/ducks/logs' ; /**
 * Actions that could arise when handling a command.
 */ export enum CommandDelegateAction { AddLog , AddToHistory , Say , SayWithoutHistory , Timeout , Whisper } /**
 * Command class.
 */ export default class Command { param message public static isCommand ( message ) { const firstCharacter = message . charAt ( 0 ) ; return firstCharacter === '/' || firstCharacter === '.' ; } param message public static isWhisperReplyCommand ( message ) { return /^[/|.]r /i . test ( message ) ; } param message public static isHelpCommand ( message ) { return /^[/|.]help(?:$|\s)/i . test ( message ) ; } param message param cursor return public static isUsernameCompletableCommandArgument ( message , cursor ) { if ( ! Command . isCommand ( message ) ) { return false ; } const words = message . split ( ' ' ) ; const commandName = words [ 0 ] . substr ( 1 ) ; const firstArgument = words [ 1 ] ; if ( _ . isNil ( firstArgument ) ) { return false ; } const firstArgumentStart = commandName . length + 2 ; const firstArgumentEnd = firstArgumentStart + firstArgument . length ; if ( cursor < firstArgumentStart || cursor > firstArgumentEnd ) { return false ; } const descriptor = Command . getDescriptor ( commandName ) ; if ( _ . isNil ( descriptor . name ) ) { return false ; } const firstArgumentDescriptor = _ . get ( descriptor , 'arguments[0]' ) as Optional < CommandArgument > ; if ( _ . isNil ( firstArgumentDescriptor ) || firstArgumentDescriptor . name !== 'username' ) { return false ; } return true ; } return public static parseWhisper ( message ) { const matches = message . match ( /^[/|.]w (\S+) (.+)/i ) ; const details = { username : undefined as Optional < string > , whisper : undefined as Optional < string > } ; if ( ! _ . isNil ( matches ) ) { details . username = matches [ 1 ] ; details . whisper = matches [ 2 ] ; } return details ; } return public static parseShrug ( message ) { const matches = message . match ( /(^|.* )[/|.]shrug($| .*)/i ) ; const result = { isShrug : false , message } ; if ( ! _ . isNil ( matches ) ) { const before = matches [ 1 ] ; const after = matches [ 2 ] ; result . isShrug = true ; result . message = `${ before }¯\\_(ツ)_/¯ ${ after }` ; } return result ; } param descriptor return public static getUsage ( descriptor ) { const args = _ . map ( descriptor . arguments , ( argument ) => { if ( argument . optional ) { return `[${ argument . name }]` ; } return `<${ argument . name }>` ; } ) ; return `/${ descriptor . name } ${ args . join ( ' ' ) }` ; } return public static getDescriptor ( commandName ) { const formattedCommandName = _ . upperFirst ( commandName ) ; if ( ! Command . isKnownCommand ( formattedCommandName ) ) { throw new Error ( 'Unknown command name.' ) ; } const name = CommandName [ formattedCommandName ] ; return { ... Commands [ name ] , name } ; } param code return private static isKnownCommand ( code ) { return code in CommandName ; } private command : string ; private arguments : string [ ] ; class param message constructor ( private message , private delegate , private delegateDataFetcher ) { const [ command , ... args ] = message . slice ( 1 ) . split ( ' ' ) ; this . command = command . toLowerCase ( ) ; this . arguments = args ; } /**
     * Handles the command.
     */ public async handle ( ) { if ( ! _ . includes ( CommandName , this . command ) ) { this . say ( this . message ) ; } else { const descriptor = Command . getDescriptor ( this . command ) ; const handler = this . getHandler ( descriptor ) ; if ( _ . isNil ( handler ) ) { await this . say ( this . message ) ; } else { await handler ( ) ; if ( ! descriptor . ignoreHistory ) { this . addToHistory ( this . message ) ; } } } } param message param ignoreHistory private async say ( message , ignoreHistory = false ) { const action = ignoreHistory ? CommandDelegateAction . SayWithoutHistory : CommandDelegateAction . Say ; this . delegate ( action , message ) ; } param message private async addToHistory ( message ) { this . delegate ( CommandDelegateAction . AddToHistory , message ) ; } param log private addLog ( log ) { this . delegate ( CommandDelegateAction . AddLog , log ) ; } param username param whisper param command private whisper ( username , whisper , command ? ) { this . delegate ( CommandDelegateAction . Whisper , username , whisper , command ) ; } param username param duration private timeout ( username , duration ) { this . delegate ( CommandDelegateAction . Timeout , username , duration ) ; } /**
     * Sends a help notice about the command.
     */ private sendHelpNotice ( ) { const descriptor = Command . getDescriptor ( this . command ) ; const notice = new Notice ( `Usage: "${ Command . getUsage ( descriptor ) }" - ${ descriptor . description }` , null ) ; this . addLog ( notice . serialize ( ) ) ; } param description return private getHandler ( descriptor ) { return this . commandHandlers [ descriptor . name ] ; } /**
     * Handles the /followed command.
     */ private handleCommandFollowed = async ( ) => { const { channelId } = this . delegateDataFetcher ( ) ; if ( ! _ . isNil ( channelId ) ) { const relationship = await Twitch . fetchRelationship ( channelId ) ; const notice = new Notice ( _ . isNil ( relationship ) ? 'Channel not followed.' : `Followed since ${ new Date ( relationship . followed_at ) . toLocaleDateString ( ) }.` , null ) ; this . addLog ( notice . serialize ( ) ) ; } } ; /**
     * Handles the /marker command.
     */ private handleCommandMarker = async ( ) => { const { channelId } = this . delegateDataFetcher ( ) ; const [ description ] = this . arguments ; if ( ! _ . isNil ( channelId ) ) { let noticeStr ; try { await Twitch . createMarker ( channelId , description ) ; noticeStr = 'Marker created successfully.' ; } catch ( error ) { noticeStr = 'Something went wrong while creating a marker.' ; const matches = /message:"(?<message>[^"]*)/ . exec ( error ) ; if ( matches ) { const message = _ . get ( matches , 'groups.message' ) ; if ( message ) { noticeStr = message ; } } } const notice = new Notice ( noticeStr , null ) ; this . addLog ( notice . serialize ( ) ) ; } } ; /**
     * Handles the /title command.
     */ private handleCommandTitle = async ( ) => { const { channelId } = this . delegateDataFetcher ( ) ; const title = this . arguments . join ( ' ' ) ; if ( ! _ . isNil ( channelId ) ) { let noticeStr ; if ( _ . isEmpty ( title ) ) { try { const channelInfos = await Twitch . fetchChannelInformations ( channelId ) ; noticeStr = `Current title: ${ channelInfos . title }` ; } catch ( error ) { noticeStr = 'Something went wrong while fetching the title.' ; } } else { try { await Twitch . updateChannelInformations ( channelId , title ) ; noticeStr = `New title: ${ title }` ; } catch ( error ) { noticeStr = 'Something went wrong while updating the title.' ; } } const notice = new Notice ( noticeStr , null ) ; this . addLog ( notice . serialize ( ) ) ; } } ; /**
     * Handles the /block & /unblock commands.
     */ private handleCommandBlockUnblock = async ( ) => { const [ username ] = this . arguments ; if ( _ . isEmpty ( username ) ) { this . sendHelpNotice ( ) ; } else { let noticeStr ; try { const user = await Twitch . fetchUserByName ( username ) ; if ( _ . isNil ( user ) ) { throw new Error ( `Invalid user name provided for the ${ this . command } command.` ) ; } const blockFn = this . command === 'block' ? Twitch . blockUser : Twitch . unblockUser ; await blockFn ( user . id ) ; noticeStr = `${ username } is now ${ this . command }ed.` ; } catch ( error ) { noticeStr = `Something went wrong while trying to ${ this . command } ${ username }.` ; } const notice = new Notice ( noticeStr , null ) ; this . addLog ( notice . serialize ( ) ) ; } } ; /**
     * Handles the /purge command.
     */ private handleCommandPurge = ( ) => { const [ username ] = this . arguments ; if ( _ . isEmpty ( username ) ) { this . sendHelpNotice ( ) ; } else { this . timeout ( username , 1 ) ; } } ; /**
     * Handles the /w command.
     */ private handleCommandWhisper = ( ) => { const [ username , ... fragments ] = this . arguments ; const whisper = fragments . join ( ' ' ) ; if ( ! _ . isEmpty ( username ) && ! _ . isEmpty ( whisper ) ) { this . whisper ( username , whisper , this . message ) ; } else { this . sendHelpNotice ( ) ; } } ; /**
     * Handles the /uniquechat command.
     */ private handleCommandUniqueChat = ( ) => { this . say ( '/r9kbeta' , true ) ; } ; /**
     * Handles the /uniquechatoff command.
     */ private handleCommandUniqueChatOff = ( ) => { this . say ( '/r9kbetaoff' , true ) ; } ; /**
     * Handles the /user command.
     */ private handleCommandUser = ( ) => { const [ username ] = this . arguments ; if ( _ . isEmpty ( username ) ) { this . sendHelpNotice ( ) ; } else { const { channel } = this . delegateDataFetcher ( ) ; if ( ! _ . isNil ( channel ) ) { Twitch . openViewerCard ( channel , username ) ; } } } ; /**
     * List of commands with special handlers.
     */ private commandHandlers : { [ key in CommandName ] ? : ( ) => void ; } = { [ CommandName . Block ] : this . handleCommandBlockUnblock , [ CommandName . Followed ] : this . handleCommandFollowed , [ CommandName . Marker ] : this . handleCommandMarker , [ CommandName . Purge ] : this . handleCommandPurge , [ CommandName . Title ] : this . handleCommandTitle , [ CommandName . Unblock ] : this . handleCommandBlockUnblock , [ CommandName . Uniquechat ] : this . handleCommandUniqueChat , [ CommandName . Uniquechatoff ] : this . handleCommandUniqueChatOff , [ CommandName . User ] : this . handleCommandUser , [ CommandName . W ] : this . handleCommandWhisper , } ; } /**
 * Command delegate data fetcher.
 */ export type CommandDelegateDataFetcher = ( ) => { channel : string | null ; channelId : SerializedRoomState [ 'roomId' ] | undefined ; } ; /**
 * The commande delegate signature.
 */ export interface CommandDelegate { ( action , log ) : void ; ( action , username , duration ) : void ; ( action , username , whisper , command ? ) : void ; ( action , message ) : void ; } /**
 * An enhanced command descriptor including the command name.
 */ type EnhancedCommandDescriptor = CommandDescriptor & { name : CommandName ; } ;