From 360715992969ffb92f7925acad72234b6abf4862 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Wed, 15 Nov 2017 21:59:13 -0500 Subject: [PATCH] Start using SourcedSet for emoticon sets so that, in the future, special events, featured channels, etc. can add and remove emote sets without worrying about reference counting or anything like that. Also implement the method to get your Twitch Inventory emote sets. --- src/modules/chat/emotes.js | 48 ++++++++++++++++------- src/modules/chat/index.js | 4 +- src/modules/chat/room.js | 80 ++++++++++++++++++++++++++++++-------- src/utilities/constants.js | 1 + src/utilities/object.js | 72 ++++++++++++++++++++++++++++++++++ 5 files changed, 172 insertions(+), 33 deletions(-) diff --git a/src/modules/chat/emotes.js b/src/modules/chat/emotes.js index c80ec8b1..27a42d07 100644 --- a/src/modules/chat/emotes.js +++ b/src/modules/chat/emotes.js @@ -6,8 +6,8 @@ import Module from 'utilities/module'; import {ManagedStyle} from 'utilities/dom'; -import {has, timeout} from 'utilities/object'; -import {API_SERVER} from 'utilities/constants'; +import {has, timeout, SourcedSet} from 'utilities/object'; +import {CLIENT_ID, API_SERVER} from 'utilities/constants'; const MODIFIERS = { @@ -61,13 +61,16 @@ export default class Emotes extends Module { this.__twitch_emote_to_set = new Map; this.__twitch_set_to_channel = new Map; - this.default_sets = new Set; - this.global_sets = new Set; + this.default_sets = new SourcedSet; + this.global_sets = new SourcedSet; this.emote_sets = {}; } onEnable() { + // Just in case there's a weird load order going on. + this.on('site:enabled', this.refresh_twitch_inventory); + this.style = new ManagedStyle('emotes'); if ( Object.keys(this.emote_sets).length ) { @@ -96,9 +99,9 @@ export default class Emotes extends Module { const room = this.parent.getRoom(room_id, room_login, true), user = this.parent.getUser(user_id, user_login, true); - return (user ? user.emote_sets : []).concat( - room ? room.emote_sets : [], - Array.from(this.default_sets) + return (user ? user.emote_sets._cache : []).concat( + room ? room.emote_sets._cache : [], + this.default_sets._cache ); } @@ -136,12 +139,11 @@ export default class Emotes extends Module { const sets = data.sets || {}; - for(const set_id of data.default_sets) - this.default_sets.add(set_id); + this.default_sets.extend('ffz-global', ...data.default_sets); for(const set_id in sets) if ( has(sets, set_id) ) { - this.global_sets.add(set_id); + this.global_sets.push('ffz-global', set_id); this.load_set_data(set_id, sets[set_id]); } @@ -160,8 +162,7 @@ export default class Emotes extends Module { const user = this.parent.getUser(undefined, login), sets = user.emote_sets; - if ( sets.indexOf(set_id) === -1 ) - sets.push(set_id); + sets.push('ffz-global', set_id); } this.log.info(`Added "${emote_set ? emote_set.title : set_id}" emote set to ${users.length} users.`); @@ -279,8 +280,27 @@ export default class Emotes extends Module { // Twitch Data Lookup // ======================================================================== - refresh_twitch_inventory() { - this.log.debug('Unimplemented: refresh_twitch_inventory'); + async refresh_twitch_inventory() { + 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 => r.json()); + + } catch(err) { + this.log.error('Error loading Twitch inventory.', err); + return; + } + + this.twitch_inventory_sets = data.emoticon_sets ? Object.keys(data.emoticon_sets) : []; + this.log.info('Twitch Inventory Sets:', this.twitch_inventory_sets); } diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index 4668b101..76e73914 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -9,7 +9,7 @@ const WEBKIT = IS_WEBKIT ? '-webkit-' : ''; import Module from 'utilities/module'; import {createElement, ManagedStyle} from 'utilities/dom'; -import {timeout, has} from 'utilities/object'; +import {timeout, has, SourcedSet} from 'utilities/object'; import Badges from './badges'; import Emotes from './emotes'; @@ -283,7 +283,7 @@ export default class Chat extends Module { return null; else - user = {id, login, badges: [], emote_sets: []}; + user = {id, login, badges: [], emote_sets: new SourcedSet}; if ( id && id !== user.id ) { // If the ID isn't what we expected, something is very wrong here. diff --git a/src/modules/chat/room.js b/src/modules/chat/room.js index ace10d80..fd7b269d 100644 --- a/src/modules/chat/room.js +++ b/src/modules/chat/room.js @@ -8,7 +8,7 @@ import {API_SERVER, IS_WEBKIT} from 'utilities/constants'; import {EventEmitter} from 'utilities/events'; import {createElement as e, ManagedStyle} from 'utilities/dom'; -import {has} from 'utilities/object'; +import {has, SourcedSet} from 'utilities/object'; const WEBKIT = IS_WEBKIT ? '-webkit-' : ''; @@ -20,42 +20,71 @@ export default class Room extends EventEmitter { this._destroy_timer = null; this.manager = manager; - this.id = id; + this._id = id; this.login = login; - if ( login ) - this.manager.rooms[login] = this; - if ( id ) this.manager.room_ids[id] = this; this.refs = new Set; this.style = new ManagedStyle(`room--${login}`); - this.emote_sets = []; + this.emote_sets = new SourcedSet; this.users = []; this.user_ids = []; - if ( this.login ) { - this.manager.socket.subscribe(`room.${login}`); - this.load_data(); - } + this.manager.emit(':room-add', this); + this.load_data(); } + destroy() { clearTimeout(this._destroy_timer); this._destroy_timer = null; + this.destroyed = true; - this.manager.socket.unsubscribe(`room.${this.login}`); + this.manager.emit(':room-remove', this); this.style.destroy(); + if ( this._login ) { + if ( this.manager.rooms[this._login] === this ) + this.manager.rooms[this._login] = null; + + this.manager.socket.unsubscribe(`room.${this.login}`); + } + if ( this.manager.room_ids[this.id] === this ) this.manager.room_ids[this.id] = null; + } - if ( this.manager.rooms[this.login] === this ) - this.manager.rooms[this.login] = null; + + get id() { + return this._id; + } + + get login() { + return this._login; + } + + set login(val) { + if ( this._login === val ) + return; + + if ( this._login ) { + if ( this.manager.rooms[this._login] === this ) + this.manager.rooms[this._login] = null; + + this.manager.socket.unsubscribe(`room.${this.login}`); + } + + this._login = val; + if ( ! val ) + return; + + this.manager.socket.subscribe(`room.${val}`); + this.manager.emit(':room-update-login', this, val); } @@ -69,7 +98,7 @@ export default class Room extends EventEmitter { let response, data; try { - response = await fetch(`${API_SERVER}/v1/room/${this.login}`); + response = await fetch(`${API_SERVER}/v1/room/${this.id ? `id/${this.id}` : this.login}`); } catch(err) { tries++; if ( tries < 10 ) @@ -89,9 +118,26 @@ export default class Room extends EventEmitter { return false; } - const d = this.data = data.room; - if ( d.set && this.emote_sets.indexOf(d) === -1 ) - this.emote_sets.push(d.set); + const d = data.room, + id = '' + d.twitch_id; + + if ( ! this._id ) { + this._id = id; + this.manager.room_ids[id] = this; + + } else if ( this._id !== id ) { + this.manager.log.warn(`Received data for ${this.id}:${this.login} with the wrong ID: ${id}`); + return false; + } + + this.login = d.id; + this.data = d; + + if ( d.set ) + this.emote_sets.set('main', d.set); + else + this.emote_sets.delete('main'); + if ( data.sets ) for(const set_id in data.sets) diff --git a/src/utilities/constants.js b/src/utilities/constants.js index 66969762..bc99c32d 100644 --- a/src/utilities/constants.js +++ b/src/utilities/constants.js @@ -3,6 +3,7 @@ export const DEBUG = localStorage.ffzDebugMode === 'true' && document.body.classList.contains('ffz-dev'); export const SERVER = DEBUG ? '//localhost:8000' : 'https://cdn.frankerfacez.com'; +export const CLIENT_ID = 'a3bc9znoz6vi8ozsoca0inlcr4fcvkl'; export const API_SERVER = '//api.frankerfacez.com'; export const WS_CLUSTERS = { diff --git a/src/utilities/object.js b/src/utilities/object.js index de114c0c..833bc6f7 100644 --- a/src/utilities/object.js +++ b/src/utilities/object.js @@ -164,4 +164,76 @@ export function maybe_call(fn, ctx, ...args) { } return fn; +} + + +export class SourcedSet { + constructor() { + this._cache = []; + } + + _rebuild() { + if ( ! this._sources ) + return; + + this._cache = []; + for(const items of this._sources.values()) + for(const i of items) + if ( ! this._cache.includes(i) ) + this._cache.push(i); + } + + get(key) { return this._sources && this._sources.get(key) } + has(key) { return this._sources ? this._sources.has(key) : false } + + delete(key) { + if ( this._sources && this._sources.has(key) ) { + this._sources.delete(key); + this._rebuild(); + } + } + + extend(key, ...items) { + if ( ! this._sources ) + this._sources = new Map; + + const had = this.has(key); + this._sources.set(key, [false, items]); + if ( had ) + this._rebuild(); + else + for(const i of items) + if ( ! this._cache.includes(i) ) + this._cache.push(i); + } + + set(key, val) { + if ( ! this._sources ) + this._sources = new Map; + + const had = this.has(key); + this._sources.set(key, [val]); + + if ( had ) + this._rebuild(); + + else if ( ! this._cache.includes(val) ) + this._cache.push(val); + } + + push(key, val) { + if ( ! this._sources ) + return this.set(key, val); + + const old_val = this._sources.get(key); + if ( old_val === undefined ) + return this.set(key, val); + + else if ( old_val.includes(val) ) + return; + + old_val.push(val); + if ( ! this._cache.includes(val) ) + this._cache.push(val); + } } \ No newline at end of file