PluginManager refactor (#6677)

* fix: refactor plugin manager

* fix: make id optional

* fix: allow add to accept single object

* fix: getHooks

* fix: tsc

* fix: remove id
This commit is contained in:
Apoorv Mishra
2024-03-16 21:22:25 +05:30
committed by GitHub
parent 6775f25425
commit 85c8f83e33
20 changed files with 187 additions and 178 deletions

View File

@@ -1,9 +1,14 @@
import { PluginManager, PluginType } from "@server/utils/PluginManager"; import { Hook, PluginManager } from "@server/utils/PluginManager";
import config from "../plugin.json"; import config from "../plugin.json";
import router from "./auth/azure"; import router from "./auth/azure";
import env from "./env"; import env from "./env";
PluginManager.register(PluginType.AuthProvider, router, { const enabled = !!env.AZURE_CLIENT_ID && !!env.AZURE_CLIENT_SECRET;
...config,
enabled: !!env.AZURE_CLIENT_ID && !!env.AZURE_CLIENT_SECRET, if (enabled) {
}); PluginManager.add({
...config,
type: Hook.AuthProvider,
value: { router, id: config.id },
});
}

View File

@@ -1,5 +1,6 @@
{ {
"id": "email", "id": "email",
"name": "Email", "name": "Email",
"priority": 200,
"description": "Adds an email magic link authentication provider." "description": "Adds an email magic link authentication provider."
} }

View File

@@ -1,9 +1,14 @@
import env from "@server/env"; import env from "@server/env";
import { PluginManager, PluginType } from "@server/utils/PluginManager"; import { Hook, PluginManager } from "@server/utils/PluginManager";
import config from "../plugin.json"; import config from "../plugin.json";
import router from "./auth/email"; import router from "./auth/email";
PluginManager.register(PluginType.AuthProvider, router, { const enabled = (!!env.SMTP_HOST && !!env.SMTP_USERNAME) || env.isDevelopment;
...config,
enabled: (!!env.SMTP_HOST && !!env.SMTP_USERNAME) || env.isDevelopment, if (enabled) {
}); PluginManager.add({
...config,
type: Hook.AuthProvider,
value: { router, id: config.id },
});
}

View File

@@ -1,9 +1,14 @@
import { PluginManager, PluginType } from "@server/utils/PluginManager"; import { PluginManager, Hook } from "@server/utils/PluginManager";
import config from "../plugin.json"; import config from "../plugin.json";
import router from "./auth/google"; import router from "./auth/google";
import env from "./env"; import env from "./env";
PluginManager.register(PluginType.AuthProvider, router, { const enabled = !!env.GOOGLE_CLIENT_ID && !!env.GOOGLE_CLIENT_SECRET;
...config,
enabled: !!env.GOOGLE_CLIENT_ID && !!env.GOOGLE_CLIENT_SECRET, if (enabled) {
}); PluginManager.add({
...config,
type: Hook.AuthProvider,
value: { router, id: config.id },
});
}

View File

@@ -1,15 +1,21 @@
import { import {
PluginManager, PluginManager,
PluginPriority, PluginPriority,
PluginType, Hook,
} from "@server/utils/PluginManager"; } from "@server/utils/PluginManager";
import env from "./env"; import env from "./env";
import Iframely from "./iframely"; import Iframely from "./iframely";
PluginManager.register(PluginType.UnfurlProvider, Iframely.get, { const enabled = !!env.IFRAMELY_API_KEY && !!env.IFRAMELY_URL;
id: "iframely",
enabled: !!env.IFRAMELY_API_KEY && !!env.IFRAMELY_URL,
// Make sure this is last in the stack to be evaluated after all other unfurl providers if (enabled) {
priority: PluginPriority.VeryLow, PluginManager.add([
}); {
type: Hook.UnfurlProvider,
value: Iframely.get,
// Make sure this is last in the stack to be evaluated after all other unfurl providers
priority: PluginPriority.VeryLow,
},
]);
}

View File

@@ -1,16 +1,21 @@
import { PluginManager, PluginType } from "@server/utils/PluginManager"; import { PluginManager, Hook } from "@server/utils/PluginManager";
import config from "../plugin.json"; import config from "../plugin.json";
import router from "./auth/oidc"; import router from "./auth/oidc";
import env from "./env"; import env from "./env";
PluginManager.register(PluginType.AuthProvider, router, { const enabled = !!(
...config, env.OIDC_CLIENT_ID &&
name: env.OIDC_DISPLAY_NAME || config.name, env.OIDC_CLIENT_SECRET &&
enabled: !!( env.OIDC_AUTH_URI &&
env.OIDC_CLIENT_ID && env.OIDC_TOKEN_URI &&
env.OIDC_CLIENT_SECRET && env.OIDC_USERINFO_URI
env.OIDC_AUTH_URI && );
env.OIDC_TOKEN_URI &&
env.OIDC_USERINFO_URI if (enabled) {
), PluginManager.add({
}); ...config,
type: Hook.AuthProvider,
value: { router, id: config.id },
name: env.OIDC_DISPLAY_NAME || config.name,
});
}

View File

@@ -1,4 +1,4 @@
import { PluginManager, PluginType } from "@server/utils/PluginManager"; import { PluginManager, Hook } from "@server/utils/PluginManager";
import config from "../plugin.json"; import config from "../plugin.json";
import hooks from "./api/hooks"; import hooks from "./api/hooks";
import router from "./auth/slack"; import router from "./auth/slack";
@@ -7,14 +7,21 @@ import SlackProcessor from "./processors/SlackProcessor";
const enabled = !!env.SLACK_CLIENT_ID && !!env.SLACK_CLIENT_SECRET; const enabled = !!env.SLACK_CLIENT_ID && !!env.SLACK_CLIENT_SECRET;
PluginManager.register(PluginType.AuthProvider, router, { if (enabled) {
...config, PluginManager.add([
enabled, {
}); ...config,
type: Hook.AuthProvider,
PluginManager.register(PluginType.API, hooks, { value: { router, id: config.id },
...config, },
enabled, {
}); ...config,
type: Hook.API,
PluginManager.registerProcessor(SlackProcessor, { enabled }); value: hooks,
},
{
type: Hook.Processor,
value: SlackProcessor,
},
]);
}

View File

@@ -1,7 +1,11 @@
import { existsSync, mkdirSync } from "fs"; import { existsSync, mkdirSync } from "fs";
import env from "@server/env"; import env from "@server/env";
import Logger from "@server/logging/Logger"; import Logger from "@server/logging/Logger";
import { PluginManager, PluginType } from "@server/utils/PluginManager"; import {
PluginManager,
PluginPriority,
Hook,
} from "@server/utils/PluginManager";
import router from "./api/files"; import router from "./api/files";
if (env.FILE_STORAGE === "local") { if (env.FILE_STORAGE === "local") {
@@ -19,13 +23,20 @@ if (env.FILE_STORAGE === "local") {
} }
} }
PluginManager.register(PluginType.API, router, { const enabled = !!(
id: "files", env.FILE_STORAGE_UPLOAD_MAX_SIZE &&
name: "Local file storage", env.FILE_STORAGE_LOCAL_ROOT_DIR &&
description: "Plugin for storing files on the local file system", env.FILE_STORAGE === "local"
enabled: !!( );
env.FILE_STORAGE_UPLOAD_MAX_SIZE &&
env.FILE_STORAGE_LOCAL_ROOT_DIR && if (enabled) {
env.FILE_STORAGE === "local" PluginManager.add([
), {
}); name: "Local file storage",
description: "Plugin for storing files on the local file system",
type: Hook.API,
value: router,
priority: PluginPriority.Normal,
},
]);
}

View File

@@ -1,5 +1,6 @@
{ {
"id": "webhooks", "id": "webhooks",
"name": "Webhooks", "name": "Webhooks",
"priority": 200,
"description": "Adds HTTP webhooks for various events." "description": "Adds HTTP webhooks for various events."
} }

View File

@@ -1,11 +1,26 @@
import { PluginManager, PluginType } from "@server/utils/PluginManager"; import { PluginManager, Hook } from "@server/utils/PluginManager";
import config from "../plugin.json"; import config from "../plugin.json";
import webhookSubscriptions from "./api/webhookSubscriptions"; import webhookSubscriptions from "./api/webhookSubscriptions";
import WebhookProcessor from "./processors/WebhookProcessor"; import WebhookProcessor from "./processors/WebhookProcessor";
import CleanupWebhookDeliveriesTask from "./tasks/CleanupWebhookDeliveriesTask"; import CleanupWebhookDeliveriesTask from "./tasks/CleanupWebhookDeliveriesTask";
import DeliverWebhookTask from "./tasks/DeliverWebhookTask"; import DeliverWebhookTask from "./tasks/DeliverWebhookTask";
PluginManager.register(PluginType.API, webhookSubscriptions, config) PluginManager.add([
.registerProcessor(WebhookProcessor) {
.registerTask(DeliverWebhookTask) ...config,
.registerTask(CleanupWebhookDeliveriesTask); type: Hook.API,
value: webhookSubscriptions,
},
{
type: Hook.Processor,
value: WebhookProcessor,
},
{
type: Hook.Task,
value: DeliverWebhookTask,
},
{
type: Hook.Task,
value: CleanupWebhookDeliveriesTask,
},
]);

View File

@@ -1,4 +1,4 @@
import { PluginManager, PluginType } from "@server/utils/PluginManager"; import { Hook, PluginManager } from "@server/utils/PluginManager";
import { requireDirectory } from "@server/utils/fs"; import { requireDirectory } from "@server/utils/fs";
import BaseEmail from "./BaseEmail"; import BaseEmail from "./BaseEmail";
@@ -14,8 +14,8 @@ requireDirectory<{ default: BaseEmail<any> }>(__dirname).forEach(
} }
); );
PluginManager.getEnabledPlugins(PluginType.EmailTemplate).forEach((plugin) => { PluginManager.getHooks(Hook.EmailTemplate).forEach((hook) => {
emails[plugin.id] = plugin.value; emails[hook.value.name] = hook.value;
}); });
export default emails; export default emails;

View File

@@ -2,7 +2,7 @@
import find from "lodash/find"; import find from "lodash/find";
import env from "@server/env"; import env from "@server/env";
import Team from "@server/models/Team"; import Team from "@server/models/Team";
import { PluginManager, PluginType } from "@server/utils/PluginManager"; import { Hook, PluginManager } from "@server/utils/PluginManager";
export default class AuthenticationHelper { export default class AuthenticationHelper {
/** /**
@@ -12,7 +12,7 @@ export default class AuthenticationHelper {
* @returns A list of authentication providers * @returns A list of authentication providers
*/ */
public static get providers() { public static get providers() {
return PluginManager.getEnabledPlugins(PluginType.AuthProvider); return PluginManager.getHooks(Hook.AuthProvider);
} }
/** /**
@@ -26,11 +26,11 @@ export default class AuthenticationHelper {
const isCloudHosted = env.isCloudHosted; const isCloudHosted = env.isCloudHosted;
return AuthenticationHelper.providers return AuthenticationHelper.providers
.sort((plugin) => (plugin.id === "email" ? 1 : -1)) .sort((hook) => (hook.value.id === "email" ? 1 : -1))
.filter((plugin) => { .filter((hook) => {
// Email sign-in is an exception as it does not have an authentication // Email sign-in is an exception as it does not have an authentication
// provider using passport, instead it exists as a boolean option. // provider using passport, instead it exists as a boolean option.
if (plugin.id === "email") { if (hook.value.id === "email") {
return team?.emailSigninEnabled; return team?.emailSigninEnabled;
} }
@@ -40,7 +40,7 @@ export default class AuthenticationHelper {
} }
const authProvider = find(team.authenticationProviders, { const authProvider = find(team.authenticationProviders, {
name: plugin.id, name: hook.value.id,
}); });
// If cloud hosted then the auth provider must be enabled for the team, // If cloud hosted then the auth provider must be enabled for the team,

View File

@@ -1,12 +1,12 @@
import { signin } from "@shared/utils/routeHelpers"; import { signin } from "@shared/utils/routeHelpers";
import { Plugin, PluginType } from "@server/utils/PluginManager"; import { Plugin, Hook } from "@server/utils/PluginManager";
export default function presentProviderConfig( export default function presentProviderConfig(
config: Plugin<PluginType.AuthProvider> config: Plugin<Hook.AuthProvider>
) { ) {
return { return {
id: config.id, id: config.value.id,
name: config.name, name: config.name,
authUrl: signin(config.id), authUrl: signin(config.value.id),
}; };
} }

View File

@@ -1,4 +1,4 @@
import { PluginManager, PluginType } from "@server/utils/PluginManager"; import { Hook, PluginManager } from "@server/utils/PluginManager";
import { requireDirectory } from "@server/utils/fs"; import { requireDirectory } from "@server/utils/fs";
import BaseProcessor from "./BaseProcessor"; import BaseProcessor from "./BaseProcessor";
@@ -13,8 +13,8 @@ requireDirectory<{ default: BaseProcessor }>(__dirname).forEach(
} }
); );
PluginManager.getEnabledPlugins(PluginType.Processor).forEach((plugin) => { PluginManager.getHooks(Hook.Processor).forEach((hook) => {
processors[plugin.id] = plugin.value; processors[hook.value.name] = hook.value;
}); });
export default processors; export default processors;

View File

@@ -1,4 +1,4 @@
import { PluginManager, PluginType } from "@server/utils/PluginManager"; import { Hook, PluginManager } from "@server/utils/PluginManager";
import { requireDirectory } from "@server/utils/fs"; import { requireDirectory } from "@server/utils/fs";
import BaseTask from "./BaseTask"; import BaseTask from "./BaseTask";
@@ -13,8 +13,8 @@ requireDirectory<{ default: BaseTask<any> }>(__dirname).forEach(
} }
); );
PluginManager.getEnabledPlugins(PluginType.Task).forEach((plugin) => { PluginManager.getHooks(Hook.Task).forEach((hook) => {
tasks[plugin.id] = plugin.value; tasks[hook.value.name] = hook.value;
}); });
export default tasks; export default tasks;

View File

@@ -89,13 +89,15 @@ router.post(
)) as AuthenticationProvider[]; )) as AuthenticationProvider[];
const data = AuthenticationHelper.providers const data = AuthenticationHelper.providers
.filter((p) => p.id !== "email") .filter((p) => p.value.id !== "email")
.map((p) => { .map((p) => {
const row = teamAuthenticationProviders.find((t) => t.name === p.id); const row = teamAuthenticationProviders.find(
(t) => t.name === p.value.id
);
return { return {
id: p.id, id: p.value.id,
name: p.id, name: p.value.id,
displayName: p.name, displayName: p.name,
isEnabled: false, isEnabled: false,
isConnected: false, isConnected: false,

View File

@@ -6,7 +6,7 @@ import env from "@server/env";
import { NotFoundError } from "@server/errors"; import { NotFoundError } from "@server/errors";
import coalesceBody from "@server/middlewares/coaleseBody"; import coalesceBody from "@server/middlewares/coaleseBody";
import { AppState, AppContext } from "@server/types"; import { AppState, AppContext } from "@server/types";
import { PluginManager, PluginType } from "@server/utils/PluginManager"; import { Hook, PluginManager } from "@server/utils/PluginManager";
import apiKeys from "./apiKeys"; import apiKeys from "./apiKeys";
import attachments from "./attachments"; import attachments from "./attachments";
import auth from "./auth"; import auth from "./auth";
@@ -59,8 +59,8 @@ api.use(apiResponse());
api.use(editor()); api.use(editor());
// Register plugin API routes before others to allow for overrides // Register plugin API routes before others to allow for overrides
PluginManager.getEnabledPlugins(PluginType.API).forEach((plugin) => PluginManager.getHooks(Hook.API).forEach((hook) =>
router.use("/", plugin.value.routes()) router.use("/", hook.value.routes())
); );
// routes // routes

View File

@@ -13,12 +13,12 @@ import { authorize } from "@server/policies";
import { presentDocument, presentMention } from "@server/presenters/unfurls"; import { presentDocument, presentMention } from "@server/presenters/unfurls";
import presentUnfurl from "@server/presenters/unfurls/unfurl"; import presentUnfurl from "@server/presenters/unfurls/unfurl";
import { APIContext } from "@server/types"; import { APIContext } from "@server/types";
import { PluginManager, PluginType } from "@server/utils/PluginManager"; import { Hook, PluginManager } from "@server/utils/PluginManager";
import { RateLimiterStrategy } from "@server/utils/RateLimiter"; import { RateLimiterStrategy } from "@server/utils/RateLimiter";
import * as T from "./schema"; import * as T from "./schema";
const router = new Router(); const router = new Router();
const plugins = PluginManager.getEnabledPlugins(PluginType.UnfurlProvider); const plugins = PluginManager.getHooks(Hook.UnfurlProvider);
router.post( router.post(
"urls.unfurl", "urls.unfurl",

View File

@@ -17,7 +17,7 @@ router.use(passport.initialize());
// dynamically load available authentication provider routes // dynamically load available authentication provider routes
AuthenticationHelper.providers.forEach((provider) => { AuthenticationHelper.providers.forEach((provider) => {
router.use("/", provider.value.routes()); router.use("/", provider.value.router.routes());
}); });
router.get("/redirect", auth(), async (ctx: APIContext) => { router.get("/redirect", auth(), async (ctx: APIContext) => {

View File

@@ -1,8 +1,8 @@
import path from "path"; import path from "path";
import { glob } from "glob"; import { glob } from "glob";
import type Router from "koa-router"; import type Router from "koa-router";
import isArray from "lodash/isArray";
import sortBy from "lodash/sortBy"; import sortBy from "lodash/sortBy";
import { v4 as uuid } from "uuid";
import { UnfurlSignature } from "@shared/types"; import { UnfurlSignature } from "@shared/types";
import type BaseEmail from "@server/emails/templates/BaseEmail"; import type BaseEmail from "@server/emails/templates/BaseEmail";
import env from "@server/env"; import env from "@server/env";
@@ -21,7 +21,7 @@ export enum PluginPriority {
/** /**
* The different types of server plugins that can be registered. * The different types of server plugins that can be registered.
*/ */
export enum PluginType { export enum Hook {
API = "api", API = "api",
AuthProvider = "authProvider", AuthProvider = "authProvider",
EmailTemplate = "emailTemplate", EmailTemplate = "emailTemplate",
@@ -35,100 +35,56 @@ export enum PluginType {
* Router. Registering an API plugin causes the router to be mounted. * Router. Registering an API plugin causes the router to be mounted.
*/ */
type PluginValueMap = { type PluginValueMap = {
[PluginType.API]: Router; [Hook.API]: Router;
[PluginType.AuthProvider]: Router; [Hook.AuthProvider]: { router: Router; id: string };
[PluginType.EmailTemplate]: typeof BaseEmail; [Hook.EmailTemplate]: typeof BaseEmail;
[PluginType.Processor]: typeof BaseProcessor; [Hook.Processor]: typeof BaseProcessor;
[PluginType.Task]: typeof BaseTask<any>; [Hook.Task]: typeof BaseTask<any>;
[PluginType.UnfurlProvider]: UnfurlSignature; [Hook.UnfurlProvider]: UnfurlSignature;
}; };
export type Plugin<T extends PluginType> = { export type Plugin<T extends Hook> = {
/** A unique ID for the plugin */ /** Plugin type */
id: string; type: T;
/** The plugin's display name */ /** The plugin's display name */
name?: string; name?: string;
/** A brief description of the plugin */ /** A brief description of the plugin */
description?: string; description?: string;
/** The plugin content */ /** The plugin content */
value: PluginValueMap[T]; value: PluginValueMap[T];
/** An optional priority, will affect order in menus and execution. Lower is earlier. */ /** Priority will affect order in menus and execution. Lower is earlier. */
priority?: number; priority?: number;
/** Whether the plugin is enabled (default: true) */
enabled?: boolean;
}; };
export class PluginManager { export class PluginManager {
private static plugins = new Map<PluginType, Plugin<PluginType>[]>(); private static plugins = new Map<Hook, Plugin<Hook>[]>();
/** /**
* Register a plugin of a given type. * Add plugins
* * @param plugins
* @param type The plugin type
* @param value The plugin value
* @param options Additional options, including whether the plugin is enabled and it's priority.
* @returns The PluginManager instance, for chaining.
*/ */
public static register<T extends PluginType>( public static add(plugins: Array<Plugin<Hook>> | Plugin<Hook>) {
type: T, if (isArray(plugins)) {
value: PluginValueMap[T], return plugins.forEach((plugin) => this.register(plugin));
options: Omit<Plugin<T>, "value"> = {
id: uuid(),
}
) {
if (!this.plugins.has(type)) {
this.plugins.set(type, []);
} }
const plugin = { this.register(plugins);
value, }
priority: PluginPriority.Normal,
...options, private static register<T extends Hook>(plugin: Plugin<T>) {
}; if (!this.plugins.has(plugin.type)) {
this.plugins.set(plugin.type, []);
}
this.plugins
.get(plugin.type)!
.push({ ...plugin, priority: plugin.priority ?? PluginPriority.Normal });
Logger.debug( Logger.debug(
"plugins", "plugins",
`Plugin ${options.enabled === false ? "disabled" : "enabled"} "${ `Plugin(type=${plugin.type}) registered ${
options.id "name" in plugin.value ? plugin.value.name : ""
}" ${options.description ? `(${options.description})` : ""}` } ${plugin.description ? `(${plugin.description})` : ""}`
); );
this.plugins.get(type)!.push(plugin);
// allow chaining
return this;
}
/**
* Syntactic sugar for registering a background Task.
*
* @param value The task class
* @param options Additional options
*/
public static registerTask(
value: PluginValueMap[PluginType.Task],
options?: Omit<Plugin<PluginType.Task>, "id" | "value">
) {
return this.register(PluginType.Task, value, {
id: value.name,
...options,
});
}
/**
* Syntactic sugar for registering a background Processor.
*
* @param value The processor class
* @param options Additional options
*/
public static registerProcessor(
value: PluginValueMap[PluginType.Processor],
options?: Omit<Plugin<PluginType.Processor>, "id" | "value">
) {
return this.register(PluginType.Processor, value, {
id: value.name,
...options,
});
} }
/** /**
@@ -137,21 +93,11 @@ export class PluginManager {
* @param type The type of plugin to filter by * @param type The type of plugin to filter by
* @returns A list of plugins * @returns A list of plugins
*/ */
public static getPlugins<T extends PluginType>(type: T) { public static getHooks<T extends Hook>(type: T) {
this.loadPlugins(); this.loadPlugins();
return sortBy(this.plugins.get(type) || [], "priority") as Plugin<T>[]; return sortBy(this.plugins.get(type) || [], "priority") as Plugin<T>[];
} }
/**
* Returns all the enabled plugins of a given type in order of priority.
*
* @param type The type of plugin to filter by
* @returns A list of plugins
*/
public static getEnabledPlugins<T extends PluginType>(type: T) {
return this.getPlugins(type).filter((plugin) => plugin.enabled !== false);
}
/** /**
* Load plugin server components (anything in the `/server/` directory of a plugin will be loaded) * Load plugin server components (anything in the `/server/` directory of a plugin will be loaded)
*/ */