1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-09-17 02:16:54 +00:00

4.0.0 Beta 1

This commit is contained in:
SirStendec 2017-11-13 01:23:39 -05:00
parent c2688646af
commit 262757a20d
187 changed files with 22878 additions and 38882 deletions

View file

@ -0,0 +1,308 @@
'use strict';
// ============================================================================
// Apollo
// Legendary Data Access Layer
// ============================================================================
import Module from 'utilities/module';
import {has} from 'utilities/object';
export default class Apollo extends Module {
constructor(...args) {
super(...args);
this.modifiers = {};
this.post_modifiers = {};
this.inject('..web_munch');
this.inject('..fine');
this.registerModifier('ChannelPage_ChannelInfoBar_User', `query {
user {
stream {
createdAt
type
}
}
}`);
this.registerModifier('FollowedIndex_CurrentUser', `query {
currentUser {
followedLiveUsers {
nodes {
profileImageURL(width: 70)
stream {
createdAt
}
}
}
followedHosts {
nodes {
hosting {
profileImageURL(width: 70)
stream {
createdAt
type
}
}
}
}
}
}`);
this.registerModifier('FollowingLive_CurrentUser', `query {
currentUser {
followedLiveUsers {
nodes {
profileImageURL(width: 70)
stream {
createdAt
}
}
}
}
}`);
this.registerModifier('ViewerCard', `query {
targetUser: user {
createdAt
profileViewCount
}
}`);
this.registerModifier('GamePage_Game', `query {
game {
streams {
edges {
node {
createdAt
type
broadcaster {
profileImageURL(width: 70)
}
}
}
}
}
}`);
}
async onEnable() {
// TODO: Come up with a better way to await something existing.
let client = this.client,
graphql = this.graphql;
if ( ! client ) {
const root = this.fine.getParent(this.fine.react),
ctx = root && root._context;
client = this.client = ctx && ctx.client;
}
if ( ! graphql )
graphql = this.graphql = await this.web_munch.findModule('graphql', m => m.parse && m.parseValue);
if ( ! client || ! graphql )
return new Promise(s => setTimeout(s,50)).then(() => this.onEnable());
// Parse the queries for modifiers that were already registered.
for(const key in this.modifiers)
if ( has(this.modifiers, key) ) {
const modifiers = this.modifiers[key];
if ( modifiers )
for(const mod of modifiers) {
if ( typeof mod === 'function' || mod[1] === false )
continue;
try {
mod[1] = graphql.parse(mod[0], {noLocation: true});
} catch(err) {
this.log.error(`Error parsing GraphQL statement for "${key}" modifier.`, err);
mod[1] = false;
}
}
}
// Register middleware so that we can intercept requests.
this.client.networkInterface.use([{
applyBatchMiddleware: (req, next) => {
if ( this.enabled )
this.apolloPreFlight(req);
next();
}
}]);
this.client.networkInterface.useAfter([{
applyBatchAfterware: (resp, next) => {
if ( this.enabled )
this.apolloPostFlight(resp);
next();
}
}]);
}
onDisable() {
// TODO: Remove Apollo middleware.
// Tear down the parsed queries.
for(const key in this.modifiers)
if ( has(this.modifiers, key) ) {
const modifiers = this.modifiers[key];
if ( modifiers )
for(const mod of modifiers) {
if ( typeof mod === 'function' )
continue;
mod[1] = null;
}
}
// And finally, remove our references.
this.client = this.graphql = null;
}
apolloPreFlight(request) {
for(const req of request.requests) {
const operation = req.operationName,
modifiers = this.modifiers[operation];
if ( modifiers )
for(const mod of modifiers) {
if ( typeof mod === 'function' )
mod(req);
else if ( mod[1] )
this.applyModifier(req, mod[1]);
}
this.emit(`:request.${operation}`, req.query, req.variables);
}
}
apolloPostFlight(response) {
for(const resp of response.responses) {
const operation = resp.extensions.operationName,
modifiers = this.post_modifiers[operation];
if ( modifiers )
for(const mod of modifiers)
mod(resp);
this.emit(`:response.${operation}`, resp.data);
}
}
applyModifier(request, modifier) { // eslint-disable-line class-methods-use-this
request.query = merge(request.query, modifier);
}
registerModifier(operation, modifier) {
if ( typeof modifier !== 'function' ) {
let parsed;
try {
parsed = this.graphql ? this.graphql.parse(modifier, {noLocation: true}) : null;
} catch(err) {
this.log.error(`Error parsing GraphQL statement for "${operation}" modifier.`, err);
parsed = false;
}
modifier = [modifier, parsed];
}
const mods = this.modifiers[operation] = this.modifiers[operation] || [];
mods.push(modifier);
}
unregisterModifier(operation, modifier) {
const mods = this.modifiers[operation];
if ( ! mods )
return;
for(let i=0; i < mods.length; i++) {
const mod = mods[i];
if ( typeof mod === 'function' ? mod === modifier : mod[0] === modifier ) {
mods.splice(i, 1);
return;
}
}
}
// ========================================================================
// Querying
// ========================================================================
getQuery(operation) {
const qm = this.client.queryManager,
name_map = qm && qm.queryIdsByName,
query_map = qm && qm.observableQueries,
query_id = name_map && name_map[operation],
query = query_map && query_map[query_id];
return query && query.observableQuery;
}
}
// ============================================================================
// Query Merging
// ============================================================================
function canMerge(a, b) {
return a.kind === b.kind &&
a.kind !== 'FragmentDefinition' &&
(a.selectionSet == null) === (b.selectionSet == null);
}
function merge(a, b) {
if ( ! canMerge(a, b) )
return a;
if ( a.definitions ) {
const a_def = a.definitions,
b_def = b.definitions;
for(let i=0; i < a_def.length && i < b_def.length; i++)
a_def[i] = merge(a_def[i], b_def[i]);
}
if ( a.selectionSet ) {
const s = a.selectionSet.selections,
selects = {};
for(const sel of b.selectionSet.selections)
selects[`${sel.name.value}:${sel.alias?sel.alias.value:null}`] = sel;
for(let i=0, l = s.length; i < l; i++) {
const sel = s[i],
name = sel.name.value,
alias = sel.alias ? sel.alias.value : null,
key = `${name}:${alias}`,
other = selects[key];
if ( other ) {
s[i] = merge(sel, other);
selects[key] = null;
}
}
for(const key in selects)
if ( has(selects, key) ) {
const val = selects[key];
if ( val )
s.push(val);
}
}
// TODO: Variables?
return a;
}

View file

@ -0,0 +1,87 @@
'use strict';
// ============================================================================
// Fine Router
// ============================================================================
import {parse, tokensToRegExp, tokensToFunction} from 'path-to-regexp';
import Module from 'utilities/module';
import {has} from 'utilities/object';
export default class FineRouter extends Module {
constructor(...args) {
super(...args);
this.inject('..fine');
this.__routes = [];
this.routes = {};
this.current = null;
this.match = null;
this.location = null;
}
onEnable() {
const root = this.fine.getParent(this.fine.react),
ctx = this.context = root && root._context,
router = ctx && ctx.router,
history = router && router.history;
if ( ! history )
return new Promise(r => setTimeout(r, 50)).then(() => this.onEnable());
history.listen(location => {
if ( this.enabled )
this._navigateTo(location);
});
this._navigateTo(history.location);
}
_navigateTo(location) {
this.log.debug('New Location', location);
const path = location.pathname;
if ( path === this.location )
return;
this.location = path;
for(const route of this.__routes) {
const match = route.regex.exec(path);
if ( match ) {
this.log.debug('Matching Route', route, match);
this.current = route;
this.match = match;
this.emit(':route', route, match);
this.emit(`:route:${route.name}`, ...match);
return;
}
}
this.current = this.match = null;
this.emit(':route', null, null);
}
route(name, path) {
if ( typeof name === 'object' ) {
for(const key in name)
if ( has(name, key) )
this.route(key, name[key]);
return;
}
const parts = parse(path),
score = parts.length,
route = this.routes[name] = {
name,
parts,
score,
regex: tokensToRegExp(parts),
url: tokensToFunction(parts)
}
this.__routes.push(route);
this.__routes.sort(r => r.score);
}
}

View file

@ -0,0 +1,486 @@
'use strict';
// ============================================================================
// Fine Lib
// It controls React.
// ============================================================================
import {EventEmitter} from 'utilities/events';
import Module from 'utilities/module';
import {has} from 'utilities/object';
export default class Fine extends Module {
constructor(...args) {
super(...args);
this._wrappers = new Map;
this._known_classes = new Map;
this._observer = null;
this._waiting = null;
}
async onEnable() {
// TODO: Move awaitElement to utilities/dom
if ( ! this.root_element )
this.root_element = await this.parent.awaitElement(this.selector || '[data-reactroot]');
const accessor = this.accessor = Fine.findAccessor(this.root_element);
if ( ! accessor )
return new Promise(r => setTimeout(r, 50)).then(() => this.onEnable());
this.react = this.getReactInstance(this.root_element);
}
onDisable() {
this.root_element = this.react = this.accessor = null;
}
static findAccessor(element) {
for(const key in element)
if ( key.startsWith('__reactInternalInstance$') )
return key;
}
// ========================================================================
// Low Level Accessors
// ========================================================================
getReactInstance(element) {
return element[this.accessor];
}
getOwner(instance) {
if ( instance._reactInternalInstance )
instance = instance._reactInternalInstance;
else if ( instance instanceof Node )
instance = this.getReactInstance(instance);
if ( ! instance )
return null;
return instance._owner || (instance._currentElement && instance._currentElement._owner);
}
getHostNode(instance) { //eslint-disable-line class-methods-use-this
if ( instance._reactInternalInstance )
instance = instance._reactInternalInstance;
else if ( instance instanceof Node )
instance = this.getReactInstance(instance);
while( instance )
if ( instance._hostNode )
return instance._hostNode;
else
instance = instance._renderedComponent;
}
getParent(instance) {
const owner = this.getOwner(instance);
return owner && this.getOwner(owner);
}
searchParent(node, criteria, max_depth=15, depth=0) {
if ( node._reactInternalInstance )
node = node._reactInternalInstance;
else if ( node instanceof Node )
node = this.getReactInstance(node);
if ( ! node || depth > max_depth )
return null;
const inst = node._instance;
if ( inst && criteria(inst) )
return inst;
if ( node._currentElement && node._currentElement._owner ) {
const result = this.searchParent(node._currentElement._owner, criteria, max_depth, depth+1);
if ( result )
return result;
}
if ( node._hostParent )
return this.searchParent(node._hostParent, criteria, max_depth, depth+1);
return null;
}
searchTree(node, criteria, max_depth=15, depth=0) {
if ( ! node )
node = this.react;
else if ( node._reactInternalInstance )
node = node._reactInternalInstance;
else if ( node instanceof Node )
node = this.getReactInstance(node);
if ( ! node || depth > max_depth )
return null;
const inst = node._instance;
if ( inst && criteria(inst) )
return inst;
const children = node._renderedChildren,
component = node._renderedComponent;
if ( children )
for(const key in children)
if ( has(children, key) ) {
const child = children[key];
const result = child && this.searchTree(child, criteria, max_depth, depth+1);
if ( result )
return result;
}
if ( component )
return this.searchTree(component, criteria, max_depth, depth+1);
}
searchAll(node, criterias, max_depth=15, depth=0, data) {
if ( ! node )
node = this.react;
else if ( node._reactInternalInstance )
node = node._reactInternalInstance;
else if ( node instanceof Node )
node = this.getReactInstance(node);
if ( ! data )
data = {
seen: new Set,
classes: criterias.map(() => null),
out: criterias.map(() => ({
cls: null, instances: new Set, depth: null
})),
max_depth: depth
};
if ( ! node || depth > max_depth )
return data.out;
if ( depth > data.max_depth )
data.max_depth = depth;
const inst = node._instance;
if ( inst ) {
const cls = inst.constructor,
idx = data.classes.indexOf(cls);
if ( idx !== -1 )
data.out[idx].instances.add(inst);
else if ( ! data.seen.has(cls) ) {
let i = criterias.length;
while(i-- > 0)
if ( criterias[i](inst) ) {
data.classes[i] = data.out[i].cls = cls;
data.out[i].instances.add(inst);
data.out[i].depth = depth;
break;
}
data.seen.add(cls);
}
}
const children = node._renderedChildren,
component = node._renderedComponent;
if ( children )
for(const key in children)
if ( has(children, key) ) {
const child = children[key];
child && this.searchAll(child, criterias, max_depth, depth+1, data);
}
if ( component )
this.searchAll(component, criterias, max_depth, depth+1, data);
return data.out;
}
// ========================================================================
// Class Wrapping
// ========================================================================
define(key, criteria) {
if ( this._wrappers.has(key) )
return this._wrappers.get(key);
if ( ! criteria )
throw new Error('cannot find definition and no criteria provided');
const wrapper = new FineWrapper(key, criteria, this);
this._wrappers.set(key, wrapper);
const data = this.searchAll(this.react, [criteria], 1000)[0];
if ( data.cls ) {
wrapper._set(data.cls, data.instances);
this._known_classes.set(data.cls, wrapper);
} else {
if ( ! this._waiting )
this._startWaiting();
this._waiting.push(wrapper);
this._waiting_crit.push(criteria);
}
return wrapper;
}
_checkWaiters(nodes) {
if ( ! this._waiting )
return;
if ( ! Array.isArray(nodes) )
nodes = [nodes];
for(let node of nodes) {
if ( ! node )
node = this.react;
else if ( node._reactInternalInstance )
node = node._reactInternalInstance;
else if ( node instanceof Node )
node = this.getReactInstance(node);
if ( ! node || ! this._waiting.length )
continue;
const data = this.searchAll(node, this._waiting_crit, 1000);
let i = data.length;
while(i-- > 0) {
if ( data[i].cls ) {
const d = data[i],
w = this._waiting.splice(i, 1)[0];
this._waiting_crit.splice(i, 1);
this.log.info(`Found class for "${w.name}" at depth ${d.depth}`, d);
w._set(d.cls, d.instances);
}
}
}
if ( ! this._waiting.length )
this._stopWaiting();
}
_startWaiting() {
this.log.info('Installing MutationObserver.');
this._waiting = [];
this._waiting_crit = [];
this._waiting_timer = setInterval(() => this._checkWaiters(), 500);
if ( ! this._observer )
this._observer = new MutationObserver(mutations =>
this._checkWaiters(mutations.map(x => x.target))
);
this._observer.observe(document.body, {
childList: true,
subtree: true
});
}
_stopWaiting() {
this.log.info('Stopping MutationObserver.');
if ( this._observer )
this._observer.disconnect();
if ( this._waiting_timer )
clearInterval(this._waiting_timer);
this._waiting = null;
this._waiting_crit = null;
this._waiting_timer = null;
}
}
const EVENTS = {
'will-mount': 'componentWillMount',
mount: 'componentDidMount',
render: 'render',
'receive-props': 'componentWillReceiveProps',
'should-update': 'shouldComponentUpdate',
'will-update': 'componentWillUpdate',
update: 'componentDidUpdate',
unmount: 'componentWillUnmount'
}
export class FineWrapper extends EventEmitter {
constructor(name, criteria, fine) {
super();
this.name = name;
this.criteria = criteria;
this.fine = fine;
this.instances = new Set;
this._wrapped = new Set;
this._class = null;
}
ready(fn) {
if ( this._class )
fn(this._class, this.instances);
else
this.once('set', fn);
}
_set(cls, instances) {
if ( this._class )
throw new Error('already have a class');
this._class = cls;
this._wrapped.add('componentWillMount');
this._wrapped.add('componentWillUnmount');
const t = this,
_instances = this.instances,
proto = cls.prototype,
o_mount = proto.componentWillMount,
o_unmount = proto.componentWillUnmount,
mount = proto.componentWillMount = o_mount ?
function(...args) {
this._ffz_mounted = true;
_instances.add(this);
t.emit('will-mount', this, ...args);
return o_mount.apply(this, args);
} :
function(...args) {
this._ffz_mounted = true;
_instances.add(this);
t.emit('will-mount', this, ...args);
},
unmount = proto.componentWillUnmount = o_unmount ?
function(...args) {
t.emit('unmount', this, ...args);
_instances.delete(this);
this._ffz_mounted = false;
return o_unmount.apply(this, args);
} :
function(...args) {
t.emit('unmount', this, ...args);
_instances.delete(this);
this._ffz_mounted = false;
};
this.__componentWillMount = [mount, o_mount];
this.__componentWillUnmount = [unmount, o_unmount];
for(const event of this.events())
this._maybeWrap(event);
if ( instances )
for(const inst of instances) {
if ( inst._reactInternalInstance && inst._reactInternalInstance._renderedComponent )
inst._ffz_mounted = true;
_instances.add(inst);
}
this.emit('set', cls, _instances);
}
_add(instances) {
for(const inst of instances)
this.instances.add(inst);
}
_maybeWrap(event) {
const key = EVENTS[event];
if ( ! this._class || ! key || this._wrapped.has(key) )
return;
this._wrap(event, key);
}
_wrap(event, key) {
if ( this._wrapped.has(key) )
return;
const t = this,
proto = this._class.prototype,
original = proto[key],
fn = proto[key] = original ?
function(...args) {
t.emit(event, this, ...args);
return original.apply(this, args);
} :
function(...args) {
t.emit(event, this, ...args);
};
this[`__${key}`] = [fn, original];
}
_unwrap(key) {
if ( ! this._wrapped.has(key) )
return;
const k = `__${key}`,
proto = this._class.prototype,
[fn, original] = this[k];
if ( proto[key] !== fn )
throw new Error('unable to unwrap -- prototype modified');
proto[key] = original;
this[k] = undefined;
this._wrapped.delete(key);
}
on(event, fn, ctx) {
this._maybeWrap(event);
return super.on(event, fn, ctx);
}
prependOn(event, fn, ctx) {
this._maybeWrap(event);
return super.prependOn(event, fn, ctx);
}
once(event, fn, ctx) {
this._maybeWrap(event);
return super.once(event, fn, ctx);
}
prependOnce(event, fn, ctx) {
this._maybeWrap(event);
return super.prependOnce(event, fn, ctx);
}
many(event, ttl, fn, ctx) {
this._maybeWrap(event);
return super.many(event, ttl, fn, ctx);
}
prependMany(event, ttl, fn, ctx) {
this._maybeWrap(event);
return super.prependMany(event, ttl, fn, ctx);
}
waitFor(event) {
this._maybeWrap(event);
return super.waitFor(event);
}
}

View file

@ -0,0 +1,168 @@
'use strict';
// ============================================================================
// WebMunch
// It consumes webpack.
// ============================================================================
import Module from 'utilities/module';
import {has} from 'utilities/object';
let last_muncher = 0;
export default class WebMunch extends Module {
constructor(...args) {
super(...args);
this._id = `_ffz$${last_muncher++}`;
this._rid = 0;
this._original_loader = null;
this._known_rules = {};
this._require = null;
this._module_names = {};
this._mod_cache = {};
this.hookLoader();
this.hookRequire();
}
// ========================================================================
// Loaded Modules
// ========================================================================
hookLoader(attempts) {
if ( this._original_loader )
return this.log.warn('Attempted to call hookLoader twice.');
this._original_loader = window.webpackJsonp;
if ( ! this._original_loader ) {
if ( attempts > 500 )
return this.log.error("Unable to find webpack's loader after two minutes.");
return setTimeout(this.hookLoader.bind(this, (attempts||0) + 1), 250);
}
this.log.info(`Found and wrapped webpack's loader after ${(attempts||0)*250}ms.`);
window.webpackJsonp = this.webpackJsonp.bind(this);
}
webpackJsonp(chunk_ids, modules) {
const names = chunk_ids.map(x => this._module_names[x] || x).join(', ');
this.log.info(`Twitch Chunk Loaded: ${chunk_ids} (${names})`);
this.log.debug(`Modules: ${Object.keys(modules)}`);
const res = this._original_loader.apply(window, arguments); // eslint-disable-line prefer-rest-params
this.emit(':loaded', chunk_ids, names, modules);
return res;
}
// ========================================================================
// Finding Modules
// ========================================================================
known(key, predicate) {
if ( typeof key === 'object' ) {
for(const k in key)
if ( has(key, k) )
this.known(k, key[k]);
return;
}
this._known_rules[key] = predicate;
}
async findModule(key, predicate) {
if ( ! this._require )
await this.getRequire();
return this.getModule(key, predicate);
}
getModule(key, predicate) {
if ( typeof key === 'function' ) {
predicate = key;
key = null;
}
if ( key && this._mod_cache[key] )
return this._mod_cache[key];
const require = this._require;
if ( ! require || ! require.c )
return null;
if ( ! predicate )
predicate = this._known_rules[key];
if ( ! predicate )
throw new Error(`no known predicate for locating ${key}`);
for(const k in require.c)
if ( has(require.c, k) ) {
const module = require.c[k],
mod = module && module.exports;
if ( mod && predicate(mod) ) {
if ( key )
this._mod_cache[key] = mod;
return mod;
}
}
}
// ========================================================================
// Grabbing Require
// ========================================================================
getRequire() {
if ( this._require )
return Promise.resolve(this._require);
return new Promise(resolve => {
const id = `${this._id}$${this._rid++}`;
(this._original_loader || window.webpackJsonp)(
[],
{
[id]: (module, exports, __webpack_require__) => {
resolve(this._require = __webpack_require__);
}
},
[id]
)
});
}
async hookRequire() {
const start_time = performance.now(),
require = await this.getRequire(),
time = performance.now() - start_time;
this.log.info(`require() grabbed in ${time.toFixed(5)}ms.`);
const loader = require.e && require.e.toString();
let modules;
if ( loader && loader.indexOf('Loading chunk') !== -1 ) {
const data = /({0:.*?})/.exec(loader);
if ( data )
try {
modules = JSON.parse(data[1].replace(/(\d+):/g, '"$1":'))
} catch(err) { } // eslint-disable-line no-empty
}
if ( modules ) {
this._module_names = modules;
this.log.info(`Loaded names for ${Object.keys(modules).length} chunks from require().`)
} else
this.log.warn(`Unable to find chunk names in require().`);
}
}