mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-08 07:10:54 +00:00
734 lines
17 KiB
Vue
734 lines
17 KiB
Vue
<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"
|
|
:selected="sample.data === message && sample.topic === topic"
|
|
:value="idx"
|
|
>
|
|
{{ sample.name }}
|
|
</option>
|
|
</select>
|
|
<input
|
|
ref="topic"
|
|
class="tw-block tw-font-size-6 tw-full-width ffz-textarea ffz-mg-t-1p"
|
|
@blur="updateMessage"
|
|
@input="onMessageChange"
|
|
/>
|
|
<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';
|
|
import { highlightJson } from 'utilities/dom';
|
|
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;
|
|
const topic = state?.ffz_ct_topic ?? samples[0].topic ?? '';
|
|
|
|
let is_custom = true;
|
|
/*for(const item of samples) {
|
|
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) {
|
|
is_custom = false;
|
|
break;
|
|
}
|
|
}*/
|
|
|
|
return {
|
|
has_client: false,
|
|
|
|
samples,
|
|
is_custom,
|
|
message,
|
|
topic,
|
|
|
|
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;
|
|
},
|
|
|
|
topic() {
|
|
if ( ! this.is_custom )
|
|
this.$refs.topic.value = this.topic;
|
|
},
|
|
|
|
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;
|
|
this.$refs.topic.value = this.topic;
|
|
},
|
|
|
|
methods: {
|
|
highlightJson(object, pretty) {
|
|
return highlightJson(object, pretty);
|
|
},
|
|
|
|
// 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) {
|
|
if ( ! item.topic )
|
|
item.topic = '';
|
|
if ( Array.isArray(item.data) )
|
|
item.data = item.data.join('\n\n');
|
|
else if ( typeof item.data !== 'string' )
|
|
item.data = JSON.stringify(item.data, null, 4);
|
|
}
|
|
|
|
LOADED_SAMPLES = values;
|
|
this.samples = deep_copy(values);
|
|
|
|
let is_custom = true;
|
|
for(const item of this.samples) {
|
|
if (item.data === this.message && item.topic === this.topic) {
|
|
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() {
|
|
const idx = this.$refs.selector.value,
|
|
item = this.samples[idx];
|
|
|
|
if ( idx !== 'custom' && item?.data ) {
|
|
this.message = item.data;
|
|
this.topic = item.topic ?? '';
|
|
this.is_custom = false;
|
|
} else
|
|
this.is_custom = true;
|
|
},
|
|
|
|
updateMessage() {
|
|
const value = this.$refs.message.value,
|
|
topic = this.$refs.topic.value;
|
|
|
|
let is_custom = true;
|
|
for(const item of this.samples) {
|
|
if (item.data === value && item.topic === topic) {
|
|
is_custom = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.is_custom = is_custom;
|
|
if ( this.is_custom ) {
|
|
this.topic = topic;
|
|
this.message = value;
|
|
}
|
|
},
|
|
|
|
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() {
|
|
// 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;
|
|
}
|
|
|
|
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) {
|
|
if ( item.pubsub ) {
|
|
const channel = this.chat.ChatService.first?.props?.channelID,
|
|
user = this.chat.resolve('site').getUser();
|
|
|
|
if ( this.replay_fix ) {
|
|
item.topic = item.topic.replace(/<channel>/gi, channel);
|
|
item.topic = item.topic.replace(/<user>/gi, user.id);
|
|
// TODO: Crawl, replacing ids.
|
|
// TODO: Update timestamps for pinned chat?
|
|
}
|
|
|
|
this.chat.resolve('site.subpump').simulateMessage(item.topic, item.data);
|
|
}
|
|
|
|
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
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|