diff --git a/.eslintrc.js b/.eslintrc.js index a62d5f81..f696d688 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -29,6 +29,10 @@ module.exports = { "require": false, "__webpack_hash__": false, "__git_commit__": false, + "__version_major__": false, + "__version_minor__": false, + "__version_patch__": false, + "__version_prerelease__": false, "FrankerFaceZ": false }, "rules": { diff --git a/package-lock.json b/package-lock.json index 7c44d860..89fcbeaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true } } }, @@ -1738,6 +1744,14 @@ "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } } }, "crypto-browserify": { @@ -2285,6 +2299,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true } } }, @@ -4695,6 +4715,14 @@ "requires": { "pify": "^4.0.1", "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } } }, "mamacro": { @@ -5280,6 +5308,14 @@ "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } } }, "normalize-path": { @@ -6610,6 +6646,12 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true } } }, @@ -6670,9 +6712,9 @@ } }, "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.1.1.tgz", + "integrity": "sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==", "dev": true }, "send": { diff --git a/package.json b/package.json index ed87f392..a026974f 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.0.0", + "version": "4.5.5", "description": "FrankerFaceZ is a Twitch enhancement suite.", "license": "Apache-2.0", "scripts": { @@ -41,6 +41,7 @@ "raw-loader": "^3.0.0", "rimraf": "^2.6.3", "sass-loader": "^7.1.0", + "semver": "^6.1.1", "terser-webpack-plugin": "^1.3.0", "vue-loader": "^15.7.0", "vue-template-compiler": "^2.6.10", diff --git a/src/bridge.js b/src/bridge.js new file mode 100644 index 00000000..19fd4f57 --- /dev/null +++ b/src/bridge.js @@ -0,0 +1,118 @@ +'use strict'; + +import RavenLogger from './raven'; + +import Logger from 'utilities/logging'; +import Module from 'utilities/module'; + +import {DEBUG} from 'utilities/constants'; + +import SettingsManager from './settings/index'; + +class FFZBridge extends Module { + constructor() { + super(); + const start_time = performance.now(), + VER = FFZBridge.version_info; + + FFZBridge.instance = this; + + this.name = 'ffz_bridge'; + 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.init = true; + + this.core_log = this.log.get('core'); + + this.log.info(`FrankerFaceZ Settings Bridge v${VER} (build ${VER.build}${VER.commit ? ` - commit ${VER.commit}` : ''})`); + + + // ======================================================================== + // Core Systems + // ======================================================================== + + this.inject('settings', SettingsManager); + + + // ======================================================================== + // Startup + // ======================================================================== + + 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 FFZBridge.instance; + } + + onEnable() { + window.addEventListener('message', this.onMessage.bind(this)); + this.settings.provider.on('changed', this.onProviderChange, this); + this.send({ + ffz_type: 'ready' + }); + } + + onMessage(event) { + const msg = event.data; + if ( ! msg || ! msg.ffz_type ) + return; + + if ( msg.ffz_type === 'load' ) { + const out = {}; + for(const [key, value] of this.settings.provider.entries()) + out[key] = value; + + this.send({ + ffz_type: 'loaded', + data: out + }); + } + } + + send(msg) { // eslint-disable-line class-methods-use-this + try { + window.parent.postMessage(msg, '*') + } catch(err) { this.log.error('send error', err); /* no-op */ } + } + + onProviderChange(key, value, deleted) { + this.send({ + ffz_type: 'change', + key, + value, + deleted + }); + } +} + +FFZBridge.Logger = Logger; + +const VER = FFZBridge.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' : ''}` +} + +window.FFZBridge = FFZBridge; +window.ffz_bridge = new FFZBridge(); \ No newline at end of file diff --git a/src/entry.js b/src/entry.js index ef3bd54c..eb58a937 100644 --- a/src/entry.js +++ b/src/entry.js @@ -6,6 +6,7 @@ return; const DEBUG = localStorage.ffzDebugMode == 'true' && document.body.classList.contains('ffz-dev') && ! window.Ember, + FLAVOR = location.pathname === '/p/ffz_bridge/' ? 'bridge' : 'avalon', SERVER = DEBUG ? '//localhost:8000' : '//cdn.frankerfacez.com', CLIPS = /clips\.twitch\.tv/.test(location.hostname) ? 'clips/' : '', @@ -14,6 +15,6 @@ script.id = 'ffz-script'; script.async = true; script.crossOrigin = 'anonymous'; - script.src = `${SERVER}/script/${CLIPS}avalon.js?_=${Date.now()}`; + script.src = `${SERVER}/script/${CLIPS}${FLAVOR}.js?_=${Date.now()}`; document.head.appendChild(script); })(); \ No newline at end of file diff --git a/src/main.js b/src/main.js index 0001dc7c..9f3c6f91 100644 --- a/src/main.js +++ b/src/main.js @@ -149,7 +149,10 @@ ${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}`).join('\n\n' FrankerFaceZ.Logger = Logger; const VER = FrankerFaceZ.version_info = { - major: 4, minor: 5, revision: 4, + major: __version_major__, + minor: __version_minor__, + revision: __version_patch__, + extra: __version_prerelease__?.length && __version_prerelease__[0], commit: __git_commit__, build: __webpack_hash__, toString: () => diff --git a/src/raven.js b/src/raven.js index a0320355..e0e82acc 100644 --- a/src/raven.js +++ b/src/raven.js @@ -58,8 +58,6 @@ export default class RavenLogger extends Module { super(...args); this.inject('settings'); - this.inject('site'); - this.inject('experiments'); // Do these in an event handler because we're initialized before // settings are even ready. @@ -131,7 +129,7 @@ export default class RavenLogger extends Module { autoBreadcrumbs: { console: false }, - release: FrankerFaceZ.version_info.toString(), + release: (window.FrankerFaceZ || window.FFZBridge).version_info.toString(), environment: DEBUG ? 'development' : 'production', captureUnhandledRejections: false, ignoreErrors: [ @@ -189,7 +187,7 @@ export default class RavenLogger extends Module { return false; if ( this.settings && this.settings.get('reports.error.include-user') ) { - const user = this.site && this.site.getUser(); + const user = this.resolve('site')?.getUser(); if ( user ) data.user = {id: user.id, username: user.login} } @@ -305,22 +303,25 @@ export default class RavenLogger extends Module { chat_settings[key] = value; } - for(const [key, value] of Object.entries(this.experiments.getTwitchExperiments())) - if ( this.experiments.usingTwitchExperiment(key) ) - twitch_experiments[value.name] = this.experiments.getTwitchAssignment(key); + const exp = this.resolve('experiments'); + if ( exp ) { + for(const [key, value] of Object.entries(exp.getTwitchExperiments())) + if ( exp.usingTwitchExperiment(key) ) + twitch_experiments[value.name] = exp.getTwitchAssignment(key); - for(const key of Object.keys(this.experiments.experiments)) - experiments[key] = this.experiments.getAssignment(key); + for(const key of Object.keys(exp.experiments)) + experiments[key] = exp.getAssignment(key); + } return out; } buildTags() { - const core = this.site.getCore(), + const core = this.resolve('site')?.getCore(), out = {}; - out.flavor = this.site.constructor.name; + out.flavor = this.site?.constructor.name; out.build = __webpack_hash__; out.git_commit = __git_commit__; diff --git a/src/settings/filters.js b/src/settings/filters.js index bd8ddaa2..81b8b6a9 100644 --- a/src/settings/filters.js +++ b/src/settings/filters.js @@ -98,7 +98,7 @@ export const Page = { parts = []; if ( Object.keys(config.values).length ) { - const ffz = FrankerFaceZ.get(), + const ffz = window.FrankerFaceZ?.get(), router = ffz && ffz.resolve('site.router'); if ( router ) { diff --git a/src/sites/twitch-clips/modules/chat/line.jsx b/src/sites/twitch-clips/modules/chat/line.jsx index 9bba1cef..d352e32d 100644 --- a/src/sites/twitch-clips/modules/chat/line.jsx +++ b/src/sites/twitch-clips/modules/chat/line.jsx @@ -116,7 +116,7 @@ export default class Line extends Module { return msg._ffz_message; const room = this.chat.getRoom(video.owner.id, null, true, true), - author = msg.commenter, + author = msg.commenter || {}, badges = {}; if ( msg.message.userBadges ) diff --git a/src/sites/twitch-clips/modules/css_tweaks/index.js b/src/sites/twitch-clips/modules/css_tweaks/index.js index 242f7f7f..04fed4f3 100644 --- a/src/sites/twitch-clips/modules/css_tweaks/index.js +++ b/src/sites/twitch-clips/modules/css_tweaks/index.js @@ -1,7 +1,7 @@ 'use strict'; // ============================================================================ -// CSS Tweaks for Twitch Twilight +// CSS Tweaks for Twitch Clips // ============================================================================ import Module from 'utilities/module'; @@ -21,7 +21,6 @@ export default class CSSTweaks extends Module { this.should_enable = true; this.inject('settings'); - this.inject('site.chat'); this.inject('site.theme'); this.style = new ManagedStyle; diff --git a/src/sites/twitch-clips/modules/css_tweaks/styles/chat-font.scss b/src/sites/twitch-clips/modules/css_tweaks/styles/chat-font.scss index 1aef9f17..db46fa38 100644 --- a/src/sites/twitch-clips/modules/css_tweaks/styles/chat-font.scss +++ b/src/sites/twitch-clips/modules/css_tweaks/styles/chat-font.scss @@ -1,5 +1,9 @@ .clips-chat-replay { - font-size: var(--ffz-chat-font-size); + a.clip-chat__message-author, + .message { + font-size: var(--ffz-chat-font-size) !important; + } + line-height: var(--ffz-chat-line-height); font-family: var(--ffz-chat-font-family); } \ No newline at end of file diff --git a/src/sites/twitch-clips/modules/settings_sync.js b/src/sites/twitch-clips/modules/settings_sync.js new file mode 100644 index 00000000..6d0e1338 --- /dev/null +++ b/src/sites/twitch-clips/modules/settings_sync.js @@ -0,0 +1,104 @@ +'use strict'; + +// ============================================================================ +// Settings Sync +// ============================================================================ + +import Module from 'utilities/module'; +import {createElement} from 'utilities/dom'; + +const VALID_KEYS = [ + 'client-id', + 'profiles' +]; + +export default class SettingsSync extends Module { + constructor(...args) { + super(...args); + + this.should_enable = true; + + this.inject('settings'); + } + + onEnable() { + const frame = this.frame = createElement('iframe'); + frame.src = '//www.twitch.tv/p/ffz_bridge/'; + frame.id = 'ffz-settings-bridge'; + frame.style.width = 0; + frame.style.height = 0; + + window.addEventListener('message', this.onMessage.bind(this)); + document.body.appendChild(frame); + } + + send(msg) { + try { + this.frame.contentWindow.postMessage(msg, '*'); + } catch(err) { this.log.error('send error', err); /* no-op */ } + } + + onMessage(event) { + const msg = event.data; + if ( ! msg || ! msg.ffz_type ) + return; + + if ( msg.ffz_type === 'ready' ) + this.send({ffz_type: 'load'}); + else if ( msg.ffz_type === 'loaded' ) + this.onLoad(msg.data); + else if ( msg.ffz_type === 'change' ) + this.onChange(msg); + else + this.log.info('Unknown Message', msg.ffz_type, msg); + } + + onLoad(data) { + if ( ! data ) + return; + + const provider = this.settings.provider, + old_keys = new Set(provider.keys()); + + for(const [key, value] of Object.entries(data)) { + old_keys.delete(key); + if ( ! this.isValidSetting(key) || provider.get(key) === value ) + continue; + + provider.set(key, value); + provider.emit('changed', key, value, false); + } + + for(const key of old_keys) { + provider.delete(key); + provider.emit('changed', key, undefined, true); + } + } + + onChange(msg) { + const key = msg.key, + value = msg.value, + deleted = msg.deleted; + + if ( ! this.isValidSetting(key) ) + return; + + if ( deleted ) + this.settings.provider.delete(key); + else + this.settings.provider.set(key, value); + + this.settings.provider.emit('changed', key, value, deleted); + } + + isValidSetting(key) { + if ( ! key.startsWith('p:') ) + return VALID_KEYS.includes(key); + + const idx = key.indexOf(':', 2); + if ( idx === -1 ) + return false; + + return this.settings.definitions.has(key.slice(idx + 1)); + } +} \ No newline at end of file diff --git a/webpack.clips.dev.js b/webpack.clips.dev.js index f0962a24..6c0c17e1 100644 --- a/webpack.clips.dev.js +++ b/webpack.clips.dev.js @@ -57,6 +57,11 @@ module.exports = merge(common, { next(); }); + app.get('/script/bridge.js', (req, res, next) => { + req.url = req.url.replace(/^\/script/, '/script/clips'); + next(); + }); + app.get('/dev_server', (req, res) => { res.json({ path: process.cwd(), diff --git a/webpack.common.js b/webpack.common.js index e1f95b77..4d5b3e81 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -1,14 +1,17 @@ const webpack = require('webpack'); const path = require('path'); +const semver = require('semver'); const VueLoaderPlugin = require('vue-loader/lib/plugin'); /* global process module __dirname */ +const VERSION = semver.parse(require('./package.json').version); const PRODUCTION = process.env.NODE_ENV === 'production'; module.exports = { entry: { + bridge: './src/bridge.js', avalon: './src/main.js' }, resolve: { @@ -43,7 +46,13 @@ module.exports = { }, plugins: [ new VueLoaderPlugin(), - new webpack.ExtendedAPIPlugin() + new webpack.ExtendedAPIPlugin(), + new webpack.DefinePlugin({ + __version_major__: VERSION.major, + __version_minor__: VERSION.minor, + __version_patch__: VERSION.patch, + __version_prerelease__: VERSION.prerelease + }), ], module: { rules: [{