import linkifyHtml from 'linkifyjs/html' ; import _ from 'lodash' ; import { Emotes , UserState } from 'twitch-js' ; import Unistring , { Word } from 'unistring' ; import LogType from 'constants/logType' ; import Theme from 'constants/theme' ; import Chatter , { SerializedChatter } from 'libs/Chatter' ; import { EmoteProviderPrefix } from 'libs/EmotesProvider' ; import { Previews } from 'libs/PreviewProvider' ; import Resources from 'libs/Resources' ; import { CheermoteImageBackground , RawCheermoteImage } from 'libs/Twitch' ; import { escape } from 'utils/html' ; import { padTimeUnit } from 'utils/time' ; /**
 * Message class representing either a chat message, an action (/me) or a whisper.
 */ export default class Message implements Serializable < SerializedMessage > { public user : Chatter ; public color : string | null ; private badges : string | null ; private id : string ; private date : string ; private message : string ; private type : LogType ; private time : string ; private purged : boolean = false ; private mentioned : boolean = false ; private previews : Previews = { } ; private ignoreHighlight : boolean ; private highlighted : boolean ; private text : string ; private read : boolean ; private twitchHighlighted : boolean ; private historical : boolean ; private replyReference ? : ReplyReference ; class param message param userstate param self param currentUsername param parseOptions constructor ( message , userstate , private self , currentUsername , private parseOptions ) { this . id = userstate . id ; this . color = userstate . color ; this . date = userstate [ 'tmi-sent-ts' ] ; this . user = new Chatter ( userstate , this . parseOptions . theme ) ; this . type = userstate [ 'message-type' ] ; this . text = message . trim ( ) ; this . highlighted = ! self && Resources . manager ( ) . shouldAlwaysHighlight ( this . user . userName ) ; this . historical = userstate . historical === true ; this . read = self || this . historical ; this . ignoreHighlight = self || Resources . manager ( ) . shouldIgnoreHighlights ( this . user . userName ) ; this . twitchHighlighted = ( _ . isString ( userstate [ 'msg-id' ] ) && userstate [ 'msg-id' ] === 'highlighted-message' && ! this . ignoreHighlight ) || ( _ . isString ( userstate [ 'custom-reward-id' ] ) && ! this . ignoreHighlight ) ; if ( _ . isString ( userstate [ 'reply-parent-msg-id' ] ) ) { this . replyReference = this . parseReplyReference ( userstate , currentUsername ) ; } const date = new Date ( parseInt ( this . date , 10 ) ) ; this . time = `${ padTimeUnit ( date . getHours ( ) ) }:${ padTimeUnit ( date . getMinutes ( ) ) }` ; this . badges = this . parseBadges ( userstate , parseOptions . hideVIPBadge ) ; this . message = this . parseMessage ( this . text , userstate , currentUsername ) ; } param newColor public updateColor ( newColor ) { this . color = newColor ; this . user . color = newColor ; } return public serialize ( ) { const serializedMessage = { badges : this . badges , color : this . color , date : this . date , highlighted : this . highlighted , historical : this . historical , id : this . id , mentioned : this . mentioned , message : this . message , previews : this . previews , purged : this . purged , read : this . read , self : this . self , text : this . text , time : this . time , twitchHighlighted : this . twitchHighlighted , type : this . type , user : this . user . serialize ( ) , } ; if ( this . replyReference ) { serializedMessage . replyReference = this . replyReference ; } return serializedMessage ; } param userstate param hideVIP return private parseBadges ( userstate , hideVIP ) { const parsedBadges = [ ] ; if ( Resources . manager ( ) . isBot ( userstate . username ) ) { parsedBadges . push ( '<img class="badge" data-tip="Bot" src="https://cdn.betterttv.net/tags/bot.png" />' ) ; } if ( _ . size ( userstate . badges ) > 0 ) { _ . forEach ( userstate . badges , ( version , name ) => { if ( hideVIP && name === 'vip' ) { return ; } const set = _ . get ( Resources . manager ( ) . getBadges ( ) , name ) ; if ( _ . isNil ( set ) ) { return ; } const badge = _ . get ( set . versions , version ) ; if ( _ . isNil ( badge ) ) { return ; } const srcset = `${ badge . image_url_1x } 1x,${ badge . image_url_2x } 2x,${ badge . image_url_4x } 4x` ; parsedBadges . push ( `<img class="badge" data-tip="${ badge . title }" src="${ badge . image_url_1x }" srcset="${ srcset }" />` ) ; return ; } ) ; } return escape ( parsedBadges ) . join ( '' ) ; } param message param userstate param currentUsername private parseMessage ( message , userstate , currentUsername ) { const words = Unistring . getWords ( message ) ; const parsedMessage = Array . from ( message ) ; const emotes = this . parseAdditionalEmotes ( words , userstate . emotes || { } ) ; this . parseEmotes ( parsedMessage , emotes ) ; this . parseHighlights ( words , parsedMessage ) ; this . parseMentions ( words , parsedMessage , currentUsername ) ; this . parsePreviews ( message ) ; let parsedMessageStr = escape ( parsedMessage ) . join ( '' ) ; if ( ! _ . isNil ( userstate . bits ) && userstate . bits > 0 ) { parsedMessageStr = this . parseCheermotes ( parsedMessageStr , userstate . bits ) ; } return linkifyHtml ( parsedMessageStr , { attributes : { 'data-tip' : '' , } , } ) ; } param words param emotes private parseAdditionalEmotes ( words , emotes ) { const parsedEmotes = { } ; Resources . manager ( ) . getEmotesProviders ( ) . forEach ( ( provider ) => { if ( provider . isTwitch ( ) && ! this . self ) { _ . merge ( parsedEmotes , emotes ) ; return ; } const providerEmotes = provider . getMessageEmotes ( words ) ; if ( _ . size ( providerEmotes ) > 0 ) { _ . merge ( parsedEmotes , providerEmotes ) ; } } ) ; return parsedEmotes ; } param message param totalBits return private parseCheermotes ( message , totalBits ) { const parsedMessage = Array . from ( message ) ; const cheermoteBackground = this . parseOptions . theme === Theme . Dark ? 'dark' : 'light' ; const replacements = [ ] ; let usedBits = 0 ; _ . forEach ( Resources . manager ( ) . getCheermotes ( ) , ( cheermote ) => { const pattern = `(^|\\b)(${ cheermote . prefix }(\\d+))(\\b|$)` ; const regExp = new RegExp ( pattern , 'gmi' ) ; let match ; while ( ( match = regExp . exec ( message ) ) != null ) { const bits = parseInt ( match [ 3 ] , 10 ) ; let currentBits = bits ; if ( bits > totalBits || usedBits > totalBits ) { continue ; } let color = null as string | null ; let images = null as RawCheermoteImage | null ; _ . forEach ( cheermote . tiers , ( tier ) => { if ( tier . min_bits <= bits ) { color = tier . color ; images = tier . images [ cheermoteBackground ] . animated ; currentBits = tier . min_bits ; } } ) ; if ( ! _ . isNil ( color ) && ! _ . isNil ( images ) ) { const potentialUsedBits = usedBits + currentBits ; if ( potentialUsedBits <= totalBits ) { const beforeStr = match [ 1 ] ; const cheerName = match [ 2 ] ; const afterStr = match [ 4 ] ; const start = match . index + beforeStr . length ; const end = start + cheerName . length + ( afterStr . length === 0 ? afterStr . length : afterStr . length - 1 ) ; const url = images [ '1' ] ; const srcset = `${ images [ '1' ] } 1x,${ images [ '2' ] } 2x,${ images [ '4' ] } 4x` ; const str = `<span class="emoteWrapper"><img class="emote cheer" src="${ url }" srcset="${ srcset }" alt="${ cheerName }" /></span><span class="cheer" style="color: ${ color }">${ bits }</span>` ; replacements . push ( { str , start , end } ) ; usedBits = potentialUsedBits ; } } } } ) ; _ . forEach ( replacements , ( { str , start , end } ) => { for ( let i = start ; i < end ; ++ i ) { parsedMessage [ i ] = '' ; } parsedMessage [ start ] = str ; } ) ; return parsedMessage . join ( '' ) ; } param parsedMessage param emotes private parseEmotes ( parsedMessage , emotes ) { _ . forEach ( emotes , ( ranges , id ) => { const [ providerPrefix , emoteId ] = id . split ( '-' ) ; _ . forEach ( ranges , ( range ) => { const strIndexes = range . split ( '-' ) ; const indexes = [ parseInt ( strIndexes [ 0 ] , 10 ) , parseInt ( strIndexes [ 1 ] , 10 ) ] ; const name = [ ] ; for ( let i = indexes [ 0 ] ; i <= indexes [ 1 ] ; ++ i ) { name . push ( parsedMessage [ i ] ) ; parsedMessage [ i ] = '' ; } const emoteName = name . join ( '' ) ; const isTwitchEmote = _ . isNil ( emoteId ) ; const provider = Resources . manager ( ) . getEmotesProvider ( isTwitchEmote ? EmoteProviderPrefix . Twitch : ( providerPrefix as EmoteProviderPrefix ) ) ; if ( ! _ . isNil ( provider ) ) { parsedMessage [ indexes [ 0 ] ] = provider . getEmoteTag ( isTwitchEmote ? id : emoteId , emoteName ) ; } } ) ; } ) ; } param words param parsedMessage param currentUsername private parseMentions ( words , parsedMessage , currentUsername ) { _ . forEach ( words , ( word , index ) => { if ( ! this . ignoreHighlight && word . text . toLowerCase ( ) === currentUsername ) { this . mentioned = true ; const previousWord = index > 0 ? words [ index - 1 ] : null ; if ( ! _ . isNil ( previousWord ) && previousWord . text . trim ( ) . length > 0 && previousWord . text !== '@' ) { return ; } const withAtSign = ! _ . isNil ( previousWord ) && previousWord . text === '@' ; const startIndex = withAtSign ? word . index - 1 : word . index ; const endIndex = word . index + currentUsername . length ; for ( let i = startIndex ; i < endIndex ; ++ i ) { parsedMessage [ i ] = '' ; } if ( ! this . replyReference || index !== 1 ) { parsedMessage [ startIndex ] = `<span class="mention self">${ withAtSign ? '@' : '' }${ word . text }</span>` ; } } else if ( word . text === '@' ) { const previousWord = index > 0 ? words [ index - 1 ] : null ; if ( ! _ . isNil ( previousWord ) && previousWord . text . trim ( ) . length > 0 && previousWord . text !== '' ) { return ; } const nextWord = index < words . length - 1 ? words [ index + 1 ] : null ; if ( ! _ . isNil ( nextWord ) && nextWord . text . trim ( ) . length > 0 ) { const startIndex = word . index ; const endIndex = nextWord . index + nextWord . length ; if ( this . replyReference || Resources . manager ( ) . shouldHighlightAllMentions ( ) ) { for ( let i = startIndex ; i < endIndex ; ++ i ) { parsedMessage [ i ] = '' ; } } if ( ( Resources . manager ( ) . shouldHighlightAllMentions ( ) && ! this . replyReference ) || ( this . replyReference && index !== 0 ) ) { parsedMessage [ startIndex ] = `<span class="mention">${ word . text }${ nextWord . text }</span>` ; } } } } ) ; } param words param parsedMessage private parseHighlights ( words , parsedMessage ) { if ( ! this . ignoreHighlight ) { _ . forEach ( Resources . manager ( ) . getHighlights ( ) , ( highlight ) => { const wordsMatchingHighlight = _ . filter ( words , ( word , index ) => { const couldMatch = word . text . toLowerCase ( ) === highlight . pattern ; if ( ! couldMatch ) { return false ; } const previousWord = index > 0 ? words [ index - 1 ] : null ; if ( ! _ . isNil ( previousWord ) && previousWord . text . trim ( ) . length > 0 && previousWord . text !== '' ) { return false ; } const nextWord = index < words . length - 1 ? words [ index + 1 ] : null ; if ( ! _ . isNil ( nextWord ) && nextWord . text . trim ( ) . length > 0 ) { return false ; } return true ; } ) ; _ . forEach ( wordsMatchingHighlight , ( word ) => { const highlightStr = word . text ; const startIndex = word . index ; const endIndex = startIndex + highlightStr . length ; for ( let i = startIndex ; i < endIndex ; ++ i ) { parsedMessage [ i ] = '' ; } parsedMessage [ startIndex ] = `<span class="highlight ${ highlight . color }">${ highlightStr }</span>` ; } ) ; } ) ; } } param message private parsePreviews ( message ) { _ . forEach ( Resources . manager ( ) . getPreviewProviders ( ) , ( provider ) => { const previews = provider . parse ( message ) ; if ( _ . size ( previews ) > 0 ) { _ . merge ( this . previews , provider . parse ( message ) ) ; } } ) ; } param userstate param currentUsername return private parseReplyReference ( userstate , currentUsername ) { const id = userstate [ 'reply-parent-msg-id' ] ; const message = userstate [ 'reply-parent-msg-body' ] ?. toString ( ) ; const userDisplayName = userstate [ 'reply-parent-display-name' ] ; const userId = userstate [ 'reply-parent-user-id' ] ; const userLogin = userstate [ 'reply-parent-user-login' ] ; if ( _ . isNil ( id ) || _ . isNil ( message ) || _ . isNil ( userDisplayName ) || _ . isNil ( userId ) || _ . isNil ( userLogin ) ) { return ; } return { id , message , self : userDisplayName . toLowerCase ( ) === currentUsername , user : { displayName : userDisplayName , id : userId , login : userLogin , } , } ; } } /**
 * Serialized message.
 */ export type SerializedMessage = { badges : string | null ; color : string | null ; user : SerializedChatter ; highlighted : boolean ; historical : boolean ; id : string ; date : string ; self : boolean ; type : LogType ; mentioned : boolean ; message : string ; purged : boolean ; text : string ; time : string ; previews : Previews ; read : boolean ; twitchHighlighted : boolean ; replyReference ? : ReplyReference ; } ; /**
 * A reply reference when the message was a response to another one (replied using the built-in Twitch feature).
 */ type ReplyReference = { id : string ; message : string ; self : boolean ; user : { displayName : string ; id : string ; login : string ; } ; } ; /**
 * Message parse options.
 */ type MessageParseOptions = { hideVIPBadge : boolean ; theme : Theme ; } ;