pleroma-fe/instance/pleroma-mod-loader.js
Sam Therapy b343a88d77
All checks were successful
continuous-integration/drone/push Build is passing
Use akkoma maybe
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2022-11-30 17:01:38 +01:00

479 lines
12 KiB
JavaScript

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