class PleromaModLoader { constructor() { this.config = { 'modDirectory': '/instance/pleroma-mods/', 'mods': [] } this.loadConfig() this.loadedMods = {} this.classes = {} } loadMods() { this.config.mods.forEach((mod) => { this.loadMod(mod) }) } loadMod(modName) { return PleromaModLoader.includeScript( this.config.modDirectory + 'pleroma-mod-' + modName + '/mod.js' ).then(() => { this.loadedMods[ modName ] = new this.classes[ PleromaModLoader.getClassName(modName) ]() }) } loadConfig() { window.fetch('/instance/pleroma-mod-config.json').then((response) => { return response.json() }).then((json) => { Object.keys(json).forEach((key) => { this.config[ key ] = json[ key ] }) this.loadMods() }).catch((error) => { console.error('can\'t load loader config') console.error(error) }) } registerClass(className, object) { this.classes[ className ] = object } waitUntilReady() { const postPanel = document.querySelector('.status-container') const loginPanel = document.querySelector('.login-form') if (postPanel || loginPanel) { Object.keys(this.loadedMods).forEach((modName) => { const settings = document.querySelector('.settings-modal div[label]:first-child') if (settings) { if (!settings.querySelector('.mod-settings')) { this.appendModSettings(settings) } } const mod = this.loadedMods[ modName ] if (mod.isEnabled) { mod.ready() } }) this.createObserver() } else { console.warn('not ready, trying again in 1s') window.setTimeout(() => { this.waitUntilReady() }, 1000) } } createCheckbox(label, mod) { const labelElement = document.createElement('label') labelElement.classList.add('checkbox') const input = document.createElement('input') input.setAttribute('type', 'checkbox') input.checked = mod.isEnabled input.addEventListener('change', (event) => { if (event.target.checked) { mod.enable() } else { mod.disable() } }) labelElement.appendChild(input) const fakeCheckbox = document.createElement('i') fakeCheckbox.classList.add('checkbox-indicator') labelElement.appendChild(fakeCheckbox) const text = document.createElement('span') text.classList.add('label') text.innerText = label labelElement.appendChild(text) return labelElement } appendModSettings(element) { const container = document.createElement('div') container.classList.add('setting-item') container.classList.add('mod-settings') const title = document.createElement('h2') title.innerText = 'Pleroma Mods' container.appendChild(title) const optionList = document.createElement('ul') optionList.classList.add('setting-list') Object.keys(this.loadedMods).sort().forEach((modName) => { const li = document.createElement('li') const enable = this.createCheckbox('enable ' + modName, this.loadedMods[ modName ]) li.appendChild(enable) const ulConfig = document.createElement('ul') ulConfig.classList.add('setting-list') Object.keys(this.loadedMods[ modName ].config).forEach((key) => { if (key === 'includes' || key === 'filter') { return } this.loadedMods[ modName ].onSettingInit(key, ulConfig, document.createElement('li')) }) li.appendChild(ulConfig) optionList.appendChild(li) }) container.appendChild(optionList) element.appendChild(container) } createObserver() { this.containers = { main: document.querySelector('.main'), notifications: document.querySelector('.notifications'), userPanel: document.querySelector('.user-panel'), settingsModal: document.querySelector('.settings-modal') } const observerConfig = { subtree: true, childList: true } this.observer = new MutationObserver((mutations, observer) => { const modal = document.querySelector('.settings-modal div[label]:first-child') if (modal && !modal.querySelector('.mod-settings')) { this.appendModSettings(modal) } Object.values(this.loadedMods).forEach((mod) => { if (mod.isEnabled) { mutations.forEach((mutation) => { mod.mutate(mutation, observer) }) } }) }) this.observer.observe(this.containers.main, observerConfig) if (this.containers.notifications) { this.observer.observe(this.containers.notifications, observerConfig) } if (this.containers.userPanel) { this.observer.observe(this.containers.userPanel, observerConfig) } if (this.containers.settingsModal) { this.observer.observe(this.containers.settingsModal, observerConfig) } } static registerMod(mod) { window.__pleromaModLoader.registerClass(mod.name, mod) } static includeScript(src) { console.log('include ' + src) return new Promise((resolve) => { const script = document.createElement('script') script.setAttribute('src', src) script.setAttribute('type', 'text/javascript') script.onload = () => { resolve() } document.querySelector('body').appendChild(script) }) } static includeCss(src) { console.log('include ' + src) return new Promise((resolve) => { const link = document.createElement('link') link.setAttribute('href', src) link.setAttribute('rel', 'stylesheet') link.setAttribute('type', 'text/css') link.onload = () => { resolve() } document.querySelector('head').appendChild(link) }) } static excludeScript(src) { return new Promise((resolve) => { const script = document.querySelector('script[src=\'' + src + '\']') if (script) { script.remove() } resolve() }) } static excludeCss(src) { return new Promise((resolve) => { const link = document.querySelector('link[href=\'' + src + '\']') if (link) { link.remove() } resolve() }) } static getVueScope(element) { if (!element) { return null } if (element.__vue__) { console.warn('old vue version, please update pleroma-fe') return element.__vue__ } if (element._vnode) { return element._vnode } if (element.__vnode) { return element.__vnode } if (element.parentNode) { return PleromaModLoader.getVueScope(element.parentNode) } return null } static getVueComponent(element) { if (!element) { return null } if (element.__vnode && element.__vnode.component) { return element.__vnode.component } if (element.__vueParentComponent) { return element.__vueParentComponent.ctx } if (element.__vueComponent__) { return element.__vueComponent__ } if (element.parentNode) { return PleromaModLoader.getVueComponent(element.parentNode) } return null } static getRootVueScope() { return PleromaModLoader.getVueScope(document.querySelector('#app')) } static getToken() { return PleromaModLoader.getRootVueScope().appContext.provides.store.getters.getUserToken() } static getModDir() { return window.__pleromaModLoader.config.modDirectory } static getClassName(name) { let className = 'PleromaMod' name.split('-').forEach((namePart) => { className += namePart.substring(0, 1).toUpperCase() className += namePart.substring(1) }) return className } static api(method, path, params) { return new Promise((resolve, reject) => { const token = PleromaModLoader.getToken() const xhr = new XMLHttpRequest() xhr.responseType = 'json' xhr.open(method, path) xhr.setRequestHeader('Content-Type', 'application/json') xhr.setRequestHeader('Authorization', 'Bearer ' + token) xhr.onreadstatechange = () => { if (xhr.readyState === 4) { if (xhr.status !== 200) { reject(new Error({ status: xhr.status, response: xhr.response, xhr: xhr })) } } } xhr.onload = () => { resolve(xhr.response) } xhr.send(JSON.stringify(params)) }) } } class PleromaMod { // eslint-disable-line no-unused-vars constructor(name) { this.name = name this.config = {} this.isRunning = false } get isEnabled() { return localStorage.getItem('pleroma_mod_' + this.name + '_enabled') !== 'false' || true } getClassName() { return PleromaModLoader.getClassName(this.name) } getModDir() { return PleromaModLoader.getModDir() + this.name + '/' } ready() { this.onReady() this.run() } destroy() { this.isRunning = false if (this.config.includes) { const styles = this.config.includes.css || [] const scripts = this.config.includes.js || [] Promise.all(styles.map((style) => { return this.excludeCss(style) }).concat(scripts.map((script) => { return this.excludeScript(script) }))).then(() => { this.onDestroy() }) return } this.onDestroy() } run() { if (this.config.includes) { const styles = this.config.includes.css || [] const scripts = this.config.includes.js || [] Promise.all(styles.map((style) => { return this.includeCss(style) }).concat(scripts.map((script) => { return this.includeScript(script) })).concat([ this.loadConfig(), this.onCreate() ])).then(() => { this.isRunning = true this.onRun() }) return } this.isRunning = true this.onRun() } mutate(mutation, observer) { if (this.isRunning) { this.onMutation(mutation, observer) } } saveConfig() { const storedConfig = {} Object.keys(this.config).filter((key) => { return key !== 'includes' && key !== 'filter' }).forEach((key) => { storedConfig[ key ] = this.config[ key ] }) localStorage.setItem(this.name + '_config', JSON.stringify(storedConfig)) } mergeConfig(newConfig) { Object.keys(newConfig).forEach((key) => { this.config[ key ] = JSON.parse(JSON.stringify(newConfig[ key ])) }) } loadConfig() { return new Promise((resolve) => { // TODO: use structuredClone when its more supported this.defaultConfig = JSON.parse(JSON.stringify(this.config)) const storedConfig = JSON.parse(localStorage.getItem(this.name + '_config')) this.onConfigLoad().then((json) => { this.mergeConfig(json) if (storedConfig) { this.mergeConfig(storedConfig) } this.saveConfig() resolve() }) }) } onReady() { } onCreate() { return new Promise((resolve) => { resolve() }) } onDestroy() { } onRun() { } onMutation(mutation, observer) { } onConfigLoad() { return new Promise((resolve) => { resolve({}) }) } onSettingInit(key, ul, li) { } includeCss(src) { return PleromaModLoader.includeCss(PleromaModLoader.getModDir() + this.name + '/' + src) } includeScript(src) { return PleromaModLoader.includeScript(PleromaModLoader.getModDir() + this.name + '/' + src) } excludeCss(src) { return PleromaModLoader.excludeCss(PleromaModLoader.getModDir() + this.name + '/' + src) } excludeScript(src) { return PleromaModLoader.excludeScript(PleromaModLoader.getModDir() + this.name + '/' + src) } fetchJson(src) { console.log('loading ' + src) return window.fetch(PleromaModLoader.getModDir() + this.name + '/' + src).then((response) => { return response.json() }) } api(method, path, params) { return PleromaModLoader.api(method, path, params) } enable() { this.ready() localStorage.setItem('pleroma_mod_' + this.name + '_enabled', true) } disable() { this.destroy() localStorage.setItem('pleroma_mod_' + this.name + '_enabled', false) } } window.__pleromaModLoader = new PleromaModLoader() window.__pleromaModLoader.waitUntilReady()