]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/app/core/plugins/plugin.service.ts
525740a01b37cd31d3efa8b00567937460e33b07
[github/Chocobozzz/PeerTube.git] / client / src / app / core / plugins / plugin.service.ts
1 import { Injectable } from '@angular/core'
2 import { Router } from '@angular/router'
3 import { ServerConfigPlugin } from '@shared/models'
4 import { ServerService } from '@app/core/server/server.service'
5 import { ClientScript } from '@shared/models/plugins/plugin-package-json.model'
6 import { PluginScope } from '@shared/models/plugins/plugin-scope.type'
7 import { environment } from '../../../environments/environment'
8 import { RegisterHookOptions } from '@shared/models/plugins/register-hook.model'
9 import { ReplaySubject } from 'rxjs'
10 import { first, shareReplay } from 'rxjs/operators'
11
12 interface HookStructValue extends RegisterHookOptions {
13 plugin: ServerConfigPlugin
14 clientScript: ClientScript
15 }
16
17 type PluginInfo = {
18 plugin: ServerConfigPlugin
19 clientScript: ClientScript
20 isTheme: boolean
21 }
22
23 @Injectable()
24 export class PluginService {
25 pluginsLoaded = new ReplaySubject<boolean>(1)
26
27 private plugins: ServerConfigPlugin[] = []
28 private scopes: { [ scopeName: string ]: PluginInfo[] } = {}
29 private loadedPlugins: { [ name: string ]: boolean } = {}
30 private loadedScripts: { [ script: string ]: boolean } = {}
31 private loadedScopes: PluginScope[] = []
32
33 private hooks: { [ name: string ]: HookStructValue[] } = {}
34
35 constructor (
36 private router: Router,
37 private server: ServerService
38 ) {
39 }
40
41 initializePlugins () {
42 this.server.configLoaded
43 .subscribe(() => {
44 this.plugins = this.server.getConfig().plugin.registered
45
46 this.buildScopeStruct()
47
48 this.pluginsLoaded.next(true)
49 })
50 }
51
52 ensurePluginsAreLoaded () {
53 return this.pluginsLoaded.asObservable()
54 .pipe(first(), shareReplay())
55 .toPromise()
56 }
57
58 addPlugin (plugin: ServerConfigPlugin, isTheme = false) {
59 const pathPrefix = this.getPluginPathPrefix(isTheme)
60
61 for (const key of Object.keys(plugin.clientScripts)) {
62 const clientScript = plugin.clientScripts[key]
63
64 for (const scope of clientScript.scopes) {
65 if (!this.scopes[scope]) this.scopes[scope] = []
66
67 this.scopes[scope].push({
68 plugin,
69 clientScript: {
70 script: environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`,
71 scopes: clientScript.scopes
72 },
73 isTheme
74 })
75
76 this.loadedScripts[clientScript.script] = false
77 }
78 }
79 }
80
81 removePlugin (plugin: ServerConfigPlugin) {
82 for (const key of Object.keys(this.scopes)) {
83 this.scopes[key] = this.scopes[key].filter(o => o.plugin.name !== plugin.name)
84 }
85 }
86
87 async reloadLoadedScopes () {
88 for (const scope of this.loadedScopes) {
89 await this.loadPluginsByScope(scope, true)
90 }
91 }
92
93 async loadPluginsByScope (scope: PluginScope, isReload = false) {
94 try {
95 await this.ensurePluginsAreLoaded()
96
97 if (!isReload) this.loadedScopes.push(scope)
98
99 const toLoad = this.scopes[ scope ]
100 if (!Array.isArray(toLoad)) return
101
102 const promises: Promise<any>[] = []
103 for (const pluginInfo of toLoad) {
104 const clientScript = pluginInfo.clientScript
105
106 if (this.loadedScripts[ clientScript.script ]) continue
107
108 promises.push(this.loadPlugin(pluginInfo))
109
110 this.loadedScripts[ clientScript.script ] = true
111 }
112
113 await Promise.all(promises)
114 } catch (err) {
115 console.error('Cannot load plugins by scope %s.', scope, err)
116 }
117 }
118
119 async runHook (hookName: string, param?: any) {
120 let result = param
121
122 if (!this.hooks[hookName]) return result
123
124 const wait = hookName.startsWith('static:')
125
126 for (const hook of this.hooks[hookName]) {
127 try {
128 if (wait) result = await hook.handler(param)
129 else result = hook.handler()
130 } catch (err) {
131 console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.plugin, hook.clientScript, err)
132 }
133 }
134
135 return result
136 }
137
138 private loadPlugin (pluginInfo: PluginInfo) {
139 const { plugin, clientScript } = pluginInfo
140
141 const registerHook = (options: RegisterHookOptions) => {
142 if (!this.hooks[options.target]) this.hooks[options.target] = []
143
144 this.hooks[options.target].push({
145 plugin,
146 clientScript,
147 target: options.target,
148 handler: options.handler,
149 priority: options.priority || 0
150 })
151 }
152
153 const peertubeHelpers = this.buildPeerTubeHelpers(pluginInfo)
154
155 console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name)
156
157 return import(/* webpackIgnore: true */ clientScript.script)
158 .then(script => script.register({ registerHook, peertubeHelpers }))
159 .then(() => this.sortHooksByPriority())
160 }
161
162 private buildScopeStruct () {
163 for (const plugin of this.plugins) {
164 this.addPlugin(plugin)
165 }
166 }
167
168 private sortHooksByPriority () {
169 for (const hookName of Object.keys(this.hooks)) {
170 this.hooks[hookName].sort((a, b) => {
171 return b.priority - a.priority
172 })
173 }
174 }
175
176 private buildPeerTubeHelpers (pluginInfo: PluginInfo) {
177 const { plugin } = pluginInfo
178
179 return {
180 getBaseStaticRoute: () => {
181 const pathPrefix = this.getPluginPathPrefix(pluginInfo.isTheme)
182 return environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/static`
183 }
184 }
185 }
186
187 private getPluginPathPrefix (isTheme: boolean) {
188 return isTheme ? '/themes' : '/plugins'
189 }
190 }