1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00
* Fixed: Color picker for chat filtering opening up and going out of view. (Can still go *down* out of view, but it's a start.)
* Fixed: Square Avatar and hide LIVE indicator not working on the channel page.
* Fixed: Modify `webmunch` and `switchboard` to better handle the latest webpack update, adding checks to make sure we don't accidentally break state by requiring an unloaded module.
This commit is contained in:
SirStendec 2021-03-11 14:25:34 -05:00
parent 16ab515b4b
commit a91907c869
12 changed files with 402 additions and 62 deletions

View file

@ -1,7 +1,7 @@
{
"name": "frankerfacez",
"author": "Dan Salvato LLC",
"version": "4.20.75",
"version": "4.20.76",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"license": "Apache-2.0",
"scripts": {

157
src/clips.js Normal file
View file

@ -0,0 +1,157 @@
'use strict';
import dayjs from 'dayjs';
import RavenLogger from './raven';
import Logger from 'utilities/logging';
import Module from 'utilities/module';
import {DEBUG} from 'utilities/constants';
import {timeout} from 'utilities/object';
import SettingsManager from './settings/index';
import AddonManager from './addons';
import ExperimentManager from './experiments';
import {TranslationManager} from './i18n';
import Site from './sites/clips';
import Tooltips from 'src/modules/tooltips';
import Chat from 'src/modules/chat';
class FrankerFaceZ extends Module {
constructor() {
super();
const start_time = performance.now(),
VER = FrankerFaceZ.version_info;
FrankerFaceZ.instance = this;
this.flavor = 'clips';
this.name = 'ffz_clips';
this.__state = 0;
this.__modules.core = this;
// ========================================================================
// Error Reporting and Logging
// ========================================================================
this.inject('raven', RavenLogger);
this.log = new Logger(null, null, null, this.raven);
this.log.label = 'FFZClips';
this.log.init = true;
this.core_log = this.log.get('core');
this.log.info(`FrankerFaceZ Standalone Clips v${VER} (build ${VER.build}${VER.commit ? ` - commit ${VER.commit}` : ''})`);
// ========================================================================
// Core Systems
// ========================================================================
this.inject('settings', SettingsManager);
this.inject('experiments', ExperimentManager);
this.inject('i18n', TranslationManager);
this.inject('site', Site);
this.inject('addons', AddonManager);
// ========================================================================
// Startup
// ========================================================================
this.inject('tooltips', Tooltips);
this.register('chat', Chat);
this.enable().then(() => {
const duration = performance.now() - start_time;
this.core_log.info(`Initialization complete in ${duration.toFixed(5)}ms.`);
this.log.init = false;
}).catch(err => {
this.core_log.error(`An error occurred during initialization.`, err);
this.log.init = false;
});
}
static get() {
return FrankerFaceZ.instance;
}
// ========================================================================
// Generate Log
// ========================================================================
async generateLog() {
const promises = [];
for(const key in this.__modules) { // eslint-disable-line guard-for-in
const module = this.__modules[key];
if ( module instanceof Module && module.generateLog && module != this )
promises.push((async () => {
try {
return [
key,
await timeout(Promise.resolve(module.generateLog()), 5000)
];
} catch(err) {
return [
key,
`Error: ${err}`
]
}
})());
}
const out = await Promise.all(promises);
if ( this.log.captured_init && this.log.captured_init.length > 0 ) {
const logs = [];
for(const msg of this.log.captured_init) {
const time = dayjs(msg.time).locale('en').format('H:mm:ss');
logs.push(`[${time}] ${msg.level} | ${msg.category || 'core'}: ${msg.message}`);
}
out.unshift(['initialization', logs.join('\n')]);
}
return out.map(x => `${x[0]}
-------------------------------------------------------------------------------
${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}`).join('\n\n');
}
}
FrankerFaceZ.Logger = Logger;
const VER = FrankerFaceZ.version_info = {
major: __version_major__,
minor: __version_minor__,
revision: __version_patch__,
extra: __version_prerelease__?.length && __version_prerelease__[0],
commit: __git_commit__,
build: __webpack_hash__,
toString: () =>
`${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}`
}
// We don't support addons in the player right now, so
FrankerFaceZ.utilities = {
addon: require('utilities/addon'),
color: require('utilities/color'),
constants: require('utilities/constants'),
dom: require('utilities/dom'),
events: require('utilities/events'),
//fontAwesome: require('utilities/font-awesome'),
//graphql: require('utilities/graphql'),
logging: require('utilities/logging'),
module: require('utilities/module'),
object: require('utilities/object'),
time: require('utilities/time'),
tooltip: require('utilities/tooltip'),
i18n: require('utilities/translation-core'),
dayjs: require('dayjs'),
popper: require('popper.js').default
}
window.FrankerFaceZ = FrankerFaceZ;
window.ffz = new FrankerFaceZ();

View file

@ -43,7 +43,7 @@
</select>
</div>
<div v-if="colored" class="tw-flex-shrink-0 tw-mg-r-05">
<color-picker v-if="editing" v-model="edit_data.c" :nullable="true" :show-input="false" :open-up="true" />
<color-picker v-if="editing" v-model="edit_data.c" :nullable="true" :show-input="false" />
<div v-else-if="term.c" class="ffz-color-preview">
<figure :style="`background-color: ${term.c}`">
&nbsp;

View file

@ -28,7 +28,7 @@
>
</div>
<div v-if="colored" class="tw-flex-shrink-0 tw-mg-l-05">
<color-picker v-if="editing" v-model="edit_data.c" :nullable="true" :show-input="false" :open-up="true" />
<color-picker v-if="editing" v-model="edit_data.c" :nullable="true" :show-input="false" />
<div v-else-if="term.c" class="ffz-color-preview">
<figure :style="`background-color: ${term.c}`">
&nbsp;

View file

@ -40,7 +40,7 @@ const CLASSES = {
'dir-live-ind': '.live-channel-card[data-ffz-type="live"] .tw-channel-status-text-indicator, article[data-ffz-type="live"] .tw-channel-status-text-indicator',
'profile-hover': '.preview-card .tw-relative:hover .ffz-channel-avatar',
'not-live-bar': 'div[data-test-selector="non-live-video-banner-layout"]',
'channel-live-ind': '.channel-header__user .tw-channel-status-text-indicator,.channel-info-content .user-avatar-animated__live',
'channel-live-ind': '.channel-header__user .tw-channel-status-text-indicator,.channel-info-content .tw-halo__indicator',
'celebration': 'body .celebration__overlay',
'mod-view': '.chat-input__buttons-container .tw-core-button[href*="/moderator"]'
};

View file

@ -6,7 +6,9 @@
}
.user-avatar-card__halo,
.player-streaminfo__picture img[src] {
.player-streaminfo__picture img[src],
.channel-info-content .tw-halo,
.channel-info-content .tw-halo:before {
border-radius: 0 !important;
}

View file

@ -7,6 +7,7 @@
import Module from 'utilities/module';
import pathToRegexp from 'path-to-regexp';
import { sleep } from 'src/utilities/object';
export default class Switchboard extends Module {
@ -21,7 +22,7 @@ export default class Switchboard extends Module {
}
awaitRouter() {
awaitRouter(count = 0) {
const router = this.fine.searchTree(null,
n => (n.logger && n.logger.category === 'default-root-router') ||
(n.onHistoryChange && n.reportInteractive) ||
@ -31,7 +32,51 @@ export default class Switchboard extends Module {
if ( router )
return Promise.resolve(router);
return new Promise(r => setTimeout(r, 50)).then(() => this.awaitRouter());
if ( count > 50 )
return Promise.resolve(null);
return sleep(50).then(() => this.awaitRouter(count + 1));
}
awaitRoutes(count = 0) {
const routes = this.fine.searchTree(null,
n => n.props?.component && n.props.path,
100, 0, false, true);
if ( routes?.size )
return Promise.resolve(routes);
if ( count > 50 )
return Promise.resolve(null);
return sleep(50).then(() => this.awaitRoutes(count + 1));
}
getSwitches(routes) {
const switches = new Set;
for(const route of routes) {
const switchy = this.fine.searchParent(route, n => n.props?.children);
if ( switchy )
switches.add(switchy);
}
return switches;
}
getPossibleRoutes(switches) { // eslint-disable-line class-methods-use-this
const routes = new Set;
for(const switchy of switches) {
if ( Array.isArray(switchy?.props?.children) )
for(const child of switchy.props.children) {
if ( child?.props?.component )
routes.add(child);
}
}
return routes;
}
@ -46,7 +91,7 @@ export default class Switchboard extends Module {
if ( count > 50 )
return Promise.resolve(null);
return new Promise(r => setTimeout(r, 50)).then(() => this.awaitRoute(count + 1));
return sleep(50).then(() => this.awaitRoute(count + 1));
}
@ -57,16 +102,16 @@ export default class Switchboard extends Module {
// Find the current route.
const route = await this.awaitRoute(),
da_switch = route && this.fine.searchParent(route, n => n.props && n.props.children);
da_switch = route && this.fine.searchParent(route, n => n.props?.children);
if ( ! da_switch )
return new Promise(r => setTimeout(r, 50)).then(() => this.onEnable());
return sleep(50).then(() => this.onEnable());
// Identify Router
const router = await this.awaitRouter();
this.log.info(`Found Route and Switch with ${da_switch.props.children.length} routes.`);
this.da_switch = da_switch;
this.possible = da_switch.props.children;
this.location = router.props.location.pathname;
//const location = router.props.location.pathname;
@ -78,9 +123,31 @@ export default class Switchboard extends Module {
});
}
async startMultiRouter() {
this.multi_router = true;
const routes = await this.awaitRoutes();
if ( ! routes?.size )
return this.log.info(`Unable to find any <Route/>s for multi-router.`);
const switches = this.getSwitches(routes);
if ( ! switches?.size )
return this.log.info(`Unable to find any switches for multi-router.`);
this.possible = this.getPossibleRoutes(switches);
this.log.info(`Found ${routes.size} Routes with ${switches.size} Switches and ${this.possible.size} routes.`);
this.loadOne();
}
loadOne() {
if ( ! this.loadRoute(false) )
this.loadRoute(true);
if ( ! this.loadRoute(true) ) {
if ( ! this.multi_router )
this.startMultiRouter();
else
this.log.info(`There are no routes that can be used to load a chunk. Tried ${this.tried.size} routes.`);
}
}
waitAndSee() {
@ -88,13 +155,13 @@ export default class Switchboard extends Module {
if ( this.web_munch._require )
return;
this.log.info('We still need require(). Trying again.');
this.log.debug('We still need require(). Trying again.');
this.loadOne();
});
}
loadRoute(with_params) {
for(const route of this.da_switch.props.children) {
for(const route of this.possible) {
if ( ! route.props || ! route.props.component )
continue;
@ -114,7 +181,7 @@ export default class Switchboard extends Module {
}
this.tried.add(route.props.path);
this.log.info('Found Non-Matching Route', route.props.path);
this.log.debug('Found Non-Matching Route', route.props.path);
const component_class = route.props.component;
@ -124,7 +191,8 @@ export default class Switchboard extends Module {
try {
component = component_class.Preload({priority: 1});
} catch(err) {
this.log.warn('Error instantiating preloader for forced chunk loading.', err);
this.log.warn('Error instantiating preloader for forced chunk loading.');
this.log.debug('Captured Error', err);
component = null;
}
@ -133,18 +201,19 @@ export default class Switchboard extends Module {
try {
component.props.loader().then(() => {
this.log.info('Successfully forced a chunk to load using route', route.props.path)
this.log.debug('Successfully loaded route', route.props.path)
this.waitAndSee();
});
} catch(err) {
this.log.warn('Unexpected result trying to use component pre-loader to force loading of another chunk.');
this.log.warn('Unexpected result trying to use component pre-loader.');
}
} else {
try {
component = new route.props.component;
} catch(err) {
this.log.warn('Error instantiating component for forced chunk loading.', err);
this.log.warn('Error instantiating component for forced chunk loading.');
this.log.debug('Captured Error', err);
component = null;
}
@ -153,11 +222,11 @@ export default class Switchboard extends Module {
try {
component.props.children.props.loader().then(() => {
this.log.info('Successfully forced a chunk to load using route', route.props.path)
this.log.debug('Successfully loaded route', route.props.path)
this.waitAndSee();
});
} catch(err) {
this.log.warn('Unexpected result trying to use component loader to force loading of another chunk.');
this.log.warn('Unexpected result trying to use component loader.');
}
}

View file

@ -47,7 +47,7 @@
v-if="open"
v-on-clickaway="closePicker"
:class="{'ffz-bottom-100': openUp}"
class="tw-absolute tw-z-above ffz-balloon--up ffz-balloon--right"
class="tw-absolute tw-z-above tw-tooltip--down tw-tooltip--align-right"
>
<chrome-picker :disable-alpha="! alpha" :value="colors" @input="onPick" />
</div>

View file

@ -246,7 +246,7 @@ export default class Fine extends Module {
}
}
searchTree(node, criteria, max_depth=15, depth=0, traverse_roots = true) {
searchTree(node, criteria, max_depth=15, depth=0, traverse_roots = true, multi = false) {
if ( ! node )
node = this.react;
else if ( node._reactInternalFiber )
@ -254,8 +254,16 @@ export default class Fine extends Module {
else if ( node instanceof Node )
node = this.getReactInstance(node);
if ( multi ) {
if ( !(multi instanceof Set) )
multi = new Set;
}
if ( multi && ! (multi instanceof Set) )
multi = new Set;
if ( ! node || node._ffz_no_scan || depth > max_depth )
return null;
return multi ? multi : null;
if ( typeof criteria === 'string' ) {
const wrapper = this._wrappers.get(criteria);
@ -263,20 +271,24 @@ export default class Fine extends Module {
throw new Error('invalid critera');
if ( ! wrapper._class )
return null;
return multi ? multi : null;
criteria = n => n && n.constructor === wrapper._class;
}
const inst = node.stateNode;
if ( inst && criteria(inst, node) )
return inst;
if ( inst && criteria(inst, node) ) {
if ( multi )
multi.add(inst);
else
return inst;
}
if ( node.child ) {
let child = node.child;
while(child) {
const result = this.searchTree(child, criteria, max_depth, depth+1, traverse_roots);
if ( result )
const result = this.searchTree(child, criteria, max_depth, depth+1, traverse_roots, multi);
if ( result && ! multi )
return result;
child = child.sibling;
}
@ -287,14 +299,17 @@ export default class Fine extends Module {
if ( root ) {
let child = root._internalRoot && root._internalRoot.current || root.current;
while(child) {
const result = this.searchTree(child, criteria, max_depth, depth+1, traverse_roots);
if ( result )
const result = this.searchTree(child, criteria, max_depth, depth+1, traverse_roots, multi);
if ( result && ! multi )
return result;
child = child.sibling;
}
}
}
if ( multi )
return multi;
}

View file

@ -55,7 +55,7 @@ export default class Subpump extends Module {
return;
}
for(const [key, val] of Object.entries(instances))
for(const val of Object.values(instances))
if ( val?._client ) {
if ( this.instance ) {
this.log.warn('Multiple PubSub instances detected. Things might act weird.');

View file

@ -6,8 +6,20 @@
// ============================================================================
import Module from 'utilities/module';
import {has, sleep} from 'utilities/object';
import { DEBUG } from '../constants';
import {has} from 'utilities/object';
import { DEBUG } from 'utilities/constants';
const Requires = Symbol('FFZRequires');
const regex_cache = {};
function getRequireRegex(name) {
if ( ! regex_cache[name] )
regex_cache[name] = new RegExp(`\\b${name}\\(([0-9a-zA-Z_+]+)\\)`, 'g');
return regex_cache[name];
}
const NAMES = [
'webpackJsonp',
@ -34,8 +46,11 @@ export default class WebMunch extends Module {
this._chunk_names = {};
this._mod_cache = {};
this._checked_module = {};
this._required_ids = new Set;
this._known_ids = new Set;
this.Requires = Requires;
this.v4 = null;
this.hookLoader();
@ -107,12 +122,9 @@ export default class WebMunch extends Module {
this._original_loader = thing.push;
// Wrap all existing modules in case any of them haven't been required yet.
// However, there's an issue with this causing loading issues on the
// dashboard. Somehow. Not sure, so just don't do it on that page.
if ( ! location.hostname.includes('dashboard') )
for(const chunk of thing)
if ( chunk && chunk[1] )
this.processModulesV4(chunk[1]);
for(const chunk of thing)
if ( chunk && chunk[1] )
this.processModulesV4(chunk[1], true);
try {
thing.push = this.webpackJsonpv4.bind(this);
@ -146,29 +158,40 @@ export default class WebMunch extends Module {
}
_resolveRequire(require) {
if ( this._require )
return;
this._require = require;
if ( this._resolve_require ) {
for(const fn of this._resolve_require)
fn(require);
this._resolve_require = null;
}
}
processModulesV4(modules) {
const t = this;
for(const [mod_id, original_module] of Object.entries(modules)) {
this._known_ids.add(mod_id);
modules[mod_id] = function(module, exports, require, ...args) {
if ( ! t._require && typeof require === 'function' ) {
t.log.info(`require() grabbed from invocation of module ${mod_id}`);
t._require = require;
if ( t._resolve_require ) {
try {
for(const fn of t._resolve_require)
fn(require);
} catch(err) {
t.log.error('An error occurred running require callbacks.', err);
}
t._resolve_require = null;
t.log.debug(`require() grabbed from invocation of module ${mod_id}`);
try {
t._resolveRequire(require);
} catch(err) {
t.log.error('An error occurred running require callbacks.', err);
}
}
return original_module.call(this, module, exports, require, ...args);
}
modules[mod_id].original = original_module;
}
}
@ -176,14 +199,15 @@ export default class WebMunch extends Module {
webpackJsonpv4(data) {
const chunk_ids = data[0],
modules = data[1],
names = Array.isArray(chunk_ids) && chunk_ids.map(x => this._chunk_names[x] || x).join(', ');
names = Array.isArray(chunk_ids) && chunk_ids.map(x => this._chunk_names[x] || x);
this.log.verbose(`Twitch Chunk Loaded: ${chunk_ids} (${names})`);
this.log.verbose(`Twitch Chunk Loaded: ${chunk_ids} (${names.join(', ')})`);
this.log.verbose(`Modules: ${Object.keys(modules)}`);
if ( modules )
this.processModulesV4(modules);
this.processModulesV4(modules, false);
this._checked_module = {};
const res = this._original_loader.apply(this._original_store, arguments); // eslint-disable-line prefer-rest-params
this.emit(':loaded', chunk_ids, names, modules);
return res;
@ -296,10 +320,16 @@ export default class WebMunch extends Module {
if ( ! this._original_store )
return null;
const out = new Set;
for(const [chunks, modules] of this._original_store) {
if ( modules[id] )
return chunks;
if ( modules[id] ) {
for(const chunk of chunks)
out.add(chunk);
}
}
return [...out];
}
chunkNameForModule(id) {
@ -316,6 +346,14 @@ export default class WebMunch extends Module {
return null;
}
chunkNamesForModule(id) {
const chunks = this._chunksForModule(id);
if ( ! chunks )
return null;
return chunks.map(id => this._chunk_names[id] || id);
}
_oldGetModule(key, predicate, require) {
if ( ! require || ! require.c )
@ -358,7 +396,7 @@ export default class WebMunch extends Module {
if ( mod ) {
const ret = predicate(mod);
if ( ret ) {
this.log.debug(`Located module "${key}" in module ${k}${DEBUG ? ` (${this.chunkNameForModule(k)})` : ''} after ${checked} tries`);
this.log.debug(`[Old] Located module "${key}" in module ${k}${DEBUG ? ` (${this.chunkNameForModule(k)})` : ''} after ${checked} tries`);
const out = predicate.use_result ? ret : mod;
if ( key )
this._mod_cache[key] = out;
@ -367,7 +405,7 @@ export default class WebMunch extends Module {
}
}
this.log.debug(`Unable to locate module "${key}"`);
this.log.debug(`[Old] Unable to locate module "${key}" despite checking ${checked} modules`);
return null;
}
@ -405,6 +443,22 @@ export default class WebMunch extends Module {
for(const id of ids) {
try {
checked++;
// If we have not previously required this module, check to see
// if we CAN require this module. We want to avoid requiring a
// module that doesn't yet have a constructor because that will
// break webpack's internal state.
if ( ! this._required_ids.has(id) ) {
let check = this._checked_module[id];
if ( check == null )
check = this._checkModule(id);
if ( check )
continue;
}
this._required_ids.add(id);
const mod = require(id);
if ( mod ) {
const ret = predicate(mod);
@ -421,11 +475,50 @@ export default class WebMunch extends Module {
}
}
this.log.debug(`Unable to locate module "${key}"`);
this.log.debug(`Unable to locate module "${key}" despite checking ${checked} modules`);
return null;
}
_checkModule(id) {
const fn = this._require?.m?.[id];
if ( fn ) {
let reqs = fn[Requires],
banned = false;
if ( reqs == null ) {
const str = fn.toString(),
name_match = /^function\([^,)]+,[^,)]+,([^,)]+)/.exec(str);
if ( name_match ) {
const regex = getRequireRegex(name_match[1]);
reqs = fn[Requires] = new Set;
regex.lastIndex = 0;
let match;
while((match = regex.exec(str))) {
const mod_id = match[1];
reqs.add(mod_id);
if ( ! this._require.m[mod_id] )
banned = true;
}
} else
fn[Requires] = false;
} else if ( reqs ) {
for(const mod_id of reqs)
if ( ! this._require.m[mod_id] )
banned = true;
}
return this._checked_module[id] = banned;
}
}
// ========================================================================
// Grabbing Require
// ========================================================================
@ -515,9 +608,11 @@ export default class WebMunch extends Module {
}
this._chunk_names = modules;
this.log.info(`Loaded names for ${Object.keys(modules).length} chunks from require().`)
this.log.debug(`Loaded names for ${Object.keys(modules).length} chunks from require().`)
} else
this.log.warn(`Unable to find chunk names in require().`);
}
}
}
WebMunch.Requires = Requires;

View file

@ -1,5 +1,7 @@
'use strict';
import { has } from 'utilities/object';
const RAVEN_LEVELS = {
1: 'debug',
2: 'info',
@ -14,7 +16,7 @@ function readLSLevel() {
return null;
const upper = level.toUpperCase();
if ( Logger[upper] )
if ( has(Logger, upper) )
return Logger[upper];
if ( /^\d+$/.test(level) )