diff --git a/package.json b/package.json index 2f3f0466..49317366 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.68.1", + "version": "4.68.2", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/experiments.json b/src/experiments.json index 836acc1d..65114fb4 100644 --- a/src/experiments.json +++ b/src/experiments.json @@ -16,8 +16,16 @@ {"value": false, "weight": 0} ] }, + "emqx_pubsub": { + "name": "EMQX MQTT-Based PubSub", + "description": "An experimental pubsub system running on an EMQX cluster, to see how that performs.", + "groups": [ + {"value": true, "weight": 25}, + {"value": false, "weight": 75} + ] + }, "cf_pubsub": { - "name": "MQTT-Based PubSub", + "name": "CF MQTT-Based PubSub", "description": "An experimental new pubsub system that should be more reliable than the existing socket cluster.", "groups": [ {"value": true, "weight": 0}, diff --git a/src/experiments.ts b/src/experiments.ts index 98587996..5fb8da02 100644 --- a/src/experiments.ts +++ b/src/experiments.ts @@ -229,6 +229,7 @@ export default class ExperimentManager extends Module<'experiments', ExperimentE this.cache = new Map; } + getControlsLocked() { if ( DEBUG ) return false; diff --git a/src/modules/emote_card/index.jsx b/src/modules/emote_card/index.jsx index b182b5b3..bc236f9f 100644 --- a/src/modules/emote_card/index.jsx +++ b/src/modules/emote_card/index.jsx @@ -104,7 +104,7 @@ export default class EmoteCard extends Module { canReportTwitch() { const site = this.resolve('site'), - core = site.getCore?.(), + //core = site.getCore?.(), user = site.getUser(), web_munch = this.resolve('site.web_munch'); @@ -115,13 +115,13 @@ export default class EmoteCard extends Module { return false; } - return !! report_form && !! user?.id && core?.store?.dispatch; + return !! report_form && !! user?.id && site?.store?.dispatch; } reportTwitchEmote(id, channel) { const site = this.resolve('site'), - core = site.getCore(), + //core = site.getCore(), user = site.getUser(), web_munch = this.resolve('site.web_munch'); @@ -132,10 +132,10 @@ export default class EmoteCard extends Module { return false; } - if ( ! user?.id || ! core?.store?.dispatch ) + if ( ! user?.id || ! site?.store?.dispatch ) return false; - core.store.dispatch({ + site.store.dispatch({ type: 'core.modal.MODAL_SHOWN', modalComponent: report_form, modalProps: { diff --git a/src/pubsub/index.ts b/src/pubsub/index.ts index 6e0a88b7..bab3cea9 100644 --- a/src/pubsub/index.ts +++ b/src/pubsub/index.ts @@ -65,6 +65,8 @@ export default class PubSub extends Module<'pubsub', PubSubEvents> { this.settings.add('pubsub.use-cluster', { default: () => { + if ( this.experiments.getAssignment('emqx_pubsub') ) + return 'EMQXTest'; if ( this.experiments.getAssignment('cf_pubsub') ) return 'Staging'; return null; diff --git a/src/sites/twitch-twilight/index.js b/src/sites/twitch-twilight/index.js index 0caa66e7..3884e1c2 100644 --- a/src/sites/twitch-twilight/index.js +++ b/src/sites/twitch-twilight/index.js @@ -265,6 +265,12 @@ Twilight.KNOWN_MODULES = { stack: n.fQ, dispatch: n.vJ }; + + if ( has(n.fQ?._currentValue, 'highlights') && typeof n.vJ?._currentValue === 'function' ) + return { + stack: n.fQ, + dispatch: n.vJ + }; } } @@ -272,6 +278,7 @@ Twilight.KNOWN_MODULES = { const VEND_CORE = n => ! n || n.includes('vendor') || n.includes('core'); Twilight.KNOWN_MODULES.core.use_result = true; +Twilight.KNOWN_MODULES.core.skip_cache = true; //Twilight.KNOWN_MODULES.core.chunks = 'core'; Twilight.KNOWN_MODULES.simplebar.chunks = VEND_CORE; diff --git a/src/sites/twitch-twilight/modules/channel.jsx b/src/sites/twitch-twilight/modules/channel.jsx index e011ed28..0e50cbbc 100644 --- a/src/sites/twitch-twilight/modules/channel.jsx +++ b/src/sites/twitch-twilight/modules/channel.jsx @@ -334,6 +334,19 @@ export default class Channel extends Module { } } + if ( ! el.querySelector('ffz--native-viewers-container') ) { + let i = 0, + vel = el.querySelector('p[data-a-target="animated-channel-viewers-count"]'); + while(vel && vel != el && i < 5) { + if ( vel.querySelector('svg') ) { + vel.classList.add('ffz--native-viewers-container'); + break; + } + vel = vel.parentElement; + i++; + } + } + const react = this.fine.getReactInstance(el); let props = react?.child?.memoizedProps; if ( ! props?.channelLogin ) diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/hide-native-viewers.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/hide-native-viewers.scss index 99cd7e3e..0690270e 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/hide-native-viewers.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/hide-native-viewers.scss @@ -1,6 +1,7 @@ .ffz--meta-tray { .tw-svg svg[type="color-text-accessible-red"], + .ffz--native-viewers-container, p[data-a-target="animated-channel-viewers-count"] { display: none !important; } -} \ No newline at end of file +} diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/hide-unfollow-button.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/hide-unfollow-button.scss index 6f18a55e..8550da0c 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/hide-unfollow-button.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/hide-unfollow-button.scss @@ -1,3 +1,7 @@ button[data-test-selector="unfollow-button"] { display: none !important; -} \ No newline at end of file +} + +div[data-target="channel-header-right"] > div { + margin-right: unset !important; +} diff --git a/src/sites/twitch-twilight/styles/chat.scss b/src/sites/twitch-twilight/styles/chat.scss index 36e2b3d0..68523beb 100644 --- a/src/sites/twitch-twilight/styles/chat.scss +++ b/src/sites/twitch-twilight/styles/chat.scss @@ -251,6 +251,7 @@ .ffz--giftee-name { cursor: pointer; outline: none; + word-break: keep-all; &:hover { text-decoration: underline; diff --git a/src/utilities/compat/webmunch.js b/src/utilities/compat/webmunch.js index e95119ce..6a5977f7 100644 --- a/src/utilities/compat/webmunch.js +++ b/src/utilities/compat/webmunch.js @@ -16,7 +16,7 @@ const regex_cache = {}; function getRequireRegex(name) { if ( ! regex_cache[name] ) - regex_cache[name] = new RegExp(`\\b${name}\\(([0-9e_+]+)\\)`, 'g'); + regex_cache[name] = new RegExp(`\\b(?> 1; const retain = (controlPacketFlags & 1) === 1; @@ -704,13 +704,13 @@ var _Bytes = class _Bytes { } static async create(opts) { const { DEBUG } = Mqtt; - const { hostname, port } = opts; + const { hostname, port, pathname } = opts; if ("accept" in WebSocket.prototype) { if (DEBUG) console.log("Found WebSocket.accept, using Cloudflare workaround"); if (port !== 443) throw new Error(`Cloudflare Workers only support outgoing WebSocket requests on port 443 (https)`); - const url2 = `https://${hostname}`; + const url2 = `https://${hostname}${port ? `:${port}` : ''}${pathname ?? ''}`; if (DEBUG) console.log(`Fetching ${url2}`); const resp = await fetch(url2, { headers: { upgrade: "websocket" } }); @@ -724,7 +724,7 @@ var _Bytes = class _Bytes { console.log("Accepted!"); return new _WebSocketConnection(webSocket); } - const url = `wss://${hostname}:${port}`; + const url = `wss://${hostname}${port ? `:${port}` : ''}${pathname ?? ''}`; const ws = new WebSocket(url, "mqtt"); if (DEBUG) console.log(`new WebSocket('${url}', 'mqtt')`); @@ -790,9 +790,10 @@ var _Bytes = class _Bytes { this.receivedDisconnect = false; /** @internal */ this.nextPacketId = 1; - const { hostname, port, protocol, maxMessagesPerSecond } = opts; + const { hostname, port, pathname, protocol, maxMessagesPerSecond } = opts; this.hostname = hostname; this.port = port; + this.pathname = pathname; this.protocol = protocol; this.maxMessagesPerSecond = maxMessagesPerSecond; } @@ -837,9 +838,9 @@ var _Bytes = class _Bytes { async connect(opts) { const { DEBUG } = Mqtt; const { clientId = "", username, password, keepAlive = DEFAULT_KEEP_ALIVE_SECONDS, clean = false } = opts; - const { protocol, hostname, port } = this; + const { protocol, hostname, port, pathname } = this; if (!this.connection) { - this.connection = await _MqttClient.protocolHandlers[protocol]({ hostname, port }); + this.connection = await _MqttClient.protocolHandlers[protocol]({ hostname, port, pathname }); this.connection.onRead = (bytes) => { this.processBytes(bytes); }; diff --git a/src/utilities/pubsub.js b/src/utilities/pubsub.js index 475a212a..a219e121 100644 --- a/src/utilities/pubsub.js +++ b/src/utilities/pubsub.js @@ -2,6 +2,7 @@ import { EventEmitter } from "./events"; import { MqttClient, DISCONNECT } from './custom_denoflare_mqtt'; // "denoflare-mqtt"; import { b64ToArrayBuffer, debounce, importRsaKey, make_enum, sleep } from "./object"; +import { EMQX_SERVERS } from "./constants"; // Only match 1-4 digit numbers, to avoid matching twitch IDs. // 9999 gives us millions of clients on a topic, so we're never @@ -112,30 +113,46 @@ export default class PubSubClient extends EventEmitter { async _loadData() { let response, data; - try { - // TODO: Send removed topics. - response = await fetch(this.server, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - id: this.id, - user: this.user ?? null, - topics: this.topics - }) - }); - if ( response.ok ) - data = await response.json(); + if ( this.server === 'emqx-test' ) { + // Hard-coded data. + const server = EMQX_SERVERS[Math.floor(Math.random() * EMQX_SERVERS.length)]; - } catch(err) { - throw new Error( - 'Unable to load PubSub data from server.', - { - cause: err - } - ); + data = { + require_signing: false, + endpoint: `wss://${server}:8084/mqtt`, + + username: 'anonymous', + password: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbm9ueW1vdXMifQ.5DZP1bScMz4-MV_jGZveUKq4pFy9x_PJF9gSzAvj-wA`, + topics: Object.fromEntries(this.topics.map(x => [x, 0])) + } + + } else { + try { + // TODO: Send removed topics. + response = await fetch(this.server, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + id: this.id, + user: this.user ?? null, + topics: this.topics + }) + }); + + if ( response.ok ) + data = await response.json(); + + } catch(err) { + throw new Error( + 'Unable to load PubSub data from server.', + { + cause: err + } + ); + } } if ( ! data?.endpoint ) @@ -411,6 +428,7 @@ export default class PubSubClient extends EventEmitter { const client = this._client = new MqttClient({ hostname: url.hostname, port: url.port ?? undefined, + pathname: url.pathname ?? undefined, protocol: 'wss', maxMessagesPerSecond: 10 }); @@ -444,9 +462,13 @@ export default class PubSubClient extends EventEmitter { return; } + let payload = message.payload; + if ( payload instanceof Uint8Array ) + payload = new TextDecoder().decode(payload); + let msg; try { - msg = JSON.parse(message.payload); + msg = JSON.parse(payload); } catch(err) { if ( this.logger ) this.logger.warn(`Error decoding PubSub message on topic "${topic}":`, err); @@ -496,10 +518,12 @@ export default class PubSubClient extends EventEmitter { return this._client.connect({ clientId: data.client_id, + username: data.username, password: data.password, keepAlive: 120, clean: true }).then(msg => { + this._conn_failures = 0; this._state = State.Connected; this.emit('connect', msg); @@ -523,6 +547,30 @@ export default class PubSubClient extends EventEmitter { }); return this._sendSubscribes() + }).catch(err => { + if ( this.logger ) + this.logger.debug('Error connecting to MQTT.', err); + + disconnected = true; + this.emit('disconnect', null); + + this._destroyClient(); + + if ( ! this._should_connect ) + return; + + this._conn_failures = (this._conn_failures || 0) + 1; + let delay = (this._conn_failures * Math.floor(Math.random() * 10) + 2) * 1000; + if ( delay > 60000 ) + delay = (Math.floor(Math.random() * 60) + 30) * 1000; + + if ( delay <= 2000 ) + delay = 2000; + + return sleep(delay).then(() => { + if ( this._should_connect && ! this._client ) + this._createClient(data); + }) }); } @@ -673,4 +721,4 @@ export default class PubSubClient extends EventEmitter { }); } -} \ No newline at end of file +}