2022-12-18 17:30:34 -05:00
< template >
< div class = "ffz--chat-tester" >
< div v-if = "context.exclusive" class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-2" >
< h3 class = "ffz-i-attention" >
{ { t ( 'debug.chat-tester.exclusive' , "Hey! This won't work here!" ) } }
< / h3 >
< markdown : source = "t('debug.chat-tester.exclusive-explain', 'This feature does not work when the FFZ Control Center is popped out. It needs to be used in a window where you can see chat.')" / >
< / div >
< div class = "tw-flex tw-align-items-start" >
< label for = "selector" class = "tw-mg-y-05" >
{ { t ( 'debug.chat-tester.message' , 'Test Message' ) } }
< / label >
< div class = "tw-flex tw-flex-column tw-mg-05 tw-full-width" >
< select
id = "selector"
ref = "selector"
class = "tw-full-width tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-font-size-6 ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05"
@ change = "onSelectChange"
>
< option :selected = "is_custom" value = "custom" >
{ { t ( 'setting.combo-box.custom' , 'Custom' ) } }
< / option >
< option
v - for = "(sample, idx) in samples"
: key = "idx"
2023-06-26 13:11:27 -04:00
: selected = "sample.data === message && sample.topic === topic"
: value = "idx"
2022-12-18 17:30:34 -05:00
>
{ { sample . name } }
< / option >
< / select >
2023-06-26 13:11:27 -04:00
< input
ref = "topic"
class = "tw-block tw-font-size-6 tw-full-width ffz-textarea ffz-mg-t-1p"
@ blur = "updateMessage"
@ input = "onMessageChange"
/ >
2022-12-18 17:30:34 -05:00
< textarea
ref = "message"
class = "tw-block tw-font-size-6 tw-full-width ffz-textarea ffz-mg-t-1p tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium"
rows = "10"
@ blur = "updateMessage"
@ input = "onMessageChange"
/ >
< / div >
< / div >
< div class = "tw-mg-t-1 tw-flex tw-align-items-center" >
< div class = "tw-flex-grow-1" / >
< div class = "tw-pd-x-1 ffz-checkbox" >
< input
id = "replay_fix"
ref = "replay_fix"
: checked = "replay_fix"
type = "checkbox"
class = "ffz-checkbox__input"
@ change = "onCheck"
>
< label for = "replay_fix" class = "ffz-checkbox__label" >
< span class = "tw-mg-l-1" >
{ { t ( 'debug.chat-tester.replay-fix' , 'Fix ID and Channel' ) } }
< / span >
< / label >
< / div >
< button
class = "tw-mg-l-1 tw-button tw-button--text"
@ click = "playMessage"
>
< span class = "tw-button__text ffz-i-play" >
{ { t ( 'debug.chat-tester.play' , 'Play Message' ) } }
< / span >
< / button >
< / div >
< div class = "tw-pd-t-1 tw-border-t tw-mg-t-1 tw-flex tw-mg-b-1 tw-align-items-center" >
< div class = "tw-flex-grow-1" / >
< div class = "tw-pd-x-1 ffz-checkbox" >
< input
id = "capture_chat"
ref = "capture_chat"
: checked = "capture_chat"
type = "checkbox"
class = "ffz-checkbox__input"
@ change = "onCheck"
>
< label for = "capture_chat" class = "ffz-checkbox__label" >
< span class = "tw-mg-l-1" >
{ { t ( 'debug.chat-tester.capture-chat' , 'Capture Chat' ) } }
< / span >
< / label >
< / div >
< div class = "tw-pd-x-1 ffz-checkbox" >
< input
id = "ignore_privmsg"
ref = "ignore_privmsg"
: checked = "ignore_privmsg"
type = "checkbox"
class = "ffz-checkbox__input"
@ change = "onCheck"
>
< label for = "ignore_privmsg" class = "ffz-checkbox__label" >
< span class = "tw-mg-l-1" >
{ { t ( 'debug.chat-tester.ignore-privmsg' , 'Ignore PRIVMSG' ) } }
< / span >
< / label >
< / div >
< div class = "tw-pd-x-1 ffz-checkbox" >
< input
id = "capture_pubsub"
ref = "capture_pubsub"
: checked = "capture_pubsub"
type = "checkbox"
class = "ffz-checkbox__input"
@ change = "onCheck"
>
< label for = "capture_pubsub" class = "ffz-checkbox__label" >
< span class = "tw-mg-l-1" >
{ { t ( 'debug.chat-tester.capture-pubsub' , 'Capture PubSub' ) } }
< / span >
< / label >
< / div >
< button
class = "tw-mg-l-1 tw-button tw-button--text"
@ click = "clearLog"
>
< span class = "tw-button__text ffz-i-trash" >
{ { t ( 'debug.chat-tester.clear-log' , 'Clear Log' ) } }
< / span >
< / button >
< / div >
< div
v - for = "item in log"
: key = "item._id"
class = "tw-elevation-1 tw-border tw-pd-y-05 tw-pd-r-1 tw-mg-y-05 tw-flex tw-flex-nowrap tw-align-items-center"
: class = "{'tw-c-background-base': item.pubsub, 'tw-c-background-alt-2': !item.pubsub}"
>
< time class = "tw-mg-l-05 tw-mg-r-1 tw-flex-shrink-0" >
{ { tTime ( item . timestamp , 'HH:mm:ss' ) } }
< / time >
< div v-if = "item.pubsub" class="tw-flex-grow-1" >
< div class = "tw-mg-b-05 tw-border-b tw-pd-b-05" > { { item . topic } } < / div >
< div v-html = "highlightJson(item.data)" / >
< / div >
< div v -else -if = " item.chat " class = "tw-flex-grow-1" >
< div v-if = "item.tags" class="ffz-ct--tags" >
@ < template v-for = "(tag, key) in item.tags"><span class="ffz-ct--tag">{{ key }}</span>=<span class="ffz-ct--tag-value" > {{ tag }} < / span > ; < / template >
< / div >
< div class = "ffz-ct--prefix" >
< template v-if = "item.prefix">:<span v-if="item.user" class="ffz-ct--user">{{ item.user }}</span><span class="ffz-ct--prefix" > {{ item.prefix }} < / span > < / template >
< span class = "ffz-ct--command" > { { item . command } } < / span >
< template v-if = "item.channel">#<span class="ffz-ct--channel" > {{ item.channel }} < / span > < / template >
< / div >
< div v-if = "item.last_param" class="ffz-ct--params" >
< span v-for = "para in item.params" class="ffz-ct--param" > {{ para }} < / span >
: < span class = "ffz-ct--param" > { { item . last _param } } < / span >
< / div >
< / div >
< div v -else class = "tw-flex-grow-1" >
{ { item . data } }
< / div >
< div class = "tw-mg-l-1 tw-flex tw-flex-wrap tw-flex-column tw-justify-content-start tw-align-items-start" >
< button
v - if = "item.chat || item.pubsub"
class = "tw-button tw-button--text"
@ click = "replayItem(item)"
>
< span class = "tw-button__text ffz-i-arrows-cw" >
{ { t ( 'debug.chat-tester.replay' , 'Replay' ) } }
< / span >
< / button >
< button
class = "tw-button tw-button--text"
@ click = "copyItem(item)"
>
< span class = "tw-button__text ffz-i-docs" >
{ { t ( 'setting.copy-json' , 'Copy' ) } }
< / span >
< / button >
< / div >
< / div >
< / div >
< / template >
< script >
import { DEBUG , SERVER } from 'utilities/constants' ;
2023-09-13 16:08:10 -04:00
import { highlightJson } from 'utilities/dom' ;
2022-12-18 17:30:34 -05:00
import { deep _copy , generateUUID } from 'utilities/object' ;
import { getBuster } from 'utilities/time' ;
import SAMPLES from '../sample-chat-messages.json' ; // eslint-disable-line no-unused-vars
const IGNORE _COMMANDS = [
'PONG' ,
'PING' ,
'366' ,
'353'
] ;
let LOADED _SAMPLES = [
{
"name" : "Ping" ,
"data" : "PING :tmi.twitch.tv"
}
] ;
let has _loaded _samples = false ;
export default {
props : [ 'item' , 'context' ] ,
data ( ) {
const state = window . history . state ;
const samples = deep _copy ( LOADED _SAMPLES ) ;
const message = state ? . ffz _ct _message ? ? samples [ 0 ] . data ;
2023-06-26 13:11:27 -04:00
const topic = state ? . ffz _ct _topic ? ? samples [ 0 ] . topic ? ? '' ;
2022-12-18 17:30:34 -05:00
let is _custom = true ;
2023-06-26 13:11:27 -04:00
/ * f o r ( c o n s t i t e m o f s a m p l e s ) {
if ( ! item . topic )
item . topic = '' ;
if ( typeof item . data !== 'string' )
item . data = JSON . stringify ( item . data , null , 4 ) ;
if ( item . data === message && item . topic === topic ) {
2022-12-18 17:30:34 -05:00
is _custom = false ;
break ;
}
2023-06-26 13:11:27 -04:00
} * /
2022-12-18 17:30:34 -05:00
return {
has _client : false ,
samples ,
is _custom ,
message ,
2023-06-26 13:11:27 -04:00
topic ,
2022-12-18 17:30:34 -05:00
replay _fix : state ? . ffz _ct _replay ? ? true ,
ignore _privmsg : state ? . ffz _ct _privmsg ? ? false ,
capture _chat : state ? . ffz _ct _chat ? ? false ,
capture _pubsub : state ? . ffz _ct _pubsub ? ? false ,
log : [ ] ,
logi : 0
}
} ,
watch : {
message ( ) {
if ( ! this . is _custom )
this . $refs . message . value = this . message ;
} ,
2023-06-26 13:11:27 -04:00
topic ( ) {
if ( ! this . is _custom )
this . $refs . topic . value = this . topic ;
} ,
2022-12-18 17:30:34 -05:00
capture _chat ( ) {
if ( this . capture _chat )
this . listenChat ( ) ;
else
this . unlistenChat ( ) ;
} ,
capture _pubsub ( ) {
if ( this . capture _pubsub )
this . listenPubsub ( ) ;
else
this . unlistenPubsub ( ) ;
}
} ,
created ( ) {
this . loadSamples ( ) ;
this . chat = this . item . getChat ( ) ;
this . client = this . chat . ChatService . first ? . client ;
this . has _client = ! ! this . client ;
if ( this . capture _chat )
this . listenChat ( ) ;
if ( this . capture _pubsub )
this . listenPubsub ( ) ;
} ,
beforeDestroy ( ) {
this . unlistenChat ( ) ;
this . unlistenPubsub ( ) ;
this . client = null ;
this . chat = null ;
} ,
mounted ( ) {
this . $refs . message . value = this . message ;
2023-06-26 13:11:27 -04:00
this . $refs . topic . value = this . topic ;
2022-12-18 17:30:34 -05:00
} ,
methods : {
2023-09-13 16:08:10 -04:00
highlightJson ( object , pretty ) {
return highlightJson ( object , pretty ) ;
2022-12-18 17:30:34 -05:00
} ,
// Samples
async loadSamples ( ) {
if ( has _loaded _samples )
return ;
const values = await fetch ( DEBUG ? SAMPLES : ` ${ SERVER } /script/sample-chat-messages.json?_= ${ getBuster ( ) } ` ) . then ( r => r . ok ? r . json ( ) : null ) ;
if ( Array . isArray ( values ) && values . length > 0 ) {
has _loaded _samples = true ;
for ( const item of values ) {
2023-06-26 13:11:27 -04:00
if ( ! item . topic )
item . topic = '' ;
2022-12-18 17:30:34 -05:00
if ( Array . isArray ( item . data ) )
item . data = item . data . join ( '\n\n' ) ;
2023-06-26 13:11:27 -04:00
else if ( typeof item . data !== 'string' )
item . data = JSON . stringify ( item . data , null , 4 ) ;
2022-12-18 17:30:34 -05:00
}
LOADED _SAMPLES = values ;
this . samples = deep _copy ( values ) ;
let is _custom = true ;
for ( const item of this . samples ) {
2023-06-26 13:11:27 -04:00
if ( item . data === this . message && item . topic === this . topic ) {
2022-12-18 17:30:34 -05:00
is _custom = false ;
break ;
}
}
this . is _custom = is _custom ;
}
} ,
// Chat
listenChat ( ) {
if ( this . listening _chat )
return ;
// Ensure we have the chat client.
if ( ! this . has _client ) {
this . client = this . chat . ChatService . first ? . client ;
this . has _client = ! ! this . client ;
if ( ! this . has _client )
return ;
}
// Hook into the connection.
const conn = this . client . connection ;
if ( ! conn . ffzOnSocketMessage )
conn . ffzOnSocketMessage = conn . onSocketMessage ;
conn . onSocketMessage = event => {
try {
this . handleChat ( event ) ;
} catch ( err ) {
/* no-op */
}
return conn . ffzOnSocketMessage ( event ) ;
}
if ( conn . ws )
conn . ws . onmessage = conn . onSocketMessage ;
this . addLog ( "Started capturing chat." ) ;
this . listening _chat = true ;
} ,
unlistenChat ( ) {
if ( ! this . listening _chat )
return ;
const conn = this . client . connection ;
conn . onSocketMessage = conn . ffzOnSocketMessage ;
if ( conn . ws )
conn . ws . onmessage = conn . onSocketMessage ;
this . addLog ( "Stopped capturing chat." ) ;
this . listening _chat = false ;
} ,
handleChat ( event ) {
for ( const raw of event . data . split ( /\r?\n/g ) ) {
const msg = this . parseChat ( raw ) ;
if ( msg ) {
if ( this . ignore _privmsg && msg . command === 'PRIVMSG' )
continue ;
if ( IGNORE _COMMANDS . includes ( msg . command ) )
continue ;
this . addLog ( msg ) ;
}
}
} ,
parseChat ( raw ) {
const msg = this . client . parser . msg ( raw ) ;
msg . chat = true ;
if ( Object . keys ( msg . tags ) . length === 0 )
msg . tags = null ;
if ( msg . params . length > 0 && msg . params [ 0 ] . startsWith ( '#' ) )
msg . channel = msg . params . shift ( ) . slice ( 1 ) ;
if ( msg . params . length > 0 )
msg . last _param = msg . params . pop ( ) ;
const idx = msg . prefix ? msg . prefix . indexOf ( '!' ) : - 1 ;
if ( idx === - 1 )
msg . user = null ;
else {
msg . user = msg . prefix . substr ( 0 , idx ) ;
msg . prefix = msg . prefix . substr ( idx ) ;
}
return msg ;
} ,
// Pubsub
listenPubsub ( ) {
if ( this . listening _pubsub )
return ;
this . chat . on ( 'site.subpump:pubsub-message' , this . handlePubsub , this ) ;
this . addLog ( "Started capturing PubSub." ) ;
this . listening _pubsub = true ;
} ,
unlistenPubsub ( ) {
if ( ! this . listening _pubsub )
return ;
this . chat . off ( 'site.subpump:pubsub-message' , this . handlePubsub , this ) ;
this . addLog ( "Stopped capturing PubSub." ) ;
this . listening _pubsub = false ;
} ,
handlePubsub ( event ) {
if ( event . prefix === 'video-playback-by-id' )
return ;
this . addLog ( {
pubsub : true ,
topic : event . topic ,
data : deep _copy ( event . message )
} ) ;
} ,
// State
saveState ( ) {
try {
window . history . replaceState ( {
... window . history . state ,
ffz _ct _replay : this . replay _fix ,
ffz _ct _message : this . message ,
ffz _ct _chat : this . capture _chat ,
ffz _ct _pubsub : this . capture _pubsub ,
ffz _ct _privmsg : this . ignore _privmsg
} , document . title ) ;
} catch ( err ) {
/* no-op */
}
} ,
// Event Handlers
onSelectChange ( ) {
2023-06-26 13:11:27 -04:00
const idx = this . $refs . selector . value ,
item = this . samples [ idx ] ;
2022-12-18 17:30:34 -05:00
2023-06-26 13:11:27 -04:00
if ( idx !== 'custom' && item ? . data ) {
this . message = item . data ;
this . topic = item . topic ? ? '' ;
2022-12-18 17:30:34 -05:00
this . is _custom = false ;
} else
this . is _custom = true ;
} ,
updateMessage ( ) {
2023-06-26 13:11:27 -04:00
const value = this . $refs . message . value ,
topic = this . $refs . topic . value ;
2022-12-18 17:30:34 -05:00
let is _custom = true ;
for ( const item of this . samples ) {
2023-06-26 13:11:27 -04:00
if ( item . data === value && item . topic === topic ) {
2022-12-18 17:30:34 -05:00
is _custom = false ;
break ;
}
}
this . is _custom = is _custom ;
2023-06-26 13:11:27 -04:00
if ( this . is _custom ) {
this . topic = topic ;
2022-12-18 17:30:34 -05:00
this . message = value ;
2023-06-26 13:11:27 -04:00
}
2022-12-18 17:30:34 -05:00
} ,
onMessageChange ( ) {
this . updateMessage ( ) ;
} ,
onCheck ( ) {
this . replay _fix = this . $refs . replay _fix . checked ;
this . capture _chat = this . $refs . capture _chat . checked ;
this . capture _pubsub = this . $refs . capture _pubsub . checked ;
this . ignore _privmsg = this . $refs . ignore _privmsg . checked ;
this . saveState ( ) ;
} ,
// Log
addLog ( msg ) {
if ( typeof msg !== 'object' )
msg = {
data : msg
} ;
msg . timestamp = Date . now ( ) ;
msg . _id = this . logi ++ ;
this . log . unshift ( msg ) ;
const extra = this . log . length - 100 ;
if ( extra > 0 )
this . log . splice ( 100 , extra ) ;
} ,
clearLog ( ) {
this . log = [ ] ;
this . addLog ( 'Cleared log.' ) ;
} ,
// Item Actions
copyItem ( item ) {
let value ;
if ( item . raw )
value = item . raw ;
else if ( item . data )
value = item . data ;
else
value = item ;
if ( typeof value !== 'string' )
value = JSON . stringify ( value ) ;
navigator . clipboard . writeText ( value ) ;
} ,
playMessage ( ) {
2023-06-26 13:11:27 -04:00
// Check for PubSub
if ( this . topic . trim ( ) . length > 0 ) {
let data ;
try {
data = JSON . parse ( this . message ) ;
} catch ( err ) {
console . error ( err ) ;
alert ( "Unable to parse message." ) ;
return ;
}
this . replayItem ( {
pubsub : true ,
topic : this . topic ,
data
} ) ;
return ;
}
2022-12-18 17:30:34 -05:00
const msgs = [ ] ;
const parts = this . message . split ( /\r?\n/g ) ;
for ( const part of parts ) {
try {
if ( part && part . length > 0 )
msgs . push ( this . parseChat ( part ) ) ;
} catch ( err ) {
console . error ( err ) ;
alert ( "Unable to parse message." ) ;
return ;
}
}
for ( const msg of msgs )
this . replayItem ( msg ) ;
} ,
replayItem ( item ) {
2023-06-26 13:11:27 -04:00
if ( item . pubsub ) {
2023-09-06 16:10:47 -04:00
const channel = this . chat . ChatService . first ? . props ? . channelID ,
user = this . chat . resolve ( 'site' ) . getUser ( ) ;
2023-06-26 13:11:27 -04:00
if ( this . replay _fix ) {
item . topic = item . topic . replace ( /<channel>/gi , channel ) ;
2023-09-06 16:10:47 -04:00
item . topic = item . topic . replace ( /<user>/gi , user . id ) ;
2023-06-26 13:11:27 -04:00
// TODO: Crawl, replacing ids.
// TODO: Update timestamps for pinned chat?
}
2023-11-16 18:41:50 -05:00
this . chat . resolve ( 'site.subpump' ) . simulateMessage ( item . topic , item . data ) ;
2023-06-26 13:11:27 -04:00
}
2022-12-18 17:30:34 -05:00
if ( item . chat ) {
// While building the string, also build colors for the console log.
const out = [ ] ;
const colors = [ ] ;
if ( item . tags ) {
out . push ( '@' ) ;
colors . push ( 'gray' ) ;
for ( const [ key , val ] of Object . entries ( item . tags ) ) {
let v = val ;
// If the tag is "id", return a new id so the message
// won't be deduplicated automatically.
if ( key === 'id' && this . replay _fix )
v = generateUUID ( ) ;
out . push ( key ) ;
out . push ( '=' ) ;
out . push ( ` ${ v } ` ) ;
out . push ( ';' ) ;
colors . push ( 'orange' ) ;
colors . push ( 'gray' ) ;
colors . push ( 'white' ) ;
colors . push ( 'gray' ) ;
}
}
if ( item . user || item . prefix ) {
if ( out . length ) {
out . push ( ' ' ) ;
colors . push ( '' ) ;
}
out . push ( ':' ) ;
colors . push ( 'gray' ) ;
if ( item . user ) {
out . push ( item . user ) ;
colors . push ( 'green' ) ;
}
if ( item . prefix ) {
out . push ( item . prefix ) ;
colors . push ( 'gray' ) ;
}
}
if ( out . length ) {
out . push ( ' ' ) ;
colors . push ( '' ) ;
}
out . push ( item . command ) ;
colors . push ( 'orange' ) ;
// If there's a channel, use the current channel rather
// than the logged channel.
if ( item . channel ) {
out . push ( ` # ` ) ;
colors . push ( 'gray' ) ;
out . push ( this . replay _fix ? this . chat . ChatService . first ? . props ? . channelLogin ? ? item . channel : item . channel ) ;
colors . push ( 'green' ) ;
}
for ( const para of item . params ) {
out . push ( ` ${ para } ` ) ;
colors . push ( 'skyblue' ) ;
}
if ( item . last _param ) {
out . push ( ` : ` ) ;
colors . push ( 'gray' ) ;
out . push ( item . last _param ) ;
colors . push ( 'skyblue' ) ;
}
const msg = out . join ( '' ) ,
conn = this . client . connection ,
handler = conn . ffzOnSocketMessage ? ? conn . onSocketMessage ;
const log _msg = out . join ( '%c' ) ,
log _colors = colors . map ( x => x ? . length ? ` color: ${ x } ; ` : '' ) ;
this . chat . log . debugColor ( ` Injecting chat message: %c ${ log _msg } ` , log _colors ) ;
handler . call ( conn , {
data : msg
} ) ;
}
}
}
}
2023-11-16 18:41:50 -05:00
< / script >