"target": "http://localhost:9000",
"secure": false
},
+ "/plugins": {
+ "target": "http://localhost:9000",
+ "secure": false
+ },
"/static": {
"target": "http://localhost:9000",
"secure": false
import { I18n } from '@ngx-translate/i18n-polyfill'
import { fromEvent } from 'rxjs'
import { ViewportScroller } from '@angular/common'
+import { PluginService } from '@app/core/plugins/plugin.service'
@Component({
selector: 'my-app',
private router: Router,
private authService: AuthService,
private serverService: ServerService,
+ private pluginService: PluginService,
private domSanitizer: DomSanitizer,
private redirectService: RedirectService,
private screenService: ScreenService,
this.serverService.loadVideoPrivacies()
this.serverService.loadVideoPlaylistPrivacies()
+ this.loadPlugins()
+
// Do not display menu on small screens
if (this.screenService.isInSmallView()) {
this.isMenuDisplayed = false
})
}
+ private async loadPlugins () {
+ this.pluginService.initializePlugins()
+
+ await this.pluginService.loadPluginsByScope('common')
+
+ this.pluginService.runHook('action:application.loaded')
+ }
+
private initHotkeys () {
this.hotkeysService.add([
new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => {
import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service'
import { ServerConfigResolver } from './routing/server-config-resolver.service'
import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service'
+import { PluginService } from '@app/core/plugins/plugin.service'
@NgModule({
imports: [
UserRightGuard,
UnloggedGuard,
+ PluginService,
+
RedirectService,
Notifier,
MessageService,
--- /dev/null
+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
+ })
+ }
+ }
+}
css: ''
}
},
+ plugins: [],
email: {
enabled: false
},
import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils'
import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component'
import { getStoredTheater } from '../../../assets/player/peertube-player-local-storage'
+import { PluginService } from '@app/core/plugins/plugin.service'
@Component({
selector: 'my-video-watch',
private serverService: ServerService,
private restExtractor: RestExtractor,
private notifier: Notifier,
+ private pluginService: PluginService,
private markdownService: MarkdownService,
private zone: NgZone,
private redirectService: RedirectService,
return this.authService.getUser()
}
- ngOnInit () {
+ async ngOnInit () {
+ await this.pluginService.loadPluginsByScope('video-watch')
+
this.configSub = this.serverService.configLoaded
.subscribe(() => {
if (
this.initHotkeys()
this.theaterEnabled = getStoredTheater()
+
+ this.pluginService.runHook('action:video-watch.loaded')
}
ngOnDestroy () {
import * as express from 'express'
import { snakeCase } from 'lodash'
-import { ServerConfig, UserRight } from '../../../shared'
+import { ServerConfig, ServerConfigPlugin, UserRight } from '../../../shared'
import { About } from '../../../shared/models/server/about.model'
import { CustomConfig } from '../../../shared/models/server/custom-config.model'
import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
import { isNumeric } from 'validator'
import { objectConverter } from '../../helpers/core-utils'
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 configRouter = express.Router()
.filter(key => CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.RESOLUTIONS[key] === true)
.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 = {
instance: {
name: CONFIG.INSTANCE.NAME,
css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
}
},
+ plugins,
email: {
enabled: Emailer.isEnabled()
},
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) {
const plugin = this.getRegisteredPlugin(name)
--- /dev/null
+export type PluginScope = 'common' | 'video-watch'
-export type RegisterHookOptions = {
+export interface RegisterHookOptions {
target: string
handler: Function
priority?: number
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 {
serverVersion: string
}
}
+ plugins: ServerConfigPlugin[]
+
email: {
enabled: boolean
}