Skip to content

Commit

Permalink
New "essentials" mechanism (#269)
Browse files Browse the repository at this point in the history
* Move AppInterface definition

* Lock events on apps load
  • Loading branch information
d-gubert authored May 17, 2020
1 parent bcdd3b0 commit e654965
Show file tree
Hide file tree
Showing 13 changed files with 157 additions and 84 deletions.
7 changes: 7 additions & 0 deletions src/definition/AppStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
36 changes: 36 additions & 0 deletions src/definition/metadata/AppInterface.ts
Original file line number Diff line number Diff line change
@@ -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',
}
2 changes: 2 additions & 0 deletions src/definition/metadata/IAppInfo.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AppInterface } from './AppInterface';
import { IAppAuthorInfo } from './IAppAuthorInfo';

export interface IAppInfo {
Expand All @@ -12,4 +13,5 @@ export interface IAppInfo {
iconFile: string;
/** Base64 string of the App's icon. */
iconFileContent?: string;
essentials?: [AppInterface];
}
2 changes: 2 additions & 0 deletions src/definition/metadata/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { IAppAuthorInfo } from './IAppAuthorInfo';
import { IAppInfo } from './IAppInfo';
import { RocketChatAssociationModel, RocketChatAssociationRecord } from './RocketChatAssociations';

export * from './AppInterface';

export {
AppMethod,
IAppAuthorInfo,
Expand Down
61 changes: 32 additions & 29 deletions src/server/AppManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions src/server/ProxiedApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/server/bridges/IListenerBridge.ts
Original file line number Diff line number Diff line change
@@ -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<void | boolean | IMessage>;
Expand Down
38 changes: 1 addition & 37 deletions src/server/compiler/AppImplements.ts
Original file line number Diff line number Diff line change
@@ -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 };

Expand Down
3 changes: 1 addition & 2 deletions src/server/compiler/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,7 +11,6 @@ export {
AppCompiler,
AppFabricationFulfillment,
AppImplements,
AppInterface,
AppPackageParser,
ICompilerFile,
ICompilerError,
Expand Down
75 changes: 66 additions & 9 deletions src/server/managers/AppListenerManager.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<string, Array<string>>;
/**
* 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<string, Set<string>>;

constructor(private readonly manager: AppManager) {
this.am = manager.getAccessorManager();
this.listeners = new Map<string, Array<string>>();
this.lockedEvents = new Map<string, Set<string>>();

Object.keys(AppInterface).forEach((intt) => this.listeners.set(intt, new Array<string>()));
Object.keys(AppInterface).forEach((intt) => {
this.listeners.set(intt, new Array<string>());
this.lockedEvents.set(intt, new Set<string>());
});
}

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 {
Expand All @@ -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<ProxiedApp> {
const results = new Array<ProxiedApp>();

Expand All @@ -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<void | boolean | IMessage | IRoom | IUser | IUIKitResponse | ILivechatRoom> {
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:
Expand Down Expand Up @@ -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;
}
}
Expand Down
Loading

0 comments on commit e654965

Please sign in to comment.