From ebb9886d4b69310aadd4a33e31d9359123bc9c1a Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Sun, 19 Nov 2023 01:43:10 +0100 Subject: [PATCH] Add messenger extensibility to extensions #753 --- .../model/extension/ExpressRouterWrapper.ts | 8 +++-- .../model/extension/ExtensionManager.ts | 9 +++--- .../extension/ExtensionMessengerHandler.ts | 31 +++++++++++++++++++ .../model/extension/ExtensionObject.ts | 3 ++ src/backend/model/extension/IExtension.ts | 29 +++++++++++++++-- .../model/messenger/ExtensionMessenger.ts | 15 +++++++++ .../model/messenger/MessengerRepository.ts | 7 +++++ 7 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 src/backend/model/extension/ExtensionMessengerHandler.ts create mode 100644 src/backend/model/messenger/ExtensionMessenger.ts diff --git a/src/backend/model/extension/ExpressRouterWrapper.ts b/src/backend/model/extension/ExpressRouterWrapper.ts index 6f033126..550d1797 100644 --- a/src/backend/model/extension/ExpressRouterWrapper.ts +++ b/src/backend/model/extension/ExpressRouterWrapper.ts @@ -62,7 +62,9 @@ export class ExpressRouteWrapper implements IExtensionRESTRoute { }, RenderingMWs.renderResult ]))); - this.extLogger.silly(`Listening on ${this.func} ${ExtensionManager.EXTENSION_API_PATH}${fullPaths}`); + const p = ExtensionManager.EXTENSION_API_PATH + fullPaths; + this.extLogger.silly(`Listening on ${this.func} ${p}`); + return p; } public rawMiddleware(paths: string[], minRole: UserRoles, mw: (req: Request, res: Response, next: NextFunction) => void | Promise) { @@ -70,6 +72,8 @@ export class ExpressRouteWrapper implements IExtensionRESTRoute { this.router[this.func](fullPaths, ...this.getAuthMWs(minRole), mw); - this.extLogger.silly(`Listening on ${this.func} ${ExtensionManager.EXTENSION_API_PATH}${fullPaths}`); + const p = ExtensionManager.EXTENSION_API_PATH + fullPaths; + this.extLogger.silly(`Listening on ${this.func} ${p}`); + return p; } } diff --git a/src/backend/model/extension/ExtensionManager.ts b/src/backend/model/extension/ExtensionManager.ts index 1d9ee898..2fd4b36e 100644 --- a/src/backend/model/extension/ExtensionManager.ts +++ b/src/backend/model/extension/ExtensionManager.ts @@ -4,7 +4,7 @@ import * as fs from 'fs'; import * as path from 'path'; import {IObjectManager} from '../database/IObjectManager'; import {Logger} from '../../Logger'; -import {IExtensionEvents, IExtensionObject, IServerExtension} from './IExtension'; +import {IExtensionEvents, IExtensionObject} from './IExtension'; import {Server} from '../../server'; import {ExtensionEvent} from './ExtensionEvent'; import * as express from 'express'; @@ -108,8 +108,8 @@ export class ExtensionManager implements IObjectManager { if (fs.existsSync(packageJsonPath)) { Logger.silly(LOG_TAG, `Running: "npm install --omit=dev" in ${extPath}`); - await exec('npm install --omit=dev' ,{ - cwd:extPath + await exec('npm install --omit=dev', { + cwd: extPath }); // eslint-disable-next-line @typescript-eslint/no-var-requires const pkg = require(packageJsonPath); @@ -138,8 +138,9 @@ export class ExtensionManager implements IObjectManager { const ext = require(serverExt); if (typeof ext?.cleanUp === 'function') { Logger.debug(LOG_TAG, 'Running Init on extension:' + extObj.extensionName); - await ext?.cleanUp(ext); + await ext?.cleanUp(extObj); } + extObj.messengers.cleanUp(); } } diff --git a/src/backend/model/extension/ExtensionMessengerHandler.ts b/src/backend/model/extension/ExtensionMessengerHandler.ts new file mode 100644 index 00000000..ff7aeddb --- /dev/null +++ b/src/backend/model/extension/ExtensionMessengerHandler.ts @@ -0,0 +1,31 @@ +import {IExtensionMessengers} from './IExtension'; +import {DynamicConfig} from '../../../common/entities/DynamicConfig'; +import {MediaDTOWithThPath, Messenger} from '../messenger/Messenger'; +import {ExtensionMessenger} from '../messenger/ExtensionMessenger'; +import {MessengerRepository} from '../messenger/MessengerRepository'; +import {ILogger} from '../../Logger'; + +export class ExtensionMessengerHandler implements IExtensionMessengers { + + messengers: Messenger[] = []; + + + constructor(private readonly extLogger: ILogger) { + } + + + addMessenger>(name: string, config: DynamicConfig[], callbacks: { + sendMedia: (config: C, media: MediaDTOWithThPath[]) => Promise + }): void { + this.extLogger.silly('Adding new Messenger:', name); + const em = new ExtensionMessenger(name, config, callbacks); + this.messengers.push(em); + MessengerRepository.Instance.register(em); + } + + cleanUp() { + this.extLogger.silly('Removing Messenger'); + this.messengers.forEach(m => MessengerRepository.Instance.remove(m)); + } + +} diff --git a/src/backend/model/extension/ExtensionObject.ts b/src/backend/model/extension/ExtensionObject.ts index dcc35ed1..3ff1aec6 100644 --- a/src/backend/model/extension/ExtensionObject.ts +++ b/src/backend/model/extension/ExtensionObject.ts @@ -6,6 +6,7 @@ import {ProjectPath} from '../../ProjectPath'; import {ExpressRouterWrapper} from './ExpressRouterWrapper'; import {createLoggerWrapper} from '../../Logger'; import * as express from 'express'; +import {ExtensionMessengerHandler} from './ExtensionMessengerHandler'; export class ExtensionObject implements IExtensionObject { @@ -16,6 +17,7 @@ export class ExtensionObject implements IExtensionObject { public readonly Logger; public readonly events; public readonly RESTApi; + public readonly messengers; constructor(public readonly extensionId: string, public readonly extensionName: string, @@ -30,6 +32,7 @@ export class ExtensionObject implements IExtensionObject { this.Logger = logger; this.events = events; this.RESTApi = new ExpressRouterWrapper(extensionRouter, extensionId, logger); + this.messengers = new ExtensionMessengerHandler(logger); } } diff --git a/src/backend/model/extension/IExtension.ts b/src/backend/model/extension/IExtension.ts index 038cdf7a..3ce85fce 100644 --- a/src/backend/model/extension/IExtension.ts +++ b/src/backend/model/extension/IExtension.ts @@ -7,6 +7,8 @@ import {ILogger} from '../../Logger'; import {UserDTO, UserRoles} from '../../../common/entities/UserDTO'; import {ParamsDictionary} from 'express-serve-static-core'; import {Connection} from 'typeorm'; +import {DynamicConfig} from '../../../common/entities/DynamicConfig'; +import {MediaDTOWithThPath} from '../messenger/Messenger'; export type IExtensionBeforeEventHandler = (input: { inputs: I }, event: { stopPropagation: boolean }) => Promise<{ inputs: I } | O>; @@ -69,16 +71,18 @@ export interface IExtensionRESTRoute { * @param paths RESTapi path, relative to the extension base endpoint * @param minRole set to null to omit auer check (ie make the endpoint public) * @param cb function callback + * @return newly added REST api path */ - jsonResponse(paths: string[], minRole: UserRoles, cb: (params?: ParamsDictionary, body?: any, user?: UserDTO) => Promise | unknown): void; + jsonResponse(paths: string[], minRole: UserRoles, cb: (params?: ParamsDictionary, body?: any, user?: UserDTO) => Promise | unknown): string; /** * Exposes a standard expressjs middleware * @param paths RESTapi path, relative to the extension base endpoint * @param minRole set to null to omit auer check (ie make the endpoint public) * @param mw expressjs middleware + * @return newly added REST api path */ - rawMiddleware(paths: string[], minRole: UserRoles, mw: (req: Request, res: Response, next: NextFunction) => void | Promise): void; + rawMiddleware(paths: string[], minRole: UserRoles, mw: (req: Request, res: Response, next: NextFunction) => void | Promise): string; } export interface IExtensionRESTApi { @@ -116,9 +120,21 @@ export interface IExtensionConfig { getConfig(): C; } +export interface IExtensionMessengers { + /** + * Adds a new messenger that the user can select e.g.: for sending top pick photos + * @param name Name of the messenger (also used as id) + * @param config config metadata for this messenger + * @param callbacks messenger logic + */ + addMessenger = Record>(name: string, config: DynamicConfig[], callbacks: { + sendMedia: (config: C, media: MediaDTOWithThPath[]) => Promise + }): void; +} + export interface IExtensionObject { /** - * ID of the extension that is internally used. By default the name and ID matches if there is no collision. + * ID of the extension that is internally used. By default, the name and ID matches if there is no collision. */ extensionId: string, @@ -159,6 +175,13 @@ export interface IExtensionObject { * Use this to define REST calls related to the extension */ RESTApi: IExtensionRESTApi; + + /** + * Object to manipulate messengers. + * Messengers are used to send messages (like emails) from the app. + * One type of message is a list of selected photos. + */ + messengers: IExtensionMessengers; } diff --git a/src/backend/model/messenger/ExtensionMessenger.ts b/src/backend/model/messenger/ExtensionMessenger.ts new file mode 100644 index 00000000..24d7caf5 --- /dev/null +++ b/src/backend/model/messenger/ExtensionMessenger.ts @@ -0,0 +1,15 @@ +import {MediaDTOWithThPath, Messenger} from './Messenger'; +import {DynamicConfig} from '../../../common/entities/DynamicConfig'; + +export class ExtensionMessenger = Record> extends Messenger { + + constructor(public readonly Name: string, + public readonly ConfigTemplate: DynamicConfig[], + private readonly callbacks: { sendMedia: (config: C, media: MediaDTOWithThPath[]) => Promise }) { + super(); + } + + protected sendMedia(config: C, media: MediaDTOWithThPath[]): Promise { + return this.callbacks.sendMedia(config, media); + } +} diff --git a/src/backend/model/messenger/MessengerRepository.ts b/src/backend/model/messenger/MessengerRepository.ts index c6d162ee..31a98163 100644 --- a/src/backend/model/messenger/MessengerRepository.ts +++ b/src/backend/model/messenger/MessengerRepository.ts @@ -18,6 +18,13 @@ export class MessengerRepository { return Object.values(this.messengers); } + remove(m: Messenger>): void { + if (!this.messengers[m.Name]) { + throw new Error('Messenger does not exist:' + m.Name); + } + delete this.messengers[m.Name]; + } + register(msgr: Messenger): void { if (typeof this.messengers[msgr.Name] !== 'undefined') { throw new Error('Messenger already exist:' + msgr.Name);