1
0
Fork 0

WIP plugins: hook on client side

This commit is contained in:
Chocobozzz 2019-07-08 15:54:08 +02:00 committed by Chocobozzz
parent 2c0539420d
commit 18a6f04c07
11 changed files with 215 additions and 3 deletions

View file

@ -3,6 +3,10 @@
"target": "http://localhost:9000", "target": "http://localhost:9000",
"secure": false "secure": false
}, },
"/plugins": {
"target": "http://localhost:9000",
"secure": false
},
"/static": { "/static": {
"target": "http://localhost:9000", "target": "http://localhost:9000",
"secure": false "secure": false

View file

@ -9,6 +9,7 @@ import { Hotkey, HotkeysService } from 'angular2-hotkeys'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
import { fromEvent } from 'rxjs' import { fromEvent } from 'rxjs'
import { ViewportScroller } from '@angular/common' import { ViewportScroller } from '@angular/common'
import { PluginService } from '@app/core/plugins/plugin.service'
@Component({ @Component({
selector: 'my-app', selector: 'my-app',
@ -27,6 +28,7 @@ export class AppComponent implements OnInit {
private router: Router, private router: Router,
private authService: AuthService, private authService: AuthService,
private serverService: ServerService, private serverService: ServerService,
private pluginService: PluginService,
private domSanitizer: DomSanitizer, private domSanitizer: DomSanitizer,
private redirectService: RedirectService, private redirectService: RedirectService,
private screenService: ScreenService, private screenService: ScreenService,
@ -69,6 +71,8 @@ export class AppComponent implements OnInit {
this.serverService.loadVideoPrivacies() this.serverService.loadVideoPrivacies()
this.serverService.loadVideoPlaylistPrivacies() this.serverService.loadVideoPlaylistPrivacies()
this.loadPlugins()
// Do not display menu on small screens // Do not display menu on small screens
if (this.screenService.isInSmallView()) { if (this.screenService.isInSmallView()) {
this.isMenuDisplayed = false this.isMenuDisplayed = false
@ -196,6 +200,14 @@ export class AppComponent implements OnInit {
}) })
} }
private async loadPlugins () {
this.pluginService.initializePlugins()
await this.pluginService.loadPluginsByScope('common')
this.pluginService.runHook('action:application.loaded')
}
private initHotkeys () { private initHotkeys () {
this.hotkeysService.add([ this.hotkeysService.add([
new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => { new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => {

View file

@ -21,6 +21,7 @@ import { MessageService } from 'primeng/api'
import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service' import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service'
import { ServerConfigResolver } from './routing/server-config-resolver.service' import { ServerConfigResolver } from './routing/server-config-resolver.service'
import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service' import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service'
import { PluginService } from '@app/core/plugins/plugin.service'
@NgModule({ @NgModule({
imports: [ imports: [
@ -61,6 +62,8 @@ import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service'
UserRightGuard, UserRightGuard,
UnloggedGuard, UnloggedGuard,
PluginService,
RedirectService, RedirectService,
Notifier, Notifier,
MessageService, MessageService,

View file

@ -0,0 +1,137 @@
import { Injectable } from '@angular/core'
import { Router } from '@angular/router'
import { ServerConfigPlugin } from '@shared/models'
import { ServerService } from '@app/core/server/server.service'
import { ClientScript } from '@shared/models/plugins/plugin-package-json.model'
import { PluginScope } from '@shared/models/plugins/plugin-scope.type'
import { environment } from '../../../environments/environment'
import { RegisterHookOptions } from '@shared/models/plugins/register.model'
import { ReplaySubject } from 'rxjs'
import { first } from 'rxjs/operators'
interface HookStructValue extends RegisterHookOptions {
plugin: ServerConfigPlugin
clientScript: ClientScript
}
@Injectable()
export class PluginService {
pluginsLoaded = new ReplaySubject<boolean>(1)
private plugins: ServerConfigPlugin[] = []
private scopes: { [ scopeName: string ]: { plugin: ServerConfigPlugin, clientScript: ClientScript }[] } = {}
private loadedScripts: { [ script: string ]: boolean } = {}
private hooks: { [ name: string ]: HookStructValue[] } = {}
constructor (
private router: Router,
private server: ServerService
) {
}
initializePlugins () {
this.server.configLoaded
.subscribe(() => {
this.plugins = this.server.getConfig().plugins
this.buildScopeStruct()
this.pluginsLoaded.next(true)
})
}
ensurePluginsAreLoaded () {
return this.pluginsLoaded.asObservable()
.pipe(first())
.toPromise()
}
async loadPluginsByScope (scope: PluginScope) {
try {
await this.ensurePluginsAreLoaded()
const toLoad = this.scopes[ scope ]
if (!Array.isArray(toLoad)) return
const promises: Promise<any>[] = []
for (const { plugin, clientScript } of toLoad) {
if (this.loadedScripts[ clientScript.script ]) continue
promises.push(this.loadPlugin(plugin, clientScript))
this.loadedScripts[ clientScript.script ] = true
}
return Promise.all(promises)
} catch (err) {
console.error('Cannot load plugins by scope %s.', scope, err)
}
}
async runHook (hookName: string, param?: any) {
let result = param
const wait = hookName.startsWith('static:')
for (const hook of this.hooks[hookName]) {
try {
if (wait) result = await hook.handler(param)
else result = hook.handler()
} catch (err) {
console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.plugin, hook.clientScript, err)
}
}
return result
}
private loadPlugin (plugin: ServerConfigPlugin, clientScript: ClientScript) {
const registerHook = (options: RegisterHookOptions) => {
if (!this.hooks[options.target]) this.hooks[options.target] = []
this.hooks[options.target].push({
plugin,
clientScript,
target: options.target,
handler: options.handler,
priority: options.priority || 0
})
}
console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name)
const url = environment.apiUrl + `/plugins/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`
return import(/* webpackIgnore: true */ url)
.then(script => script.register({ registerHook }))
.then(() => this.sortHooksByPriority())
}
private buildScopeStruct () {
for (const plugin of this.plugins) {
for (const key of Object.keys(plugin.clientScripts)) {
const clientScript = plugin.clientScripts[key]
for (const scope of clientScript.scopes) {
if (!this.scopes[scope]) this.scopes[scope] = []
this.scopes[scope].push({
plugin,
clientScript
})
this.loadedScripts[clientScript.script] = false
}
}
}
}
private sortHooksByPriority () {
for (const hookName of Object.keys(this.hooks)) {
this.hooks[hookName].sort((a, b) => {
return b.priority - a.priority
})
}
}
}

View file

@ -42,6 +42,7 @@ export class ServerService {
css: '' css: ''
} }
}, },
plugins: [],
email: { email: {
enabled: false enabled: false
}, },

View file

@ -32,6 +32,7 @@ import { Video } from '@app/shared/video/video.model'
import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils' import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils'
import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component' import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component'
import { getStoredTheater } from '../../../assets/player/peertube-player-local-storage' import { getStoredTheater } from '../../../assets/player/peertube-player-local-storage'
import { PluginService } from '@app/core/plugins/plugin.service'
@Component({ @Component({
selector: 'my-video-watch', selector: 'my-video-watch',
@ -85,6 +86,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
private serverService: ServerService, private serverService: ServerService,
private restExtractor: RestExtractor, private restExtractor: RestExtractor,
private notifier: Notifier, private notifier: Notifier,
private pluginService: PluginService,
private markdownService: MarkdownService, private markdownService: MarkdownService,
private zone: NgZone, private zone: NgZone,
private redirectService: RedirectService, private redirectService: RedirectService,
@ -98,7 +100,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
return this.authService.getUser() return this.authService.getUser()
} }
ngOnInit () { async ngOnInit () {
await this.pluginService.loadPluginsByScope('video-watch')
this.configSub = this.serverService.configLoaded this.configSub = this.serverService.configLoaded
.subscribe(() => { .subscribe(() => {
if ( if (
@ -126,6 +130,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.initHotkeys() this.initHotkeys()
this.theaterEnabled = getStoredTheater() this.theaterEnabled = getStoredTheater()
this.pluginService.runHook('action:video-watch.loaded')
} }
ngOnDestroy () { ngOnDestroy () {

View file

@ -1,6 +1,6 @@
import * as express from 'express' import * as express from 'express'
import { snakeCase } from 'lodash' import { snakeCase } from 'lodash'
import { ServerConfig, UserRight } from '../../../shared' import { ServerConfig, ServerConfigPlugin, UserRight } from '../../../shared'
import { About } from '../../../shared/models/server/about.model' import { About } from '../../../shared/models/server/about.model'
import { CustomConfig } from '../../../shared/models/server/custom-config.model' import { CustomConfig } from '../../../shared/models/server/custom-config.model'
import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup' import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
@ -15,6 +15,8 @@ import { Emailer } from '../../lib/emailer'
import { isNumeric } from 'validator' import { isNumeric } from 'validator'
import { objectConverter } from '../../helpers/core-utils' import { objectConverter } from '../../helpers/core-utils'
import { CONFIG, reloadConfig } from '../../initializers/config' import { CONFIG, reloadConfig } from '../../initializers/config'
import { PluginManager } from '../../lib/plugins/plugin-manager'
import { PluginType } from '../../../shared/models/plugins/plugin.type'
const packageJSON = require('../../../../package.json') const packageJSON = require('../../../../package.json')
const configRouter = express.Router() const configRouter = express.Router()
@ -54,6 +56,20 @@ async function getConfig (req: express.Request, res: express.Response) {
.filter(key => CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.RESOLUTIONS[key] === true) .filter(key => CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.RESOLUTIONS[key] === true)
.map(r => parseInt(r, 10)) .map(r => parseInt(r, 10))
const plugins: ServerConfigPlugin[] = []
const registeredPlugins = PluginManager.Instance.getRegisteredPlugins()
for (const pluginName of Object.keys(registeredPlugins)) {
const plugin = registeredPlugins[ pluginName ]
if (plugin.type !== PluginType.PLUGIN) continue
plugins.push({
name: plugin.name,
version: plugin.version,
description: plugin.description,
clientScripts: plugin.clientScripts
})
}
const json: ServerConfig = { const json: ServerConfig = {
instance: { instance: {
name: CONFIG.INSTANCE.NAME, name: CONFIG.INSTANCE.NAME,
@ -66,6 +82,7 @@ async function getConfig (req: express.Request, res: express.Response) {
css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
} }
}, },
plugins,
email: { email: {
enabled: Emailer.isEnabled() enabled: Emailer.isEnabled()
}, },

View file

@ -75,6 +75,27 @@ export class PluginManager {
return registered return registered
} }
getRegisteredPlugins () {
return this.registeredPlugins
}
async runHook (hookName: string, param?: any) {
let result = param
const wait = hookName.startsWith('static:')
for (const hook of this.hooks[hookName]) {
try {
if (wait) result = await hook.handler(param)
else result = hook.handler()
} catch (err) {
logger.error('Cannot run hook %s of plugin %s.', hookName, hook.pluginName, { err })
}
}
return result
}
async unregister (name: string) { async unregister (name: string) {
const plugin = this.getRegisteredPlugin(name) const plugin = this.getRegisteredPlugin(name)

View file

@ -0,0 +1 @@
export type PluginScope = 'common' | 'video-watch'

View file

@ -1,4 +1,4 @@
export type RegisterHookOptions = { export interface RegisterHookOptions {
target: string target: string
handler: Function handler: Function
priority?: number priority?: number

View file

@ -1,4 +1,12 @@
import { NSFWPolicyType } from '../videos/nsfw-policy.type' import { NSFWPolicyType } from '../videos/nsfw-policy.type'
import { ClientScript } from '../plugins/plugin-package-json.model'
export type ServerConfigPlugin = {
name: string
version: string
description: string
clientScripts: { [name: string]: ClientScript }
}
export interface ServerConfig { export interface ServerConfig {
serverVersion: string serverVersion: string
@ -16,6 +24,8 @@ export interface ServerConfig {
} }
} }
plugins: ServerConfigPlugin[]
email: { email: {
enabled: boolean enabled: boolean
} }