diff options
Diffstat (limited to 'client/src/app/core')
-rw-r--r-- | client/src/app/core/core.module.ts | 2 | ||||
-rw-r--r-- | client/src/app/core/plugins/hooks.service.ts | 44 | ||||
-rw-r--r-- | client/src/app/core/plugins/plugin.service.ts | 60 |
3 files changed, 78 insertions, 28 deletions
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts index 436c0dfb8..5943af4da 100644 --- a/client/src/app/core/core.module.ts +++ b/client/src/app/core/core.module.ts | |||
@@ -22,6 +22,7 @@ import { UserNotificationSocket } from '@app/core/notification/user-notification | |||
22 | import { ServerConfigResolver } from './routing/server-config-resolver.service' | 22 | import { ServerConfigResolver } from './routing/server-config-resolver.service' |
23 | import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service' | 23 | import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service' |
24 | import { PluginService } from '@app/core/plugins/plugin.service' | 24 | import { PluginService } from '@app/core/plugins/plugin.service' |
25 | import { HooksService } from '@app/core/plugins/hooks.service' | ||
25 | 26 | ||
26 | @NgModule({ | 27 | @NgModule({ |
27 | imports: [ | 28 | imports: [ |
@@ -63,6 +64,7 @@ import { PluginService } from '@app/core/plugins/plugin.service' | |||
63 | UnloggedGuard, | 64 | UnloggedGuard, |
64 | 65 | ||
65 | PluginService, | 66 | PluginService, |
67 | HooksService, | ||
66 | 68 | ||
67 | RedirectService, | 69 | RedirectService, |
68 | Notifier, | 70 | Notifier, |
diff --git a/client/src/app/core/plugins/hooks.service.ts b/client/src/app/core/plugins/hooks.service.ts new file mode 100644 index 000000000..80c57869c --- /dev/null +++ b/client/src/app/core/plugins/hooks.service.ts | |||
@@ -0,0 +1,44 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { PluginService } from '@app/core/plugins/plugin.service' | ||
3 | import { ClientActionHookName, ClientFilterHookName } from '@shared/models/plugins/client-hook.model' | ||
4 | import { from, Observable } from 'rxjs' | ||
5 | import { mergeMap, switchMap } from 'rxjs/operators' | ||
6 | import { ServerService } from '@app/core/server' | ||
7 | import { PluginClientScope } from '@shared/models/plugins/plugin-client-scope.type' | ||
8 | |||
9 | type RawFunction<U, T> = (params: U) => T | ||
10 | type ObservableFunction<U, T> = RawFunction<U, Observable<T>> | ||
11 | |||
12 | @Injectable() | ||
13 | export class HooksService { | ||
14 | constructor ( | ||
15 | private server: ServerService, | ||
16 | private pluginService: PluginService | ||
17 | ) { } | ||
18 | |||
19 | wrapObject<T, U extends ClientFilterHookName> (result: T, hookName: U) { | ||
20 | return this.pluginService.runHook(hookName, result) | ||
21 | } | ||
22 | |||
23 | wrapObsFun | ||
24 | <P, R, H1 extends ClientFilterHookName, H2 extends ClientFilterHookName> | ||
25 | (fun: ObservableFunction<P, R>, params: P, scope: PluginClientScope, hookParamName: H1, hookResultName: H2) { | ||
26 | return from(this.pluginService.ensurePluginsAreLoaded(scope)) | ||
27 | .pipe( | ||
28 | mergeMap(() => this.wrapObject(params, hookParamName)), | ||
29 | switchMap(params => fun(params)), | ||
30 | mergeMap(result => this.pluginService.runHook(hookResultName, result, params)) | ||
31 | ) | ||
32 | } | ||
33 | |||
34 | async wrapFun<U, T, V extends ClientFilterHookName> (fun: RawFunction<U, T>, params: U, hookName: V) { | ||
35 | const result = fun(params) | ||
36 | |||
37 | return this.pluginService.runHook(hookName, result, params) | ||
38 | } | ||
39 | |||
40 | runAction<T, U extends ClientActionHookName> (hookName: U, params?: T) { | ||
41 | this.pluginService.runHook(hookName, params) | ||
42 | .catch((err: any) => console.error('Fatal hook error.', { err })) | ||
43 | } | ||
44 | } | ||
diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts index af330c2eb..14310f093 100644 --- a/client/src/app/core/plugins/plugin.service.ts +++ b/client/src/app/core/plugins/plugin.service.ts | |||
@@ -3,11 +3,13 @@ import { Router } from '@angular/router' | |||
3 | import { ServerConfigPlugin } from '@shared/models' | 3 | import { ServerConfigPlugin } from '@shared/models' |
4 | import { ServerService } from '@app/core/server/server.service' | 4 | import { ServerService } from '@app/core/server/server.service' |
5 | import { ClientScript } from '@shared/models/plugins/plugin-package-json.model' | 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' | 6 | import { environment } from '../../../environments/environment' |
8 | import { RegisterHookOptions } from '@shared/models/plugins/register-hook.model' | 7 | import { RegisterHookOptions } from '@shared/models/plugins/register-hook.model' |
9 | import { ReplaySubject } from 'rxjs' | 8 | import { ReplaySubject } from 'rxjs' |
10 | import { first, shareReplay } from 'rxjs/operators' | 9 | import { first, shareReplay } from 'rxjs/operators' |
10 | import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks' | ||
11 | import { ClientHook, ClientHookName } from '@shared/models/plugins/client-hook.model' | ||
12 | import { PluginClientScope } from '@shared/models/plugins/plugin-client-scope.type' | ||
11 | 13 | ||
12 | interface HookStructValue extends RegisterHookOptions { | 14 | interface HookStructValue extends RegisterHookOptions { |
13 | plugin: ServerConfigPlugin | 15 | plugin: ServerConfigPlugin |
@@ -21,14 +23,18 @@ type PluginInfo = { | |||
21 | } | 23 | } |
22 | 24 | ||
23 | @Injectable() | 25 | @Injectable() |
24 | export class PluginService { | 26 | export class PluginService implements ClientHook { |
25 | pluginsLoaded = new ReplaySubject<boolean>(1) | 27 | pluginsBuilt = new ReplaySubject<boolean>(1) |
28 | |||
29 | pluginsLoaded: { [ scope in PluginClientScope ]: ReplaySubject<boolean> } = { | ||
30 | common: new ReplaySubject<boolean>(1), | ||
31 | 'video-watch': new ReplaySubject<boolean>(1) | ||
32 | } | ||
26 | 33 | ||
27 | private plugins: ServerConfigPlugin[] = [] | 34 | private plugins: ServerConfigPlugin[] = [] |
28 | private scopes: { [ scopeName: string ]: PluginInfo[] } = {} | 35 | private scopes: { [ scopeName: string ]: PluginInfo[] } = {} |
29 | private loadedPlugins: { [ name: string ]: boolean } = {} | ||
30 | private loadedScripts: { [ script: string ]: boolean } = {} | 36 | private loadedScripts: { [ script: string ]: boolean } = {} |
31 | private loadedScopes: PluginScope[] = [] | 37 | private loadedScopes: PluginClientScope[] = [] |
32 | 38 | ||
33 | private hooks: { [ name: string ]: HookStructValue[] } = {} | 39 | private hooks: { [ name: string ]: HookStructValue[] } = {} |
34 | 40 | ||
@@ -45,12 +51,18 @@ export class PluginService { | |||
45 | 51 | ||
46 | this.buildScopeStruct() | 52 | this.buildScopeStruct() |
47 | 53 | ||
48 | this.pluginsLoaded.next(true) | 54 | this.pluginsBuilt.next(true) |
49 | }) | 55 | }) |
50 | } | 56 | } |
51 | 57 | ||
52 | ensurePluginsAreLoaded () { | 58 | ensurePluginsAreBuilt () { |
53 | return this.pluginsLoaded.asObservable() | 59 | return this.pluginsBuilt.asObservable() |
60 | .pipe(first(), shareReplay()) | ||
61 | .toPromise() | ||
62 | } | ||
63 | |||
64 | ensurePluginsAreLoaded (scope: PluginClientScope) { | ||
65 | return this.pluginsLoaded[scope].asObservable() | ||
54 | .pipe(first(), shareReplay()) | 66 | .pipe(first(), shareReplay()) |
55 | .toPromise() | 67 | .toPromise() |
56 | } | 68 | } |
@@ -90,9 +102,9 @@ export class PluginService { | |||
90 | } | 102 | } |
91 | } | 103 | } |
92 | 104 | ||
93 | async loadPluginsByScope (scope: PluginScope, isReload = false) { | 105 | async loadPluginsByScope (scope: PluginClientScope, isReload = false) { |
94 | try { | 106 | try { |
95 | await this.ensurePluginsAreLoaded() | 107 | await this.ensurePluginsAreBuilt() |
96 | 108 | ||
97 | if (!isReload) this.loadedScopes.push(scope) | 109 | if (!isReload) this.loadedScopes.push(scope) |
98 | 110 | ||
@@ -111,32 +123,24 @@ export class PluginService { | |||
111 | } | 123 | } |
112 | 124 | ||
113 | await Promise.all(promises) | 125 | await Promise.all(promises) |
126 | |||
127 | this.pluginsLoaded[scope].next(true) | ||
114 | } catch (err) { | 128 | } catch (err) { |
115 | console.error('Cannot load plugins by scope %s.', scope, err) | 129 | console.error('Cannot load plugins by scope %s.', scope, err) |
116 | } | 130 | } |
117 | } | 131 | } |
118 | 132 | ||
119 | async runHook (hookName: string, param?: any) { | 133 | async runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T> { |
120 | let result = param | 134 | if (!this.hooks[hookName]) return Promise.resolve(result) |
121 | |||
122 | if (!this.hooks[hookName]) return result | ||
123 | 135 | ||
124 | const wait = hookName.startsWith('static:') | 136 | const hookType = getHookType(hookName) |
125 | 137 | ||
126 | for (const hook of this.hooks[hookName]) { | 138 | for (const hook of this.hooks[hookName]) { |
127 | try { | 139 | console.log('Running hook %s of plugin %s.', hookName, hook.plugin.name) |
128 | const p = hook.handler(param) | 140 | |
129 | 141 | result = await internalRunHook(hook.handler, hookType, result, params, err => { | |
130 | if (wait) { | 142 | console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.clientScript.script, hook.plugin.name, err) |
131 | result = await p | 143 | }) |
132 | } else if (p.catch) { | ||
133 | p.catch((err: Error) => { | ||
134 | console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.plugin, hook.clientScript, err) | ||
135 | }) | ||
136 | } | ||
137 | } catch (err) { | ||
138 | console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.plugin, hook.clientScript, err) | ||
139 | } | ||
140 | } | 144 | } |
141 | 145 | ||
142 | return result | 146 | return result |