1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-25 03:58:30 +00:00
* API Added: Certain methods will now log warnings when in developer mode if being called incorrectly. For example, if an add-on registers new badge data without including its add-on ID as the data source. Expect these to be expanded over time.
* API Added: Add-on modules and sub-modules will all have an `addon_id` property containing their source add-on's ID, as well as an `addon_root` property with a reference to the add-on's root module.
* API Changed: The `ffz_user_class` property of chat messages should now be a `Set`, if it is defined.
* API Changed: Add-on module proxies are now cached.
This commit is contained in:
SirStendec 2023-11-03 14:40:58 -04:00
parent 04969cc57e
commit 71f347ab70
12 changed files with 202 additions and 45 deletions

View file

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

View file

@ -346,6 +346,9 @@ export default class AddonManager extends Module {
other[name] = null; other[name] = null;
} }
// Send off a signal for other modules to unload related data.
this.emit('addon:fully-unload', module.addon_id);
// Clean up the global reference. // Clean up the global reference.
if ( this.__modules[module.__path] === module ) if ( this.__modules[module.__path] === module )
delete this.__modules[module.__path]; /* = [ delete this.__modules[module.__path]; /* = [
@ -363,7 +366,7 @@ export default class AddonManager extends Module {
// Clean up all settings. // Clean up all settings.
for(const [key, def] of Array.from(this.settings.definitions.entries())) { for(const [key, def] of Array.from(this.settings.definitions.entries())) {
if ( def && def.__source === module.__path ) { if ( def && def.__source === module.addon_id ) {
this.settings.remove(key); this.settings.remove(key);
} }
} }

View file

@ -395,6 +395,14 @@ export default class Badges extends Module {
else else
store = badge.addon ? addon : ffz; store = badge.addon ? addon : ffz;
let name = badge.title;
let extra;
try {
extra = maybe_call(badge.tooltipExtra, this, null, badge);
} catch(err) { extra = null; }
if ( extra && !(extra instanceof Promise) )
name = name + extra;
const id = badge.base_id ?? key, const id = badge.base_id ?? key,
is_this = id === key; is_this = id === key;
let existing = addon_badges_by_id[id]; let existing = addon_badges_by_id[id];
@ -411,14 +419,14 @@ export default class Badges extends Module {
existing.versions.push({ existing.versions.push({
version: key, version: key,
name: badge.title, name,
color, color,
image: image1x, image: image1x,
styleImage: `url("${image1x}")` styleImage: `url("${image1x}")`
}); });
if ( is_this ) { if ( is_this ) {
existing.name = badge.title; existing.name = name;
existing.color = color; existing.color = color;
existing.image = image; existing.image = image;
existing.styleImage = `url("${image}")`; existing.styleImage = `url("${image}")`;
@ -429,7 +437,7 @@ export default class Badges extends Module {
id, id,
key, key,
provider: 'ffz', provider: 'ffz',
name: badge.title, name,
color, color,
image, image,
image1x, image1x,
@ -542,8 +550,9 @@ export default class Badges extends Module {
</div>); </div>);
} else if ( p === 'ffz' ) { } else if ( p === 'ffz' ) {
const badge = this.badges[d.id], const full_badge = this.badges[d.id],
extra = maybe_call(badge?.tooltipExtra, this, ds, d, target, tip); badge = d.badge,
extra = maybe_call(badge?.tooltipExtra ?? full_badge?.tooltipExtra, this, ds, badge, target, tip);
if ( extra instanceof Promise ) { if ( extra instanceof Promise ) {
promises = true; promises = true;
@ -582,33 +591,55 @@ export default class Badges extends Module {
// Add-On Proxy // Add-On Proxy
// ======================================================================== // ========================================================================
getAddonProxy(module) { getAddonProxy(addon_id, addon, module) {
const path = module.__path; if ( ! addon_id )
if ( ! path.startsWith('addon.') )
return this; return this;
const addon_id = path.slice(6); const is_dev = addon?.dev ?? false;
const loadBadgeData = (badge_id, data, ...args) => { const overrides = {};
overrides.loadBadgeData = (badge_id, data, ...args) => {
if ( data && data.addon === undefined ) if ( data && data.addon === undefined )
data.addon = addon_id; data.addon = addon_id;
return this.loadBadgeData(badge_id, data, ...args); return this.loadBadgeData(badge_id, data, ...args);
}; };
const handler = { if ( is_dev ) {
overrides.setBulk = (source, ...args) => {
if ( ! source.includes(addon_id) )
module.log.warn('[DEV-CHECK] Call to badges.setBulk did not include addon ID in source:', source);
return this.setBulk(source, ...args);
};
overrides.deleteBulk = (source, ...args) => {
if ( ! source.includes(addon_id) )
module.log.warn('[DEV-CHECK] Call to badges.deleteBulk did not include addon ID in source:', source);
return this.deleteBulk(source, ...args);
}
overrides.extendBulk = (source, ...args) => {
if ( ! source.includes(addon_id) )
module.log.warn('[DEV-CHECK] Call to badges.extendBulk did not include addon ID in source:', source);
return this.extendBulk(source, ...args);
}
}
return new Proxy(this, {
get(obj, prop) { get(obj, prop) {
if ( prop === 'loadBadgeData' ) const thing = overrides[prop];
return loadBadgeData; if ( thing )
return thing;
return Reflect.get(...arguments); return Reflect.get(...arguments);
} }
}; });
return new Proxy(this, handler);
} }
getBadgeData(target) { getBadgeData(target) {
let container = target.parentElement?.parentElement; let container = target.parentElement?.parentElement;
if ( ! container?.dataset?.roomId ) if ( ! container?.dataset?.roomId )
@ -860,6 +891,7 @@ export default class Badges extends Module {
bd = { bd = {
provider: 'ffz', provider: 'ffz',
id: badge.id, id: badge.id,
badge,
image: bu[4] || bu[2] || bu[1], image: bu[4] || bu[2] || bu[1],
color: badge.color || full_badge.color, color: badge.color || full_badge.color,
title: badge.title || full_badge.title, title: badge.title || full_badge.title,
@ -1165,17 +1197,22 @@ export default class Badges extends Module {
data.click_url = 'https://www.frankerfacez.com/subscribe'; data.click_url = 'https://www.frankerfacez.com/subscribe';
if ( ! data.addon && (data.name === 'subwoofer') ) if ( ! data.addon && (data.name === 'subwoofer') )
data.tooltipExtra = async data => { data.tooltipExtra = data => {
const d = await this.getSubwooferMonths(data.user_id); if ( ! data?.user_id )
if ( ! d?.months ) return null;
return;
if ( d.lifetime ) return this.getSubwooferMonths(data.user_id)
return '\n' + this.i18n.t('badges.subwoofer.lifetime', 'Lifetime Subwoofer'); .then(d => {
if ( ! d?.months )
return;
return '\n' + this.i18n.t('badges.subwoofer.months', '({count, plural, one {# Month} other {# Months}})', { if ( d.lifetime )
count: d.months return '\n' + this.i18n.t('badges.subwoofer.lifetime', 'Lifetime Subwoofer');
});
return '\n' + this.i18n.t('badges.subwoofer.months', '({count, plural, one {# Month} other {# Months}})', {
count: d.months
});
})
}; };
} }

View file

@ -599,6 +599,54 @@ export default class Emotes extends Module {
this.animLeave = this.animLeave.bind(this); this.animLeave = this.animLeave.bind(this);
} }
getAddonProxy(addon_id, addon, module) {
if ( ! addon_id )
return this;
const overrides = {};
if ( addon?.dev ) {
overrides.addDefaultSet = (provider, ...args) => {
if ( ! provider.includes(addon_id) )
module.log.warn('[DEV-CHECK] Call to emotes.addDefaultSet did not include addon ID in provider:', provider);
return this.addDefaultSet(provider, ...args);
}
overrides.removeDefaultSet = (provider, ...args) => {
if ( ! provider.includes(addon_id) )
module.log.warn('[DEV-CHECK] Call to emotes.removeDefaultSet did not include addon ID in provider:', provider);
return this.removeDefaultSet(provider, ...args);
}
overrides.addSubSet = (provider, ...args) => {
if ( ! provider.includes(addon_id) )
module.log.warn('[DEV-CHECK] Call to emotes.addSubSet did not include addon ID in provider:', provider);
return this.addSubSet(provider, ...args);
}
overrides.removeSubSet = (provider, ...args) => {
if ( ! provider.includes(addon_id) )
module.log.warn('[DEV-CHECK] Call to emotes.removeSubSet did not include addon ID in provider:', provider);
return this.removeSubSet(provider, ...args);
}
}
return new Proxy(this, {
get(obj, prop) {
const thing = overrides[prop];
if ( thing )
return thing;
return Reflect.get(...arguments);
}
});
}
onEnable() { onEnable() {
this.style = new ManagedStyle('emotes'); this.style = new ManagedStyle('emotes');
this.effect_style = new ManagedStyle('effects'); this.effect_style = new ManagedStyle('effects');

View file

@ -557,6 +557,31 @@ export default class Metadata extends Module {
} }
} }
getAddonProxy(addon_id) {
if ( ! addon_id )
return this;
const overrides = {};
overrides.define = (key, definition) => {
if ( definition )
definition.__source = addon_id;
return this.define(key, definition);
};
return new Proxy(this, {
get(obj, prop) {
const thing = overrides[prop];
if ( thing )
return thing;
return Reflect.get(...arguments);
}
});
}
onEnable() { onEnable() {
const md = this.tooltips.types.metadata = target => { const md = this.tooltips.types.metadata = target => {
let el = target; let el = target;
@ -592,6 +617,19 @@ export default class Metadata extends Module {
opts.modifiers.flip = {behavior: ['bottom','top']}; opts.modifiers.flip = {behavior: ['bottom','top']};
return opts; return opts;
} }
this.on('addon:fully-unload', addon_id => {
const removed = new Set;
for(const [key,def] of Object.entries(this.definitions)) {
if ( def?.__source === addon_id ) {
removed.add(key);
this.definitions[key] = undefined;
}
}
if ( removed.size )
this.updateMetadata([...removed]);
});
} }

View file

@ -997,22 +997,23 @@ export default class SettingsManager extends Module {
// Add-On Proxy // Add-On Proxy
// ======================================================================== // ========================================================================
getAddonProxy(module) { getAddonProxy(addon_id) {
const path = module.__path; if ( ! addon_id )
return this;
const add = (key, definition) => { const add = (key, definition) => {
return this.add(key, definition, path); return this.add(key, definition, addon_id);
} }
const addUI = (key, definition) => { const addUI = (key, definition) => {
return this.addUI(key, definition, path); return this.addUI(key, definition, addon_id);
} }
const addClearable = (key, definition) => { const addClearable = (key, definition) => {
return this.addClearable(key, definition, path); return this.addClearable(key, definition, addon_id);
} }
const handler = { return new Proxy(this, {
get(obj, prop) { get(obj, prop) {
if ( prop === 'add' ) if ( prop === 'add' )
return add; return add;
@ -1022,9 +1023,7 @@ export default class SettingsManager extends Module {
return addClearable; return addClearable;
return Reflect.get(...arguments); return Reflect.get(...arguments);
} }
} });
return new Proxy(this, handler);
} }
// ======================================================================== // ========================================================================

View file

@ -100,7 +100,9 @@ export default class Line extends Module {
const override_name = t.overrides.getName(user.id); const override_name = t.overrides.getName(user.id);
let user_class = msg.ffz_user_class; let user_class = msg.ffz_user_class;
if ( Array.isArray(user_class) ) if ( user_class instanceof Set )
user_class = [...user_class].join(' ');
else if ( Array.isArray(user_class) )
user_class = user_class.join(' '); user_class = user_class.join(' ');
const user_props = { const user_props = {

View file

@ -1711,14 +1711,14 @@ export default class PlayerBase extends Module {
clearTimeout(timer); clearTimeout(timer);
ctx.removeEventListener('statechange', evt); ctx.removeEventListener('statechange', evt);
if (ctx.state === 'suspended') { if (ctx.state === 'suspended') {
this.log.info('Aborting due to browser auto-play policy.'); this.log.debug('Aborting due to browser auto-play policy.');
return; return;
} }
this.createCompressor(inst, video, comp); this.createCompressor(inst, video, comp);
} }
this.log.info('Attempting to resume suspended AudioContext.'); this.log.debug('Attempting to resume suspended AudioContext.');
timer = setTimeout(evt, 100); timer = setTimeout(evt, 100);
try { try {
ctx.addEventListener('statechange', evt); ctx.addEventListener('statechange', evt);

View file

@ -969,7 +969,9 @@ other {# messages were deleted by a moderator.}
override_name = t.overrides.getName(user.id); override_name = t.overrides.getName(user.id);
let user_class = msg.ffz_user_class; let user_class = msg.ffz_user_class;
if ( Array.isArray(user_class) ) if ( user_class instanceof Set )
user_class = [...user_class].join(' ');
else if ( Array.isArray(user_class) )
user_class = user_class.join(' '); user_class = user_class.join(' ');
const user_props = { const user_props = {

View file

@ -280,7 +280,9 @@ export default class VideoChatHook extends Module {
const override_name = t.overrides.getName(user.id); const override_name = t.overrides.getName(user.id);
let user_class = msg.ffz_user_class; let user_class = msg.ffz_user_class;
if ( Array.isArray(user_class) ) if ( user_class instanceof Set )
user_class = [...user_class].join(' ');
else if ( Array.isArray(user_class) )
user_class = user_class.join(' '); user_class = user_class.join(' ');
const user_props = { const user_props = {

View file

@ -1,9 +1,18 @@
import Module from 'utilities/module'; import Module from 'utilities/module';
const EXTRACTOR = /^addon\.([^.]+)(?:\.|$)/i;
function extractAddonId(path) {
const match = EXTRACTOR.exec(path);
if ( match )
return match[1];
}
export class Addon extends Module { export class Addon extends Module {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
this.addon_id = extractAddonId(this.__path);
this.addon_root = this; this.addon_root = this;
this.inject('i18n'); this.inject('i18n');

View file

@ -37,7 +37,10 @@ export class Module extends EventEmitter {
this.__modules = parent ? parent.__modules : {}; this.__modules = parent ? parent.__modules : {};
this.children = {}; this.children = {};
this.addon_root = parent ? parent.addon_root : null; if ( parent?.addon_id ) {
this.addon_id = parent.addon_id;
this.addon_root = parent.addon_root;
}
if ( parent && ! parent.children[this.name] ) if ( parent && ! parent.children[this.name] )
parent.children[this.name] = this; parent.children[this.name] = this;
@ -547,8 +550,22 @@ export class Module extends EventEmitter {
__processModule(module, name) { __processModule(module, name) {
if ( this.addon_root && module.getAddonProxy ) if ( this.addon_root && module.getAddonProxy ) {
return module.getAddonProxy(this.addon_root, this); const addon_id = this.addon_id;
if ( ! module.__proxies )
module.__proxies = {};
if ( module.__proxies[addon_id] )
return module.__proxies[addon_id];
const addon = this.resolve('addons')?.getAddon?.(addon_id),
out = module.getAddonProxy(addon_id, addon, this.addon_root, this);
if ( out !== module )
module.__proxies[addon_id] = out;
return out;
}
return module; return module;
} }