From d257eb8253472b89dd0d84ac72e41245b710d615 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 19 May 2020 10:34:39 -0300 Subject: [PATCH] New room events (#266) * Fix security problem * Add IPreRoomUserJoined event * Add the IPostRoomUserJoined event * New "essentials" mechanism (#269) --- src/definition/AppStatus.ts | 7 + .../exceptions/AppsEngineException.ts | 14 ++ .../EssentialAppDisabledException.ts | 17 +++ .../exceptions/UserNotAllowedException.ts | 14 ++ src/definition/exceptions/index.ts | 3 + src/definition/metadata/AppInterface.ts | 36 +++++ src/definition/metadata/AppMethod.ts | 2 + src/definition/metadata/IAppInfo.ts | 2 + src/definition/metadata/index.ts | 2 + src/definition/rooms/IPostRoomUserJoined.ts | 19 +++ src/definition/rooms/IPreRoomUserJoined.ts | 18 +++ .../rooms/IRoomUserJoinedContext.ts | 23 ++++ src/definition/rooms/index.ts | 4 + src/server/AppManager.ts | 61 +++++---- src/server/ProxiedApp.ts | 10 +- src/server/bridges/IListenerBridge.ts | 2 +- src/server/compiler/AppImplements.ts | 36 +---- src/server/compiler/index.ts | 3 +- src/server/managers/AppListenerManager.ts | 129 ++++++++++++++++-- src/server/rooms/Room.ts | 15 +- .../AppFabricationFulfillment.spec.ts | 5 +- tests/server/compiler/AppImplements.spec.ts | 4 +- .../managers/AppListenerManager.spec.ts | 2 +- 23 files changed, 341 insertions(+), 87 deletions(-) create mode 100644 src/definition/exceptions/AppsEngineException.ts create mode 100644 src/definition/exceptions/EssentialAppDisabledException.ts create mode 100644 src/definition/exceptions/UserNotAllowedException.ts create mode 100644 src/definition/exceptions/index.ts create mode 100644 src/definition/metadata/AppInterface.ts create mode 100644 src/definition/rooms/IPostRoomUserJoined.ts create mode 100644 src/definition/rooms/IPreRoomUserJoined.ts create mode 100644 src/definition/rooms/IRoomUserJoinedContext.ts diff --git a/src/definition/AppStatus.ts b/src/definition/AppStatus.ts index 3990ce4cc..30e843e06 100644 --- a/src/definition/AppStatus.ts +++ b/src/definition/AppStatus.ts @@ -51,6 +51,13 @@ export class AppStatusUtilsDef { return false; } } + + public isError(status: AppStatus): boolean { + return [ + AppStatus.ERROR_DISABLED, + AppStatus.COMPILER_ERROR_DISABLED, + ].includes(status); + } } export const AppStatusUtils = new AppStatusUtilsDef(); diff --git a/src/definition/exceptions/AppsEngineException.ts b/src/definition/exceptions/AppsEngineException.ts new file mode 100644 index 000000000..0496bc414 --- /dev/null +++ b/src/definition/exceptions/AppsEngineException.ts @@ -0,0 +1,14 @@ +/** + * The internal exception from the framework + * + * It's used to signal to the outside world that + * a _known_ exception has happened during the execution + * of the apps. + * + * It's the base exception for other known classes + * such as UserNotAllowedException, which is used + * to inform the host that an app identified + * that a user cannot perform some action, e.g. + * join a room + */ +export class AppsEngineException extends Error {} diff --git a/src/definition/exceptions/EssentialAppDisabledException.ts b/src/definition/exceptions/EssentialAppDisabledException.ts new file mode 100644 index 000000000..be7d2dc1a --- /dev/null +++ b/src/definition/exceptions/EssentialAppDisabledException.ts @@ -0,0 +1,17 @@ +import { AppsEngineException } from '.'; + +/** + * This exception informs the host system that an + * app essential to the execution of a system action + * is disabled, so the action should be halted. + * + * Apps can register to be considered essential to + * the execution of internal events of the framework + * such as `IPreMessageSentPrevent`, `IPreRoomUserJoined`, + * etc. + * + * This is used interally by the framework and is not + * intended to be thrown manually by apps. + */ +export class EssentialAppDisabledException extends AppsEngineException { +} diff --git a/src/definition/exceptions/UserNotAllowedException.ts b/src/definition/exceptions/UserNotAllowedException.ts new file mode 100644 index 000000000..2b3dd4d0e --- /dev/null +++ b/src/definition/exceptions/UserNotAllowedException.ts @@ -0,0 +1,14 @@ +import { AppsEngineException } from '.'; + +/** + * This exception informs the host system that an + * app has determined that an user is not allowed + * to perform a specific action. + * + * Currently it is expected to be thrown by the + * following events: + * - IPreRoomCreatePrevent + * - IPreRoomUserJoined + */ +export class UserNotAllowedException extends AppsEngineException { +} diff --git a/src/definition/exceptions/index.ts b/src/definition/exceptions/index.ts new file mode 100644 index 000000000..cd378444e --- /dev/null +++ b/src/definition/exceptions/index.ts @@ -0,0 +1,3 @@ +export * from './AppsEngineException'; +export * from './EssentialAppDisabledException'; +export * from './UserNotAllowedException'; diff --git a/src/definition/metadata/AppInterface.ts b/src/definition/metadata/AppInterface.ts new file mode 100644 index 000000000..42083112b --- /dev/null +++ b/src/definition/metadata/AppInterface.ts @@ -0,0 +1,36 @@ +export enum AppInterface { + // Messages + IPreMessageSentPrevent = 'IPreMessageSentPrevent', + IPreMessageSentExtend = 'IPreMessageSentExtend', + IPreMessageSentModify = 'IPreMessageSentModify', + IPostMessageSent = 'IPostMessageSent', + IPreMessageDeletePrevent = 'IPreMessageDeletePrevent', + IPostMessageDeleted = 'IPostMessageDeleted', + IPreMessageUpdatedPrevent = 'IPreMessageUpdatedPrevent', + IPreMessageUpdatedExtend = 'IPreMessageUpdatedExtend', + IPreMessageUpdatedModify = 'IPreMessageUpdatedModify', + IPostMessageUpdated = 'IPostMessageUpdated', + // Rooms + IPreRoomCreatePrevent = 'IPreRoomCreatePrevent', + IPreRoomCreateExtend = 'IPreRoomCreateExtend', + IPreRoomCreateModify = 'IPreRoomCreateModify', + IPostRoomCreate = 'IPostRoomCreate', + IPreRoomDeletePrevent = 'IPreRoomDeletePrevent', + IPostRoomDeleted = 'IPostRoomDeleted', + IPreRoomUserJoined = 'IPreRoomUserJoined', + IPostRoomUserJoined = 'IPostRoomUserJoined', + // External Components + IPostExternalComponentOpened = 'IPostExternalComponentOpened', + IPostExternalComponentClosed = 'IPostExternalComponentClosed', + // Blocks + IUIKitInteractionHandler = 'IUIKitInteractionHandler', + // Livechat + IPostLivechatRoomStarted = 'IPostLivechatRoomStarted', + IPostLivechatRoomClosed = 'IPostLivechatRoomClosed', + /** + * @deprecated please use the AppMethod.EXECUTE_POST_LIVECHAT_ROOM_CLOSED method + */ + ILivechatRoomClosedHandler = 'ILivechatRoomClosedHandler', + IPostLivechatAgentAssigned = 'IPostLivechatAgentAssigned', + IPostLivechatAgentUnassigned = 'IPostLivechatAgentUnassigned', +} diff --git a/src/definition/metadata/AppMethod.ts b/src/definition/metadata/AppMethod.ts index 56ecacfd1..968d084e0 100644 --- a/src/definition/metadata/AppMethod.ts +++ b/src/definition/metadata/AppMethod.ts @@ -43,6 +43,8 @@ export enum AppMethod { EXECUTEPREROOMDELETEPREVENT = 'executePreRoomDeletePrevent', CHECKPOSTROOMDELETED = 'checkPostRoomDeleted', EXECUTEPOSTROOMDELETED = 'executePostRoomDeleted', + EXECUTE_PRE_ROOM_USER_JOINED = 'executePreRoomUserJoined', + EXECUTE_POST_ROOM_USER_JOINED = 'executePostRoomUserJoined', // External Component handlers EXECUTEPOSTEXTERNALCOMPONENTOPENED = 'executePostExternalComponentOpened', EXECUTEPOSTEXTERNALCOMPONENTCLOSED = 'executePostExternalComponentClosed', diff --git a/src/definition/metadata/IAppInfo.ts b/src/definition/metadata/IAppInfo.ts index e48075951..3fa484869 100644 --- a/src/definition/metadata/IAppInfo.ts +++ b/src/definition/metadata/IAppInfo.ts @@ -1,3 +1,4 @@ +import { AppInterface } from './AppInterface'; import { IAppAuthorInfo } from './IAppAuthorInfo'; export interface IAppInfo { @@ -12,4 +13,5 @@ export interface IAppInfo { iconFile: string; /** Base64 string of the App's icon. */ iconFileContent?: string; + essentials?: Array; } diff --git a/src/definition/metadata/index.ts b/src/definition/metadata/index.ts index f0c7e95b7..5894b2eca 100644 --- a/src/definition/metadata/index.ts +++ b/src/definition/metadata/index.ts @@ -3,6 +3,8 @@ import { IAppAuthorInfo } from './IAppAuthorInfo'; import { IAppInfo } from './IAppInfo'; import { RocketChatAssociationModel, RocketChatAssociationRecord } from './RocketChatAssociations'; +export * from './AppInterface'; + export { AppMethod, IAppAuthorInfo, diff --git a/src/definition/rooms/IPostRoomUserJoined.ts b/src/definition/rooms/IPostRoomUserJoined.ts new file mode 100644 index 000000000..f05308590 --- /dev/null +++ b/src/definition/rooms/IPostRoomUserJoined.ts @@ -0,0 +1,19 @@ +import { IHttp, IPersistence, IRead } from '../accessors'; +import { IRoomUserJoinedContext } from './IRoomUserJoinedContext'; + +/** + * Event interface that allows an app to + * register as a handler of the `IPostRoomUserJoined` + * event + * + * This event is triggered after an user succcessfully joined + * a room. + * + * This event does not allow an app to prevent any action from + * happening. For that, see its "pre counterpart(s)": + * + * - IPreRoomUserJoined + */ +export interface IPostRoomUserJoined { + executePostRoomUserJoined(context: IRoomUserJoinedContext, read: IRead, http: IHttp, persistence: IPersistence): Promise; +} diff --git a/src/definition/rooms/IPreRoomUserJoined.ts b/src/definition/rooms/IPreRoomUserJoined.ts new file mode 100644 index 000000000..525332553 --- /dev/null +++ b/src/definition/rooms/IPreRoomUserJoined.ts @@ -0,0 +1,18 @@ +import { IHttp, IPersistence, IRead } from '../accessors'; +import { IRoomUserJoinedContext } from './IRoomUserJoinedContext'; + +/** + * Event interface that allows an app to + * register as a handler of the `IPreRoomUserJoined` + * event + * + * This event is triggered prior to an user succcessfully + * joining a room. To prevent the user from executing + * such action, an app should throw the `UserNotAllowedException`. + * + * This event is not triggered before a room has been created. For that, + * check the `IPreRoomCreate` events + */ +export interface IPreRoomUserJoined { + executePreRoomUserJoined(context: IRoomUserJoinedContext, read: IRead, http: IHttp, persistence: IPersistence): Promise; +} diff --git a/src/definition/rooms/IRoomUserJoinedContext.ts b/src/definition/rooms/IRoomUserJoinedContext.ts new file mode 100644 index 000000000..c76e9d9ba --- /dev/null +++ b/src/definition/rooms/IRoomUserJoinedContext.ts @@ -0,0 +1,23 @@ +import { IUser } from '../users'; +import { IRoom } from './IRoom'; + +/** + * The context of execution for the following events: + * - IPreRoomUserJoined + * - IPostRoomUserJoined + */ +export interface IRoomUserJoinedContext { + /** + * The user that is being added to the room + */ + joiningUser: IUser; + /** + * The room to which the user is being added + */ + room: IRoom; + /** + * The user that has invited `joiningUser` to `room`, + * if any. + */ + inviter?: IUser; +} diff --git a/src/definition/rooms/index.ts b/src/definition/rooms/index.ts index 3e83b3181..12b88c923 100644 --- a/src/definition/rooms/index.ts +++ b/src/definition/rooms/index.ts @@ -17,3 +17,7 @@ export { IPreRoomCreatePrevent, IPreRoomDeletePrevent, }; + +export * from './IPreRoomUserJoined'; +export * from './IPostRoomUserJoined'; +export * from './IRoomUserJoinedContext'; diff --git a/src/server/AppManager.ts b/src/server/AppManager.ts index d8b84882c..f9135e983 100644 --- a/src/server/AppManager.ts +++ b/src/server/AppManager.ts @@ -222,9 +222,11 @@ export class AppManager { // Now let's enable the apps which were once enabled // but are not currently disabled. - for (const rl of this.apps.values()) { - if (!AppStatusUtils.isDisabled(rl.getStatus()) && AppStatusUtils.isEnabled(rl.getPreviousStatus())) { - await this.enableApp(items.get(rl.getID()), rl, true, rl.getPreviousStatus() === AppStatus.MANUALLY_ENABLED).catch(console.error); + for (const app of this.apps.values()) { + if (!AppStatusUtils.isDisabled(app.getStatus()) && AppStatusUtils.isEnabled(app.getPreviousStatus())) { + await this.enableApp(items.get(app.getID()), app, true, app.getPreviousStatus() === AppStatus.MANUALLY_ENABLED).catch(console.error); + } else if (!AppStatusUtils.isError(app.getStatus())) { + this.listenerManager.lockEssentialEvents(app); } } @@ -239,21 +241,18 @@ export class AppManager { return; } - for (const rl of this.apps.values()) { - if (AppStatusUtils.isDisabled(rl.getStatus())) { - continue; - } - - if (rl.getStatus() === AppStatus.INITIALIZED) { - this.listenerManager.unregisterListeners(rl); - this.commandManager.unregisterCommands(rl.getID()); - this.externalComponentManager.unregisterExternalComponents(rl.getID()); - this.apiManager.unregisterApis(rl.getID()); - this.accessorManager.purifyApp(rl.getID()); - continue; + for (const app of this.apps.values()) { + if (app.getStatus() === AppStatus.INITIALIZED) { + this.listenerManager.unregisterListeners(app); + this.commandManager.unregisterCommands(app.getID()); + this.externalComponentManager.unregisterExternalComponents(app.getID()); + this.apiManager.unregisterApis(app.getID()); + this.accessorManager.purifyApp(app.getID()); + } else if (!AppStatusUtils.isDisabled(app.getStatus())) { + await this.disable(app.getID(), isManual ? AppStatus.MANUALLY_DISABLED : AppStatus.DISABLED); } - await this.disable(rl.getID(), isManual ? AppStatus.MANUALLY_DISABLED : AppStatus.DISABLED); + this.listenerManager.releaseEssentialEvents(app); } // Remove all the apps from the system now that we have unloaded everything @@ -350,33 +349,34 @@ export class AppManager { throw new Error('Invalid disabled status'); } - const rl = this.apps.get(id); + const app = this.apps.get(id); - if (!rl) { + if (!app) { throw new Error(`No App by the id "${id}" exists.`); } - if (AppStatusUtils.isEnabled(rl.getStatus())) { - await rl.call(AppMethod.ONDISABLE, this.accessorManager.getConfigurationModify(rl.getID())) + if (AppStatusUtils.isEnabled(app.getStatus())) { + await app.call(AppMethod.ONDISABLE, this.accessorManager.getConfigurationModify(app.getID())) .catch((e) => console.warn('Error while disabling:', e)); } - this.listenerManager.unregisterListeners(rl); - this.commandManager.unregisterCommands(rl.getID()); - this.externalComponentManager.unregisterExternalComponents(rl.getID()); - this.apiManager.unregisterApis(rl.getID()); - this.accessorManager.purifyApp(rl.getID()); + this.listenerManager.unregisterListeners(app); + this.listenerManager.lockEssentialEvents(app); + this.commandManager.unregisterCommands(app.getID()); + this.externalComponentManager.unregisterExternalComponents(app.getID()); + this.apiManager.unregisterApis(app.getID()); + this.accessorManager.purifyApp(app.getID()); - await rl.setStatus(status, silent); + await app.setStatus(status, silent); const storageItem = await this.storage.retrieveOne(id); - rl.getStorageItem().marketplaceInfo = storageItem.marketplaceInfo; - await rl.validateLicense().catch(); + app.getStorageItem().marketplaceInfo = storageItem.marketplaceInfo; + await app.validateLicense().catch(); // This is async, but we don't care since it only updates in the database // and it should not mutate any properties we care about - storageItem.status = rl.getStatus(); + storageItem.status = app.getStatus(); await this.storage.update(storageItem).catch(); return true; @@ -462,6 +462,7 @@ export class AppManager { } this.listenerManager.unregisterListeners(app); + this.listenerManager.releaseEssentialEvents(app); this.commandManager.unregisterCommands(app.getID()); this.externalComponentManager.purgeExternalComponents(app.getID()); this.apiManager.unregisterApis(app.getID()); @@ -796,10 +797,12 @@ export class AppManager { this.externalComponentManager.registerExternalComponents(app.getID()); this.apiManager.registerApis(app.getID()); this.listenerManager.registerListeners(app); + this.listenerManager.releaseEssentialEvents(app); } else { this.commandManager.unregisterCommands(app.getID()); this.externalComponentManager.unregisterExternalComponents(app.getID()); this.apiManager.unregisterApis(app.getID()); + this.listenerManager.lockEssentialEvents(app); } if (saveToDb) { diff --git a/src/server/ProxiedApp.ts b/src/server/ProxiedApp.ts index dbb929744..6d210bc85 100644 --- a/src/server/ProxiedApp.ts +++ b/src/server/ProxiedApp.ts @@ -1,8 +1,8 @@ import * as vm from 'vm'; - import { IAppAccessors, ILogger } from '../definition/accessors'; import { App } from '../definition/App'; import { AppStatus } from '../definition/AppStatus'; +import { AppsEngineException } from '../definition/exceptions'; import { IApp } from '../definition/IApp'; import { AppMethod, IAppAuthorInfo, IAppInfo } from '../definition/metadata'; import { AppManager } from './AppManager'; @@ -88,6 +88,10 @@ export class ProxiedApp implements IApp { } catch (e) { logger.error(e); logger.debug(`'${method}' was unsuccessful.`); + + if (e instanceof AppsEngineException) { + throw e; + } } this.manager.getLogStorage().storeEntries(this.getID(), logger); @@ -150,6 +154,10 @@ export class ProxiedApp implements IApp { return this.app.getAccessors(); } + public getEssentials(): IAppInfo['essentials'] { + return this.getInfo().essentials; + } + public getLatestLicenseValidationResult(): AppLicenseValidationResult { return this.latestLicenseValidationResult; } diff --git a/src/server/bridges/IListenerBridge.ts b/src/server/bridges/IListenerBridge.ts index fbf103678..cce42858e 100644 --- a/src/server/bridges/IListenerBridge.ts +++ b/src/server/bridges/IListenerBridge.ts @@ -1,7 +1,7 @@ import { IMessage } from '../../definition/messages'; +import { AppInterface } from '../../definition/metadata'; import { IRoom } from '../../definition/rooms'; import { IUIKitIncomingInteraction } from '../../definition/uikit'; -import { AppInterface } from '../compiler'; export interface IListenerBridge { messageEvent(int: AppInterface, message: IMessage): Promise; diff --git a/src/server/compiler/AppImplements.ts b/src/server/compiler/AppImplements.ts index 78890505e..459138a33 100644 --- a/src/server/compiler/AppImplements.ts +++ b/src/server/compiler/AppImplements.ts @@ -1,40 +1,6 @@ +import { AppInterface } from '../../definition/metadata/AppInterface'; import { Utilities } from '../misc/Utilities'; -export enum AppInterface { - // Messages - IPreMessageSentPrevent = 'IPreMessageSentPrevent', - IPreMessageSentExtend = 'IPreMessageSentExtend', - IPreMessageSentModify = 'IPreMessageSentModify', - IPostMessageSent = 'IPostMessageSent', - IPreMessageDeletePrevent = 'IPreMessageDeletePrevent', - IPostMessageDeleted = 'IPostMessageDeleted', - IPreMessageUpdatedPrevent = 'IPreMessageUpdatedPrevent', - IPreMessageUpdatedExtend = 'IPreMessageUpdatedExtend', - IPreMessageUpdatedModify = 'IPreMessageUpdatedModify', - IPostMessageUpdated = 'IPostMessageUpdated', - // Rooms - IPreRoomCreatePrevent = 'IPreRoomCreatePrevent', - IPreRoomCreateExtend = 'IPreRoomCreateExtend', - IPreRoomCreateModify = 'IPreRoomCreateModify', - IPostRoomCreate = 'IPostRoomCreate', - IPreRoomDeletePrevent = 'IPreRoomDeletePrevent', - IPostRoomDeleted = 'IPostRoomDeleted', - // External Components - IPostExternalComponentOpened = 'IPostExternalComponentOpened', - IPostExternalComponentClosed = 'IPostExternalComponentClosed', - // Blocks - IUIKitInteractionHandler = 'IUIKitInteractionHandler', - // Livechat - IPostLivechatRoomStarted = 'IPostLivechatRoomStarted', - IPostLivechatRoomClosed = 'IPostLivechatRoomClosed', - /** - * @deprecated please use the AppMethod.EXECUTE_POST_LIVECHAT_ROOM_CLOSED method - */ - ILivechatRoomClosedHandler = 'ILivechatRoomClosedHandler', - IPostLivechatAgentAssigned = 'IPostLivechatAgentAssigned', - IPostLivechatAgentUnassigned = 'IPostLivechatAgentUnassigned', -} - export class AppImplements { private implemented: { [key: string]: boolean }; diff --git a/src/server/compiler/index.ts b/src/server/compiler/index.ts index 7e72478ab..4bd13fe1a 100644 --- a/src/server/compiler/index.ts +++ b/src/server/compiler/index.ts @@ -1,6 +1,6 @@ import { AppCompiler } from './AppCompiler'; import { AppFabricationFulfillment } from './AppFabricationFulfillment'; -import { AppImplements, AppInterface } from './AppImplements'; +import { AppImplements } from './AppImplements'; import { AppPackageParser } from './AppPackageParser'; import { ICompilerError } from './ICompilerError'; import { ICompilerFile } from './ICompilerFile'; @@ -11,7 +11,6 @@ export { AppCompiler, AppFabricationFulfillment, AppImplements, - AppInterface, AppPackageParser, ICompilerFile, ICompilerError, diff --git a/src/server/managers/AppListenerManager.ts b/src/server/managers/AppListenerManager.ts index d046184a8..fb1da07c5 100644 --- a/src/server/managers/AppListenerManager.ts +++ b/src/server/managers/AppListenerManager.ts @@ -1,8 +1,9 @@ +import { EssentialAppDisabledException } from '../../definition/exceptions'; import { IExternalComponent } from '../../definition/externalComponent'; import { ILivechatEventContext, ILivechatRoom } from '../../definition/livechat'; import { IMessage } from '../../definition/messages'; -import { AppMethod } from '../../definition/metadata'; -import { IRoom } from '../../definition/rooms'; +import { AppInterface, AppMethod } from '../../definition/metadata'; +import { IRoom, IRoomUserJoinedContext } from '../../definition/rooms'; import { IUIKitIncomingInteraction, IUIKitResponse, IUIKitView, UIKitIncomingInteractionType } from '../../definition/uikit'; import { IUIKitIncomingInteractionMessageContainer, @@ -16,7 +17,6 @@ import { import { IUser } from '../../definition/users'; import { MessageBuilder, MessageExtender, RoomBuilder, RoomExtender } from '../accessors'; import { AppManager } from '../AppManager'; -import { AppInterface } from '../compiler'; import { Message } from '../messages/Message'; import { Utilities } from '../misc/Utilities'; import { ProxiedApp } from '../ProxiedApp'; @@ -26,22 +26,35 @@ import { AppAccessorManager } from './AppAccessorManager'; export class AppListenerManager { private am: AppAccessorManager; private listeners: Map>; + /** + * Locked events are those who are listed in an app's + * "essentials" list but the app is disabled. + * + * They will throw a EssentialAppDisabledException upon call + */ + private lockedEvents: Map>; constructor(private readonly manager: AppManager) { this.am = manager.getAccessorManager(); this.listeners = new Map>(); + this.lockedEvents = new Map>(); - Object.keys(AppInterface).forEach((intt) => this.listeners.set(intt, new Array())); + Object.keys(AppInterface).forEach((intt) => { + this.listeners.set(intt, new Array()); + this.lockedEvents.set(intt, new Set()); + }); } public registerListeners(app: ProxiedApp): void { this.unregisterListeners(app); - const impleList = app.getImplementationList(); - for (const int in app.getImplementationList()) { - if (impleList[int]) { - this.listeners.get(int).push(app.getID()); + + Object.entries(app.getImplementationList()).forEach(([event, isImplemented]) => { + if (!isImplemented) { + return; } - } + + this.listeners.get(event).push(app.getID()); + }); } public unregisterListeners(app: ProxiedApp): void { @@ -53,6 +66,38 @@ export class AppListenerManager { }); } + public releaseEssentialEvents(app: ProxiedApp): void { + if (!app.getEssentials()) { + return; + } + + app.getEssentials().forEach((event) => { + const lockedEvent = this.lockedEvents.get(event); + + if (!lockedEvent) { + return; + } + + lockedEvent.delete(app.getID()); + }); + } + + public lockEssentialEvents(app: ProxiedApp): void { + if (!app.getEssentials()) { + return; + } + + app.getEssentials().forEach((event) => { + const lockedEvent = this.lockedEvents.get(event); + + if (!lockedEvent) { + return; + } + + lockedEvent.add(app.getID()); + }); + } + public getListeners(int: AppInterface): Array { const results = new Array(); @@ -63,8 +108,18 @@ export class AppListenerManager { return results; } + public isEventBlocked(event: AppInterface): boolean { + const lockedEventList = this.lockedEvents.get(event); + + return !!(lockedEventList && lockedEventList.size); + } + // tslint:disable-next-line - public async executeListener(int: AppInterface, data: IMessage | IRoom | IUser | ILivechatRoom | IUIKitIncomingInteraction | IExternalComponent | ILivechatEventContext): Promise { + public async executeListener(int: AppInterface, data: IMessage | IRoom | IUser | ILivechatRoom | IUIKitIncomingInteraction | IExternalComponent | ILivechatEventContext | IRoomUserJoinedContext): Promise { + if (this.isEventBlocked(int)) { + throw new EssentialAppDisabledException('There is one or more apps that are essential to this event but are disabled'); + } + switch (int) { // Messages case AppInterface.IPreMessageSentPrevent: @@ -105,6 +160,10 @@ export class AppListenerManager { case AppInterface.IPostRoomDeleted: this.executePostRoomDeleted(data as IRoom); return; + case AppInterface.IPreRoomUserJoined: + return this.executePreRoomUserJoined(data as IRoomUserJoinedContext); + case AppInterface.IPostRoomUserJoined: + return this.executePostRoomUserJoined(data as IRoomUserJoinedContext); // External Components case AppInterface.IPostExternalComponentOpened: this.executePostExternalComponentOpened(data as IExternalComponent); @@ -133,7 +192,7 @@ export class AppListenerManager { this.executePostLivechatAgentUnassigned(data as ILivechatEventContext); return; default: - console.warn('Unimplemented (or invalid) AppInterface was just tried to execute.'); + console.warn('An invalid listener was called'); return; } } @@ -618,6 +677,54 @@ export class AppListenerManager { } } + private async executePreRoomUserJoined(externalData: IRoomUserJoinedContext): Promise { + const data = Utilities.deepClone(externalData); + + data.room = new Room(Utilities.deepFreeze(data.room), this.manager); + Utilities.deepFreeze(data.joiningUser); + + if (data.inviter) { + Utilities.deepFreeze(data.inviter); + } + + for (const appId of this.listeners.get(AppInterface.IPreRoomUserJoined)) { + const app = this.manager.getOneById(appId); + + if (app.hasMethod(AppMethod.EXECUTE_PRE_ROOM_USER_JOINED)) { + await app.call(AppMethod.EXECUTE_PRE_ROOM_USER_JOINED, + data, + this.am.getReader(appId), + this.am.getHttp(appId), + this.am.getPersistence(appId), + ); + } + } + } + + private async executePostRoomUserJoined(externalData: IRoomUserJoinedContext): Promise { + const data = Utilities.deepClone(externalData); + + data.room = new Room(Utilities.deepFreeze(data.room), this.manager); + Utilities.deepFreeze(data.joiningUser); + + if (data.inviter) { + Utilities.deepFreeze(data.inviter); + } + + for (const appId of this.listeners.get(AppInterface.IPostRoomUserJoined)) { + const app = this.manager.getOneById(appId); + + if (app.hasMethod(AppMethod.EXECUTE_POST_ROOM_USER_JOINED)) { + await app.call(AppMethod.EXECUTE_POST_ROOM_USER_JOINED, + data, + this.am.getReader(appId), + this.am.getHttp(appId), + this.am.getPersistence(appId), + ); + } + } + } + // External Components private async executePostExternalComponentOpened(data: IExternalComponent): Promise { const cfExternalComponent = Utilities.deepCloneAndFreeze(data); diff --git a/src/server/rooms/Room.ts b/src/server/rooms/Room.ts index 0c787c004..bf32819c8 100644 --- a/src/server/rooms/Room.ts +++ b/src/server/rooms/Room.ts @@ -2,6 +2,8 @@ import { IRoom, RoomType } from '../../definition/rooms'; import { IUser } from '../../definition/users'; import { AppManager } from '../AppManager'; +const PrivateManager = Symbol('RoomPrivateManager'); + export class Room implements IRoom { public id: string; public displayName?: string; @@ -19,13 +21,15 @@ export class Room implements IRoom { public userIds?: Array; private _USERNAMES: Array; + private [PrivateManager]: AppManager; + /** * @deprecated */ public get usernames(): Array { // Get usernames if (!this._USERNAMES) { - this._USERNAMES = this.manager.getBridges().getInternalBridge().getUsernamesOfRoomById(this.id); + this._USERNAMES = this[PrivateManager].getBridges().getInternalBridge().getUsernamesOfRoomById(this.id); } return this._USERNAMES; @@ -35,8 +39,15 @@ export class Room implements IRoom { return; } - public constructor(room: IRoom, private manager: AppManager) { + public constructor(room: IRoom, manager: AppManager) { Object.assign(this, room); + + Object.defineProperty(this, PrivateManager, { + configurable: false, + enumerable: false, + writable: false, + value: manager, + }); } get value(): object { diff --git a/tests/server/compiler/AppFabricationFulfillment.spec.ts b/tests/server/compiler/AppFabricationFulfillment.spec.ts index a77a11c74..ea55b783b 100644 --- a/tests/server/compiler/AppFabricationFulfillment.spec.ts +++ b/tests/server/compiler/AppFabricationFulfillment.spec.ts @@ -1,10 +1,9 @@ import { Expect, Test } from 'alsatian'; import { App } from '../../../src/definition/App'; import { AppStatus } from '../../../src/definition/AppStatus'; -import { IAppInfo } from '../../../src/definition/metadata'; - +import { AppInterface, IAppInfo } from '../../../src/definition/metadata'; import { AppManager } from '../../../src/server/AppManager'; -import { AppFabricationFulfillment, AppInterface, ICompilerError } from '../../../src/server/compiler'; +import { AppFabricationFulfillment, ICompilerError } from '../../../src/server/compiler'; import { ProxiedApp } from '../../../src/server/ProxiedApp'; import { IAppStorageItem } from '../../../src/server/storage'; diff --git a/tests/server/compiler/AppImplements.spec.ts b/tests/server/compiler/AppImplements.spec.ts index bca227534..ee2e55647 100644 --- a/tests/server/compiler/AppImplements.spec.ts +++ b/tests/server/compiler/AppImplements.spec.ts @@ -1,6 +1,6 @@ import { Expect, Test } from 'alsatian'; - -import { AppImplements, AppInterface } from '../../../src/server/compiler'; +import { AppInterface } from '../../../src/definition/metadata'; +import { AppImplements } from '../../../src/server/compiler'; export class AppImplementsTestFixture { @Test() diff --git a/tests/server/managers/AppListenerManager.spec.ts b/tests/server/managers/AppListenerManager.spec.ts index 33e75e88c..45e23f76c 100644 --- a/tests/server/managers/AppListenerManager.spec.ts +++ b/tests/server/managers/AppListenerManager.spec.ts @@ -1,6 +1,6 @@ import { Expect, SetupFixture, Test } from 'alsatian'; +import { AppInterface } from '../../../src/definition/metadata'; import { AppManager } from '../../../src/server/AppManager'; -import { AppInterface } from '../../../src/server/compiler'; import { AppListenerManager } from '../../../src/server/managers'; import { ProxiedApp } from '../../../src/server/ProxiedApp';