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();