diff --git a/src/api.js b/src/api.js new file mode 100644 index 00000000..69c5c4ab --- /dev/null +++ b/src/api.js @@ -0,0 +1,294 @@ +'use strict'; + +// ======================================================================== +// Legacy API +// ======================================================================== + +import Module from 'utilities/module'; +import {has} from 'utilities/object'; +import {EventEmitter} from 'utilities/events'; + + +export default class ApiModule extends Module { + constructor(...args) { + super(...args); + + this.inject('chat'); + this.inject('chat.emotes'); + + this._apis = {}; + + if ( ! this._known_apis ) { + this._known_apis = {}; + var stored_val = localStorage.getItem("ffz_known_apis"); + if ( stored_val !== null ) + try { + this._known_apis = JSON.parse(stored_val); + } catch(err) { + this.error("Error loading Known APIs", err); + } + } + } + + create(...args) { + return new LegacyAPI(this, ...args); + } +} + + +export class LegacyAPI extends EventEmitter { + constructor(instance, name, icon = null, version = null, name_key = null) { + super(); + + this.ffz = instance.root; + this.parent = instance; + + if ( name ) { + for(const id in this.parent._known_apis) { + if ( this.parent._known_apis[id] === name ) { + this.id = id; + break; + } + } + } + + if ( ! this.id ) { + let i = 0; + while ( ! this.id ) { + if ( ! has(this.parent._known_apis, i) ) { + this.id = i; + break; + } + i++; + } + + if ( name ) { + this.parent._known_apis[this.id] = name; + localStorage.ffz_known_apis = JSON.stringify(this.parent._known_apis); + } + } + + this.parent._apis[this.id] = this; + + this.emote_sets = {}; + this.global_sets = []; + this.default_sets = []; + + this.badges = {}; + this.users = {}; + + this.name = name || `Extension#${this.id}`; + this.name_key = name_key || this.name.replace(/[^A-Z0-9_\-]/g, '').toLowerCase(); + + if ( /^[0-9]/.test(this.name_key) ) + this.name_key = `_${this.name_key}`; + + this.icon = icon; + this.version = version; + + this.parent.log.info(`Registered New Extension #${this.id} (${this.name_key}): ${this.name}`); + } + + log(msg, data) { + this.parent.log.info(`Ext #${this.id} (${this.name_key}): ${msg}`, data); + } + + error(msg, error) { + this.parent.log.error(`Ext #${this.id} (${this.name_key}): ${msg}`, error); + } + + register_metadata(key, data) { } + unregister_metadata(key, data) { } + update_metadata(key, full_update) { } + + + _load_set(real_id, set_id, data) { + if ( ! data ) + return null; + + const emote_set = Object.assign({ + source: this.name, + icon: this.icon || null, + title: 'Global Emoticons', + _type: 0 + }, data, { + source_ext: this.id, + source_id: set_id, + id: real_id, + count: 0 + }); + + this.emote_sets[set_id] = emote_set; + this.parent.emotes.load_set_data(real_id, emote_set); + + return emote_set; + } + + + load_set(set_id, emote_set) { + const real_id = `${this.id}-${set_id}`; + return this._load_set(real_id, set_id, emote_set); + } + + + unload_set(set_id) { + const real_id = `${this.id}-${set_id}`, + emote_set = this.emote_sets[set_id]; + + if ( ! emote_set ) + return; + + this.unregister_global_set(set_id); + + // TODO: Unload sets + + return emote_set; + } + + + get_set(set_id) { + return this.emote_sets[set_id]; + } + + + register_global_set(set_id, emote_set) { + const real_id = `${this.id}-${set_id}`; + if ( emote_set ) + emote_set = this.load_set(set_id, emote_set); + else + emote_set = this.emote_sets[set_id]; + + if ( ! emote_set ) + throw new Error('Invalid set ID.'); + + if ( this.parent.emotes.emote_sets && ! this.parent.emotes.emote_sets[real_id] ) + this.parent.emotes.emote_sets[real_id] = emote_set; + + if ( this.global_sets.indexOf(set_id) === -1 ) + this.global_sets.push(set_id); + + if ( this.default_sets.indexOf(set_id) === -1 ) + this.default_sets.push(set_id); + + this.parent.emotes.global_sets.push(`api--${this.id}`, real_id); + this.parent.emotes.default_sets.push(`api--${this.id}`, real_id); + } + + unregister_global_set(set_id) { + const real_id = `${this.id}-${set_id}`, + emote_set = this.emote_sets[set_id]; + + if ( ! emote_set ) + return; + + let ind = this.global_sets.indexOf(set_id); + if ( ind !== -1 ) + this.global_sets.splice(ind,1); + + ind = this.default_sets.indexOf(set_id); + if ( ind !== -1 ) + this.default_sets.splice(ind,1); + + this.parent.emote.global_sets.remove(`api--${this.id}`, real_id); + this.parent.emote.default_sets.remove(`api--${this.id}`, real_id); + } + + + register_room_set(room_login, set_id, emote_set) { + const real_id = `${this.id}-${set_id}`, + room = this.parent.chat.getRoom(null, room_login, true); + + if ( ! room ) + throw new Error('Room not loaded'); + + if ( emote_set ) { + emote_set.title = emote_set.title || `Channel: ${room.data && room.data.display_name || room_login}`; + emote_set._type = emote_set._type || 1; + + emote_set = this.load_set(set_id, emote_set); + } else + emote_set = this.emote_sets[set_id]; + + if ( ! emote_set ) + throw new Error('Invalid set ID.'); + + if ( this.parent.emotes.emote_sets && ! this.parent.emotes.emote_sets[real_id] ) + this.parent.emotes.emote_sets[real_id] = emote_set; + + room.emote_sets.push(`api--${this.id}`, real_id); + emote_set.users++; + } + + unregister_room_set(room_login, set_id) { + const real_id = `${this.id}-${set_id}`, + emote_set = this.emote_sets[set_id], + room = this.parent.chat.getRoom(null, room_login, true); + + if ( ! emote_set || ! room ) + return; + + room.emote_sets.remove(`api--${this.id}`, real_id); + emote_set.users--; + } + + + add_badge() { } + remove_badge() { } + user_add_badge() { } + user_remove_badge() { } + room_add_user_badge() { } + room_remove_user_badge() { } + + user_add_set(username, set_id) { + + } + + user_remove_set(username, set_id) { + + } + + + retokenize_messages() { } + + + register_chat_filter(filter) { + this.on('room-message', filter); + } + + unregister_chat_filter(filter) { + this.off('room-message', filter); + } + + + iterate_chat_views(func) { } + iterate_rooms(func) { + if ( func === undefined ) + func = this.emit.bind(this, 'room-add'); + + const chat = this.parent.resolve('chat'); + for(const room_id in chat.rooms) + func(room_id); + } + + register_on_room_callback(callback, dont_iterate) { + const thing = room_id => { + return callback(room_id, this.register_room_set.bind(this, room_id)); + } + + thing.original_func = callback; + callback.__wrapped = thing; + + this.on('room-add', thing); + if ( ! dont_iterate ) + this.iterate_rooms(thing); + } + + unregister_on_room_callback(callback) { + if ( ! callback.__wrapped ) + return; + + this.off('room-add', callback.__wrapped); + callback.__wrapped = null; + } + +} \ No newline at end of file diff --git a/src/main.js b/src/main.js index 25923e22..6913aa32 100644 --- a/src/main.js +++ b/src/main.js @@ -9,6 +9,7 @@ import SettingsManager from './settings/index'; import {TranslationManager} from './i18n'; import SocketClient from './socket'; import Site from 'site'; +import LegacyAPI from './api'; import Vue from 'utilities/vue'; class FrankerFaceZ extends Module { @@ -38,6 +39,8 @@ class FrankerFaceZ extends Module { this.inject('socket', SocketClient); this.inject('site', Site); + this.inject('_api', LegacyAPI); + this.register('vue', Vue); @@ -87,8 +90,8 @@ class FrankerFaceZ extends Module { /* eslint class-methods-use-this: off */ - api() { - throw new Error('Not Implemented'); + api(...args) { + return this._api.create(...args); } } @@ -117,4 +120,13 @@ FrankerFaceZ.utilities = { window.FrankerFaceZ = FrankerFaceZ; -window.ffz = new FrankerFaceZ(); \ No newline at end of file +window.ffz = new FrankerFaceZ(); + +// Make FFZ:AP Run +FrankerFaceZ.chat_commands = {}; +FrankerFaceZ.settings_info = {}; +FrankerFaceZ.utils = { + process_int(a,b,c) { return a } +} +window.App = true; +jQuery.noty = {themes: {}}; \ No newline at end of file diff --git a/src/modules/chat/room.js b/src/modules/chat/room.js index fd7b269d..7d7a659d 100644 --- a/src/modules/chat/room.js +++ b/src/modules/chat/room.js @@ -73,9 +73,11 @@ export default class Room extends EventEmitter { return; if ( this._login ) { - if ( this.manager.rooms[this._login] === this ) - this.manager.rooms[this._login] = null; + const old_room = this.manager.rooms[this._login]; + if ( old_room !== this ) + old_room.login = null; + this.manager.rooms[this._login] = null; this.manager.socket.unsubscribe(`room.${this.login}`); } @@ -83,6 +85,11 @@ export default class Room extends EventEmitter { if ( ! val ) return; + const old_room = this.manager.rooms[val]; + if ( old_room && old_room !== this ) + old_room.login = null; + + this.manager.rooms[val] = this; this.manager.socket.subscribe(`room.${val}`); this.manager.emit(':room-update-login', this, val); } diff --git a/src/settings/index.js b/src/settings/index.js index c60d0847..09f20edd 100644 --- a/src/settings/index.js +++ b/src/settings/index.js @@ -13,6 +13,15 @@ import SettingsContext from './context'; import MigrationManager from './migration'; +const OVERRIDE_GET = { + ffz_enable_highlight_sound: false, + ffz_highlight_sound_volume: 0, + bttv_channel_emotes: true, + bttv_global_emotes: true, + bttv_gif_emotes: 1 +} + + // ============================================================================ // SettingsManager // ============================================================================ @@ -322,7 +331,12 @@ export default class SettingsManager extends Module { // ======================================================================== context(env) { return this.main_context.context(env) } - get(key) { return this.main_context.get(key) } + get(key) { + if ( has(OVERRIDE_GET, key) ) + return OVERRIDE_GET[key]; + + return this.main_context.get(key); + } uses(key) { return this.main_context.uses(key) } update(key) { return this.main_context.update(key) } diff --git a/src/utilities/object.js b/src/utilities/object.js index 833bc6f7..dc6e9de7 100644 --- a/src/utilities/object.js +++ b/src/utilities/object.js @@ -186,6 +186,10 @@ export class SourcedSet { get(key) { return this._sources && this._sources.get(key) } has(key) { return this._sources ? this._sources.has(key) : false } + includes(val) { + return this._cache.includes(val); + } + delete(key) { if ( this._sources && this._sources.has(key) ) { this._sources.delete(key); @@ -236,4 +240,18 @@ export class SourcedSet { if ( ! this._cache.includes(val) ) this._cache.push(val); } + + remove(key, val) { + if ( ! this.has(key) ) + return; + + const old_val = this._sources.get(key), + idx = old_val.indexOf(val); + + if ( idx === -1 ) + return; + + old_val.splice(idx, 1); + this._rebuild(); + } } \ No newline at end of file