pleroma-fe/static/pleroma-mod-loader.js

478 lines
12 KiB
JavaScript
Executable File

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