X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=client%2Fsrc%2Froot-helpers%2Fplugins-manager.ts;h=6c64e2b014167f3559a74d1180e265a8f2737c47;hb=91a4893063402d7beabb3104f9b989b8f88b6038;hp=f919db8afb4ebb27c1a3164c72cd7a2fd5a97959;hpb=72f611ca1549d711f8cbf5dd08a3f1562a32fabb;p=github%2FChocobozzz%2FPeerTube.git diff --git a/client/src/root-helpers/plugins-manager.ts b/client/src/root-helpers/plugins-manager.ts index f919db8af..6c64e2b01 100644 --- a/client/src/root-helpers/plugins-manager.ts +++ b/client/src/root-helpers/plugins-manager.ts @@ -1,43 +1,55 @@ +/* eslint-disable @typescript-eslint/no-implied-eval */ import * as debug from 'debug' -import { ReplaySubject } from 'rxjs' +import { firstValueFrom, ReplaySubject } from 'rxjs' import { first, shareReplay } from 'rxjs/operators' import { RegisterClientHelpers } from 'src/types/register-client-option.model' import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks' import { ClientHookName, clientHookObject, - ClientScript, + ClientScriptJSON, HTMLServerConfig, PluginClientScope, PluginType, RegisterClientFormFieldOptions, RegisterClientHookOptions, - RegisterClientSettingsScript, + RegisterClientRouteOptions, + RegisterClientSettingsScriptOptions, RegisterClientVideoFieldOptions, + RegisteredExternalAuthConfig, ServerConfigPlugin -} from '../../../shared/models' +} from '@shared/models' import { environment } from '../environments/environment' -import { ClientScript as ClientScriptModule } from '../types/client-script.model' +import { ClientScript } from '../types' +import { logger } from './logger' interface HookStructValue extends RegisterClientHookOptions { plugin: ServerConfigPlugin - clientScript: ClientScript + clientScript: ClientScriptJSON } type Hooks = { [ name: string ]: HookStructValue[] } type PluginInfo = { plugin: ServerConfigPlugin - clientScript: ClientScript + clientScript: ClientScriptJSON pluginType: PluginType isTheme: boolean } type PeertubeHelpersFactory = (pluginInfo: PluginInfo) => RegisterClientHelpers -type OnFormFields = (options: RegisterClientFormFieldOptions, videoFormOptions: RegisterClientVideoFieldOptions) => void -type OnSettingsScripts = (pluginInfo: PluginInfo, options: RegisterClientSettingsScript) => void -const logger = debug('peertube:plugins') +type OnFormFields = ( + pluginInfo: PluginInfo, + options: RegisterClientFormFieldOptions, + videoFormOptions: RegisterClientVideoFieldOptions +) => void + +type OnSettingsScripts = (pluginInfo: PluginInfo, options: RegisterClientSettingsScriptOptions) => void + +type OnClientRoute = (options: RegisterClientRouteOptions) => void + +const debugLogger = debug('peertube:plugins') class PluginsManager { private hooks: Hooks = {} @@ -56,27 +68,37 @@ class PluginsManager { signup: new ReplaySubject(1), login: new ReplaySubject(1), 'video-edit': new ReplaySubject(1), - embed: new ReplaySubject(1) + embed: new ReplaySubject(1), + 'my-library': new ReplaySubject(1), + 'video-channel': new ReplaySubject(1) } private readonly peertubeHelpersFactory: PeertubeHelpersFactory private readonly onFormFields: OnFormFields private readonly onSettingsScripts: OnSettingsScripts + private readonly onClientRoute: OnClientRoute constructor (options: { peertubeHelpersFactory: PeertubeHelpersFactory onFormFields?: OnFormFields onSettingsScripts?: OnSettingsScripts + onClientRoute?: OnClientRoute }) { this.peertubeHelpersFactory = options.peertubeHelpersFactory this.onFormFields = options.onFormFields this.onSettingsScripts = options.onSettingsScripts + this.onClientRoute = options.onClientRoute } static getPluginPathPrefix (isTheme: boolean) { return isTheme ? '/themes' : '/plugins' } + static getExternalAuthHref (auth: RegisteredExternalAuthConfig) { + return environment.apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}` + + } + loadPluginsList (config: HTMLServerConfig) { for (const plugin of config.plugin.registered) { this.addPlugin(plugin) @@ -89,10 +111,16 @@ class PluginsManager { const hookType = getHookType(hookName) for (const hook of this.hooks[hookName]) { - console.log('Running hook %s of plugin %s.', hookName, hook.plugin.name) - - result = await internalRunHook(hook.handler, hookType, result, params, err => { - console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.clientScript.script, hook.plugin.name, err) + logger.info(`Running hook ${hookName} of plugin ${hook.plugin.name}`) + + result = await internalRunHook({ + handler: hook.handler, + hookType, + result, + params, + onError: err => { + logger.error(`Cannot run hook ${hookName} of script ${hook.clientScript.script} of plugin ${hook.plugin.name}`, err) + } }) } @@ -102,9 +130,10 @@ class PluginsManager { ensurePluginsAreLoaded (scope: PluginClientScope) { this.loadPluginsByScope(scope) - return this.pluginsLoaded[scope].asObservable() + const obs = this.pluginsLoaded[scope].asObservable() .pipe(first(), shareReplay()) - .toPromise() + + return firstValueFrom(obs) } async reloadLoadedScopes () { @@ -149,17 +178,17 @@ class PluginsManager { this.loadingScopes[scope] = true - logger('Loading scope %s', scope) + debugLogger('Loading scope %s', scope) try { if (!isReload) this.loadedScopes.push(scope) - const toLoad = this.scopes[ scope ] + const toLoad = this.scopes[scope] if (!Array.isArray(toLoad)) { this.loadingScopes[scope] = false this.pluginsLoaded[scope].next(true) - logger('Nothing to load for scope %s', scope) + debugLogger('Nothing to load for scope %s', scope) return } @@ -167,11 +196,11 @@ class PluginsManager { for (const pluginInfo of toLoad) { const clientScript = pluginInfo.clientScript - if (this.loadedScripts[ clientScript.script ]) continue + if (this.loadedScripts[clientScript.script]) continue promises.push(this.loadPlugin(pluginInfo)) - this.loadedScripts[ clientScript.script ] = true + this.loadedScripts[clientScript.script] = true } await Promise.all(promises) @@ -179,9 +208,9 @@ class PluginsManager { this.pluginsLoaded[scope].next(true) this.loadingScopes[scope] = false - logger('Scope %s loaded', scope) + debugLogger('Scope %s loaded', scope) } catch (err) { - console.error('Cannot load plugins by scope %s.', scope, err) + logger.error(`Cannot load plugins by scope ${scope}`, err) } } @@ -190,7 +219,7 @@ class PluginsManager { const registerHook = (options: RegisterClientHookOptions) => { if (clientHookObject[options.target] !== true) { - console.error('Unknown hook %s of plugin %s. Skipping.', options.target, plugin.name) + logger.error(`Unknown hook ${options.target} of plugin ${plugin.name}. Skipping.`) return } @@ -210,10 +239,10 @@ class PluginsManager { throw new Error('Video field registration is not supported') } - return this.onFormFields(commonOptions, videoFormOptions) + return this.onFormFields(pluginInfo, commonOptions, videoFormOptions) } - const registerSettingsScript = (options: RegisterClientSettingsScript) => { + const registerSettingsScript = (options: RegisterClientSettingsScriptOptions) => { if (!this.onSettingsScripts) { throw new Error('Registering settings script is not supported') } @@ -221,15 +250,31 @@ class PluginsManager { return this.onSettingsScripts(pluginInfo, options) } + const registerClientRoute = (options: RegisterClientRouteOptions) => { + if (!this.onClientRoute) { + throw new Error('Registering client route is not supported') + } + + return this.onClientRoute(options) + } + const peertubeHelpers = this.peertubeHelpersFactory(pluginInfo) - console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name) + logger.info(`Loading script ${clientScript.script} of plugin ${plugin.name}`) const absURL = (environment.apiUrl || window.location.origin) + clientScript.script - return import(/* webpackIgnore: true */ absURL) - .then((script: ClientScriptModule) => script.register({ registerHook, registerVideoField, registerSettingsScript, peertubeHelpers })) + return dynamicImport(absURL) + .then((script: ClientScript) => { + return script.register({ + registerHook, + registerVideoField, + registerSettingsScript, + registerClientRoute, + peertubeHelpers + }) + }) .then(() => this.sortHooksByPriority()) - .catch(err => console.error('Cannot import or register plugin %s.', pluginInfo.plugin.name, err)) + .catch(err => logger.error(`Cannot import or register plugin ${pluginInfo.plugin.name}`, err)) } private sortHooksByPriority () { @@ -249,3 +294,45 @@ export { OnFormFields, OnSettingsScripts } + +// --------------------------------------------------------------------------- + +async function dynamicImport (url: string) { + try { + // eslint-disable-next-line no-new-func + return new Function(`return import('${url}')`)() + } catch { + logger.info('Fallback to import polyfill') + + return new Promise((resolve, reject) => { + const vector = '$importModule$' + Math.random().toString(32).slice(2) + const script = document.createElement('script') + + const destructor = () => { + delete window[vector] + script.onerror = null + script.onload = null + script.remove() + URL.revokeObjectURL(script.src) + script.src = '' + } + + script.defer = true + script.type = 'module' + + script.onerror = () => { + reject(new Error(`Failed to import: ${url}`)) + destructor() + } + script.onload = () => { + resolve(window[vector]) + destructor() + } + const loader = `import * as m from "${url}"; window.${vector} = m;` // export Module + const blob = new Blob([ loader ], { type: 'text/javascript' }) + script.src = URL.createObjectURL(blob) + + document.head.appendChild(script) + }) + } +}