1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-28 05:15:54 +00:00
FrankerFaceZ/src/modules/chat/emotes.js

716 lines
18 KiB
JavaScript
Raw Normal View History

2017-11-13 01:23:39 -05:00
'use strict';
// ============================================================================
// Emote Handling and Default Provider
// ============================================================================
import Module from 'utilities/module';
import {ManagedStyle} from 'utilities/dom';
import {has, timeout, SourcedSet} from 'utilities/object';
import {CLIENT_ID, API_SERVER, IS_OSX} from 'utilities/constants';
2017-11-13 01:23:39 -05:00
const MOD_KEY = IS_OSX ? 'metaKey' : 'ctrlKey';
2017-11-13 01:23:39 -05:00
const EXTRA_INVENTORY = [33563];
2017-11-13 01:23:39 -05:00
const MODIFIERS = {
59847: {
modifier_offset: '0 15px 15px 0',
modifier: true
},
70852: {
modifier: true,
modifier_offset: '0 5px 20px 0',
extra_width: 5,
shrink_to_fit: true
},
70854: {
modifier: true,
modifier_offset: '30px 0 0'
},
147049: {
modifier: true,
modifier_offset: '4px 1px 0 3px'
},
147011: {
modifier: true,
modifier_offset: '0'
},
70864: {
modifier: true,
modifier_offset: '0'
},
147038: {
modifier: true,
modifier_offset: '0'
}
};
export default class Emotes extends Module {
constructor(...args) {
super(...args);
this.inject('socket');
this.inject('settings');
2017-11-13 01:23:39 -05:00
this.twitch_inventory_sets = new Set(EXTRA_INVENTORY);
2017-11-13 01:23:39 -05:00
this.__twitch_emote_to_set = new Map;
this.__twitch_set_to_channel = new Map;
this.default_sets = new SourcedSet;
this.global_sets = new SourcedSet;
2017-11-13 01:23:39 -05:00
this.providers = new Map;
this.providers.set('featured', {
name: 'Featured',
i18n_key: 'emote-menu.featured',
sort_key: 75
})
2017-11-13 01:23:39 -05:00
this.emote_sets = {};
this._set_refs = {};
this._set_timers = {};
this.settings.add('chat.fix-bad-emotes', {
default: true,
ui: {
path: 'Chat > Appearance >> Emotes',
title: 'Fix Bad Twitch Global Emotes',
description: 'Clean up the images for bad Twitch global emotes, removing white borders and solid backgrounds.',
component: 'setting-check-box'
}
});
// Because this may be used elsewhere.
this.handleClick = this.handleClick.bind(this);
2017-11-13 01:23:39 -05:00
}
onEnable() {
// Just in case there's a weird load order going on.
this.on('site:enabled', this.loadTwitchInventory);
2017-11-13 01:23:39 -05:00
this.style = new ManagedStyle('emotes');
if ( Object.keys(this.emote_sets).length ) {
this.log.info('Generating CSS for existing emote sets.');
for(const set_id in this.emote_sets)
if ( has(this.emote_sets, set_id) ) {
const emote_set = this.emote_sets[set_id];
if ( emote_set && emote_set.pending_css ) {
this.style.set(`es--${set_id}`, emote_set.pending_css + (emote_set.css || ''));
emote_set.pending_css = null;
}
}
}
this.socket.on(':command:follow_sets', this.updateFollowSets, this);
this.loadGlobalSets();
this.loadTwitchInventory();
2017-11-13 01:23:39 -05:00
}
// ========================================================================
// Featured Sets
// ========================================================================
updateFollowSets(data) {
for(const room_login in data)
if ( has(data, room_login) ) {
const room = this.parent.getRoom(null, room_login, true),
new_sets = data[room_login] || [],
emote_sets = room.emote_sets,
providers = emote_sets._sources;
if ( providers && providers.has('featured') )
for(const item of providers.get('featured'))
if ( ! new_sets.includes(item) )
room.removeSet('featured', item);
for(const set_id of new_sets) {
room.addSet('featured', set_id);
if ( ! this.emote_sets[set_id] )
this.loadSet(set_id);
}
}
}
// ========================================================================
// Favorite Checking
// ========================================================================
toggleFavorite(source, id, value = null) {
const key = `favorite-emotes.${source}`,
p = this.settings.provider,
favorites = p.get(key) || [],
idx = favorites.indexOf(id);
if ( value === null )
value = idx === -1;
if ( value && idx === -1 )
favorites.push(id);
else if ( ! value && idx !== -1 )
favorites.splice(idx, 1);
else
return;
if ( favorites.length )
p.set(key, favorites);
else
p.delete(key);
this.emit(':change-favorite', source, id, value);
}
isFavorite(source, id) {
const favorites = this.settings.provider.get(`favorite-emotes.${source}`);
return favorites && favorites.includes(id);
}
getFavorites(source) {
return this.settings.provider.get(`favorite-emotes.${source}`) || [];
}
handleClick(event) {
const target = event.target,
ds = target && target.dataset;
if ( ! ds )
return;
const provider = ds.provider;
if ( event[MOD_KEY] ) {
// Favoriting Emotes
let source, id;
if ( provider === 'twitch' ) {
source = 'twitch';
id = parseInt(ds.id, 10);
} else if ( provider === 'ffz' ) {
const emote_set = this.emote_sets[ds.set],
emote = emote_set && emote_set.emotes[ds.id];
if ( ! emote )
return;
source = emote_set.source || 'ffz';
id = emote.id;
} else
return;
this.toggleFavorite(source, id);
const tt = target._ffz_tooltip$0;
if ( tt && tt.visible ) {
tt.hide();
setTimeout(() => document.contains(target) && tt.show(), 0);
}
return true;
}
if ( event.shiftKey && this.parent.context.get('chat.click-emotes') ) {
let url;
if ( provider === 'twitch' )
url = `https://twitchemotes.com/emotes/${ds.id}`;
else if ( provider === 'ffz' ) {
const emote_set = this.emote_sets[ds.set],
emote = emote_set && emote_set.emotes[ds.id];
if ( ! emote )
return;
if ( emote.click_url )
url = emote.click_url;
else if ( ! emote_set.source )
url = `https://www.frankerfacez.com/emoticons/${emote.id}`;
}
if ( url ) {
const win = window.open();
win.opener = null;
win.location = url;
}
return true;
}
}
2017-11-13 01:23:39 -05:00
// ========================================================================
// Access
// ========================================================================
getSetIDs(user_id, user_login, room_id, room_login) {
const room = this.parent.getRoom(room_id, room_login, true),
room_user = room && room.getUser(user_id, user_login, true),
2017-11-13 01:23:39 -05:00
user = this.parent.getUser(user_id, user_login, true);
return (user ? user.emote_sets._cache : []).concat(
room_user ? room_user.emote_sets._cache : [],
room ? room.emote_sets._cache : [],
this.default_sets._cache
2017-11-13 01:23:39 -05:00
);
}
getSets(user_id, user_login, room_id, room_login) {
return this.getSetIDs(user_id, user_login, room_id, room_login)
.map(set_id => this.emote_sets[set_id]);
}
_withSources(out, seen, emote_sets) { // eslint-disable-line class-methods-use-this
if ( ! emote_sets._sources )
return;
for(const [provider, data] of emote_sets._sources)
for(const item of data)
if ( ! seen.has(item) ) {
out.push([item, provider]);
seen.add(item);
}
return out;
}
getRoomSetIDsWithSources(user_id, user_login, room_id, room_login) {
const room = this.parent.getRoom(room_id, room_login, true),
room_user = room && room.getUser(user_id, user_login, true);
if ( ! room )
return [];
const out = [], seen = new Set;
this._withSources(out, seen, room.emote_sets);
if ( room_user )
this._withSources(out, seen, room_user);
return out;
}
getRoomSetsWithSources(user_id, user_login, room_id, room_login) {
return this.getRoomSetIDsWithSources(user_id, user_login, room_id, room_login)
.map(([set_id, source]) => [this.emote_sets[set_id], source]);
}
getRoomSetIDs(user_id, user_login, room_id, room_login) {
const room = this.parent.getRoom(room_id, room_login, true),
room_user = room && room.getUser(user_id, user_login, true);
if ( ! room )
return [];
if ( ! room_user )
return room.emote_sets._cache;
return room_user.emote_sets._cache.concat(room.emote_sets._cache);
}
getRoomSets(user_id, user_login, room_id, room_login) {
return this.getRoomSetIDs(user_id, user_login, room_id, room_login)
.map(set_id => this.emote_sets[set_id]);
}
getGlobalSetIDsWithSources(user_id, user_login) {
const user = this.parent.getUser(user_id, user_login, true),
out = [], seen = new Set;
this._withSources(out, seen, this.default_sets);
if ( user )
this._withSources(out, seen, user.emote_sets);
return out;
}
getGlobalSetsWithSources(user_id, user_login) {
return this.getGlobalSetIDsWithSources(user_id, user_login)
.map(([set_id, source]) => [this.emote_sets[set_id], source]);
}
getGlobalSetIDs(user_id, user_login) {
const user = this.parent.getUser(user_id, user_login, true);
if ( ! user )
return this.default_sets._cache;
return user.emote_sets._cache.concat(this.default_sets._cache);
}
getGlobalSets(user_id, user_login) {
return this.getGlobalSetIDs(user_id, user_login)
.map(set_id => this.emote_sets[set_id]);
}
getEmotes(user_id, user_login, room_id, room_login) {
const emotes = {};
for(const emote_set of this.getSets(user_id, user_login, room_id, room_login))
if ( emote_set && emote_set.emotes )
for(const emote of Object.values(emote_set.emotes) )
if ( emote && ! has(emotes, emote.name) )
emotes[emote.name] = emote;
return emotes;
}
2017-11-13 01:23:39 -05:00
// ========================================================================
// Emote Set Ref Counting
// ========================================================================
addDefaultSet(provider, set_id, data) {
let changed = false;
if ( ! this.default_sets.sourceIncludes(provider, set_id) ) {
this.default_sets.push(provider, set_id);
this.refSet(set_id);
changed = true;
}
if ( data )
this.loadSetData(set_id, data);
if ( changed )
this.emit(':update-default-sets', provider, set_id, true);
}
removeDefaultSet(provider, set_id) {
let changed = false;
if ( this.default_sets.sourceIncludes(provider, set_id) ) {
this.default_sets.remove(provider, set_id);
this.unrefSet(set_id);
changed = true;
}
if ( changed )
this.emit(':update-default-sets', provider, set_id, false);
}
refSet(set_id) {
this._set_refs[set_id] = (this._set_refs[set_id] || 0) + 1;
if ( this._set_timers[set_id] ) {
clearTimeout(this._set_timers[set_id]);
this._set_timers[set_id] = null;
}
}
unrefSet(set_id) {
const c = this._set_refs[set_id] = (this._set_refs[set_id] || 1) - 1;
if ( c <= 0 && ! this._set_timers[set_id] )
this._set_timers[set_id] = setTimeout(() => this.unloadSet(set_id), 5000);
}
// ========================================================================
// Emote Set Loading
2017-11-13 01:23:39 -05:00
// ========================================================================
async loadGlobalSets(tries = 0) {
2017-11-13 01:23:39 -05:00
let response, data;
try {
response = await fetch(`${API_SERVER}/v1/set/global`)
} catch(err) {
tries++;
if ( tries < 10 )
return setTimeout(() => this.loadGlobalSets(tries), 500 * tries);
2017-11-13 01:23:39 -05:00
this.log.error('Error loading global emote sets.', err);
return false;
}
if ( ! response.ok )
return false;
try {
data = await response.json();
} catch(err) {
this.log.error('Error parsing global emote data.', err);
return false;
}
const sets = data.sets || {};
for(const set_id of data.default_sets)
this.addDefaultSet('ffz-global', set_id);
2017-11-13 01:23:39 -05:00
for(const set_id in sets)
if ( has(sets, set_id) )
this.loadSetData(set_id, sets[set_id]);
2017-11-13 01:23:39 -05:00
if ( data.users )
this.loadSetUsers(data.users);
2017-11-13 01:23:39 -05:00
}
async loadSet(set_id, suppress_log = false, tries = 0) {
let response, data;
try {
response = await fetch(`${API_SERVER}/v1/set/${set_id}`)
} catch(err) {
tries++;
if ( tries < 10 )
return setTimeout(() => this.loadGlobalSets(tries), 500 * tries);
this.log.error(`Error loading data for set "${set_id}".`, err);
return false;
}
if ( ! response.ok )
return false;
try {
data = await response.json();
} catch(err) {
this.log.error(`Error parsing data for set "${set_id}".`, err);
return false;
}
const set = data.set;
if ( set )
this.loadSetData(set.id, set, suppress_log);
if ( data.users )
this.loadSetUsers(data.users);
return true;
}
loadSetUsers(data, suppress_log = false) {
2017-11-13 01:23:39 -05:00
for(const set_id in data)
if ( has(data, set_id) ) {
const emote_set = this.emote_sets[set_id],
users = data[set_id];
for(const login of users)
this.parent.getUser(undefined, login)
.addSet('ffz-global', set_id);
2017-11-13 01:23:39 -05:00
if ( ! suppress_log )
this.log.info(`Added "${emote_set ? emote_set.title : set_id}" emote set to ${users.length} users.`);
2017-11-13 01:23:39 -05:00
}
}
loadSetData(set_id, data, suppress_log = false) {
2017-11-13 01:23:39 -05:00
const old_set = this.emote_sets[set_id];
if ( ! data ) {
if ( old_set )
this.emote_sets[set_id] = null;
return;
}
this.emote_sets[set_id] = data;
let count = 0;
const ems = data.emotes || data.emoticons,
new_ems = data.emotes = {},
css = [];
data.id = set_id;
2017-11-13 01:23:39 -05:00
data.emoticons = undefined;
const bad_emotes = [];
2017-11-13 01:23:39 -05:00
for(const emote of ems) {
if ( ! emote.id || ! emote.name || ! emote.urls ) {
bad_emotes.push(emote);
continue;
}
2017-11-13 01:23:39 -05:00
emote.set_id = set_id;
emote.srcSet = `${emote.urls[1]} 1x`;
if ( emote.urls[2] )
emote.srcSet += `, ${emote.urls[2]} 2x`;
if ( emote.urls[4] )
emote.srcSet += `, ${emote.urls[4]} 4x`;
emote.token = {
type: 'emote',
id: emote.id,
set: set_id,
provider: 'ffz',
src: emote.urls[1],
srcSet: emote.srcSet,
text: emote.hidden ? '???' : emote.name,
length: emote.name.length
};
if ( has(MODIFIERS, emote.id) )
Object.assign(emote, MODIFIERS[emote.id]);
const emote_css = this.generateEmoteCSS(emote);
if ( emote_css )
css.push(emote_css);
count++;
new_ems[emote.id] = emote;
}
if ( bad_emotes.length )
this.log.warn(`Bad Emote Data for Set #${set_id}`, bad_emotes);
2017-11-13 01:23:39 -05:00
data.count = count;
if ( this.style && (css.length || data.css) )
this.style.set(`es--${set_id}`, css.join('') + (data.css || ''));
else if ( css.length )
data.pending_css = css.join('');
if ( ! suppress_log )
this.log.info(`Loaded emote set #${set_id}: ${data.title} (${count} emotes)`);
2017-11-13 01:23:39 -05:00
this.emit(':loaded', set_id, data);
}
unloadSet(set_id, force = false, suppress_log = false) {
const old_set = this.emote_sets[set_id],
count = this._set_refs[set_id] || 0;
if ( ! old_set )
return;
if ( count > 0 ) {
if ( ! force )
return this.log.warn(`Attempted to unload emote set #${set_id} with ${count} users.`);
this.log.warn(`Unloading emote set ${set_id} with ${count} users.`);
}
if ( ! suppress_log )
this.log.info(`Unloaded emote set #${set_id}: ${old_set.title}`);
this.emit(':unloaded', set_id, old_set);
this.emote_sets[set_id] = null;
}
2017-11-13 01:23:39 -05:00
// ========================================================================
// Emote CSS
// ========================================================================
generateEmoteCSS(emote) { // eslint-disable-line class-methods-use-this
if ( ! emote.margins && ( ! emote.modifier || ( ! emote.modifier_offset && ! emote.extra_width && ! emote.shrink_to_fit ) ) && ! emote.css )
return '';
let output = '';
if ( emote.modifier && (emote.modifier_offset || emote.margins || emote.extra_width || emote.shrink_to_fit) ) {
let margins = emote.modifier_offset || emote.margins || '0';
margins = margins.split(/\s+/).map(x => parseInt(x, 10));
if ( margins.length === 3 )
margins.push(margins[1]);
const l = margins.length,
m_top = margins[0 % l],
m_right = margins[1 % l],
m_bottom = margins[2 % l],
m_left = margins[3 % l];
output = `.modified-emote span .ffz-emote[data-id="${emote.id}"] {
padding: ${m_top}px ${m_right}px ${m_bottom}px ${m_left}px;
${emote.shrink_to_fit ? `max-width: calc(100% - ${40 - m_left - m_right - (emote.extra_width||0)}px);` : ''}
margin: 0 !important;
}`;
}
return `${output}.ffz-emote[data-id="${emote.id}"] {
${(emote.margins && ! emote.modifier) ? `margin: ${emote.margins} !important;` : ''}
${emote.css||''}
}`;
}
// ========================================================================
// Twitch Data Lookup
// ========================================================================
async loadTwitchInventory() {
const user = this.resolve('site').getUser();
if ( ! user )
return;
let data;
try {
data = await fetch('https://api.twitch.tv/v5/inventory/emoticons', {
headers: {
'Client-ID': CLIENT_ID,
'Authorization': `OAuth ${user.authToken}`
}
}).then(r => {
if ( r.ok )
return r.json();
throw r.status;
});
} catch(err) {
this.log.error('Error loading Twitch inventory.', err);
return;
}
const sets = this.twitch_inventory_sets = new Set(EXTRA_INVENTORY);
for(const set in data.emoticon_sets)
if ( has(data.emoticon_sets, set) )
sets.add(parseInt(set, 10));
this.log.info('Twitch Inventory Sets:', this.twitch_inventory_sets);
2017-11-13 01:23:39 -05:00
}
getTwitchEmoteSet(emote_id, callback) {
2017-11-13 01:23:39 -05:00
const tes = this.__twitch_emote_to_set;
if ( isNaN(emote_id) || ! isFinite(emote_id) )
return null;
if ( tes.has(emote_id) )
return tes.get(emote_id);
tes.set(emote_id, null);
timeout(this.socket.call('get_emote', emote_id), 1000).then(data => {
const set_id = data['s_id'];
tes.set(emote_id, set_id);
this.__twitch_set_to_channel.set(set_id, data);
if ( callback )
callback(data['s_id']);
}).catch(() => tes.delete(emote_id));
}
getTwitchSetChannel(set_id, callback) {
2017-11-13 01:23:39 -05:00
const tes = this.__twitch_set_to_channel;
if ( isNaN(set_id) || ! isFinite(set_id) )
return null;
if ( tes.has(set_id) )
return tes.get(set_id);
tes.set(set_id, null);
timeout(this.socket.call('get_emote_set', set_id), 1000).then(data => {
tes.set(set_id, data);
if ( callback )
callback(data);
}).catch(() => tes.delete(set_id));
}
}