aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-08-20 11:46:25 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-08-20 14:23:57 +0200
commitf95628636b6ccdf3eae2449ca718e075b072f678 (patch)
tree35d51980c87b7d6747bdff6e37bdfe37e3c989dc
parenta9f6802e7dac4f21599076bc1119bb6ff16961dc (diff)
downloadPeerTube-f95628636b6ccdf3eae2449ca718e075b072f678.tar.gz
PeerTube-f95628636b6ccdf3eae2449ca718e075b072f678.tar.zst
PeerTube-f95628636b6ccdf3eae2449ca718e075b072f678.zip
Support plugin hooks in embed
-rw-r--r--client/src/app/core/plugins/plugin.service.ts79
-rw-r--r--client/src/app/helpers/utils.ts36
-rw-r--r--client/src/environments/environment.ts6
-rw-r--r--client/src/root-helpers/plugins.ts81
-rw-r--r--client/src/root-helpers/utils.ts38
-rw-r--r--client/src/standalone/videos/embed.ts81
-rw-r--r--shared/models/plugins/client-hook.model.ts8
-rw-r--r--shared/models/plugins/plugin-client-scope.type.ts2
8 files changed, 217 insertions, 114 deletions
diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts
index dc115c0e1..871613b89 100644
--- a/client/src/app/core/plugins/plugin.service.ts
+++ b/client/src/app/core/plugins/plugin.service.ts
@@ -7,38 +7,22 @@ import { Notifier } from '@app/core/notification'
7import { MarkdownService } from '@app/core/renderer' 7import { MarkdownService } from '@app/core/renderer'
8import { RestExtractor } from '@app/core/rest' 8import { RestExtractor } from '@app/core/rest'
9import { ServerService } from '@app/core/server/server.service' 9import { ServerService } from '@app/core/server/server.service'
10import { getDevLocale, importModule, isOnDevLocale } from '@app/helpers' 10import { getDevLocale, isOnDevLocale } from '@app/helpers'
11import { CustomModalComponent } from '@app/modal/custom-modal.component' 11import { CustomModalComponent } from '@app/modal/custom-modal.component'
12import { Hooks, loadPlugin, PluginInfo, runHook } from '@root-helpers/plugins'
12import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n' 13import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n'
13import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
14import { 14import {
15 ClientHook, 15 ClientHook,
16 ClientHookName, 16 ClientHookName,
17 clientHookObject,
18 ClientScript,
19 PluginClientScope, 17 PluginClientScope,
20 PluginTranslation, 18 PluginTranslation,
21 PluginType, 19 PluginType,
22 PublicServerSetting, 20 PublicServerSetting,
23 RegisterClientHookOptions,
24 ServerConfigPlugin 21 ServerConfigPlugin
25} from '@shared/models' 22} from '@shared/models'
26import { environment } from '../../../environments/environment' 23import { environment } from '../../../environments/environment'
27import { ClientScript as ClientScriptModule } from '../../../types/client-script.model'
28import { RegisterClientHelpers } from '../../../types/register-client-option.model' 24import { RegisterClientHelpers } from '../../../types/register-client-option.model'
29 25
30interface HookStructValue extends RegisterClientHookOptions {
31 plugin: ServerConfigPlugin
32 clientScript: ClientScript
33}
34
35type PluginInfo = {
36 plugin: ServerConfigPlugin
37 clientScript: ClientScript
38 pluginType: PluginType
39 isTheme: boolean
40}
41
42@Injectable() 26@Injectable()
43export class PluginService implements ClientHook { 27export class PluginService implements ClientHook {
44 private static BASE_PLUGIN_API_URL = environment.apiUrl + '/api/v1/plugins' 28 private static BASE_PLUGIN_API_URL = environment.apiUrl + '/api/v1/plugins'
@@ -51,7 +35,8 @@ export class PluginService implements ClientHook {
51 search: new ReplaySubject<boolean>(1), 35 search: new ReplaySubject<boolean>(1),
52 'video-watch': new ReplaySubject<boolean>(1), 36 'video-watch': new ReplaySubject<boolean>(1),
53 signup: new ReplaySubject<boolean>(1), 37 signup: new ReplaySubject<boolean>(1),
54 login: new ReplaySubject<boolean>(1) 38 login: new ReplaySubject<boolean>(1),
39 embed: new ReplaySubject<boolean>(1)
55 } 40 }
56 41
57 translationsObservable: Observable<PluginTranslation> 42 translationsObservable: Observable<PluginTranslation>
@@ -64,7 +49,7 @@ export class PluginService implements ClientHook {
64 private loadedScopes: PluginClientScope[] = [] 49 private loadedScopes: PluginClientScope[] = []
65 private loadingScopes: { [id in PluginClientScope]?: boolean } = {} 50 private loadingScopes: { [id in PluginClientScope]?: boolean } = {}
66 51
67 private hooks: { [ name: string ]: HookStructValue[] } = {} 52 private hooks: Hooks = {}
68 53
69 constructor ( 54 constructor (
70 private authService: AuthService, 55 private authService: AuthService,
@@ -120,7 +105,7 @@ export class PluginService implements ClientHook {
120 this.scopes[scope].push({ 105 this.scopes[scope].push({
121 plugin, 106 plugin,
122 clientScript: { 107 clientScript: {
123 script: environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`, 108 script: `${pathPrefix}/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`,
124 scopes: clientScript.scopes 109 scopes: clientScript.scopes
125 }, 110 },
126 pluginType: isTheme ? PluginType.THEME : PluginType.PLUGIN, 111 pluginType: isTheme ? PluginType.THEME : PluginType.PLUGIN,
@@ -184,20 +169,8 @@ export class PluginService implements ClientHook {
184 } 169 }
185 170
186 runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T> { 171 runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T> {
187 return this.zone.runOutsideAngular(async () => { 172 return this.zone.runOutsideAngular(() => {
188 if (!this.hooks[ hookName ]) return result 173 return runHook(this.hooks, hookName, result, params)
189
190 const hookType = getHookType(hookName)
191
192 for (const hook of this.hooks[ hookName ]) {
193 console.log('Running hook %s of plugin %s.', hookName, hook.plugin.name)
194
195 result = await internalRunHook(hook.handler, hookType, result, params, err => {
196 console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.clientScript.script, hook.plugin.name, err)
197 })
198 }
199
200 return result
201 }) 174 })
202 } 175 }
203 176
@@ -216,34 +189,8 @@ export class PluginService implements ClientHook {
216 } 189 }
217 190
218 private loadPlugin (pluginInfo: PluginInfo) { 191 private loadPlugin (pluginInfo: PluginInfo) {
219 const { plugin, clientScript } = pluginInfo
220
221 const registerHook = (options: RegisterClientHookOptions) => {
222 if (clientHookObject[options.target] !== true) {
223 console.error('Unknown hook %s of plugin %s. Skipping.', options.target, plugin.name)
224 return
225 }
226
227 if (!this.hooks[options.target]) this.hooks[options.target] = []
228
229 this.hooks[options.target].push({
230 plugin,
231 clientScript,
232 target: options.target,
233 handler: options.handler,
234 priority: options.priority || 0
235 })
236 }
237
238 const peertubeHelpers = this.buildPeerTubeHelpers(pluginInfo)
239
240 console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name)
241
242 return this.zone.runOutsideAngular(() => { 192 return this.zone.runOutsideAngular(() => {
243 return importModule(clientScript.script) 193 return loadPlugin(this.hooks, pluginInfo, pluginInfo => this.buildPeerTubeHelpers(pluginInfo))
244 .then((script: ClientScriptModule) => script.register({ registerHook, peertubeHelpers }))
245 .then(() => this.sortHooksByPriority())
246 .catch(err => console.error('Cannot import or register plugin %s.', pluginInfo.plugin.name, err))
247 }) 194 })
248 } 195 }
249 196
@@ -253,14 +200,6 @@ export class PluginService implements ClientHook {
253 } 200 }
254 } 201 }
255 202
256 private sortHooksByPriority () {
257 for (const hookName of Object.keys(this.hooks)) {
258 this.hooks[hookName].sort((a, b) => {
259 return b.priority - a.priority
260 })
261 }
262 }
263
264 private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers { 203 private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers {
265 const { plugin } = pluginInfo 204 const { plugin } = pluginInfo
266 const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType) 205 const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType)
diff --git a/client/src/app/helpers/utils.ts b/client/src/app/helpers/utils.ts
index d05541ca9..d9007dd77 100644
--- a/client/src/app/helpers/utils.ts
+++ b/client/src/app/helpers/utils.ts
@@ -148,41 +148,6 @@ function scrollToTop () {
148 window.scroll(0, 0) 148 window.scroll(0, 0)
149} 149}
150 150
151// Thanks: https://github.com/uupaa/dynamic-import-polyfill
152function importModule (path: string) {
153 return new Promise((resolve, reject) => {
154 const vector = '$importModule$' + Math.random().toString(32).slice(2)
155 const script = document.createElement('script')
156
157 const destructor = () => {
158 delete window[ vector ]
159 script.onerror = null
160 script.onload = null
161 script.remove()
162 URL.revokeObjectURL(script.src)
163 script.src = ''
164 }
165
166 script.defer = true
167 script.type = 'module'
168
169 script.onerror = () => {
170 reject(new Error(`Failed to import: ${path}`))
171 destructor()
172 }
173 script.onload = () => {
174 resolve(window[ vector ])
175 destructor()
176 }
177 const absURL = (environment.apiUrl || window.location.origin) + path
178 const loader = `import * as m from "${absURL}"; window.${vector} = m;` // export Module
179 const blob = new Blob([ loader ], { type: 'text/javascript' })
180 script.src = URL.createObjectURL(blob)
181
182 document.head.appendChild(script)
183 })
184}
185
186function isInViewport (el: HTMLElement) { 151function isInViewport (el: HTMLElement) {
187 const bounding = el.getBoundingClientRect() 152 const bounding = el.getBoundingClientRect()
188 return ( 153 return (
@@ -216,7 +181,6 @@ export {
216 getAbsoluteEmbedUrl, 181 getAbsoluteEmbedUrl,
217 objectLineFeedToHtml, 182 objectLineFeedToHtml,
218 removeElementFromArray, 183 removeElementFromArray,
219 importModule,
220 scrollToTop, 184 scrollToTop,
221 isInViewport, 185 isInViewport,
222 isXPercentInViewport 186 isXPercentInViewport
diff --git a/client/src/environments/environment.ts b/client/src/environments/environment.ts
index 4816e3060..e00523976 100644
--- a/client/src/environments/environment.ts
+++ b/client/src/environments/environment.ts
@@ -9,8 +9,8 @@
9import 'core-js/features/reflect' 9import 'core-js/features/reflect'
10 10
11export const environment = { 11export const environment = {
12 production: false, 12 production: true,
13 hmr: false, 13 hmr: false,
14 apiUrl: 'http://localhost:9000', 14 apiUrl: '',
15 embedUrl: 'http://localhost:9000' 15 embedUrl: ''
16} 16}
diff --git a/client/src/root-helpers/plugins.ts b/client/src/root-helpers/plugins.ts
new file mode 100644
index 000000000..011721761
--- /dev/null
+++ b/client/src/root-helpers/plugins.ts
@@ -0,0 +1,81 @@
1import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
2import { ClientHookName, ClientScript, RegisterClientHookOptions, ServerConfigPlugin, PluginType, clientHookObject } from '../../../shared/models'
3import { RegisterClientHelpers } from 'src/types/register-client-option.model'
4import { ClientScript as ClientScriptModule } from '../types/client-script.model'
5import { importModule } from './utils'
6
7interface HookStructValue extends RegisterClientHookOptions {
8 plugin: ServerConfigPlugin
9 clientScript: ClientScript
10}
11
12type Hooks = { [ name: string ]: HookStructValue[] }
13
14type PluginInfo = {
15 plugin: ServerConfigPlugin
16 clientScript: ClientScript
17 pluginType: PluginType
18 isTheme: boolean
19}
20
21async function runHook<T> (hooks: Hooks, hookName: ClientHookName, result?: T, params?: any) {
22 if (!hooks[hookName]) return result
23
24 const hookType = getHookType(hookName)
25
26 for (const hook of hooks[hookName]) {
27 console.log('Running hook %s of plugin %s.', hookName, hook.plugin.name)
28
29 result = await internalRunHook(hook.handler, hookType, result, params, err => {
30 console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.clientScript.script, hook.plugin.name, err)
31 })
32 }
33
34 return result
35}
36
37function loadPlugin (hooks: Hooks, pluginInfo: PluginInfo, peertubeHelpersFactory: (pluginInfo: PluginInfo) => RegisterClientHelpers) {
38 const { plugin, clientScript } = pluginInfo
39
40 const registerHook = (options: RegisterClientHookOptions) => {
41 if (clientHookObject[options.target] !== true) {
42 console.error('Unknown hook %s of plugin %s. Skipping.', options.target, plugin.name)
43 return
44 }
45
46 if (!hooks[options.target]) hooks[options.target] = []
47
48 hooks[options.target].push({
49 plugin,
50 clientScript,
51 target: options.target,
52 handler: options.handler,
53 priority: options.priority || 0
54 })
55 }
56
57 const peertubeHelpers = peertubeHelpersFactory(pluginInfo)
58
59 console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name)
60
61 return importModule(clientScript.script)
62 .then((script: ClientScriptModule) => script.register({ registerHook, peertubeHelpers }))
63 .then(() => sortHooksByPriority(hooks))
64 .catch(err => console.error('Cannot import or register plugin %s.', pluginInfo.plugin.name, err))
65}
66
67export {
68 HookStructValue,
69 Hooks,
70 PluginInfo,
71 loadPlugin,
72 runHook
73}
74
75function sortHooksByPriority (hooks: Hooks) {
76 for (const hookName of Object.keys(hooks)) {
77 hooks[hookName].sort((a, b) => {
78 return b.priority - a.priority
79 })
80 }
81}
diff --git a/client/src/root-helpers/utils.ts b/client/src/root-helpers/utils.ts
index acfb565a3..6df151ad9 100644
--- a/client/src/root-helpers/utils.ts
+++ b/client/src/root-helpers/utils.ts
@@ -1,3 +1,5 @@
1import { environment } from '../environments/environment'
2
1function objectToUrlEncoded (obj: any) { 3function objectToUrlEncoded (obj: any) {
2 const str: string[] = [] 4 const str: string[] = []
3 for (const key of Object.keys(obj)) { 5 for (const key of Object.keys(obj)) {
@@ -7,6 +9,42 @@ function objectToUrlEncoded (obj: any) {
7 return str.join('&') 9 return str.join('&')
8} 10}
9 11
12// Thanks: https://github.com/uupaa/dynamic-import-polyfill
13function importModule (path: string) {
14 return new Promise((resolve, reject) => {
15 const vector = '$importModule$' + Math.random().toString(32).slice(2)
16 const script = document.createElement('script')
17
18 const destructor = () => {
19 delete window[ vector ]
20 script.onerror = null
21 script.onload = null
22 script.remove()
23 URL.revokeObjectURL(script.src)
24 script.src = ''
25 }
26
27 script.defer = true
28 script.type = 'module'
29
30 script.onerror = () => {
31 reject(new Error(`Failed to import: ${path}`))
32 destructor()
33 }
34 script.onload = () => {
35 resolve(window[ vector ])
36 destructor()
37 }
38 const absURL = (environment.apiUrl || window.location.origin) + path
39 const loader = `import * as m from "${absURL}"; window.${vector} = m;` // export Module
40 const blob = new Blob([ loader ], { type: 'text/javascript' })
41 script.src = URL.createObjectURL(blob)
42
43 document.head.appendChild(script)
44 })
45}
46
10export { 47export {
48 importModule,
11 objectToUrlEncoded 49 objectToUrlEncoded
12} 50}
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index adba32a31..fe65794f7 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -1,7 +1,5 @@
1import './embed.scss' 1import './embed.scss'
2import videojs from 'video.js' 2import videojs from 'video.js'
3import { objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index'
4import { Tokens } from '@root-helpers/users'
5import { peertubeTranslate } from '../../../../shared/core-utils/i18n' 3import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
6import { 4import {
7 ResultList, 5 ResultList,
@@ -11,12 +9,19 @@ import {
11 VideoDetails, 9 VideoDetails,
12 VideoPlaylist, 10 VideoPlaylist,
13 VideoPlaylistElement, 11 VideoPlaylistElement,
14 VideoStreamingPlaylistType 12 VideoStreamingPlaylistType,
13 PluginType,
14 ClientHookName
15} from '../../../../shared/models' 15} from '../../../../shared/models'
16import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager' 16import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager'
17import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' 17import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
18import { TranslationsManager } from '../../assets/player/translations-manager' 18import { TranslationsManager } from '../../assets/player/translations-manager'
19import { Hooks, loadPlugin, runHook } from '../../root-helpers/plugins'
20import { Tokens } from '../../root-helpers/users'
21import { peertubeLocalStorage } from '../../root-helpers/peertube-web-storage'
22import { objectToUrlEncoded } from '../../root-helpers/utils'
19import { PeerTubeEmbedApi } from './embed-api' 23import { PeerTubeEmbedApi } from './embed-api'
24import { RegisterClientHelpers } from '../../types/register-client-option.model'
20 25
21type Translations = { [ id: string ]: string } 26type Translations = { [ id: string ]: string }
22 27
@@ -60,6 +65,9 @@ export class PeerTubeEmbed {
60 65
61 private wrapperElement: HTMLElement 66 private wrapperElement: HTMLElement
62 67
68 private peertubeHooks: Hooks = {}
69 private loadedScripts = new Set<string>()
70
63 static async main () { 71 static async main () {
64 const videoContainerId = 'video-wrapper' 72 const videoContainerId = 'video-wrapper'
65 const embed = new PeerTubeEmbed(videoContainerId) 73 const embed = new PeerTubeEmbed(videoContainerId)
@@ -473,6 +481,8 @@ export class PeerTubeEmbed {
473 this.PeertubePlayerManagerModulePromise 481 this.PeertubePlayerManagerModulePromise
474 ]) 482 ])
475 483
484 await this.ensurePluginsAreLoaded(config, serverTranslations)
485
476 const videoInfo: VideoDetails = videoInfoTmp 486 const videoInfo: VideoDetails = videoInfoTmp
477 487
478 const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager 488 const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
@@ -577,6 +587,8 @@ export class PeerTubeEmbed {
577 this.playNextVideo() 587 this.playNextVideo()
578 }) 588 })
579 } 589 }
590
591 this.runHook('action:embed.player.loaded', undefined, { player: this.player })
580 } 592 }
581 593
582 private async initCore () { 594 private async initCore () {
@@ -714,6 +726,69 @@ export class PeerTubeEmbed {
714 private isPlaylistEmbed () { 726 private isPlaylistEmbed () {
715 return window.location.pathname.split('/')[1] === 'video-playlists' 727 return window.location.pathname.split('/')[1] === 'video-playlists'
716 } 728 }
729
730 private async ensurePluginsAreLoaded (config: ServerConfig, translations?: { [ id: string ]: string }) {
731 if (config.plugin.registered.length === 0) return
732
733 for (const plugin of config.plugin.registered) {
734 for (const key of Object.keys(plugin.clientScripts)) {
735 const clientScript = plugin.clientScripts[key]
736
737 if (clientScript.scopes.includes('embed') === false) continue
738
739 const script = `/plugins/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`
740
741 if (this.loadedScripts.has(script)) continue
742
743 const pluginInfo = {
744 plugin,
745 clientScript: {
746 script,
747 scopes: clientScript.scopes
748 },
749 pluginType: PluginType.PLUGIN,
750 isTheme: false
751 }
752
753 await loadPlugin(this.peertubeHooks, pluginInfo, _ => this.buildPeerTubeHelpers(translations))
754 }
755 }
756 }
757
758 private buildPeerTubeHelpers (translations?: { [ id: string ]: string }): RegisterClientHelpers {
759 function unimplemented (): any {
760 throw new Error('This helper is not implemented in embed.')
761 }
762
763 return {
764 getBaseStaticRoute: unimplemented,
765
766 getSettings: unimplemented,
767
768 isLoggedIn: unimplemented,
769
770 notifier: {
771 info: unimplemented,
772 error: unimplemented,
773 success: unimplemented
774 },
775
776 showModal: unimplemented,
777
778 markdownRenderer: {
779 textMarkdownToHTML: unimplemented,
780 enhancedMarkdownToHTML: unimplemented
781 },
782
783 translate: (value: string) => {
784 return Promise.resolve(peertubeTranslate(value, translations))
785 }
786 }
787 }
788
789 private runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T> {
790 return runHook(this.peertubeHooks, hookName, result, params)
791 }
717} 792}
718 793
719PeerTubeEmbed.main() 794PeerTubeEmbed.main()
diff --git a/shared/models/plugins/client-hook.model.ts b/shared/models/plugins/client-hook.model.ts
index b53b8de99..193a3f646 100644
--- a/shared/models/plugins/client-hook.model.ts
+++ b/shared/models/plugins/client-hook.model.ts
@@ -80,7 +80,13 @@ export const clientActionHookObject = {
80 'action:router.navigation-end': true, 80 'action:router.navigation-end': true,
81 81
82 // Fired when the registration page is being initialized 82 // Fired when the registration page is being initialized
83 'action:signup.register.init': true 83 'action:signup.register.init': true,
84
85 // ####### Embed hooks #######
86 // In embed scope, peertube helpers are not available
87
88 // Fired when the embed loaded the player
89 'action:embed.player.loaded': true
84} 90}
85 91
86export type ClientActionHookName = keyof typeof clientActionHookObject 92export type ClientActionHookName = keyof typeof clientActionHookObject
diff --git a/shared/models/plugins/plugin-client-scope.type.ts b/shared/models/plugins/plugin-client-scope.type.ts
index d112434e8..a3c669fe7 100644
--- a/shared/models/plugins/plugin-client-scope.type.ts
+++ b/shared/models/plugins/plugin-client-scope.type.ts
@@ -1 +1 @@
export type PluginClientScope = 'common' | 'video-watch' | 'search' | 'signup' | 'login' export type PluginClientScope = 'common' | 'video-watch' | 'search' | 'signup' | 'login' | 'embed'