mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-28 05:15:54 +00:00
4.58.0
* Fixed: When unloading an emote set, also unload its CSS block. * API Added: `badges.removeBadge()` method for removing a badge from the system. * API Added: `chat.iterateUsers()` method to iterate all User instances. * API Added: `getUser().removeAllBadges()` method to remove all badges from a provider. * API Added: `getRoom().iterateUsers()` method to iterate all User instances on a Room instance. * API Added: `tooltips.define()` method to add a tool-tip handler to the system. * API Changed: All core modules now use the add-on proxies to track which add-ons are responsible for injecting content, and to make it easier to unload (and reload) add-ons. * API Changed: Many core module methods now display warning messages in the console when used improperly. For now, this mainly checks that IDs are correct, but there may be more warnings in the future. * API Fixed: Calling `resolve()` will now properly use add-on proxies. * Experiment Changed: Improved handling for server disconnects with the MQTT experiment, as well as handling for topic mapping changes.
This commit is contained in:
parent
c4da7fa9d9
commit
675512e811
13 changed files with 1008 additions and 146 deletions
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "frankerfacez",
|
"name": "frankerfacez",
|
||||||
"author": "Dan Salvato LLC",
|
"author": "Dan Salvato LLC",
|
||||||
"version": "4.57.4",
|
"version": "4.58.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",
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
// Emoji Handling
|
// Emoji Handling
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
import Module from 'utilities/module';
|
import { DEBUG } from 'utilities/constants';
|
||||||
|
import Module, { buildAddonProxy } from 'utilities/module';
|
||||||
import {has, maybe_call, deep_copy} from 'utilities/object';
|
import {has, maybe_call, deep_copy} from 'utilities/object';
|
||||||
import {createElement, ClickOutside} from 'utilities/dom';
|
import {createElement, ClickOutside} from 'utilities/dom';
|
||||||
import Tooltip from 'utilities/tooltip';
|
import Tooltip from 'utilities/tooltip';
|
||||||
|
@ -261,6 +262,91 @@ export default class Actions extends Module {
|
||||||
for(const key in RENDERERS)
|
for(const key in RENDERERS)
|
||||||
if ( has(RENDERERS, key) )
|
if ( has(RENDERERS, key) )
|
||||||
this.addRenderer(key, RENDERERS[key]);
|
this.addRenderer(key, RENDERERS[key]);
|
||||||
|
|
||||||
|
this.on('addon:fully-unload', addon_id => {
|
||||||
|
let removed = 0;
|
||||||
|
for(const [key, def] of Object.entries(this.actions)) {
|
||||||
|
if ( def?.__source === addon_id ) {
|
||||||
|
removed++;
|
||||||
|
delete this.actions[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const [key, def] of Object.entries(this.renderers)) {
|
||||||
|
if ( def?.__source === addon_id ) {
|
||||||
|
removed++;
|
||||||
|
delete this.renderers[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( removed ) {
|
||||||
|
this.log.debug(`Cleaned up ${removed} entries when unloading addon:`, addon_id);
|
||||||
|
this._updateContexts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getAddonProxy(addon_id, addon, module) {
|
||||||
|
if ( ! addon_id )
|
||||||
|
return this;
|
||||||
|
|
||||||
|
const overrides = {},
|
||||||
|
warnings = {},
|
||||||
|
is_dev = DEBUG || addon?.dev;
|
||||||
|
|
||||||
|
overrides.addAction = (key, data) => {
|
||||||
|
if ( data )
|
||||||
|
data.__source = addon_id;
|
||||||
|
|
||||||
|
if ( is_dev && ! key.includes(addon_id) )
|
||||||
|
module.log.warn('[DEV-CHECK] Call to actions.addAction() did not include add-on ID in key:', key);
|
||||||
|
|
||||||
|
return this.addAction(key, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
overrides.addRenderer = (key, data) => {
|
||||||
|
if ( data )
|
||||||
|
data.__source = addon_id;
|
||||||
|
|
||||||
|
if ( is_dev && ! key.includes(addon_id) )
|
||||||
|
module.log.warn('[DEV-CHECK] Call to actions.addRenderer() did not include add-on ID in key:', key);
|
||||||
|
|
||||||
|
return this.addRenderer(key, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( is_dev ) {
|
||||||
|
overrides.removeAction = key => {
|
||||||
|
const existing = this.actions[key];
|
||||||
|
if ( existing && existing.__source !== addon_id )
|
||||||
|
module.log.warn('[DEV-CHECK] Removed un-owned action with actions.removeAction:', key, ' owner:', existing.__source ?? 'ffz');
|
||||||
|
|
||||||
|
return this.removeAction(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
overrides.removeRenderer = key => {
|
||||||
|
const existing = this.renderers[key];
|
||||||
|
if ( existing && existing.__source !== addon_id )
|
||||||
|
module.log.warn('[DEV-CHECK] Removed un-owned renderer with actions.removeRenderer:', key, ' owner:', existing.__source ?? 'ffz');
|
||||||
|
|
||||||
|
return this.removeRenderer(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
warnings.actions = 'Please use addAction() or removeAction()';
|
||||||
|
warnings.renderers = 'Please use addRenderer() or removeRenderer()';
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildAddonProxy(module, this, 'actions', overrides, warnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_updateContexts() {
|
||||||
|
for(const ctx of this.settings.__contexts) {
|
||||||
|
ctx.update('chat.actions.inline');
|
||||||
|
ctx.update('chat.actions.hover');
|
||||||
|
ctx.update('chat.actions.user-context');
|
||||||
|
ctx.update('chat.actions.room');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -269,13 +355,7 @@ export default class Actions extends Module {
|
||||||
return this.log.warn(`Attempted to add action "${key}" which is already defined.`);
|
return this.log.warn(`Attempted to add action "${key}" which is already defined.`);
|
||||||
|
|
||||||
this.actions[key] = data;
|
this.actions[key] = data;
|
||||||
|
this._updateContexts();
|
||||||
for(const ctx of this.settings.__contexts) {
|
|
||||||
ctx.update('chat.actions.inline');
|
|
||||||
ctx.update('chat.actions.hover');
|
|
||||||
ctx.update('chat.actions.user-context');
|
|
||||||
ctx.update('chat.actions.room');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -284,14 +364,25 @@ export default class Actions extends Module {
|
||||||
return this.log.warn(`Attempted to add renderer "${key}" which is already defined.`);
|
return this.log.warn(`Attempted to add renderer "${key}" which is already defined.`);
|
||||||
|
|
||||||
this.renderers[key] = data;
|
this.renderers[key] = data;
|
||||||
|
this._updateContexts();
|
||||||
for(const ctx of this.settings.__contexts) {
|
|
||||||
ctx.update('chat.actions.inline');
|
|
||||||
ctx.update('chat.actions.inline');
|
|
||||||
ctx.update('chat.actions.hover');
|
|
||||||
ctx.update('chat.actions.user-context');
|
|
||||||
ctx.update('chat.actions.room');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
removeAction(key) {
|
||||||
|
if ( ! has(this.actions, key) )
|
||||||
|
return;
|
||||||
|
|
||||||
|
delete this.actions[key];
|
||||||
|
this._updateContexts();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
removeRenderer(key) {
|
||||||
|
if ( ! has(this.renderers, key) )
|
||||||
|
return;
|
||||||
|
|
||||||
|
delete this.renderers[key];
|
||||||
|
this._updateContexts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,13 @@
|
||||||
// Badge Handling
|
// Badge Handling
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
import {NEW_API, SERVER, IS_WEBKIT, IS_FIREFOX, WEBKIT_CSS as WEBKIT} from 'utilities/constants';
|
import {NEW_API, SERVER, IS_WEBKIT, IS_FIREFOX, WEBKIT_CSS as WEBKIT, DEBUG} from 'utilities/constants';
|
||||||
|
|
||||||
import {createElement, ManagedStyle} from 'utilities/dom';
|
import {createElement, ManagedStyle} from 'utilities/dom';
|
||||||
import {has, maybe_call, SourcedSet} from 'utilities/object';
|
import {has, makeAddonIdChecker, maybe_call, SourcedSet} from 'utilities/object';
|
||||||
import Module from 'utilities/module';
|
import Module, { buildAddonProxy } from 'utilities/module';
|
||||||
import { ColorAdjuster } from 'src/utilities/color';
|
import { ColorAdjuster } from 'utilities/color';
|
||||||
import { NoContent } from 'src/utilities/tooltip';
|
import { NoContent } from 'utilities/tooltip';
|
||||||
|
|
||||||
const CSS_BADGES = {
|
const CSS_BADGES = {
|
||||||
1: {
|
1: {
|
||||||
|
@ -513,6 +513,22 @@ export default class Badges extends Module {
|
||||||
this.loadGlobalBadges();
|
this.loadGlobalBadges();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.on('addon:fully-unload', addon_id => {
|
||||||
|
let removed = 0;
|
||||||
|
for(const [key, val] of Object.entries(this.badges)) {
|
||||||
|
if ( val?.__source === addon_id ) {
|
||||||
|
removed++;
|
||||||
|
this.removeBadge(key, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( removed ) {
|
||||||
|
this.log.debug(`Cleaned up ${removed} entries when unloading addon:`, addon_id);
|
||||||
|
this.generateBadgeCSS();
|
||||||
|
// TODO: Debounced re-badge all chat messages.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.tooltips.types.badge = (target, tip) => {
|
this.tooltips.types.badge = (target, tip) => {
|
||||||
tip.add_class = 'ffz__tooltip--badges';
|
tip.add_class = 'ffz__tooltip--badges';
|
||||||
|
|
||||||
|
@ -608,48 +624,61 @@ export default class Badges extends Module {
|
||||||
if ( ! addon_id )
|
if ( ! addon_id )
|
||||||
return this;
|
return this;
|
||||||
|
|
||||||
const is_dev = addon?.dev ?? false;
|
const is_dev = DEBUG || (addon?.dev ?? false),
|
||||||
|
id_checker = makeAddonIdChecker(addon_id);
|
||||||
|
|
||||||
const overrides = {};
|
const overrides = {},
|
||||||
|
warnings = {};
|
||||||
|
|
||||||
overrides.loadBadgeData = (badge_id, data, ...args) => {
|
overrides.loadBadgeData = (badge_id, data, ...args) => {
|
||||||
if ( data && data.addon === undefined )
|
if ( data && data.addon === undefined )
|
||||||
data.addon = addon_id;
|
data.addon = addon_id;
|
||||||
|
|
||||||
|
if ( is_dev && ! id_checker.test(badge_id) )
|
||||||
|
module.log.warn('[DEV-CHECK] Call to chat.badges.loadBadgeData() did not include addon ID in badge_id:', badge_id);
|
||||||
|
|
||||||
return this.loadBadgeData(badge_id, data, ...args);
|
return this.loadBadgeData(badge_id, data, ...args);
|
||||||
};
|
};
|
||||||
|
|
||||||
if ( is_dev ) {
|
if ( is_dev ) {
|
||||||
|
overrides.removeBadge = (badge_id, ...args) => {
|
||||||
|
// Note: We aren't checking that the badge_id contains the add-on
|
||||||
|
// ID because that should be handled by loadBadgeData for badges
|
||||||
|
// from this add-on. Checking if we're removing a badge from
|
||||||
|
// another source covers the rest.
|
||||||
|
|
||||||
|
const existing = this.badges[badge_id];
|
||||||
|
if ( existing && existing.addon !== addon_id )
|
||||||
|
module.log.warn('[DEV-CHECK] Removed un-owned badge with chat.badges.removeBadge():', key, ' owner:', existing.addon ?? 'ffz');
|
||||||
|
|
||||||
|
return this.removeBadge(badge_id, ...args);
|
||||||
|
};
|
||||||
|
|
||||||
overrides.setBulk = (source, ...args) => {
|
overrides.setBulk = (source, ...args) => {
|
||||||
if ( ! source.includes(addon_id) )
|
if ( ! id_checker.test(source) )
|
||||||
module.log.warn('[DEV-CHECK] Call to badges.setBulk did not include addon ID in source:', source);
|
module.log.warn('[DEV-CHECK] Call to chat.badges.setBulk() did not include addon ID in source:', source);
|
||||||
|
|
||||||
return this.setBulk(source, ...args);
|
return this.setBulk(source, ...args);
|
||||||
};
|
};
|
||||||
|
|
||||||
overrides.deleteBulk = (source, ...args) => {
|
overrides.deleteBulk = (source, ...args) => {
|
||||||
if ( ! source.includes(addon_id) )
|
if ( ! id_checker.test(source) )
|
||||||
module.log.warn('[DEV-CHECK] Call to badges.deleteBulk did not include addon ID in source:', source);
|
module.log.warn('[DEV-CHECK] Call to chat.badges.deleteBulk() did not include addon ID in source:', source);
|
||||||
|
|
||||||
return this.deleteBulk(source, ...args);
|
return this.deleteBulk(source, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
overrides.extendBulk = (source, ...args) => {
|
overrides.extendBulk = (source, ...args) => {
|
||||||
if ( ! source.includes(addon_id) )
|
if ( ! id_checker.test(source) )
|
||||||
module.log.warn('[DEV-CHECK] Call to badges.extendBulk did not include addon ID in source:', source);
|
module.log.warn('[DEV-CHECK] Call to chat.badges.extendBulk() did not include addon ID in source:', source);
|
||||||
|
|
||||||
return this.extendBulk(source, ...args);
|
return this.extendBulk(source, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
warnings.badges = 'Please use loadBadgeData() or removeBadge()';
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Proxy(this, {
|
return buildAddonProxy(module, this, 'chat.badges', overrides, warnings);
|
||||||
get(obj, prop) {
|
|
||||||
const thing = overrides[prop];
|
|
||||||
if ( thing )
|
|
||||||
return thing;
|
|
||||||
return Reflect.get(...arguments);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1194,6 +1223,17 @@ export default class Badges extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
removeBadge(badge_id, generate_css = true) {
|
||||||
|
if ( ! this.badges[badge_id] )
|
||||||
|
return;
|
||||||
|
|
||||||
|
delete this.badges[badge_id];
|
||||||
|
|
||||||
|
if ( generate_css )
|
||||||
|
this.buildBadgeCSS();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
loadBadgeData(badge_id, data, generate_css = true) {
|
loadBadgeData(badge_id, data, generate_css = true) {
|
||||||
this.badges[badge_id] = data;
|
this.badges[badge_id] = data;
|
||||||
|
|
||||||
|
|
|
@ -4,14 +4,14 @@
|
||||||
// Emote Handling and Default Provider
|
// Emote Handling and Default Provider
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
import Module from 'utilities/module';
|
import Module, { buildAddonProxy } from 'utilities/module';
|
||||||
import {ManagedStyle} from 'utilities/dom';
|
import {ManagedStyle} from 'utilities/dom';
|
||||||
import {get, has, timeout, SourcedSet, make_enum_flags} from 'utilities/object';
|
import { FFZEvent } from 'utilities/events';
|
||||||
import {NEW_API, IS_OSX, EmoteTypes, TWITCH_GLOBAL_SETS, TWITCH_POINTS_SETS, TWITCH_PRIME_SETS} from 'utilities/constants';
|
import {get, has, timeout, SourcedSet, make_enum_flags, makeAddonIdChecker} from 'utilities/object';
|
||||||
|
import {NEW_API, IS_OSX, EmoteTypes, TWITCH_GLOBAL_SETS, TWITCH_POINTS_SETS, TWITCH_PRIME_SETS, DEBUG} from 'utilities/constants';
|
||||||
|
|
||||||
import GET_EMOTE from './emote_info.gql';
|
import GET_EMOTE from './emote_info.gql';
|
||||||
import GET_EMOTE_SET from './emote_set_info.gql';
|
import GET_EMOTE_SET from './emote_set_info.gql';
|
||||||
import { FFZEvent } from 'src/utilities/events';
|
|
||||||
|
|
||||||
const HoverRAF = Symbol('FFZ:Hover:RAF');
|
const HoverRAF = Symbol('FFZ:Hover:RAF');
|
||||||
const HoverState = Symbol('FFZ:Hover:State');
|
const HoverState = Symbol('FFZ:Hover:State');
|
||||||
|
@ -434,6 +434,7 @@ export default class Emotes extends Module {
|
||||||
this.EmoteTypes = EmoteTypes;
|
this.EmoteTypes = EmoteTypes;
|
||||||
this.ModifierFlags = MODIFIER_FLAGS;
|
this.ModifierFlags = MODIFIER_FLAGS;
|
||||||
|
|
||||||
|
this.inject('i18n');
|
||||||
this.inject('settings');
|
this.inject('settings');
|
||||||
this.inject('experiments');
|
this.inject('experiments');
|
||||||
this.inject('staging');
|
this.inject('staging');
|
||||||
|
@ -604,46 +605,72 @@ export default class Emotes extends Module {
|
||||||
if ( ! addon_id )
|
if ( ! addon_id )
|
||||||
return this;
|
return this;
|
||||||
|
|
||||||
const overrides = {};
|
const is_dev = DEBUG || addon?.dev,
|
||||||
|
id_checker = makeAddonIdChecker(addon_id);
|
||||||
|
|
||||||
if ( addon?.dev ) {
|
const overrides = {},
|
||||||
overrides.addDefaultSet = (provider, ...args) => {
|
warnings = {};
|
||||||
if ( ! provider.includes(addon_id) )
|
|
||||||
|
overrides.addDefaultSet = (provider, set_id, data) => {
|
||||||
|
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);
|
||||||
|
|
||||||
return this.addDefaultSet(provider, ...args);
|
if ( data ) {
|
||||||
|
if ( is_dev && ! id_checker.test(set_id) )
|
||||||
|
module.log.warn('[DEV-CHECK] Call to emotes.addDefaultSet loaded set data but did not include addon ID in set ID:', set_id);
|
||||||
|
|
||||||
|
data.__source = addon_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.addDefaultSet(provider, set_id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
overrides.addSubSet = (provider, set_id, data) => {
|
||||||
|
if ( is_dev && ! id_checker.test(provider) )
|
||||||
|
module.log.warn('[DEV-CHECK] Call to emotes.addSubSet did not include addon ID in provider:', provider);
|
||||||
|
|
||||||
|
if ( data ) {
|
||||||
|
if ( is_dev && ! id_checker.test(set_id) )
|
||||||
|
module.log.warn('[DEV-CHECK] Call to emotes.addSubSet loaded set data but did not include addon ID in set ID:', set_id);
|
||||||
|
|
||||||
|
data.__source = addon_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.addSubSet(provider, set_id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
overrides.loadSetData = (set_id, data, ...args) => {
|
||||||
|
if ( is_dev && ! id_checker.test(set_id) )
|
||||||
|
module.log.warn('[DEV-CHECK] Call to emotes.loadSetData did not include addon ID in set ID:', set_id);
|
||||||
|
|
||||||
|
if ( data )
|
||||||
|
data.__source = addon_id;
|
||||||
|
|
||||||
|
return this.loadSetData(set_id, data, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( is_dev ) {
|
||||||
overrides.removeDefaultSet = (provider, ...args) => {
|
overrides.removeDefaultSet = (provider, ...args) => {
|
||||||
if ( ! provider.includes(addon_id) )
|
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);
|
||||||
|
|
||||||
return this.removeDefaultSet(provider, ...args);
|
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) => {
|
overrides.removeSubSet = (provider, ...args) => {
|
||||||
if ( ! provider.includes(addon_id) )
|
if ( ! id_checker.test(provider) )
|
||||||
module.log.warn('[DEV-CHECK] Call to emotes.removeSubSet did not include addon ID in provider:', provider);
|
module.log.warn('[DEV-CHECK] Call to emotes.removeSubSet did not include addon ID in provider:', provider);
|
||||||
|
|
||||||
return this.removeSubSet(provider, ...args);
|
return this.removeSubSet(provider, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
warnings.style = true;
|
||||||
|
warnings.effect_style = true;
|
||||||
|
warnings.emote_sets = true;
|
||||||
|
warnings.loadSetUserIds = warnings.loadSetUsers = 'This method is meant for internal use.';
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Proxy(this, {
|
return buildAddonProxy(module, this, 'emotes', overrides, warnings);
|
||||||
get(obj, prop) {
|
|
||||||
const thing = overrides[prop];
|
|
||||||
if ( thing )
|
|
||||||
return thing;
|
|
||||||
return Reflect.get(...arguments);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -690,14 +717,21 @@ export default class Emotes extends Module {
|
||||||
|
|
||||||
this.on('pubsub:command:add_emote', msg => {
|
this.on('pubsub:command:add_emote', msg => {
|
||||||
const set_id = msg.set_id,
|
const set_id = msg.set_id,
|
||||||
emote = msg.emote;
|
emote = msg.emote,
|
||||||
|
|
||||||
if ( ! this.emote_sets[set_id] )
|
emote_set = this.emote_sets[set_id];
|
||||||
|
|
||||||
|
if ( ! emote_set )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.addEmoteToSet(set_id, emote);
|
const has_old = !! emote_set.emotes?.[emote.id];
|
||||||
|
const processed = this.addEmoteToSet(set_id, emote);
|
||||||
|
|
||||||
// TODO: Notify users?
|
this.maybeNotifyChange(
|
||||||
|
has_old ? 'modified' : 'added',
|
||||||
|
set_id,
|
||||||
|
processed
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on('pubsub:command:remove_emote', msg => {
|
this.on('pubsub:command:remove_emote', msg => {
|
||||||
|
@ -707,9 +741,17 @@ export default class Emotes extends Module {
|
||||||
if ( ! this.emote_sets[set_id] )
|
if ( ! this.emote_sets[set_id] )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.removeEmoteFromSet(set_id, emote_id);
|
// If removing it returns nothing, there was no
|
||||||
|
// emote to remove with that ID.
|
||||||
|
const removed = this.removeEmoteFromSet(set_id, emote_id);
|
||||||
|
if ( ! removed )
|
||||||
|
return;
|
||||||
|
|
||||||
// TODO: Notify users?
|
this.maybeNotifyChange(
|
||||||
|
'removed',
|
||||||
|
set_id,
|
||||||
|
removed
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on('chat:reload-data', flags => {
|
this.on('chat:reload-data', flags => {
|
||||||
|
@ -717,10 +759,122 @@ export default class Emotes extends Module {
|
||||||
this.loadGlobalSets();
|
this.loadGlobalSets();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.on('addon:fully-unload', addon_id => {
|
||||||
|
let removed = 0;
|
||||||
|
for(const [key, set] of Object.entries(this.emote_sets)) {
|
||||||
|
if ( set?.__source === addon_id ) {
|
||||||
|
removed++;
|
||||||
|
this.loadSetData(key, null, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( removed ) {
|
||||||
|
this.log.debug(`Cleaned up ${removed} entries when unloading addon:`, addon_id);
|
||||||
|
// TODO: Debounced retokenize all chat messages.
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.loadGlobalSets();
|
this.loadGlobalSets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Chat Notices
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
maybeNotifyChange(action, set_id, emote) {
|
||||||
|
if ( ! this._pending_notifications )
|
||||||
|
this._pending_notifications = [];
|
||||||
|
|
||||||
|
this._pending_notifications.push({action, set_id, emote});
|
||||||
|
|
||||||
|
if ( ! this._pending_timer )
|
||||||
|
this._pending_timer = setTimeout(() => this._handleNotifyChange(), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleNotifyChange() {
|
||||||
|
clearTimeout(this._pending_timer);
|
||||||
|
this._pending_timer = null;
|
||||||
|
|
||||||
|
const notices = this._pending_notifications;
|
||||||
|
this._pending_notifications = null;
|
||||||
|
|
||||||
|
// Make sure we are equipped to send notices.
|
||||||
|
const chat = this.resolve('site.chat');
|
||||||
|
if ( ! chat?.addNotice )
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Get the current user.
|
||||||
|
const me = this.resolve('site').getUser();
|
||||||
|
if ( ! me?.id )
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Get the current channel.
|
||||||
|
let room_id = this.parent.context.get('context.channelID'),
|
||||||
|
room_login = this.parent.context.get('context.channel');
|
||||||
|
|
||||||
|
// And now get the current user's available emote sets.
|
||||||
|
const sets = this.getSetIDs(me.id, me.login, room_id, room_login);
|
||||||
|
const set_changes = {};
|
||||||
|
|
||||||
|
// Build a data structure for reducing the needed number of notices.
|
||||||
|
for(const notice of notices) {
|
||||||
|
// Make sure the set ID is a string.
|
||||||
|
const set_id = `${notice.set_id}`,
|
||||||
|
action = notice.action;
|
||||||
|
|
||||||
|
if ( sets.includes(set_id) ) {
|
||||||
|
const changes = set_changes[set_id] = set_changes[set_id] || {},
|
||||||
|
list = changes[action] = changes[action] || [];
|
||||||
|
|
||||||
|
// Deduplicate while we're at it.
|
||||||
|
if ( list.find(em => em.id === notice.emote.id) )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
list.push(notice.emote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over everything, sending chat notices.
|
||||||
|
for(const [set_id, notices] of Object.entries(set_changes)) {
|
||||||
|
const emote_set = this.emote_sets[set_id];
|
||||||
|
if ( ! emote_set )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for(const [action, emotes] of Object.entries(notices)) {
|
||||||
|
const emote_list = emotes
|
||||||
|
.map(emote => emote.name)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
let msg;
|
||||||
|
if ( action === 'added' )
|
||||||
|
msg = this.i18n.t('emote-updates.added', 'The {count, plural, one {emote {emotes} has} other {emotes {emotes} have}} been added to {set}.', {
|
||||||
|
count: emotes.length,
|
||||||
|
emotes: emote_list,
|
||||||
|
set: emote_set.title
|
||||||
|
});
|
||||||
|
|
||||||
|
else if ( action === 'modified' )
|
||||||
|
msg = this.i18n.t('emote-updates.modified', 'The {count, plural, one {emote {emotes} has} other {emotes {emotes} have}} been updated in {set}.', {
|
||||||
|
count: emotes.length,
|
||||||
|
emotes: emote_list,
|
||||||
|
set: emote_set.title
|
||||||
|
});
|
||||||
|
|
||||||
|
else if ( action === 'removed' )
|
||||||
|
msg = this.i18n.t('emote-updates.removed', 'The {count, plural, one {emote {emotes} has} other {emotes {emotes} have}} been removed from {set}.', {
|
||||||
|
count: emotes.length,
|
||||||
|
emotes: emote_list,
|
||||||
|
set: emote_set.title
|
||||||
|
});
|
||||||
|
|
||||||
|
if ( msg )
|
||||||
|
chat.addNotice('*', `[FFZ] ${msg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Load Modifier Effects
|
// Load Modifier Effects
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
@ -1845,6 +1999,9 @@ export default class Emotes extends Module {
|
||||||
|
|
||||||
// Send a loaded event because this emote set changed.
|
// Send a loaded event because this emote set changed.
|
||||||
this.emit(':loaded', set_id, set);
|
this.emit(':loaded', set_id, set);
|
||||||
|
|
||||||
|
// Return the processed emote object.
|
||||||
|
return processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1894,14 +2051,20 @@ export default class Emotes extends Module {
|
||||||
|
|
||||||
// Send a loaded event because this emote set changed.
|
// Send a loaded event because this emote set changed.
|
||||||
this.emit(':loaded', set_id, set);
|
this.emit(':loaded', set_id, set);
|
||||||
|
|
||||||
|
// Return the removed emote.
|
||||||
|
return emote;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
loadSetData(set_id, data, suppress_log = false) {
|
loadSetData(set_id, data, suppress_log = false) {
|
||||||
const old_set = this.emote_sets[set_id];
|
const old_set = this.emote_sets[set_id];
|
||||||
if ( ! data ) {
|
if ( ! data ) {
|
||||||
if ( old_set )
|
if ( old_set ) {
|
||||||
|
if ( this.style )
|
||||||
|
this.style.delete(`es--${set_id}`);
|
||||||
this.emote_sets[set_id] = null;
|
this.emote_sets[set_id] = null;
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,13 @@
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import Module from 'utilities/module';
|
import { DEBUG, LINK_DATA_HOSTS } from 'utilities/constants';
|
||||||
|
import Module, { buildAddonProxy } from 'utilities/module';
|
||||||
import {Color} from 'utilities/color';
|
import {Color} from 'utilities/color';
|
||||||
import {createElement, ManagedStyle} from 'utilities/dom';
|
import {createElement, ManagedStyle} from 'utilities/dom';
|
||||||
import {FFZEvent} from 'utilities/events';
|
import {FFZEvent} from 'utilities/events';
|
||||||
import {getFontsList} from 'utilities/fonts';
|
import {getFontsList} from 'utilities/fonts';
|
||||||
import {timeout, has, addWordSeparators, glob_to_regex, escape_regex, split_chars} from 'utilities/object';
|
import {timeout, has, addWordSeparators, glob_to_regex, escape_regex, split_chars, makeAddonIdChecker} from 'utilities/object';
|
||||||
|
|
||||||
import Badges from './badges';
|
import Badges from './badges';
|
||||||
import Emotes from './emotes';
|
import Emotes from './emotes';
|
||||||
|
@ -25,7 +26,6 @@ import * as RICH_PROVIDERS from './rich_providers';
|
||||||
import * as LINK_PROVIDERS from './link_providers';
|
import * as LINK_PROVIDERS from './link_providers';
|
||||||
|
|
||||||
import Actions from './actions/actions';
|
import Actions from './actions/actions';
|
||||||
import { LINK_DATA_HOSTS } from 'src/utilities/constants';
|
|
||||||
|
|
||||||
|
|
||||||
function sortPriorityColorTerms(list) {
|
function sortPriorityColorTerms(list) {
|
||||||
|
@ -1281,6 +1281,208 @@ export default class Chat extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getAddonProxy(addon_id, addon, module) {
|
||||||
|
if ( ! addon_id )
|
||||||
|
return this;
|
||||||
|
|
||||||
|
const is_dev = DEBUG || addon?.dev,
|
||||||
|
id_checker = makeAddonIdChecker(addon_id);
|
||||||
|
|
||||||
|
const overrides = {},
|
||||||
|
warnings = {};
|
||||||
|
|
||||||
|
const user_proxy = buildAddonProxy(module, null, 'getUser()', {
|
||||||
|
addBadge: is_dev ? function(provider, ...args) {
|
||||||
|
if ( ! id_checker.test(provider) )
|
||||||
|
module.log.warn('[DEV-CHECK] Call to getUser().addBadge() did not include addon ID in provider:', provider);
|
||||||
|
|
||||||
|
return this.addBadge(provider, ...args);
|
||||||
|
} : undefined,
|
||||||
|
|
||||||
|
removeBadge: is_dev ? function(provider, ...args) {
|
||||||
|
if ( ! id_checker.test(provider) )
|
||||||
|
module.log.warn('[DEV-CHECK] Call to getUser().removeBadge() did not include addon ID in provider:', provider);
|
||||||
|
|
||||||
|
return this.removeBadge(provider, ...args);
|
||||||
|
} : undefined,
|
||||||
|
|
||||||
|
addSet(provider, set_id, data) {
|
||||||
|
if ( is_dev && ! id_checker.test(provider) )
|
||||||
|
module.log.warn('[DEV-CHECK] Call to getUser().addSet() did not include addon ID in provider:', provider);
|
||||||
|
|
||||||
|
if ( data ) {
|
||||||
|
if ( is_dev && ! id_checker.test(set_id) )
|
||||||
|
module.log.warn('[DEV-CHECK] Call to getUser().addSet() loaded set data but did not include addon ID in set ID:', set_id);
|
||||||
|
data.__source = addon_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.addSet(provider, set_id, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeAllSets: is_dev ? function(provider) {
|
||||||
|
if ( ! id_checker.test(provider) )
|
||||||
|
module.log.warn('[DEV-CHECK] Call to getUser().removeAllSets() did not include addon ID in provider:', provider);
|
||||||
|
|
||||||
|
return this.removeAllSets(provider);
|
||||||
|
} : undefined,
|
||||||
|
|
||||||
|
removeSet: is_dev ? function(provider, ...args) {
|
||||||
|
if ( ! id_checker.test(provider) )
|
||||||
|
module.log.warn('[DEV-CHECK] Call to getUser().removeSet() did not include addon ID in provider:', provider);
|
||||||
|
|
||||||
|
return this.removeSet(provider, ...args);
|
||||||
|
} : undefined
|
||||||
|
|
||||||
|
}, is_dev ? {
|
||||||
|
badges: 'Please use addBadge(), getBadge(), or removeBadge()',
|
||||||
|
emote_sets: 'Please use addSet(), removeSet(), or removeAllSets()',
|
||||||
|
room: true
|
||||||
|
} : null, true);
|
||||||
|
|
||||||
|
const room_proxy = buildAddonProxy(module, null, 'getRoom()', {
|
||||||
|
getUser(...args) {
|
||||||
|
const result = this.getUser(...args);
|
||||||
|
if ( result )
|
||||||
|
return new Proxy(result, user_proxy);
|
||||||
|
},
|
||||||
|
|
||||||
|
addSet(provider, set_id, data) {
|
||||||
|
if ( is_dev && ! id_checker.test(provider) )
|
||||||
|
module.log.warn('[DEV-CHECK] Call to getRoom().addSet() did not include addon ID in provider:', provider);
|
||||||
|
|
||||||
|
if ( data ) {
|
||||||
|
if ( is_dev && ! id_checker.test(set_id) )
|
||||||
|
module.log.warn('[DEV-CHECK] Call to getRoom().addSet() loaded set data but did not include addon ID in set ID:', set_id);
|
||||||
|
data.__source = addon_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.addSet(provider, set_id, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeAllSets: is_dev ? function(provider) {
|
||||||
|
if ( ! id_checker.test(provider) )
|
||||||
|
module.log.warn('[DEV-CHECK] Call to getRoom().removeAllSets() did not include addon ID in provider:', provider);
|
||||||
|
|
||||||
|
return this.removeAllSets(provider);
|
||||||
|
} : undefined,
|
||||||
|
|
||||||
|
removeSet: is_dev ? function(provider, ...args) {
|
||||||
|
if ( ! id_checker.test(provider) )
|
||||||
|
module.log.warn('[DEV-CHECK] Call to getRoom().removeSet() did not include addon ID in provider:', provider);
|
||||||
|
|
||||||
|
return this.removeSet(provider, ...args);
|
||||||
|
} : undefined
|
||||||
|
|
||||||
|
}, {
|
||||||
|
badges: true,
|
||||||
|
load_data: true,
|
||||||
|
emote_sets: 'Please use addSet(), removeSet(), or removeAllSets()',
|
||||||
|
refs: 'Please use ref() or unref()',
|
||||||
|
style: true,
|
||||||
|
users: 'Please use getUser()',
|
||||||
|
user_ids: 'Please use getUser()'
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
overrides.iterateUsers = function*() {
|
||||||
|
for(const user of this.iterateUsers())
|
||||||
|
yield new Proxy(user, user_proxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
overrides.iterateRooms = function*() {
|
||||||
|
for(const room of this.iterateRooms())
|
||||||
|
yield new Proxy(room, room_proxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
overrides.iterateAllRoomsAndUsers = function*() {
|
||||||
|
for(const thing of this.iterateAllRoomsAndUsers())
|
||||||
|
yield new Proxy(thing, (thing instanceof Room)
|
||||||
|
? room_proxy
|
||||||
|
: user_proxy
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
overrides.addTokenizer = tokenizer => {
|
||||||
|
if ( tokenizer )
|
||||||
|
tokenizer.__source = addon_id;
|
||||||
|
|
||||||
|
return this.addTokenizer(tokenizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
overrides.addLinkProvider = provider => {
|
||||||
|
if ( provider )
|
||||||
|
provider.__source = addon_id;
|
||||||
|
|
||||||
|
return this.addLinkProvider(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
overrides.addRichProvider = provider => {
|
||||||
|
if ( provider )
|
||||||
|
provider.__source = addon_id;
|
||||||
|
|
||||||
|
return this.addRichProvider(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( is_dev ) {
|
||||||
|
overrides.getUser = (...args) => {
|
||||||
|
let result = this.getUser(...args);
|
||||||
|
if ( result )
|
||||||
|
return new Proxy(result, user_proxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
overrides.getRoom = (...args) => {
|
||||||
|
let result = this.getRoom(...args);
|
||||||
|
if ( result )
|
||||||
|
return new Proxy(result, room_proxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
overrides.removeTokenizer = tokenizer => {
|
||||||
|
let type;
|
||||||
|
if ( typeof tokenizer === 'string' )
|
||||||
|
type = tokenizer;
|
||||||
|
else
|
||||||
|
type = tokenizer.type;
|
||||||
|
|
||||||
|
const existing = this.tokenizers[type];
|
||||||
|
if ( existing && existing.__source !== addon_id )
|
||||||
|
module.log.warn('[DEV-CHECK] Removed un-owned tokenizer with chat.removeTokenizer:', type, ' owner:', existing.__source ?? 'ffz');
|
||||||
|
|
||||||
|
return this.removeTokenizer(tokenizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
overrides.removeLinkProvider = provider => {
|
||||||
|
let type;
|
||||||
|
if ( typeof provider === 'string' )
|
||||||
|
type = provider;
|
||||||
|
else
|
||||||
|
type = provider.type;
|
||||||
|
|
||||||
|
const existing = this.link_providers[type];
|
||||||
|
if ( existing && existing.__source !== addon_id )
|
||||||
|
module.log.warn('[DEV-CHECK] Removed un-owned link provider with chat.removeLinkProvider:', type, ' owner:', existing.__source ?? 'ffz');
|
||||||
|
|
||||||
|
return this.removeLinkProvider(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
overrides.removeRichProvider = provider => {
|
||||||
|
let type;
|
||||||
|
if ( typeof provider === 'string' )
|
||||||
|
type = provider;
|
||||||
|
else
|
||||||
|
type = provider.type;
|
||||||
|
|
||||||
|
const existing = this.link_providers[type];
|
||||||
|
if ( existing && existing.__source !== addon_id )
|
||||||
|
module.log.warn('[DEV-CHECK] Removed un-owned rich provider with chat.removeRichProvider:', type, ' owner:', existing.__source ?? 'ffz');
|
||||||
|
|
||||||
|
return this.removeRichProvider(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildAddonProxy(module, this, 'chat', overrides, warnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
onEnable() {
|
onEnable() {
|
||||||
this.socket = this.resolve('socket');
|
this.socket = this.resolve('socket');
|
||||||
this.pubsub = this.resolve('pubsub');
|
this.pubsub = this.resolve('pubsub');
|
||||||
|
@ -1339,6 +1541,40 @@ export default class Chat extends Module {
|
||||||
|
|
||||||
this.triggered_reload = false;
|
this.triggered_reload = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.on('addon:fully-unload', addon_id => {
|
||||||
|
let removed = 0;
|
||||||
|
for(const [key, def] of Object.entries(this.link_providers)) {
|
||||||
|
if ( def?.__source === addon_id ) {
|
||||||
|
removed++;
|
||||||
|
this.removeLinkProvider(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const [key, def] of Object.entries(this.rich_providers)) {
|
||||||
|
if ( def?.__source === addon_id ) {
|
||||||
|
removed++;
|
||||||
|
this.removeRichProvider(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const [key, def] of Object.entries(this.tokenizers)) {
|
||||||
|
if ( def?.__source === addon_id ) {
|
||||||
|
removed++;
|
||||||
|
this.removeTokenizer(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const item of this.iterateAllRoomsAndUsers())
|
||||||
|
removed += item._unloadAddon(addon_id) ?? 0;
|
||||||
|
|
||||||
|
// If we removed things, retokenize all chat messages.
|
||||||
|
// TODO: Debounce this.
|
||||||
|
if ( removed ) {
|
||||||
|
this.log.debug(`Cleaned up ${removed} entries when unloading addon:`, addon_id);
|
||||||
|
this.emit(':update-line-tokens');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1500,21 +1736,46 @@ export default class Chat extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
*iterateAllRoomsAndUsers() {
|
||||||
|
for(const room of this.iterateRooms()) {
|
||||||
|
yield room;
|
||||||
|
for(const user of room.iterateUsers())
|
||||||
|
yield user;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const user of this.iterateUsers())
|
||||||
|
yield user;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
*iterateUsers() {
|
||||||
|
const visited = new Set;
|
||||||
|
|
||||||
|
for(const user of Object.values(this.user_ids)) {
|
||||||
|
if ( user && ! user.destroyed ) {
|
||||||
|
visited.add(user);
|
||||||
|
yield user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const user of Object.values(this.users)) {
|
||||||
|
if ( user && ! user.destroyed )
|
||||||
|
yield user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
*iterateRooms() {
|
*iterateRooms() {
|
||||||
const visited = new Set;
|
const visited = new Set;
|
||||||
|
|
||||||
for(const id in this.room_ids)
|
for(const room of Object.values(this.room_ids)) {
|
||||||
if ( has(this.room_ids, id) ) {
|
|
||||||
const room = this.room_ids[id];
|
|
||||||
if ( room && ! room.destroyed ) {
|
if ( room && ! room.destroyed ) {
|
||||||
visited.add(room);
|
visited.add(room);
|
||||||
yield room;
|
yield room;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for(const login in this.rooms)
|
for(const room of Object.values(this.rooms)) {
|
||||||
if ( has(this.rooms, login) ) {
|
|
||||||
const room = this.rooms[login];
|
|
||||||
if ( room && ! room.destroyed && ! visited.has(room) )
|
if ( room && ! room.destroyed && ! visited.has(room) )
|
||||||
yield room;
|
yield room;
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,6 +150,12 @@ export default class Room {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_unloadAddon(addon_id) {
|
||||||
|
// TODO: This
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
get id() {
|
get id() {
|
||||||
return this._id;
|
return this._id;
|
||||||
}
|
}
|
||||||
|
@ -188,6 +194,23 @@ export default class Room {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
*iterateUsers() {
|
||||||
|
const visited = new Set;
|
||||||
|
|
||||||
|
for(const user of Object.values(this.user_ids)) {
|
||||||
|
if ( user && ! user.destroyed ) {
|
||||||
|
visited.add(user);
|
||||||
|
yield user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const user of Object.values(this.users)) {
|
||||||
|
if ( user && ! user.destroyed )
|
||||||
|
yield user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
getUser(id, login, no_create, no_login, error = false) {
|
getUser(id, login, no_create, no_login, error = false) {
|
||||||
if ( this.destroyed )
|
if ( this.destroyed )
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -64,6 +64,11 @@ export default class User {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_unloadAddon(addon_id) {
|
||||||
|
// TODO: This
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
get id() {
|
get id() {
|
||||||
return this._id;
|
return this._id;
|
||||||
}
|
}
|
||||||
|
@ -153,6 +158,18 @@ export default class User {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
removeAllBadges(provider) {
|
||||||
|
if ( this.destroyed || ! this.badges )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if ( ! this.badges.has(provider) )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Just yeet them all since we don't ref badges.
|
||||||
|
this.badges.delete(provider);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Emote Sets
|
// Emote Sets
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {duration_to_string, durationForURL} from 'utilities/time';
|
||||||
|
|
||||||
import Tooltip from 'utilities/tooltip';
|
import Tooltip from 'utilities/tooltip';
|
||||||
import Module from 'utilities/module';
|
import Module from 'utilities/module';
|
||||||
|
import { DEBUG } from 'src/utilities/constants';
|
||||||
|
|
||||||
const CLIP_URL = /^https:\/\/[^/]+\.(?:twitch\.tv|twitchcdn\.net)\/.+?\.mp4(?:\?.*)?$/;
|
const CLIP_URL = /^https:\/\/[^/]+\.(?:twitch\.tv|twitchcdn\.net)\/.+?\.mp4(?:\?.*)?$/;
|
||||||
|
|
||||||
|
@ -558,11 +559,12 @@ export default class Metadata extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getAddonProxy(addon_id) {
|
getAddonProxy(addon_id, addon, module) {
|
||||||
if ( ! addon_id )
|
if ( ! addon_id )
|
||||||
return this;
|
return this;
|
||||||
|
|
||||||
const overrides = {};
|
const overrides = {},
|
||||||
|
is_dev = DEBUG || addon?.dev;
|
||||||
|
|
||||||
overrides.define = (key, definition) => {
|
overrides.define = (key, definition) => {
|
||||||
if ( definition )
|
if ( definition )
|
||||||
|
@ -576,6 +578,9 @@ export default class Metadata extends Module {
|
||||||
const thing = overrides[prop];
|
const thing = overrides[prop];
|
||||||
if ( thing )
|
if ( thing )
|
||||||
return thing;
|
return thing;
|
||||||
|
if ( prop === 'definitions' && is_dev )
|
||||||
|
module.log.warn('[DEV-CHECK] Accessed metadata.definitions directly. Please use define()');
|
||||||
|
|
||||||
return Reflect.get(...arguments);
|
return Reflect.get(...arguments);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -627,8 +632,10 @@ export default class Metadata extends Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( removed.size )
|
if ( removed.size ) {
|
||||||
|
this.log.debug(`Cleaned up ${removed.size} entries when unloading addon:`, addon_id);
|
||||||
this.updateMetadata([...removed]);
|
this.updateMetadata([...removed]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {has, maybe_call, once} from 'utilities/object';
|
||||||
import Tooltip from 'utilities/tooltip';
|
import Tooltip from 'utilities/tooltip';
|
||||||
import Module from 'utilities/module';
|
import Module from 'utilities/module';
|
||||||
import awaitMD, {getMD} from 'utilities/markdown';
|
import awaitMD, {getMD} from 'utilities/markdown';
|
||||||
|
import { DEBUG } from 'src/utilities/constants';
|
||||||
|
|
||||||
export default class TooltipProvider extends Module {
|
export default class TooltipProvider extends Module {
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
|
@ -74,6 +75,41 @@ export default class TooltipProvider extends Module {
|
||||||
this.onFSChange = this.onFSChange.bind(this);
|
this.onFSChange = this.onFSChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getAddonProxy(addon_id, addon, module) {
|
||||||
|
if ( ! addon_id )
|
||||||
|
return this;
|
||||||
|
|
||||||
|
const overrides = {},
|
||||||
|
is_dev = DEBUG || addon?.dev;
|
||||||
|
|
||||||
|
overrides.define = (key, handler) => {
|
||||||
|
if ( handler )
|
||||||
|
handler.__source = addon_id;
|
||||||
|
|
||||||
|
return this.define(key, handler);
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( is_dev )
|
||||||
|
overrides.cleanup = () => {
|
||||||
|
module.log.warn('[DEV-CHECK] Instead of calling tooltips.cleanup(), you can emit the event "tooltips:cleanup"');
|
||||||
|
return this.cleanup();
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Proxy(this, {
|
||||||
|
get(obj, prop) {
|
||||||
|
const thing = overrides[prop];
|
||||||
|
if ( thing )
|
||||||
|
return thing;
|
||||||
|
if ( prop === 'types' && is_dev )
|
||||||
|
module.log.warn('[DEV-CHECK] Accessed tooltips.types directly. Please use tooltips.define()');
|
||||||
|
|
||||||
|
return Reflect.get(...arguments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
onEnable() {
|
onEnable() {
|
||||||
const container = this.getRoot();
|
const container = this.getRoot();
|
||||||
|
|
||||||
|
@ -86,7 +122,28 @@ export default class TooltipProvider extends Module {
|
||||||
this.tips = this._createInstance(container);
|
this.tips = this._createInstance(container);
|
||||||
|
|
||||||
this.on(':cleanup', this.cleanup);
|
this.on(':cleanup', this.cleanup);
|
||||||
|
|
||||||
|
this.on('addon:fully-unload', addon_id => {
|
||||||
|
let removed = 0;
|
||||||
|
for(const [key, handler] of Object.entries(this.types)) {
|
||||||
|
if ( handler?.__source === addon_id ) {
|
||||||
|
removed++;
|
||||||
|
this.types[key] = undefined;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( removed ) {
|
||||||
|
this.log.debug(`Cleaned up ${removed} entries when unloading addon:`, addon_id);
|
||||||
|
this.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
define(key, handler) {
|
||||||
|
this.types[key] = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
getRoot() { // eslint-disable-line class-methods-use-this
|
getRoot() { // eslint-disable-line class-methods-use-this
|
||||||
return document.querySelector('.sunlight-root') ||
|
return document.querySelector('.sunlight-root') ||
|
||||||
|
|
|
@ -146,12 +146,12 @@ export default class PubSub extends Module {
|
||||||
} : null
|
} : null
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('connect', () => {
|
client.on('connect', msg => {
|
||||||
this.log.info('Connected to PubSub.');
|
this.log.info('Connected to PubSub.', msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('disconnect', () => {
|
client.on('disconnect', msg => {
|
||||||
this.log.info('Disconnected from PubSub.');
|
this.log.info('Disconnected from PubSub.', msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('error', err => {
|
client.on('error', err => {
|
||||||
|
|
|
@ -132,7 +132,7 @@ export class Module extends EventEmitter {
|
||||||
return Promise.reject(new CyclicDependencyError(`cyclic load requirements when loading ${initial}`, [...chain, this]));
|
return Promise.reject(new CyclicDependencyError(`cyclic load requirements when loading ${initial}`, [...chain, this]));
|
||||||
else if ( this.load_requires )
|
else if ( this.load_requires )
|
||||||
for(const name of this.load_requires) {
|
for(const name of this.load_requires) {
|
||||||
const module = this.resolve(name);
|
const module = this.__resolve(name);
|
||||||
if ( module && chain.includes(module) )
|
if ( module && chain.includes(module) )
|
||||||
return Promise.reject(new CyclicDependencyError(`cyclic load requirements when loading ${initial}`, [...chain, this, module]));
|
return Promise.reject(new CyclicDependencyError(`cyclic load requirements when loading ${initial}`, [...chain, this, module]));
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,7 @@ export class Module extends EventEmitter {
|
||||||
if ( this.load_requires ) {
|
if ( this.load_requires ) {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for(const name of this.load_requires) {
|
for(const name of this.load_requires) {
|
||||||
const module = this.resolve(name);
|
const module = this.__resolve(name);
|
||||||
if ( ! module || !(module instanceof Module) )
|
if ( ! module || !(module instanceof Module) )
|
||||||
throw new ModuleError(`cannot find required module ${name} when loading ${path}`);
|
throw new ModuleError(`cannot find required module ${name} when loading ${path}`);
|
||||||
|
|
||||||
|
@ -194,7 +194,7 @@ export class Module extends EventEmitter {
|
||||||
chain.push(this);
|
chain.push(this);
|
||||||
|
|
||||||
for(const dep of this.load_dependents) {
|
for(const dep of this.load_dependents) {
|
||||||
const module = this.resolve(dep);
|
const module = this.__resolve(dep);
|
||||||
if ( module ) {
|
if ( module ) {
|
||||||
if ( chain.includes(module) )
|
if ( chain.includes(module) )
|
||||||
throw new CyclicDependencyError(`cyclic load requirements when checking if can unload ${initial}`, [...chain, this, module]);
|
throw new CyclicDependencyError(`cyclic load requirements when checking if can unload ${initial}`, [...chain, this, module]);
|
||||||
|
@ -229,7 +229,7 @@ export class Module extends EventEmitter {
|
||||||
return Promise.reject(new CyclicDependencyError(`cyclic load requirements when unloading ${initial}`, [...chain, this]));
|
return Promise.reject(new CyclicDependencyError(`cyclic load requirements when unloading ${initial}`, [...chain, this]));
|
||||||
else if ( this.load_dependents )
|
else if ( this.load_dependents )
|
||||||
for(const dep of this.load_dependents) {
|
for(const dep of this.load_dependents) {
|
||||||
const module = this.resolve(dep);
|
const module = this.__resolve(dep);
|
||||||
if ( module && chain.includes(module) )
|
if ( module && chain.includes(module) )
|
||||||
return Promise.reject(new CyclicDependencyError(`cyclic load requirements when unloading ${initial}`, [...chain, this, module]));
|
return Promise.reject(new CyclicDependencyError(`cyclic load requirements when unloading ${initial}`, [...chain, this, module]));
|
||||||
}
|
}
|
||||||
|
@ -257,8 +257,8 @@ export class Module extends EventEmitter {
|
||||||
if ( this.load_dependents ) {
|
if ( this.load_dependents ) {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for(const name of this.load_dependents) {
|
for(const name of this.load_dependents) {
|
||||||
const module = this.resolve(name);
|
const module = this.__resolve(name);
|
||||||
if ( ! module )
|
if ( ! module || !(module instanceof Module) )
|
||||||
//throw new ModuleError(`cannot find depending module ${name} when unloading ${path}`);
|
//throw new ModuleError(`cannot find depending module ${name} when unloading ${path}`);
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
@ -296,7 +296,7 @@ export class Module extends EventEmitter {
|
||||||
return Promise.reject(new CyclicDependencyError(`cyclic requirements when enabling ${initial}`, [...chain, this]));
|
return Promise.reject(new CyclicDependencyError(`cyclic requirements when enabling ${initial}`, [...chain, this]));
|
||||||
else if ( this.requires )
|
else if ( this.requires )
|
||||||
for(const name of this.requires) {
|
for(const name of this.requires) {
|
||||||
const module = this.resolve(name);
|
const module = this.__resolve(name);
|
||||||
if ( module && chain.includes(module) )
|
if ( module && chain.includes(module) )
|
||||||
return Promise.reject(new CyclicDependencyError(`cyclic requirements when enabling ${initial}`, [...chain, this, module]));
|
return Promise.reject(new CyclicDependencyError(`cyclic requirements when enabling ${initial}`, [...chain, this, module]));
|
||||||
}
|
}
|
||||||
|
@ -329,7 +329,7 @@ export class Module extends EventEmitter {
|
||||||
|
|
||||||
if ( requires )
|
if ( requires )
|
||||||
for(const name of requires) {
|
for(const name of requires) {
|
||||||
const module = this.resolve(name);
|
const module = this.__resolve(name);
|
||||||
if ( ! module || !(module instanceof Module) )
|
if ( ! module || !(module instanceof Module) )
|
||||||
throw new ModuleError(`cannot find required module ${name} when enabling ${path}`);
|
throw new ModuleError(`cannot find required module ${name} when enabling ${path}`);
|
||||||
|
|
||||||
|
@ -368,8 +368,8 @@ export class Module extends EventEmitter {
|
||||||
chain.push(this);
|
chain.push(this);
|
||||||
|
|
||||||
for(const dep of this.dependents) {
|
for(const dep of this.dependents) {
|
||||||
const module = this.resolve(dep);
|
const module = this.__resolve(dep);
|
||||||
if ( module ) {
|
if ( module && (module instanceof Module) ) {
|
||||||
if ( chain.includes(module) )
|
if ( chain.includes(module) )
|
||||||
throw new CyclicDependencyError(`cyclic load requirements when checking if can disable ${initial}`, [...chain, this, module]);
|
throw new CyclicDependencyError(`cyclic load requirements when checking if can disable ${initial}`, [...chain, this, module]);
|
||||||
|
|
||||||
|
@ -400,7 +400,7 @@ export class Module extends EventEmitter {
|
||||||
return Promise.reject(new CyclicDependencyError(`cyclic requirements when disabling ${initial}`, [...chain, this]));
|
return Promise.reject(new CyclicDependencyError(`cyclic requirements when disabling ${initial}`, [...chain, this]));
|
||||||
else if ( this.dependents )
|
else if ( this.dependents )
|
||||||
for(const dep of this.dependents) {
|
for(const dep of this.dependents) {
|
||||||
const module = this.resolve(dep);
|
const module = this.__resolve(dep);
|
||||||
if ( module && chain.includes(module) )
|
if ( module && chain.includes(module) )
|
||||||
return Promise.reject(new CyclicDependencyError(`cyclic requirements when disabling ${initial}`, [...chain, this, dep]));
|
return Promise.reject(new CyclicDependencyError(`cyclic requirements when disabling ${initial}`, [...chain, this, dep]));
|
||||||
}
|
}
|
||||||
|
@ -430,8 +430,8 @@ export class Module extends EventEmitter {
|
||||||
if ( this.dependents ) {
|
if ( this.dependents ) {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for(const name of this.dependents) {
|
for(const name of this.dependents) {
|
||||||
const module = this.resolve(name);
|
const module = this.__resolve(name);
|
||||||
if ( ! module )
|
if ( ! module || !(module instanceof Module) )
|
||||||
// Assume a non-existent module isn't enabled.
|
// Assume a non-existent module isn't enabled.
|
||||||
//throw new ModuleError(`cannot find depending module ${name} when disabling ${path}`);
|
//throw new ModuleError(`cannot find depending module ${name} when disabling ${path}`);
|
||||||
continue;
|
continue;
|
||||||
|
@ -499,19 +499,19 @@ export class Module extends EventEmitter {
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
loadModules(...names) {
|
loadModules(...names) {
|
||||||
return Promise.all(names.map(n => this.resolve(n).load()))
|
return Promise.all(names.map(n => this.__resolve(n).load()))
|
||||||
}
|
}
|
||||||
|
|
||||||
unloadModules(...names) {
|
unloadModules(...names) {
|
||||||
return Promise.all(names.map(n => this.resolve(n).unload()))
|
return Promise.all(names.map(n => this.__resolve(n).unload()))
|
||||||
}
|
}
|
||||||
|
|
||||||
enableModules(...names) {
|
enableModules(...names) {
|
||||||
return Promise.all(names.map(n => this.resolve(n).enable()))
|
return Promise.all(names.map(n => this.__resolve(n).enable()))
|
||||||
}
|
}
|
||||||
|
|
||||||
disableModules(...names) {
|
disableModules(...names) {
|
||||||
return Promise.all(names.map(n => this.resolve(n).disable()))
|
return Promise.all(names.map(n => this.__resolve(n).disable()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -519,13 +519,24 @@ export class Module extends EventEmitter {
|
||||||
// Module Management
|
// Module Management
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
resolve(name) {
|
__resolve(name) {
|
||||||
if ( name instanceof Module )
|
if ( name instanceof Module )
|
||||||
return name;
|
return name;
|
||||||
|
|
||||||
return this.__modules[this.abs_path(name)];
|
return this.__modules[this.abs_path(name)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolve(name) {
|
||||||
|
let module = this.__resolve(name);
|
||||||
|
if ( !(module instanceof Module) )
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if ( this.__processModule )
|
||||||
|
module = this.__processModule(module);
|
||||||
|
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
hasModule(name) {
|
hasModule(name) {
|
||||||
const module = this.__modules[this.abs_path(name)];
|
const module = this.__modules[this.abs_path(name)];
|
||||||
|
@ -549,7 +560,7 @@ export class Module extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
__processModule(module, name) {
|
__processModule(module) {
|
||||||
if ( this.addon_root && module.getAddonProxy ) {
|
if ( this.addon_root && module.getAddonProxy ) {
|
||||||
const addon_id = this.addon_id;
|
const addon_id = this.addon_id;
|
||||||
if ( ! module.__proxies )
|
if ( ! module.__proxies )
|
||||||
|
@ -558,7 +569,7 @@ export class Module extends EventEmitter {
|
||||||
if ( module.__proxies[addon_id] )
|
if ( module.__proxies[addon_id] )
|
||||||
return module.__proxies[addon_id];
|
return module.__proxies[addon_id];
|
||||||
|
|
||||||
const addon = this.resolve('addons')?.getAddon?.(addon_id),
|
const addon = this.__resolve('addons')?.getAddon?.(addon_id),
|
||||||
out = module.getAddonProxy(addon_id, addon, this.addon_root, this);
|
out = module.getAddonProxy(addon_id, addon, this.addon_root, this);
|
||||||
|
|
||||||
if ( out !== module )
|
if ( out !== module )
|
||||||
|
@ -596,7 +607,7 @@ export class Module extends EventEmitter {
|
||||||
// Just a Name
|
// Just a Name
|
||||||
const full_name = name;
|
const full_name = name;
|
||||||
name = name.replace(/^(?:[^.]*\.)+/, '');
|
name = name.replace(/^(?:[^.]*\.)+/, '');
|
||||||
module = this.resolve(full_name);
|
module = this.__resolve(full_name);
|
||||||
|
|
||||||
// Allow injecting a module that doesn't exist yet?
|
// Allow injecting a module that doesn't exist yet?
|
||||||
|
|
||||||
|
@ -625,8 +636,8 @@ export class Module extends EventEmitter {
|
||||||
|
|
||||||
module.references.push([this.__path, name]);
|
module.references.push([this.__path, name]);
|
||||||
|
|
||||||
if ( this.__processModule )
|
if ( (module instanceof Module) && this.__processModule )
|
||||||
module = this.__processModule(module, name);
|
module = this.__processModule(module);
|
||||||
|
|
||||||
return this[name] = module;
|
return this[name] = module;
|
||||||
}
|
}
|
||||||
|
@ -657,7 +668,7 @@ export class Module extends EventEmitter {
|
||||||
// Just a Name
|
// Just a Name
|
||||||
const full_name = name;
|
const full_name = name;
|
||||||
name = name.replace(/^(?:[^.]*\.)+/, '');
|
name = name.replace(/^(?:[^.]*\.)+/, '');
|
||||||
module = this.resolve(full_name);
|
module = this.__resolve(full_name);
|
||||||
|
|
||||||
// Allow injecting a module that doesn't exist yet?
|
// Allow injecting a module that doesn't exist yet?
|
||||||
|
|
||||||
|
@ -687,7 +698,7 @@ export class Module extends EventEmitter {
|
||||||
|
|
||||||
module.references.push([this.__path, variable]);
|
module.references.push([this.__path, variable]);
|
||||||
|
|
||||||
if ( this.__processModule )
|
if ( (module instanceof Module) && this.__processModule )
|
||||||
module = this.__processModule(module, name);
|
module = this.__processModule(module, name);
|
||||||
|
|
||||||
return this[variable] = module;
|
return this[variable] = module;
|
||||||
|
@ -711,9 +722,9 @@ export class Module extends EventEmitter {
|
||||||
if ( old_val instanceof Module )
|
if ( old_val instanceof Module )
|
||||||
throw new ModuleError(`Name Collision for Module ${path}`);
|
throw new ModuleError(`Name Collision for Module ${path}`);
|
||||||
|
|
||||||
const dependents = old_val || [[], [], []],
|
const dependents = old_val || [[], [], []];
|
||||||
inst = this.__modules[path] = new module(name, this),
|
let inst = this.__modules[path] = new module(name, this);
|
||||||
requires = inst.requires = inst.__get_requires() || [],
|
const requires = inst.requires = inst.__get_requires() || [],
|
||||||
load_requires = inst.load_requires = inst.__get_load_requires() || [];
|
load_requires = inst.load_requires = inst.__get_load_requires() || [];
|
||||||
|
|
||||||
inst.dependents = dependents[0];
|
inst.dependents = dependents[0];
|
||||||
|
@ -748,13 +759,16 @@ export class Module extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
for(const [in_path, in_name] of dependents[2]) {
|
for(const [in_path, in_name] of dependents[2]) {
|
||||||
const in_mod = this.resolve(in_path);
|
const in_mod = this.__resolve(in_path);
|
||||||
if ( in_mod )
|
if ( in_mod )
|
||||||
in_mod[in_name] = inst;
|
in_mod[in_name] = inst;
|
||||||
else
|
else
|
||||||
this.log.warn(`Unable to find module "${in_path}" that wanted "${in_name}".`);
|
this.log.warn(`Unable to find module "${in_path}" that wanted "${in_name}".`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( (inst instanceof Module) && this.__processModule )
|
||||||
|
inst = this.__processModule(inst, name);
|
||||||
|
|
||||||
if ( inject_reference )
|
if ( inject_reference )
|
||||||
this[name] = inst;
|
this[name] = inst;
|
||||||
|
|
||||||
|
@ -806,6 +820,45 @@ export class SiteModule extends Module {
|
||||||
export default Module;
|
export default Module;
|
||||||
|
|
||||||
|
|
||||||
|
export function buildAddonProxy(accessor, thing, name, overrides, access_warnings, no_proxy = false) {
|
||||||
|
|
||||||
|
const handler = {
|
||||||
|
get(obj, prop) {
|
||||||
|
// First, handle basic overrides behavior.
|
||||||
|
let value = overrides[prop];
|
||||||
|
if ( value !== undefined ) {
|
||||||
|
// Check for functions, and bind their this.
|
||||||
|
if ( typeof value === 'function' )
|
||||||
|
return value.bind(obj);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, handle access warnings.
|
||||||
|
const warning = access_warnings && access_warnings[prop];
|
||||||
|
if ( accessor?.log && warning )
|
||||||
|
accessor.log.warn(`[DEV-CHECK] Accessed ${name}.${prop} directly. ${typeof warning === 'string' ? warning : ''}`)
|
||||||
|
|
||||||
|
// Check for functions, and bind their this.
|
||||||
|
value = obj[prop];
|
||||||
|
if ( typeof value === 'function' )
|
||||||
|
return value.bind(obj);
|
||||||
|
|
||||||
|
// Make sure all module access is proxied.
|
||||||
|
if ( accessor && (value instanceof Module) )
|
||||||
|
return accessor.resolve(value);
|
||||||
|
|
||||||
|
// Return whatever it would be normally.
|
||||||
|
return Reflect.get(...arguments);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return no_proxy ? handler : new Proxy(thing, handler);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Module.buildAddonProxy = buildAddonProxy;
|
||||||
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Errors
|
// Errors
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
@ -580,6 +580,27 @@ export function deep_copy(object, seen) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function normalizeAddonIdForComparison(input) {
|
||||||
|
return input.toLowerCase().replace(/[\.\_\-]+/, '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeAddonIdChecker(input) {
|
||||||
|
input = escape_regex(normalizeAddonIdForComparison(input));
|
||||||
|
input = input.replace(/-+/g, '[\.\_\-]+');
|
||||||
|
|
||||||
|
// Special: ffzap-bttv
|
||||||
|
input = input.replace(/\bbttv\b/g, '(?:bttv|betterttv)');
|
||||||
|
|
||||||
|
// Special: which seven tho
|
||||||
|
input = input.replace(/\b7tv\b/g, '(?:7tv|seventv)');
|
||||||
|
|
||||||
|
// Special: pronouns (badges)
|
||||||
|
input = input.replace(/\bpronouns\b/g, '(?:pronouns|addon-pn)');
|
||||||
|
|
||||||
|
return new RegExp('\\b' + input + '\\b', 'i');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function maybe_call(fn, ctx, ...args) {
|
export function maybe_call(fn, ctx, ...args) {
|
||||||
if ( typeof fn === 'function' ) {
|
if ( typeof fn === 'function' ) {
|
||||||
if ( ctx )
|
if ( ctx )
|
||||||
|
|
|
@ -46,6 +46,11 @@ export default class PubSubClient extends EventEmitter {
|
||||||
// Topics is a map of topics to sub-topic IDs.
|
// Topics is a map of topics to sub-topic IDs.
|
||||||
this._topics = new Map;
|
this._topics = new Map;
|
||||||
|
|
||||||
|
// Incorrect Topics is a map of every incorrect topic mapping
|
||||||
|
// we are currently subscribed to, for the purpose of unsubscribing
|
||||||
|
// from them.
|
||||||
|
this._incorrect_topics = new Map;
|
||||||
|
|
||||||
// Live Topics is a set of every topic we have sent subscribe
|
// Live Topics is a set of every topic we have sent subscribe
|
||||||
// packets to the server for.
|
// packets to the server for.
|
||||||
this._live_topics = new Set;
|
this._live_topics = new Set;
|
||||||
|
@ -60,6 +65,8 @@ export default class PubSubClient extends EventEmitter {
|
||||||
// Debounce a few things.
|
// Debounce a few things.
|
||||||
this.scheduleHeartbeat = this.scheduleHeartbeat.bind(this);
|
this.scheduleHeartbeat = this.scheduleHeartbeat.bind(this);
|
||||||
this._sendHeartbeat = this._sendHeartbeat.bind(this);
|
this._sendHeartbeat = this._sendHeartbeat.bind(this);
|
||||||
|
this.scheduleResub = this.scheduleResub.bind(this);
|
||||||
|
this._sendResub = this._sendResub.bind(this);
|
||||||
|
|
||||||
this._fetchNewTopics = this._fetchNewTopics.bind(this);
|
this._fetchNewTopics = this._fetchNewTopics.bind(this);
|
||||||
this._sendSubscribes = debounce(this._sendSubscribes, 250);
|
this._sendSubscribes = debounce(this._sendSubscribes, 250);
|
||||||
|
@ -157,9 +164,28 @@ export default class PubSubClient extends EventEmitter {
|
||||||
// Record all the topic mappings we just got.
|
// Record all the topic mappings we just got.
|
||||||
// TODO: Check for subtopic mismatches.
|
// TODO: Check for subtopic mismatches.
|
||||||
// TODO: Check for removed subtopic assignments.
|
// TODO: Check for removed subtopic assignments.
|
||||||
if ( data.topics )
|
if ( data.topics ) {
|
||||||
for(const [key, val] of Object.entries(data.topics))
|
const removed = new Set(this._topics.keys());
|
||||||
|
|
||||||
|
for(const [key, val] of Object.entries(data.topics)) {
|
||||||
|
removed.delete(key);
|
||||||
|
const existing = this._topics.get(key);
|
||||||
|
if ( existing != null && existing != val )
|
||||||
|
this._incorrect_topics.set(key, existing);
|
||||||
this._topics.set(key, val);
|
this._topics.set(key, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const key of removed) {
|
||||||
|
const existing = this._topics.get(key);
|
||||||
|
this._topics.delete(key);
|
||||||
|
this._incorrect_topics.set(key, existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a mismatch, handle it.
|
||||||
|
if ( this._incorrect_topics.size )
|
||||||
|
this._sendUnsubscribes()
|
||||||
|
.then(() => this._sendSubscribes());
|
||||||
|
}
|
||||||
|
|
||||||
// Update the heartbeat timer.
|
// Update the heartbeat timer.
|
||||||
this.scheduleHeartbeat();
|
this.scheduleHeartbeat();
|
||||||
|
@ -307,7 +333,7 @@ export default class PubSubClient extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Client Management
|
// Keep Alives
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
clearHeartbeat() {
|
clearHeartbeat() {
|
||||||
|
@ -329,10 +355,38 @@ export default class PubSubClient extends EventEmitter {
|
||||||
.finally(this.scheduleHeartbeat);
|
.finally(this.scheduleHeartbeat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
clearResubTimer() {
|
||||||
|
if ( this._resub_timer ) {
|
||||||
|
clearTimeout(this._resub_timer);
|
||||||
|
this._resub_timer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleResub() {
|
||||||
|
if ( this._resub_timer )
|
||||||
|
clearTimeout(this._resub_timer);
|
||||||
|
|
||||||
|
// Send a resubscription every 30 minutes.
|
||||||
|
this._resub_timer = setTimeout(this._sendResub, 30 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendResub() {
|
||||||
|
this._resendSubscribes()
|
||||||
|
.finally(this.scheduleResub);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Client Management
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
_destroyClient() {
|
_destroyClient() {
|
||||||
if ( ! this._client )
|
if ( ! this._client )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
this.clearResubTimer();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this._client.disconnect().catch(() => {});
|
this._client.disconnect().catch(() => {});
|
||||||
} catch(err) { /* no-op */ }
|
} catch(err) { /* no-op */ }
|
||||||
|
@ -361,8 +415,14 @@ export default class PubSubClient extends EventEmitter {
|
||||||
maxMessagesPerSecond: 10
|
maxMessagesPerSecond: 10
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let disconnected = false;
|
||||||
|
|
||||||
this._client.onMqttMessage = message => {
|
this._client.onMqttMessage = message => {
|
||||||
if ( message.type === DISCONNECT ) {
|
if ( message.type === DISCONNECT ) {
|
||||||
|
if ( disconnected )
|
||||||
|
return;
|
||||||
|
|
||||||
|
disconnected = true;
|
||||||
this.emit('disconnect', message);
|
this.emit('disconnect', message);
|
||||||
this._destroyClient();
|
this._destroyClient();
|
||||||
|
|
||||||
|
@ -441,12 +501,64 @@ export default class PubSubClient extends EventEmitter {
|
||||||
clean: true
|
clean: true
|
||||||
}).then(msg => {
|
}).then(msg => {
|
||||||
this._state = State.Connected;
|
this._state = State.Connected;
|
||||||
this.emit('connect');
|
this.emit('connect', msg);
|
||||||
|
|
||||||
|
// Periodically re-send our subscriptions. This
|
||||||
|
// is done because the broker seems to be forgetful
|
||||||
|
// for long-lived connections.
|
||||||
|
this.scheduleResub();
|
||||||
|
|
||||||
|
// Reconnect when this connection ends.
|
||||||
|
if ( client.connectionCompletion )
|
||||||
|
client.connectionCompletion.finally(() => {
|
||||||
|
if ( disconnected )
|
||||||
|
return;
|
||||||
|
|
||||||
|
disconnected = true;
|
||||||
|
this.emit('disconnect', null);
|
||||||
|
this._destroyClient();
|
||||||
|
|
||||||
|
if ( this._should_connect )
|
||||||
|
this._createClient(data);
|
||||||
|
});
|
||||||
|
|
||||||
return this._sendSubscribes()
|
return this._sendSubscribes()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_resendSubscribes() {
|
||||||
|
if ( ! this._client )
|
||||||
|
return Promise.resolve();
|
||||||
|
|
||||||
|
const topics = [],
|
||||||
|
batch = [];
|
||||||
|
|
||||||
|
for(const topic of this._live_topics) {
|
||||||
|
const subtopic = this._topics.get(topic);
|
||||||
|
if ( subtopic != null ) {
|
||||||
|
if ( subtopic === 0 )
|
||||||
|
topics.push(topic);
|
||||||
|
else
|
||||||
|
topics.push(`${topic}/s${subtopic}`);
|
||||||
|
|
||||||
|
batch.push(topic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! topics.length )
|
||||||
|
return Promise.resolve();
|
||||||
|
|
||||||
|
return this._client.subscribe({topicFilter: topics})
|
||||||
|
.catch(() => {
|
||||||
|
// If there was an error, we did NOT subscribe.
|
||||||
|
for(const topic of batch)
|
||||||
|
this._live_topics.delete(topic);
|
||||||
|
|
||||||
|
if ( this._live_topics.size != this._active_topics.size )
|
||||||
|
return sleep(2000).then(() => this._sendSubscribes());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_sendSubscribes() {
|
_sendSubscribes() {
|
||||||
if ( ! this._client )
|
if ( ! this._client )
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
@ -474,7 +586,9 @@ export default class PubSubClient extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( topics.length )
|
if ( ! topics.length )
|
||||||
|
return Promise.resolve();
|
||||||
|
|
||||||
return this._client.subscribe({topicFilter: topics })
|
return this._client.subscribe({topicFilter: topics })
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// If there was an error, we did NOT subscribe.
|
// If there was an error, we did NOT subscribe.
|
||||||
|
@ -482,17 +596,17 @@ export default class PubSubClient extends EventEmitter {
|
||||||
this._live_topics.delete(topic);
|
this._live_topics.delete(topic);
|
||||||
|
|
||||||
// Call sendSubscribes again after a bit.
|
// Call sendSubscribes again after a bit.
|
||||||
|
if ( this._live_topics.size != this._active_topics.size )
|
||||||
return sleep(2000).then(() => this._sendSubscribes());
|
return sleep(2000).then(() => this._sendSubscribes());
|
||||||
});
|
});
|
||||||
else
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_sendUnsubscribes() {
|
_sendUnsubscribes() {
|
||||||
if ( ! this._client )
|
if ( ! this._client )
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
|
||||||
const topics = [];
|
const topics = [],
|
||||||
|
batch = new Set;
|
||||||
|
|
||||||
// iterate over a copy to support removal
|
// iterate over a copy to support removal
|
||||||
for(const topic of [...this._live_topics]) {
|
for(const topic of [...this._live_topics]) {
|
||||||
|
@ -511,17 +625,32 @@ export default class PubSubClient extends EventEmitter {
|
||||||
real_topic = `${topic}/s${subtopic}`;
|
real_topic = `${topic}/s${subtopic}`;
|
||||||
|
|
||||||
topics.push(real_topic);
|
topics.push(real_topic);
|
||||||
|
batch.add(topic);
|
||||||
this._live_topics.delete(topic);
|
this._live_topics.delete(topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( topics.length )
|
// handle incorrect topics
|
||||||
|
for(const [topic, subtopic] of this._incorrect_topics) {
|
||||||
|
if ( batch.has(topic) )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
batch.add(topic);
|
||||||
|
if ( subtopic === 0 )
|
||||||
|
topics.push(topic);
|
||||||
|
else
|
||||||
|
topics.push(`${topic}/s${subtopic}`);
|
||||||
|
|
||||||
|
this._live_topics.delete(topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! topics.length )
|
||||||
|
return Promise.resolve();
|
||||||
|
|
||||||
return this._client.unsubscribe({topicFilter: topics})
|
return this._client.unsubscribe({topicFilter: topics})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if ( this.logger )
|
if ( this.logger )
|
||||||
this.logger.warn('Received error when unsubscribing from topics:', error);
|
this.logger.warn('Received error when unsubscribing from topics:', error);
|
||||||
});
|
});
|
||||||
else
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue