diff --git a/.eslintrc.js b/.eslintrc.js index 83360e50..25fd1d7e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,6 +34,7 @@ module.exports = { '__version_minor__': false, '__version_patch__': false, '__version_prerelease__': false, + '__extension__': false, 'FrankerFaceZ': false }, 'rules': { diff --git a/package.json b/package.json index 421f994b..96d562e3 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.47.1", + "version": "4.48.0", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/addons.js b/src/addons.js index 8ad6e432..cd22f3c3 100644 --- a/src/addons.js +++ b/src/addons.js @@ -5,7 +5,7 @@ // ============================================================================ import Module from 'utilities/module'; -import { SERVER } from 'utilities/constants'; +import { EXTENSION, SERVER_OR_EXT } from 'utilities/constants'; import { createElement } from 'utilities/dom'; import { timeout, has } from 'utilities/object'; import { getBuster } from 'utilities/time'; @@ -69,15 +69,16 @@ export default class AddonManager extends Module { off: (...args) => this.off(...args) }); - this.settings.add('addons.dev.server', { - default: false, - ui: { - path: 'Add-Ons >> Development', - title: 'Use Local Development Server', - description: 'Attempt to load add-ons from local development server on port 8001.', - component: 'setting-check-box' - } - }); + if ( ! EXTENSION ) + this.settings.add('addons.dev.server', { + default: false, + ui: { + path: 'Add-Ons >> Development', + title: 'Use Local Development Server', + description: 'Attempt to load add-ons from local development server on port 8001.', + component: 'setting-check-box' + } + }); this.on('i18n:update', this.rebuildAddonSearch, this); @@ -151,9 +152,13 @@ export default class AddonManager extends Module { async loadAddonData() { const [cdn_data, local_data] = await Promise.all([ - fetchJSON(`${SERVER}/script/addons.json?_=${getBuster(30)}`), - this.settings.get('addons.dev.server') ? - fetchJSON(`https://localhost:8001/script/addons.json?_=${getBuster()}`) : null + fetchJSON(`${SERVER_OR_EXT}/addons.json?_=${getBuster(30)}`), + + // Do not attempt to load local add-ons if using the extension, as + // loading external code is against the policy of basically everyone. + (! EXTENSION && this.settings.get('addons.dev.server')) + ? fetchJSON(`https://localhost:8001/script/addons.json?_=${getBuster()}`) + : null ]); if ( Array.isArray(cdn_data) ) @@ -469,7 +474,7 @@ export default class AddonManager extends Module { document.head.appendChild(createElement('script', { id: `ffz-loaded-addon-${addon.id}`, type: 'text/javascript', - src: addon.src || `${addon.dev ? 'https://localhost:8001' : SERVER}/script/addons/${addon.id}/script.js?_=${getBuster(30)}`, + src: addon.src || `${addon.dev ? 'https://localhost:8001/script' : SERVER_OR_EXT}/addons/${addon.id}/script.js?_=${getBuster(30)}`, crossorigin: 'anonymous' })); diff --git a/src/entry_ext.js b/src/entry_ext.js new file mode 100644 index 00000000..6a9d9f57 --- /dev/null +++ b/src/entry_ext.js @@ -0,0 +1,25 @@ +/* eslint strict: off */ +'use strict'; +(() => { + // Don't run on certain sub-domains. + if ( /^(?:localhost\.rig|blog|im|chatdepot|tmi|api|brand|dev|gql|passport)\./.test(location.hostname) ) + return; + + const HOST = location.hostname, + SERVER = __EXTENSION_PATH__, + script = document.createElement('script'); + + let FLAVOR = + HOST.includes('player') ? 'player' : + HOST.includes('clips') ? 'clips' : + (location.pathname === '/p/ffz_bridge/' ? 'bridge' : 'avalon'); + + if (FLAVOR === 'clips' && location.pathname === '/embed') + FLAVOR = 'player'; + + script.id = 'ffz-script'; + script.async = true; + script.crossOrigin = 'anonymous'; + script.src = `${SERVER}/${FLAVOR}.js?_=${Date.now()}`; + document.head.appendChild(script); +})(); diff --git a/src/load_tracker.jsx b/src/load_tracker.jsx index 9bcdd419..cd68089f 100644 --- a/src/load_tracker.jsx +++ b/src/load_tracker.jsx @@ -67,9 +67,11 @@ export default class LoadTracker extends Module { data.success = true; if ( ! data.pending.size ) { - this.log.debug('complete', type, Object.keys(data.timers)); + const keys = Object.keys(data.timers); + + this.log.debug('complete', type, keys); if ( data.success ) - this.emit(`:complete:${type}`); + this.emit(`:complete:${type}`, keys); this.pending_loads.delete(type); } } diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index 5b5dc397..98635679 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -1271,7 +1271,7 @@ export default class Chat extends Module { this.on('chat:get-tab-commands', event => { event.commands.push({ - name: 'ffz reload', + name: 'ffz:reload', description: this.i18n.t('chat.command.reload', 'Reload FFZ and add-on chat data (emotes, badges, etc.)'), permissionLevel: 0, ffz_group: 'FrankerFaceZ' @@ -1292,11 +1292,11 @@ export default class Chat extends Module { this.emit('chat:reload-data'); }); - this.on('load_tracker:complete:chat-data', () => { + this.on('load_tracker:complete:chat-data', (list) => { if ( this.triggered_reload ) { const sc = this.resolve('site.chat'); if ( sc?.addNotice ) - sc.addNotice('*', this.i18n.t('chat.command.reload.done', 'FFZ has finished reloading data.')); + sc.addNotice('*', this.i18n.t('chat.command.reload.done', 'FFZ has finished reloading data. (Sources: {list})', {list: list.join(', ')})); } this.triggered_reload = false; diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx index 2e627df1..a6d747d2 100644 --- a/src/modules/chat/tokenizers.jsx +++ b/src/modules/chat/tokenizers.jsx @@ -22,7 +22,7 @@ const SHRINK_X = MODIFIER_FLAGS.ShrinkX, const EMOTE_CLASS = 'chat-image chat-line__message--emote', //WHITESPACE = /^\s*$/, //LINK_REGEX = /([^\w@#%\-+=:~])?((?:(https?:\/\/)?(?:[\w@#%\-+=:~]+\.)+[a-z]{2,6}(?:\/[\w./@#%&()\-+=:?~]*)?))([^\w./@#%&()\-+=:?~]|\s|$)/g, - NEW_LINK_REGEX = /(?:(https?:\/\/)?((?:[\w#%\-+=:~]+\.)+[a-z]{2,10}(?:\/[\w./#%&@()\-+=:?~]*[^\.!,?])?))/g, + NEW_LINK_REGEX = /(?:(https?:\/\/)?((?:[\w#%\-+=:~]+\.)+[a-z]{2,10}(?:\/[\w./#%&@()\-+=:?~]*[^\s\.!,?])?))/g, //OLD_NEW_LINK_REGEX = /(?:(https?:\/\/)?((?:[\w#%\-+=:~]+\.)+[a-z]{2,10}(?:\/[\w./#%&@()\-+=:?~]*)?))/g, //MENTION_REGEX = /([^\w@#%\-+=:~])?(@([^\u0000-\u007F]+|\w+)+)([^\w./@#%&()\-+=:?~]|\s|$)/g; // eslint-disable-line no-control-regex MENTION_REGEX = /^(['"*([{<\\/]*)(@)((?:[^\u0000-\u007F]|[\w-])+)(?:\b|$)/; // eslint-disable-line no-control-regex diff --git a/src/modules/metadata.jsx b/src/modules/metadata.jsx index 32997324..f40307b1 100644 --- a/src/modules/metadata.jsx +++ b/src/modules/metadata.jsx @@ -361,6 +361,12 @@ export default class Metadata extends Module { } } + // Get the video element. + const video = maybe_call(player.getHTMLVideoElement, player); + stats.avOffset = 0; + if ( video?._ffz_context ) + stats.avOffset = (video._ffz_context_offset ?? 0) + video._ffz_context.currentTime - video.currentTime; + let tampered = false; try { const url = player.core.state.path; @@ -493,6 +499,14 @@ export default class Metadata extends Module { stats ); + const desync = data.avOffset !== 0 + ? (