mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2024-12-01 04:49:16 -06:00
WIP plugins: hook on client side
This commit is contained in:
parent
2c0539420d
commit
18a6f04c07
@ -3,6 +3,10 @@
|
||||
"target": "http://localhost:9000",
|
||||
"secure": false
|
||||
},
|
||||
"/plugins": {
|
||||
"target": "http://localhost:9000",
|
||||
"secure": false
|
||||
},
|
||||
"/static": {
|
||||
"target": "http://localhost:9000",
|
||||
"secure": false
|
||||
|
@ -9,6 +9,7 @@ import { Hotkey, HotkeysService } from 'angular2-hotkeys'
|
||||
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',
|
||||
@ -27,6 +28,7 @@ export class AppComponent implements OnInit {
|
||||
private router: Router,
|
||||
private authService: AuthService,
|
||||
private serverService: ServerService,
|
||||
private pluginService: PluginService,
|
||||
private domSanitizer: DomSanitizer,
|
||||
private redirectService: RedirectService,
|
||||
private screenService: ScreenService,
|
||||
@ -69,6 +71,8 @@ export class AppComponent implements OnInit {
|
||||
this.serverService.loadVideoPrivacies()
|
||||
this.serverService.loadVideoPlaylistPrivacies()
|
||||
|
||||
this.loadPlugins()
|
||||
|
||||
// Do not display menu on small screens
|
||||
if (this.screenService.isInSmallView()) {
|
||||
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 () {
|
||||
this.hotkeysService.add([
|
||||
new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => {
|
||||
|
@ -21,6 +21,7 @@ import { MessageService } from 'primeng/api'
|
||||
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: [
|
||||
@ -61,6 +62,8 @@ import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service'
|
||||
UserRightGuard,
|
||||
UnloggedGuard,
|
||||
|
||||
PluginService,
|
||||
|
||||
RedirectService,
|
||||
Notifier,
|
||||
MessageService,
|
||||
|
137
client/src/app/core/plugins/plugin.service.ts
Normal file
137
client/src/app/core/plugins/plugin.service.ts
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -42,6 +42,7 @@ export class ServerService {
|
||||
css: ''
|
||||
}
|
||||
},
|
||||
plugins: [],
|
||||
email: {
|
||||
enabled: false
|
||||
},
|
||||
|
@ -32,6 +32,7 @@ import { Video } from '@app/shared/video/video.model'
|
||||
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',
|
||||
@ -85,6 +86,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||
private serverService: ServerService,
|
||||
private restExtractor: RestExtractor,
|
||||
private notifier: Notifier,
|
||||
private pluginService: PluginService,
|
||||
private markdownService: MarkdownService,
|
||||
private zone: NgZone,
|
||||
private redirectService: RedirectService,
|
||||
@ -98,7 +100,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||
return this.authService.getUser()
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
async ngOnInit () {
|
||||
await this.pluginService.loadPluginsByScope('video-watch')
|
||||
|
||||
this.configSub = this.serverService.configLoaded
|
||||
.subscribe(() => {
|
||||
if (
|
||||
@ -126,6 +130,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||
this.initHotkeys()
|
||||
|
||||
this.theaterEnabled = getStoredTheater()
|
||||
|
||||
this.pluginService.runHook('action:video-watch.loaded')
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
|
@ -1,6 +1,6 @@
|
||||
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'
|
||||
@ -15,6 +15,8 @@ import { Emailer } from '../../lib/emailer'
|
||||
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()
|
||||
@ -54,6 +56,20 @@ async function getConfig (req: express.Request, res: express.Response) {
|
||||
.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,
|
||||
@ -66,6 +82,7 @@ async function getConfig (req: express.Request, res: express.Response) {
|
||||
css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
|
||||
}
|
||||
},
|
||||
plugins,
|
||||
email: {
|
||||
enabled: Emailer.isEnabled()
|
||||
},
|
||||
|
@ -75,6 +75,27 @@ export class PluginManager {
|
||||
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)
|
||||
|
||||
|
1
shared/models/plugins/plugin-scope.type.ts
Normal file
1
shared/models/plugins/plugin-scope.type.ts
Normal file
@ -0,0 +1 @@
|
||||
export type PluginScope = 'common' | 'video-watch'
|
@ -1,4 +1,4 @@
|
||||
export type RegisterHookOptions = {
|
||||
export interface RegisterHookOptions {
|
||||
target: string
|
||||
handler: Function
|
||||
priority?: number
|
||||
|
@ -1,4 +1,12 @@
|
||||
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
|
||||
@ -16,6 +24,8 @@ export interface ServerConfig {
|
||||
}
|
||||
}
|
||||
|
||||
plugins: ServerConfigPlugin[]
|
||||
|
||||
email: {
|
||||
enabled: boolean
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user