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/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/IAppInfo.ts b/src/definition/metadata/IAppInfo.ts index e48075951..ab8b26a64 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?: [AppInterface]; } 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/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 85e03095c..6d210bc85 100644 --- a/src/server/ProxiedApp.ts +++ b/src/server/ProxiedApp.ts @@ -154,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 1894ab38f..459138a33 100644 --- a/src/server/compiler/AppImplements.ts +++ b/src/server/compiler/AppImplements.ts @@ -1,42 +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', - 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', -} - 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 2d886a95f..a2014e05e 100644 --- a/src/server/managers/AppListenerManager.ts +++ b/src/server/managers/AppListenerManager.ts @@ -1,7 +1,8 @@ +import { AppsEngineException } 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 { AppInterface, AppMethod } from '../../definition/metadata'; import { IRoom, IRoomUserJoinedContext } from '../../definition/rooms'; import { IUIKitIncomingInteraction, IUIKitResponse, IUIKitView, UIKitIncomingInteractionType } from '../../definition/uikit'; import { @@ -16,32 +17,46 @@ 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'; import { Room } from '../rooms/Room'; import { AppAccessorManager } from './AppAccessorManager'; +export class EssentialAppDisabledException extends AppsEngineException {} + 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 +68,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 +110,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 | 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: @@ -137,7 +194,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; } } 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';