1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-09-16 18:06:55 +00:00
* Fixed: FFZ failing to load correctly due to an issue with a pubsub module.
* Fixed: The Subtember banner not being hidden.
* Fixed: Some themed elements not having their colors correctly changed.
* API Changed: Introduce way for the user-script to register a settings provider for cross-origin storage of settings.
* API Changed: Partial work for registering a settings provider for extension storage.
This commit is contained in:
SirStendec 2025-09-02 14:52:52 -04:00
parent 6e49b689e9
commit b62e2f7530
10 changed files with 196 additions and 127 deletions

View file

@ -1,7 +1,7 @@
{
"name": "frankerfacez",
"author": "Dan Salvato LLC",
"version": "4.77.12",
"version": "4.78.0",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"private": true,
"license": "Apache-2.0",

View file

@ -2,6 +2,7 @@
'use strict';
(() => {
const browser = globalThis.browser ?? globalThis.chrome;
const is_firefox = (typeof browser === 'object' && browser.runtime.getURL('').startsWith('moz'));
if (
// Don't run on certain sub-domains.
@ -21,6 +22,7 @@
}
document.body.dataset.ffzSource = 'extension';
document.body.dataset.ffzExtension = browser.runtime.id;
// Make sure to wake the service worker up early.
browser.runtime.sendMessage({
@ -29,75 +31,34 @@
// Set up a bridge for connections, since Firefox
// doesn't support externally_connectable.
const connections = new Map;
function handleConnect(id) {
if ( connections.has(id) )
return;
const port = browser.runtime.connect();
connections.set(id, port);
if (is_firefox) {
const port = browser.runtime.connect({ name: 'ffz-cs-bridge' });
port.onMessage.addListener(msg => {
window.postMessage({
type: 'ffz-con-message',
id,
payload: msg
})
ffz_from_worker: true,
...msg
}, window.location.origin);
});
port.onDisconnect.addListener(() => {
connections.delete(id);
window.postMessage({
type: 'ffz-con-disconnect',
id
});
});
}
function handleDisconnect(id) {
const port = connections.get(id);
if ( port ) {
connections.delete(id);
port.disconnect();
}
}
function handleMessage(id, payload) {
const port = connection.get(id);
if ( port ) {
port.postMessage(payload);
}
}
window.addEventListener('message', evt => {
if (evt.source !== window || ! evt.data )
if (evt.source !== window || ! evt.data?.ffz_to_worker )
return;
const { type, id, payload } = evt.data;
if ( type === 'ffz-con-connect' )
handleConnect(id);
else if ( type === 'ffz-con-message' )
handleMessage(id, payload);
else if ( type === 'ffz-con-disconnect' )
handleDisconnect(id);
port.postMessage(evt.data);
});
}
// Let the extension send messages to the page directly.
browser.runtime.onMessage.addListener((msg, sender) => {
if (msg?.type === 'ffz_to_page')
window.postMessage(msg.data, '*');
browser.runtime.onMessage.addListener(msg => {
if (msg?.ffz_from_worker)
window.postMessage(msg, window.location.origin);
return false;
});
// Now, inject our script into the page context.
const HOST = location.hostname,
SERVER = browser.runtime.getURL("web"),
SERVER = browser.runtime.getURL('web'),
script = document.createElement('script');
let FLAVOR =

View file

@ -38,6 +38,7 @@ import * as Utility_Time from 'utilities/time';
import * as Utility_Tooltip from 'utilities/tooltip';
import * as Utility_I18n from 'utilities/translation-core';
import * as Utility_Filtering from 'utilities/filtering';
import { installPort } from './utilities/extension_port';
class FrankerFaceZ extends Module {
@ -128,6 +129,9 @@ class FrankerFaceZ extends Module {
// Core Systems
// ========================================================================
if (Utility_Constants.EXTENSION)
installPort(this);
this.inject('settings', SettingsManager);
this.inject('experiments', ExperimentManager);
this.inject('i18n', TranslationManager);

View file

@ -19,8 +19,9 @@ import * as CLEARABLES from './clearables';
import type { SettingsProfileMetadata, ContextData, ExportedFullDump, SettingsClearable, SettingDefinition, SettingProcessor, SettingUiDefinition, SettingValidator, SettingType, ExportedBlobMetadata, SettingsKeys, AllSettingsKeys, ConcreteLocalStorageData } from './types';
import type { FilterType } from 'utilities/filtering';
import { AdvancedSettingsProvider, IndexedDBProvider, LocalStorageProvider, Providers, type SettingsProvider } from './providers';
import { AdvancedSettingsProvider, IndexedDBProvider, LocalStorageProvider, Providers, SettingsProvider } from './providers';
import type { AddonInfo, SettingsTypeMap } from 'utilities/types';
import { FFZEvent } from '../utilities/events';
export {parse as parse_path} from 'utilities/path-parser';
@ -42,6 +43,21 @@ export const NO_SYNC_KEYS = ['session'];
// Registration
// ============================================================================
type FFZProviderConstructorEvent = {
settings: SettingsManager;
Provider: typeof SettingsProvider;
AdvancedProvider: typeof AdvancedSettingsProvider;
registerProvider: (key: string, provider: typeof SettingsProvider) => void;
};
type FFZProviderConstructor = (evt: FFZProviderConstructorEvent) => void;
declare global {
interface Window {
ffz_providers?: FFZProviderConstructor[];
}
}
declare module 'utilities/types' {
interface ModuleEventMap {
settings: SettingsEvents;
@ -147,10 +163,33 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
this.providers = {};
for(const [key, provider] of Object.entries(Providers)) {
if ( provider.supported() )
if ( provider.supported(this) )
this.providers[key] = provider;
}
// Load any dynamic providers that have been registered.
// Now that we're here, no further providers can be registered, so seal them.
window.ffz_providers = window.ffz_providers || [];
Object.seal(window.ffz_providers);
if ( window.ffz_providers.length > 0 ) {
const evt = {
settings: this,
Provider: SettingsProvider,
AdvancedProvider: AdvancedSettingsProvider,
registerProvider: (key: string, provider: typeof SettingsProvider) => {
if ( ! this.providers[key] && provider.supported(this) )
this.providers[key] = provider;
}
};
for(const p of window.ffz_providers)
try {
p(evt);
} catch(err) {
this.log.error('Error while registering external settings provider:', err);
}
}
// This cannot be modified at a future time, as providers NEED
// to be ready very early in FFZ intitialization. Seal it.
Object.seal(this.providers);
@ -689,13 +728,35 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
* @returns {SettingsProvider} The provider to store everything.
*/
async _createProvider() {
// If we should be using Cross-Origin Storage Bridge, do so.
//if ( this.providers.cosb && this.providers.cosb.supported() )
// return new this.providers.cosb(this);
// There are a couple situations where we might want to ignore a
// pre-set provider and sniff anyways. These are situations where
// a user can't open the control center. So, the player embeds,
// clips pages, and embedded chat that's embedded within other sites.
let ignore_choice = false;
if ( (this.root as any).host === 'twitch' ) {
// Player or clips
if ( location.hostname.startsWith('player.') || location.hostname.startsWith('clips.') )
ignore_choice = true;
let wanted = localStorage.ffzProviderv2;
// Embedded stuff
else if ( location.pathname.startsWith('/embed/') && (
// ancestorOrigins > 0
( location.ancestorOrigins && location.ancestorOrigins.length )
||
// parent.location mismatch
( window.parent && window.parent.location !== location )
) )
ignore_choice = true;
}
let wanted;
if (ignore_choice)
wanted = await this.sniffProvider();
else {
wanted = localStorage.ffzProviderv2;
if ( wanted == null )
wanted = localStorage.ffzProviderv2 = await this.sniffProvider();
}
if ( this.providers[wanted] ) {
const provider = new (this.providers[wanted] as any)(this) as SettingsProvider;
@ -726,7 +787,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
);
for(const [key, provider] of providers) {
if ( provider.supported() && await provider.hasContent() ) // eslint-disable-line no-await-in-loop
if ( provider.supported(this) && await provider.hasContent(this) ) // eslint-disable-line no-await-in-loop
return key;
}

View file

@ -51,7 +51,7 @@ export abstract class SettingsProvider extends EventEmitter<ProviderEvents> {
static title: string;
static description: string;
static hasContent: () => OptionalPromise<boolean>;
static hasContent: (manager: SettingsManager) => OptionalPromise<boolean>;
manager: SettingsManager;
@ -70,7 +70,7 @@ export abstract class SettingsProvider extends EventEmitter<ProviderEvents> {
this.disabled = false;
}
static supported() {
static supported(manager: SettingsManager) {
return false;
}
@ -103,14 +103,14 @@ export abstract class SettingsProvider extends EventEmitter<ProviderEvents> {
): ProviderTypeMap[K];
abstract get<K extends keyof ProviderTypeMap>(
key: K
): ProviderTypeMap[K] | null;
): ProviderTypeMap[K] | undefined;
abstract get<T>(
key: Exclude<string, keyof ProviderTypeMap>,
default_value: T
): T;
abstract get<T>(
key: Exclude<string, keyof ProviderTypeMap>
): T | null;
): T | undefined;
abstract set<K extends keyof ProviderTypeMap>(key: K, value: ProviderTypeMap[K]): void;
abstract set<K extends string>(key: Exclude<K, keyof ProviderTypeMap>, value: unknown): void;
@ -230,7 +230,12 @@ export abstract class RemoteSettingsProvider extends AdvancedSettingsProvider {
// Provider Methods
get<T>(key: string, default_value?: T): T {
get<T>(key: string): T | undefined;
get<T>(key: string, default_value: T): T;
get<T>(
key: string,
default_value?: T
): T | undefined {
return this._cached.has(key)
? this._cached.get(key)
: default_value;
@ -412,9 +417,8 @@ export class LocalStorageProvider extends SettingsProvider {
return true;
}
static hasContent(prefix?: string) {
if ( ! prefix )
prefix = 'FFZ:setting:';
static hasContent() {
const prefix = 'FFZ:setting:';
for(const key in localStorage)
if ( key.startsWith(prefix) && has(localStorage, key) )
@ -432,9 +436,9 @@ export class LocalStorageProvider extends SettingsProvider {
private _boundHandleMessage?: ((event: MessageEvent) => void) | null;
private _boundHandleStorage?: ((event: StorageEvent) => void) | null;
constructor(manager: SettingsManager, prefix?: string) {
constructor(manager: SettingsManager) {
super(manager);
this.prefix = prefix = prefix == null ? 'FFZ:setting:' : prefix;
const prefix = this.prefix = 'FFZ:setting:';
const cache = this._cached = new Map,
len = prefix.length;
@ -564,10 +568,12 @@ export class LocalStorageProvider extends SettingsProvider {
}
}
get<T>(key: string): T | undefined;
get<T>(key: string, default_value: T): T;
get<T>(
key: string,
default_value?: T
): T {
): T | undefined {
return this._cached.has(key)
? this._cached.get(key)
: default_value;
@ -887,7 +893,12 @@ export class IndexedDBProvider extends AdvancedSettingsProvider {
// Synchronous Methods
get<T>(key: string, default_value?: T): T {
get<T>(key: string): T | undefined;
get<T>(key: string, default_value: T): T;
get<T>(
key: string,
default_value?: T
): T | undefined {
return this._cached.has(key)
? this._cached.get(key)
: default_value;
@ -1513,7 +1524,7 @@ export class ExtensionProvider extends RemoteSettingsProvider {
static supported() { return EXTENSION }
static hasContent() {
static hasContent(manager: SettingsManager) {
if ( ! ExtensionProvider.supported() )
return false;
@ -1523,20 +1534,12 @@ export class ExtensionProvider extends RemoteSettingsProvider {
let responded = false,
timeout: ReturnType<typeof setTimeout> | null = null ;
const listener = (evt: MessageEvent<any>) => {
if (evt.source !== window)
return;
if (evt.data && evt.data.type === 'ffz_from_ext') {
const msg = evt.data.data,
type = msg?.ffz_type;
if (type === 'has-keys') {
const listener = (msg: any) => {
if ( msg.type === 'has-keys' ) {
responded = true;
resolve(msg.value);
cleanup();
}
}
};
const cleanup = () => {
@ -1550,17 +1553,11 @@ export class ExtensionProvider extends RemoteSettingsProvider {
timeout = null;
}
window.removeEventListener('message', listener);
manager.off('ext:message', listener);
}
window.addEventListener('message', listener);
window.postMessage({
type: 'ffz_to_ext',
data: {
ffz_type: 'check-has-keys'
}
}, '*');
manager.on('ext:message', listener);
manager.emit('ext:message', { type: 'check-has-keys' });
timeout = setTimeout(cleanup, 1000);
});
@ -1578,8 +1575,8 @@ export class ExtensionProvider extends RemoteSettingsProvider {
constructor(manager: SettingsManager) {
super(manager);
this.onExtMessage = this.onExtMessage.bind(this);
window.addEventListener('message', this.onExtMessage);
manager.on('ext:message', this.handleMessage, this);
this.send('ready');
}
// Stuff
@ -1589,36 +1586,16 @@ export class ExtensionProvider extends RemoteSettingsProvider {
}
disableEvents() {
this.manager.off('ext:message', this.handleMessage, this);
}
// Communication
onExtMessage(evt: MessageEvent<any>) {
if (evt.source !== window)
return;
if (evt.data?.type === 'ffz_from_ext' && evt.data.data?.ffz_type)
this.handleMessage(evt.data.data);
}
send(msg: string | CorsMessage, transfer?: OptionalArray<Transferable>) {
if ( typeof msg === 'string' )
msg = {ffz_type: msg} as any;
try {
window.postMessage(
{
type: 'ffz_to_ext',
data: msg
},
'*',
transfer ? (Array.isArray(transfer) ? transfer : [transfer]) : undefined
);
} catch(err) {
this.manager.log.error('Error sending message to extension.', err, msg, transfer);
}
this.manager.emit('ext:message', msg);
}
}

View file

@ -23,6 +23,7 @@ declare module 'utilities/types' {
interface SettingsTypeMap {
'chat.hype.show-pinned': boolean;
'layout.turbo-cta': boolean;
'layout.subtember': boolean;
}
}
@ -95,6 +96,10 @@ export default class Loadable extends Module {
this.toggle('TopNav__TurboButton_Available', val);
});
this.settings.getChanges('layout.subtember', val => {
this.toggle('TokenizedCommerceBanner', val);
});
this.ErrorBoundaryComponent.ready((cls, instances) => {
this.log.debug('Found Error Boundary component wrapper.');

View file

@ -575,7 +575,7 @@ export default class ThemeEngine extends Module {
const selector = dark ? '' : `:not(.settings-menu-button-component--forced-dark-theme)`;
if ( bits.length )
this.css_tweaks.set('colors', `body,body .tw-root--theme-light,body .tw-root--theme-dark${selector} {${bits.join('\n')}}.channel-info-content .tw-accent-region,.channel-info-content div[class^="ScAccentRegion"]{${accent_bits.join('\n')}}`);
this.css_tweaks.set('colors', `body,body .tw-root--theme-light.tw-root--theme-light,body .tw-root--theme-dark.tw-root--theme-dark${selector} {${bits.join('\n')}}.channel-info-content .tw-accent-region,.channel-info-content div[class^="ScAccentRegion"]{${accent_bits.join('\n')}}`);
else
this.css_tweaks.delete('colors');
}

View file

@ -150,7 +150,12 @@ export default class Subpump extends Module<'site.subpump', SubpumpEvents> {
if ( instance ) {
this.instance = instance;
try {
this.hookClient(instance);
} catch(err) {
this.instance = null;
this.log.error('Error hooking PubSub client.', err);
}
}
/*

View file

@ -0,0 +1,54 @@
import Module from "./module";
type PortType = {
postMessage: (msg: any) => void;
}
export function installPort(module: Module) {
let port: PortType | null = null;
function initialize() {
try {
// Try connecting with externally_connectable first.
const cp = port = chrome.runtime.connect(document.body.dataset.ffzExtension!, {
name: 'ffz-ext-port'
});
cp.onMessage.addListener(msg => {
module.emit('ext:message', msg);
});
cp.onDisconnect.addListener(p => {
module.log.warn('Extension port disconnected.', (p as any)?.error ?? chrome.runtime.lastError);
port = null;
});
return;
} catch(err) {
module.log.info('Unable to connect using externally_connectable, falling back to bridge.');
}
window.addEventListener('message', evt => {
if ( evt.source !== window || ! evt.data || evt.data.ffz_from_worker )
return;
module.emit('ext:message', evt.data);
});
port = {
postMessage(msg) {
window.postMessage({
ffz_to_worker: true,
...msg
}, window.location.origin);
}
};
}
module.on('ext:post-message', msg => {
if ( ! port )
initialize();
port!.postMessage(msg);
});
}

View file

@ -60,7 +60,9 @@ export type ModuleEvents = {
':loaded': [module: GenericModule],
':unloaded': [module: GenericModule],
':enabled': [module: GenericModule],
':disabled': [module: GenericModule]
':disabled': [module: GenericModule],
'ext:message': [msg: any],
'ext:post-message': [msg: any]
};
export type GenericModule = Module<any, any, any, any>;