diff --git a/package.json b/package.json index f3f49b18..47581bbd 100755 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/clips.js b/src/clips.js new file mode 100644 index 00000000..9df1f3ab --- /dev/null +++ b/src/clips.js @@ -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(); diff --git a/src/modules/main_menu/components/badge-term-editor.vue b/src/modules/main_menu/components/badge-term-editor.vue index f7fa2038..6585b1aa 100644 --- a/src/modules/main_menu/components/badge-term-editor.vue +++ b/src/modules/main_menu/components/badge-term-editor.vue @@ -43,7 +43,7 @@
- +
  diff --git a/src/modules/main_menu/components/term-editor.vue b/src/modules/main_menu/components/term-editor.vue index 3000cd43..9ffd9fd4 100644 --- a/src/modules/main_menu/components/term-editor.vue +++ b/src/modules/main_menu/components/term-editor.vue @@ -28,7 +28,7 @@ >
- +
  diff --git a/src/sites/twitch-twilight/modules/css_tweaks/index.js b/src/sites/twitch-twilight/modules/css_tweaks/index.js index 0d29144c..d0b83dc2 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/index.js +++ b/src/sites/twitch-twilight/modules/css_tweaks/index.js @@ -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"]' }; diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/square-avatars.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/square-avatars.scss index 35fc2ad1..c0805b24 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/square-avatars.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/square-avatars.scss @@ -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; } diff --git a/src/sites/twitch-twilight/switchboard.js b/src/sites/twitch-twilight/switchboard.js index b08c0538..aba0b0af 100644 --- a/src/sites/twitch-twilight/switchboard.js +++ b/src/sites/twitch-twilight/switchboard.js @@ -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 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.'); } } diff --git a/src/std-components/color-picker.vue b/src/std-components/color-picker.vue index 75cbcfa1..936e0d93 100644 --- a/src/std-components/color-picker.vue +++ b/src/std-components/color-picker.vue @@ -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" >
diff --git a/src/utilities/compat/fine.js b/src/utilities/compat/fine.js index b1d17ee7..d9aa1d2f 100644 --- a/src/utilities/compat/fine.js +++ b/src/utilities/compat/fine.js @@ -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; } diff --git a/src/utilities/compat/subpump.js b/src/utilities/compat/subpump.js index 638c8b0d..5eb84836 100644 --- a/src/utilities/compat/subpump.js +++ b/src/utilities/compat/subpump.js @@ -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.'); diff --git a/src/utilities/compat/webmunch.js b/src/utilities/compat/webmunch.js index 0d8017e3..6174b305 100644 --- a/src/utilities/compat/webmunch.js +++ b/src/utilities/compat/webmunch.js @@ -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().`); } -} \ No newline at end of file +} + +WebMunch.Requires = Requires; \ No newline at end of file diff --git a/src/utilities/logging.js b/src/utilities/logging.js index e647ef9a..06df7456 100644 --- a/src/utilities/logging.js +++ b/src/utilities/logging.js @@ -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) )