mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.68.0
* API Added: New interface for filtering emotes, allowing add-ons to, for example, remove any emotes with names that are common words. This filtering completely removes all traces of an emote, leaving normal words behind in chat.
This commit is contained in:
parent
f89938ba5a
commit
659ce1c430
4 changed files with 233 additions and 13 deletions
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "frankerfacez",
|
"name": "frankerfacez",
|
||||||
"author": "Dan Salvato LLC",
|
"author": "Dan Salvato LLC",
|
||||||
"version": "4.67.2",
|
"version": "4.68.0",
|
||||||
"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",
|
||||||
|
|
|
@ -434,6 +434,9 @@ export default class Emotes extends Module {
|
||||||
this.EmoteTypes = EmoteTypes;
|
this.EmoteTypes = EmoteTypes;
|
||||||
this.ModifierFlags = MODIFIER_FLAGS;
|
this.ModifierFlags = MODIFIER_FLAGS;
|
||||||
|
|
||||||
|
this.filters = {};
|
||||||
|
this.__filters = [];
|
||||||
|
|
||||||
this.inject('i18n');
|
this.inject('i18n');
|
||||||
this.inject('settings');
|
this.inject('settings');
|
||||||
this.inject('experiments');
|
this.inject('experiments');
|
||||||
|
@ -611,6 +614,13 @@ export default class Emotes extends Module {
|
||||||
const overrides = {},
|
const overrides = {},
|
||||||
warnings = {};
|
warnings = {};
|
||||||
|
|
||||||
|
overrides.addFilter = (filter) => {
|
||||||
|
if ( filter )
|
||||||
|
filter.__source = addon_id;
|
||||||
|
|
||||||
|
return this.addFilter(filter);
|
||||||
|
}
|
||||||
|
|
||||||
overrides.addDefaultSet = (provider, set_id, data) => {
|
overrides.addDefaultSet = (provider, set_id, data) => {
|
||||||
if ( is_dev && ! id_checker.test(provider) )
|
if ( is_dev && ! id_checker.test(provider) )
|
||||||
module.log.warn('[DEV-CHECK] Call to emotes.addDefaultSet did not include addon ID in provider:', provider);
|
module.log.warn('[DEV-CHECK] Call to emotes.addDefaultSet did not include addon ID in provider:', provider);
|
||||||
|
@ -650,6 +660,18 @@ export default class Emotes extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( is_dev ) {
|
if ( is_dev ) {
|
||||||
|
overrides.removeFilter = (filter) => {
|
||||||
|
let type;
|
||||||
|
if ( typeof filter === 'string' ) type = filter;
|
||||||
|
else type = filter.type;
|
||||||
|
|
||||||
|
const existing = this.filters[type];
|
||||||
|
if ( existing && existing.__source !== addon_id )
|
||||||
|
module.log.warn('[DEV-CHECK] Removed un-owned emote filter with emotes.removeFilter:', type, ' owner:', existing.__source ?? 'ffz');
|
||||||
|
|
||||||
|
return this.removeFilter(filter);
|
||||||
|
}
|
||||||
|
|
||||||
overrides.removeDefaultSet = (provider, ...args) => {
|
overrides.removeDefaultSet = (provider, ...args) => {
|
||||||
if ( ! id_checker.test(provider) )
|
if ( ! id_checker.test(provider) )
|
||||||
module.log.warn('[DEV-CHECK] Call to emotes.removeDefaultSet did not include addon ID in provider:', provider);
|
module.log.warn('[DEV-CHECK] Call to emotes.removeDefaultSet did not include addon ID in provider:', provider);
|
||||||
|
@ -724,7 +746,7 @@ export default class Emotes extends Module {
|
||||||
if ( ! emote_set )
|
if ( ! emote_set )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const has_old = !! emote_set.emotes?.[emote.id];
|
const has_old = !! emote_set.emotes?.[emote.id] || !! emote_set.disabled_emotes?.[emote.id];
|
||||||
const processed = this.addEmoteToSet(set_id, emote);
|
const processed = this.addEmoteToSet(set_id, emote);
|
||||||
|
|
||||||
this.maybeNotifyChange(
|
this.maybeNotifyChange(
|
||||||
|
@ -768,6 +790,13 @@ export default class Emotes extends Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for(const [key, def] of Object.entries(this.filters)) {
|
||||||
|
if ( def?.__source === addon_id ) {
|
||||||
|
removed++;
|
||||||
|
this.removeFilter(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ( removed ) {
|
if ( removed ) {
|
||||||
this.log.debug(`Cleaned up ${removed} entries when unloading addon:`, addon_id);
|
this.log.debug(`Cleaned up ${removed} entries when unloading addon:`, addon_id);
|
||||||
// TODO: Debounced retokenize all chat messages.
|
// TODO: Debounced retokenize all chat messages.
|
||||||
|
@ -778,6 +807,128 @@ export default class Emotes extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Emote Filtering
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
addFilter(filter, should_update = true) {
|
||||||
|
const type = filter.type;
|
||||||
|
if ( has(this.filters, type) ) {
|
||||||
|
this.log.warn(`Tried adding emote filter of type '${type}' when one was already present.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.filters[type] = filter;
|
||||||
|
if ( filter.priority == null )
|
||||||
|
filter.priotiy = 0;
|
||||||
|
|
||||||
|
this.__filters.push(filter);
|
||||||
|
this.__filters.sort((a, b) => {
|
||||||
|
if ( a.priority > b.priority ) return -1;
|
||||||
|
if ( a.priority < b.priority ) return 1;
|
||||||
|
return a.type < b.type;
|
||||||
|
});
|
||||||
|
|
||||||
|
if ( should_update )
|
||||||
|
this.updateFiltered();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFilter(filter, should_update = true) {
|
||||||
|
let type;
|
||||||
|
if ( typeof filter === 'string' ) type = filter;
|
||||||
|
else type = filter.type;
|
||||||
|
|
||||||
|
filter = this.filters[type];
|
||||||
|
if ( ! filter )
|
||||||
|
return null;
|
||||||
|
|
||||||
|
delete this.filters[type];
|
||||||
|
|
||||||
|
const idx = this.__filters.indexOf(filter);
|
||||||
|
if ( idx !== -1 ) {
|
||||||
|
this.__filters.splice(idx, 1);
|
||||||
|
if ( should_update )
|
||||||
|
this.updateFiltered();
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldFilterEmote(emote, set, set_id) {
|
||||||
|
for(const filter of this.__filters)
|
||||||
|
if ( filter.test(emote, set, set_id) )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFiltered() {
|
||||||
|
// Iterate over every emote set, updating filtered emotes.
|
||||||
|
for(const set of Object.values(this.emote_sets)) {
|
||||||
|
|
||||||
|
const emotes = {},
|
||||||
|
filtered = {};
|
||||||
|
|
||||||
|
let count = 0,
|
||||||
|
fcount = 0,
|
||||||
|
changed = false;
|
||||||
|
|
||||||
|
for(const em of Object.values(set.emotes)) {
|
||||||
|
if ( ! this.shouldFilterEmote(em, set, set.id) ) {
|
||||||
|
emotes[em.id] = em;
|
||||||
|
count++;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
filtered[em.id] = em;
|
||||||
|
fcount++;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const em of Object.values(set.disabled_emotes)) {
|
||||||
|
if ( this.shouldFilterEmote(em, set, set.id)) {
|
||||||
|
filtered[em.id] = em;
|
||||||
|
fcount++;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
emotes[em.id] = em;
|
||||||
|
count++;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! changed )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Save the changes.
|
||||||
|
this.log.info(`Filtered emote set #${set.id}: ${set.title} (total: ${count+fcount}, old: ${set.disabled_count}, new: ${fcount})`);
|
||||||
|
|
||||||
|
set.emotes = emotes;
|
||||||
|
set.count = count;
|
||||||
|
set.disabled_emotes = filtered;
|
||||||
|
set.disabled_count = fcount;
|
||||||
|
|
||||||
|
// Update the CSS for the set.
|
||||||
|
const css = [];
|
||||||
|
for(const em of Object.values(emotes)) {
|
||||||
|
const emote_css = this.generateEmoteCSS(em);
|
||||||
|
if ( emote_css?.length )
|
||||||
|
css.push(emote_css);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.style && (css.length || set.css) )
|
||||||
|
this.style.set(`es--${set.id}`, css.join('') + (set.css || ''));
|
||||||
|
else if ( css.length )
|
||||||
|
set.pending_css = css.join('');
|
||||||
|
|
||||||
|
// And emit an event because this emote set changed.
|
||||||
|
this.emit(':loaded', set.id, set);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Summary, maybe? Or update chat? Who knows?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Chat Notices
|
// Chat Notices
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
@ -1247,7 +1398,7 @@ export default class Emotes extends Module {
|
||||||
|
|
||||||
} else if ( provider === 'ffz' ) {
|
} else if ( provider === 'ffz' ) {
|
||||||
const emote_set = this.emote_sets[ds.set],
|
const emote_set = this.emote_sets[ds.set],
|
||||||
emote = emote_set && emote_set.emotes[ds.id];
|
emote = emote_set && (emote_set.emotes[ds.id] || emote_set.disabled_emotes?.[ds.id]);
|
||||||
|
|
||||||
if ( ! emote )
|
if ( ! emote )
|
||||||
return;
|
return;
|
||||||
|
@ -1283,7 +1434,7 @@ export default class Emotes extends Module {
|
||||||
|
|
||||||
} else if ( provider === 'ffz' ) {
|
} else if ( provider === 'ffz' ) {
|
||||||
const emote_set = this.emote_sets[ds.set],
|
const emote_set = this.emote_sets[ds.set],
|
||||||
emote = emote_set && emote_set.emotes[ds.id];
|
emote = emote_set && (emote_set.emotes[ds.id] || emote_set.disabled_emotes?.[ds.id]);
|
||||||
|
|
||||||
if ( ! emote )
|
if ( ! emote )
|
||||||
return;
|
return;
|
||||||
|
@ -1964,19 +2115,68 @@ export default class Emotes extends Module {
|
||||||
if ( ! processed )
|
if ( ! processed )
|
||||||
throw new Error("Invalid emote data object.");
|
throw new Error("Invalid emote data object.");
|
||||||
|
|
||||||
|
const is_disabled = this.shouldFilterEmote(processed, set, set_id);
|
||||||
|
|
||||||
|
// Possible logic paths:
|
||||||
|
// 1. No old emote. New emote accepted.
|
||||||
|
// 2. No old emote. New emote disabled.
|
||||||
|
// 3. Old emote. New emote accepted.
|
||||||
|
// 4. Old emote. New emote disabled.
|
||||||
|
// 5. Old emote disabled. New emote accepted.
|
||||||
|
// 6. Old emote disabled. New emote disabled.
|
||||||
|
|
||||||
|
// Are we removing a disabled emote?
|
||||||
|
let removed = set.disabled_emotes[processed.id];
|
||||||
|
if ( removed ) {
|
||||||
|
delete set.disabled_emotes[processed.id];
|
||||||
|
set.disabled_count--;
|
||||||
|
}
|
||||||
|
|
||||||
// Are we removing an existing emote?
|
// Are we removing an existing emote?
|
||||||
const old_emote = set.emotes[processed.id],
|
const old_emote = set.emotes[processed.id],
|
||||||
old_css = old_emote && this.generateEmoteCSS(old_emote);
|
old_css = old_emote && this.generateEmoteCSS(old_emote);
|
||||||
|
|
||||||
// Store the emote.
|
// Store the emote.
|
||||||
set.emotes[processed.id] = processed;
|
if ( is_disabled ) {
|
||||||
if ( ! old_emote )
|
set.disabled_emotes[processed.id] = processed;
|
||||||
set.count++;
|
set.disabled_count++;
|
||||||
|
|
||||||
|
// If there was an old emote, we need to decrement
|
||||||
|
// the use count and remove it from the emote list.
|
||||||
|
if ( old_emote ) {
|
||||||
|
const new_emotes = {};
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for(const em of Object.values(set.emotes)) {
|
||||||
|
if ( em.id == processed.id )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
new_emotes[em.id] = em;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
set.emotes = new_emotes;
|
||||||
|
set.count = count;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// If there was no old emote, then we can stop now.
|
||||||
|
return processed;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Not disabled. This is a live emote.
|
||||||
|
set.emotes[processed.id] = processed;
|
||||||
|
|
||||||
|
// If there was no old emote, update the set count.
|
||||||
|
if ( ! old_emote )
|
||||||
|
set.count++;
|
||||||
|
}
|
||||||
|
|
||||||
// Now we need to update the CSS. If we had old emote CSS, then we
|
// Now we need to update the CSS. If we had old emote CSS, then we
|
||||||
// will need to totally rebuild the CSS.
|
// will need to totally rebuild the CSS.
|
||||||
const style_key = `es--${set_id}`;
|
const style_key = `es--${set_id}`;
|
||||||
|
|
||||||
|
// Rebuild the full CSS if we have an old emote.
|
||||||
if ( old_css && old_css.length ) {
|
if ( old_css && old_css.length ) {
|
||||||
const css = [];
|
const css = [];
|
||||||
for(const em of Object.values(set.emotes)) {
|
for(const em of Object.values(set.emotes)) {
|
||||||
|
@ -1990,7 +2190,9 @@ export default class Emotes extends Module {
|
||||||
else if ( css.length )
|
else if ( css.length )
|
||||||
set.pending_css = css.join('');
|
set.pending_css = css.join('');
|
||||||
|
|
||||||
} else {
|
} else if ( ! is_disabled ) {
|
||||||
|
// If there wasn't an old emote, only add our CSS if the emote
|
||||||
|
// isn't disabled.
|
||||||
const emote_css = this.generateEmoteCSS(processed);
|
const emote_css = this.generateEmoteCSS(processed);
|
||||||
if ( emote_css && emote_css.length ) {
|
if ( emote_css && emote_css.length ) {
|
||||||
if ( this.style )
|
if ( this.style )
|
||||||
|
@ -2016,6 +2218,15 @@ export default class Emotes extends Module {
|
||||||
if ( emote_id && emote_id.id )
|
if ( emote_id && emote_id.id )
|
||||||
emote_id = emote_id.id;
|
emote_id = emote_id.id;
|
||||||
|
|
||||||
|
// If the emote was present but disabled, just return it
|
||||||
|
// without having to do most of our logic.
|
||||||
|
const removed = set.disabled_emotes?.[emote_id];
|
||||||
|
if ( removed ) {
|
||||||
|
set.disabled_count--;
|
||||||
|
delete set.disabled_emotes[emote_id];
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
const emote = set.emotes[emote_id];
|
const emote = set.emotes[emote_id];
|
||||||
if ( ! emote )
|
if ( ! emote )
|
||||||
return;
|
return;
|
||||||
|
@ -2074,9 +2285,11 @@ export default class Emotes extends Module {
|
||||||
|
|
||||||
this.emote_sets[set_id] = data;
|
this.emote_sets[set_id] = data;
|
||||||
|
|
||||||
let count = 0;
|
let count = 0,
|
||||||
|
fcount = 0;
|
||||||
const ems = data.emotes || data.emoticons,
|
const ems = data.emotes || data.emoticons,
|
||||||
new_ems = data.emotes = {},
|
new_ems = data.emotes = {},
|
||||||
|
filtered = data.disabled_emotes = {},
|
||||||
css = [];
|
css = [];
|
||||||
|
|
||||||
data.id = set_id;
|
data.id = set_id;
|
||||||
|
@ -2091,6 +2304,12 @@ export default class Emotes extends Module {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( this.shouldFilterEmote(processed, data, set_id) ) {
|
||||||
|
filtered[processed.id] = processed;
|
||||||
|
fcount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const emote_css = this.generateEmoteCSS(processed);
|
const emote_css = this.generateEmoteCSS(processed);
|
||||||
if ( emote_css )
|
if ( emote_css )
|
||||||
css.push(emote_css);
|
css.push(emote_css);
|
||||||
|
@ -2103,6 +2322,7 @@ export default class Emotes extends Module {
|
||||||
this.log.warn(`Bad Emote Data for Set #${set_id}`, bad_emotes);
|
this.log.warn(`Bad Emote Data for Set #${set_id}`, bad_emotes);
|
||||||
|
|
||||||
data.count = count;
|
data.count = count;
|
||||||
|
data.disabled_count = fcount;
|
||||||
|
|
||||||
if ( this.style && (css.length || data.css) )
|
if ( this.style && (css.length || data.css) )
|
||||||
this.style.set(`es--${set_id}`, css.join('') + (data.css || ''));
|
this.style.set(`es--${set_id}`, css.join('') + (data.css || ''));
|
||||||
|
@ -2110,7 +2330,7 @@ export default class Emotes extends Module {
|
||||||
data.pending_css = css.join('');
|
data.pending_css = css.join('');
|
||||||
|
|
||||||
if ( ! suppress_log )
|
if ( ! suppress_log )
|
||||||
this.log.info(`Loaded emote set #${set_id}: ${data.title} (${count} emotes)`);
|
this.log.info(`Loaded emote set #${set_id}: ${data.title} (${count} emotes, ${fcount} filtered)`);
|
||||||
|
|
||||||
this.emit(':loaded', set_id, data);
|
this.emit(':loaded', set_id, data);
|
||||||
|
|
||||||
|
|
|
@ -1509,7 +1509,7 @@ export const AddonEmotes = {
|
||||||
</span>);
|
</span>);
|
||||||
|
|
||||||
const emote_set = this.emotes.emote_sets[set_id],
|
const emote_set = this.emotes.emote_sets[set_id],
|
||||||
emote = emote_set && emote_set.emotes[emote_id];
|
emote = emote_set && (emote_set.emotes[emote_id] || emote_set.disabled_emotes?.[emote_id]);
|
||||||
|
|
||||||
if ( emote )
|
if ( emote )
|
||||||
return (<span class="tw-mg-05">
|
return (<span class="tw-mg-05">
|
||||||
|
@ -1574,7 +1574,7 @@ export const AddonEmotes = {
|
||||||
|
|
||||||
} else if ( provider === 'ffz' ) {
|
} else if ( provider === 'ffz' ) {
|
||||||
const emote_set = this.emotes.emote_sets[ds.set],
|
const emote_set = this.emotes.emote_sets[ds.set],
|
||||||
emote = emote_set && emote_set.emotes[ds.id];
|
emote = emote_set && (emote_set.emotes[ds.id] || emote_set.disabled_emotes?.[ds.id]);
|
||||||
|
|
||||||
if ( emote_set ) {
|
if ( emote_set ) {
|
||||||
source = emote_set.source_line || (`${emote_set.source || 'FFZ'} ${emote_set.title || 'Global'}`);
|
source = emote_set.source_line || (`${emote_set.source || 'FFZ'} ${emote_set.title || 'Global'}`);
|
||||||
|
|
|
@ -396,7 +396,7 @@ export default class EmoteCard extends Module {
|
||||||
|
|
||||||
// Try to get the emote set.
|
// Try to get the emote set.
|
||||||
const emote_set = this.emotes.emote_sets[emote.set],
|
const emote_set = this.emotes.emote_sets[emote.set],
|
||||||
data = emote_set?.emotes?.[emote.id];
|
data = emote_set ? emote_set.emotes?.[emote.id] || emote_set.disabled_emotes?.[emote.id] : null;
|
||||||
|
|
||||||
if ( ! data )
|
if ( ! data )
|
||||||
throw new Error('Unable to load emote data');
|
throw new Error('Unable to load emote data');
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue