1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-02 16:08:31 +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",
"author": "Dan Salvato LLC",
"version": "4.57.1",
"version": "4.57.2",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"private": true,
"license": "Apache-2.0",

View file

@ -346,6 +346,9 @@ export default class AddonManager extends Module {
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.
if ( this.__modules[module.__path] === module )
delete this.__modules[module.__path]; /* = [
@ -363,7 +366,7 @@ export default class AddonManager extends Module {
// Clean up all settings.
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);
}
}

View file

@ -395,6 +395,14 @@ export default class Badges extends Module {
else
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,
is_this = id === key;
let existing = addon_badges_by_id[id];
@ -411,14 +419,14 @@ export default class Badges extends Module {
existing.versions.push({
version: key,
name: badge.title,
name,
color,
image: image1x,
styleImage: `url("${image1x}")`
});
if ( is_this ) {
existing.name = badge.title;
existing.name = name;
existing.color = color;
existing.image = image;
existing.styleImage = `url("${image}")`;
@ -429,7 +437,7 @@ export default class Badges extends Module {
id,
key,
provider: 'ffz',
name: badge.title,
name,
color,
image,
image1x,
@ -542,8 +550,9 @@ export default class Badges extends Module {
</div>);
} else if ( p === 'ffz' ) {
const badge = this.badges[d.id],
extra = maybe_call(badge?.tooltipExtra, this, ds, d, target, tip);
const full_badge = this.badges[d.id],
badge = d.badge,
extra = maybe_call(badge?.tooltipExtra ?? full_badge?.tooltipExtra, this, ds, badge, target, tip);
if ( extra instanceof Promise ) {
promises = true;
@ -582,33 +591,55 @@ export default class Badges extends Module {
// Add-On Proxy
// ========================================================================
getAddonProxy(module) {
const path = module.__path;
if ( ! path.startsWith('addon.') )
getAddonProxy(addon_id, addon, module) {
if ( ! addon_id )
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 )
data.addon = addon_id;
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) {
if ( prop === 'loadBadgeData' )
return loadBadgeData;
const thing = overrides[prop];
if ( thing )
return thing;
return Reflect.get(...arguments);
}
};
return new Proxy(this, handler);
});
}
getBadgeData(target) {
let container = target.parentElement?.parentElement;
if ( ! container?.dataset?.roomId )
@ -860,6 +891,7 @@ export default class Badges extends Module {
bd = {
provider: 'ffz',
id: badge.id,
badge,
image: bu[4] || bu[2] || bu[1],
color: badge.color || full_badge.color,
title: badge.title || full_badge.title,
@ -1165,17 +1197,22 @@ export default class Badges extends Module {
data.click_url = 'https://www.frankerfacez.com/subscribe';
if ( ! data.addon && (data.name === 'subwoofer') )
data.tooltipExtra = async data => {
const d = await this.getSubwooferMonths(data.user_id);
if ( ! d?.months )
return;
data.tooltipExtra = data => {
if ( ! data?.user_id )
return null;
if ( d.lifetime )
return '\n' + this.i18n.t('badges.subwoofer.lifetime', 'Lifetime Subwoofer');
return this.getSubwooferMonths(data.user_id)
.then(d => {
if ( ! d?.months )
return;
return '\n' + this.i18n.t('badges.subwoofer.months', '({count, plural, one {# Month} other {# Months}})', {
count: d.months
});
if ( d.lifetime )
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);
}
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() {
this.style = new ManagedStyle('emotes');
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() {
const md = this.tooltips.types.metadata = target => {
let el = target;
@ -592,6 +617,19 @@ export default class Metadata extends Module {
opts.modifiers.flip = {behavior: ['bottom','top']};
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
// ========================================================================
getAddonProxy(module) {
const path = module.__path;
getAddonProxy(addon_id) {
if ( ! addon_id )
return this;
const add = (key, definition) => {
return this.add(key, definition, path);
return this.add(key, definition, addon_id);
}
const addUI = (key, definition) => {
return this.addUI(key, definition, path);
return this.addUI(key, definition, addon_id);
}
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) {
if ( prop === 'add' )
return add;
@ -1022,9 +1023,7 @@ export default class SettingsManager extends Module {
return addClearable;
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);
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(' ');
const user_props = {

View file

@ -1711,14 +1711,14 @@ export default class PlayerBase extends Module {
clearTimeout(timer);
ctx.removeEventListener('statechange', evt);
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;
}
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);
try {
ctx.addEventListener('statechange', evt);

View file

@ -969,7 +969,9 @@ other {# messages were deleted by a moderator.}
override_name = t.overrides.getName(user.id);
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(' ');
const user_props = {

View file

@ -280,7 +280,9 @@ export default class VideoChatHook extends Module {
const override_name = t.overrides.getName(user.id);
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(' ');
const user_props = {

View file

@ -1,9 +1,18 @@
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 {
constructor(...args) {
super(...args);
this.addon_id = extractAddonId(this.__path);
this.addon_root = this;
this.inject('i18n');

View file

@ -37,7 +37,10 @@ export class Module extends EventEmitter {
this.__modules = parent ? parent.__modules : {};
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] )
parent.children[this.name] = this;
@ -547,8 +550,22 @@ export class Module extends EventEmitter {
__processModule(module, name) {
if ( this.addon_root && module.getAddonProxy )
return module.getAddonProxy(this.addon_root, this);
if ( this.addon_root && module.getAddonProxy ) {
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;
}