From 2ab8b327db2b2642ff2ecdde6f305874233fd5a1 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Apr 2026 13:12:47 -0300 Subject: [PATCH 01/19] feat(apps-engine): export ENGINE_VERSION from definition layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `src/definition/version.ts` which reads the package version via `resolveJsonModule` and exports it as `ENGINE_VERSION`. Replaces `AppPackageParser.getEngineVersion()` — which resolved the version by traversing the filesystem relative to `__dirname` — with a direct import of `ENGINE_VERSION`. This removes the assumption that `package.json` lives at a predictable relative path, which will break when `AppPackageParser` moves to a different package during the apps-engine split. Co-Authored-By: Claude Sonnet 4.6 fix(apps-engine): fix ENGINE_VERSION runtime path in compiled output Static `import { version } from '../../package.json'` resolves correctly during TypeScript compilation (source lives at src/definition/) but the emitted require('../../package.json') exits the package root at runtime once compiled to definition/version.js (outDir='.', rootDir='./src'). Switching to require('../package.json') — which is the correct path relative to the compiled output — and bypassing TypeScript's compile-time module resolution avoids the path mismatch entirely. Co-Authored-By: Claude Sonnet 4.6 --- .../apps-engine/src/definition/version.ts | 11 +++++++++ .../src/server/compiler/AppPackageParser.ts | 24 ++----------------- packages/apps-engine/tsconfig.json | 1 + 3 files changed, 14 insertions(+), 22 deletions(-) create mode 100644 packages/apps-engine/src/definition/version.ts diff --git a/packages/apps-engine/src/definition/version.ts b/packages/apps-engine/src/definition/version.ts new file mode 100644 index 0000000000000..2c9883d2ba725 --- /dev/null +++ b/packages/apps-engine/src/definition/version.ts @@ -0,0 +1,11 @@ +/** + * The version of the Apps-Engine package. + * Consumed by host-side code (e.g. AppPackageParser) to validate app compatibility + * without relying on filesystem path traversal. + * + * Uses require() instead of a static import so TypeScript does not resolve the path + * at compile time. The compiled output lands at definition/version.js, so + * '../package.json' correctly points to the package root at runtime. + */ +// eslint-disable-next-line @typescript-eslint/no-require-imports +export const ENGINE_VERSION: string = (require('../package.json') as { version: string }).version; diff --git a/packages/apps-engine/src/server/compiler/AppPackageParser.ts b/packages/apps-engine/src/server/compiler/AppPackageParser.ts index 757fe60878bed..3d3af9c210806 100644 --- a/packages/apps-engine/src/server/compiler/AppPackageParser.ts +++ b/packages/apps-engine/src/server/compiler/AppPackageParser.ts @@ -1,4 +1,3 @@ -import * as fs from 'fs'; import * as path from 'path'; import * as AdmZip from 'adm-zip'; @@ -7,6 +6,7 @@ import { v4 as uuidv4 } from 'uuid'; import { AppImplements } from '.'; import type { IAppInfo } from '../../definition/metadata/IAppInfo'; +import { ENGINE_VERSION } from '../../definition/version'; import { RequiredApiVersionError } from '../errors'; import type { IParseAppPackageResult } from './IParseAppPackageResult'; @@ -15,11 +15,7 @@ export class AppPackageParser { private allowedIconExts: Array = ['.png', '.jpg', '.jpeg', '.gif']; - private appsEngineVersion: string; - - constructor() { - this.appsEngineVersion = this.getEngineVersion(); - } + private appsEngineVersion: string = ENGINE_VERSION; public async unpackageApp(appPackage: Buffer): Promise { const zip = new AdmZip(appPackage); @@ -144,20 +140,4 @@ export class AppPackageParser { return `data:image/${ext.replace('.', '')};base64,${base64}`; } - private getEngineVersion(): string { - const devLocation = path.join(__dirname, '../../../package.json'); - const prodLocation = path.join(__dirname, '../../package.json'); - - let info: { version: string }; - - if (fs.existsSync(devLocation)) { - info = JSON.parse(fs.readFileSync(devLocation, 'utf8')); - } else if (fs.existsSync(prodLocation)) { - info = JSON.parse(fs.readFileSync(prodLocation, 'utf8')); - } else { - throw new Error('Could not find the Apps TypeScript Definition Package Version!'); - } - - return info.version.replace(/^[^0-9]/, '').split('-')[0]; - } } diff --git a/packages/apps-engine/tsconfig.json b/packages/apps-engine/tsconfig.json index fe9e53b01cb85..2d70ec6e00a34 100644 --- a/packages/apps-engine/tsconfig.json +++ b/packages/apps-engine/tsconfig.json @@ -10,6 +10,7 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "moduleResolution": "node", + "resolveJsonModule": true, "types": ["node"], "lib": ["es2017", "dom"], "rootDir": "./src", From a4a14c1803146c799135c7ac2a0d05f493d8930f Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Wed, 22 Apr 2026 15:45:22 -0300 Subject: [PATCH 02/19] fix: path detection for version.ts --- packages/apps-engine/src/definition/version.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/apps-engine/src/definition/version.ts b/packages/apps-engine/src/definition/version.ts index 2c9883d2ba725..452c419d1513c 100644 --- a/packages/apps-engine/src/definition/version.ts +++ b/packages/apps-engine/src/definition/version.ts @@ -4,8 +4,12 @@ * without relying on filesystem path traversal. * * Uses require() instead of a static import so TypeScript does not resolve the path - * at compile time. The compiled output lands at definition/version.js, so - * '../package.json' correctly points to the package root at runtime. + * at compile time. + * + * When running for tests, using ts-node, package.json is located two levels above the current file. + * When running in production, package.json is located one level above the compiled version of this file. */ -// eslint-disable-next-line @typescript-eslint/no-require-imports -export const ENGINE_VERSION: string = (require('../package.json') as { version: string }).version; +const runningFromSource = __dirname.endsWith('src/definition'); +const requirePath = runningFromSource ? '../../package.json' : '../package.json'; + +export const ENGINE_VERSION: string = require(requirePath).version; From 61e4d45e2f660094cd2e87ec1cb4a5b59df6f9aa Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Apr 2026 13:15:54 -0300 Subject: [PATCH 03/19] feat(apps): copy apps-engine server code into @rocket.chat/apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copies src/server/ from @rocket.chat/apps-engine verbatim, then rewrites all relative definition/ imports to package imports (`@rocket.chat/apps-engine/definition/...`). apps-engine still contains its server code at this point — this is an additive copy only. The deletion happens in a later PR once @packages/apps is confirmed working independently. Co-Authored-By: Claude Sonnet 4.6 --- packages/apps/src/server/AppManager.ts | 1240 ++++++++++++++++ packages/apps/src/server/IGetAppsFilter.ts | 9 + packages/apps/src/server/ProxiedApp.ts | 162 +++ .../apps/src/server/accessors/ApiExtend.ts | 14 + .../apps/src/server/accessors/AppAccessors.ts | 39 + .../server/accessors/CloudWorkspaceRead.ts | 14 + .../server/accessors/ConfigurationExtend.ts | 26 + .../server/accessors/ConfigurationModify.ts | 9 + .../src/server/accessors/ContactCreator.ts | 24 + .../apps/src/server/accessors/ContactRead.ts | 14 + .../src/server/accessors/DiscussionBuilder.ts | 47 + .../apps/src/server/accessors/EmailCreator.ts | 14 + .../src/server/accessors/EnvironmentRead.ts | 21 + .../src/server/accessors/EnvironmentWrite.ts | 16 + .../accessors/EnvironmentalVariableRead.ts | 21 + .../src/server/accessors/ExperimentalRead.ts | 9 + .../accessors/ExternalComponentsExtend.ts | 14 + packages/apps/src/server/accessors/Http.ts | 77 + .../apps/src/server/accessors/HttpExtend.ts | 58 + .../src/server/accessors/LivechatCreator.ts | 38 + .../accessors/LivechatMessageBuilder.ts | 191 +++ .../apps/src/server/accessors/LivechatRead.ts | 78 + .../src/server/accessors/LivechatUpdater.ts | 33 + .../src/server/accessors/MessageBuilder.ts | 224 +++ .../src/server/accessors/MessageExtender.ts | 50 + .../apps/src/server/accessors/MessageRead.ts | 36 + .../src/server/accessors/ModerationModify.ts | 23 + packages/apps/src/server/accessors/Modify.ts | 92 ++ .../src/server/accessors/ModifyCreator.ts | 274 ++++ .../src/server/accessors/ModifyDeleter.ts | 38 + .../src/server/accessors/ModifyExtender.ts | 49 + .../src/server/accessors/ModifyUpdater.ts | 109 ++ .../apps/src/server/accessors/Notifier.ts | 53 + .../src/server/accessors/OAuthAppsModify.ts | 22 + .../src/server/accessors/OAuthAppsReader.ts | 18 + .../OutboundCommunicationProviderExtend.ts | 18 + .../apps/src/server/accessors/Persistence.ts | 46 + .../src/server/accessors/PersistenceRead.ts | 22 + packages/apps/src/server/accessors/Reader.ts | 98 ++ .../apps/src/server/accessors/RoleRead.ts | 18 + .../apps/src/server/accessors/RoomBuilder.ts | 155 ++ .../apps/src/server/accessors/RoomExtender.ts | 56 + .../apps/src/server/accessors/RoomRead.ts | 115 ++ .../src/server/accessors/SchedulerExtend.ts | 14 + .../src/server/accessors/SchedulerModify.ts | 30 + .../src/server/accessors/ServerSettingRead.ts | 37 + .../server/accessors/ServerSettingUpdater.ts | 18 + .../server/accessors/ServerSettingsModify.ts | 26 + .../apps/src/server/accessors/SettingRead.ts | 25 + .../src/server/accessors/SettingUpdater.ts | 66 + .../src/server/accessors/SettingsExtend.ts | 26 + .../server/accessors/SlashCommandsExtend.ts | 14 + .../server/accessors/SlashCommandsModify.ts | 22 + .../apps/src/server/accessors/ThreadRead.ts | 14 + .../apps/src/server/accessors/UIController.ts | 118 ++ .../apps/src/server/accessors/UIExtend.ts | 14 + .../src/server/accessors/UploadCreator.ts | 28 + .../apps/src/server/accessors/UploadRead.ts | 24 + .../apps/src/server/accessors/UserBuilder.ts | 74 + .../apps/src/server/accessors/UserRead.ts | 30 + .../apps/src/server/accessors/UserUpdater.ts | 31 + .../accessors/VideoConfProviderExtend.ts | 14 + .../accessors/VideoConferenceBuilder.ts | 83 ++ .../server/accessors/VideoConferenceExtend.ts | 64 + .../server/accessors/VideoConferenceRead.ts | 14 + packages/apps/src/server/accessors/index.ts | 97 ++ packages/apps/src/server/bridges/ApiBridge.ts | 49 + .../src/server/bridges/AppActivationBridge.ts | 35 + .../apps/src/server/bridges/AppBridges.ts | 113 ++ .../server/bridges/AppDetailChangesBridge.ts | 16 + .../apps/src/server/bridges/BaseBridge.ts | 6 + .../server/bridges/CloudWorkspaceBridge.ts | 30 + .../apps/src/server/bridges/CommandBridge.ts | 117 ++ .../apps/src/server/bridges/ContactBridge.ts | 69 + .../apps/src/server/bridges/EmailBridge.ts | 30 + .../bridges/EnvironmentalVariableBridge.ts | 45 + .../src/server/bridges/ExperimentalBridge.ts | 10 + .../apps/src/server/bridges/HttpBridge.ts | 37 + .../src/server/bridges/IInternalBridge.ts | 7 + .../bridges/IInternalFederationBridge.ts | 15 + .../bridges/IInternalPersistenceBridge.ts | 9 + .../bridges/IInternalSchedulerBridge.ts | 8 + .../src/server/bridges/IInternalUserBridge.ts | 8 + .../src/server/bridges/IListenerBridge.ts | 10 + .../apps/src/server/bridges/InternalBridge.ts | 22 + .../apps/src/server/bridges/ListenerBridge.ts | 18 + .../apps/src/server/bridges/LivechatBridge.ts | 305 ++++ .../apps/src/server/bridges/MessageBridge.ts | 116 ++ .../src/server/bridges/ModerationBridge.ts | 47 + .../src/server/bridges/OAuthAppsBridge.ts | 85 ++ .../server/bridges/OutboundMessagesBridge.ts | 50 + .../src/server/bridges/PersistenceBridge.ts | 174 +++ .../apps/src/server/bridges/RoleBridge.ts | 38 + .../apps/src/server/bridges/RoomBridge.ts | 252 ++++ .../src/server/bridges/SchedulerBridge.ts | 62 + .../src/server/bridges/ServerSettingBridge.ts | 93 ++ .../apps/src/server/bridges/ThreadBridge.ts | 35 + .../src/server/bridges/UiInteractionBridge.ts | 31 + .../apps/src/server/bridges/UploadBridge.ts | 62 + .../apps/src/server/bridges/UserBridge.ts | 157 ++ .../server/bridges/VideoConferenceBridge.ts | 94 ++ packages/apps/src/server/bridges/index.ts | 58 + .../apps/src/server/compiler/AppCompiler.ts | 30 + .../compiler/AppFabricationFulfillment.ts | 75 + .../apps/src/server/compiler/AppImplements.ts | 32 + .../src/server/compiler/AppPackageParser.ts | 163 +++ .../server/compiler/IParseAppPackageResult.ts | 9 + packages/apps/src/server/compiler/index.ts | 7 + .../apps/src/server/compiler/modules/index.ts | 55 + .../src/server/compiler/modules/networking.ts | 36 + .../server/errors/AppOutboundProcessError.ts | 12 + .../errors/CommandAlreadyExistsError.ts | 9 + .../CommandHasAlreadyBeenTouchedError.ts | 9 + .../apps/src/server/errors/CompilerError.ts | 9 + .../server/errors/InvalidInstallationError.ts | 5 + .../src/server/errors/InvalidLicenseError.ts | 7 + .../server/errors/MustContainFunctionError.ts | 9 + .../src/server/errors/MustExtendAppError.ts | 5 + .../errors/NotEnoughMethodArgumentsError.ts | 9 + .../server/errors/PathAlreadyExistsError.ts | 9 + .../server/errors/PermissionDeniedError.ts | 25 + .../server/errors/RequiredApiVersionError.ts | 21 + .../VideoConfProviderAlreadyExistsError.ts | 9 + .../VideoConfProviderNotRegisteredError.ts | 9 + packages/apps/src/server/errors/index.ts | 25 + .../apps/src/server/logging/AppConsole.ts | 121 ++ .../src/server/logging/ILoggerStorageEntry.ts | 14 + packages/apps/src/server/logging/index.ts | 4 + .../src/server/managers/AppAccessorManager.ts | 251 ++++ packages/apps/src/server/managers/AppApi.ts | 99 ++ .../apps/src/server/managers/AppApiManager.ts | 165 +++ .../managers/AppExternalComponentManager.ts | 142 ++ .../src/server/managers/AppLicenseManager.ts | 99 ++ .../src/server/managers/AppListenerManager.ts | 1283 +++++++++++++++++ .../AppOutboundCommunicationProvider.ts | 52 + ...AppOutboundCommunicationProviderManager.ts | 138 ++ .../server/managers/AppPermissionManager.ts | 40 + .../src/server/managers/AppRuntimeManager.ts | 76 + .../server/managers/AppSchedulerManager.ts | 98 ++ .../src/server/managers/AppSettingsManager.ts | 56 + .../server/managers/AppSignatureManager.ts | 85 ++ .../src/server/managers/AppSlashCommand.ts | 80 + .../server/managers/AppSlashCommandManager.ts | 477 ++++++ .../server/managers/AppVideoConfProvider.ts | 113 ++ .../managers/AppVideoConfProviderManager.ts | 216 +++ .../server/managers/UIActionButtonManager.ts | 95 ++ packages/apps/src/server/managers/index.ts | 23 + .../server/marketplace/IAppLicenseMetadata.ts | 5 + .../server/marketplace/IMarketplaceInfo.ts | 24 + .../marketplace/IMarketplacePricingPlan.ts | 11 + .../marketplace/IMarketplacePricingTier.ts | 6 + .../IMarketplaceSimpleBundleInfo.ts | 4 + .../IMarketplaceSubscriptionInfo.ts | 15 + .../marketplace/MarketplacePricingStrategy.ts | 5 + .../marketplace/MarketplacePurchaseType.ts | 4 + .../MarketplaceSubscriptionStatus.ts | 10 + .../MarketplaceSubscriptionType.ts | 4 + packages/apps/src/server/marketplace/index.ts | 15 + .../license/AppLicenseValidationResult.ts | 56 + .../src/server/marketplace/license/Crypto.ts | 26 + .../src/server/marketplace/license/index.ts | 4 + packages/apps/src/server/messages/Message.ts | 109 ++ packages/apps/src/server/misc/UIHelper.ts | 32 + packages/apps/src/server/misc/Utilities.ts | 36 + .../apps/src/server/oauth2/OAuth2Client.ts | 337 +++++ .../src/server/permissions/AppPermissions.ts | 168 +++ packages/apps/src/server/rooms/Room.ts | 104 ++ .../server/runtime/AppsEngineEmptyRuntime.ts | 21 + .../server/runtime/AppsEngineNodeRuntime.ts | 74 + .../src/server/runtime/AppsEngineRuntime.ts | 29 + .../apps/src/server/runtime/EmptyRuntime.ts | 50 + .../src/server/runtime/IRuntimeController.ts | 34 + .../runtime/deno/AppsEngineDenoRuntime.ts | 739 ++++++++++ .../server/runtime/deno/LivenessManager.ts | 254 ++++ .../server/runtime/deno/ProcessMessenger.ts | 57 + .../apps/src/server/runtime/deno/bundler.ts | 90 ++ .../apps/src/server/runtime/deno/codec.ts | 45 + .../apps/src/server/storage/AppLogStorage.ts | 27 + .../src/server/storage/AppMetadataStorage.ts | 36 + .../src/server/storage/AppSourceStorage.ts | 40 + .../src/server/storage/IAppStorageItem.ts | 31 + packages/apps/src/server/storage/index.ts | 4 + 182 files changed, 13692 insertions(+) create mode 100644 packages/apps/src/server/AppManager.ts create mode 100644 packages/apps/src/server/IGetAppsFilter.ts create mode 100644 packages/apps/src/server/ProxiedApp.ts create mode 100644 packages/apps/src/server/accessors/ApiExtend.ts create mode 100644 packages/apps/src/server/accessors/AppAccessors.ts create mode 100644 packages/apps/src/server/accessors/CloudWorkspaceRead.ts create mode 100644 packages/apps/src/server/accessors/ConfigurationExtend.ts create mode 100644 packages/apps/src/server/accessors/ConfigurationModify.ts create mode 100644 packages/apps/src/server/accessors/ContactCreator.ts create mode 100644 packages/apps/src/server/accessors/ContactRead.ts create mode 100644 packages/apps/src/server/accessors/DiscussionBuilder.ts create mode 100644 packages/apps/src/server/accessors/EmailCreator.ts create mode 100644 packages/apps/src/server/accessors/EnvironmentRead.ts create mode 100644 packages/apps/src/server/accessors/EnvironmentWrite.ts create mode 100644 packages/apps/src/server/accessors/EnvironmentalVariableRead.ts create mode 100644 packages/apps/src/server/accessors/ExperimentalRead.ts create mode 100644 packages/apps/src/server/accessors/ExternalComponentsExtend.ts create mode 100644 packages/apps/src/server/accessors/Http.ts create mode 100644 packages/apps/src/server/accessors/HttpExtend.ts create mode 100644 packages/apps/src/server/accessors/LivechatCreator.ts create mode 100644 packages/apps/src/server/accessors/LivechatMessageBuilder.ts create mode 100644 packages/apps/src/server/accessors/LivechatRead.ts create mode 100644 packages/apps/src/server/accessors/LivechatUpdater.ts create mode 100644 packages/apps/src/server/accessors/MessageBuilder.ts create mode 100644 packages/apps/src/server/accessors/MessageExtender.ts create mode 100644 packages/apps/src/server/accessors/MessageRead.ts create mode 100644 packages/apps/src/server/accessors/ModerationModify.ts create mode 100644 packages/apps/src/server/accessors/Modify.ts create mode 100644 packages/apps/src/server/accessors/ModifyCreator.ts create mode 100644 packages/apps/src/server/accessors/ModifyDeleter.ts create mode 100644 packages/apps/src/server/accessors/ModifyExtender.ts create mode 100644 packages/apps/src/server/accessors/ModifyUpdater.ts create mode 100644 packages/apps/src/server/accessors/Notifier.ts create mode 100644 packages/apps/src/server/accessors/OAuthAppsModify.ts create mode 100644 packages/apps/src/server/accessors/OAuthAppsReader.ts create mode 100644 packages/apps/src/server/accessors/OutboundCommunicationProviderExtend.ts create mode 100644 packages/apps/src/server/accessors/Persistence.ts create mode 100644 packages/apps/src/server/accessors/PersistenceRead.ts create mode 100644 packages/apps/src/server/accessors/Reader.ts create mode 100644 packages/apps/src/server/accessors/RoleRead.ts create mode 100644 packages/apps/src/server/accessors/RoomBuilder.ts create mode 100644 packages/apps/src/server/accessors/RoomExtender.ts create mode 100644 packages/apps/src/server/accessors/RoomRead.ts create mode 100644 packages/apps/src/server/accessors/SchedulerExtend.ts create mode 100644 packages/apps/src/server/accessors/SchedulerModify.ts create mode 100644 packages/apps/src/server/accessors/ServerSettingRead.ts create mode 100644 packages/apps/src/server/accessors/ServerSettingUpdater.ts create mode 100644 packages/apps/src/server/accessors/ServerSettingsModify.ts create mode 100644 packages/apps/src/server/accessors/SettingRead.ts create mode 100644 packages/apps/src/server/accessors/SettingUpdater.ts create mode 100644 packages/apps/src/server/accessors/SettingsExtend.ts create mode 100644 packages/apps/src/server/accessors/SlashCommandsExtend.ts create mode 100644 packages/apps/src/server/accessors/SlashCommandsModify.ts create mode 100644 packages/apps/src/server/accessors/ThreadRead.ts create mode 100644 packages/apps/src/server/accessors/UIController.ts create mode 100644 packages/apps/src/server/accessors/UIExtend.ts create mode 100644 packages/apps/src/server/accessors/UploadCreator.ts create mode 100644 packages/apps/src/server/accessors/UploadRead.ts create mode 100644 packages/apps/src/server/accessors/UserBuilder.ts create mode 100644 packages/apps/src/server/accessors/UserRead.ts create mode 100644 packages/apps/src/server/accessors/UserUpdater.ts create mode 100644 packages/apps/src/server/accessors/VideoConfProviderExtend.ts create mode 100644 packages/apps/src/server/accessors/VideoConferenceBuilder.ts create mode 100644 packages/apps/src/server/accessors/VideoConferenceExtend.ts create mode 100644 packages/apps/src/server/accessors/VideoConferenceRead.ts create mode 100644 packages/apps/src/server/accessors/index.ts create mode 100644 packages/apps/src/server/bridges/ApiBridge.ts create mode 100644 packages/apps/src/server/bridges/AppActivationBridge.ts create mode 100644 packages/apps/src/server/bridges/AppBridges.ts create mode 100644 packages/apps/src/server/bridges/AppDetailChangesBridge.ts create mode 100644 packages/apps/src/server/bridges/BaseBridge.ts create mode 100644 packages/apps/src/server/bridges/CloudWorkspaceBridge.ts create mode 100644 packages/apps/src/server/bridges/CommandBridge.ts create mode 100644 packages/apps/src/server/bridges/ContactBridge.ts create mode 100644 packages/apps/src/server/bridges/EmailBridge.ts create mode 100644 packages/apps/src/server/bridges/EnvironmentalVariableBridge.ts create mode 100644 packages/apps/src/server/bridges/ExperimentalBridge.ts create mode 100644 packages/apps/src/server/bridges/HttpBridge.ts create mode 100644 packages/apps/src/server/bridges/IInternalBridge.ts create mode 100644 packages/apps/src/server/bridges/IInternalFederationBridge.ts create mode 100644 packages/apps/src/server/bridges/IInternalPersistenceBridge.ts create mode 100644 packages/apps/src/server/bridges/IInternalSchedulerBridge.ts create mode 100644 packages/apps/src/server/bridges/IInternalUserBridge.ts create mode 100644 packages/apps/src/server/bridges/IListenerBridge.ts create mode 100644 packages/apps/src/server/bridges/InternalBridge.ts create mode 100644 packages/apps/src/server/bridges/ListenerBridge.ts create mode 100644 packages/apps/src/server/bridges/LivechatBridge.ts create mode 100644 packages/apps/src/server/bridges/MessageBridge.ts create mode 100644 packages/apps/src/server/bridges/ModerationBridge.ts create mode 100644 packages/apps/src/server/bridges/OAuthAppsBridge.ts create mode 100644 packages/apps/src/server/bridges/OutboundMessagesBridge.ts create mode 100644 packages/apps/src/server/bridges/PersistenceBridge.ts create mode 100644 packages/apps/src/server/bridges/RoleBridge.ts create mode 100644 packages/apps/src/server/bridges/RoomBridge.ts create mode 100644 packages/apps/src/server/bridges/SchedulerBridge.ts create mode 100644 packages/apps/src/server/bridges/ServerSettingBridge.ts create mode 100644 packages/apps/src/server/bridges/ThreadBridge.ts create mode 100644 packages/apps/src/server/bridges/UiInteractionBridge.ts create mode 100644 packages/apps/src/server/bridges/UploadBridge.ts create mode 100644 packages/apps/src/server/bridges/UserBridge.ts create mode 100644 packages/apps/src/server/bridges/VideoConferenceBridge.ts create mode 100644 packages/apps/src/server/bridges/index.ts create mode 100644 packages/apps/src/server/compiler/AppCompiler.ts create mode 100644 packages/apps/src/server/compiler/AppFabricationFulfillment.ts create mode 100644 packages/apps/src/server/compiler/AppImplements.ts create mode 100644 packages/apps/src/server/compiler/AppPackageParser.ts create mode 100644 packages/apps/src/server/compiler/IParseAppPackageResult.ts create mode 100644 packages/apps/src/server/compiler/index.ts create mode 100644 packages/apps/src/server/compiler/modules/index.ts create mode 100644 packages/apps/src/server/compiler/modules/networking.ts create mode 100644 packages/apps/src/server/errors/AppOutboundProcessError.ts create mode 100644 packages/apps/src/server/errors/CommandAlreadyExistsError.ts create mode 100644 packages/apps/src/server/errors/CommandHasAlreadyBeenTouchedError.ts create mode 100644 packages/apps/src/server/errors/CompilerError.ts create mode 100644 packages/apps/src/server/errors/InvalidInstallationError.ts create mode 100644 packages/apps/src/server/errors/InvalidLicenseError.ts create mode 100644 packages/apps/src/server/errors/MustContainFunctionError.ts create mode 100644 packages/apps/src/server/errors/MustExtendAppError.ts create mode 100644 packages/apps/src/server/errors/NotEnoughMethodArgumentsError.ts create mode 100644 packages/apps/src/server/errors/PathAlreadyExistsError.ts create mode 100644 packages/apps/src/server/errors/PermissionDeniedError.ts create mode 100644 packages/apps/src/server/errors/RequiredApiVersionError.ts create mode 100644 packages/apps/src/server/errors/VideoConfProviderAlreadyExistsError.ts create mode 100644 packages/apps/src/server/errors/VideoConfProviderNotRegisteredError.ts create mode 100644 packages/apps/src/server/errors/index.ts create mode 100644 packages/apps/src/server/logging/AppConsole.ts create mode 100644 packages/apps/src/server/logging/ILoggerStorageEntry.ts create mode 100644 packages/apps/src/server/logging/index.ts create mode 100644 packages/apps/src/server/managers/AppAccessorManager.ts create mode 100644 packages/apps/src/server/managers/AppApi.ts create mode 100644 packages/apps/src/server/managers/AppApiManager.ts create mode 100644 packages/apps/src/server/managers/AppExternalComponentManager.ts create mode 100644 packages/apps/src/server/managers/AppLicenseManager.ts create mode 100644 packages/apps/src/server/managers/AppListenerManager.ts create mode 100644 packages/apps/src/server/managers/AppOutboundCommunicationProvider.ts create mode 100644 packages/apps/src/server/managers/AppOutboundCommunicationProviderManager.ts create mode 100644 packages/apps/src/server/managers/AppPermissionManager.ts create mode 100644 packages/apps/src/server/managers/AppRuntimeManager.ts create mode 100644 packages/apps/src/server/managers/AppSchedulerManager.ts create mode 100644 packages/apps/src/server/managers/AppSettingsManager.ts create mode 100644 packages/apps/src/server/managers/AppSignatureManager.ts create mode 100644 packages/apps/src/server/managers/AppSlashCommand.ts create mode 100644 packages/apps/src/server/managers/AppSlashCommandManager.ts create mode 100644 packages/apps/src/server/managers/AppVideoConfProvider.ts create mode 100644 packages/apps/src/server/managers/AppVideoConfProviderManager.ts create mode 100644 packages/apps/src/server/managers/UIActionButtonManager.ts create mode 100644 packages/apps/src/server/managers/index.ts create mode 100644 packages/apps/src/server/marketplace/IAppLicenseMetadata.ts create mode 100644 packages/apps/src/server/marketplace/IMarketplaceInfo.ts create mode 100644 packages/apps/src/server/marketplace/IMarketplacePricingPlan.ts create mode 100644 packages/apps/src/server/marketplace/IMarketplacePricingTier.ts create mode 100644 packages/apps/src/server/marketplace/IMarketplaceSimpleBundleInfo.ts create mode 100644 packages/apps/src/server/marketplace/IMarketplaceSubscriptionInfo.ts create mode 100644 packages/apps/src/server/marketplace/MarketplacePricingStrategy.ts create mode 100644 packages/apps/src/server/marketplace/MarketplacePurchaseType.ts create mode 100644 packages/apps/src/server/marketplace/MarketplaceSubscriptionStatus.ts create mode 100644 packages/apps/src/server/marketplace/MarketplaceSubscriptionType.ts create mode 100644 packages/apps/src/server/marketplace/index.ts create mode 100644 packages/apps/src/server/marketplace/license/AppLicenseValidationResult.ts create mode 100644 packages/apps/src/server/marketplace/license/Crypto.ts create mode 100644 packages/apps/src/server/marketplace/license/index.ts create mode 100644 packages/apps/src/server/messages/Message.ts create mode 100644 packages/apps/src/server/misc/UIHelper.ts create mode 100644 packages/apps/src/server/misc/Utilities.ts create mode 100644 packages/apps/src/server/oauth2/OAuth2Client.ts create mode 100644 packages/apps/src/server/permissions/AppPermissions.ts create mode 100644 packages/apps/src/server/rooms/Room.ts create mode 100644 packages/apps/src/server/runtime/AppsEngineEmptyRuntime.ts create mode 100644 packages/apps/src/server/runtime/AppsEngineNodeRuntime.ts create mode 100644 packages/apps/src/server/runtime/AppsEngineRuntime.ts create mode 100644 packages/apps/src/server/runtime/EmptyRuntime.ts create mode 100644 packages/apps/src/server/runtime/IRuntimeController.ts create mode 100644 packages/apps/src/server/runtime/deno/AppsEngineDenoRuntime.ts create mode 100644 packages/apps/src/server/runtime/deno/LivenessManager.ts create mode 100644 packages/apps/src/server/runtime/deno/ProcessMessenger.ts create mode 100644 packages/apps/src/server/runtime/deno/bundler.ts create mode 100644 packages/apps/src/server/runtime/deno/codec.ts create mode 100644 packages/apps/src/server/storage/AppLogStorage.ts create mode 100644 packages/apps/src/server/storage/AppMetadataStorage.ts create mode 100644 packages/apps/src/server/storage/AppSourceStorage.ts create mode 100644 packages/apps/src/server/storage/IAppStorageItem.ts create mode 100644 packages/apps/src/server/storage/index.ts diff --git a/packages/apps/src/server/AppManager.ts b/packages/apps/src/server/AppManager.ts new file mode 100644 index 0000000000000..c7ddc42b63623 --- /dev/null +++ b/packages/apps/src/server/AppManager.ts @@ -0,0 +1,1240 @@ +import { Buffer } from 'buffer'; + +import type { IGetAppsFilter } from './IGetAppsFilter'; +import { ProxiedApp } from './ProxiedApp'; +import type { PersistenceBridge, UserBridge } from './bridges'; +import { AppBridges } from './bridges'; +import { AppStatus, AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; +import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; +import { AppMethod } from '@rocket.chat/apps-engine/definition/metadata'; +import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import { UserType } from '@rocket.chat/apps-engine/definition/users'; +import type { IInternalPersistenceBridge } from './bridges/IInternalPersistenceBridge'; +import type { IInternalUserBridge } from './bridges/IInternalUserBridge'; +import { AppCompiler, AppFabricationFulfillment, AppPackageParser } from './compiler'; +import { InvalidLicenseError } from './errors'; +import { InvalidInstallationError } from './errors/InvalidInstallationError'; +import { + AppAccessorManager, + AppApiManager, + AppExternalComponentManager, + AppLicenseManager, + AppListenerManager, + AppSchedulerManager, + AppSettingsManager, + AppSlashCommandManager, + AppVideoConfProviderManager, +} from './managers'; +import { AppOutboundCommunicationProviderManager } from './managers/AppOutboundCommunicationProviderManager'; +import { AppRuntimeManager } from './managers/AppRuntimeManager'; +import { AppSignatureManager } from './managers/AppSignatureManager'; +import { UIActionButtonManager } from './managers/UIActionButtonManager'; +import type { IMarketplaceInfo } from './marketplace'; +import { defaultPermissions } from './permissions/AppPermissions'; +import { EmptyRuntime } from './runtime/EmptyRuntime'; +import type { IAppStorageItem } from './storage'; +import { AppLogStorage, AppMetadataStorage } from './storage'; +import { AppSourceStorage } from './storage/AppSourceStorage'; +import { AppInstallationSource } from './storage/IAppStorageItem'; + +export interface IAppInstallParameters { + enable: boolean; + marketplaceInfo?: IMarketplaceInfo[]; + permissionsGranted?: Array; + user: IUser; +} + +export interface IAppUninstallParameters { + user: IUser; +} + +export interface IAppManagerDeps { + metadataStorage: AppMetadataStorage; + logStorage: AppLogStorage; + bridges: AppBridges; + sourceStorage: AppSourceStorage; + /** + * Path to temporary file storage. + * + * Needs to be accessible for reading and writing. + */ + tempFilePath: string; +} + +interface IPurgeAppConfigOpts { + keepScheduledJobs?: boolean; + keepSlashcommands?: boolean; + keepOutboundCommunicationProviders?: boolean; +} + +export class AppManager { + public static Instance: AppManager; + + // apps contains all of the Apps + private readonly apps: Map; + + private readonly appMetadataStorage: AppMetadataStorage; + + private appSourceStorage: AppSourceStorage; + + private readonly logStorage: AppLogStorage; + + private readonly bridges: AppBridges; + + private readonly parser: AppPackageParser; + + private readonly compiler: AppCompiler; + + private readonly accessorManager: AppAccessorManager; + + private readonly listenerManager: AppListenerManager; + + private readonly commandManager: AppSlashCommandManager; + + private readonly apiManager: AppApiManager; + + private readonly externalComponentManager: AppExternalComponentManager; + + private readonly settingsManager: AppSettingsManager; + + private readonly licenseManager: AppLicenseManager; + + private readonly schedulerManager: AppSchedulerManager; + + private readonly uiActionButtonManager: UIActionButtonManager; + + private readonly videoConfProviderManager: AppVideoConfProviderManager; + + private readonly outboundCommunicationProviderManager: AppOutboundCommunicationProviderManager; + + private readonly signatureManager: AppSignatureManager; + + private readonly runtime: AppRuntimeManager; + + private readonly tempFilePath: string; + + private isLoaded: boolean; + + constructor({ metadataStorage, logStorage, bridges, sourceStorage, tempFilePath }: IAppManagerDeps) { + // Singleton style. There can only ever be one AppManager instance + if (typeof AppManager.Instance !== 'undefined') { + throw new Error('There is already a valid AppManager instance'); + } + + if (metadataStorage instanceof AppMetadataStorage) { + this.appMetadataStorage = metadataStorage; + } else { + throw new Error('Invalid instance of the AppMetadataStorage'); + } + + if (logStorage instanceof AppLogStorage) { + this.logStorage = logStorage; + } else { + throw new Error('Invalid instance of the AppLogStorage'); + } + + if (bridges instanceof AppBridges) { + this.bridges = bridges; + } else { + throw new Error('Invalid instance of the AppBridges'); + } + + if (sourceStorage instanceof AppSourceStorage) { + this.appSourceStorage = sourceStorage; + } else { + throw new Error('Invalid instance of the AppSourceStorage'); + } + + this.tempFilePath = tempFilePath; + + this.apps = new Map(); + + this.parser = new AppPackageParser(); + this.compiler = new AppCompiler(); + this.accessorManager = new AppAccessorManager(this); + this.listenerManager = new AppListenerManager(this); + this.commandManager = new AppSlashCommandManager(this); + this.apiManager = new AppApiManager(this); + this.externalComponentManager = new AppExternalComponentManager(); + this.settingsManager = new AppSettingsManager(this); + this.licenseManager = new AppLicenseManager(this); + this.schedulerManager = new AppSchedulerManager(this); + this.uiActionButtonManager = new UIActionButtonManager(this); + this.videoConfProviderManager = new AppVideoConfProviderManager(this); + this.outboundCommunicationProviderManager = new AppOutboundCommunicationProviderManager(this); + this.signatureManager = new AppSignatureManager(this); + this.runtime = new AppRuntimeManager(this); + + this.isLoaded = false; + AppManager.Instance = this; + } + + /** + * Gets the path to the temporary file storage. + * + * Mainly used for upload events + */ + public getTempFilePath(): string { + return this.tempFilePath; + } + + /** Gets the instance of the storage connector. */ + public getStorage(): AppMetadataStorage { + return this.appMetadataStorage; + } + + /** Gets the instance of the log storage connector. */ + public getLogStorage(): AppLogStorage { + return this.logStorage; + } + + /** Gets the instance of the App package parser. */ + public getParser(): AppPackageParser { + return this.parser; + } + + /** Gets the compiler instance. */ + public getCompiler(): AppCompiler { + return this.compiler; + } + + /** Gets the accessor manager instance. */ + public getAccessorManager(): AppAccessorManager { + return this.accessorManager; + } + + /** Gets the instance of the Bridge manager. */ + public getBridges(): AppBridges { + return this.bridges; + } + + /** Gets the instance of the listener manager. */ + public getListenerManager(): AppListenerManager { + return this.listenerManager; + } + + /** Gets the command manager's instance. */ + public getCommandManager(): AppSlashCommandManager { + return this.commandManager; + } + + public getVideoConfProviderManager(): AppVideoConfProviderManager { + return this.videoConfProviderManager; + } + + public getOutboundCommunicationProviderManager(): AppOutboundCommunicationProviderManager { + return this.outboundCommunicationProviderManager; + } + + public getLicenseManager(): AppLicenseManager { + return this.licenseManager; + } + + /** Gets the api manager's instance. */ + public getApiManager(): AppApiManager { + return this.apiManager; + } + + /** Gets the external component manager's instance. */ + public getExternalComponentManager(): AppExternalComponentManager { + return this.externalComponentManager; + } + + /** Gets the manager of the settings, updates and getting. */ + public getSettingsManager(): AppSettingsManager { + return this.settingsManager; + } + + public getSchedulerManager(): AppSchedulerManager { + return this.schedulerManager; + } + + public getUIActionButtonManager(): UIActionButtonManager { + return this.uiActionButtonManager; + } + + public getSignatureManager(): AppSignatureManager { + return this.signatureManager; + } + + public getRuntime(): AppRuntimeManager { + return this.runtime; + } + + /** Gets whether the Apps have been loaded or not. */ + public areAppsLoaded(): boolean { + return this.isLoaded; + } + + public setSourceStorage(storage: AppSourceStorage): void { + this.appSourceStorage = storage; + } + + /** + * Goes through the entire loading up process. + * Expect this to take some time, as it goes through a very + * long process of loading all the Apps up. + */ + public async load(): Promise { + // You can not load the AppManager system again + // if it has already been loaded. + if (this.isLoaded) { + return true; + } + + const items: Map = await this.appMetadataStorage.retrieveAll(); + + for (const item of items.values()) { + try { + const appPackage = await this.appSourceStorage.fetch(item); + const unpackageResult = await this.getParser().unpackageApp(appPackage); + + const app = await this.getCompiler().toSandBox(this, item, unpackageResult); + + this.apps.set(item.id, app); + } catch (e) { + console.warn(`Error while compiling the App "${item.info.name} (${item.id})":`); + console.error(e); + + const prl = new ProxiedApp(this, item, new EmptyRuntime(item.id)); + + this.apps.set(item.id, prl); + } + } + + this.isLoaded = true; + return true; + } + + public async enableAll(): Promise> { + const affs: Array = []; + + // Let's initialize them + for (const rl of this.apps.values()) { + const aff = new AppFabricationFulfillment(); + + aff.setAppInfo(rl.getInfo()); + aff.setImplementedInterfaces(rl.getImplementationList()); + aff.setApp(rl); + affs.push(aff); + + if (AppStatusUtils.isDisabled(await rl.getStatus())) { + // Usually if an App is disabled before it's initialized, + // then something (such as an error) occured while + // it was compiled or something similar. + // We still have to validate its license, though + await rl.validateLicense(); + + continue; + } + + await this.initializeApp(rl, true).catch(console.error); + } + + // Let's ensure the required settings are all set + for (const rl of this.apps.values()) { + if (AppStatusUtils.isDisabled(await rl.getStatus())) { + continue; + } + + if (!this.areRequiredSettingsSet(rl.getStorageItem())) { + await rl.setStatus(AppStatus.INVALID_SETTINGS_DISABLED).catch(console.error); + } + } + + // Now let's enable the apps which were once enabled + // but are not currently disabled. + for (const app of this.apps.values()) { + const status = await app.getStatus(); + if (!AppStatusUtils.isDisabled(status) && AppStatusUtils.isEnabled(app.getPreviousStatus())) { + await this.enableApp(app).catch(console.error); + } else if (!AppStatusUtils.isError(status)) { + this.listenerManager.lockEssentialEvents(app); + this.uiActionButtonManager.clearAppActionButtons(app.getID()); + } + } + + return affs; + } + + public async unload(isManual: boolean): Promise { + // If the AppManager hasn't been loaded yet, then + // there is nothing to unload + if (!this.isLoaded) { + return; + } + + for (const app of this.apps.values()) { + const status = await app.getStatus(); + if (status === AppStatus.INITIALIZED) { + await this.purgeAppConfig(app); + } else if (!AppStatusUtils.isDisabled(status)) { + await this.disable(app.getID(), isManual ? AppStatus.MANUALLY_DISABLED : AppStatus.DISABLED); + } + + this.listenerManager.releaseEssentialEvents(app); + + app.getRuntimeController().stopApp(); + } + + // Remove all the apps from the system now that we have unloaded everything + this.apps.clear(); + + this.isLoaded = false; + } + + /** Gets the Apps which match the filter passed in. */ + public async get(filter?: IGetAppsFilter): Promise { + let rls: Array = []; + + if (typeof filter === 'undefined') { + this.apps.forEach((rl) => rls.push(rl)); + + return rls; + } + + let nothing = true; + + if (typeof filter.enabled === 'boolean' && filter.enabled) { + for (const rl of this.apps.values()) { + if (AppStatusUtils.isEnabled(await rl.getStatus())) { + rls.push(rl); + } + } + + nothing = false; + } + + if (typeof filter.disabled === 'boolean' && filter.disabled) { + for (const rl of this.apps.values()) { + if (AppStatusUtils.isDisabled(await rl.getStatus())) { + rls.push(rl); + } + } + + nothing = false; + } + + if (nothing) { + this.apps.forEach((rl) => rls.push(rl)); + } + + if (typeof filter.ids !== 'undefined') { + rls = rls.filter((rl) => filter.ids.includes(rl.getID())); + } + + if (typeof filter.installationSource !== 'undefined') { + rls = rls.filter((rl) => rl.getInstallationSource() === filter.installationSource); + } + + if (typeof filter.name === 'string') { + rls = rls.filter((rl) => rl.getName() === filter.name); + } else if (filter.name instanceof RegExp) { + rls = rls.filter((rl) => (filter.name as RegExp).test(rl.getName())); + } + + return rls; + } + + /** Gets a single App by the id passed in. */ + public getOneById(appId: string): ProxiedApp { + return this.apps.get(appId); + } + + public getPermissionsById(appId: string): Array { + const app = this.apps.get(appId); + + if (!app) { + return []; + } + const { permissionsGranted } = app.getStorageItem(); + + return permissionsGranted || defaultPermissions; + } + + public async enable(id: string): Promise { + const rl = this.apps.get(id); + + if (!rl) { + throw new Error(`No App by the id "${id}" exists.`); + } + + const status = await rl.getStatus(); + + if (AppStatusUtils.isEnabled(status)) { + return true; + } + + if (status === AppStatus.COMPILER_ERROR_DISABLED) { + throw new Error('The App had compiler errors, can not enable it.'); + } + + const storageItem = await this.appMetadataStorage.retrieveOne(id); + + if (!storageItem) { + throw new Error(`Could not enable an App with the id of "${id}" as it doesn't exist.`); + } + + const isSetup = await this.runStartUpProcess(storageItem, rl, false); + + return isSetup; + } + + public async disable(id: string, status: AppStatus = AppStatus.DISABLED, silent?: boolean): Promise { + if (!AppStatusUtils.isDisabled(status)) { + throw new Error('Invalid disabled status'); + } + + const app = this.apps.get(id); + + if (!app) { + throw new Error(`No App by the id "${id}" exists.`); + } + + if (AppStatusUtils.isEnabled(await app.getStatus())) { + await app.call(AppMethod.ONDISABLE).catch((e) => console.warn('Error while disabling:', e)); + } + + await this.purgeAppConfig(app, { + keepScheduledJobs: true, + keepSlashcommands: true, + keepOutboundCommunicationProviders: true, + }); + + await app.setStatus(status, silent); + + const storageItem = await this.appMetadataStorage.retrieveOne(id); + + app.getStorageItem().marketplaceInfo = storageItem.marketplaceInfo; + await app.validateLicense().catch(() => {}); + + return true; + } + + public async migrate(id: string): Promise { + const app = this.apps.get(id); + + if (!app) { + throw new Error(`No App by the id "${id}" exists.`); + } + + await app.call(AppMethod.ONUPDATE).catch((e) => console.warn('Error while migrating:', e)); + + await this.purgeAppConfig(app, { keepScheduledJobs: true }); + + const storageItem = await this.appMetadataStorage.retrieveOne(id); + + app.getStorageItem().marketplaceInfo = storageItem.marketplaceInfo; + await app.validateLicense().catch(() => {}); + + storageItem.migrated = true; + storageItem.signature = await this.getSignatureManager().signApp(storageItem); + + const { marketplaceInfo, signature, migrated, _id } = storageItem; + const stored = await this.appMetadataStorage.updatePartialAndReturnDocument({ marketplaceInfo, signature, migrated, _id }); + + await this.updateLocal(stored, app); + await this.bridges + .getAppActivationBridge() + .doAppUpdated(app) + .catch(() => {}); + + return true; + } + + public async addLocal(appId: string): Promise { + const storageItem = await this.appMetadataStorage.retrieveOne(appId); + + if (!storageItem) { + throw new Error(`App with id ${appId} couldn't be found`); + } + + const appPackage = await this.appSourceStorage.fetch(storageItem); + + if (!appPackage) { + throw new Error(`Package file for app "${storageItem.info.name}" (${appId}) couldn't be found`); + } + + const parsedPackage = await this.getParser().unpackageApp(appPackage); + const app = await this.getCompiler().toSandBox(this, storageItem, parsedPackage); + + this.apps.set(app.getID(), app); + + await this.loadOne(appId); + } + + public async add(appPackage: Buffer, installationParameters: IAppInstallParameters): Promise { + const { enable = true, marketplaceInfo, permissionsGranted, user } = installationParameters; + + const aff = new AppFabricationFulfillment(); + const result = await this.getParser().unpackageApp(appPackage); + const undoSteps: Array<() => void> = []; + + aff.setAppInfo(result.info); + aff.setImplementedInterfaces(result.implemented.getValues()); + + const descriptor: IAppStorageItem = { + id: result.info.id, + info: result.info, + status: enable ? AppStatus.MANUALLY_ENABLED : AppStatus.MANUALLY_DISABLED, + settings: {}, + implemented: result.implemented.getValues(), + installationSource: marketplaceInfo ? AppInstallationSource.MARKETPLACE : AppInstallationSource.PRIVATE, + marketplaceInfo, + permissionsGranted, + languageContent: result.languageContent, + }; + + try { + descriptor.sourcePath = await this.appSourceStorage.store(descriptor, appPackage); + + undoSteps.push(() => this.appSourceStorage.remove(descriptor)); + } catch (error) { + aff.setStorageError('Failed to store app package'); + + return aff; + } + + let app: ProxiedApp; + + try { + app = await this.getCompiler().toSandBox(this, descriptor, result); + } catch (error) { + await Promise.all(undoSteps.map((undoer) => undoer())); + + throw error; + } + + undoSteps.push(() => + this.getRuntime() + .stopRuntime(app.getRuntimeController()) + .catch(() => {}), + ); + + // Create a user for the app + try { + await this.createAppUser(result.info); + + undoSteps.push(() => this.removeAppUser(app)); + } catch (err) { + aff.setAppUserError({ + username: `${result.info.nameSlug}.bot`, + message: 'Failed to create an app user for this app.', + }); + + await Promise.all(undoSteps.map((undoer) => undoer())); + + return aff; + } + + descriptor.signature = await this.getSignatureManager().signApp(descriptor); + const created = await this.appMetadataStorage.create(descriptor); + + if (!created) { + aff.setStorageError('Failed to create the App, the storage did not return it.'); + + await Promise.all(undoSteps.map((undoer) => undoer())); + + return aff; + } + + app.getStorageItem()._id = created._id; + + this.apps.set(app.getID(), app); + aff.setApp(app); + + // Let everyone know that the App has been added + await this.bridges + .getAppActivationBridge() + .doAppAdded(app) + .catch(() => { + // If an error occurs during this, oh well. + }); + + await this.installApp(app, user); + + // Should enable === true, then we go through the entire start up process + // Otherwise, we only initialize it. + if (enable) { + // Start up the app + await this.runStartUpProcess(created, app, false); + } else { + await this.initializeApp(app); + } + + return aff; + } + + /** + * Uninstalls specified app from the server and remove + * all database records regarding it + * + * @returns the instance of the removed ProxiedApp + */ + public async remove(id: string, uninstallationParameters: IAppUninstallParameters): Promise { + const app = this.apps.get(id); + const { user } = uninstallationParameters; + + // First remove the app + await this.uninstallApp(app, user); + await this.removeLocal(id); + + // Then let everyone know that the App has been removed + await this.bridges.getAppActivationBridge().doAppRemoved(app).catch(); + + return app; + } + + /** + * Removes the app instance from the local Apps container + * and every type of data associated with it + */ + public async removeLocal(id: string): Promise { + const app = this.apps.get(id); + + if (AppStatusUtils.isEnabled(await app.getStatus())) { + await this.disable(id); + } + + await this.purgeAppConfig(app); + this.listenerManager.releaseEssentialEvents(app); + await this.removeAppUser(app); + await (this.bridges.getPersistenceBridge() as IInternalPersistenceBridge & PersistenceBridge).purge(app.getID()); + await this.appMetadataStorage.remove(app.getID()); + await this.appSourceStorage.remove(app.getStorageItem()).catch(() => {}); + + // Errors here don't really prevent the process from dying, so we don't really need to do anything on the catch + await this.getRuntime() + .stopRuntime(app.getRuntimeController()) + .catch(() => {}); + + this.apps.delete(app.getID()); + } + + public async update( + appPackage: Buffer, + permissionsGranted: Array, + updateOptions: { loadApp?: boolean; user?: IUser } = { loadApp: true }, + ): Promise { + const aff = new AppFabricationFulfillment(); + const result = await this.getParser().unpackageApp(appPackage); + + aff.setAppInfo(result.info); + aff.setImplementedInterfaces(result.implemented.getValues()); + + const old = await this.appMetadataStorage.retrieveOne(result.info.id); + + if (!old) { + throw new Error('Can not update an App that does not currently exist.'); + } + + // If there is any error during disabling, it doesn't really matter + await this.disable(old.id).catch(() => {}); + + const descriptor: IAppStorageItem = { + ...old, + id: result.info.id, + info: result.info, + languageContent: result.languageContent, + implemented: result.implemented.getValues(), + }; + + if (!permissionsGranted) { + delete descriptor.permissionsGranted; + } else { + descriptor.permissionsGranted = permissionsGranted; + } + + try { + descriptor.sourcePath = await this.appSourceStorage.update(descriptor, appPackage); + } catch (error) { + aff.setStorageError('Failed to storage app package'); + + return aff; + } + + descriptor.signature = await this.signatureManager.signApp(descriptor); + const stored = await this.appMetadataStorage.updatePartialAndReturnDocument(descriptor, { + unsetPermissionsGranted: typeof permissionsGranted === 'undefined', + }); + + // Errors here don't really prevent the process from dying, so we don't really need to do anything on the catch + await this.getRuntime() + .stopRuntime(this.apps.get(old.id).getRuntimeController()) + .catch(() => {}); + + const app = await this.getCompiler().toSandBox(this, descriptor, result); + + // Ensure there is an user for the app + try { + await this.createAppUser(result.info); + } catch (err) { + aff.setAppUserError({ + username: `${result.info.nameSlug}.bot`, + message: 'Failed to create an app user for this app.', + }); + + return aff; + } + + aff.setApp(app); + + if (updateOptions.loadApp) { + const shouldEnableApp = AppStatusUtils.isEnabled(old.status); + if (shouldEnableApp) { + await this.updateAndStartupLocal(stored, app); + } else { + await this.updateAndInitializeLocal(stored, app); + } + + await this.bridges + .getAppActivationBridge() + .doAppUpdated(app) + .catch(() => {}); + } + + await this.updateApp(app, updateOptions.user, old.info.version); + + return aff; + } + + /** + * Updates the local instance of an app. + * + * If the second parameter is a Buffer of an app package, + * unpackage and instantiate the app's main class + * + * With an instance of a ProxiedApp, start it up and replace + * the reference in the local app collection + */ + async updateLocal(stored: IAppStorageItem, appPackageOrInstance: ProxiedApp | Buffer): Promise { + const app = await (async () => { + if (appPackageOrInstance instanceof Buffer) { + const parseResult = await this.getParser().unpackageApp(appPackageOrInstance); + + // Errors here don't really prevent the process from dying, so we don't really need to do anything on the catch + await this.getRuntime() + .stopRuntime(this.apps.get(stored.id).getRuntimeController()) + .catch(() => {}); + + return this.getCompiler().toSandBox(this, stored, parseResult); + } + + if (appPackageOrInstance instanceof ProxiedApp) { + return appPackageOrInstance; + } + })(); + + // We don't keep slashcommands here as the update could potentially not provide the same list + await this.purgeAppConfig(app, { keepScheduledJobs: true }); + + this.apps.set(app.getID(), app); + return app; + } + + public async updateAndStartupLocal(stored: IAppStorageItem, appPackageOrInstance: ProxiedApp | Buffer) { + const app = await this.updateLocal(stored, appPackageOrInstance); + await this.runStartUpProcess(stored, app, true); + } + + public async updateAndInitializeLocal(stored: IAppStorageItem, appPackageOrInstance: ProxiedApp | Buffer) { + const app = await this.updateLocal(stored, appPackageOrInstance); + await this.initializeApp(app, true); + } + + public getLanguageContent(): { [key: string]: object } { + const langs: { [key: string]: object } = {}; + + this.apps.forEach((rl) => { + const content = rl.getStorageItem().languageContent; + + Object.keys(content).forEach((key) => { + langs[key] = Object.assign(langs[key] || {}, content[key]); + }); + }); + + return langs; + } + + public async changeStatus(appId: string, status: AppStatus): Promise { + switch (status) { + case AppStatus.MANUALLY_DISABLED: + case AppStatus.MANUALLY_ENABLED: + break; + default: + throw new Error('Invalid status to change an App to, must be manually disabled or enabled.'); + } + + const rl = this.apps.get(appId); + + if (!rl) { + throw new Error('Can not change the status of an App which does not currently exist.'); + } + + const storageItem = await rl.getStorageItem(); + + if (AppStatusUtils.isEnabled(status)) { + // Then enable it + if (AppStatusUtils.isEnabled(await rl.getStatus())) { + throw new Error('Can not enable an App which is already enabled.'); + } + + await this.enable(rl.getID()); + + storageItem.status = AppStatus.MANUALLY_ENABLED; + await this.appMetadataStorage.updateStatus(storageItem._id, AppStatus.MANUALLY_ENABLED); + } else { + if (!AppStatusUtils.isEnabled(await rl.getStatus())) { + throw new Error('Can not disable an App which is not enabled.'); + } + + await this.disable(rl.getID(), AppStatus.MANUALLY_DISABLED); + + storageItem.status = AppStatus.MANUALLY_DISABLED; + await this.appMetadataStorage.updateStatus(storageItem._id, AppStatus.MANUALLY_DISABLED); + } + + return rl; + } + + public async updateAppsMarketplaceInfo(appsOverview: Array<{ latest: IMarketplaceInfo }>): Promise { + await Promise.all( + appsOverview.map(async ({ latest: appInfo }) => { + if (!appInfo.subscriptionInfo) { + return; + } + + const app = this.apps.get(appInfo.id); + + if (!app) { + return; + } + + const appStorageItem = app.getStorageItem(); + const { subscriptionInfo } = appStorageItem.marketplaceInfo?.[0] || {}; + + if (subscriptionInfo && subscriptionInfo.license.license === appInfo.subscriptionInfo.license.license) { + return; + } + + appStorageItem.marketplaceInfo[0].subscriptionInfo = appInfo.subscriptionInfo; + appStorageItem.signature = await this.getSignatureManager().signApp(appStorageItem); + + return this.appMetadataStorage.updatePartialAndReturnDocument({ + _id: appStorageItem._id, + marketplaceInfo: appStorageItem.marketplaceInfo, + signature: appStorageItem.signature, + }); + }), + ).catch(() => {}); + + const queue = [] as Array>; + + this.apps.forEach((app) => + queue.push( + app + .validateLicense() + .then(async () => { + if ((await app.getStatus()) !== AppStatus.INVALID_LICENSE_DISABLED) { + return; + } + + return app.setStatus(AppStatus.DISABLED); + }) + .catch(async (error) => { + if (!(error instanceof InvalidLicenseError)) { + console.error(error); + return; + } + + await this.purgeAppConfig(app, { keepScheduledJobs: true }); + + return app.setStatus(AppStatus.INVALID_LICENSE_DISABLED); + }) + .then(async () => { + const status = await app.getStatus(); + if (status === app.getPreviousStatus()) { + return; + } + + const storageItem = app.getStorageItem(); + storageItem.status = status; + + return this.appMetadataStorage.updateStatus(storageItem._id, storageItem.status).catch(console.error) as Promise; + }), + ), + ); + + await Promise.all(queue); + } + + /** + * Goes through the entire loading up process. + * + * @param appId the id of the application to load + */ + public async loadOne(appId: string, silenceStatus = false): Promise { + const rl = this.apps.get(appId); + + if (!rl) { + throw new Error(`No App found by the id of: "${appId}"`); + } + + const item = rl.getStorageItem(); + + await this.initializeApp(rl, silenceStatus); + + if (!this.areRequiredSettingsSet(item)) { + await rl.setStatus(AppStatus.INVALID_SETTINGS_DISABLED); + } + + if (!AppStatusUtils.isDisabled(await rl.getStatus()) && AppStatusUtils.isEnabled(rl.getPreviousStatus())) { + await this.enableApp(rl, silenceStatus); + } + + return this.apps.get(item.id); + } + + private async runStartUpProcess(storageItem: IAppStorageItem, app: ProxiedApp, silenceStatus: boolean): Promise { + if ((await app.getStatus()) !== AppStatus.INITIALIZED) { + const isInitialized = await this.initializeApp(app, silenceStatus); + if (!isInitialized) { + return false; + } + } + + if (!this.areRequiredSettingsSet(storageItem)) { + await app.setStatus(AppStatus.INVALID_SETTINGS_DISABLED, silenceStatus); + return false; + } + + return this.enableApp(app, silenceStatus); + } + + private async installApp(app: ProxiedApp, user: IUser): Promise { + let result: boolean; + const context = { user }; + + try { + await app.call(AppMethod.ONINSTALL, context); + + result = true; + } catch (e) { + const status = AppStatus.ERROR_DISABLED; + + result = false; + + await app.setStatus(status); + } + + return result; + } + + private async updateApp(app: ProxiedApp, user: IUser | null, oldAppVersion: string): Promise { + let result: boolean; + + try { + await app.call(AppMethod.ONUPDATE, { oldAppVersion, user }); + + result = true; + } catch (e) { + const status = AppStatus.ERROR_DISABLED; + + result = false; + + await app.setStatus(status); + } + + return result; + } + + private async initializeApp(app: ProxiedApp, silenceStatus = false): Promise { + let result: boolean; + + try { + await app.validateLicense(); + await app.validateInstallation(); + + await app.call(AppMethod.INITIALIZE); + await app.setStatus(AppStatus.INITIALIZED, silenceStatus); + + await this.commandManager.registerCommands(app.getID()); + + result = true; + } catch (e) { + let status = AppStatus.ERROR_DISABLED; + + if (e instanceof InvalidLicenseError) { + status = AppStatus.INVALID_LICENSE_DISABLED; + } + + if (e instanceof InvalidInstallationError) { + status = AppStatus.INVALID_INSTALLATION_DISABLED; + } + + await this.purgeAppConfig(app); + result = false; + + await app.setStatus(status, silenceStatus); + } + + return result; + } + + private async purgeAppConfig(app: ProxiedApp, opts: IPurgeAppConfigOpts = {}) { + if (!opts.keepScheduledJobs) { + await this.schedulerManager.cleanUp(app.getID()); + } + + if (!opts.keepSlashcommands) { + await this.commandManager.unregisterCommands(app.getID()); + } + + this.listenerManager.unregisterListeners(app); + this.listenerManager.lockEssentialEvents(app); + this.externalComponentManager.unregisterExternalComponents(app.getID()); + await this.apiManager.unregisterApis(app.getID()); + this.accessorManager.purifyApp(app.getID()); + this.uiActionButtonManager.clearAppActionButtons(app.getID()); + this.videoConfProviderManager.unregisterProviders(app.getID()); + await this.outboundCommunicationProviderManager.unregisterProviders(app.getID(), { + keepReferences: opts.keepOutboundCommunicationProviders, + }); + } + + /** + * Determines if the App's required settings are set or not. + * Should a packageValue be provided and not empty, then it's considered set. + */ + private areRequiredSettingsSet(storageItem: IAppStorageItem): boolean { + let result = true; + + for (const setk of Object.keys(storageItem.settings)) { + const sett = storageItem.settings[setk]; + // If it's not required, ignore + if (!sett.required) { + continue; + } + + if (sett.value !== 'undefined' || sett.packageValue !== 'undefined') { + continue; + } + + result = false; + } + + return result; + } + + private async enableApp(app: ProxiedApp, silenceStatus = false): Promise { + let enable: boolean; + let status = AppStatus.ERROR_DISABLED; + + try { + await app.validateLicense(); + await app.validateInstallation(); + + enable = (await app.call(AppMethod.ONENABLE)) as boolean; + + if (enable) { + status = AppStatus.MANUALLY_ENABLED; + } else { + status = AppStatus.DISABLED; + console.warn(`The App (${app.getID()}) disabled itself when being enabled. \nCheck the "onEnable" implementation for details.`); + } + } catch (e) { + enable = false; + + if (e instanceof InvalidLicenseError) { + status = AppStatus.INVALID_LICENSE_DISABLED; + } + + if (e instanceof InvalidInstallationError) { + status = AppStatus.INVALID_INSTALLATION_DISABLED; + } + + console.error(e); + } + + if (enable) { + this.externalComponentManager.registerExternalComponents(app.getID()); + await this.apiManager.registerApis(app.getID()); + this.listenerManager.registerListeners(app); + this.listenerManager.releaseEssentialEvents(app); + this.videoConfProviderManager.registerProviders(app.getID()); + await this.outboundCommunicationProviderManager.registerProviders(app.getID()); + } else { + await this.purgeAppConfig(app, { + keepScheduledJobs: true, + keepSlashcommands: true, + keepOutboundCommunicationProviders: true, + }); + } + + await app.setStatus(status, silenceStatus); + + return enable; + } + + private async createAppUser(appInfo: IAppInfo): Promise { + const appUser = await (this.bridges.getUserBridge() as IInternalUserBridge & UserBridge).getAppUser(appInfo.id); + + if (appUser) { + return appUser.id; + } + + const userData: Partial = { + username: `${appInfo.nameSlug}.bot`, + name: appInfo.name, + roles: ['app'], + appId: appInfo.id, + type: UserType.APP, + status: 'online', + isEnabled: true, + }; + + return (this.bridges.getUserBridge() as IInternalUserBridge & UserBridge).create(userData, appInfo.id, { + avatarUrl: appInfo.iconFileContent || appInfo.iconFile, + joinDefaultChannels: true, + sendWelcomeEmail: false, + }); + } + + private async removeAppUser(app: ProxiedApp): Promise { + const appUser = await (this.bridges.getUserBridge() as IInternalUserBridge & UserBridge).getAppUser(app.getID()); + + if (!appUser) { + return true; + } + + return (this.bridges.getUserBridge() as IInternalUserBridge & UserBridge).remove(appUser, app.getID()); + } + + private async uninstallApp(app: ProxiedApp, user: IUser): Promise { + let result: boolean; + const context = { user }; + + try { + await app.call(AppMethod.ONUNINSTALL, context); + + result = true; + } catch (e) { + const status = AppStatus.ERROR_DISABLED; + + result = false; + + await app.setStatus(status); + } + + return result; + } +} + +export const getPermissionsByAppId = (appId: string) => { + if (!AppManager.Instance) { + console.error('AppManager should be instantiated first'); + return []; + } + return AppManager.Instance.getPermissionsById(appId); +}; diff --git a/packages/apps/src/server/IGetAppsFilter.ts b/packages/apps/src/server/IGetAppsFilter.ts new file mode 100644 index 0000000000000..7829725634445 --- /dev/null +++ b/packages/apps/src/server/IGetAppsFilter.ts @@ -0,0 +1,9 @@ +import type { AppInstallationSource } from './storage'; + +export interface IGetAppsFilter { + ids?: Array; + name?: string | RegExp; + enabled?: boolean; + disabled?: boolean; + installationSource?: AppInstallationSource; +} diff --git a/packages/apps/src/server/ProxiedApp.ts b/packages/apps/src/server/ProxiedApp.ts new file mode 100644 index 0000000000000..8523975db2973 --- /dev/null +++ b/packages/apps/src/server/ProxiedApp.ts @@ -0,0 +1,162 @@ +import { inspect } from 'util'; + +import * as mem from 'mem'; + +import type { AppManager } from './AppManager'; +import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; +import type { IAppAuthorInfo, IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; +import { AppMethod } from '@rocket.chat/apps-engine/definition/metadata'; +import { InvalidInstallationError } from './errors/InvalidInstallationError'; +import { AppConsole } from './logging'; +import { AppLicenseValidationResult } from './marketplace/license'; +import type { AppsEngineRuntime } from './runtime/AppsEngineRuntime'; +import type { IRuntimeController } from './runtime/IRuntimeController'; +import { JSONRPC_METHOD_NOT_FOUND } from './runtime/deno/AppsEngineDenoRuntime'; +import type { AppInstallationSource, IAppStorageItem } from './storage'; + +export class ProxiedApp { + private previousStatus: AppStatus; + + private latestLicenseValidationResult: AppLicenseValidationResult; + + constructor( + private readonly manager: AppManager, + private storageItem: IAppStorageItem, + private readonly appRuntime: IRuntimeController, + ) { + this.previousStatus = storageItem.status; + + this.appRuntime.on('processExit', () => mem.clear(this.getStatus)); + } + + public getRuntime(): AppsEngineRuntime { + return this.manager.getRuntime(); + } + + public getRuntimeController(): IRuntimeController { + return this.appRuntime; + } + + public getStorageItem(): IAppStorageItem { + return this.storageItem; + } + + public setStorageItem(item: IAppStorageItem): void { + this.storageItem = item; + } + + public getPreviousStatus(): AppStatus { + return this.previousStatus; + } + + public getImplementationList(): { [inter: string]: boolean } { + return this.storageItem.implemented; + } + + public setupLogger(method: `${AppMethod}`): AppConsole { + const logger = new AppConsole(method); + + return logger; + } + + // We'll need to refactor this method to remove the rest parameters so we can pass an options parameter + public async call(method: `${AppMethod}`, ...args: Array): Promise { + let options; + + try { + return await this.appRuntime.sendRequest({ method: `app:${method}`, params: args }, options); + } catch (e) { + if (e.code === AppsEngineException.JSONRPC_ERROR_CODE) { + throw new AppsEngineException(e.message); + } + + if (e.code === JSONRPC_METHOD_NOT_FOUND) { + throw e; + } + + // We cannot throw this error as the previous implementation swallowed those + // and since the server is not prepared to handle those we might crash it if we throw + // Range of JSON-RPC error codes: https://www.jsonrpc.org/specification#error_object + if (e.code >= -32999 || e.code <= -32000) { + // we really need to receive a logger from rocket.chat + console.error('JSON-RPC error received: ', inspect(e, { depth: 10 })); + } + } + } + + public getStatus = mem(() => this.appRuntime.getStatus().catch(() => AppStatus.UNKNOWN), { maxAge: 1000 * 60 * 5 }); + + public async setStatus(status: AppStatus, silent?: boolean): Promise { + await this.call(AppMethod.SETSTATUS, status); + mem.clear(this.getStatus); + if (!silent) { + await this.manager.getBridges().getAppActivationBridge().doAppStatusChanged(this, status); + } + } + + public getName(): string { + return this.storageItem.info.name; + } + + public getNameSlug(): string { + return this.storageItem.info.nameSlug; + } + + // @deprecated This method will be removed in the next major version + public getAppUserUsername(): string { + return `${this.storageItem.info.nameSlug}.bot`; + } + + public getID(): string { + return this.storageItem.id; + } + + public getInstallationSource(): AppInstallationSource { + return this.storageItem.installationSource; + } + + public getVersion(): string { + return this.storageItem.info.version; + } + + public getDescription(): string { + return this.storageItem.info.description; + } + + public getRequiredApiVersion(): string { + return this.storageItem.info.requiredApiVersion; + } + + public getAuthorInfo(): IAppAuthorInfo { + return this.storageItem.info.author; + } + + public getInfo(): IAppInfo { + return this.storageItem.info; + } + + public getEssentials(): IAppInfo['essentials'] { + return this.getInfo().essentials; + } + + public getLatestLicenseValidationResult(): AppLicenseValidationResult { + return this.latestLicenseValidationResult; + } + + public async validateInstallation(): Promise { + try { + await this.manager.getSignatureManager().verifySignedApp(this.getStorageItem()); + } catch (e) { + throw new InvalidInstallationError(e.message); + } + } + + public validateLicense(): Promise { + const { marketplaceInfo } = this.getStorageItem(); + + this.latestLicenseValidationResult = new AppLicenseValidationResult(); + + return this.manager.getLicenseManager().validate(this.latestLicenseValidationResult, marketplaceInfo); + } +} diff --git a/packages/apps/src/server/accessors/ApiExtend.ts b/packages/apps/src/server/accessors/ApiExtend.ts new file mode 100644 index 0000000000000..37bd5a2680617 --- /dev/null +++ b/packages/apps/src/server/accessors/ApiExtend.ts @@ -0,0 +1,14 @@ +import type { IApiExtend } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IApi } from '@rocket.chat/apps-engine/definition/api'; +import type { AppApiManager } from '../managers/AppApiManager'; + +export class ApiExtend implements IApiExtend { + constructor( + private readonly manager: AppApiManager, + private readonly appId: string, + ) {} + + public provideApi(api: IApi): Promise { + return Promise.resolve(this.manager.addApi(this.appId, api)); + } +} diff --git a/packages/apps/src/server/accessors/AppAccessors.ts b/packages/apps/src/server/accessors/AppAccessors.ts new file mode 100644 index 0000000000000..2ffd38fa6b4b2 --- /dev/null +++ b/packages/apps/src/server/accessors/AppAccessors.ts @@ -0,0 +1,39 @@ +import type { IAppAccessors, IEnvironmentRead, IEnvironmentWrite, IHttp, IRead } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api'; +import type { AppManager } from '../AppManager'; +import type { AppAccessorManager } from '../managers/AppAccessorManager'; +import type { AppApiManager } from '../managers/AppApiManager'; + +export class AppAccessors implements IAppAccessors { + private accessorManager: AppAccessorManager; + + private apiManager: AppApiManager; + + constructor( + manager: AppManager, + private readonly appId: string, + ) { + this.accessorManager = manager.getAccessorManager(); + this.apiManager = manager.getApiManager(); + } + + public get environmentReader(): IEnvironmentRead { + return this.accessorManager.getEnvironmentRead(this.appId); + } + + public get environmentWriter(): IEnvironmentWrite { + return this.accessorManager.getEnvironmentWrite(this.appId); + } + + public get reader(): IRead { + return this.accessorManager.getReader(this.appId); + } + + public get http(): IHttp { + return this.accessorManager.getHttp(this.appId); + } + + public get providedApiEndpoints(): Array { + return this.apiManager.listApis(this.appId); + } +} diff --git a/packages/apps/src/server/accessors/CloudWorkspaceRead.ts b/packages/apps/src/server/accessors/CloudWorkspaceRead.ts new file mode 100644 index 0000000000000..2d858ca916e1b --- /dev/null +++ b/packages/apps/src/server/accessors/CloudWorkspaceRead.ts @@ -0,0 +1,14 @@ +import type { ICloudWorkspaceRead } from '@rocket.chat/apps-engine/definition/accessors/ICloudWorkspaceRead'; +import type { IWorkspaceToken } from '@rocket.chat/apps-engine/definition/cloud/IWorkspaceToken'; +import type { CloudWorkspaceBridge } from '../bridges/CloudWorkspaceBridge'; + +export class CloudWorkspaceRead implements ICloudWorkspaceRead { + constructor( + private readonly cloudBridge: CloudWorkspaceBridge, + private readonly appId: string, + ) {} + + public async getWorkspaceToken(scope: string): Promise { + return this.cloudBridge.doGetWorkspaceToken(scope, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/ConfigurationExtend.ts b/packages/apps/src/server/accessors/ConfigurationExtend.ts new file mode 100644 index 0000000000000..a1629114efd20 --- /dev/null +++ b/packages/apps/src/server/accessors/ConfigurationExtend.ts @@ -0,0 +1,26 @@ +import type { + IApiExtend, + IConfigurationExtend, + IExternalComponentsExtend, + IHttpExtend, + ISchedulerExtend, + ISettingsExtend, + ISlashCommandsExtend, + IUIExtend, + IVideoConfProvidersExtend, + IOutboundCommunicationProviderExtend, +} from '@rocket.chat/apps-engine/definition/accessors'; + +export class ConfigurationExtend implements IConfigurationExtend { + constructor( + public readonly http: IHttpExtend, + public readonly settings: ISettingsExtend, + public readonly slashCommands: ISlashCommandsExtend, + public readonly api: IApiExtend, + public readonly externalComponents: IExternalComponentsExtend, + public readonly scheduler: ISchedulerExtend, + public readonly ui: IUIExtend, + public readonly videoConfProviders: IVideoConfProvidersExtend, + public readonly outboundCommunication: IOutboundCommunicationProviderExtend, + ) {} +} diff --git a/packages/apps/src/server/accessors/ConfigurationModify.ts b/packages/apps/src/server/accessors/ConfigurationModify.ts new file mode 100644 index 0000000000000..4e7b302143f1f --- /dev/null +++ b/packages/apps/src/server/accessors/ConfigurationModify.ts @@ -0,0 +1,9 @@ +import type { IConfigurationModify, ISchedulerModify, IServerSettingsModify, ISlashCommandsModify } from '@rocket.chat/apps-engine/definition/accessors'; + +export class ConfigurationModify implements IConfigurationModify { + constructor( + public readonly serverSettings: IServerSettingsModify, + public readonly slashCommands: ISlashCommandsModify, + public readonly scheduler: ISchedulerModify, + ) {} +} diff --git a/packages/apps/src/server/accessors/ContactCreator.ts b/packages/apps/src/server/accessors/ContactCreator.ts new file mode 100644 index 0000000000000..238a5335c9354 --- /dev/null +++ b/packages/apps/src/server/accessors/ContactCreator.ts @@ -0,0 +1,24 @@ +import type { IContactCreator } from '@rocket.chat/apps-engine/definition/accessors/IContactCreator'; +import type { ILivechatContact } from '@rocket.chat/apps-engine/definition/livechat'; +import type { AppBridges } from '../bridges'; + +export class ContactCreator implements IContactCreator { + constructor( + private readonly bridges: AppBridges, + private readonly appId: string, + ) {} + + verifyContact(verifyContactChannelParams: { + contactId: string; + field: string; + value: string; + visitorId: string; + roomId: string; + }): Promise { + return this.bridges.getContactBridge().doVerifyContact(verifyContactChannelParams, this.appId); + } + + addContactEmail(contactId: ILivechatContact['_id'], email: string): Promise { + return this.bridges.getContactBridge().doAddContactEmail(contactId, email, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/ContactRead.ts b/packages/apps/src/server/accessors/ContactRead.ts new file mode 100644 index 0000000000000..ddabcf064e22f --- /dev/null +++ b/packages/apps/src/server/accessors/ContactRead.ts @@ -0,0 +1,14 @@ +import type { IContactRead } from '@rocket.chat/apps-engine/definition/accessors/IContactRead'; +import type { ILivechatContact } from '@rocket.chat/apps-engine/definition/livechat'; +import type { AppBridges } from '../bridges'; + +export class ContactRead implements IContactRead { + constructor( + private readonly bridges: AppBridges, + private readonly appId: string, + ) {} + + public getById(contactId: ILivechatContact['_id']): Promise { + return this.bridges.getContactBridge().doGetById(contactId, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/DiscussionBuilder.ts b/packages/apps/src/server/accessors/DiscussionBuilder.ts new file mode 100644 index 0000000000000..55723eaadeccb --- /dev/null +++ b/packages/apps/src/server/accessors/DiscussionBuilder.ts @@ -0,0 +1,47 @@ +import { RoomBuilder } from './RoomBuilder'; +import type { IDiscussionBuilder } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; + +export class DiscussionBuilder extends RoomBuilder implements IDiscussionBuilder { + public kind: RocketChatAssociationModel.DISCUSSION; + + private reply: string; + + private parentMessage: IMessage; + + constructor(data?: Partial) { + super(data); + this.kind = RocketChatAssociationModel.DISCUSSION; + this.room.type = RoomType.PRIVATE_GROUP; + } + + public setParentRoom(parentRoom: IRoom): IDiscussionBuilder { + this.room.parentRoom = parentRoom; + return this; + } + + public getParentRoom(): IRoom { + return this.room.parentRoom; + } + + public setReply(reply: string): IDiscussionBuilder { + this.reply = reply; + return this; + } + + public getReply(): string { + return this.reply; + } + + public setParentMessage(parentMessage: IMessage): IDiscussionBuilder { + this.parentMessage = parentMessage; + return this; + } + + public getParentMessage(): IMessage { + return this.parentMessage; + } +} diff --git a/packages/apps/src/server/accessors/EmailCreator.ts b/packages/apps/src/server/accessors/EmailCreator.ts new file mode 100644 index 0000000000000..5c384c49b351c --- /dev/null +++ b/packages/apps/src/server/accessors/EmailCreator.ts @@ -0,0 +1,14 @@ +import type { IEmailCreator } from '@rocket.chat/apps-engine/definition/accessors/IEmailCreator'; +import type { IEmail } from '@rocket.chat/apps-engine/definition/email'; +import type { AppBridges } from '../bridges'; + +export class EmailCreator implements IEmailCreator { + constructor( + private readonly bridges: AppBridges, + private readonly appId: string, + ) {} + + public async send(email: IEmail): Promise { + return this.bridges.getEmailBridge().doSendEmail(email, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/EnvironmentRead.ts b/packages/apps/src/server/accessors/EnvironmentRead.ts new file mode 100644 index 0000000000000..579b8b6db151f --- /dev/null +++ b/packages/apps/src/server/accessors/EnvironmentRead.ts @@ -0,0 +1,21 @@ +import type { IEnvironmentalVariableRead, IEnvironmentRead, IServerSettingRead, ISettingRead } from '@rocket.chat/apps-engine/definition/accessors'; + +export class EnvironmentRead implements IEnvironmentRead { + constructor( + private readonly settings: ISettingRead, + private readonly serverSettings: IServerSettingRead, + private readonly envRead: IEnvironmentalVariableRead, + ) {} + + public getSettings(): ISettingRead { + return this.settings; + } + + public getServerSettings(): IServerSettingRead { + return this.serverSettings; + } + + public getEnvironmentVariables(): IEnvironmentalVariableRead { + return this.envRead; + } +} diff --git a/packages/apps/src/server/accessors/EnvironmentWrite.ts b/packages/apps/src/server/accessors/EnvironmentWrite.ts new file mode 100644 index 0000000000000..d13bc5b834f64 --- /dev/null +++ b/packages/apps/src/server/accessors/EnvironmentWrite.ts @@ -0,0 +1,16 @@ +import type { IEnvironmentWrite, IServerSettingUpdater, ISettingUpdater } from '@rocket.chat/apps-engine/definition/accessors'; + +export class EnvironmentWrite implements IEnvironmentWrite { + constructor( + private readonly settings: ISettingUpdater, + private readonly serverSettings: IServerSettingUpdater, + ) {} + + public getSettings(): ISettingUpdater { + return this.settings; + } + + public getServerSettings(): IServerSettingUpdater { + return this.serverSettings; + } +} diff --git a/packages/apps/src/server/accessors/EnvironmentalVariableRead.ts b/packages/apps/src/server/accessors/EnvironmentalVariableRead.ts new file mode 100644 index 0000000000000..11277832f4d46 --- /dev/null +++ b/packages/apps/src/server/accessors/EnvironmentalVariableRead.ts @@ -0,0 +1,21 @@ +import type { IEnvironmentalVariableRead } from '@rocket.chat/apps-engine/definition/accessors'; +import type { EnvironmentalVariableBridge } from '../bridges'; + +export class EnvironmentalVariableRead implements IEnvironmentalVariableRead { + constructor( + private readonly bridge: EnvironmentalVariableBridge, + private readonly appId: string, + ) {} + + public getValueByName(envVarName: string): Promise { + return this.bridge.doGetValueByName(envVarName, this.appId); + } + + public isReadable(envVarName: string): Promise { + return this.bridge.doIsReadable(envVarName, this.appId); + } + + public isSet(envVarName: string): Promise { + return this.bridge.doIsSet(envVarName, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/ExperimentalRead.ts b/packages/apps/src/server/accessors/ExperimentalRead.ts new file mode 100644 index 0000000000000..3219e1dee62b5 --- /dev/null +++ b/packages/apps/src/server/accessors/ExperimentalRead.ts @@ -0,0 +1,9 @@ +import type { IExperimentalRead } from '@rocket.chat/apps-engine/definition/accessors'; +import type { ExperimentalBridge } from '../bridges'; + +export class ExperimentalRead implements IExperimentalRead { + constructor( + protected readonly experimentalBridge: ExperimentalBridge, + protected readonly appId: string, + ) {} +} diff --git a/packages/apps/src/server/accessors/ExternalComponentsExtend.ts b/packages/apps/src/server/accessors/ExternalComponentsExtend.ts new file mode 100644 index 0000000000000..6942e199d7205 --- /dev/null +++ b/packages/apps/src/server/accessors/ExternalComponentsExtend.ts @@ -0,0 +1,14 @@ +import type { IExternalComponentsExtend } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/externalComponent/IExternalComponent'; +import type { AppExternalComponentManager } from '../managers/AppExternalComponentManager'; + +export class ExternalComponentsExtend implements IExternalComponentsExtend { + constructor( + private readonly manager: AppExternalComponentManager, + private readonly appId: string, + ) {} + + public async register(externalComponent: IExternalComponent): Promise { + return Promise.resolve(this.manager.addExternalComponent(this.appId, externalComponent)); + } +} diff --git a/packages/apps/src/server/accessors/Http.ts b/packages/apps/src/server/accessors/Http.ts new file mode 100644 index 0000000000000..3016e9903c74d --- /dev/null +++ b/packages/apps/src/server/accessors/Http.ts @@ -0,0 +1,77 @@ +import type { IHttp, IHttpExtend, IHttpRequest, IHttpResponse } from '@rocket.chat/apps-engine/definition/accessors'; +import { RequestMethod } from '@rocket.chat/apps-engine/definition/accessors'; +import type { AppBridges } from '../bridges/AppBridges'; +import type { AppAccessorManager } from '../managers/AppAccessorManager'; + +export class Http implements IHttp { + constructor( + private readonly accessManager: AppAccessorManager, + private readonly bridges: AppBridges, + private readonly httpExtender: IHttpExtend, + private readonly appId: string, + ) {} + + public get(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, RequestMethod.GET, options); + } + + public put(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, RequestMethod.PUT, options); + } + + public post(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, RequestMethod.POST, options); + } + + public del(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, RequestMethod.DELETE, options); + } + + public patch(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, RequestMethod.PATCH, options); + } + + private async _processHandler(url: string, method: RequestMethod, options?: IHttpRequest): Promise { + let request = options || {}; + + if (typeof request.headers === 'undefined') { + request.headers = {}; + } + + this.httpExtender.getDefaultHeaders().forEach((value: string, key: string) => { + if (typeof request.headers[key] !== 'string') { + request.headers[key] = value; + } + }); + + if (typeof request.params === 'undefined') { + request.params = {}; + } + + this.httpExtender.getDefaultParams().forEach((value: string, key: string) => { + if (typeof request.params[key] !== 'string') { + request.params[key] = value; + } + }); + + const reader = this.accessManager.getReader(this.appId); + const persis = this.accessManager.getPersistence(this.appId); + + for (const handler of this.httpExtender.getPreRequestHandlers()) { + request = await handler.executePreHttpRequest(url, request, reader, persis); + } + + let response = await this.bridges.getHttpBridge().doCall({ + appId: this.appId, + method, + url, + request, + }); + + for (const handler of this.httpExtender.getPreResponseHandlers()) { + response = await handler.executePreHttpResponse(response, reader, persis); + } + + return response; + } +} diff --git a/packages/apps/src/server/accessors/HttpExtend.ts b/packages/apps/src/server/accessors/HttpExtend.ts new file mode 100644 index 0000000000000..98311a633577b --- /dev/null +++ b/packages/apps/src/server/accessors/HttpExtend.ts @@ -0,0 +1,58 @@ +import type { IHttpExtend, IHttpPreRequestHandler, IHttpPreResponseHandler } from '@rocket.chat/apps-engine/definition/accessors'; + +export class HttpExtend implements IHttpExtend { + private headers: Map; + + private params: Map; + + private requests: Array; + + private responses: Array; + + constructor() { + this.headers = new Map(); + this.params = new Map(); + this.requests = []; + this.responses = []; + } + + public provideDefaultHeader(key: string, value: string): void { + this.headers.set(key, value); + } + + public provideDefaultHeaders(headers: { [key: string]: string }): void { + Object.keys(headers).forEach((key) => this.headers.set(key, headers[key])); + } + + public provideDefaultParam(key: string, value: string): void { + this.params.set(key, value); + } + + public provideDefaultParams(params: { [key: string]: string }): void { + Object.keys(params).forEach((key) => this.params.set(key, params[key])); + } + + public providePreRequestHandler(handler: IHttpPreRequestHandler): void { + this.requests.push(handler); + } + + public providePreResponseHandler(handler: IHttpPreResponseHandler): void { + this.responses.push(handler); + } + + public getDefaultHeaders(): Map { + return new Map(this.headers); + } + + public getDefaultParams(): Map { + return new Map(this.params); + } + + public getPreRequestHandlers(): Array { + return Array.from(this.requests); + } + + public getPreResponseHandlers(): Array { + return Array.from(this.responses); + } +} diff --git a/packages/apps/src/server/accessors/LivechatCreator.ts b/packages/apps/src/server/accessors/LivechatCreator.ts new file mode 100644 index 0000000000000..6e386ecc83002 --- /dev/null +++ b/packages/apps/src/server/accessors/LivechatCreator.ts @@ -0,0 +1,38 @@ +import { randomBytes } from 'crypto'; + +import type { ILivechatCreator } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IExtraRoomParams } from '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator'; +import type { ILivechatRoom } from '@rocket.chat/apps-engine/definition/livechat/ILivechatRoom'; +import type { IVisitorExternalIdentifier, IVisitor, ResolveVisitorContactData } from '@rocket.chat/apps-engine/definition/livechat/IVisitor'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import type { AppBridges } from '../bridges'; + +export class LivechatCreator implements ILivechatCreator { + constructor( + private readonly bridges: AppBridges, + private readonly appId: string, + ) {} + + public resolveVisitor(externalId: IVisitorExternalIdentifier, contactData?: ResolveVisitorContactData): Promise { + return this.bridges.getLivechatBridge().doResolveVisitor(externalId, contactData, this.appId); + } + + public createRoom(visitor: IVisitor, agent: IUser, extraParams?: IExtraRoomParams): Promise { + return this.bridges.getLivechatBridge().doCreateRoom(visitor, agent, this.appId, extraParams); + } + + /** + * @deprecated Use `createAndReturnVisitor` instead. + */ + public createVisitor(visitor: IVisitor): Promise { + return this.bridges.getLivechatBridge().doCreateVisitor(visitor, this.appId); + } + + public createAndReturnVisitor(visitor: IVisitor): Promise { + return this.bridges.getLivechatBridge().doCreateAndReturnVisitor(visitor, this.appId); + } + + public createToken(): string { + return randomBytes(16).toString('hex'); // Ensures 128 bits of entropy + } +} diff --git a/packages/apps/src/server/accessors/LivechatMessageBuilder.ts b/packages/apps/src/server/accessors/LivechatMessageBuilder.ts new file mode 100644 index 0000000000000..d83b0ec3455eb --- /dev/null +++ b/packages/apps/src/server/accessors/LivechatMessageBuilder.ts @@ -0,0 +1,191 @@ +import { MessageBuilder } from './MessageBuilder'; +import type { ILivechatMessageBuilder, IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors'; +import type { ILivechatMessage } from '@rocket.chat/apps-engine/definition/livechat/ILivechatMessage'; +import type { IVisitor } from '@rocket.chat/apps-engine/definition/livechat/IVisitor'; +import type { IMessage, IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; + +export class LivechatMessageBuilder implements ILivechatMessageBuilder { + public kind: RocketChatAssociationModel.LIVECHAT_MESSAGE; + + private msg: ILivechatMessage; + + constructor(message?: ILivechatMessage) { + this.kind = RocketChatAssociationModel.LIVECHAT_MESSAGE; + this.msg = message || ({} as ILivechatMessage); + } + + public setData(data: ILivechatMessage): ILivechatMessageBuilder { + delete data.id; + this.msg = data; + + return this; + } + + public setRoom(room: IRoom): ILivechatMessageBuilder { + this.msg.room = room; + return this; + } + + public getRoom(): IRoom { + return this.msg.room; + } + + public setSender(sender: IUser): ILivechatMessageBuilder { + this.msg.sender = sender; + delete this.msg.visitor; + + return this; + } + + public getSender(): IUser { + return this.msg.sender; + } + + public setText(text: string): ILivechatMessageBuilder { + this.msg.text = text; + return this; + } + + public getText(): string { + return this.msg.text; + } + + public setEmojiAvatar(emoji: string): ILivechatMessageBuilder { + this.msg.emoji = emoji; + return this; + } + + public getEmojiAvatar(): string { + return this.msg.emoji; + } + + public setAvatarUrl(avatarUrl: string): ILivechatMessageBuilder { + this.msg.avatarUrl = avatarUrl; + return this; + } + + public getAvatarUrl(): string { + return this.msg.avatarUrl; + } + + public setUsernameAlias(alias: string): ILivechatMessageBuilder { + this.msg.alias = alias; + return this; + } + + public getUsernameAlias(): string { + return this.msg.alias; + } + + public addAttachment(attachment: IMessageAttachment): ILivechatMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + this.msg.attachments.push(attachment); + return this; + } + + public setAttachments(attachments: Array): ILivechatMessageBuilder { + this.msg.attachments = attachments; + return this; + } + + public getAttachments(): Array { + return this.msg.attachments; + } + + public replaceAttachment(position: number, attachment: IMessageAttachment): ILivechatMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + if (!this.msg.attachments[position]) { + throw new Error(`No attachment found at the index of "${position}" to replace.`); + } + + this.msg.attachments[position] = attachment; + return this; + } + + public removeAttachment(position: number): ILivechatMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + if (!this.msg.attachments[position]) { + throw new Error(`No attachment found at the index of "${position}" to remove.`); + } + + this.msg.attachments.splice(position, 1); + + return this; + } + + public setEditor(user: IUser): ILivechatMessageBuilder { + this.msg.editor = user; + return this; + } + + public getEditor(): IUser { + return this.msg.editor; + } + + public setGroupable(groupable: boolean): ILivechatMessageBuilder { + this.msg.groupable = groupable; + return this; + } + + public getGroupable(): boolean { + return this.msg.groupable; + } + + public setParseUrls(parseUrls: boolean): ILivechatMessageBuilder { + this.msg.parseUrls = parseUrls; + return this; + } + + public getParseUrls(): boolean { + return this.msg.parseUrls; + } + + public setToken(token: string): ILivechatMessageBuilder { + this.msg.token = token; + return this; + } + + public getToken(): string { + return this.msg.token; + } + + public setVisitor(visitor: IVisitor): ILivechatMessageBuilder { + this.msg.visitor = visitor; + delete this.msg.sender; + + return this; + } + + public getVisitor(): IVisitor { + return this.msg.visitor; + } + + public getMessage(): ILivechatMessage { + if (!this.msg.room) { + throw new Error('The "room" property is required.'); + } + + if (this.msg.room.type !== RoomType.LIVE_CHAT) { + throw new Error('The room is not a Livechat room'); + } + + return this.msg; + } + + public getMessageBuilder(): IMessageBuilder { + return new MessageBuilder(this.msg as IMessage); + } +} diff --git a/packages/apps/src/server/accessors/LivechatRead.ts b/packages/apps/src/server/accessors/LivechatRead.ts new file mode 100644 index 0000000000000..5db4b480c79e0 --- /dev/null +++ b/packages/apps/src/server/accessors/LivechatRead.ts @@ -0,0 +1,78 @@ +import type { ILivechatRead } from '@rocket.chat/apps-engine/definition/accessors/ILivechatRead'; +import type { IDepartment } from '@rocket.chat/apps-engine/definition/livechat'; +import type { ILivechatRoom } from '@rocket.chat/apps-engine/definition/livechat/ILivechatRoom'; +import type { IVisitor } from '@rocket.chat/apps-engine/definition/livechat/IVisitor'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { LivechatBridge } from '../bridges/LivechatBridge'; + +export class LivechatRead implements ILivechatRead { + constructor( + private readonly livechatBridge: LivechatBridge, + private readonly appId: string, + ) {} + + /** + * @deprecated please use the `isOnlineAsync` method instead. + * In the next major, this method will be `async` + */ + public isOnline(departmentId?: string): boolean { + console.warn( + "The `LivechatRead.isOnline` method is deprecated and won't behave as intended. Please use `LivechatRead.isOnlineAsync` instead", + ); + + return this.livechatBridge.doIsOnline(departmentId, this.appId); + } + + public isOnlineAsync(departmentId?: string): Promise { + return this.livechatBridge.doIsOnlineAsync(departmentId, this.appId); + } + + public getDepartmentsEnabledWithAgents(): Promise> { + return this.livechatBridge.doFindDepartmentsEnabledWithAgents(this.appId); + } + + public getLivechatRooms(visitor: IVisitor, departmentId?: string): Promise> { + return this.livechatBridge.doFindRooms(visitor, departmentId, this.appId); + } + + public getLivechatTotalOpenRoomsByAgentId(agentId: string): Promise { + return this.livechatBridge.doCountOpenRoomsByAgentId(agentId, this.appId); + } + + public getLivechatOpenRoomsByAgentId(agentId: string): Promise> { + return this.livechatBridge.doFindOpenRoomsByAgentId(agentId, this.appId); + } + + /** + * @deprecated This method does not adhere to the conversion practices applied + * elsewhere in the Apps-Engine and will be removed in the next major version. + * Prefer the alternative methods to fetch visitors. + */ + public getLivechatVisitors(query: object): Promise> { + return this.livechatBridge.doFindVisitors(query, this.appId); + } + + public getLivechatVisitorById(id: string): Promise { + return this.livechatBridge.doFindVisitorById(id, this.appId); + } + + public getLivechatVisitorByEmail(email: string): Promise { + return this.livechatBridge.doFindVisitorByEmail(email, this.appId); + } + + public getLivechatVisitorByToken(token: string): Promise { + return this.livechatBridge.doFindVisitorByToken(token, this.appId); + } + + public getLivechatVisitorByPhoneNumber(phoneNumber: string): Promise { + return this.livechatBridge.doFindVisitorByPhoneNumber(phoneNumber, this.appId); + } + + public getLivechatDepartmentByIdOrName(value: string): Promise { + return this.livechatBridge.doFindDepartmentByIdOrName(value, this.appId); + } + + public _fetchLivechatRoomMessages(roomId: string): Promise> { + return this.livechatBridge.do_fetchLivechatRoomMessages(this.appId, roomId); + } +} diff --git a/packages/apps/src/server/accessors/LivechatUpdater.ts b/packages/apps/src/server/accessors/LivechatUpdater.ts new file mode 100644 index 0000000000000..5e7c349e427fc --- /dev/null +++ b/packages/apps/src/server/accessors/LivechatUpdater.ts @@ -0,0 +1,33 @@ +import type { ILivechatUpdater } from '@rocket.chat/apps-engine/definition/accessors'; +import type { ILivechatRoom, ILivechatTransferData, IVisitor, IVisitorExternalIdentifier } from '@rocket.chat/apps-engine/definition/livechat'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import type { AppBridges } from '../bridges'; + +export class LivechatUpdater implements ILivechatUpdater { + constructor( + private readonly bridges: AppBridges, + private readonly appId: string, + ) {} + + public transferVisitor(visitor: IVisitor, transferData: ILivechatTransferData): Promise { + return this.bridges.getLivechatBridge().doTransferVisitor(visitor, transferData, this.appId); + } + + public closeRoom(room: ILivechatRoom, comment: string, closer?: IUser): Promise { + return this.bridges.getLivechatBridge().doCloseRoom(room, comment, closer, this.appId); + } + + public setCustomFields(token: IVisitor['token'], key: string, value: string, overwrite: boolean): Promise { + return this.bridges + .getLivechatBridge() + .doSetCustomFields({ token, key, value, overwrite }, this.appId) + .then((result) => result > 0); + } + + public updateVisitorExternalId( + visitorId: string, + externalId: Omit, + ): Promise { + return this.bridges.getLivechatBridge().doUpdateVisitorExternalId(visitorId, externalId, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/MessageBuilder.ts b/packages/apps/src/server/accessors/MessageBuilder.ts new file mode 100644 index 0000000000000..1d30ad35f9410 --- /dev/null +++ b/packages/apps/src/server/accessors/MessageBuilder.ts @@ -0,0 +1,224 @@ +import type { LayoutBlock } from '@rocket.chat/ui-kit'; + +import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IMessage, IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit'; +import { BlockBuilder } from '@rocket.chat/apps-engine/definition/uikit'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; + +export class MessageBuilder implements IMessageBuilder { + public kind: RocketChatAssociationModel.MESSAGE; + + private msg: IMessage; + + constructor(message?: IMessage) { + this.kind = RocketChatAssociationModel.MESSAGE; + this.msg = message || ({} as IMessage); + } + + public setData(data: IMessage): IMessageBuilder { + delete data.id; + this.msg = data; + + return this; + } + + public setUpdateData(data: IMessage, editor: IUser): IMessageBuilder { + this.msg = data; + this.msg.editor = editor; + this.msg.editedAt = new Date(); + + return this; + } + + public setThreadId(threadId: string): IMessageBuilder { + this.msg.threadId = threadId; + + return this; + } + + public getThreadId(): string { + return this.msg.threadId; + } + + public setRoom(room: IRoom): IMessageBuilder { + this.msg.room = room; + return this; + } + + public getRoom(): IRoom { + return this.msg.room; + } + + public setSender(sender: IUser): IMessageBuilder { + this.msg.sender = sender; + return this; + } + + public getSender(): IUser { + return this.msg.sender; + } + + public setText(text: string): IMessageBuilder { + this.msg.text = text; + return this; + } + + public getText(): string { + return this.msg.text; + } + + public setEmojiAvatar(emoji: string): IMessageBuilder { + this.msg.emoji = emoji; + return this; + } + + public getEmojiAvatar(): string { + return this.msg.emoji; + } + + public setAvatarUrl(avatarUrl: string): IMessageBuilder { + this.msg.avatarUrl = avatarUrl; + return this; + } + + public getAvatarUrl(): string { + return this.msg.avatarUrl; + } + + public setUsernameAlias(alias: string): IMessageBuilder { + this.msg.alias = alias; + return this; + } + + public getUsernameAlias(): string { + return this.msg.alias; + } + + public addAttachment(attachment: IMessageAttachment): IMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + this.msg.attachments.push(attachment); + return this; + } + + public setAttachments(attachments: Array): IMessageBuilder { + this.msg.attachments = attachments; + return this; + } + + public getAttachments(): Array { + return this.msg.attachments; + } + + public replaceAttachment(position: number, attachment: IMessageAttachment): IMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + if (!this.msg.attachments[position]) { + throw new Error(`No attachment found at the index of "${position}" to replace.`); + } + + this.msg.attachments[position] = attachment; + return this; + } + + public removeAttachment(position: number): IMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + if (!this.msg.attachments[position]) { + throw new Error(`No attachment found at the index of "${position}" to remove.`); + } + + this.msg.attachments.splice(position, 1); + + return this; + } + + public setEditor(user: IUser): IMessageBuilder { + this.msg.editor = user; + return this; + } + + public getEditor(): IUser { + return this.msg.editor; + } + + public setGroupable(groupable: boolean): IMessageBuilder { + this.msg.groupable = groupable; + return this; + } + + public getGroupable(): boolean { + return this.msg.groupable; + } + + public setParseUrls(parseUrls: boolean): IMessageBuilder { + this.msg.parseUrls = parseUrls; + return this; + } + + public getParseUrls(): boolean { + return this.msg.parseUrls; + } + + public getMessage(): IMessage { + if (!this.msg.room) { + throw new Error('The "room" property is required.'); + } + + return this.msg; + } + + public addBlocks(blocks: BlockBuilder | Array) { + if (!Array.isArray(this.msg.blocks)) { + this.msg.blocks = []; + } + + if (blocks instanceof BlockBuilder) { + this.msg.blocks.push(...blocks.getBlocks()); + } else { + this.msg.blocks.push(...blocks); + } + + return this; + } + + public setBlocks(blocks: BlockBuilder | Array) { + if (blocks instanceof BlockBuilder) { + this.msg.blocks = blocks.getBlocks(); + } else { + this.msg.blocks = blocks; + } + + return this; + } + + public getBlocks() { + return this.msg.blocks; + } + + public addCustomField(key: string, value: any): IMessageBuilder { + if (!this.msg.customFields) { + this.msg.customFields = {}; + } + + if (this.msg.customFields[key]) { + throw new Error(`The message already contains a custom field by the key: ${key}`); + } + + if (key.includes('.')) { + throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`); + } + + this.msg.customFields[key] = value; + return this; + } +} diff --git a/packages/apps/src/server/accessors/MessageExtender.ts b/packages/apps/src/server/accessors/MessageExtender.ts new file mode 100644 index 0000000000000..e569ccf7efbcc --- /dev/null +++ b/packages/apps/src/server/accessors/MessageExtender.ts @@ -0,0 +1,50 @@ +import type { IMessageExtender } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IMessage, IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata'; +import { Utilities } from '../misc/Utilities'; + +export class MessageExtender implements IMessageExtender { + public readonly kind: RocketChatAssociationModel.MESSAGE; + + constructor(private msg: IMessage) { + this.kind = RocketChatAssociationModel.MESSAGE; + + if (!Array.isArray(msg.attachments)) { + this.msg.attachments = []; + } + } + + public addCustomField(key: string, value: any): IMessageExtender { + if (!this.msg.customFields) { + this.msg.customFields = {}; + } + + if (this.msg.customFields[key]) { + throw new Error(`The message already contains a custom field by the key: ${key}`); + } + + if (key.includes('.')) { + throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`); + } + + this.msg.customFields[key] = value; + + return this; + } + + public addAttachment(attachment: IMessageAttachment): IMessageExtender { + this.msg.attachments.push(attachment); + + return this; + } + + public addAttachments(attachments: Array): IMessageExtender { + this.msg.attachments = this.msg.attachments.concat(attachments); + + return this; + } + + public getMessage(): IMessage { + return Utilities.deepClone(this.msg); + } +} diff --git a/packages/apps/src/server/accessors/MessageRead.ts b/packages/apps/src/server/accessors/MessageRead.ts new file mode 100644 index 0000000000000..f3bd44fc0dce4 --- /dev/null +++ b/packages/apps/src/server/accessors/MessageRead.ts @@ -0,0 +1,36 @@ +import type { IMessageRead } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import type { MessageBridge } from '../bridges/MessageBridge'; + +export class MessageRead implements IMessageRead { + constructor( + private messageBridge: MessageBridge, + private appId: string, + ) {} + + public getById(id: string): Promise { + return this.messageBridge.doGetById(id, this.appId); + } + + public async getSenderUser(messageId: string): Promise { + const msg = await this.messageBridge.doGetById(messageId, this.appId); + + if (!msg) { + return undefined; + } + + return msg.sender; + } + + public async getRoom(messageId: string): Promise { + const msg = await this.messageBridge.doGetById(messageId, this.appId); + + if (!msg) { + return undefined; + } + + return msg.room; + } +} diff --git a/packages/apps/src/server/accessors/ModerationModify.ts b/packages/apps/src/server/accessors/ModerationModify.ts new file mode 100644 index 0000000000000..a8a308053876b --- /dev/null +++ b/packages/apps/src/server/accessors/ModerationModify.ts @@ -0,0 +1,23 @@ +import type { IModerationModify } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import type { ModerationBridge } from '../bridges'; + +export class ModerationModify implements IModerationModify { + constructor( + private moderationBridge: ModerationBridge, + appId: string, + ) {} + + public report(messageId: string, description: string, userId: string, appId: string): Promise { + return this.moderationBridge.doReport(messageId, description, userId, appId); + } + + public dismissReportsByMessageId(messageId: IMessage['id'], reason: string, action: string, appId: string): Promise { + return this.moderationBridge.doDismissReportsByMessageId(messageId, reason, action, appId); + } + + public dismissReportsByUserId(userId: IUser['id'], reason: string, action: string, appId: string): Promise { + return this.moderationBridge.doDismissReportsByUserId(userId, reason, action, appId); + } +} diff --git a/packages/apps/src/server/accessors/Modify.ts b/packages/apps/src/server/accessors/Modify.ts new file mode 100644 index 0000000000000..b16c44ce64b3f --- /dev/null +++ b/packages/apps/src/server/accessors/Modify.ts @@ -0,0 +1,92 @@ +import type { + IModify, + IModifyCreator, + IModifyDeleter, + IModifyExtender, + IModifyUpdater, + INotifier, + ISchedulerModify, + IUIController, +} from '@rocket.chat/apps-engine/definition/accessors'; +import type { IOAuthAppsModify } from '@rocket.chat/apps-engine/definition/accessors/IOAuthAppsModify'; +import type { AppBridges } from '../bridges'; +import { ModerationModify } from './ModerationModify'; +import { ModifyCreator } from './ModifyCreator'; +import { ModifyDeleter } from './ModifyDeleter'; +import { ModifyExtender } from './ModifyExtender'; +import { ModifyUpdater } from './ModifyUpdater'; +import { Notifier } from './Notifier'; +import { OAuthAppsModify } from './OAuthAppsModify'; +import { SchedulerModify } from './SchedulerModify'; +import { UIController } from './UIController'; + +export class Modify implements IModify { + private creator: IModifyCreator; + + private deleter: IModifyDeleter; + + private updater: IModifyUpdater; + + private extender: IModifyExtender; + + private notifier: INotifier; + + private uiController: IUIController; + + private scheduler: ISchedulerModify; + + private oauthApps: IOAuthAppsModify; + + private moderation: ModerationModify; + + constructor( + private readonly bridges: AppBridges, + private readonly appId: string, + ) { + this.creator = new ModifyCreator(this.bridges, this.appId); + this.deleter = new ModifyDeleter(this.bridges, this.appId); + this.updater = new ModifyUpdater(this.bridges, this.appId); + this.extender = new ModifyExtender(this.bridges, this.appId); + this.notifier = new Notifier(this.bridges.getUserBridge(), this.bridges.getMessageBridge(), this.appId); + this.uiController = new UIController(this.appId, this.bridges); + this.scheduler = new SchedulerModify(this.bridges.getSchedulerBridge(), this.appId); + this.oauthApps = new OAuthAppsModify(this.bridges.getOAuthAppsBridge(), this.appId); + this.moderation = new ModerationModify(this.bridges.getModerationBridge(), this.appId); + } + + public getCreator(): IModifyCreator { + return this.creator; + } + + public getDeleter(): IModifyDeleter { + return this.deleter; + } + + public getUpdater(): IModifyUpdater { + return this.updater; + } + + public getExtender(): IModifyExtender { + return this.extender; + } + + public getNotifier(): INotifier { + return this.notifier; + } + + public getUiController(): IUIController { + return this.uiController; + } + + public getScheduler(): ISchedulerModify { + return this.scheduler; + } + + public getOAuthAppsModifier() { + return this.oauthApps; + } + + public getModerationModifier() { + return this.moderation; + } +} diff --git a/packages/apps/src/server/accessors/ModifyCreator.ts b/packages/apps/src/server/accessors/ModifyCreator.ts new file mode 100644 index 0000000000000..03ff764d032d5 --- /dev/null +++ b/packages/apps/src/server/accessors/ModifyCreator.ts @@ -0,0 +1,274 @@ +import { ContactCreator } from './ContactCreator'; +import { DiscussionBuilder } from './DiscussionBuilder'; +import { EmailCreator } from './EmailCreator'; +import { LivechatCreator } from './LivechatCreator'; +import { LivechatMessageBuilder } from './LivechatMessageBuilder'; +import { MessageBuilder } from './MessageBuilder'; +import { RoomBuilder } from './RoomBuilder'; +import { UploadCreator } from './UploadCreator'; +import { UserBuilder } from './UserBuilder'; +import { VideoConferenceBuilder } from './VideoConferenceBuilder'; +import type { + IDiscussionBuilder, + ILivechatCreator, + ILivechatMessageBuilder, + IMessageBuilder, + IModifyCreator, + IRoomBuilder, + IUploadCreator, + IUserBuilder, + IVideoConferenceBuilder, +} from '@rocket.chat/apps-engine/definition/accessors'; +import type { IContactCreator } from '@rocket.chat/apps-engine/definition/accessors/IContactCreator'; +import type { IEmailCreator } from '@rocket.chat/apps-engine/definition/accessors/IEmailCreator'; +import type { ILivechatMessage } from '@rocket.chat/apps-engine/definition/livechat/ILivechatMessage'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import { BlockBuilder } from '@rocket.chat/apps-engine/definition/uikit'; +import type { IBotUser } from '@rocket.chat/apps-engine/definition/users/IBotUser'; +import { UserType } from '@rocket.chat/apps-engine/definition/users/UserType'; +import type { AppVideoConference } from '@rocket.chat/apps-engine/definition/videoConferences'; +import type { AppBridges } from '../bridges'; +import { UIHelper } from '../misc/UIHelper'; + +export class ModifyCreator implements IModifyCreator { + private livechatCreator: LivechatCreator; + + private uploadCreator: UploadCreator; + + private emailCreator: EmailCreator; + + private contactCreator: ContactCreator; + + constructor( + private readonly bridges: AppBridges, + private readonly appId: string, + ) { + this.livechatCreator = new LivechatCreator(bridges, appId); + this.uploadCreator = new UploadCreator(bridges, appId); + this.emailCreator = new EmailCreator(bridges, appId); + this.contactCreator = new ContactCreator(bridges, appId); + } + + public getLivechatCreator(): ILivechatCreator { + return this.livechatCreator; + } + + public getUploadCreator(): IUploadCreator { + return this.uploadCreator; + } + + public getEmailCreator(): IEmailCreator { + return this.emailCreator; + } + + public getContactCreator(): IContactCreator { + return this.contactCreator; + } + + /** + * @deprecated please prefer the rocket.chat/ui-kit components + */ + public getBlockBuilder(): BlockBuilder { + return new BlockBuilder(this.appId); + } + + public startMessage(data?: IMessage): IMessageBuilder { + if (data) { + delete data.id; + } + + return new MessageBuilder(data); + } + + public startLivechatMessage(data?: ILivechatMessage): ILivechatMessageBuilder { + if (data) { + delete data.id; + } + + return new LivechatMessageBuilder(data); + } + + public startRoom(data?: IRoom): IRoomBuilder { + if (data) { + delete data.id; + } + + return new RoomBuilder(data); + } + + public startDiscussion(data?: Partial): IDiscussionBuilder { + if (data) { + delete data.id; + } + + return new DiscussionBuilder(data); + } + + public startVideoConference(data?: Partial): IVideoConferenceBuilder { + return new VideoConferenceBuilder(data); + } + + public startBotUser(data?: Partial): IUserBuilder { + if (data) { + delete data.id; + + const { roles } = data; + + if (roles?.length) { + const hasRole = roles + .map((role) => role.toLocaleLowerCase()) + .some((role) => role === 'admin' || role === 'owner' || role === 'moderator'); + + if (hasRole) { + throw new Error('Invalid role assigned to the user. Should not be admin, owner or moderator.'); + } + } + + if (!data.type) { + data.type = UserType.BOT; + } + } + + return new UserBuilder(data); + } + + public finish( + builder: IMessageBuilder | ILivechatMessageBuilder | IRoomBuilder | IDiscussionBuilder | IVideoConferenceBuilder | IUserBuilder, + ): Promise { + switch (builder.kind) { + case RocketChatAssociationModel.MESSAGE: + return this._finishMessage(builder); + case RocketChatAssociationModel.LIVECHAT_MESSAGE: + return this._finishLivechatMessage(builder); + case RocketChatAssociationModel.ROOM: + return this._finishRoom(builder); + case RocketChatAssociationModel.DISCUSSION: + return this._finishDiscussion(builder as IDiscussionBuilder); + case RocketChatAssociationModel.VIDEO_CONFERENCE: + return this._finishVideoConference(builder); + case RocketChatAssociationModel.USER: + return this._finishUser(builder); + default: + throw new Error('Invalid builder passed to the ModifyCreator.finish function.'); + } + } + + private async _finishMessage(builder: IMessageBuilder): Promise { + const result = builder.getMessage(); + delete result.id; + + if (!result.sender?.id) { + const appUser = await this.bridges.getUserBridge().doGetAppUser(this.appId); + + if (!appUser) { + throw new Error('Invalid sender assigned to the message.'); + } + + result.sender = appUser; + } + + if (result.blocks?.length) { + result.blocks = UIHelper.assignIds(result.blocks, this.appId); + } + + return this.bridges.getMessageBridge().doCreate(result, this.appId); + } + + private _finishLivechatMessage(builder: ILivechatMessageBuilder): Promise { + if (builder.getSender() && !builder.getVisitor()) { + return this._finishMessage(builder.getMessageBuilder()); + } + + const result = builder.getMessage(); + delete result.id; + + if (!result.token && !result.visitor?.token) { + throw new Error('Invalid visitor sending the message'); + } + + result.token = result.visitor ? result.visitor.token : result.token; + + return this.bridges.getLivechatBridge().doCreateMessage(result, this.appId); + } + + private _finishRoom(builder: IRoomBuilder): Promise { + const result = builder.getRoom(); + delete result.id; + + if (!result.type) { + throw new Error('Invalid type assigned to the room.'); + } + + if (result.type !== RoomType.LIVE_CHAT) { + if (!result.creator?.id) { + throw new Error('Invalid creator assigned to the room.'); + } + } + + if (result.type !== RoomType.DIRECT_MESSAGE) { + if (result.type !== RoomType.LIVE_CHAT) { + if (!result.slugifiedName?.trim()) { + throw new Error('Invalid slugifiedName assigned to the room.'); + } + } + + if (!result.displayName?.trim()) { + throw new Error('Invalid displayName assigned to the room.'); + } + } + + return this.bridges.getRoomBridge().doCreate(result, builder.getMembersToBeAddedUsernames(), this.appId); + } + + private _finishDiscussion(builder: IDiscussionBuilder): Promise { + const room = builder.getRoom(); + delete room.id; + + if (!room.creator?.id) { + throw new Error('Invalid creator assigned to the discussion.'); + } + + if (!room.slugifiedName?.trim()) { + throw new Error('Invalid slugifiedName assigned to the discussion.'); + } + + if (!room.displayName?.trim()) { + throw new Error('Invalid displayName assigned to the discussion.'); + } + + if (!room.parentRoom?.id) { + throw new Error('Invalid parentRoom assigned to the discussion.'); + } + + return this.bridges + .getRoomBridge() + .doCreateDiscussion(room, builder.getParentMessage(), builder.getReply(), builder.getMembersToBeAddedUsernames(), this.appId); + } + + private _finishVideoConference(builder: IVideoConferenceBuilder): Promise { + const videoConference = builder.getVideoConference(); + + if (!videoConference.createdBy) { + throw new Error('Invalid creator assigned to the video conference.'); + } + + if (!videoConference.providerName?.trim()) { + throw new Error('Invalid provider name assigned to the video conference.'); + } + + if (!videoConference.rid) { + throw new Error('Invalid roomId assigned to the video conference.'); + } + + return this.bridges.getVideoConferenceBridge().doCreate(videoConference, this.appId); + } + + private _finishUser(builder: IUserBuilder): Promise { + const user = builder.getUser(); + + return this.bridges.getUserBridge().doCreate(user, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/ModifyDeleter.ts b/packages/apps/src/server/accessors/ModifyDeleter.ts new file mode 100644 index 0000000000000..a7dedc3d1eb99 --- /dev/null +++ b/packages/apps/src/server/accessors/ModifyDeleter.ts @@ -0,0 +1,38 @@ +import type { IModifyDeleter } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { IUser, UserType } from '@rocket.chat/apps-engine/definition/users'; +import type { AppBridges } from '../bridges'; + +export class ModifyDeleter implements IModifyDeleter { + constructor( + private readonly bridges: AppBridges, + private readonly appId: string, + ) {} + + public async deleteRoom(roomId: string): Promise { + return this.bridges.getRoomBridge().doDelete(roomId, this.appId); + } + + public async deleteUsers(appId: Exclude, userType: UserType.APP | UserType.BOT): Promise { + return this.bridges.getUserBridge().doDeleteUsersCreatedByApp(appId, userType); + } + + public async deleteMessage(message: IMessage, user: IUser): Promise { + return this.bridges.getMessageBridge().doDelete(message, user, this.appId); + } + + /** + * Removes `usernames` from the room's member list + * + * For performance reasons, it is only possible to remove 50 users in one + * call to this method. Removing users is an expensive operation due to the + * amount of entity relationships that need to be modified. + */ + public async removeUsersFromRoom(roomId: string, usernames: Array) { + if (usernames.length > 50) { + throw new Error('A maximum of 50 members can be removed in a single call'); + } + + return this.bridges.getRoomBridge().doRemoveUsers(roomId, usernames, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/ModifyExtender.ts b/packages/apps/src/server/accessors/ModifyExtender.ts new file mode 100644 index 0000000000000..0b324b3bdb1af --- /dev/null +++ b/packages/apps/src/server/accessors/ModifyExtender.ts @@ -0,0 +1,49 @@ +import { MessageExtender } from './MessageExtender'; +import { RoomExtender } from './RoomExtender'; +import { VideoConferenceExtender } from './VideoConferenceExtend'; +import type { IMessageExtender, IModifyExtender, IRoomExtender, IVideoConferenceExtender } from '@rocket.chat/apps-engine/definition/accessors'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import type { AppBridges } from '../bridges/AppBridges'; + +export class ModifyExtender implements IModifyExtender { + constructor( + private readonly bridges: AppBridges, + private readonly appId: string, + ) {} + + public async extendMessage(messageId: string, updater: IUser): Promise { + const msg = await this.bridges.getMessageBridge().doGetById(messageId, this.appId); + msg.editor = updater; + msg.editedAt = new Date(); + + return new MessageExtender(msg); + } + + public async extendRoom(roomId: string, updater: IUser): Promise { + const room = await this.bridges.getRoomBridge().doGetById(roomId, this.appId); + room.updatedAt = new Date(); + + return new RoomExtender(room); + } + + public async extendVideoConference(id: string): Promise { + const call = await this.bridges.getVideoConferenceBridge().doGetById(id, this.appId); + call._updatedAt = new Date(); + + return new VideoConferenceExtender(call); + } + + public finish(extender: IMessageExtender | IRoomExtender | IVideoConferenceExtender): Promise { + switch (extender.kind) { + case RocketChatAssociationModel.MESSAGE: + return this.bridges.getMessageBridge().doUpdate(extender.getMessage(), this.appId); + case RocketChatAssociationModel.ROOM: + return this.bridges.getRoomBridge().doUpdate(extender.getRoom(), extender.getUsernamesOfMembersBeingAdded(), this.appId); + case RocketChatAssociationModel.VIDEO_CONFERENCE: + return this.bridges.getVideoConferenceBridge().doUpdate(extender.getVideoConference(), this.appId); + default: + throw new Error('Invalid extender passed to the ModifyExtender.finish function.'); + } + } +} diff --git a/packages/apps/src/server/accessors/ModifyUpdater.ts b/packages/apps/src/server/accessors/ModifyUpdater.ts new file mode 100644 index 0000000000000..1133738b6e743 --- /dev/null +++ b/packages/apps/src/server/accessors/ModifyUpdater.ts @@ -0,0 +1,109 @@ +import { LivechatUpdater } from './LivechatUpdater'; +import { MessageBuilder } from './MessageBuilder'; +import { RoomBuilder } from './RoomBuilder'; +import { UserUpdater } from './UserUpdater'; +import type { ILivechatUpdater, IMessageBuilder, IMessageUpdater, IModifyUpdater, IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IUserUpdater } from '@rocket.chat/apps-engine/definition/accessors/IUserUpdater'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import type { AppBridges } from '../bridges'; +import { UIHelper } from '../misc/UIHelper'; + +export class ModifyUpdater implements IModifyUpdater { + private livechatUpdater: ILivechatUpdater; + + private userUpdater: IUserUpdater; + + private messageUpdater: IMessageUpdater; + + constructor( + private readonly bridges: AppBridges, + private readonly appId: string, + ) { + this.livechatUpdater = new LivechatUpdater(this.bridges, this.appId); + this.userUpdater = new UserUpdater(this.bridges, this.appId); + } + + public getLivechatUpdater(): ILivechatUpdater { + return this.livechatUpdater; + } + + public getUserUpdater(): IUserUpdater { + return this.userUpdater; + } + + public getMessageUpdater(): IMessageUpdater { + return this.messageUpdater; + } + + public async message(messageId: string, updater: IUser): Promise { + const msg = await this.bridges.getMessageBridge().doGetById(messageId, this.appId); + + return new MessageBuilder(msg); + } + + public async room(roomId: string, updater: IUser): Promise { + const room = await this.bridges.getRoomBridge().doGetById(roomId, this.appId); + + return new RoomBuilder(room); + } + + public finish(builder: IMessageBuilder | IRoomBuilder): Promise { + switch (builder.kind) { + case RocketChatAssociationModel.MESSAGE: + return this._finishMessage(builder); + case RocketChatAssociationModel.ROOM: + return this._finishRoom(builder); + default: + throw new Error('Invalid builder passed to the ModifyUpdater.finish function.'); + } + } + + private _finishMessage(builder: IMessageBuilder): Promise { + const result = builder.getMessage(); + + if (!result.id) { + throw new Error("Invalid message, can't update a message without an id."); + } + + if (!result.sender?.id) { + throw new Error('Invalid sender assigned to the message.'); + } + + if (result.blocks?.length) { + result.blocks = UIHelper.assignIds(result.blocks, this.appId); + // result.blocks = this._assignIds(result.blocks); + } + + return this.bridges.getMessageBridge().doUpdate(result, this.appId); + } + + private _finishRoom(builder: IRoomBuilder): Promise { + const result = builder.getRoom(); + + if (!result.id) { + throw new Error('Invalid room, can not update a room without an id.'); + } + + if (!result.type) { + throw new Error('Invalid type assigned to the room.'); + } + + if (result.type !== RoomType.LIVE_CHAT) { + if (!result.creator?.id) { + throw new Error('Invalid creator assigned to the room.'); + } + + if (!result.slugifiedName?.trim()) { + throw new Error('Invalid slugifiedName assigned to the room.'); + } + } + + if (!result.displayName?.trim()) { + throw new Error('Invalid displayName assigned to the room.'); + } + + return this.bridges.getRoomBridge().doUpdate(result, builder.getMembersToBeAddedUsernames(), this.appId); + } +} diff --git a/packages/apps/src/server/accessors/Notifier.ts b/packages/apps/src/server/accessors/Notifier.ts new file mode 100644 index 0000000000000..142fde36dcf82 --- /dev/null +++ b/packages/apps/src/server/accessors/Notifier.ts @@ -0,0 +1,53 @@ +import type { IMessageBuilder, INotifier } from '@rocket.chat/apps-engine/definition/accessors'; +import type { ITypingOptions } from '@rocket.chat/apps-engine/definition/accessors/INotifier'; +import { TypingScope } from '@rocket.chat/apps-engine/definition/accessors/INotifier'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import type { MessageBridge, UserBridge } from '../bridges'; +import { MessageBuilder } from './MessageBuilder'; + +export class Notifier implements INotifier { + constructor( + private readonly userBridge: UserBridge, + private readonly msgBridge: MessageBridge, + private readonly appId: string, + ) {} + + public async notifyUser(user: IUser, message: IMessage): Promise { + if (!message.sender?.id) { + const appUser = await this.userBridge.doGetAppUser(this.appId); + + message.sender = appUser; + } + + await this.msgBridge.doNotifyUser(user, message, this.appId); + } + + public async notifyRoom(room: IRoom, message: IMessage): Promise { + if (!message.sender?.id) { + const appUser = await this.userBridge.doGetAppUser(this.appId); + + message.sender = appUser; + } + + await this.msgBridge.doNotifyRoom(room, message, this.appId); + } + + public async typing(options: ITypingOptions): Promise<() => Promise> { + options.scope = options.scope || TypingScope.Room; + + if (!options.username) { + const appUser = await this.userBridge.doGetAppUser(this.appId); + options.username = appUser?.name || ''; + } + + this.msgBridge.doTyping({ ...options, isTyping: true }, this.appId); + + return () => this.msgBridge.doTyping({ ...options, isTyping: false }, this.appId); + } + + public getMessageBuilder(): IMessageBuilder { + return new MessageBuilder(); + } +} diff --git a/packages/apps/src/server/accessors/OAuthAppsModify.ts b/packages/apps/src/server/accessors/OAuthAppsModify.ts new file mode 100644 index 0000000000000..7c78ea45e75c0 --- /dev/null +++ b/packages/apps/src/server/accessors/OAuthAppsModify.ts @@ -0,0 +1,22 @@ +import type { IOAuthAppParams } from '@rocket.chat/apps-engine/definition/accessors/IOAuthApp'; +import type { IOAuthAppsModify } from '@rocket.chat/apps-engine/definition/accessors/IOAuthAppsModify'; +import type { OAuthAppsBridge } from '../bridges/OAuthAppsBridge'; + +export class OAuthAppsModify implements IOAuthAppsModify { + constructor( + private readonly oauthAppsBridge: OAuthAppsBridge, + private readonly appId: string, + ) {} + + public async createOAuthApp(oAuthApp: IOAuthAppParams): Promise { + return this.oauthAppsBridge.doCreate(oAuthApp, this.appId); + } + + public async updateOAuthApp(oAuthApp: IOAuthAppParams, id: string): Promise { + return this.oauthAppsBridge.doUpdate(oAuthApp, id, this.appId); + } + + public async deleteOAuthApp(id: string): Promise { + return this.oauthAppsBridge.doDelete(id, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/OAuthAppsReader.ts b/packages/apps/src/server/accessors/OAuthAppsReader.ts new file mode 100644 index 0000000000000..18c375e168054 --- /dev/null +++ b/packages/apps/src/server/accessors/OAuthAppsReader.ts @@ -0,0 +1,18 @@ +import type { IOAuthApp } from '@rocket.chat/apps-engine/definition/accessors/IOAuthApp'; +import type { IOAuthAppsReader } from '@rocket.chat/apps-engine/definition/accessors/IOAuthAppsReader'; +import type { OAuthAppsBridge } from '../bridges/OAuthAppsBridge'; + +export class OAuthAppsReader implements IOAuthAppsReader { + constructor( + private readonly oauthAppsBridge: OAuthAppsBridge, + private readonly appId: string, + ) {} + + public async getOAuthAppById(id: string): Promise { + return this.oauthAppsBridge.doGetByid(id, this.appId); + } + + public async getOAuthAppByName(name: string): Promise> { + return this.oauthAppsBridge.doGetByName(name, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/OutboundCommunicationProviderExtend.ts b/packages/apps/src/server/accessors/OutboundCommunicationProviderExtend.ts new file mode 100644 index 0000000000000..9bcc2147eb885 --- /dev/null +++ b/packages/apps/src/server/accessors/OutboundCommunicationProviderExtend.ts @@ -0,0 +1,18 @@ +import type { IOutboundCommunicationProviderExtend } from '@rocket.chat/apps-engine/definition/accessors/IOutboundCommunicationProviderExtend'; +import type { IOutboundPhoneMessageProvider, IOutboundEmailMessageProvider } from '@rocket.chat/apps-engine/definition/outboundCommunication'; +import type { AppOutboundCommunicationProviderManager } from '../managers/AppOutboundCommunicationProviderManager'; + +export class OutboundMessageProviderExtend implements IOutboundCommunicationProviderExtend { + constructor( + private readonly manager: AppOutboundCommunicationProviderManager, + private readonly appId: string, + ) {} + + public registerPhoneProvider(provider: IOutboundPhoneMessageProvider): Promise { + return Promise.resolve(this.manager.addProvider(this.appId, provider)); + } + + public registerEmailProvider(provider: IOutboundEmailMessageProvider): Promise { + return Promise.resolve(this.manager.addProvider(this.appId, provider)); + } +} diff --git a/packages/apps/src/server/accessors/Persistence.ts b/packages/apps/src/server/accessors/Persistence.ts new file mode 100644 index 0000000000000..6106cf16c7fb2 --- /dev/null +++ b/packages/apps/src/server/accessors/Persistence.ts @@ -0,0 +1,46 @@ +import type { IPersistence } from '@rocket.chat/apps-engine/definition/accessors'; +import type { RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata'; +import type { PersistenceBridge } from '../bridges/PersistenceBridge'; + +export class Persistence implements IPersistence { + constructor( + private persistBridge: PersistenceBridge, + private appId: string, + ) {} + + public create(data: object): Promise { + return this.persistBridge.doCreate(data, this.appId); + } + + public createWithAssociation(data: object, association: RocketChatAssociationRecord): Promise { + return this.persistBridge.doCreateWithAssociations(data, new Array(association), this.appId); + } + + public createWithAssociations(data: object, associations: Array): Promise { + return this.persistBridge.doCreateWithAssociations(data, associations, this.appId); + } + + public update(id: string, data: object, upsert = false): Promise { + return this.persistBridge.doUpdate(id, data, upsert, this.appId); + } + + public updateByAssociation(association: RocketChatAssociationRecord, data: object, upsert = false): Promise { + return this.persistBridge.doUpdateByAssociations(new Array(association), data, upsert, this.appId); + } + + public updateByAssociations(associations: Array, data: object, upsert = false): Promise { + return this.persistBridge.doUpdateByAssociations(associations, data, upsert, this.appId); + } + + public remove(id: string): Promise { + return this.persistBridge.doRemove(id, this.appId); + } + + public removeByAssociation(association: RocketChatAssociationRecord): Promise> { + return this.persistBridge.doRemoveByAssociations(new Array(association), this.appId); + } + + public removeByAssociations(associations: Array): Promise> { + return this.persistBridge.doRemoveByAssociations(associations, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/PersistenceRead.ts b/packages/apps/src/server/accessors/PersistenceRead.ts new file mode 100644 index 0000000000000..d342849a40d23 --- /dev/null +++ b/packages/apps/src/server/accessors/PersistenceRead.ts @@ -0,0 +1,22 @@ +import type { IPersistenceRead } from '@rocket.chat/apps-engine/definition/accessors'; +import type { RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata'; +import type { PersistenceBridge } from '../bridges'; + +export class PersistenceRead implements IPersistenceRead { + constructor( + private persistBridge: PersistenceBridge, + private appId: string, + ) {} + + public read(id: string): Promise { + return this.persistBridge.doReadById(id, this.appId); + } + + public readByAssociation(association: RocketChatAssociationRecord): Promise> { + return this.persistBridge.doReadByAssociations(new Array(association), this.appId); + } + + public readByAssociations(associations: Array): Promise> { + return this.persistBridge.doReadByAssociations(associations, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/Reader.ts b/packages/apps/src/server/accessors/Reader.ts new file mode 100644 index 0000000000000..2f73452b1af85 --- /dev/null +++ b/packages/apps/src/server/accessors/Reader.ts @@ -0,0 +1,98 @@ +import type { + ICloudWorkspaceRead, + IEnvironmentRead, + IExperimentalRead, + ILivechatRead, + IMessageRead, + INotifier, + IPersistenceRead, + IRead, + IRoomRead, + IUploadRead, + IUserRead, + IVideoConferenceRead, +} from '@rocket.chat/apps-engine/definition/accessors'; +import type { IContactRead } from '@rocket.chat/apps-engine/definition/accessors/IContactRead'; +import type { IOAuthAppsReader } from '@rocket.chat/apps-engine/definition/accessors/IOAuthAppsReader'; +import type { IRoleRead } from '@rocket.chat/apps-engine/definition/accessors/IRoleRead'; +import type { IThreadRead } from '@rocket.chat/apps-engine/definition/accessors/IThreadRead'; + +export class Reader implements IRead { + constructor( + private env: IEnvironmentRead, + private message: IMessageRead, + private persist: IPersistenceRead, + private room: IRoomRead, + private user: IUserRead, + private noti: INotifier, + private livechat: ILivechatRead, + private upload: IUploadRead, + private cloud: ICloudWorkspaceRead, + private videoConf: IVideoConferenceRead, + private contactRead: IContactRead, + private oauthApps: IOAuthAppsReader, + private thread: IThreadRead, + private role: IRoleRead, + private experimental: IExperimentalRead, + ) {} + + public getEnvironmentReader(): IEnvironmentRead { + return this.env; + } + + public getThreadReader(): IThreadRead { + return this.thread; + } + + public getMessageReader(): IMessageRead { + return this.message; + } + + public getPersistenceReader(): IPersistenceRead { + return this.persist; + } + + public getRoomReader(): IRoomRead { + return this.room; + } + + public getUserReader(): IUserRead { + return this.user; + } + + public getNotifier(): INotifier { + return this.noti; + } + + public getLivechatReader(): ILivechatRead { + return this.livechat; + } + + public getUploadReader(): IUploadRead { + return this.upload; + } + + public getCloudWorkspaceReader(): ICloudWorkspaceRead { + return this.cloud; + } + + public getVideoConferenceReader(): IVideoConferenceRead { + return this.videoConf; + } + + public getOAuthAppsReader(): IOAuthAppsReader { + return this.oauthApps; + } + + public getRoleReader(): IRoleRead { + return this.role; + } + + public getContactReader(): IContactRead { + return this.contactRead; + } + + public getExperimentalReader(): IExperimentalRead { + return this.experimental; + } +} diff --git a/packages/apps/src/server/accessors/RoleRead.ts b/packages/apps/src/server/accessors/RoleRead.ts new file mode 100644 index 0000000000000..21994806f37ba --- /dev/null +++ b/packages/apps/src/server/accessors/RoleRead.ts @@ -0,0 +1,18 @@ +import type { IRoleRead } from '@rocket.chat/apps-engine/definition/accessors/IRoleRead'; +import type { IRole } from '@rocket.chat/apps-engine/definition/roles'; +import type { RoleBridge } from '../bridges'; + +export class RoleRead implements IRoleRead { + constructor( + private roleBridge: RoleBridge, + private appId: string, + ) {} + + public getOneByIdOrName(idOrName: IRole['id'] | IRole['name']): Promise { + return this.roleBridge.doGetOneByIdOrName(idOrName, this.appId); + } + + public getCustomRoles(): Promise> { + return this.roleBridge.doGetCustomRoles(this.appId); + } +} diff --git a/packages/apps/src/server/accessors/RoomBuilder.ts b/packages/apps/src/server/accessors/RoomBuilder.ts new file mode 100644 index 0000000000000..aa1e4c31d43b7 --- /dev/null +++ b/packages/apps/src/server/accessors/RoomBuilder.ts @@ -0,0 +1,155 @@ +import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata'; +import type { IRoom, RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; + +export class RoomBuilder implements IRoomBuilder { + public kind: RocketChatAssociationModel.ROOM | RocketChatAssociationModel.DISCUSSION; + + protected room: IRoom; + + private members: Array; + + constructor(data?: Partial) { + this.kind = RocketChatAssociationModel.ROOM; + this.room = (data || { customFields: {} }) as IRoom; + this.members = []; + } + + public setData(data: Partial): IRoomBuilder { + delete data.id; + this.room = data as IRoom; + + return this; + } + + public setDisplayName(name: string): IRoomBuilder { + this.room.displayName = name; + return this; + } + + public getDisplayName(): string { + return this.room.displayName; + } + + public setSlugifiedName(name: string): IRoomBuilder { + this.room.slugifiedName = name; + return this; + } + + public getSlugifiedName(): string { + return this.room.slugifiedName; + } + + public setType(type: RoomType): IRoomBuilder { + this.room.type = type; + return this; + } + + public getType(): RoomType { + return this.room.type; + } + + public setCreator(creator: IUser): IRoomBuilder { + this.room.creator = creator; + return this; + } + + public getCreator(): IUser { + return this.room.creator; + } + + /** + * @deprecated + */ + public addUsername(username: string): IRoomBuilder { + this.addMemberToBeAddedByUsername(username); + return this; + } + + /** + * @deprecated + */ + public setUsernames(usernames: Array): IRoomBuilder { + this.setMembersToBeAddedByUsernames(usernames); + return this; + } + + /** + * @deprecated + */ + public getUsernames(): Array { + const usernames = this.getMembersToBeAddedUsernames(); + if (usernames && usernames.length > 0) { + return usernames; + } + return this.room.usernames || []; + } + + public addMemberToBeAddedByUsername(username: string): IRoomBuilder { + this.members.push(username); + return this; + } + + public setMembersToBeAddedByUsernames(usernames: Array): IRoomBuilder { + this.members = usernames; + return this; + } + + public getMembersToBeAddedUsernames(): Array { + return this.members; + } + + public setDefault(isDefault: boolean): IRoomBuilder { + this.room.isDefault = isDefault; + return this; + } + + public getIsDefault(): boolean { + return this.room.isDefault; + } + + public setReadOnly(isReadOnly: boolean): IRoomBuilder { + this.room.isReadOnly = isReadOnly; + return this; + } + + public getIsReadOnly(): boolean { + return this.room.isReadOnly; + } + + public setDisplayingOfSystemMessages(displaySystemMessages: boolean): IRoomBuilder { + this.room.displaySystemMessages = displaySystemMessages; + return this; + } + + public getDisplayingOfSystemMessages(): boolean { + return this.room.displaySystemMessages; + } + + public addCustomField(key: string, value: object): IRoomBuilder { + if (typeof this.room.customFields !== 'object') { + this.room.customFields = {}; + } + + this.room.customFields[key] = value; + return this; + } + + public setCustomFields(fields: { [key: string]: object }): IRoomBuilder { + this.room.customFields = fields; + return this; + } + + public getCustomFields(): { [key: string]: object } { + return this.room.customFields; + } + + public getUserIds(): Array { + return this.room.userIds; + } + + public getRoom(): IRoom { + return this.room; + } +} diff --git a/packages/apps/src/server/accessors/RoomExtender.ts b/packages/apps/src/server/accessors/RoomExtender.ts new file mode 100644 index 0000000000000..c33ac6e536788 --- /dev/null +++ b/packages/apps/src/server/accessors/RoomExtender.ts @@ -0,0 +1,56 @@ +import type { IRoomExtender } from '@rocket.chat/apps-engine/definition/accessors'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import { Utilities } from '../misc/Utilities'; + +export class RoomExtender implements IRoomExtender { + public kind: RocketChatAssociationModel.ROOM; + + private members: Array; + + constructor(private room: IRoom) { + this.kind = RocketChatAssociationModel.ROOM; + this.members = []; + } + + public addCustomField(key: string, value: any): IRoomExtender { + if (!this.room.customFields) { + this.room.customFields = {}; + } + + if (this.room.customFields[key]) { + throw new Error(`The room already contains a custom field by the key: ${key}`); + } + + if (key.includes('.')) { + throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`); + } + + this.room.customFields[key] = value; + + return this; + } + + public addMember(user: IUser): IRoomExtender { + if (this.members.find((u) => u.username === user.username)) { + throw new Error('The user is already in the room.'); + } + + this.members.push(user); + + return this; + } + + public getMembersBeingAdded(): Array { + return this.members; + } + + public getUsernamesOfMembersBeingAdded(): Array { + return this.members.map((u) => u.username); + } + + public getRoom(): IRoom { + return Utilities.deepClone(this.room); + } +} diff --git a/packages/apps/src/server/accessors/RoomRead.ts b/packages/apps/src/server/accessors/RoomRead.ts new file mode 100644 index 0000000000000..858a599befc2b --- /dev/null +++ b/packages/apps/src/server/accessors/RoomRead.ts @@ -0,0 +1,115 @@ +import type { IRoomRead } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IMessageRaw } from '@rocket.chat/apps-engine/definition/messages'; +import type { IRoom, IRoomRaw } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import type { RoomBridge } from '../bridges'; +import { type GetMessagesOptions, type GetRoomsFilters, type GetRoomsOptions, GetMessagesSortableFields } from '../bridges/RoomBridge'; + +export class RoomRead implements IRoomRead { + constructor( + private roomBridge: RoomBridge, + private appId: string, + ) {} + + public getById(id: string): Promise { + return this.roomBridge.doGetById(id, this.appId); + } + + public getCreatorUserById(id: string): Promise { + return this.roomBridge.doGetCreatorById(id, this.appId); + } + + public getByName(name: string): Promise { + return this.roomBridge.doGetByName(name, this.appId); + } + + public getCreatorUserByName(name: string): Promise { + return this.roomBridge.doGetCreatorByName(name, this.appId); + } + + public getMessages(roomId: string, options: Partial = {}): Promise { + if (typeof options.limit !== 'undefined' && (!Number.isFinite(options.limit) || options.limit > 100)) { + throw new Error(`Invalid limit provided. Expected number <= 100, got ${options.limit}`); + } + + options.limit ??= 100; + options.showThreadMessages ??= true; + + if (options.sort) { + this.validateSort(options.sort); + } + + return this.roomBridge.doGetMessages(roomId, options as GetMessagesOptions, this.appId); + } + + public getMembers(roomId: string): Promise> { + return this.roomBridge.doGetMembers(roomId, this.appId); + } + + public getAllRooms(filters: GetRoomsFilters = {}, { limit = 100, skip = 0 }: GetRoomsOptions = {}): Promise | undefined> { + if (!Number.isFinite(limit) || limit <= 0 || limit > 100) { + throw new Error(`Invalid limit provided. Expected number between 1 and 100, got ${limit}`); + } + + if (!Number.isFinite(skip) || skip < 0) { + throw new Error(`Invalid skip provided. Expected number >= 0, got ${skip}`); + } + + return this.roomBridge.doGetAllRooms( + filters, + { limit, skip }, + this.appId, + ); + } + + public getDirectByUsernames(usernames: Array): Promise { + return this.roomBridge.doGetDirectByUsernames(usernames, this.appId); + } + + public getModerators(roomId: string): Promise> { + return this.roomBridge.doGetModerators(roomId, this.appId); + } + + public getOwners(roomId: string): Promise> { + return this.roomBridge.doGetOwners(roomId, this.appId); + } + + public getLeaders(roomId: string): Promise> { + return this.roomBridge.doGetLeaders(roomId, this.appId); + } + + public async getUnreadByUser(roomId: string, uid: string, options: Partial = {}): Promise { + const { limit = 100, sort = { createdAt: 'asc' }, skip = 0, showThreadMessages = true } = options; + + if (typeof roomId !== 'string' || roomId.trim().length === 0) { + throw new Error('Invalid roomId: must be a non-empty string'); + } + + if (!Number.isFinite(limit) || limit <= 0 || limit > 100) { + throw new Error(`Invalid limit provided. Expected number between 1 and 100, got ${limit}`); + } + + this.validateSort(sort); + + const completeOptions: GetMessagesOptions = { limit, sort, skip, showThreadMessages }; + + return this.roomBridge.doGetUnreadByUser(roomId, uid, completeOptions, this.appId); + } + + public getUserUnreadMessageCount(roomId: string, uid: string): Promise { + return this.roomBridge.doGetUserUnreadMessageCount(roomId, uid, this.appId); + } + + // If there are any invalid fields or values, throw + private validateSort(sort: Record) { + Object.entries(sort).forEach(([key, value]) => { + if (!GetMessagesSortableFields.includes(key as (typeof GetMessagesSortableFields)[number])) { + throw new Error(`Invalid key "${key}" used in sort. Available keys for sorting are ${GetMessagesSortableFields.join(', ')}`); + } + + if (value !== 'asc' && value !== 'desc') { + throw new Error(`Invalid sort direction for field "${key}". Expected "asc" or "desc", got ${value}`); + } + }); + } +} diff --git a/packages/apps/src/server/accessors/SchedulerExtend.ts b/packages/apps/src/server/accessors/SchedulerExtend.ts new file mode 100644 index 0000000000000..612c7a9e149c5 --- /dev/null +++ b/packages/apps/src/server/accessors/SchedulerExtend.ts @@ -0,0 +1,14 @@ +import type { ISchedulerExtend } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IProcessor } from '@rocket.chat/apps-engine/definition/scheduler'; +import type { AppSchedulerManager } from '../managers/AppSchedulerManager'; + +export class SchedulerExtend implements ISchedulerExtend { + constructor( + private readonly manager: AppSchedulerManager, + private readonly appId: string, + ) {} + + public async registerProcessors(processors: Array = []): Promise> { + return this.manager.registerProcessors(processors, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/SchedulerModify.ts b/packages/apps/src/server/accessors/SchedulerModify.ts new file mode 100644 index 0000000000000..b74a6c0aac70a --- /dev/null +++ b/packages/apps/src/server/accessors/SchedulerModify.ts @@ -0,0 +1,30 @@ +import type { ISchedulerModify } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IOnetimeSchedule, IRecurringSchedule } from '@rocket.chat/apps-engine/definition/scheduler'; +import type { SchedulerBridge } from '../bridges'; + +function createProcessorId(jobId: string, appId: string): string { + return jobId.includes(`_${appId}`) ? jobId : `${jobId}_${appId}`; +} + +export class SchedulerModify implements ISchedulerModify { + constructor( + private readonly bridge: SchedulerBridge, + private readonly appId: string, + ) {} + + public async scheduleOnce(job: IOnetimeSchedule): Promise { + return this.bridge.doScheduleOnce({ ...job, id: createProcessorId(job.id, this.appId) }, this.appId); + } + + public async scheduleRecurring(job: IRecurringSchedule): Promise { + return this.bridge.doScheduleRecurring({ ...job, id: createProcessorId(job.id, this.appId) }, this.appId); + } + + public async cancelJob(jobId: string): Promise { + return this.bridge.doCancelJob(createProcessorId(jobId, this.appId), this.appId); + } + + public async cancelAllJobs(): Promise { + return this.bridge.doCancelAllJobs(this.appId); + } +} diff --git a/packages/apps/src/server/accessors/ServerSettingRead.ts b/packages/apps/src/server/accessors/ServerSettingRead.ts new file mode 100644 index 0000000000000..ace69a7bf7893 --- /dev/null +++ b/packages/apps/src/server/accessors/ServerSettingRead.ts @@ -0,0 +1,37 @@ +import type { IServerSettingRead } from '@rocket.chat/apps-engine/definition/accessors'; +import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; +import type { ServerSettingBridge } from '../bridges/ServerSettingBridge'; + +export class ServerSettingRead implements IServerSettingRead { + constructor( + private readonly settingBridge: ServerSettingBridge, + private readonly appId: string, + ) {} + + public getOneById(id: string): Promise { + return this.settingBridge.doGetOneById(id, this.appId); + } + + public async getValueById(id: string): Promise { + const set = await this.settingBridge.doGetOneById(id, this.appId); + + if (typeof set === 'undefined') { + throw new Error(`No Server Setting found, or it is unaccessible, by the id of "${id}".`); + } + + if (set.value === undefined || set.value === null) { + return set.packageValue; + } + + return set.value; + } + + public getAll(): Promise> { + throw new Error('Method not implemented.'); + // return this.settingBridge.getAll(this.appId); + } + + public isReadableById(id: string): Promise { + return this.settingBridge.doIsReadableById(id, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/ServerSettingUpdater.ts b/packages/apps/src/server/accessors/ServerSettingUpdater.ts new file mode 100644 index 0000000000000..e78c2fef880b3 --- /dev/null +++ b/packages/apps/src/server/accessors/ServerSettingUpdater.ts @@ -0,0 +1,18 @@ +import type { IServerSettingUpdater } from '@rocket.chat/apps-engine/definition/accessors'; +import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; +import type { AppBridges } from '../bridges'; + +export class ServerSettingUpdater implements IServerSettingUpdater { + constructor( + private readonly bridges: AppBridges, + private readonly appId: string, + ) {} + + public async updateOne(setting: ISetting): Promise { + await this.bridges.getServerSettingBridge().doUpdateOne(setting, this.appId); + } + + public async incrementValue(id: ISetting['id'], value = 1): Promise { + await this.bridges.getServerSettingBridge().doIncrementValue(id, value, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/ServerSettingsModify.ts b/packages/apps/src/server/accessors/ServerSettingsModify.ts new file mode 100644 index 0000000000000..e14b3b9390320 --- /dev/null +++ b/packages/apps/src/server/accessors/ServerSettingsModify.ts @@ -0,0 +1,26 @@ +import type { IServerSettingsModify } from '@rocket.chat/apps-engine/definition/accessors'; +import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; +import type { ServerSettingBridge } from '../bridges/ServerSettingBridge'; + +export class ServerSettingsModify implements IServerSettingsModify { + constructor( + private readonly bridge: ServerSettingBridge, + private readonly appId: string, + ) {} + + public async hideGroup(name: string): Promise { + await this.bridge.doHideGroup(name, this.appId); + } + + public async hideSetting(id: string): Promise { + await this.bridge.doHideSetting(id, this.appId); + } + + public async modifySetting(setting: ISetting): Promise { + await this.bridge.doUpdateOne(setting, this.appId); + } + + public async incrementValue(id: ISetting['id'], value = 1): Promise { + await this.bridge.doIncrementValue(id, value, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/SettingRead.ts b/packages/apps/src/server/accessors/SettingRead.ts new file mode 100644 index 0000000000000..fee7137cff82f --- /dev/null +++ b/packages/apps/src/server/accessors/SettingRead.ts @@ -0,0 +1,25 @@ +import type { ISettingRead } from '@rocket.chat/apps-engine/definition/accessors'; +import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; +import type { ProxiedApp } from '../ProxiedApp'; + +export class SettingRead implements ISettingRead { + constructor(private readonly app: ProxiedApp) {} + + public getById(id: string): Promise { + return Promise.resolve(this.app.getStorageItem().settings[id]); + } + + public async getValueById(id: string): Promise { + const set = await this.getById(id); + + if (typeof set === 'undefined') { + throw new Error(`Setting "${id}" does not exist.`); + } + + if (set.value === undefined || set.value === null) { + return set.packageValue; + } + + return set.value; + } +} diff --git a/packages/apps/src/server/accessors/SettingUpdater.ts b/packages/apps/src/server/accessors/SettingUpdater.ts new file mode 100644 index 0000000000000..315980508a4ff --- /dev/null +++ b/packages/apps/src/server/accessors/SettingUpdater.ts @@ -0,0 +1,66 @@ +import type { ISettingUpdater } from '@rocket.chat/apps-engine/definition/accessors/ISettingUpdater'; +import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; +import type { ProxiedApp } from '../ProxiedApp'; +import type { AppSettingsManager } from '../managers'; + +/** + * Implementation of ISettingUpdater that provides methods to update app settings. + */ +export class SettingUpdater implements ISettingUpdater { + constructor( + private readonly app: ProxiedApp, + private readonly manager: AppSettingsManager, + ) {} + + /** + * Updates a single setting value + * @param id The setting ID to update + * @param value The new value to set + * @returns Promise that resolves when the update is complete + * @throws Error if the setting doesn't exist + */ + public async updateValue(id: ISetting['id'], value: ISetting['value']): Promise { + const appId = this.app.getID(); + const storageItem = this.app.getStorageItem(); + + if (!storageItem.settings?.[id]) { + throw new Error(`Setting "${id}" not found for app ${appId}`); + } + + const setting = this.manager.getAppSetting(appId, id); + + this.manager.updateAppSetting(appId, { + ...setting, + updatedAt: new Date(), + value, + }); + } + + /** + * Updates the values for a multi-value setting by overwriting them + * @param id The setting ID to update + * @param values The new values to set + * @returns Promise that resolves when the update is complete + * @throws Error if the setting doesn't exist + */ + public async updateSelectOptions(id: ISetting['id'], values: ISetting['values']): Promise { + const appId = this.app.getID(); + const storageItem = this.app.getStorageItem(); + + if (!storageItem.settings?.[id]) { + throw new Error(`Setting "${id}" not found for app ${appId}`); + } + + const setting = this.manager.getAppSetting(appId, id); + + // TODO: This operation completely overwrites existing values + // which could lead to loss of selected values. Consider: + // Adding warning logs when selected value will be removed + + this.manager.updateAppSetting(appId, { + ...setting, + updatedAt: new Date(), + values, // Overwrite the values instead of merging + }); + } +} diff --git a/packages/apps/src/server/accessors/SettingsExtend.ts b/packages/apps/src/server/accessors/SettingsExtend.ts new file mode 100644 index 0000000000000..8ec83593efc8c --- /dev/null +++ b/packages/apps/src/server/accessors/SettingsExtend.ts @@ -0,0 +1,26 @@ +import type { ISettingsExtend } from '@rocket.chat/apps-engine/definition/accessors'; +import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; +import type { ProxiedApp } from '../ProxiedApp'; + +export class SettingsExtend implements ISettingsExtend { + constructor(private readonly app: ProxiedApp) {} + + public async provideSetting(setting: ISetting): Promise { + if (this.app.getStorageItem().settings[setting.id]) { + // :see_no_evil: + const old = await Promise.resolve(this.app.getStorageItem().settings[setting.id]); + + setting.createdAt = old.createdAt; + setting.updatedAt = new Date(); + setting.value = old.value; + + this.app.getStorageItem().settings[setting.id] = setting; + + return; + } + + setting.createdAt = new Date(); + setting.updatedAt = new Date(); + this.app.getStorageItem().settings[setting.id] = setting; + } +} diff --git a/packages/apps/src/server/accessors/SlashCommandsExtend.ts b/packages/apps/src/server/accessors/SlashCommandsExtend.ts new file mode 100644 index 0000000000000..567fa24bf91e0 --- /dev/null +++ b/packages/apps/src/server/accessors/SlashCommandsExtend.ts @@ -0,0 +1,14 @@ +import type { ISlashCommandsExtend } from '@rocket.chat/apps-engine/definition/accessors'; +import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands'; +import type { AppSlashCommandManager } from '../managers/AppSlashCommandManager'; + +export class SlashCommandsExtend implements ISlashCommandsExtend { + constructor( + private readonly manager: AppSlashCommandManager, + private readonly appId: string, + ) {} + + public async provideSlashCommand(slashCommand: ISlashCommand): Promise { + await this.manager.addCommand(this.appId, slashCommand); + } +} diff --git a/packages/apps/src/server/accessors/SlashCommandsModify.ts b/packages/apps/src/server/accessors/SlashCommandsModify.ts new file mode 100644 index 0000000000000..76c81eac940a2 --- /dev/null +++ b/packages/apps/src/server/accessors/SlashCommandsModify.ts @@ -0,0 +1,22 @@ +import type { ISlashCommandsModify } from '@rocket.chat/apps-engine/definition/accessors'; +import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands'; +import type { AppSlashCommandManager } from '../managers'; + +export class SlashCommandsModify implements ISlashCommandsModify { + constructor( + private readonly manager: AppSlashCommandManager, + private readonly appId: string, + ) {} + + public modifySlashCommand(slashCommand: ISlashCommand): Promise { + return Promise.resolve(this.manager.modifyCommand(this.appId, slashCommand)); + } + + public disableSlashCommand(command: string): Promise { + return Promise.resolve(this.manager.disableCommand(this.appId, command)); + } + + public enableSlashCommand(command: string): Promise { + return Promise.resolve(this.manager.enableCommand(this.appId, command)); + } +} diff --git a/packages/apps/src/server/accessors/ThreadRead.ts b/packages/apps/src/server/accessors/ThreadRead.ts new file mode 100644 index 0000000000000..41d13db78779b --- /dev/null +++ b/packages/apps/src/server/accessors/ThreadRead.ts @@ -0,0 +1,14 @@ +import type { IThreadRead } from '@rocket.chat/apps-engine/definition/accessors/IThreadRead'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { ThreadBridge } from '../bridges/ThreadBridge'; + +export class ThreadRead implements IThreadRead { + constructor( + private threadBridge: ThreadBridge, + private appId: string, + ) {} + + public getThreadById(id: string): Promise> { + return this.threadBridge.doGetById(id, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/UIController.ts b/packages/apps/src/server/accessors/UIController.ts new file mode 100644 index 0000000000000..97d4f1d99b63d --- /dev/null +++ b/packages/apps/src/server/accessors/UIController.ts @@ -0,0 +1,118 @@ +import type { IUIController } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IUIKitErrorInteractionParam, IUIKitInteractionParam, IUIKitSurfaceViewParam } from '@rocket.chat/apps-engine/definition/accessors/IUIController'; +import { UIKitInteractionType, UIKitSurfaceType } from '@rocket.chat/apps-engine/definition/uikit'; +import { + formatContextualBarInteraction, + formatErrorInteraction, + formatModalInteraction, +} from '@rocket.chat/apps-engine/definition/uikit/UIKitInteractionPayloadFormatter'; +import type { IUIKitContextualBarViewParam, IUIKitModalViewParam } from '@rocket.chat/apps-engine/definition/uikit/UIKitInteractionResponder'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import type { AppBridges, UiInteractionBridge } from '../bridges'; +import { UIHelper } from '../misc/UIHelper'; + +export class UIController implements IUIController { + private readonly uiInteractionBridge: UiInteractionBridge; + + constructor( + private readonly appId: string, + bridges: AppBridges, + ) { + this.uiInteractionBridge = bridges.getUiInteractionBridge(); + } + + /** + * @deprecated please prefer the `openSurfaceView` method + */ + public openModalView(view: IUIKitModalViewParam, context: IUIKitInteractionParam, user: IUser) { + return this.openModal(view, context, user); + } + + /** + * @deprecated please prefer the `updateSurfaceView` method + */ + public updateModalView(view: IUIKitModalViewParam, context: IUIKitInteractionParam, user: IUser) { + return this.openModal(view, context, user, true); + } + + /** + * @deprecated please prefer the `openSurfaceView` method + */ + public openContextualBarView(view: IUIKitContextualBarViewParam, context: IUIKitInteractionParam, user: IUser) { + return this.openContextualBar(view, context, user); + } + + /** + * @deprecated please prefer the `updateSurfaceView` method + */ + public updateContextualBarView(view: IUIKitContextualBarViewParam, context: IUIKitInteractionParam, user: IUser) { + return this.openContextualBar(view, context, user, true); + } + + public openSurfaceView(view: IUIKitSurfaceViewParam, context: IUIKitInteractionParam, user: IUser) { + const blocks = UIHelper.assignIds(view.blocks, this.appId); + const viewWithIds = { ...view, blocks }; + + switch (view.type) { + case UIKitSurfaceType.CONTEXTUAL_BAR: + return this.openContextualBar(viewWithIds, context, user); + case UIKitSurfaceType.MODAL: + return this.openModal(viewWithIds, context, user); + } + } + + public updateSurfaceView(view: IUIKitSurfaceViewParam, context: IUIKitInteractionParam, user: IUser) { + const blocks = UIHelper.assignIds(view.blocks, this.appId); + const viewWithIds = { ...view, blocks }; + + switch (view.type) { + case UIKitSurfaceType.CONTEXTUAL_BAR: + return this.openContextualBar(viewWithIds, context, user, true); + case UIKitSurfaceType.MODAL: + return this.openModal(viewWithIds, context, user, true); + } + } + + public setViewError(errorInteraction: IUIKitErrorInteractionParam, context: IUIKitInteractionParam, user: IUser) { + const interactionContext = { + ...context, + type: UIKitInteractionType.ERRORS, + appId: this.appId, + }; + + return this.uiInteractionBridge.doNotifyUser(user, formatErrorInteraction(errorInteraction, interactionContext), this.appId); + } + + private openContextualBar( + view: IUIKitContextualBarViewParam, + context: IUIKitInteractionParam, + user: IUser, + isUpdate = false, + ): Promise { + let type = UIKitInteractionType.CONTEXTUAL_BAR_OPEN; + if (isUpdate) { + type = UIKitInteractionType.CONTEXTUAL_BAR_UPDATE; + } + const interactionContext = { + ...context, + type, + appId: this.appId, + }; + + return this.uiInteractionBridge.doNotifyUser(user, formatContextualBarInteraction(view, interactionContext), this.appId); + } + + private openModal(view: IUIKitModalViewParam, context: IUIKitInteractionParam, user: IUser, isUpdate = false): Promise { + let type = UIKitInteractionType.MODAL_OPEN; + if (isUpdate) { + type = UIKitInteractionType.MODAL_UPDATE; + } + const interactionContext = { + ...context, + type, + appId: this.appId, + }; + + return this.uiInteractionBridge.doNotifyUser(user, formatModalInteraction(view, interactionContext), this.appId); + } +} diff --git a/packages/apps/src/server/accessors/UIExtend.ts b/packages/apps/src/server/accessors/UIExtend.ts new file mode 100644 index 0000000000000..96de3e623aded --- /dev/null +++ b/packages/apps/src/server/accessors/UIExtend.ts @@ -0,0 +1,14 @@ +import type { IUIExtend } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IUIActionButtonDescriptor } from '@rocket.chat/apps-engine/definition/ui'; +import type { UIActionButtonManager } from '../managers/UIActionButtonManager'; + +export class UIExtend implements IUIExtend { + constructor( + private readonly manager: UIActionButtonManager, + private readonly appId: string, + ) {} + + public registerButton(button: IUIActionButtonDescriptor): void { + this.manager.registerActionButton(this.appId, button); + } +} diff --git a/packages/apps/src/server/accessors/UploadCreator.ts b/packages/apps/src/server/accessors/UploadCreator.ts new file mode 100644 index 0000000000000..1090e13760d87 --- /dev/null +++ b/packages/apps/src/server/accessors/UploadCreator.ts @@ -0,0 +1,28 @@ +import type { IUploadCreator } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IUpload } from '@rocket.chat/apps-engine/definition/uploads'; +import type { IUploadDescriptor } from '@rocket.chat/apps-engine/definition/uploads/IUploadDescriptor'; +import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails'; +import type { AppBridges } from '../bridges'; + +export class UploadCreator implements IUploadCreator { + constructor( + private readonly bridges: AppBridges, + private readonly appId: string, + ) {} + + public async uploadBuffer(buffer: Buffer, descriptor: IUploadDescriptor): Promise { + if (!descriptor.hasOwnProperty('user') && !descriptor.visitorToken) { + descriptor.user = await this.bridges.getUserBridge().doGetAppUser(this.appId); + } + + const details = { + name: descriptor.filename, + size: buffer.length, + rid: descriptor.room.id, + userId: descriptor.user?.id, + visitorToken: descriptor.visitorToken, + } as IUploadDetails; + + return this.bridges.getUploadBridge().doCreateUpload(details, buffer, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/UploadRead.ts b/packages/apps/src/server/accessors/UploadRead.ts new file mode 100644 index 0000000000000..5f953d5ddf643 --- /dev/null +++ b/packages/apps/src/server/accessors/UploadRead.ts @@ -0,0 +1,24 @@ +import type { IUploadRead } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IUpload } from '@rocket.chat/apps-engine/definition/uploads'; +import type { UploadBridge } from '../bridges/UploadBridge'; + +export class UploadRead implements IUploadRead { + constructor( + private readonly uploadBridge: UploadBridge, + private readonly appId: string, + ) {} + + public getById(id: string): Promise { + return this.uploadBridge.doGetById(id, this.appId); + } + + public getBuffer(upload: IUpload): Promise { + return this.uploadBridge.doGetBuffer(upload, this.appId); + } + + public async getBufferById(id: string): Promise { + const upload = await this.uploadBridge.doGetById(id, this.appId); + + return this.uploadBridge.doGetBuffer(upload, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/UserBuilder.ts b/packages/apps/src/server/accessors/UserBuilder.ts new file mode 100644 index 0000000000000..f1c891ec73bd6 --- /dev/null +++ b/packages/apps/src/server/accessors/UserBuilder.ts @@ -0,0 +1,74 @@ +import type { IUserBuilder } from '@rocket.chat/apps-engine/definition/accessors'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata'; +import type { IUser, IUserEmail } from '@rocket.chat/apps-engine/definition/users'; +import type { IUserSettings } from '@rocket.chat/apps-engine/definition/users/IUserSettings'; + +export class UserBuilder implements IUserBuilder { + public kind: RocketChatAssociationModel.USER; + + private user: Partial; + + constructor(user?: Partial) { + this.kind = RocketChatAssociationModel.USER; + this.user = user || ({} as Partial); + } + + public setData(data: Partial): IUserBuilder { + delete data.id; + this.user = data; + + return this; + } + + public setEmails(emails: Array): IUserBuilder { + this.user.emails = emails; + return this; + } + + public getEmails(): Array { + return this.user.emails; + } + + public setDisplayName(name: string): IUserBuilder { + this.user.name = name; + return this; + } + + public getDisplayName(): string { + return this.user.name; + } + + public setUsername(username: string): IUserBuilder { + this.user.username = username; + return this; + } + + public getUsername(): string { + return this.user.username; + } + + public setRoles(roles: Array): IUserBuilder { + this.user.roles = roles; + return this; + } + + public getRoles(): Array { + return this.user.roles; + } + + public getSettings(): Partial { + return this.user.settings; + } + + public getUser(): Partial { + if (!this.user.username) { + throw new Error('The "username" property is required.'); + } + + if (!this.user.name) { + throw new Error('The "name" property is required.'); + } + + return this.user; + } +} diff --git a/packages/apps/src/server/accessors/UserRead.ts b/packages/apps/src/server/accessors/UserRead.ts new file mode 100644 index 0000000000000..8d38a08818335 --- /dev/null +++ b/packages/apps/src/server/accessors/UserRead.ts @@ -0,0 +1,30 @@ +import type { IUserRead } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import type { UserBridge } from '../bridges/UserBridge'; + +export class UserRead implements IUserRead { + constructor( + private userBridge: UserBridge, + private appId: string, + ) {} + + public getById(id: string): Promise { + return this.userBridge.doGetById(id, this.appId); + } + + public getByUsername(username: string): Promise { + return this.userBridge.doGetByUsername(username, this.appId); + } + + public getAppUser(appId: string = this.appId): Promise { + return this.userBridge.doGetAppUser(appId); + } + + public getUserUnreadMessageCount(uid: string): Promise { + return this.userBridge.doGetUserUnreadMessageCount(uid, this.appId); + } + + public getUserRoomIds(userId: string): Promise { + return this.userBridge.doGetUserRoomIds(userId, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/UserUpdater.ts b/packages/apps/src/server/accessors/UserUpdater.ts new file mode 100644 index 0000000000000..e43ae9644b809 --- /dev/null +++ b/packages/apps/src/server/accessors/UserUpdater.ts @@ -0,0 +1,31 @@ +import type { IUserUpdater } from '@rocket.chat/apps-engine/definition/accessors/IUserUpdater'; +import type { UserStatusConnection } from '@rocket.chat/apps-engine/definition/users'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; +import type { AppBridges } from '../bridges'; + +export class UserUpdater implements IUserUpdater { + constructor( + private readonly bridges: AppBridges, + private readonly appId: string, + ) {} + + public async updateStatusText(user: IUser, statusText: IUser['statusText']) { + return this.bridges.getUserBridge().doUpdate(user, { statusText }, this.appId); + } + + public async updateStatus(user: IUser, statusText: IUser['statusText'], status: UserStatusConnection) { + return this.bridges.getUserBridge().doUpdate(user, { statusText, status }, this.appId); + } + + public async updateBio(user: IUser, bio: IUser['bio']) { + return this.bridges.getUserBridge().doUpdate(user, { bio }, this.appId); + } + + public async updateCustomFields(user: IUser, customFields: IUser['customFields']) { + return this.bridges.getUserBridge().doUpdate(user, { customFields }, this.appId); + } + + public async deactivate(userId: IUser['id'], confirmRelinquish: boolean) { + return this.bridges.getUserBridge().doDeactivate(userId, confirmRelinquish, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/VideoConfProviderExtend.ts b/packages/apps/src/server/accessors/VideoConfProviderExtend.ts new file mode 100644 index 0000000000000..ad24e7d1b9201 --- /dev/null +++ b/packages/apps/src/server/accessors/VideoConfProviderExtend.ts @@ -0,0 +1,14 @@ +import type { IVideoConfProvidersExtend } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders'; +import type { AppVideoConfProviderManager } from '../managers/AppVideoConfProviderManager'; + +export class VideoConfProviderExtend implements IVideoConfProvidersExtend { + constructor( + private readonly manager: AppVideoConfProviderManager, + private readonly appId: string, + ) {} + + public provideVideoConfProvider(provider: IVideoConfProvider): Promise { + return Promise.resolve(this.manager.addProvider(this.appId, provider)); + } +} diff --git a/packages/apps/src/server/accessors/VideoConferenceBuilder.ts b/packages/apps/src/server/accessors/VideoConferenceBuilder.ts new file mode 100644 index 0000000000000..a7c101b9851e7 --- /dev/null +++ b/packages/apps/src/server/accessors/VideoConferenceBuilder.ts @@ -0,0 +1,83 @@ +import type { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/accessors'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata'; +import type { AppVideoConference } from '@rocket.chat/apps-engine/definition/videoConferences'; + +export class VideoConferenceBuilder implements IVideoConferenceBuilder { + public kind: RocketChatAssociationModel.VIDEO_CONFERENCE = RocketChatAssociationModel.VIDEO_CONFERENCE; + + protected call: AppVideoConference; + + constructor(data?: Partial) { + this.call = (data || {}) as AppVideoConference; + } + + public setData(data: Partial): IVideoConferenceBuilder { + this.call = { + rid: data.rid, + createdBy: data.createdBy, + providerName: data.providerName, + title: data.title, + discussionRid: data.discussionRid, + }; + + return this; + } + + public setRoomId(rid: string): IVideoConferenceBuilder { + this.call.rid = rid; + return this; + } + + public getRoomId(): string { + return this.call.rid; + } + + public setCreatedBy(userId: string): IVideoConferenceBuilder { + this.call.createdBy = userId; + return this; + } + + public getCreatedBy(): string { + return this.call.createdBy; + } + + public setProviderName(userId: string): IVideoConferenceBuilder { + this.call.providerName = userId; + return this; + } + + public getProviderName(): string { + return this.call.providerName; + } + + public setProviderData(data: Record | undefined): IVideoConferenceBuilder { + this.call.providerData = data; + return this; + } + + public getProviderData(): Record | undefined { + return this.call.providerData; + } + + public setTitle(userId: string): IVideoConferenceBuilder { + this.call.title = userId; + return this; + } + + public getTitle(): string { + return this.call.title; + } + + public setDiscussionRid(rid: AppVideoConference['discussionRid']): IVideoConferenceBuilder { + this.call.discussionRid = rid; + return this; + } + + public getDiscussionRid(): AppVideoConference['discussionRid'] { + return this.call.discussionRid; + } + + public getVideoConference(): AppVideoConference { + return this.call; + } +} diff --git a/packages/apps/src/server/accessors/VideoConferenceExtend.ts b/packages/apps/src/server/accessors/VideoConferenceExtend.ts new file mode 100644 index 0000000000000..1a6a252b89e65 --- /dev/null +++ b/packages/apps/src/server/accessors/VideoConferenceExtend.ts @@ -0,0 +1,64 @@ +import type { IVideoConferenceExtender } from '@rocket.chat/apps-engine/definition/accessors'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata'; +import type { IVideoConferenceUser, VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences'; +import type { VideoConferenceMember } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference'; +import { Utilities } from '../misc/Utilities'; + +export class VideoConferenceExtender implements IVideoConferenceExtender { + public kind: RocketChatAssociationModel.VIDEO_CONFERENCE; + + constructor(private videoConference: VideoConference) { + this.kind = RocketChatAssociationModel.VIDEO_CONFERENCE; + } + + public setProviderData(value: Record): IVideoConferenceExtender { + this.videoConference.providerData = value; + + return this; + } + + public setStatus(value: VideoConference['status']): IVideoConferenceExtender { + this.videoConference.status = value; + + return this; + } + + public setEndedBy(value: IVideoConferenceUser['_id']): IVideoConferenceExtender { + this.videoConference.endedBy = { + _id: value, + // Name and username will be loaded automatically by the bridge + username: '', + name: '', + }; + + return this; + } + + public setEndedAt(value: VideoConference['endedAt']): IVideoConferenceExtender { + this.videoConference.endedAt = value; + + return this; + } + + public addUser(userId: VideoConferenceMember['_id'], ts?: VideoConferenceMember['ts']): IVideoConferenceExtender { + this.videoConference.users.push({ + _id: userId, + ts, + // Name and username will be loaded automatically by the bridge + username: '', + name: '', + }); + + return this; + } + + public setDiscussionRid(rid: VideoConference['discussionRid']): IVideoConferenceExtender { + this.videoConference.discussionRid = rid; + + return this; + } + + public getVideoConference(): VideoConference { + return Utilities.deepClone(this.videoConference); + } +} diff --git a/packages/apps/src/server/accessors/VideoConferenceRead.ts b/packages/apps/src/server/accessors/VideoConferenceRead.ts new file mode 100644 index 0000000000000..18f06697a3254 --- /dev/null +++ b/packages/apps/src/server/accessors/VideoConferenceRead.ts @@ -0,0 +1,14 @@ +import type { IVideoConferenceRead } from '@rocket.chat/apps-engine/definition/accessors'; +import type { VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences'; +import type { VideoConferenceBridge } from '../bridges'; + +export class VideoConferenceRead implements IVideoConferenceRead { + constructor( + private videoConfBridge: VideoConferenceBridge, + private appId: string, + ) {} + + public getById(id: string): Promise { + return this.videoConfBridge.doGetById(id, this.appId); + } +} diff --git a/packages/apps/src/server/accessors/index.ts b/packages/apps/src/server/accessors/index.ts new file mode 100644 index 0000000000000..eb5cbdc0218c7 --- /dev/null +++ b/packages/apps/src/server/accessors/index.ts @@ -0,0 +1,97 @@ +import { ApiExtend } from './ApiExtend'; +import { AppAccessors } from './AppAccessors'; +import { ConfigurationExtend } from './ConfigurationExtend'; +import { ConfigurationModify } from './ConfigurationModify'; +import { EnvironmentRead } from './EnvironmentRead'; +import { EnvironmentWrite } from './EnvironmentWrite'; +import { EnvironmentalVariableRead } from './EnvironmentalVariableRead'; +import { ExternalComponentsExtend } from './ExternalComponentsExtend'; +import { Http } from './Http'; +import { HttpExtend } from './HttpExtend'; +import { LivechatRead } from './LivechatRead'; +import { MessageBuilder } from './MessageBuilder'; +import { MessageExtender } from './MessageExtender'; +import { MessageRead } from './MessageRead'; +import { ModerationModify } from './ModerationModify'; +import { Modify } from './Modify'; +import { ModifyCreator } from './ModifyCreator'; +import { ModifyExtender } from './ModifyExtender'; +import { ModifyUpdater } from './ModifyUpdater'; +import { Notifier } from './Notifier'; +import { OAuthAppsModify } from './OAuthAppsModify'; +import { OAuthAppsReader } from './OAuthAppsReader'; +import { OutboundMessageProviderExtend } from './OutboundCommunicationProviderExtend'; +import { Persistence } from './Persistence'; +import { PersistenceRead } from './PersistenceRead'; +import { Reader } from './Reader'; +import { RoleRead } from './RoleRead'; +import { RoomBuilder } from './RoomBuilder'; +import { RoomExtender } from './RoomExtender'; +import { RoomRead } from './RoomRead'; +import { SchedulerExtend } from './SchedulerExtend'; +import { SchedulerModify } from './SchedulerModify'; +import { ServerSettingRead } from './ServerSettingRead'; +import { ServerSettingUpdater } from './ServerSettingUpdater'; +import { ServerSettingsModify } from './ServerSettingsModify'; +import { SettingRead } from './SettingRead'; +import { SettingUpdater } from './SettingUpdater'; +import { SettingsExtend } from './SettingsExtend'; +import { SlashCommandsExtend } from './SlashCommandsExtend'; +import { SlashCommandsModify } from './SlashCommandsModify'; +import { UploadRead } from './UploadRead'; +import { UserBuilder } from './UserBuilder'; +import { UserRead } from './UserRead'; +import { VideoConfProviderExtend } from './VideoConfProviderExtend'; +import { VideoConferenceBuilder } from './VideoConferenceBuilder'; +import { VideoConferenceExtender } from './VideoConferenceExtend'; +import { VideoConferenceRead } from './VideoConferenceRead'; + +export { + ApiExtend, + AppAccessors, + ConfigurationExtend, + ConfigurationModify, + EnvironmentalVariableRead, + EnvironmentRead, + EnvironmentWrite, + ExternalComponentsExtend, + Http, + HttpExtend, + LivechatRead, + MessageBuilder, + MessageExtender, + MessageRead, + ModerationModify, + Modify, + ModifyCreator, + ModifyExtender, + ModifyUpdater, + Notifier, + Persistence, + PersistenceRead, + Reader, + RoleRead, + RoomBuilder, + RoomExtender, + RoomRead, + ServerSettingRead, + ServerSettingsModify, + ServerSettingUpdater, + SettingRead, + SettingsExtend, + SettingUpdater, + SlashCommandsExtend, + SlashCommandsModify, + UploadRead, + UserBuilder, + UserRead, + SchedulerExtend, + SchedulerModify, + VideoConferenceBuilder, + VideoConferenceExtender, + VideoConferenceRead, + VideoConfProviderExtend, + OAuthAppsModify, + OAuthAppsReader, + OutboundMessageProviderExtend, +}; diff --git a/packages/apps/src/server/bridges/ApiBridge.ts b/packages/apps/src/server/bridges/ApiBridge.ts new file mode 100644 index 0000000000000..47be5b27e92e0 --- /dev/null +++ b/packages/apps/src/server/bridges/ApiBridge.ts @@ -0,0 +1,49 @@ +import { BaseBridge } from './BaseBridge'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import type { AppApi } from '../managers/AppApi'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export abstract class ApiBridge extends BaseBridge { + public async doRegisterApi(api: AppApi, appId: string): Promise { + if (this.hasDefaultPermission(appId)) { + return this.registerApi(api, appId); + } + } + + public async doUnregisterApis(appId: string): Promise { + if (this.hasDefaultPermission(appId)) { + return this.unregisterApis(appId); + } + } + + /** + * Registers an api with the system which is being bridged. + * + * @param api the api to register + * @param appId the id of the app calling this + */ + protected abstract registerApi(api: AppApi, appId: string): Promise; + + /** + * Unregisters all provided api's of an app from the bridged system. + * + * @param appId the id of the app calling this + */ + protected abstract unregisterApis(appId: string): Promise; + + private hasDefaultPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.apis.default)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.apis.default], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/AppActivationBridge.ts b/packages/apps/src/server/bridges/AppActivationBridge.ts new file mode 100644 index 0000000000000..947987a77417c --- /dev/null +++ b/packages/apps/src/server/bridges/AppActivationBridge.ts @@ -0,0 +1,35 @@ +import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import type { ProxiedApp } from '../ProxiedApp'; +import { BaseBridge } from './BaseBridge'; + +export abstract class AppActivationBridge extends BaseBridge { + public async doAppAdded(app: ProxiedApp): Promise { + return this.appAdded(app); + } + + public async doAppUpdated(app: ProxiedApp): Promise { + return this.appUpdated(app); + } + + public async doAppRemoved(app: ProxiedApp): Promise { + return this.appRemoved(app); + } + + public async doAppStatusChanged(app: ProxiedApp, status: AppStatus): Promise { + return this.appStatusChanged(app, status); + } + + public async doActionsChanged(): Promise { + return this.actionsChanged(); + } + + protected abstract appAdded(app: ProxiedApp): Promise; + + protected abstract appUpdated(app: ProxiedApp): Promise; + + protected abstract appRemoved(app: ProxiedApp): Promise; + + protected abstract appStatusChanged(app: ProxiedApp, status: AppStatus): Promise; + + protected abstract actionsChanged(): Promise; +} diff --git a/packages/apps/src/server/bridges/AppBridges.ts b/packages/apps/src/server/bridges/AppBridges.ts new file mode 100644 index 0000000000000..5e5ef7ca12106 --- /dev/null +++ b/packages/apps/src/server/bridges/AppBridges.ts @@ -0,0 +1,113 @@ +import type { ApiBridge } from './ApiBridge'; +import type { AppActivationBridge } from './AppActivationBridge'; +import type { AppDetailChangesBridge } from './AppDetailChangesBridge'; +import type { CloudWorkspaceBridge } from './CloudWorkspaceBridge'; +import type { CommandBridge } from './CommandBridge'; +import type { ContactBridge } from './ContactBridge'; +import type { EmailBridge } from './EmailBridge'; +import type { EnvironmentalVariableBridge } from './EnvironmentalVariableBridge'; +import type { ExperimentalBridge } from './ExperimentalBridge'; +import type { HttpBridge } from './HttpBridge'; +import type { IInternalBridge } from './IInternalBridge'; +import type { IInternalFederationBridge } from './IInternalFederationBridge'; +import type { IListenerBridge } from './IListenerBridge'; +import type { LivechatBridge } from './LivechatBridge'; +import type { MessageBridge } from './MessageBridge'; +import type { ModerationBridge } from './ModerationBridge'; +import type { OAuthAppsBridge } from './OAuthAppsBridge'; +import type { OutboundMessageBridge } from './OutboundMessagesBridge'; +import type { PersistenceBridge } from './PersistenceBridge'; +import type { RoleBridge } from './RoleBridge'; +import type { RoomBridge } from './RoomBridge'; +import type { SchedulerBridge } from './SchedulerBridge'; +import type { ServerSettingBridge } from './ServerSettingBridge'; +import type { ThreadBridge } from './ThreadBridge'; +import type { UiInteractionBridge } from './UiInteractionBridge'; +import type { UploadBridge } from './UploadBridge'; +import type { UserBridge } from './UserBridge'; +import type { VideoConferenceBridge } from './VideoConferenceBridge'; + +export type Bridge = + | CommandBridge + | ContactBridge + | ApiBridge + | AppDetailChangesBridge + | EnvironmentalVariableBridge + | HttpBridge + | IListenerBridge + | LivechatBridge + | MessageBridge + | PersistenceBridge + | AppActivationBridge + | RoomBridge + | IInternalBridge + | ServerSettingBridge + | EmailBridge + | ExperimentalBridge + | UploadBridge + | UserBridge + | UiInteractionBridge + | SchedulerBridge + | VideoConferenceBridge + | OAuthAppsBridge + | ModerationBridge + | RoleBridge + | OutboundMessageBridge; + +export abstract class AppBridges { + public abstract getCommandBridge(): CommandBridge; + + public abstract getContactBridge(): ContactBridge; + + public abstract getApiBridge(): ApiBridge; + + public abstract getAppDetailChangesBridge(): AppDetailChangesBridge; + + public abstract getEnvironmentalVariableBridge(): EnvironmentalVariableBridge; + + public abstract getHttpBridge(): HttpBridge; + + public abstract getListenerBridge(): IListenerBridge; + + public abstract getLivechatBridge(): LivechatBridge; + + public abstract getMessageBridge(): MessageBridge; + + public abstract getPersistenceBridge(): PersistenceBridge; + + public abstract getAppActivationBridge(): AppActivationBridge; + + public abstract getRoomBridge(): RoomBridge; + + public abstract getInternalBridge(): IInternalBridge; + + public abstract getInternalFederationBridge(): IInternalFederationBridge; + + public abstract getServerSettingBridge(): ServerSettingBridge; + + public abstract getUploadBridge(): UploadBridge; + + public abstract getEmailBridge(): EmailBridge; + + public abstract getUserBridge(): UserBridge; + + public abstract getUiInteractionBridge(): UiInteractionBridge; + + public abstract getSchedulerBridge(): SchedulerBridge; + + public abstract getCloudWorkspaceBridge(): CloudWorkspaceBridge; + + public abstract getVideoConferenceBridge(): VideoConferenceBridge; + + public abstract getOAuthAppsBridge(): OAuthAppsBridge; + + public abstract getModerationBridge(): ModerationBridge; + + public abstract getThreadBridge(): ThreadBridge; + + public abstract getRoleBridge(): RoleBridge; + + public abstract getOutboundMessageBridge(): OutboundMessageBridge; + + public abstract getExperimentalBridge(): ExperimentalBridge; +} diff --git a/packages/apps/src/server/bridges/AppDetailChangesBridge.ts b/packages/apps/src/server/bridges/AppDetailChangesBridge.ts new file mode 100644 index 0000000000000..bf6ab17d72bdf --- /dev/null +++ b/packages/apps/src/server/bridges/AppDetailChangesBridge.ts @@ -0,0 +1,16 @@ +import { BaseBridge } from './BaseBridge'; +import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; + +/** + * An abstract class which will contain various methods related to Apps + * which are called for various inner detail working changes. This + * allows for us to notify various external components of internal + * changes. + */ +export abstract class AppDetailChangesBridge extends BaseBridge { + public doOnAppSettingsChange(appId: string, setting: ISetting): void { + return this.onAppSettingsChange(appId, setting); + } + + protected abstract onAppSettingsChange(appId: string, setting: ISetting): void; +} diff --git a/packages/apps/src/server/bridges/BaseBridge.ts b/packages/apps/src/server/bridges/BaseBridge.ts new file mode 100644 index 0000000000000..7085c3d54db78 --- /dev/null +++ b/packages/apps/src/server/bridges/BaseBridge.ts @@ -0,0 +1,6 @@ +/** + * This class will be used for identification + * of the instances the host sends over to + * the Apps-Engine + */ +export abstract class BaseBridge {} diff --git a/packages/apps/src/server/bridges/CloudWorkspaceBridge.ts b/packages/apps/src/server/bridges/CloudWorkspaceBridge.ts new file mode 100644 index 0000000000000..abccdea2bc3bf --- /dev/null +++ b/packages/apps/src/server/bridges/CloudWorkspaceBridge.ts @@ -0,0 +1,30 @@ +import { BaseBridge } from './BaseBridge'; +import type { IWorkspaceToken } from '@rocket.chat/apps-engine/definition/cloud/IWorkspaceToken'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export abstract class CloudWorkspaceBridge extends BaseBridge { + public doGetWorkspaceToken(scope: string, appId: string): Promise { + if (this.hasCloudTokenPermission(appId)) { + return this.getWorkspaceToken(scope, appId); + } + } + + protected abstract getWorkspaceToken(scope: string, appId: string): Promise; + + private hasCloudTokenPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.cloud['workspace-token'])) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.cloud['workspace-token']], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/CommandBridge.ts b/packages/apps/src/server/bridges/CommandBridge.ts new file mode 100644 index 0000000000000..2a7c2d78197c0 --- /dev/null +++ b/packages/apps/src/server/bridges/CommandBridge.ts @@ -0,0 +1,117 @@ +import { BaseBridge } from './BaseBridge'; +import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export abstract class CommandBridge extends BaseBridge { + public async doDoesCommandExist(command: string, appId: string): Promise { + if (this.hasDefaultPermission(appId)) { + return this.doesCommandExist(command, appId); + } + } + + public async doEnableCommand(command: string, appId: string): Promise { + if (this.hasDefaultPermission(appId)) { + return this.enableCommand(command, appId); + } + } + + public async doDisableCommand(command: string, appId: string): Promise { + if (this.hasDefaultPermission(appId)) { + return this.disableCommand(command, appId); + } + } + + public async doModifyCommand(command: ISlashCommand, appId: string): Promise { + if (this.hasDefaultPermission(appId)) { + return this.modifyCommand(command, appId); + } + } + + public async doRegisterCommand(command: ISlashCommand, appId: string): Promise { + if (this.hasDefaultPermission(appId)) { + return this.registerCommand(command, appId); + } + } + + public async doUnregisterCommand(command: string, appId: string): Promise { + if (this.hasDefaultPermission(appId)) { + return this.unregisterCommand(command, appId); + } + } + + /** + * Checks if the provided command already exists inside of the + * system which is being bridged. This does not check if the app + * registered it but it will return whether the supplied command is + * already defined by something else or not. + * + * @param command the command to check if it exists + * @param appId the id of the app calling this + * @returns whether the command is already in the system + */ + protected abstract doesCommandExist(command: string, appId: string): Promise; + + /** + * Enables an existing command from the bridged system. The callee + * must ensure that the command that's being enabled is defined by + * the bridged system and not another App since the bridged system + * will not check that. + * + * @param command the command to enable + * @param appId the id of the app calling this + */ + protected abstract enableCommand(command: string, appId: string): Promise; + + /** + * Disables an existing command from the bridged system, the callee must + * ensure the command disabling is defined by the system and not another + * App since the bridged system won't check that. + * + * @param command the command which to disable + * @param appId the id of the app calling this + */ + protected abstract disableCommand(command: string, appId: string): Promise; + + /** + * Changes how a system slash command behaves, allows Apps to provide + * different executors per system commands. + * + * @param command the modified slash command + * @param appId the id of the app calling this + */ + protected abstract modifyCommand(command: ISlashCommand, appId: string): Promise; + + /** + * Registers a command with the system which is being bridged. + * + * @param command the command to register + * @param appId the id of the app calling this + * @param toRun the executor which is called when the command is ran + */ + protected abstract registerCommand(command: ISlashCommand, appId: string): Promise; + + /** + * Unregisters the provided command from the bridged system. + * + * @param command the command to unregister + * @param appId the id of the app calling this + */ + protected abstract unregisterCommand(command: string, appId: string): Promise; + + private hasDefaultPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.command.default)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.command.default], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/ContactBridge.ts b/packages/apps/src/server/bridges/ContactBridge.ts new file mode 100644 index 0000000000000..dd45d67f4afec --- /dev/null +++ b/packages/apps/src/server/bridges/ContactBridge.ts @@ -0,0 +1,69 @@ +import { BaseBridge } from './BaseBridge'; +import type { ILivechatContact } from '@rocket.chat/apps-engine/definition/livechat/ILivechatContact'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export type VerifyContactChannelParams = { + contactId: string; + field: string; + value: string; + visitorId: string; + roomId: string; +}; + +export abstract class ContactBridge extends BaseBridge { + public async doGetById(contactId: ILivechatContact['_id'], appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getById(contactId, appId); + } + } + + public async doVerifyContact(verifyContactChannelParams: VerifyContactChannelParams, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.verifyContact(verifyContactChannelParams, appId); + } + } + + public async doAddContactEmail(contactId: ILivechatContact['_id'], email: string, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.addContactEmail(contactId, email, appId); + } + } + + protected abstract getById(contactId: ILivechatContact['_id'], appId: string): Promise; + + protected abstract verifyContact(verifyContactChannelParams: VerifyContactChannelParams, appId: string): Promise; + + protected abstract addContactEmail(contactId: ILivechatContact['_id'], email: string, appId: string): Promise; + + private hasReadPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.contact.read)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.contact.read], + }), + ); + + return false; + } + + private hasWritePermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.contact.write)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.contact.write], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/EmailBridge.ts b/packages/apps/src/server/bridges/EmailBridge.ts new file mode 100644 index 0000000000000..3692f3d7562bb --- /dev/null +++ b/packages/apps/src/server/bridges/EmailBridge.ts @@ -0,0 +1,30 @@ +import { BaseBridge } from './BaseBridge'; +import type { IEmail } from '@rocket.chat/apps-engine/definition/email'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export abstract class EmailBridge extends BaseBridge { + public async doSendEmail(email: IEmail, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.sendEmail(email, appId); + } + } + + protected abstract sendEmail(email: IEmail, appId: string): Promise; + + private hasWritePermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.email.send)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.email.send], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/EnvironmentalVariableBridge.ts b/packages/apps/src/server/bridges/EnvironmentalVariableBridge.ts new file mode 100644 index 0000000000000..79b29bf316c41 --- /dev/null +++ b/packages/apps/src/server/bridges/EnvironmentalVariableBridge.ts @@ -0,0 +1,45 @@ +import { BaseBridge } from './BaseBridge'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export abstract class EnvironmentalVariableBridge extends BaseBridge { + public async doGetValueByName(envVarName: string, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getValueByName(envVarName, appId); + } + } + + public async doIsReadable(envVarName: string, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.isReadable(envVarName, appId); + } + } + + public async doIsSet(envVarName: string, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.isSet(envVarName, appId); + } + } + + protected abstract getValueByName(envVarName: string, appId: string): Promise; + + protected abstract isReadable(envVarName: string, appId: string): Promise; + + protected abstract isSet(envVarName: string, appId: string): Promise; + + private hasReadPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.env.read)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.env.read], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/ExperimentalBridge.ts b/packages/apps/src/server/bridges/ExperimentalBridge.ts new file mode 100644 index 0000000000000..806b0d66fef37 --- /dev/null +++ b/packages/apps/src/server/bridges/ExperimentalBridge.ts @@ -0,0 +1,10 @@ +import { BaseBridge } from './BaseBridge'; + +/** + * @description + * Experimental bridge for experimental features. + * Methods in this class are not guaranteed to be stable between updates as the + * team evaluates the proper signature, underlying implementation and performance + * impact of candidates for future APIs + */ +export abstract class ExperimentalBridge extends BaseBridge {} diff --git a/packages/apps/src/server/bridges/HttpBridge.ts b/packages/apps/src/server/bridges/HttpBridge.ts new file mode 100644 index 0000000000000..20d25e50d7063 --- /dev/null +++ b/packages/apps/src/server/bridges/HttpBridge.ts @@ -0,0 +1,37 @@ +import { BaseBridge } from './BaseBridge'; +import type { IHttpRequest, IHttpResponse, RequestMethod } from '@rocket.chat/apps-engine/definition/accessors'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export interface IHttpBridgeRequestInfo { + appId: string; + method: RequestMethod; + url: string; + request: IHttpRequest; +} + +export abstract class HttpBridge extends BaseBridge { + public async doCall(info: IHttpBridgeRequestInfo): Promise { + if (this.hasDefaultPermission(info.appId)) { + return this.call(info); + } + } + + protected abstract call(info: IHttpBridgeRequestInfo): Promise; + + private hasDefaultPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.networking.default)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.networking.default], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/IInternalBridge.ts b/packages/apps/src/server/bridges/IInternalBridge.ts new file mode 100644 index 0000000000000..f4b94b3111e4e --- /dev/null +++ b/packages/apps/src/server/bridges/IInternalBridge.ts @@ -0,0 +1,7 @@ +import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; + +export interface IInternalBridge { + doGetUsernamesOfRoomById(roomId: string): Promise>; + doGetUsernamesOfRoomByIdSync(roomId: string): Array; + doGetWorkspacePublicKey(): Promise; +} diff --git a/packages/apps/src/server/bridges/IInternalFederationBridge.ts b/packages/apps/src/server/bridges/IInternalFederationBridge.ts new file mode 100644 index 0000000000000..382f69679b842 --- /dev/null +++ b/packages/apps/src/server/bridges/IInternalFederationBridge.ts @@ -0,0 +1,15 @@ +export interface IInternalFederationBridge { + /** + * Get Federation's private key. + * For apps engine's internal use + * + */ + getPrivateKey(): Promise; + + /** + * Get Federation's public key. + * For apps engine's internal use + * + */ + getPublicKey(): Promise; +} diff --git a/packages/apps/src/server/bridges/IInternalPersistenceBridge.ts b/packages/apps/src/server/bridges/IInternalPersistenceBridge.ts new file mode 100644 index 0000000000000..e1dcd9b247697 --- /dev/null +++ b/packages/apps/src/server/bridges/IInternalPersistenceBridge.ts @@ -0,0 +1,9 @@ +export interface IInternalPersistenceBridge { + /** + * Purges the App's persistant storage data from the persistent storage. + * For apps engine's internal use + * + * @argument appId the id of the app's data to remove + */ + purge(appId: string): Promise; +} diff --git a/packages/apps/src/server/bridges/IInternalSchedulerBridge.ts b/packages/apps/src/server/bridges/IInternalSchedulerBridge.ts new file mode 100644 index 0000000000000..2064043128a39 --- /dev/null +++ b/packages/apps/src/server/bridges/IInternalSchedulerBridge.ts @@ -0,0 +1,8 @@ +export interface IInternalSchedulerBridge { + /** + * Cancels all the running jobs from the app + * For apps-engine's internal use + * @param appId the id of the app calling this + */ + cancelAllJobs(appId: string): Promise; +} diff --git a/packages/apps/src/server/bridges/IInternalUserBridge.ts b/packages/apps/src/server/bridges/IInternalUserBridge.ts new file mode 100644 index 0000000000000..a539f8b7d23f8 --- /dev/null +++ b/packages/apps/src/server/bridges/IInternalUserBridge.ts @@ -0,0 +1,8 @@ +import type { IUser, IUserCreationOptions } from '@rocket.chat/apps-engine/definition/users'; + +export interface IInternalUserBridge { + create(data: Partial, appId: string, options?: IUserCreationOptions): Promise; + getAppUser(appId?: string): Promise; + remove(user: IUser, appId: string): Promise; + getActiveUserCount(): Promise; +} diff --git a/packages/apps/src/server/bridges/IListenerBridge.ts b/packages/apps/src/server/bridges/IListenerBridge.ts new file mode 100644 index 0000000000000..b418b77dafc43 --- /dev/null +++ b/packages/apps/src/server/bridges/IListenerBridge.ts @@ -0,0 +1,10 @@ +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { AppInterface } from '@rocket.chat/apps-engine/definition/metadata'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import type { UIKitIncomingInteraction } from '@rocket.chat/apps-engine/definition/uikit'; + +export interface IListenerBridge { + messageEvent(int: AppInterface, message: IMessage): Promise; + roomEvent(int: AppInterface, room: IRoom): Promise; + uiKitInteractionEvent(int: AppInterface, action: UIKitIncomingInteraction): Promise; +} diff --git a/packages/apps/src/server/bridges/InternalBridge.ts b/packages/apps/src/server/bridges/InternalBridge.ts new file mode 100644 index 0000000000000..739f5f19dd7a0 --- /dev/null +++ b/packages/apps/src/server/bridges/InternalBridge.ts @@ -0,0 +1,22 @@ +import { BaseBridge } from './BaseBridge'; +import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; + +export abstract class InternalBridge extends BaseBridge { + public doGetUsernamesOfRoomById(roomId: string): Promise> { + return this.getUsernamesOfRoomById(roomId); + } + + public doGetUsernamesOfRoomByIdSync(roomId: string): Array { + return this.getUsernamesOfRoomByIdSync(roomId); + } + + public async doGetWorkspacePublicKey(): Promise { + return this.getWorkspacePublicKey(); + } + + protected abstract getUsernamesOfRoomById(roomId: string): Promise>; + + protected abstract getUsernamesOfRoomByIdSync(roomId: string): Array; + + protected abstract getWorkspacePublicKey(): Promise; +} diff --git a/packages/apps/src/server/bridges/ListenerBridge.ts b/packages/apps/src/server/bridges/ListenerBridge.ts new file mode 100644 index 0000000000000..e906339797d29 --- /dev/null +++ b/packages/apps/src/server/bridges/ListenerBridge.ts @@ -0,0 +1,18 @@ +import { BaseBridge } from './BaseBridge'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { AppInterface } from '@rocket.chat/apps-engine/definition/metadata'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; + +export abstract class ListenerBridge extends BaseBridge { + public async doMessageEvent(int: AppInterface, message: IMessage): Promise { + return this.messageEvent(int, message); + } + + public async doRoomEvent(int: AppInterface, room: IRoom): Promise { + return this.roomEvent(int, room); + } + + protected abstract messageEvent(int: AppInterface, message: IMessage): Promise; + + protected abstract roomEvent(int: AppInterface, room: IRoom): Promise; +} diff --git a/packages/apps/src/server/bridges/LivechatBridge.ts b/packages/apps/src/server/bridges/LivechatBridge.ts new file mode 100644 index 0000000000000..37d7b7623df8a --- /dev/null +++ b/packages/apps/src/server/bridges/LivechatBridge.ts @@ -0,0 +1,305 @@ +import { BaseBridge } from './BaseBridge'; +import type { IExtraRoomParams } from '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator'; +import type { + IDepartment, + IVisitorExternalIdentifier, + ILivechatMessage, + ILivechatRoom, + ILivechatTransferData, + IVisitor, + ResolveVisitorContactData, +} from '@rocket.chat/apps-engine/definition/livechat'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +type LivechatReadPermissions = keyof Pick< + typeof AppPermissions, + 'livechat-department' | 'livechat-message' | 'livechat-room' | 'livechat-status' | 'livechat-visitor' +>; + +type LivechatWritePermissions = keyof Pick< + typeof AppPermissions, + 'livechat-custom-fields' | 'livechat-department' | 'livechat-message' | 'livechat-room' | 'livechat-visitor' +>; + +type LivechatMultiplePermissions = keyof Pick; + +export abstract class LivechatBridge extends BaseBridge { + public doIsOnline(departmentId?: string, appId?: string): boolean { + if (this.hasReadPermission(appId, 'livechat-status')) { + return this.isOnline(departmentId, appId); + } + } + + public async doIsOnlineAsync(departmentId?: string, appId?: string): Promise { + if (this.hasReadPermission(appId, 'livechat-status')) { + return this.isOnlineAsync(departmentId, appId); + } + } + + public async doCreateMessage(message: ILivechatMessage, appId: string): Promise { + if (this.hasWritePermission(appId, 'livechat-message')) { + return this.createMessage(message, appId); + } + } + + public async doGetMessageById(messageId: string, appId: string): Promise { + if (this.hasReadPermission(appId, 'livechat-message')) { + return this.getMessageById(messageId, appId); + } + } + + public async doUpdateMessage(message: ILivechatMessage, appId: string): Promise { + if (this.hasWritePermission(appId, 'livechat-message')) { + return this.updateMessage(message, appId); + } + } + + /** + * @deprecated please use the `doCreateAndReturnVisitor` method instead. + */ + public async doCreateVisitor(visitor: IVisitor, appId: string): Promise { + if (this.hasWritePermission(appId, 'livechat-visitor')) { + return this.createVisitor(visitor, appId); + } + } + + public async doCreateAndReturnVisitor(visitor: IVisitor, appId: string): Promise { + if (this.hasWritePermission(appId, 'livechat-visitor')) { + return this.createAndReturnVisitor(visitor, appId); + } + } + + public async doFindVisitors(query: object, appId: string): Promise> { + if (this.hasReadPermission(appId, 'livechat-visitor')) { + return this.findVisitors(query, appId); + } + } + + public async doFindVisitorById(id: string, appId: string): Promise { + if (this.hasReadPermission(appId, 'livechat-visitor')) { + return this.findVisitorById(id, appId); + } + } + + public async doFindVisitorByEmail(email: string, appId: string): Promise { + if (this.hasReadPermission(appId, 'livechat-visitor')) { + return this.findVisitorByEmail(email, appId); + } + } + + public async doFindVisitorByToken(token: string, appId: string): Promise { + if (this.hasReadPermission(appId, 'livechat-visitor')) { + return this.findVisitorByToken(token, appId); + } + } + + public async doFindVisitorByPhoneNumber(phoneNumber: string, appId: string): Promise { + if (this.hasReadPermission(appId, 'livechat-visitor')) { + return this.findVisitorByPhoneNumber(phoneNumber, appId); + } + } + + public async doResolveVisitor( + externalId: Omit, + contactData: ResolveVisitorContactData | undefined, + appId: string, + ): Promise { + if (this.hasWritePermission(appId, 'livechat-visitor')) { + return this.resolveVisitor(externalId, contactData, appId); + } + } + + public async doTransferVisitor(visitor: IVisitor, transferData: ILivechatTransferData, appId: string): Promise { + if (this.hasWritePermission(appId, 'livechat-visitor')) { + return this.transferVisitor(visitor, transferData, appId); + } + } + + public async doUpdateVisitorExternalId( + visitorId: string, + externalId: Omit, + appId: string, + ): Promise { + if (this.hasWritePermission(appId, 'livechat-visitor')) { + return this.updateVisitorExternalId(visitorId, externalId, appId); + } + } + + public async doCreateRoom(visitor: IVisitor, agent: IUser, appId: string, extraParams?: IExtraRoomParams): Promise { + if (this.hasWritePermission(appId, 'livechat-room')) { + return this.createRoom(visitor, agent, appId, extraParams); + } + } + + public async doCloseRoom(room: ILivechatRoom, comment: string, closer: IUser | undefined, appId: string): Promise { + if (this.hasWritePermission(appId, 'livechat-room')) { + return this.closeRoom(room, comment, closer, appId); + } + } + + public async doCountOpenRoomsByAgentId(agentId: string, appId: string): Promise { + if (this.hasReadPermission(appId, 'livechat-room')) { + return this.countOpenRoomsByAgentId(agentId, appId); + } + } + + public async doFindOpenRoomsByAgentId(agentId: string, appId: string): Promise> { + if (this.hasReadPermission(appId, 'livechat-room')) { + return this.findOpenRoomsByAgentId(agentId, appId); + } + } + + public async doFindRooms(visitor: IVisitor, departmentId: string | null, appId: string): Promise> { + if (this.hasReadPermission(appId, 'livechat-room')) { + return this.findRooms(visitor, departmentId, appId); + } + } + + public async doFindDepartmentByIdOrName(value: string, appId: string): Promise { + if (this.hasReadPermission(appId, 'livechat-department') || this.hasMultiplePermission(appId, 'livechat-department')) { + return this.findDepartmentByIdOrName(value, appId); + } + } + + public async doFindDepartmentsEnabledWithAgents(appId: string): Promise> { + if (this.hasMultiplePermission(appId, 'livechat-department')) { + return this.findDepartmentsEnabledWithAgents(appId); + } + } + + public async do_fetchLivechatRoomMessages(appId: string, roomId: string): Promise> { + if (this.hasMultiplePermission(appId, 'livechat-message')) { + return this._fetchLivechatRoomMessages(appId, roomId); + } + } + + public async doSetCustomFields( + data: { token: IVisitor['token']; key: string; value: string; overwrite: boolean }, + appId: string, + ): Promise { + if (this.hasWritePermission(appId, 'livechat-custom-fields')) { + return this.setCustomFields(data, appId); + } + } + + /** + * @deprecated please use the `isOnlineAsync` method instead. + * In the next major, this method will be `async` + */ + protected abstract isOnline(departmentId?: string, appId?: string): boolean; + + protected abstract isOnlineAsync(departmentId?: string, appId?: string): Promise; + + protected abstract createMessage(message: ILivechatMessage, appId: string): Promise; + + protected abstract getMessageById(messageId: string, appId: string): Promise; + + protected abstract updateMessage(message: ILivechatMessage, appId: string): Promise; + + /** + * @deprecated please use `createAndReturnVisitor` instead. + * It returns the created record rather than the ID. + */ + protected abstract createVisitor(visitor: IVisitor, appId: string): Promise; + + protected abstract createAndReturnVisitor(visitor: IVisitor, appId: string): Promise; + + /** + * @deprecated This method does not adhere to the conversion practices applied + * elsewhere in the Apps-Engine and will be removed in the next major version. + * Prefer other methods that fetch visitors. + */ + protected abstract findVisitors(query: object, appId: string): Promise>; + + protected abstract findVisitorById(id: string, appId: string): Promise; + + protected abstract findVisitorByEmail(email: string, appId: string): Promise; + + protected abstract findVisitorByToken(token: string, appId: string): Promise; + + protected abstract findVisitorByPhoneNumber(phoneNumber: string, appId: string): Promise; + + protected abstract resolveVisitor( + externalId: Omit, + contactData: ResolveVisitorContactData | undefined, + appId: string, + ): Promise; + + protected abstract transferVisitor(visitor: IVisitor, transferData: ILivechatTransferData, appId: string): Promise; + + protected abstract updateVisitorExternalId( + visitorId: string, + externalId: Omit, + appId: string, + ): Promise; + + protected abstract createRoom(visitor: IVisitor, agent: IUser, appId: string, extraParams?: IExtraRoomParams): Promise; + + protected abstract closeRoom(room: ILivechatRoom, comment: string, closer: IUser | undefined, appId: string): Promise; + + protected abstract countOpenRoomsByAgentId(agentId: string, appId: string): Promise; + + protected abstract findOpenRoomsByAgentId(agentId: string, appId: string): Promise>; + + protected abstract findRooms(visitor: IVisitor, departmentId: string | null, appId: string): Promise>; + + protected abstract findDepartmentByIdOrName(value: string, appId: string): Promise; + + protected abstract findDepartmentsEnabledWithAgents(appId: string): Promise>; + + protected abstract _fetchLivechatRoomMessages(appId: string, roomId: string): Promise>; + + protected abstract setCustomFields( + data: { token: IVisitor['token']; key: string; value: string; overwrite: boolean }, + appId: string, + ): Promise; + + private hasReadPermission(appId: string, scope: LivechatReadPermissions): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions[scope].read)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions[scope].read], + }), + ); + + return false; + } + + private hasWritePermission(appId: string, scope: LivechatWritePermissions): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions[scope].write)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions[scope].write], + }), + ); + + return false; + } + + private hasMultiplePermission(appId: string, scope: LivechatMultiplePermissions): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions[scope].multiple)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions[scope].multiple], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/MessageBridge.ts b/packages/apps/src/server/bridges/MessageBridge.ts new file mode 100644 index 0000000000000..9d714f1339bcd --- /dev/null +++ b/packages/apps/src/server/bridges/MessageBridge.ts @@ -0,0 +1,116 @@ +import { BaseBridge } from './BaseBridge'; +import type { ITypingOptions } from '@rocket.chat/apps-engine/definition/accessors/INotifier'; +import type { IMessage, Reaction } from '@rocket.chat/apps-engine/definition/messages'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export interface ITypingDescriptor extends ITypingOptions { + isTyping: boolean; +} + +export abstract class MessageBridge extends BaseBridge { + public async doCreate(message: IMessage, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.create(message, appId); + } + } + + public async doUpdate(message: IMessage, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.update(message, appId); + } + } + + public async doNotifyUser(user: IUser, message: IMessage, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.notifyUser(user, message, appId); + } + } + + public async doNotifyRoom(room: IRoom, message: IMessage, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.notifyRoom(room, message, appId); + } + } + + public async doTyping(options: ITypingDescriptor, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.typing(options, appId); + } + } + + public async doGetById(messageId: string, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getById(messageId, appId); + } + } + + public async doDelete(message: IMessage, user: IUser, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.delete(message, user, appId); + } + } + + public async doAddReaction(messageId: string, userId: string, reaction: Reaction, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.addReaction(messageId, userId, reaction); + } + } + + public async doRemoveReaction(messageId: string, userId: string, reaction: Reaction, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.removeReaction(messageId, userId, reaction); + } + } + + protected abstract create(message: IMessage, appId: string): Promise; + + protected abstract update(message: IMessage, appId: string): Promise; + + protected abstract notifyUser(user: IUser, message: IMessage, appId: string): Promise; + + protected abstract notifyRoom(room: IRoom, message: IMessage, appId: string): Promise; + + protected abstract typing(options: ITypingDescriptor, appId: string): Promise; + + protected abstract getById(messageId: string, appId: string): Promise; + + protected abstract delete(message: IMessage, user: IUser, appId: string): Promise; + + protected abstract addReaction(messageId: string, userId: string, reaction: Reaction): Promise; + + protected abstract removeReaction(messageId: string, userId: string, reaction: Reaction): Promise; + + private hasReadPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.message.read)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.message.read], + }), + ); + + return false; + } + + private hasWritePermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.message.write)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.message.write], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/ModerationBridge.ts b/packages/apps/src/server/bridges/ModerationBridge.ts new file mode 100644 index 0000000000000..1ad81ece722cf --- /dev/null +++ b/packages/apps/src/server/bridges/ModerationBridge.ts @@ -0,0 +1,47 @@ +import { BaseBridge } from './BaseBridge'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export abstract class ModerationBridge extends BaseBridge { + public async doReport(messageId: IMessage['id'], description: string, userId: string, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.report(messageId, description, userId, appId); + } + } + + public async doDismissReportsByMessageId(messageId: IMessage['id'], reason: string, action: string, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.dismissReportsByMessageId(messageId, reason, action, appId); + } + } + + public async doDismissReportsByUserId(userId: IUser['id'], reason: string, action: string, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.dismissReportsByUserId(userId, reason, action, appId); + } + } + + protected abstract report(messageId: string, description: string, userId: string, appId: string): Promise; + + protected abstract dismissReportsByMessageId(messageId: string, reason: string, action: string, appId: string): Promise; + + protected abstract dismissReportsByUserId(userId: string, reason: string, action: string, appId: string): Promise; + + private hasWritePermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.moderation.write)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.moderation.write], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/OAuthAppsBridge.ts b/packages/apps/src/server/bridges/OAuthAppsBridge.ts new file mode 100644 index 0000000000000..d8b193b6f649b --- /dev/null +++ b/packages/apps/src/server/bridges/OAuthAppsBridge.ts @@ -0,0 +1,85 @@ +import { BaseBridge } from './BaseBridge'; +import type { IOAuthApp, IOAuthAppParams } from '@rocket.chat/apps-engine/definition/accessors/IOAuthApp'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export abstract class OAuthAppsBridge extends BaseBridge { + public async doCreate(oAuthApp: IOAuthAppParams, appId: string) { + if (this.hasWritePermission(appId)) { + return this.create(oAuthApp, appId); + } + } + + public async doGetByid(id: string, appId: string) { + if (this.hasReadPermission(appId)) { + return this.getById(id, appId); + } + } + + public async doGetByName(name: string, appId: string) { + if (this.hasReadPermission(appId)) { + return this.getByName(name, appId); + } + } + + public async doUpdate(oAuthApp: IOAuthAppParams, id: string, appId: string) { + if (this.hasWritePermission(appId)) { + return this.update(oAuthApp, id, appId); + } + } + + public async doDelete(id: string, appId: string) { + if (this.hasWritePermission(appId)) { + return this.delete(id, appId); + } + } + + public async doPurge(appId: string) { + if (this.hasWritePermission(appId)) { + return this.purge(appId); + } + } + + protected abstract create(oAuthApp: IOAuthAppParams, appId: string): Promise; + + protected abstract getById(id: string, appId: string): Promise; + + protected abstract getByName(name: string, appId: string): Promise>; + + protected abstract update(oAuthApp: IOAuthAppParams, id: string, appId: string): Promise; + + protected abstract delete(id: string, appId: string): Promise; + + protected abstract purge(appId: string): Promise; + + private hasWritePermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions['oauth-app'].write)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions['oauth-app'].write], + }), + ); + + return false; + } + + private hasReadPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions['oauth-app'].read)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions['oauth-app'].read], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/OutboundMessagesBridge.ts b/packages/apps/src/server/bridges/OutboundMessagesBridge.ts new file mode 100644 index 0000000000000..bd45312a468d9 --- /dev/null +++ b/packages/apps/src/server/bridges/OutboundMessagesBridge.ts @@ -0,0 +1,50 @@ +import { BaseBridge } from './BaseBridge'; +import type { + IOutboundEmailMessageProvider, + IOutboundMessageProviders, + IOutboundPhoneMessageProvider, +} from '@rocket.chat/apps-engine/definition/outboundCommunication'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export abstract class OutboundMessageBridge extends BaseBridge { + public async doRegisterPhoneProvider(info: IOutboundPhoneMessageProvider, appId: string): Promise { + if (this.hasProviderPermission(appId)) { + return this.registerPhoneProvider(info, appId); + } + } + + public async doRegisterEmailProvider(info: IOutboundEmailMessageProvider, appId: string): Promise { + if (this.hasProviderPermission(appId)) { + return this.registerEmailProvider(info, appId); + } + } + + public async doUnRegisterProvider(info: IOutboundMessageProviders, appId: string): Promise { + if (this.hasProviderPermission(appId)) { + return this.unRegisterProvider(info, appId); + } + } + + private hasProviderPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.outboundComms.provide)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.outboundComms.provide], + }), + ); + + return false; + } + + protected abstract registerPhoneProvider(info: IOutboundPhoneMessageProvider, appId: string): Promise; + + protected abstract registerEmailProvider(info: IOutboundEmailMessageProvider, appId: string): Promise; + + protected abstract unRegisterProvider(info: IOutboundMessageProviders, appId: string): Promise; +} diff --git a/packages/apps/src/server/bridges/PersistenceBridge.ts b/packages/apps/src/server/bridges/PersistenceBridge.ts new file mode 100644 index 0000000000000..7380cfdac46ba --- /dev/null +++ b/packages/apps/src/server/bridges/PersistenceBridge.ts @@ -0,0 +1,174 @@ +import { BaseBridge } from './BaseBridge'; +import type { RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export abstract class PersistenceBridge extends BaseBridge { + public async doPurge(appId: string): Promise { + if (this.hasDefaultPermission(appId)) { + return this.purge(appId); + } + } + + public async doCreate(data: object, appId: string): Promise { + if (this.hasDefaultPermission(appId)) { + return this.create(data, appId); + } + } + + public async doCreateWithAssociations(data: object, associations: Array, appId: string): Promise { + if (this.hasDefaultPermission(appId)) { + return this.createWithAssociations(data, associations, appId); + } + } + + public async doReadById(id: string, appId: string): Promise { + if (this.hasDefaultPermission(appId)) { + return this.readById(id, appId); + } + } + + public async doReadByAssociations(associations: Array, appId: string): Promise> { + if (this.hasDefaultPermission(appId)) { + return this.readByAssociations(associations, appId); + } + } + + public async doRemove(id: string, appId: string): Promise { + if (this.hasDefaultPermission(appId)) { + return this.remove(id, appId); + } + } + + public async doRemoveByAssociations(associations: Array, appId: string): Promise | undefined> { + if (this.hasDefaultPermission(appId)) { + return this.removeByAssociations(associations, appId); + } + } + + public async doUpdate(id: string, data: object, upsert: boolean, appId: string): Promise { + if (this.hasDefaultPermission(appId)) { + return this.update(id, data, upsert, appId); + } + } + + public async doUpdateByAssociations( + associations: Array, + data: object, + upsert: boolean, + appId: string, + ): Promise { + if (this.hasDefaultPermission(appId)) { + return this.updateByAssociations(associations, data, upsert, appId); + } + } + + /** + * Purges the App's persistant storage data from the persistent storage. + * + * @argument appId the id of the app's data to remove + */ + protected abstract purge(appId: string): Promise; + + /** + * Creates a new persistant record with the provided data attached. + * + * @argument data the data to store in persistent storage + * @argument appId the id of the app which is storing the data + * @returns the id of the stored record + */ + protected abstract create(data: object, appId: string): Promise; + + /** + * Creates a new record in the App's persistent storage with the data being + * associated with at least one Rocket.Chat record. + * + * @argument data the data to store in the persistent storage + * @argument associations the associations records this data is associated with + * @argument appId the id of the app which is storing the data + * @returns the id of the stored record + */ + protected abstract createWithAssociations(data: object, associations: Array, appId: string): Promise; + + /** + * Retrieves from the persistent storage the record by the id provided. + * + * @argument id the record id to read + * @argument appId the id of the app calling this + * @returns the data stored in the persistent storage, or undefined + */ + protected abstract readById(id: string, appId: string): Promise; + + /** + * Retrieves the data which is associated with the provided records. + * + * @argument associations the association records to query about + * @argument appId the id of the app calling this + * @returns an array of records if they exist, an empty array otherwise + */ + protected abstract readByAssociations(associations: Array, appId: string): Promise>; + + /** + * Removes the record which matches the provided id. + * + * @argument id the id of the record + * @argument appId the id of the app calling this + * @returns the data being removed + */ + protected abstract remove(id: string, appId: string): Promise; + + /** + * Removes any data which has been associated with the provided records. + * + * @argument associations the associations which to remove records + * @argument appId the id of the app calling this + * @returns the data of the removed records + */ + protected abstract removeByAssociations( + associations: Array, + appId: string, + ): Promise | undefined>; + + /** + * Updates the record in the database, with the option of creating a new one if it doesn't exist. + * + * @argument id the id of the record to update + * @argument data the updated data to set in the record + * @argument upsert whether to create if the id doesn't exist + * @argument appId the id of the app calling this + * @returns the id, whether the new one or the existing one + */ + protected abstract update(id: string, data: object, upsert: boolean, appId: string): Promise; + + /** + * Updates the record in the database, with the option of creating a new one if it doesn't exist. + * + * @argument associations the association records to update + * @argument data the updated data to set in the record + * @argument upsert whether to create if the id doesn't exist + * @argument appId the id of the app calling this + * @returns the id, whether the new one or the existing one + */ + protected abstract updateByAssociations( + associations: Array, + data: object, + upsert: boolean, + appId: string, + ): Promise; + + private hasDefaultPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.persistence.default)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.persistence.default], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/RoleBridge.ts b/packages/apps/src/server/bridges/RoleBridge.ts new file mode 100644 index 0000000000000..14fcbe2a1032d --- /dev/null +++ b/packages/apps/src/server/bridges/RoleBridge.ts @@ -0,0 +1,38 @@ +import { BaseBridge } from './BaseBridge'; +import type { IRole } from '@rocket.chat/apps-engine/definition/roles'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export abstract class RoleBridge extends BaseBridge { + public async doGetOneByIdOrName(idOrName: IRole['id'] | IRole['name'], appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getOneByIdOrName(idOrName, appId); + } + } + + public async doGetCustomRoles(appId: string): Promise> { + if (this.hasReadPermission(appId)) { + return this.getCustomRoles(appId); + } + } + + protected abstract getOneByIdOrName(idOrName: IRole['id'] | IRole['name'], appId: string): Promise; + + protected abstract getCustomRoles(appId: string): Promise>; + + private hasReadPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.role.read)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.role.read], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/RoomBridge.ts b/packages/apps/src/server/bridges/RoomBridge.ts new file mode 100644 index 0000000000000..5cef99d5ed08a --- /dev/null +++ b/packages/apps/src/server/bridges/RoomBridge.ts @@ -0,0 +1,252 @@ +import { BaseBridge } from './BaseBridge'; +import type { IMessage, IMessageRaw } from '@rocket.chat/apps-engine/definition/messages'; +import type { IRoom, IRoomRaw, RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export const GetMessagesSortableFields = ['createdAt'] as const; + +export type GetMessagesOptions = { + limit: number; + skip: number; + sort: Record<(typeof GetMessagesSortableFields)[number], 'asc' | 'desc'>; + showThreadMessages: boolean; +}; + +/** + * Filters for querying rooms in the system. + */ +export type GetRoomsFilters = { + /** + * When specified, only rooms matching the provided types will be returned. + */ + types?: Array; + /** + * Filter to include or exclude discussion rooms. + * + * When undefined (default), discussions are included in the result set. + * + * When true, ONLY discussions are included in the result set (remove non-discussions). + * When false, discussion rooms are excluded from the result set. + */ + discussions?: boolean; + /** + * Filter to include or exclude team main rooms. + * + * When undefined (default), team main rooms are included in the result set. + * + * When true, ONLY team main rooms are included in the result set (remove non-teams). + * When false, team main rooms are excluded from the result set. + */ + teams?: boolean; +}; + +export type GetRoomsOptions = { + limit?: number; + skip?: number; +}; + +export abstract class RoomBridge extends BaseBridge { + public async doCreate(room: IRoom, members: Array, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.create(room, members, appId); + } + } + + public async doGetById(roomId: string, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getById(roomId, appId); + } + } + + public async doGetByName(roomName: string, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getByName(roomName, appId); + } + } + + public async doGetCreatorById(roomId: string, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getCreatorById(roomId, appId); + } + } + + public async doGetCreatorByName(roomName: string, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getCreatorByName(roomName, appId); + } + } + + public async doGetDirectByUsernames(usernames: Array, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getDirectByUsernames(usernames, appId); + } + } + + public async doGetMembers(roomId: string, appId: string): Promise> { + if (this.hasReadPermission(appId)) { + return this.getMembers(roomId, appId); + } + } + + public async doGetAllRooms(filters: GetRoomsFilters = {}, options: GetRoomsOptions = {}, appId: string): Promise | undefined> { + if (this.hasViewAllRoomsPermission(appId)) { + return this.getAllRooms(filters, options, appId); + } + } + + public async doUpdate(room: IRoom, members: Array, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.update(room, members, appId); + } + } + + public async doCreateDiscussion( + room: IRoom, + parentMessage: IMessage | undefined, + reply: string | undefined, + members: Array, + appId: string, + ): Promise { + if (this.hasWritePermission(appId)) { + return this.createDiscussion(room, parentMessage, reply, members, appId); + } + } + + public async doDelete(room: string, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.delete(room, appId); + } + } + + public async doGetModerators(roomId: string, appId: string): Promise> { + if (this.hasReadPermission(appId)) { + return this.getModerators(roomId, appId); + } + } + + public async doGetOwners(roomId: string, appId: string): Promise> { + if (this.hasReadPermission(appId)) { + return this.getOwners(roomId, appId); + } + } + + public async doGetLeaders(roomId: string, appId: string): Promise> { + if (this.hasReadPermission(appId)) { + return this.getLeaders(roomId, appId); + } + } + + public async doGetMessages(roomId: string, options: GetMessagesOptions, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getMessages(roomId, options, appId); + } + } + + public async doRemoveUsers(roomId: string, usernames: Array, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.removeUsers(roomId, usernames, appId); + } + } + + public async doGetUnreadByUser(roomId: string, uid: string, options: GetMessagesOptions, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getUnreadByUser(roomId, uid, options, appId); + } + } + + public async doGetUserUnreadMessageCount(roomId: string, uid: string, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getUserUnreadMessageCount(roomId, uid, appId); + } + } + + protected abstract create(room: IRoom, members: Array, appId: string): Promise; + + protected abstract getById(roomId: string, appId: string): Promise; + + protected abstract getByName(roomName: string, appId: string): Promise; + + protected abstract getCreatorById(roomId: string, appId: string): Promise; + + protected abstract getCreatorByName(roomName: string, appId: string): Promise; + + protected abstract getDirectByUsernames(usernames: Array, appId: string): Promise; + + protected abstract getMembers(roomId: string, appId: string): Promise>; + + protected abstract getAllRooms(filters: GetRoomsFilters, options: GetRoomsOptions, appId: string): Promise>; + + protected abstract update(room: IRoom, members: Array, appId: string): Promise; + + protected abstract createDiscussion( + room: IRoom, + parentMessage: IMessage | undefined, + reply: string | undefined, + members: Array, + appId: string, + ): Promise; + + protected abstract delete(room: string, appId: string): Promise; + + protected abstract getModerators(roomId: string, appId: string): Promise>; + + protected abstract getOwners(roomId: string, appId: string): Promise>; + + protected abstract getLeaders(roomId: string, appId: string): Promise>; + + protected abstract getMessages(roomId: string, options: GetMessagesOptions, appId: string): Promise; + + protected abstract removeUsers(roomId: string, usernames: Array, appId: string): Promise; + + protected abstract getUnreadByUser(roomId: string, uid: string, options: GetMessagesOptions, appId: string): Promise; + + protected abstract getUserUnreadMessageCount(roomId: string, uid: string, appId: string): Promise; + + private hasWritePermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.room.write)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.room.write], + }), + ); + + return false; + } + + private hasReadPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.room.read)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.room.read], + }), + ); + + return false; + } + + private hasViewAllRoomsPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.room['system-view-all'])) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.room['system-view-all']], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/SchedulerBridge.ts b/packages/apps/src/server/bridges/SchedulerBridge.ts new file mode 100644 index 0000000000000..02b32ba61ceb9 --- /dev/null +++ b/packages/apps/src/server/bridges/SchedulerBridge.ts @@ -0,0 +1,62 @@ +import { BaseBridge } from './BaseBridge'; +import type { IOnetimeSchedule, IProcessor, IRecurringSchedule } from '@rocket.chat/apps-engine/definition/scheduler'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export abstract class SchedulerBridge extends BaseBridge { + public async doRegisterProcessors(processors: Array = [], appId: string): Promise> { + if (this.hasDefaultPermission(appId)) { + return this.registerProcessors(processors, appId); + } + } + + public async doScheduleOnce(job: IOnetimeSchedule, appId: string): Promise { + if (this.hasDefaultPermission(appId)) { + return this.scheduleOnce(job, appId); + } + } + + public async doScheduleRecurring(job: IRecurringSchedule, appId: string): Promise { + if (this.hasDefaultPermission(appId)) { + return this.scheduleRecurring(job, appId); + } + } + + public async doCancelJob(jobId: string, appId: string): Promise { + if (this.hasDefaultPermission(appId)) { + return this.cancelJob(jobId, appId); + } + } + + public async doCancelAllJobs(appId: string): Promise { + if (this.hasDefaultPermission(appId)) { + return this.cancelAllJobs(appId); + } + } + + protected abstract registerProcessors(processors: Array, appId: string): Promise>; + + protected abstract scheduleOnce(job: IOnetimeSchedule, appId: string): Promise; + + protected abstract scheduleRecurring(job: IRecurringSchedule, appId: string): Promise; + + protected abstract cancelJob(jobId: string, appId: string): Promise; + + protected abstract cancelAllJobs(appId: string): Promise; + + private hasDefaultPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.scheduler.default)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.scheduler.default], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/ServerSettingBridge.ts b/packages/apps/src/server/bridges/ServerSettingBridge.ts new file mode 100644 index 0000000000000..0508937371206 --- /dev/null +++ b/packages/apps/src/server/bridges/ServerSettingBridge.ts @@ -0,0 +1,93 @@ +import { BaseBridge } from './BaseBridge'; +import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export abstract class ServerSettingBridge extends BaseBridge { + public async doGetAll(appId: string): Promise> { + if (this.hasReadPermission(appId)) { + return this.getAll(appId); + } + } + + public async doGetOneById(id: string, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getOneById(id, appId); + } + } + + public async doHideGroup(name: string, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.hideGroup(name, appId); + } + } + + public async doHideSetting(id: string, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.hideSetting(id, appId); + } + } + + public async doIsReadableById(id: string, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.isReadableById(id, appId); + } + } + + public async doUpdateOne(setting: ISetting, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.updateOne(setting, appId); + } + } + + public async doIncrementValue(id: ISetting['id'], value: number, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.incrementValue(id, value, appId); + } + } + + protected abstract getAll(appId: string): Promise>; + + protected abstract getOneById(id: string, appId: string): Promise; + + protected abstract hideGroup(name: string, appId: string): Promise; + + protected abstract hideSetting(id: string, appId: string): Promise; + + protected abstract isReadableById(id: string, appId: string): Promise; + + protected abstract updateOne(setting: ISetting, appId: string): Promise; + + protected abstract incrementValue(id: ISetting['id'], value: number, appId: string): Promise; + + private hasWritePermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.setting.write)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.setting.write], + }), + ); + + return false; + } + + private hasReadPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.setting.read)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.setting.read], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/ThreadBridge.ts b/packages/apps/src/server/bridges/ThreadBridge.ts new file mode 100644 index 0000000000000..da812fc01bbd8 --- /dev/null +++ b/packages/apps/src/server/bridges/ThreadBridge.ts @@ -0,0 +1,35 @@ +import { BaseBridge } from './BaseBridge'; +import type { ITypingOptions } from '@rocket.chat/apps-engine/definition/accessors/INotifier'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export interface ITypingDescriptor extends ITypingOptions { + isTyping: boolean; +} + +export abstract class ThreadBridge extends BaseBridge { + public async doGetById(messageId: string, appId: string): Promise> { + if (this.hasReadPermission(appId)) { + return this.getById(messageId, appId); + } + } + + protected abstract getById(messageId: string, appId: string): Promise>; + + private hasReadPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.threads.read)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.threads.read], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/UiInteractionBridge.ts b/packages/apps/src/server/bridges/UiInteractionBridge.ts new file mode 100644 index 0000000000000..eee981d3ff850 --- /dev/null +++ b/packages/apps/src/server/bridges/UiInteractionBridge.ts @@ -0,0 +1,31 @@ +import { BaseBridge } from './BaseBridge'; +import type { IUIKitInteraction } from '@rocket.chat/apps-engine/definition/uikit'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export abstract class UiInteractionBridge extends BaseBridge { + public async doNotifyUser(user: IUser, interaction: IUIKitInteraction, appId: string): Promise { + if (this.hasInteractionPermission(appId)) { + return this.notifyUser(user, interaction, appId); + } + } + + protected abstract notifyUser(user: IUser, interaction: IUIKitInteraction, appId: string): Promise; + + private hasInteractionPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.ui.interaction)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.ui.interaction], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/UploadBridge.ts b/packages/apps/src/server/bridges/UploadBridge.ts new file mode 100644 index 0000000000000..68806a2c5b121 --- /dev/null +++ b/packages/apps/src/server/bridges/UploadBridge.ts @@ -0,0 +1,62 @@ +import { BaseBridge } from './BaseBridge'; +import type { IUpload } from '@rocket.chat/apps-engine/definition/uploads'; +import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export abstract class UploadBridge extends BaseBridge { + public async doGetById(id: string, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getById(id, appId); + } + } + + public async doGetBuffer(upload: IUpload, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getBuffer(upload, appId); + } + } + + public async doCreateUpload(details: IUploadDetails, buffer: Buffer, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.createUpload(details, buffer, appId); + } + } + + protected abstract getById(id: string, appId: string): Promise; + + protected abstract getBuffer(upload: IUpload, appId: string): Promise; + + protected abstract createUpload(details: IUploadDetails, buffer: Buffer, appId: string): Promise; + + private hasReadPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.upload.read)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.upload.read], + }), + ); + + return false; + } + + private hasWritePermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.upload.write)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.upload.write], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/UserBridge.ts b/packages/apps/src/server/bridges/UserBridge.ts new file mode 100644 index 0000000000000..0d31c556da382 --- /dev/null +++ b/packages/apps/src/server/bridges/UserBridge.ts @@ -0,0 +1,157 @@ +import { BaseBridge } from './BaseBridge'; +import type { IUser, IUserCreationOptions, UserType } from '@rocket.chat/apps-engine/definition/users'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export abstract class UserBridge extends BaseBridge { + public async doGetById(id: string, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getById(id, appId); + } + } + + public async doGetByUsername(username: string, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getByUsername(username, appId); + } + } + + public async doGetAppUser(appId?: string): Promise { + return this.getAppUser(appId); + } + + public async doCreate(data: Partial, appId: string, options?: IUserCreationOptions): Promise { + if (this.hasWritePermission(appId)) { + return this.create(data, appId, options || {}); + } + } + + public async doRemove(user: IUser, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.remove(user, appId); + } + } + + public async doUpdate(user: IUser, updates: Partial, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.update(user, updates, appId); + } + } + + public async doGetUserUnreadMessageCount(uid: string, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getUserUnreadMessageCount(uid, appId); + } + } + + public async doGetUserRoomIds(userId: string, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getUserRoomIds(userId, appId); + } + } + + public async doDeleteUsersCreatedByApp(appId: string, type: UserType.BOT | UserType.APP): Promise { + if (this.hasWritePermission(appId)) { + return this.deleteUsersCreatedByApp(appId, type); + } + } + + public async doDeactivate(userId: IUser['id'], confirmRelinquish: boolean, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.deactivate(userId, confirmRelinquish, appId); + } + } + + protected abstract getById(id: string, appId: string): Promise; + + protected abstract getByUsername(username: string, appId: string): Promise; + + protected abstract getAppUser(appId?: string): Promise; + + protected abstract getActiveUserCount(): Promise; + + protected abstract getUserUnreadMessageCount(uid: string, appId: string): Promise; + + protected abstract getUserRoomIds(userId: string, appId: string): Promise; + + /** + * Creates a user. + * @param data the essential data for creating a user + * @param appId the id of the app calling this + * @param options options for passing extra data + */ + protected abstract create(data: Partial, appId: string, options?: IUserCreationOptions): Promise; + + /** + * Remove a user. + * + * @param user the user object to be removed + * @param appId the id of the app executing the call + */ + protected abstract remove(user: IUser, appId: string): Promise; + + /** + * Updates a user. + * + * Note: the actual methods used by apps to update + * user properties are much more granular, but at a + * bridge level we can adopt a more practical approach + * since it is only accessible internally by the framework + * + * @param user the user to be updated + * @param updates a map of properties to be updated + * @param appId the id of the app executing the call + */ + protected abstract update(user: IUser, updates: Partial, appId: string): Promise; + + /** + * Deletes all bot or app users created by the App. + * @param appId the App's ID. + * @param type the type of the user to be deleted. + * @returns true if any user was deleted, false otherwise. + */ + protected abstract deleteUsersCreatedByApp(appId: string, type: UserType.APP | UserType.BOT): Promise; + + /** + * Deactivates a user. + * @param userId the user's ID. + * @param confirmRelinquish whether the user confirmed the relinquish of the account. + * @param appId the App's ID. + * @returns true if the user was deactivated, false otherwise. + * @throws {Error} if the user is not found. + * @throws {Error} if the user is the last admin. + * @throws {Error} if the user is the last owner, if confirmRelinquish is false. + */ + protected abstract deactivate(userId: IUser['id'], confirmRelinquish: boolean, appId: string): Promise; + + private hasReadPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.user.read)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.user.read], + }), + ); + + return false; + } + + private hasWritePermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.user.write)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.user.write], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/VideoConferenceBridge.ts b/packages/apps/src/server/bridges/VideoConferenceBridge.ts new file mode 100644 index 0000000000000..d65a5e664980e --- /dev/null +++ b/packages/apps/src/server/bridges/VideoConferenceBridge.ts @@ -0,0 +1,94 @@ +import { BaseBridge } from './BaseBridge'; +import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders'; +import type { AppVideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/AppVideoConference'; +import type { VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export abstract class VideoConferenceBridge extends BaseBridge { + public async doGetById(callId: string, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getById(callId, appId); + } + } + + public async doCreate(call: AppVideoConference, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.create(call, appId); + } + } + + public async doUpdate(call: VideoConference, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.update(call, appId); + } + } + + public async doRegisterProvider(info: IVideoConfProvider, appId: string): Promise { + if (this.hasProviderPermission(appId)) { + return this.registerProvider(info, appId); + } + } + + public async doUnRegisterProvider(info: IVideoConfProvider, appId: string): Promise { + if (this.hasProviderPermission(appId)) { + return this.unRegisterProvider(info, appId); + } + } + + protected abstract create(call: AppVideoConference, appId: string): Promise; + + protected abstract getById(callId: string, appId: string): Promise; + + protected abstract update(call: VideoConference, appId: string): Promise; + + protected abstract registerProvider(info: IVideoConfProvider, appId: string): Promise; + + protected abstract unRegisterProvider(info: IVideoConfProvider, appId: string): Promise; + + private hasWritePermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.videoConference.write)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.videoConference.write], + }), + ); + + return false; + } + + private hasReadPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.videoConference.read)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.videoConference.read], + }), + ); + + return false; + } + + private hasProviderPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.videoConference.provider)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.videoConference.provider], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/bridges/index.ts b/packages/apps/src/server/bridges/index.ts new file mode 100644 index 0000000000000..c472ad2293acb --- /dev/null +++ b/packages/apps/src/server/bridges/index.ts @@ -0,0 +1,58 @@ +import { ApiBridge } from './ApiBridge'; +import { AppActivationBridge } from './AppActivationBridge'; +import { AppBridges } from './AppBridges'; +import { AppDetailChangesBridge } from './AppDetailChangesBridge'; +import { CloudWorkspaceBridge } from './CloudWorkspaceBridge'; +import { CommandBridge } from './CommandBridge'; +import { ContactBridge } from './ContactBridge'; +import { EmailBridge } from './EmailBridge'; +import { EnvironmentalVariableBridge } from './EnvironmentalVariableBridge'; +import { ExperimentalBridge } from './ExperimentalBridge'; +import { HttpBridge, IHttpBridgeRequestInfo } from './HttpBridge'; +import { IInternalBridge } from './IInternalBridge'; +import { IInternalFederationBridge } from './IInternalFederationBridge'; +import { IListenerBridge } from './IListenerBridge'; +import { LivechatBridge } from './LivechatBridge'; +import { MessageBridge } from './MessageBridge'; +import { ModerationBridge } from './ModerationBridge'; +import { OutboundMessageBridge } from './OutboundMessagesBridge'; +import { PersistenceBridge } from './PersistenceBridge'; +import { RoleBridge } from './RoleBridge'; +import { RoomBridge } from './RoomBridge'; +import { SchedulerBridge } from './SchedulerBridge'; +import { ServerSettingBridge } from './ServerSettingBridge'; +import { UiInteractionBridge } from './UiInteractionBridge'; +import { UploadBridge } from './UploadBridge'; +import { UserBridge } from './UserBridge'; +import { VideoConferenceBridge } from './VideoConferenceBridge'; + +export { + CloudWorkspaceBridge, + ContactBridge, + EnvironmentalVariableBridge, + HttpBridge, + IHttpBridgeRequestInfo, + IListenerBridge, + LivechatBridge, + MessageBridge, + PersistenceBridge, + AppActivationBridge, + AppDetailChangesBridge, + CommandBridge, + ApiBridge, + RoomBridge, + IInternalBridge, + ServerSettingBridge, + UserBridge, + UploadBridge, + EmailBridge, + ExperimentalBridge, + UiInteractionBridge, + SchedulerBridge, + AppBridges, + VideoConferenceBridge, + IInternalFederationBridge, + ModerationBridge, + RoleBridge, + OutboundMessageBridge, +}; diff --git a/packages/apps/src/server/compiler/AppCompiler.ts b/packages/apps/src/server/compiler/AppCompiler.ts new file mode 100644 index 0000000000000..9ae3b12ba67a0 --- /dev/null +++ b/packages/apps/src/server/compiler/AppCompiler.ts @@ -0,0 +1,30 @@ +import * as path from 'path'; + +import type { AppManager } from '../AppManager'; +import { ProxiedApp } from '../ProxiedApp'; +import type { IAppStorageItem } from '../storage'; +import type { IParseAppPackageResult } from './IParseAppPackageResult'; + +export class AppCompiler { + public normalizeStorageFiles(files: { [key: string]: string }): { [key: string]: string } { + const result: { [key: string]: string } = {}; + + Object.entries(files).forEach(([name, content]) => { + result[name.replace(/\$/g, '.')] = content; + }); + + return result; + } + + public async toSandBox(manager: AppManager, storage: IAppStorageItem, packageResult: IParseAppPackageResult): Promise { + if (typeof packageResult.files[path.normalize(storage.info.classFile)] === 'undefined') { + throw new Error(`Invalid App package for "${storage.info.name}". Could not find the classFile (${storage.info.classFile}) file.`); + } + + const runtime = await manager.getRuntime().startRuntimeForApp(packageResult, storage); + + const app = new ProxiedApp(manager, storage, runtime); + + return app; + } +} diff --git a/packages/apps/src/server/compiler/AppFabricationFulfillment.ts b/packages/apps/src/server/compiler/AppFabricationFulfillment.ts new file mode 100644 index 0000000000000..bc528080bf6df --- /dev/null +++ b/packages/apps/src/server/compiler/AppFabricationFulfillment.ts @@ -0,0 +1,75 @@ +import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; +import type { ProxiedApp } from '../ProxiedApp'; +import { AppLicenseValidationResult } from '../marketplace/license'; + +export class AppFabricationFulfillment { + public info: IAppInfo; + + public app: ProxiedApp; + + public implemented: { [int: string]: boolean }; + + public licenseValidationResult: AppLicenseValidationResult; + + public storageError: string; + + public appUserError: object; + + constructor() { + this.licenseValidationResult = new AppLicenseValidationResult(); + } + + public setAppInfo(information: IAppInfo): void { + this.info = structuredClone(information); + + this.licenseValidationResult.setAppId(information.id); + } + + public getAppInfo(): IAppInfo { + return this.info; + } + + public setApp(application: ProxiedApp): void { + this.app = application; + } + + public getApp(): ProxiedApp { + return this.app; + } + + public setImplementedInterfaces(interfaces: { [int: string]: boolean }): void { + this.implemented = structuredClone(interfaces); + } + + public getImplementedInferfaces(): { [int: string]: boolean } { + return this.implemented; + } + + public setStorageError(errorMessage: string): void { + this.storageError = errorMessage; + } + + public setAppUserError(error: object): void { + this.appUserError = error; + } + + public getStorageError(): string { + return this.storageError; + } + + public getAppUserError(): object { + return this.appUserError; + } + + public hasStorageError(): boolean { + return !!this.storageError; + } + + public hasAppUserError(): boolean { + return !!this.appUserError; + } + + public getLicenseValidationResult(): AppLicenseValidationResult { + return this.licenseValidationResult; + } +} diff --git a/packages/apps/src/server/compiler/AppImplements.ts b/packages/apps/src/server/compiler/AppImplements.ts new file mode 100644 index 0000000000000..5b13e648c3269 --- /dev/null +++ b/packages/apps/src/server/compiler/AppImplements.ts @@ -0,0 +1,32 @@ +import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata/AppInterface'; +import { Utilities } from '../misc/Utilities'; + +export class AppImplements { + private implemented: Record; + + constructor() { + this.implemented = {} as Record; + + Object.keys(AppInterface).forEach((int: AppInterface) => { + this.implemented[int] = false; + }); + } + + public setImplements(int: AppInterface): void { + if (int in AppInterface) { + this.implemented[int] = true; + } + } + + public doesImplement(int: AppInterface): boolean { + return this.implemented[int]; + } + + public getValues(): Record { + return Utilities.deepCloneAndFreeze(this.implemented); + } + + public toJSON(): Record { + return this.getValues(); + } +} diff --git a/packages/apps/src/server/compiler/AppPackageParser.ts b/packages/apps/src/server/compiler/AppPackageParser.ts new file mode 100644 index 0000000000000..7ea364d19ea3b --- /dev/null +++ b/packages/apps/src/server/compiler/AppPackageParser.ts @@ -0,0 +1,163 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +import * as AdmZip from 'adm-zip'; +import * as semver from 'semver'; +import { v4 as uuidv4 } from 'uuid'; + +import { AppImplements } from '.'; +import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata/IAppInfo'; +import { RequiredApiVersionError } from '../errors'; +import type { IParseAppPackageResult } from './IParseAppPackageResult'; + +export class AppPackageParser { + public static uuid4Regex = /^[0-9a-fA-f]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/; + + private allowedIconExts: Array = ['.png', '.jpg', '.jpeg', '.gif']; + + private appsEngineVersion: string; + + constructor() { + this.appsEngineVersion = this.getEngineVersion(); + } + + public async unpackageApp(appPackage: Buffer): Promise { + const zip = new AdmZip(appPackage); + const infoZip = zip.getEntry('app.json'); + let info: IAppInfo; + + if (infoZip && !infoZip.isDirectory) { + try { + info = JSON.parse(infoZip.getData().toString()) as IAppInfo; + + if (!AppPackageParser.uuid4Regex.test(info.id)) { + info.id = uuidv4(); + console.warn( + 'WARNING: We automatically generated a uuid v4 id for', + info.name, + 'since it did not provide us an id. This is NOT', + 'recommended as the same App can be installed several times.', + ); + } + } catch (e) { + throw new Error('Invalid App package. The "app.json" file is not valid json.'); + } + } else { + throw new Error('Invalid App package. No "app.json" file.'); + } + + info.classFile = info.classFile.replace('.ts', '.js'); + + if (!semver.satisfies(this.appsEngineVersion, info.requiredApiVersion)) { + throw new RequiredApiVersionError(info, this.appsEngineVersion); + } + + // Load all of the TypeScript only files + const files: { [s: string]: string } = {}; + + zip + .getEntries() + .filter((entry) => !entry.isDirectory && entry.entryName.endsWith('.js')) + .forEach((entry) => { + const norm = path.normalize(entry.entryName); + + // Files which start with `.` are supposed to be hidden + if (norm.startsWith('.')) { + return; + } + + files[norm] = entry.getData().toString(); + }); + + // Ensure that the main class file exists + if (!files[path.normalize(info.classFile)]) { + throw new Error(`Invalid App package. Could not find the classFile (${info.classFile}) file.`); + } + + const languageContent = this.getLanguageContent(zip); + + // Get the icon's content + const iconFile = this.getIconFile(zip, info.iconFile); + if (iconFile) { + info.iconFileContent = iconFile; + } + + const implemented = new AppImplements(); + + if (Array.isArray(info.implements)) { + info.implements.forEach((interfaceName) => implemented.setImplements(interfaceName)); + } + + return { + info, + files, + languageContent, + implemented, + }; + } + + private getLanguageContent(zip: AdmZip): { [key: string]: object } { + const languageContent: { [key: string]: object } = {}; + + zip + .getEntries() + .filter((entry) => !entry.isDirectory && entry.entryName.startsWith('i18n/') && entry.entryName.endsWith('.json')) + .forEach((entry) => { + const entrySplit = entry.entryName.split('/'); + const lang = entrySplit[entrySplit.length - 1].split('.')[0].toLowerCase(); + + let content; + try { + content = JSON.parse(entry.getData().toString()); + } catch (e) { + // Failed to parse it, maybe warn them? idk yet + } + + languageContent[lang] = Object.assign(languageContent[lang] || {}, content); + }); + + return languageContent; + } + + private getIconFile(zip: AdmZip, filePath: string): string { + if (!filePath) { + return undefined; + } + + const ext = path.extname(filePath); + if (!this.allowedIconExts.includes(ext)) { + return undefined; + } + + const entry = zip.getEntry(filePath); + + if (!entry) { + return undefined; + } + + if (entry.isDirectory) { + return undefined; + } + + const base64 = entry.getData().toString('base64'); + + return `data:image/${ext.replace('.', '')};base64,${base64}`; + } + + private getEngineVersion(): string { + const devLocation = path.join(__dirname, '../../../package.json'); + const prodLocation = path.join(__dirname, '../../package.json'); + + let info: { version: string }; + + if (fs.existsSync(devLocation)) { + info = JSON.parse(fs.readFileSync(devLocation, 'utf8')); + } else if (fs.existsSync(prodLocation)) { + info = JSON.parse(fs.readFileSync(prodLocation, 'utf8')); + } else { + throw new Error('Could not find the Apps TypeScript Definition Package Version!'); + } + + return info.version.replace(/^[^0-9]/, '').split('-')[0]; + } +} diff --git a/packages/apps/src/server/compiler/IParseAppPackageResult.ts b/packages/apps/src/server/compiler/IParseAppPackageResult.ts new file mode 100644 index 0000000000000..0285c8908a46f --- /dev/null +++ b/packages/apps/src/server/compiler/IParseAppPackageResult.ts @@ -0,0 +1,9 @@ +import type { AppImplements } from './AppImplements'; +import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; + +export interface IParseAppPackageResult { + info: IAppInfo; + files: { [key: string]: string }; + languageContent: { [key: string]: object }; + implemented: AppImplements; +} diff --git a/packages/apps/src/server/compiler/index.ts b/packages/apps/src/server/compiler/index.ts new file mode 100644 index 0000000000000..466be8c786bf5 --- /dev/null +++ b/packages/apps/src/server/compiler/index.ts @@ -0,0 +1,7 @@ +import { AppCompiler } from './AppCompiler'; +import { AppFabricationFulfillment } from './AppFabricationFulfillment'; +import { AppImplements } from './AppImplements'; +import { AppPackageParser } from './AppPackageParser'; +import { IParseAppPackageResult } from './IParseAppPackageResult'; + +export { AppCompiler, AppFabricationFulfillment, AppImplements, AppPackageParser, IParseAppPackageResult }; diff --git a/packages/apps/src/server/compiler/modules/index.ts b/packages/apps/src/server/compiler/modules/index.ts new file mode 100644 index 0000000000000..31cbec5af30c9 --- /dev/null +++ b/packages/apps/src/server/compiler/modules/index.ts @@ -0,0 +1,55 @@ +import { moduleHandlerFactory } from './networking'; + +export enum AllowedInternalModules { + path = 'path', + url = 'url', + crypto = 'crypto', + buffer = 'buffer', + stream = 'stream', + net = 'net', + http = 'http', + https = 'https', + zlib = 'zlib', + util = 'util', + punycode = 'punycode', + os = 'os', + querystring = 'querystring', +} + +export class ForbiddenNativeModuleAccess extends Error { + constructor(module: string, prop: string) { + super(`Access to property ${prop} in module ${module} is forbidden`); + } +} + +const defaultHandler = () => ({}); + +const noopHandler = () => ({ + get: (): undefined => undefined, +}); + +const proxyHandlers = { + path: defaultHandler, + url: defaultHandler, + crypto: defaultHandler, + buffer: defaultHandler, + stream: defaultHandler, + net: moduleHandlerFactory('net'), + http: moduleHandlerFactory('http'), + https: moduleHandlerFactory('https'), + zlib: defaultHandler, + util: defaultHandler, + punycode: defaultHandler, + os: noopHandler, + querystring: defaultHandler, +}; + +export function requireNativeModule(module: AllowedInternalModules, appId: string, requirer: any) { + const requiredModule = requirer(module); + + return new Proxy( + requiredModule, + // Creates a proxy handler that is aware of the appId requiring the module + Reflect.apply(proxyHandlers[module], undefined, [appId]), + ); +} diff --git a/packages/apps/src/server/compiler/modules/networking.ts b/packages/apps/src/server/compiler/modules/networking.ts new file mode 100644 index 0000000000000..d8ed3c8e744b7 --- /dev/null +++ b/packages/apps/src/server/compiler/modules/networking.ts @@ -0,0 +1,36 @@ +import type * as http from 'http'; +import type * as https from 'https'; +import type * as net from 'net'; + +import { ForbiddenNativeModuleAccess } from '.'; +import { PermissionDeniedError } from '../../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../../managers/AppPermissionManager'; +import { AppPermissions } from '../../permissions/AppPermissions'; + +type IHttp = typeof http; +type IHttps = typeof https; +type INet = typeof net; + +type NetworkingLibs = IHttp | IHttps | INet; + +const networkingModuleBlockList = ['createServer', 'Server']; + +export const moduleHandlerFactory = (module: string) => { + return (appId: string): ProxyHandler => ({ + get(target, prop: string, receiver) { + if (networkingModuleBlockList.includes(prop)) { + throw new ForbiddenNativeModuleAccess(module, prop); + } + + if (!AppPermissionManager.hasPermission(appId, AppPermissions.networking.default)) { + throw new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.networking.default], + methodName: `${module}.${prop}`, + }); + } + + return Reflect.get(target, prop, receiver); + }, + }); +}; diff --git a/packages/apps/src/server/errors/AppOutboundProcessError.ts b/packages/apps/src/server/errors/AppOutboundProcessError.ts new file mode 100644 index 0000000000000..a7e905ae90b90 --- /dev/null +++ b/packages/apps/src/server/errors/AppOutboundProcessError.ts @@ -0,0 +1,12 @@ +export class AppOutboundProcessError implements Error { + public name = 'OutboundProviderError'; + + public message: string; + + constructor(message: string, where?: string) { + this.message = message; + if (where) { + this.message += ` (${where})`; + } + } +} diff --git a/packages/apps/src/server/errors/CommandAlreadyExistsError.ts b/packages/apps/src/server/errors/CommandAlreadyExistsError.ts new file mode 100644 index 0000000000000..8c49b4315e083 --- /dev/null +++ b/packages/apps/src/server/errors/CommandAlreadyExistsError.ts @@ -0,0 +1,9 @@ +export class CommandAlreadyExistsError implements Error { + public name = 'CommandAlreadyExists'; + + public message: string; + + constructor(command: string) { + this.message = `The command "${command}" already exists in the system.`; + } +} diff --git a/packages/apps/src/server/errors/CommandHasAlreadyBeenTouchedError.ts b/packages/apps/src/server/errors/CommandHasAlreadyBeenTouchedError.ts new file mode 100644 index 0000000000000..bbc0e39c02cea --- /dev/null +++ b/packages/apps/src/server/errors/CommandHasAlreadyBeenTouchedError.ts @@ -0,0 +1,9 @@ +export class CommandHasAlreadyBeenTouchedError implements Error { + public name = 'CommandHasAlreadyBeenTouched'; + + public message: string; + + constructor(command: string) { + this.message = `The command "${command}" has already been touched by another App.`; + } +} diff --git a/packages/apps/src/server/errors/CompilerError.ts b/packages/apps/src/server/errors/CompilerError.ts new file mode 100644 index 0000000000000..3d71ad50d443a --- /dev/null +++ b/packages/apps/src/server/errors/CompilerError.ts @@ -0,0 +1,9 @@ +export class CompilerError implements Error { + public name = 'CompilerError'; + + public message: string; + + constructor(detail: string) { + this.message = `An error occured while compiling an App: ${detail}`; + } +} diff --git a/packages/apps/src/server/errors/InvalidInstallationError.ts b/packages/apps/src/server/errors/InvalidInstallationError.ts new file mode 100644 index 0000000000000..4388261c17c91 --- /dev/null +++ b/packages/apps/src/server/errors/InvalidInstallationError.ts @@ -0,0 +1,5 @@ +export class InvalidInstallationError extends Error { + public constructor(message: string) { + super(`Invalid app installation: ${message}`); + } +} diff --git a/packages/apps/src/server/errors/InvalidLicenseError.ts b/packages/apps/src/server/errors/InvalidLicenseError.ts new file mode 100644 index 0000000000000..4ab70cdfc4dcf --- /dev/null +++ b/packages/apps/src/server/errors/InvalidLicenseError.ts @@ -0,0 +1,7 @@ +import type { AppLicenseValidationResult } from '../marketplace/license/AppLicenseValidationResult'; + +export class InvalidLicenseError extends Error { + public constructor(public readonly validationResult: AppLicenseValidationResult) { + super('Invalid app license'); + } +} diff --git a/packages/apps/src/server/errors/MustContainFunctionError.ts b/packages/apps/src/server/errors/MustContainFunctionError.ts new file mode 100644 index 0000000000000..bbe2d56a3d1cf --- /dev/null +++ b/packages/apps/src/server/errors/MustContainFunctionError.ts @@ -0,0 +1,9 @@ +export class MustContainFunctionError implements Error { + public name = 'MustContainFunction'; + + public message: string; + + constructor(fileName: string, funcName: string) { + this.message = `The App (${fileName}) doesn't have a "${funcName}" function which is required.`; + } +} diff --git a/packages/apps/src/server/errors/MustExtendAppError.ts b/packages/apps/src/server/errors/MustExtendAppError.ts new file mode 100644 index 0000000000000..bdb2fb3f21a56 --- /dev/null +++ b/packages/apps/src/server/errors/MustExtendAppError.ts @@ -0,0 +1,5 @@ +export class MustExtendAppError implements Error { + public name = 'MustExtendApp'; + + public message = 'App must extend the "App" abstract class.'; +} diff --git a/packages/apps/src/server/errors/NotEnoughMethodArgumentsError.ts b/packages/apps/src/server/errors/NotEnoughMethodArgumentsError.ts new file mode 100644 index 0000000000000..9fc6a01bfca1f --- /dev/null +++ b/packages/apps/src/server/errors/NotEnoughMethodArgumentsError.ts @@ -0,0 +1,9 @@ +export class NotEnoughMethodArgumentsError implements Error { + public readonly name: string = 'NotEnoughMethodArgumentsError'; + + public readonly message: string; + + constructor(method: string, requiredCount: number, providedCount: number) { + this.message = `The method "${method}" requires ${requiredCount} parameters but was only passed ${providedCount}.`; + } +} diff --git a/packages/apps/src/server/errors/PathAlreadyExistsError.ts b/packages/apps/src/server/errors/PathAlreadyExistsError.ts new file mode 100644 index 0000000000000..9f2a7a671b3c5 --- /dev/null +++ b/packages/apps/src/server/errors/PathAlreadyExistsError.ts @@ -0,0 +1,9 @@ +export class PathAlreadyExistsError implements Error { + public name = 'PathAlreadyExists'; + + public message: string; + + constructor(path: string) { + this.message = `The api path "${path}" already exists in the system.`; + } +} diff --git a/packages/apps/src/server/errors/PermissionDeniedError.ts b/packages/apps/src/server/errors/PermissionDeniedError.ts new file mode 100644 index 0000000000000..c850bc7f6276f --- /dev/null +++ b/packages/apps/src/server/errors/PermissionDeniedError.ts @@ -0,0 +1,25 @@ +import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; + +interface IPermissionDeniedErrorParams { + appId: string; + missingPermissions: Array; + methodName?: string; + reason?: string; + message?: string; +} + +export class PermissionDeniedError extends Error { + constructor({ appId, missingPermissions, methodName, reason, message }: IPermissionDeniedErrorParams) { + if (message) { + super(message); + } else { + const permissions = missingPermissions.map((permission) => `"${permission.name}"`).join(', '); + + super( + `Failed to call the method ${methodName ? `"${methodName}"` : ''} as the app (${appId}) lacks the following permissions:\n` + + `[${permissions}]. Declare them in your app.json to fix the issue.\n` + + `reason: ${reason}`, + ); + } + } +} diff --git a/packages/apps/src/server/errors/RequiredApiVersionError.ts b/packages/apps/src/server/errors/RequiredApiVersionError.ts new file mode 100644 index 0000000000000..46f10652325d2 --- /dev/null +++ b/packages/apps/src/server/errors/RequiredApiVersionError.ts @@ -0,0 +1,21 @@ +import * as semver from 'semver'; + +import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; + +export class RequiredApiVersionError implements Error { + public name = 'RequiredApiVersion'; + + public message: string; + + constructor(info: IAppInfo, versionInstalled: string) { + let moreInfo = ''; + if (semver.gt(versionInstalled, info.requiredApiVersion)) { + moreInfo = ' Please tell the author to update their App as it is out of date.'; + } + + this.message = + `Failed to load the App "${info.name}" (${info.id}) as it requires ` + + `v${info.requiredApiVersion} of the App API however your server comes with ` + + `v${versionInstalled}.${moreInfo}`; + } +} diff --git a/packages/apps/src/server/errors/VideoConfProviderAlreadyExistsError.ts b/packages/apps/src/server/errors/VideoConfProviderAlreadyExistsError.ts new file mode 100644 index 0000000000000..934d2a35dd3ca --- /dev/null +++ b/packages/apps/src/server/errors/VideoConfProviderAlreadyExistsError.ts @@ -0,0 +1,9 @@ +export class VideoConfProviderAlreadyExistsError implements Error { + public name = 'VideoConfProviderAlreadyExists'; + + public message: string; + + constructor(name: string) { + this.message = `The video conference provider "${name}" was already registered by another App.`; + } +} diff --git a/packages/apps/src/server/errors/VideoConfProviderNotRegisteredError.ts b/packages/apps/src/server/errors/VideoConfProviderNotRegisteredError.ts new file mode 100644 index 0000000000000..9ba854f9ea74e --- /dev/null +++ b/packages/apps/src/server/errors/VideoConfProviderNotRegisteredError.ts @@ -0,0 +1,9 @@ +export class VideoConfProviderNotRegisteredError implements Error { + public name = 'VideoConfProviderNotRegistered'; + + public message: string; + + constructor(providerName: string) { + this.message = `The video conference provider "${providerName}" is not registered in the system.`; + } +} diff --git a/packages/apps/src/server/errors/index.ts b/packages/apps/src/server/errors/index.ts new file mode 100644 index 0000000000000..f93385e5abb9a --- /dev/null +++ b/packages/apps/src/server/errors/index.ts @@ -0,0 +1,25 @@ +import { CommandAlreadyExistsError } from './CommandAlreadyExistsError'; +import { CommandHasAlreadyBeenTouchedError } from './CommandHasAlreadyBeenTouchedError'; +import { CompilerError } from './CompilerError'; +import { InvalidLicenseError } from './InvalidLicenseError'; +import { MustContainFunctionError } from './MustContainFunctionError'; +import { MustExtendAppError } from './MustExtendAppError'; +import { NotEnoughMethodArgumentsError } from './NotEnoughMethodArgumentsError'; +import { PathAlreadyExistsError } from './PathAlreadyExistsError'; +import { RequiredApiVersionError } from './RequiredApiVersionError'; +import { VideoConfProviderAlreadyExistsError } from './VideoConfProviderAlreadyExistsError'; +import { VideoConfProviderNotRegisteredError } from './VideoConfProviderNotRegisteredError'; + +export { + CommandAlreadyExistsError, + CommandHasAlreadyBeenTouchedError, + PathAlreadyExistsError, + CompilerError, + MustContainFunctionError, + MustExtendAppError, + NotEnoughMethodArgumentsError, + RequiredApiVersionError, + InvalidLicenseError, + VideoConfProviderAlreadyExistsError, + VideoConfProviderNotRegisteredError, +}; diff --git a/packages/apps/src/server/logging/AppConsole.ts b/packages/apps/src/server/logging/AppConsole.ts new file mode 100644 index 0000000000000..004482113f154 --- /dev/null +++ b/packages/apps/src/server/logging/AppConsole.ts @@ -0,0 +1,121 @@ +import * as stackTrace from 'stack-trace'; + +import type { ILoggerStorageEntry } from './ILoggerStorageEntry'; +import type { ILogEntry, ILogger } from '@rocket.chat/apps-engine/definition/accessors'; +import { LogMessageSeverity } from '@rocket.chat/apps-engine/definition/accessors'; +import type { AppMethod } from '@rocket.chat/apps-engine/definition/metadata'; + +export class AppConsole implements ILogger { + public static toStorageEntry(appId: string, logger: AppConsole): ILoggerStorageEntry { + return { + appId, + method: logger.getMethod(), + entries: logger.getEntries(), + startTime: logger.getStartTime(), + endTime: logger.getEndTime(), + totalTime: logger.getTotalTime(), + _createdAt: new Date(), + }; + } + + public method: `${AppMethod}`; + + private entries: Array; + + private start: Date; + + constructor(method: `${AppMethod}`) { + this.method = method; + this.entries = []; + this.start = new Date(); + } + + public debug(...items: Array): void { + this.addEntry(LogMessageSeverity.DEBUG, this.getFunc(stackTrace.get()), ...items); + } + + public info(...items: Array): void { + this.addEntry(LogMessageSeverity.INFORMATION, this.getFunc(stackTrace.get()), ...items); + } + + public log(...items: Array): void { + this.addEntry(LogMessageSeverity.LOG, this.getFunc(stackTrace.get()), ...items); + } + + public warn(...items: Array): void { + this.addEntry(LogMessageSeverity.WARNING, this.getFunc(stackTrace.get()), ...items); + } + + public error(...items: Array): void { + this.addEntry(LogMessageSeverity.ERROR, this.getFunc(stackTrace.get()), ...items); + } + + public success(...items: Array): void { + this.addEntry(LogMessageSeverity.SUCCESS, this.getFunc(stackTrace.get()), ...items); + } + + public getEntries(): Array { + return Array.from(this.entries); + } + + public getMethod(): `${AppMethod}` { + return this.method; + } + + public getStartTime(): Date { + return this.start; + } + + public getEndTime(): Date { + return new Date(); + } + + public getTotalTime(): number { + return this.getEndTime().getTime() - this.getStartTime().getTime(); + } + + private addEntry(severity: LogMessageSeverity, caller: string, ...items: Array): void { + const i = items.map((v) => { + if (v instanceof Error) { + return JSON.stringify(v, Object.getOwnPropertyNames(v)); + } + if (typeof v === 'object' && typeof v.stack === 'string' && typeof v.message === 'string') { + return JSON.stringify(v, Object.getOwnPropertyNames(v)); + } + const str = JSON.stringify(v, null, 2); + return str ? JSON.parse(str) : str; // force call toJSON to prevent circular references + }); + + this.entries.push({ + caller, + severity, + timestamp: new Date(), + args: i, + }); + + // This should be a setting? :thinking: + // console.log(`${ severity.toUpperCase() }:`, i); + } + + private getFunc(stack: Array): string { + let func = 'anonymous'; + + if (stack.length === 1) { + return func; + } + + const frame: stackTrace.StackFrame = stack[1]; + + if (frame.getMethodName() === null) { + func = 'anonymous OR constructor'; + } else { + func = frame.getMethodName(); + } + + if (frame.getFunctionName() !== null) { + func = `${func} -> ${frame.getFunctionName()}`; + } + + return func; + } +} diff --git a/packages/apps/src/server/logging/ILoggerStorageEntry.ts b/packages/apps/src/server/logging/ILoggerStorageEntry.ts new file mode 100644 index 0000000000000..12c6f47dfb01d --- /dev/null +++ b/packages/apps/src/server/logging/ILoggerStorageEntry.ts @@ -0,0 +1,14 @@ +import type { ILogEntry } from '@rocket.chat/apps-engine/definition/accessors'; +import type { AppMethod } from '@rocket.chat/apps-engine/definition/metadata'; + +export interface ILoggerStorageEntry { + appId: string; + method: `${AppMethod}`; + entries: Array; + startTime: Date; + endTime: Date; + totalTime: number; + instanceId?: string; + // Internal value to be used for sorting + _createdAt: Date; +} diff --git a/packages/apps/src/server/logging/index.ts b/packages/apps/src/server/logging/index.ts new file mode 100644 index 0000000000000..e17cdd6e9e6c2 --- /dev/null +++ b/packages/apps/src/server/logging/index.ts @@ -0,0 +1,4 @@ +import { AppConsole } from './AppConsole'; +import { ILoggerStorageEntry } from './ILoggerStorageEntry'; + +export { AppConsole, ILoggerStorageEntry }; diff --git a/packages/apps/src/server/managers/AppAccessorManager.ts b/packages/apps/src/server/managers/AppAccessorManager.ts new file mode 100644 index 0000000000000..b93d5d6043053 --- /dev/null +++ b/packages/apps/src/server/managers/AppAccessorManager.ts @@ -0,0 +1,251 @@ +import type { + IConfigurationExtend, + IConfigurationModify, + IEnvironmentRead, + IEnvironmentWrite, + IHttp, + IHttpExtend, + IModify, + IPersistence, + IRead, +} from '@rocket.chat/apps-engine/definition/accessors'; +import type { AppManager } from '../AppManager'; +import { + ApiExtend, + ConfigurationExtend, + ConfigurationModify, + EnvironmentalVariableRead, + EnvironmentRead, + EnvironmentWrite, + ExternalComponentsExtend, + Http, + HttpExtend, + LivechatRead, + MessageRead, + Modify, + Notifier, + OAuthAppsReader, + OutboundMessageProviderExtend, + Persistence, + PersistenceRead, + Reader, + RoleRead, + RoomRead, + SchedulerExtend, + SchedulerModify, + ServerSettingRead, + ServerSettingsModify, + ServerSettingUpdater, + SettingRead, + SettingsExtend, + SettingUpdater, + SlashCommandsExtend, + SlashCommandsModify, + UploadRead, + UserRead, + VideoConferenceRead, + VideoConfProviderExtend, +} from '../accessors'; +import { CloudWorkspaceRead } from '../accessors/CloudWorkspaceRead'; +import { ContactRead } from '../accessors/ContactRead'; +import { ExperimentalRead } from '../accessors/ExperimentalRead'; +import { ThreadRead } from '../accessors/ThreadRead'; +import { UIExtend } from '../accessors/UIExtend'; +import type { AppBridges } from '../bridges/AppBridges'; + +export class AppAccessorManager { + private readonly bridges: AppBridges; + + private readonly configExtenders: Map; + + private readonly envReaders: Map; + + private readonly envWriters: Map; + + private readonly configModifiers: Map; + + private readonly readers: Map; + + private readonly modifiers: Map; + + private readonly persists: Map; + + private readonly https: Map; + + constructor(private readonly manager: AppManager) { + this.bridges = this.manager.getBridges(); + this.configExtenders = new Map(); + this.envReaders = new Map(); + this.envWriters = new Map(); + this.configModifiers = new Map(); + this.readers = new Map(); + this.modifiers = new Map(); + this.persists = new Map(); + this.https = new Map(); + } + + /** + * Purifies the accessors for the provided App. + * + * @param appId The id of the App to purge the accessors for. + */ + public purifyApp(appId: string): void { + this.configExtenders.delete(appId); + this.envReaders.delete(appId); + this.envWriters.delete(appId); + this.configModifiers.delete(appId); + this.readers.delete(appId); + this.modifiers.delete(appId); + this.persists.delete(appId); + this.https.delete(appId); + } + + public getConfigurationExtend(appId: string): IConfigurationExtend { + if (!this.configExtenders.has(appId)) { + const rl = this.manager.getOneById(appId); + + if (!rl) { + throw new Error(`No App found by the provided id: ${appId}`); + } + + const htt = new HttpExtend(); + const cmds = new SlashCommandsExtend(this.manager.getCommandManager(), appId); + const videoConf = new VideoConfProviderExtend(this.manager.getVideoConfProviderManager(), appId); + const apis = new ApiExtend(this.manager.getApiManager(), appId); + const sets = new SettingsExtend(rl); + const excs = new ExternalComponentsExtend(this.manager.getExternalComponentManager(), appId); + const scheduler = new SchedulerExtend(this.manager.getSchedulerManager(), appId); + const ui = new UIExtend(this.manager.getUIActionButtonManager(), appId); + const outboundComms = new OutboundMessageProviderExtend(this.manager.getOutboundCommunicationProviderManager(), appId); + + this.configExtenders.set(appId, new ConfigurationExtend(htt, sets, cmds, apis, excs, scheduler, ui, videoConf, outboundComms)); + } + + return this.configExtenders.get(appId); + } + + public getEnvironmentRead(appId: string): IEnvironmentRead { + if (!this.envReaders.has(appId)) { + const rl = this.manager.getOneById(appId); + + if (!rl) { + throw new Error(`No App found by the provided id: ${appId}`); + } + + const sets = new SettingRead(rl); + const servsets = new ServerSettingRead(this.bridges.getServerSettingBridge(), appId); + const env = new EnvironmentalVariableRead(this.bridges.getEnvironmentalVariableBridge(), appId); + + this.envReaders.set(appId, new EnvironmentRead(sets, servsets, env)); + } + + return this.envReaders.get(appId); + } + + public getEnvironmentWrite(appId: string): IEnvironmentWrite { + if (!this.envWriters.has(appId)) { + const rl = this.manager.getOneById(appId); + + if (!rl) { + throw new Error(`No App found by the provided id: ${appId}`); + } + + const sets = new SettingUpdater(rl, this.manager.getSettingsManager()); + const serverSetting = new ServerSettingUpdater(this.bridges, appId); + + this.envWriters.set(appId, new EnvironmentWrite(sets, serverSetting)); + } + + return this.envWriters.get(appId); + } + + public getConfigurationModify(appId: string): IConfigurationModify { + if (!this.configModifiers.has(appId)) { + this.configModifiers.set( + appId, + new ConfigurationModify( + new ServerSettingsModify(this.bridges.getServerSettingBridge(), appId), + new SlashCommandsModify(this.manager.getCommandManager(), appId), + new SchedulerModify(this.bridges.getSchedulerBridge(), appId), + ), + ); + } + + return this.configModifiers.get(appId); + } + + public getReader(appId: string): IRead { + if (!this.readers.has(appId)) { + const env = this.getEnvironmentRead(appId); + const msg = new MessageRead(this.bridges.getMessageBridge(), appId); + const persist = new PersistenceRead(this.bridges.getPersistenceBridge(), appId); + const room = new RoomRead(this.bridges.getRoomBridge(), appId); + const user = new UserRead(this.bridges.getUserBridge(), appId); + const noti = new Notifier(this.bridges.getUserBridge(), this.bridges.getMessageBridge(), appId); + const livechat = new LivechatRead(this.bridges.getLivechatBridge(), appId); + const upload = new UploadRead(this.bridges.getUploadBridge(), appId); + const cloud = new CloudWorkspaceRead(this.bridges.getCloudWorkspaceBridge(), appId); + const videoConf = new VideoConferenceRead(this.bridges.getVideoConferenceBridge(), appId); + const oauthApps = new OAuthAppsReader(this.bridges.getOAuthAppsBridge(), appId); + const contactReader = new ContactRead(this.bridges, appId); + const thread = new ThreadRead(this.bridges.getThreadBridge(), appId); + const role = new RoleRead(this.bridges.getRoleBridge(), appId); + const experimental = new ExperimentalRead(this.bridges.getExperimentalBridge(), appId); + + this.readers.set( + appId, + new Reader( + env, + msg, + persist, + room, + user, + noti, + livechat, + upload, + cloud, + videoConf, + contactReader, + oauthApps, + thread, + role, + experimental, + ), + ); + } + + return this.readers.get(appId); + } + + public getModifier(appId: string): IModify { + if (!this.modifiers.has(appId)) { + this.modifiers.set(appId, new Modify(this.bridges, appId)); + } + + return this.modifiers.get(appId); + } + + public getPersistence(appId: string): IPersistence { + if (!this.persists.has(appId)) { + this.persists.set(appId, new Persistence(this.bridges.getPersistenceBridge(), appId)); + } + + return this.persists.get(appId); + } + + public getHttp(appId: string): IHttp { + if (!this.https.has(appId)) { + let ext: IHttpExtend; + if (this.configExtenders.has(appId)) { + ext = this.configExtenders.get(appId).http; + } else { + const cf = this.getConfigurationExtend(appId); + ext = cf.http; + } + + this.https.set(appId, new Http(this, this.bridges, ext, appId)); + } + + return this.https.get(appId); + } +} diff --git a/packages/apps/src/server/managers/AppApi.ts b/packages/apps/src/server/managers/AppApi.ts new file mode 100644 index 0000000000000..826f8023ff590 --- /dev/null +++ b/packages/apps/src/server/managers/AppApi.ts @@ -0,0 +1,99 @@ +import type { IApi, IApiRequest, IApiResponse } from '@rocket.chat/apps-engine/definition/api'; +import { ApiSecurity, ApiVisibility } from '@rocket.chat/apps-engine/definition/api'; +import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api/IApiEndpoint'; +import type { IApiEndpointInfo } from '@rocket.chat/apps-engine/definition/api/IApiEndpointInfo'; +import type { ProxiedApp } from '../ProxiedApp'; +import type { AppLogStorage } from '../storage'; +import type { AppAccessorManager } from './AppAccessorManager'; + +export class AppApi { + public readonly computedPath: string; + + public readonly basePath: string; + + public readonly appId: string; + + public readonly hash?: string; + + public readonly implementedMethods: Array; + + constructor( + public app: ProxiedApp, + public api: IApi, + public endpoint: IApiEndpoint, + ) { + this.appId = app.getID(); + + switch (this.api.visibility) { + case ApiVisibility.PUBLIC: + this.basePath = `/api/apps/public/${app.getID()}`; + break; + + case ApiVisibility.PRIVATE: + this.basePath = `/api/apps/private/${app.getID()}/${app.getStorageItem()._id}`; + this.hash = app.getStorageItem()._id; + break; + } + + this.computedPath = `${this.basePath}/${endpoint.path}`; + + this.implementedMethods = endpoint._availableMethods; + } + + public async runExecutor(request: IApiRequest, logStorage: AppLogStorage, accessors: AppAccessorManager): Promise { + const { path } = this.endpoint; + + const { method } = request; + + if (!this.validateVisibility(request)) { + return { + status: 404, + }; + } + + if (!this.validateSecurity(request)) { + return { + status: 401, + }; + } + + const endpoint: IApiEndpointInfo = { + basePath: this.basePath, + fullPath: this.computedPath, + appId: this.appId, + hash: this.hash, + }; + + try { + const result = await this.app.getRuntimeController().sendRequest({ + method: `api:${path}:${method}`, + params: [request, endpoint], + }); + + return result as IApiResponse; + } catch (e) { + console.error(e); + throw e; + } + } + + private validateVisibility(request: IApiRequest): boolean { + if (this.api.visibility === ApiVisibility.PUBLIC) { + return true; + } + + if (this.api.visibility === ApiVisibility.PRIVATE) { + return this.app.getStorageItem()._id === request.privateHash; + } + + return false; + } + + private validateSecurity(request: IApiRequest): boolean { + if (this.api.security === ApiSecurity.UNSECURE) { + return true; + } + + return false; + } +} diff --git a/packages/apps/src/server/managers/AppApiManager.ts b/packages/apps/src/server/managers/AppApiManager.ts new file mode 100644 index 0000000000000..2a742faedbf8c --- /dev/null +++ b/packages/apps/src/server/managers/AppApiManager.ts @@ -0,0 +1,165 @@ +import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; +import { HttpStatusCode } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IApi, IApiEndpointMetadata, IApiRequest, IApiResponse } from '@rocket.chat/apps-engine/definition/api'; +import type { AppManager } from '../AppManager'; +import type { ApiBridge } from '../bridges'; +import { PathAlreadyExistsError } from '../errors'; +import type { AppAccessorManager } from './AppAccessorManager'; +import { AppApi } from './AppApi'; + +/** + * The api manager for the Apps. + * + * An App will add api's during their `initialize` method. + * Then once an App's `onEnable` is called and it returns true, + * only then will that App's api's be enabled. + */ +export class AppApiManager { + private readonly bridge: ApiBridge; + + private readonly accessors: AppAccessorManager; + + // Variable that contains the api's which have been provided by apps. + // The key of the top map is app id and the key of the inner map is the path + private providedApis: Map>; + + constructor(private readonly manager: AppManager) { + this.bridge = this.manager.getBridges().getApiBridge(); + this.accessors = this.manager.getAccessorManager(); + this.providedApis = new Map>(); + } + + /** + * Adds an to *be* registered. This will *not register* it with the + * bridged system yet as this is only called on an App's + * `initialize` method and an App might not get enabled. + * When adding an api, it can *not* already exist in the system. + * + * @param appId the app's id which the api belongs to + * @param api the api to add to the system + */ + public addApi(appId: string, api: IApi): void { + if (api.endpoints.length === 0) { + throw new Error('Invalid Api parameter provided, endpoints must contain, at least, one IApiEndpoint.'); + } + + const app = this.manager.getOneById(appId); + if (!app) { + throw new Error('App must exist in order for an api to be added.'); + } + + // Verify the api's path doesn't exist already + if (this.providedApis.get(appId)) { + api.endpoints.forEach((endpoint) => { + if (this.providedApis.get(appId).has(endpoint.path)) { + throw new PathAlreadyExistsError(endpoint.path); + } + }); + } + + if (!this.providedApis.has(appId)) { + this.providedApis.set(appId, new Map()); + } + + api.endpoints.forEach((endpoint) => { + this.providedApis.get(appId).set(endpoint.path, new AppApi(app, api, endpoint)); + }); + } + + /** + * Registers all of the api's for the provided app inside + * of the bridged system which then enables them. + * + * @param appId The app's id of which to register it's api's with the bridged system + */ + public async registerApis(appId: string): Promise { + if (!this.providedApis.has(appId)) { + return; + } + + await this.bridge.doUnregisterApis(appId); + for await (const [, apiApp] of this.providedApis.get(appId).entries()) { + await this.registerApi(appId, apiApp); + } + } + + /** + * Unregisters the api's from the system. + * + * @param appId the appId for the api's to purge + */ + public async unregisterApis(appId: string): Promise { + if (this.providedApis.has(appId)) { + await this.bridge.doUnregisterApis(appId); + + this.providedApis.delete(appId); + } + } + + /** + * Executes an App's api. + * + * @param appId the app which is providing the api + * @param path the path to be executed in app's api's + * @param request the request data to be evaluated byt the app + */ + public async executeApi(appId: string, path: string, request: IApiRequest): Promise { + const api = this.providedApis.get(appId).get(path); + + if (!api) { + return { + status: HttpStatusCode.NOT_FOUND, + }; + } + + const app = this.manager.getOneById(appId); + + if (!app || AppStatusUtils.isDisabled(await app.getStatus())) { + // Just in case someone decides to do something they shouldn't + // let's ensure the app actually exists + return { + status: HttpStatusCode.NOT_FOUND, + }; + } + + return api.runExecutor(request, this.manager.getLogStorage(), this.accessors); + } + + /** + * Return a list of api's for a certain app + * + * @param appId the app which is providing the api + */ + public listApis(appId: string): Array { + const apis = this.providedApis.get(appId); + + if (!apis) { + return []; + } + + const result = []; + + for (const api of apis.values()) { + const metadata: IApiEndpointMetadata = { + path: api.endpoint.path, + computedPath: api.computedPath, + methods: api.implementedMethods, + examples: api.endpoint.examples || {}, + }; + + result.push(metadata); + } + + return result; + } + + /** + * Actually goes and provide's the bridged system with the api information. + * + * @param appId the app which is providing the api + * @param info the api's registration information + */ + private async registerApi(appId: string, api: AppApi): Promise { + await this.bridge.doRegisterApi(api, appId); + } +} diff --git a/packages/apps/src/server/managers/AppExternalComponentManager.ts b/packages/apps/src/server/managers/AppExternalComponentManager.ts new file mode 100644 index 0000000000000..a03c984173863 --- /dev/null +++ b/packages/apps/src/server/managers/AppExternalComponentManager.ts @@ -0,0 +1,142 @@ +import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/externalComponent'; + +/** + * The external component manager for the apps. + * + * An app will register external components during its `initialize` method. + * Then once an app's `onEnable` method is called and it returns true, + * only then will that app's external components be enabled. + */ +export class AppExternalComponentManager { + /** + * The map that maintains all registered components. + * The key of the top map is app id and the key of inner map is the + * external component name. + */ + private registeredExternalComponents: Map>; + + /** + * Contains the apps and the external components they have touhed. + * The key of the top map is app id and the key of inner map is the + * external component name. + * Doesn't matter whether the app provided, modified, disabled, + * or enabled. As long as an app touched external components, then + * they are listed here. + */ + private appTouchedExternalComponents: Map>; + + constructor() { + this.registeredExternalComponents = new Map>(); + this.appTouchedExternalComponents = new Map>(); + } + + /** + * Get all registered components. + */ + public getRegisteredExternalComponents(): Map> { + return this.registeredExternalComponents; + } + + /** + * Get all external components that apps have registered + * before, including disabled apps' external components. + */ + public getAppTouchedExternalComponents(): Map> { + return this.appTouchedExternalComponents; + } + + /** + * Get all external components of an app by specifying the appId. + * + * @param appId the id of the app + */ + public getExternalComponents(appId: string): Map { + if (this.appTouchedExternalComponents.has(appId)) { + return this.appTouchedExternalComponents.get(appId); + } + + return null; + } + + /** + * Get an array of external components which are enabled and ready for usage. + */ + public getProvidedComponents(): Array { + const registeredExternalComponents = this.getRegisteredExternalComponents(); + const providedComponents: Array = []; + + registeredExternalComponents.forEach((appExternalComponents) => { + Array.from(appExternalComponents.values()).forEach((externalComponent) => { + providedComponents.push(externalComponent); + }); + }); + + return providedComponents; + } + + /** + * Add an external component to the appTouchedExternalComponents. + * If you call this method twice and the component + * has the same name as before, the first one will be + * overwritten as the names provided **must** be unique. + * + * @param appId the id of the app + * @param externalComponent the external component about to be added + */ + public addExternalComponent(appId: string, externalComponent: IExternalComponent): void { + externalComponent.appId = appId; + + if (!this.appTouchedExternalComponents.get(appId)) { + this.appTouchedExternalComponents.set(appId, new Map(Object.entries({ [externalComponent.name]: externalComponent }))); + } else { + const appExternalComponents = this.appTouchedExternalComponents.get(appId); + + appExternalComponents.set(externalComponent.name, externalComponent); + } + } + + /** + * Add enabled apps' external components from the appTouchedExternalComponents + * to the registeredExternalComponents. + * + * @param appId the id of the app + */ + public registerExternalComponents(appId: string): void { + if (!this.appTouchedExternalComponents.has(appId)) { + return; + } + const externalComponents = this.appTouchedExternalComponents.get(appId); + + if (externalComponents.size > 0) { + this.registeredExternalComponents.set(appId, externalComponents); + } + } + + /** + * Remove all external components of an app from the + * registeredExternalComponents by specifying the appId. + * + * @param appId the id of the app + */ + public unregisterExternalComponents(appId: string): void { + if (this.registeredExternalComponents.has(appId)) { + this.registeredExternalComponents.delete(appId); + } + } + + /** + * Remove all external components of an app from both the + * registeredExternalComponents and the appTouchedComponents + * by specifying the appId. + * + * @param appId the id of the app + */ + public purgeExternalComponents(appId: string): void { + if (this.appTouchedExternalComponents.has(appId)) { + this.appTouchedExternalComponents.delete(appId); + } + if (this.registeredExternalComponents.has(appId)) { + this.registeredExternalComponents.delete(appId); + } + } +} diff --git a/packages/apps/src/server/managers/AppLicenseManager.ts b/packages/apps/src/server/managers/AppLicenseManager.ts new file mode 100644 index 0000000000000..b72de6a5fd69a --- /dev/null +++ b/packages/apps/src/server/managers/AppLicenseManager.ts @@ -0,0 +1,99 @@ +import type { AppManager } from '../AppManager'; +import type { UserBridge } from '../bridges'; +import type { IInternalUserBridge } from '../bridges/IInternalUserBridge'; +import { InvalidLicenseError } from '../errors'; +import type { IMarketplaceInfo } from '../marketplace'; +import { MarketplacePurchaseType } from '../marketplace/MarketplacePurchaseType'; +import { Crypto } from '../marketplace/license'; +import type { AppLicenseValidationResult } from '../marketplace/license'; + +enum LicenseVersion { + v1 = 1, +} + +export class AppLicenseManager { + private readonly crypto: Crypto; + + private readonly userBridge: UserBridge; + + constructor(private readonly manager: AppManager) { + this.crypto = new Crypto(this.manager.getBridges().getInternalBridge()); + this.userBridge = this.manager.getBridges().getUserBridge(); + } + + public async validate(validationResult: AppLicenseValidationResult, appMarketplaceInfo?: IMarketplaceInfo[]): Promise { + const marketplaceInfo = appMarketplaceInfo?.[0]; + if (!marketplaceInfo || marketplaceInfo.purchaseType !== MarketplacePurchaseType.PurchaseTypeSubscription) { + return; + } + + validationResult.setValidated(true); + + const encryptedLicense = marketplaceInfo.subscriptionInfo.license.license; + + if (!encryptedLicense) { + validationResult.addError('license', 'License for app is invalid'); + + throw new InvalidLicenseError(validationResult); + } + + let license; + try { + license = (await this.crypto.decryptLicense(encryptedLicense)) as any; + } catch (err) { + validationResult.addError('publicKey', err.message); + + throw new InvalidLicenseError(validationResult); + } + + switch (license.version) { + case LicenseVersion.v1: + await this.validateV1(marketplaceInfo, license, validationResult); + break; + } + } + + private async validateV1( + appMarketplaceInfo: IMarketplaceInfo, + license: any, + validationResult: AppLicenseValidationResult, + ): Promise { + if (license.isBundle && !appMarketplaceInfo.bundledIn?.find((value) => value.bundleId === license.appId)) { + validationResult.addError('bundle', 'License issued for a bundle that does not contain the app'); + } else if (!license.isBundle && license.appId !== appMarketplaceInfo.id) { + validationResult.addError('appId', `License hasn't been issued for this app`); + } + + const renewal = new Date(license.renewalDate); + const expire = new Date(license.expireDate); + const now = new Date(); + + if (expire < now) { + validationResult.addError('expire', 'License is no longer valid and needs to be renewed'); + } + + const currentActiveUsers = await (this.userBridge as UserBridge & IInternalUserBridge).getActiveUserCount(); + + if (license.maxSeats < currentActiveUsers) { + validationResult.addError( + 'maxSeats', + 'License does not accomodate the current amount of active users. Please increase the number of seats', + ); + } + + if (validationResult.hasErrors) { + throw new InvalidLicenseError(validationResult); + } + + if (renewal < now) { + validationResult.addWarning('renewal', 'License has expired and needs to be renewed'); + } + + if (license.seats < currentActiveUsers) { + validationResult.addWarning( + 'seats', + 'License does not have enough seats to accommodate the current amount of active users. Please increase the number of seats', + ); + } + } +} diff --git a/packages/apps/src/server/managers/AppListenerManager.ts b/packages/apps/src/server/managers/AppListenerManager.ts new file mode 100644 index 0000000000000..46d8651c85b3a --- /dev/null +++ b/packages/apps/src/server/managers/AppListenerManager.ts @@ -0,0 +1,1283 @@ +import type { AppAccessorManager } from './AppAccessorManager'; +import type { IEmailDescriptor, IPreEmailSentContext } from '@rocket.chat/apps-engine/definition/email'; +import { EssentialAppDisabledException } from '@rocket.chat/apps-engine/definition/exceptions'; +import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/externalComponent'; +import type { ILivechatEventContext, ILivechatRoom, ILivechatTransferEventContext, IVisitor } from '@rocket.chat/apps-engine/definition/livechat'; +import type { ILivechatDepartmentEventContext } from '@rocket.chat/apps-engine/definition/livechat/ILivechatEventContext'; +import type { + IMessage, + IMessageDeleteContext, + IMessageFollowContext, + IMessagePinContext, + IMessageReactionContext, + IMessageReportContext, + IMessageStarContext, +} from '@rocket.chat/apps-engine/definition/messages'; +import { AppInterface, AppMethod } from '@rocket.chat/apps-engine/definition/metadata'; +import type { IRoom, IRoomUserJoinedContext, IRoomUserLeaveContext } from '@rocket.chat/apps-engine/definition/rooms'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import { UIActionButtonContext } from '@rocket.chat/apps-engine/definition/ui'; +import type { IUIKitResponse, IUIKitSurface, UIKitIncomingInteraction } from '@rocket.chat/apps-engine/definition/uikit'; +import { UIKitIncomingInteractionType } from '@rocket.chat/apps-engine/definition/uikit'; +import { isUIKitIncomingInteractionActionButtonMessageBox } from '@rocket.chat/apps-engine/definition/uikit/IUIKitIncomingInteractionActionButton'; +import type { + IUIKitIncomingInteractionMessageContainer, + IUIKitIncomingInteractionModalContainer, +} from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionContainer'; +import type { IUIKitLivechatBlockIncomingInteraction, IUIKitLivechatIncomingInteraction } from '@rocket.chat/apps-engine/definition/uikit/livechat'; +import type { IFileUploadInternalContext } from '@rocket.chat/apps-engine/definition/uploads/IFileUploadContext'; +import type { IUser, IUserContext, IUserStatusContext, IUserUpdateContext } from '@rocket.chat/apps-engine/definition/users'; +import type { AppManager } from '../AppManager'; +import type { ProxiedApp } from '../ProxiedApp'; +import { Utilities } from '../misc/Utilities'; +import { JSONRPC_METHOD_NOT_FOUND } from '../runtime/deno/AppsEngineDenoRuntime'; + +export interface IListenerExecutor { + [AppInterface.IPreMessageSentPrevent]: { + args: [IMessage]; + result: boolean; + }; + [AppInterface.IPreMessageSentExtend]: { + args: [IMessage]; + result: IMessage; + }; + [AppInterface.IPreMessageSentModify]: { + args: [IMessage]; + result: IMessage; + }; + [AppInterface.IPostSystemMessageSent]: { + args: [IMessage]; + result: void; + }; + [AppInterface.IPostMessageSent]: { + args: [IMessage]; + result: void; + }; + [AppInterface.IPreMessageDeletePrevent]: { + args: [IMessage]; + result: boolean; + }; + [AppInterface.IPostMessageDeleted]: { + args: [IMessageDeleteContext]; + result: void; + }; + [AppInterface.IPreMessageUpdatedPrevent]: { + args: [IMessage]; + result: unknown; + }; + [AppInterface.IPreMessageUpdatedExtend]: { + args: [IMessage]; + result: boolean; + }; + [AppInterface.IPreMessageUpdatedModify]: { + args: [IMessage]; + result: IMessage; + }; + [AppInterface.IPostMessageUpdated]: { + args: [IMessage]; + result: IMessage; + }; + [AppInterface.IPostMessageReacted]: { + args: [IMessageReactionContext]; + result: void; + }; + [AppInterface.IPostMessageFollowed]: { + args: [IMessageFollowContext]; + result: void; + }; + [AppInterface.IPostMessagePinned]: { + args: [IMessagePinContext]; + result: void; + }; + [AppInterface.IPostMessageStarred]: { + args: [IMessageStarContext]; + result: void; + }; + [AppInterface.IPostMessageReported]: { + args: [IMessageReportContext]; + result: void; + }; + // Rooms + [AppInterface.IPreRoomCreatePrevent]: { + args: [IRoom]; + result: boolean; + }; + [AppInterface.IPreRoomCreateExtend]: { + args: [IRoom]; + result: IRoom; + }; + [AppInterface.IPreRoomCreateModify]: { + args: [IRoom]; + result: IRoom; + }; + [AppInterface.IPostRoomCreate]: { + args: [IRoom]; + result: void; + }; + [AppInterface.IPreRoomDeletePrevent]: { + args: [IRoom]; + result: boolean; + }; + [AppInterface.IPostRoomDeleted]: { + args: [IRoom]; + result: void; + }; + [AppInterface.IPreRoomUserJoined]: { + args: [IRoomUserJoinedContext]; + result: void; + }; + [AppInterface.IPostRoomUserJoined]: { + args: [IRoomUserJoinedContext]; + result: void; + }; + [AppInterface.IPreRoomUserLeave]: { + args: [IRoomUserLeaveContext]; + result: void; + }; + [AppInterface.IPostRoomUserLeave]: { + args: [IRoomUserLeaveContext]; + result: void; + }; + // External Components + [AppInterface.IPostExternalComponentOpened]: { + args: [IExternalComponent]; + result: void; + }; + [AppInterface.IPostExternalComponentClosed]: { + args: [IExternalComponent]; + result: void; + }; + [AppInterface.IUIKitInteractionHandler]: { + args: [UIKitIncomingInteraction]; + result: IUIKitResponse; + }; + [AppInterface.IUIKitLivechatInteractionHandler]: { + args: [IUIKitLivechatIncomingInteraction]; + result: IUIKitResponse; + }; + // Livechat + [AppInterface.IPostLivechatRoomStarted]: { + args: [ILivechatRoom]; + result: void; + }; + /** + * @deprecated please prefer the AppInterface.IPostLivechatRoomClosed event + */ + [AppInterface.ILivechatRoomClosedHandler]: { + args: [ILivechatRoom]; + result: void; + }; + [AppInterface.IPreLivechatRoomCreatePrevent]: { + args: [ILivechatRoom]; + result: void; + }; + [AppInterface.IPostLivechatRoomClosed]: { + args: [ILivechatRoom]; + result: void; + }; + [AppInterface.IPostLivechatRoomSaved]: { + args: [ILivechatRoom]; + result: void; + }; + [AppInterface.IPostLivechatAgentAssigned]: { + args: [ILivechatEventContext]; + result: void; + }; + [AppInterface.IPostLivechatAgentUnassigned]: { + args: [ILivechatEventContext]; + result: void; + }; + [AppInterface.IPostLivechatRoomTransferred]: { + args: [ILivechatTransferEventContext]; + result: void; + }; + [AppInterface.IPostLivechatGuestSaved]: { + args: [IVisitor]; + result: void; + }; + [AppInterface.IPostLivechatDepartmentRemoved]: { + args: [ILivechatDepartmentEventContext]; + result: void; + }; + [AppInterface.IPostLivechatDepartmentDisabled]: { + args: [ILivechatDepartmentEventContext]; + result: void; + }; + // FileUpload + [AppInterface.IPreFileUpload]: { + args: [IFileUploadInternalContext]; + result: void; + }; + // Email + [AppInterface.IPreEmailSent]: { + args: [IPreEmailSentContext]; + result: IUIKitResponse; + }; + // User + [AppInterface.IPostUserCreated]: { + args: [IUserContext]; + result: void; + }; + [AppInterface.IPostUserUpdated]: { + args: [IUserContext]; + result: void; + }; + [AppInterface.IPostUserDeleted]: { + args: [IUserContext]; + result: void; + }; + [AppInterface.IPostUserLoggedIn]: { + args: [IUser]; + result: void; + }; + [AppInterface.IPostUserLoggedOut]: { + args: [IUser]; + result: void; + }; + [AppInterface.IPostUserStatusChanged]: { + args: [IUserStatusContext]; + result: void; + }; +} + +// type EventReturn = void | boolean | IMessage | IRoom | IUser | IUIKitResponse | ILivechatRoom | IEmailDescriptor; + +export class AppListenerManager { + private am: AppAccessorManager; + + private listeners: Map>; + + private defaultHandlers = new 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, []); + this.lockedEvents.set(intt, new Set()); + }); + + this.defaultHandlers.set('executeViewClosedHandler', { success: true }); + } + + public registerListeners(app: ProxiedApp): void { + this.unregisterListeners(app); + + Object.entries(app.getImplementationList()).forEach(([event, isImplemented]) => { + if (!isImplemented) { + return; + } + + this.listeners.get(event).push(app.getID()); + }); + } + + public unregisterListeners(app: ProxiedApp): void { + this.listeners.forEach((apps, int) => { + if (apps.includes(app.getID())) { + const where = apps.indexOf(app.getID()); + this.listeners.get(int).splice(where, 1); + } + }); + } + + 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: Array = []; + + for (const appId of this.listeners.get(int)) { + results.push(this.manager.getOneById(appId)); + } + + return results; + } + + public isEventBlocked(event: AppInterface): boolean { + const lockedEventList = this.lockedEvents.get(event); + + return !!lockedEventList?.size; + } + + /* eslint-disable-next-line complexity */ + public async executeListener( + int: I, + data: IListenerExecutor[I]['args'][0], + ): 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: + return this.executePreMessageSentPrevent(data as IMessage); + case AppInterface.IPreMessageSentExtend: + return this.executePreMessageSentExtend(data as IMessage); + case AppInterface.IPreMessageSentModify: + return this.executePreMessageSentModify(data as IMessage); + case AppInterface.IPostMessageSent: + this.executePostMessageSent(data as IMessage); + return; + case AppInterface.IPostSystemMessageSent: + this.executePostSystemMessageSent(data as IMessage); + return; + case AppInterface.IPreMessageDeletePrevent: + return this.executePreMessageDeletePrevent(data as IMessage); + case AppInterface.IPostMessageDeleted: + this.executePostMessageDelete(data as IMessageDeleteContext); + return; + case AppInterface.IPreMessageUpdatedPrevent: + return this.executePreMessageUpdatedPrevent(data as IMessage); + case AppInterface.IPreMessageUpdatedExtend: + return this.executePreMessageUpdatedExtend(data as IMessage); + case AppInterface.IPreMessageUpdatedModify: + return this.executePreMessageUpdatedModify(data as IMessage); + case AppInterface.IPostMessageUpdated: + this.executePostMessageUpdated(data as IMessage); + return; + case AppInterface.IPostMessageReacted: + return this.executePostMessageReacted(data as IMessageReactionContext); + case AppInterface.IPostMessageFollowed: + return this.executePostMessageFollowed(data as IMessageFollowContext); + case AppInterface.IPostMessagePinned: + return this.executePostMessagePinned(data as IMessagePinContext); + case AppInterface.IPostMessageStarred: + return this.executePostMessageStarred(data as IMessageStarContext); + case AppInterface.IPostMessageReported: + return this.executePostMessageReported(data as IMessageReportContext); + // Rooms + case AppInterface.IPreRoomCreatePrevent: + return this.executePreRoomCreatePrevent(data as IRoom); + case AppInterface.IPreRoomCreateExtend: + return this.executePreRoomCreateExtend(data as IRoom); + case AppInterface.IPreRoomCreateModify: + return this.executePreRoomCreateModify(data as IRoom); + case AppInterface.IPostRoomCreate: + this.executePostRoomCreate(data as IRoom); + return; + case AppInterface.IPreRoomDeletePrevent: + return this.executePreRoomDeletePrevent(data as IRoom); + 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); + case AppInterface.IPreRoomUserLeave: + return this.executePreRoomUserLeave(data as IRoomUserLeaveContext); + case AppInterface.IPostRoomUserLeave: + return this.executePostRoomUserLeave(data as IRoomUserLeaveContext); + // External Components + case AppInterface.IPostExternalComponentOpened: + this.executePostExternalComponentOpened(data as IExternalComponent); + return; + case AppInterface.IPostExternalComponentClosed: + this.executePostExternalComponentClosed(data as IExternalComponent); + return; + case AppInterface.IUIKitInteractionHandler: + return this.executeUIKitInteraction(data as UIKitIncomingInteraction); + case AppInterface.IUIKitLivechatInteractionHandler: + return this.executeUIKitLivechatInteraction(data as IUIKitLivechatIncomingInteraction); + // Livechat + case AppInterface.IPostLivechatRoomStarted: + return this.executePostLivechatRoomStarted(data as ILivechatRoom); + /** + * @deprecated please prefer the AppInterface.IPostLivechatRoomClosed event + */ + case AppInterface.ILivechatRoomClosedHandler: + return this.executeLivechatRoomClosedHandler(data as ILivechatRoom); + case AppInterface.IPreLivechatRoomCreatePrevent: + return this.executePreLivechatRoomCreatePrevent(data as ILivechatRoom); + case AppInterface.IPostLivechatRoomClosed: + return this.executePostLivechatRoomClosed(data as ILivechatRoom); + case AppInterface.IPostLivechatRoomSaved: + return this.executePostLivechatRoomSaved(data as ILivechatRoom); + case AppInterface.IPostLivechatAgentAssigned: + return this.executePostLivechatAgentAssigned(data as ILivechatEventContext); + case AppInterface.IPostLivechatAgentUnassigned: + return this.executePostLivechatAgentUnassigned(data as ILivechatEventContext); + case AppInterface.IPostLivechatRoomTransferred: + return this.executePostLivechatRoomTransferred(data as ILivechatTransferEventContext); + case AppInterface.IPostLivechatDepartmentRemoved: + return this.executePostLivechatDepartmentRemoved(data as ILivechatDepartmentEventContext); + case AppInterface.IPostLivechatDepartmentDisabled: + return this.executePostLivechatDepartmentDisabled(data as ILivechatDepartmentEventContext); + case AppInterface.IPostLivechatGuestSaved: + return this.executePostLivechatGuestSaved(data as IVisitor); + // FileUpload + case AppInterface.IPreFileUpload: + return this.executePreFileUpload(data as IFileUploadInternalContext); + // Email + case AppInterface.IPreEmailSent: + return this.executePreEmailSent(data as IPreEmailSentContext); + // User + case AppInterface.IPostUserCreated: + return this.executePostUserCreated(data as IUserContext); + case AppInterface.IPostUserUpdated: + return this.executePostUserUpdated(data as IUserContext); + case AppInterface.IPostUserDeleted: + return this.executePostUserDeleted(data as IUserContext); + case AppInterface.IPostUserLoggedIn: + return this.executePostUserLoggedIn(data as IUser); + case AppInterface.IPostUserLoggedOut: + return this.executePostUserLoggedOut(data as IUser); + case AppInterface.IPostUserStatusChanged: + return this.executePostUserStatusChanged(data as IUserStatusContext); + default: + console.warn('An invalid listener was called'); + } + } + + // Messages + private async executePreMessageSentPrevent(data: IMessage): Promise { + let prevented = false; + + for (const appId of this.listeners.get(AppInterface.IPreMessageSentPrevent)) { + const app = this.manager.getOneById(appId); + + const continueOn = (await app.call(AppMethod.CHECKPREMESSAGESENTPREVENT, data).catch((error) => { + // This method is optional, so if it doesn't exist, we should continue + if (error?.code === JSONRPC_METHOD_NOT_FOUND) { + return true; + } + + throw error; + })) as boolean; + + if (!continueOn) { + continue; + } + + prevented = (await app.call(AppMethod.EXECUTEPREMESSAGESENTPREVENT, data)) as boolean; + + if (prevented) { + return prevented; + } + } + + return prevented; + } + + private async executePreMessageSentExtend(data: IMessage): Promise { + let msg = data; + + for (const appId of this.listeners.get(AppInterface.IPreMessageSentExtend)) { + const app = this.manager.getOneById(appId); + + const continueOn = (await app.call(AppMethod.CHECKPREMESSAGESENTEXTEND, data).catch((error) => { + // This method is optional, so if it doesn't exist, we should continue + if (error?.code === JSONRPC_METHOD_NOT_FOUND) { + return true; + } + + throw error; + })) as boolean; + + if (continueOn) { + msg = await app.call(AppMethod.EXECUTEPREMESSAGESENTEXTEND, msg); + } + } + + return msg; + } + + private async executePreMessageSentModify(data: IMessage): Promise { + let msg = data; + + for (const appId of this.listeners.get(AppInterface.IPreMessageSentModify)) { + const app = this.manager.getOneById(appId); + + const continueOn = (await app.call(AppMethod.CHECKPREMESSAGESENTMODIFY, msg).catch((error) => { + // This method is optional, so if it doesn't exist, we should continue + if (error?.code === JSONRPC_METHOD_NOT_FOUND) { + return true; + } + + throw error; + })) as boolean; + + if (continueOn) { + msg = (await app.call(AppMethod.EXECUTEPREMESSAGESENTMODIFY, msg)) as IMessage; + } + } + + return msg; + } + + private async executePostMessageSent(data: IMessage): Promise { + // First check if the app implements Bot DM handlers and check if the dm contains more than one user + if (data.room.type === RoomType.DIRECT_MESSAGE && data.room.userIds.length > 1) { + for (const appId of this.listeners.get(AppInterface.IPostMessageSentToBot)) { + const app = this.manager.getOneById(appId); + + const reader = this.am.getReader(appId); + const bot = await reader.getUserReader().getAppUser(); + if (!bot) { + continue; + } + + // if the sender is the bot just ignore it + + if (bot.id === data.sender.id) { + continue; + } + // if the user doesnt belong to the room ignore it + if (!data.room.userIds.includes(bot.id)) { + continue; + } + + await app.call(AppMethod.EXECUTEPOSTMESSAGESENTTOBOT, data); + } + } + + for (const appId of this.listeners.get(AppInterface.IPostMessageSent)) { + const app = this.manager.getOneById(appId); + + const continueOn = (await app.call(AppMethod.CHECKPOSTMESSAGESENT, data).catch((error) => { + // This method is optional, so if it doesn't exist, we should continue + if (error?.code === JSONRPC_METHOD_NOT_FOUND) { + return true; + } + + throw error; + })) as boolean; + + if (continueOn) { + await app.call(AppMethod.EXECUTEPOSTMESSAGESENT, data); + } + } + } + + private async executePostSystemMessageSent(data: IMessage): Promise { + for (const appId of this.listeners.get(AppInterface.IPostSystemMessageSent)) { + const app = this.manager.getOneById(appId); + await app.call(AppMethod.EXECUTEPOSTSYSTEMMESSAGESENT, data); + } + } + + private async executePreMessageDeletePrevent(data: IMessage): Promise { + let prevented = false; + + for (const appId of this.listeners.get(AppInterface.IPreMessageDeletePrevent)) { + const app = this.manager.getOneById(appId); + + const continueOn = (await app.call(AppMethod.CHECKPREMESSAGEDELETEPREVENT, data).catch((error) => { + // This method is optional, so if it doesn't exist, we should continue + if (error?.code === JSONRPC_METHOD_NOT_FOUND) { + return true; + } + + throw error; + })) as boolean; + + if (continueOn) { + prevented = (await app.call(AppMethod.EXECUTEPREMESSAGEDELETEPREVENT, data)) as boolean; + + if (prevented) { + return prevented; + } + } + } + + return prevented; + } + + private async executePostMessageDelete(data: IMessageDeleteContext): Promise { + const context = Utilities.deepCloneAndFreeze(data); + const { message } = context; + + for (const appId of this.listeners.get(AppInterface.IPostMessageDeleted)) { + const app = this.manager.getOneById(appId); + + const continueOn = (await app + .call( + AppMethod.CHECKPOSTMESSAGEDELETED, + // `context` has more information about the event, but + // we had to keep this `message` here for compatibility + message, + context, + ) + .catch((error) => { + // This method is optional, so if it doesn't exist, we should continue + if (error?.code === JSONRPC_METHOD_NOT_FOUND) { + return true; + } + + throw error; + })) as boolean; + + if (continueOn) { + await app.call(AppMethod.EXECUTEPOSTMESSAGEDELETED, message, context); + } + } + } + + private async executePreMessageUpdatedPrevent(data: IMessage): Promise { + let prevented = false; + + for (const appId of this.listeners.get(AppInterface.IPreMessageUpdatedPrevent)) { + const app = this.manager.getOneById(appId); + + const continueOn = (await app.call(AppMethod.CHECKPREMESSAGEUPDATEDPREVENT, data).catch((error) => { + // This method is optional, so if it doesn't exist, we should continue + if (error?.code === JSONRPC_METHOD_NOT_FOUND) { + return true; + } + + throw error; + })) as boolean; + + if (continueOn) { + prevented = (await app.call(AppMethod.EXECUTEPREMESSAGEUPDATEDPREVENT, data)) as boolean; + + if (prevented) { + return prevented; + } + } + } + + return prevented; + } + + private async executePreMessageUpdatedExtend(data: IMessage): Promise { + let msg = data; + + for (const appId of this.listeners.get(AppInterface.IPreMessageUpdatedExtend)) { + const app = this.manager.getOneById(appId); + + const continueOn = (await app.call(AppMethod.CHECKPREMESSAGEUPDATEDEXTEND, msg).catch((error) => { + // This method is optional, so if it doesn't exist, we should continue + if (error?.code === JSONRPC_METHOD_NOT_FOUND) { + return true; + } + + throw error; + })) as boolean; + + if (continueOn) { + msg = await app.call(AppMethod.EXECUTEPREMESSAGEUPDATEDEXTEND, msg); + } + } + + return msg; + } + + private async executePreMessageUpdatedModify(data: IMessage): Promise { + let msg = data; + + for (const appId of this.listeners.get(AppInterface.IPreMessageUpdatedModify)) { + const app = this.manager.getOneById(appId); + + const continueOn = (await app.call(AppMethod.CHECKPREMESSAGEUPDATEDMODIFY, msg).catch((error) => { + // This method is optional, so if it doesn't exist, we should continue + if (error?.code === JSONRPC_METHOD_NOT_FOUND) { + return true; + } + + throw error; + })) as boolean; + + if (continueOn) { + msg = (await app.call(AppMethod.EXECUTEPREMESSAGEUPDATEDMODIFY, msg)) as IMessage; + } + } + + return msg; + } + + private async executePostMessageUpdated(data: IMessage): Promise { + for (const appId of this.listeners.get(AppInterface.IPostMessageUpdated)) { + const app = this.manager.getOneById(appId); + + const continueOn = (await app.call(AppMethod.CHECKPOSTMESSAGEUPDATED, data).catch((error) => { + // This method is optional, so if it doesn't exist, we should continue + if (error?.code === JSONRPC_METHOD_NOT_FOUND) { + return true; + } + + throw error; + })) as boolean; + + if (continueOn) { + await app.call(AppMethod.EXECUTEPOSTMESSAGEUPDATED, data); + } + } + } + + // Rooms + private async executePreRoomCreatePrevent(data: IRoom): Promise { + let prevented = false; + + for (const appId of this.listeners.get(AppInterface.IPreRoomCreatePrevent)) { + const app = this.manager.getOneById(appId); + + const continueOn = (await app.call(AppMethod.CHECKPREROOMCREATEPREVENT, data).catch((error) => { + // This method is optional, so if it doesn't exist, we should continue + if (error?.code === JSONRPC_METHOD_NOT_FOUND) { + return true; + } + + throw error; + })) as boolean; + + if (continueOn) { + prevented = (await app.call(AppMethod.EXECUTEPREROOMCREATEPREVENT, data)) as boolean; + + if (prevented) { + return prevented; + } + } + } + + return prevented; + } + + private async executePreRoomCreateExtend(data: IRoom): Promise { + let room = data; + + for (const appId of this.listeners.get(AppInterface.IPreRoomCreateExtend)) { + const app = this.manager.getOneById(appId); + + const continueOn = (await app.call(AppMethod.CHECKPREROOMCREATEEXTEND, room).catch((error) => { + // This method is optional, so if it doesn't exist, we should continue + if (error?.code === JSONRPC_METHOD_NOT_FOUND) { + return true; + } + + throw error; + })) as boolean; + + if (continueOn) { + room = await app.call(AppMethod.EXECUTEPREROOMCREATEEXTEND, room); + } + } + + return room; + } + + private async executePreRoomCreateModify(data: IRoom): Promise { + let room = data; + + for (const appId of this.listeners.get(AppInterface.IPreRoomCreateModify)) { + const app = this.manager.getOneById(appId); + + const continueOn = (await app.call(AppMethod.CHECKPREROOMCREATEMODIFY, room).catch((error) => { + // This method is optional, so if it doesn't exist, we should continue + if (error?.code === JSONRPC_METHOD_NOT_FOUND) { + return true; + } + + throw error; + })) as boolean; + + if (continueOn) { + room = (await app.call(AppMethod.EXECUTEPREROOMCREATEMODIFY, room)) as IRoom; + } + } + + return room; + } + + private async executePostRoomCreate(data: IRoom): Promise { + for (const appId of this.listeners.get(AppInterface.IPostRoomCreate)) { + const app = this.manager.getOneById(appId); + + const continueOn = (await app.call(AppMethod.CHECKPOSTROOMCREATE, data).catch((error) => { + // This method is optional, so if it doesn't exist, we should continue + if (error?.code === JSONRPC_METHOD_NOT_FOUND) { + return true; + } + + throw error; + })) as boolean; + + if (continueOn) { + await app.call(AppMethod.EXECUTEPOSTROOMCREATE, data); + } + } + } + + private async executePreRoomDeletePrevent(data: IRoom): Promise { + let prevented = false; + + for (const appId of this.listeners.get(AppInterface.IPreRoomDeletePrevent)) { + const app = this.manager.getOneById(appId); + + const continueOn = (await app.call(AppMethod.CHECKPREROOMDELETEPREVENT, data).catch((error) => { + // This method is optional, so if it doesn't exist, we should continue + if (error?.code === JSONRPC_METHOD_NOT_FOUND) { + return true; + } + + throw error; + })) as boolean; + + if (continueOn) { + prevented = (await app.call(AppMethod.EXECUTEPREROOMDELETEPREVENT, data)) as boolean; + + if (prevented) { + return prevented; + } + } + } + + return prevented; + } + + private async executePostRoomDeleted(data: IRoom): Promise { + for (const appId of this.listeners.get(AppInterface.IPostRoomDeleted)) { + const app = this.manager.getOneById(appId); + + const continueOn = (await app.call(AppMethod.CHECKPOSTROOMDELETED, data).catch((error) => { + // This method is optional, so if it doesn't exist, we should continue + if (error?.code === JSONRPC_METHOD_NOT_FOUND) { + return true; + } + + throw error; + })) as boolean; + + if (continueOn) { + await app.call(AppMethod.EXECUTEPOSTROOMDELETED, data); + } + } + } + + private async executePreRoomUserJoined(externalData: IRoomUserJoinedContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPreRoomUserJoined)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_PRE_ROOM_USER_JOINED, externalData); + } + } + + private async executePostRoomUserJoined(externalData: IRoomUserJoinedContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostRoomUserJoined)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_ROOM_USER_JOINED, externalData); + } + } + + private async executePreRoomUserLeave(externalData: IRoomUserLeaveContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPreRoomUserLeave)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_PRE_ROOM_USER_LEAVE, externalData); + } + } + + private async executePostRoomUserLeave(externalData: IRoomUserLeaveContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostRoomUserLeave)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_ROOM_USER_LEAVE, externalData); + } + } + + // External Components + private async executePostExternalComponentOpened(data: IExternalComponent): Promise { + for (const appId of this.listeners.get(AppInterface.IPostExternalComponentOpened)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTEPOSTEXTERNALCOMPONENTOPENED, data); + } + } + + private async executePostExternalComponentClosed(data: IExternalComponent): Promise { + for (const appId of this.listeners.get(AppInterface.IPostExternalComponentClosed)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTEPOSTEXTERNALCOMPONENTCLOSED, data); + } + } + + private async executeUIKitInteraction(data: UIKitIncomingInteraction): Promise { + const { appId } = data; + + const app = this.manager.getOneById(appId); + const handleError = (method: string) => (error: unknown) => { + if ((error as Record)?.code === JSONRPC_METHOD_NOT_FOUND) { + if (this.defaultHandlers.has(method)) { + console.warn( + `App ${appId} triggered an interaction but it doesn't exist or doesn't have method ${method}. Falling back to default handler.`, + ); + return this.defaultHandlers.get(method); + } + + console.warn( + `App ${appId} triggered an interaction but it doesn't exist or doesn't have method ${method} and there is no default handler for it.`, + ); + return; + } + + throw error; + }; + + const { actionId, user, triggerId } = data; + + switch (data.type) { + case UIKitIncomingInteractionType.BLOCK: { + const method = 'executeBlockActionHandler'; + const { value, blockId } = data.payload as { value: string; blockId: string }; + + return app + .call(method, { + appId, + actionId, + blockId, + user, + room: data.room, + triggerId, + value, + message: data.message, + container: data.container, + }) + .catch(handleError(method)); + } + case UIKitIncomingInteractionType.VIEW_SUBMIT: { + const method = 'executeViewSubmitHandler'; + const { view } = data.payload as { view: IUIKitSurface }; + + return app + .call(method, { + appId, + actionId, + view, + room: data.room, + triggerId, + user, + }) + .catch(handleError(method)); + } + case UIKitIncomingInteractionType.VIEW_CLOSED: { + const method = 'executeViewClosedHandler'; + const { view, isCleared } = data.payload as { view: IUIKitSurface; isCleared: boolean }; + + return app + .call(method, { + appId, + actionId, + view, + room: data.room, + isCleared, + user, + }) + .catch(handleError(method)); + } + case 'actionButton': { + const method = 'executeActionButtonHandler'; + + if (isUIKitIncomingInteractionActionButtonMessageBox(data)) { + return app + .call(method, { + appId, + actionId, + buttonContext: UIActionButtonContext.MESSAGE_BOX_ACTION, + room: data.room, + triggerId, + user, + threadId: data.tmid, + ...('message' in data.payload && { text: data.payload.message }), + }) + .catch(handleError(method)); + } + + return app + .call(method, { + appId, + actionId, + triggerId, + buttonContext: data.payload.context as UIActionButtonContext, + room: ('room' in data && data.room) || undefined, + user, + ...('message' in data && { message: data.message }), + }) + .catch(handleError(method)); + } + } + } + + private async executeUIKitLivechatInteraction(data: IUIKitLivechatIncomingInteraction): Promise { + const { appId, type } = data; + + const method = ((interactionType: string) => { + switch (interactionType) { + case UIKitIncomingInteractionType.BLOCK: + return AppMethod.UIKIT_LIVECHAT_BLOCK_ACTION; + } + })(type); + + const app = this.manager.getOneById(appId); + + const interactionData = (( + interactionType: UIKitIncomingInteractionType, + interaction: IUIKitLivechatIncomingInteraction, + ): IUIKitLivechatBlockIncomingInteraction => { + const { actionId, message, visitor, room, triggerId, container } = interaction; + + switch (interactionType) { + case UIKitIncomingInteractionType.BLOCK: { + const { value, blockId } = interaction.payload as { value: string; blockId: string }; + + return { + appId, + actionId, + blockId, + visitor, + room, + triggerId, + value, + message, + container: container as IUIKitIncomingInteractionModalContainer | IUIKitIncomingInteractionMessageContainer, + }; + } + } + })(type, data); + + return app.call(method, interactionData); + } + + // Livechat + private async executePreLivechatRoomCreatePrevent(data: ILivechatRoom): Promise { + for (const appId of this.listeners.get(AppInterface.IPreLivechatRoomCreatePrevent)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_PRE_LIVECHAT_ROOM_CREATE_PREVENT, data); + } + } + + private async executePostLivechatRoomStarted(data: ILivechatRoom): Promise { + for (const appId of this.listeners.get(AppInterface.IPostLivechatRoomStarted)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_LIVECHAT_ROOM_STARTED, data); + } + } + + private async executeLivechatRoomClosedHandler(data: ILivechatRoom): Promise { + for (const appId of this.listeners.get(AppInterface.ILivechatRoomClosedHandler)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_LIVECHAT_ROOM_CLOSED_HANDLER, data); + } + } + + private async executePostLivechatRoomClosed(data: ILivechatRoom): Promise { + for (const appId of this.listeners.get(AppInterface.IPostLivechatRoomClosed)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_LIVECHAT_ROOM_CLOSED, data); + } + } + + private async executePostLivechatAgentAssigned(data: ILivechatEventContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostLivechatAgentAssigned)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_LIVECHAT_AGENT_ASSIGNED, data); + } + } + + private async executePostLivechatAgentUnassigned(data: ILivechatEventContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostLivechatAgentUnassigned)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_LIVECHAT_AGENT_UNASSIGNED, data); + } + } + + private async executePostLivechatRoomTransferred(data: ILivechatTransferEventContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostLivechatRoomTransferred)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_LIVECHAT_ROOM_TRANSFERRED, data); + } + } + + private async executePostLivechatGuestSaved(data: IVisitor): Promise { + for (const appId of this.listeners.get(AppInterface.IPostLivechatGuestSaved)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_LIVECHAT_GUEST_SAVED, data); + } + } + + private async executePostLivechatRoomSaved(data: ILivechatRoom): Promise { + for (const appId of this.listeners.get(AppInterface.IPostLivechatRoomSaved)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_LIVECHAT_ROOM_SAVED, data); + } + } + + private async executePostLivechatDepartmentRemoved(data: ILivechatDepartmentEventContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostLivechatDepartmentRemoved)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_LIVECHAT_DEPARTMENT_REMOVED, data); + } + } + + private async executePostLivechatDepartmentDisabled(data: ILivechatDepartmentEventContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostLivechatDepartmentDisabled)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_LIVECHAT_DEPARTMENT_DISABLED, data); + } + } + + // FileUpload + private async executePreFileUpload(data: IFileUploadInternalContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPreFileUpload)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_PRE_FILE_UPLOAD, data); + } + } + + private async executePreEmailSent(data: IPreEmailSentContext): Promise { + let descriptor = data.email; + + for (const appId of this.listeners.get(AppInterface.IPreEmailSent)) { + const app = this.manager.getOneById(appId); + + descriptor = await app.call(AppMethod.EXECUTE_PRE_EMAIL_SENT, { + context: data.context, + email: descriptor, + }); + } + + return descriptor; + } + + private async executePostMessageReacted(data: IMessageReactionContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostMessageReacted)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_MESSAGE_REACTED, data); + } + } + + private async executePostMessageFollowed(data: IMessageFollowContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostMessageFollowed)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_MESSAGE_FOLLOWED, data); + } + } + + private async executePostMessagePinned(data: IMessagePinContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostMessagePinned)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_MESSAGE_PINNED, data); + } + } + + private async executePostMessageStarred(data: IMessageStarContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostMessageStarred)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_MESSAGE_STARRED, data); + } + } + + private async executePostMessageReported(data: IMessageReportContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostMessageReported)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_MESSAGE_REPORTED, data); + } + } + + private async executePostUserCreated(data: IUserContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostUserCreated)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_USER_CREATED, data); + } + } + + private async executePostUserUpdated(data: IUserUpdateContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostUserUpdated)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_USER_UPDATED, data); + } + } + + private async executePostUserDeleted(data: IUserContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostUserDeleted)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_USER_DELETED, data); + } + } + + private async executePostUserLoggedIn(data: IUser): Promise { + for (const appId of this.listeners.get(AppInterface.IPostUserLoggedIn)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_USER_LOGGED_IN, data); + } + } + + private async executePostUserLoggedOut(data: IUser): Promise { + for (const appId of this.listeners.get(AppInterface.IPostUserLoggedOut)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_USER_LOGGED_OUT, data); + } + } + + private async executePostUserStatusChanged(data: IUserStatusContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostUserStatusChanged)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_USER_STATUS_CHANGED, data); + } + } +} diff --git a/packages/apps/src/server/managers/AppOutboundCommunicationProvider.ts b/packages/apps/src/server/managers/AppOutboundCommunicationProvider.ts new file mode 100644 index 0000000000000..7239f84161f67 --- /dev/null +++ b/packages/apps/src/server/managers/AppOutboundCommunicationProvider.ts @@ -0,0 +1,52 @@ +import type { AppAccessorManager } from '.'; +import { AppMethod } from '@rocket.chat/apps-engine/definition/metadata'; +import type { IOutboundMessage, IOutboundMessageProviders, ProviderMetadata } from '@rocket.chat/apps-engine/definition/outboundCommunication'; +import type { ProxiedApp } from '../ProxiedApp'; +import { AppOutboundProcessError } from '../errors/AppOutboundProcessError'; +import type { AppLogStorage } from '../storage'; + +export class OutboundMessageProvider { + public isRegistered: boolean; + + constructor( + public app: ProxiedApp, + public provider: IOutboundMessageProviders, + ) { + this.isRegistered = false; + } + + public async runGetProviderMetadata(logStorage: AppLogStorage, accessors: AppAccessorManager): Promise { + return this.runTheCode(AppMethod._OUTBOUND_GET_PROVIDER_METADATA, logStorage, accessors, []); + } + + public async runSendOutboundMessage(logStorage: AppLogStorage, accessors: AppAccessorManager, body: IOutboundMessage): Promise { + await this.runTheCode(AppMethod._OUTBOUND_SEND_MESSAGE, logStorage, accessors, [body]); + } + + private async runTheCode( + method: AppMethod._OUTBOUND_GET_PROVIDER_METADATA | AppMethod._OUTBOUND_SEND_MESSAGE, + logStorage: AppLogStorage, + accessors: AppAccessorManager, + runContextArgs: Array, + ): Promise { + const provider = `${this.provider.name}-${this.provider.type}`; + + try { + const result = await this.app.getRuntimeController().sendRequest({ + method: `outboundCommunication:${provider}:${method}`, + params: runContextArgs, + }); + + return result as T; + } catch (e) { + if (e?.message === 'error-invalid-provider') { + throw new Error('error-provider-not-registered'); + } + throw new AppOutboundProcessError(e.message, method); + } + } + + public setRegistered(registered: boolean): void { + this.isRegistered = registered; + } +} diff --git a/packages/apps/src/server/managers/AppOutboundCommunicationProviderManager.ts b/packages/apps/src/server/managers/AppOutboundCommunicationProviderManager.ts new file mode 100644 index 0000000000000..996692715cfb2 --- /dev/null +++ b/packages/apps/src/server/managers/AppOutboundCommunicationProviderManager.ts @@ -0,0 +1,138 @@ +import type { AppAccessorManager } from '.'; +import type { + IOutboundMessageProviders, + IOutboundEmailMessageProvider, + IOutboundPhoneMessageProvider, + ValidOutboundProvider, + IOutboundMessage, +} from '@rocket.chat/apps-engine/definition/outboundCommunication'; +import type { AppManager } from '../AppManager'; +import type { OutboundMessageBridge } from '../bridges'; +import { OutboundMessageProvider } from './AppOutboundCommunicationProvider'; +import { AppPermissionManager } from './AppPermissionManager'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export class AppOutboundCommunicationProviderManager { + private readonly accessors: AppAccessorManager; + + private readonly bridge: OutboundMessageBridge; + + private outboundMessageProviders: Map>; + + constructor(private readonly manager: AppManager) { + this.bridge = this.manager.getBridges().getOutboundMessageBridge(); + this.accessors = this.manager.getAccessorManager(); + + this.outboundMessageProviders = new Map>(); + } + + public isAlreadyDefined(providerId: string, providerType: ValidOutboundProvider): boolean { + const providersByApp = this.outboundMessageProviders.get(providerId); + if (!providersByApp) { + return false; + } + if (!providersByApp.get(providerType)) { + return false; + } + return true; + } + + public addProvider(appId: string, provider: IOutboundMessageProviders): void { + const app = this.manager.getOneById(appId); + if (!app) { + throw new Error('App must exist in order for an outbound provider to be added.'); + } + + if (!AppPermissionManager.hasPermission(appId, AppPermissions.outboundComms.provide)) { + throw new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.outboundComms.provide], + }); + } + + if (!this.outboundMessageProviders.has(appId)) { + this.outboundMessageProviders.set(appId, new Map()); + } + + this.outboundMessageProviders.get(appId).set(provider.type, new OutboundMessageProvider(app, provider)); + } + + public async registerProviders(appId: string): Promise { + if (!this.outboundMessageProviders.has(appId)) { + return; + } + + const appProviders = this.outboundMessageProviders.get(appId); + if (!appProviders) { + return; + } + + for await (const [, providerInfo] of appProviders) { + if (providerInfo.isRegistered) { + continue; + } + + if (providerInfo.provider.type === 'phone') { + await this.registerPhoneProvider(appId, providerInfo.provider); + providerInfo.setRegistered(true); + } else if (providerInfo.provider.type === 'email') { + await this.registerEmailProvider(appId, providerInfo.provider); + providerInfo.setRegistered(true); + } + } + } + + public async unregisterProviders(appId: string, opts?: { keepReferences: boolean }): Promise { + if (!this.outboundMessageProviders.has(appId)) { + return; + } + + const appProviders = this.outboundMessageProviders.get(appId); + for await (const [, providerInfo] of appProviders) { + await this.unregisterProvider(appId, providerInfo, opts); + } + + if (!opts?.keepReferences) { + this.outboundMessageProviders.delete(appId); + } + } + + private async registerPhoneProvider(appId: string, provider: IOutboundPhoneMessageProvider): Promise { + await this.bridge.doRegisterPhoneProvider(provider, appId); + } + + private async registerEmailProvider(appId: string, provider: IOutboundEmailMessageProvider): Promise { + await this.bridge.doRegisterEmailProvider(provider, appId); + } + + private async unregisterProvider(appId: string, info: OutboundMessageProvider, opts?: { keepReferences: boolean }): Promise { + const key = info.provider.type; + + await this.bridge.doUnRegisterProvider(info.provider, appId); + + info.setRegistered(false); + + if (!opts?.keepReferences) { + this.outboundMessageProviders.get(appId)?.delete(key); + } + } + + public getProviderMetadata(appId: string, providerType: ValidOutboundProvider) { + const providerInfo = this.outboundMessageProviders.get(appId)?.get(providerType); + if (!providerInfo) { + throw new Error('provider-not-registered'); + } + + return providerInfo.runGetProviderMetadata(this.manager.getLogStorage(), this.accessors); + } + + public sendOutboundMessage(appId: string, providerType: ValidOutboundProvider, body: IOutboundMessage) { + const providerInfo = this.outboundMessageProviders.get(appId)?.get(providerType); + if (!providerInfo) { + throw new Error('provider-not-registered'); + } + + return providerInfo.runSendOutboundMessage(this.manager.getLogStorage(), this.accessors, body); + } +} diff --git a/packages/apps/src/server/managers/AppPermissionManager.ts b/packages/apps/src/server/managers/AppPermissionManager.ts new file mode 100644 index 0000000000000..915ef35ea8992 --- /dev/null +++ b/packages/apps/src/server/managers/AppPermissionManager.ts @@ -0,0 +1,40 @@ +import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; +import { getPermissionsByAppId } from '../AppManager'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { APPS_ENGINE_RUNTIME_FILE_PREFIX } from '../runtime/AppsEngineRuntime'; + +export class AppPermissionManager { + /** + * It returns the declaration of the permission if the app declared, or it returns `undefined`. + */ + public static hasPermission

(appId: string, permission: P): P | undefined { + if (process.env.NODE_ENV === 'test') { + return permission; + } + + const grantedPermission = getPermissionsByAppId(appId).find(({ name }) => name === permission.name) as unknown; + + if (!grantedPermission) { + return undefined; + } + + return grantedPermission as P; + } + + public static notifyAboutError(err: Error): void { + if (err instanceof PermissionDeniedError) { + const { name, message } = err; + + console.error(`${name}: ${message}\n${this.getCallStack()}`); + } else { + console.error(err); + } + } + + private static getCallStack(): string { + const stack = new Error().stack.toString().split('\n'); + const appStackIndex = stack.findIndex((position) => position.includes(APPS_ENGINE_RUNTIME_FILE_PREFIX)); + + return stack.slice(4, appStackIndex).join('\n'); + } +} diff --git a/packages/apps/src/server/managers/AppRuntimeManager.ts b/packages/apps/src/server/managers/AppRuntimeManager.ts new file mode 100644 index 0000000000000..dd631bc8eebd0 --- /dev/null +++ b/packages/apps/src/server/managers/AppRuntimeManager.ts @@ -0,0 +1,76 @@ +import type { AppManager } from '../AppManager'; +import type { IParseAppPackageResult } from '../compiler'; +import type { IRuntimeController } from '../runtime/IRuntimeController'; +import { DenoRuntimeSubprocessController } from '../runtime/deno/AppsEngineDenoRuntime'; +import type { IAppStorageItem } from '../storage'; + +export type AppRuntimeParams = { + appId: string; + appSource: string; +}; + +export type ExecRequestContext = { + method: string; + params: unknown[]; +}; + +export type ExecRequestOptions = { + timeout?: number; +}; + +const defaultRuntimeFactory = (manager: AppManager, appPackage: IParseAppPackageResult, storageItem: IAppStorageItem) => + new DenoRuntimeSubprocessController(manager, appPackage, storageItem); + +export class AppRuntimeManager { + private readonly subprocesses: Record = {}; + + constructor( + private readonly manager: AppManager, + private readonly runtimeFactory = defaultRuntimeFactory, + ) {} + + public async startRuntimeForApp( + appPackage: IParseAppPackageResult, + storageItem: IAppStorageItem, + options = { force: false }, + ): Promise { + const { id: appId } = appPackage.info; + + if (appId in this.subprocesses && !options.force) { + throw new Error('App already has an associated runtime'); + } + + this.subprocesses[appId] = this.runtimeFactory(this.manager, appPackage, storageItem); + + try { + await this.subprocesses[appId].setupApp(); + } catch (error) { + const subprocess = this.subprocesses[appId]; + delete this.subprocesses[appId]; + await subprocess.stopApp(); + throw error; + } + + return this.subprocesses[appId]; + } + + public async runInSandbox(appId: string, execRequest: ExecRequestContext, options?: ExecRequestOptions): Promise { + const subprocess = this.subprocesses[appId]; + + if (!subprocess) { + throw new Error('App does not have an associated runtime'); + } + + return subprocess.sendRequest(execRequest); + } + + public async stopRuntime(controller: IRuntimeController): Promise { + await controller.stopApp(); + + const appId = controller.getAppId(); + + if (appId in this.subprocesses) { + delete this.subprocesses[appId]; + } + } +} diff --git a/packages/apps/src/server/managers/AppSchedulerManager.ts b/packages/apps/src/server/managers/AppSchedulerManager.ts new file mode 100644 index 0000000000000..b6a7ba0e4ee29 --- /dev/null +++ b/packages/apps/src/server/managers/AppSchedulerManager.ts @@ -0,0 +1,98 @@ +import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import type { IJobContext, IOnetimeSchedule, IProcessor, IRecurringSchedule } from '@rocket.chat/apps-engine/definition/scheduler'; +import type { AppManager } from '../AppManager'; +import type { IInternalSchedulerBridge } from '../bridges/IInternalSchedulerBridge'; +import type { SchedulerBridge } from '../bridges/SchedulerBridge'; + +function createProcessorId(jobId: string, appId: string): string { + return jobId.includes(`_${appId}`) ? jobId : `${jobId}_${appId}`; +} + +export class AppSchedulerManager { + private readonly bridge: SchedulerBridge; + + private registeredProcessors: Map; + + constructor(private readonly manager: AppManager) { + this.bridge = this.manager.getBridges().getSchedulerBridge(); + this.registeredProcessors = new Map(); + } + + public async registerProcessors(processors: Array = [], appId: string): Promise> { + if (!this.registeredProcessors.get(appId)) { + this.registeredProcessors.set(appId, {}); + } + + return this.bridge.doRegisterProcessors( + processors.map((processor) => { + const processorId = createProcessorId(processor.id, appId); + + this.registeredProcessors.get(appId)[processorId] = processor; + + return { + id: processorId, + processor: this.wrapProcessor(appId, processorId).bind(this), + startupSetting: processor.startupSetting, + }; + }), + appId, + ); + } + + public wrapProcessor(appId: string, processorId: string): IProcessor['processor'] { + return async (jobContext: IJobContext) => { + const processor = this.registeredProcessors.get(appId)[processorId]; + + if (!processor) { + throw new Error(`Processor ${processorId} not available`); + } + + const app = this.manager.getOneById(appId); + const status = await app.getStatus(); + const previousStatus = app.getPreviousStatus(); + + const isNotToRunJob = this.isNotToRunJob(status, previousStatus); + + if (isNotToRunJob) { + return; + } + + try { + await app.getRuntimeController().sendRequest({ + method: `scheduler:${processor.id}`, + params: [jobContext], + }); + } catch (e) { + console.error(e); + throw e; + } + }; + } + + public async scheduleOnce(job: IOnetimeSchedule, appId: string): Promise { + return this.bridge.doScheduleOnce({ ...job, id: createProcessorId(job.id, appId) }, appId); + } + + public async scheduleRecurring(job: IRecurringSchedule, appId: string): Promise { + return this.bridge.doScheduleRecurring({ ...job, id: createProcessorId(job.id, appId) }, appId); + } + + public async cancelJob(jobId: string, appId: string): Promise { + return this.bridge.doCancelJob(createProcessorId(jobId, appId), appId); + } + + public async cancelAllJobs(appId: string): Promise { + return this.bridge.doCancelAllJobs(appId); + } + + public async cleanUp(appId: string): Promise { + await (this.bridge as IInternalSchedulerBridge & SchedulerBridge).cancelAllJobs(appId); + } + + private isNotToRunJob(status: AppStatus, previousStatus: AppStatus): boolean { + const isAppCurrentDisabled = status === AppStatus.DISABLED || status === AppStatus.MANUALLY_DISABLED; + const wasAppDisabled = previousStatus === AppStatus.DISABLED || previousStatus === AppStatus.MANUALLY_DISABLED; + + return (status === AppStatus.INITIALIZED && wasAppDisabled) || isAppCurrentDisabled; + } +} diff --git a/packages/apps/src/server/managers/AppSettingsManager.ts b/packages/apps/src/server/managers/AppSettingsManager.ts new file mode 100644 index 0000000000000..a039008ba57e7 --- /dev/null +++ b/packages/apps/src/server/managers/AppSettingsManager.ts @@ -0,0 +1,56 @@ +import { AppMethod } from '@rocket.chat/apps-engine/definition/metadata'; +import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; +import type { ISettingUpdateContext } from '@rocket.chat/apps-engine/definition/settings/ISettingUpdateContext'; +import type { AppManager } from '../AppManager'; +import { Utilities } from '../misc/Utilities'; + +export class AppSettingsManager { + constructor(private manager: AppManager) {} + + public getAppSettings(appId: string): { [key: string]: ISetting } { + const rl = this.manager.getOneById(appId); + + if (!rl) { + throw new Error('No App found by the provided id.'); + } + + return Utilities.deepCloneAndFreeze(rl.getStorageItem().settings); + } + + public getAppSetting(appId: string, settingId: string): ISetting { + const settings = this.getAppSettings(appId); + + if (!settings[settingId]) { + throw new Error('No setting found for the App by the provided id.'); + } + + return Utilities.deepCloneAndFreeze(settings[settingId]); + } + + public async updateAppSetting(appId: string, setting: ISetting): Promise { + const rl = this.manager.getOneById(appId); + + if (!rl) { + throw new Error('No App found by the provided id.'); + } + + const storageItem = rl.getStorageItem(); + + const oldSetting = storageItem.settings[setting.id]; + if (!oldSetting) { + throw new Error('No setting found for the App by the provided id.'); + } + + const decoratedSetting = + (await rl.call(AppMethod.ON_PRE_SETTING_UPDATE, { oldSetting, newSetting: setting } as ISettingUpdateContext)) || setting; + + decoratedSetting.updatedAt = new Date(); + storageItem.settings[decoratedSetting.id] = decoratedSetting; + + await this.manager.getStorage().updateSetting(storageItem._id, decoratedSetting); + + this.manager.getBridges().getAppDetailChangesBridge().doOnAppSettingsChange(appId, decoratedSetting); + + await rl.call(AppMethod.ONSETTINGUPDATED, decoratedSetting); + } +} diff --git a/packages/apps/src/server/managers/AppSignatureManager.ts b/packages/apps/src/server/managers/AppSignatureManager.ts new file mode 100644 index 0000000000000..1b24cd9d7d4da --- /dev/null +++ b/packages/apps/src/server/managers/AppSignatureManager.ts @@ -0,0 +1,85 @@ +import { createHash } from 'crypto'; + +import * as jose from 'jose'; + +import type { AppManager } from '../AppManager'; +import type { IInternalFederationBridge } from '../bridges'; +import type { IAppStorageItem } from '../storage'; + +export class AppSignatureManager { + private readonly federationBridge: IInternalFederationBridge; + + private readonly checksumAlgorithm = 'SHA256'; + + private readonly signingAlgorithm = 'RS512'; + + private privateKey: string; + + private publicKey: string; + + constructor(private readonly manager: AppManager) { + this.federationBridge = this.manager.getBridges().getInternalFederationBridge(); + } + + public async verifySignedApp(app: IAppStorageItem): Promise { + const publicKey = await jose.importSPKI(await this.getPublicKey(), 'pem'); + const { payload } = await jose.jwtVerify(app.signature, publicKey); + + const checksum = this.calculateChecksumForApp(app); + + if (payload.checksum !== checksum) { + throw new Error('Invalid checksum'); + } + } + + public async signApp(app: IAppStorageItem): Promise { + const checksum = this.calculateChecksumForApp(app); + const privateKey = await jose.importPKCS8(await this.getPrivateKey(), this.signingAlgorithm); + const signature = await new jose.SignJWT({ checksum, calg: this.checksumAlgorithm }) + .setProtectedHeader({ alg: this.signingAlgorithm }) + .setIssuedAt() + .sign(privateKey); + + return signature; + } + + private async getPrivateKey(): Promise { + if (!this.privateKey) { + this.privateKey = await this.federationBridge.getPrivateKey(); + } + return this.privateKey; + } + + private async getPublicKey(): Promise { + if (!this.publicKey) { + this.publicKey = await this.federationBridge.getPublicKey(); + } + return this.publicKey; + } + + private calculateChecksumForApp(app: IAppStorageItem, alg = this.checksumAlgorithm): string { + return createHash(alg).update(this.getFieldsForChecksum(app)).digest('hex'); + } + + private getFieldsForChecksum(obj: IAppStorageItem): string { + // These fields don't hold valuable information and should NOT invalidate + // the checksum + const fieldsToIgnore = ['_id', 'status', 'signature', 'updatedAt', 'createdAt', '_updatedAt', '_createdAt', 'settings']; + + // TODO revisit algorithm + const allKeys: Array = []; + const seen: Record = {}; + + JSON.stringify(obj, (key, value) => { + if (!(key in seen)) { + allKeys.push(key); + seen[key] = null; + } + return value; + }); + + const filteredKeys = allKeys.sort().filter((key) => !fieldsToIgnore.includes(key)); + + return JSON.stringify(obj, filteredKeys); + } +} diff --git a/packages/apps/src/server/managers/AppSlashCommand.ts b/packages/apps/src/server/managers/AppSlashCommand.ts new file mode 100644 index 0000000000000..798c9a563b05f --- /dev/null +++ b/packages/apps/src/server/managers/AppSlashCommand.ts @@ -0,0 +1,80 @@ +import { AppMethod } from '@rocket.chat/apps-engine/definition/metadata'; +import type { ISlashCommand, ISlashCommandPreview, ISlashCommandPreviewItem, SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands'; +import type { ProxiedApp } from '../ProxiedApp'; +import type { AppLogStorage } from '../storage'; +import type { AppAccessorManager } from './AppAccessorManager'; + +export class AppSlashCommand { + /** + * States whether this command has been registered into the Rocket.Chat system or not. + */ + public isRegistered: boolean; + + /** + * Declares whether this command has been enabled or not, + * does not have to be inside of the Rocket.Chat system if `isRegistered` is false. + */ + public isEnabled: boolean; + + /** + * Proclaims whether this command has been disabled or not, + * does not have to be inside the Rocket.Chat system if `isRegistered` is false. + */ + public isDisabled: boolean; + + constructor( + public app: ProxiedApp, + public slashCommand: ISlashCommand, + ) { + this.isRegistered = false; + this.isEnabled = false; + this.isDisabled = false; + } + + public hasBeenRegistered(): void { + this.isDisabled = false; + this.isEnabled = true; + this.isRegistered = true; + } + + public async runExecutorOrPreviewer( + method: AppMethod._COMMAND_EXECUTOR | AppMethod._COMMAND_PREVIEWER, + context: SlashCommandContext, + logStorage: AppLogStorage, + accessors: AppAccessorManager, + ): Promise { + return this.runTheCode(method, logStorage, accessors, context, []); + } + + public async runPreviewExecutor( + previewItem: ISlashCommandPreviewItem, + context: SlashCommandContext, + logStorage: AppLogStorage, + accessors: AppAccessorManager, + ): Promise { + await this.runTheCode(AppMethod._COMMAND_PREVIEW_EXECUTOR, logStorage, accessors, context, [previewItem]); + } + + private async runTheCode( + method: AppMethod._COMMAND_EXECUTOR | AppMethod._COMMAND_PREVIEWER | AppMethod._COMMAND_PREVIEW_EXECUTOR, + _logStorage: AppLogStorage, + _accessors: AppAccessorManager, + context: SlashCommandContext, + runContextArgs: Array, + ): Promise { + const { command } = this.slashCommand; + + try { + const result = await this.app.getRuntimeController().sendRequest({ + method: `slashcommand:${command}:${method}`, + params: [...runContextArgs, context], + }); + + return result as void | ISlashCommandPreview; + } catch (e) { + // @TODO this needs to be revisited + console.error(e); + throw e; + } + } +} diff --git a/packages/apps/src/server/managers/AppSlashCommandManager.ts b/packages/apps/src/server/managers/AppSlashCommandManager.ts new file mode 100644 index 0000000000000..fcb32f8ad6f90 --- /dev/null +++ b/packages/apps/src/server/managers/AppSlashCommandManager.ts @@ -0,0 +1,477 @@ +import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; +import { AppMethod } from '@rocket.chat/apps-engine/definition/metadata'; +import type { ISlashCommand, ISlashCommandPreview, ISlashCommandPreviewItem } from '@rocket.chat/apps-engine/definition/slashcommands'; +import { SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands'; +import type { AppManager } from '../AppManager'; +import type { CommandBridge } from '../bridges'; +import { CommandAlreadyExistsError, CommandHasAlreadyBeenTouchedError } from '../errors'; +import type { AppAccessorManager } from './AppAccessorManager'; +import { AppSlashCommand } from './AppSlashCommand'; +import { Room } from '../rooms/Room'; + +/** + * The command manager for the Apps. + * + * An App will add commands during their `initialize` method. + * Then once an App's `onEnable` is called and it returns true, + * only then will that App's commands be enabled. + * + * Registered means the command has been provided to the bridged system. + */ +export class AppSlashCommandManager { + private readonly bridge: CommandBridge; + + private readonly accessors: AppAccessorManager; + + /** + * Variable that contains the commands which have been provided by apps. + * The key of the top map is app id and the key of the inner map is the command + */ + private providedCommands: Map>; + + /** + * Contains the commands which have modified the system commands + */ + private modifiedCommands: Map; + + /** + * Contains the commands as keys and appId that touched it. + * Doesn't matter whether the app provided, modified, disabled, or enabled. + * As long as an app touched the command (besides to see if it exists), then it is listed here. + */ + private touchedCommandsToApps: Map; + + /** + * Contains the apps and the commands they have touched. The key is the appId and value is the commands. + * Doesn't matter whether the app provided, modified, disabled, or enabled. + * As long as an app touched the command (besides to see if it exists), then it is listed here. + */ + private appsTouchedCommands: Map>; + + constructor(private readonly manager: AppManager) { + this.bridge = this.manager.getBridges().getCommandBridge(); + this.accessors = this.manager.getAccessorManager(); + this.touchedCommandsToApps = new Map(); + this.appsTouchedCommands = new Map>(); + this.providedCommands = new Map>(); + this.modifiedCommands = new Map(); + } + + /** + * Checks whether an App can touch a command or not. There are only two ways an App can touch + * a command: + * 1. The command has yet to be touched + * 2. The app has already touched the command + * + * When do we consider an App touching a command? Whenever it adds, modifies, + * or removes one that it didn't provide. + * + * @param appId the app's id which to check for + * @param command the command to check about + * @returns whether or not the app can touch the command + */ + public canCommandBeTouchedBy(appId: string, command: string): boolean { + const cmd = command.toLowerCase().trim(); + return cmd && (!this.touchedCommandsToApps.has(cmd) || this.touchedCommandsToApps.get(cmd) === appId); + } + + /** + * Determines whether the command is already provided by an App or not. + * It is case insensitive. + * + * @param command the command to check if it exists or not + * @returns whether or not it is already provided + */ + public isAlreadyDefined(command: string): boolean { + const search = command.toLowerCase().trim(); + let exists = false; + + this.providedCommands.forEach((cmds) => { + if (cmds.has(search)) { + exists = true; + } + }); + + return exists; + } + + /** + * Adds a command to *be* registered. This will *not register* it with the + * bridged system yet as this is only called on an App's + * `initialize` method and an App might not get enabled. + * When adding a command, it can *not* already exist in the system + * (to overwrite) and another App can *not* have already touched or provided it. + * Apps are on a first come first serve basis for providing and modifying commands. + * + * @param appId the app's id which the command belongs to + * @param command the command to add to the system + */ + public async addCommand(appId: string, command: ISlashCommand): Promise { + command.command = command.command.toLowerCase().trim(); + + // Ensure the app can touch this command + if (!this.canCommandBeTouchedBy(appId, command.command)) { + throw new CommandHasAlreadyBeenTouchedError(command.command); + } + + // Verify the command doesn't exist already + if ((await this.bridge.doDoesCommandExist(command.command, appId)) || this.isAlreadyDefined(command.command)) { + throw new CommandAlreadyExistsError(command.command); + } + + const app = this.manager.getOneById(appId); + if (!app) { + throw new Error('App must exist in order for a command to be added.'); + } + + if (!this.providedCommands.has(appId)) { + this.providedCommands.set(appId, new Map()); + } + + this.providedCommands.get(appId).set(command.command, new AppSlashCommand(app, command)); + + // The app has now touched the command, so let's set it + this.setAsTouched(appId, command.command); + } + + /** + * Modifies an existing command. The command must either be the App's + * own command or a system command. One App can not modify another + * App's command. Apps are on a first come first serve basis as to whether + * or not they can touch or provide a command. If App "A" first provides, + * or overwrites, a command then App "B" can not touch that command. + * + * @param appId the app's id of the command to modify + * @param command the modified command to replace the current one with + */ + public async modifyCommand(appId: string, command: ISlashCommand): Promise { + command.command = command.command.toLowerCase().trim(); + + // Ensure the app can touch this command + if (!this.canCommandBeTouchedBy(appId, command.command)) { + throw new CommandHasAlreadyBeenTouchedError(command.command); + } + + const app = this.manager.getOneById(appId); + if (!app) { + throw new Error('App must exist in order to modify a command.'); + } + + const hasNotProvidedIt = !this.providedCommands.has(appId) || !this.providedCommands.get(appId).has(command.command); + + // They haven't provided (added) it and the bridged system doesn't have it, error out + if (hasNotProvidedIt && !(await this.bridge.doDoesCommandExist(command.command, appId))) { + throw new Error('You must first register a command before you can modify it.'); + } + + if (hasNotProvidedIt) { + await this.bridge.doModifyCommand(command, appId); + const regInfo = new AppSlashCommand(app, command); + regInfo.isDisabled = false; + regInfo.isEnabled = true; + regInfo.isRegistered = true; + this.modifiedCommands.set(command.command, regInfo); + } else { + this.providedCommands.get(appId).get(command.command).slashCommand = command; + } + + this.setAsTouched(appId, command.command); + } + + /** + * Goes and enables a command in the bridged system. The command + * which is being enabled must either be the App's or a system + * command which has yet to be touched by an App. + * + * @param appId the id of the app enabling the command + * @param command the command which is being enabled + */ + public async enableCommand(appId: string, command: string): Promise { + const cmd = command.toLowerCase().trim(); + + // Ensure the app can touch this command + if (!this.canCommandBeTouchedBy(appId, cmd)) { + throw new CommandHasAlreadyBeenTouchedError(cmd); + } + + // Handle if the App provided the command fist + if (this.providedCommands.has(appId) && this.providedCommands.get(appId).has(cmd)) { + const cmdInfo = this.providedCommands.get(appId).get(cmd); + + // A command marked as disabled can then be "enabled" but not be registered. + // This happens when an App is not enabled and they change the status of + // command based upon a setting they provide which a User can change. + if (!cmdInfo.isRegistered) { + cmdInfo.isDisabled = false; + cmdInfo.isEnabled = true; + } + + return; + } + + if (!(await this.bridge.doDoesCommandExist(cmd, appId))) { + throw new Error(`The command "${cmd}" does not exist to enable.`); + } + + await this.bridge.doEnableCommand(cmd, appId); + this.setAsTouched(appId, cmd); + } + + /** + * Renders an existing slash command un-usable. Whether that command is provided + * by the App calling this or a command provided by the bridged system, we don't care. + * However, an App can not disable a command which has already been touched + * by another App in some way. + * + * @param appId the app's id which is disabling the command + * @param command the command to disable in the bridged system + */ + public async disableCommand(appId: string, command: string): Promise { + const cmd = command.toLowerCase().trim(); + + // Ensure the app can touch this command + if (!this.canCommandBeTouchedBy(appId, cmd)) { + throw new CommandHasAlreadyBeenTouchedError(cmd); + } + + // Handle if the App provided the command fist + if (this.providedCommands.has(appId) && this.providedCommands.get(appId).has(cmd)) { + const cmdInfo = this.providedCommands.get(appId).get(cmd); + + // A command marked as enabled can then be "disabled" but not yet be registered. + // This happens when an App is not enabled and they change the status of + // command based upon a setting they provide which a User can change. + if (!cmdInfo.isRegistered) { + cmdInfo.isDisabled = true; + cmdInfo.isEnabled = false; + } + + return; + } + + if (!(await this.bridge.doDoesCommandExist(cmd, appId))) { + throw new Error(`The command "${cmd}" does not exist to disable.`); + } + + await this.bridge.doDisableCommand(cmd, appId); + this.setAsTouched(appId, cmd); + } + + /** + * Registers all of the commands for the provided app inside + * of the bridged system which then enables them. + * + * @param appId The app's id of which to register it's commands with the bridged system + */ + public async registerCommands(appId: string): Promise { + if (!this.providedCommands.has(appId)) { + return; + } + + const commands = this.providedCommands.get(appId); + for await (const [, appSlashCommand] of commands) { + if (appSlashCommand.isDisabled) { + continue; + } + await this.registerCommand(appId, appSlashCommand); + } + } + + /** + * Unregisters the commands from the system and restores the commands + * which the app modified in the system. + * + * @param appId the appId for the commands to purge + */ + public async unregisterCommands(appId: string): Promise { + if (this.providedCommands.has(appId)) { + const commands = this.providedCommands.get(appId); + for await (const [, appSlashCommand] of commands) { + const cmd = appSlashCommand.slashCommand.command; + await this.bridge.doUnregisterCommand(cmd, appId); + this.touchedCommandsToApps.delete(cmd); + if (!this.appsTouchedCommands.has(appId)) { + continue; + } + const ind = this.appsTouchedCommands.get(appId).indexOf(cmd); + this.appsTouchedCommands.get(appId).splice(ind, 1); + appSlashCommand.isRegistered = true; + } + + this.providedCommands.delete(appId); + } + + if (this.appsTouchedCommands.has(appId)) { + // The commands inside the appsTouchedCommands should now + // only be the ones which the App has enabled, disabled, or modified. + // We call restore to enable the commands provided by the bridged system + // or unmodify the commands modified by the App + this.appsTouchedCommands.get(appId).forEach((cmd) => { + // @NOTE this "restore" method isn't present in the bridge + // this.bridge.doRestoreCommand(cmd, appId); + this.modifiedCommands.get(cmd).isRegistered = false; + this.modifiedCommands.delete(cmd); + this.touchedCommandsToApps.delete(cmd); + }); + + this.appsTouchedCommands.delete(appId); + } + } + + /** + * Executes an App's command. + * + * @param command the command to execute + * @param context the context in which the command was entered + */ + public async executeCommand(command: string, context: SlashCommandContext): Promise { + const cmd = command.toLowerCase().trim(); + + if (!this.shouldCommandFunctionsRun(cmd)) { + return; + } + + const app = this.manager.getOneById(this.touchedCommandsToApps.get(cmd)); + + if (!app) { + throw new Error('App not found'); + } + + if (!AppStatusUtils.isEnabled(await app.getStatus())) { + throw new Error('App not enabled'); + } + + const appCmd = this.retrieveCommandInfo(cmd, app.getID()); + await appCmd.runExecutorOrPreviewer( + AppMethod._COMMAND_EXECUTOR, + this.ensureContext(context), + this.manager.getLogStorage(), + this.accessors, + ); + } + + public async getPreviews(command: string, context: SlashCommandContext): Promise { + const cmd = command.toLowerCase().trim(); + + if (!this.shouldCommandFunctionsRun(cmd)) { + return; + } + + const app = this.manager.getOneById(this.touchedCommandsToApps.get(cmd)); + + if (!app || AppStatusUtils.isDisabled(await app.getStatus())) { + // Just in case someone decides to do something they shouldn't + // let's ensure the app actually exists + return; + } + + const appCmd = this.retrieveCommandInfo(cmd, app.getID()); + + const result = await appCmd.runExecutorOrPreviewer( + AppMethod._COMMAND_PREVIEWER, + this.ensureContext(context), + this.manager.getLogStorage(), + this.accessors, + ); + + if (!result) { + // Failed to get the preview, thus returning is fine + return; + } + + return result; + } + + public async executePreview(command: string, previewItem: ISlashCommandPreviewItem, context: SlashCommandContext): Promise { + const cmd = command.toLowerCase().trim(); + + if (!this.shouldCommandFunctionsRun(cmd)) { + return; + } + + const app = this.manager.getOneById(this.touchedCommandsToApps.get(cmd)); + + if (!app || AppStatusUtils.isDisabled(await app.getStatus())) { + // Just in case someone decides to do something they shouldn't + // let's ensure the app actually exists + return; + } + + const appCmd = this.retrieveCommandInfo(cmd, app.getID()); + await appCmd.runPreviewExecutor(previewItem, this.ensureContext(context), this.manager.getLogStorage(), this.accessors); + } + + private ensureContext(context: SlashCommandContext): SlashCommandContext { + // Due to the internal changes for the usernames property, we need to ensure the room + // is a class and not just an interface + let room: Room; + if (context.getRoom() instanceof Room) { + room = context.getRoom() as Room; + } else { + room = new Room(context.getRoom(), this.manager); + } + + return new SlashCommandContext(context.getSender(), room, context.getArguments(), context.getThreadId(), context.getTriggerId()); + } + + /** + * Determines if the command's functions should run, + * this way the code isn't duplicated three times. + * + * @param command the lowercase and trimmed command + * @returns whether or not to continue + */ + private shouldCommandFunctionsRun(command: string): boolean { + // None of the Apps have touched the command to execute, + // thus we don't care so exit out + if (!this.touchedCommandsToApps.has(command)) { + return false; + } + + const appId = this.touchedCommandsToApps.get(command); + const cmdInfo = this.retrieveCommandInfo(command, appId); + + // Should the command information really not exist + // Or if the command hasn't been registered + // Or the command is disabled on our side + // then let's not execute it, as the App probably doesn't want it yet + if (!cmdInfo?.isRegistered || cmdInfo?.isDisabled) { + return false; + } + + return true; + } + + private retrieveCommandInfo(command: string, appId: string): AppSlashCommand { + return this.modifiedCommands.get(command) || this.providedCommands.get(appId).get(command); + } + + /** + * Sets that an App has been touched. + * + * @param appId the app's id which has touched the command + * @param command the command, lowercase and trimmed, which has been touched + */ + private setAsTouched(appId: string, command: string): void { + if (!this.appsTouchedCommands.has(appId)) { + this.appsTouchedCommands.set(appId, []); + } + + if (!this.appsTouchedCommands.get(appId).includes(command)) { + this.appsTouchedCommands.get(appId).push(command); + } + + this.touchedCommandsToApps.set(command, appId); + } + + /** + * Actually goes and provide's the bridged system with the command information. + * + * @param appId the app which is providing the command + * @param info the command's registration information + */ + private async registerCommand(appId: string, info: AppSlashCommand): Promise { + await this.bridge.doRegisterCommand(info.slashCommand, appId); + info.hasBeenRegistered(); + } +} diff --git a/packages/apps/src/server/managers/AppVideoConfProvider.ts b/packages/apps/src/server/managers/AppVideoConfProvider.ts new file mode 100644 index 0000000000000..b0e76f4073aa3 --- /dev/null +++ b/packages/apps/src/server/managers/AppVideoConfProvider.ts @@ -0,0 +1,113 @@ +import { AppMethod } from '@rocket.chat/apps-engine/definition/metadata'; +import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit'; +import type { + IVideoConferenceOptions, + IVideoConfProvider, + VideoConfData, + VideoConfDataExtended, +} from '@rocket.chat/apps-engine/definition/videoConfProviders'; +import type { VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences'; +import type { IVideoConferenceUser } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConferenceUser'; +import type { ProxiedApp } from '../ProxiedApp'; +import { JSONRPC_METHOD_NOT_FOUND } from '../runtime/deno/AppsEngineDenoRuntime'; +import type { AppLogStorage } from '../storage'; +import type { AppAccessorManager } from './AppAccessorManager'; + +export class AppVideoConfProvider { + /** + * States whether this provider has been registered into the Rocket.Chat system or not. + */ + public isRegistered: boolean; + + constructor( + public app: ProxiedApp, + public provider: IVideoConfProvider, + ) { + this.isRegistered = false; + } + + public hasBeenRegistered(): void { + this.isRegistered = true; + } + + public async runIsFullyConfigured(logStorage: AppLogStorage, accessors: AppAccessorManager): Promise { + return !!(await this.runTheCode(AppMethod._VIDEOCONF_IS_CONFIGURED, logStorage, accessors, [])) as boolean; + } + + public async runGenerateUrl(call: VideoConfData, logStorage: AppLogStorage, accessors: AppAccessorManager): Promise { + return (await this.runTheCode(AppMethod._VIDEOCONF_GENERATE_URL, logStorage, accessors, [call])) as string; + } + + public async runCustomizeUrl( + call: VideoConfDataExtended, + user: IVideoConferenceUser | undefined, + options: IVideoConferenceOptions = {}, + logStorage: AppLogStorage, + accessors: AppAccessorManager, + ): Promise { + return (await this.runTheCode(AppMethod._VIDEOCONF_CUSTOMIZE_URL, logStorage, accessors, [call, user, options])) as string; + } + + public async runOnNewVideoConference(call: VideoConference, logStorage: AppLogStorage, accessors: AppAccessorManager): Promise { + await this.runTheCode(AppMethod._VIDEOCONF_NEW, logStorage, accessors, [call]); + } + + public async runOnVideoConferenceChanged(call: VideoConference, logStorage: AppLogStorage, accessors: AppAccessorManager): Promise { + await this.runTheCode(AppMethod._VIDEOCONF_CHANGED, logStorage, accessors, [call]); + } + + public async runOnUserJoin( + call: VideoConference, + user: IVideoConferenceUser | undefined, + logStorage: AppLogStorage, + accessors: AppAccessorManager, + ): Promise { + await this.runTheCode(AppMethod._VIDEOCONF_USER_JOINED, logStorage, accessors, [call, user]); + } + + public async runGetVideoConferenceInfo( + call: VideoConference, + user: IVideoConferenceUser | undefined, + logStorage: AppLogStorage, + accessors: AppAccessorManager, + ): Promise | undefined> { + return (await this.runTheCode(AppMethod._VIDEOCONF_GET_INFO, logStorage, accessors, [call, user])) as Array | undefined; + } + + private async runTheCode( + method: + | AppMethod._VIDEOCONF_GENERATE_URL + | AppMethod._VIDEOCONF_CUSTOMIZE_URL + | AppMethod._VIDEOCONF_IS_CONFIGURED + | AppMethod._VIDEOCONF_NEW + | AppMethod._VIDEOCONF_CHANGED + | AppMethod._VIDEOCONF_GET_INFO + | AppMethod._VIDEOCONF_USER_JOINED, + _logStorage: AppLogStorage, + _accessors: AppAccessorManager, + runContextArgs: Array, + ): Promise | undefined> { + const provider = this.provider.name; + + try { + const result = await this.app.getRuntimeController().sendRequest({ + method: `videoconference:${provider}:${method}`, + params: runContextArgs, + }); + + return result as string | boolean | Array | undefined; + } catch (e) { + if (e?.code === JSONRPC_METHOD_NOT_FOUND) { + if (method === AppMethod._VIDEOCONF_IS_CONFIGURED) { + return true; + } + if (![AppMethod._VIDEOCONF_GENERATE_URL, AppMethod._VIDEOCONF_CUSTOMIZE_URL].includes(method)) { + return undefined; + } + } + + // @TODO add error handling + console.log(e); + } + } +} diff --git a/packages/apps/src/server/managers/AppVideoConfProviderManager.ts b/packages/apps/src/server/managers/AppVideoConfProviderManager.ts new file mode 100644 index 0000000000000..c961b5b1ccf79 --- /dev/null +++ b/packages/apps/src/server/managers/AppVideoConfProviderManager.ts @@ -0,0 +1,216 @@ +import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit'; +import type { + IVideoConferenceOptions, + IVideoConfProvider, + VideoConfData, + VideoConfDataExtended, +} from '@rocket.chat/apps-engine/definition/videoConfProviders'; +import type { VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences'; +import type { IVideoConferenceUser } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConferenceUser'; +import type { AppManager } from '../AppManager'; +import type { VideoConferenceBridge } from '../bridges'; +import { VideoConfProviderAlreadyExistsError, VideoConfProviderNotRegisteredError } from '../errors'; +import type { AppAccessorManager } from './AppAccessorManager'; +import { AppPermissionManager } from './AppPermissionManager'; +import { AppVideoConfProvider } from './AppVideoConfProvider'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export class AppVideoConfProviderManager { + private readonly accessors: AppAccessorManager; + + private readonly bridge: VideoConferenceBridge; + + private videoConfProviders: Map>; + + private providerApps: Map; + + constructor(private readonly manager: AppManager) { + this.bridge = this.manager.getBridges().getVideoConferenceBridge(); + this.accessors = this.manager.getAccessorManager(); + + this.videoConfProviders = new Map>(); + this.providerApps = new Map(); + } + + public canProviderBeTouchedBy(appId: string, providerName: string): boolean { + const key = providerName.toLowerCase().trim(); + return (key && (!this.providerApps.has(key) || this.providerApps.get(key) === appId)) || false; + } + + public isAlreadyDefined(providerName: string): boolean { + const search = providerName.toLowerCase().trim(); + + for (const [, providers] of this.videoConfProviders) { + if (providers.has(search)) { + return true; + } + } + + return false; + } + + public addProvider(appId: string, provider: IVideoConfProvider): void { + const app = this.manager.getOneById(appId); + if (!app) { + throw new Error('App must exist in order for a video conference provider to be added.'); + } + + if (!AppPermissionManager.hasPermission(appId, AppPermissions.videoConference.provider)) { + throw new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.videoConference.provider], + }); + } + + const providerName = provider.name.toLowerCase().trim(); + if (!this.canProviderBeTouchedBy(appId, providerName)) { + throw new VideoConfProviderAlreadyExistsError(provider.name); + } + + if (!this.videoConfProviders.has(appId)) { + this.videoConfProviders.set(appId, new Map()); + } + + this.videoConfProviders.get(appId).set(providerName, new AppVideoConfProvider(app, provider)); + this.linkAppProvider(appId, providerName); + } + + public registerProviders(appId: string): void { + if (!this.videoConfProviders.has(appId)) { + return; + } + + const appProviders = this.videoConfProviders.get(appId); + if (!appProviders) { + return; + } + + for (const [, providerInfo] of appProviders) { + this.registerProvider(appId, providerInfo); + } + } + + public unregisterProviders(appId: string): void { + if (!this.videoConfProviders.has(appId)) { + return; + } + + const appProviders = this.videoConfProviders.get(appId); + for (const [, providerInfo] of appProviders) { + this.unregisterProvider(appId, providerInfo); + } + + this.videoConfProviders.delete(appId); + } + + public async isFullyConfigured(providerName: string): Promise { + const providerInfo = this.retrieveProviderInfo(providerName); + if (!providerInfo) { + throw new VideoConfProviderNotRegisteredError(providerName); + } + + return providerInfo.runIsFullyConfigured(this.manager.getLogStorage(), this.accessors); + } + + public async onNewVideoConference(providerName: string, call: VideoConference): Promise { + const providerInfo = this.retrieveProviderInfo(providerName); + if (!providerInfo) { + throw new VideoConfProviderNotRegisteredError(providerName); + } + + return providerInfo.runOnNewVideoConference(call, this.manager.getLogStorage(), this.accessors); + } + + public async onVideoConferenceChanged(providerName: string, call: VideoConference): Promise { + const providerInfo = this.retrieveProviderInfo(providerName); + if (!providerInfo) { + throw new VideoConfProviderNotRegisteredError(providerName); + } + + return providerInfo.runOnVideoConferenceChanged(call, this.manager.getLogStorage(), this.accessors); + } + + public async onUserJoin(providerName: string, call: VideoConference, user?: IVideoConferenceUser): Promise { + const providerInfo = this.retrieveProviderInfo(providerName); + if (!providerInfo) { + throw new VideoConfProviderNotRegisteredError(providerName); + } + + return providerInfo.runOnUserJoin(call, user, this.manager.getLogStorage(), this.accessors); + } + + public async getVideoConferenceInfo( + providerName: string, + call: VideoConference, + user?: IVideoConferenceUser, + ): Promise | undefined> { + const providerInfo = this.retrieveProviderInfo(providerName); + if (!providerInfo) { + throw new VideoConfProviderNotRegisteredError(providerName); + } + + return providerInfo.runGetVideoConferenceInfo(call, user, this.manager.getLogStorage(), this.accessors); + } + + public async generateUrl(providerName: string, call: VideoConfData): Promise { + const providerInfo = this.retrieveProviderInfo(providerName); + if (!providerInfo) { + throw new VideoConfProviderNotRegisteredError(providerName); + } + + return providerInfo.runGenerateUrl(call, this.manager.getLogStorage(), this.accessors); + } + + public async customizeUrl( + providerName: string, + call: VideoConfDataExtended, + user?: IVideoConferenceUser, + options?: IVideoConferenceOptions, + ): Promise { + const providerInfo = this.retrieveProviderInfo(providerName); + if (!providerInfo) { + throw new VideoConfProviderNotRegisteredError(providerName); + } + + return providerInfo.runCustomizeUrl(call, user, options, this.manager.getLogStorage(), this.accessors); + } + + private retrieveProviderInfo(providerName: string): AppVideoConfProvider | undefined { + const key = providerName.toLowerCase().trim(); + + for (const [, providers] of this.videoConfProviders) { + if (!providers.has(key)) { + continue; + } + + const provider = providers.get(key); + if (provider.isRegistered) { + return provider; + } + } + } + + private linkAppProvider(appId: string, providerName: string): void { + this.providerApps.set(providerName, appId); + } + + private registerProvider(appId: string, info: AppVideoConfProvider): void { + this.bridge.doRegisterProvider(info.provider, appId); + info.hasBeenRegistered(); + } + + private unregisterProvider(appId: string, info: AppVideoConfProvider): void { + const key = info.provider.name.toLowerCase().trim(); + + this.bridge.doUnRegisterProvider(info.provider, appId); + this.providerApps.delete(key); + + info.isRegistered = false; + + const map = this.videoConfProviders.get(appId); + if (map) { + map.delete(key); + } + } +} diff --git a/packages/apps/src/server/managers/UIActionButtonManager.ts b/packages/apps/src/server/managers/UIActionButtonManager.ts new file mode 100644 index 0000000000000..6b47448c3b09d --- /dev/null +++ b/packages/apps/src/server/managers/UIActionButtonManager.ts @@ -0,0 +1,95 @@ +import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; +import type { IUIActionButton, IUIActionButtonDescriptor } from '@rocket.chat/apps-engine/definition/ui'; +import type { AppManager } from '../AppManager'; +import type { AppActivationBridge } from '../bridges'; +import { AppPermissionManager } from './AppPermissionManager'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export class UIActionButtonManager { + private readonly activationBridge: AppActivationBridge; + + private readonly manager: AppManager; + + private registeredActionButtons = new Map>(); + + constructor(manager: AppManager) { + this.manager = manager; + this.activationBridge = manager.getBridges().getAppActivationBridge(); + } + + public registerActionButton(appId: string, button: IUIActionButtonDescriptor) { + if (!this.hasPermission(appId)) { + return false; + } + + if (!this.registeredActionButtons.has(appId)) { + this.registeredActionButtons.set(appId, new Map()); + } + + this.registeredActionButtons.get(appId).set(button.actionId, button); + + this.activationBridge.doActionsChanged(); + + return true; + } + + public clearAppActionButtons(appId: string) { + this.registeredActionButtons.set(appId, new Map()); + this.activationBridge.doActionsChanged(); + } + + public getAppActionButtons(appId: string) { + return this.registeredActionButtons.get(appId); + } + + public async getAllActionButtons(): Promise> { + const buttonList: Array = []; + + // Flatten map to a simple list of buttons from enabled apps only + for (const [appId, appButtons] of this.registeredActionButtons) { + const app = this.manager.getOneById(appId); + + // Skip if app doesn't exist + if (!app) { + continue; + } + + // or if it is not enabled + try { + const appStatus = await app.getStatus(); + if (!AppStatusUtils.isEnabled(appStatus)) { + continue; + } + } catch (error) { + // If we can't get the app status, skip this app's buttons + continue; + } + + // Add buttons from this enabled app + appButtons.forEach((button) => + buttonList.push({ + ...button, + appId, + }), + ); + } + + return buttonList; + } + + private hasPermission(appId: string) { + if (AppPermissionManager.hasPermission(appId, AppPermissions.ui.registerButtons)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.ui.registerButtons], + }), + ); + + return false; + } +} diff --git a/packages/apps/src/server/managers/index.ts b/packages/apps/src/server/managers/index.ts new file mode 100644 index 0000000000000..9d7b22c79bc53 --- /dev/null +++ b/packages/apps/src/server/managers/index.ts @@ -0,0 +1,23 @@ +import { AppAccessorManager } from './AppAccessorManager'; +import { AppApiManager } from './AppApiManager'; +import { AppExternalComponentManager } from './AppExternalComponentManager'; +import { AppLicenseManager } from './AppLicenseManager'; +import { AppListenerManager } from './AppListenerManager'; +import { AppOutboundCommunicationProviderManager } from './AppOutboundCommunicationProviderManager'; +import { AppSchedulerManager } from './AppSchedulerManager'; +import { AppSettingsManager } from './AppSettingsManager'; +import { AppSlashCommandManager } from './AppSlashCommandManager'; +import { AppVideoConfProviderManager } from './AppVideoConfProviderManager'; + +export { + AppAccessorManager, + AppLicenseManager, + AppListenerManager, + AppExternalComponentManager, + AppSettingsManager, + AppSlashCommandManager, + AppApiManager, + AppSchedulerManager, + AppVideoConfProviderManager, + AppOutboundCommunicationProviderManager, +}; diff --git a/packages/apps/src/server/marketplace/IAppLicenseMetadata.ts b/packages/apps/src/server/marketplace/IAppLicenseMetadata.ts new file mode 100644 index 0000000000000..0c8ceb9411979 --- /dev/null +++ b/packages/apps/src/server/marketplace/IAppLicenseMetadata.ts @@ -0,0 +1,5 @@ +export interface IAppLicenseMetadata { + license: string; + version: number; + expireDate: Date; +} diff --git a/packages/apps/src/server/marketplace/IMarketplaceInfo.ts b/packages/apps/src/server/marketplace/IMarketplaceInfo.ts new file mode 100644 index 0000000000000..fbf132869e1c1 --- /dev/null +++ b/packages/apps/src/server/marketplace/IMarketplaceInfo.ts @@ -0,0 +1,24 @@ +import type { IMarketplacePricingPlan } from './IMarketplacePricingPlan'; +import type { IMarketplaceSimpleBundleInfo } from './IMarketplaceSimpleBundleInfo'; +import type { IMarketplaceSubscriptionInfo } from './IMarketplaceSubscriptionInfo'; +import type { MarketplacePurchaseType } from './MarketplacePurchaseType'; +import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; + +export interface IMarketplaceInfo extends IAppInfo { + categories: Array; + status: string; + reviewedNote?: string; + rejectionNote?: string; + isVisible: boolean; + isPurchased: boolean; + isSubscribed: boolean; + isBundled: boolean; + createdDate: string; + modifiedDate: string; + price: number; + subscriptionInfo?: IMarketplaceSubscriptionInfo; + purchaseType: MarketplacePurchaseType; + pricingPlans?: Array; + bundledIn?: Array; + isEnterpriseOnly?: boolean; +} diff --git a/packages/apps/src/server/marketplace/IMarketplacePricingPlan.ts b/packages/apps/src/server/marketplace/IMarketplacePricingPlan.ts new file mode 100644 index 0000000000000..7d3a7f5958c11 --- /dev/null +++ b/packages/apps/src/server/marketplace/IMarketplacePricingPlan.ts @@ -0,0 +1,11 @@ +import type { IMarketplacePricingTier } from './IMarketplacePricingTier'; +import type { MarketplacePricingStrategy } from './MarketplacePricingStrategy'; + +export interface IMarketplacePricingPlan { + id: string; + enabled: boolean; + price: number; + isPerSeat: boolean; + strategy: MarketplacePricingStrategy; + tiers?: Array; +} diff --git a/packages/apps/src/server/marketplace/IMarketplacePricingTier.ts b/packages/apps/src/server/marketplace/IMarketplacePricingTier.ts new file mode 100644 index 0000000000000..65d3f593f7bff --- /dev/null +++ b/packages/apps/src/server/marketplace/IMarketplacePricingTier.ts @@ -0,0 +1,6 @@ +export interface IMarketplacePricingTier { + perUnit: boolean; + minimum: number; + maximum: number; + price: number; +} diff --git a/packages/apps/src/server/marketplace/IMarketplaceSimpleBundleInfo.ts b/packages/apps/src/server/marketplace/IMarketplaceSimpleBundleInfo.ts new file mode 100644 index 0000000000000..4fc68e39afc9b --- /dev/null +++ b/packages/apps/src/server/marketplace/IMarketplaceSimpleBundleInfo.ts @@ -0,0 +1,4 @@ +export interface IMarketplaceSimpleBundleInfo { + bundleId: string; + bundleName: string; +} diff --git a/packages/apps/src/server/marketplace/IMarketplaceSubscriptionInfo.ts b/packages/apps/src/server/marketplace/IMarketplaceSubscriptionInfo.ts new file mode 100644 index 0000000000000..4be076b629ad9 --- /dev/null +++ b/packages/apps/src/server/marketplace/IMarketplaceSubscriptionInfo.ts @@ -0,0 +1,15 @@ +import type { IAppLicenseMetadata } from './IAppLicenseMetadata'; +import type { MarketplaceSubscriptionStatus } from './MarketplaceSubscriptionStatus'; +import type { MarketplaceSubscriptionType } from './MarketplaceSubscriptionType'; + +export interface IMarketplaceSubscriptionInfo { + seats: number; + maxSeats: number; + startDate: string; + periodEnd: string; + isSubscripbedViaBundle: boolean; + endDate?: string; + typeOf: MarketplaceSubscriptionType; + status: MarketplaceSubscriptionStatus; + license: IAppLicenseMetadata; +} diff --git a/packages/apps/src/server/marketplace/MarketplacePricingStrategy.ts b/packages/apps/src/server/marketplace/MarketplacePricingStrategy.ts new file mode 100644 index 0000000000000..473f4428430c0 --- /dev/null +++ b/packages/apps/src/server/marketplace/MarketplacePricingStrategy.ts @@ -0,0 +1,5 @@ +export enum MarketplacePricingStrategy { + PricingStrategyOnce = 'once', + PricingStrategyMonthly = 'monthly', + PricingStrategyYearly = 'yearly', +} diff --git a/packages/apps/src/server/marketplace/MarketplacePurchaseType.ts b/packages/apps/src/server/marketplace/MarketplacePurchaseType.ts new file mode 100644 index 0000000000000..e4deef5b7f9c3 --- /dev/null +++ b/packages/apps/src/server/marketplace/MarketplacePurchaseType.ts @@ -0,0 +1,4 @@ +export enum MarketplacePurchaseType { + PurchaseTypeBuy = 'buy', + PurchaseTypeSubscription = 'subscription', +} diff --git a/packages/apps/src/server/marketplace/MarketplaceSubscriptionStatus.ts b/packages/apps/src/server/marketplace/MarketplaceSubscriptionStatus.ts new file mode 100644 index 0000000000000..c57bc59440cee --- /dev/null +++ b/packages/apps/src/server/marketplace/MarketplaceSubscriptionStatus.ts @@ -0,0 +1,10 @@ +export enum MarketplaceSubscriptionStatus { + // PurchaseSubscriptionStatusTrialing is when the subscription is in the trial phase + PurchaseSubscriptionStatusTrialing = 'trialing', + // PurchaseSubscriptionStatusActive is when the subscription is active and being billed for + PurchaseSubscriptionStatusActive = 'active', + // PurchaseSubscriptionStatusCanceled is when the subscription is inactive due to being canceled + PurchaseSubscriptionStatusCanceled = 'canceled', + // PurchaseSubscriptionStatusPastDue is when the subscription was active but is now past due as a result of incorrect billing information + PurchaseSubscriptionStatusPastDue = 'pastDue', +} diff --git a/packages/apps/src/server/marketplace/MarketplaceSubscriptionType.ts b/packages/apps/src/server/marketplace/MarketplaceSubscriptionType.ts new file mode 100644 index 0000000000000..30256fc48cf1e --- /dev/null +++ b/packages/apps/src/server/marketplace/MarketplaceSubscriptionType.ts @@ -0,0 +1,4 @@ +export enum MarketplaceSubscriptionType { + SubscriptionTypeApp = 'app', + SubscriptionTypeService = 'service', +} diff --git a/packages/apps/src/server/marketplace/index.ts b/packages/apps/src/server/marketplace/index.ts new file mode 100644 index 0000000000000..d06ea988cc43d --- /dev/null +++ b/packages/apps/src/server/marketplace/index.ts @@ -0,0 +1,15 @@ +import { IAppLicenseMetadata } from './IAppLicenseMetadata'; +import { IMarketplaceInfo } from './IMarketplaceInfo'; +import { IMarketplacePricingPlan } from './IMarketplacePricingPlan'; +import { IMarketplacePricingTier } from './IMarketplacePricingTier'; +import { IMarketplaceSimpleBundleInfo } from './IMarketplaceSimpleBundleInfo'; +import { IMarketplaceSubscriptionInfo } from './IMarketplaceSubscriptionInfo'; + +export { + IAppLicenseMetadata, + IMarketplaceInfo, + IMarketplacePricingPlan, + IMarketplacePricingTier, + IMarketplaceSimpleBundleInfo, + IMarketplaceSubscriptionInfo, +}; diff --git a/packages/apps/src/server/marketplace/license/AppLicenseValidationResult.ts b/packages/apps/src/server/marketplace/license/AppLicenseValidationResult.ts new file mode 100644 index 0000000000000..2946d40111f54 --- /dev/null +++ b/packages/apps/src/server/marketplace/license/AppLicenseValidationResult.ts @@ -0,0 +1,56 @@ +export class AppLicenseValidationResult { + private errors: { [key: string]: string } = {}; + + private warnings: { [key: string]: string } = {}; + + private validated = false; + + private appId: string; + + public addError(field: string, message: string): void { + this.errors[field] = message; + } + + public addWarning(field: string, message: string): void { + this.warnings[field] = message; + } + + public get hasErrors(): boolean { + return !!Object.keys(this.errors).length; + } + + public get hasWarnings(): boolean { + return !!Object.keys(this.warnings).length; + } + + public get hasBeenValidated(): boolean { + return this.validated; + } + + public setValidated(validated: boolean): void { + this.validated = validated; + } + + public setAppId(appId: string): void { + this.appId = appId; + } + + public getAppId(): string { + return this.appId; + } + + public getErrors(): object { + return this.errors; + } + + public getWarnings(): object { + return this.warnings; + } + + public toJSON(): object { + return { + errors: this.errors, + warnings: this.warnings, + }; + } +} diff --git a/packages/apps/src/server/marketplace/license/Crypto.ts b/packages/apps/src/server/marketplace/license/Crypto.ts new file mode 100644 index 0000000000000..95e1db449fcd9 --- /dev/null +++ b/packages/apps/src/server/marketplace/license/Crypto.ts @@ -0,0 +1,26 @@ +import { publicDecrypt } from 'crypto'; + +import type { IInternalBridge } from '../../bridges'; + +export class Crypto { + constructor(private readonly internalBridge: IInternalBridge) {} + + public async decryptLicense(content: string): Promise { + const publicKeySetting = await this.internalBridge.doGetWorkspacePublicKey(); + + if (!publicKeySetting?.value) { + throw new Error('Public key not available, cannot decrypt'); // TODO: add custom error? + } + + const decoded = publicDecrypt(publicKeySetting.value, Buffer.from(content, 'base64')); + + let license; + try { + license = JSON.parse(decoded.toString()); + } catch (error) { + throw new Error('Invalid license provided'); + } + + return license; + } +} diff --git a/packages/apps/src/server/marketplace/license/index.ts b/packages/apps/src/server/marketplace/license/index.ts new file mode 100644 index 0000000000000..573b1e7fbae52 --- /dev/null +++ b/packages/apps/src/server/marketplace/license/index.ts @@ -0,0 +1,4 @@ +import { AppLicenseValidationResult } from './AppLicenseValidationResult'; +import { Crypto } from './Crypto'; + +export { AppLicenseValidationResult, Crypto }; diff --git a/packages/apps/src/server/messages/Message.ts b/packages/apps/src/server/messages/Message.ts new file mode 100644 index 0000000000000..af429593f3002 --- /dev/null +++ b/packages/apps/src/server/messages/Message.ts @@ -0,0 +1,109 @@ +import type { LayoutBlock } from '@rocket.chat/ui-kit'; + +import type { IMessage, IMessageAttachment, IMessageFile, IMessageReactions } from '@rocket.chat/apps-engine/definition/messages'; +import type { IUser, IUserLookup } from '@rocket.chat/apps-engine/definition/users'; +import type { AppManager } from '../AppManager'; +import { Room } from '../rooms/Room'; + +export class Message implements IMessage { + public id?: string; + + public sender: IUser; + + public text?: string; + + public createdAt?: Date; + + public updatedAt?: Date; + + public editor?: IUser; + + public editedAt?: Date; + + public emoji?: string; + + public avatarUrl?: string; + + public alias?: string; + + public attachments?: Array; + + public reactions?: IMessageReactions; + + public groupable?: boolean; + + public parseUrls?: boolean; + + public customFields?: { [key: string]: any }; + + public threadId?: string; + + public file?: IMessageFile; + + public blocks?: Array; + + public starred?: Array<{ _id: string }>; + + public pinned?: boolean; + + public pinnedAt?: Date; + + public pinnedBy?: IUserLookup; + + private _ROOM: Room; + + public get room(): Room { + return this._ROOM; + } + + public set room(room) { + this._ROOM = new Room(room, this.manager); + } + + public constructor( + message: IMessage, + private manager: AppManager, + ) { + Object.assign(this, message); + } + + get value(): object { + return { + id: this.id, + sender: this.sender, + text: this.text, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + editor: this.editor, + editedAt: this.editedAt, + emoji: this.emoji, + avatarUrl: this.avatarUrl, + alias: this.alias, + attachments: this.attachments, + reactions: this.reactions, + groupable: this.groupable, + parseUrls: this.parseUrls, + customFields: this.customFields, + threadId: this.threadId, + room: this.room, + file: this.file, + blocks: this.blocks, + starred: this.starred, + pinned: this.pinned, + pinnedAt: this.pinnedAt, + pinnedBy: this.pinnedBy, + }; + } + + public toJSON() { + return this.value; + } + + public toString() { + return this.value; + } + + public valueOf() { + return this.value; + } +} diff --git a/packages/apps/src/server/misc/UIHelper.ts b/packages/apps/src/server/misc/UIHelper.ts new file mode 100644 index 0000000000000..70989cd6665fe --- /dev/null +++ b/packages/apps/src/server/misc/UIHelper.ts @@ -0,0 +1,32 @@ +import type { LayoutBlock } from '@rocket.chat/ui-kit'; +import { v4 as uuid } from 'uuid'; + +import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit'; + +export class UIHelper { + /** + * Assign blockId, appId and actionId to every block/element inside the array + * @param blocks the blocks that will be iterated and assigned the ids + * @param appId the appId that will be assigned to + * @returns the array of block with the ids properties assigned + */ + public static assignIds(blocks: Array, appId: string): Array { + blocks.forEach((block: (IBlock | LayoutBlock) & { appId?: string; blockId?: string; elements?: Array }) => { + if (!block.appId) { + block.appId = appId; + } + if (!block.blockId) { + block.blockId = uuid(); + } + if (block.elements) { + block.elements.forEach((element) => { + if (!element.actionId) { + element.actionId = uuid(); + } + }); + } + }); + + return blocks; + } +} diff --git a/packages/apps/src/server/misc/Utilities.ts b/packages/apps/src/server/misc/Utilities.ts new file mode 100644 index 0000000000000..f126d379752e8 --- /dev/null +++ b/packages/apps/src/server/misc/Utilities.ts @@ -0,0 +1,36 @@ +import cloneDeep = require('lodash.clonedeep'); + +export class Utilities { + public static deepClone(item: T): T { + return cloneDeep(item); + } + + public static deepFreeze(item: any): T { + Object.freeze(item); + + Object.getOwnPropertyNames(item).forEach((prop: string) => { + if ( + item.hasOwnProperty(prop) && + item[prop] !== null && + (typeof item[prop] === 'object' || typeof item[prop] === 'function') && + !Object.isFrozen(item[prop]) + ) { + Utilities.deepFreeze(item[prop]); + } + }); + + return item; + } + + public static deepCloneAndFreeze(item: T): T { + return Utilities.deepFreeze(Utilities.deepClone(item)); + } + + public static omit(object: { [key: string]: any }, keys: Array) { + const cloned = this.deepClone(object); + for (const key of keys) { + delete cloned[key]; + } + return cloned; + } +} diff --git a/packages/apps/src/server/oauth2/OAuth2Client.ts b/packages/apps/src/server/oauth2/OAuth2Client.ts new file mode 100644 index 0000000000000..7a6ee989ef8b6 --- /dev/null +++ b/packages/apps/src/server/oauth2/OAuth2Client.ts @@ -0,0 +1,337 @@ +import { URL } from 'url'; + +import type { App } from '@rocket.chat/apps-engine/definition/App'; +import type { IConfigurationExtend, IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors'; +import { HttpStatusCode } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IApiEndpointInfo, IApiRequest, IApiResponse } from '@rocket.chat/apps-engine/definition/api'; +import { ApiSecurity, ApiVisibility } from '@rocket.chat/apps-engine/definition/api'; +import { RocketChatAssociationModel, RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata'; +import type { IAuthData, IOAuth2Client, IOAuth2ClientOptions } from '@rocket.chat/apps-engine/definition/oauth2/IOAuth2'; +import { SettingType } from '@rocket.chat/apps-engine/definition/settings'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; + +export enum GrantType { + RefreshToken = 'refresh_token', + AuthorizationCode = 'authorization_code', +} + +export class OAuth2Client implements IOAuth2Client { + private defaultContents = { + success: `
\ +

\ + Authorization went successfully
\ + You can close this tab now
\ +

\ +
`, + failed: `
\ +

\ + Oops, something went wrong, please try again or in case it still does not work, contact the administrator.\ +

\ +
`, + }; + + constructor( + private readonly app: App, + private readonly config: IOAuth2ClientOptions, + ) {} + + public async setup(configuration: IConfigurationExtend): Promise { + configuration.api.provideApi({ + security: ApiSecurity.UNSECURE, + visibility: ApiVisibility.PUBLIC, + endpoints: [ + { + path: `${this.config.alias}-callback`, + get: this.handleOAuthCallback.bind(this), + }, + ], + }); + + await Promise.all([ + configuration.settings.provideSetting({ + id: `${this.config.alias}-oauth-client-id`, + type: SettingType.STRING, + public: true, + required: true, + packageValue: '', + i18nLabel: `${this.config.alias}-oauth-client-id`, + }), + + configuration.settings.provideSetting({ + id: `${this.config.alias}-oauth-clientsecret`, + type: SettingType.STRING, + public: true, + required: true, + packageValue: '', + i18nLabel: `${this.config.alias}-oauth-client-secret`, + }), + ]); + } + + public async getUserAuthorizationUrl(user: IUser, scopes?: Array): Promise { + const redirectUri = this.app.getAccessors().providedApiEndpoints[0].computedPath.substring(1); + + const siteUrl = await this.getBaseURLWithoutTrailingSlash(); + + const finalScopes = ([] as Array).concat(this.config.defaultScopes || [], scopes || []); + + const { authUri } = this.config; + + const clientId = await this.app + .getAccessors() + .reader.getEnvironmentReader() + .getSettings() + .getValueById(`${this.config.alias}-oauth-client-id`); + + const url = new URL(authUri, siteUrl); + + url.searchParams.set('response_type', 'code'); + url.searchParams.set('redirect_uri', `${siteUrl}/${redirectUri}`); + url.searchParams.set('state', user.id); + url.searchParams.set('client_id', clientId); + url.searchParams.set('access_type', 'offline'); + + if (finalScopes.length > 0) { + url.searchParams.set('scope', finalScopes.join(' ')); + } + + return url; + } + + public async getAccessTokenForUser(user: IUser): Promise { + const associations = [ + new RocketChatAssociationRecord(RocketChatAssociationModel.USER, user.id), + new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, `${this.config.alias}-oauth-connection`), + ]; + + const [result] = (await this.app.getAccessors().reader.getPersistenceReader().readByAssociations(associations)) as unknown as Array< + IAuthData | undefined + >; + + return result; + } + + public async refreshUserAccessToken(user: IUser, persis: IPersistence): Promise { + try { + const tokenInfo = await this.getAccessTokenForUser(user); + + if (!tokenInfo) { + throw new Error('User has no access token information'); + } + + if (!tokenInfo.refreshToken) { + throw new Error('User token information has no refresh token available'); + } + + const { + config: { refreshTokenUri }, + } = this; + + const clientId = await this.app + .getAccessors() + .reader.getEnvironmentReader() + .getSettings() + .getValueById(`${this.config.alias}-oauth-client-id`); + + const clientSecret = await this.app + .getAccessors() + .reader.getEnvironmentReader() + .getSettings() + .getValueById(`${this.config.alias}-oauth-clientsecret`); + + const siteUrl = await this.getBaseURLWithoutTrailingSlash(); + + const redirectUri = this.app.getAccessors().providedApiEndpoints[0].computedPath.substring(1); + + const url = new URL(refreshTokenUri); + + url.searchParams.set('client_id', clientId); + url.searchParams.set('client_secret', clientSecret); + url.searchParams.set('redirect_uri', `${siteUrl}/${redirectUri}`); + url.searchParams.set('refresh_token', tokenInfo.refreshToken); + url.searchParams.set('grant_type', GrantType.RefreshToken); + + const { content, statusCode } = await this.app.getAccessors().http.post(url.href); + + if (statusCode !== 200) { + throw new Error('Request to provider was unsuccessful. Check logs for more information'); + } + + const { access_token, expires_in, refresh_token, scope } = JSON.parse(content as string); + + if (!access_token) { + throw new Error('No access token returned by the provider'); + } + + const authData: IAuthData = { + scope, + token: access_token, + expiresAt: expires_in, + refreshToken: refresh_token || tokenInfo.refreshToken, + }; + + await this.saveToken(authData, user.id, persis); + + return authData; + } catch (error) { + this.app.getLogger().error(error); + throw error; + } + } + + public async revokeUserAccessToken(user: IUser, persis: IPersistence): Promise { + try { + const tokenInfo = await this.getAccessTokenForUser(user); + + if (!tokenInfo?.token) { + throw new Error('No access token available for this user.'); + } + + const url = new URL(this.config.revokeTokenUri); + + url.searchParams.set('token', tokenInfo?.token); + + const result = await this.app.getAccessors().http.post(url.href); + + if (result.statusCode !== 200) { + throw new Error('Provider did not allow token to be revoked'); + } + + await this.removeToken({ userId: user.id, persis }); + + return true; + } catch (error) { + this.app.getLogger().error(error); + return false; + } + } + + private async getBaseURLWithoutTrailingSlash(): Promise { + const SITE_URL = 'Site_Url'; + const url = await this.app.getAccessors().environmentReader.getServerSettings().getValueById(SITE_URL); + + if (url.endsWith('/')) { + return url.substr(0, url.length - 1); + } + return url; + } + + private async handleOAuthCallback( + request: IApiRequest, + endpoint: IApiEndpointInfo, + read: IRead, + modify: IModify, + http: IHttp, + persis: IPersistence, + ): Promise { + try { + const { + query: { code, state }, + } = request; + + const user = await this.app.getAccessors().reader.getUserReader().getById(state); + + if (!user) { + throw new Error('User could not be determined.'); + } + + // User chose not to authorize the access + if (!code) { + const failedResult = await this.config.authorizationCallback?.(undefined, user, read, modify, http, persis); + + return { + status: HttpStatusCode.UNAUTHORIZED, + content: failedResult?.responseContent || this.defaultContents.failed, + }; + } + + const siteUrl = await this.getBaseURLWithoutTrailingSlash(); + + const accessTokenUrl = this.config.accessTokenUri; + + const redirectUri = this.app.getAccessors().providedApiEndpoints[0].computedPath.substring(1); + + const clientId = await this.app + .getAccessors() + .reader.getEnvironmentReader() + .getSettings() + .getValueById(`${this.config.alias}-oauth-client-id`); + + const clientSecret = await this.app + .getAccessors() + .reader.getEnvironmentReader() + .getSettings() + .getValueById(`${this.config.alias}-oauth-clientsecret`); + + const url = new URL(accessTokenUrl, siteUrl); + + url.searchParams.set('client_id', clientId); + url.searchParams.set('redirect_uri', `${siteUrl}/${redirectUri}`); + url.searchParams.set('code', code); + url.searchParams.set('client_secret', clientSecret); + url.searchParams.set('access_type', 'offline'); + url.searchParams.set('grant_type', GrantType.AuthorizationCode); + + const { content, statusCode } = await http.post(url.href, { + headers: { Accept: 'application/json' }, + }); + + // If provider had a server error, nothing we can do + if (statusCode >= 500) { + throw new Error('Request for access token failed. Check logs for more information'); + } + + const response = JSON.parse(content as string); + const { access_token, expires_in, refresh_token, scope } = response; + + const authData: IAuthData = { + scope, + token: access_token, + expiresAt: expires_in, + refreshToken: refresh_token, + }; + + const result = await this.config.authorizationCallback?.(authData, user, read, modify, http, persis); + + await this.saveToken(authData, user.id, persis); + + return { + status: statusCode, + content: result?.responseContent || this.defaultContents.success, + }; + } catch (error) { + this.app.getLogger().error(error); + return { + status: HttpStatusCode.INTERNAL_SERVER_ERROR, + content: this.defaultContents.failed, + }; + } + } + + private async saveToken(authData: IAuthData, userId: string, persis: IPersistence): Promise { + const { scope, token, expiresAt, refreshToken } = authData; + + return persis.updateByAssociations( + [ + new RocketChatAssociationRecord(RocketChatAssociationModel.USER, userId), + new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, `${this.config.alias}-oauth-connection`), + ], + { + scope, + token, + expiresAt: expiresAt || '', + refreshToken: refreshToken || '', + }, + true, // we want to create the record if it doesn't exist + ); + } + + private async removeToken({ userId, persis }: { userId: string; persis: IPersistence }): Promise { + const [result] = (await persis.removeByAssociations([ + new RocketChatAssociationRecord(RocketChatAssociationModel.USER, userId), + new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, `${this.config.alias}-oauth-connection`), + ])) as unknown as Array; + + return result; + } +} diff --git a/packages/apps/src/server/permissions/AppPermissions.ts b/packages/apps/src/server/permissions/AppPermissions.ts new file mode 100644 index 0000000000000..a2ced62c63660 --- /dev/null +++ b/packages/apps/src/server/permissions/AppPermissions.ts @@ -0,0 +1,168 @@ +import type { + INetworkingPermission, + IPermission, + IReadSettingPermission, + IWorkspaceTokenPermission, +} from '@rocket.chat/apps-engine/definition/permissions/IPermission'; + +/** + * @description + * + * App Permission naming rules: + * + * 'scope-name': { + * 'permission-name': { name: 'scope-name.permission-name' } + * } + * + * You can retrive this permission by using: + * AppPermissions['scope-name']['permission-name'] -> { name: 'scope-name.permission-name' } + * + * @example + * + * AppPermissions.upload.read // { name: 'upload.read', domains: [] } + */ +export const AppPermissions = { + 'user': { + read: { name: 'user.read' }, + write: { name: 'user.write' }, + }, + 'upload': { + read: { name: 'upload.read' }, + write: { name: 'upload.write' }, + }, + 'email': { + send: { name: 'email.send' }, + }, + 'ui': { + interaction: { name: 'ui.interact' }, + registerButtons: { name: 'ui.registerButtons' }, + }, + 'setting': { + read: { name: 'server-setting.read', hiddenSettings: [] } as IReadSettingPermission, + write: { name: 'server-setting.write' }, + }, + 'room': { + read: { name: 'room.read' }, + write: { name: 'room.write' }, + 'system-view-all': { name: 'room.system.view-all' }, + }, + 'role': { + read: { name: 'role.read' }, + write: { name: 'role.write' }, + }, + 'message': { + read: { name: 'message.read' }, + write: { name: 'message.write' }, + }, + 'moderation': { + read: { name: 'moderation.read' }, + write: { name: 'moderation.write' }, + }, + 'contact': { + read: { name: 'contact.read' }, + write: { name: 'contact.write' }, + }, + 'threads': { + read: { name: 'threads.read' }, + }, + 'livechat-status': { + read: { name: 'livechat-status.read' }, + }, + 'livechat-custom-fields': { + write: { name: 'livechat-custom-fields.write' }, + }, + 'livechat-visitor': { + read: { name: 'livechat-visitor.read' }, + write: { name: 'livechat-visitor.write' }, + }, + 'livechat-message': { + read: { name: 'livechat-message.read' }, + write: { name: 'livechat-message.write' }, + multiple: { name: 'livechat-message.multiple' }, + }, + 'livechat-room': { + read: { name: 'livechat-room.read' }, + write: { name: 'livechat-room.write' }, + }, + 'livechat-department': { + read: { name: 'livechat-department.read' }, + write: { name: 'livechat-department.write' }, + multiple: { name: 'livechat-department.multiple' }, + }, + 'env': { + read: { name: 'env.read' }, + }, + 'cloud': { + 'workspace-token': { name: 'cloud.workspace-token', scopes: [] } as IWorkspaceTokenPermission, + }, + // Internal permissions + 'scheduler': { + default: { name: 'scheduler' }, + }, + 'networking': { + default: { name: 'networking', domains: [] } as INetworkingPermission, + }, + 'persistence': { + default: { name: 'persistence' }, + }, + 'command': { + default: { name: 'slashcommand' }, + }, + 'videoConference': { + read: { name: 'video-conference.read' }, + write: { name: 'video-conference.write' }, + provider: { name: 'video-conference-provider' }, + }, + 'apis': { + default: { name: 'api' }, + }, + 'oauth-app': { + read: { name: 'oauth-app.read' }, + write: { name: 'oauth-app.write' }, + }, + 'outboundComms': { + provide: { name: 'outbound-communication.provide' }, + }, + 'experimental': { + default: { name: 'experimental.default' }, + }, +}; + +/** + * @description + * Default permissions for apps + * Used to ensure backward compatibility with apps + * that were developed before the permission system was introduced. + */ +export const defaultPermissions: Array = [ + AppPermissions.user.read, + AppPermissions.user.write, + AppPermissions.upload.read, + AppPermissions.upload.write, + AppPermissions.ui.interaction, + AppPermissions.setting.read, + AppPermissions.setting.write, + AppPermissions.room.read, + AppPermissions.room.write, + AppPermissions.message.read, + AppPermissions.message.write, + AppPermissions['livechat-department'].read, + AppPermissions['livechat-department'].write, + AppPermissions['livechat-room'].read, + AppPermissions['livechat-room'].write, + AppPermissions['livechat-message'].read, + AppPermissions['livechat-message'].write, + AppPermissions['livechat-visitor'].read, + AppPermissions['livechat-visitor'].write, + AppPermissions['livechat-status'].read, + AppPermissions['livechat-custom-fields'].write, + AppPermissions.scheduler.default, + AppPermissions.networking.default, + AppPermissions.persistence.default, + AppPermissions.env.read, + AppPermissions.command.default, + AppPermissions.videoConference.provider, + AppPermissions.videoConference.read, + AppPermissions.videoConference.write, + AppPermissions.apis.default, +]; diff --git a/packages/apps/src/server/rooms/Room.ts b/packages/apps/src/server/rooms/Room.ts new file mode 100644 index 0000000000000..c0ea200c470e7 --- /dev/null +++ b/packages/apps/src/server/rooms/Room.ts @@ -0,0 +1,104 @@ +import type { IRoom, RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import type { AppManager } from '../AppManager'; + +const PrivateManager = Symbol('RoomPrivateManager'); + +export class Room implements IRoom { + public id: string; + + public displayName?: string; + + public slugifiedName: string; + + public type: RoomType; + + public creator: IUser; + + public isDefault?: boolean; + + public isReadOnly?: boolean; + + public displaySystemMessages?: boolean; + + public messageCount?: number; + + public createdAt?: Date; + + public updatedAt?: Date; + + public lastModifiedAt?: Date; + + public customFields?: { [key: string]: any }; + + public userIds?: Array; + + private _USERNAMES: Array; + + private [PrivateManager]: AppManager; + + /** + * @deprecated + */ + public get usernames(): Array { + // Get usernames + if (!this._USERNAMES) { + this._USERNAMES = this[PrivateManager].getBridges().getInternalBridge().doGetUsernamesOfRoomByIdSync(this.id); + } + + return this._USERNAMES; + } + + public set usernames(usernames) {} + + 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 { + return { + id: this.id, + displayName: this.displayName, + slugifiedName: this.slugifiedName, + type: this.type, + creator: this.creator, + isDefault: this.isDefault, + isReadOnly: this.isReadOnly, + displaySystemMessages: this.displaySystemMessages, + messageCount: this.messageCount, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + lastModifiedAt: this.lastModifiedAt, + customFields: this.customFields, + userIds: this.userIds, + }; + } + + public async getUsernames(): Promise> { + // Get usernames + if (!this._USERNAMES) { + this._USERNAMES = await this[PrivateManager].getBridges().getInternalBridge().doGetUsernamesOfRoomById(this.id); + } + + return this._USERNAMES; + } + + public toJSON() { + return this.value; + } + + public toString() { + return this.value; + } + + public valueOf() { + return this.value; + } +} diff --git a/packages/apps/src/server/runtime/AppsEngineEmptyRuntime.ts b/packages/apps/src/server/runtime/AppsEngineEmptyRuntime.ts new file mode 100644 index 0000000000000..3b0a6f90bbefb --- /dev/null +++ b/packages/apps/src/server/runtime/AppsEngineEmptyRuntime.ts @@ -0,0 +1,21 @@ +import type { IAppsEngineRuntimeOptions } from './AppsEngineRuntime'; +import { AppsEngineRuntime } from './AppsEngineRuntime'; +import type { App } from '@rocket.chat/apps-engine/definition/App'; + +export class AppsEngineEmptyRuntime extends AppsEngineRuntime { + public static async runCode(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { + throw new Error('Empty runtime does not support code execution'); + } + + public static runCodeSync(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): any { + throw new Error('Empty runtime does not support code execution'); + } + + constructor(readonly app: App) { + super(app, () => {}); + } + + public async runInSandbox(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { + return Promise.reject(new Error('Empty runtime does not support execution')); + } +} diff --git a/packages/apps/src/server/runtime/AppsEngineNodeRuntime.ts b/packages/apps/src/server/runtime/AppsEngineNodeRuntime.ts new file mode 100644 index 0000000000000..a9cf20b1d8fa4 --- /dev/null +++ b/packages/apps/src/server/runtime/AppsEngineNodeRuntime.ts @@ -0,0 +1,74 @@ +import * as timers from 'timers'; +import * as vm from 'vm'; + +import type { IAppsEngineRuntimeOptions } from './AppsEngineRuntime'; +import { APPS_ENGINE_RUNTIME_DEFAULT_TIMEOUT, AppsEngineRuntime, getFilenameForApp } from './AppsEngineRuntime'; +import type { App } from '@rocket.chat/apps-engine/definition/App'; + +export class AppsEngineNodeRuntime extends AppsEngineRuntime { + public static defaultRuntimeOptions = { + timeout: APPS_ENGINE_RUNTIME_DEFAULT_TIMEOUT, + }; + + public static defaultContext = { + ...timers, + Buffer, + console, + process: {}, + exports: {}, + }; + + public static async runCode(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { + return new Promise((resolve, reject) => { + process.nextTick(() => { + try { + resolve(this.runCodeSync(code, sandbox, options)); + } catch (e) { + reject(e); + } + }); + }); + } + + public static runCodeSync(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): any { + return vm.runInNewContext( + code, + { ...AppsEngineNodeRuntime.defaultContext, ...sandbox }, + { ...AppsEngineNodeRuntime.defaultRuntimeOptions, ...(options || {}) }, + ); + } + + constructor( + private readonly app: App, + private readonly customRequire: (mod: string) => any, + ) { + super(app, customRequire); + } + + public async runInSandbox(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { + return new Promise((resolve, reject) => { + process.nextTick(async () => { + try { + sandbox ??= {}; + + const result = await vm.runInNewContext( + code, + { + ...AppsEngineNodeRuntime.defaultContext, + ...sandbox, + require: this.customRequire, + }, + { + ...AppsEngineNodeRuntime.defaultRuntimeOptions, + filename: getFilenameForApp(options?.filename || this.app.getName()), + }, + ); + + resolve(result); + } catch (e) { + reject(e); + } + }); + }); + } +} diff --git a/packages/apps/src/server/runtime/AppsEngineRuntime.ts b/packages/apps/src/server/runtime/AppsEngineRuntime.ts new file mode 100644 index 0000000000000..69f32092a7401 --- /dev/null +++ b/packages/apps/src/server/runtime/AppsEngineRuntime.ts @@ -0,0 +1,29 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App'; + +export const APPS_ENGINE_RUNTIME_DEFAULT_TIMEOUT = 1000; + +export const APPS_ENGINE_RUNTIME_FILE_PREFIX = '$RocketChat_App$'; + +export function getFilenameForApp(filename: string): string { + return `${APPS_ENGINE_RUNTIME_FILE_PREFIX}_${filename}`; +} + +export abstract class AppsEngineRuntime { + public static async runCode(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { + throw new Error(`Can't call this method on abstract class. Override it in a proper runtime class.`); + } + + public static runCodeSync(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): any { + throw new Error(`Can't call this method on abstract class. Override it in a proper runtime class.`); + } + + constructor(app: App, customRequire: (module: string) => any) {} + + public abstract runInSandbox(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise; +} + +export interface IAppsEngineRuntimeOptions { + timeout?: number; + filename?: string; + returnAllExports?: boolean; +} diff --git a/packages/apps/src/server/runtime/EmptyRuntime.ts b/packages/apps/src/server/runtime/EmptyRuntime.ts new file mode 100644 index 0000000000000..f8de8482ac4b4 --- /dev/null +++ b/packages/apps/src/server/runtime/EmptyRuntime.ts @@ -0,0 +1,50 @@ +import { EventEmitter } from 'events'; + +import type { IRuntimeController, RuntimeRequestOptions } from './IRuntimeController'; +import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; + +export class EmptyRuntime extends EventEmitter implements IRuntimeController { + private readonly appId: string; + + constructor(appId: string) { + super(); + this.appId = appId; + } + + /** + * Returns a disabled status since this is an empty runtime + */ + public async getStatus(): Promise { + return Promise.resolve(AppStatus.COMPILER_ERROR_DISABLED); + } + + /** + * Stub implementation that throws an error since this runtime cannot handle requests + */ + public async sendRequest(message: { method: string; params: any[] }, options?: RuntimeRequestOptions): Promise { + throw new Error(`EmptyRuntime cannot handle requests. Method: ${message.method}`); + } + + /** + * Stub implementation for setting up the runtime + */ + public async setupApp(): Promise { + // Nothing to setup in an empty runtime + return Promise.resolve(); + } + + /** + * Stub implementation for stopping the runtime + */ + public async stopApp(): Promise { + // Nothing to stop in an empty runtime + return Promise.resolve(); + } + + /** + * Get the app ID associated with this runtime + */ + public getAppId(): string { + return this.appId; + } +} diff --git a/packages/apps/src/server/runtime/IRuntimeController.ts b/packages/apps/src/server/runtime/IRuntimeController.ts new file mode 100644 index 0000000000000..69791e25e0ab8 --- /dev/null +++ b/packages/apps/src/server/runtime/IRuntimeController.ts @@ -0,0 +1,34 @@ +import type { EventEmitter } from 'events'; + +import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; + +export type RuntimeRequestOptions = { + timeout: number; +}; + +export interface IRuntimeController extends EventEmitter { + /** + * Get the current status of the app runtime + */ + getStatus(): Promise; + + /** + * Send a request to the app runtime + */ + sendRequest(message: { method: string; params: any[] }, options?: RuntimeRequestOptions): Promise; + + /** + * Setup the app runtime + */ + setupApp(): Promise; + + /** + * Stop the app runtime + */ + stopApp(): Promise; + + /** + * Get the app ID associated with this runtime + */ + getAppId(): string; +} diff --git a/packages/apps/src/server/runtime/deno/AppsEngineDenoRuntime.ts b/packages/apps/src/server/runtime/deno/AppsEngineDenoRuntime.ts new file mode 100644 index 0000000000000..efffe811cf3e6 --- /dev/null +++ b/packages/apps/src/server/runtime/deno/AppsEngineDenoRuntime.ts @@ -0,0 +1,739 @@ +import * as child_process from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import { type Readable, EventEmitter } from 'stream'; +import { inspect as utilInspect } from 'util'; + +import debugFactory from 'debug'; +import * as jsonrpc from 'jsonrpc-lite'; + +import { LivenessManager } from './LivenessManager'; +import { ProcessMessenger } from './ProcessMessenger'; +import { bundleLegacyApp } from './bundler'; +import { newDecoder } from './codec'; +import { AppStatus, AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; +import { AppMethod } from '@rocket.chat/apps-engine/definition/metadata'; +import type { AppManager } from '../../AppManager'; +import type { AppBridges } from '../../bridges'; +import type { IParseAppPackageResult } from '../../compiler'; +import { AppConsole, type ILoggerStorageEntry } from '../../logging'; +import type { AppAccessorManager, AppApiManager } from '../../managers'; +import type { AppLogStorage, IAppStorageItem } from '../../storage'; +import type { IRuntimeController } from '../IRuntimeController'; + +const baseDebug = debugFactory('appsEngine:runtime:deno'); + +const inspect = (value: unknown) => utilInspect(value, { depth: 10, compact: true, breakLength: Infinity }); + +export const ALLOWED_ACCESSOR_METHODS = [ + 'getConfigurationExtend', + 'getEnvironmentRead', + 'getEnvironmentWrite', + 'getConfigurationModify', + 'getReader', + 'getPersistence', + 'getHttp', + 'getModifier', +] as Array< + keyof Pick< + AppAccessorManager, + | 'getConfigurationExtend' + | 'getEnvironmentRead' + | 'getEnvironmentWrite' + | 'getConfigurationModify' + | 'getReader' + | 'getPersistence' + | 'getHttp' + | 'getModifier' + > +>; + +// Trying to access environment variables in Deno throws an error where in vm2 it simply returned `undefined` +// So here we define the allowed envvars to prevent the process (and the compatibility) from breaking +export const ALLOWED_ENVIRONMENT_VARIABLES = [ + 'NODE_EXTRA_CA_CERTS', // Accessed by the `https` node module +]; + +const COMMAND_PONG = '_zPONG'; + +export const JSONRPC_METHOD_NOT_FOUND = -32601; + +export function getRuntimeTimeout() { + const defaultTimeout = 30000; + const envValue = isFinite(process.env.APPS_ENGINE_RUNTIME_TIMEOUT as any) + ? Number(process.env.APPS_ENGINE_RUNTIME_TIMEOUT) + : defaultTimeout; + + if (envValue < 0) { + console.log('Environment variable APPS_ENGINE_RUNTIME_TIMEOUT has a negative value, ignoring...'); + return defaultTimeout; + } + + return envValue; +} + +export function isValidOrigin(accessor: string): accessor is (typeof ALLOWED_ACCESSOR_METHODS)[number] { + return ALLOWED_ACCESSOR_METHODS.includes(accessor as any); +} + +export function getDenoConfigPath(): string { + try { + // This path is relative to the compiled version of the Apps-Engine source + return require.resolve('../../../deno-runtime/deno.jsonc'); + } catch { + // This path is relative to the original Apps-Engine files - used during tests + return require.resolve('../../../../deno-runtime/deno.jsonc'); + } +} + +type AbortFunction = (reason?: any) => void; + +export class DenoRuntimeSubprocessController extends EventEmitter implements IRuntimeController { + private deno: child_process.ChildProcess | undefined; + + private state: 'uninitialized' | 'ready' | 'invalid' | 'restarting' | 'unknown' | 'stopped'; + + /** + * Incremental id that keeps track of how many times we've spawned a process for this app + */ + private spawnId = 0; + + private readonly debug: debug.Debugger; + + private readonly options = { + timeout: getRuntimeTimeout(), + }; + + private readonly accessors: AppAccessorManager; + + private readonly api: AppApiManager; + + private readonly logStorage: AppLogStorage; + + private readonly bridges: AppBridges; + + private readonly messenger: ProcessMessenger; + + private readonly livenessManager: LivenessManager; + + private readonly tempFilePath: string; + + private readonly denoRuntimePath: string; + + private readonly denoConfigPath: string; + + constructor( + manager: AppManager, + // We need to keep the appSource around in case the Deno process needs to be restarted + private readonly appPackage: IParseAppPackageResult, + private readonly storageItem: IAppStorageItem, + ) { + super(); + + this.tempFilePath = manager.getTempFilePath(); + this.denoRuntimePath = path.join(this.tempFilePath, 'deno-runtime', 'main.ts'); + this.denoConfigPath = getDenoConfigPath(); + + /** + * Deno 2.x refuses to run scripts inside the node_modules, so we create a symlink to the deno runtime files in the temp directory + * The temp directory is the same we are given by the host to store temporary upload files + */ + try { + fs.symlinkSync( + path.dirname(this.denoConfigPath), + path.dirname(this.denoRuntimePath), + 'dir' + ); + } catch (reason: unknown) { + if ((reason as NodeJS.ErrnoException).code !== 'EEXIST') { + throw reason; + } + } + + this.debug = baseDebug.extend(appPackage.info.id); + this.messenger = new ProcessMessenger(); + this.livenessManager = new LivenessManager({ + controller: this, + messenger: this.messenger, + debug: this.debug, + }); + + this.state = 'uninitialized'; + + this.accessors = manager.getAccessorManager(); + this.api = manager.getApiManager(); + this.logStorage = manager.getLogStorage(); + this.bridges = manager.getBridges(); + } + + public spawnProcess(): void { + try { + const denoExePath = 'deno'; + + const denoWrapperPath = this.denoRuntimePath; + // During development, the appsEngineDir is enough to run the deno process + const appsEngineDir = path.dirname(path.join(this.denoConfigPath, '..')); + const DENO_DIR = process.env.DENO_DIR ?? path.join(appsEngineDir, '.deno-cache'); + // When running in production, we're likely inside a node_modules which the Deno + // process must be able to read in order to include files that use NPM packages + const parentNodeModulesDir = path.dirname(path.join(appsEngineDir, '..')); + + const allowedDirs = [appsEngineDir, parentNodeModulesDir, this.tempFilePath]; + + const options = [ + 'run', + '--cached-only', + `--config=${this.denoConfigPath}`, + `--allow-read=${allowedDirs.join(',')}`, + `--allow-env=${ALLOWED_ENVIRONMENT_VARIABLES.join(',')}`, + denoWrapperPath, + '--subprocess', + this.appPackage.info.id, + '--spawnId', + String(this.spawnId++), + ]; + + // If the app doesn't request any permissions, it gets the default set of permissions, which includes "networking" + // If the app requests specific permissions, we need to check whether it requests "networking" or not + if (!this.appPackage.info.permissions || this.appPackage.info.permissions.findIndex((p) => p.name === 'networking') !== -1) { + options.splice(1, 0, '--allow-net'); + } + + const environment = { + env: { + // We need to pass the PATH, otherwise the shell won't find the deno executable + // But the runtime itself won't have access to the env var because of the parameters + PATH: process.env.PATH, + DENO_DIR, + }, + }; + + // SECURITY: We control the command, the arguments and the script that will be executed. + this.deno = child_process.spawn(denoExePath, options, environment); + this.messenger.setReceiver(this.deno); + this.livenessManager.attach(this.deno); + + this.debug('Started subprocess %d with options %s and env %s', this.deno.pid, inspect(options), inspect(environment)); + + this.setupListeners(); + } catch (e) { + this.state = 'invalid'; + console.error(`Failed to start Deno subprocess for app ${this.getAppId()}`, e); + } + } + + /** + * Attempts to kill the process currently controlled by this.deno + * + * @returns boolean - if a process has been killed or not + */ + public async killProcess(): Promise { + if (!this.deno) { + this.debug('No child process reference'); + return false; + } + + let { killed } = this.deno; + + // This field is not populated if the process is killed by the OS + if (killed) { + this.debug('App process was already killed'); + return killed; + } + + // What else should we do? + if (this.deno.kill('SIGKILL')) { + // Let's wait until we get confirmation the process exited + await new Promise((r) => this.deno.on('exit', r)); + killed = true; + } else { + this.debug('Tried killing the process but failed. Was it already dead?'); + killed = false; + } + + delete this.deno; + this.messenger.clearReceiver(); + return killed; + } + + // Debug purposes, could be deleted later + emit(eventName: string | symbol, ...args: any[]): boolean { + const hadListeners = super.emit(eventName, ...args); + + if (!hadListeners) { + this.debug('Emitted but no one listened: ', eventName, args); + } + + return hadListeners; + } + + public getProcessState() { + return this.state; + } + + public async getStatus(): Promise { + // If the process has been terminated, we can't get the status + if (!this.deno || this.deno.exitCode !== null) { + return AppStatus.UNKNOWN; + } + + return this.sendRequest({ method: 'app:getStatus', params: [] }) as Promise; + } + + public async setupApp() { + this.debug('Setting up app subprocess'); + this.spawnProcess(); + + // If there is more than one file in the package, then it is a legacy app that has not been bundled + if (Object.keys(this.appPackage.files).length > 1) { + await bundleLegacyApp(this.appPackage); + } + + await this.waitUntilReady(); + + await this.sendRequest({ method: 'app:construct', params: [this.appPackage] }); + + this.emit('constructed'); + } + + public async stopApp() { + this.debug('Stopping app subprocess'); + + this.state = 'stopped'; + + await this.killProcess(); + } + + public async restartApp() { + this.debug('Restarting app subprocess'); + const logger = new AppConsole('runtime:restart'); + + logger.info({ msg: 'Starting restart procedure for app subprocess...', runtimeData: this.livenessManager.getRuntimeData() }); + + this.state = 'restarting'; + + try { + const pid = this.deno?.pid; + + const hasKilled = await this.killProcess(); + + if (hasKilled) { + logger.debug({ msg: 'Process successfully terminated', pid }); + } else { + logger.warn({ msg: 'Could not terminate process. Maybe it was already dead?', pid }); + } + + await this.setupApp(); + logger.info({ msg: 'New subprocess successfully spawned', pid: this.deno.pid }); + + // setupApp() changes the state to 'ready' - we'll need to workaround that for now + this.state = 'restarting'; + + await this.sendRequest({ method: 'app:initialize' }); + await this.sendRequest({ method: 'app:setStatus', params: [this.storageItem.status] }); + + if (AppStatusUtils.isEnabled(this.storageItem.status)) { + await this.sendRequest({ method: 'app:onEnable' }); + } + + this.state = 'ready'; + + logger.info('Successfully restarted app subprocess'); + } catch (e) { + logger.error({ msg: "Failed to restart app's subprocess", err: e }); + throw e; + } finally { + await this.logStorage.storeEntries(AppConsole.toStorageEntry(this.getAppId(), logger)); + } + } + + public getAppId(): string { + return this.appPackage.info.id; + } + + public async sendRequest(message: Pick, options = this.options): Promise { + const id = String(Math.random().toString(36)).substring(2); + + const start = Date.now(); + + const request = jsonrpc.request(id, message.method, message.params); + + const { promise, abort } = this.waitForResponse(request, options); + + try { + this.debug('Sending message to subprocess %s', inspect(message)); + this.messenger.send(request); + } catch (e) { + abort(e); + } + + return promise.finally(() => { + this.debug('Request %s for method %s took %dms', id, message.method, Date.now() - start); + }); + } + + private waitUntilReady(): Promise { + if (this.state === 'ready') { + return; + } + + return new Promise((resolve, reject) => { + let timeoutId: NodeJS.Timeout; + + const handler = () => { + clearTimeout(timeoutId); + resolve(); + }; + + timeoutId = setTimeout(() => { + this.off('ready', handler); + reject(new Error(`[${this.getAppId()}] Timeout: app process not ready`)); + }, this.options.timeout); + + this.once('ready', handler); + }); + } + + private waitForResponse(req: jsonrpc.RequestObject, options = this.options): { abort: AbortFunction; promise: Promise } { + const controller = new AbortController(); + const { abort, signal } = controller; + + return { + abort: abort.bind(controller), + promise: new Promise((resolve, reject) => { + const eventName = `result:${req.id}`; + + const responseCallback = (result: unknown, error: jsonrpc.IParsedObjectError['payload']['error'] | Error) => { + this.off(eventName, responseCallback); + clearTimeout(timeoutId); + + if (error) { + reject(error); + } + + resolve(result); + }; + + const timeoutId = setTimeout( + () => + responseCallback( + undefined, + new Error(`[${this.getAppId()}] Request "${req.id}" for method "${req.method}" timed out after ${options.timeout}ms`), + ), + options.timeout, + ); + + signal.onabort = () => + responseCallback(undefined, signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason))); + + this.once(eventName, responseCallback); + }), + }; + } + + private onReady(): void { + this.state = 'ready'; + } + + /** + * Listeners need to be setup every time the reference + * in `this.deno` changes, i.e. every time the subprocess + * is restarted + */ + private setupListeners(): void { + if (!this.deno) { + return; + } + + this.deno.stderr.on('data', this.parseError.bind(this)); + this.deno.on('error', (err) => { + this.state = 'invalid'; + console.error(`Failed to startup Deno subprocess for app ${this.getAppId()}`, err); + }); + + this.deno.once('exit', (code) => this.emit('processExit', code)); + + this.once('ready', this.onReady.bind(this)); + + this.parseStdout(this.deno.stdout); + } + + // Probable should extract this to a separate file + private async handleAccessorMessage({ payload: { method, id, params } }: jsonrpc.IParsedObjectRequest): Promise { + const accessorMethods = method.substring(9).split(':'); // First 9 characters are always 'accessor:' + + this.debug('Handling accessor message %s with params %s', inspect(accessorMethods), inspect(params)); + + const managerOrigin = accessorMethods.shift(); + const tailMethodName = accessorMethods.pop(); + + // If we're restarting the app, we can't register resources again, so we + // hijack requests for the `ConfigurationExtend` accessor and don't let them through + // This needs to be refactored ASAP + if (this.state === 'restarting' && managerOrigin === 'getConfigurationExtend') { + return jsonrpc.success(id, null); + } + + if (managerOrigin === 'api' && tailMethodName === 'listApis') { + const result = this.api.listApis(this.appPackage.info.id); + + return jsonrpc.success(id, result); + } + + /** + * At this point, the accessorMethods array will contain the path to the accessor from the origin (AppAccessorManager) + * The accessor is the one that contains the actual method the app wants to call + * + * Most of the times, it will take one step from origin to accessor + * For example, for the call AppAccessorManager.getEnvironmentRead().getServerSettings().getValueById() we'll have + * the following: + * + * ``` + * const managerOrigin = 'getEnvironmentRead' + * const tailMethod = 'getValueById' + * const accessorMethods = ['getServerSettings'] + * ``` + * + * But sometimes there can be more steps, like in the following example: + * AppAccessorManager.getReader().getEnvironmentReader().getEnvironmentVariables().getValueByName() + * In this case, we'll have: + * + * ``` + * const managerOrigin = 'getReader' + * const tailMethod = 'getValueByName' + * const accessorMethods = ['getEnvironmentReader', 'getEnvironmentVariables'] + * ``` + **/ + // Prevent app from trying to get properties from the manager that + // are not intended for public access + if (!isValidOrigin(managerOrigin)) { + throw new Error(`Invalid accessor namespace "${managerOrigin}"`); + } + + // Need to fix typing of return value + const getAccessorForOrigin = ( + accessorMethods: string[], + managerOrigin: (typeof ALLOWED_ACCESSOR_METHODS)[number], + accessorManager: AppAccessorManager, + ) => { + const origin = accessorManager[managerOrigin](this.appPackage.info.id); + + if (managerOrigin === 'getHttp' || managerOrigin === 'getPersistence') { + return origin; + } + + if (managerOrigin === 'getConfigurationExtend' || managerOrigin === 'getConfigurationModify') { + return origin[accessorMethods[0] as keyof typeof origin]; + } + + let accessor = origin; + + // Call all intermediary objects to "resolve" the accessor + accessorMethods.forEach((methodName) => { + const method = accessor[methodName as keyof typeof accessor] as unknown; + + if (typeof method !== 'function') { + throw new Error(`Invalid accessor method "${methodName}"`); + } + + accessor = method.apply(accessor); + }); + + return accessor; + }; + + const accessor = getAccessorForOrigin(accessorMethods, managerOrigin, this.accessors); + + const tailMethod = accessor[tailMethodName as keyof typeof accessor] as unknown; + + if (typeof tailMethod !== 'function') { + throw new Error(`Invalid accessor method "${tailMethodName}"`); + } + + const result = await tailMethod.apply(accessor, params); + + return jsonrpc.success(id, typeof result === 'undefined' ? null : result); + } + + private async handleBridgeMessage({ + payload: { method, id, params }, + }: jsonrpc.IParsedObjectRequest): Promise { + const [bridgeName, bridgeMethod] = method.substring(8).split(':'); + + this.debug('Handling bridge message %s().%s() with params %s', bridgeName, bridgeMethod, inspect(params)); + + const bridge = this.bridges[bridgeName as keyof typeof this.bridges]; + + if (!bridgeMethod.startsWith('do') || typeof bridge !== 'function' || !Array.isArray(params)) { + throw new Error('Invalid bridge request'); + } + + const bridgeInstance = bridge.call(this.bridges); + + const methodRef = bridgeInstance[bridgeMethod as keyof typeof bridge] as unknown; + + if (typeof methodRef !== 'function') { + throw new Error('Invalid bridge request'); + } + + let result; + try { + result = await methodRef.apply( + bridgeInstance, + // Should the protocol expect the placeholder APP_ID value or should the Deno process send the actual appId? + // If we do not expect the APP_ID, the Deno process will be able to impersonate other apps, potentially + params.map((value: unknown) => (value === 'APP_ID' ? this.appPackage.info.id : value)), + ); + } catch (error) { + this.debug('Error executing bridge method %s().%s() %s', bridgeName, bridgeMethod, inspect(error.message)); + const jsonRpcError = new jsonrpc.JsonRpcError(error.message, -32000, error); + return jsonrpc.error(id, jsonRpcError); + } + + return jsonrpc.success(id, typeof result === 'undefined' ? null : result); + } + + private async handleIncomingMessage(message: jsonrpc.IParsedObjectNotification | jsonrpc.IParsedObjectRequest): Promise { + const { method } = message.payload; + + if (method.startsWith('accessor:')) { + let result: jsonrpc.SuccessObject | jsonrpc.ErrorObject; + + try { + result = await this.handleAccessorMessage(message as jsonrpc.IParsedObjectRequest); + } catch (e) { + result = jsonrpc.error((message.payload as jsonrpc.RequestObject).id, new jsonrpc.JsonRpcError(e.message, 1000)); + } + + this.messenger.send(result); + + return; + } + + if (method.startsWith('bridges:')) { + let result: jsonrpc.SuccessObject | jsonrpc.ErrorObject; + + try { + result = await this.handleBridgeMessage(message as jsonrpc.IParsedObjectRequest); + } catch (e) { + result = jsonrpc.error((message.payload as jsonrpc.RequestObject).id, new jsonrpc.JsonRpcError(e.message, 1000)); + } + + this.messenger.send(result); + + return; + } + + switch (method) { + case 'ready': + this.emit('ready'); + break; + case 'log': + console.log('SUBPROCESS LOG', message); + break; + case 'unhandledRejection': + case 'uncaughtException': + await this.logUnhandledError(`runtime:${method}`, message); + break; + default: + console.warn('Unrecognized method from sub process'); + break; + } + } + + private async logUnhandledError( + method: `${AppMethod.RUNTIME_UNCAUGHT_EXCEPTION | AppMethod.RUNTIME_UNHANDLED_REJECTION}`, + message: jsonrpc.IParsedObjectRequest | jsonrpc.IParsedObjectNotification, + ) { + this.debug('Unhandled error of type "%s" caught in subprocess', method); + + const logger = new AppConsole(method); + logger.error(message.payload); + + await this.logStorage.storeEntries(AppConsole.toStorageEntry(this.getAppId(), logger)); + } + + private async handleResultMessage(message: jsonrpc.IParsedObjectError | jsonrpc.IParsedObjectSuccess): Promise { + const { id } = message.payload; + + let result: unknown; + let error: jsonrpc.IParsedObjectError['payload']['error'] | undefined; + let logs: ILoggerStorageEntry; + + if (message.type === 'success') { + const params = message.payload.result as { value: unknown; logs?: ILoggerStorageEntry }; + result = params.value; + logs = params.logs; + } else { + error = message.payload.error; + logs = message.payload.error.data?.logs as ILoggerStorageEntry; + } + + // Should we try to make sure all result messages have logs? + if (logs) { + await this.logStorage.storeEntries(logs); + } + + this.emit(`result:${id}`, result, error); + } + + private async parseStdout(stream: Readable): Promise { + try { + for await (const message of newDecoder().decodeStream(stream)) { + this.debug('Received message from subprocess %s', inspect(message)); + try { + // Process PONG resonse first as it is not JSON RPC + if (message === COMMAND_PONG) { + this.emit('pong'); + continue; + } + + const JSONRPCMessage = jsonrpc.parseObject(message); + + if (Array.isArray(JSONRPCMessage)) { + throw new Error('Invalid message format'); + } + + this.emit('heartbeat'); + + if (JSONRPCMessage.type === 'request' || JSONRPCMessage.type === 'notification') { + this.handleIncomingMessage(JSONRPCMessage).catch((reason) => + console.error(`[${this.getAppId()}] Error executing handler`, reason, message), + ); + continue; + } + + if (JSONRPCMessage.type === 'success' || JSONRPCMessage.type === 'error') { + this.handleResultMessage(JSONRPCMessage).catch((reason) => + console.error(`[${this.getAppId()}] Error executing handler`, reason, message), + ); + continue; + } + + console.error('Unrecognized message type', JSONRPCMessage); + } catch (e) { + // SyntaxError is thrown when the message is not a valid JSON + if (e instanceof SyntaxError) { + console.error(`[${this.getAppId()}] Failed to parse message`); + continue; + } + + console.error(`[${this.getAppId()}] Error executing handler`, e, message); + } + } + } catch (e) { + console.error(`[${this.getAppId()}]`, e); + this.emit('error', new Error('DECODE_ERROR')); + } + } + + private async parseError(chunk: Buffer): Promise { + try { + const data = JSON.parse(chunk.toString()); + + this.debug('Metrics received from subprocess (via stderr): %s', inspect(data)); + } catch (e) { + console.error('Subprocess stderr', chunk.toString()); + } + } +} diff --git a/packages/apps/src/server/runtime/deno/LivenessManager.ts b/packages/apps/src/server/runtime/deno/LivenessManager.ts new file mode 100644 index 0000000000000..f7ba66ed37c04 --- /dev/null +++ b/packages/apps/src/server/runtime/deno/LivenessManager.ts @@ -0,0 +1,254 @@ +import type { ChildProcess } from 'child_process'; +import { EventEmitter } from 'stream'; + +import type { DenoRuntimeSubprocessController } from './AppsEngineDenoRuntime'; +import type { ProcessMessenger } from './ProcessMessenger'; + +export const COMMAND_PING = '_zPING'; + +const defaultOptions: LivenessManager['options'] = { + pingTimeoutInMS: 1000, + pingIntervalInMS: 10000, + consecutiveTimeoutLimit: 4, + maxRestarts: Infinity, + restartAttemptDelayInMS: 1000, +}; + +/** + * Responsible for pinging the Deno subprocess and for restarting it + * if something doesn't look right + */ +export class LivenessManager { + private readonly controller: DenoRuntimeSubprocessController; + + private readonly messenger: ProcessMessenger; + + private readonly debug: debug.Debugger; + + private readonly options: { + // How long should we wait for a response to the ping request + pingTimeoutInMS: number; + + // How long is the delay between ping messages + pingIntervalInMS: number; + + // Limit of times the process can timeout the ping response before we consider it as unresponsive + consecutiveTimeoutLimit: number; + + // Limit of times we can try to restart a process + maxRestarts: number; + + // Time to delay the next restart attempt after a failed one + restartAttemptDelayInMS: number; + }; + + private subprocess: ChildProcess; + + private watchdogTimeout: NodeJS.Timeout | null = null; + + private lastHeartbeatTimestamp = NaN; + + // A promise tracking the current ping process - used mostly for testing + private pendingPing: Promise | null; + + // This is the perfect use-case for an AbortController, but it's experimental in Node 14.x + private pingAbortController: EventEmitter; + + private pingTimeoutConsecutiveCount = 0; + + private restartCount = 0; + + private restartLog: Record[] = []; + + constructor( + deps: { + controller: DenoRuntimeSubprocessController; + messenger: ProcessMessenger; + debug: debug.Debugger; + }, + options: Partial = {}, + ) { + this.controller = deps.controller; + this.messenger = deps.messenger; + this.debug = deps.debug; + this.pingAbortController = new EventEmitter(); + + this.options = Object.assign({}, defaultOptions, options); + + this.controller.on('heartbeat', () => { + this.lastHeartbeatTimestamp = Date.now(); + this.pingTimeoutConsecutiveCount = 0; + }); + + this.controller.on('error', async (reason) => { + if (reason instanceof Error && reason.message.startsWith('DECODE_ERROR')) { + await this.restartProcess('Decode error', 'controller'); + } + }); + } + + public getRuntimeData() { + const { lastHeartbeatTimestamp, restartCount, pingTimeoutConsecutiveCount, restartLog } = this; + + return { + lastHeartbeatTimestamp, + restartCount, + pingTimeoutConsecutiveCount, + restartLog, + }; + } + + public attach(deno: ChildProcess) { + this.subprocess = deno; + + this.pingTimeoutConsecutiveCount = 0; + + this.subprocess.once('exit', this.handleExit.bind(this)); + this.subprocess.once('error', this.handleError.bind(this)); + + this.controller.once('constructed', this.start.bind(this)); + } + + public start() { + this.lastHeartbeatTimestamp = Date.now(); + + this.watchdogTimeout = setInterval(() => { + if (Date.now() - this.lastHeartbeatTimestamp < this.options.pingIntervalInMS) { + return; + } + + try { + this.ping(); + } catch { + // If the ping call fails synchronously, it's because we couldn't send the ping message + // then likely the process isn't running, so we stop everything + this.debug('[LivenessManager] Failed to send ping to subprocess, stopping watchdog...'); + this.stop(); + } + }, this.options.pingIntervalInMS); + + this.watchdogTimeout.unref(); + } + + public stop() { + this.pingAbortController.emit('abort'); + clearInterval(this.watchdogTimeout); + this.watchdogTimeout = null; + this.pendingPing = null; + } + + public getPendingPing() { + return this.pendingPing; + } + + /** + * Start up the process of ping/pong for liveness check + * + * The message exchange does not use JSON RPC as it adds a lot of overhead + * with the creation and encoding of a full object for transfer. By using a + * string the process is less intensive. + */ + private ping() { + const start = Date.now(); + + this.pendingPing = new Promise((resolve, reject) => { + const onceCallback = () => { + const now = Date.now(); + this.debug('Ping successful in %d ms', now - start); + clearTimeout(timeoutId); + this.pingTimeoutConsecutiveCount = 0; + this.lastHeartbeatTimestamp = now; + resolve(true); + }; + + const timeoutCallback = () => { + this.debug('Ping failed in %d ms (consecutive failure #%d)', Date.now() - start, this.pingTimeoutConsecutiveCount); + this.controller.off('pong', onceCallback); + this.pingTimeoutConsecutiveCount++; + reject('timeout'); + }; + + this.pingAbortController.once('abort', () => { + this.debug('Ping aborted'); + reject('abort'); + }); + + const timeoutId = setTimeout(timeoutCallback, this.options.pingTimeoutInMS); + + this.controller.once('pong', onceCallback); + }) + .catch((reason) => { + if (reason === 'abort') { + return false; + } + + if (reason === 'timeout' && this.pingTimeoutConsecutiveCount >= this.options.consecutiveTimeoutLimit) { + this.debug( + 'Subprocess failed to respond to pings %d consecutive times. Attempting restart...', + this.options.consecutiveTimeoutLimit, + ); + this.restartProcess('Too many pings timed out'); + return false; + } + + return true; + }) + .finally(() => { + this.pingAbortController.removeAllListeners('abort'); + }); + + this.messenger.send(COMMAND_PING); + } + + private handleError(err: Error) { + this.debug('App has failed to start.`', err); + this.restartProcess(err.message); + } + + private handleExit(exitCode: number, signal: string) { + const processState = this.controller.getProcessState(); + // If the we're restarting the process, or want to stop the process, or it exited cleanly, nothing else for us to do + if (processState === 'restarting' || processState === 'stopped' || (exitCode === 0 && !signal)) { + return; + } + + let reason: string; + + // Otherwise we attempt to restart the process + if (signal) { + this.debug('App has been killed (%s). Attempting restart #%d...', signal, this.restartCount + 1); + reason = `App has been killed with signal ${signal}`; + } else { + this.debug('App has exited with code %d. Attempting restart #%d...', exitCode, this.restartCount + 1); + reason = `App has exited with code ${exitCode}`; + } + + this.restartProcess(reason); + } + + private async restartProcess(reason: string, source = 'liveness-manager') { + this.stop(); + + if (this.restartCount >= this.options.maxRestarts) { + this.debug('Limit of restarts reached (%d). Aborting restart...', this.options.maxRestarts); + this.controller.stopApp(); + return; + } + + this.restartLog.push({ + reason, + source, + restartedAt: new Date(), + pid: this.subprocess.pid, + }); + + try { + await this.controller.restartApp(); + } catch (e) { + this.debug('Restart attempt failed. Retrying in %dms', this.options.restartAttemptDelayInMS); + setTimeout(() => this.restartProcess('Failed restart attempt'), this.options.restartAttemptDelayInMS); + } + + this.restartCount++; + } +} diff --git a/packages/apps/src/server/runtime/deno/ProcessMessenger.ts b/packages/apps/src/server/runtime/deno/ProcessMessenger.ts new file mode 100644 index 0000000000000..c5c2394e56dfa --- /dev/null +++ b/packages/apps/src/server/runtime/deno/ProcessMessenger.ts @@ -0,0 +1,57 @@ +import type { ChildProcess } from 'child_process'; + +import type { JsonRpc } from 'jsonrpc-lite'; + +import type { COMMAND_PING } from './LivenessManager'; +import type { Encoder } from './codec'; +import { newEncoder } from './codec'; + +type Message = JsonRpc | typeof COMMAND_PING; + +export class ProcessMessenger { + private deno: ChildProcess | undefined; + + private encoder: Encoder | undefined; + + private _sendStrategy: (message: Message) => void; + + constructor() { + this._sendStrategy = this.strategyError; + } + + public send(message: Message) { + this._sendStrategy(message); + } + + public setReceiver(deno: ChildProcess) { + this.deno = deno; + + this.switchStrategy(); + } + + public clearReceiver() { + delete this.deno; + delete this.encoder; + + this.switchStrategy(); + } + + private switchStrategy() { + if (this.deno?.stdin?.writable) { + this._sendStrategy = this.strategySend.bind(this); + + // Get a clean encoder + this.encoder = newEncoder(); + } else { + this._sendStrategy = this.strategyError.bind(this); + } + } + + private strategyError(_message: Message) { + throw new Error('No process configured to receive a message'); + } + + private strategySend(message: Message) { + this.deno.stdin.write(this.encoder.encode(message)); + } +} diff --git a/packages/apps/src/server/runtime/deno/bundler.ts b/packages/apps/src/server/runtime/deno/bundler.ts new file mode 100644 index 0000000000000..e6442a336b83c --- /dev/null +++ b/packages/apps/src/server/runtime/deno/bundler.ts @@ -0,0 +1,90 @@ +import * as path from 'path'; + +import { build, type PluginBuild, type OnLoadArgs, type OnResolveArgs } from 'esbuild'; + +import type { IParseAppPackageResult } from '../../compiler'; + +/** + * Some legacy apps that might be installed in workspaces have not been bundled after compilation, + * leading to multiple files being sent to the subprocess and requiring further logic to require one another. + * This makes running the app in the Deno Runtime much more difficult, so instead we bundle the files at runtime. + */ +export async function bundleLegacyApp(appPackage: IParseAppPackageResult) { + const buildResult = await build({ + write: false, + bundle: true, + minify: true, + platform: 'node', + target: ['node10'], + define: { + 'global.Promise': 'Promise', + }, + external: ['@rocket.chat/apps-engine/*'], + stdin: { + contents: appPackage.files[appPackage.info.classFile], + sourcefile: appPackage.info.classFile, + loader: 'js', + }, + plugins: [ + { + name: 'legacy-app', + setup(build: PluginBuild) { + build.onResolve({ filter: /.*/ }, (args: OnResolveArgs) => { + if (args.namespace === 'file') { + return; + } + + const modulePath = path.join(path.dirname(args.importer), args.path).concat('.js'); + + const hasFile = !!appPackage.files[modulePath]; + + if (hasFile) { + return { + namespace: 'app-source', + path: modulePath, + }; + } + + // require('../') or require('./') are both valid, but aren't included in the files record in the same way + // we need to treat those differently + if (/\.\.?\//.test(args.path)) { + const indexModulePath = modulePath.replace(/\.js$/, `${path.sep}index.js`); + + if (appPackage.files[indexModulePath]) { + return { + namespace: 'app-source', + path: indexModulePath, + }; + } + } + + return { + path: args.path, + external: true, + }; + }); + + build.onLoad({ filter: /.*/, namespace: 'app-source' }, (args: OnLoadArgs) => { + if (!appPackage.files[args.path]) { + return { + errors: [ + { + text: `File ${args.path} could not be found`, + }, + ], + }; + } + + return { + contents: appPackage.files[args.path], + }; + }); + }, + }, + ], + }); + + const [{ text: bundle }] = buildResult.outputFiles; + + appPackage.files = { [appPackage.info.classFile]: bundle }; +} diff --git a/packages/apps/src/server/runtime/deno/codec.ts b/packages/apps/src/server/runtime/deno/codec.ts new file mode 100644 index 0000000000000..53b05846565ee --- /dev/null +++ b/packages/apps/src/server/runtime/deno/codec.ts @@ -0,0 +1,45 @@ +import { Decoder as _Decoder, Encoder as _Encoder, ExtensionCodec } from '@msgpack/msgpack'; + +const extensionCodec = new ExtensionCodec(); + +extensionCodec.register({ + type: 0, + encode: (object: unknown) => { + // We don't care about functions, but also don't want to throw an error + if (typeof object === 'function') { + return new Uint8Array([0]); + } + }, + + decode: (_data: Uint8Array) => undefined, +}); + +// We need to handle Buffers because Deno needs its own decoding +extensionCodec.register({ + type: 1, + encode: (object: unknown) => { + if (object instanceof Buffer) { + return new Uint8Array(object.buffer, object.byteOffset, object.byteLength); + } + }, + + // msgpack will reuse the Uint8Array instance, so WE NEED to copy it instead of simply creating a view + decode: (data: Uint8Array) => Buffer.from(data), +}); + +/** + * The Encoder and Decoder classes perform "stateful" operations, i.e. they read from a + * stream, store the data locally and decode it from its buffer. + * + * In practice, this affects the decoder when there is decode error. After an error, the decoder + * keeps the malformed data in its buffer, and even if we try to decode from another source (e.g. different stream) + * it will fail again as there's still data in the buffer. + * + * For that reason, we can't have a singleton instance of Encoder and Decoder, but rather one + * instance for each time we create a new subprocess + */ +export const newEncoder = () => new _Encoder({ extensionCodec }); +export const newDecoder = () => new _Decoder({ extensionCodec }); + +export type Encoder = _Encoder; +export type Decoder = _Decoder; diff --git a/packages/apps/src/server/storage/AppLogStorage.ts b/packages/apps/src/server/storage/AppLogStorage.ts new file mode 100644 index 0000000000000..8a3487a5587be --- /dev/null +++ b/packages/apps/src/server/storage/AppLogStorage.ts @@ -0,0 +1,27 @@ +import type { ILoggerStorageEntry } from '../logging'; + +export interface IAppLogStorageFindOptions { + sort?: Record; + skip?: number; + limit?: number; + projection?: Record; +} + +export abstract class AppLogStorage { + constructor(private readonly engine: string) {} + + public getEngine() { + return this.engine; + } + + public abstract findPaginated( + query: { [field: string]: any }, + options?: IAppLogStorageFindOptions, + ): Promise<{ logs: ILoggerStorageEntry[]; total: number }>; + + public abstract storeEntries(logEntry: ILoggerStorageEntry): Promise; + + public abstract getEntriesFor(appId: string): Promise>; + + public abstract removeEntriesFor(appId: string): Promise; +} diff --git a/packages/apps/src/server/storage/AppMetadataStorage.ts b/packages/apps/src/server/storage/AppMetadataStorage.ts new file mode 100644 index 0000000000000..574b213347dac --- /dev/null +++ b/packages/apps/src/server/storage/AppMetadataStorage.ts @@ -0,0 +1,36 @@ +import type { IAppStorageItem } from './IAppStorageItem'; +import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata/IAppInfo'; +import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; +import type { IMarketplaceInfo } from '../marketplace'; + +export abstract class AppMetadataStorage { + constructor(private readonly engine: string) {} + + public getEngine() { + return this.engine; + } + + public abstract create(item: IAppStorageItem): Promise; + + public abstract retrieveOne(id: string): Promise; + + public abstract retrieveAll(): Promise>; + + public abstract retrieveAllPrivate(): Promise>; + + public abstract remove(id: string): Promise<{ success: boolean }>; + + public abstract updatePartialAndReturnDocument( + item: Partial, + options?: { unsetPermissionsGranted?: boolean }, + ): Promise; + + public abstract updateStatus(_id: string, status: AppStatus): Promise; + + public abstract updateSetting(_id: string, setting: ISetting): Promise; + + public abstract updateAppInfo(_id: string, info: IAppInfo): Promise; + + public abstract updateMarketplaceInfo(_id: string, marketplaceInfo: IMarketplaceInfo[]): Promise; +} diff --git a/packages/apps/src/server/storage/AppSourceStorage.ts b/packages/apps/src/server/storage/AppSourceStorage.ts new file mode 100644 index 0000000000000..c0c0f8ea61971 --- /dev/null +++ b/packages/apps/src/server/storage/AppSourceStorage.ts @@ -0,0 +1,40 @@ +import type { IAppStorageItem } from './IAppStorageItem'; + +export abstract class AppSourceStorage { + /** + * Stores an app package (zip file) in the underlying + * storage provided by the host + * + * @param item descriptor of the App + * @param zip the app package file contents + * + * @returns the path in which the pacakge has been stored + */ + public abstract store(item: IAppStorageItem, zip: Buffer): Promise; + + /** + * Fetches an app's package file contents + * + * @param item descriptor of the App + * + * @returns buffer containing the file contents of the app's package + */ + public abstract fetch(item: IAppStorageItem): Promise; + + /** + * Updates an app package (zip file) in the underlying + * storage provided by the host + * + * @param item descriptor of the App + * @param zip the app package file contents + * + * @returns the path in which the pacakge has been stored + */ + public abstract update(item: IAppStorageItem, zip: Buffer): Promise; + + /** + * + * @param item descriptor of the App + */ + public abstract remove(item: IAppStorageItem): Promise; +} diff --git a/packages/apps/src/server/storage/IAppStorageItem.ts b/packages/apps/src/server/storage/IAppStorageItem.ts new file mode 100644 index 0000000000000..93c51b23e795b --- /dev/null +++ b/packages/apps/src/server/storage/IAppStorageItem.ts @@ -0,0 +1,31 @@ +import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; +import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; +import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; +import type { IMarketplaceInfo } from '../marketplace'; + +export interface IAppStorageItem { + _id?: string; + id: string; + createdAt?: Date; + updatedAt?: Date; + status: AppStatus; + info: IAppInfo; + installationSource: AppInstallationSource; + /** + * The path that represents where the source of the app storaged. + */ + sourcePath?: string; + languageContent: { [key: string]: object }; + settings: { [id: string]: ISetting }; + implemented: { [int: string]: boolean }; + marketplaceInfo?: IMarketplaceInfo[]; + permissionsGranted?: Array; + signature?: string; + migrated?: boolean; +} + +export enum AppInstallationSource { + MARKETPLACE = 'marketplace', + PRIVATE = 'private', +} diff --git a/packages/apps/src/server/storage/index.ts b/packages/apps/src/server/storage/index.ts new file mode 100644 index 0000000000000..5f0993129bf3a --- /dev/null +++ b/packages/apps/src/server/storage/index.ts @@ -0,0 +1,4 @@ +export { AppLogStorage, IAppLogStorageFindOptions } from './AppLogStorage'; +export { AppMetadataStorage } from './AppMetadataStorage'; +export { IAppStorageItem, AppInstallationSource } from './IAppStorageItem'; +export { AppSourceStorage } from './AppSourceStorage'; From 3269f86f2aa868faf9c4b8868d02f5ae42f6da36 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Apr 2026 13:16:28 -0300 Subject: [PATCH 04/19] feat(apps): copy apps-engine client UI host code into @rocket.chat/apps Copies src/client/ (AppClientManager, AppsEngineUIHost, AppsEngineUIClient) from @rocket.chat/apps-engine, rewriting relative definition/ imports to package imports. This code is a known rough edge: browser-side UI host logic does not semantically belong in a server orchestration package. It is consolidated here for pragmatic simplicity during the apps-engine split. A future @rocket.chat/apps-client package is tracked in the TODO comment added to src/client/index.ts. Co-Authored-By: Claude Sonnet 4.6 --- packages/apps/src/client/AppClientManager.ts | 31 ++++++++ .../apps/src/client/AppServerCommunicator.ts | 16 ++++ .../apps/src/client/AppsEngineUIClient.ts | 70 +++++++++++++++++ packages/apps/src/client/AppsEngineUIHost.ts | 78 +++++++++++++++++++ packages/apps/src/client/constants/index.ts | 6 ++ .../client/definition/AppsEngineUIMethods.ts | 7 ++ .../definition/IAppsEngineUIResponse.ts | 19 +++++ .../definition/IExternalComponentRoomInfo.ts | 16 ++++ .../definition/IExternalComponentUserInfo.ts | 14 ++++ packages/apps/src/client/definition/index.ts | 4 + packages/apps/src/client/index.ts | 10 +++ packages/apps/src/client/utils/index.ts | 18 +++++ 12 files changed, 289 insertions(+) create mode 100644 packages/apps/src/client/AppClientManager.ts create mode 100644 packages/apps/src/client/AppServerCommunicator.ts create mode 100644 packages/apps/src/client/AppsEngineUIClient.ts create mode 100644 packages/apps/src/client/AppsEngineUIHost.ts create mode 100644 packages/apps/src/client/constants/index.ts create mode 100644 packages/apps/src/client/definition/AppsEngineUIMethods.ts create mode 100644 packages/apps/src/client/definition/IAppsEngineUIResponse.ts create mode 100644 packages/apps/src/client/definition/IExternalComponentRoomInfo.ts create mode 100644 packages/apps/src/client/definition/IExternalComponentUserInfo.ts create mode 100644 packages/apps/src/client/definition/index.ts create mode 100644 packages/apps/src/client/index.ts create mode 100644 packages/apps/src/client/utils/index.ts diff --git a/packages/apps/src/client/AppClientManager.ts b/packages/apps/src/client/AppClientManager.ts new file mode 100644 index 0000000000000..bba33e76f25a9 --- /dev/null +++ b/packages/apps/src/client/AppClientManager.ts @@ -0,0 +1,31 @@ +import { AppServerCommunicator } from './AppServerCommunicator'; +import { AppsEngineUIHost } from './AppsEngineUIHost'; +import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; + +export class AppClientManager { + private apps: Array; + + constructor( + private readonly appsEngineUIHost: AppsEngineUIHost, + private readonly communicator?: AppServerCommunicator, + ) { + if (!(appsEngineUIHost instanceof AppsEngineUIHost)) { + throw new Error('The appClientUIHost must extend appClientUIHost'); + } + + if (communicator && !(communicator instanceof AppServerCommunicator)) { + throw new Error('The communicator must extend AppServerCommunicator'); + } + + this.apps = []; + } + + public async load(): Promise { + this.apps = await this.communicator.getEnabledApps(); + console.log('Enabled apps:', this.apps); + } + + public async initialize(): Promise { + this.appsEngineUIHost.initialize(); + } +} diff --git a/packages/apps/src/client/AppServerCommunicator.ts b/packages/apps/src/client/AppServerCommunicator.ts new file mode 100644 index 0000000000000..c40e139fc4f0f --- /dev/null +++ b/packages/apps/src/client/AppServerCommunicator.ts @@ -0,0 +1,16 @@ +import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; + +export abstract class AppServerCommunicator { + public abstract getEnabledApps(): Promise>; + + public abstract getDisabledApps(): Promise>; + + // Map> + public abstract getLanguageAdditions(): Promise>>; + + // Map> + public abstract getSlashCommands(): Promise>>; + + // Map> + public abstract getContextualBarButtons(): Promise>>; +} diff --git a/packages/apps/src/client/AppsEngineUIClient.ts b/packages/apps/src/client/AppsEngineUIClient.ts new file mode 100644 index 0000000000000..09e87f3da34e7 --- /dev/null +++ b/packages/apps/src/client/AppsEngineUIClient.ts @@ -0,0 +1,70 @@ +import { ACTION_ID_LENGTH, MESSAGE_ID } from './constants'; +import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from './definition'; +import { AppsEngineUIMethods } from './definition/AppsEngineUIMethods'; +import { randomString } from './utils'; + +/** + * Represents the SDK provided to the external component. + */ +export class AppsEngineUIClient { + private listener: (this: Window, ev: MessageEvent) => any; + + private callbacks: Map any>; + + constructor() { + this.listener = () => console.log('init'); + this.callbacks = new Map(); + } + + /** + * Get the current user's information. + * + * @return the information of the current user. + */ + public getUserInfo(): Promise { + return this.call(AppsEngineUIMethods.GET_USER_INFO); + } + + /** + * Get the current room's information. + * + * @return the information of the current room. + */ + public getRoomInfo(): Promise { + return this.call(AppsEngineUIMethods.GET_ROOM_INFO); + } + + /** + * Initialize the app SDK for communicating with Rocket.Chat + */ + public init(): void { + this.listener = ({ data }) => { + if (!data?.hasOwnProperty(MESSAGE_ID)) { + return; + } + + const { + [MESSAGE_ID]: { id, payload }, + } = data; + + if (this.callbacks.has(id)) { + const resolve = this.callbacks.get(id); + + if (typeof resolve === 'function') { + resolve(payload); + } + this.callbacks.delete(id); + } + }; + window.addEventListener('message', this.listener); + } + + private call(action: string, payload?: any): Promise { + return new Promise((resolve) => { + const id = randomString(ACTION_ID_LENGTH); + + window.parent.postMessage({ [MESSAGE_ID]: { action, payload, id } }, '*'); + this.callbacks.set(id, resolve); + }); + } +} diff --git a/packages/apps/src/client/AppsEngineUIHost.ts b/packages/apps/src/client/AppsEngineUIHost.ts new file mode 100644 index 0000000000000..c10067e6624bc --- /dev/null +++ b/packages/apps/src/client/AppsEngineUIHost.ts @@ -0,0 +1,78 @@ +import { MESSAGE_ID } from './constants'; +import type { IAppsEngineUIResponse, IExternalComponentRoomInfo, IExternalComponentUserInfo } from './definition'; +import { AppsEngineUIMethods } from './definition'; + +type HandleActionData = IExternalComponentUserInfo | IExternalComponentRoomInfo; + +/** + * Represents the host which handles API calls from external components. + */ +export abstract class AppsEngineUIHost { + /** + * The message emitter who calling the API. + */ + private responseDestination!: Window; + + constructor() { + this.initialize(); + } + + /** + * initialize the AppClientUIHost by registering window `message` listener + */ + public initialize() { + window.addEventListener('message', async ({ data, source }) => { + if (!data?.hasOwnProperty(MESSAGE_ID)) { + return; + } + + this.responseDestination = source as Window; + + const { + [MESSAGE_ID]: { action, id }, + } = data; + + switch (action) { + case AppsEngineUIMethods.GET_USER_INFO: + this.handleAction(action, id, await this.getClientUserInfo()); + break; + case AppsEngineUIMethods.GET_ROOM_INFO: + this.handleAction(action, id, await this.getClientRoomInfo()); + break; + } + }); + } + + /** + * Get the current user's information. + */ + public abstract getClientUserInfo(): Promise; + + /** + * Get the opened room's information. + */ + public abstract getClientRoomInfo(): Promise; + + /** + * Handle the action sent from the external component. + * @param action the name of the action + * @param id the unique id of the API call + * @param data The data that will return to the caller + */ + private async handleAction(action: AppsEngineUIMethods, id: string, data: HandleActionData): Promise { + if (this.responseDestination instanceof MessagePort || this.responseDestination instanceof ServiceWorker) { + return; + } + + this.responseDestination.postMessage( + { + [MESSAGE_ID]: { + id, + action, + payload: data, + } as IAppsEngineUIResponse, + }, + '*', + ); + } +} diff --git a/packages/apps/src/client/constants/index.ts b/packages/apps/src/client/constants/index.ts new file mode 100644 index 0000000000000..bd7f2e779ca1b --- /dev/null +++ b/packages/apps/src/client/constants/index.ts @@ -0,0 +1,6 @@ +/** + * The id length of each action. + */ +export const ACTION_ID_LENGTH = 80; + +export const MESSAGE_ID = 'rc-apps-engine-ui'; diff --git a/packages/apps/src/client/definition/AppsEngineUIMethods.ts b/packages/apps/src/client/definition/AppsEngineUIMethods.ts new file mode 100644 index 0000000000000..6eb3fb908e5a1 --- /dev/null +++ b/packages/apps/src/client/definition/AppsEngineUIMethods.ts @@ -0,0 +1,7 @@ +/** + * The actions provided by the AppClientSDK. + */ +export enum AppsEngineUIMethods { + GET_USER_INFO = 'getUserInfo', + GET_ROOM_INFO = 'getRoomInfo', +} diff --git a/packages/apps/src/client/definition/IAppsEngineUIResponse.ts b/packages/apps/src/client/definition/IAppsEngineUIResponse.ts new file mode 100644 index 0000000000000..d8690b9b31781 --- /dev/null +++ b/packages/apps/src/client/definition/IAppsEngineUIResponse.ts @@ -0,0 +1,19 @@ +import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from './index'; + +/** + * The response to the AppClientSDK's API call. + */ +export interface IAppsEngineUIResponse { + /** + * The name of the action + */ + action: string; + /** + * The unique id of the API call + */ + id: string; + /** + * The data that will return to the caller + */ + payload: IExternalComponentUserInfo | IExternalComponentRoomInfo; +} diff --git a/packages/apps/src/client/definition/IExternalComponentRoomInfo.ts b/packages/apps/src/client/definition/IExternalComponentRoomInfo.ts new file mode 100644 index 0000000000000..5ecd812270359 --- /dev/null +++ b/packages/apps/src/client/definition/IExternalComponentRoomInfo.ts @@ -0,0 +1,16 @@ +import type { IExternalComponentUserInfo } from './IExternalComponentUserInfo'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; + +type ClientRoomInfo = Pick; + +/** + * Represents the room's information returned to the + * external component. + */ +export interface IExternalComponentRoomInfo extends ClientRoomInfo { + /** + * the list that contains all the users belonging + * to this room. + */ + members: Array; +} diff --git a/packages/apps/src/client/definition/IExternalComponentUserInfo.ts b/packages/apps/src/client/definition/IExternalComponentUserInfo.ts new file mode 100644 index 0000000000000..aca1be1405aa3 --- /dev/null +++ b/packages/apps/src/client/definition/IExternalComponentUserInfo.ts @@ -0,0 +1,14 @@ +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; + +type ClientUserInfo = Pick; + +/** + * Represents the user's information returned to + * the external component. + */ +export interface IExternalComponentUserInfo extends ClientUserInfo { + /** + * the avatar URL of the Rocket.Chat user + */ + avatarUrl: string; +} diff --git a/packages/apps/src/client/definition/index.ts b/packages/apps/src/client/definition/index.ts new file mode 100644 index 0000000000000..70a1fe884a6a9 --- /dev/null +++ b/packages/apps/src/client/definition/index.ts @@ -0,0 +1,4 @@ +export * from './AppsEngineUIMethods'; +export * from './IExternalComponentUserInfo'; +export * from './IExternalComponentRoomInfo'; +export * from './IAppsEngineUIResponse'; diff --git a/packages/apps/src/client/index.ts b/packages/apps/src/client/index.ts new file mode 100644 index 0000000000000..6bd5e4b6e00a8 --- /dev/null +++ b/packages/apps/src/client/index.ts @@ -0,0 +1,10 @@ +// TODO: The browser-side UI host code (AppClientManager, AppsEngineUIHost, +// AppsEngineUIClient) does not semantically belong in @rocket.chat/apps — +// this package is server-side orchestration. It was moved here as part of +// the apps-engine split for pragmatic consolidation. A future split into a +// dedicated @rocket.chat/apps-client package is tracked in: +// https://github.com/RocketChat/Rocket.Chat/issues/PLACEHOLDER +import { AppClientManager } from './AppClientManager'; +import { AppServerCommunicator } from './AppServerCommunicator'; + +export { AppClientManager, AppServerCommunicator }; diff --git a/packages/apps/src/client/utils/index.ts b/packages/apps/src/client/utils/index.ts new file mode 100644 index 0000000000000..ff726ee5934f4 --- /dev/null +++ b/packages/apps/src/client/utils/index.ts @@ -0,0 +1,18 @@ +/** + * Generate a random string with the specified length. + * @param length the length for the generated random string. + */ +export function randomString(length: number): string { + const buffer: Array = []; + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + for (let i = 0; i < length; i++) { + buffer.push(chars[getRandomInt(chars.length)]); + } + + return buffer.join(''); +} + +function getRandomInt(max: number): number { + return Math.floor(Math.random() * Math.floor(max)); +} From d15f9cbd8b50ebecd3c2faa3ac5c0139b06827aa Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Apr 2026 13:16:54 -0300 Subject: [PATCH 05/19] feat(apps): copy deno-runtime into @rocket.chat/apps Copies deno-runtime/ verbatim from @rocket.chat/apps-engine. The import map in deno.jsonc still points to ./../src/ which is only valid in the current location (apps-engine). Making the import map location-independent (using a runtime-generated map) is handled in a dedicated follow-up PR to keep the diff focused. Co-Authored-By: Claude Sonnet 4.6 --- packages/apps/deno-runtime/.gitignore | 1 + .../apps/deno-runtime/AppObjectRegistry.ts | 25 + packages/apps/deno-runtime/acorn-walk.d.ts | 175 ++++ packages/apps/deno-runtime/acorn.d.ts | 915 ++++++++++++++++++ packages/apps/deno-runtime/deno.jsonc | 26 + packages/apps/deno-runtime/deno.lock | 128 +++ packages/apps/deno-runtime/error-handlers.ts | 33 + .../apps/deno-runtime/handlers/api-handler.ts | 50 + .../deno-runtime/handlers/app/construct.ts | 132 +++ .../handlers/app/handleGetStatus.ts | 15 + .../handlers/app/handleInitialize.ts | 24 + .../handlers/app/handleOnDisable.ts | 20 + .../handlers/app/handleOnEnable.ts | 22 + .../handlers/app/handleOnInstall.ts | 34 + .../handlers/app/handleOnPreSettingUpdate.ts | 31 + .../handlers/app/handleOnSettingUpdated.ts | 33 + .../handlers/app/handleOnUninstall.ts | 34 + .../handlers/app/handleOnUpdate.ts | 34 + .../handlers/app/handleSetStatus.ts | 33 + .../handlers/app/handleUploadEvents.ts | 78 ++ .../apps/deno-runtime/handlers/app/handler.ts | 115 +++ .../deno-runtime/handlers/lib/assertions.ts | 51 + .../deno-runtime/handlers/listener/handler.ts | 153 +++ .../handlers/outboundcomms-handler.ts | 37 + .../handlers/scheduler-handler.ts | 65 ++ .../handlers/slashcommand-handler.ts | 128 +++ .../handlers/tests/api-handler.test.ts | 118 +++ .../handlers/tests/helpers/mod.ts | 29 + .../handlers/tests/listener-handler.test.ts | 234 +++++ .../handlers/tests/scheduler-handler.test.ts | 41 + .../tests/slashcommand-handler.test.ts | 159 +++ .../handlers/tests/uikit-handler.test.ts | 105 ++ .../tests/upload-event-handler.test.ts | 107 ++ .../tests/videoconference-handler.test.ts | 122 +++ .../deno-runtime/handlers/uikit/handler.ts | 88 ++ .../handlers/videoconference-handler.ts | 52 + .../lib/accessors/builders/BlockBuilder.ts | 215 ++++ .../accessors/builders/DiscussionBuilder.ts | 59 ++ .../builders/LivechatMessageBuilder.ts | 204 ++++ .../lib/accessors/builders/MessageBuilder.ts | 271 ++++++ .../lib/accessors/builders/RoomBuilder.ts | 197 ++++ .../lib/accessors/builders/UserBuilder.ts | 81 ++ .../builders/VideoConferenceBuilder.ts | 94 ++ .../lib/accessors/extenders/HttpExtender.ts | 58 ++ .../accessors/extenders/MessageExtender.ts | 66 ++ .../lib/accessors/extenders/RoomExtender.ts | 61 ++ .../extenders/VideoConferenceExtend.ts | 69 ++ .../accessors/formatResponseErrorHandler.ts | 14 + .../apps/deno-runtime/lib/accessors/http.ts | 92 ++ .../apps/deno-runtime/lib/accessors/mod.ts | 322 ++++++ .../lib/accessors/modify/ModifyCreator.ts | 383 ++++++++ .../lib/accessors/modify/ModifyExtender.ts | 106 ++ .../lib/accessors/modify/ModifyUpdater.ts | 170 ++++ .../deno-runtime/lib/accessors/notifier.ts | 84 ++ .../lib/accessors/tests/AppAccessors.test.ts | 122 +++ .../lib/accessors/tests/ModifyCreator.test.ts | 259 +++++ .../accessors/tests/ModifyExtender.test.ts | 244 +++++ .../lib/accessors/tests/ModifyUpdater.test.ts | 234 +++++ .../tests/formatResponseErrorHandler.test.ts | 211 ++++ .../lib/accessors/tests/http.test.ts | 164 ++++ packages/apps/deno-runtime/lib/ast/mod.ts | 70 ++ .../apps/deno-runtime/lib/ast/operations.ts | 237 +++++ .../lib/ast/tests/data/ast_blocks.ts | 436 +++++++++ .../lib/ast/tests/operations.test.ts | 261 +++++ packages/apps/deno-runtime/lib/codec.ts | 43 + packages/apps/deno-runtime/lib/logger.ts | 142 +++ packages/apps/deno-runtime/lib/messenger.ts | 202 ++++ .../apps/deno-runtime/lib/metricsCollector.ts | 20 + packages/apps/deno-runtime/lib/parseArgs.ts | 11 + .../apps/deno-runtime/lib/requestContext.ts | 10 + packages/apps/deno-runtime/lib/require.ts | 15 + packages/apps/deno-runtime/lib/room.ts | 104 ++ packages/apps/deno-runtime/lib/roomFactory.ts | 29 + .../lib/sanitizeDeprecatedUsage.ts | 20 + .../deno-runtime/lib/tests/logger.test.ts | 110 +++ .../deno-runtime/lib/tests/messenger.test.ts | 99 ++ .../deno-runtime/lib/wrapAppForRequest.ts | 60 ++ packages/apps/deno-runtime/main.ts | 132 +++ 78 files changed, 9228 insertions(+) create mode 100644 packages/apps/deno-runtime/.gitignore create mode 100644 packages/apps/deno-runtime/AppObjectRegistry.ts create mode 100644 packages/apps/deno-runtime/acorn-walk.d.ts create mode 100644 packages/apps/deno-runtime/acorn.d.ts create mode 100644 packages/apps/deno-runtime/deno.jsonc create mode 100644 packages/apps/deno-runtime/deno.lock create mode 100644 packages/apps/deno-runtime/error-handlers.ts create mode 100644 packages/apps/deno-runtime/handlers/api-handler.ts create mode 100644 packages/apps/deno-runtime/handlers/app/construct.ts create mode 100644 packages/apps/deno-runtime/handlers/app/handleGetStatus.ts create mode 100644 packages/apps/deno-runtime/handlers/app/handleInitialize.ts create mode 100644 packages/apps/deno-runtime/handlers/app/handleOnDisable.ts create mode 100644 packages/apps/deno-runtime/handlers/app/handleOnEnable.ts create mode 100644 packages/apps/deno-runtime/handlers/app/handleOnInstall.ts create mode 100644 packages/apps/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts create mode 100644 packages/apps/deno-runtime/handlers/app/handleOnSettingUpdated.ts create mode 100644 packages/apps/deno-runtime/handlers/app/handleOnUninstall.ts create mode 100644 packages/apps/deno-runtime/handlers/app/handleOnUpdate.ts create mode 100644 packages/apps/deno-runtime/handlers/app/handleSetStatus.ts create mode 100644 packages/apps/deno-runtime/handlers/app/handleUploadEvents.ts create mode 100644 packages/apps/deno-runtime/handlers/app/handler.ts create mode 100644 packages/apps/deno-runtime/handlers/lib/assertions.ts create mode 100644 packages/apps/deno-runtime/handlers/listener/handler.ts create mode 100644 packages/apps/deno-runtime/handlers/outboundcomms-handler.ts create mode 100644 packages/apps/deno-runtime/handlers/scheduler-handler.ts create mode 100644 packages/apps/deno-runtime/handlers/slashcommand-handler.ts create mode 100644 packages/apps/deno-runtime/handlers/tests/api-handler.test.ts create mode 100644 packages/apps/deno-runtime/handlers/tests/helpers/mod.ts create mode 100644 packages/apps/deno-runtime/handlers/tests/listener-handler.test.ts create mode 100644 packages/apps/deno-runtime/handlers/tests/scheduler-handler.test.ts create mode 100644 packages/apps/deno-runtime/handlers/tests/slashcommand-handler.test.ts create mode 100644 packages/apps/deno-runtime/handlers/tests/uikit-handler.test.ts create mode 100644 packages/apps/deno-runtime/handlers/tests/upload-event-handler.test.ts create mode 100644 packages/apps/deno-runtime/handlers/tests/videoconference-handler.test.ts create mode 100644 packages/apps/deno-runtime/handlers/uikit/handler.ts create mode 100644 packages/apps/deno-runtime/handlers/videoconference-handler.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/builders/BlockBuilder.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/builders/MessageBuilder.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/builders/RoomBuilder.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/builders/UserBuilder.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/extenders/HttpExtender.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/extenders/MessageExtender.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/extenders/RoomExtender.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/formatResponseErrorHandler.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/http.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/mod.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/modify/ModifyCreator.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/modify/ModifyExtender.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/modify/ModifyUpdater.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/notifier.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/tests/AppAccessors.test.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/tests/formatResponseErrorHandler.test.ts create mode 100644 packages/apps/deno-runtime/lib/accessors/tests/http.test.ts create mode 100644 packages/apps/deno-runtime/lib/ast/mod.ts create mode 100644 packages/apps/deno-runtime/lib/ast/operations.ts create mode 100644 packages/apps/deno-runtime/lib/ast/tests/data/ast_blocks.ts create mode 100644 packages/apps/deno-runtime/lib/ast/tests/operations.test.ts create mode 100644 packages/apps/deno-runtime/lib/codec.ts create mode 100644 packages/apps/deno-runtime/lib/logger.ts create mode 100644 packages/apps/deno-runtime/lib/messenger.ts create mode 100644 packages/apps/deno-runtime/lib/metricsCollector.ts create mode 100644 packages/apps/deno-runtime/lib/parseArgs.ts create mode 100644 packages/apps/deno-runtime/lib/requestContext.ts create mode 100644 packages/apps/deno-runtime/lib/require.ts create mode 100644 packages/apps/deno-runtime/lib/room.ts create mode 100644 packages/apps/deno-runtime/lib/roomFactory.ts create mode 100644 packages/apps/deno-runtime/lib/sanitizeDeprecatedUsage.ts create mode 100644 packages/apps/deno-runtime/lib/tests/logger.test.ts create mode 100644 packages/apps/deno-runtime/lib/tests/messenger.test.ts create mode 100644 packages/apps/deno-runtime/lib/wrapAppForRequest.ts create mode 100644 packages/apps/deno-runtime/main.ts diff --git a/packages/apps/deno-runtime/.gitignore b/packages/apps/deno-runtime/.gitignore new file mode 100644 index 0000000000000..5942ea3a153e7 --- /dev/null +++ b/packages/apps/deno-runtime/.gitignore @@ -0,0 +1 @@ +.deno/ diff --git a/packages/apps/deno-runtime/AppObjectRegistry.ts b/packages/apps/deno-runtime/AppObjectRegistry.ts new file mode 100644 index 0000000000000..c9c05137a4a3d --- /dev/null +++ b/packages/apps/deno-runtime/AppObjectRegistry.ts @@ -0,0 +1,25 @@ +export type Maybe = T | null | undefined; + +export const AppObjectRegistry = new class { + registry: Record = {}; + + public get(key: string): Maybe { + return this.registry[key] as Maybe; + } + + public set(key: string, value: unknown): void { + this.registry[key] = value; + } + + public has(key: string): boolean { + return key in this.registry; + } + + public delete(key: string): void { + delete this.registry[key]; + } + + public clear(): void { + this.registry = {}; + } +}(); diff --git a/packages/apps/deno-runtime/acorn-walk.d.ts b/packages/apps/deno-runtime/acorn-walk.d.ts new file mode 100644 index 0000000000000..56db3bc38e9d2 --- /dev/null +++ b/packages/apps/deno-runtime/acorn-walk.d.ts @@ -0,0 +1,175 @@ +import type acorn from './acorn.d.ts'; + +export type FullWalkerCallback = ( + node: acorn.AnyNode, + state: TState, + type: string, +) => void; + +export type FullAncestorWalkerCallback = ( + node: acorn.AnyNode, + state: TState, + ancestors: acorn.AnyNode[], + type: string, +) => void; + +type AggregateType = { + Expression: acorn.Expression; + Statement: acorn.Statement; + Pattern: acorn.Pattern; + ForInit: acorn.VariableDeclaration | acorn.Expression; +}; + +export type SimpleVisitors = + & { + [type in acorn.AnyNode['type']]?: (node: Extract, state: TState) => void; + } + & { + [type in keyof AggregateType]?: (node: AggregateType[type], state: TState) => void; + }; + +export type AncestorVisitors = + & { + [type in acorn.AnyNode['type']]?: (node: Extract, state: TState, ancestors: acorn.Node[]) => void; + } + & { + [type in keyof AggregateType]?: (node: AggregateType[type], state: TState, ancestors: acorn.Node[]) => void; + }; + +export type WalkerCallback = (node: acorn.Node, state: TState) => void; + +export type RecursiveVisitors = + & { + [type in acorn.AnyNode['type']]?: (node: Extract, state: TState, callback: WalkerCallback) => void; + } + & { + [type in keyof AggregateType]?: (node: AggregateType[type], state: TState, callback: WalkerCallback) => void; + }; + +export type FindPredicate = (type: string, node: acorn.Node) => boolean; + +export interface Found { + node: acorn.Node; + state: TState; +} + +/** + * does a 'simple' walk over a tree + * @param node the AST node to walk + * @param visitors an object with properties whose names correspond to node types in the {@link https://github.com/estree/estree | ESTree spec}. The properties should contain functions that will be called with the node object and, if applicable the state at that point. + * @param base a walker algorithm + * @param state a start state. The default walker will simply visit all statements and expressions and not produce a meaningful state. (An example of a use of state is to track scope at each point in the tree.) + */ +export function simple( + node: acorn.Node, + visitors: SimpleVisitors, + base?: RecursiveVisitors, + state?: TState, +): void; + +/** + * does a 'simple' walk over a tree, building up an array of ancestor nodes (including the current node) and passing the array to the callbacks as a third parameter. + * @param node + * @param visitors + * @param base + * @param state + */ +export function ancestor( + node: acorn.Node, + visitors: AncestorVisitors, + base?: RecursiveVisitors, + state?: TState, +): void; + +/** + * does a 'recursive' walk, where the walker functions are responsible for continuing the walk on the child nodes of their target node. + * @param node + * @param state the start state + * @param functions contain an object that maps node types to walker functions + * @param base provides the fallback walker functions for node types that aren't handled in the {@link functions} object. If not given, the default walkers will be used. + */ +export function recursive( + node: acorn.Node, + state: TState, + functions: RecursiveVisitors, + base?: RecursiveVisitors, +): void; + +/** + * does a 'full' walk over a tree, calling the {@link callback} with the arguments (node, state, type) for each node + * @param node + * @param callback + * @param base + * @param state + */ +export function full( + node: acorn.Node, + callback: FullWalkerCallback, + base?: RecursiveVisitors, + state?: TState, +): void; + +/** + * does a 'full' walk over a tree, building up an array of ancestor nodes (including the current node) and passing the array to the callbacks as a third parameter. + * @param node + * @param callback + * @param base + * @param state + */ +export function fullAncestor( + node: acorn.AnyNode, + callback: FullAncestorWalkerCallback, + base?: RecursiveVisitors, + state?: TState, +): void; + +/** + * builds a new walker object by using the walker functions in {@link functions} and filling in the missing ones by taking defaults from {@link base}. + * @param functions + * @param base + */ +export function make( + functions: RecursiveVisitors, + base?: RecursiveVisitors, +): RecursiveVisitors; + +/** + * tries to locate a node in a tree at the given start and/or end offsets, which satisfies the predicate test. {@link start} and {@link end} can be either `null` (as wildcard) or a `number`. {@link test} may be a string (indicating a node type) or a function that takes (nodeType, node) arguments and returns a boolean indicating whether this node is interesting. {@link base} and {@link state} are optional, and can be used to specify a custom walker. Nodes are tested from inner to outer, so if two nodes match the boundaries, the inner one will be preferred. + * @param node + * @param start + * @param end + * @param type + * @param base + * @param state + */ +export function findNodeAt( + node: acorn.AnyNode, + start: number | undefined, + end?: number | undefined, + type?: FindPredicate | string, + base?: RecursiveVisitors, + state?: TState, +): Found | undefined; + +/** + * like {@link findNodeAt}, but will match any node that exists 'around' (spanning) the given position. + * @param node + * @param start + * @param type + * @param base + * @param state + */ +export function findNodeAround( + node: acorn.AnyNode, + start: number | undefined, + type?: FindPredicate | string, + base?: RecursiveVisitors, + state?: TState, +): Found | undefined; + +/** + * similar to {@link findNodeAround}, but will match all nodes after the given position (testing outer nodes before inner nodes). + */ +export const findNodeAfter: typeof findNodeAround; + +export const base: RecursiveVisitors; diff --git a/packages/apps/deno-runtime/acorn.d.ts b/packages/apps/deno-runtime/acorn.d.ts new file mode 100644 index 0000000000000..7ba007d999b11 --- /dev/null +++ b/packages/apps/deno-runtime/acorn.d.ts @@ -0,0 +1,915 @@ +export interface Node { + start?: number; + end?: number; + type: string; + range?: [number, number]; + loc?: SourceLocation | null; +} + +export interface SourceLocation { + source?: string | null; + start: Position; + end: Position; +} + +export interface Position { + /** 1-based */ + line: number; + /** 0-based */ + column: number; +} + +export interface Identifier extends Node { + type: 'Identifier'; + name: string; +} + +export interface Literal extends Node { + type: 'Literal'; + value?: string | boolean | null | number | RegExp | bigint; + raw?: string; + regex?: { + pattern: string; + flags: string; + }; + bigint?: string; +} + +export interface Program extends Node { + type: 'Program'; + body: Array; + sourceType: 'script' | 'module'; +} + +export interface Function extends Node { + id?: Identifier | null; + params: Array; + body: BlockStatement | Expression; + generator: boolean; + expression: boolean; + async: boolean; +} + +export interface ExpressionStatement extends Node { + type: 'ExpressionStatement'; + expression: Expression | Literal; + directive?: string; +} + +export interface BlockStatement extends Node { + type: 'BlockStatement'; + body: Array; +} + +export interface EmptyStatement extends Node { + type: 'EmptyStatement'; +} + +export interface DebuggerStatement extends Node { + type: 'DebuggerStatement'; +} + +export interface WithStatement extends Node { + type: 'WithStatement'; + object: Expression; + body: Statement; +} + +export interface ReturnStatement extends Node { + type: 'ReturnStatement'; + argument?: Expression | null; +} + +export interface LabeledStatement extends Node { + type: 'LabeledStatement'; + label: Identifier; + body: Statement; +} + +export interface BreakStatement extends Node { + type: 'BreakStatement'; + label?: Identifier | null; +} + +export interface ContinueStatement extends Node { + type: 'ContinueStatement'; + label?: Identifier | null; +} + +export interface IfStatement extends Node { + type: 'IfStatement'; + test: Expression; + consequent: Statement; + alternate?: Statement | null; +} + +export interface SwitchStatement extends Node { + type: 'SwitchStatement'; + discriminant: Expression; + cases: Array; +} + +export interface SwitchCase extends Node { + type: 'SwitchCase'; + test?: Expression | null; + consequent: Array; +} + +export interface ThrowStatement extends Node { + type: 'ThrowStatement'; + argument: Expression; +} + +export interface TryStatement extends Node { + type: 'TryStatement'; + block: BlockStatement; + handler?: CatchClause | null; + finalizer?: BlockStatement | null; +} + +export interface CatchClause extends Node { + type: 'CatchClause'; + param?: Pattern | null; + body: BlockStatement; +} + +export interface WhileStatement extends Node { + type: 'WhileStatement'; + test: Expression; + body: Statement; +} + +export interface DoWhileStatement extends Node { + type: 'DoWhileStatement'; + body: Statement; + test: Expression; +} + +export interface ForStatement extends Node { + type: 'ForStatement'; + init?: VariableDeclaration | Expression | null; + test?: Expression | null; + update?: Expression | null; + body: Statement; +} + +export interface ForInStatement extends Node { + type: 'ForInStatement'; + left: VariableDeclaration | Pattern; + right: Expression; + body: Statement; +} + +export interface FunctionDeclaration extends Function { + type: 'FunctionDeclaration'; + id: Identifier; + body: BlockStatement; +} + +export interface VariableDeclaration extends Node { + type: 'VariableDeclaration'; + declarations: Array; + kind: 'var' | 'let' | 'const'; +} + +export interface VariableDeclarator extends Node { + type: 'VariableDeclarator'; + id: Pattern; + init?: Expression | null; +} + +export interface ThisExpression extends Node { + type: 'ThisExpression'; +} + +export interface ArrayExpression extends Node { + type: 'ArrayExpression'; + elements: Array; +} + +export interface ObjectExpression extends Node { + type: 'ObjectExpression'; + properties: Array; +} + +export interface Property extends Node { + type: 'Property'; + key: Expression; + value: Expression; + kind: 'init' | 'get' | 'set'; + method: boolean; + shorthand: boolean; + computed: boolean; +} + +export interface FunctionExpression extends Function { + type: 'FunctionExpression'; + body: BlockStatement; +} + +export interface UnaryExpression extends Node { + type: 'UnaryExpression'; + operator: UnaryOperator; + prefix: boolean; + argument: Expression; +} + +export type UnaryOperator = '-' | '+' | '!' | '~' | 'typeof' | 'void' | 'delete'; + +export interface UpdateExpression extends Node { + type: 'UpdateExpression'; + operator: UpdateOperator; + argument: Expression; + prefix: boolean; +} + +export type UpdateOperator = '++' | '--'; + +export interface BinaryExpression extends Node { + type: 'BinaryExpression'; + operator: BinaryOperator; + left: Expression | PrivateIdentifier; + right: Expression; +} + +export type BinaryOperator = + | '==' + | '!=' + | '===' + | '!==' + | '<' + | '<=' + | '>' + | '>=' + | '<<' + | '>>' + | '>>>' + | '+' + | '-' + | '*' + | '/' + | '%' + | '|' + | '^' + | '&' + | 'in' + | 'instanceof' + | '**'; + +export interface AssignmentExpression extends Node { + type: 'AssignmentExpression'; + operator: AssignmentOperator; + left: Pattern; + right: Expression; +} + +export type AssignmentOperator = '=' | '+=' | '-=' | '*=' | '/=' | '%=' | '<<=' | '>>=' | '>>>=' | '|=' | '^=' | '&=' | '**=' | '||=' | '&&=' | '??='; + +export interface LogicalExpression extends Node { + type: 'LogicalExpression'; + operator: LogicalOperator; + left: Expression; + right: Expression; +} + +export type LogicalOperator = '||' | '&&' | '??'; + +export interface MemberExpression extends Node { + type: 'MemberExpression'; + object: Expression | Super; + property: Expression | PrivateIdentifier; + computed: boolean; + optional: boolean; +} + +export interface ConditionalExpression extends Node { + type: 'ConditionalExpression'; + test: Expression; + alternate: Expression; + consequent: Expression; +} + +export interface CallExpression extends Node { + type: 'CallExpression'; + callee: Expression | Super; + arguments: Array; + optional: boolean; +} + +export interface NewExpression extends Node { + type: 'NewExpression'; + callee: Expression; + arguments: Array; +} + +export interface SequenceExpression extends Node { + type: 'SequenceExpression'; + expressions: Array; +} + +export interface ForOfStatement extends Node { + type: 'ForOfStatement'; + left: VariableDeclaration | Pattern; + right: Expression; + body: Statement; + await: boolean; +} + +export interface Super extends Node { + type: 'Super'; +} + +export interface SpreadElement extends Node { + type: 'SpreadElement'; + argument: Expression; +} + +export interface ArrowFunctionExpression extends Function { + type: 'ArrowFunctionExpression'; +} + +export interface YieldExpression extends Node { + type: 'YieldExpression'; + argument?: Expression | null; + delegate: boolean; +} + +export interface TemplateLiteral extends Node { + type: 'TemplateLiteral'; + quasis: Array; + expressions: Array; +} + +export interface TaggedTemplateExpression extends Node { + type: 'TaggedTemplateExpression'; + tag: Expression; + quasi: TemplateLiteral; +} + +export interface TemplateElement extends Node { + type: 'TemplateElement'; + tail: boolean; + value: { + cooked?: string | null; + raw: string; + }; +} + +export interface AssignmentProperty extends Node { + type: 'Property'; + key: Expression; + value: Pattern; + kind: 'init'; + method: false; + shorthand: boolean; + computed: boolean; +} + +export interface ObjectPattern extends Node { + type: 'ObjectPattern'; + properties: Array; +} + +export interface ArrayPattern extends Node { + type: 'ArrayPattern'; + elements: Array; +} + +export interface RestElement extends Node { + type: 'RestElement'; + argument: Pattern; +} + +export interface AssignmentPattern extends Node { + type: 'AssignmentPattern'; + left: Pattern; + right: Expression; +} + +export interface Class extends Node { + id?: Identifier | null; + superClass?: Expression | null; + body: ClassBody; +} + +export interface ClassBody extends Node { + type: 'ClassBody'; + body: Array; +} + +export interface MethodDefinition extends Node { + type: 'MethodDefinition'; + key: Expression | PrivateIdentifier; + value: FunctionExpression; + kind: 'constructor' | 'method' | 'get' | 'set'; + computed: boolean; + static: boolean; +} + +export interface ClassDeclaration extends Class { + type: 'ClassDeclaration'; + id: Identifier; +} + +export interface ClassExpression extends Class { + type: 'ClassExpression'; +} + +export interface MetaProperty extends Node { + type: 'MetaProperty'; + meta: Identifier; + property: Identifier; +} + +export interface ImportDeclaration extends Node { + type: 'ImportDeclaration'; + specifiers: Array; + source: Literal; +} + +export interface ImportSpecifier extends Node { + type: 'ImportSpecifier'; + imported: Identifier | Literal; + local: Identifier; +} + +export interface ImportDefaultSpecifier extends Node { + type: 'ImportDefaultSpecifier'; + local: Identifier; +} + +export interface ImportNamespaceSpecifier extends Node { + type: 'ImportNamespaceSpecifier'; + local: Identifier; +} + +export interface ExportNamedDeclaration extends Node { + type: 'ExportNamedDeclaration'; + declaration?: Declaration | null; + specifiers: Array; + source?: Literal | null; +} + +export interface ExportSpecifier extends Node { + type: 'ExportSpecifier'; + exported: Identifier | Literal; + local: Identifier | Literal; +} + +export interface AnonymousFunctionDeclaration extends Function { + type: 'FunctionDeclaration'; + id: null; + body: BlockStatement; +} + +export interface AnonymousClassDeclaration extends Class { + type: 'ClassDeclaration'; + id: null; +} + +export interface ExportDefaultDeclaration extends Node { + type: 'ExportDefaultDeclaration'; + declaration: AnonymousFunctionDeclaration | FunctionDeclaration | AnonymousClassDeclaration | ClassDeclaration | Expression; +} + +export interface ExportAllDeclaration extends Node { + type: 'ExportAllDeclaration'; + source: Literal; + exported?: Identifier | Literal | null; +} + +export interface AwaitExpression extends Node { + type: 'AwaitExpression'; + argument: Expression; +} + +export interface ChainExpression extends Node { + type: 'ChainExpression'; + expression: MemberExpression | CallExpression; +} + +export interface ImportExpression extends Node { + type: 'ImportExpression'; + source: Expression; +} + +export interface ParenthesizedExpression extends Node { + type: 'ParenthesizedExpression'; + expression: Expression; +} + +export interface PropertyDefinition extends Node { + type: 'PropertyDefinition'; + key: Expression | PrivateIdentifier; + value?: Expression | null; + computed: boolean; + static: boolean; +} + +export interface PrivateIdentifier extends Node { + type: 'PrivateIdentifier'; + name: string; +} + +export interface StaticBlock extends Node { + type: 'StaticBlock'; + body: Array; +} + +export type Statement = + | ExpressionStatement + | BlockStatement + | EmptyStatement + | DebuggerStatement + | WithStatement + | ReturnStatement + | LabeledStatement + | BreakStatement + | ContinueStatement + | IfStatement + | SwitchStatement + | ThrowStatement + | TryStatement + | WhileStatement + | DoWhileStatement + | ForStatement + | ForInStatement + | ForOfStatement + | Declaration; + +export type Declaration = + | FunctionDeclaration + | VariableDeclaration + | ClassDeclaration; + +export type Expression = + | Identifier + | Literal + | ThisExpression + | ArrayExpression + | ObjectExpression + | FunctionExpression + | UnaryExpression + | UpdateExpression + | BinaryExpression + | AssignmentExpression + | LogicalExpression + | MemberExpression + | ConditionalExpression + | CallExpression + | NewExpression + | SequenceExpression + | ArrowFunctionExpression + | YieldExpression + | TemplateLiteral + | TaggedTemplateExpression + | ClassExpression + | MetaProperty + | AwaitExpression + | ChainExpression + | ImportExpression + | ParenthesizedExpression; + +export type Pattern = + | Identifier + | MemberExpression + | ObjectPattern + | ArrayPattern + | RestElement + | AssignmentPattern; + +export type ModuleDeclaration = + | ImportDeclaration + | ExportNamedDeclaration + | ExportDefaultDeclaration + | ExportAllDeclaration; + +export type AnyNode = + | Statement + | Expression + | Declaration + | ModuleDeclaration + | Literal + | Program + | SwitchCase + | CatchClause + | Property + | Super + | SpreadElement + | TemplateElement + | AssignmentProperty + | ObjectPattern + | ArrayPattern + | RestElement + | AssignmentPattern + | ClassBody + | MethodDefinition + | MetaProperty + | ImportSpecifier + | ImportDefaultSpecifier + | ImportNamespaceSpecifier + | ExportSpecifier + | AnonymousFunctionDeclaration + | AnonymousClassDeclaration + | PropertyDefinition + | PrivateIdentifier + | StaticBlock + | VariableDeclaration + | VariableDeclarator; + +export function parse(input: string, options: Options): Program; + +export function parseExpressionAt(input: string, pos: number, options: Options): Expression; + +export function tokenizer(input: string, options: Options): { + getToken(): Token; + [Symbol.iterator](): Iterator; +}; + +export type ecmaVersion = 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 'latest'; + +export interface Options { + /** + * `ecmaVersion` indicates the ECMAScript version to parse. Must be + * either 3, 5, 6 (or 2015), 7 (2016), 8 (2017), 9 (2018), 10 + * (2019), 11 (2020), 12 (2021), 13 (2022), 14 (2023), or `"latest"` + * (the latest version the library supports). This influences + * support for strict mode, the set of reserved words, and support + * for new syntax features. + */ + ecmaVersion: ecmaVersion; + + /** + * `sourceType` indicates the mode the code should be parsed in. + * Can be either `"script"` or `"module"`. This influences global + * strict mode and parsing of `import` and `export` declarations. + */ + sourceType?: 'script' | 'module'; + + /** + * a callback that will be called when a semicolon is automatically inserted. + * @param lastTokEnd the position of the comma as an offset + * @param lastTokEndLoc location if {@link locations} is enabled + */ + onInsertedSemicolon?: (lastTokEnd: number, lastTokEndLoc?: Position) => void; + + /** + * similar to `onInsertedSemicolon`, but for trailing commas + * @param lastTokEnd the position of the comma as an offset + * @param lastTokEndLoc location if `locations` is enabled + */ + onTrailingComma?: (lastTokEnd: number, lastTokEndLoc?: Position) => void; + + /** + * By default, reserved words are only enforced if ecmaVersion >= 5. + * Set `allowReserved` to a boolean value to explicitly turn this on + * an off. When this option has the value "never", reserved words + * and keywords can also not be used as property names. + */ + allowReserved?: boolean | 'never'; + + /** + * When enabled, a return at the top level is not considered an error. + */ + allowReturnOutsideFunction?: boolean; + + /** + * When enabled, import/export statements are not constrained to + * appearing at the top of the program, and an import.meta expression + * in a script isn't considered an error. + */ + allowImportExportEverywhere?: boolean; + + /** + * By default, `await` identifiers are allowed to appear at the top-level scope only if {@link ecmaVersion} >= 2022. + * When enabled, await identifiers are allowed to appear at the top-level scope, + * but they are still not allowed in non-async functions. + */ + allowAwaitOutsideFunction?: boolean; + + /** + * When enabled, super identifiers are not constrained to + * appearing in methods and do not raise an error when they appear elsewhere. + */ + allowSuperOutsideMethod?: boolean; + + /** + * When enabled, hashbang directive in the beginning of file is + * allowed and treated as a line comment. Enabled by default when + * {@link ecmaVersion} >= 2023. + */ + allowHashBang?: boolean; + + /** + * By default, the parser will verify that private properties are + * only used in places where they are valid and have been declared. + * Set this to false to turn such checks off. + */ + checkPrivateFields?: boolean; + + /** + * When `locations` is on, `loc` properties holding objects with + * `start` and `end` properties as {@link Position} objects will be attached to the + * nodes. + */ + locations?: boolean; + + /** + * a callback that will cause Acorn to call that export function with object in the same + * format as tokens returned from `tokenizer().getToken()`. Note + * that you are not allowed to call the parser from the + * callback—that will corrupt its internal state. + */ + onToken?: ((token: Token) => void) | Token[]; + + /** + * This takes a export function or an array. + * + * When a export function is passed, Acorn will call that export function with `(block, text, start, + * end)` parameters whenever a comment is skipped. `block` is a + * boolean indicating whether this is a block (`/* *\/`) comment, + * `text` is the content of the comment, and `start` and `end` are + * character offsets that denote the start and end of the comment. + * When the {@link locations} option is on, two more parameters are + * passed, the full locations of {@link Position} export type of the start and + * end of the comments. + * + * When a array is passed, each found comment of {@link Comment} export type is pushed to the array. + * + * Note that you are not allowed to call the + * parser from the callback—that will corrupt its internal state. + */ + onComment?: + | (( + isBlock: boolean, + text: string, + start: number, + end: number, + startLoc?: Position, + endLoc?: Position, + ) => void) + | Comment[]; + + /** + * Nodes have their start and end characters offsets recorded in + * `start` and `end` properties (directly on the node, rather than + * the `loc` object, which holds line/column data. To also add a + * [semi-standardized][range] `range` property holding a `[start, + * end]` array with the same numbers, set the `ranges` option to + * `true`. + */ + ranges?: boolean; + + /** + * It is possible to parse multiple files into a single AST by + * passing the tree produced by parsing the first file as + * `program` option in subsequent parses. This will add the + * toplevel forms of the parsed file to the `Program` (top) node + * of an existing parse tree. + */ + program?: Node; + + /** + * When {@link locations} is on, you can pass this to record the source + * file in every node's `loc` object. + */ + sourceFile?: string; + + /** + * This value, if given, is stored in every node, whether {@link locations} is on or off. + */ + directSourceFile?: string; + + /** + * When enabled, parenthesized expressions are represented by + * (non-standard) ParenthesizedExpression nodes + */ + preserveParens?: boolean; +} + +export class Parser { + options: Options; + input: string; + + constructor(options: Options, input: string, startPos?: number); + parse(): Program; + + static parse(input: string, options: Options): Program; + static parseExpressionAt(input: string, pos: number, options: Options): Expression; + static tokenizer(input: string, options: Options): { + getToken(): Token; + [Symbol.iterator](): Iterator; + }; + static extend(...plugins: ((BaseParser: typeof Parser) => typeof Parser)[]): typeof Parser; +} + +export const defaultOptions: Options; + +export function getLineInfo(input: string, offset: number): Position; + +export class TokenType { + label: string; + keyword: string | undefined; +} + +export const tokTypes: { + num: TokenType; + regexp: TokenType; + string: TokenType; + name: TokenType; + privateId: TokenType; + eof: TokenType; + + bracketL: TokenType; + bracketR: TokenType; + braceL: TokenType; + braceR: TokenType; + parenL: TokenType; + parenR: TokenType; + comma: TokenType; + semi: TokenType; + colon: TokenType; + dot: TokenType; + question: TokenType; + questionDot: TokenType; + arrow: TokenType; + template: TokenType; + invalidTemplate: TokenType; + ellipsis: TokenType; + backQuote: TokenType; + dollarBraceL: TokenType; + + eq: TokenType; + assign: TokenType; + incDec: TokenType; + prefix: TokenType; + logicalOR: TokenType; + logicalAND: TokenType; + bitwiseOR: TokenType; + bitwiseXOR: TokenType; + bitwiseAND: TokenType; + equality: TokenType; + relational: TokenType; + bitShift: TokenType; + plusMin: TokenType; + modulo: TokenType; + star: TokenType; + slash: TokenType; + starstar: TokenType; + coalesce: TokenType; + + _break: TokenType; + _case: TokenType; + _catch: TokenType; + _continue: TokenType; + _debugger: TokenType; + _default: TokenType; + _do: TokenType; + _else: TokenType; + _finally: TokenType; + _for: TokenType; + _function: TokenType; + _if: TokenType; + _return: TokenType; + _switch: TokenType; + _throw: TokenType; + _try: TokenType; + _var: TokenType; + _const: TokenType; + _while: TokenType; + _with: TokenType; + _new: TokenType; + _this: TokenType; + _super: TokenType; + _class: TokenType; + _extends: TokenType; + _export: TokenType; + _import: TokenType; + _null: TokenType; + _true: TokenType; + _false: TokenType; + _in: TokenType; + _instanceof: TokenType; + _typeof: TokenType; + _void: TokenType; + _delete: TokenType; +}; + +export interface Comment { + type: 'Line' | 'Block'; + value: string; + start: number; + end: number; + loc?: SourceLocation; + range?: [number, number]; +} + +export class Token { + type: TokenType; + start: number; + end: number; + loc?: SourceLocation; + range?: [number, number]; +} + +export const version: string; diff --git a/packages/apps/deno-runtime/deno.jsonc b/packages/apps/deno-runtime/deno.jsonc new file mode 100644 index 0000000000000..4fa3142b99261 --- /dev/null +++ b/packages/apps/deno-runtime/deno.jsonc @@ -0,0 +1,26 @@ +{ + "imports": { + "@msgpack/msgpack": "npm:@msgpack/msgpack@3.0.0-beta2", + "@rocket.chat/apps-engine/": "./../src/", + "@rocket.chat/ui-kit": "npm:@rocket.chat/ui-kit@^0.31.22", + "@std/cli": "jsr:@std/cli@^1.0.9", + "@std/io": "jsr:@std/io@^0.225.3", + "@std/streams": "jsr:@std/streams@^1.0.16", + "acorn": "npm:acorn@8.10.0", + "acorn-walk": "npm:acorn-walk@8.2.0", + "astring": "npm:astring@1.8.6", + "jsonrpc-lite": "npm:jsonrpc-lite@2.2.0", + "stack-trace": "npm:stack-trace@0.0.10", + "uuid": "npm:uuid@8.3.2" + }, + "unstable": ["detect-cjs"], + "tasks": { + "test": "deno test --no-check --allow-read=../../../,/tmp --allow-write=/tmp" + }, + "fmt": { + "lineWidth": 160, + "useTabs": true, + "indentWidth": 4, + "singleQuote": true + } +} diff --git a/packages/apps/deno-runtime/deno.lock b/packages/apps/deno-runtime/deno.lock new file mode 100644 index 0000000000000..bc87cb5e87d84 --- /dev/null +++ b/packages/apps/deno-runtime/deno.lock @@ -0,0 +1,128 @@ +{ + "version": "5", + "specifiers": { + "jsr:@std/bytes@^1.0.6": "1.0.6", + "jsr:@std/cli@^1.0.9": "1.0.13", + "jsr:@std/io@~0.225.3": "0.225.3", + "jsr:@std/streams@^1.0.16": "1.0.16", + "npm:@msgpack/msgpack@3.0.0-beta2": "3.0.0-beta2", + "npm:@rocket.chat/ui-kit@~0.31.22": "0.31.25_@rocket.chat+icons@0.32.0", + "npm:acorn-walk@8.2.0": "8.2.0", + "npm:acorn@8.10.0": "8.10.0", + "npm:astring@1.8.6": "1.8.6", + "npm:jsonrpc-lite@2.2.0": "2.2.0", + "npm:stack-trace@*": "0.0.10", + "npm:stack-trace@0.0.10": "0.0.10", + "npm:uuid@8.3.2": "8.3.2" + }, + "jsr": { + "@std/bytes@1.0.6": { + "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" + }, + "@std/cli@1.0.13": { + "integrity": "5db2d95ab2dca3bca9fb6ad3c19908c314e93d6391c8b026725e4892d4615a69" + }, + "@std/io@0.225.3": { + "integrity": "27b07b591384d12d7b568f39e61dff966b8230559122df1e9fd11cc068f7ddd1", + "dependencies": [ + "jsr:@std/bytes" + ] + }, + "@std/streams@1.0.16": { + "integrity": "85030627befb1767c60d4f65cb30fa2f94af1d6ee6e5b2515b76157a542e89c4", + "dependencies": [ + "jsr:@std/bytes" + ] + } + }, + "npm": { + "@msgpack/msgpack@3.0.0-beta2": { + "integrity": "sha512-y+l1PNV0XDyY8sM3YtuMLK5vE3/hkfId+Do8pLo/OPxfxuFAUwcGz3oiiUuV46/aBpwTzZ+mRWVMtlSKbradhw==" + }, + "@rocket.chat/icons@0.32.0": { + "integrity": "sha512-7yhhELKNLb9kUtXCvau0V+iMXraV2bOsxcPjc/ZtLR5VeeIDTeaflqRWGtLroX6f3bE+J1n5qB5zi8A4YXuH2g==" + }, + "@rocket.chat/ui-kit@0.31.25_@rocket.chat+icons@0.32.0": { + "integrity": "sha512-yTgTKDw9SMlJ6p8n0PDO6zSvox/nHYUrwCIvILQeAK6PvTrgSe/u9CvU7ATTYjnQiQ603yEGR6dxjF4euCGdNA==", + "dependencies": [ + "@rocket.chat/icons" + ] + }, + "acorn-walk@8.2.0": { + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" + }, + "acorn@8.10.0": { + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "bin": true + }, + "astring@1.8.6": { + "integrity": "sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==", + "bin": true + }, + "jsonrpc-lite@2.2.0": { + "integrity": "sha512-/cbbSxtZWs1O7R4tWqabrCM/t3N8qKUZMAg9IUqpPvUs6UyRvm6pCNYkskyKN/XU0UgffW+NY2ZRr8t0AknX7g==" + }, + "stack-trace@0.0.10": { + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==" + }, + "uuid@8.3.2": { + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": true + } + }, + "remote": { + "https://deno.land/std@0.203.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9", + "https://deno.land/std@0.203.0/assert/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea", + "https://deno.land/std@0.203.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", + "https://deno.land/std@0.203.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", + "https://deno.land/std@0.203.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c", + "https://deno.land/std@0.203.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9", + "https://deno.land/std@0.203.0/assert/assert_equals.ts": "d8ec8a22447fbaf2fc9d7c3ed2e66790fdb74beae3e482855d75782218d68227", + "https://deno.land/std@0.203.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7", + "https://deno.land/std@0.203.0/assert/assert_false.ts": "0ccbcaae910f52c857192ff16ea08bda40fdc79de80846c206bfc061e8c851c6", + "https://deno.land/std@0.203.0/assert/assert_greater.ts": "ae2158a2d19313bf675bf7251d31c6dc52973edb12ac64ac8fc7064152af3e63", + "https://deno.land/std@0.203.0/assert/assert_greater_or_equal.ts": "1439da5ebbe20855446cac50097ac78b9742abe8e9a43e7de1ce1426d556e89c", + "https://deno.land/std@0.203.0/assert/assert_instance_of.ts": "3aedb3d8186e120812d2b3a5dea66a6e42bf8c57a8bd927645770bd21eea554c", + "https://deno.land/std@0.203.0/assert/assert_is_error.ts": "c21113094a51a296ffaf036767d616a78a2ae5f9f7bbd464cd0197476498b94b", + "https://deno.land/std@0.203.0/assert/assert_less.ts": "aec695db57db42ec3e2b62e97e1e93db0063f5a6ec133326cc290ff4b71b47e4", + "https://deno.land/std@0.203.0/assert/assert_less_or_equal.ts": "5fa8b6a3ffa20fd0a05032fe7257bf985d207b85685fdbcd23651b70f928c848", + "https://deno.land/std@0.203.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b", + "https://deno.land/std@0.203.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754", + "https://deno.land/std@0.203.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22", + "https://deno.land/std@0.203.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0", + "https://deno.land/std@0.203.0/assert/assert_not_strict_equals.ts": "ca6c6d645e95fbc873d25320efeb8c4c6089a9a5e09f92d7c1c4b6e935c2a6ad", + "https://deno.land/std@0.203.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54", + "https://deno.land/std@0.203.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057", + "https://deno.land/std@0.203.0/assert/assert_strict_equals.ts": "b1f538a7ea5f8348aeca261d4f9ca603127c665e0f2bbfeb91fa272787c87265", + "https://deno.land/std@0.203.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c", + "https://deno.land/std@0.203.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd", + "https://deno.land/std@0.203.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", + "https://deno.land/std@0.203.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece", + "https://deno.land/std@0.203.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278", + "https://deno.land/std@0.203.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085", + "https://deno.land/std@0.203.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a", + "https://deno.land/std@0.203.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536", + "https://deno.land/std@0.203.0/fmt/colors.ts": "c51c4642678eb690dcf5ffee5918b675bf01a33fba82acf303701ae1a4f8c8d9", + "https://deno.land/std@0.203.0/testing/_test_suite.ts": "30f018feeb3835f12ab198d8a518f9089b1bcb2e8c838a8b615ab10d5005465c", + "https://deno.land/std@0.203.0/testing/bdd.ts": "3f446df5ef8e856a869e8eec54c8482590415741ff0b6358a00c43486cc15769", + "https://deno.land/std@0.203.0/testing/mock.ts": "6576b4aa55ee20b1990d656a78fff83599e190948c00e9f25a7f3ac5e9d6492d", + "https://deno.land/std@0.216.0/io/types.ts": "748bbb3ac96abda03594ef5a0db15ce5450dcc6c0d841c8906f8b10ac8d32c96", + "https://deno.land/std@0.216.0/io/write_all.ts": "24aac2312bb21096ae3ae0b102b22c26164d3249dff96dbac130958aa736f038", + "https://jsr.io/@std/cli/1.0.9/parse_args.ts": "29ac18602d8836d2723cab1d90111ff954acc369f184626a3f9f677e3185caef" + }, + "workspace": { + "dependencies": [ + "jsr:@std/cli@^1.0.9", + "jsr:@std/io@~0.225.3", + "jsr:@std/streams@^1.0.16", + "npm:@msgpack/msgpack@3.0.0-beta2", + "npm:@rocket.chat/ui-kit@~0.31.22", + "npm:acorn-walk@8.2.0", + "npm:acorn@8.10.0", + "npm:astring@1.8.6", + "npm:jsonrpc-lite@2.2.0", + "npm:stack-trace@0.0.10", + "npm:uuid@8.3.2" + ] + } +} diff --git a/packages/apps/deno-runtime/error-handlers.ts b/packages/apps/deno-runtime/error-handlers.ts new file mode 100644 index 0000000000000..e26a5ad6b2d86 --- /dev/null +++ b/packages/apps/deno-runtime/error-handlers.ts @@ -0,0 +1,33 @@ +import * as Messenger from './lib/messenger.ts'; + +export function unhandledRejectionListener(event: PromiseRejectionEvent) { + event.preventDefault(); + + const { type, reason } = event; + + Messenger.sendNotification({ + method: 'unhandledRejection', + params: [ + { + type, + reason: reason instanceof Error ? reason.message : reason, + timestamp: new Date(), + }, + ], + }); +} + +export function unhandledExceptionListener(event: ErrorEvent) { + event.preventDefault(); + + const { type, message, filename, lineno, colno } = event; + Messenger.sendNotification({ + method: 'uncaughtException', + params: [{ type, message, filename, lineno, colno }], + }); +} + +export default function registerErrorListeners() { + addEventListener('unhandledrejection', unhandledRejectionListener); + addEventListener('error', unhandledExceptionListener); +} diff --git a/packages/apps/deno-runtime/handlers/api-handler.ts b/packages/apps/deno-runtime/handlers/api-handler.ts new file mode 100644 index 0000000000000..1b88a92551ea7 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/api-handler.ts @@ -0,0 +1,50 @@ +import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api/IApiEndpoint.ts'; +import { Defined, JsonRpcError } from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; +import { RequestContext } from '../lib/requestContext.ts'; +import { wrapComposedApp } from '../lib/wrapAppForRequest.ts'; + +export default async function apiHandler(request: RequestContext): Promise { + const { method: call, params } = request; + const [/* always "api" */, ...parts] = call.split(':'); + const httpMethod = parts.pop(); + const path = parts.join(':'); + + const endpoint = AppObjectRegistry.get(`api:${path}`); + const { logger } = request.context; + + if (!endpoint) { + return new JsonRpcError(`Endpoint ${path} not found`, -32000); + } + + const method = endpoint[httpMethod as keyof IApiEndpoint]; + + if (typeof method !== 'function') { + return new JsonRpcError(`${path}'s ${httpMethod} not exists`, -32000); + } + + const [requestData, endpointInfo] = params as Array; + + logger.debug(`${path}'s ${call} is being executed...`, requestData); + + try { + // deno-lint-ignore ban-types + const result = await (method as Function).apply(wrapComposedApp(endpoint, request), [ + requestData, + endpointInfo, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getModifier(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + ]); + + logger.debug(`${path}'s ${call} was successfully executed.`); + + return result; + } catch (e) { + logger.debug(`${path}'s ${call} was unsuccessful.`); + return new JsonRpcError(e.message || 'Internal server error', -32000); + } +} diff --git a/packages/apps/deno-runtime/handlers/app/construct.ts b/packages/apps/deno-runtime/handlers/app/construct.ts new file mode 100644 index 0000000000000..b391088fee217 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/app/construct.ts @@ -0,0 +1,132 @@ +import { Socket } from 'node:net'; + +import type { IParseAppPackageResult } from '@rocket.chat/apps-engine/server/compiler/IParseAppPackageResult.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { require } from '../../lib/require.ts'; +import { sanitizeDeprecatedUsage } from '../../lib/sanitizeDeprecatedUsage.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; + +const ALLOWED_NATIVE_MODULES = ['path', 'url', 'crypto', 'buffer', 'stream', 'net', 'http', 'https', 'zlib', 'util', 'punycode', 'os', 'querystring', 'fs']; +const ALLOWED_EXTERNAL_MODULES = ['uuid']; + +function prepareEnvironment() { + // Deno does not behave equally to Node when it comes to piping content to a socket + // So we intervene here + const originalFinal = Socket.prototype._final; + // deno-lint-ignore no-explicit-any + Socket.prototype._final = function _final(cb: any) { + // Deno closes the readable stream in the Socket earlier than Node + // The exact reason for that is yet unknown, so we'll need to simply delay the execution + // which allows data to be read in a response + setTimeout(() => originalFinal.call(this, cb), 1); + }; +} + +// As the apps are bundled, the only times they will call require are +// 1. To require native modules +// 2. To require external npm packages we may provide +// 3. To require apps-engine files +function buildRequire(): (module: string) => unknown { + return (module: string): unknown => { + // Normalize Node built-in specifiers: accept both 'crypto' and 'node:crypto' + const normalized = module.replace('node:', ''); + + if (ALLOWED_NATIVE_MODULES.includes(normalized)) { + return require(`node:${normalized}`); + } + + if (ALLOWED_EXTERNAL_MODULES.includes(module)) { + return require(`npm:${module}`); + } + + if (module.startsWith('@rocket.chat/apps-engine')) { + // Our `require` function knows how to handle these + return require(module); + } + + throw new Error(`Module ${module} is not allowed`); + }; +} + +function wrapAppCode(code: string): (require: (module: string) => unknown) => Promise> { + return new Function( + 'require', + ` + const { Buffer } = require('buffer'); + const exports = {}; + const module = { exports }; + const _error = console.error.bind(console); + const _console = { + log: _error, + error: _error, + debug: _error, + info: _error, + warn: _error, + }; + + const result = (async (exports,module,require,Buffer,console,globalThis,Deno) => { + ${code}; + })(exports,module,require,Buffer,_console,undefined,undefined); + + return result.then(() => module.exports);`, + ) as (require: (module: string) => unknown) => Promise>; +} + +export default async function handleConstructApp(request: RequestContext): Promise { + const { params } = request; + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [appPackage] = params as [IParseAppPackageResult]; + + if (!appPackage?.info?.id || !appPackage?.info?.classFile || !appPackage?.files) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + prepareEnvironment(); + + AppObjectRegistry.set('id', appPackage.info.id); + const source = sanitizeDeprecatedUsage(appPackage.files[appPackage.info.classFile]); + + const require = buildRequire(); + const exports = await wrapAppCode(source)(require); + + // This is the same naive logic we've been using in the App Compiler + // Applying the correct type here is quite difficult because of the dynamic nature of the code + // deno-lint-ignore no-explicit-any + const appClass = Object.values(exports)[0] as any; + + const app = new appClass(appPackage.info, request.context.logger, AppAccessorsInstance.getDefaultAppAccessors()); + + if (typeof app.getName !== 'function') { + throw new Error('App must contain a getName function'); + } + + if (typeof app.getNameSlug !== 'function') { + throw new Error('App must contain a getNameSlug function'); + } + + if (typeof app.getVersion !== 'function') { + throw new Error('App must contain a getVersion function'); + } + + if (typeof app.getID !== 'function') { + throw new Error('App must contain a getID function'); + } + + if (typeof app.getDescription !== 'function') { + throw new Error('App must contain a getDescription function'); + } + + if (typeof app.getRequiredApiVersion !== 'function') { + throw new Error('App must contain a getRequiredApiVersion function'); + } + + AppObjectRegistry.set('app', app); + + return true; +} diff --git a/packages/apps/deno-runtime/handlers/app/handleGetStatus.ts b/packages/apps/deno-runtime/handlers/app/handleGetStatus.ts new file mode 100644 index 0000000000000..8bd454b98ca7f --- /dev/null +++ b/packages/apps/deno-runtime/handlers/app/handleGetStatus.ts @@ -0,0 +1,15 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; + +export default function handleGetStatus(): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.getStatus !== 'function') { + throw new Error('App must contain a getStatus function', { + cause: 'invalid_app', + }); + } + + return app.getStatus(); +} diff --git a/packages/apps/deno-runtime/handlers/app/handleInitialize.ts b/packages/apps/deno-runtime/handlers/app/handleInitialize.ts new file mode 100644 index 0000000000000..e8ee4ed1de136 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/app/handleInitialize.ts @@ -0,0 +1,24 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; + +export default async function handleInitialize(request: RequestContext): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.initialize !== 'function') { + throw new Error('App must contain an initialize function', { + cause: 'invalid_app', + }); + } + + await app.initialize.call( + wrapAppForRequest(app, request), + AppAccessorsInstance.getConfigurationExtend(), + AppAccessorsInstance.getEnvironmentRead() + ); + + return true; +} diff --git a/packages/apps/deno-runtime/handlers/app/handleOnDisable.ts b/packages/apps/deno-runtime/handlers/app/handleOnDisable.ts new file mode 100644 index 0000000000000..ffac456cd9bc9 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/app/handleOnDisable.ts @@ -0,0 +1,20 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; + +export default async function handleOnDisable(request: RequestContext): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onDisable !== 'function') { + throw new Error('App must contain an onDisable function', { + cause: 'invalid_app', + }); + } + + await app.onDisable.call(wrapAppForRequest(app, request), AppAccessorsInstance.getConfigurationModify()); + + return true; +} diff --git a/packages/apps/deno-runtime/handlers/app/handleOnEnable.ts b/packages/apps/deno-runtime/handlers/app/handleOnEnable.ts new file mode 100644 index 0000000000000..34c1d49b0f367 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/app/handleOnEnable.ts @@ -0,0 +1,22 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; + +export default function handleOnEnable(request: RequestContext): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onEnable !== 'function') { + throw new Error('App must contain an onEnable function', { + cause: 'invalid_app', + }); + } + + return app.onEnable.call( + wrapAppForRequest(app, request), + AppAccessorsInstance.getEnvironmentRead(), + AppAccessorsInstance.getConfigurationModify() + ); +} diff --git a/packages/apps/deno-runtime/handlers/app/handleOnInstall.ts b/packages/apps/deno-runtime/handlers/app/handleOnInstall.ts new file mode 100644 index 0000000000000..d6e4ada5cf6f0 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/app/handleOnInstall.ts @@ -0,0 +1,34 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; + +export default async function handleOnInstall(request: RequestContext): Promise { + const { params } = request; + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onInstall !== 'function') { + throw new Error('App must contain an onInstall function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [context] = params as [Record]; + + await app.onInstall.call( + wrapAppForRequest(app, request), + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + AppAccessorsInstance.getModifier(), + ); + + return true; +} diff --git a/packages/apps/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts b/packages/apps/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts new file mode 100644 index 0000000000000..601e1429be025 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts @@ -0,0 +1,31 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; + +export default function handleOnPreSettingUpdate(request: RequestContext): Promise { + const { params } = request; + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onPreSettingUpdate !== 'function') { + throw new Error('App must contain an onPreSettingUpdate function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [setting] = params as [Record]; + + return app.onPreSettingUpdate.call( + wrapAppForRequest(app, request), + setting, + AppAccessorsInstance.getConfigurationModify(), + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + ); +} diff --git a/packages/apps/deno-runtime/handlers/app/handleOnSettingUpdated.ts b/packages/apps/deno-runtime/handlers/app/handleOnSettingUpdated.ts new file mode 100644 index 0000000000000..e78ece63dda92 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/app/handleOnSettingUpdated.ts @@ -0,0 +1,33 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; + +export default async function handleOnSettingUpdated(request: RequestContext): Promise { + const { params } = request; + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onSettingUpdated !== 'function') { + throw new Error('App must contain an onSettingUpdated function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [setting] = params as [Record]; + + await app.onSettingUpdated.call( + wrapAppForRequest(app, request), + setting, + AppAccessorsInstance.getConfigurationModify(), + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + ); + + return true; +} diff --git a/packages/apps/deno-runtime/handlers/app/handleOnUninstall.ts b/packages/apps/deno-runtime/handlers/app/handleOnUninstall.ts new file mode 100644 index 0000000000000..34b02c2b45f1f --- /dev/null +++ b/packages/apps/deno-runtime/handlers/app/handleOnUninstall.ts @@ -0,0 +1,34 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; + +export default async function handleOnUninstall(request: RequestContext): Promise { + const { params } = request; + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onUninstall !== 'function') { + throw new Error('App must contain an onUninstall function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [context] = params as [Record]; + + await app.onUninstall.call( + wrapAppForRequest(app, request), + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + AppAccessorsInstance.getModifier(), + ); + + return true; +} diff --git a/packages/apps/deno-runtime/handlers/app/handleOnUpdate.ts b/packages/apps/deno-runtime/handlers/app/handleOnUpdate.ts new file mode 100644 index 0000000000000..0eb8643c928f4 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/app/handleOnUpdate.ts @@ -0,0 +1,34 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; + +export default async function handleOnUpdate(request: RequestContext): Promise { + const { params } = request; + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onUpdate !== 'function') { + throw new Error('App must contain an onUpdate function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [context] = params as [Record]; + + await app.onUpdate.call( + wrapAppForRequest(app, request), + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + AppAccessorsInstance.getModifier(), + ); + + return true; +} diff --git a/packages/apps/deno-runtime/handlers/app/handleSetStatus.ts b/packages/apps/deno-runtime/handlers/app/handleSetStatus.ts new file mode 100644 index 0000000000000..163fa3684ae67 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/app/handleSetStatus.ts @@ -0,0 +1,33 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; +import type { AppStatus as _AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { require } from '../../lib/require.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; + +const { AppStatus } = require('@rocket.chat/apps-engine/definition/AppStatus.js') as { + AppStatus: typeof _AppStatus; +}; + +export default async function handleSetStatus(request: RequestContext): Promise { + const { params } = request; + + if (!Array.isArray(params) || !Object.values(AppStatus).includes(params[0])) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [status] = params as [typeof AppStatus]; + + const app = AppObjectRegistry.get('app'); + + if (!app || typeof app['setStatus'] !== 'function') { + throw new Error('App must contain a setStatus function', { + cause: 'invalid_app', + }); + } + + await app['setStatus'].call(wrapAppForRequest(app, request), status); + + return null; +} diff --git a/packages/apps/deno-runtime/handlers/app/handleUploadEvents.ts b/packages/apps/deno-runtime/handlers/app/handleUploadEvents.ts new file mode 100644 index 0000000000000..72d58801d537c --- /dev/null +++ b/packages/apps/deno-runtime/handlers/app/handleUploadEvents.ts @@ -0,0 +1,78 @@ +import { Buffer } from 'node:buffer'; + +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; +import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions/AppsEngineException.ts'; +import type { IFileUploadContext } from '@rocket.chat/apps-engine/definition/uploads/IFileUploadContext.ts' +import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails.ts' +import { toArrayBuffer } from '@std/streams'; +import { Defined, JsonRpcError } from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { assertAppAvailable, assertHandlerFunction, isPlainObject } from '../lib/assertions.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; + +export const uploadEvents = ['executePreFileUpload'] as const; + +function assertIsUpload(v: unknown): asserts v is IUploadDetails { + if (isPlainObject(v) && !!v.rid && (!!v.userId || !!v.visitorToken)) return; + + throw JsonRpcError.invalidParams({ err: `Invalid 'file' parameter. Expected IUploadDetails, got`, value: v }); +} + +function assertString(v: unknown): asserts v is string { + if (v && typeof v === 'string') return; + + throw JsonRpcError.invalidParams({ err: `Invalid 'path' parameter. Expected string, got`, value: v }); +} + +export default async function handleUploadEvents(request: RequestContext): Promise { + const { method: rawMethod, params } = request as { method: `app:${typeof uploadEvents[number]}`; params: [{ file?: IUploadDetails, path?: string }]}; + const [, method] = rawMethod.split(':') as ['app', typeof uploadEvents[number]]; + + try { + const [{ file, path }] = params; + + const app = AppObjectRegistry.get('app'); + const handlerFunction = app?.[method as keyof App] as unknown; + + assertAppAvailable(app); + assertHandlerFunction(handlerFunction); + assertIsUpload(file); + assertString(path); + + using tempFile = await Deno.open(path, { read: true, create: false }); + let context: IFileUploadContext; + + switch (method) { + case 'executePreFileUpload': { + const fileContents = await toArrayBuffer(tempFile.readable); + context = { file, content: Buffer.from(fileContents) }; + break; + } + } + + return await handlerFunction.call( + wrapAppForRequest(app, request), + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + AppAccessorsInstance.getModifier(), + ); + } catch(e) { + if (e?.name === AppsEngineException.name) { + return new JsonRpcError(e.message, AppsEngineException.JSONRPC_ERROR_CODE, { name: e.name }); + } + + if (e instanceof JsonRpcError) { + return e; + } + + return JsonRpcError.internalError({ + err: e.message, + ...(e.code && { code: e.code }), + }); + } +} diff --git a/packages/apps/deno-runtime/handlers/app/handler.ts b/packages/apps/deno-runtime/handlers/app/handler.ts new file mode 100644 index 0000000000000..e0e8085813347 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/app/handler.ts @@ -0,0 +1,115 @@ +import { Defined, JsonRpcError } from 'jsonrpc-lite'; + +import handleConstructApp from './construct.ts'; +import handleInitialize from './handleInitialize.ts'; +import handleGetStatus from './handleGetStatus.ts'; +import handleSetStatus from './handleSetStatus.ts'; +import handleOnEnable from './handleOnEnable.ts'; +import handleOnInstall from './handleOnInstall.ts'; +import handleOnDisable from './handleOnDisable.ts'; +import handleOnUninstall from './handleOnUninstall.ts'; +import handleOnPreSettingUpdate from './handleOnPreSettingUpdate.ts'; +import handleOnSettingUpdated from './handleOnSettingUpdated.ts'; +import handleOnUpdate from './handleOnUpdate.ts'; +import handleUploadEvents, { uploadEvents } from './handleUploadEvents.ts'; +import { isOneOf } from '../lib/assertions.ts'; +import handleListener from '../listener/handler.ts'; +import handleUIKitInteraction, { uikitInteractions } from '../uikit/handler.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; + +export default async function handleApp(request: RequestContext): Promise { + const { method } = request; + const { logger } = request.context; + const [, appMethod] = method.split(':'); + + try { + // We don't want the getStatus method to generate logs, so we handle it separately + if (appMethod === 'getStatus') { + return await handleGetStatus(); + } + + logger.debug({ msg: `A method is being called...`, appMethod }); + + const formatResult = (result: Defined | JsonRpcError): Defined | JsonRpcError => { + if (result instanceof JsonRpcError) { + logger.debug({ + msg: `'${appMethod}' was unsuccessful.`, + appMethod, + err: result, + errorMessage: result.message, + }); + } else { + logger.debug({ + msg: `'${appMethod}' was successfully called! The result is:`, + appMethod, + result, + }); + } + + return result; + }; + + let result: Promise | undefined = undefined; + + if (isOneOf(appMethod, uploadEvents)) { + result = handleUploadEvents(request); + } else if (isOneOf(appMethod, uikitInteractions)) { + result = handleUIKitInteraction(request); + } else if (appMethod.startsWith('check') || appMethod.startsWith('execute')) { + result = handleListener(request); + } + + switch (appMethod) { + case 'construct': + result = handleConstructApp(request); + break; + case 'initialize': + result = handleInitialize(request); + break; + case 'setStatus': + result = handleSetStatus(request); + break; + case 'onEnable': + result = handleOnEnable(request); + break; + case 'onDisable': + result = handleOnDisable(request); + break; + case 'onInstall': + result = handleOnInstall(request); + break; + case 'onUninstall': + result = handleOnUninstall(request); + break; + case 'onPreSettingUpdate': + result = handleOnPreSettingUpdate(request); + break; + case 'onSettingUpdated': + result = handleOnSettingUpdated(request); + break; + case 'onUpdate': + result = handleOnUpdate(request); + break; + } + + if (typeof result === 'undefined') { + throw new JsonRpcError(`Unknown method "${appMethod}"`, -32601); + } + + return await result.then(formatResult); + } catch (e: unknown) { + if (!(e instanceof Error)) { + return new JsonRpcError('Unknown error', -32000, e); + } + + if ((e.cause as string)?.includes('invalid_param_type')) { + return JsonRpcError.invalidParams(null); + } + + if ((e.cause as string)?.includes('invalid_app')) { + return JsonRpcError.internalError({ message: 'App unavailable' }); + } + + return new JsonRpcError(e.message, -32000, e); + } +} diff --git a/packages/apps/deno-runtime/handlers/lib/assertions.ts b/packages/apps/deno-runtime/handlers/lib/assertions.ts new file mode 100644 index 0000000000000..d6dcd0a4c9651 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/lib/assertions.ts @@ -0,0 +1,51 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; +import { JsonRpcError } from 'jsonrpc-lite'; + +/** + * Known failures that can happen in the runtime. + * + * DRT = Deno RunTime + */ +export const Errors = { + DRT_APP_NOT_AVAILABLE: 'DRT_APP_NOT_AVAILABLE', + DRT_EVENT_HANDLER_FUNCTION_MISSING: 'DRT_EVENT_HANDLER_FUNCTION_MISSING', +} + +export function isRecord(v: unknown): v is Record { + return !!v && typeof v === 'object' && !Array.isArray(v); +} + +export function isPlainObject(v: unknown): v is Record { + if (!isRecord(v)) { + return false; + } + + const prototype = Object.getPrototypeOf(v); + + return prototype === null || prototype.constructor === Object; +} + +/** + * Type guard function to check if a value is included in a readonly array + * and narrow its type accordingly. + */ +export function isOneOf(value: unknown, array: readonly T[]): value is T { + return array.includes(value as T); +} + +export function isApp(v: unknown): v is App { + return !!v && typeof (v as App)['extendConfiguration'] === 'function'; +} + +export function assertAppAvailable(v: unknown): asserts v is App { + if (isApp(v)) return; + + throw JsonRpcError.internalError({ err: 'App object not available', code: Errors.DRT_APP_NOT_AVAILABLE }); +} + +// deno-lint-ignore ban-types -- Function is the best we can do at this time +export function assertHandlerFunction(v: unknown): asserts v is Function { + if (v instanceof Function) return; + + throw JsonRpcError.internalError({ err: `Expected handler function, got ${v}`, code: Errors.DRT_EVENT_HANDLER_FUNCTION_MISSING }); +} diff --git a/packages/apps/deno-runtime/handlers/listener/handler.ts b/packages/apps/deno-runtime/handlers/listener/handler.ts new file mode 100644 index 0000000000000..88bc09b81403d --- /dev/null +++ b/packages/apps/deno-runtime/handlers/listener/handler.ts @@ -0,0 +1,153 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { AppsEngineException as _AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions/AppsEngineException.ts'; +import { Defined, JsonRpcError } from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { MessageExtender } from '../../lib/accessors/extenders/MessageExtender.ts'; +import { RoomExtender } from '../../lib/accessors/extenders/RoomExtender.ts'; +import { MessageBuilder } from '../../lib/accessors/builders/MessageBuilder.ts'; +import { RoomBuilder } from '../../lib/accessors/builders/RoomBuilder.ts'; +import { AppAccessors, AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { require } from '../../lib/require.ts'; +import createRoom from '../../lib/roomFactory.ts'; +import { Room } from '../../lib/room.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; + +const { AppsEngineException } = require('@rocket.chat/apps-engine/definition/exceptions/AppsEngineException.js') as { + AppsEngineException: typeof _AppsEngineException; +}; + +export default async function handleListener(request: RequestContext): Promise { + const { method, params } = request; + const [, evtInterface] = method.split(':'); + const app = AppObjectRegistry.get('app'); + + const eventExecutor = app?.[evtInterface as keyof App]; + + if (!app || typeof eventExecutor !== 'function') { + return JsonRpcError.methodNotFound({ + message: 'Invalid event interface called on app', + }); + } + + if (!Array.isArray(params) || params.length < 1 || params.length > 2) { + return JsonRpcError.invalidParams(null); + } + + try { + const args = parseArgs({ AppAccessorsInstance }, evtInterface, params); + return await (eventExecutor as (...args: unknown[]) => Promise).apply(wrapAppForRequest(app, request), args); + } catch (e) { + if (e instanceof JsonRpcError) { + return e; + } + + if (e instanceof AppsEngineException) { + return new JsonRpcError(e.message, AppsEngineException.JSONRPC_ERROR_CODE, { name: e.name }); + } + + return JsonRpcError.internalError({ message: e.message }); + } +} + +export function parseArgs(deps: { AppAccessorsInstance: AppAccessors }, evtMethod: string, params: unknown[]): unknown[] { + const { AppAccessorsInstance } = deps; + /** + * param1 is the context for the event handler execution + * param2 is an optional extra content that some hanlers require + */ + const [param1, param2] = params as [unknown, unknown]; + + if (!param1) { + throw JsonRpcError.invalidParams(null); + } + + let context = param1; + + if (evtMethod.includes('Message')) { + context = hydrateMessageObjects(context) as Record; + } else if (evtMethod.endsWith('RoomUserJoined') || evtMethod.endsWith('RoomUserLeave')) { + (context as Record).room = createRoom((context as Record).room as IRoom, AppAccessorsInstance.getSenderFn()); + } else if (evtMethod.includes('PreRoom')) { + context = createRoom(context as IRoom, AppAccessorsInstance.getSenderFn()); + } + + const args: unknown[] = [context, AppAccessorsInstance.getReader(), AppAccessorsInstance.getHttp()]; + + // "check" events will only go this far - (context, reader, http) + if (evtMethod.startsWith('check')) { + // "checkPostMessageDeleted" has an extra param - (context, reader, http, extraContext) + if (param2) { + args.push(hydrateMessageObjects(param2)); + } + + return args; + } + + // From this point on, all events will require (reader, http, persistence) injected + args.push(AppAccessorsInstance.getPersistence()); + + // "extend" events have an additional "Extender" param - (context, extender, reader, http, persistence) + if (evtMethod.endsWith('Extend')) { + if (evtMethod.includes('Message')) { + args.splice(1, 0, new MessageExtender(param1 as IMessage)); + } else if (evtMethod.includes('Room')) { + args.splice(1, 0, new RoomExtender(param1 as IRoom)); + } + + return args; + } + + // "Modify" events have an additional "Builder" param - (context, builder, reader, http, persistence) + if (evtMethod.endsWith('Modify')) { + if (evtMethod.includes('Message')) { + args.splice(1, 0, new MessageBuilder(param1 as IMessage)); + } else if (evtMethod.includes('Room')) { + args.splice(1, 0, new RoomBuilder(param1 as IRoom)); + } + + return args; + } + + // From this point on, all events will require (reader, http, persistence, modifier) injected + args.push(AppAccessorsInstance.getModifier()); + + // This guy gets an extra one + if (evtMethod === 'executePostMessageDeleted') { + if (!param2) { + throw JsonRpcError.invalidParams(null); + } + + args.push(hydrateMessageObjects(param2)); + } + + return args; +} + +/** + * Hydrate the context object with the correct IMessage + * + * Some information is lost upon serializing the data from listeners through the pipes, + * so here we hydrate the complete object as necessary + */ +function hydrateMessageObjects(context: unknown): unknown { + if (objectIsRawMessage(context)) { + context.room = createRoom(context.room as IRoom, AppAccessorsInstance.getSenderFn()); + } else if ((context as Record)?.message) { + (context as Record).message = hydrateMessageObjects((context as Record).message); + } + + return context; +} + +function objectIsRawMessage(value: unknown): value is IMessage { + if (!value) return false; + + const { id, room, sender, createdAt } = value as Record; + + // Check if we have the fields of a message and the room hasn't already been hydrated + return !!(id && room && sender && createdAt) && !(room instanceof Room); +} diff --git a/packages/apps/deno-runtime/handlers/outboundcomms-handler.ts b/packages/apps/deno-runtime/handlers/outboundcomms-handler.ts new file mode 100644 index 0000000000000..cb425e61684f5 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/outboundcomms-handler.ts @@ -0,0 +1,37 @@ +import type { IOutboundMessageProviders } from '@rocket.chat/apps-engine/definition/outboundCommunication/IOutboundCommsProvider.ts'; +import { JsonRpcError, Defined } from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; +import { RequestContext } from '../lib/requestContext.ts'; +import { wrapComposedApp } from '../lib/wrapAppForRequest.ts'; + +export default async function outboundMessageHandler(request: RequestContext): Promise { + const { method: call, params } = request; + const [, providerName, methodName] = call.split(':'); + + const provider = AppObjectRegistry.get(`outboundCommunication:${providerName}`); + + if (!provider) { + return new JsonRpcError('error-invalid-provider', -32000); + } + + const method = provider[methodName as keyof IOutboundMessageProviders]; + const { logger } = request.context; + const args = (params as Array) ?? []; + + try { + logger.debug(`Executing ${methodName} on outbound communication provider...`); + + // deno-lint-ignore ban-types + return await (method as Function).apply(wrapComposedApp(provider, request), [ + ...args, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getModifier(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + ]); + } catch (e) { + return new JsonRpcError(e.message, -32000); + } +} diff --git a/packages/apps/deno-runtime/handlers/scheduler-handler.ts b/packages/apps/deno-runtime/handlers/scheduler-handler.ts new file mode 100644 index 0000000000000..23c969ed46131 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/scheduler-handler.ts @@ -0,0 +1,65 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; +import type { IProcessor } from '@rocket.chat/apps-engine/definition/scheduler/IProcessor.ts'; +import { Defined, JsonRpcError } from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; +import { RequestContext } from '../lib/requestContext.ts'; +import { wrapAppForRequest } from '../lib/wrapAppForRequest.ts'; +import { assertAppAvailable } from './lib/assertions.ts'; + +export default async function handleScheduler(request: RequestContext): Promise { + const { method, params } = request; + const { logger } = request.context; + + const [, processorId] = method.split(':'); + if (!Array.isArray(params)) { + return JsonRpcError.invalidParams({ message: 'Invalid params' }); + } + + const [context] = params as [Record]; + + // AppSchedulerManager will append the appId to the processor name to avoid conflicts + const processor = AppObjectRegistry.get(`scheduler:${processorId}`); + + if (!processor) { + return JsonRpcError.methodNotFound({ + message: `Could not find processor for method ${method}`, + }); + } + + logger.debug({ msg: 'Job processor is being executed...', processorId: processor.id }); + + const app = AppObjectRegistry.get('app'); + + try { + assertAppAvailable(app); + + await processor.processor.call( + // Processor registration doesn't require the App dev to instantiate a class passing + // a reference to an App object, so we don't have a good way of hijacking the Logger + // we need. + // The only way we have to provide a durable Logger instance for the processor is by + // binding its execution to the proxied App reference itself. Unfortunately, the API + // ends up being opaque, but there isn't much we can do for now. + wrapAppForRequest(app, request), + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getModifier(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + ); + + logger.debug({ msg: 'Job processor was successfully executed', processorId: processor.id }); + + return null; + } catch (err) { + logger.error({ err, msg: 'Job processor was unsuccessful', processorId: processor.id }); + + if (err instanceof JsonRpcError) { + return err; + } + + return JsonRpcError.internalError({ message: err.message }); + } +} diff --git a/packages/apps/deno-runtime/handlers/slashcommand-handler.ts b/packages/apps/deno-runtime/handlers/slashcommand-handler.ts new file mode 100644 index 0000000000000..de1a9ecd1efe1 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/slashcommand-handler.ts @@ -0,0 +1,128 @@ +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands/ISlashCommand.ts'; +import type { SlashCommandContext as _SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands/SlashCommandContext.ts'; +import type { Room as _Room } from '@rocket.chat/apps-engine/server/rooms/Room.ts'; +import { Defined, JsonRpcError } from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; +import { AppAccessors, AppAccessorsInstance } from '../lib/accessors/mod.ts'; +import { require } from '../lib/require.ts'; +import createRoom from '../lib/roomFactory.ts'; +import { RequestContext } from '../lib/requestContext.ts'; +import { wrapComposedApp } from '../lib/wrapAppForRequest.ts'; + +// For some reason Deno couldn't understand the typecast to the original interfaces and said it wasn't a constructor type +const { SlashCommandContext } = require('@rocket.chat/apps-engine/definition/slashcommands/SlashCommandContext.js') as { + SlashCommandContext: typeof _SlashCommandContext; +}; + +export default async function slashCommandHandler(request: RequestContext): Promise { + const { method: call, params } = request; + const { logger } = request.context; + + const [, commandName, method] = call.split(':'); + + const command = AppObjectRegistry.get(`slashcommand:${commandName}`); + + if (!command) { + return new JsonRpcError(`Slashcommand ${commandName} not found`, -32000); + } + + let result: Awaited> | Awaited>; + + logger.debug({ msg: `Command is being executed...`, commandName, method, params }); + + try { + if (method === 'executor' || method === 'previewer') { + result = await handleExecutor({ AppAccessorsInstance, request }, command, method, params); + } else if (method === 'executePreviewItem') { + result = await handlePreviewItem({ AppAccessorsInstance, request }, command, params); + } else { + return new JsonRpcError(`Method ${method} not found on slashcommand ${commandName}`, -32000); + } + + logger.debug({ msg: `Command was successfully executed.`, commandName, method }); + } catch (error) { + logger.debug({ msg: `Command was unsuccessful.`, commandName, method, err: error }); + + return new JsonRpcError(error.message, -32000); + } + + return result; +} + +type Deps = { + AppAccessorsInstance: AppAccessors, + request: RequestContext; +} + +/** + * @param deps Dependencies that need to be injected into the slashcommand + * @param command The slashcommand that is being executed + * @param method The method that is being executed + * @param params The parameters that are being passed to the method + */ +export function handleExecutor(deps: Deps, command: ISlashCommand, method: 'executor' | 'previewer', params: unknown) { + const executor = command[method]; + + if (typeof executor !== 'function') { + throw new Error(`Method ${method} not found on slashcommand ${command.command}`); + } + + if (!Array.isArray(params) || typeof params[0] !== 'object' || !params[0]) { + throw new Error(`First parameter must be an object`); + } + + const { sender, room, params: args, threadId, triggerId } = params[0] as Record; + + const context = new SlashCommandContext( + sender as _SlashCommandContext['sender'], + createRoom(room as IRoom, deps.AppAccessorsInstance.getSenderFn()), + args as _SlashCommandContext['params'], + threadId as _SlashCommandContext['threadId'], + triggerId as _SlashCommandContext['triggerId'], + ); + + return executor.apply(wrapComposedApp(command, deps.request), [ + context, + deps.AppAccessorsInstance.getReader(), + deps.AppAccessorsInstance.getModifier(), + deps.AppAccessorsInstance.getHttp(), + deps.AppAccessorsInstance.getPersistence(), + ]); +} + +/** + * @param deps Dependencies that need to be injected into the slashcommand + * @param command The slashcommand that is being executed + * @param params The parameters that are being passed to the method + */ +export function handlePreviewItem(deps: Deps, command: ISlashCommand, params: unknown) { + if (typeof command.executePreviewItem !== 'function') { + throw new Error(`Method not found on slashcommand ${command.command}`); + } + + if (!Array.isArray(params) || typeof params[0] !== 'object' || !params[0]) { + throw new Error(`First parameter must be an object`); + } + + const [previewItem, { sender, room, params: args, threadId, triggerId }] = params as [Record, Record]; + + const context = new SlashCommandContext( + sender as _SlashCommandContext['sender'], + createRoom(room as IRoom, deps.AppAccessorsInstance.getSenderFn()), + args as _SlashCommandContext['params'], + threadId as _SlashCommandContext['threadId'], + triggerId as _SlashCommandContext['triggerId'], + ); + + return command.executePreviewItem.call( + wrapComposedApp(command, deps.request), + previewItem, + context, + deps.AppAccessorsInstance.getReader(), + deps.AppAccessorsInstance.getModifier(), + deps.AppAccessorsInstance.getHttp(), + deps.AppAccessorsInstance.getPersistence(), + ); +} diff --git a/packages/apps/deno-runtime/handlers/tests/api-handler.test.ts b/packages/apps/deno-runtime/handlers/tests/api-handler.test.ts new file mode 100644 index 0000000000000..26ccbd43fb80f --- /dev/null +++ b/packages/apps/deno-runtime/handlers/tests/api-handler.test.ts @@ -0,0 +1,118 @@ +// deno-lint-ignore-file no-explicit-any +import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api/IApiEndpoint.ts'; +import { assertEquals, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; +import { assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/assert_instance_of.ts'; +import { JsonRpcError } from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import apiHandler from '../api-handler.ts'; +import { createMockRequest } from './helpers/mod.ts'; + +describe('handlers > api', () => { + const mockEndpoint: IApiEndpoint = { + path: '/test', + // deno-lint-ignore no-unused-vars + get: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('ok'), + // deno-lint-ignore no-unused-vars + post: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('ok'), + // deno-lint-ignore no-unused-vars + put: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => { + throw new Error('Method execution error example'); + }, + }; + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('api:/test', mockEndpoint); + }); + + it('correctly handles execution of an api endpoint method GET', async () => { + const _spy = spy(mockEndpoint, 'get'); + + const result = await apiHandler(createMockRequest({ method: 'api:/test:get', params: ['request', 'endpointInfo'] })); + + assertEquals(result, 'ok'); + assertEquals(_spy.calls[0].args.length, 6); + assertEquals(_spy.calls[0].args[0], 'request'); + assertEquals(_spy.calls[0].args[1], 'endpointInfo'); + }); + + it('correctly handles execution of an api endpoint method POST', async () => { + const _spy = spy(mockEndpoint, 'post'); + + const result = await apiHandler(createMockRequest({ method: 'api:/test:post', params: ['request', 'endpointInfo'] })); + + assertEquals(result, 'ok'); + assertEquals(_spy.calls[0].args.length, 6); + assertEquals(_spy.calls[0].args[0], 'request'); + assertEquals(_spy.calls[0].args[1], 'endpointInfo'); + }); + + it('correctly handles an error if the method not exists for the selected endpoint', async () => { + const result = await apiHandler(createMockRequest({ method: `api:/test:delete`, params: ['request', 'endpointInfo'] })); + + assertInstanceOf(result, JsonRpcError); + assertObjectMatch(result, { + message: `/test's delete not exists`, + code: -32000, + }); + }); + + it('correctly handles an error if endpoint not exists', async () => { + const result = await apiHandler(createMockRequest({ method: `api:/error:get`, params: ['request', 'endpointInfo'] })); + + assertInstanceOf(result, JsonRpcError); + assertObjectMatch(result, { + message: `Endpoint /error not found`, + code: -32000, + }); + }); + + it('correctly handles an error if the method execution fails', async () => { + const result = await apiHandler(createMockRequest({ method: `api:/test:put`, params: ['request', 'endpointInfo'] })); + + assertInstanceOf(result, JsonRpcError); + assertObjectMatch(result, { + message: `Method execution error example`, + code: -32000, + }); + }); + + it('correctly handles dynamic paths with parameters (e.g., webhook/:event)', async () => { + const mockDynamicEndpoint: IApiEndpoint = { + path: 'webhook/:event', + // deno-lint-ignore no-unused-vars + post: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('webhook handled'), + }; + + AppObjectRegistry.set('api:webhook/:event', mockDynamicEndpoint); + + const _spy = spy(mockDynamicEndpoint, 'post'); + + const result = await apiHandler(createMockRequest({ method: 'api:webhook/:event:post', params: ['request', 'endpointInfo'] })); + + assertEquals(result, 'webhook handled'); + assertEquals(_spy.calls[0].args.length, 6); + assertEquals(_spy.calls[0].args[0], 'request'); + assertEquals(_spy.calls[0].args[1], 'endpointInfo'); + }); + + it('correctly handles paths with multiple segments and colons', async () => { + const mockComplexEndpoint: IApiEndpoint = { + path: 'api/v1/:resource/:id', + // deno-lint-ignore no-unused-vars + get: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('complex path'), + }; + + AppObjectRegistry.set('api:api/v1/:resource/:id', mockComplexEndpoint); + + const _spy = spy(mockComplexEndpoint, 'get'); + + const result = await apiHandler(createMockRequest({ method: 'api:api/v1/:resource/:id:get', params: ['request', 'endpointInfo'] })); + + assertEquals(result, 'complex path'); + assertEquals(_spy.calls[0].args.length, 6); + }); +}); diff --git a/packages/apps/deno-runtime/handlers/tests/helpers/mod.ts b/packages/apps/deno-runtime/handlers/tests/helpers/mod.ts new file mode 100644 index 0000000000000..581d95e4c3484 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/tests/helpers/mod.ts @@ -0,0 +1,29 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { Logger } from '../../../lib/logger.ts'; +import { RequestDescriptor } from '../../../lib/messenger.ts'; +import { RequestContext } from '../../../lib/requestContext.ts'; + +export function createMockRequest({ method, params }: RequestDescriptor): RequestContext { + return { + jsonrpc: '2.0', + id: 1, + method, + params, + context: { + logger: new Logger(method), + }, + serialize: () => '', + } +} + +export function createMockApp(): App { + return { + extendConfiguration: () => {}, + getID: () => 'mockApp', + getLogger: () => ({ + debug: () => {}, + error: () => {}, + }), + } as unknown as App; +} diff --git a/packages/apps/deno-runtime/handlers/tests/listener-handler.test.ts b/packages/apps/deno-runtime/handlers/tests/listener-handler.test.ts new file mode 100644 index 0000000000000..8e355f6ac4d3d --- /dev/null +++ b/packages/apps/deno-runtime/handlers/tests/listener-handler.test.ts @@ -0,0 +1,234 @@ +// deno-lint-ignore-file no-explicit-any +import { assertEquals, assertInstanceOf, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; + +import { parseArgs } from '../listener/handler.ts'; +import { AppAccessors } from '../../lib/accessors/mod.ts'; +import { Room } from '../../lib/room.ts'; +import { MessageExtender } from '../../lib/accessors/extenders/MessageExtender.ts'; +import { RoomExtender } from '../../lib/accessors/extenders/RoomExtender.ts'; +import { MessageBuilder } from '../../lib/accessors/builders/MessageBuilder.ts'; +import { RoomBuilder } from '../../lib/accessors/builders/RoomBuilder.ts'; + +describe('handlers > listeners', () => { + const mockAppAccessors = { + getReader: () => ({ __type: 'reader' }), + getHttp: () => ({ __type: 'http' }), + getModifier: () => ({ __type: 'modifier' }), + getPersistence: () => ({ __type: 'persistence' }), + getSenderFn: () => (id: string) => Promise.resolve([{ __type: 'bridgeCall' }, { id }]), + } as unknown as AppAccessors; + + it('correctly parses the arguments for a request to trigger the "checkPreMessageSentPrevent" method', () => { + const evtMethod = 'checkPreMessageSentPrevent'; + // For the 'checkPreMessageSentPrevent' method, the context will be a message in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 3); + assertEquals(params[0], { __type: 'context' }); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + }); + + it('correctly parses the arguments for a request to trigger the "checkPostMessageDeleted" method', () => { + const evtMethod = 'checkPostMessageDeleted'; + // For the 'checkPostMessageDeleted' method, the context will be a message in a real scenario, + // and the extraContext will provide further information such the user who deleted the message + const evtArgs = [{ __type: 'context' }, { __type: 'extraContext' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 4); + assertEquals(params[0], { __type: 'context' }); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'extraContext' }); + }); + + it('correctly parses the arguments for a request to trigger the "checkPreRoomCreateExtend" method', () => { + const evtMethod = 'checkPreRoomCreateExtend'; + // For the 'checkPreRoomCreateExtend' method, the context will be a room in a real scenario + const evtArgs = [ + { + id: 'fake', + type: 'fake', + slugifiedName: 'fake', + creator: 'fake', + createdAt: Date.now(), + }, + ]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 3); + + assertInstanceOf(params[0], Room); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePreMessageSentExtend" method', () => { + const evtMethod = 'executePreMessageSentExtend'; + // For the 'executePreMessageSentExtend' method, the context will be a message in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + // Instantiating the MessageExtender might modify the original object, so we need to assert it matches instead of equals + assertObjectMatch(params[0] as Record, { + __type: 'context', + }); + assertInstanceOf(params[1], MessageExtender); + assertEquals(params[2], { __type: 'reader' }); + assertEquals(params[3], { __type: 'http' }); + assertEquals(params[4], { __type: 'persistence' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePreRoomCreateExtend" method', () => { + const evtMethod = 'executePreRoomCreateExtend'; + // For the 'executePreRoomCreateExtend' method, the context will be a room in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + // Instantiating the RoomExtender might modify the original object, so we need to assert it matches instead of equals + assertObjectMatch(params[0] as Record, { + __type: 'context', + }); + assertInstanceOf(params[1], RoomExtender); + assertEquals(params[2], { __type: 'reader' }); + assertEquals(params[3], { __type: 'http' }); + assertEquals(params[4], { __type: 'persistence' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePreMessageSentModify" method', () => { + const evtMethod = 'executePreMessageSentModify'; + // For the 'executePreMessageSentModify' method, the context will be a message in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + // Instantiating the MessageBuilder might modify the original object, so we need to assert it matches instead of equals + assertObjectMatch(params[0] as Record, { + __type: 'context', + }); + assertInstanceOf(params[1], MessageBuilder); + assertEquals(params[2], { __type: 'reader' }); + assertEquals(params[3], { __type: 'http' }); + assertEquals(params[4], { __type: 'persistence' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePreRoomCreateModify" method', () => { + const evtMethod = 'executePreRoomCreateModify'; + // For the 'executePreRoomCreateModify' method, the context will be a room in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + // Instantiating the RoomBuilder might modify the original object, so we need to assert it matches instead of equals + assertObjectMatch(params[0] as Record, { + __type: 'context', + }); + assertInstanceOf(params[1], RoomBuilder); + assertEquals(params[2], { __type: 'reader' }); + assertEquals(params[3], { __type: 'http' }); + assertEquals(params[4], { __type: 'persistence' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePostRoomUserJoined" method', () => { + const evtMethod = 'executePostRoomUserJoined'; + // For the 'executePostRoomUserJoined' method, the context will be a room in a real scenario + const room = { + id: 'fake', + type: 'fake', + slugifiedName: 'fake', + creator: 'fake', + createdAt: Date.now(), + }; + + const evtArgs = [{ __type: 'context', room }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + assertInstanceOf((params[0] as any).room, Room); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'persistence' }); + assertEquals(params[4], { __type: 'modifier' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePostRoomUserLeave" method', () => { + const evtMethod = 'executePostRoomUserLeave'; + // For the 'executePostRoomUserLeave' method, the context will be a room in a real scenario + const room = { + id: 'fake', + type: 'fake', + slugifiedName: 'fake', + creator: 'fake', + createdAt: Date.now(), + }; + + const evtArgs = [{ __type: 'context', room }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + assertInstanceOf((params[0] as any).room, Room); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'persistence' }); + assertEquals(params[4], { __type: 'modifier' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePostMessageDeleted" method', () => { + const evtMethod = 'executePostMessageDeleted'; + // For the 'executePostMessageDeleted' method, the context will be a message in a real scenario + const evtArgs = [{ __type: 'context' }, { __type: 'extraContext' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 6); + assertEquals(params[0], { __type: 'context' }); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'persistence' }); + assertEquals(params[4], { __type: 'modifier' }); + assertEquals(params[5], { __type: 'extraContext' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePostMessageSent" method', () => { + const evtMethod = 'executePostMessageSent'; + // For the 'executePostMessageDeleted' method, the context will be a message in a real scenario + const evtArgs = [ + { + id: 'fake', + sender: 'fake', + createdAt: Date.now(), + room: { + id: 'fake-room', + type: 'fake', + slugifiedName: 'fake', + creator: 'fake', + createdAt: Date.now(), + }, + }, + ]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + assertObjectMatch(params[0] as Record, { id: 'fake' }); + assertInstanceOf((params[0] as any).room, Room); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'persistence' }); + assertEquals(params[4], { __type: 'modifier' }); + }); +}); diff --git a/packages/apps/deno-runtime/handlers/tests/scheduler-handler.test.ts b/packages/apps/deno-runtime/handlers/tests/scheduler-handler.test.ts new file mode 100644 index 0000000000000..7f5c6eccaf569 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/tests/scheduler-handler.test.ts @@ -0,0 +1,41 @@ +import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessors } from '../../lib/accessors/mod.ts'; +import handleScheduler from '../scheduler-handler.ts'; +import { createMockApp, createMockRequest } from './helpers/mod.ts'; + +describe('handlers > scheduler', () => { + const mockAppAccessors = new AppAccessors(() => + Promise.resolve({ + id: 'mockId', + result: {}, + jsonrpc: '2.0', + serialize: () => '', + }) + ); + + const mockApp = createMockApp(); + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('app', mockApp); + mockAppAccessors.getConfigurationExtend().scheduler.registerProcessors([ + { + id: 'mockId', + processor: () => Promise.resolve('it works!'), + }, + ]); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('correctly executes a request to a processor', async () => { + const result = await handleScheduler(createMockRequest({ method: 'scheduler:mockId', params: [{}] })); + + assertEquals(result, null); + }); +}); diff --git a/packages/apps/deno-runtime/handlers/tests/slashcommand-handler.test.ts b/packages/apps/deno-runtime/handlers/tests/slashcommand-handler.test.ts new file mode 100644 index 0000000000000..7114aa1f85bea --- /dev/null +++ b/packages/apps/deno-runtime/handlers/tests/slashcommand-handler.test.ts @@ -0,0 +1,159 @@ +// deno-lint-ignore-file no-explicit-any +import { assertEquals, assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessors } from '../../lib/accessors/mod.ts'; +import { handleExecutor, handlePreviewItem } from '../slashcommand-handler.ts'; +import { Room } from '../../lib/room.ts'; +import { createMockRequest } from './helpers/mod.ts'; + +describe('handlers > slashcommand', () => { + const mockAppAccessors = { + getReader: () => ({ __type: 'reader' }), + getHttp: () => ({ __type: 'http' }), + getModifier: () => ({ __type: 'modifier' }), + getPersistence: () => ({ __type: 'persistence' }), + getSenderFn: () => (id: string) => Promise.resolve([{ __type: 'bridgeCall' }, { id }]), + } as unknown as AppAccessors; + + const mockCommandExecutorOnly = { + command: 'executor-only', + i18nParamsExample: 'test', + i18nDescription: 'test', + providesPreview: false, + // deno-lint-ignore no-unused-vars + async executor(context: any, read: any, modify: any, http: any, persis: any): Promise {}, + }; + + const mockCommandExecutorAndPreview = { + command: 'executor-and-preview', + i18nParamsExample: 'test', + i18nDescription: 'test', + providesPreview: true, + // deno-lint-ignore no-unused-vars + async executor(context: any, read: any, modify: any, http: any, persis: any): Promise {}, + // deno-lint-ignore no-unused-vars + async previewer(context: any, read: any, modify: any, http: any, persis: any): Promise {}, + // deno-lint-ignore no-unused-vars + async executePreviewItem(previewItem: any, context: any, read: any, modify: any, http: any, persis: any): Promise {}, + }; + + const mockCommandPreviewWithNoExecutor = { + command: 'preview-with-no-executor', + i18nParamsExample: 'test', + i18nDescription: 'test', + providesPreview: true, + // deno-lint-ignore no-unused-vars + async previewer(context: any, read: any, modify: any, http: any, persis: any): Promise {}, + // deno-lint-ignore no-unused-vars + async executePreviewItem(previewItem: any, context: any, read: any, modify: any, http: any, persis: any): Promise {}, + }; + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('slashcommand:executor-only', mockCommandExecutorOnly); + AppObjectRegistry.set('slashcommand:executor-and-preview', mockCommandExecutorAndPreview); + AppObjectRegistry.set('slashcommand:preview-with-no-executor', mockCommandPreviewWithNoExecutor); + }); + + it('correctly handles execution of a slash command', async () => { + const mockContext = { + sender: { __type: 'sender' }, + room: { __type: 'room' }, + params: { __type: 'params' }, + threadId: 'threadId', + triggerId: 'triggerId', + }; + + const _spy = spy(mockCommandExecutorOnly, 'executor'); + + const mockRequest = createMockRequest({ method: 'slashcommand:executor-only:executor', params: [mockContext] }); + + await handleExecutor({ AppAccessorsInstance: mockAppAccessors, request: mockRequest }, mockCommandExecutorOnly, 'executor', [mockContext]); + + const context = _spy.calls[0].args[0]; + + assertInstanceOf(context.getRoom(), Room); + assertEquals(context.getSender(), { __type: 'sender' }); + assertEquals(context.getArguments(), { __type: 'params' }); + assertEquals(context.getThreadId(), 'threadId'); + assertEquals(context.getTriggerId(), 'triggerId'); + + assertEquals(_spy.calls[0].args[1], mockAppAccessors.getReader()); + assertEquals(_spy.calls[0].args[2], mockAppAccessors.getModifier()); + assertEquals(_spy.calls[0].args[3], mockAppAccessors.getHttp()); + assertEquals(_spy.calls[0].args[4], mockAppAccessors.getPersistence()); + + _spy.restore(); + }); + + it('correctly handles execution of a slash command previewer', async () => { + const mockContext = { + sender: { __type: 'sender' }, + room: { __type: 'room' }, + params: { __type: 'params' }, + threadId: 'threadId', + triggerId: 'triggerId', + }; + + const _spy = spy(mockCommandExecutorAndPreview, 'previewer'); + + const mockRequest = createMockRequest({ method: 'slashcommand:executor-and-preview:previewer', params: [mockContext] }); + + await handleExecutor({ AppAccessorsInstance: mockAppAccessors, request: mockRequest }, mockCommandExecutorAndPreview, 'previewer', [mockContext]); + + const context = _spy.calls[0].args[0]; + + assertInstanceOf(context.getRoom(), Room); + assertEquals(context.getSender(), { __type: 'sender' }); + assertEquals(context.getArguments(), { __type: 'params' }); + assertEquals(context.getThreadId(), 'threadId'); + assertEquals(context.getTriggerId(), 'triggerId'); + + assertEquals(_spy.calls[0].args[1], mockAppAccessors.getReader()); + assertEquals(_spy.calls[0].args[2], mockAppAccessors.getModifier()); + assertEquals(_spy.calls[0].args[3], mockAppAccessors.getHttp()); + assertEquals(_spy.calls[0].args[4], mockAppAccessors.getPersistence()); + + _spy.restore(); + }); + + it('correctly handles execution of a slash command preview item executor', async () => { + const mockContext = { + sender: { __type: 'sender' }, + room: { __type: 'room' }, + params: { __type: 'params' }, + threadId: 'threadId', + triggerId: 'triggerId', + }; + + const mockPreviewItem = { + id: 'previewItemId', + type: 'image', + value: 'https://example.com/image.png', + }; + + const _spy = spy(mockCommandExecutorAndPreview, 'executePreviewItem'); + + const mockRequest = createMockRequest({ method: 'slashcommand:executor-and-preview:executePreviewItem', params: [mockPreviewItem, mockContext] }); + + await handlePreviewItem({ AppAccessorsInstance: mockAppAccessors, request: mockRequest }, mockCommandExecutorAndPreview, [mockPreviewItem, mockContext]); + + const context = _spy.calls[0].args[1]; + + assertInstanceOf(context.getRoom(), Room); + assertEquals(context.getSender(), { __type: 'sender' }); + assertEquals(context.getArguments(), { __type: 'params' }); + assertEquals(context.getThreadId(), 'threadId'); + assertEquals(context.getTriggerId(), 'triggerId'); + + assertEquals(_spy.calls[0].args[2], mockAppAccessors.getReader()); + assertEquals(_spy.calls[0].args[3], mockAppAccessors.getModifier()); + assertEquals(_spy.calls[0].args[4], mockAppAccessors.getHttp()); + assertEquals(_spy.calls[0].args[5], mockAppAccessors.getPersistence()); + + _spy.restore(); + }); +}); diff --git a/packages/apps/deno-runtime/handlers/tests/uikit-handler.test.ts b/packages/apps/deno-runtime/handlers/tests/uikit-handler.test.ts new file mode 100644 index 0000000000000..b663bd2ae6833 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/tests/uikit-handler.test.ts @@ -0,0 +1,105 @@ +// deno-lint-ignore-file no-explicit-any +import { assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import jsonrpc from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import handleUIKitInteraction, { + UIKitActionButtonInteractionContext, + UIKitBlockInteractionContext, + UIKitLivechatBlockInteractionContext, + UIKitViewCloseInteractionContext, + UIKitViewSubmitInteractionContext, +} from '../uikit/handler.ts'; + +describe('handlers > uikit', () => { + const mockApp = { + getID: (): string => 'appId', + executeBlockActionHandler: (context: any): Promise => Promise.resolve(context), + executeViewSubmitHandler: (context: any): Promise => Promise.resolve(context), + executeViewClosedHandler: (context: any): Promise => Promise.resolve(context), + executeActionButtonHandler: (context: any): Promise => Promise.resolve(context), + executeLivechatBlockActionHandler: (context: any): Promise => Promise.resolve(context), + }; + + beforeEach(() => { + AppObjectRegistry.set('app', mockApp); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('successfully handles a call for "executeBlockActionHandler"', async () => { + const request = jsonrpc.request(1, 'app:executeBlockActionHandler', [ + { + actionId: 'actionId', + blockId: 'blockId', + value: 'value', + viewId: 'viewId', + }, + ]); + + const result = await handleUIKitInteraction(request); + assertInstanceOf(result, UIKitBlockInteractionContext); + }); + + it('successfully handles a call for "executeViewSubmitHandler"', async () => { + const request = jsonrpc.request(1, 'app:executeViewSubmitHandler', [ + { + viewId: 'viewId', + appId: 'appId', + userId: 'userId', + isAppUser: true, + values: {}, + }, + ]); + + const result = await handleUIKitInteraction(request); + assertInstanceOf(result, UIKitViewSubmitInteractionContext); + }); + + it('successfully handles a call for "executeViewClosedHandler"', async () => { + const request = jsonrpc.request(1, 'app:executeViewClosedHandler', [ + { + viewId: 'viewId', + appId: 'appId', + userId: 'userId', + isAppUser: true, + }, + ]); + + const result = await handleUIKitInteraction(request); + assertInstanceOf(result, UIKitViewCloseInteractionContext); + }); + + it('successfully handles a call for "executeActionButtonHandler"', async () => { + const request = jsonrpc.request(1, 'app:executeActionButtonHandler', [ + { + actionId: 'actionId', + appId: 'appId', + userId: 'userId', + isAppUser: true, + }, + ]); + + const result = await handleUIKitInteraction(request); + assertInstanceOf(result, UIKitActionButtonInteractionContext); + }); + + it('successfully handles a call for "executeLivechatBlockActionHandler"', async () => { + const request = jsonrpc.request(1, 'app:executeLivechatBlockActionHandler', [ + { + actionId: 'actionId', + appId: 'appId', + userId: 'userId', + visitor: {}, + isAppUser: true, + room: {}, + }, + ]); + + const result = await handleUIKitInteraction(request); + assertInstanceOf(result, UIKitLivechatBlockInteractionContext); + }); +}); diff --git a/packages/apps/deno-runtime/handlers/tests/upload-event-handler.test.ts b/packages/apps/deno-runtime/handlers/tests/upload-event-handler.test.ts new file mode 100644 index 0000000000000..182d32eda3353 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/tests/upload-event-handler.test.ts @@ -0,0 +1,107 @@ +// deno-lint-ignore-file no-explicit-any +import { Buffer } from 'node:buffer'; + +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; +import type { IPreFileUpload } from '@rocket.chat/apps-engine/definition/uploads/IPreFileUpload.ts'; +import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails.ts'; +import { assertInstanceOf, assertNotInstanceOf, assertEquals, assertStringIncludes } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { afterEach, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { assertSpyCalls, spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; +import { JsonRpcError } from 'jsonrpc-lite'; + +import { createMockRequest } from './helpers/mod.ts'; +import handleUploadEvents from '../app/handleUploadEvents.ts'; +import { Errors } from '../lib/assertions.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; + +describe('handlers > upload', () => { + let app: App & IPreFileUpload; + let path: string; + let file: IUploadDetails; + + beforeEach(async () => { + AppObjectRegistry.clear(); + + path = await Deno.makeTempFile(); + + app = { + extendConfiguration: () => {}, + executePreFileUpload: () => Promise.resolve(), + } as unknown as App; + + AppObjectRegistry.set('app', app); + + const content = 'Temp file for testing'; + + await Deno.writeTextFile(path, content); + + file = { + name: 'TempFile.txt', + size: content.length, + type: 'text/plain', + rid: 'RandomRoomId', + userId: 'RandomUserId', + }; + }); + + afterEach(async () => { + await Deno.remove(path).catch((e) => e?.code !== 'ENOENT' && console.warn(`Failed to remove temp file at ${path}`, e)); + }); + + it('correctly handles valid parameters', async () => { + const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); + + assertNotInstanceOf(result, JsonRpcError, 'result is JsonRpcError'); + }); + + it('correctly loads the file contents for IPreFileUpload', async () => { + const _spy = spy(app as any, 'executePreFileUpload'); + + const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); + + assertNotInstanceOf(result, JsonRpcError, 'result is JsonRpcError'); + assertSpyCalls(_spy, 1); + assertInstanceOf((_spy.calls[0].args[0] as any)?.content, Buffer); + }); + + it('fails when app object is not on registry', async () => { + AppObjectRegistry.clear(); + + const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); + + assertInstanceOf(result, JsonRpcError); + assertEquals(result.data.code, Errors.DRT_APP_NOT_AVAILABLE); + }); + + it('fails when the app does not implement the IPreFileUpload event handler', async () => { + delete (app as any)['executePreFileUpload']; + + const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); + + assertInstanceOf(result, JsonRpcError); + assertEquals(result.data.code, Errors.DRT_EVENT_HANDLER_FUNCTION_MISSING); + }); + + it('fails when "file" is not a proper IUploadDetails object', async () => { + const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file: { nope: "bad" }, path }] })); + + assertInstanceOf(result, JsonRpcError); + assertStringIncludes(result.data.err, 'Expected IUploadDetails'); + }); + + it('fails when "path" is not a proper string', async () => { + const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path: {} }] })); + + assertInstanceOf(result, JsonRpcError); + assertStringIncludes(result.data.err, 'Expected string'); + }); + + it('fails when "path" is not a readable file path', async () => { + await Deno.remove(path); + + const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); + + assertInstanceOf(result, JsonRpcError); + assertEquals(result.data.code, "ENOENT"); + }); +}); diff --git a/packages/apps/deno-runtime/handlers/tests/videoconference-handler.test.ts b/packages/apps/deno-runtime/handlers/tests/videoconference-handler.test.ts new file mode 100644 index 0000000000000..7632b08c39258 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/tests/videoconference-handler.test.ts @@ -0,0 +1,122 @@ +// deno-lint-ignore-file no-explicit-any +import { assertEquals, assertObjectMatch, assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; +import { JsonRpcError } from 'jsonrpc-lite'; + +import { createMockRequest } from './helpers/mod.ts'; +import videoconfHandler from '../videoconference-handler.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; + +describe('handlers > videoconference', () => { + // deno-lint-ignore no-unused-vars + const mockMethodWithoutParam = (read: any, modify: any, http: any, persis: any): Promise => Promise.resolve('ok none'); + // deno-lint-ignore no-unused-vars + const mockMethodWithOneParam = (call: any, read: any, modify: any, http: any, persis: any): Promise => Promise.resolve('ok one'); + // deno-lint-ignore no-unused-vars + const mockMethodWithTwoParam = (call: any, user: any, read: any, modify: any, http: any, persis: any): Promise => Promise.resolve('ok two'); + // deno-lint-ignore no-unused-vars + const mockMethodWithThreeParam = (call: any, user: any, options: any, read: any, modify: any, http: any, persis: any): Promise => + Promise.resolve('ok three'); + const mockProvider = { + empty: mockMethodWithoutParam, + one: mockMethodWithOneParam, + two: mockMethodWithTwoParam, + three: mockMethodWithThreeParam, + notAFunction: true, + error: () => { + throw new Error('Method execution error example'); + }, + }; + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('videoConfProvider:test-provider', mockProvider); + }); + + it('correctly handles execution of a videoconf method without additional params', async () => { + const _spy = spy(mockProvider, 'empty'); + + const result = await videoconfHandler(createMockRequest({ method: 'videoconference:test-provider:empty', params: [] })); + + assertEquals(result, 'ok none'); + assertEquals(_spy.calls[0].args.length, 4); + + _spy.restore(); + }); + + it('correctly handles execution of a videoconf method with one param', async () => { + const _spy = spy(mockProvider, 'one'); + + const result = await videoconfHandler(createMockRequest({ method: 'videoconference:test-provider:one', params: ['call'] })); + + assertEquals(result, 'ok one'); + assertEquals(_spy.calls[0].args.length, 5); + assertEquals(_spy.calls[0].args[0], 'call'); + + _spy.restore(); + }); + + it('correctly handles execution of a videoconf method with two params', async () => { + const _spy = spy(mockProvider, 'two'); + + const result = await videoconfHandler(createMockRequest({ method: 'videoconference:test-provider:two', params: ['call', 'user'] })); + + assertEquals(result, 'ok two'); + assertEquals(_spy.calls[0].args.length, 6); + assertEquals(_spy.calls[0].args[0], 'call'); + assertEquals(_spy.calls[0].args[1], 'user'); + + _spy.restore(); + }); + + it('correctly handles execution of a videoconf method with three params', async () => { + const _spy = spy(mockProvider, 'three'); + + const result = await videoconfHandler(createMockRequest({ method: 'videoconference:test-provider:three', params: ['call', 'user', 'options'] })); + + assertEquals(result, 'ok three'); + assertEquals(_spy.calls[0].args.length, 7); + assertEquals(_spy.calls[0].args[0], 'call'); + assertEquals(_spy.calls[0].args[1], 'user'); + assertEquals(_spy.calls[0].args[2], 'options'); + + _spy.restore(); + }); + + it('correctly handles an error on execution of a videoconf method', async () => { + const result = await videoconfHandler(createMockRequest({ method: 'videoconference:test-provider:error', params: [] })); + + assertInstanceOf(result, JsonRpcError); + assertObjectMatch(result, { + message: 'Method execution error example', + code: -32000, + }); + }); + + it('correctly handles an error when provider is not found', async () => { + const providerName = 'error-provider'; + const result = await videoconfHandler(createMockRequest({ method: `videoconference:${providerName}:method`, params: [] })); + + assertInstanceOf(result, JsonRpcError); + assertObjectMatch(result, { + message: `Provider ${providerName} not found`, + code: -32000, + }); + }); + + it('correctly handles an error if method is not a function of provider', async () => { + const methodName = 'notAFunction'; + const providerName = 'test-provider'; + const result = await videoconfHandler(createMockRequest({ method: `videoconference:${providerName}:${methodName}`, params: [] })); + + assertInstanceOf(result, JsonRpcError); + assertObjectMatch(result, { + message: 'Method not found', + code: -32601, + data: { + message: `Method ${methodName} not found on provider ${providerName}`, + }, + }); + }); +}); diff --git a/packages/apps/deno-runtime/handlers/uikit/handler.ts b/packages/apps/deno-runtime/handlers/uikit/handler.ts new file mode 100644 index 0000000000000..8d352d21927e7 --- /dev/null +++ b/packages/apps/deno-runtime/handlers/uikit/handler.ts @@ -0,0 +1,88 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; +import { Defined, JsonRpcError } from 'jsonrpc-lite'; + +import { require } from '../../lib/require.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; +import { isOneOf } from '../lib/assertions.ts'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; + +export const uikitInteractions = [ + 'executeBlockActionHandler', + 'executeViewSubmitHandler', + 'executeViewClosedHandler', + 'executeActionButtonHandler', + 'executeLivechatBlockActionHandler', +] as const; + +export const { + UIKitBlockInteractionContext, + UIKitViewSubmitInteractionContext, + UIKitViewCloseInteractionContext, + UIKitActionButtonInteractionContext, +} = require('@rocket.chat/apps-engine/definition/uikit/UIKitInteractionContext.js'); + +export const { UIKitLivechatBlockInteractionContext } = require('@rocket.chat/apps-engine/definition/uikit/livechat/UIKitLivechatInteractionContext.js'); + +export default async function handleUIKitInteraction(request: RequestContext): Promise { + const { method: reqMethod, params } = request; + const [, method] = reqMethod.split(':'); + + if (!isOneOf(method, uikitInteractions)) { + return JsonRpcError.methodNotFound(null); + } + + if (!Array.isArray(params)) { + return JsonRpcError.invalidParams(null); + } + + const app = AppObjectRegistry.get('app'); + + const interactionHandler = app?.[method as keyof App] as unknown; + + if (!app || typeof interactionHandler !== 'function') { + return JsonRpcError.methodNotFound({ + message: `App does not implement method "${method}"`, + }); + } + + const [payload] = params as [Record]; + + if (!payload) { + return JsonRpcError.invalidParams(null); + } + + let context; + + switch (method) { + case 'executeBlockActionHandler': + context = new UIKitBlockInteractionContext(payload); + break; + case 'executeViewSubmitHandler': + context = new UIKitViewSubmitInteractionContext(payload); + break; + case 'executeViewClosedHandler': + context = new UIKitViewCloseInteractionContext(payload); + break; + case 'executeActionButtonHandler': + context = new UIKitActionButtonInteractionContext(payload); + break; + case 'executeLivechatBlockActionHandler': + context = new UIKitLivechatBlockInteractionContext(payload); + break; + } + + try { + return await interactionHandler.call( + wrapAppForRequest(app, request), + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + AppAccessorsInstance.getModifier(), + ); + } catch (e) { + return JsonRpcError.internalError({ message: e.message }); + } +} diff --git a/packages/apps/deno-runtime/handlers/videoconference-handler.ts b/packages/apps/deno-runtime/handlers/videoconference-handler.ts new file mode 100644 index 0000000000000..5c82aedd3a63c --- /dev/null +++ b/packages/apps/deno-runtime/handlers/videoconference-handler.ts @@ -0,0 +1,52 @@ +import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders/IVideoConfProvider.ts'; +import { Defined, JsonRpcError } from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; +import { RequestContext } from '../lib/requestContext.ts'; +import { wrapComposedApp } from '../lib/wrapAppForRequest.ts'; + +export default async function videoConferenceHandler(request: RequestContext): Promise { + const { method: call, params } = request; + const { logger } = request.context; + + const [, providerName, methodName] = call.split(':'); + + const provider = AppObjectRegistry.get(`videoConfProvider:${providerName}`); + + if (!provider) { + return new JsonRpcError(`Provider ${providerName} not found`, -32000); + } + + const method = provider[methodName as keyof IVideoConfProvider]; + + if (typeof method !== 'function') { + return JsonRpcError.methodNotFound({ + message: `Method ${methodName} not found on provider ${providerName}`, + }); + } + + const [videoconf, user, options] = params as Array; + + logger.debug(`Executing ${methodName} on video conference provider...`); + + const args = [...(videoconf ? [videoconf] : []), ...(user ? [user] : []), ...(options ? [options] : [])]; + + try { + // deno-lint-ignore ban-types + const result = await (method as Function).apply(wrapComposedApp(provider, request), [ + ...args, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getModifier(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + ]); + + logger.debug(`Video Conference Provider's ${methodName} was successfully executed.`); + + return result; + } catch (e) { + logger.debug(`Video Conference Provider's ${methodName} was unsuccessful.`); + return new JsonRpcError(e.message, -32000); + } +} diff --git a/packages/apps/deno-runtime/lib/accessors/builders/BlockBuilder.ts b/packages/apps/deno-runtime/lib/accessors/builders/BlockBuilder.ts new file mode 100644 index 0000000000000..de103fe50be02 --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/builders/BlockBuilder.ts @@ -0,0 +1,215 @@ +import { v1 as uuid } from 'uuid'; + +import type { + BlockType as _BlockType, + IActionsBlock, + IBlock, + IConditionalBlock, + IConditionalBlockFilters, + IContextBlock, + IImageBlock, + IInputBlock, + ISectionBlock, +} from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks.ts'; +import type { + BlockElementType as _BlockElementType, + IBlockElement, + IButtonElement, + IImageElement, + IInputElement, + IInteractiveElement, + IMultiStaticSelectElement, + IOverflowMenuElement, + IPlainTextInputElement, + ISelectElement, + IStaticSelectElement, +} from '@rocket.chat/apps-engine/definition/uikit/blocks/Elements.ts'; +import type { ITextObject, TextObjectType as _TextObjectType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Objects.ts'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { require } from '../../../lib/require.ts'; + +const { BlockType } = require('@rocket.chat/apps-engine/definition/uikit/blocks/Blocks.js') as { BlockType: typeof _BlockType }; +const { BlockElementType } = require('@rocket.chat/apps-engine/definition/uikit/blocks/Elements.js') as { BlockElementType: typeof _BlockElementType }; +const { TextObjectType } = require('@rocket.chat/apps-engine/definition/uikit/blocks/Objects.js') as { TextObjectType: typeof _TextObjectType }; + +type BlockFunctionParameter = Omit; +type ElementFunctionParameter = T extends IInteractiveElement ? Omit | Partial> + : Omit; + +type SectionBlockParam = BlockFunctionParameter; +type ImageBlockParam = BlockFunctionParameter; +type ActionsBlockParam = BlockFunctionParameter; +type ContextBlockParam = BlockFunctionParameter; +type InputBlockParam = BlockFunctionParameter; + +type ButtonElementParam = ElementFunctionParameter; +type ImageElementParam = ElementFunctionParameter; +type OverflowMenuElementParam = ElementFunctionParameter; +type PlainTextInputElementParam = ElementFunctionParameter; +type StaticSelectElementParam = ElementFunctionParameter; +type MultiStaticSelectElementParam = ElementFunctionParameter; + +/** + * @deprecated please prefer the rocket.chat/ui-kit components + */ +export class BlockBuilder { + private readonly blocks: Array; + private readonly appId: string; + + constructor() { + this.blocks = []; + this.appId = String(AppObjectRegistry.get('id')); + } + + public addSectionBlock(block: SectionBlockParam): BlockBuilder { + this.addBlock({ type: BlockType.SECTION, ...block } as ISectionBlock); + + return this; + } + + public addImageBlock(block: ImageBlockParam): BlockBuilder { + this.addBlock({ type: BlockType.IMAGE, ...block } as IImageBlock); + + return this; + } + + public addDividerBlock(): BlockBuilder { + this.addBlock({ type: BlockType.DIVIDER }); + + return this; + } + + public addActionsBlock(block: ActionsBlockParam): BlockBuilder { + this.addBlock({ type: BlockType.ACTIONS, ...block } as IActionsBlock); + + return this; + } + + public addContextBlock(block: ContextBlockParam): BlockBuilder { + this.addBlock({ type: BlockType.CONTEXT, ...block } as IContextBlock); + + return this; + } + + public addInputBlock(block: InputBlockParam): BlockBuilder { + this.addBlock({ type: BlockType.INPUT, ...block } as IInputBlock); + + return this; + } + + public addConditionalBlock(innerBlocks: BlockBuilder | Array, condition?: IConditionalBlockFilters): BlockBuilder { + const render = innerBlocks instanceof BlockBuilder ? innerBlocks.getBlocks() : innerBlocks; + + this.addBlock({ + type: BlockType.CONDITIONAL, + render, + when: condition, + } as IConditionalBlock); + + return this; + } + + public getBlocks() { + return this.blocks; + } + + public newPlainTextObject(text: string, emoji = false): ITextObject { + return { + type: TextObjectType.PLAINTEXT, + text, + emoji, + }; + } + + public newMarkdownTextObject(text: string): ITextObject { + return { + type: TextObjectType.MARKDOWN, + text, + }; + } + + public newButtonElement(info: ButtonElementParam): IButtonElement { + return this.newInteractiveElement({ + type: BlockElementType.BUTTON, + ...info, + } as IButtonElement); + } + + public newImageElement(info: ImageElementParam): IImageElement { + return { + type: BlockElementType.IMAGE, + ...info, + }; + } + + public newOverflowMenuElement(info: OverflowMenuElementParam): IOverflowMenuElement { + return this.newInteractiveElement({ + type: BlockElementType.OVERFLOW_MENU, + ...info, + } as IOverflowMenuElement); + } + + public newPlainTextInputElement(info: PlainTextInputElementParam): IPlainTextInputElement { + return this.newInputElement({ + type: BlockElementType.PLAIN_TEXT_INPUT, + ...info, + } as IPlainTextInputElement); + } + + public newStaticSelectElement(info: StaticSelectElementParam): IStaticSelectElement { + return this.newSelectElement({ + type: BlockElementType.STATIC_SELECT, + ...info, + } as IStaticSelectElement); + } + + public newMultiStaticElement(info: MultiStaticSelectElementParam): IMultiStaticSelectElement { + return this.newSelectElement({ + type: BlockElementType.MULTI_STATIC_SELECT, + ...info, + } as IMultiStaticSelectElement); + } + + private newInteractiveElement(element: T): T { + if (!element.actionId) { + element.actionId = this.generateActionId(); + } + + return element; + } + + private newInputElement(element: T): T { + if (!element.actionId) { + element.actionId = this.generateActionId(); + } + + return element; + } + + private newSelectElement(element: T): T { + if (!element.actionId) { + element.actionId = this.generateActionId(); + } + + return element; + } + + private addBlock(block: IBlock): void { + if (!block.blockId) { + block.blockId = this.generateBlockId(); + } + + block.appId = this.appId; + + this.blocks.push(block); + } + + private generateBlockId(): string { + return uuid(); + } + + private generateActionId(): string { + return uuid(); + } +} diff --git a/packages/apps/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts b/packages/apps/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts new file mode 100644 index 0000000000000..adbf060182e1d --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts @@ -0,0 +1,59 @@ +import type { IDiscussionBuilder as _IDiscussionBuilder } from '@rocket.chat/apps-engine/definition/accessors/IDiscussionBuilder.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts'; + +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; +import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; + +import { RoomBuilder } from './RoomBuilder.ts'; +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; + +export interface IDiscussionBuilder extends _IDiscussionBuilder, IRoomBuilder {} + +export class DiscussionBuilder extends RoomBuilder implements IDiscussionBuilder { + public kind: _RocketChatAssociationModel.DISCUSSION; + + private reply?: string; + + private parentMessage?: IMessage; + + constructor(data?: Partial) { + super(data); + this.kind = RocketChatAssociationModel.DISCUSSION; + this.room.type = RoomType.PRIVATE_GROUP; + } + + public setParentRoom(parentRoom: IRoom): IDiscussionBuilder { + this.room.parentRoom = parentRoom; + return this; + } + + public getParentRoom(): IRoom { + return this.room.parentRoom!; + } + + public setReply(reply: string): IDiscussionBuilder { + this.reply = reply; + return this; + } + + public getReply(): string { + return this.reply!; + } + + public setParentMessage(parentMessage: IMessage): IDiscussionBuilder { + this.parentMessage = parentMessage; + return this; + } + + public getParentMessage(): IMessage { + return this.parentMessage!; + } +} diff --git a/packages/apps/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts b/packages/apps/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts new file mode 100644 index 0000000000000..b39a418c5aec0 --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts @@ -0,0 +1,204 @@ +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; +import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; + +import type { ILivechatMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/ILivechatMessageBuilder.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; +import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; +import type { ILivechatMessage as EngineLivechatMessage } from '@rocket.chat/apps-engine/definition/livechat/ILivechatMessage.ts'; +import type { IVisitor } from '@rocket.chat/apps-engine/definition/livechat/IVisitor.ts'; +import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts'; + +import { MessageBuilder } from './MessageBuilder.ts'; +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; + +export interface ILivechatMessage extends EngineLivechatMessage, IMessage {} + +export class LivechatMessageBuilder implements ILivechatMessageBuilder { + public kind: _RocketChatAssociationModel.LIVECHAT_MESSAGE; + + private msg: ILivechatMessage; + + constructor(message?: ILivechatMessage) { + this.kind = RocketChatAssociationModel.LIVECHAT_MESSAGE; + this.msg = message || ({} as ILivechatMessage); + } + + public setData(data: ILivechatMessage): ILivechatMessageBuilder { + delete data.id; + this.msg = data; + + return this; + } + + public setRoom(room: IRoom): ILivechatMessageBuilder { + this.msg.room = room; + return this; + } + + public getRoom(): IRoom { + return this.msg.room; + } + + public setSender(sender: IUser): ILivechatMessageBuilder { + this.msg.sender = sender; + delete this.msg.visitor; + + return this; + } + + public getSender(): IUser { + return this.msg.sender; + } + + public setText(text: string): ILivechatMessageBuilder { + this.msg.text = text; + return this; + } + + public getText(): string { + return this.msg.text!; + } + + public setEmojiAvatar(emoji: string): ILivechatMessageBuilder { + this.msg.emoji = emoji; + return this; + } + + public getEmojiAvatar(): string { + return this.msg.emoji!; + } + + public setAvatarUrl(avatarUrl: string): ILivechatMessageBuilder { + this.msg.avatarUrl = avatarUrl; + return this; + } + + public getAvatarUrl(): string { + return this.msg.avatarUrl!; + } + + public setUsernameAlias(alias: string): ILivechatMessageBuilder { + this.msg.alias = alias; + return this; + } + + public getUsernameAlias(): string { + return this.msg.alias!; + } + + public addAttachment(attachment: IMessageAttachment): ILivechatMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + this.msg.attachments.push(attachment); + return this; + } + + public setAttachments(attachments: Array): ILivechatMessageBuilder { + this.msg.attachments = attachments; + return this; + } + + public getAttachments(): Array { + return this.msg.attachments!; + } + + public replaceAttachment(position: number, attachment: IMessageAttachment): ILivechatMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + if (!this.msg.attachments[position]) { + throw new Error(`No attachment found at the index of "${position}" to replace.`); + } + + this.msg.attachments[position] = attachment; + return this; + } + + public removeAttachment(position: number): ILivechatMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + if (!this.msg.attachments[position]) { + throw new Error(`No attachment found at the index of "${position}" to remove.`); + } + + this.msg.attachments.splice(position, 1); + + return this; + } + + public setEditor(user: IUser): ILivechatMessageBuilder { + this.msg.editor = user; + return this; + } + + public getEditor(): IUser { + return this.msg.editor; + } + + public setGroupable(groupable: boolean): ILivechatMessageBuilder { + this.msg.groupable = groupable; + return this; + } + + public getGroupable(): boolean { + return this.msg.groupable!; + } + + public setParseUrls(parseUrls: boolean): ILivechatMessageBuilder { + this.msg.parseUrls = parseUrls; + return this; + } + + public getParseUrls(): boolean { + return this.msg.parseUrls!; + } + + public setToken(token: string): ILivechatMessageBuilder { + this.msg.token = token; + return this; + } + + public getToken(): string { + return this.msg.token!; + } + + public setVisitor(visitor: IVisitor): ILivechatMessageBuilder { + this.msg.visitor = visitor; + delete this.msg.sender; + + return this; + } + + public getVisitor(): IVisitor { + return this.msg.visitor; + } + + public getMessage(): ILivechatMessage { + if (!this.msg.room) { + throw new Error('The "room" property is required.'); + } + + if (this.msg.room.type !== RoomType.LIVE_CHAT) { + throw new Error('The room is not a Livechat room'); + } + + return this.msg; + } + + public getMessageBuilder(): IMessageBuilder { + return new MessageBuilder(this.msg as IMessage); + } +} diff --git a/packages/apps/deno-runtime/lib/accessors/builders/MessageBuilder.ts b/packages/apps/deno-runtime/lib/accessors/builders/MessageBuilder.ts new file mode 100644 index 0000000000000..032b4ba2552e9 --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/builders/MessageBuilder.ts @@ -0,0 +1,271 @@ +import { LayoutBlock } from '@rocket.chat/ui-kit'; + +import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; +import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks.ts'; + +import { BlockBuilder } from './BlockBuilder.ts'; +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class MessageBuilder implements IMessageBuilder { + public kind: _RocketChatAssociationModel.MESSAGE; + + private msg: IMessage; + + private changes: Partial = {}; + private attachmentsChanged = false; + private customFieldsChanged = false; + + constructor(message?: IMessage) { + this.kind = RocketChatAssociationModel.MESSAGE; + this.msg = message || ({} as IMessage); + } + + public setData(data: IMessage): IMessageBuilder { + delete data.id; + this.msg = data; + + return this as IMessageBuilder; + } + + public setUpdateData(data: IMessage, editor: IUser): IMessageBuilder { + this.msg = data; + this.msg.editor = editor; + this.msg.editedAt = new Date(); + + this.changes = structuredClone(this.msg); + + return this as IMessageBuilder; + } + + public setThreadId(threadId: string): IMessageBuilder { + this.msg.threadId = threadId; + this.changes.threadId = threadId; + + return this as IMessageBuilder; + } + + public getThreadId(): string { + return this.msg.threadId!; + } + + public setRoom(room: IRoom): IMessageBuilder { + this.msg.room = room; + this.changes.room = room; + + return this as IMessageBuilder; + } + + public getRoom(): IRoom { + return this.msg.room; + } + + public setSender(sender: IUser): IMessageBuilder { + this.msg.sender = sender; + this.changes.sender = sender; + + return this as IMessageBuilder; + } + + public getSender(): IUser { + return this.msg.sender; + } + + public setText(text: string): IMessageBuilder { + this.msg.text = text; + this.changes.text = text; + + return this as IMessageBuilder; + } + + public getText(): string { + return this.msg.text!; + } + + public setEmojiAvatar(emoji: string): IMessageBuilder { + this.msg.emoji = emoji; + this.changes.emoji = emoji; + + return this as IMessageBuilder; + } + + public getEmojiAvatar(): string { + return this.msg.emoji!; + } + + public setAvatarUrl(avatarUrl: string): IMessageBuilder { + this.msg.avatarUrl = avatarUrl; + this.changes.avatarUrl = avatarUrl; + + return this as IMessageBuilder; + } + + public getAvatarUrl(): string { + return this.msg.avatarUrl!; + } + + public setUsernameAlias(alias: string): IMessageBuilder { + this.msg.alias = alias; + this.changes.alias = alias; + + return this as IMessageBuilder; + } + + public getUsernameAlias(): string { + return this.msg.alias!; + } + + public addAttachment(attachment: IMessageAttachment): IMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + this.msg.attachments.push(attachment); + this.attachmentsChanged = true; + + return this as IMessageBuilder; + } + + public setAttachments(attachments: Array): IMessageBuilder { + this.msg.attachments = attachments; + this.attachmentsChanged = true; + + return this as IMessageBuilder; + } + + public getAttachments(): Array { + return this.msg.attachments!; + } + + public replaceAttachment(position: number, attachment: IMessageAttachment): IMessageBuilder { + if (!this.msg.attachments?.[position]) { + throw new Error(`No attachment found at the index of "${position}" to replace.`); + } + + this.msg.attachments[position] = attachment; + this.attachmentsChanged = true; + + return this as IMessageBuilder; + } + + public removeAttachment(position: number): IMessageBuilder { + if (!this.msg.attachments?.[position]) { + throw new Error(`No attachment found at the index of "${position}" to remove.`); + } + + this.msg.attachments.splice(position, 1); + this.attachmentsChanged = true; + + return this as IMessageBuilder; + } + + public setEditor(user: IUser): IMessageBuilder { + this.msg.editor = user; + this.changes.editor = user; + + return this as IMessageBuilder; + } + + public getEditor(): IUser { + return this.msg.editor; + } + + public setGroupable(groupable: boolean): IMessageBuilder { + this.msg.groupable = groupable; + this.changes.groupable = groupable; + + return this as IMessageBuilder; + } + + public getGroupable(): boolean { + return this.msg.groupable!; + } + + public setParseUrls(parseUrls: boolean): IMessageBuilder { + this.msg.parseUrls = parseUrls; + this.changes.parseUrls = parseUrls; + + return this as IMessageBuilder; + } + + public getParseUrls(): boolean { + return this.msg.parseUrls!; + } + + public getMessage(): IMessage { + if (!this.msg.room) { + throw new Error('The "room" property is required.'); + } + + return this.msg; + } + + public addBlocks(blocks: BlockBuilder | Array) { + if (!Array.isArray(this.msg.blocks)) { + this.msg.blocks = []; + } + + if (blocks instanceof BlockBuilder) { + this.msg.blocks.push(...blocks.getBlocks()); + } else { + this.msg.blocks.push(...blocks); + } + + return this as IMessageBuilder; + } + + public setBlocks(blocks: BlockBuilder | Array) { + const blockArray: Array = blocks instanceof BlockBuilder ? blocks.getBlocks() : blocks; + + this.msg.blocks = blockArray; + this.changes.blocks = blockArray; + + return this as IMessageBuilder; + } + + public getBlocks() { + return this.msg.blocks!; + } + + public addCustomField(key: string, value: unknown): IMessageBuilder { + if (!this.msg.customFields) { + this.msg.customFields = {}; + } + + if (this.msg.customFields[key]) { + throw new Error(`The message already contains a custom field by the key: ${key}`); + } + + if (key.includes('.')) { + throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`); + } + + this.msg.customFields[key] = value; + + this.customFieldsChanged = true; + + return this as IMessageBuilder; + } + + public getChanges(): Partial { + const changes: typeof this.changes = structuredClone(this.changes); + + if (this.attachmentsChanged) { + changes.attachments = structuredClone(this.msg.attachments); + } + + if (this.customFieldsChanged) { + changes.customFields = structuredClone(this.msg.customFields); + } + + return changes; + } +} diff --git a/packages/apps/deno-runtime/lib/accessors/builders/RoomBuilder.ts b/packages/apps/deno-runtime/lib/accessors/builders/RoomBuilder.ts new file mode 100644 index 0000000000000..208d476d32162 --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/builders/RoomBuilder.ts @@ -0,0 +1,197 @@ +import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; + +import type { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; + +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class RoomBuilder implements IRoomBuilder { + public kind: _RocketChatAssociationModel.ROOM | _RocketChatAssociationModel.DISCUSSION; + + protected room: IRoom; + + private members: Array; + + private changes: Partial = {}; + private customFieldsChanged = false; + + constructor(data?: Partial) { + this.kind = RocketChatAssociationModel.ROOM; + this.room = (data || { customFields: {} }) as IRoom; + this.members = []; + } + + public setData(data: Partial): IRoomBuilder { + delete data.id; + this.room = data as IRoom; + + this.changes = structuredClone(this.room); + + return this; + } + + public setDisplayName(name: string): IRoomBuilder { + this.room.displayName = name; + this.changes.displayName = name; + + return this; + } + + public getDisplayName(): string { + return this.room.displayName!; + } + + public setSlugifiedName(name: string): IRoomBuilder { + this.room.slugifiedName = name; + this.changes.slugifiedName = name; + + return this; + } + + public getSlugifiedName(): string { + return this.room.slugifiedName; + } + + public setType(type: RoomType): IRoomBuilder { + this.room.type = type; + this.changes.type = type; + + return this; + } + + public getType(): RoomType { + return this.room.type; + } + + public setCreator(creator: IUser): IRoomBuilder { + this.room.creator = creator; + this.changes.creator = creator; + + return this; + } + + public getCreator(): IUser { + return this.room.creator; + } + + /** + * @deprecated + */ + public addUsername(username: string): IRoomBuilder { + this.addMemberToBeAddedByUsername(username); + return this; + } + + /** + * @deprecated + */ + public setUsernames(usernames: Array): IRoomBuilder { + this.setMembersToBeAddedByUsernames(usernames); + return this; + } + + /** + * @deprecated + */ + public getUsernames(): Array { + const usernames = this.getMembersToBeAddedUsernames(); + if (usernames && usernames.length > 0) { + return usernames; + } + return this.room.usernames || []; + } + + public addMemberToBeAddedByUsername(username: string): IRoomBuilder { + this.members.push(username); + return this; + } + + public setMembersToBeAddedByUsernames(usernames: Array): IRoomBuilder { + this.members = usernames; + return this; + } + + public getMembersToBeAddedUsernames(): Array { + return this.members; + } + + public setDefault(isDefault: boolean): IRoomBuilder { + this.room.isDefault = isDefault; + this.changes.isDefault = isDefault; + + return this; + } + + public getIsDefault(): boolean { + return this.room.isDefault!; + } + + public setReadOnly(isReadOnly: boolean): IRoomBuilder { + this.room.isReadOnly = isReadOnly; + this.changes.isReadOnly = isReadOnly; + + return this; + } + + public getIsReadOnly(): boolean { + return this.room.isReadOnly!; + } + + public setDisplayingOfSystemMessages(displaySystemMessages: boolean): IRoomBuilder { + this.room.displaySystemMessages = displaySystemMessages; + this.changes.displaySystemMessages = displaySystemMessages; + + return this; + } + + public getDisplayingOfSystemMessages(): boolean { + return this.room.displaySystemMessages!; + } + + public addCustomField(key: string, value: object): IRoomBuilder { + if (typeof this.room.customFields !== 'object') { + this.room.customFields = {}; + } + + this.room.customFields[key] = value; + + this.customFieldsChanged = true; + + return this; + } + + public setCustomFields(fields: { [key: string]: object }): IRoomBuilder { + this.room.customFields = fields; + this.customFieldsChanged = true; + + return this; + } + + public getCustomFields(): { [key: string]: object } { + return this.room.customFields!; + } + + public getUserIds(): Array { + return this.room.userIds!; + } + + public getRoom(): IRoom { + return this.room; + } + + public getChanges() { + const changes: Partial = structuredClone(this.changes); + + if (this.customFieldsChanged) { + changes.customFields = structuredClone(this.room.customFields); + } + + return changes; + } +} diff --git a/packages/apps/deno-runtime/lib/accessors/builders/UserBuilder.ts b/packages/apps/deno-runtime/lib/accessors/builders/UserBuilder.ts new file mode 100644 index 0000000000000..caaf9a69d5941 --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/builders/UserBuilder.ts @@ -0,0 +1,81 @@ +import type { IUserBuilder } from '@rocket.chat/apps-engine/definition/accessors/IUserBuilder.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; +import type { IUserSettings } from '@rocket.chat/apps-engine/definition/users/IUserSettings.ts'; +import type { IUserEmail } from '@rocket.chat/apps-engine/definition/users/IUserEmail.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; + +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class UserBuilder implements IUserBuilder { + public kind: _RocketChatAssociationModel.USER; + + private user: Partial; + + constructor(user?: Partial) { + this.kind = RocketChatAssociationModel.USER; + this.user = user || ({} as Partial); + } + + public setData(data: Partial): IUserBuilder { + delete data.id; + this.user = data; + + return this; + } + + public setEmails(emails: Array): IUserBuilder { + this.user.emails = emails; + return this; + } + + public getEmails(): Array { + return this.user.emails!; + } + + public setDisplayName(name: string): IUserBuilder { + this.user.name = name; + return this; + } + + public getDisplayName(): string { + return this.user.name!; + } + + public setUsername(username: string): IUserBuilder { + this.user.username = username; + return this; + } + + public getUsername(): string { + return this.user.username!; + } + + public setRoles(roles: Array): IUserBuilder { + this.user.roles = roles; + return this; + } + + public getRoles(): Array { + return this.user.roles!; + } + + public getSettings(): Partial { + return this.user.settings; + } + + public getUser(): Partial { + if (!this.user.username) { + throw new Error('The "username" property is required.'); + } + + if (!this.user.name) { + throw new Error('The "name" property is required.'); + } + + return this.user; + } +} diff --git a/packages/apps/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts b/packages/apps/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts new file mode 100644 index 0000000000000..e1bc3f3cf5b24 --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts @@ -0,0 +1,94 @@ +import type { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceBuilder.ts'; +import type { IGroupVideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference.ts'; + +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; + +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export type AppVideoConference = Pick & { + createdBy: IGroupVideoConference['createdBy']['_id']; +}; + +export class VideoConferenceBuilder implements IVideoConferenceBuilder { + public kind: _RocketChatAssociationModel.VIDEO_CONFERENCE = RocketChatAssociationModel.VIDEO_CONFERENCE; + + protected call: AppVideoConference; + + constructor(data?: Partial) { + this.call = (data || {}) as AppVideoConference; + } + + public setData(data: Partial): IVideoConferenceBuilder { + this.call = { + rid: data.rid!, + createdBy: data.createdBy, + providerName: data.providerName!, + title: data.title!, + discussionRid: data.discussionRid, + }; + + return this; + } + + public setRoomId(rid: string): IVideoConferenceBuilder { + this.call.rid = rid; + return this; + } + + public getRoomId(): string { + return this.call.rid; + } + + public setCreatedBy(userId: string): IVideoConferenceBuilder { + this.call.createdBy = userId; + return this; + } + + public getCreatedBy(): string { + return this.call.createdBy; + } + + public setProviderName(userId: string): IVideoConferenceBuilder { + this.call.providerName = userId; + return this; + } + + public getProviderName(): string { + return this.call.providerName; + } + + public setProviderData(data: Record | undefined): IVideoConferenceBuilder { + this.call.providerData = data; + return this; + } + + public getProviderData(): Record { + return this.call.providerData!; + } + + public setTitle(userId: string): IVideoConferenceBuilder { + this.call.title = userId; + return this; + } + + public getTitle(): string { + return this.call.title; + } + + public setDiscussionRid(rid: AppVideoConference['discussionRid']): IVideoConferenceBuilder { + this.call.discussionRid = rid; + return this; + } + + public getDiscussionRid(): AppVideoConference['discussionRid'] { + return this.call.discussionRid; + } + + public getVideoConference(): AppVideoConference { + return this.call; + } +} diff --git a/packages/apps/deno-runtime/lib/accessors/extenders/HttpExtender.ts b/packages/apps/deno-runtime/lib/accessors/extenders/HttpExtender.ts new file mode 100644 index 0000000000000..8342850975017 --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/extenders/HttpExtender.ts @@ -0,0 +1,58 @@ +import type { IHttpExtend, IHttpPreRequestHandler, IHttpPreResponseHandler } from '@rocket.chat/apps-engine/definition/accessors/IHttp.ts'; + +export class HttpExtend implements IHttpExtend { + private headers: Map; + + private params: Map; + + private requests: Array; + + private responses: Array; + + constructor() { + this.headers = new Map(); + this.params = new Map(); + this.requests = []; + this.responses = []; + } + + public provideDefaultHeader(key: string, value: string): void { + this.headers.set(key, value); + } + + public provideDefaultHeaders(headers: { [key: string]: string }): void { + Object.keys(headers).forEach((key) => this.headers.set(key, headers[key])); + } + + public provideDefaultParam(key: string, value: string): void { + this.params.set(key, value); + } + + public provideDefaultParams(params: { [key: string]: string }): void { + Object.keys(params).forEach((key) => this.params.set(key, params[key])); + } + + public providePreRequestHandler(handler: IHttpPreRequestHandler): void { + this.requests.push(handler); + } + + public providePreResponseHandler(handler: IHttpPreResponseHandler): void { + this.responses.push(handler); + } + + public getDefaultHeaders(): Map { + return new Map(this.headers); + } + + public getDefaultParams(): Map { + return new Map(this.params); + } + + public getPreRequestHandlers(): Array { + return Array.from(this.requests); + } + + public getPreResponseHandlers(): Array { + return Array.from(this.responses); + } +} diff --git a/packages/apps/deno-runtime/lib/accessors/extenders/MessageExtender.ts b/packages/apps/deno-runtime/lib/accessors/extenders/MessageExtender.ts new file mode 100644 index 0000000000000..1f45137e15d4b --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/extenders/MessageExtender.ts @@ -0,0 +1,66 @@ +import type { IMessageExtender } from '@rocket.chat/apps-engine/definition/accessors/IMessageExtender.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; +import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment.ts'; + +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class MessageExtender implements IMessageExtender { + public readonly kind: _RocketChatAssociationModel.MESSAGE; + + constructor(private msg: IMessage) { + this.kind = RocketChatAssociationModel.MESSAGE; + + if (!Array.isArray(msg.attachments)) { + this.msg.attachments = []; + } + } + + public addCustomField(key: string, value: unknown): IMessageExtender { + if (!this.msg.customFields) { + this.msg.customFields = {}; + } + + if (this.msg.customFields[key]) { + throw new Error(`The message already contains a custom field by the key: ${key}`); + } + + if (key.includes('.')) { + throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`); + } + + this.msg.customFields[key] = value; + + return this; + } + + public addAttachment(attachment: IMessageAttachment): IMessageExtender { + this.ensureAttachment(); + + this.msg.attachments!.push(attachment); + + return this; + } + + public addAttachments(attachments: Array): IMessageExtender { + this.ensureAttachment(); + + this.msg.attachments = this.msg.attachments!.concat(attachments); + + return this; + } + + public getMessage(): IMessage { + return structuredClone(this.msg); + } + + private ensureAttachment(): void { + if (!Array.isArray(this.msg.attachments)) { + this.msg.attachments = []; + } + } +} diff --git a/packages/apps/deno-runtime/lib/accessors/extenders/RoomExtender.ts b/packages/apps/deno-runtime/lib/accessors/extenders/RoomExtender.ts new file mode 100644 index 0000000000000..a138e08d2d28e --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/extenders/RoomExtender.ts @@ -0,0 +1,61 @@ +import type { IRoomExtender } from '@rocket.chat/apps-engine/definition/accessors/IRoomExtender.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; + +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class RoomExtender implements IRoomExtender { + public kind: _RocketChatAssociationModel.ROOM; + + private members: Array; + + constructor(private room: IRoom) { + this.kind = RocketChatAssociationModel.ROOM; + this.members = []; + } + + public addCustomField(key: string, value: unknown): IRoomExtender { + if (!this.room.customFields) { + this.room.customFields = {}; + } + + if (this.room.customFields[key]) { + throw new Error(`The room already contains a custom field by the key: ${key}`); + } + + if (key.includes('.')) { + throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`); + } + + this.room.customFields[key] = value; + + return this; + } + + public addMember(user: IUser): IRoomExtender { + if (this.members.find((u) => u.username === user.username)) { + throw new Error('The user is already in the room.'); + } + + this.members.push(user); + + return this; + } + + public getMembersBeingAdded(): Array { + return this.members; + } + + public getUsernamesOfMembersBeingAdded(): Array { + return this.members.map((u) => u.username); + } + + public getRoom(): IRoom { + return structuredClone(this.room); + } +} diff --git a/packages/apps/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts b/packages/apps/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts new file mode 100644 index 0000000000000..c4b154f46cb32 --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts @@ -0,0 +1,69 @@ +import type { IVideoConferenceExtender } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceExtend.ts'; +import type { VideoConference, VideoConferenceMember } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference.ts'; +import type { IVideoConferenceUser } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConferenceUser.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; + +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class VideoConferenceExtender implements IVideoConferenceExtender { + public kind: _RocketChatAssociationModel.VIDEO_CONFERENCE; + + constructor(private videoConference: VideoConference) { + this.kind = RocketChatAssociationModel.VIDEO_CONFERENCE; + } + + public setProviderData(value: Record): IVideoConferenceExtender { + this.videoConference.providerData = value; + + return this; + } + + public setStatus(value: VideoConference['status']): IVideoConferenceExtender { + this.videoConference.status = value; + + return this; + } + + public setEndedBy(value: IVideoConferenceUser['_id']): IVideoConferenceExtender { + this.videoConference.endedBy = { + _id: value, + // Name and username will be loaded automatically by the bridge + username: '', + name: '', + }; + + return this; + } + + public setEndedAt(value: VideoConference['endedAt']): IVideoConferenceExtender { + this.videoConference.endedAt = value; + + return this; + } + + public addUser(userId: VideoConferenceMember['_id'], ts?: VideoConferenceMember['ts']): IVideoConferenceExtender { + this.videoConference.users.push({ + _id: userId, + ts, + // Name and username will be loaded automatically by the bridge + username: '', + name: '', + }); + + return this; + } + + public setDiscussionRid(rid: VideoConference['discussionRid']): IVideoConferenceExtender { + this.videoConference.discussionRid = rid; + + return this; + } + + public getVideoConference(): VideoConference { + return structuredClone(this.videoConference); + } +} diff --git a/packages/apps/deno-runtime/lib/accessors/formatResponseErrorHandler.ts b/packages/apps/deno-runtime/lib/accessors/formatResponseErrorHandler.ts new file mode 100644 index 0000000000000..6840c3ab5baa3 --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/formatResponseErrorHandler.ts @@ -0,0 +1,14 @@ +import { ErrorObject } from 'jsonrpc-lite'; + +// deno-lint-ignore no-explicit-any -- that is the type we get from `catch` +export const formatErrorResponse = (error: any): Error => { + if (error instanceof ErrorObject || typeof error?.error?.message === 'string') { + return new Error(error.error.message); + } + + if (error instanceof Error) { + return error; + } + + return new Error('An unknown error occurred', { cause: error }); +}; diff --git a/packages/apps/deno-runtime/lib/accessors/http.ts b/packages/apps/deno-runtime/lib/accessors/http.ts new file mode 100644 index 0000000000000..41f1025150fdc --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/http.ts @@ -0,0 +1,92 @@ +import type { IHttp, IHttpExtend, IHttpRequest, IHttpResponse } from '@rocket.chat/apps-engine/definition/accessors/IHttp.ts'; +import type { IPersistence } from '@rocket.chat/apps-engine/definition/accessors/IPersistence.ts'; +import type { IRead } from '@rocket.chat/apps-engine/definition/accessors/IRead.ts'; + +import * as Messenger from '../messenger.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { formatErrorResponse } from './formatResponseErrorHandler.ts'; + +type RequestMethod = 'get' | 'post' | 'put' | 'head' | 'delete' | 'patch'; + +export class Http implements IHttp { + private httpExtender: IHttpExtend; + private read: IRead; + private persistence: IPersistence; + private senderFn: typeof Messenger.sendRequest; + + constructor(read: IRead, persistence: IPersistence, httpExtender: IHttpExtend, senderFn: typeof Messenger.sendRequest) { + this.read = read; + this.persistence = persistence; + this.httpExtender = httpExtender; + this.senderFn = senderFn; + // this.httpExtender = new HttpExtend(); + } + + public get(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'get', options); + } + + public put(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'put', options); + } + + public post(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'post', options); + } + + public del(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'delete', options); + } + + public patch(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'patch', options); + } + + private async _processHandler(url: string, method: RequestMethod, options?: IHttpRequest): Promise { + let request = options || {}; + + if (typeof request.headers === 'undefined') { + request.headers = {}; + } + + this.httpExtender.getDefaultHeaders().forEach((value: string, key: string) => { + if (typeof request.headers?.[key] !== 'string') { + request.headers![key] = value; + } + }); + + if (typeof request.params === 'undefined') { + request.params = {}; + } + + this.httpExtender.getDefaultParams().forEach((value: string, key: string) => { + if (typeof request.params?.[key] !== 'string') { + request.params![key] = value; + } + }); + + for (const handler of this.httpExtender.getPreRequestHandlers()) { + request = await handler.executePreHttpRequest(url, request, this.read, this.persistence); + } + + let { result: response } = await this.senderFn({ + method: `bridges:getHttpBridge:doCall`, + params: [ + { + appId: AppObjectRegistry.get('id'), + method, + url, + request, + }, + ], + }).catch((error) => { + throw formatErrorResponse(error); + }); + + for (const handler of this.httpExtender.getPreResponseHandlers()) { + response = await handler.executePreHttpResponse(response as IHttpResponse, this.read, this.persistence); + } + + return response as IHttpResponse; + } +} diff --git a/packages/apps/deno-runtime/lib/accessors/mod.ts b/packages/apps/deno-runtime/lib/accessors/mod.ts new file mode 100644 index 0000000000000..fc2fb6a3f6669 --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/mod.ts @@ -0,0 +1,322 @@ +import type { IAppAccessors } from '@rocket.chat/apps-engine/definition/accessors/IAppAccessors.ts'; +import type { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api/IApiEndpointMetadata.ts'; +import type { IEnvironmentWrite } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentWrite.ts'; +import type { IEnvironmentRead } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentRead.ts'; +import type { IConfigurationModify } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationModify.ts'; +import type { IRead } from '@rocket.chat/apps-engine/definition/accessors/IRead.ts'; +import type { IModify } from '@rocket.chat/apps-engine/definition/accessors/IModify.ts'; +import type { INotifier } from '@rocket.chat/apps-engine/definition/accessors/INotifier.ts'; +import type { IPersistence } from '@rocket.chat/apps-engine/definition/accessors/IPersistence.ts'; +import type { IHttp, IHttpExtend } from '@rocket.chat/apps-engine/definition/accessors/IHttp.ts'; +import type { IConfigurationExtend } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationExtend.ts'; +import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands/ISlashCommand.ts'; +import type { IProcessor } from '@rocket.chat/apps-engine/definition/scheduler/IProcessor.ts'; +import type { IApi } from '@rocket.chat/apps-engine/definition/api/IApi.ts'; +import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders/IVideoConfProvider.ts'; +import type { + IOutboundPhoneMessageProvider, + IOutboundEmailMessageProvider, +} from '@rocket.chat/apps-engine/definition/outboundCommunication/IOutboundCommsProvider.ts'; + +import { Http } from './http.ts'; +import { HttpExtend } from './extenders/HttpExtender.ts'; +import * as Messenger from '../messenger.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { ModifyCreator } from './modify/ModifyCreator.ts'; +import { ModifyUpdater } from './modify/ModifyUpdater.ts'; +import { ModifyExtender } from './modify/ModifyExtender.ts'; +import { Notifier } from './notifier.ts'; +import { formatErrorResponse } from './formatResponseErrorHandler.ts'; + +const httpMethods = ['get', 'post', 'put', 'delete', 'head', 'options', 'patch'] as const; + +// We need to create this object first thing, as we'll handle references to it later on +if (!AppObjectRegistry.has('apiEndpoints')) { + AppObjectRegistry.set('apiEndpoints', []); +} + +export class AppAccessors { + private defaultAppAccessors?: IAppAccessors; + private environmentRead?: IEnvironmentRead; + private environmentWriter?: IEnvironmentWrite; + private configModifier?: IConfigurationModify; + private configExtender?: IConfigurationExtend; + private reader?: IRead; + private modifier?: IModify; + private persistence?: IPersistence; + private creator?: ModifyCreator; + private updater?: ModifyUpdater; + private extender?: ModifyExtender; + private httpExtend: IHttpExtend = new HttpExtend(); + private http?: IHttp; + private notifier?: INotifier; + + private proxify: (namespace: string, overrides?: Record unknown>) => T; + + constructor(private readonly senderFn: typeof Messenger.sendRequest) { + this.proxify = (namespace: string, overrides: Record unknown> = {}): T => + new Proxy( + { __kind: `accessor:${namespace}` }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => { + // We don't want to send a request for this prop + if (prop === 'toJSON') { + return {}; + } + + // If the prop is inteded to be overriden by the caller + if (prop in overrides) { + return overrides[prop].apply(undefined, params); + } + + return senderFn({ + method: `accessor:${namespace}:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw formatErrorResponse(err); + }); + }, + }, + ) as T; + + this.http = new Http(this.getReader(), this.getPersistence(), this.httpExtend, this.getSenderFn()); + this.notifier = new Notifier(this.getSenderFn()); + } + + public getSenderFn() { + return this.senderFn; + } + + public getEnvironmentRead(): IEnvironmentRead { + if (!this.environmentRead) { + this.environmentRead = { + getSettings: () => this.proxify('getEnvironmentRead:getSettings'), + getServerSettings: () => this.proxify('getEnvironmentRead:getServerSettings'), + getEnvironmentVariables: () => this.proxify('getEnvironmentRead:getEnvironmentVariables'), + }; + } + + return this.environmentRead; + } + + public getEnvironmentWrite() { + if (!this.environmentWriter) { + this.environmentWriter = { + getSettings: () => this.proxify('getEnvironmentWrite:getSettings'), + getServerSettings: () => this.proxify('getEnvironmentWrite:getServerSettings'), + }; + } + + return this.environmentWriter; + } + + public getConfigurationModify() { + if (!this.configModifier) { + this.configModifier = { + scheduler: this.proxify('getConfigurationModify:scheduler'), + slashCommands: { + _proxy: this.proxify('getConfigurationModify:slashCommands'), + modifySlashCommand(slashcommand: ISlashCommand) { + // Store the slashcommand instance to use when the Apps-Engine calls the slashcommand + AppObjectRegistry.set(`slashcommand:${slashcommand.command}`, slashcommand); + + return this._proxy.modifySlashCommand(slashcommand); + }, + disableSlashCommand(command: string) { + return this._proxy.disableSlashCommand(command); + }, + enableSlashCommand(command: string) { + return this._proxy.enableSlashCommand(command); + }, + }, + serverSettings: this.proxify('getConfigurationModify:serverSettings'), + }; + } + + return this.configModifier; + } + + public getConfigurationExtend() { + if (!this.configExtender) { + const senderFn = this.senderFn; + + this.configExtender = { + ui: this.proxify('getConfigurationExtend:ui'), + http: this.httpExtend, + settings: this.proxify('getConfigurationExtend:settings'), + externalComponents: this.proxify('getConfigurationExtend:externalComponents'), + api: { + _proxy: this.proxify('getConfigurationExtend:api'), + async provideApi(api: IApi) { + const apiEndpoints = AppObjectRegistry.get('apiEndpoints')!; + + api.endpoints.forEach((endpoint) => { + endpoint._availableMethods = httpMethods.filter((method) => typeof endpoint[method] === 'function'); + + // We need to keep a reference to the endpoint around for us to call the executor later + AppObjectRegistry.set(`api:${endpoint.path}`, endpoint); + }); + + const result = await this._proxy.provideApi(api); + + // Let's call the listApis method to cache the info from the endpoints + // Also, since this is a side-effect, we do it async so we can return to the caller + senderFn({ method: 'accessor:api:listApis' }) + .then((response) => apiEndpoints.push(...(response.result as IApiEndpointMetadata[]))) + .catch((err) => err.error); + + return result; + }, + }, + scheduler: { + _proxy: this.proxify('getConfigurationExtend:scheduler'), + registerProcessors(processors: IProcessor[]) { + // Store the processor instance to use when the Apps-Engine calls the processor + processors.forEach((processor) => { + AppObjectRegistry.set(`scheduler:${processor.id}`, processor); + }); + + return this._proxy.registerProcessors(processors); + }, + }, + videoConfProviders: { + _proxy: this.proxify('getConfigurationExtend:videoConfProviders'), + provideVideoConfProvider(provider: IVideoConfProvider) { + // Store the videoConfProvider instance to use when the Apps-Engine calls the videoConfProvider + AppObjectRegistry.set(`videoConfProvider:${provider.name}`, provider); + + return this._proxy.provideVideoConfProvider(provider); + }, + }, + outboundCommunication: { + _proxy: this.proxify('getConfigurationExtend:outboundCommunication'), + registerEmailProvider(provider: IOutboundEmailMessageProvider) { + AppObjectRegistry.set(`outboundCommunication:${provider.name}-${provider.type}`, provider); + return this._proxy.registerEmailProvider(provider); + }, + registerPhoneProvider(provider: IOutboundPhoneMessageProvider) { + AppObjectRegistry.set(`outboundCommunication:${provider.name}-${provider.type}`, provider); + return this._proxy.registerPhoneProvider(provider); + }, + }, + slashCommands: { + _proxy: this.proxify('getConfigurationExtend:slashCommands'), + provideSlashCommand(slashcommand: ISlashCommand) { + // Store the slashcommand instance to use when the Apps-Engine calls the slashcommand + AppObjectRegistry.set(`slashcommand:${slashcommand.command}`, slashcommand); + + return this._proxy.provideSlashCommand(slashcommand); + }, + }, + }; + } + + return this.configExtender; + } + + public getDefaultAppAccessors() { + if (!this.defaultAppAccessors) { + this.defaultAppAccessors = { + environmentReader: this.getEnvironmentRead(), + environmentWriter: this.getEnvironmentWrite(), + reader: this.getReader(), + http: this.getHttp(), + providedApiEndpoints: AppObjectRegistry.get('apiEndpoints') as IApiEndpointMetadata[], + }; + } + + return this.defaultAppAccessors; + } + + public getReader() { + if (!this.reader) { + this.reader = { + getEnvironmentReader: () => ({ + getSettings: () => this.proxify('getReader:getEnvironmentReader:getSettings'), + getServerSettings: () => this.proxify('getReader:getEnvironmentReader:getServerSettings'), + getEnvironmentVariables: () => this.proxify('getReader:getEnvironmentReader:getEnvironmentVariables'), + }), + getMessageReader: () => this.proxify('getReader:getMessageReader'), + getPersistenceReader: () => this.proxify('getReader:getPersistenceReader'), + getRoomReader: () => this.proxify('getReader:getRoomReader'), + getUserReader: () => this.proxify('getReader:getUserReader'), + getNotifier: () => this.getNotifier(), + getLivechatReader: () => this.proxify('getReader:getLivechatReader'), + getUploadReader: () => this.proxify('getReader:getUploadReader'), + getCloudWorkspaceReader: () => this.proxify('getReader:getCloudWorkspaceReader'), + getVideoConferenceReader: () => this.proxify('getReader:getVideoConferenceReader'), + getOAuthAppsReader: () => this.proxify('getReader:getOAuthAppsReader'), + getThreadReader: () => this.proxify('getReader:getThreadReader'), + getRoleReader: () => this.proxify('getReader:getRoleReader'), + getContactReader: () => this.proxify('getReader:getContactReader'), + getExperimentalReader: () => this.proxify('getReader:getExperimentalReader'), + }; + } + + return this.reader; + } + + public getModifier() { + if (!this.modifier) { + this.modifier = { + getCreator: this.getCreator.bind(this), + getUpdater: this.getUpdater.bind(this), + getExtender: this.getExtender.bind(this), + getDeleter: () => this.proxify('getModifier:getDeleter'), + getNotifier: () => this.getNotifier(), + getUiController: () => this.proxify('getModifier:getUiController'), + getScheduler: () => this.proxify('getModifier:getScheduler'), + getOAuthAppsModifier: () => this.proxify('getModifier:getOAuthAppsModifier'), + getModerationModifier: () => this.proxify('getModifier:getModerationModifier'), + }; + } + + return this.modifier; + } + + public getPersistence() { + if (!this.persistence) { + this.persistence = this.proxify('getPersistence'); + } + + return this.persistence; + } + + public getHttp() { + return this.http; + } + + private getCreator() { + if (!this.creator) { + this.creator = new ModifyCreator(this.senderFn); + } + + return this.creator; + } + + private getUpdater() { + if (!this.updater) { + this.updater = new ModifyUpdater(this.senderFn); + } + + return this.updater; + } + + private getExtender() { + if (!this.extender) { + this.extender = new ModifyExtender(this.senderFn); + } + + return this.extender; + } + + private getNotifier() { + return this.notifier; + } +} + +export const AppAccessorsInstance = new AppAccessors(Messenger.sendRequest); diff --git a/packages/apps/deno-runtime/lib/accessors/modify/ModifyCreator.ts b/packages/apps/deno-runtime/lib/accessors/modify/ModifyCreator.ts new file mode 100644 index 0000000000000..d30e22c1be182 --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/modify/ModifyCreator.ts @@ -0,0 +1,383 @@ +import type { IModifyCreator } from '@rocket.chat/apps-engine/definition/accessors/IModifyCreator.ts'; +import type { IUploadCreator } from '@rocket.chat/apps-engine/definition/accessors/IUploadCreator.ts'; +import type { IEmailCreator } from '@rocket.chat/apps-engine/definition/accessors/IEmailCreator.ts'; +import type { IContactCreator } from '@rocket.chat/apps-engine/definition/accessors/IContactCreator.ts'; +import type { ILivechatCreator } from '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { IBotUser } from '@rocket.chat/apps-engine/definition/users/IBotUser.ts'; +import type { UserType as _UserType } from '@rocket.chat/apps-engine/definition/users/UserType.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; +import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts'; +import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts'; +import type { IUserBuilder } from '@rocket.chat/apps-engine/definition/accessors/IUserBuilder.ts'; +import type { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceBuilder.ts'; +import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; +import type { ILivechatMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/ILivechatMessageBuilder.ts'; +import type { UIHelper as _UIHelper } from '@rocket.chat/apps-engine/server/misc/UIHelper.ts'; + +import * as Messenger from '../../messenger.ts'; +import { randomBytes } from 'node:crypto'; + +import { BlockBuilder } from '../builders/BlockBuilder.ts'; +import { MessageBuilder } from '../builders/MessageBuilder.ts'; +import { DiscussionBuilder, IDiscussionBuilder } from '../builders/DiscussionBuilder.ts'; +import { ILivechatMessage, LivechatMessageBuilder } from '../builders/LivechatMessageBuilder.ts'; +import { RoomBuilder } from '../builders/RoomBuilder.ts'; +import { UserBuilder } from '../builders/UserBuilder.ts'; +import { AppVideoConference, VideoConferenceBuilder } from '../builders/VideoConferenceBuilder.ts'; +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { require } from '../../../lib/require.ts'; +import { formatErrorResponse } from '../formatResponseErrorHandler.ts'; + +const { UIHelper } = require('@rocket.chat/apps-engine/server/misc/UIHelper.js') as { UIHelper: typeof _UIHelper }; +const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; +const { UserType } = require('@rocket.chat/apps-engine/definition/users/UserType.js') as { UserType: typeof _UserType }; +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class ModifyCreator implements IModifyCreator { + constructor(private readonly senderFn: typeof Messenger.sendRequest) {} + + getLivechatCreator(): ILivechatCreator { + return new Proxy( + { __kind: 'getLivechatCreator' }, + { + get: (_target: unknown, prop: string) => { + // It's not worthwhile to make an asynchronous request for such a simple method + if (prop === 'createToken') { + return () => randomBytes(16).toString('hex'); + } + + if (prop === 'toJSON') { + return () => ({}); + } + + return (...params: unknown[]) => + this.senderFn({ + method: `accessor:getModifier:getCreator:getLivechatCreator:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw formatErrorResponse(err); + }); + }, + }, + ) as ILivechatCreator; + } + + getUploadCreator(): IUploadCreator { + return new Proxy( + { __kind: 'getUploadCreator' }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + prop === 'toJSON' + ? {} + : this.senderFn({ + method: `accessor:getModifier:getCreator:getUploadCreator:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw formatErrorResponse(err); + }), + }, + ) as IUploadCreator; + } + + getEmailCreator(): IEmailCreator { + return new Proxy( + { __kind: 'getEmailCreator' }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + prop === 'toJSON' + ? {} + : this.senderFn({ + method: `accessor:getModifier:getCreator:getEmailCreator:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw formatErrorResponse(err); + }), + }, + ); + } + + getContactCreator(): IContactCreator { + return new Proxy( + { __kind: 'getContactCreator' }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + prop === 'toJSON' + ? {} + : this.senderFn({ + method: `accessor:getModifier:getCreator:getContactCreator:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw formatErrorResponse(err); + }), + }, + ); + } + + getBlockBuilder() { + return new BlockBuilder(); + } + + startMessage(data?: IMessage) { + if (data) { + delete data.id; + } + + return new MessageBuilder(data); + } + + startLivechatMessage(data?: ILivechatMessage) { + if (data) { + delete data.id; + } + + return new LivechatMessageBuilder(data); + } + + startRoom(data?: IRoom) { + if (data) { + // @ts-ignore - this has been imported from the Apps-Engine + delete data.id; + } + + return new RoomBuilder(data); + } + + startDiscussion(data?: Partial) { + if (data) { + delete data.id; + } + + return new DiscussionBuilder(data); + } + + startVideoConference(data?: Partial) { + return new VideoConferenceBuilder(data); + } + + startBotUser(data?: Partial) { + if (data) { + delete data.id; + + const { roles } = data; + + if (roles?.length) { + const hasRole = roles + .map((role: string) => role.toLocaleLowerCase()) + .some((role: string) => role === 'admin' || role === 'owner' || role === 'moderator'); + + if (hasRole) { + throw new Error('Invalid role assigned to the user. Should not be admin, owner or moderator.'); + } + } + + if (!data.type) { + data.type = UserType.BOT; + } + } + + return new UserBuilder(data); + } + + public finish( + builder: IMessageBuilder | ILivechatMessageBuilder | IRoomBuilder | IDiscussionBuilder | IVideoConferenceBuilder | IUserBuilder, + ): Promise { + switch (builder.kind) { + case RocketChatAssociationModel.MESSAGE: + return this._finishMessage(builder as IMessageBuilder); + case RocketChatAssociationModel.LIVECHAT_MESSAGE: + return this._finishLivechatMessage(builder as ILivechatMessageBuilder); + case RocketChatAssociationModel.ROOM: + return this._finishRoom(builder as IRoomBuilder); + case RocketChatAssociationModel.DISCUSSION: + return this._finishDiscussion(builder as IDiscussionBuilder); + case RocketChatAssociationModel.VIDEO_CONFERENCE: + return this._finishVideoConference(builder as IVideoConferenceBuilder); + case RocketChatAssociationModel.USER: + return this._finishUser(builder as IUserBuilder); + default: + throw new Error('Invalid builder passed to the ModifyCreator.finish function.'); + } + } + + private async _finishMessage(builder: IMessageBuilder): Promise { + const result = builder.getMessage(); + delete result.id; + + if (!result.sender || !result.sender.id) { + const response = await this.senderFn({ + method: 'bridges:getUserBridge:doGetAppUser', + params: ['APP_ID'], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + const appUser = response.result; + + if (!appUser) { + throw new Error('Invalid sender assigned to the message.'); + } + + result.sender = appUser; + } + + if (result.blocks?.length) { + // Can we move this elsewhere? This AppObjectRegistry usage doesn't really belong here, but where? + result.blocks = UIHelper.assignIds(result.blocks, AppObjectRegistry.get('id') || ''); + } + + const response = await this.senderFn({ + method: 'bridges:getMessageBridge:doCreate', + params: [result, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + return String(response.result); + } + + private async _finishLivechatMessage(builder: ILivechatMessageBuilder): Promise { + if (builder.getSender() && !builder.getVisitor()) { + return this._finishMessage(builder.getMessageBuilder()); + } + + const result = builder.getMessage(); + delete result.id; + + if (!result.token && (!result.visitor || !result.visitor.token)) { + throw new Error('Invalid visitor sending the message'); + } + + result.token = result.visitor ? result.visitor.token : result.token; + + const response = await this.senderFn({ + method: 'bridges:getLivechatBridge:doCreateMessage', + params: [result, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + return String(response.result); + } + + private async _finishRoom(builder: IRoomBuilder): Promise { + const result = builder.getRoom(); + delete result.id; + + if (!result.type) { + throw new Error('Invalid type assigned to the room.'); + } + + if (result.type !== RoomType.LIVE_CHAT) { + if (!result.creator || !result.creator.id) { + throw new Error('Invalid creator assigned to the room.'); + } + } + + if (result.type !== RoomType.DIRECT_MESSAGE) { + if (result.type !== RoomType.LIVE_CHAT) { + if (!result.slugifiedName || !result.slugifiedName.trim()) { + throw new Error('Invalid slugifiedName assigned to the room.'); + } + } + + if (!result.displayName || !result.displayName.trim()) { + throw new Error('Invalid displayName assigned to the room.'); + } + } + + const response = await this.senderFn({ + method: 'bridges:getRoomBridge:doCreate', + params: [result, builder.getMembersToBeAddedUsernames(), AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + return String(response.result); + } + + private async _finishDiscussion(builder: IDiscussionBuilder): Promise { + const room = builder.getRoom(); + delete room.id; + + if (!room.creator || !room.creator.id) { + throw new Error('Invalid creator assigned to the discussion.'); + } + + if (!room.slugifiedName || !room.slugifiedName.trim()) { + throw new Error('Invalid slugifiedName assigned to the discussion.'); + } + + if (!room.displayName || !room.displayName.trim()) { + throw new Error('Invalid displayName assigned to the discussion.'); + } + + if (!room.parentRoom || !room.parentRoom.id) { + throw new Error('Invalid parentRoom assigned to the discussion.'); + } + + const response = await this.senderFn({ + method: 'bridges:getRoomBridge:doCreateDiscussion', + params: [room, builder.getParentMessage(), builder.getReply(), builder.getMembersToBeAddedUsernames(), AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + return String(response.result); + } + + private async _finishVideoConference(builder: IVideoConferenceBuilder): Promise { + const videoConference = builder.getVideoConference(); + + if (!videoConference.createdBy) { + throw new Error('Invalid creator assigned to the video conference.'); + } + + if (!videoConference.providerName?.trim()) { + throw new Error('Invalid provider name assigned to the video conference.'); + } + + if (!videoConference.rid) { + throw new Error('Invalid roomId assigned to the video conference.'); + } + + const response = await this.senderFn({ + method: 'bridges:getVideoConferenceBridge:doCreate', + params: [videoConference, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + return String(response.result); + } + + private async _finishUser(builder: IUserBuilder): Promise { + const user = builder.getUser(); + + const response = await this.senderFn({ + method: 'bridges:getUserBridge:doCreate', + params: [user, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + return String(response.result); + } +} diff --git a/packages/apps/deno-runtime/lib/accessors/modify/ModifyExtender.ts b/packages/apps/deno-runtime/lib/accessors/modify/ModifyExtender.ts new file mode 100644 index 0000000000000..5f8e0c53ec04a --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/modify/ModifyExtender.ts @@ -0,0 +1,106 @@ +import type { IModifyExtender } from '@rocket.chat/apps-engine/definition/accessors/IModifyExtender.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; +import type { IMessageExtender } from '@rocket.chat/apps-engine/definition/accessors/IMessageExtender.ts'; +import type { IRoomExtender } from '@rocket.chat/apps-engine/definition/accessors/IRoomExtender.ts'; +import type { IVideoConferenceExtender } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceExtend.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; +import type { VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; + +import * as Messenger from '../../messenger.ts'; +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { MessageExtender } from '../extenders/MessageExtender.ts'; +import { RoomExtender } from '../extenders/RoomExtender.ts'; +import { VideoConferenceExtender } from '../extenders/VideoConferenceExtend.ts'; +import { require } from '../../../lib/require.ts'; +import { formatErrorResponse } from '../formatResponseErrorHandler.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class ModifyExtender implements IModifyExtender { + constructor(private readonly senderFn: typeof Messenger.sendRequest) {} + + public async extendMessage(messageId: string, updater: IUser): Promise { + const result = await this.senderFn({ + method: 'bridges:getMessageBridge:doGetById', + params: [messageId, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + const msg = result.result as IMessage; + + msg.editor = updater; + msg.editedAt = new Date(); + + return new MessageExtender(msg); + } + + public async extendRoom(roomId: string, _updater: IUser): Promise { + const result = await this.senderFn({ + method: 'bridges:getRoomBridge:doGetById', + params: [roomId, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + const room = result.result as IRoom; + + room.updatedAt = new Date(); + + return new RoomExtender(room); + } + + public async extendVideoConference(id: string): Promise { + const result = await this.senderFn({ + method: 'bridges:getVideoConferenceBridge:doGetById', + params: [id, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + const call = result.result as VideoConference; + + call._updatedAt = new Date(); + + return new VideoConferenceExtender(call); + } + + public async finish(extender: IMessageExtender | IRoomExtender | IVideoConferenceExtender): Promise { + switch (extender.kind) { + case RocketChatAssociationModel.MESSAGE: + await this.senderFn({ + method: 'bridges:getMessageBridge:doUpdate', + params: [(extender as IMessageExtender).getMessage(), AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + break; + case RocketChatAssociationModel.ROOM: + await this.senderFn({ + method: 'bridges:getRoomBridge:doUpdate', + params: [ + (extender as IRoomExtender).getRoom(), + (extender as IRoomExtender).getUsernamesOfMembersBeingAdded(), + AppObjectRegistry.get('id'), + ], + }).catch((err) => { + throw formatErrorResponse(err); + }); + break; + case RocketChatAssociationModel.VIDEO_CONFERENCE: + await this.senderFn({ + method: 'bridges:getVideoConferenceBridge:doUpdate', + params: [(extender as IVideoConferenceExtender).getVideoConference(), AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + break; + default: + throw new Error('Invalid extender passed to the ModifyExtender.finish function.'); + } + } +} diff --git a/packages/apps/deno-runtime/lib/accessors/modify/ModifyUpdater.ts b/packages/apps/deno-runtime/lib/accessors/modify/ModifyUpdater.ts new file mode 100644 index 0000000000000..dc9178be1f52d --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/modify/ModifyUpdater.ts @@ -0,0 +1,170 @@ +import type { IModifyUpdater } from '@rocket.chat/apps-engine/definition/accessors/IModifyUpdater.ts'; +import type { ILivechatUpdater } from '@rocket.chat/apps-engine/definition/accessors/ILivechatUpdater.ts'; +import type { IUserUpdater } from '@rocket.chat/apps-engine/definition/accessors/IUserUpdater.ts'; +import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts'; +import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; + +import type { UIHelper as _UIHelper } from '@rocket.chat/apps-engine/server/misc/UIHelper.ts'; +import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; + +import * as Messenger from '../../messenger.ts'; + +import { MessageBuilder } from '../builders/MessageBuilder.ts'; +import { RoomBuilder } from '../builders/RoomBuilder.ts'; +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; + +import { require } from '../../../lib/require.ts'; +import { formatErrorResponse } from '../formatResponseErrorHandler.ts'; + +const { UIHelper } = require('@rocket.chat/apps-engine/server/misc/UIHelper.js') as { UIHelper: typeof _UIHelper }; +const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class ModifyUpdater implements IModifyUpdater { + constructor(private readonly senderFn: typeof Messenger.sendRequest) {} + + public getLivechatUpdater(): ILivechatUpdater { + return new Proxy( + { __kind: 'getLivechatUpdater' }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + prop === 'toJSON' + ? {} + : this.senderFn({ + method: `accessor:getModifier:getUpdater:getLivechatUpdater:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw formatErrorResponse(err); + }), + }, + ) as ILivechatUpdater; + } + + public getUserUpdater(): IUserUpdater { + return new Proxy( + { __kind: 'getUserUpdater' }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + prop === 'toJSON' + ? {} + : this.senderFn({ + method: `accessor:getModifier:getUpdater:getUserUpdater:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw formatErrorResponse(err); + }), + }, + ) as IUserUpdater; + } + + public async message(messageId: string, editor: IUser): Promise { + const response = await this.senderFn({ + method: 'bridges:getMessageBridge:doGetById', + params: [messageId, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + const builder = new MessageBuilder(response.result as IMessage); + + builder.setEditor(editor); + + return builder; + } + + public async room(roomId: string, _updater: IUser): Promise { + const response = await this.senderFn({ + method: 'bridges:getRoomBridge:doGetById', + params: [roomId, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + return new RoomBuilder(response.result as IRoom); + } + + public finish(builder: IMessageBuilder | IRoomBuilder): Promise { + switch (builder.kind) { + case RocketChatAssociationModel.MESSAGE: + return this._finishMessage(builder as MessageBuilder); + case RocketChatAssociationModel.ROOM: + return this._finishRoom(builder as RoomBuilder); + default: + throw new Error('Invalid builder passed to the ModifyUpdater.finish function.'); + } + } + + private async _finishMessage(builder: MessageBuilder): Promise { + const result = builder.getMessage(); + + if (!result.id) { + throw new Error("Invalid message, can't update a message without an id."); + } + + if (!result.sender?.id) { + throw new Error('Invalid sender assigned to the message.'); + } + + if (result.blocks?.length) { + result.blocks = UIHelper.assignIds(result.blocks, AppObjectRegistry.get('id') || ''); + } + + const changes = { id: result.id, ...builder.getChanges() }; + + await this.senderFn({ + method: 'bridges:getMessageBridge:doUpdate', + params: [changes, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + } + + private async _finishRoom(builder: RoomBuilder): Promise { + const room = builder.getRoom(); + + if (!room.id) { + throw new Error("Invalid room, can't update a room without an id."); + } + + if (!room.type) { + throw new Error('Invalid type assigned to the room.'); + } + + if (room.type !== RoomType.LIVE_CHAT) { + if (!room.creator || !room.creator.id) { + throw new Error('Invalid creator assigned to the room.'); + } + + if (!room.slugifiedName || !room.slugifiedName.trim()) { + throw new Error('Invalid slugifiedName assigned to the room.'); + } + } + + if (!room.displayName || !room.displayName.trim()) { + throw new Error('Invalid displayName assigned to the room.'); + } + + const changes = { id: room.id, ...builder.getChanges() }; + + await this.senderFn({ + method: 'bridges:getRoomBridge:doUpdate', + params: [changes, builder.getMembersToBeAddedUsernames(), AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + } +} diff --git a/packages/apps/deno-runtime/lib/accessors/notifier.ts b/packages/apps/deno-runtime/lib/accessors/notifier.ts new file mode 100644 index 0000000000000..1a85cc12b579f --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/notifier.ts @@ -0,0 +1,84 @@ +import type { IMessageBuilder, INotifier } from '@rocket.chat/apps-engine/definition/accessors'; +import type { ITypingOptions } from '@rocket.chat/apps-engine/definition/accessors/INotifier.ts'; +import type { _TypingScope } from '@rocket.chat/apps-engine/definition/accessors/INotifier.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import { MessageBuilder } from './builders/MessageBuilder.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import * as Messenger from '../messenger.ts'; +import { require } from '../require.ts'; +import { formatErrorResponse } from './formatResponseErrorHandler.ts'; + +const { TypingScope } = require('@rocket.chat/apps-engine/definition/accessors/INotifier.js') as { + TypingScope: typeof _TypingScope; +}; + +export class Notifier implements INotifier { + private senderFn: typeof Messenger.sendRequest; + + constructor(senderFn: typeof Messenger.sendRequest) { + this.senderFn = senderFn; + } + + public async notifyUser(user: IUser, message: IMessage): Promise { + if (!message.sender || !message.sender.id) { + const appUser = await this.getAppUser(); + + message.sender = appUser; + } + + await this.callMessageBridge('doNotifyUser', [user, message, AppObjectRegistry.get('id')]); + } + + public async notifyRoom(room: IRoom, message: IMessage): Promise { + if (!message.sender || !message.sender.id) { + const appUser = await this.getAppUser(); + + message.sender = appUser; + } + + await this.callMessageBridge('doNotifyRoom', [room, message, AppObjectRegistry.get('id')]); + } + + public async typing(options: ITypingOptions): Promise<() => Promise> { + options.scope = options.scope || TypingScope.Room; + + if (!options.username) { + const appUser = await this.getAppUser(); + options.username = (appUser && appUser.name) || ''; + } + + const appId = AppObjectRegistry.get('id'); + + await this.callMessageBridge('doTyping', [{ ...options, isTyping: true }, appId]); + + return async () => { + await this.callMessageBridge('doTyping', [{ ...options, isTyping: false }, appId]); + }; + } + + public getMessageBuilder(): IMessageBuilder { + return new MessageBuilder(); + } + + private async callMessageBridge(method: string, params: Array): Promise { + await this.senderFn({ + method: `bridges:getMessageBridge:${method}`, + params, + }).catch((err) => { + throw formatErrorResponse(err); + }); + } + + private async getAppUser(): Promise { + const response = await this.senderFn({ + method: 'bridges:getUserBridge:doGetAppUser', + params: [AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + return response.result; + } +} diff --git a/packages/apps/deno-runtime/lib/accessors/tests/AppAccessors.test.ts b/packages/apps/deno-runtime/lib/accessors/tests/AppAccessors.test.ts new file mode 100644 index 0000000000000..ffc77b6904bb7 --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/tests/AppAccessors.test.ts @@ -0,0 +1,122 @@ +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { assertEquals } from 'https://deno.land/std@0.203.0/assert/assert_equals.ts'; + +import { AppAccessors } from '../mod.ts'; +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; + +describe('AppAccessors', () => { + let appAccessors: AppAccessors; + const senderFn = (r: object) => + Promise.resolve({ + id: Math.random().toString(36).substring(2), + jsonrpc: '2.0', + result: r, + serialize() { + return JSON.stringify(this); + }, + }); + + beforeEach(() => { + appAccessors = new AppAccessors(senderFn); + AppObjectRegistry.clear(); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('creates the correct format for IRead calls', async () => { + const roomRead = appAccessors.getReader().getRoomReader(); + const room = await roomRead.getById('123'); + + assertEquals(room, { + params: ['123'], + method: 'accessor:getReader:getRoomReader:getById', + }); + }); + + it('creates the correct format for IEnvironmentRead calls from IRead', async () => { + const reader = appAccessors.getReader().getEnvironmentReader().getEnvironmentVariables(); + const room = await reader.getValueByName('NODE_ENV'); + + assertEquals(room, { + params: ['NODE_ENV'], + method: 'accessor:getReader:getEnvironmentReader:getEnvironmentVariables:getValueByName', + }); + }); + + it('creates the correct format for IEvironmentRead calls', async () => { + const envRead = appAccessors.getEnvironmentRead(); + const env = await envRead.getServerSettings().getValueById('123'); + + assertEquals(env, { + params: ['123'], + method: 'accessor:getEnvironmentRead:getServerSettings:getValueById', + }); + }); + + it('creates the correct format for IEvironmentWrite calls', async () => { + const envRead = appAccessors.getEnvironmentWrite(); + const env = await envRead.getServerSettings().incrementValue('123', 6); + + assertEquals(env, { + params: ['123', 6], + method: 'accessor:getEnvironmentWrite:getServerSettings:incrementValue', + }); + }); + + it('creates the correct format for IConfigurationModify calls', async () => { + const configModify = appAccessors.getConfigurationModify(); + const command = await configModify.slashCommands.modifySlashCommand({ + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + }); + + assertEquals(command, { + params: [ + { + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + }, + ], + method: 'accessor:getConfigurationModify:slashCommands:modifySlashCommand', + }); + }); + + it('correctly stores a reference to a slashcommand object and sends a request via proxy', async () => { + const configExtend = appAccessors.getConfigurationExtend(); + + const slashcommand = { + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + executor() { + return Promise.resolve(); + }, + }; + + const result = await configExtend.slashCommands.provideSlashCommand(slashcommand); + + assertEquals(AppObjectRegistry.get('slashcommand:test'), slashcommand); + + // The function will not be serialized and sent to the main process + delete result.params[0].executor; + + assertEquals(result, { + method: 'accessor:getConfigurationExtend:slashCommands:provideSlashCommand', + params: [ + { + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + }, + ], + }); + }); +}); diff --git a/packages/apps/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts b/packages/apps/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts new file mode 100644 index 0000000000000..d88690a77dbfa --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts @@ -0,0 +1,259 @@ +// deno-lint-ignore-file no-explicit-any +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { assertSpyCall, spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; +import { assert, assertEquals, assertNotInstanceOf, assertRejects } from 'https://deno.land/std@0.203.0/assert/mod.ts'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { ModifyCreator } from '../modify/ModifyCreator.ts'; + +describe('ModifyCreator', () => { + const senderFn = (r: any) => + Promise.resolve({ + id: Math.random().toString(36).substring(2), + jsonrpc: '2.0', + result: r, + serialize() { + return JSON.stringify(this); + }, + }); + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('id', 'deno-test'); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('sends the correct payload in the request to create a message', async () => { + const spying = spy(senderFn); + const modifyCreator = new ModifyCreator(spying); + const messageBuilder = modifyCreator.startMessage(); + + // Importing types from the Apps-Engine is problematic, so we'll go with `any` here + messageBuilder + .setRoom({ id: '123' } as any) + .setSender({ id: '456' } as any) + .setText('Hello World') + .setUsernameAlias('alias') + .setAvatarUrl('https://avatars.com/123'); + + // We can't get a legitimate return value here, so we ignore it + // but we need to know that the request sent was well formed + await modifyCreator.finish(messageBuilder); + + assertSpyCall(spying, 0, { + args: [ + { + method: 'bridges:getMessageBridge:doCreate', + params: [ + { + room: { id: '123' }, + sender: { id: '456' }, + text: 'Hello World', + alias: 'alias', + avatarUrl: 'https://avatars.com/123', + }, + 'deno-test', + ], + }, + ], + }); + }); + + it('sends the correct payload in the request to upload a buffer', async () => { + const modifyCreator = new ModifyCreator(senderFn); + + const result = await modifyCreator.getUploadCreator().uploadBuffer(new Uint8Array([1, 2, 3, 4]), 'text/plain'); + + assertEquals(result, { + method: 'accessor:getModifier:getCreator:getUploadCreator:uploadBuffer', + params: [new Uint8Array([1, 2, 3, 4]), 'text/plain'], + }); + }); + + it('sends the correct payload in the request to create a visitor', async () => { + const modifyCreator = new ModifyCreator(senderFn); + + const result = (await modifyCreator.getLivechatCreator().createVisitor({ + token: 'random token', + username: 'random username for visitor', + name: 'Random Visitor', + })) as any; // We modified the send function so it changed the original return type of the function + + assertEquals(result, { + method: 'accessor:getModifier:getCreator:getLivechatCreator:createVisitor', + params: [ + { + token: 'random token', + username: 'random username for visitor', + name: 'Random Visitor', + }, + ], + }); + }); + + // This test is important because if we return a promise we break API compatibility + it('does not return a promise for calls of the createToken() method of the LivechatCreator', () => { + const modifyCreator = new ModifyCreator(senderFn); + + const result = modifyCreator.getLivechatCreator().createToken(); + + assertNotInstanceOf(result, Promise); + assert(typeof result === 'string', `Expected "${result}" to be of type "string", but got "${typeof result}"`); + }); + + it('throws an error when a proxy method of getLivechatCreator fails', async () => { + const failingSenderFn = () => Promise.reject(new Error('Test error')); + const modifyCreator = new ModifyCreator(failingSenderFn); + const livechatCreator = modifyCreator.getLivechatCreator(); + + await assertRejects( + () => + livechatCreator.createAndReturnVisitor({ + token: 'visitor-token', + username: 'visitor-username', + name: 'Visitor Name', + }), + Error, + 'Test error', + ); + }); + + it('throws an instance of Error when getLivechatCreator fails with a specific error object', async () => { + const failingSenderFn = () => Promise.reject({ error: { message: 'Livechat method error' } }); + const modifyCreator = new ModifyCreator(failingSenderFn); + const livechatCreator = modifyCreator.getLivechatCreator(); + + await assertRejects( + () => + livechatCreator.createVisitor({ + token: 'visitor-token', + username: 'visitor-username', + name: 'Visitor Name', + }), + Error, + 'Livechat method error', + ); + }); + + it('throws a default Error when getLivechatCreator fails with an unknown error object', async () => { + const failingSenderFn = () => Promise.reject({ error: {} }); + const modifyCreator = new ModifyCreator(failingSenderFn); + const livechatCreator = modifyCreator.getLivechatCreator(); + + await assertRejects( + () => + livechatCreator.createVisitor({ + token: 'visitor-token', + username: 'visitor-username', + name: 'Visitor Name', + }), + Error, + 'An unknown error occurred', + ); + }); + + it('throws an error when a proxy method of getUploadCreator fails', async () => { + const failingSenderFn = () => Promise.reject(new Error('Upload error')); + const modifyCreator = new ModifyCreator(failingSenderFn); + const uploadCreator = modifyCreator.getUploadCreator(); + + await assertRejects(() => uploadCreator.uploadBuffer(new Uint8Array([9, 10, 11, 12]), 'image/png'), Error, 'Upload error'); + }); + + it('throws an instance of Error when getUploadCreator fails with a specific error object', async () => { + const failingSenderFn = () => Promise.reject({ error: { message: 'Upload method error' } }); + const modifyCreator = new ModifyCreator(failingSenderFn); + const uploadCreator = modifyCreator.getUploadCreator(); + + await assertRejects(() => uploadCreator.uploadBuffer(new Uint8Array([1, 2, 3]), 'image/png'), Error, 'Upload method error'); + }); + + it('throws a default Error when getUploadCreator fails with an unknown error object', async () => { + const failingSenderFn = () => Promise.reject({ error: {} }); + const modifyCreator = new ModifyCreator(failingSenderFn); + const uploadCreator = modifyCreator.getUploadCreator(); + + await assertRejects(() => uploadCreator.uploadBuffer(new Uint8Array([1, 2, 3]), 'image/png'), Error, 'An unknown error occurred'); + }); + + it('throws an error when a proxy method of getEmailCreator fails', async () => { + const failingSenderFn = () => Promise.reject(new Error('Email error')); + const modifyCreator = new ModifyCreator(failingSenderFn); + const emailCreator = modifyCreator.getEmailCreator(); + + await assertRejects( + () => + emailCreator.send({ + to: 'test@example.com', + from: 'sender@example.com', + subject: 'Test Email', + text: 'This is a test email.', + }), + Error, + 'Email error', + ); + }); + + it('throws an instance of Error when getEmailCreator fails with a specific error object', async () => { + const failingSenderFn = () => Promise.reject({ error: { message: 'Email method error' } }); + const modifyCreator = new ModifyCreator(failingSenderFn); + const emailCreator = modifyCreator.getEmailCreator(); + + await assertRejects( + () => + emailCreator.send({ + to: 'test@example.com', + from: 'sender@example.com', + subject: 'Test Email', + text: 'This is a test email.', + }), + Error, + 'Email method error', + ); + }); + + it('throws a default Error when getEmailCreator fails with an unknown error object', async () => { + const failingSenderFn = () => Promise.reject({ error: {} }); + const modifyCreator = new ModifyCreator(failingSenderFn); + const emailCreator = modifyCreator.getEmailCreator(); + + await assertRejects( + () => + emailCreator.send({ + to: 'test@example.com', + from: 'sender@example.com', + subject: 'Test Email', + text: 'This is a test email.', + }), + Error, + 'An unknown error occurred', + ); + }); + + it('throws an error when a proxy method of getContactCreator fails', async () => { + const failingSenderFn = () => Promise.reject(new Error('Contact creation error')); + const modifyCreator = new ModifyCreator(failingSenderFn); + const contactCreator = modifyCreator.getContactCreator(); + + await assertRejects(() => contactCreator.addContactEmail('test-contact-id', 'test@example.com'), Error, 'Contact creation error'); + }); + + it('throws an instance of Error when getContactCreator fails with a specific error object', async () => { + const failingSenderFn = () => Promise.reject({ error: { message: 'Contact creation error' } }); + const modifyCreator = new ModifyCreator(failingSenderFn); + const contactCreator = modifyCreator.getContactCreator(); + + await assertRejects(() => contactCreator.addContactEmail('test-contact-id', 'test@example.com'), Error, 'Contact creation error'); + }); + + it('throws a default Error when getContactCreator fails with an unknown error object', async () => { + const failingSenderFn = () => Promise.reject({ error: {} }); + const modifyCreator = new ModifyCreator(failingSenderFn); + const contactCreator = modifyCreator.getContactCreator(); + + await assertRejects(() => contactCreator.addContactEmail('test-contact-id', 'test@example.com'), Error, 'An unknown error occurred'); + }); +}); diff --git a/packages/apps/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts b/packages/apps/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts new file mode 100644 index 0000000000000..de6fd4a7053b3 --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts @@ -0,0 +1,244 @@ +// deno-lint-ignore-file no-explicit-any +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { assertSpyCall, spy, stub } from 'https://deno.land/std@0.203.0/testing/mock.ts'; +import { assertRejects } from 'https://deno.land/std@0.203.0/assert/mod.ts'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { ModifyExtender } from '../modify/ModifyExtender.ts'; +import jsonrpc from 'jsonrpc-lite'; + +describe('ModifyExtender', () => { + let extender: ModifyExtender; + + const senderFn = (r: any) => + Promise.resolve({ + id: Math.random().toString(36).substring(2), + jsonrpc: '2.0', + result: structuredClone(r), + serialize() { + return JSON.stringify(this); + }, + }); + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('id', 'deno-test'); + extender = new ModifyExtender(senderFn); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('correctly formats requests for the extend message requests', async () => { + const _spy = spy(extender, 'senderFn' as keyof ModifyExtender); + + const messageExtender = await extender.extendMessage('message-id', { _id: 'user-id' } as any); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getMessageBridge:doGetById', + params: ['message-id', 'deno-test'], + }, + ], + }); + + messageExtender.addCustomField('key', 'value'); + + await extender.finish(messageExtender); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getMessageBridge:doUpdate', + params: [messageExtender.getMessage(), 'deno-test'], + }, + ], + }); + + _spy.restore(); + }); + + it('correctly formats requests for the extend room requests', async () => { + const _spy = spy(extender, 'senderFn' as keyof ModifyExtender); + + const roomExtender = await extender.extendRoom('room-id', { _id: 'user-id' } as any); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getRoomBridge:doGetById', + params: ['room-id', 'deno-test'], + }, + ], + }); + + roomExtender.addCustomField('key', 'value'); + + await extender.finish(roomExtender); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getRoomBridge:doUpdate', + params: [roomExtender.getRoom(), [], 'deno-test'], + }, + ], + }); + + _spy.restore(); + }); + + it('correctly formats requests for the extend video conference requests', async () => { + const _spy = spy(extender, 'senderFn' as keyof ModifyExtender); + + const videoConferenceExtender = await extender.extendVideoConference('video-conference-id'); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getVideoConferenceBridge:doGetById', + params: ['video-conference-id', 'deno-test'], + }, + ], + }); + + videoConferenceExtender.setStatus(4); + + await extender.finish(videoConferenceExtender); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getVideoConferenceBridge:doUpdate', + params: [videoConferenceExtender.getVideoConference(), 'deno-test'], + }, + ], + }); + + _spy.restore(); + }); + + describe('Error Handling', () => { + describe('extendMessage', () => { + it('throws an instance of Error when senderFn throws an error', async () => { + const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject(new Error('unit-test-error')) as any); + + await assertRejects(() => extender.extendMessage('message-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { + const _stub = stub( + extender, + 'senderFn' as keyof ModifyExtender, + () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, + ); + + await assertRejects(() => extender.extendMessage('message-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws an unknown value', async () => { + const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject({}) as any); + + await assertRejects(() => extender.extendMessage('message-id', { _id: 'user-id' } as any), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + }); + + describe('extendRoom', () => { + it('throws an instance of Error when senderFn throws an error', async () => { + const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject(new Error('unit-test-error')) as any); + + await assertRejects(() => extender.extendRoom('room-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { + const _stub = stub( + extender, + 'senderFn' as keyof ModifyExtender, + () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, + ); + + await assertRejects(() => extender.extendRoom('room-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws an unknown value', async () => { + const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject({}) as any); + + await assertRejects(() => extender.extendRoom('room-id', { _id: 'user-id' } as any), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + }); + + describe('extendVideoConference', () => { + it('throws an instance of Error when senderFn throws an error', async () => { + const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject(new Error('unit-test-error')) as any); + + await assertRejects(() => extender.extendVideoConference('video-conference-id'), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { + const _stub = stub( + extender, + 'senderFn' as keyof ModifyExtender, + () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, + ); + + await assertRejects(() => extender.extendVideoConference('video-conference-id'), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws an unknown value', async () => { + const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject({}) as any); + + await assertRejects(() => extender.extendVideoConference('video-conference-id'), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + }); + + describe('finish', () => { + it('throws an instance of Error when senderFn throws an error', async () => { + const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject(new Error('unit-test-error')) as any); + + await assertRejects(() => extender.finish({ kind: 'message', getMessage: () => ({}) } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { + const _stub = stub( + extender, + 'senderFn' as keyof ModifyExtender, + () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, + ); + + await assertRejects(() => extender.finish({ kind: 'message', getMessage: () => ({}) } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws an unknown value', async () => { + const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject({}) as any); + + await assertRejects(() => extender.finish({ kind: 'message', getMessage: () => ({}) } as any), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + }); + }); +}); diff --git a/packages/apps/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts b/packages/apps/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts new file mode 100644 index 0000000000000..487c10725028a --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts @@ -0,0 +1,234 @@ +// deno-lint-ignore-file no-explicit-any +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { assertSpyCall, spy, stub } from 'https://deno.land/std@0.203.0/testing/mock.ts'; +import { assertEquals, assertRejects } from 'https://deno.land/std@0.203.0/assert/mod.ts'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { ModifyUpdater } from '../modify/ModifyUpdater.ts'; +import { RoomBuilder } from '../builders/RoomBuilder.ts'; +import jsonrpc from 'jsonrpc-lite'; + +describe('ModifyUpdater', () => { + let modifyUpdater: ModifyUpdater; + + const senderFn = (r: any) => + Promise.resolve({ + id: Math.random().toString(36).substring(2), + jsonrpc: '2.0', + result: structuredClone(r), + serialize() { + return JSON.stringify(this); + }, + }); + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('id', 'deno-test'); + modifyUpdater = new ModifyUpdater(senderFn); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('correctly formats requests for the update message flow', async () => { + const _spy = spy(modifyUpdater, 'senderFn' as keyof ModifyUpdater); + + const messageBuilder = await modifyUpdater.message('123', { id: '456' } as any); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getMessageBridge:doGetById', + params: ['123', 'deno-test'], + }, + ], + }); + + messageBuilder.setUpdateData( + { + id: '123', + room: { id: '123' }, + sender: { id: '456' }, + text: 'Hello World', + }, + { + id: '456', + }, + ); + + await modifyUpdater.finish(messageBuilder); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getMessageBridge:doUpdate', + params: [{ id: '123', ...messageBuilder.getChanges() }, 'deno-test'], + }, + ], + }); + + _spy.restore(); + }); + + it('correctly formats requests for the update room flow', async () => { + const _spy = spy(modifyUpdater, 'senderFn' as keyof ModifyUpdater); + + const roomBuilder = (await modifyUpdater.room('123', { id: '456' } as any)) as RoomBuilder; + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getRoomBridge:doGetById', + params: ['123', 'deno-test'], + }, + ], + }); + + roomBuilder.setData({ + id: '123', + type: 'c', + displayName: 'Test Room', + slugifiedName: 'test-room', + creator: { id: '456' }, + }); + + roomBuilder.setMembersToBeAddedByUsernames(['username1', 'username2']); + + // We need to sneak in the id as the `modifyUpdater.room` call won't have legitimate data + roomBuilder.getRoom().id = '123'; + + await modifyUpdater.finish(roomBuilder); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getRoomBridge:doUpdate', + params: [{ id: '123', ...roomBuilder.getChanges() }, roomBuilder.getMembersToBeAddedUsernames(), 'deno-test'], + }, + ], + }); + }); + + it('correctly formats requests to UserUpdater methods', async () => { + const result = (await modifyUpdater.getUserUpdater().updateStatusText({ id: '123' } as any, 'Hello World')) as any; + + assertEquals(result, { + method: 'accessor:getModifier:getUpdater:getUserUpdater:updateStatusText', + params: [{ id: '123' }, 'Hello World'], + }); + }); + + it('correctly formats requests to LivechatUpdater methods', async () => { + const result = (await modifyUpdater.getLivechatUpdater().closeRoom({ id: '123' } as any, 'close it!')) as any; + + assertEquals(result, { + method: 'accessor:getModifier:getUpdater:getLivechatUpdater:closeRoom', + params: [{ id: '123' }, 'close it!'], + }); + }); + + describe('Error Handling', () => { + describe('message', () => { + it('throws an instance of Error when senderFn throws an error', async () => { + const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject(new Error('unit-test-error')) as any); + + await assertRejects(() => modifyUpdater.message('message-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { + const _stub = stub( + modifyUpdater, + 'senderFn' as keyof ModifyUpdater, + () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, + ); + + await assertRejects(() => modifyUpdater.message('message-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws an unknown value', async () => { + const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject({}) as any); + + await assertRejects(() => modifyUpdater.message('message-id', { _id: 'user-id' } as any), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + }); + + describe('room', () => { + it('throws an instance of Error when senderFn throws an error', async () => { + const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject(new Error('unit-test-error')) as any); + + await assertRejects(() => modifyUpdater.room('room-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { + const _stub = stub( + modifyUpdater, + 'senderFn' as keyof ModifyUpdater, + () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, + ); + + await assertRejects(() => modifyUpdater.room('room-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws an unknown value', async () => { + const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject({}) as any); + + await assertRejects(() => modifyUpdater.room('room-id', { _id: 'user-id' } as any), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + }); + + describe('finish', () => { + const messageUpdater = { + kind: 'message', + getMessage: () => ({ + id: 'message-id', + sender: { id: 'sender-id' }, + }), + getChanges: () => ({ + id: 'message-id', + sender: { id: 'sender-id' }, + }), + } as any; + + it('throws an instance of Error when senderFn throws an error', async () => { + const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject(new Error('unit-test-error')) as any); + + await assertRejects(() => modifyUpdater.finish(messageUpdater), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { + const _stub = stub( + modifyUpdater, + 'senderFn' as keyof ModifyUpdater, + () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, + ); + + await assertRejects(() => modifyUpdater.finish(messageUpdater), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws an unknown value', async () => { + const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject({}) as any); + + await assertRejects(() => modifyUpdater.finish(messageUpdater), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + }); + }); +}); diff --git a/packages/apps/deno-runtime/lib/accessors/tests/formatResponseErrorHandler.test.ts b/packages/apps/deno-runtime/lib/accessors/tests/formatResponseErrorHandler.test.ts new file mode 100644 index 0000000000000..c909fecdd04a1 --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/tests/formatResponseErrorHandler.test.ts @@ -0,0 +1,211 @@ +// deno-lint-ignore-file no-explicit-any +import { assertEquals, assertInstanceOf, assertStrictEquals } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import * as jsonrpc from 'jsonrpc-lite'; + +import { formatErrorResponse } from '../formatResponseErrorHandler.ts'; + +describe('formatErrorResponse', () => { + describe('JSON-RPC ErrorObject handling', () => { + it('formats ErrorObject instances correctly', () => { + const errorObject = jsonrpc.error('test-id', new jsonrpc.JsonRpcError('Test error message', 1000)); + const result = formatErrorResponse(errorObject); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'Test error message'); + }); + + it('formats objects with error.message structure', () => { + const errorLikeObject = { + error: { + message: 'Custom error message', + code: 404, + }, + }; + const result = formatErrorResponse(errorLikeObject); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'Custom error message'); + }); + + it('handles nested error objects with complex structure', () => { + const complexError = { + error: { + message: 'Database connection failed', + details: { + host: 'localhost', + port: 5432, + }, + }, + id: 'req-123', + }; + const result = formatErrorResponse(complexError); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'Database connection failed'); + }); + + it('handles error objects with empty message', () => { + const emptyMessageError = { + error: { + message: '', + code: 500, + }, + }; + const result = formatErrorResponse(emptyMessageError); + + assertInstanceOf(result, Error); + assertEquals(result.message, ''); + }); + }); + + describe('Error instance passthrough', () => { + it('returns existing Error instances unchanged', () => { + const originalError = new Error('Original error message'); + const result = formatErrorResponse(originalError); + + assertStrictEquals(result, originalError); + assertEquals(result.message, 'Original error message'); + }); + + it('returns custom Error subclasses unchanged', () => { + class CustomError extends Error { + constructor( + message: string, + public code: number, + ) { + super(message); + this.name = 'CustomError'; + } + } + + const customError = new CustomError('Custom error', 404); + const result = formatErrorResponse(customError); + + assertStrictEquals(result, customError); + assertEquals(result.message, 'Custom error'); + assertEquals((result as CustomError).code, 404); + }); + + it('handles Error instances with additional properties', () => { + const errorWithProps = new Error('Error with props') as any; + errorWithProps.statusCode = 500; + errorWithProps.details = { reason: 'timeout' }; + + const result = formatErrorResponse(errorWithProps); + + assertStrictEquals(result, errorWithProps); + assertEquals(result.message, 'Error with props'); + assertEquals((result as any).statusCode, 500); + }); + }); + + describe('Unknown error handling', () => { + it('wraps string errors with default message and cause', () => { + const stringError = 'Simple string error'; + const result = formatErrorResponse(stringError); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + assertEquals(result.cause, stringError); + }); + + it('wraps number errors with default message and cause', () => { + const numberError = 404; + const result = formatErrorResponse(numberError); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + assertEquals(result.cause, numberError); + }); + + it('wraps boolean errors with default message and cause', () => { + const booleanError = false; + const result = formatErrorResponse(booleanError); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + assertEquals(result.cause, booleanError); + }); + + it('wraps null with default message and cause', () => { + const result = formatErrorResponse(null); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + assertEquals(result.cause, null); + }); + + it('wraps undefined with default message and cause', () => { + const result = formatErrorResponse(undefined); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + assertEquals(result.cause, undefined); + }); + + it('wraps arrays with default message and cause', () => { + const arrayError = ['error', 'details']; + const result = formatErrorResponse(arrayError); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + assertEquals(result.cause, arrayError); + }); + + it('wraps functions with default message and cause', () => { + const functionError = () => 'error'; + const result = formatErrorResponse(functionError); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + assertEquals(result.cause, functionError); + }); + + it('wraps plain objects without error.message with default message and cause', () => { + const plainObject = { + status: 'failed', + reason: 'timeout', + data: { id: 123 }, + }; + const result = formatErrorResponse(plainObject); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + assertEquals(result.cause, plainObject); + }); + + it('wraps objects with error property but no message with default message and cause', () => { + const errorObjectNoMessage = { + error: { + code: 500, + details: 'Internal server error', + }, + }; + const result = formatErrorResponse(errorObjectNoMessage); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + assertEquals(result.cause, errorObjectNoMessage); + }); + }); + + it('ensures all returned values are proper Error instances', () => { + const testCases = ['string error', 123, null, undefined, { error: { message: 'test' } }, new Error('test'), { plain: 'object' }]; + + for (const testCase of testCases) { + const result = formatErrorResponse(testCase); + assertInstanceOf(result, Error, `Failed for input: ${JSON.stringify(testCase)}`); + } + }); + + it('prevents "[object Object]" error messages for plain objects', () => { + const plainObject = { status: 'error', code: 500 }; + const result = formatErrorResponse(plainObject); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + // Ensure the message is not "[object Object]" + assertEquals(result.message !== '[object Object]', true); + }); +}); diff --git a/packages/apps/deno-runtime/lib/accessors/tests/http.test.ts b/packages/apps/deno-runtime/lib/accessors/tests/http.test.ts new file mode 100644 index 0000000000000..88392dec774cc --- /dev/null +++ b/packages/apps/deno-runtime/lib/accessors/tests/http.test.ts @@ -0,0 +1,164 @@ +// deno-lint-ignore-file no-explicit-any +import { assertRejects } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { beforeEach, describe, it, afterAll } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; + +import { Http } from '../http.ts'; +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { stub } from 'https://deno.land/std@0.203.0/testing/mock.ts'; + +describe('Http accessor error handling integration', () => { + let http: Http; + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('id', 'test-app-id'); + + const mockHttpExtend = { + getDefaultHeaders: () => new Map(), + getDefaultParams: () => new Map(), + getPreRequestHandlers: () => [], + getPreResponseHandlers: () => [], + }; + + const mockRead = {}; + const mockPersistence = {}; + + http = new Http(mockRead as any, mockPersistence as any, mockHttpExtend as any, () => Promise.resolve({}) as any); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + describe('HTTP method error handling', () => { + it('formats JSON-RPC errors correctly for GET requests', async () => { + const _stub = stub(http, 'senderFn' as keyof Http, () => + Promise.reject({ + error: { + message: 'HTTP GET request failed', + code: 404, + }, + }), + ); + + await assertRejects(() => http.get('https://api.example.com/data'), Error, 'HTTP GET request failed'); + + _stub.restore(); + }); + + it('formats JSON-RPC errors correctly for POST requests', async () => { + const _stub = stub(http, 'senderFn' as keyof Http, () => + Promise.reject({ + error: { + message: 'HTTP POST request validation failed', + code: 400, + }, + }), + ); + + await assertRejects( + () => http.post('https://api.example.com/create', { data: { name: 'test' } }), + Error, + 'HTTP POST request validation failed', + ); + + _stub.restore(); + }); + + it('formats JSON-RPC errors correctly for PUT requests', async () => { + const _stub = stub(http, 'senderFn' as keyof Http, () => + Promise.reject({ + error: { + message: 'HTTP PUT request unauthorized', + code: 401, + }, + }), + ); + + await assertRejects( + () => http.put('https://api.example.com/update/123', { data: { name: 'updated' } }), + Error, + 'HTTP PUT request unauthorized', + ); + + _stub.restore(); + }); + + it('formats JSON-RPC errors correctly for DELETE requests', async () => { + const _stub = stub(http, 'senderFn' as keyof Http, () => + Promise.reject({ + error: { + message: 'HTTP DELETE request forbidden', + code: 403, + }, + }), + ); + + await assertRejects(() => http.del('https://api.example.com/delete/123'), Error, 'HTTP DELETE request forbidden'); + + _stub.restore(); + }); + + it('formats JSON-RPC errors correctly for PATCH requests', async () => { + const _stub = stub(http, 'senderFn' as keyof Http, () => + Promise.reject({ + error: { + message: 'HTTP PATCH request conflict', + code: 409, + }, + }), + ); + + await assertRejects( + () => http.patch('https://api.example.com/patch/123', { data: { status: 'active' } }), + Error, + 'HTTP PATCH request conflict', + ); + + _stub.restore(); + }); + }); + + describe('Error instance passthrough', () => { + it('passes through existing Error instances unchanged for HTTP requests', async () => { + const originalError = new Error('Network timeout error'); + const _stub = stub(http, 'senderFn' as keyof Http, () => Promise.reject(originalError)); + + await assertRejects(() => http.get('https://api.example.com/data'), Error, 'Network timeout error'); + + _stub.restore(); + }); + }); + + describe('Unknown error handling', () => { + it('wraps unknown object errors with default message for HTTP requests', async () => { + const unknownError = { + status: 'failed', + details: 'Something went wrong', + timestamp: Date.now(), + }; + const _stub = stub(http, 'senderFn' as keyof Http, () => Promise.reject(unknownError)); + + await assertRejects(() => http.get('https://api.example.com/data'), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + + it('wraps string errors with default message for HTTP requests', async () => { + const stringError = 'Connection refused'; + const _stub = stub(http, 'senderFn' as keyof Http, () => Promise.reject(stringError)); + + await assertRejects(() => http.get('https://api.example.com/data'), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + + it('wraps null/undefined errors with default message for HTTP requests', async () => { + const _stub = stub(http, 'senderFn' as keyof Http, () => Promise.reject(null)); + + await assertRejects(() => http.get('https://api.example.com/data'), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + }); +}); diff --git a/packages/apps/deno-runtime/lib/ast/mod.ts b/packages/apps/deno-runtime/lib/ast/mod.ts new file mode 100644 index 0000000000000..555b4defc36a0 --- /dev/null +++ b/packages/apps/deno-runtime/lib/ast/mod.ts @@ -0,0 +1,70 @@ +import { generate } from 'astring'; +// @deno-types="../../acorn.d.ts" +import { parse, Program } from 'acorn'; +// @deno-types="../../acorn-walk.d.ts" +import { fullAncestor } from 'acorn-walk'; + +import * as operations from './operations.ts'; +import type { WalkerState } from './operations.ts'; + +function fixAst(ast: Program): boolean { + const pendingOperations = [ + operations.fixLivechatIsOnlineCalls, + operations.checkReassignmentOfModifiedIdentifiers, + operations.fixRoomUsernamesCalls, + ]; + + // Have we touched the tree? + let isModified = false; + + while (pendingOperations.length) { + const ops = pendingOperations.splice(0); + const state: WalkerState = { + isModified: false, + functionIdentifiers: new Set(), + }; + + fullAncestor( + ast, + (node, state, ancestors, type) => { + ops.forEach((operation) => operation(node, state, ancestors, type)); + }, + undefined, + state, + ); + + if (state.isModified) { + isModified = true; + } + + if (state.functionIdentifiers.size) { + pendingOperations.push( + operations.buildFixModifiedFunctionsOperation(state.functionIdentifiers), + operations.checkReassignmentOfModifiedIdentifiers, + ); + } + } + + return isModified; +} + +export function fixBrokenSynchronousAPICalls(appSource: string): string { + const astRootNode = parse(appSource, { + // Latest ecma version supported by this version of acorn. + ecmaVersion: "latest", + // Allow everything, we don't want to complain if code is badly written + // Also, since the code itself has been transpiled, the chance of getting + // shenanigans is lower + allowReserved: true, + allowReturnOutsideFunction: true, + allowImportExportEverywhere: true, + allowAwaitOutsideFunction: true, + allowSuperOutsideMethod: true, + }); + + if (fixAst(astRootNode)) { + return generate(astRootNode); + } + + return appSource; +} diff --git a/packages/apps/deno-runtime/lib/ast/operations.ts b/packages/apps/deno-runtime/lib/ast/operations.ts new file mode 100644 index 0000000000000..7a5a4993ad297 --- /dev/null +++ b/packages/apps/deno-runtime/lib/ast/operations.ts @@ -0,0 +1,237 @@ +// @deno-types="../../acorn.d.ts" +import { AnyNode, AssignmentExpression, AwaitExpression, Expression, Function, Identifier, MethodDefinition, Property } from 'acorn'; +// @deno-types="../../acorn-walk.d.ts" +import { FullAncestorWalkerCallback } from 'acorn-walk'; + +export type WalkerState = { + isModified: boolean; + functionIdentifiers: Set; +}; + +export function getFunctionIdentifier(ancestors: AnyNode[], functionNodeIndex: number) { + const parent = ancestors[functionNodeIndex - 1]; + + // If there is a parent node and it's not a computed property, we can try to + // extract an identifier for our function from it. This needs to be done first + // because when functions are assigned to named symbols, this will be the only + // way to call it, even if the function itself has an identifier + // Consider the following block: + // + // const foo = function bar() {} + // + // Even though the function itself has a name, the only way to call it in the + // program is wiht `foo()` + if (parent && !(parent as Property | MethodDefinition).computed) { + // Several node types can have an id prop of type Identifier + const { id } = parent as unknown as { id?: Identifier }; + if (id?.type === 'Identifier') { + return id.name; + } + + // Usually assignments to object properties (MethodDefinition, Property) + const { key } = parent as MethodDefinition | Property; + if (key?.type === 'Identifier') { + return key.name; + } + + // Variable assignments have left hand side that can be used as Identifier + const { left } = parent as AssignmentExpression; + + // Simple assignment: `const fn = () => {}` + if (left?.type === 'Identifier') { + return left.name; + } + + // Object property assignment: `obj.fn = () => {}` + if (left?.type === 'MemberExpression' && !left.computed) { + return (left.property as Identifier).name; + } + } + + // nodeIndex needs to be the index of a Function node (either FunctionDeclaration or FunctionExpression) + const currentNode = ancestors[functionNodeIndex] as Function; + + // Function declarations or expressions can be directly named + if (currentNode.id?.type === 'Identifier') { + return currentNode.id.name; + } +} + +export function wrapWithAwait(node: Expression) { + if (!node.type.endsWith('Expression')) { + throw new Error(`Can't wrap "${node.type}" with await`); + } + + const innerNode: Expression = { ...node }; + + node.type = 'AwaitExpression'; + // starting here node has become an AwaitExpression + (node as AwaitExpression).argument = innerNode; + + Object.keys(node).forEach((key) => !['type', 'argument'].includes(key) && delete node[key as keyof AnyNode]); +} + +export function asyncifyScope(ancestors: AnyNode[], state: WalkerState) { + const functionNodeIndex = ancestors.findLastIndex((n) => 'async' in n); + if (functionNodeIndex === -1) return; + + // At this point this is a node with an "async" property, so it has to be + // of type Function - let TS know about that + const functionScopeNode = ancestors[functionNodeIndex] as Function; + + if (functionScopeNode.async) { + return; + } + + functionScopeNode.async = true; + + // If the parent of a function node is a call expression, we're talking about an IIFE + // Should we care about this case as well? + // const parentNode = ancestors[functionScopeIndex-1]; + // if (parentNode?.type === 'CallExpression' && ancestors[functionScopeIndex-2] && ancestors[functionScopeIndex-2].type !== 'AwaitExpression') { + // pendingOperations.push(buildFunctionPredicate(getFunctionIdentifier(ancestors, functionScopeIndex-2))); + // } + + const identifier = getFunctionIdentifier(ancestors, functionNodeIndex); + + // We can't fix calls of functions which name we can't determine at compile time + if (!identifier) return; + + state.functionIdentifiers.add(identifier); +} + +export function buildFixModifiedFunctionsOperation(functionIdentifiers: Set): FullAncestorWalkerCallback { + return function _fixModifiedFunctionsOperation(node, state, ancestors) { + if (node.type !== 'CallExpression') return; + + let isWrappable = false; + + // This node is a simple call to a function, like `fn()` + isWrappable = node.callee.type === 'Identifier' && functionIdentifiers.has(node.callee.name); + + // This node is a call to an object property or instance method, like `obj.fn()`, but not computed like `obj[fn]()` + isWrappable ||= node.callee.type === 'MemberExpression' && + !node.callee.computed && + node.callee.property?.type === 'Identifier' && + functionIdentifiers.has(node.callee.property.name); + + // This is a weird dereferencing technique used by bundlers, and since we'll be dealing with bundled sources we have to check for it + // e.g. `r=(0,fn)(e)` + if (!isWrappable && node.callee.type === 'SequenceExpression') { + const [, secondExpression] = node.callee.expressions; + isWrappable = secondExpression?.type === 'Identifier' && functionIdentifiers.has(secondExpression.name); + isWrappable ||= secondExpression?.type === 'MemberExpression' && + !secondExpression.computed && + secondExpression.property.type === 'Identifier' && + functionIdentifiers.has(secondExpression.property.name); + } + + if (!isWrappable) return; + + // ancestors[ancestors.length-1] === node, so here we're checking for parent node + const parentNode = ancestors[ancestors.length - 2]; + if (!parentNode || parentNode.type === 'AwaitExpression') return; + + wrapWithAwait(node); + asyncifyScope(ancestors, state); + + state.isModified = true; + }; +} + +export const checkReassignmentOfModifiedIdentifiers: FullAncestorWalkerCallback = (node, { functionIdentifiers }, _ancestors) => { + if (node.type === 'AssignmentExpression') { + if (node.operator !== '=') return; + + let identifier = ''; + + if (node.left.type === 'Identifier') identifier = node.left.name; + + if (node.left.type === 'MemberExpression' && !node.left.computed) { + identifier = (node.left.property as Identifier).name; + } + + if (!identifier || node.right.type !== 'Identifier' || !functionIdentifiers.has(node.right.name)) return; + + functionIdentifiers.add(identifier); + + return; + } + + if (node.type === 'VariableDeclarator') { + if (node.id.type !== 'Identifier' || functionIdentifiers.has(node.id.name)) return; + + if (node.init?.type !== 'Identifier' || !functionIdentifiers.has(node.init?.name)) return; + + functionIdentifiers.add(node.id.name); + + return; + } + + // "Property" is for plain objects, "PropertyDefinition" is for classes + // but both share the same structure + if (node.type === 'Property' || node.type === 'PropertyDefinition') { + if (node.key.type !== 'Identifier' || functionIdentifiers.has(node.key.name)) return; + + if (node.value?.type !== 'Identifier' || !functionIdentifiers.has(node.value.name)) return; + + functionIdentifiers.add(node.key.name); + + return; + } +}; + +export const fixLivechatIsOnlineCalls: FullAncestorWalkerCallback = (node, state, ancestors) => { + if (node.type !== 'MemberExpression' || node.computed) return; + + if ((node.property as Identifier).name !== 'isOnline') return; + + if (node.object.type !== 'CallExpression') return; + + if (node.object.callee.type !== 'MemberExpression') return; + + if ((node.object.callee.property as Identifier).name !== 'getLivechatReader') return; + + let parentIndex = ancestors.length - 2; + let targetNode = ancestors[parentIndex]; + + if (targetNode.type !== 'CallExpression') { + targetNode = node; + } else { + parentIndex--; + } + + // If we're already wrapped with an await, nothing to do + if (ancestors[parentIndex].type === 'AwaitExpression') return; + + // If we're in the middle of a chained member access, we can't wrap with await + if (ancestors[parentIndex].type === 'MemberExpression') return; + + wrapWithAwait(targetNode); + asyncifyScope(ancestors, state); + + state.isModified = true; +}; + +export const fixRoomUsernamesCalls: FullAncestorWalkerCallback = (node, state, ancestors) => { + if (node.type !== 'MemberExpression' || node.computed) return; + + if ((node.property as Identifier).name !== 'usernames') return; + + let parentIndex = ancestors.length - 2; + let targetNode = ancestors[parentIndex]; + + if (targetNode.type !== 'CallExpression') { + targetNode = node; + } else { + parentIndex--; + } + + // If we're already wrapped with an await, nothing to do + if (ancestors[parentIndex].type === 'AwaitExpression') return; + + wrapWithAwait(targetNode); + asyncifyScope(ancestors, state); + + state.isModified = true; +}; diff --git a/packages/apps/deno-runtime/lib/ast/tests/data/ast_blocks.ts b/packages/apps/deno-runtime/lib/ast/tests/data/ast_blocks.ts new file mode 100644 index 0000000000000..8e750e6eaf587 --- /dev/null +++ b/packages/apps/deno-runtime/lib/ast/tests/data/ast_blocks.ts @@ -0,0 +1,436 @@ +// @deno-types="../../../../acorn.d.ts" +import { AnyNode, ClassDeclaration, ExpressionStatement, FunctionDeclaration, VariableDeclaration } from 'acorn'; + +/** + * Partial AST blocks to support testing. + * `start` and `end` properties are omitted for brevity. + */ + +type TestNodeExcerpt = { + code: string; + node: N; +}; + +export const FunctionDeclarationFoo: TestNodeExcerpt = { + code: 'function foo() {}', + node: { + type: 'FunctionDeclaration', + id: { + type: 'Identifier', + name: 'foo', + }, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, +}; + +export const ConstFooAssignedFunctionExpression: TestNodeExcerpt = { + code: 'const foo = function() {}', + node: { + type: 'VariableDeclaration', + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: 'foo', + }, + init: { + type: 'FunctionExpression', + id: null, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, + }, + ], + }, +}; + +export const AssignmentExpressionOfArrowFunctionToFooIdentifier: TestNodeExcerpt = { + code: 'foo = () => {}', + node: { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'Identifier', + name: 'foo', + }, + right: { + type: 'ArrowFunctionExpression', + id: null, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, + }, + }, +}; + +export const AssignmentExpressionOfNamedFunctionToFooMemberExpression: TestNodeExcerpt = { + code: 'obj.foo = function bar() {}', + node: { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'a', + }, + property: { + type: 'Identifier', + name: 'foo', + }, + computed: false, + optional: false, + }, + right: { + type: 'FunctionExpression', + id: null, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, + }, + }, +}; + +export const MethodDefinitionOfFooInClassBar: TestNodeExcerpt = { + code: 'class Bar { foo() {} }', + node: { + type: 'ClassDeclaration', + id: { + type: 'Identifier', + name: 'Bar', + }, + superClass: null, + body: { + type: 'ClassBody', + body: [ + { + type: 'MethodDefinition', + key: { + type: 'Identifier', + name: 'foo', + }, + value: { + type: 'FunctionExpression', + id: null, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, + kind: 'method', + computed: false, + static: false, + }, + ], + }, + }, +}; + +export const SimpleCallExpressionOfFoo: TestNodeExcerpt = { + code: 'foo()', + node: { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'foo', + }, + arguments: [], + optional: false, + }, + }, +}; + +export const SyncFunctionDeclarationWithAsyncCallExpression: TestNodeExcerpt = { + // NOTE: this is invalid syntax, it won't be parsed by acorn + // but it can be an intermediary state of the AST after we run + // `wrapWithAwait` on "bar" call expressions, for instance + code: 'function foo() { return () => await bar() }', + node: { + type: 'FunctionDeclaration', + id: { + type: 'Identifier', + name: 'foo', + }, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [ + { + type: 'ReturnStatement', + argument: { + type: 'ArrowFunctionExpression', + id: null, + expression: true, + generator: false, + async: false, + params: [], + body: { + type: 'AwaitExpression', + argument: { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'bar', + }, + arguments: [], + optional: false, + }, + }, + }, + }, + ], + }, + }, +}; + +export const AssignmentOfFooToBar: TestNodeExcerpt = { + code: 'bar = foo', + node: { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'Identifier', + name: 'bar', + }, + right: { + type: 'Identifier', + name: 'foo', + }, + }, + }, +}; + +export const AssignmentOfFooToBarMemberExpression: TestNodeExcerpt = { + code: 'obj.bar = foo', + node: { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + computed: false, + optional: false, + object: { + type: 'Identifier', + name: 'obj', + }, + property: { + type: 'Identifier', + name: 'bar', + }, + }, + right: { + type: 'Identifier', + name: 'foo', + }, + }, + }, +}; + +export const AssignmentOfFooToBarVariableDeclarator: TestNodeExcerpt = { + code: 'const bar = foo', + node: { + type: 'VariableDeclaration', + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: 'bar', + }, + init: { + type: 'Identifier', + name: 'foo', + }, + }, + ], + }, +}; + +export const AssignmentOfFooToBarPropertyDefinition: TestNodeExcerpt = { + code: 'class baz { bar = foo }', + node: { + type: 'ClassDeclaration', + id: { + type: 'Identifier', + name: 'baz', + }, + superClass: null, + body: { + type: 'ClassBody', + body: [ + { + type: 'PropertyDefinition', + static: false, + computed: false, + key: { + type: 'Identifier', + name: 'bar', + }, + value: { + type: 'Identifier', + name: 'foo', + }, + }, + ], + }, + }, +}; + +const fixSimpleCallExpressionCode = ` +function bar() { + const a = foo(); + + return a; +}`; + +export const FixSimpleCallExpression: TestNodeExcerpt = { + code: fixSimpleCallExpressionCode, + node: { + type: 'FunctionDeclaration', + id: { + type: 'Identifier', + name: 'bar', + }, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [ + { + type: 'VariableDeclaration', + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: 'a', + }, + init: { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'foo', + }, + arguments: [], + optional: false, + }, + }, + ], + }, + { + type: 'ReturnStatement', + argument: { + type: 'Identifier', + name: 'a', + }, + }, + ], + }, + }, +}; + +export const ArrowFunctionDerefCallExpression: TestNodeExcerpt = { + // NOTE: this call strategy is widely used by bundlers; it's used to sever the `this` + // reference in the method from the object that contains it. This is mostly because + // the bundler wants to ensure that it does not messes up the bindings in the code it + // generates. + // + // This would be similar to doing `foo.call(undefined)` + code: 'const bar = () => (0, e.foo)();', + node: { + type: 'VariableDeclaration', + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: 'bar', + }, + init: { + type: 'ArrowFunctionExpression', + id: null, + expression: true, + generator: false, + async: false, + params: [], + body: { + type: 'CallExpression', + optional: false, + arguments: [], + callee: { + type: 'SequenceExpression', + expressions: [ + { + type: 'Literal', + value: 0, + }, + { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'e', + }, + property: { + type: 'Identifier', + name: 'foo', + }, + computed: false, + optional: false, + }, + ], + }, + }, + }, + }, + ], + }, +}; diff --git a/packages/apps/deno-runtime/lib/ast/tests/operations.test.ts b/packages/apps/deno-runtime/lib/ast/tests/operations.test.ts new file mode 100644 index 0000000000000..809de475013c9 --- /dev/null +++ b/packages/apps/deno-runtime/lib/ast/tests/operations.test.ts @@ -0,0 +1,261 @@ +import { assertEquals, assertThrows } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; + +import { + asyncifyScope, + buildFixModifiedFunctionsOperation, + checkReassignmentOfModifiedIdentifiers, + getFunctionIdentifier, + WalkerState, + wrapWithAwait, +} from '../operations.ts'; +import { + ArrowFunctionDerefCallExpression, + AssignmentExpressionOfArrowFunctionToFooIdentifier, + AssignmentExpressionOfNamedFunctionToFooMemberExpression, + AssignmentOfFooToBar, + AssignmentOfFooToBarMemberExpression, + AssignmentOfFooToBarPropertyDefinition, + AssignmentOfFooToBarVariableDeclarator, + ConstFooAssignedFunctionExpression, + FixSimpleCallExpression, + FunctionDeclarationFoo, + MethodDefinitionOfFooInClassBar, + SimpleCallExpressionOfFoo, + SyncFunctionDeclarationWithAsyncCallExpression, +} from './data/ast_blocks.ts'; +import { + AnyNode, + ArrowFunctionExpression, + AssignmentExpression, + AwaitExpression, + Expression, + MethodDefinition, + ReturnStatement, + VariableDeclaration, +} from '../../../acorn.d.ts'; +import { assertNotEquals } from 'https://deno.land/std@0.203.0/assert/assert_not_equals.ts'; + +describe('getFunctionIdentifier', () => { + it(`identifies the name "foo" for the code \`${FunctionDeclarationFoo.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [FunctionDeclarationFoo.node]; + const functionNodeIndex = 0; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); + + it(`identifies the name "foo" for the code \`${ConstFooAssignedFunctionExpression.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [ + ConstFooAssignedFunctionExpression.node, // VariableDeclaration + ConstFooAssignedFunctionExpression.node.declarations[0], // VariableDeclarator + ConstFooAssignedFunctionExpression.node.declarations[0].init!, // FunctionExpression + ]; + const functionNodeIndex = 2; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); + + it(`identifies the name "foo" for the code \`${AssignmentExpressionOfArrowFunctionToFooIdentifier.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [ + AssignmentExpressionOfArrowFunctionToFooIdentifier.node, // ExpressionStatement + AssignmentExpressionOfArrowFunctionToFooIdentifier.node.expression, // AssignmentExpression + (AssignmentExpressionOfArrowFunctionToFooIdentifier.node.expression as AssignmentExpression).right, // ArrowFunctionExpression + ]; + const functionNodeIndex = 2; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); + + it(`identifies the name "foo" for the code \`${AssignmentExpressionOfNamedFunctionToFooMemberExpression.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [ + AssignmentExpressionOfNamedFunctionToFooMemberExpression.node, // ExpressionStatement + AssignmentExpressionOfNamedFunctionToFooMemberExpression.node.expression, // AssignmentExpression + (AssignmentExpressionOfNamedFunctionToFooMemberExpression.node.expression as AssignmentExpression).right, // FunctionExpression + ]; + const functionNodeIndex = 2; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); + + it(`identifies the name "foo" for the code \`${MethodDefinitionOfFooInClassBar.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [ + MethodDefinitionOfFooInClassBar.node, // ClassDeclaration + MethodDefinitionOfFooInClassBar.node.body, // ClassBody + MethodDefinitionOfFooInClassBar.node.body!.body[0], // MethodDefinition + (MethodDefinitionOfFooInClassBar.node.body!.body[0] as MethodDefinition).value, // FunctionExpression + ]; + const functionNodeIndex = 3; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); +}); + +describe('wrapWithAwait', () => { + it('wraps a call expression with await', () => { + const node = structuredClone(SimpleCallExpressionOfFoo.node.expression); + wrapWithAwait(node); + + assertEquals('AwaitExpression', node.type); + assertNotEquals(SimpleCallExpressionOfFoo.node.expression.type, node.type); + assertEquals(SimpleCallExpressionOfFoo.node.expression, (node as AwaitExpression).argument); + }); + + it('throws if node is not an expression', () => { + const node = structuredClone(SimpleCallExpressionOfFoo.node); + assertThrows(() => wrapWithAwait(node as unknown as Expression)); + }); +}); + +describe('asyncifyScope', () => { + it('makes only the first function scope async', () => { + const node = structuredClone(SyncFunctionDeclarationWithAsyncCallExpression.node); + const ancestors: AnyNode[] = [ + node, // FunctionDeclaration + node.body, // BlockStatement + node.body!.body[0], // ReturnStatement + (node.body!.body[0] as ReturnStatement).argument!, // ArrowFunctionExpression + ((node.body!.body[0] as ReturnStatement).argument! as ArrowFunctionExpression).body, // AwaitExpression + (((node.body!.body[0] as ReturnStatement).argument! as ArrowFunctionExpression).body as AwaitExpression).argument, // CallExpression + ]; + const state: WalkerState = { + isModified: false, + functionIdentifiers: new Set(), + }; + + asyncifyScope(ancestors, state); + + // Assert the function did indeed change the expression to async + assertEquals(((node.body.body[0] as ReturnStatement).argument as ArrowFunctionExpression).async, true); + + // Assert the function did NOT change all ancestors in the chain + assertEquals(node.async, false); + + // Assert it couldn't find a function identifier + assertEquals(state.functionIdentifiers.size, 0); + }); +}); + +describe('checkReassignmentofModifiedIdentifiers', () => { + it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBar.code}"`, () => { + const node = structuredClone(AssignmentOfFooToBar.node); + const ancestors: AnyNode[] = [ + node, // ExpressionStatement + node.expression, // AssignmentExpression + (node.expression as AssignmentExpression).right, // Identifier + ]; + const state: WalkerState = { + isModified: true, + functionIdentifiers: new Set(['foo']), + }; + + checkReassignmentOfModifiedIdentifiers(node.expression, state, ancestors, ''); + + assertEquals(state.functionIdentifiers.has('bar'), true); + }); + + it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBarMemberExpression.code}"`, () => { + const node = structuredClone(AssignmentOfFooToBarMemberExpression.node); + const ancestors: AnyNode[] = [ + node, // ExpressionStatement + node.expression, // AssignmentExpression + (node.expression as AssignmentExpression).right, // Identifier + ]; + const state: WalkerState = { + isModified: true, + functionIdentifiers: new Set(['foo']), + }; + + checkReassignmentOfModifiedIdentifiers(node.expression, state, ancestors, ''); + + assertEquals(state.functionIdentifiers.has('bar'), true); + }); + + it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBarVariableDeclarator.code}"`, () => { + const node = structuredClone(AssignmentOfFooToBarVariableDeclarator.node); + const ancestors: AnyNode[] = [ + node, // VariableDeclaration + node.declarations[0], // VariableDeclarator + ]; + const state: WalkerState = { + isModified: true, + functionIdentifiers: new Set(['foo']), + }; + + checkReassignmentOfModifiedIdentifiers(node.declarations[0], state, ancestors, ''); + + assertEquals(state.functionIdentifiers.has('bar'), true); + }); + + it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBarPropertyDefinition.code}"`, () => { + const node = structuredClone(AssignmentOfFooToBarPropertyDefinition.node); + const ancestors: AnyNode[] = [ + node, // ClassDeclaration + node.body, // ClassBody + node.body.body[0], // PropertyDefinition + ]; + const state: WalkerState = { + isModified: true, + functionIdentifiers: new Set(['foo']), + }; + + checkReassignmentOfModifiedIdentifiers(node.body.body[0], state, ancestors, ''); + + assertEquals(state.functionIdentifiers.has('bar'), true); + }); +}); + +describe('buildFixModifiedFunctionsOperation', function () { + const state: WalkerState = { + isModified: false, + functionIdentifiers: new Set(['foo']), + }; + + const fixFunction = buildFixModifiedFunctionsOperation(state.functionIdentifiers); + + beforeEach(() => { + state.isModified = false; + state.functionIdentifiers = new Set(['foo']); + }); + + it(`fixes calls of "foo" in the code "${FixSimpleCallExpression.code}"`, () => { + const node = structuredClone(FixSimpleCallExpression.node); + const ancestors: AnyNode[] = [ + node, // FunctionDeclaration + node.body, // BlockStatement + node.body.body[0], // VariableDeclaration + (node.body.body[0] as VariableDeclaration).declarations[0], // VariableDeclarator + (node.body.body[0] as VariableDeclaration).declarations[0].init!, // CallExpression + ]; + + fixFunction(ancestors[4], state, ancestors, ''); + + assertEquals(state.isModified, true); + assertEquals(state.functionIdentifiers.has('bar'), true); + assertNotEquals(FixSimpleCallExpression.node, node); + assertEquals(node.async, true); + assertEquals(ancestors[4].type, 'AwaitExpression'); + }); + + it(`fixes calls of "foo" in the code "${ArrowFunctionDerefCallExpression.code}"`, () => { + const node = structuredClone(ArrowFunctionDerefCallExpression.node); + const ancestors: AnyNode[] = [ + node, // VariableDeclaration + node.declarations[0], // VariableDeclarator + node.declarations[0].init!, // ArrowFunctionExpression + (node.declarations[0].init as ArrowFunctionExpression).body, // CallExpression + ]; + + fixFunction(ancestors[3], state, ancestors, ''); + + // Recorded that a modification has been made + assertEquals(state.isModified, true); + // Recorded that the enclosing scope of the call also requires fixing + assertEquals(state.functionIdentifiers.has('bar'), true); + // Original node and fixed node are different + assertNotEquals(ArrowFunctionDerefCallExpression.node, node); + // The function call is now await'ed + assertEquals(ancestors[3].type, 'AwaitExpression'); + // The parent function of the call is now marked as async + assertEquals((ancestors[2] as ArrowFunctionExpression).async, true); + }); +}); diff --git a/packages/apps/deno-runtime/lib/codec.ts b/packages/apps/deno-runtime/lib/codec.ts new file mode 100644 index 0000000000000..95bbfa1a2aa26 --- /dev/null +++ b/packages/apps/deno-runtime/lib/codec.ts @@ -0,0 +1,43 @@ +import { Buffer } from 'node:buffer'; +import { Decoder, Encoder, ExtensionCodec } from '@msgpack/msgpack'; + +import type { App as _App } from '@rocket.chat/apps-engine/definition/App.ts'; +import { require } from './require.ts'; + +const { App } = require('@rocket.chat/apps-engine/definition/App.js') as { + App: typeof _App; +}; + +const extensionCodec = new ExtensionCodec(); + +extensionCodec.register({ + type: 0, + encode: (object: unknown) => { + // We don't care about functions, but also don't want to throw an error + if (typeof object === 'function' || object instanceof App) { + return new Uint8Array(0); + } + + return null; + }, + decode: (_data: Uint8Array) => undefined, +}); + +// Since Deno doesn't have Buffer by default, we need to use Uint8Array +extensionCodec.register({ + type: 1, + encode: (object: unknown) => { + if (object instanceof Buffer) { + return new Uint8Array(object.buffer, object.byteOffset, object.byteLength); + } + + return null; + }, + // msgpack will reuse the Uint8Array instance, so WE NEED to copy it instead of simply creating a view + decode: (data: Uint8Array) => { + return Buffer.from(data); + }, +}); + +export const encoder = new Encoder({ extensionCodec }); +export const decoder = new Decoder({ extensionCodec }); diff --git a/packages/apps/deno-runtime/lib/logger.ts b/packages/apps/deno-runtime/lib/logger.ts new file mode 100644 index 0000000000000..336c420080d37 --- /dev/null +++ b/packages/apps/deno-runtime/lib/logger.ts @@ -0,0 +1,142 @@ +import stackTrace from 'stack-trace'; +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; + +export interface StackFrame { + getTypeName(): string; + getFunctionName(): string; + getMethodName(): string; + getFileName(): string; + getLineNumber(): number; + getColumnNumber(): number; + isNative(): boolean; + isConstructor(): boolean; +} + +enum LogMessageSeverity { + DEBUG = 'debug', + INFORMATION = 'info', + LOG = 'log', + WARNING = 'warning', + ERROR = 'error', + SUCCESS = 'success', +} + +type Entry = { + caller: string; + severity: LogMessageSeverity; + method: string; + timestamp: Date; + args: Array; +}; + +interface ILoggerStorageEntry { + appId: string; + method: string; + entries: Array; + startTime: Date; + endTime: Date; + totalTime: number; + _createdAt: Date; +} + +export class Logger { + private entries: Array; + private start: Date; + private method: string; + + constructor(method: string) { + this.method = method; + this.entries = []; + this.start = new Date(); + } + + public debug(...args: Array): void { + this.addEntry(LogMessageSeverity.DEBUG, this.getStack(stackTrace.get()), ...args); + } + + public info(...args: Array): void { + this.addEntry(LogMessageSeverity.INFORMATION, this.getStack(stackTrace.get()), ...args); + } + + public log(...args: Array): void { + this.addEntry(LogMessageSeverity.LOG, this.getStack(stackTrace.get()), ...args); + } + + public warn(...args: Array): void { + this.addEntry(LogMessageSeverity.WARNING, this.getStack(stackTrace.get()), ...args); + } + + public error(...args: Array): void { + this.addEntry(LogMessageSeverity.ERROR, this.getStack(stackTrace.get()), ...args); + } + + public success(...args: Array): void { + this.addEntry(LogMessageSeverity.SUCCESS, this.getStack(stackTrace.get()), ...args); + } + + private addEntry(severity: LogMessageSeverity, caller: string, ...items: Array): void { + const i = items.map((args) => { + if (args instanceof Error) { + return JSON.stringify(args, Object.getOwnPropertyNames(args)); + } + if (typeof args === 'object' && args !== null && 'stack' in args) { + return JSON.stringify(args, Object.getOwnPropertyNames(args)); + } + if (typeof args === 'object' && args !== null && 'message' in args) { + return JSON.stringify(args, Object.getOwnPropertyNames(args)); + } + const str = JSON.stringify(args, null, 2); + return str ? JSON.parse(str) : str; // force call toJSON to prevent circular references + }); + + this.entries.push({ + caller, + severity, + method: this.method, + timestamp: new Date(), + args: i, + }); + } + + private getStack(stack: Array): string { + let func = 'anonymous'; + + if (stack.length === 1) { + return func; + } + + const frame = stack[1]; + + if (frame.getMethodName() === null) { + func = 'anonymous OR constructor'; + } else { + func = frame.getMethodName(); + } + + if (frame.getFunctionName() !== null) { + func = `${func} -> ${frame.getFunctionName()}`; + } + + return func; + } + + private getTotalTime(): number { + return new Date().getTime() - this.start.getTime(); + } + + public hasEntries(): boolean { + return this.entries.length > 0; + } + + public getLogs(): ILoggerStorageEntry { + return { + appId: AppObjectRegistry.get('id')!, + method: this.method, + entries: this.entries, + startTime: this.start, + endTime: new Date(), + totalTime: this.getTotalTime(), + _createdAt: new Date(), + }; + } +} diff --git a/packages/apps/deno-runtime/lib/messenger.ts b/packages/apps/deno-runtime/lib/messenger.ts new file mode 100644 index 0000000000000..3a55aba594f7c --- /dev/null +++ b/packages/apps/deno-runtime/lib/messenger.ts @@ -0,0 +1,202 @@ +import { writeAll } from '@std/io'; + +import * as jsonrpc from 'jsonrpc-lite'; + +import { encoder } from './codec.ts'; +import { RequestContext } from './requestContext.ts'; + +export type RequestDescriptor = Pick; + +export type NotificationDescriptor = Pick; + +export type SuccessResponseDescriptor = Pick; + +export type ErrorResponseDescriptor = Pick; + +export type JsonRpcRequest = jsonrpc.IParsedObjectRequest | jsonrpc.IParsedObjectNotification; +export type JsonRpcResponse = jsonrpc.IParsedObjectSuccess | jsonrpc.IParsedObjectError; + +export function isRequest(message: jsonrpc.IParsedObject): message is JsonRpcRequest { + return message.type === 'request' || message.type === 'notification'; +} + +export function isResponse(message: jsonrpc.IParsedObject): message is JsonRpcResponse { + return message.type === 'success' || message.type === 'error'; +} + +export function isErrorResponse(message: jsonrpc.JsonRpc): message is jsonrpc.ErrorObject { + return message instanceof jsonrpc.ErrorObject; +} + +const COMMAND_PONG = '_zPONG'; + +export const RPCResponseObserver = new EventTarget(); + +export const Queue = new (class Queue { + private queue: Uint8Array[] = []; + private isProcessing = false; + + private async processQueue() { + if (this.isProcessing) { + return; + } + + this.isProcessing = true; + + while (this.queue.length) { + const message = this.queue.shift(); + + if (message) { + await Transport.send(message); + } + } + + this.isProcessing = false; + } + + public enqueue(message: jsonrpc.JsonRpc | typeof COMMAND_PONG) { + this.queue.push(encoder.encode(message)); + this.processQueue(); + } + + public getCurrentSize() { + return this.queue.length; + } +})(); + +export const Transport = new (class Transporter { + private selectedTransport: Transporter['stdoutTransport'] | Transporter['noopTransport']; + + constructor() { + this.selectedTransport = this.stdoutTransport.bind(this); + } + + private async stdoutTransport(message: Uint8Array): Promise { + await writeAll(Deno.stdout, message); + } + + private async noopTransport(_message: Uint8Array): Promise {} + + public selectTransport(transport: 'stdout' | 'noop'): void { + switch (transport) { + case 'stdout': + this.selectedTransport = this.stdoutTransport.bind(this); + break; + case 'noop': + this.selectedTransport = this.noopTransport.bind(this); + break; + } + } + + public send(message: Uint8Array): Promise { + return this.selectedTransport(message); + } +})(); + +export function parseMessage(message: string | Record) { + let parsed: jsonrpc.IParsedObject | jsonrpc.IParsedObject[]; + + if (typeof message === 'string') { + parsed = jsonrpc.parse(message); + } else { + parsed = jsonrpc.parseObject(message); + } + + if (Array.isArray(parsed)) { + throw jsonrpc.error(null, jsonrpc.JsonRpcError.invalidRequest(null)); + } + + if (parsed.type === 'invalid') { + throw jsonrpc.error(null, parsed.payload); + } + + return parsed; +} + +export async function sendInvalidRequestError(): Promise { + const rpc = jsonrpc.error(null, jsonrpc.JsonRpcError.invalidRequest(null)); + + await Queue.enqueue(rpc); +} + +export async function sendInvalidParamsError(id: jsonrpc.ID): Promise { + const rpc = jsonrpc.error(id, jsonrpc.JsonRpcError.invalidParams(null)); + + await Queue.enqueue(rpc); +} + +export async function sendParseError(): Promise { + const rpc = jsonrpc.error(null, jsonrpc.JsonRpcError.parseError(null)); + + await Queue.enqueue(rpc); +} + +export async function sendMethodNotFound(id: jsonrpc.ID): Promise { + const rpc = jsonrpc.error(id, jsonrpc.JsonRpcError.methodNotFound(null)); + + await Queue.enqueue(rpc); +} + +export async function errorResponse({ error: { message, code = -32000, data = {} }, id }: ErrorResponseDescriptor, req?: RequestContext): Promise { + const { logger } = req?.context || {}; + + if (logger?.hasEntries()) { + data.logs = logger.getLogs(); + } + + const rpc = jsonrpc.error(id, new jsonrpc.JsonRpcError(message, code, data)); + + await Queue.enqueue(rpc); +} + +export async function successResponse({ id, result }: SuccessResponseDescriptor, req: RequestContext): Promise { + const payload = { value: result } as Record; + const { logger } = req.context; + + if (logger.hasEntries()) { + payload.logs = logger.getLogs(); + } + + const rpc = jsonrpc.success(id, payload); + + await Queue.enqueue(rpc); +} + +export function pongResponse(): Promise { + return Promise.resolve(Queue.enqueue(COMMAND_PONG)); +} + +export async function sendRequest(requestDescriptor: RequestDescriptor): Promise { + const request = jsonrpc.request(Math.random().toString(36).slice(2), requestDescriptor.method, requestDescriptor.params); + + // TODO: add timeout to this + const responsePromise = new Promise((resolve, reject) => { + const handler = (event: Event) => { + if (event instanceof ErrorEvent) { + reject(event.error); + } + + if (event instanceof CustomEvent) { + resolve(event.detail); + } + + RPCResponseObserver.removeEventListener(`response:${request.id}`, handler); + }; + + RPCResponseObserver.addEventListener(`response:${request.id}`, handler); + }); + + await Queue.enqueue(request); + + return responsePromise as Promise; +} + +export function sendNotification({ method, params }: NotificationDescriptor) { + const request = jsonrpc.notification(method, params); + + Queue.enqueue(request); +} + +export function log(params: jsonrpc.RpcParams) { + sendNotification({ method: 'log', params }); +} diff --git a/packages/apps/deno-runtime/lib/metricsCollector.ts b/packages/apps/deno-runtime/lib/metricsCollector.ts new file mode 100644 index 0000000000000..8484aef826f9b --- /dev/null +++ b/packages/apps/deno-runtime/lib/metricsCollector.ts @@ -0,0 +1,20 @@ +import { writeAll } from '@std/io'; +import { Queue } from './messenger.ts'; + +export function collectMetrics() { + return { + pid: Deno.pid, + queueSize: Queue.getCurrentSize(), + }; +} + +const encoder = new TextEncoder(); + +/** + * Sends metrics collected from the system via stderr + */ +export async function sendMetrics() { + const metrics = collectMetrics(); + + await writeAll(Deno.stderr, encoder.encode(JSON.stringify(metrics))); +} diff --git a/packages/apps/deno-runtime/lib/parseArgs.ts b/packages/apps/deno-runtime/lib/parseArgs.ts new file mode 100644 index 0000000000000..a9c4844154990 --- /dev/null +++ b/packages/apps/deno-runtime/lib/parseArgs.ts @@ -0,0 +1,11 @@ +import { parseArgs as $parseArgs } from '@std/cli/parse-args'; + +export type ParsedArgs = { + subprocess: string; + spawnId: number; + metricsReportFrequencyInMs?: number; +}; + +export function parseArgs(args: string[]): ParsedArgs { + return $parseArgs(args); +} diff --git a/packages/apps/deno-runtime/lib/requestContext.ts b/packages/apps/deno-runtime/lib/requestContext.ts new file mode 100644 index 0000000000000..91e9346f34bd4 --- /dev/null +++ b/packages/apps/deno-runtime/lib/requestContext.ts @@ -0,0 +1,10 @@ +import { RequestObject } from 'jsonrpc-lite'; + +import { Logger } from './logger.ts'; + +export type RequestContext = RequestObject & { + context: { + logger: Logger; + [key: string]: unknown; + } +}; diff --git a/packages/apps/deno-runtime/lib/require.ts b/packages/apps/deno-runtime/lib/require.ts new file mode 100644 index 0000000000000..7d842d829e598 --- /dev/null +++ b/packages/apps/deno-runtime/lib/require.ts @@ -0,0 +1,15 @@ +import { createRequire } from 'node:module'; + +const _require = createRequire(import.meta.url); + +export const require = (mod: string) => { + // When we try to import something from the apps-engine, we resolve the path using import maps from Deno + // However, the import maps are configured to look at the source folder for typescript files, but during + // runtime those files are not available + if (mod.startsWith('@rocket.chat/apps-engine')) { + // Only remove "src/" substring when it comes after "apps-engine/" + mod = import.meta.resolve(mod).replace('file://', '').replace('apps-engine/src/', 'apps-engine/'); + } + + return _require(mod); +}; diff --git a/packages/apps/deno-runtime/lib/room.ts b/packages/apps/deno-runtime/lib/room.ts new file mode 100644 index 0000000000000..282ded4a90457 --- /dev/null +++ b/packages/apps/deno-runtime/lib/room.ts @@ -0,0 +1,104 @@ +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; +import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager.ts'; + +const PrivateManager = Symbol('RoomPrivateManager'); + +export class Room { + public id: string | undefined; + + public displayName?: string; + + public slugifiedName: string | undefined; + + public type: RoomType | undefined; + + public creator: IUser | undefined; + + public isDefault?: boolean; + + public isReadOnly?: boolean; + + public displaySystemMessages?: boolean; + + public messageCount?: number; + + public createdAt?: Date; + + public updatedAt?: Date; + + public lastModifiedAt?: Date; + + public customFields?: { [key: string]: unknown }; + + public userIds?: Array; + + private _USERNAMES: Promise> | undefined; + + private [PrivateManager]: AppManager | undefined; + + /** + * @deprecated + */ + public get usernames(): Promise> { + if (!this._USERNAMES) { + this._USERNAMES = this[PrivateManager]?.getBridges().getInternalBridge().doGetUsernamesOfRoomById(this.id); + } + + return this._USERNAMES || Promise.resolve([]); + } + + public set usernames(usernames) {} + + 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 { + return { + id: this.id, + displayName: this.displayName, + slugifiedName: this.slugifiedName, + type: this.type, + creator: this.creator, + isDefault: this.isDefault, + isReadOnly: this.isReadOnly, + displaySystemMessages: this.displaySystemMessages, + messageCount: this.messageCount, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + lastModifiedAt: this.lastModifiedAt, + customFields: this.customFields, + userIds: this.userIds, + }; + } + + public async getUsernames(): Promise> { + // Get usernames + if (!this._USERNAMES) { + this._USERNAMES = await this[PrivateManager]?.getBridges().getInternalBridge().doGetUsernamesOfRoomById(this.id); + } + + return this._USERNAMES || []; + } + + public toJSON() { + return this.value; + } + + public toString() { + return this.value; + } + + public valueOf() { + return this.value; + } +} diff --git a/packages/apps/deno-runtime/lib/roomFactory.ts b/packages/apps/deno-runtime/lib/roomFactory.ts new file mode 100644 index 0000000000000..e0c2b9f1c4c80 --- /dev/null +++ b/packages/apps/deno-runtime/lib/roomFactory.ts @@ -0,0 +1,29 @@ +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager.ts'; + +import { AppAccessors } from './accessors/mod.ts'; +import { Room } from './room.ts'; +import { formatErrorResponse } from './accessors/formatResponseErrorHandler.ts'; + +const getMockAppManager = (senderFn: AppAccessors['senderFn']) => ({ + getBridges: () => ({ + getInternalBridge: () => ({ + doGetUsernamesOfRoomById: (roomId: string) => { + return senderFn({ + method: 'bridges:getInternalBridge:doGetUsernamesOfRoomById', + params: [roomId], + }) + .then((result) => result.result) + .catch((err) => { + throw formatErrorResponse(err); + }); + }, + }), + }), +}); + +export default function createRoom(room: IRoom, senderFn: AppAccessors['senderFn']) { + const mockAppManager = getMockAppManager(senderFn); + + return new Room(room, mockAppManager as unknown as AppManager); +} diff --git a/packages/apps/deno-runtime/lib/sanitizeDeprecatedUsage.ts b/packages/apps/deno-runtime/lib/sanitizeDeprecatedUsage.ts new file mode 100644 index 0000000000000..4b5838bce12d1 --- /dev/null +++ b/packages/apps/deno-runtime/lib/sanitizeDeprecatedUsage.ts @@ -0,0 +1,20 @@ +import { fixBrokenSynchronousAPICalls } from './ast/mod.ts'; + +function hasPotentialDeprecatedUsage(source: string) { + return ( + // potential usage of Room.usernames getter + source.includes('.usernames') || + // potential usage of LivechatRead.isOnline method + source.includes('.isOnline(') || + // potential usage of LivechatCreator.createToken method + source.includes('.createToken(') + ); +} + +export function sanitizeDeprecatedUsage(source: string) { + if (!hasPotentialDeprecatedUsage(source)) { + return source; + } + + return fixBrokenSynchronousAPICalls(source); +} diff --git a/packages/apps/deno-runtime/lib/tests/logger.test.ts b/packages/apps/deno-runtime/lib/tests/logger.test.ts new file mode 100644 index 0000000000000..7ccc49b3b9ca4 --- /dev/null +++ b/packages/apps/deno-runtime/lib/tests/logger.test.ts @@ -0,0 +1,110 @@ +import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { Logger } from '../logger.ts'; + +describe('Logger', () => { + it('getLogs should return an array of entries', () => { + const logger = new Logger('test'); + logger.info('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.method, 'test'); + }); + + it('should be able to add entries of different severity', () => { + const logger = new Logger('test'); + logger.info('test'); + logger.debug('test'); + logger.error('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 3); + assertEquals(logs.entries[0].severity, 'info'); + assertEquals(logs.entries[1].severity, 'debug'); + assertEquals(logs.entries[2].severity, 'error'); + }); + + it('should be able to add an info entry', () => { + const logger = new Logger('test'); + logger.info('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'info'); + }); + + it('should be able to add an debug entry', () => { + const logger = new Logger('test'); + logger.debug('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'debug'); + }); + + it('should be able to add an error entry', () => { + const logger = new Logger('test'); + logger.error('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'error'); + }); + + it('should be able to add an success entry', () => { + const logger = new Logger('test'); + logger.success('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'success'); + }); + + it('should be able to add an warning entry', () => { + const logger = new Logger('test'); + logger.warn('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'warning'); + }); + + it('should be able to add an log entry', () => { + const logger = new Logger('test'); + logger.log('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'log'); + }); + + it('should be able to add an entry with multiple arguments', () => { + const logger = new Logger('test'); + logger.log('test', 'test', 'test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].args[1], 'test'); + assertEquals(logs.entries[0].args[2], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'log'); + }); + + it('should be able to add an entry with multiple arguments of different types', () => { + const logger = new Logger('test'); + logger.log('test', 1, true, { foo: 'bar' }); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].args[1], 1); + assertEquals(logs.entries[0].args[2], true); + assertEquals(logs.entries[0].args[3], { foo: 'bar' }); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'log'); + }); +}); diff --git a/packages/apps/deno-runtime/lib/tests/messenger.test.ts b/packages/apps/deno-runtime/lib/tests/messenger.test.ts new file mode 100644 index 0000000000000..47b46f0db6e33 --- /dev/null +++ b/packages/apps/deno-runtime/lib/tests/messenger.test.ts @@ -0,0 +1,99 @@ +import { assertEquals, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; + +import * as Messenger from '../messenger.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { createMockRequest } from '../../handlers/tests/helpers/mod.ts'; +import { RequestContext } from '../requestContext.ts'; +import { JsonRpc } from 'jsonrpc-lite'; + +describe('Messenger', () => { + let context: RequestContext; + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('id', 'test'); + Messenger.Transport.selectTransport('noop'); + + context = createMockRequest({ method: 'test', params: [] }); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + Messenger.Transport.selectTransport('stdout'); + }); + + it('should add logs to success responses', async () => { + const theSpy = spy(Messenger.Queue, 'enqueue'); + const { logger } = context.context; + + logger.info('test'); + + await Messenger.successResponse({ id: 'test', result: 'test' }, context); + + assertEquals(theSpy.calls.length, 1); + + const [responseArgument] = theSpy.calls[0].args; + + assertObjectMatch(responseArgument as JsonRpc, { + jsonrpc: '2.0', + id: 'test', + result: { + value: 'test', + logs: { + appId: 'test', + method: 'test', + entries: [ + { + severity: 'info', + method: 'test', + args: ['test'], + caller: 'anonymous OR constructor', + }, + ], + }, + }, + }); + + theSpy.restore(); + }); + + it('should add logs to error responses', async () => { + const theSpy = spy(Messenger.Queue, 'enqueue'); + const { logger } = context.context; + + logger.info('test'); + + await Messenger.errorResponse({ id: 'test', error: { code: -32000, message: 'test' } }, context); + + assertEquals(theSpy.calls.length, 1); + + const [responseArgument] = theSpy.calls[0].args; + + assertObjectMatch(responseArgument as JsonRpc, { + jsonrpc: '2.0', + id: 'test', + error: { + code: -32000, + message: 'test', + data: { + logs: { + appId: 'test', + method: 'test', + entries: [ + { + severity: 'info', + method: 'test', + args: ['test'], + caller: 'anonymous OR constructor', + }, + ], + }, + }, + }, + }); + + theSpy.restore(); + }); +}); diff --git a/packages/apps/deno-runtime/lib/wrapAppForRequest.ts b/packages/apps/deno-runtime/lib/wrapAppForRequest.ts new file mode 100644 index 0000000000000..e9643c2c0274a --- /dev/null +++ b/packages/apps/deno-runtime/lib/wrapAppForRequest.ts @@ -0,0 +1,60 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { RequestContext } from './requestContext.ts'; +import { isApp, isRecord } from '../handlers/lib/assertions.ts'; + +export function wrapAppForRequest(app: App, req: RequestContext): App { + return new Proxy(app, { + get(target, property, receiver) { + if (property === 'logger') { + return req.context.logger; + } + + return Reflect.get(target, property, receiver); + }, + }); +} + +// Instances of objects that have a reference to an App instance won't change throughout the +// lifetime of the runtime, so we can cache the results to avoid iterating the same object multiple times +const composedCache = new WeakMap, ReturnType>(); + +function findAppProperty(v: NonNullable): [string, App] | undefined { + const cachedEntry = composedCache.get(v); + + if (cachedEntry) { + return cachedEntry; + } + + if (!isRecord(v)) { + // Enables us to avoid having to determine whether the value is a record again + composedCache.set(v, undefined); + return undefined; + } + + const entry = Object.entries(v).find(([_,v]) => isApp(v)) as [string, App] | undefined; + + composedCache.set(v, entry); + + return entry; +} + +export function wrapComposedApp>(composed: T, req: RequestContext): T { + const prop = findAppProperty(composed); + + if (!prop) { + return composed; + } + + const proxy = wrapAppForRequest(prop[1], req); + + return new Proxy(composed, { + get(target, property, receiver) { + if (property === prop[0]) { + return proxy; + } + + return Reflect.get(target, property, receiver); + }, + }) +} diff --git a/packages/apps/deno-runtime/main.ts b/packages/apps/deno-runtime/main.ts new file mode 100644 index 0000000000000..dc7dacbcea7d0 --- /dev/null +++ b/packages/apps/deno-runtime/main.ts @@ -0,0 +1,132 @@ +if (!Deno.args.includes('--subprocess')) { + Deno.stderr.writeSync( + new TextEncoder().encode(` + This is a Deno wrapper for Rocket.Chat Apps. It is not meant to be executed stand-alone; + It is instead meant to be executed as a subprocess by the Apps-Engine framework. + `), + ); + Deno.exit(1001); +} + +import { JsonRpcError } from 'jsonrpc-lite'; + +import * as Messenger from './lib/messenger.ts'; +import { decoder } from './lib/codec.ts'; +import { Logger } from './lib/logger.ts'; + +import slashcommandHandler from './handlers/slashcommand-handler.ts'; +import videoConferenceHandler from './handlers/videoconference-handler.ts'; +import apiHandler from './handlers/api-handler.ts'; +import handleApp from './handlers/app/handler.ts'; +import handleScheduler from './handlers/scheduler-handler.ts'; +import registerErrorListeners from './error-handlers.ts'; +import { sendMetrics } from './lib/metricsCollector.ts'; +import outboundMessageHandler from './handlers/outboundcomms-handler.ts'; +import { RequestContext } from './lib/requestContext.ts'; + +type Handlers = { + app: typeof handleApp; + api: typeof apiHandler; + slashcommand: typeof slashcommandHandler; + videoconference: typeof videoConferenceHandler; + outboundCommunication: typeof outboundMessageHandler; + scheduler: typeof handleScheduler; + ping: (request: RequestContext) => 'pong'; +}; + +const COMMAND_PING = '_zPING'; + +async function requestRouter({ type, payload }: Messenger.JsonRpcRequest): Promise { + const methodHandlers: Handlers = { + app: handleApp, + api: apiHandler, + slashcommand: slashcommandHandler, + videoconference: videoConferenceHandler, + outboundCommunication: outboundMessageHandler, + scheduler: handleScheduler, + ping: (_request) => 'pong', + }; + + // We're not handling notifications at the moment + if (type === 'notification') { + return Messenger.sendInvalidRequestError(); + } + + const { id, method } = payload; + + const logger = new Logger(method); + + const context: RequestContext = Object.assign(payload, { + context: { logger } + }) + + const [methodPrefix] = method.split(':') as [keyof Handlers]; + const handler = methodHandlers[methodPrefix]; + + if (!handler) { + return Messenger.errorResponse({ + error: { message: 'Method not found', code: -32601 }, + id, + }, context); + } + + const result = await handler(context); + + if (result instanceof JsonRpcError) { + return Messenger.errorResponse({ id, error: result }, context); + } + + return Messenger.successResponse({ id, result }, context); +} + +function handleResponse(response: Messenger.JsonRpcResponse): void { + let event: Event; + + if (response.type === 'error') { + event = new ErrorEvent(`response:${response.payload.id}`, { + error: response.payload, + }); + } else { + event = new CustomEvent(`response:${response.payload.id}`, { + detail: response.payload, + }); + } + + Messenger.RPCResponseObserver.dispatchEvent(event); +} + +async function main() { + Messenger.sendNotification({ method: 'ready' }); + + for await (const message of decoder.decodeStream(Deno.stdin.readable)) { + try { + // Process PING command first as it is not JSON RPC + if (message === COMMAND_PING) { + void Messenger.pongResponse(); + void sendMetrics(); + continue; + } + + const JSONRPCMessage = Messenger.parseMessage(message as Record); + + if (Messenger.isRequest(JSONRPCMessage)) { + void requestRouter(JSONRPCMessage); + continue; + } + + if (Messenger.isResponse(JSONRPCMessage)) { + handleResponse(JSONRPCMessage); + } + } catch (error) { + if (Messenger.isErrorResponse(error)) { + await Messenger.errorResponse(error); + } else { + await Messenger.sendParseError(); + } + } + } +} + +registerErrorListeners(); + +main(); From 75fb7769799c0018d7c3f2d384a0e2eafc565cd4 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Apr 2026 13:18:07 -0300 Subject: [PATCH 06/19] refactor(apps): merge IListenerBridge augmentation into the interface directly The old src/bridges/IListenerBridge.ts used module augmentation (`declare module '@rocket.chat/apps-engine/server/bridges'`) to extend IListenerBridge with core-typings-specific overloads. Now that IListenerBridge lives in this package, the augmentation workaround is no longer needed. The extra overload signatures are merged directly into src/server/bridges/IListenerBridge.ts and the augmentation file is deleted. Co-Authored-By: Claude Sonnet 4.6 --- packages/apps/src/bridges/IListenerBridge.ts | 55 ------------------ packages/apps/src/index.ts | 2 - .../src/server/bridges/IListenerBridge.ts | 57 +++++++++++++++++-- 3 files changed, 53 insertions(+), 61 deletions(-) delete mode 100644 packages/apps/src/bridges/IListenerBridge.ts diff --git a/packages/apps/src/bridges/IListenerBridge.ts b/packages/apps/src/bridges/IListenerBridge.ts deleted file mode 100644 index 83c3910d152cc..0000000000000 --- a/packages/apps/src/bridges/IListenerBridge.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { IMessage, IRoom, IUser, ILivechatDepartment, ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings'; - -import type { AppEvents } from '../AppsEngine'; - -declare module '@rocket.chat/apps-engine/server/bridges' { - interface IListenerBridge { - messageEvent(int: 'IPostMessageDeleted', message: IMessage, userDeleted: IUser): Promise; - messageEvent(int: 'IPostMessageReacted', message: IMessage, userReacted: IUser, reaction: string, isReacted: boolean): Promise; - messageEvent(int: 'IPostMessageFollowed', message: IMessage, userFollowed: IUser, isFollowed: boolean): Promise; - messageEvent(int: 'IPostMessagePinned', message: IMessage, userPinned: IUser, isPinned: boolean): Promise; - messageEvent(int: 'IPostMessageStarred', message: IMessage, userStarred: IUser, isStarred: boolean): Promise; - messageEvent(int: 'IPostMessageReported', message: IMessage, userReported: IUser, reason: boolean): Promise; - - messageEvent( - int: 'IPreMessageSentPrevent' | 'IPreMessageDeletePrevent' | 'IPreMessageUpdatedPrevent', - message: IMessage, - ): Promise; - messageEvent( - int: 'IPreMessageSentExtend' | 'IPreMessageSentModify' | 'IPreMessageUpdatedExtend' | 'IPreMessageUpdatedModify', - message: IMessage, - ): Promise; - messageEvent(int: 'IPostMessageSent' | 'IPostMessageUpdated' | 'IPostSystemMessageSent', message: IMessage): Promise; - - roomEvent(int: 'IPreRoomUserJoined' | 'IPostRoomUserJoined', room: IRoom, joiningUser: IUser, invitingUser?: IUser): Promise; - roomEvent(int: 'IPreRoomUserLeave' | 'IPostRoomUserLeave', room: IRoom, leavingUser: IUser): Promise; - - roomEvent(int: 'IPreRoomCreatePrevent' | 'IPreRoomDeletePrevent', room: IRoom): Promise; - roomEvent(int: 'IPreRoomCreateExtend' | 'IPreRoomCreateModify', room: IRoom): Promise; - roomEvent(int: 'IPostRoomCreate' | 'IPostRoomDeleted', room: IRoom): Promise; - - livechatEvent( - int: - | 'IPostLivechatAgentAssigned' - | 'IPostLivechatAgentUnassigned' - | 'IPostLivechatDepartmentRemoved' - | 'IPostLivechatDepartmentDisabled', - data: { user: IUser; room: IOmnichannelRoom }, - ): Promise; - livechatEvent( - int: 'IPostLivechatRoomTransferred', - data: { type: 'agent'; room: IRoom['_id']; from: IUser['_id']; to: IUser['_id'] }, - ): Promise; - livechatEvent( - int: 'IPostLivechatRoomTransferred', - data: { type: 'department'; room: IRoom['_id']; from: ILivechatDepartment['_id']; to: ILivechatDepartment['_id'] }, - ): Promise; - livechatEvent(int: 'IPostLivechatGuestSaved', data: ILivechatVisitor['_id']): Promise; - livechatEvent(int: 'IPostLivechatRoomSaved', data: IRoom['_id']): Promise; - livechatEvent( - int: 'ILivechatRoomClosedHandler' | 'IPostLivechatRoomStarted' | 'IPostLivechatRoomClosed' | 'IPreLivechatRoomCreatePrevent', - data: IRoom, - ): Promise; - livechatEvent(int: AppEvents | AppEvents[keyof AppEvents], data: any): Promise; - } -} diff --git a/packages/apps/src/index.ts b/packages/apps/src/index.ts index 31a44b6ce0578..89f846217284a 100644 --- a/packages/apps/src/index.ts +++ b/packages/apps/src/index.ts @@ -1,5 +1,3 @@ -import './bridges/IListenerBridge'; - export type * from './converters'; export * from './AppsEngine'; export type * from './IAppServerNotifier'; diff --git a/packages/apps/src/server/bridges/IListenerBridge.ts b/packages/apps/src/server/bridges/IListenerBridge.ts index b418b77dafc43..d4a595ed88c1e 100644 --- a/packages/apps/src/server/bridges/IListenerBridge.ts +++ b/packages/apps/src/server/bridges/IListenerBridge.ts @@ -1,10 +1,59 @@ -import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { ILivechatDepartment, ILivechatVisitor, IMessage, IOmnichannelRoom, IRoom, IUser } from '@rocket.chat/core-typings'; + +import type { AppEvents } from '../../AppsEngine'; +import type { IMessage as IAppsMessage } from '@rocket.chat/apps-engine/definition/messages'; import type { AppInterface } from '@rocket.chat/apps-engine/definition/metadata'; -import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IRoom as IAppsRoom } from '@rocket.chat/apps-engine/definition/rooms'; import type { UIKitIncomingInteraction } from '@rocket.chat/apps-engine/definition/uikit'; export interface IListenerBridge { - messageEvent(int: AppInterface, message: IMessage): Promise; - roomEvent(int: AppInterface, room: IRoom): Promise; + messageEvent(int: AppInterface, message: IAppsMessage): Promise; + roomEvent(int: AppInterface, room: IAppsRoom): Promise; uiKitInteractionEvent(int: AppInterface, action: UIKitIncomingInteraction): Promise; + + messageEvent(int: 'IPostMessageDeleted', message: IMessage, userDeleted: IUser): Promise; + messageEvent(int: 'IPostMessageReacted', message: IMessage, userReacted: IUser, reaction: string, isReacted: boolean): Promise; + messageEvent(int: 'IPostMessageFollowed', message: IMessage, userFollowed: IUser, isFollowed: boolean): Promise; + messageEvent(int: 'IPostMessagePinned', message: IMessage, userPinned: IUser, isPinned: boolean): Promise; + messageEvent(int: 'IPostMessageStarred', message: IMessage, userStarred: IUser, isStarred: boolean): Promise; + messageEvent(int: 'IPostMessageReported', message: IMessage, userReported: IUser, reason: boolean): Promise; + messageEvent( + int: 'IPreMessageSentPrevent' | 'IPreMessageDeletePrevent' | 'IPreMessageUpdatedPrevent', + message: IMessage, + ): Promise; + messageEvent( + int: 'IPreMessageSentExtend' | 'IPreMessageSentModify' | 'IPreMessageUpdatedExtend' | 'IPreMessageUpdatedModify', + message: IMessage, + ): Promise; + messageEvent(int: 'IPostMessageSent' | 'IPostMessageUpdated' | 'IPostSystemMessageSent', message: IMessage): Promise; + + roomEvent(int: 'IPreRoomUserJoined' | 'IPostRoomUserJoined', room: IRoom, joiningUser: IUser, invitingUser?: IUser): Promise; + roomEvent(int: 'IPreRoomUserLeave' | 'IPostRoomUserLeave', room: IRoom, leavingUser: IUser): Promise; + roomEvent(int: 'IPreRoomCreatePrevent' | 'IPreRoomDeletePrevent', room: IRoom): Promise; + roomEvent(int: 'IPreRoomCreateExtend' | 'IPreRoomCreateModify', room: IRoom): Promise; + roomEvent(int: 'IPostRoomCreate' | 'IPostRoomDeleted', room: IRoom): Promise; + + livechatEvent( + int: + | 'IPostLivechatAgentAssigned' + | 'IPostLivechatAgentUnassigned' + | 'IPostLivechatDepartmentRemoved' + | 'IPostLivechatDepartmentDisabled', + data: { user: IUser; room: IOmnichannelRoom }, + ): Promise; + livechatEvent( + int: 'IPostLivechatRoomTransferred', + data: { type: 'agent'; room: IRoom['_id']; from: IUser['_id']; to: IUser['_id'] }, + ): Promise; + livechatEvent( + int: 'IPostLivechatRoomTransferred', + data: { type: 'department'; room: IRoom['_id']; from: ILivechatDepartment['_id']; to: ILivechatDepartment['_id'] }, + ): Promise; + livechatEvent(int: 'IPostLivechatGuestSaved', data: ILivechatVisitor['_id']): Promise; + livechatEvent(int: 'IPostLivechatRoomSaved', data: IRoom['_id']): Promise; + livechatEvent( + int: 'ILivechatRoomClosedHandler' | 'IPostLivechatRoomStarted' | 'IPostLivechatRoomClosed' | 'IPreLivechatRoomCreatePrevent', + data: IRoom, + ): Promise; + livechatEvent(int: AppEvents | AppEvents[keyof AppEvents], data: any): Promise; } From 0f89a7a026f8e1a05601785d2269d6e700d77d48 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Apr 2026 13:18:54 -0300 Subject: [PATCH 07/19] chore(apps): add build config for server code and deno-runtime - package.json: add all runtime deps from apps-engine (msgpack, adm-zip, esbuild, jose, semver, etc.), deno-related devDeps (npm-run-all, rimraf, ts-node), build/test scripts, and include deno-runtime/ and scripts/ in published files - tsconfig.json: enable experimentalDecorators and emitDecoratorMetadata required by the incoming server code - turbo.json: declare build outputs (dist/, deno-runtime/, .deno-cache/) - scripts/deno-cache.js: copied from apps-engine; validates Deno version and pre-caches deno-runtime dependencies Co-Authored-By: Claude Sonnet 4.6 --- packages/apps/package.json | 36 ++++++++++-- packages/apps/scripts/deno-cache.js | 89 +++++++++++++++++++++++++++++ packages/apps/tsconfig.json | 4 +- packages/apps/turbo.json | 9 +++ 4 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 packages/apps/scripts/deno-cache.js create mode 100644 packages/apps/turbo.json diff --git a/packages/apps/package.json b/packages/apps/package.json index 8354a0adc1ed6..6950e17f2b97b 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -5,22 +5,50 @@ "main": "./dist/index.js", "typings": "./dist/index.d.ts", "files": [ - "/dist" + "/dist", + "/deno-runtime", + "/scripts" ], "scripts": { - "build": "rm -rf dist && tsc -p tsconfig.json", + "build": "run-s build:clean build:default build:deno-cache", + "build:clean": "rimraf dist", + "build:default": "tsc -p tsconfig.json", + "build:deno-cache": "node scripts/deno-cache.js", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", "lint": "eslint .", - "lint:fix": "eslint --fix ." + "lint:fix": "eslint --fix .", + "test:deno": "cd deno-runtime && deno task test", + "test:node": "NODE_ENV=test node --require ts-node/register/transpile-only --test-reporter spec --test \"tests/**/*.test.ts\"" }, "dependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/model-typings": "workspace:^" + "@rocket.chat/model-typings": "workspace:^", + "@msgpack/msgpack": "3.0.0-beta2", + "adm-zip": "^0.5.16", + "debug": "^4.3.7", + "esbuild": "~0.27.3", + "jose": "^4.15.9", + "jsonrpc-lite": "^2.2.0", + "lodash.clonedeep": "^4.5.0", + "semver": "^7.6.3", + "stack-trace": "0.0.10", + "uuid": "~11.0.5" }, "devDependencies": { "@rocket.chat/tsconfig": "workspace:*", + "@seald-io/nedb": "^4.1.2", + "@types/adm-zip": "^0.5.7", + "@types/debug": "^4.1.12", + "@types/lodash.clonedeep": "^4.5.9", + "@types/node": "~22.16.5", + "@types/semver": "^7.5.8", + "@types/stack-trace": "0.0.33", + "@types/uuid": "~10.0.0", "eslint": "~9.39.3", + "npm-run-all": "^4.1.5", + "rimraf": "^6.0.1", + "ts-node": "^6.2.0", "typescript": "~5.9.3" }, "volta": { diff --git a/packages/apps/scripts/deno-cache.js b/packages/apps/scripts/deno-cache.js new file mode 100644 index 0000000000000..c1626e32ff0d2 --- /dev/null +++ b/packages/apps/scripts/deno-cache.js @@ -0,0 +1,89 @@ +const childProcess = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const SHELL_ERR_CMD_NOT_FOUND = 127; +const { CI } = process.env; + +/** + * Matches 'deno 2.3.1' or 'Deno 2.7.11-alpha3.24' or even 'some deno and-anything in between 1.43.5' (as long as everything is in the same line) + * and extracts the correct version string from those ('2.3.1', '2.7.11' and '1.43.5' respectively). + * + * Doesn't match 'denoing 2.3.1' or 'deno2.3.1' or 'mydeno 2.7.11alpha3.24' or 'deno\n1.43.5' + * + * The expression gets a bit complicated because the word boundary assertion (\b) identifies the dash (-) as a valid word boundary, + * but that is not the case for use, as we don't want to match "make-deno" for instance. So, for correctness, we use a negative lookbehind + * assertion ("(? /(?\d+\.\d+\.\d+)\b/.exec(input)?.groups?.version; + +try { + const toolVersionsPath = path.resolve(__dirname, '..', '..', '..', '.tool-versions'); + const denoToolVersion = extractDenoVersion(fs.readFileSync(toolVersionsPath).toString()); + + if (!denoToolVersion) { + throw new Error(`Invalid Deno version in ${toolVersionsPath}, aborting...`); + } + + const installedVersion = extractDenoVersion(childProcess.execSync('deno --version').toString()); + + if (!installedVersion) { + throw new Error( + `Couldn't determine version of installed Deno. Try validating the version with 'deno --version' and make sure it is a valid Deno installation`, + ); + } + + if (installedVersion !== denoToolVersion) { + const message = `Incorrect Deno version. Required '${denoToolVersion}', found '${installedVersion}'.${CI ? '' : " The server will likely work, but it may cause your deno.lock to change - do not commit it. Make sure your Deno version matches the required one so you don't see this message again."}`; + + if (CI) { + throw new Error(message); + } + + // We don't need to fail if a dev environment doesn't have a matching Deno version, just the warning is enough + console.warn(message); + } +} catch (e) { + if (e.status === SHELL_ERR_CMD_NOT_FOUND) { + console.error( + new Error( + [ + 'Could not execute "deno" in the system. It is now a requirement for the Apps-Engine framework, and Rocket.Chat apps will not work without it.', + 'Make sure to install Deno and run the installation process for the Apps-Engine again. More info on https://docs.deno.com/runtime/manual/getting_started/installation', + ].join('\n'), + { cause: e }, + ), + ); + } else { + console.error(e); + } + + process.exit(1); +} + +const rootPath = path.join(__dirname, '..'); +const denoRuntimePath = path.join(rootPath, 'deno-runtime'); +const DENO_DIR = process.env.DENO_DIR ?? path.join(rootPath, '.deno-cache'); + +// In CI envs, break if lockfile changes; in dev envs, it's alright +const commandLine = CI ? 'deno install --frozen --entrypoint main.ts' : 'deno install --entrypoint main.ts'; + +childProcess.execSync(commandLine, { + cwd: denoRuntimePath, + env: { + ...process.env, + DENO_DIR, + }, + stdio: 'inherit', +}); diff --git a/packages/apps/tsconfig.json b/packages/apps/tsconfig.json index e00a45b253fa4..b96364dddaa17 100644 --- a/packages/apps/tsconfig.json +++ b/packages/apps/tsconfig.json @@ -3,7 +3,9 @@ "compilerOptions": { "declaration": true, "rootDir": "./src", - "outDir": "./dist" + "outDir": "./dist", + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, "include": ["./src/**/*"] } diff --git a/packages/apps/turbo.json b/packages/apps/turbo.json new file mode 100644 index 0000000000000..df725f06a5e0a --- /dev/null +++ b/packages/apps/turbo.json @@ -0,0 +1,9 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", "deno-runtime/**", "scripts/**", ".deno-cache/**"] + } + } +} From 8bbe587902fcf93065861a1f0d776d2c02d7a2d6 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Apr 2026 13:22:01 -0300 Subject: [PATCH 08/19] chore(deps): install new dependencies for @rocket.chat/apps Installs runtime and dev dependencies added to @rocket.chat/apps in the previous commit (adm-zip, debug, esbuild, jose, jsonrpc-lite, lodash.clonedeep, msgpack, semver, stack-trace, uuid, npm-run-all, rimraf, ts-node). Co-Authored-By: Claude Sonnet 4.6 --- yarn.lock | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/yarn.lock b/yarn.lock index ba967915f200f..a600cafbb80f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8884,12 +8884,33 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/apps@workspace:packages/apps" dependencies: + "@msgpack/msgpack": "npm:3.0.0-beta2" "@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/tsconfig": "workspace:*" + "@seald-io/nedb": "npm:^4.1.2" + "@types/adm-zip": "npm:^0.5.7" + "@types/debug": "npm:^4.1.12" + "@types/lodash.clonedeep": "npm:^4.5.9" + "@types/node": "npm:~22.16.5" + "@types/semver": "npm:^7.5.8" + "@types/stack-trace": "npm:0.0.33" + "@types/uuid": "npm:~10.0.0" + adm-zip: "npm:^0.5.16" + debug: "npm:^4.3.7" + esbuild: "npm:~0.27.3" eslint: "npm:~9.39.3" + jose: "npm:^4.15.9" + jsonrpc-lite: "npm:^2.2.0" + lodash.clonedeep: "npm:^4.5.0" + npm-run-all: "npm:^4.1.5" + rimraf: "npm:^6.0.1" + semver: "npm:^7.6.3" + stack-trace: "npm:0.0.10" + ts-node: "npm:^6.2.0" typescript: "npm:~5.9.3" + uuid: "npm:~11.0.5" languageName: unknown linkType: soft From 8ce8b4952ae48eff3bffbd257b619047991f995a Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Apr 2026 19:14:07 -0300 Subject: [PATCH 09/19] fix: typo in regex range Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- packages/apps/src/server/compiler/AppPackageParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apps/src/server/compiler/AppPackageParser.ts b/packages/apps/src/server/compiler/AppPackageParser.ts index 7ea364d19ea3b..525ad48407390 100644 --- a/packages/apps/src/server/compiler/AppPackageParser.ts +++ b/packages/apps/src/server/compiler/AppPackageParser.ts @@ -11,7 +11,7 @@ import { RequiredApiVersionError } from '../errors'; import type { IParseAppPackageResult } from './IParseAppPackageResult'; export class AppPackageParser { - public static uuid4Regex = /^[0-9a-fA-f]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/; + public static uuid4Regex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/; private allowedIconExts: Array = ['.png', '.jpg', '.jpeg', '.gif']; From dab63e58b5dc4dbb4d16a6e1a149b2a8dfa90c39 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Apr 2026 13:24:12 -0300 Subject: [PATCH 10/19] refactor(apps): remove AppManager dependency from deno-runtime Room class room.ts previously imported `AppManager` from the server layer (`@rocket.chat/apps-engine/server/AppManager.ts`) solely to type the private [PrivateManager] symbol field that backs the deprecated `usernames` getter. This created a cross-boundary import from the Deno subprocess into the Node.js host. Replace with a minimal inline `IRoomManager` interface that exposes only the one method actually called: getBridges().getInternalBridge().doGetUsernamesOfRoomById() Co-Authored-By: Claude Sonnet 4.6 --- packages/apps/deno-runtime/lib/room.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/apps/deno-runtime/lib/room.ts b/packages/apps/deno-runtime/lib/room.ts index 282ded4a90457..f128852abde56 100644 --- a/packages/apps/deno-runtime/lib/room.ts +++ b/packages/apps/deno-runtime/lib/room.ts @@ -1,7 +1,15 @@ import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; import type { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; -import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager.ts'; + +/** Minimal interface covering the only AppManager capability used by Room */ +interface IRoomManager { + getBridges(): { + getInternalBridge(): { + doGetUsernamesOfRoomById(id: string | undefined): Promise>; + }; + }; +} const PrivateManager = Symbol('RoomPrivateManager'); @@ -36,7 +44,7 @@ export class Room { private _USERNAMES: Promise> | undefined; - private [PrivateManager]: AppManager | undefined; + private [PrivateManager]: IRoomManager | undefined; /** * @deprecated @@ -51,7 +59,7 @@ export class Room { public set usernames(usernames) {} - public constructor(room: IRoom, manager: AppManager) { + public constructor(room: IRoom, manager: IRoomManager) { Object.assign(this, room); Object.defineProperty(this, PrivateManager, { From af58d57b1df6d73632b842d7af2112961f478bac Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Apr 2026 13:24:57 -0300 Subject: [PATCH 11/19] feat(apps): generate deno-runtime config with resolved apps-engine path at spawn time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The static deno.jsonc used `"@rocket.chat/apps-engine/": "./../src/"` — a relative path that only works when deno-runtime/ and apps-engine/src/ are siblings in the same package. Now that deno-runtime lives in @rocket.chat/apps and the definition source lives in @rocket.chat/apps-engine, the relative path would resolve to the wrong location. Solution: generate a `deno_runtime.jsonc` into the temp directory before each Deno subprocess spawn. The generated config copies all settings from the static deno.jsonc and injects `@rocket.chat/apps-engine/` as an absolute `file:` URL resolved via `require.resolve('@rocket.chat/apps-engine/package.json')`. This works correctly in any environment (monorepo dev, Meteor bundle, CI) without assumptions about directory layout. Changes: - `getAppsEngineSourceDir()`: resolves apps-engine src/ via require.resolve - `generateRuntimeDenoConfig()`: reads static config, injects resolved path, writes to tempDir/deno_runtime.jsonc, returns the path - `spawnProcess()`: calls generateRuntimeDenoConfig, uses the generated config, adds appsEngineSrcDir to --allow-read - deno.jsonc: remove the now-redundant static @rocket.chat/apps-engine/ entry Co-Authored-By: Claude Sonnet 4.6 --- packages/apps/deno-runtime/deno.jsonc | 1 - .../runtime/deno/AppsEngineDenoRuntime.ts | 47 +++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/packages/apps/deno-runtime/deno.jsonc b/packages/apps/deno-runtime/deno.jsonc index 4fa3142b99261..35d6fa9a6b9ed 100644 --- a/packages/apps/deno-runtime/deno.jsonc +++ b/packages/apps/deno-runtime/deno.jsonc @@ -1,7 +1,6 @@ { "imports": { "@msgpack/msgpack": "npm:@msgpack/msgpack@3.0.0-beta2", - "@rocket.chat/apps-engine/": "./../src/", "@rocket.chat/ui-kit": "npm:@rocket.chat/ui-kit@^0.31.22", "@std/cli": "jsr:@std/cli@^1.0.9", "@std/io": "jsr:@std/io@^0.225.3", diff --git a/packages/apps/src/server/runtime/deno/AppsEngineDenoRuntime.ts b/packages/apps/src/server/runtime/deno/AppsEngineDenoRuntime.ts index efffe811cf3e6..c74f22d9f7a01 100644 --- a/packages/apps/src/server/runtime/deno/AppsEngineDenoRuntime.ts +++ b/packages/apps/src/server/runtime/deno/AppsEngineDenoRuntime.ts @@ -86,6 +86,42 @@ export function getDenoConfigPath(): string { } } +/** + * Resolves the absolute path to @rocket.chat/apps-engine's src/ directory. + * Uses require.resolve so it works regardless of the runtime environment + * (monorepo dev, Meteor bundle, standalone node_modules). + */ +export function getAppsEngineSourceDir(): string { + const pkgJsonPath = require.resolve('@rocket.chat/apps-engine/package.json'); + return path.join(path.dirname(pkgJsonPath), 'src'); +} + +/** + * Generates a runtime deno.jsonc at `/deno_runtime.jsonc` by reading + * the static config and injecting the resolved absolute path for + * `@rocket.chat/apps-engine/`. This makes deno-runtime location-independent: + * the path is always correct regardless of where this package is installed. + * + * Returns the path to the generated config file. + */ +export function generateRuntimeDenoConfig(staticConfigPath: string, tempDir: string): string { + const staticConfig = JSON.parse(fs.readFileSync(staticConfigPath, 'utf8')); + const appsEngineSrcUrl = `file://${getAppsEngineSourceDir()}/`; + + const runtimeConfig = { + ...staticConfig, + imports: { + ...staticConfig.imports, + '@rocket.chat/apps-engine/': appsEngineSrcUrl, + }, + }; + + const runtimeConfigPath = path.join(tempDir, 'deno_runtime.jsonc'); + fs.writeFileSync(runtimeConfigPath, JSON.stringify(runtimeConfig, null, '\t')); + + return runtimeConfigPath; +} + type AbortFunction = (reason?: any) => void; export class DenoRuntimeSubprocessController extends EventEmitter implements IRuntimeController { @@ -171,19 +207,24 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu const denoExePath = 'deno'; const denoWrapperPath = this.denoRuntimePath; - // During development, the appsEngineDir is enough to run the deno process + // The apps package dir (where deno-runtime/ and .deno-cache/ live) const appsEngineDir = path.dirname(path.join(this.denoConfigPath, '..')); const DENO_DIR = process.env.DENO_DIR ?? path.join(appsEngineDir, '.deno-cache'); // When running in production, we're likely inside a node_modules which the Deno // process must be able to read in order to include files that use NPM packages const parentNodeModulesDir = path.dirname(path.join(appsEngineDir, '..')); + // The definition/ source files from @rocket.chat/apps-engine that deno-runtime imports + const appsEngineSrcDir = getAppsEngineSourceDir(); + + const allowedDirs = [appsEngineDir, parentNodeModulesDir, appsEngineSrcDir, this.tempFilePath]; - const allowedDirs = [appsEngineDir, parentNodeModulesDir, this.tempFilePath]; + // Generate a runtime config with the resolved absolute path for @rocket.chat/apps-engine/ + const runtimeConfigPath = generateRuntimeDenoConfig(this.denoConfigPath, this.tempFilePath); const options = [ 'run', '--cached-only', - `--config=${this.denoConfigPath}`, + `--config=${runtimeConfigPath}`, `--allow-read=${allowedDirs.join(',')}`, `--allow-env=${ALLOWED_ENVIRONMENT_VARIABLES.join(',')}`, denoWrapperPath, From e87065b390c9d26cb443e5a0fad5940434527f7e Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Apr 2026 13:27:56 -0300 Subject: [PATCH 12/19] refactor(apps): point AppsEngine facade and IAppServerOrchestrator to local server/ paths These files previously re-exported AppManager, AppBridges, and AppMetadataStorage from @rocket.chat/apps-engine/server/. Now that server/ lives in this package, the imports are updated to local paths. Co-Authored-By: Claude Sonnet 4.6 --- packages/apps/src/AppsEngine.ts | 6 +++--- packages/apps/src/IAppServerOrchestrator.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/apps/src/AppsEngine.ts b/packages/apps/src/AppsEngine.ts index 4bffbbbc810fb..1184c237b060d 100644 --- a/packages/apps/src/AppsEngine.ts +++ b/packages/apps/src/AppsEngine.ts @@ -20,6 +20,6 @@ export type { IVideoConference as IAppsVideoConference, VideoConference as AppsVideoConference, } from '@rocket.chat/apps-engine/definition/videoConferences'; -export { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; -export { AppBridges } from '@rocket.chat/apps-engine/server/bridges'; -export { AppMetadataStorage } from '@rocket.chat/apps-engine/server/storage'; +export { AppManager } from './server/AppManager'; +export { AppBridges } from './server/bridges'; +export { AppMetadataStorage } from './server/storage'; diff --git a/packages/apps/src/IAppServerOrchestrator.ts b/packages/apps/src/IAppServerOrchestrator.ts index 2f1f7db5d4b50..20e1edef03f35 100644 --- a/packages/apps/src/IAppServerOrchestrator.ts +++ b/packages/apps/src/IAppServerOrchestrator.ts @@ -1,5 +1,5 @@ -import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; -import type { AppSourceStorage } from '@rocket.chat/apps-engine/server/storage'; +import type { AppManager } from './server/AppManager'; +import type { AppSourceStorage } from './server/storage'; import type { Logger } from '@rocket.chat/logger'; import type { IAppsPersistenceModel } from '@rocket.chat/model-typings'; From 1eef341d8984519ceaa76467d86226ee05c48393 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Apr 2026 13:28:45 -0300 Subject: [PATCH 13/19] refactor(meteor): migrate apps-engine/server/ imports to @rocket.chat/apps/server/ Hard-cut all import paths from `@rocket.chat/apps-engine/server/*` to `@rocket.chat/apps/server/*` across apps/meteor. Covers bridges, storage, managers, marketplace, and service files (50 files, purely mechanical rename). Co-Authored-By: Claude Sonnet 4.6 --- apps/meteor/app/apps/server/bridges/activation.ts | 4 ++-- apps/meteor/app/apps/server/bridges/api.ts | 4 ++-- apps/meteor/app/apps/server/bridges/bridges.js | 2 +- apps/meteor/app/apps/server/bridges/cloud.ts | 2 +- apps/meteor/app/apps/server/bridges/commands.ts | 2 +- apps/meteor/app/apps/server/bridges/contact.ts | 2 +- apps/meteor/app/apps/server/bridges/details.ts | 2 +- apps/meteor/app/apps/server/bridges/email.ts | 2 +- apps/meteor/app/apps/server/bridges/environmental.ts | 2 +- apps/meteor/app/apps/server/bridges/experimental.ts | 2 +- apps/meteor/app/apps/server/bridges/http.ts | 4 ++-- apps/meteor/app/apps/server/bridges/internal.ts | 2 +- apps/meteor/app/apps/server/bridges/internalFederation.ts | 2 +- apps/meteor/app/apps/server/bridges/livechat.ts | 2 +- apps/meteor/app/apps/server/bridges/messages.ts | 4 ++-- apps/meteor/app/apps/server/bridges/moderation.ts | 2 +- apps/meteor/app/apps/server/bridges/oauthApps.ts | 2 +- .../meteor/app/apps/server/bridges/outboundCommunication.ts | 2 +- apps/meteor/app/apps/server/bridges/persistence.ts | 2 +- apps/meteor/app/apps/server/bridges/roles.ts | 2 +- apps/meteor/app/apps/server/bridges/rooms.ts | 4 ++-- apps/meteor/app/apps/server/bridges/scheduler.ts | 2 +- apps/meteor/app/apps/server/bridges/settings.ts | 2 +- apps/meteor/app/apps/server/bridges/thread.ts | 2 +- apps/meteor/app/apps/server/bridges/uiInteraction.ts | 2 +- apps/meteor/app/apps/server/bridges/uploads.ts | 2 +- apps/meteor/app/apps/server/bridges/users.ts | 2 +- apps/meteor/app/apps/server/bridges/videoConferences.ts | 2 +- apps/meteor/app/statistics/server/lib/getAppsStatistics.ts | 2 +- apps/meteor/ee/app/license/server/canEnableApp.ts | 2 +- apps/meteor/ee/lib/misc/formatAppInstanceForRest.ts | 6 +++--- .../server/apps/communication/endpoints/appsCountHandler.ts | 2 +- apps/meteor/ee/server/apps/communication/rest.ts | 4 ++-- apps/meteor/ee/server/apps/cron.ts | 2 +- apps/meteor/ee/server/apps/orchestrator.js | 2 +- .../ee/server/apps/storage/AppFileSystemSourceStorage.ts | 4 ++-- .../meteor/ee/server/apps/storage/AppGridFSSourceStorage.ts | 4 ++-- apps/meteor/ee/server/apps/storage/AppRealLogStorage.ts | 6 +++--- apps/meteor/ee/server/apps/storage/AppRealStorage.ts | 6 +++--- .../ee/server/apps/storage/ConfigurableAppSourceStorage.ts | 4 ++-- .../meteor/ee/tests/unit/server/apps/AppRealStorage.spec.ts | 2 +- .../lib/apps/getInstallationSourceFromAppStorageItem.ts | 2 +- apps/meteor/server/services/apps-engine/service.ts | 4 ++-- apps/meteor/server/services/video-conference/service.ts | 2 +- apps/meteor/server/startup/migrations/v294.ts | 4 ++-- apps/meteor/server/startup/migrations/v307.ts | 4 ++-- apps/meteor/tests/end-to-end/apps/app-logs-export.ts | 2 +- .../tests/end-to-end/apps/app-logs-nested-requests.ts | 2 +- apps/meteor/tests/end-to-end/apps/app-logs.ts | 2 +- .../tests/unit/app/license/server/canEnableApp.spec.ts | 4 ++-- 50 files changed, 69 insertions(+), 69 deletions(-) diff --git a/apps/meteor/app/apps/server/bridges/activation.ts b/apps/meteor/app/apps/server/bridges/activation.ts index 399dfd285e65e..e33626ea967ce 100644 --- a/apps/meteor/app/apps/server/bridges/activation.ts +++ b/apps/meteor/app/apps/server/bridges/activation.ts @@ -1,6 +1,6 @@ import type { IAppServerOrchestrator, AppStatus } from '@rocket.chat/apps'; -import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; -import { AppActivationBridge as ActivationBridge } from '@rocket.chat/apps-engine/server/bridges/AppActivationBridge'; +import type { ProxiedApp } from '@rocket.chat/apps/server/ProxiedApp'; +import { AppActivationBridge as ActivationBridge } from '@rocket.chat/apps/server/bridges/AppActivationBridge'; import { UserStatus } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; diff --git a/apps/meteor/app/apps/server/bridges/api.ts b/apps/meteor/app/apps/server/bridges/api.ts index f115897c82d82..c4cfc481e0795 100644 --- a/apps/meteor/app/apps/server/bridges/api.ts +++ b/apps/meteor/app/apps/server/bridges/api.ts @@ -1,8 +1,8 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { RequestMethod } from '@rocket.chat/apps-engine/definition/accessors'; import type { IApiRequest, IApiEndpoint, IApi } from '@rocket.chat/apps-engine/definition/api'; -import { ApiBridge } from '@rocket.chat/apps-engine/server/bridges/ApiBridge'; -import type { AppApi } from '@rocket.chat/apps-engine/server/managers/AppApi'; +import { ApiBridge } from '@rocket.chat/apps/server/bridges/ApiBridge'; +import type { AppApi } from '@rocket.chat/apps/server/managers/AppApi'; import type { Response, Request, IRouter, RequestHandler } from 'express'; import express from 'express'; import { Meteor } from 'meteor/meteor'; diff --git a/apps/meteor/app/apps/server/bridges/bridges.js b/apps/meteor/app/apps/server/bridges/bridges.js index 3b49cd91394e9..a62aae2436595 100644 --- a/apps/meteor/app/apps/server/bridges/bridges.js +++ b/apps/meteor/app/apps/server/bridges/bridges.js @@ -1,4 +1,4 @@ -import { AppBridges } from '@rocket.chat/apps-engine/server/bridges'; +import { AppBridges } from '@rocket.chat/apps/server/bridges'; import { AppActivationBridge } from './activation'; import { AppApisBridge } from './api'; diff --git a/apps/meteor/app/apps/server/bridges/cloud.ts b/apps/meteor/app/apps/server/bridges/cloud.ts index 0f908ccfe0a39..f762ee7b44fa6 100644 --- a/apps/meteor/app/apps/server/bridges/cloud.ts +++ b/apps/meteor/app/apps/server/bridges/cloud.ts @@ -1,6 +1,6 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IWorkspaceToken } from '@rocket.chat/apps-engine/definition/cloud/IWorkspaceToken'; -import { CloudWorkspaceBridge } from '@rocket.chat/apps-engine/server/bridges/CloudWorkspaceBridge'; +import { CloudWorkspaceBridge } from '@rocket.chat/apps/server/bridges/CloudWorkspaceBridge'; import { getWorkspaceAccessTokenWithScope } from '../../../cloud/server'; diff --git a/apps/meteor/app/apps/server/bridges/commands.ts b/apps/meteor/app/apps/server/bridges/commands.ts index 0f378a4a7abc4..7a1b522286ff3 100644 --- a/apps/meteor/app/apps/server/bridges/commands.ts +++ b/apps/meteor/app/apps/server/bridges/commands.ts @@ -1,7 +1,7 @@ import type { IAppServerOrchestrator, IAppsRoom, IAppsUser } from '@rocket.chat/apps'; import type { ISlashCommand, ISlashCommandPreview, ISlashCommandPreviewItem } from '@rocket.chat/apps-engine/definition/slashcommands'; import { SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands'; -import { CommandBridge } from '@rocket.chat/apps-engine/server/bridges/CommandBridge'; +import { CommandBridge } from '@rocket.chat/apps/server/bridges/CommandBridge'; import type { IMessage, RequiredField, SlashCommand, SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { Utilities } from '../../../../ee/lib/misc/Utilities'; diff --git a/apps/meteor/app/apps/server/bridges/contact.ts b/apps/meteor/app/apps/server/bridges/contact.ts index 802b0bb3ec16b..fab0976b716dd 100644 --- a/apps/meteor/app/apps/server/bridges/contact.ts +++ b/apps/meteor/app/apps/server/bridges/contact.ts @@ -1,6 +1,6 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { ILivechatContact } from '@rocket.chat/apps-engine/definition/livechat'; -import { ContactBridge } from '@rocket.chat/apps-engine/server/bridges'; +import { ContactBridge } from '@rocket.chat/apps/server/bridges'; import { addContactEmail } from '../../../livechat/server/lib/contacts/addContactEmail'; import { verifyContactChannel } from '../../../livechat/server/lib/contacts/verifyContactChannel'; diff --git a/apps/meteor/app/apps/server/bridges/details.ts b/apps/meteor/app/apps/server/bridges/details.ts index 3930cdd451cc7..6da3433f58702 100644 --- a/apps/meteor/app/apps/server/bridges/details.ts +++ b/apps/meteor/app/apps/server/bridges/details.ts @@ -1,6 +1,6 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; -import { AppDetailChangesBridge as DetailChangesBridge } from '@rocket.chat/apps-engine/server/bridges/AppDetailChangesBridge'; +import { AppDetailChangesBridge as DetailChangesBridge } from '@rocket.chat/apps/server/bridges/AppDetailChangesBridge'; export class AppDetailChangesBridge extends DetailChangesBridge { constructor(private readonly orch: IAppServerOrchestrator) { diff --git a/apps/meteor/app/apps/server/bridges/email.ts b/apps/meteor/app/apps/server/bridges/email.ts index 6d75a45044832..53d1db8f1a47c 100644 --- a/apps/meteor/app/apps/server/bridges/email.ts +++ b/apps/meteor/app/apps/server/bridges/email.ts @@ -1,6 +1,6 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IEmail } from '@rocket.chat/apps-engine/definition/email'; -import { EmailBridge } from '@rocket.chat/apps-engine/server/bridges'; +import { EmailBridge } from '@rocket.chat/apps/server/bridges'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; diff --git a/apps/meteor/app/apps/server/bridges/environmental.ts b/apps/meteor/app/apps/server/bridges/environmental.ts index 705a27186dee7..2b980948fbccb 100644 --- a/apps/meteor/app/apps/server/bridges/environmental.ts +++ b/apps/meteor/app/apps/server/bridges/environmental.ts @@ -1,5 +1,5 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; -import { EnvironmentalVariableBridge } from '@rocket.chat/apps-engine/server/bridges/EnvironmentalVariableBridge'; +import { EnvironmentalVariableBridge } from '@rocket.chat/apps/server/bridges/EnvironmentalVariableBridge'; export class AppEnvironmentalVariableBridge extends EnvironmentalVariableBridge { allowed: Array; diff --git a/apps/meteor/app/apps/server/bridges/experimental.ts b/apps/meteor/app/apps/server/bridges/experimental.ts index d505a54d5665d..05ccccdca5a03 100644 --- a/apps/meteor/app/apps/server/bridges/experimental.ts +++ b/apps/meteor/app/apps/server/bridges/experimental.ts @@ -1,5 +1,5 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; -import { ExperimentalBridge } from '@rocket.chat/apps-engine/server/bridges'; +import { ExperimentalBridge } from '@rocket.chat/apps/server/bridges'; export class AppExperimentalBridge extends ExperimentalBridge { constructor(protected readonly orch: IAppServerOrchestrator) { diff --git a/apps/meteor/app/apps/server/bridges/http.ts b/apps/meteor/app/apps/server/bridges/http.ts index aab0d56d301f0..5eed6e0b7367f 100644 --- a/apps/meteor/app/apps/server/bridges/http.ts +++ b/apps/meteor/app/apps/server/bridges/http.ts @@ -1,7 +1,7 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IHttpResponse } from '@rocket.chat/apps-engine/definition/accessors'; -import type { IHttpBridgeRequestInfo } from '@rocket.chat/apps-engine/server/bridges'; -import { HttpBridge } from '@rocket.chat/apps-engine/server/bridges/HttpBridge'; +import type { IHttpBridgeRequestInfo } from '@rocket.chat/apps/server/bridges'; +import { HttpBridge } from '@rocket.chat/apps/server/bridges/HttpBridge'; import { serverFetch as fetch, type ExtendedFetchOptions } from '@rocket.chat/server-fetch'; import { settings } from '../../../settings/server'; diff --git a/apps/meteor/app/apps/server/bridges/internal.ts b/apps/meteor/app/apps/server/bridges/internal.ts index 22e44a2bb2d0b..223f5af55ab8f 100644 --- a/apps/meteor/app/apps/server/bridges/internal.ts +++ b/apps/meteor/app/apps/server/bridges/internal.ts @@ -1,5 +1,5 @@ import type { IAppServerOrchestrator, IAppsSetting } from '@rocket.chat/apps'; -import { InternalBridge } from '@rocket.chat/apps-engine/server/bridges/InternalBridge'; +import { InternalBridge } from '@rocket.chat/apps/server/bridges/InternalBridge'; import type { ISetting, ISubscription } from '@rocket.chat/core-typings'; import { Settings, Subscriptions } from '@rocket.chat/models'; import { isTruthy } from '@rocket.chat/tools'; diff --git a/apps/meteor/app/apps/server/bridges/internalFederation.ts b/apps/meteor/app/apps/server/bridges/internalFederation.ts index 9bf3b34439ce5..008e55cd672f0 100644 --- a/apps/meteor/app/apps/server/bridges/internalFederation.ts +++ b/apps/meteor/app/apps/server/bridges/internalFederation.ts @@ -1,4 +1,4 @@ -import type { IInternalFederationBridge } from '@rocket.chat/apps-engine/server/bridges/IInternalFederationBridge'; +import type { IInternalFederationBridge } from '@rocket.chat/apps/server/bridges/IInternalFederationBridge'; import { FederationKeys } from '@rocket.chat/models'; export class AppInternalFederationBridge implements IInternalFederationBridge { diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index ab0faf0b2c3b4..927e74d7e7d3e 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -10,7 +10,7 @@ import type { } from '@rocket.chat/apps-engine/definition/livechat'; import type { IMessage as IAppsEngineMessage } from '@rocket.chat/apps-engine/definition/messages'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; -import { LivechatBridge } from '@rocket.chat/apps-engine/server/bridges/LivechatBridge'; +import { LivechatBridge } from '@rocket.chat/apps/server/bridges/LivechatBridge'; import type { ILivechatDepartment, IOmnichannelRoom, SelectedAgent, IMessage, ILivechatVisitor } from '@rocket.chat/core-typings'; import { OmnichannelSourceType } from '@rocket.chat/core-typings'; import { LivechatVisitors, LivechatRooms, LivechatDepartment, Users } from '@rocket.chat/models'; diff --git a/apps/meteor/app/apps/server/bridges/messages.ts b/apps/meteor/app/apps/server/bridges/messages.ts index 16366626c07c8..40b9526b401ac 100644 --- a/apps/meteor/app/apps/server/bridges/messages.ts +++ b/apps/meteor/app/apps/server/bridges/messages.ts @@ -1,8 +1,8 @@ import type { IAppServerOrchestrator, IAppsMessage, IAppsUser } from '@rocket.chat/apps'; import type { Reaction } from '@rocket.chat/apps-engine/definition/messages'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; -import type { ITypingDescriptor } from '@rocket.chat/apps-engine/server/bridges/MessageBridge'; -import { MessageBridge } from '@rocket.chat/apps-engine/server/bridges/MessageBridge'; +import type { ITypingDescriptor } from '@rocket.chat/apps/server/bridges/MessageBridge'; +import { MessageBridge } from '@rocket.chat/apps/server/bridges/MessageBridge'; import { api } from '@rocket.chat/core-services'; import type { IMessage } from '@rocket.chat/core-typings'; import { Users, Subscriptions } from '@rocket.chat/models'; diff --git a/apps/meteor/app/apps/server/bridges/moderation.ts b/apps/meteor/app/apps/server/bridges/moderation.ts index 0f1e56bbdec35..c8158d322ccfc 100644 --- a/apps/meteor/app/apps/server/bridges/moderation.ts +++ b/apps/meteor/app/apps/server/bridges/moderation.ts @@ -1,7 +1,7 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; -import { ModerationBridge } from '@rocket.chat/apps-engine/server/bridges/ModerationBridge'; +import { ModerationBridge } from '@rocket.chat/apps/server/bridges/ModerationBridge'; import { ModerationReports } from '@rocket.chat/models'; import { reportMessage } from '../../../../server/lib/moderation/reportMessage'; diff --git a/apps/meteor/app/apps/server/bridges/oauthApps.ts b/apps/meteor/app/apps/server/bridges/oauthApps.ts index bfd72917a367c..9461d8acb1510 100644 --- a/apps/meteor/app/apps/server/bridges/oauthApps.ts +++ b/apps/meteor/app/apps/server/bridges/oauthApps.ts @@ -2,7 +2,7 @@ import { randomUUID } from 'crypto'; import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IOAuthApp, IOAuthAppParams } from '@rocket.chat/apps-engine/definition/accessors/IOAuthApp'; -import { OAuthAppsBridge } from '@rocket.chat/apps-engine/server/bridges/OAuthAppsBridge'; +import { OAuthAppsBridge } from '@rocket.chat/apps/server/bridges/OAuthAppsBridge'; import type { IOAuthApps } from '@rocket.chat/core-typings'; import { OAuthApps, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; diff --git a/apps/meteor/app/apps/server/bridges/outboundCommunication.ts b/apps/meteor/app/apps/server/bridges/outboundCommunication.ts index 146ae18b38ead..7d973e1b53cd9 100644 --- a/apps/meteor/app/apps/server/bridges/outboundCommunication.ts +++ b/apps/meteor/app/apps/server/bridges/outboundCommunication.ts @@ -4,7 +4,7 @@ import type { IOutboundMessageProviders, IOutboundPhoneMessageProvider, } from '@rocket.chat/apps-engine/definition/outboundCommunication'; -import { OutboundMessageBridge } from '@rocket.chat/apps-engine/server/bridges'; +import { OutboundMessageBridge } from '@rocket.chat/apps/server/bridges'; import { getOutboundService } from '../../../livechat/server/lib/outboundcommunication'; diff --git a/apps/meteor/app/apps/server/bridges/persistence.ts b/apps/meteor/app/apps/server/bridges/persistence.ts index 857f6a561ed6f..4dd1b5dd7cbf6 100644 --- a/apps/meteor/app/apps/server/bridges/persistence.ts +++ b/apps/meteor/app/apps/server/bridges/persistence.ts @@ -1,6 +1,6 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata'; -import { PersistenceBridge } from '@rocket.chat/apps-engine/server/bridges/PersistenceBridge'; +import { PersistenceBridge } from '@rocket.chat/apps/server/bridges/PersistenceBridge'; import type { InsertOneResult } from 'mongodb'; export class AppPersistenceBridge extends PersistenceBridge { diff --git a/apps/meteor/app/apps/server/bridges/roles.ts b/apps/meteor/app/apps/server/bridges/roles.ts index aa0fcdc7b80b7..f0d5b5381c09e 100644 --- a/apps/meteor/app/apps/server/bridges/roles.ts +++ b/apps/meteor/app/apps/server/bridges/roles.ts @@ -1,5 +1,5 @@ import type { IAppServerOrchestrator, IAppsRole } from '@rocket.chat/apps'; -import { RoleBridge } from '@rocket.chat/apps-engine/server/bridges'; +import { RoleBridge } from '@rocket.chat/apps/server/bridges'; import type { IRole } from '@rocket.chat/core-typings'; import { Roles } from '@rocket.chat/models'; diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index 473614edfe25c..1f68497347ee1 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -3,8 +3,8 @@ import type { IMessage, IMessageRaw } from '@rocket.chat/apps-engine/definition/ import type { IRoom, IRoomRaw } from '@rocket.chat/apps-engine/definition/rooms'; import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; -import type { GetMessagesOptions, GetRoomsFilters, GetRoomsOptions } from '@rocket.chat/apps-engine/server/bridges/RoomBridge'; -import { RoomBridge } from '@rocket.chat/apps-engine/server/bridges/RoomBridge'; +import type { GetMessagesOptions, GetRoomsFilters, GetRoomsOptions } from '@rocket.chat/apps/server/bridges/RoomBridge'; +import { RoomBridge } from '@rocket.chat/apps/server/bridges/RoomBridge'; import type { ISubscription, IUser as ICoreUser, IRoom as ICoreRoom, IMessage as ICoreMessage } from '@rocket.chat/core-typings'; import { Subscriptions, Users, Rooms, Messages } from '@rocket.chat/models'; import type { FindOptions, Sort } from 'mongodb'; diff --git a/apps/meteor/app/apps/server/bridges/scheduler.ts b/apps/meteor/app/apps/server/bridges/scheduler.ts index b08d49182c9bc..0b909457b5d22 100644 --- a/apps/meteor/app/apps/server/bridges/scheduler.ts +++ b/apps/meteor/app/apps/server/bridges/scheduler.ts @@ -3,7 +3,7 @@ import { Agenda } from '@rocket.chat/agenda'; import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IProcessor, IOnetimeSchedule, IRecurringSchedule, IJobContext } from '@rocket.chat/apps-engine/definition/scheduler'; import { StartupType } from '@rocket.chat/apps-engine/definition/scheduler'; -import { SchedulerBridge } from '@rocket.chat/apps-engine/server/bridges/SchedulerBridge'; +import { SchedulerBridge } from '@rocket.chat/apps/server/bridges/SchedulerBridge'; import { ObjectId } from 'bson'; import { MongoInternals } from 'meteor/mongo'; diff --git a/apps/meteor/app/apps/server/bridges/settings.ts b/apps/meteor/app/apps/server/bridges/settings.ts index 7c569a3f12fd7..5707db884e7a7 100644 --- a/apps/meteor/app/apps/server/bridges/settings.ts +++ b/apps/meteor/app/apps/server/bridges/settings.ts @@ -1,7 +1,7 @@ import { Apps, type IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IReadSettingPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; -import { ServerSettingBridge } from '@rocket.chat/apps-engine/server/bridges/ServerSettingBridge'; +import { ServerSettingBridge } from '@rocket.chat/apps/server/bridges/ServerSettingBridge'; import { Settings } from '@rocket.chat/models'; import { updateAuditedByApp } from '../../../../server/settings/lib/auditedSettingUpdates'; diff --git a/apps/meteor/app/apps/server/bridges/thread.ts b/apps/meteor/app/apps/server/bridges/thread.ts index 099fe9184e004..b379394468ba8 100644 --- a/apps/meteor/app/apps/server/bridges/thread.ts +++ b/apps/meteor/app/apps/server/bridges/thread.ts @@ -1,6 +1,6 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; -import { ThreadBridge } from '@rocket.chat/apps-engine/server/bridges/ThreadBridge'; +import { ThreadBridge } from '@rocket.chat/apps/server/bridges/ThreadBridge'; export class AppThreadBridge extends ThreadBridge { constructor(private readonly orch: IAppServerOrchestrator) { diff --git a/apps/meteor/app/apps/server/bridges/uiInteraction.ts b/apps/meteor/app/apps/server/bridges/uiInteraction.ts index fc68e4e30d3ff..bf22a9bc0f331 100644 --- a/apps/meteor/app/apps/server/bridges/uiInteraction.ts +++ b/apps/meteor/app/apps/server/bridges/uiInteraction.ts @@ -1,7 +1,7 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IUIKitInteraction } from '@rocket.chat/apps-engine/definition/uikit'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; -import { UiInteractionBridge as AppsEngineUiInteractionBridge } from '@rocket.chat/apps-engine/server/bridges/UiInteractionBridge'; +import { UiInteractionBridge as AppsEngineUiInteractionBridge } from '@rocket.chat/apps/server/bridges/UiInteractionBridge'; import { api } from '@rocket.chat/core-services'; import type * as UiKit from '@rocket.chat/ui-kit'; diff --git a/apps/meteor/app/apps/server/bridges/uploads.ts b/apps/meteor/app/apps/server/bridges/uploads.ts index b9d0ff67de58e..74fbd99096582 100644 --- a/apps/meteor/app/apps/server/bridges/uploads.ts +++ b/apps/meteor/app/apps/server/bridges/uploads.ts @@ -1,7 +1,7 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IUpload } from '@rocket.chat/apps-engine/definition/uploads'; import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails'; -import { UploadBridge } from '@rocket.chat/apps-engine/server/bridges/UploadBridge'; +import { UploadBridge } from '@rocket.chat/apps/server/bridges/UploadBridge'; import { determineFileType } from '../../../../ee/lib/misc/determineFileType'; import { FileUpload } from '../../../file-upload/server'; diff --git a/apps/meteor/app/apps/server/bridges/users.ts b/apps/meteor/app/apps/server/bridges/users.ts index 43cd7f9ea89e1..4735dcd6462d2 100644 --- a/apps/meteor/app/apps/server/bridges/users.ts +++ b/apps/meteor/app/apps/server/bridges/users.ts @@ -1,6 +1,6 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IUserCreationOptions, IUser, UserType } from '@rocket.chat/apps-engine/definition/users'; -import { UserBridge } from '@rocket.chat/apps-engine/server/bridges/UserBridge'; +import { UserBridge } from '@rocket.chat/apps/server/bridges/UserBridge'; import { Presence } from '@rocket.chat/core-services'; import type { UserStatus } from '@rocket.chat/core-typings'; import { Subscriptions, Users } from '@rocket.chat/models'; diff --git a/apps/meteor/app/apps/server/bridges/videoConferences.ts b/apps/meteor/app/apps/server/bridges/videoConferences.ts index 8986c15a3c6dd..36f6ce4a8e6b2 100644 --- a/apps/meteor/app/apps/server/bridges/videoConferences.ts +++ b/apps/meteor/app/apps/server/bridges/videoConferences.ts @@ -1,7 +1,7 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders'; import type { AppVideoConference, VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences'; -import { VideoConferenceBridge } from '@rocket.chat/apps-engine/server/bridges/VideoConferenceBridge'; +import { VideoConferenceBridge } from '@rocket.chat/apps/server/bridges/VideoConferenceBridge'; import { VideoConf } from '@rocket.chat/core-services'; import { videoConfProviders } from '../../../../server/lib/videoConfProviders'; diff --git a/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts b/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts index 90e05d1923563..def9e15511b61 100644 --- a/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts +++ b/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts @@ -1,6 +1,6 @@ import { Apps } from '@rocket.chat/apps'; import { AppStatus, AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; -import { AppInstallationSource } from '@rocket.chat/apps-engine/server/storage'; +import { AppInstallationSource } from '@rocket.chat/apps/server/storage'; import mem from 'mem'; import { SystemLogger } from '../../../../server/lib/logger/system'; diff --git a/apps/meteor/ee/app/license/server/canEnableApp.ts b/apps/meteor/ee/app/license/server/canEnableApp.ts index c18ad9efb6619..647168c15d6a8 100644 --- a/apps/meteor/ee/app/license/server/canEnableApp.ts +++ b/apps/meteor/ee/app/license/server/canEnableApp.ts @@ -1,4 +1,4 @@ -import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import type { IAppStorageItem } from '@rocket.chat/apps/server/storage'; import { Apps } from '@rocket.chat/core-services'; import type { LicenseModule } from '@rocket.chat/core-typings'; import { License, type LicenseImp } from '@rocket.chat/license'; diff --git a/apps/meteor/ee/lib/misc/formatAppInstanceForRest.ts b/apps/meteor/ee/lib/misc/formatAppInstanceForRest.ts index 7819404525d5f..f34391c528055 100644 --- a/apps/meteor/ee/lib/misc/formatAppInstanceForRest.ts +++ b/apps/meteor/ee/lib/misc/formatAppInstanceForRest.ts @@ -1,8 +1,8 @@ import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; -import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; -import type { AppLicenseValidationResult } from '@rocket.chat/apps-engine/server/marketplace/license'; -import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import type { ProxiedApp } from '@rocket.chat/apps/server/ProxiedApp'; +import type { AppLicenseValidationResult } from '@rocket.chat/apps/server/marketplace/license'; +import type { IAppStorageItem } from '@rocket.chat/apps/server/storage'; import type { AppStatusReport } from '@rocket.chat/core-services'; import type { App } from '@rocket.chat/core-typings'; diff --git a/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts b/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts index 402ceb709a16a..ad10d68e43e2f 100644 --- a/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts +++ b/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts @@ -1,4 +1,4 @@ -import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; +import type { AppManager } from '@rocket.chat/apps/server/AppManager'; import { License } from '@rocket.chat/license'; import { API } from '../../../../../app/api/server'; diff --git a/apps/meteor/ee/server/apps/communication/rest.ts b/apps/meteor/ee/server/apps/communication/rest.ts index 62af65d40987f..add85f1c642ee 100644 --- a/apps/meteor/ee/server/apps/communication/rest.ts +++ b/apps/meteor/ee/server/apps/communication/rest.ts @@ -1,7 +1,7 @@ import { AppStatus, AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; -import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; -import type { IMarketplaceInfo } from '@rocket.chat/apps-engine/server/marketplace'; +import type { AppManager } from '@rocket.chat/apps/server/AppManager'; +import type { IMarketplaceInfo } from '@rocket.chat/apps/server/marketplace'; import type { AppStatusReport } from '@rocket.chat/core-services'; import type { IMessage, IUser } from '@rocket.chat/core-typings'; import { License } from '@rocket.chat/license'; diff --git a/apps/meteor/ee/server/apps/cron.ts b/apps/meteor/ee/server/apps/cron.ts index d5e7111972821..1cd937d805ea5 100644 --- a/apps/meteor/ee/server/apps/cron.ts +++ b/apps/meteor/ee/server/apps/cron.ts @@ -1,5 +1,5 @@ import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; -import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; +import type { ProxiedApp } from '@rocket.chat/apps/server/ProxiedApp'; import { cronJobs } from '@rocket.chat/cron'; import { Settings, Users } from '@rocket.chat/models'; diff --git a/apps/meteor/ee/server/apps/orchestrator.js b/apps/meteor/ee/server/apps/orchestrator.js index 7188695ac3f43..90987f121a8cd 100644 --- a/apps/meteor/ee/server/apps/orchestrator.js +++ b/apps/meteor/ee/server/apps/orchestrator.js @@ -4,7 +4,7 @@ import * as path from 'path'; import { registerOrchestrator } from '@rocket.chat/apps'; import { EssentialAppDisabledException } from '@rocket.chat/apps-engine/definition/exceptions'; -import { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; +import { AppManager } from '@rocket.chat/apps/server/AppManager'; import { Logger } from '@rocket.chat/logger'; import { AppLogs, Apps as AppsModel, AppsPersistence, Statistics } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; diff --git a/apps/meteor/ee/server/apps/storage/AppFileSystemSourceStorage.ts b/apps/meteor/ee/server/apps/storage/AppFileSystemSourceStorage.ts index 54e83a11df5d5..31c34a6d09bed 100644 --- a/apps/meteor/ee/server/apps/storage/AppFileSystemSourceStorage.ts +++ b/apps/meteor/ee/server/apps/storage/AppFileSystemSourceStorage.ts @@ -1,8 +1,8 @@ import { promises as fs } from 'fs'; import { join, normalize } from 'path'; -import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; -import { AppSourceStorage } from '@rocket.chat/apps-engine/server/storage'; +import type { IAppStorageItem } from '@rocket.chat/apps/server/storage'; +import { AppSourceStorage } from '@rocket.chat/apps/server/storage'; export class AppFileSystemSourceStorage extends AppSourceStorage { private pathPrefix = 'fs:/'; diff --git a/apps/meteor/ee/server/apps/storage/AppGridFSSourceStorage.ts b/apps/meteor/ee/server/apps/storage/AppGridFSSourceStorage.ts index 2fd4232ee6d35..d9561c45ed7db 100644 --- a/apps/meteor/ee/server/apps/storage/AppGridFSSourceStorage.ts +++ b/apps/meteor/ee/server/apps/storage/AppGridFSSourceStorage.ts @@ -1,5 +1,5 @@ -import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; -import { AppSourceStorage } from '@rocket.chat/apps-engine/server/storage'; +import type { IAppStorageItem } from '@rocket.chat/apps/server/storage'; +import { AppSourceStorage } from '@rocket.chat/apps/server/storage'; import { streamToBuffer } from '@rocket.chat/tools'; import { MongoInternals } from 'meteor/mongo'; import { NpmModuleMongodb } from 'meteor/npm-mongo'; diff --git a/apps/meteor/ee/server/apps/storage/AppRealLogStorage.ts b/apps/meteor/ee/server/apps/storage/AppRealLogStorage.ts index 62f61699f2e47..4a94699d2f272 100644 --- a/apps/meteor/ee/server/apps/storage/AppRealLogStorage.ts +++ b/apps/meteor/ee/server/apps/storage/AppRealLogStorage.ts @@ -1,6 +1,6 @@ -import type { ILoggerStorageEntry } from '@rocket.chat/apps-engine/server/logging'; -import type { IAppLogStorageFindOptions } from '@rocket.chat/apps-engine/server/storage'; -import { AppLogStorage } from '@rocket.chat/apps-engine/server/storage'; +import type { ILoggerStorageEntry } from '@rocket.chat/apps/server/logging'; +import type { IAppLogStorageFindOptions } from '@rocket.chat/apps/server/storage'; +import { AppLogStorage } from '@rocket.chat/apps/server/storage'; import { InstanceStatus } from '@rocket.chat/instance-status'; import type { AppLogs } from '@rocket.chat/models'; diff --git a/apps/meteor/ee/server/apps/storage/AppRealStorage.ts b/apps/meteor/ee/server/apps/storage/AppRealStorage.ts index db96013af0c76..09f92b3328805 100644 --- a/apps/meteor/ee/server/apps/storage/AppRealStorage.ts +++ b/apps/meteor/ee/server/apps/storage/AppRealStorage.ts @@ -1,9 +1,9 @@ import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; -import type { IMarketplaceInfo } from '@rocket.chat/apps-engine/server/marketplace'; -import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; -import { AppMetadataStorage } from '@rocket.chat/apps-engine/server/storage'; +import type { IMarketplaceInfo } from '@rocket.chat/apps/server/marketplace'; +import type { IAppStorageItem } from '@rocket.chat/apps/server/storage'; +import { AppMetadataStorage } from '@rocket.chat/apps/server/storage'; import type { Apps } from '@rocket.chat/models'; import { removeEmpty } from '@rocket.chat/tools'; import type { UpdateFilter } from 'mongodb'; diff --git a/apps/meteor/ee/server/apps/storage/ConfigurableAppSourceStorage.ts b/apps/meteor/ee/server/apps/storage/ConfigurableAppSourceStorage.ts index ee70cd38b409f..d201dc2d76bb2 100644 --- a/apps/meteor/ee/server/apps/storage/ConfigurableAppSourceStorage.ts +++ b/apps/meteor/ee/server/apps/storage/ConfigurableAppSourceStorage.ts @@ -1,5 +1,5 @@ -import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; -import { AppSourceStorage } from '@rocket.chat/apps-engine/server/storage'; +import type { IAppStorageItem } from '@rocket.chat/apps/server/storage'; +import { AppSourceStorage } from '@rocket.chat/apps/server/storage'; import { AppFileSystemSourceStorage } from './AppFileSystemSourceStorage'; import { AppGridFSSourceStorage } from './AppGridFSSourceStorage'; diff --git a/apps/meteor/ee/tests/unit/server/apps/AppRealStorage.spec.ts b/apps/meteor/ee/tests/unit/server/apps/AppRealStorage.spec.ts index 3ea794fd06614..c90331956d90f 100644 --- a/apps/meteor/ee/tests/unit/server/apps/AppRealStorage.spec.ts +++ b/apps/meteor/ee/tests/unit/server/apps/AppRealStorage.spec.ts @@ -1,7 +1,7 @@ import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata'; import { SettingType } from '@rocket.chat/apps-engine/definition/settings'; -import { AppInstallationSource, type IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import { AppInstallationSource, type IAppStorageItem } from '@rocket.chat/apps/server/storage'; import { expect } from 'chai'; import sinon from 'sinon'; diff --git a/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts b/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts index d8fd5a48f79f3..ec346e7ddd22d 100644 --- a/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts +++ b/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts @@ -1,4 +1,4 @@ -import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import type { IAppStorageItem } from '@rocket.chat/apps/server/storage'; import type { LicenseAppSources } from '@rocket.chat/core-typings'; /** diff --git a/apps/meteor/server/services/apps-engine/service.ts b/apps/meteor/server/services/apps-engine/service.ts index 0be60a579908d..ab535fad01b8e 100644 --- a/apps/meteor/server/services/apps-engine/service.ts +++ b/apps/meteor/server/services/apps-engine/service.ts @@ -2,8 +2,8 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; -import type { IGetAppsFilter } from '@rocket.chat/apps-engine/server/IGetAppsFilter'; -import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import type { IGetAppsFilter } from '@rocket.chat/apps/server/IGetAppsFilter'; +import type { IAppStorageItem } from '@rocket.chat/apps/server/storage'; import type { AppStatusReport, IAppsEngineService } from '@rocket.chat/core-services'; import { ServiceClassInternal } from '@rocket.chat/core-services'; diff --git a/apps/meteor/server/services/video-conference/service.ts b/apps/meteor/server/services/video-conference/service.ts index 3c33979fdd3a0..b98f7f7064863 100644 --- a/apps/meteor/server/services/video-conference/service.ts +++ b/apps/meteor/server/services/video-conference/service.ts @@ -1,6 +1,6 @@ import { Apps } from '@rocket.chat/apps'; import type { VideoConfData, VideoConfDataExtended } from '@rocket.chat/apps-engine/definition/videoConfProviders'; -import type { AppVideoConfProviderManager } from '@rocket.chat/apps-engine/server/managers'; +import type { AppVideoConfProviderManager } from '@rocket.chat/apps/server/managers'; import type { IVideoConfService, VideoConferenceJoinOptions } from '@rocket.chat/core-services'; import { api, ServiceClassInternal, Room } from '@rocket.chat/core-services'; import type { diff --git a/apps/meteor/server/startup/migrations/v294.ts b/apps/meteor/server/startup/migrations/v294.ts index 07f6dc2acc1fe..f31cd6a1d6bc1 100644 --- a/apps/meteor/server/startup/migrations/v294.ts +++ b/apps/meteor/server/startup/migrations/v294.ts @@ -1,6 +1,6 @@ import { Apps } from '@rocket.chat/apps'; -import type { AppSignatureManager } from '@rocket.chat/apps-engine/server/managers/AppSignatureManager'; -import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import type { AppSignatureManager } from '@rocket.chat/apps/server/managers/AppSignatureManager'; +import type { IAppStorageItem } from '@rocket.chat/apps/server/storage'; import type { AppRealStorage } from '../../../ee/server/apps/storage'; import { addMigration } from '../../lib/migrations'; diff --git a/apps/meteor/server/startup/migrations/v307.ts b/apps/meteor/server/startup/migrations/v307.ts index d16d16220edc8..3be8a80f0bca6 100644 --- a/apps/meteor/server/startup/migrations/v307.ts +++ b/apps/meteor/server/startup/migrations/v307.ts @@ -1,6 +1,6 @@ import { Apps } from '@rocket.chat/apps'; -import type { AppSignatureManager } from '@rocket.chat/apps-engine/server/managers/AppSignatureManager'; -import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import type { AppSignatureManager } from '@rocket.chat/apps/server/managers/AppSignatureManager'; +import type { IAppStorageItem } from '@rocket.chat/apps/server/storage'; import { License } from '@rocket.chat/license'; import type { AppRealStorage } from '../../../ee/server/apps/storage'; diff --git a/apps/meteor/tests/end-to-end/apps/app-logs-export.ts b/apps/meteor/tests/end-to-end/apps/app-logs-export.ts index 0d8b1fc088a44..30422bffa7bf3 100644 --- a/apps/meteor/tests/end-to-end/apps/app-logs-export.ts +++ b/apps/meteor/tests/end-to-end/apps/app-logs-export.ts @@ -1,4 +1,4 @@ -import type { ILoggerStorageEntry } from '@rocket.chat/apps-engine/server/logging'; +import type { ILoggerStorageEntry } from '@rocket.chat/apps/server/logging'; import type { App } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; diff --git a/apps/meteor/tests/end-to-end/apps/app-logs-nested-requests.ts b/apps/meteor/tests/end-to-end/apps/app-logs-nested-requests.ts index 76080fe704c72..302ec44961139 100644 --- a/apps/meteor/tests/end-to-end/apps/app-logs-nested-requests.ts +++ b/apps/meteor/tests/end-to-end/apps/app-logs-nested-requests.ts @@ -1,4 +1,4 @@ -import type { ILoggerStorageEntry } from '@rocket.chat/apps-engine/server/logging'; +import type { ILoggerStorageEntry } from '@rocket.chat/apps/server/logging'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; diff --git a/apps/meteor/tests/end-to-end/apps/app-logs.ts b/apps/meteor/tests/end-to-end/apps/app-logs.ts index e21f4ce98aee1..1aca3c183225a 100644 --- a/apps/meteor/tests/end-to-end/apps/app-logs.ts +++ b/apps/meteor/tests/end-to-end/apps/app-logs.ts @@ -1,4 +1,4 @@ -import type { ILoggerStorageEntry } from '@rocket.chat/apps-engine/server/logging'; +import type { ILoggerStorageEntry } from '@rocket.chat/apps/server/logging'; import type { App } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; diff --git a/apps/meteor/tests/unit/app/license/server/canEnableApp.spec.ts b/apps/meteor/tests/unit/app/license/server/canEnableApp.spec.ts index 0cb68734fb462..c2548d1add1af 100644 --- a/apps/meteor/tests/unit/app/license/server/canEnableApp.spec.ts +++ b/apps/meteor/tests/unit/app/license/server/canEnableApp.spec.ts @@ -1,6 +1,6 @@ import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; -import type { IMarketplaceInfo } from '@rocket.chat/apps-engine/server/marketplace'; -import { AppInstallationSource, type IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import type { IMarketplaceInfo } from '@rocket.chat/apps/server/marketplace'; +import { AppInstallationSource, type IAppStorageItem } from '@rocket.chat/apps/server/storage'; import type { Apps } from '@rocket.chat/core-services'; import type { LicenseImp } from '@rocket.chat/license'; import { expect } from 'chai'; From 0e0fc071c54748601bf4842523e157fc5f65cf54 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Apr 2026 13:29:28 -0300 Subject: [PATCH 14/19] refactor(meteor): migrate apps-engine/client/ imports to @rocket.chat/apps/client/ Hard-cut all import paths from `@rocket.chat/apps-engine/client/*` to `@rocket.chat/apps/client/*` across apps/meteor (5 files). Co-Authored-By: Claude Sonnet 4.6 --- apps/meteor/client/apps/RealAppsEngineUIHost.ts | 4 ++-- apps/meteor/client/apps/orchestrator.ts | 4 ++-- apps/meteor/client/contexts/AppsContext.tsx | 2 +- apps/meteor/tests/mocks/client/marketplace.tsx | 6 +++--- apps/meteor/tests/mocks/data.ts | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/meteor/client/apps/RealAppsEngineUIHost.ts b/apps/meteor/client/apps/RealAppsEngineUIHost.ts index c6a746f6bb7a8..61e7ce516aab6 100644 --- a/apps/meteor/client/apps/RealAppsEngineUIHost.ts +++ b/apps/meteor/client/apps/RealAppsEngineUIHost.ts @@ -1,5 +1,5 @@ -import { AppsEngineUIHost } from '@rocket.chat/apps-engine/client/AppsEngineUIHost'; -import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from '@rocket.chat/apps-engine/client/definition'; +import { AppsEngineUIHost } from '@rocket.chat/apps/client/AppsEngineUIHost'; +import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from '@rocket.chat/apps/client/definition'; import { getUserAvatarURL } from '../../app/utils/client/getUserAvatarURL'; import { sdk } from '../../app/utils/client/lib/SDKClient'; diff --git a/apps/meteor/client/apps/orchestrator.ts b/apps/meteor/client/apps/orchestrator.ts index d08641da34d22..82529f6938000 100644 --- a/apps/meteor/client/apps/orchestrator.ts +++ b/apps/meteor/client/apps/orchestrator.ts @@ -1,5 +1,5 @@ -import { AppClientManager } from '@rocket.chat/apps-engine/client/AppClientManager'; -import type { AppsEngineUIHost } from '@rocket.chat/apps-engine/client/AppsEngineUIHost'; +import { AppClientManager } from '@rocket.chat/apps/client/AppClientManager'; +import type { AppsEngineUIHost } from '@rocket.chat/apps/client/AppsEngineUIHost'; import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import type { Serialized } from '@rocket.chat/core-typings'; diff --git a/apps/meteor/client/contexts/AppsContext.tsx b/apps/meteor/client/contexts/AppsContext.tsx index 39929c20aed08..8361b80bdfe27 100644 --- a/apps/meteor/client/contexts/AppsContext.tsx +++ b/apps/meteor/client/contexts/AppsContext.tsx @@ -1,4 +1,4 @@ -import type { AppClientManager } from '@rocket.chat/apps-engine/client/AppClientManager'; +import type { AppClientManager } from '@rocket.chat/apps/client/AppClientManager'; import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import type { Serialized } from '@rocket.chat/core-typings'; diff --git a/apps/meteor/tests/mocks/client/marketplace.tsx b/apps/meteor/tests/mocks/client/marketplace.tsx index 52f1acae2c584..5c26331333a25 100644 --- a/apps/meteor/tests/mocks/client/marketplace.tsx +++ b/apps/meteor/tests/mocks/client/marketplace.tsx @@ -1,6 +1,6 @@ -import { AppClientManager } from '@rocket.chat/apps-engine/client/AppClientManager'; -import { AppsEngineUIHost } from '@rocket.chat/apps-engine/client/AppsEngineUIHost'; -import type { IExternalComponentRoomInfo } from '@rocket.chat/apps-engine/client/definition'; +import { AppClientManager } from '@rocket.chat/apps/client/AppClientManager'; +import { AppsEngineUIHost } from '@rocket.chat/apps/client/AppsEngineUIHost'; +import type { IExternalComponentRoomInfo } from '@rocket.chat/apps/client/definition'; import type { ReactNode } from 'react'; import { AppsContext, type IAppsOrchestrator } from '../../../client/contexts/AppsContext'; diff --git a/apps/meteor/tests/mocks/data.ts b/apps/meteor/tests/mocks/data.ts index fb8859a3055b2..a4ecfa5ba4b95 100644 --- a/apps/meteor/tests/mocks/data.ts +++ b/apps/meteor/tests/mocks/data.ts @@ -1,5 +1,5 @@ import { faker } from '@faker-js/faker'; -import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from '@rocket.chat/apps-engine/client/definition'; +import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from '@rocket.chat/apps/client/definition'; import type { ILivechatContact } from '@rocket.chat/apps-engine/definition/livechat'; import { AppSubscriptionStatus, From 2118bf6049fd4e6fdfbd250079e90803df6110fc Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Apr 2026 13:29:44 -0300 Subject: [PATCH 15/19] refactor(core-services): migrate apps-engine/server/ imports to @rocket.chat/apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IAppsEngineService imported IGetAppsFilter and IAppStorageItem from @rocket.chat/apps-engine/server/ — server internals that now live in @rocket.chat/apps. Update imports and swap the devDependency accordingly. Co-Authored-By: Claude Sonnet 4.6 --- packages/core-services/package.json | 2 +- packages/core-services/src/types/IAppsEngineService.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 541e2376e9161..dffa27072ea8a 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -29,7 +29,7 @@ "@rocket.chat/ui-kit": "workspace:~" }, "devDependencies": { - "@rocket.chat/apps-engine": "workspace:^", + "@rocket.chat/apps": "workspace:^", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", "@types/jest": "~30.0.0", diff --git a/packages/core-services/src/types/IAppsEngineService.ts b/packages/core-services/src/types/IAppsEngineService.ts index 5d026c5149fb9..7c5ddccf24822 100644 --- a/packages/core-services/src/types/IAppsEngineService.ts +++ b/packages/core-services/src/types/IAppsEngineService.ts @@ -1,7 +1,7 @@ import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; -import type { IGetAppsFilter } from '@rocket.chat/apps-engine/server/IGetAppsFilter'; -import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import type { IGetAppsFilter } from '@rocket.chat/apps/server/IGetAppsFilter'; +import type { IAppStorageItem } from '@rocket.chat/apps/server/storage'; export type AppStatusReport = { [appId: string]: { instanceId: string; isLocal: boolean; status: AppStatus }[]; From 0720d8e45e0cc14a48b9abc0363847fe1985e4b4 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Apr 2026 14:10:50 -0300 Subject: [PATCH 16/19] refactor(apps-engine): promote cross-boundary types to definition layer OAuth2Client, IExternalComponentRoomInfo/UserInfo, and the room query option types (GetMessagesOptions, GetRoomsFilters, GetRoomsOptions) were defined in server/ or client/ but were imported by definition/ files. Move them into the definition layer so the public API is self-contained. Co-Authored-By: Claude Sonnet 4.6 --- .../src/definition/accessors/IRoomRead.ts | 2 +- .../IExternalComponentRoomInfo.ts | 16 + .../IExternalComponentState.ts | 3 +- .../IExternalComponentUserInfo.ts | 14 + .../src/definition/externalComponent/index.ts | 3 + .../src/definition/oauth2/OAuth2.ts | 2 +- .../src/definition/oauth2/OAuth2Client.ts | 337 ++++++++++++++++++ .../definition/rooms/IGetMessagesOptions.ts | 43 +++ .../apps-engine/src/definition/rooms/index.ts | 1 + 9 files changed, 418 insertions(+), 3 deletions(-) create mode 100644 packages/apps-engine/src/definition/externalComponent/IExternalComponentRoomInfo.ts create mode 100644 packages/apps-engine/src/definition/externalComponent/IExternalComponentUserInfo.ts create mode 100644 packages/apps-engine/src/definition/oauth2/OAuth2Client.ts create mode 100644 packages/apps-engine/src/definition/rooms/IGetMessagesOptions.ts diff --git a/packages/apps-engine/src/definition/accessors/IRoomRead.ts b/packages/apps-engine/src/definition/accessors/IRoomRead.ts index ae086150784f9..bfd35b33c9a90 100644 --- a/packages/apps-engine/src/definition/accessors/IRoomRead.ts +++ b/packages/apps-engine/src/definition/accessors/IRoomRead.ts @@ -1,4 +1,4 @@ -import type { GetMessagesOptions, GetRoomsFilters, GetRoomsOptions } from '../../server/bridges/RoomBridge'; +import type { GetMessagesOptions, GetRoomsFilters, GetRoomsOptions } from '../rooms/IGetMessagesOptions'; import type { IMessageRaw } from '../messages/index'; import type { IRoom, IRoomRaw } from '../rooms/index'; import type { IUser } from '../users/index'; diff --git a/packages/apps-engine/src/definition/externalComponent/IExternalComponentRoomInfo.ts b/packages/apps-engine/src/definition/externalComponent/IExternalComponentRoomInfo.ts new file mode 100644 index 0000000000000..090b9b6903379 --- /dev/null +++ b/packages/apps-engine/src/definition/externalComponent/IExternalComponentRoomInfo.ts @@ -0,0 +1,16 @@ +import type { IRoom } from '../rooms'; +import type { IExternalComponentUserInfo } from './IExternalComponentUserInfo'; + +type ClientRoomInfo = Pick; + +/** + * Represents the room's information returned to the + * external component. + */ +export interface IExternalComponentRoomInfo extends ClientRoomInfo { + /** + * the list that contains all the users belonging + * to this room. + */ + members: Array; +} diff --git a/packages/apps-engine/src/definition/externalComponent/IExternalComponentState.ts b/packages/apps-engine/src/definition/externalComponent/IExternalComponentState.ts index ba33072a28281..db726a8cd177f 100644 --- a/packages/apps-engine/src/definition/externalComponent/IExternalComponentState.ts +++ b/packages/apps-engine/src/definition/externalComponent/IExternalComponentState.ts @@ -1,4 +1,5 @@ -import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from '../../client/definition'; +import type { IExternalComponentRoomInfo } from './IExternalComponentRoomInfo'; +import type { IExternalComponentUserInfo } from './IExternalComponentUserInfo'; /** * The state of an external component, which contains the diff --git a/packages/apps-engine/src/definition/externalComponent/IExternalComponentUserInfo.ts b/packages/apps-engine/src/definition/externalComponent/IExternalComponentUserInfo.ts new file mode 100644 index 0000000000000..dd681db8d42bf --- /dev/null +++ b/packages/apps-engine/src/definition/externalComponent/IExternalComponentUserInfo.ts @@ -0,0 +1,14 @@ +import type { IUser } from '../users'; + +type ClientUserInfo = Pick; + +/** + * Represents the user's information returned to + * the external component. + */ +export interface IExternalComponentUserInfo extends ClientUserInfo { + /** + * the avatar URL of the Rocket.Chat user + */ + avatarUrl: string; +} diff --git a/packages/apps-engine/src/definition/externalComponent/index.ts b/packages/apps-engine/src/definition/externalComponent/index.ts index acd4bbf44982e..0dcf8ad0e2680 100644 --- a/packages/apps-engine/src/definition/externalComponent/index.ts +++ b/packages/apps-engine/src/definition/externalComponent/index.ts @@ -3,3 +3,6 @@ import { IPostExternalComponentClosed } from './IPostExternalComponentClosed'; import { IPostExternalComponentOpened } from './IPostExternalComponentOpened'; export { IExternalComponent, IPostExternalComponentClosed, IPostExternalComponentOpened }; +export * from './IExternalComponentState'; +export * from './IExternalComponentRoomInfo'; +export * from './IExternalComponentUserInfo'; diff --git a/packages/apps-engine/src/definition/oauth2/OAuth2.ts b/packages/apps-engine/src/definition/oauth2/OAuth2.ts index 6339e5ea224a4..d4d0dfe9fdc98 100644 --- a/packages/apps-engine/src/definition/oauth2/OAuth2.ts +++ b/packages/apps-engine/src/definition/oauth2/OAuth2.ts @@ -1,4 +1,4 @@ -import { OAuth2Client } from '../../server/oauth2/OAuth2Client'; +import { OAuth2Client } from './OAuth2Client'; import type { App } from '../App'; import type { IOAuth2ClientOptions } from './IOAuth2'; diff --git a/packages/apps-engine/src/definition/oauth2/OAuth2Client.ts b/packages/apps-engine/src/definition/oauth2/OAuth2Client.ts new file mode 100644 index 0000000000000..ba6f8a90d454f --- /dev/null +++ b/packages/apps-engine/src/definition/oauth2/OAuth2Client.ts @@ -0,0 +1,337 @@ +import { URL } from 'url'; + +import type { App } from '../App'; +import type { IConfigurationExtend, IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { HttpStatusCode } from '../accessors'; +import type { IApiEndpointInfo, IApiRequest, IApiResponse } from '../api'; +import { ApiSecurity, ApiVisibility } from '../api'; +import { RocketChatAssociationModel, RocketChatAssociationRecord } from '../metadata'; +import type { IAuthData, IOAuth2Client, IOAuth2ClientOptions } from './IOAuth2'; +import { SettingType } from '../settings'; +import type { IUser } from '../users'; + +export enum GrantType { + RefreshToken = 'refresh_token', + AuthorizationCode = 'authorization_code', +} + +export class OAuth2Client implements IOAuth2Client { + private defaultContents = { + success: `
\ +

\ + Authorization went successfully
\ + You can close this tab now
\ +

\ +
`, + failed: `
\ +

\ + Oops, something went wrong, please try again or in case it still does not work, contact the administrator.\ +

\ +
`, + }; + + constructor( + private readonly app: App, + private readonly config: IOAuth2ClientOptions, + ) {} + + public async setup(configuration: IConfigurationExtend): Promise { + configuration.api.provideApi({ + security: ApiSecurity.UNSECURE, + visibility: ApiVisibility.PUBLIC, + endpoints: [ + { + path: `${this.config.alias}-callback`, + get: this.handleOAuthCallback.bind(this), + }, + ], + }); + + await Promise.all([ + configuration.settings.provideSetting({ + id: `${this.config.alias}-oauth-client-id`, + type: SettingType.STRING, + public: true, + required: true, + packageValue: '', + i18nLabel: `${this.config.alias}-oauth-client-id`, + }), + + configuration.settings.provideSetting({ + id: `${this.config.alias}-oauth-clientsecret`, + type: SettingType.STRING, + public: true, + required: true, + packageValue: '', + i18nLabel: `${this.config.alias}-oauth-client-secret`, + }), + ]); + } + + public async getUserAuthorizationUrl(user: IUser, scopes?: Array): Promise { + const redirectUri = this.app.getAccessors().providedApiEndpoints[0].computedPath.substring(1); + + const siteUrl = await this.getBaseURLWithoutTrailingSlash(); + + const finalScopes = ([] as Array).concat(this.config.defaultScopes || [], scopes || []); + + const { authUri } = this.config; + + const clientId = await this.app + .getAccessors() + .reader.getEnvironmentReader() + .getSettings() + .getValueById(`${this.config.alias}-oauth-client-id`); + + const url = new URL(authUri, siteUrl); + + url.searchParams.set('response_type', 'code'); + url.searchParams.set('redirect_uri', `${siteUrl}/${redirectUri}`); + url.searchParams.set('state', user.id); + url.searchParams.set('client_id', clientId); + url.searchParams.set('access_type', 'offline'); + + if (finalScopes.length > 0) { + url.searchParams.set('scope', finalScopes.join(' ')); + } + + return url; + } + + public async getAccessTokenForUser(user: IUser): Promise { + const associations = [ + new RocketChatAssociationRecord(RocketChatAssociationModel.USER, user.id), + new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, `${this.config.alias}-oauth-connection`), + ]; + + const [result] = (await this.app.getAccessors().reader.getPersistenceReader().readByAssociations(associations)) as unknown as Array< + IAuthData | undefined + >; + + return result; + } + + public async refreshUserAccessToken(user: IUser, persis: IPersistence): Promise { + try { + const tokenInfo = await this.getAccessTokenForUser(user); + + if (!tokenInfo) { + throw new Error('User has no access token information'); + } + + if (!tokenInfo.refreshToken) { + throw new Error('User token information has no refresh token available'); + } + + const { + config: { refreshTokenUri }, + } = this; + + const clientId = await this.app + .getAccessors() + .reader.getEnvironmentReader() + .getSettings() + .getValueById(`${this.config.alias}-oauth-client-id`); + + const clientSecret = await this.app + .getAccessors() + .reader.getEnvironmentReader() + .getSettings() + .getValueById(`${this.config.alias}-oauth-clientsecret`); + + const siteUrl = await this.getBaseURLWithoutTrailingSlash(); + + const redirectUri = this.app.getAccessors().providedApiEndpoints[0].computedPath.substring(1); + + const url = new URL(refreshTokenUri); + + url.searchParams.set('client_id', clientId); + url.searchParams.set('client_secret', clientSecret); + url.searchParams.set('redirect_uri', `${siteUrl}/${redirectUri}`); + url.searchParams.set('refresh_token', tokenInfo.refreshToken); + url.searchParams.set('grant_type', GrantType.RefreshToken); + + const { content, statusCode } = await this.app.getAccessors().http.post(url.href); + + if (statusCode !== 200) { + throw new Error('Request to provider was unsuccessful. Check logs for more information'); + } + + const { access_token, expires_in, refresh_token, scope } = JSON.parse(content as string); + + if (!access_token) { + throw new Error('No access token returned by the provider'); + } + + const authData: IAuthData = { + scope, + token: access_token, + expiresAt: expires_in, + refreshToken: refresh_token || tokenInfo.refreshToken, + }; + + await this.saveToken(authData, user.id, persis); + + return authData; + } catch (error) { + this.app.getLogger().error(error); + throw error; + } + } + + public async revokeUserAccessToken(user: IUser, persis: IPersistence): Promise { + try { + const tokenInfo = await this.getAccessTokenForUser(user); + + if (!tokenInfo?.token) { + throw new Error('No access token available for this user.'); + } + + const url = new URL(this.config.revokeTokenUri); + + url.searchParams.set('token', tokenInfo?.token); + + const result = await this.app.getAccessors().http.post(url.href); + + if (result.statusCode !== 200) { + throw new Error('Provider did not allow token to be revoked'); + } + + await this.removeToken({ userId: user.id, persis }); + + return true; + } catch (error) { + this.app.getLogger().error(error); + return false; + } + } + + private async getBaseURLWithoutTrailingSlash(): Promise { + const SITE_URL = 'Site_Url'; + const url = await this.app.getAccessors().environmentReader.getServerSettings().getValueById(SITE_URL); + + if (url.endsWith('/')) { + return url.substr(0, url.length - 1); + } + return url; + } + + private async handleOAuthCallback( + request: IApiRequest, + endpoint: IApiEndpointInfo, + read: IRead, + modify: IModify, + http: IHttp, + persis: IPersistence, + ): Promise { + try { + const { + query: { code, state }, + } = request; + + const user = await this.app.getAccessors().reader.getUserReader().getById(state); + + if (!user) { + throw new Error('User could not be determined.'); + } + + // User chose not to authorize the access + if (!code) { + const failedResult = await this.config.authorizationCallback?.(undefined, user, read, modify, http, persis); + + return { + status: HttpStatusCode.UNAUTHORIZED, + content: failedResult?.responseContent || this.defaultContents.failed, + }; + } + + const siteUrl = await this.getBaseURLWithoutTrailingSlash(); + + const accessTokenUrl = this.config.accessTokenUri; + + const redirectUri = this.app.getAccessors().providedApiEndpoints[0].computedPath.substring(1); + + const clientId = await this.app + .getAccessors() + .reader.getEnvironmentReader() + .getSettings() + .getValueById(`${this.config.alias}-oauth-client-id`); + + const clientSecret = await this.app + .getAccessors() + .reader.getEnvironmentReader() + .getSettings() + .getValueById(`${this.config.alias}-oauth-clientsecret`); + + const url = new URL(accessTokenUrl, siteUrl); + + url.searchParams.set('client_id', clientId); + url.searchParams.set('redirect_uri', `${siteUrl}/${redirectUri}`); + url.searchParams.set('code', code); + url.searchParams.set('client_secret', clientSecret); + url.searchParams.set('access_type', 'offline'); + url.searchParams.set('grant_type', GrantType.AuthorizationCode); + + const { content, statusCode } = await http.post(url.href, { + headers: { Accept: 'application/json' }, + }); + + // If provider had a server error, nothing we can do + if (statusCode >= 500) { + throw new Error('Request for access token failed. Check logs for more information'); + } + + const response = JSON.parse(content as string); + const { access_token, expires_in, refresh_token, scope } = response; + + const authData: IAuthData = { + scope, + token: access_token, + expiresAt: expires_in, + refreshToken: refresh_token, + }; + + const result = await this.config.authorizationCallback?.(authData, user, read, modify, http, persis); + + await this.saveToken(authData, user.id, persis); + + return { + status: statusCode, + content: result?.responseContent || this.defaultContents.success, + }; + } catch (error) { + this.app.getLogger().error(error); + return { + status: HttpStatusCode.INTERNAL_SERVER_ERROR, + content: this.defaultContents.failed, + }; + } + } + + private async saveToken(authData: IAuthData, userId: string, persis: IPersistence): Promise { + const { scope, token, expiresAt, refreshToken } = authData; + + return persis.updateByAssociations( + [ + new RocketChatAssociationRecord(RocketChatAssociationModel.USER, userId), + new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, `${this.config.alias}-oauth-connection`), + ], + { + scope, + token, + expiresAt: expiresAt || '', + refreshToken: refreshToken || '', + }, + true, // we want to create the record if it doesn't exist + ); + } + + private async removeToken({ userId, persis }: { userId: string; persis: IPersistence }): Promise { + const [result] = (await persis.removeByAssociations([ + new RocketChatAssociationRecord(RocketChatAssociationModel.USER, userId), + new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, `${this.config.alias}-oauth-connection`), + ])) as unknown as Array; + + return result; + } +} diff --git a/packages/apps-engine/src/definition/rooms/IGetMessagesOptions.ts b/packages/apps-engine/src/definition/rooms/IGetMessagesOptions.ts new file mode 100644 index 0000000000000..b2a36adde9843 --- /dev/null +++ b/packages/apps-engine/src/definition/rooms/IGetMessagesOptions.ts @@ -0,0 +1,43 @@ +import type { RoomType } from './RoomType'; + +export const GetMessagesSortableFields = ['createdAt'] as const; + +export type GetMessagesOptions = { + limit: number; + skip: number; + sort: Record<(typeof GetMessagesSortableFields)[number], 'asc' | 'desc'>; + showThreadMessages: boolean; +}; + +/** + * Filters for querying rooms in the system. + */ +export type GetRoomsFilters = { + /** + * When specified, only rooms matching the provided types will be returned. + */ + types?: Array; + /** + * Filter to include or exclude discussion rooms. + * + * When undefined (default), discussions are included in the result set. + * + * When true, ONLY discussions are included in the result set (remove non-discussions). + * When false, discussion rooms are excluded from the result set. + */ + discussions?: boolean; + /** + * Filter to include or exclude team main rooms. + * + * When undefined (default), team main rooms are included in the result set. + * + * When true, ONLY team main rooms are included in the result set (remove non-teams). + * When false, team main rooms are excluded from the result set. + */ + teams?: boolean; +}; + +export type GetRoomsOptions = { + limit?: number; + skip?: number; +}; diff --git a/packages/apps-engine/src/definition/rooms/index.ts b/packages/apps-engine/src/definition/rooms/index.ts index a7212f8ee82de..c50be9c9635fc 100644 --- a/packages/apps-engine/src/definition/rooms/index.ts +++ b/packages/apps-engine/src/definition/rooms/index.ts @@ -26,3 +26,4 @@ export * from './IRoomUserJoinedContext'; export * from './IPreRoomUserLeave'; export * from './IPostRoomUserLeave'; export * from './IRoomUserLeaveContext'; +export * from './IGetMessagesOptions'; From 5d4a6f7dd8456c5c519b99c88c4a26d06339a1ee Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Apr 2026 14:10:56 -0300 Subject: [PATCH 17/19] refactor(apps-engine): remove server, client, deno-runtime and scripts Strip @rocket.chat/apps-engine down to the definition layer only. All runtime code (AppManager, bridges, compiler, deno-runtime, etc.) has already been moved to @rocket.chat/apps in previous commits. Updates package.json, tsconfig.json, and turbo.json to reflect the narrower scope (definition/** and lib/** outputs only). Co-Authored-By: Claude Sonnet 4.6 --- packages/apps-engine/deno-runtime/.gitignore | 1 - .../deno-runtime/AppObjectRegistry.ts | 25 - .../apps-engine/deno-runtime/acorn-walk.d.ts | 175 --- packages/apps-engine/deno-runtime/acorn.d.ts | 915 ------------ packages/apps-engine/deno-runtime/deno.jsonc | 26 - packages/apps-engine/deno-runtime/deno.lock | 128 -- .../deno-runtime/error-handlers.ts | 33 - .../deno-runtime/handlers/api-handler.ts | 50 - .../deno-runtime/handlers/app/construct.ts | 132 -- .../handlers/app/handleGetStatus.ts | 15 - .../handlers/app/handleInitialize.ts | 24 - .../handlers/app/handleOnDisable.ts | 20 - .../handlers/app/handleOnEnable.ts | 22 - .../handlers/app/handleOnInstall.ts | 34 - .../handlers/app/handleOnPreSettingUpdate.ts | 31 - .../handlers/app/handleOnSettingUpdated.ts | 33 - .../handlers/app/handleOnUninstall.ts | 34 - .../handlers/app/handleOnUpdate.ts | 34 - .../handlers/app/handleSetStatus.ts | 33 - .../handlers/app/handleUploadEvents.ts | 78 - .../deno-runtime/handlers/app/handler.ts | 115 -- .../deno-runtime/handlers/lib/assertions.ts | 51 - .../deno-runtime/handlers/listener/handler.ts | 153 -- .../handlers/outboundcomms-handler.ts | 37 - .../handlers/scheduler-handler.ts | 65 - .../handlers/slashcommand-handler.ts | 128 -- .../handlers/tests/api-handler.test.ts | 118 -- .../handlers/tests/helpers/mod.ts | 29 - .../handlers/tests/listener-handler.test.ts | 234 --- .../handlers/tests/scheduler-handler.test.ts | 41 - .../tests/slashcommand-handler.test.ts | 159 -- .../handlers/tests/uikit-handler.test.ts | 105 -- .../tests/upload-event-handler.test.ts | 107 -- .../tests/videoconference-handler.test.ts | 122 -- .../deno-runtime/handlers/uikit/handler.ts | 88 -- .../handlers/videoconference-handler.ts | 52 - .../lib/accessors/builders/BlockBuilder.ts | 215 --- .../accessors/builders/DiscussionBuilder.ts | 59 - .../builders/LivechatMessageBuilder.ts | 204 --- .../lib/accessors/builders/MessageBuilder.ts | 271 ---- .../lib/accessors/builders/RoomBuilder.ts | 197 --- .../lib/accessors/builders/UserBuilder.ts | 81 -- .../builders/VideoConferenceBuilder.ts | 94 -- .../lib/accessors/extenders/HttpExtender.ts | 58 - .../accessors/extenders/MessageExtender.ts | 66 - .../lib/accessors/extenders/RoomExtender.ts | 61 - .../extenders/VideoConferenceExtend.ts | 69 - .../accessors/formatResponseErrorHandler.ts | 14 - .../deno-runtime/lib/accessors/http.ts | 92 -- .../deno-runtime/lib/accessors/mod.ts | 322 ----- .../lib/accessors/modify/ModifyCreator.ts | 383 ----- .../lib/accessors/modify/ModifyExtender.ts | 106 -- .../lib/accessors/modify/ModifyUpdater.ts | 170 --- .../deno-runtime/lib/accessors/notifier.ts | 84 -- .../lib/accessors/tests/AppAccessors.test.ts | 122 -- .../lib/accessors/tests/ModifyCreator.test.ts | 259 ---- .../accessors/tests/ModifyExtender.test.ts | 244 ---- .../lib/accessors/tests/ModifyUpdater.test.ts | 234 --- .../tests/formatResponseErrorHandler.test.ts | 211 --- .../lib/accessors/tests/http.test.ts | 164 --- .../apps-engine/deno-runtime/lib/ast/mod.ts | 70 - .../deno-runtime/lib/ast/operations.ts | 237 --- .../lib/ast/tests/data/ast_blocks.ts | 436 ------ .../lib/ast/tests/operations.test.ts | 261 ---- .../apps-engine/deno-runtime/lib/codec.ts | 43 - .../apps-engine/deno-runtime/lib/logger.ts | 142 -- .../apps-engine/deno-runtime/lib/messenger.ts | 202 --- .../deno-runtime/lib/metricsCollector.ts | 20 - .../apps-engine/deno-runtime/lib/parseArgs.ts | 11 - .../deno-runtime/lib/requestContext.ts | 10 - .../apps-engine/deno-runtime/lib/require.ts | 15 - packages/apps-engine/deno-runtime/lib/room.ts | 104 -- .../deno-runtime/lib/roomFactory.ts | 29 - .../lib/sanitizeDeprecatedUsage.ts | 20 - .../deno-runtime/lib/tests/logger.test.ts | 110 -- .../deno-runtime/lib/tests/messenger.test.ts | 99 -- .../deno-runtime/lib/wrapAppForRequest.ts | 60 - packages/apps-engine/deno-runtime/main.ts | 132 -- packages/apps-engine/package.json | 68 +- packages/apps-engine/scripts/deno-cache.js | 89 -- .../src/client/AppClientManager.ts | 31 - .../src/client/AppServerCommunicator.ts | 16 - .../src/client/AppsEngineUIClient.ts | 70 - .../src/client/AppsEngineUIHost.ts | 78 - .../apps-engine/src/client/constants/index.ts | 6 - .../client/definition/AppsEngineUIMethods.ts | 7 - .../definition/IAppsEngineUIResponse.ts | 19 - .../definition/IExternalComponentRoomInfo.ts | 16 - .../definition/IExternalComponentUserInfo.ts | 14 - .../src/client/definition/index.ts | 4 - packages/apps-engine/src/client/index.ts | 4 - .../apps-engine/src/client/utils/index.ts | 18 - packages/apps-engine/src/server/AppManager.ts | 1240 ---------------- .../apps-engine/src/server/IGetAppsFilter.ts | 9 - packages/apps-engine/src/server/ProxiedApp.ts | 162 --- .../src/server/accessors/ApiExtend.ts | 14 - .../src/server/accessors/AppAccessors.ts | 39 - .../server/accessors/CloudWorkspaceRead.ts | 14 - .../server/accessors/ConfigurationExtend.ts | 26 - .../server/accessors/ConfigurationModify.ts | 9 - .../src/server/accessors/ContactCreator.ts | 24 - .../src/server/accessors/ContactRead.ts | 14 - .../src/server/accessors/DiscussionBuilder.ts | 47 - .../src/server/accessors/EmailCreator.ts | 14 - .../src/server/accessors/EnvironmentRead.ts | 21 - .../src/server/accessors/EnvironmentWrite.ts | 16 - .../accessors/EnvironmentalVariableRead.ts | 21 - .../src/server/accessors/ExperimentalRead.ts | 9 - .../accessors/ExternalComponentsExtend.ts | 14 - .../apps-engine/src/server/accessors/Http.ts | 77 - .../src/server/accessors/HttpExtend.ts | 58 - .../src/server/accessors/LivechatCreator.ts | 38 - .../accessors/LivechatMessageBuilder.ts | 191 --- .../src/server/accessors/LivechatRead.ts | 78 - .../src/server/accessors/LivechatUpdater.ts | 33 - .../src/server/accessors/MessageBuilder.ts | 224 --- .../src/server/accessors/MessageExtender.ts | 50 - .../src/server/accessors/MessageRead.ts | 36 - .../src/server/accessors/ModerationModify.ts | 23 - .../src/server/accessors/Modify.ts | 92 -- .../src/server/accessors/ModifyCreator.ts | 274 ---- .../src/server/accessors/ModifyDeleter.ts | 38 - .../src/server/accessors/ModifyExtender.ts | 49 - .../src/server/accessors/ModifyUpdater.ts | 109 -- .../src/server/accessors/Notifier.ts | 53 - .../src/server/accessors/OAuthAppsModify.ts | 22 - .../src/server/accessors/OAuthAppsReader.ts | 18 - .../OutboundCommunicationProviderExtend.ts | 18 - .../src/server/accessors/Persistence.ts | 46 - .../src/server/accessors/PersistenceRead.ts | 22 - .../src/server/accessors/Reader.ts | 98 -- .../src/server/accessors/RoleRead.ts | 18 - .../src/server/accessors/RoomBuilder.ts | 155 -- .../src/server/accessors/RoomExtender.ts | 56 - .../src/server/accessors/RoomRead.ts | 115 -- .../src/server/accessors/SchedulerExtend.ts | 14 - .../src/server/accessors/SchedulerModify.ts | 30 - .../src/server/accessors/ServerSettingRead.ts | 37 - .../server/accessors/ServerSettingUpdater.ts | 18 - .../server/accessors/ServerSettingsModify.ts | 26 - .../src/server/accessors/SettingRead.ts | 25 - .../src/server/accessors/SettingUpdater.ts | 66 - .../src/server/accessors/SettingsExtend.ts | 26 - .../server/accessors/SlashCommandsExtend.ts | 14 - .../server/accessors/SlashCommandsModify.ts | 22 - .../src/server/accessors/ThreadRead.ts | 14 - .../src/server/accessors/UIController.ts | 118 -- .../src/server/accessors/UIExtend.ts | 14 - .../src/server/accessors/UploadCreator.ts | 28 - .../src/server/accessors/UploadRead.ts | 24 - .../src/server/accessors/UserBuilder.ts | 74 - .../src/server/accessors/UserRead.ts | 30 - .../src/server/accessors/UserUpdater.ts | 31 - .../accessors/VideoConfProviderExtend.ts | 14 - .../accessors/VideoConferenceBuilder.ts | 83 -- .../server/accessors/VideoConferenceExtend.ts | 64 - .../server/accessors/VideoConferenceRead.ts | 14 - .../apps-engine/src/server/accessors/index.ts | 97 -- .../src/server/bridges/ApiBridge.ts | 49 - .../src/server/bridges/AppActivationBridge.ts | 35 - .../src/server/bridges/AppBridges.ts | 113 -- .../server/bridges/AppDetailChangesBridge.ts | 16 - .../src/server/bridges/BaseBridge.ts | 6 - .../server/bridges/CloudWorkspaceBridge.ts | 30 - .../src/server/bridges/CommandBridge.ts | 117 -- .../src/server/bridges/ContactBridge.ts | 69 - .../src/server/bridges/EmailBridge.ts | 30 - .../bridges/EnvironmentalVariableBridge.ts | 45 - .../src/server/bridges/ExperimentalBridge.ts | 10 - .../src/server/bridges/HttpBridge.ts | 37 - .../src/server/bridges/IInternalBridge.ts | 7 - .../bridges/IInternalFederationBridge.ts | 15 - .../bridges/IInternalPersistenceBridge.ts | 9 - .../bridges/IInternalSchedulerBridge.ts | 8 - .../src/server/bridges/IInternalUserBridge.ts | 8 - .../src/server/bridges/IListenerBridge.ts | 10 - .../src/server/bridges/InternalBridge.ts | 22 - .../src/server/bridges/ListenerBridge.ts | 18 - .../src/server/bridges/LivechatBridge.ts | 305 ---- .../src/server/bridges/MessageBridge.ts | 116 -- .../src/server/bridges/ModerationBridge.ts | 47 - .../src/server/bridges/OAuthAppsBridge.ts | 85 -- .../server/bridges/OutboundMessagesBridge.ts | 50 - .../src/server/bridges/PersistenceBridge.ts | 174 --- .../src/server/bridges/RoleBridge.ts | 38 - .../src/server/bridges/RoomBridge.ts | 252 ---- .../src/server/bridges/SchedulerBridge.ts | 62 - .../src/server/bridges/ServerSettingBridge.ts | 93 -- .../src/server/bridges/ThreadBridge.ts | 35 - .../src/server/bridges/UiInteractionBridge.ts | 31 - .../src/server/bridges/UploadBridge.ts | 62 - .../src/server/bridges/UserBridge.ts | 157 -- .../server/bridges/VideoConferenceBridge.ts | 94 -- .../apps-engine/src/server/bridges/index.ts | 58 - .../src/server/compiler/AppCompiler.ts | 30 - .../compiler/AppFabricationFulfillment.ts | 75 - .../src/server/compiler/AppImplements.ts | 32 - .../src/server/compiler/AppPackageParser.ts | 143 -- .../server/compiler/IParseAppPackageResult.ts | 9 - .../apps-engine/src/server/compiler/index.ts | 7 - .../src/server/compiler/modules/index.ts | 55 - .../src/server/compiler/modules/networking.ts | 36 - .../server/errors/AppOutboundProcessError.ts | 12 - .../errors/CommandAlreadyExistsError.ts | 9 - .../CommandHasAlreadyBeenTouchedError.ts | 9 - .../src/server/errors/CompilerError.ts | 9 - .../server/errors/InvalidInstallationError.ts | 5 - .../src/server/errors/InvalidLicenseError.ts | 7 - .../server/errors/MustContainFunctionError.ts | 9 - .../src/server/errors/MustExtendAppError.ts | 5 - .../errors/NotEnoughMethodArgumentsError.ts | 9 - .../server/errors/PathAlreadyExistsError.ts | 9 - .../server/errors/PermissionDeniedError.ts | 25 - .../server/errors/RequiredApiVersionError.ts | 21 - .../VideoConfProviderAlreadyExistsError.ts | 9 - .../VideoConfProviderNotRegisteredError.ts | 9 - .../apps-engine/src/server/errors/index.ts | 25 - .../src/server/logging/AppConsole.ts | 121 -- .../src/server/logging/ILoggerStorageEntry.ts | 14 - .../apps-engine/src/server/logging/index.ts | 4 - .../src/server/managers/AppAccessorManager.ts | 251 ---- .../apps-engine/src/server/managers/AppApi.ts | 99 -- .../src/server/managers/AppApiManager.ts | 165 --- .../managers/AppExternalComponentManager.ts | 142 -- .../src/server/managers/AppLicenseManager.ts | 99 -- .../src/server/managers/AppListenerManager.ts | 1283 ----------------- .../AppOutboundCommunicationProvider.ts | 52 - ...AppOutboundCommunicationProviderManager.ts | 138 -- .../server/managers/AppPermissionManager.ts | 40 - .../src/server/managers/AppRuntimeManager.ts | 76 - .../server/managers/AppSchedulerManager.ts | 98 -- .../src/server/managers/AppSettingsManager.ts | 56 - .../server/managers/AppSignatureManager.ts | 85 -- .../src/server/managers/AppSlashCommand.ts | 80 - .../server/managers/AppSlashCommandManager.ts | 477 ------ .../server/managers/AppVideoConfProvider.ts | 113 -- .../managers/AppVideoConfProviderManager.ts | 216 --- .../server/managers/UIActionButtonManager.ts | 95 -- .../apps-engine/src/server/managers/index.ts | 23 - .../server/marketplace/IAppLicenseMetadata.ts | 5 - .../server/marketplace/IMarketplaceInfo.ts | 24 - .../marketplace/IMarketplacePricingPlan.ts | 11 - .../marketplace/IMarketplacePricingTier.ts | 6 - .../IMarketplaceSimpleBundleInfo.ts | 4 - .../IMarketplaceSubscriptionInfo.ts | 15 - .../marketplace/MarketplacePricingStrategy.ts | 5 - .../marketplace/MarketplacePurchaseType.ts | 4 - .../MarketplaceSubscriptionStatus.ts | 10 - .../MarketplaceSubscriptionType.ts | 4 - .../src/server/marketplace/index.ts | 15 - .../license/AppLicenseValidationResult.ts | 56 - .../src/server/marketplace/license/Crypto.ts | 26 - .../src/server/marketplace/license/index.ts | 4 - .../src/server/messages/Message.ts | 109 -- .../apps-engine/src/server/misc/UIHelper.ts | 32 - .../apps-engine/src/server/misc/Utilities.ts | 36 - .../src/server/oauth2/OAuth2Client.ts | 337 ----- .../src/server/permissions/AppPermissions.ts | 168 --- packages/apps-engine/src/server/rooms/Room.ts | 104 -- .../server/runtime/AppsEngineEmptyRuntime.ts | 21 - .../server/runtime/AppsEngineNodeRuntime.ts | 74 - .../src/server/runtime/AppsEngineRuntime.ts | 29 - .../src/server/runtime/EmptyRuntime.ts | 50 - .../src/server/runtime/IRuntimeController.ts | 34 - .../runtime/deno/AppsEngineDenoRuntime.ts | 739 ---------- .../server/runtime/deno/LivenessManager.ts | 254 ---- .../server/runtime/deno/ProcessMessenger.ts | 57 - .../src/server/runtime/deno/bundler.ts | 90 -- .../src/server/runtime/deno/codec.ts | 45 - .../src/server/storage/AppLogStorage.ts | 27 - .../src/server/storage/AppMetadataStorage.ts | 36 - .../src/server/storage/AppSourceStorage.ts | 40 - .../src/server/storage/IAppStorageItem.ts | 31 - .../apps-engine/src/server/storage/index.ts | 4 - packages/apps-engine/tsconfig.json | 4 +- packages/apps-engine/turbo.json | 2 +- .../src/server/compiler/AppPackageParser.ts | 25 +- 277 files changed, 16 insertions(+), 23355 deletions(-) delete mode 100644 packages/apps-engine/deno-runtime/.gitignore delete mode 100644 packages/apps-engine/deno-runtime/AppObjectRegistry.ts delete mode 100644 packages/apps-engine/deno-runtime/acorn-walk.d.ts delete mode 100644 packages/apps-engine/deno-runtime/acorn.d.ts delete mode 100644 packages/apps-engine/deno-runtime/deno.jsonc delete mode 100644 packages/apps-engine/deno-runtime/deno.lock delete mode 100644 packages/apps-engine/deno-runtime/error-handlers.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/api-handler.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/app/construct.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleGetStatus.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleInitialize.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnDisable.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnEnable.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnInstall.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnSettingUpdated.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnUninstall.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnUpdate.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleSetStatus.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleUploadEvents.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/app/handler.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/lib/assertions.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/listener/handler.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/outboundcomms-handler.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/scheduler-handler.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/slashcommand-handler.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/tests/api-handler.test.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/tests/helpers/mod.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/tests/listener-handler.test.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/tests/scheduler-handler.test.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/tests/slashcommand-handler.test.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/tests/uikit-handler.test.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/tests/upload-event-handler.test.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/tests/videoconference-handler.test.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/uikit/handler.ts delete mode 100644 packages/apps-engine/deno-runtime/handlers/videoconference-handler.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/BlockBuilder.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/RoomBuilder.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/UserBuilder.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/extenders/HttpExtender.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/extenders/MessageExtender.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/extenders/RoomExtender.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/formatResponseErrorHandler.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/http.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/mod.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyExtender.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyUpdater.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/notifier.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/tests/AppAccessors.test.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/tests/formatResponseErrorHandler.test.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/accessors/tests/http.test.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/ast/mod.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/ast/operations.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/ast/tests/data/ast_blocks.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/ast/tests/operations.test.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/codec.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/logger.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/messenger.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/metricsCollector.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/parseArgs.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/requestContext.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/require.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/room.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/roomFactory.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/sanitizeDeprecatedUsage.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/tests/logger.test.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/tests/messenger.test.ts delete mode 100644 packages/apps-engine/deno-runtime/lib/wrapAppForRequest.ts delete mode 100644 packages/apps-engine/deno-runtime/main.ts delete mode 100644 packages/apps-engine/scripts/deno-cache.js delete mode 100644 packages/apps-engine/src/client/AppClientManager.ts delete mode 100644 packages/apps-engine/src/client/AppServerCommunicator.ts delete mode 100644 packages/apps-engine/src/client/AppsEngineUIClient.ts delete mode 100644 packages/apps-engine/src/client/AppsEngineUIHost.ts delete mode 100644 packages/apps-engine/src/client/constants/index.ts delete mode 100644 packages/apps-engine/src/client/definition/AppsEngineUIMethods.ts delete mode 100644 packages/apps-engine/src/client/definition/IAppsEngineUIResponse.ts delete mode 100644 packages/apps-engine/src/client/definition/IExternalComponentRoomInfo.ts delete mode 100644 packages/apps-engine/src/client/definition/IExternalComponentUserInfo.ts delete mode 100644 packages/apps-engine/src/client/definition/index.ts delete mode 100644 packages/apps-engine/src/client/index.ts delete mode 100644 packages/apps-engine/src/client/utils/index.ts delete mode 100644 packages/apps-engine/src/server/AppManager.ts delete mode 100644 packages/apps-engine/src/server/IGetAppsFilter.ts delete mode 100644 packages/apps-engine/src/server/ProxiedApp.ts delete mode 100644 packages/apps-engine/src/server/accessors/ApiExtend.ts delete mode 100644 packages/apps-engine/src/server/accessors/AppAccessors.ts delete mode 100644 packages/apps-engine/src/server/accessors/CloudWorkspaceRead.ts delete mode 100644 packages/apps-engine/src/server/accessors/ConfigurationExtend.ts delete mode 100644 packages/apps-engine/src/server/accessors/ConfigurationModify.ts delete mode 100644 packages/apps-engine/src/server/accessors/ContactCreator.ts delete mode 100644 packages/apps-engine/src/server/accessors/ContactRead.ts delete mode 100644 packages/apps-engine/src/server/accessors/DiscussionBuilder.ts delete mode 100644 packages/apps-engine/src/server/accessors/EmailCreator.ts delete mode 100644 packages/apps-engine/src/server/accessors/EnvironmentRead.ts delete mode 100644 packages/apps-engine/src/server/accessors/EnvironmentWrite.ts delete mode 100644 packages/apps-engine/src/server/accessors/EnvironmentalVariableRead.ts delete mode 100644 packages/apps-engine/src/server/accessors/ExperimentalRead.ts delete mode 100644 packages/apps-engine/src/server/accessors/ExternalComponentsExtend.ts delete mode 100644 packages/apps-engine/src/server/accessors/Http.ts delete mode 100644 packages/apps-engine/src/server/accessors/HttpExtend.ts delete mode 100644 packages/apps-engine/src/server/accessors/LivechatCreator.ts delete mode 100644 packages/apps-engine/src/server/accessors/LivechatMessageBuilder.ts delete mode 100644 packages/apps-engine/src/server/accessors/LivechatRead.ts delete mode 100644 packages/apps-engine/src/server/accessors/LivechatUpdater.ts delete mode 100644 packages/apps-engine/src/server/accessors/MessageBuilder.ts delete mode 100644 packages/apps-engine/src/server/accessors/MessageExtender.ts delete mode 100644 packages/apps-engine/src/server/accessors/MessageRead.ts delete mode 100644 packages/apps-engine/src/server/accessors/ModerationModify.ts delete mode 100644 packages/apps-engine/src/server/accessors/Modify.ts delete mode 100644 packages/apps-engine/src/server/accessors/ModifyCreator.ts delete mode 100644 packages/apps-engine/src/server/accessors/ModifyDeleter.ts delete mode 100644 packages/apps-engine/src/server/accessors/ModifyExtender.ts delete mode 100644 packages/apps-engine/src/server/accessors/ModifyUpdater.ts delete mode 100644 packages/apps-engine/src/server/accessors/Notifier.ts delete mode 100644 packages/apps-engine/src/server/accessors/OAuthAppsModify.ts delete mode 100644 packages/apps-engine/src/server/accessors/OAuthAppsReader.ts delete mode 100644 packages/apps-engine/src/server/accessors/OutboundCommunicationProviderExtend.ts delete mode 100644 packages/apps-engine/src/server/accessors/Persistence.ts delete mode 100644 packages/apps-engine/src/server/accessors/PersistenceRead.ts delete mode 100644 packages/apps-engine/src/server/accessors/Reader.ts delete mode 100644 packages/apps-engine/src/server/accessors/RoleRead.ts delete mode 100644 packages/apps-engine/src/server/accessors/RoomBuilder.ts delete mode 100644 packages/apps-engine/src/server/accessors/RoomExtender.ts delete mode 100644 packages/apps-engine/src/server/accessors/RoomRead.ts delete mode 100644 packages/apps-engine/src/server/accessors/SchedulerExtend.ts delete mode 100644 packages/apps-engine/src/server/accessors/SchedulerModify.ts delete mode 100644 packages/apps-engine/src/server/accessors/ServerSettingRead.ts delete mode 100644 packages/apps-engine/src/server/accessors/ServerSettingUpdater.ts delete mode 100644 packages/apps-engine/src/server/accessors/ServerSettingsModify.ts delete mode 100644 packages/apps-engine/src/server/accessors/SettingRead.ts delete mode 100644 packages/apps-engine/src/server/accessors/SettingUpdater.ts delete mode 100644 packages/apps-engine/src/server/accessors/SettingsExtend.ts delete mode 100644 packages/apps-engine/src/server/accessors/SlashCommandsExtend.ts delete mode 100644 packages/apps-engine/src/server/accessors/SlashCommandsModify.ts delete mode 100644 packages/apps-engine/src/server/accessors/ThreadRead.ts delete mode 100644 packages/apps-engine/src/server/accessors/UIController.ts delete mode 100644 packages/apps-engine/src/server/accessors/UIExtend.ts delete mode 100644 packages/apps-engine/src/server/accessors/UploadCreator.ts delete mode 100644 packages/apps-engine/src/server/accessors/UploadRead.ts delete mode 100644 packages/apps-engine/src/server/accessors/UserBuilder.ts delete mode 100644 packages/apps-engine/src/server/accessors/UserRead.ts delete mode 100644 packages/apps-engine/src/server/accessors/UserUpdater.ts delete mode 100644 packages/apps-engine/src/server/accessors/VideoConfProviderExtend.ts delete mode 100644 packages/apps-engine/src/server/accessors/VideoConferenceBuilder.ts delete mode 100644 packages/apps-engine/src/server/accessors/VideoConferenceExtend.ts delete mode 100644 packages/apps-engine/src/server/accessors/VideoConferenceRead.ts delete mode 100644 packages/apps-engine/src/server/accessors/index.ts delete mode 100644 packages/apps-engine/src/server/bridges/ApiBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/AppActivationBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/AppBridges.ts delete mode 100644 packages/apps-engine/src/server/bridges/AppDetailChangesBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/BaseBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/CloudWorkspaceBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/CommandBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/ContactBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/EmailBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/EnvironmentalVariableBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/ExperimentalBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/HttpBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/IInternalBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/IInternalFederationBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/IInternalPersistenceBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/IInternalSchedulerBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/IInternalUserBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/IListenerBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/InternalBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/ListenerBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/LivechatBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/MessageBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/ModerationBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/OAuthAppsBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/OutboundMessagesBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/PersistenceBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/RoleBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/RoomBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/SchedulerBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/ServerSettingBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/ThreadBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/UiInteractionBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/UploadBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/UserBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/VideoConferenceBridge.ts delete mode 100644 packages/apps-engine/src/server/bridges/index.ts delete mode 100644 packages/apps-engine/src/server/compiler/AppCompiler.ts delete mode 100644 packages/apps-engine/src/server/compiler/AppFabricationFulfillment.ts delete mode 100644 packages/apps-engine/src/server/compiler/AppImplements.ts delete mode 100644 packages/apps-engine/src/server/compiler/AppPackageParser.ts delete mode 100644 packages/apps-engine/src/server/compiler/IParseAppPackageResult.ts delete mode 100644 packages/apps-engine/src/server/compiler/index.ts delete mode 100644 packages/apps-engine/src/server/compiler/modules/index.ts delete mode 100644 packages/apps-engine/src/server/compiler/modules/networking.ts delete mode 100644 packages/apps-engine/src/server/errors/AppOutboundProcessError.ts delete mode 100644 packages/apps-engine/src/server/errors/CommandAlreadyExistsError.ts delete mode 100644 packages/apps-engine/src/server/errors/CommandHasAlreadyBeenTouchedError.ts delete mode 100644 packages/apps-engine/src/server/errors/CompilerError.ts delete mode 100644 packages/apps-engine/src/server/errors/InvalidInstallationError.ts delete mode 100644 packages/apps-engine/src/server/errors/InvalidLicenseError.ts delete mode 100644 packages/apps-engine/src/server/errors/MustContainFunctionError.ts delete mode 100644 packages/apps-engine/src/server/errors/MustExtendAppError.ts delete mode 100644 packages/apps-engine/src/server/errors/NotEnoughMethodArgumentsError.ts delete mode 100644 packages/apps-engine/src/server/errors/PathAlreadyExistsError.ts delete mode 100644 packages/apps-engine/src/server/errors/PermissionDeniedError.ts delete mode 100644 packages/apps-engine/src/server/errors/RequiredApiVersionError.ts delete mode 100644 packages/apps-engine/src/server/errors/VideoConfProviderAlreadyExistsError.ts delete mode 100644 packages/apps-engine/src/server/errors/VideoConfProviderNotRegisteredError.ts delete mode 100644 packages/apps-engine/src/server/errors/index.ts delete mode 100644 packages/apps-engine/src/server/logging/AppConsole.ts delete mode 100644 packages/apps-engine/src/server/logging/ILoggerStorageEntry.ts delete mode 100644 packages/apps-engine/src/server/logging/index.ts delete mode 100644 packages/apps-engine/src/server/managers/AppAccessorManager.ts delete mode 100644 packages/apps-engine/src/server/managers/AppApi.ts delete mode 100644 packages/apps-engine/src/server/managers/AppApiManager.ts delete mode 100644 packages/apps-engine/src/server/managers/AppExternalComponentManager.ts delete mode 100644 packages/apps-engine/src/server/managers/AppLicenseManager.ts delete mode 100644 packages/apps-engine/src/server/managers/AppListenerManager.ts delete mode 100644 packages/apps-engine/src/server/managers/AppOutboundCommunicationProvider.ts delete mode 100644 packages/apps-engine/src/server/managers/AppOutboundCommunicationProviderManager.ts delete mode 100644 packages/apps-engine/src/server/managers/AppPermissionManager.ts delete mode 100644 packages/apps-engine/src/server/managers/AppRuntimeManager.ts delete mode 100644 packages/apps-engine/src/server/managers/AppSchedulerManager.ts delete mode 100644 packages/apps-engine/src/server/managers/AppSettingsManager.ts delete mode 100644 packages/apps-engine/src/server/managers/AppSignatureManager.ts delete mode 100644 packages/apps-engine/src/server/managers/AppSlashCommand.ts delete mode 100644 packages/apps-engine/src/server/managers/AppSlashCommandManager.ts delete mode 100644 packages/apps-engine/src/server/managers/AppVideoConfProvider.ts delete mode 100644 packages/apps-engine/src/server/managers/AppVideoConfProviderManager.ts delete mode 100644 packages/apps-engine/src/server/managers/UIActionButtonManager.ts delete mode 100644 packages/apps-engine/src/server/managers/index.ts delete mode 100644 packages/apps-engine/src/server/marketplace/IAppLicenseMetadata.ts delete mode 100644 packages/apps-engine/src/server/marketplace/IMarketplaceInfo.ts delete mode 100644 packages/apps-engine/src/server/marketplace/IMarketplacePricingPlan.ts delete mode 100644 packages/apps-engine/src/server/marketplace/IMarketplacePricingTier.ts delete mode 100644 packages/apps-engine/src/server/marketplace/IMarketplaceSimpleBundleInfo.ts delete mode 100644 packages/apps-engine/src/server/marketplace/IMarketplaceSubscriptionInfo.ts delete mode 100644 packages/apps-engine/src/server/marketplace/MarketplacePricingStrategy.ts delete mode 100644 packages/apps-engine/src/server/marketplace/MarketplacePurchaseType.ts delete mode 100644 packages/apps-engine/src/server/marketplace/MarketplaceSubscriptionStatus.ts delete mode 100644 packages/apps-engine/src/server/marketplace/MarketplaceSubscriptionType.ts delete mode 100644 packages/apps-engine/src/server/marketplace/index.ts delete mode 100644 packages/apps-engine/src/server/marketplace/license/AppLicenseValidationResult.ts delete mode 100644 packages/apps-engine/src/server/marketplace/license/Crypto.ts delete mode 100644 packages/apps-engine/src/server/marketplace/license/index.ts delete mode 100644 packages/apps-engine/src/server/messages/Message.ts delete mode 100644 packages/apps-engine/src/server/misc/UIHelper.ts delete mode 100644 packages/apps-engine/src/server/misc/Utilities.ts delete mode 100644 packages/apps-engine/src/server/oauth2/OAuth2Client.ts delete mode 100644 packages/apps-engine/src/server/permissions/AppPermissions.ts delete mode 100644 packages/apps-engine/src/server/rooms/Room.ts delete mode 100644 packages/apps-engine/src/server/runtime/AppsEngineEmptyRuntime.ts delete mode 100644 packages/apps-engine/src/server/runtime/AppsEngineNodeRuntime.ts delete mode 100644 packages/apps-engine/src/server/runtime/AppsEngineRuntime.ts delete mode 100644 packages/apps-engine/src/server/runtime/EmptyRuntime.ts delete mode 100644 packages/apps-engine/src/server/runtime/IRuntimeController.ts delete mode 100644 packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts delete mode 100644 packages/apps-engine/src/server/runtime/deno/LivenessManager.ts delete mode 100644 packages/apps-engine/src/server/runtime/deno/ProcessMessenger.ts delete mode 100644 packages/apps-engine/src/server/runtime/deno/bundler.ts delete mode 100644 packages/apps-engine/src/server/runtime/deno/codec.ts delete mode 100644 packages/apps-engine/src/server/storage/AppLogStorage.ts delete mode 100644 packages/apps-engine/src/server/storage/AppMetadataStorage.ts delete mode 100644 packages/apps-engine/src/server/storage/AppSourceStorage.ts delete mode 100644 packages/apps-engine/src/server/storage/IAppStorageItem.ts delete mode 100644 packages/apps-engine/src/server/storage/index.ts diff --git a/packages/apps-engine/deno-runtime/.gitignore b/packages/apps-engine/deno-runtime/.gitignore deleted file mode 100644 index 5942ea3a153e7..0000000000000 --- a/packages/apps-engine/deno-runtime/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.deno/ diff --git a/packages/apps-engine/deno-runtime/AppObjectRegistry.ts b/packages/apps-engine/deno-runtime/AppObjectRegistry.ts deleted file mode 100644 index c9c05137a4a3d..0000000000000 --- a/packages/apps-engine/deno-runtime/AppObjectRegistry.ts +++ /dev/null @@ -1,25 +0,0 @@ -export type Maybe = T | null | undefined; - -export const AppObjectRegistry = new class { - registry: Record = {}; - - public get(key: string): Maybe { - return this.registry[key] as Maybe; - } - - public set(key: string, value: unknown): void { - this.registry[key] = value; - } - - public has(key: string): boolean { - return key in this.registry; - } - - public delete(key: string): void { - delete this.registry[key]; - } - - public clear(): void { - this.registry = {}; - } -}(); diff --git a/packages/apps-engine/deno-runtime/acorn-walk.d.ts b/packages/apps-engine/deno-runtime/acorn-walk.d.ts deleted file mode 100644 index 56db3bc38e9d2..0000000000000 --- a/packages/apps-engine/deno-runtime/acorn-walk.d.ts +++ /dev/null @@ -1,175 +0,0 @@ -import type acorn from './acorn.d.ts'; - -export type FullWalkerCallback = ( - node: acorn.AnyNode, - state: TState, - type: string, -) => void; - -export type FullAncestorWalkerCallback = ( - node: acorn.AnyNode, - state: TState, - ancestors: acorn.AnyNode[], - type: string, -) => void; - -type AggregateType = { - Expression: acorn.Expression; - Statement: acorn.Statement; - Pattern: acorn.Pattern; - ForInit: acorn.VariableDeclaration | acorn.Expression; -}; - -export type SimpleVisitors = - & { - [type in acorn.AnyNode['type']]?: (node: Extract, state: TState) => void; - } - & { - [type in keyof AggregateType]?: (node: AggregateType[type], state: TState) => void; - }; - -export type AncestorVisitors = - & { - [type in acorn.AnyNode['type']]?: (node: Extract, state: TState, ancestors: acorn.Node[]) => void; - } - & { - [type in keyof AggregateType]?: (node: AggregateType[type], state: TState, ancestors: acorn.Node[]) => void; - }; - -export type WalkerCallback = (node: acorn.Node, state: TState) => void; - -export type RecursiveVisitors = - & { - [type in acorn.AnyNode['type']]?: (node: Extract, state: TState, callback: WalkerCallback) => void; - } - & { - [type in keyof AggregateType]?: (node: AggregateType[type], state: TState, callback: WalkerCallback) => void; - }; - -export type FindPredicate = (type: string, node: acorn.Node) => boolean; - -export interface Found { - node: acorn.Node; - state: TState; -} - -/** - * does a 'simple' walk over a tree - * @param node the AST node to walk - * @param visitors an object with properties whose names correspond to node types in the {@link https://github.com/estree/estree | ESTree spec}. The properties should contain functions that will be called with the node object and, if applicable the state at that point. - * @param base a walker algorithm - * @param state a start state. The default walker will simply visit all statements and expressions and not produce a meaningful state. (An example of a use of state is to track scope at each point in the tree.) - */ -export function simple( - node: acorn.Node, - visitors: SimpleVisitors, - base?: RecursiveVisitors, - state?: TState, -): void; - -/** - * does a 'simple' walk over a tree, building up an array of ancestor nodes (including the current node) and passing the array to the callbacks as a third parameter. - * @param node - * @param visitors - * @param base - * @param state - */ -export function ancestor( - node: acorn.Node, - visitors: AncestorVisitors, - base?: RecursiveVisitors, - state?: TState, -): void; - -/** - * does a 'recursive' walk, where the walker functions are responsible for continuing the walk on the child nodes of their target node. - * @param node - * @param state the start state - * @param functions contain an object that maps node types to walker functions - * @param base provides the fallback walker functions for node types that aren't handled in the {@link functions} object. If not given, the default walkers will be used. - */ -export function recursive( - node: acorn.Node, - state: TState, - functions: RecursiveVisitors, - base?: RecursiveVisitors, -): void; - -/** - * does a 'full' walk over a tree, calling the {@link callback} with the arguments (node, state, type) for each node - * @param node - * @param callback - * @param base - * @param state - */ -export function full( - node: acorn.Node, - callback: FullWalkerCallback, - base?: RecursiveVisitors, - state?: TState, -): void; - -/** - * does a 'full' walk over a tree, building up an array of ancestor nodes (including the current node) and passing the array to the callbacks as a third parameter. - * @param node - * @param callback - * @param base - * @param state - */ -export function fullAncestor( - node: acorn.AnyNode, - callback: FullAncestorWalkerCallback, - base?: RecursiveVisitors, - state?: TState, -): void; - -/** - * builds a new walker object by using the walker functions in {@link functions} and filling in the missing ones by taking defaults from {@link base}. - * @param functions - * @param base - */ -export function make( - functions: RecursiveVisitors, - base?: RecursiveVisitors, -): RecursiveVisitors; - -/** - * tries to locate a node in a tree at the given start and/or end offsets, which satisfies the predicate test. {@link start} and {@link end} can be either `null` (as wildcard) or a `number`. {@link test} may be a string (indicating a node type) or a function that takes (nodeType, node) arguments and returns a boolean indicating whether this node is interesting. {@link base} and {@link state} are optional, and can be used to specify a custom walker. Nodes are tested from inner to outer, so if two nodes match the boundaries, the inner one will be preferred. - * @param node - * @param start - * @param end - * @param type - * @param base - * @param state - */ -export function findNodeAt( - node: acorn.AnyNode, - start: number | undefined, - end?: number | undefined, - type?: FindPredicate | string, - base?: RecursiveVisitors, - state?: TState, -): Found | undefined; - -/** - * like {@link findNodeAt}, but will match any node that exists 'around' (spanning) the given position. - * @param node - * @param start - * @param type - * @param base - * @param state - */ -export function findNodeAround( - node: acorn.AnyNode, - start: number | undefined, - type?: FindPredicate | string, - base?: RecursiveVisitors, - state?: TState, -): Found | undefined; - -/** - * similar to {@link findNodeAround}, but will match all nodes after the given position (testing outer nodes before inner nodes). - */ -export const findNodeAfter: typeof findNodeAround; - -export const base: RecursiveVisitors; diff --git a/packages/apps-engine/deno-runtime/acorn.d.ts b/packages/apps-engine/deno-runtime/acorn.d.ts deleted file mode 100644 index 7ba007d999b11..0000000000000 --- a/packages/apps-engine/deno-runtime/acorn.d.ts +++ /dev/null @@ -1,915 +0,0 @@ -export interface Node { - start?: number; - end?: number; - type: string; - range?: [number, number]; - loc?: SourceLocation | null; -} - -export interface SourceLocation { - source?: string | null; - start: Position; - end: Position; -} - -export interface Position { - /** 1-based */ - line: number; - /** 0-based */ - column: number; -} - -export interface Identifier extends Node { - type: 'Identifier'; - name: string; -} - -export interface Literal extends Node { - type: 'Literal'; - value?: string | boolean | null | number | RegExp | bigint; - raw?: string; - regex?: { - pattern: string; - flags: string; - }; - bigint?: string; -} - -export interface Program extends Node { - type: 'Program'; - body: Array; - sourceType: 'script' | 'module'; -} - -export interface Function extends Node { - id?: Identifier | null; - params: Array; - body: BlockStatement | Expression; - generator: boolean; - expression: boolean; - async: boolean; -} - -export interface ExpressionStatement extends Node { - type: 'ExpressionStatement'; - expression: Expression | Literal; - directive?: string; -} - -export interface BlockStatement extends Node { - type: 'BlockStatement'; - body: Array; -} - -export interface EmptyStatement extends Node { - type: 'EmptyStatement'; -} - -export interface DebuggerStatement extends Node { - type: 'DebuggerStatement'; -} - -export interface WithStatement extends Node { - type: 'WithStatement'; - object: Expression; - body: Statement; -} - -export interface ReturnStatement extends Node { - type: 'ReturnStatement'; - argument?: Expression | null; -} - -export interface LabeledStatement extends Node { - type: 'LabeledStatement'; - label: Identifier; - body: Statement; -} - -export interface BreakStatement extends Node { - type: 'BreakStatement'; - label?: Identifier | null; -} - -export interface ContinueStatement extends Node { - type: 'ContinueStatement'; - label?: Identifier | null; -} - -export interface IfStatement extends Node { - type: 'IfStatement'; - test: Expression; - consequent: Statement; - alternate?: Statement | null; -} - -export interface SwitchStatement extends Node { - type: 'SwitchStatement'; - discriminant: Expression; - cases: Array; -} - -export interface SwitchCase extends Node { - type: 'SwitchCase'; - test?: Expression | null; - consequent: Array; -} - -export interface ThrowStatement extends Node { - type: 'ThrowStatement'; - argument: Expression; -} - -export interface TryStatement extends Node { - type: 'TryStatement'; - block: BlockStatement; - handler?: CatchClause | null; - finalizer?: BlockStatement | null; -} - -export interface CatchClause extends Node { - type: 'CatchClause'; - param?: Pattern | null; - body: BlockStatement; -} - -export interface WhileStatement extends Node { - type: 'WhileStatement'; - test: Expression; - body: Statement; -} - -export interface DoWhileStatement extends Node { - type: 'DoWhileStatement'; - body: Statement; - test: Expression; -} - -export interface ForStatement extends Node { - type: 'ForStatement'; - init?: VariableDeclaration | Expression | null; - test?: Expression | null; - update?: Expression | null; - body: Statement; -} - -export interface ForInStatement extends Node { - type: 'ForInStatement'; - left: VariableDeclaration | Pattern; - right: Expression; - body: Statement; -} - -export interface FunctionDeclaration extends Function { - type: 'FunctionDeclaration'; - id: Identifier; - body: BlockStatement; -} - -export interface VariableDeclaration extends Node { - type: 'VariableDeclaration'; - declarations: Array; - kind: 'var' | 'let' | 'const'; -} - -export interface VariableDeclarator extends Node { - type: 'VariableDeclarator'; - id: Pattern; - init?: Expression | null; -} - -export interface ThisExpression extends Node { - type: 'ThisExpression'; -} - -export interface ArrayExpression extends Node { - type: 'ArrayExpression'; - elements: Array; -} - -export interface ObjectExpression extends Node { - type: 'ObjectExpression'; - properties: Array; -} - -export interface Property extends Node { - type: 'Property'; - key: Expression; - value: Expression; - kind: 'init' | 'get' | 'set'; - method: boolean; - shorthand: boolean; - computed: boolean; -} - -export interface FunctionExpression extends Function { - type: 'FunctionExpression'; - body: BlockStatement; -} - -export interface UnaryExpression extends Node { - type: 'UnaryExpression'; - operator: UnaryOperator; - prefix: boolean; - argument: Expression; -} - -export type UnaryOperator = '-' | '+' | '!' | '~' | 'typeof' | 'void' | 'delete'; - -export interface UpdateExpression extends Node { - type: 'UpdateExpression'; - operator: UpdateOperator; - argument: Expression; - prefix: boolean; -} - -export type UpdateOperator = '++' | '--'; - -export interface BinaryExpression extends Node { - type: 'BinaryExpression'; - operator: BinaryOperator; - left: Expression | PrivateIdentifier; - right: Expression; -} - -export type BinaryOperator = - | '==' - | '!=' - | '===' - | '!==' - | '<' - | '<=' - | '>' - | '>=' - | '<<' - | '>>' - | '>>>' - | '+' - | '-' - | '*' - | '/' - | '%' - | '|' - | '^' - | '&' - | 'in' - | 'instanceof' - | '**'; - -export interface AssignmentExpression extends Node { - type: 'AssignmentExpression'; - operator: AssignmentOperator; - left: Pattern; - right: Expression; -} - -export type AssignmentOperator = '=' | '+=' | '-=' | '*=' | '/=' | '%=' | '<<=' | '>>=' | '>>>=' | '|=' | '^=' | '&=' | '**=' | '||=' | '&&=' | '??='; - -export interface LogicalExpression extends Node { - type: 'LogicalExpression'; - operator: LogicalOperator; - left: Expression; - right: Expression; -} - -export type LogicalOperator = '||' | '&&' | '??'; - -export interface MemberExpression extends Node { - type: 'MemberExpression'; - object: Expression | Super; - property: Expression | PrivateIdentifier; - computed: boolean; - optional: boolean; -} - -export interface ConditionalExpression extends Node { - type: 'ConditionalExpression'; - test: Expression; - alternate: Expression; - consequent: Expression; -} - -export interface CallExpression extends Node { - type: 'CallExpression'; - callee: Expression | Super; - arguments: Array; - optional: boolean; -} - -export interface NewExpression extends Node { - type: 'NewExpression'; - callee: Expression; - arguments: Array; -} - -export interface SequenceExpression extends Node { - type: 'SequenceExpression'; - expressions: Array; -} - -export interface ForOfStatement extends Node { - type: 'ForOfStatement'; - left: VariableDeclaration | Pattern; - right: Expression; - body: Statement; - await: boolean; -} - -export interface Super extends Node { - type: 'Super'; -} - -export interface SpreadElement extends Node { - type: 'SpreadElement'; - argument: Expression; -} - -export interface ArrowFunctionExpression extends Function { - type: 'ArrowFunctionExpression'; -} - -export interface YieldExpression extends Node { - type: 'YieldExpression'; - argument?: Expression | null; - delegate: boolean; -} - -export interface TemplateLiteral extends Node { - type: 'TemplateLiteral'; - quasis: Array; - expressions: Array; -} - -export interface TaggedTemplateExpression extends Node { - type: 'TaggedTemplateExpression'; - tag: Expression; - quasi: TemplateLiteral; -} - -export interface TemplateElement extends Node { - type: 'TemplateElement'; - tail: boolean; - value: { - cooked?: string | null; - raw: string; - }; -} - -export interface AssignmentProperty extends Node { - type: 'Property'; - key: Expression; - value: Pattern; - kind: 'init'; - method: false; - shorthand: boolean; - computed: boolean; -} - -export interface ObjectPattern extends Node { - type: 'ObjectPattern'; - properties: Array; -} - -export interface ArrayPattern extends Node { - type: 'ArrayPattern'; - elements: Array; -} - -export interface RestElement extends Node { - type: 'RestElement'; - argument: Pattern; -} - -export interface AssignmentPattern extends Node { - type: 'AssignmentPattern'; - left: Pattern; - right: Expression; -} - -export interface Class extends Node { - id?: Identifier | null; - superClass?: Expression | null; - body: ClassBody; -} - -export interface ClassBody extends Node { - type: 'ClassBody'; - body: Array; -} - -export interface MethodDefinition extends Node { - type: 'MethodDefinition'; - key: Expression | PrivateIdentifier; - value: FunctionExpression; - kind: 'constructor' | 'method' | 'get' | 'set'; - computed: boolean; - static: boolean; -} - -export interface ClassDeclaration extends Class { - type: 'ClassDeclaration'; - id: Identifier; -} - -export interface ClassExpression extends Class { - type: 'ClassExpression'; -} - -export interface MetaProperty extends Node { - type: 'MetaProperty'; - meta: Identifier; - property: Identifier; -} - -export interface ImportDeclaration extends Node { - type: 'ImportDeclaration'; - specifiers: Array; - source: Literal; -} - -export interface ImportSpecifier extends Node { - type: 'ImportSpecifier'; - imported: Identifier | Literal; - local: Identifier; -} - -export interface ImportDefaultSpecifier extends Node { - type: 'ImportDefaultSpecifier'; - local: Identifier; -} - -export interface ImportNamespaceSpecifier extends Node { - type: 'ImportNamespaceSpecifier'; - local: Identifier; -} - -export interface ExportNamedDeclaration extends Node { - type: 'ExportNamedDeclaration'; - declaration?: Declaration | null; - specifiers: Array; - source?: Literal | null; -} - -export interface ExportSpecifier extends Node { - type: 'ExportSpecifier'; - exported: Identifier | Literal; - local: Identifier | Literal; -} - -export interface AnonymousFunctionDeclaration extends Function { - type: 'FunctionDeclaration'; - id: null; - body: BlockStatement; -} - -export interface AnonymousClassDeclaration extends Class { - type: 'ClassDeclaration'; - id: null; -} - -export interface ExportDefaultDeclaration extends Node { - type: 'ExportDefaultDeclaration'; - declaration: AnonymousFunctionDeclaration | FunctionDeclaration | AnonymousClassDeclaration | ClassDeclaration | Expression; -} - -export interface ExportAllDeclaration extends Node { - type: 'ExportAllDeclaration'; - source: Literal; - exported?: Identifier | Literal | null; -} - -export interface AwaitExpression extends Node { - type: 'AwaitExpression'; - argument: Expression; -} - -export interface ChainExpression extends Node { - type: 'ChainExpression'; - expression: MemberExpression | CallExpression; -} - -export interface ImportExpression extends Node { - type: 'ImportExpression'; - source: Expression; -} - -export interface ParenthesizedExpression extends Node { - type: 'ParenthesizedExpression'; - expression: Expression; -} - -export interface PropertyDefinition extends Node { - type: 'PropertyDefinition'; - key: Expression | PrivateIdentifier; - value?: Expression | null; - computed: boolean; - static: boolean; -} - -export interface PrivateIdentifier extends Node { - type: 'PrivateIdentifier'; - name: string; -} - -export interface StaticBlock extends Node { - type: 'StaticBlock'; - body: Array; -} - -export type Statement = - | ExpressionStatement - | BlockStatement - | EmptyStatement - | DebuggerStatement - | WithStatement - | ReturnStatement - | LabeledStatement - | BreakStatement - | ContinueStatement - | IfStatement - | SwitchStatement - | ThrowStatement - | TryStatement - | WhileStatement - | DoWhileStatement - | ForStatement - | ForInStatement - | ForOfStatement - | Declaration; - -export type Declaration = - | FunctionDeclaration - | VariableDeclaration - | ClassDeclaration; - -export type Expression = - | Identifier - | Literal - | ThisExpression - | ArrayExpression - | ObjectExpression - | FunctionExpression - | UnaryExpression - | UpdateExpression - | BinaryExpression - | AssignmentExpression - | LogicalExpression - | MemberExpression - | ConditionalExpression - | CallExpression - | NewExpression - | SequenceExpression - | ArrowFunctionExpression - | YieldExpression - | TemplateLiteral - | TaggedTemplateExpression - | ClassExpression - | MetaProperty - | AwaitExpression - | ChainExpression - | ImportExpression - | ParenthesizedExpression; - -export type Pattern = - | Identifier - | MemberExpression - | ObjectPattern - | ArrayPattern - | RestElement - | AssignmentPattern; - -export type ModuleDeclaration = - | ImportDeclaration - | ExportNamedDeclaration - | ExportDefaultDeclaration - | ExportAllDeclaration; - -export type AnyNode = - | Statement - | Expression - | Declaration - | ModuleDeclaration - | Literal - | Program - | SwitchCase - | CatchClause - | Property - | Super - | SpreadElement - | TemplateElement - | AssignmentProperty - | ObjectPattern - | ArrayPattern - | RestElement - | AssignmentPattern - | ClassBody - | MethodDefinition - | MetaProperty - | ImportSpecifier - | ImportDefaultSpecifier - | ImportNamespaceSpecifier - | ExportSpecifier - | AnonymousFunctionDeclaration - | AnonymousClassDeclaration - | PropertyDefinition - | PrivateIdentifier - | StaticBlock - | VariableDeclaration - | VariableDeclarator; - -export function parse(input: string, options: Options): Program; - -export function parseExpressionAt(input: string, pos: number, options: Options): Expression; - -export function tokenizer(input: string, options: Options): { - getToken(): Token; - [Symbol.iterator](): Iterator; -}; - -export type ecmaVersion = 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 'latest'; - -export interface Options { - /** - * `ecmaVersion` indicates the ECMAScript version to parse. Must be - * either 3, 5, 6 (or 2015), 7 (2016), 8 (2017), 9 (2018), 10 - * (2019), 11 (2020), 12 (2021), 13 (2022), 14 (2023), or `"latest"` - * (the latest version the library supports). This influences - * support for strict mode, the set of reserved words, and support - * for new syntax features. - */ - ecmaVersion: ecmaVersion; - - /** - * `sourceType` indicates the mode the code should be parsed in. - * Can be either `"script"` or `"module"`. This influences global - * strict mode and parsing of `import` and `export` declarations. - */ - sourceType?: 'script' | 'module'; - - /** - * a callback that will be called when a semicolon is automatically inserted. - * @param lastTokEnd the position of the comma as an offset - * @param lastTokEndLoc location if {@link locations} is enabled - */ - onInsertedSemicolon?: (lastTokEnd: number, lastTokEndLoc?: Position) => void; - - /** - * similar to `onInsertedSemicolon`, but for trailing commas - * @param lastTokEnd the position of the comma as an offset - * @param lastTokEndLoc location if `locations` is enabled - */ - onTrailingComma?: (lastTokEnd: number, lastTokEndLoc?: Position) => void; - - /** - * By default, reserved words are only enforced if ecmaVersion >= 5. - * Set `allowReserved` to a boolean value to explicitly turn this on - * an off. When this option has the value "never", reserved words - * and keywords can also not be used as property names. - */ - allowReserved?: boolean | 'never'; - - /** - * When enabled, a return at the top level is not considered an error. - */ - allowReturnOutsideFunction?: boolean; - - /** - * When enabled, import/export statements are not constrained to - * appearing at the top of the program, and an import.meta expression - * in a script isn't considered an error. - */ - allowImportExportEverywhere?: boolean; - - /** - * By default, `await` identifiers are allowed to appear at the top-level scope only if {@link ecmaVersion} >= 2022. - * When enabled, await identifiers are allowed to appear at the top-level scope, - * but they are still not allowed in non-async functions. - */ - allowAwaitOutsideFunction?: boolean; - - /** - * When enabled, super identifiers are not constrained to - * appearing in methods and do not raise an error when they appear elsewhere. - */ - allowSuperOutsideMethod?: boolean; - - /** - * When enabled, hashbang directive in the beginning of file is - * allowed and treated as a line comment. Enabled by default when - * {@link ecmaVersion} >= 2023. - */ - allowHashBang?: boolean; - - /** - * By default, the parser will verify that private properties are - * only used in places where they are valid and have been declared. - * Set this to false to turn such checks off. - */ - checkPrivateFields?: boolean; - - /** - * When `locations` is on, `loc` properties holding objects with - * `start` and `end` properties as {@link Position} objects will be attached to the - * nodes. - */ - locations?: boolean; - - /** - * a callback that will cause Acorn to call that export function with object in the same - * format as tokens returned from `tokenizer().getToken()`. Note - * that you are not allowed to call the parser from the - * callback—that will corrupt its internal state. - */ - onToken?: ((token: Token) => void) | Token[]; - - /** - * This takes a export function or an array. - * - * When a export function is passed, Acorn will call that export function with `(block, text, start, - * end)` parameters whenever a comment is skipped. `block` is a - * boolean indicating whether this is a block (`/* *\/`) comment, - * `text` is the content of the comment, and `start` and `end` are - * character offsets that denote the start and end of the comment. - * When the {@link locations} option is on, two more parameters are - * passed, the full locations of {@link Position} export type of the start and - * end of the comments. - * - * When a array is passed, each found comment of {@link Comment} export type is pushed to the array. - * - * Note that you are not allowed to call the - * parser from the callback—that will corrupt its internal state. - */ - onComment?: - | (( - isBlock: boolean, - text: string, - start: number, - end: number, - startLoc?: Position, - endLoc?: Position, - ) => void) - | Comment[]; - - /** - * Nodes have their start and end characters offsets recorded in - * `start` and `end` properties (directly on the node, rather than - * the `loc` object, which holds line/column data. To also add a - * [semi-standardized][range] `range` property holding a `[start, - * end]` array with the same numbers, set the `ranges` option to - * `true`. - */ - ranges?: boolean; - - /** - * It is possible to parse multiple files into a single AST by - * passing the tree produced by parsing the first file as - * `program` option in subsequent parses. This will add the - * toplevel forms of the parsed file to the `Program` (top) node - * of an existing parse tree. - */ - program?: Node; - - /** - * When {@link locations} is on, you can pass this to record the source - * file in every node's `loc` object. - */ - sourceFile?: string; - - /** - * This value, if given, is stored in every node, whether {@link locations} is on or off. - */ - directSourceFile?: string; - - /** - * When enabled, parenthesized expressions are represented by - * (non-standard) ParenthesizedExpression nodes - */ - preserveParens?: boolean; -} - -export class Parser { - options: Options; - input: string; - - constructor(options: Options, input: string, startPos?: number); - parse(): Program; - - static parse(input: string, options: Options): Program; - static parseExpressionAt(input: string, pos: number, options: Options): Expression; - static tokenizer(input: string, options: Options): { - getToken(): Token; - [Symbol.iterator](): Iterator; - }; - static extend(...plugins: ((BaseParser: typeof Parser) => typeof Parser)[]): typeof Parser; -} - -export const defaultOptions: Options; - -export function getLineInfo(input: string, offset: number): Position; - -export class TokenType { - label: string; - keyword: string | undefined; -} - -export const tokTypes: { - num: TokenType; - regexp: TokenType; - string: TokenType; - name: TokenType; - privateId: TokenType; - eof: TokenType; - - bracketL: TokenType; - bracketR: TokenType; - braceL: TokenType; - braceR: TokenType; - parenL: TokenType; - parenR: TokenType; - comma: TokenType; - semi: TokenType; - colon: TokenType; - dot: TokenType; - question: TokenType; - questionDot: TokenType; - arrow: TokenType; - template: TokenType; - invalidTemplate: TokenType; - ellipsis: TokenType; - backQuote: TokenType; - dollarBraceL: TokenType; - - eq: TokenType; - assign: TokenType; - incDec: TokenType; - prefix: TokenType; - logicalOR: TokenType; - logicalAND: TokenType; - bitwiseOR: TokenType; - bitwiseXOR: TokenType; - bitwiseAND: TokenType; - equality: TokenType; - relational: TokenType; - bitShift: TokenType; - plusMin: TokenType; - modulo: TokenType; - star: TokenType; - slash: TokenType; - starstar: TokenType; - coalesce: TokenType; - - _break: TokenType; - _case: TokenType; - _catch: TokenType; - _continue: TokenType; - _debugger: TokenType; - _default: TokenType; - _do: TokenType; - _else: TokenType; - _finally: TokenType; - _for: TokenType; - _function: TokenType; - _if: TokenType; - _return: TokenType; - _switch: TokenType; - _throw: TokenType; - _try: TokenType; - _var: TokenType; - _const: TokenType; - _while: TokenType; - _with: TokenType; - _new: TokenType; - _this: TokenType; - _super: TokenType; - _class: TokenType; - _extends: TokenType; - _export: TokenType; - _import: TokenType; - _null: TokenType; - _true: TokenType; - _false: TokenType; - _in: TokenType; - _instanceof: TokenType; - _typeof: TokenType; - _void: TokenType; - _delete: TokenType; -}; - -export interface Comment { - type: 'Line' | 'Block'; - value: string; - start: number; - end: number; - loc?: SourceLocation; - range?: [number, number]; -} - -export class Token { - type: TokenType; - start: number; - end: number; - loc?: SourceLocation; - range?: [number, number]; -} - -export const version: string; diff --git a/packages/apps-engine/deno-runtime/deno.jsonc b/packages/apps-engine/deno-runtime/deno.jsonc deleted file mode 100644 index 4fa3142b99261..0000000000000 --- a/packages/apps-engine/deno-runtime/deno.jsonc +++ /dev/null @@ -1,26 +0,0 @@ -{ - "imports": { - "@msgpack/msgpack": "npm:@msgpack/msgpack@3.0.0-beta2", - "@rocket.chat/apps-engine/": "./../src/", - "@rocket.chat/ui-kit": "npm:@rocket.chat/ui-kit@^0.31.22", - "@std/cli": "jsr:@std/cli@^1.0.9", - "@std/io": "jsr:@std/io@^0.225.3", - "@std/streams": "jsr:@std/streams@^1.0.16", - "acorn": "npm:acorn@8.10.0", - "acorn-walk": "npm:acorn-walk@8.2.0", - "astring": "npm:astring@1.8.6", - "jsonrpc-lite": "npm:jsonrpc-lite@2.2.0", - "stack-trace": "npm:stack-trace@0.0.10", - "uuid": "npm:uuid@8.3.2" - }, - "unstable": ["detect-cjs"], - "tasks": { - "test": "deno test --no-check --allow-read=../../../,/tmp --allow-write=/tmp" - }, - "fmt": { - "lineWidth": 160, - "useTabs": true, - "indentWidth": 4, - "singleQuote": true - } -} diff --git a/packages/apps-engine/deno-runtime/deno.lock b/packages/apps-engine/deno-runtime/deno.lock deleted file mode 100644 index bc87cb5e87d84..0000000000000 --- a/packages/apps-engine/deno-runtime/deno.lock +++ /dev/null @@ -1,128 +0,0 @@ -{ - "version": "5", - "specifiers": { - "jsr:@std/bytes@^1.0.6": "1.0.6", - "jsr:@std/cli@^1.0.9": "1.0.13", - "jsr:@std/io@~0.225.3": "0.225.3", - "jsr:@std/streams@^1.0.16": "1.0.16", - "npm:@msgpack/msgpack@3.0.0-beta2": "3.0.0-beta2", - "npm:@rocket.chat/ui-kit@~0.31.22": "0.31.25_@rocket.chat+icons@0.32.0", - "npm:acorn-walk@8.2.0": "8.2.0", - "npm:acorn@8.10.0": "8.10.0", - "npm:astring@1.8.6": "1.8.6", - "npm:jsonrpc-lite@2.2.0": "2.2.0", - "npm:stack-trace@*": "0.0.10", - "npm:stack-trace@0.0.10": "0.0.10", - "npm:uuid@8.3.2": "8.3.2" - }, - "jsr": { - "@std/bytes@1.0.6": { - "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" - }, - "@std/cli@1.0.13": { - "integrity": "5db2d95ab2dca3bca9fb6ad3c19908c314e93d6391c8b026725e4892d4615a69" - }, - "@std/io@0.225.3": { - "integrity": "27b07b591384d12d7b568f39e61dff966b8230559122df1e9fd11cc068f7ddd1", - "dependencies": [ - "jsr:@std/bytes" - ] - }, - "@std/streams@1.0.16": { - "integrity": "85030627befb1767c60d4f65cb30fa2f94af1d6ee6e5b2515b76157a542e89c4", - "dependencies": [ - "jsr:@std/bytes" - ] - } - }, - "npm": { - "@msgpack/msgpack@3.0.0-beta2": { - "integrity": "sha512-y+l1PNV0XDyY8sM3YtuMLK5vE3/hkfId+Do8pLo/OPxfxuFAUwcGz3oiiUuV46/aBpwTzZ+mRWVMtlSKbradhw==" - }, - "@rocket.chat/icons@0.32.0": { - "integrity": "sha512-7yhhELKNLb9kUtXCvau0V+iMXraV2bOsxcPjc/ZtLR5VeeIDTeaflqRWGtLroX6f3bE+J1n5qB5zi8A4YXuH2g==" - }, - "@rocket.chat/ui-kit@0.31.25_@rocket.chat+icons@0.32.0": { - "integrity": "sha512-yTgTKDw9SMlJ6p8n0PDO6zSvox/nHYUrwCIvILQeAK6PvTrgSe/u9CvU7ATTYjnQiQ603yEGR6dxjF4euCGdNA==", - "dependencies": [ - "@rocket.chat/icons" - ] - }, - "acorn-walk@8.2.0": { - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" - }, - "acorn@8.10.0": { - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "bin": true - }, - "astring@1.8.6": { - "integrity": "sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==", - "bin": true - }, - "jsonrpc-lite@2.2.0": { - "integrity": "sha512-/cbbSxtZWs1O7R4tWqabrCM/t3N8qKUZMAg9IUqpPvUs6UyRvm6pCNYkskyKN/XU0UgffW+NY2ZRr8t0AknX7g==" - }, - "stack-trace@0.0.10": { - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==" - }, - "uuid@8.3.2": { - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": true - } - }, - "remote": { - "https://deno.land/std@0.203.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9", - "https://deno.land/std@0.203.0/assert/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea", - "https://deno.land/std@0.203.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", - "https://deno.land/std@0.203.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", - "https://deno.land/std@0.203.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c", - "https://deno.land/std@0.203.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9", - "https://deno.land/std@0.203.0/assert/assert_equals.ts": "d8ec8a22447fbaf2fc9d7c3ed2e66790fdb74beae3e482855d75782218d68227", - "https://deno.land/std@0.203.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7", - "https://deno.land/std@0.203.0/assert/assert_false.ts": "0ccbcaae910f52c857192ff16ea08bda40fdc79de80846c206bfc061e8c851c6", - "https://deno.land/std@0.203.0/assert/assert_greater.ts": "ae2158a2d19313bf675bf7251d31c6dc52973edb12ac64ac8fc7064152af3e63", - "https://deno.land/std@0.203.0/assert/assert_greater_or_equal.ts": "1439da5ebbe20855446cac50097ac78b9742abe8e9a43e7de1ce1426d556e89c", - "https://deno.land/std@0.203.0/assert/assert_instance_of.ts": "3aedb3d8186e120812d2b3a5dea66a6e42bf8c57a8bd927645770bd21eea554c", - "https://deno.land/std@0.203.0/assert/assert_is_error.ts": "c21113094a51a296ffaf036767d616a78a2ae5f9f7bbd464cd0197476498b94b", - "https://deno.land/std@0.203.0/assert/assert_less.ts": "aec695db57db42ec3e2b62e97e1e93db0063f5a6ec133326cc290ff4b71b47e4", - "https://deno.land/std@0.203.0/assert/assert_less_or_equal.ts": "5fa8b6a3ffa20fd0a05032fe7257bf985d207b85685fdbcd23651b70f928c848", - "https://deno.land/std@0.203.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b", - "https://deno.land/std@0.203.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754", - "https://deno.land/std@0.203.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22", - "https://deno.land/std@0.203.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0", - "https://deno.land/std@0.203.0/assert/assert_not_strict_equals.ts": "ca6c6d645e95fbc873d25320efeb8c4c6089a9a5e09f92d7c1c4b6e935c2a6ad", - "https://deno.land/std@0.203.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54", - "https://deno.land/std@0.203.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057", - "https://deno.land/std@0.203.0/assert/assert_strict_equals.ts": "b1f538a7ea5f8348aeca261d4f9ca603127c665e0f2bbfeb91fa272787c87265", - "https://deno.land/std@0.203.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c", - "https://deno.land/std@0.203.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd", - "https://deno.land/std@0.203.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", - "https://deno.land/std@0.203.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece", - "https://deno.land/std@0.203.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278", - "https://deno.land/std@0.203.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085", - "https://deno.land/std@0.203.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a", - "https://deno.land/std@0.203.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536", - "https://deno.land/std@0.203.0/fmt/colors.ts": "c51c4642678eb690dcf5ffee5918b675bf01a33fba82acf303701ae1a4f8c8d9", - "https://deno.land/std@0.203.0/testing/_test_suite.ts": "30f018feeb3835f12ab198d8a518f9089b1bcb2e8c838a8b615ab10d5005465c", - "https://deno.land/std@0.203.0/testing/bdd.ts": "3f446df5ef8e856a869e8eec54c8482590415741ff0b6358a00c43486cc15769", - "https://deno.land/std@0.203.0/testing/mock.ts": "6576b4aa55ee20b1990d656a78fff83599e190948c00e9f25a7f3ac5e9d6492d", - "https://deno.land/std@0.216.0/io/types.ts": "748bbb3ac96abda03594ef5a0db15ce5450dcc6c0d841c8906f8b10ac8d32c96", - "https://deno.land/std@0.216.0/io/write_all.ts": "24aac2312bb21096ae3ae0b102b22c26164d3249dff96dbac130958aa736f038", - "https://jsr.io/@std/cli/1.0.9/parse_args.ts": "29ac18602d8836d2723cab1d90111ff954acc369f184626a3f9f677e3185caef" - }, - "workspace": { - "dependencies": [ - "jsr:@std/cli@^1.0.9", - "jsr:@std/io@~0.225.3", - "jsr:@std/streams@^1.0.16", - "npm:@msgpack/msgpack@3.0.0-beta2", - "npm:@rocket.chat/ui-kit@~0.31.22", - "npm:acorn-walk@8.2.0", - "npm:acorn@8.10.0", - "npm:astring@1.8.6", - "npm:jsonrpc-lite@2.2.0", - "npm:stack-trace@0.0.10", - "npm:uuid@8.3.2" - ] - } -} diff --git a/packages/apps-engine/deno-runtime/error-handlers.ts b/packages/apps-engine/deno-runtime/error-handlers.ts deleted file mode 100644 index e26a5ad6b2d86..0000000000000 --- a/packages/apps-engine/deno-runtime/error-handlers.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as Messenger from './lib/messenger.ts'; - -export function unhandledRejectionListener(event: PromiseRejectionEvent) { - event.preventDefault(); - - const { type, reason } = event; - - Messenger.sendNotification({ - method: 'unhandledRejection', - params: [ - { - type, - reason: reason instanceof Error ? reason.message : reason, - timestamp: new Date(), - }, - ], - }); -} - -export function unhandledExceptionListener(event: ErrorEvent) { - event.preventDefault(); - - const { type, message, filename, lineno, colno } = event; - Messenger.sendNotification({ - method: 'uncaughtException', - params: [{ type, message, filename, lineno, colno }], - }); -} - -export default function registerErrorListeners() { - addEventListener('unhandledrejection', unhandledRejectionListener); - addEventListener('error', unhandledExceptionListener); -} diff --git a/packages/apps-engine/deno-runtime/handlers/api-handler.ts b/packages/apps-engine/deno-runtime/handlers/api-handler.ts deleted file mode 100644 index 1b88a92551ea7..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/api-handler.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api/IApiEndpoint.ts'; -import { Defined, JsonRpcError } from 'jsonrpc-lite'; - -import { AppObjectRegistry } from '../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; -import { RequestContext } from '../lib/requestContext.ts'; -import { wrapComposedApp } from '../lib/wrapAppForRequest.ts'; - -export default async function apiHandler(request: RequestContext): Promise { - const { method: call, params } = request; - const [/* always "api" */, ...parts] = call.split(':'); - const httpMethod = parts.pop(); - const path = parts.join(':'); - - const endpoint = AppObjectRegistry.get(`api:${path}`); - const { logger } = request.context; - - if (!endpoint) { - return new JsonRpcError(`Endpoint ${path} not found`, -32000); - } - - const method = endpoint[httpMethod as keyof IApiEndpoint]; - - if (typeof method !== 'function') { - return new JsonRpcError(`${path}'s ${httpMethod} not exists`, -32000); - } - - const [requestData, endpointInfo] = params as Array; - - logger.debug(`${path}'s ${call} is being executed...`, requestData); - - try { - // deno-lint-ignore ban-types - const result = await (method as Function).apply(wrapComposedApp(endpoint, request), [ - requestData, - endpointInfo, - AppAccessorsInstance.getReader(), - AppAccessorsInstance.getModifier(), - AppAccessorsInstance.getHttp(), - AppAccessorsInstance.getPersistence(), - ]); - - logger.debug(`${path}'s ${call} was successfully executed.`); - - return result; - } catch (e) { - logger.debug(`${path}'s ${call} was unsuccessful.`); - return new JsonRpcError(e.message || 'Internal server error', -32000); - } -} diff --git a/packages/apps-engine/deno-runtime/handlers/app/construct.ts b/packages/apps-engine/deno-runtime/handlers/app/construct.ts deleted file mode 100644 index b391088fee217..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/app/construct.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Socket } from 'node:net'; - -import type { IParseAppPackageResult } from '@rocket.chat/apps-engine/server/compiler/IParseAppPackageResult.ts'; - -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { require } from '../../lib/require.ts'; -import { sanitizeDeprecatedUsage } from '../../lib/sanitizeDeprecatedUsage.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; - -const ALLOWED_NATIVE_MODULES = ['path', 'url', 'crypto', 'buffer', 'stream', 'net', 'http', 'https', 'zlib', 'util', 'punycode', 'os', 'querystring', 'fs']; -const ALLOWED_EXTERNAL_MODULES = ['uuid']; - -function prepareEnvironment() { - // Deno does not behave equally to Node when it comes to piping content to a socket - // So we intervene here - const originalFinal = Socket.prototype._final; - // deno-lint-ignore no-explicit-any - Socket.prototype._final = function _final(cb: any) { - // Deno closes the readable stream in the Socket earlier than Node - // The exact reason for that is yet unknown, so we'll need to simply delay the execution - // which allows data to be read in a response - setTimeout(() => originalFinal.call(this, cb), 1); - }; -} - -// As the apps are bundled, the only times they will call require are -// 1. To require native modules -// 2. To require external npm packages we may provide -// 3. To require apps-engine files -function buildRequire(): (module: string) => unknown { - return (module: string): unknown => { - // Normalize Node built-in specifiers: accept both 'crypto' and 'node:crypto' - const normalized = module.replace('node:', ''); - - if (ALLOWED_NATIVE_MODULES.includes(normalized)) { - return require(`node:${normalized}`); - } - - if (ALLOWED_EXTERNAL_MODULES.includes(module)) { - return require(`npm:${module}`); - } - - if (module.startsWith('@rocket.chat/apps-engine')) { - // Our `require` function knows how to handle these - return require(module); - } - - throw new Error(`Module ${module} is not allowed`); - }; -} - -function wrapAppCode(code: string): (require: (module: string) => unknown) => Promise> { - return new Function( - 'require', - ` - const { Buffer } = require('buffer'); - const exports = {}; - const module = { exports }; - const _error = console.error.bind(console); - const _console = { - log: _error, - error: _error, - debug: _error, - info: _error, - warn: _error, - }; - - const result = (async (exports,module,require,Buffer,console,globalThis,Deno) => { - ${code}; - })(exports,module,require,Buffer,_console,undefined,undefined); - - return result.then(() => module.exports);`, - ) as (require: (module: string) => unknown) => Promise>; -} - -export default async function handleConstructApp(request: RequestContext): Promise { - const { params } = request; - - if (!Array.isArray(params)) { - throw new Error('Invalid params', { cause: 'invalid_param_type' }); - } - - const [appPackage] = params as [IParseAppPackageResult]; - - if (!appPackage?.info?.id || !appPackage?.info?.classFile || !appPackage?.files) { - throw new Error('Invalid params', { cause: 'invalid_param_type' }); - } - - prepareEnvironment(); - - AppObjectRegistry.set('id', appPackage.info.id); - const source = sanitizeDeprecatedUsage(appPackage.files[appPackage.info.classFile]); - - const require = buildRequire(); - const exports = await wrapAppCode(source)(require); - - // This is the same naive logic we've been using in the App Compiler - // Applying the correct type here is quite difficult because of the dynamic nature of the code - // deno-lint-ignore no-explicit-any - const appClass = Object.values(exports)[0] as any; - - const app = new appClass(appPackage.info, request.context.logger, AppAccessorsInstance.getDefaultAppAccessors()); - - if (typeof app.getName !== 'function') { - throw new Error('App must contain a getName function'); - } - - if (typeof app.getNameSlug !== 'function') { - throw new Error('App must contain a getNameSlug function'); - } - - if (typeof app.getVersion !== 'function') { - throw new Error('App must contain a getVersion function'); - } - - if (typeof app.getID !== 'function') { - throw new Error('App must contain a getID function'); - } - - if (typeof app.getDescription !== 'function') { - throw new Error('App must contain a getDescription function'); - } - - if (typeof app.getRequiredApiVersion !== 'function') { - throw new Error('App must contain a getRequiredApiVersion function'); - } - - AppObjectRegistry.set('app', app); - - return true; -} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleGetStatus.ts b/packages/apps-engine/deno-runtime/handlers/app/handleGetStatus.ts deleted file mode 100644 index 8bd454b98ca7f..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/app/handleGetStatus.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; - -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; - -export default function handleGetStatus(): Promise { - const app = AppObjectRegistry.get('app'); - - if (typeof app?.getStatus !== 'function') { - throw new Error('App must contain a getStatus function', { - cause: 'invalid_app', - }); - } - - return app.getStatus(); -} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleInitialize.ts b/packages/apps-engine/deno-runtime/handlers/app/handleInitialize.ts deleted file mode 100644 index e8ee4ed1de136..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/app/handleInitialize.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; - -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; - -export default async function handleInitialize(request: RequestContext): Promise { - const app = AppObjectRegistry.get('app'); - - if (typeof app?.initialize !== 'function') { - throw new Error('App must contain an initialize function', { - cause: 'invalid_app', - }); - } - - await app.initialize.call( - wrapAppForRequest(app, request), - AppAccessorsInstance.getConfigurationExtend(), - AppAccessorsInstance.getEnvironmentRead() - ); - - return true; -} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnDisable.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnDisable.ts deleted file mode 100644 index ffac456cd9bc9..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/app/handleOnDisable.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; - -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; - -export default async function handleOnDisable(request: RequestContext): Promise { - const app = AppObjectRegistry.get('app'); - - if (typeof app?.onDisable !== 'function') { - throw new Error('App must contain an onDisable function', { - cause: 'invalid_app', - }); - } - - await app.onDisable.call(wrapAppForRequest(app, request), AppAccessorsInstance.getConfigurationModify()); - - return true; -} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnEnable.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnEnable.ts deleted file mode 100644 index 34c1d49b0f367..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/app/handleOnEnable.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; - -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; - -export default function handleOnEnable(request: RequestContext): Promise { - const app = AppObjectRegistry.get('app'); - - if (typeof app?.onEnable !== 'function') { - throw new Error('App must contain an onEnable function', { - cause: 'invalid_app', - }); - } - - return app.onEnable.call( - wrapAppForRequest(app, request), - AppAccessorsInstance.getEnvironmentRead(), - AppAccessorsInstance.getConfigurationModify() - ); -} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnInstall.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnInstall.ts deleted file mode 100644 index d6e4ada5cf6f0..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/app/handleOnInstall.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; - -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; - -export default async function handleOnInstall(request: RequestContext): Promise { - const { params } = request; - const app = AppObjectRegistry.get('app'); - - if (typeof app?.onInstall !== 'function') { - throw new Error('App must contain an onInstall function', { - cause: 'invalid_app', - }); - } - - if (!Array.isArray(params)) { - throw new Error('Invalid params', { cause: 'invalid_param_type' }); - } - - const [context] = params as [Record]; - - await app.onInstall.call( - wrapAppForRequest(app, request), - context, - AppAccessorsInstance.getReader(), - AppAccessorsInstance.getHttp(), - AppAccessorsInstance.getPersistence(), - AppAccessorsInstance.getModifier(), - ); - - return true; -} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts deleted file mode 100644 index 601e1429be025..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; - -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; - -export default function handleOnPreSettingUpdate(request: RequestContext): Promise { - const { params } = request; - const app = AppObjectRegistry.get('app'); - - if (typeof app?.onPreSettingUpdate !== 'function') { - throw new Error('App must contain an onPreSettingUpdate function', { - cause: 'invalid_app', - }); - } - - if (!Array.isArray(params)) { - throw new Error('Invalid params', { cause: 'invalid_param_type' }); - } - - const [setting] = params as [Record]; - - return app.onPreSettingUpdate.call( - wrapAppForRequest(app, request), - setting, - AppAccessorsInstance.getConfigurationModify(), - AppAccessorsInstance.getReader(), - AppAccessorsInstance.getHttp(), - ); -} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnSettingUpdated.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnSettingUpdated.ts deleted file mode 100644 index e78ece63dda92..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/app/handleOnSettingUpdated.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; - -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; - -export default async function handleOnSettingUpdated(request: RequestContext): Promise { - const { params } = request; - const app = AppObjectRegistry.get('app'); - - if (typeof app?.onSettingUpdated !== 'function') { - throw new Error('App must contain an onSettingUpdated function', { - cause: 'invalid_app', - }); - } - - if (!Array.isArray(params)) { - throw new Error('Invalid params', { cause: 'invalid_param_type' }); - } - - const [setting] = params as [Record]; - - await app.onSettingUpdated.call( - wrapAppForRequest(app, request), - setting, - AppAccessorsInstance.getConfigurationModify(), - AppAccessorsInstance.getReader(), - AppAccessorsInstance.getHttp(), - ); - - return true; -} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnUninstall.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnUninstall.ts deleted file mode 100644 index 34b02c2b45f1f..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/app/handleOnUninstall.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; - -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; - -export default async function handleOnUninstall(request: RequestContext): Promise { - const { params } = request; - const app = AppObjectRegistry.get('app'); - - if (typeof app?.onUninstall !== 'function') { - throw new Error('App must contain an onUninstall function', { - cause: 'invalid_app', - }); - } - - if (!Array.isArray(params)) { - throw new Error('Invalid params', { cause: 'invalid_param_type' }); - } - - const [context] = params as [Record]; - - await app.onUninstall.call( - wrapAppForRequest(app, request), - context, - AppAccessorsInstance.getReader(), - AppAccessorsInstance.getHttp(), - AppAccessorsInstance.getPersistence(), - AppAccessorsInstance.getModifier(), - ); - - return true; -} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnUpdate.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnUpdate.ts deleted file mode 100644 index 0eb8643c928f4..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/app/handleOnUpdate.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; - -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; - -export default async function handleOnUpdate(request: RequestContext): Promise { - const { params } = request; - const app = AppObjectRegistry.get('app'); - - if (typeof app?.onUpdate !== 'function') { - throw new Error('App must contain an onUpdate function', { - cause: 'invalid_app', - }); - } - - if (!Array.isArray(params)) { - throw new Error('Invalid params', { cause: 'invalid_param_type' }); - } - - const [context] = params as [Record]; - - await app.onUpdate.call( - wrapAppForRequest(app, request), - context, - AppAccessorsInstance.getReader(), - AppAccessorsInstance.getHttp(), - AppAccessorsInstance.getPersistence(), - AppAccessorsInstance.getModifier(), - ); - - return true; -} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleSetStatus.ts b/packages/apps-engine/deno-runtime/handlers/app/handleSetStatus.ts deleted file mode 100644 index 163fa3684ae67..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/app/handleSetStatus.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; -import type { AppStatus as _AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus.ts'; - -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { require } from '../../lib/require.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; - -const { AppStatus } = require('@rocket.chat/apps-engine/definition/AppStatus.js') as { - AppStatus: typeof _AppStatus; -}; - -export default async function handleSetStatus(request: RequestContext): Promise { - const { params } = request; - - if (!Array.isArray(params) || !Object.values(AppStatus).includes(params[0])) { - throw new Error('Invalid params', { cause: 'invalid_param_type' }); - } - - const [status] = params as [typeof AppStatus]; - - const app = AppObjectRegistry.get('app'); - - if (!app || typeof app['setStatus'] !== 'function') { - throw new Error('App must contain a setStatus function', { - cause: 'invalid_app', - }); - } - - await app['setStatus'].call(wrapAppForRequest(app, request), status); - - return null; -} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleUploadEvents.ts b/packages/apps-engine/deno-runtime/handlers/app/handleUploadEvents.ts deleted file mode 100644 index 72d58801d537c..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/app/handleUploadEvents.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Buffer } from 'node:buffer'; - -import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; -import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions/AppsEngineException.ts'; -import type { IFileUploadContext } from '@rocket.chat/apps-engine/definition/uploads/IFileUploadContext.ts' -import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails.ts' -import { toArrayBuffer } from '@std/streams'; -import { Defined, JsonRpcError } from 'jsonrpc-lite'; - -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { assertAppAvailable, assertHandlerFunction, isPlainObject } from '../lib/assertions.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; - -export const uploadEvents = ['executePreFileUpload'] as const; - -function assertIsUpload(v: unknown): asserts v is IUploadDetails { - if (isPlainObject(v) && !!v.rid && (!!v.userId || !!v.visitorToken)) return; - - throw JsonRpcError.invalidParams({ err: `Invalid 'file' parameter. Expected IUploadDetails, got`, value: v }); -} - -function assertString(v: unknown): asserts v is string { - if (v && typeof v === 'string') return; - - throw JsonRpcError.invalidParams({ err: `Invalid 'path' parameter. Expected string, got`, value: v }); -} - -export default async function handleUploadEvents(request: RequestContext): Promise { - const { method: rawMethod, params } = request as { method: `app:${typeof uploadEvents[number]}`; params: [{ file?: IUploadDetails, path?: string }]}; - const [, method] = rawMethod.split(':') as ['app', typeof uploadEvents[number]]; - - try { - const [{ file, path }] = params; - - const app = AppObjectRegistry.get('app'); - const handlerFunction = app?.[method as keyof App] as unknown; - - assertAppAvailable(app); - assertHandlerFunction(handlerFunction); - assertIsUpload(file); - assertString(path); - - using tempFile = await Deno.open(path, { read: true, create: false }); - let context: IFileUploadContext; - - switch (method) { - case 'executePreFileUpload': { - const fileContents = await toArrayBuffer(tempFile.readable); - context = { file, content: Buffer.from(fileContents) }; - break; - } - } - - return await handlerFunction.call( - wrapAppForRequest(app, request), - context, - AppAccessorsInstance.getReader(), - AppAccessorsInstance.getHttp(), - AppAccessorsInstance.getPersistence(), - AppAccessorsInstance.getModifier(), - ); - } catch(e) { - if (e?.name === AppsEngineException.name) { - return new JsonRpcError(e.message, AppsEngineException.JSONRPC_ERROR_CODE, { name: e.name }); - } - - if (e instanceof JsonRpcError) { - return e; - } - - return JsonRpcError.internalError({ - err: e.message, - ...(e.code && { code: e.code }), - }); - } -} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handler.ts b/packages/apps-engine/deno-runtime/handlers/app/handler.ts deleted file mode 100644 index e0e8085813347..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/app/handler.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Defined, JsonRpcError } from 'jsonrpc-lite'; - -import handleConstructApp from './construct.ts'; -import handleInitialize from './handleInitialize.ts'; -import handleGetStatus from './handleGetStatus.ts'; -import handleSetStatus from './handleSetStatus.ts'; -import handleOnEnable from './handleOnEnable.ts'; -import handleOnInstall from './handleOnInstall.ts'; -import handleOnDisable from './handleOnDisable.ts'; -import handleOnUninstall from './handleOnUninstall.ts'; -import handleOnPreSettingUpdate from './handleOnPreSettingUpdate.ts'; -import handleOnSettingUpdated from './handleOnSettingUpdated.ts'; -import handleOnUpdate from './handleOnUpdate.ts'; -import handleUploadEvents, { uploadEvents } from './handleUploadEvents.ts'; -import { isOneOf } from '../lib/assertions.ts'; -import handleListener from '../listener/handler.ts'; -import handleUIKitInteraction, { uikitInteractions } from '../uikit/handler.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; - -export default async function handleApp(request: RequestContext): Promise { - const { method } = request; - const { logger } = request.context; - const [, appMethod] = method.split(':'); - - try { - // We don't want the getStatus method to generate logs, so we handle it separately - if (appMethod === 'getStatus') { - return await handleGetStatus(); - } - - logger.debug({ msg: `A method is being called...`, appMethod }); - - const formatResult = (result: Defined | JsonRpcError): Defined | JsonRpcError => { - if (result instanceof JsonRpcError) { - logger.debug({ - msg: `'${appMethod}' was unsuccessful.`, - appMethod, - err: result, - errorMessage: result.message, - }); - } else { - logger.debug({ - msg: `'${appMethod}' was successfully called! The result is:`, - appMethod, - result, - }); - } - - return result; - }; - - let result: Promise | undefined = undefined; - - if (isOneOf(appMethod, uploadEvents)) { - result = handleUploadEvents(request); - } else if (isOneOf(appMethod, uikitInteractions)) { - result = handleUIKitInteraction(request); - } else if (appMethod.startsWith('check') || appMethod.startsWith('execute')) { - result = handleListener(request); - } - - switch (appMethod) { - case 'construct': - result = handleConstructApp(request); - break; - case 'initialize': - result = handleInitialize(request); - break; - case 'setStatus': - result = handleSetStatus(request); - break; - case 'onEnable': - result = handleOnEnable(request); - break; - case 'onDisable': - result = handleOnDisable(request); - break; - case 'onInstall': - result = handleOnInstall(request); - break; - case 'onUninstall': - result = handleOnUninstall(request); - break; - case 'onPreSettingUpdate': - result = handleOnPreSettingUpdate(request); - break; - case 'onSettingUpdated': - result = handleOnSettingUpdated(request); - break; - case 'onUpdate': - result = handleOnUpdate(request); - break; - } - - if (typeof result === 'undefined') { - throw new JsonRpcError(`Unknown method "${appMethod}"`, -32601); - } - - return await result.then(formatResult); - } catch (e: unknown) { - if (!(e instanceof Error)) { - return new JsonRpcError('Unknown error', -32000, e); - } - - if ((e.cause as string)?.includes('invalid_param_type')) { - return JsonRpcError.invalidParams(null); - } - - if ((e.cause as string)?.includes('invalid_app')) { - return JsonRpcError.internalError({ message: 'App unavailable' }); - } - - return new JsonRpcError(e.message, -32000, e); - } -} diff --git a/packages/apps-engine/deno-runtime/handlers/lib/assertions.ts b/packages/apps-engine/deno-runtime/handlers/lib/assertions.ts deleted file mode 100644 index d6dcd0a4c9651..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/lib/assertions.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; -import { JsonRpcError } from 'jsonrpc-lite'; - -/** - * Known failures that can happen in the runtime. - * - * DRT = Deno RunTime - */ -export const Errors = { - DRT_APP_NOT_AVAILABLE: 'DRT_APP_NOT_AVAILABLE', - DRT_EVENT_HANDLER_FUNCTION_MISSING: 'DRT_EVENT_HANDLER_FUNCTION_MISSING', -} - -export function isRecord(v: unknown): v is Record { - return !!v && typeof v === 'object' && !Array.isArray(v); -} - -export function isPlainObject(v: unknown): v is Record { - if (!isRecord(v)) { - return false; - } - - const prototype = Object.getPrototypeOf(v); - - return prototype === null || prototype.constructor === Object; -} - -/** - * Type guard function to check if a value is included in a readonly array - * and narrow its type accordingly. - */ -export function isOneOf(value: unknown, array: readonly T[]): value is T { - return array.includes(value as T); -} - -export function isApp(v: unknown): v is App { - return !!v && typeof (v as App)['extendConfiguration'] === 'function'; -} - -export function assertAppAvailable(v: unknown): asserts v is App { - if (isApp(v)) return; - - throw JsonRpcError.internalError({ err: 'App object not available', code: Errors.DRT_APP_NOT_AVAILABLE }); -} - -// deno-lint-ignore ban-types -- Function is the best we can do at this time -export function assertHandlerFunction(v: unknown): asserts v is Function { - if (v instanceof Function) return; - - throw JsonRpcError.internalError({ err: `Expected handler function, got ${v}`, code: Errors.DRT_EVENT_HANDLER_FUNCTION_MISSING }); -} diff --git a/packages/apps-engine/deno-runtime/handlers/listener/handler.ts b/packages/apps-engine/deno-runtime/handlers/listener/handler.ts deleted file mode 100644 index 88bc09b81403d..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/listener/handler.ts +++ /dev/null @@ -1,153 +0,0 @@ -import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; -import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; -import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; -import type { AppsEngineException as _AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions/AppsEngineException.ts'; -import { Defined, JsonRpcError } from 'jsonrpc-lite'; - -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { MessageExtender } from '../../lib/accessors/extenders/MessageExtender.ts'; -import { RoomExtender } from '../../lib/accessors/extenders/RoomExtender.ts'; -import { MessageBuilder } from '../../lib/accessors/builders/MessageBuilder.ts'; -import { RoomBuilder } from '../../lib/accessors/builders/RoomBuilder.ts'; -import { AppAccessors, AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { require } from '../../lib/require.ts'; -import createRoom from '../../lib/roomFactory.ts'; -import { Room } from '../../lib/room.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; - -const { AppsEngineException } = require('@rocket.chat/apps-engine/definition/exceptions/AppsEngineException.js') as { - AppsEngineException: typeof _AppsEngineException; -}; - -export default async function handleListener(request: RequestContext): Promise { - const { method, params } = request; - const [, evtInterface] = method.split(':'); - const app = AppObjectRegistry.get('app'); - - const eventExecutor = app?.[evtInterface as keyof App]; - - if (!app || typeof eventExecutor !== 'function') { - return JsonRpcError.methodNotFound({ - message: 'Invalid event interface called on app', - }); - } - - if (!Array.isArray(params) || params.length < 1 || params.length > 2) { - return JsonRpcError.invalidParams(null); - } - - try { - const args = parseArgs({ AppAccessorsInstance }, evtInterface, params); - return await (eventExecutor as (...args: unknown[]) => Promise).apply(wrapAppForRequest(app, request), args); - } catch (e) { - if (e instanceof JsonRpcError) { - return e; - } - - if (e instanceof AppsEngineException) { - return new JsonRpcError(e.message, AppsEngineException.JSONRPC_ERROR_CODE, { name: e.name }); - } - - return JsonRpcError.internalError({ message: e.message }); - } -} - -export function parseArgs(deps: { AppAccessorsInstance: AppAccessors }, evtMethod: string, params: unknown[]): unknown[] { - const { AppAccessorsInstance } = deps; - /** - * param1 is the context for the event handler execution - * param2 is an optional extra content that some hanlers require - */ - const [param1, param2] = params as [unknown, unknown]; - - if (!param1) { - throw JsonRpcError.invalidParams(null); - } - - let context = param1; - - if (evtMethod.includes('Message')) { - context = hydrateMessageObjects(context) as Record; - } else if (evtMethod.endsWith('RoomUserJoined') || evtMethod.endsWith('RoomUserLeave')) { - (context as Record).room = createRoom((context as Record).room as IRoom, AppAccessorsInstance.getSenderFn()); - } else if (evtMethod.includes('PreRoom')) { - context = createRoom(context as IRoom, AppAccessorsInstance.getSenderFn()); - } - - const args: unknown[] = [context, AppAccessorsInstance.getReader(), AppAccessorsInstance.getHttp()]; - - // "check" events will only go this far - (context, reader, http) - if (evtMethod.startsWith('check')) { - // "checkPostMessageDeleted" has an extra param - (context, reader, http, extraContext) - if (param2) { - args.push(hydrateMessageObjects(param2)); - } - - return args; - } - - // From this point on, all events will require (reader, http, persistence) injected - args.push(AppAccessorsInstance.getPersistence()); - - // "extend" events have an additional "Extender" param - (context, extender, reader, http, persistence) - if (evtMethod.endsWith('Extend')) { - if (evtMethod.includes('Message')) { - args.splice(1, 0, new MessageExtender(param1 as IMessage)); - } else if (evtMethod.includes('Room')) { - args.splice(1, 0, new RoomExtender(param1 as IRoom)); - } - - return args; - } - - // "Modify" events have an additional "Builder" param - (context, builder, reader, http, persistence) - if (evtMethod.endsWith('Modify')) { - if (evtMethod.includes('Message')) { - args.splice(1, 0, new MessageBuilder(param1 as IMessage)); - } else if (evtMethod.includes('Room')) { - args.splice(1, 0, new RoomBuilder(param1 as IRoom)); - } - - return args; - } - - // From this point on, all events will require (reader, http, persistence, modifier) injected - args.push(AppAccessorsInstance.getModifier()); - - // This guy gets an extra one - if (evtMethod === 'executePostMessageDeleted') { - if (!param2) { - throw JsonRpcError.invalidParams(null); - } - - args.push(hydrateMessageObjects(param2)); - } - - return args; -} - -/** - * Hydrate the context object with the correct IMessage - * - * Some information is lost upon serializing the data from listeners through the pipes, - * so here we hydrate the complete object as necessary - */ -function hydrateMessageObjects(context: unknown): unknown { - if (objectIsRawMessage(context)) { - context.room = createRoom(context.room as IRoom, AppAccessorsInstance.getSenderFn()); - } else if ((context as Record)?.message) { - (context as Record).message = hydrateMessageObjects((context as Record).message); - } - - return context; -} - -function objectIsRawMessage(value: unknown): value is IMessage { - if (!value) return false; - - const { id, room, sender, createdAt } = value as Record; - - // Check if we have the fields of a message and the room hasn't already been hydrated - return !!(id && room && sender && createdAt) && !(room instanceof Room); -} diff --git a/packages/apps-engine/deno-runtime/handlers/outboundcomms-handler.ts b/packages/apps-engine/deno-runtime/handlers/outboundcomms-handler.ts deleted file mode 100644 index cb425e61684f5..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/outboundcomms-handler.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { IOutboundMessageProviders } from '@rocket.chat/apps-engine/definition/outboundCommunication/IOutboundCommsProvider.ts'; -import { JsonRpcError, Defined } from 'jsonrpc-lite'; - -import { AppObjectRegistry } from '../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; -import { RequestContext } from '../lib/requestContext.ts'; -import { wrapComposedApp } from '../lib/wrapAppForRequest.ts'; - -export default async function outboundMessageHandler(request: RequestContext): Promise { - const { method: call, params } = request; - const [, providerName, methodName] = call.split(':'); - - const provider = AppObjectRegistry.get(`outboundCommunication:${providerName}`); - - if (!provider) { - return new JsonRpcError('error-invalid-provider', -32000); - } - - const method = provider[methodName as keyof IOutboundMessageProviders]; - const { logger } = request.context; - const args = (params as Array) ?? []; - - try { - logger.debug(`Executing ${methodName} on outbound communication provider...`); - - // deno-lint-ignore ban-types - return await (method as Function).apply(wrapComposedApp(provider, request), [ - ...args, - AppAccessorsInstance.getReader(), - AppAccessorsInstance.getModifier(), - AppAccessorsInstance.getHttp(), - AppAccessorsInstance.getPersistence(), - ]); - } catch (e) { - return new JsonRpcError(e.message, -32000); - } -} diff --git a/packages/apps-engine/deno-runtime/handlers/scheduler-handler.ts b/packages/apps-engine/deno-runtime/handlers/scheduler-handler.ts deleted file mode 100644 index 23c969ed46131..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/scheduler-handler.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; -import type { IProcessor } from '@rocket.chat/apps-engine/definition/scheduler/IProcessor.ts'; -import { Defined, JsonRpcError } from 'jsonrpc-lite'; - -import { AppObjectRegistry } from '../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; -import { RequestContext } from '../lib/requestContext.ts'; -import { wrapAppForRequest } from '../lib/wrapAppForRequest.ts'; -import { assertAppAvailable } from './lib/assertions.ts'; - -export default async function handleScheduler(request: RequestContext): Promise { - const { method, params } = request; - const { logger } = request.context; - - const [, processorId] = method.split(':'); - if (!Array.isArray(params)) { - return JsonRpcError.invalidParams({ message: 'Invalid params' }); - } - - const [context] = params as [Record]; - - // AppSchedulerManager will append the appId to the processor name to avoid conflicts - const processor = AppObjectRegistry.get(`scheduler:${processorId}`); - - if (!processor) { - return JsonRpcError.methodNotFound({ - message: `Could not find processor for method ${method}`, - }); - } - - logger.debug({ msg: 'Job processor is being executed...', processorId: processor.id }); - - const app = AppObjectRegistry.get('app'); - - try { - assertAppAvailable(app); - - await processor.processor.call( - // Processor registration doesn't require the App dev to instantiate a class passing - // a reference to an App object, so we don't have a good way of hijacking the Logger - // we need. - // The only way we have to provide a durable Logger instance for the processor is by - // binding its execution to the proxied App reference itself. Unfortunately, the API - // ends up being opaque, but there isn't much we can do for now. - wrapAppForRequest(app, request), - context, - AppAccessorsInstance.getReader(), - AppAccessorsInstance.getModifier(), - AppAccessorsInstance.getHttp(), - AppAccessorsInstance.getPersistence(), - ); - - logger.debug({ msg: 'Job processor was successfully executed', processorId: processor.id }); - - return null; - } catch (err) { - logger.error({ err, msg: 'Job processor was unsuccessful', processorId: processor.id }); - - if (err instanceof JsonRpcError) { - return err; - } - - return JsonRpcError.internalError({ message: err.message }); - } -} diff --git a/packages/apps-engine/deno-runtime/handlers/slashcommand-handler.ts b/packages/apps-engine/deno-runtime/handlers/slashcommand-handler.ts deleted file mode 100644 index de1a9ecd1efe1..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/slashcommand-handler.ts +++ /dev/null @@ -1,128 +0,0 @@ -import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; -import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands/ISlashCommand.ts'; -import type { SlashCommandContext as _SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands/SlashCommandContext.ts'; -import type { Room as _Room } from '@rocket.chat/apps-engine/server/rooms/Room.ts'; -import { Defined, JsonRpcError } from 'jsonrpc-lite'; - -import { AppObjectRegistry } from '../AppObjectRegistry.ts'; -import { AppAccessors, AppAccessorsInstance } from '../lib/accessors/mod.ts'; -import { require } from '../lib/require.ts'; -import createRoom from '../lib/roomFactory.ts'; -import { RequestContext } from '../lib/requestContext.ts'; -import { wrapComposedApp } from '../lib/wrapAppForRequest.ts'; - -// For some reason Deno couldn't understand the typecast to the original interfaces and said it wasn't a constructor type -const { SlashCommandContext } = require('@rocket.chat/apps-engine/definition/slashcommands/SlashCommandContext.js') as { - SlashCommandContext: typeof _SlashCommandContext; -}; - -export default async function slashCommandHandler(request: RequestContext): Promise { - const { method: call, params } = request; - const { logger } = request.context; - - const [, commandName, method] = call.split(':'); - - const command = AppObjectRegistry.get(`slashcommand:${commandName}`); - - if (!command) { - return new JsonRpcError(`Slashcommand ${commandName} not found`, -32000); - } - - let result: Awaited> | Awaited>; - - logger.debug({ msg: `Command is being executed...`, commandName, method, params }); - - try { - if (method === 'executor' || method === 'previewer') { - result = await handleExecutor({ AppAccessorsInstance, request }, command, method, params); - } else if (method === 'executePreviewItem') { - result = await handlePreviewItem({ AppAccessorsInstance, request }, command, params); - } else { - return new JsonRpcError(`Method ${method} not found on slashcommand ${commandName}`, -32000); - } - - logger.debug({ msg: `Command was successfully executed.`, commandName, method }); - } catch (error) { - logger.debug({ msg: `Command was unsuccessful.`, commandName, method, err: error }); - - return new JsonRpcError(error.message, -32000); - } - - return result; -} - -type Deps = { - AppAccessorsInstance: AppAccessors, - request: RequestContext; -} - -/** - * @param deps Dependencies that need to be injected into the slashcommand - * @param command The slashcommand that is being executed - * @param method The method that is being executed - * @param params The parameters that are being passed to the method - */ -export function handleExecutor(deps: Deps, command: ISlashCommand, method: 'executor' | 'previewer', params: unknown) { - const executor = command[method]; - - if (typeof executor !== 'function') { - throw new Error(`Method ${method} not found on slashcommand ${command.command}`); - } - - if (!Array.isArray(params) || typeof params[0] !== 'object' || !params[0]) { - throw new Error(`First parameter must be an object`); - } - - const { sender, room, params: args, threadId, triggerId } = params[0] as Record; - - const context = new SlashCommandContext( - sender as _SlashCommandContext['sender'], - createRoom(room as IRoom, deps.AppAccessorsInstance.getSenderFn()), - args as _SlashCommandContext['params'], - threadId as _SlashCommandContext['threadId'], - triggerId as _SlashCommandContext['triggerId'], - ); - - return executor.apply(wrapComposedApp(command, deps.request), [ - context, - deps.AppAccessorsInstance.getReader(), - deps.AppAccessorsInstance.getModifier(), - deps.AppAccessorsInstance.getHttp(), - deps.AppAccessorsInstance.getPersistence(), - ]); -} - -/** - * @param deps Dependencies that need to be injected into the slashcommand - * @param command The slashcommand that is being executed - * @param params The parameters that are being passed to the method - */ -export function handlePreviewItem(deps: Deps, command: ISlashCommand, params: unknown) { - if (typeof command.executePreviewItem !== 'function') { - throw new Error(`Method not found on slashcommand ${command.command}`); - } - - if (!Array.isArray(params) || typeof params[0] !== 'object' || !params[0]) { - throw new Error(`First parameter must be an object`); - } - - const [previewItem, { sender, room, params: args, threadId, triggerId }] = params as [Record, Record]; - - const context = new SlashCommandContext( - sender as _SlashCommandContext['sender'], - createRoom(room as IRoom, deps.AppAccessorsInstance.getSenderFn()), - args as _SlashCommandContext['params'], - threadId as _SlashCommandContext['threadId'], - triggerId as _SlashCommandContext['triggerId'], - ); - - return command.executePreviewItem.call( - wrapComposedApp(command, deps.request), - previewItem, - context, - deps.AppAccessorsInstance.getReader(), - deps.AppAccessorsInstance.getModifier(), - deps.AppAccessorsInstance.getHttp(), - deps.AppAccessorsInstance.getPersistence(), - ); -} diff --git a/packages/apps-engine/deno-runtime/handlers/tests/api-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/api-handler.test.ts deleted file mode 100644 index 26ccbd43fb80f..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/tests/api-handler.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -// deno-lint-ignore-file no-explicit-any -import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api/IApiEndpoint.ts'; -import { assertEquals, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod.ts'; -import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; -import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; -import { assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/assert_instance_of.ts'; -import { JsonRpcError } from 'jsonrpc-lite'; - -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import apiHandler from '../api-handler.ts'; -import { createMockRequest } from './helpers/mod.ts'; - -describe('handlers > api', () => { - const mockEndpoint: IApiEndpoint = { - path: '/test', - // deno-lint-ignore no-unused-vars - get: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('ok'), - // deno-lint-ignore no-unused-vars - post: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('ok'), - // deno-lint-ignore no-unused-vars - put: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => { - throw new Error('Method execution error example'); - }, - }; - - beforeEach(() => { - AppObjectRegistry.clear(); - AppObjectRegistry.set('api:/test', mockEndpoint); - }); - - it('correctly handles execution of an api endpoint method GET', async () => { - const _spy = spy(mockEndpoint, 'get'); - - const result = await apiHandler(createMockRequest({ method: 'api:/test:get', params: ['request', 'endpointInfo'] })); - - assertEquals(result, 'ok'); - assertEquals(_spy.calls[0].args.length, 6); - assertEquals(_spy.calls[0].args[0], 'request'); - assertEquals(_spy.calls[0].args[1], 'endpointInfo'); - }); - - it('correctly handles execution of an api endpoint method POST', async () => { - const _spy = spy(mockEndpoint, 'post'); - - const result = await apiHandler(createMockRequest({ method: 'api:/test:post', params: ['request', 'endpointInfo'] })); - - assertEquals(result, 'ok'); - assertEquals(_spy.calls[0].args.length, 6); - assertEquals(_spy.calls[0].args[0], 'request'); - assertEquals(_spy.calls[0].args[1], 'endpointInfo'); - }); - - it('correctly handles an error if the method not exists for the selected endpoint', async () => { - const result = await apiHandler(createMockRequest({ method: `api:/test:delete`, params: ['request', 'endpointInfo'] })); - - assertInstanceOf(result, JsonRpcError); - assertObjectMatch(result, { - message: `/test's delete not exists`, - code: -32000, - }); - }); - - it('correctly handles an error if endpoint not exists', async () => { - const result = await apiHandler(createMockRequest({ method: `api:/error:get`, params: ['request', 'endpointInfo'] })); - - assertInstanceOf(result, JsonRpcError); - assertObjectMatch(result, { - message: `Endpoint /error not found`, - code: -32000, - }); - }); - - it('correctly handles an error if the method execution fails', async () => { - const result = await apiHandler(createMockRequest({ method: `api:/test:put`, params: ['request', 'endpointInfo'] })); - - assertInstanceOf(result, JsonRpcError); - assertObjectMatch(result, { - message: `Method execution error example`, - code: -32000, - }); - }); - - it('correctly handles dynamic paths with parameters (e.g., webhook/:event)', async () => { - const mockDynamicEndpoint: IApiEndpoint = { - path: 'webhook/:event', - // deno-lint-ignore no-unused-vars - post: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('webhook handled'), - }; - - AppObjectRegistry.set('api:webhook/:event', mockDynamicEndpoint); - - const _spy = spy(mockDynamicEndpoint, 'post'); - - const result = await apiHandler(createMockRequest({ method: 'api:webhook/:event:post', params: ['request', 'endpointInfo'] })); - - assertEquals(result, 'webhook handled'); - assertEquals(_spy.calls[0].args.length, 6); - assertEquals(_spy.calls[0].args[0], 'request'); - assertEquals(_spy.calls[0].args[1], 'endpointInfo'); - }); - - it('correctly handles paths with multiple segments and colons', async () => { - const mockComplexEndpoint: IApiEndpoint = { - path: 'api/v1/:resource/:id', - // deno-lint-ignore no-unused-vars - get: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('complex path'), - }; - - AppObjectRegistry.set('api:api/v1/:resource/:id', mockComplexEndpoint); - - const _spy = spy(mockComplexEndpoint, 'get'); - - const result = await apiHandler(createMockRequest({ method: 'api:api/v1/:resource/:id:get', params: ['request', 'endpointInfo'] })); - - assertEquals(result, 'complex path'); - assertEquals(_spy.calls[0].args.length, 6); - }); -}); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/helpers/mod.ts b/packages/apps-engine/deno-runtime/handlers/tests/helpers/mod.ts deleted file mode 100644 index 581d95e4c3484..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/tests/helpers/mod.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; - -import { Logger } from '../../../lib/logger.ts'; -import { RequestDescriptor } from '../../../lib/messenger.ts'; -import { RequestContext } from '../../../lib/requestContext.ts'; - -export function createMockRequest({ method, params }: RequestDescriptor): RequestContext { - return { - jsonrpc: '2.0', - id: 1, - method, - params, - context: { - logger: new Logger(method), - }, - serialize: () => '', - } -} - -export function createMockApp(): App { - return { - extendConfiguration: () => {}, - getID: () => 'mockApp', - getLogger: () => ({ - debug: () => {}, - error: () => {}, - }), - } as unknown as App; -} diff --git a/packages/apps-engine/deno-runtime/handlers/tests/listener-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/listener-handler.test.ts deleted file mode 100644 index 8e355f6ac4d3d..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/tests/listener-handler.test.ts +++ /dev/null @@ -1,234 +0,0 @@ -// deno-lint-ignore-file no-explicit-any -import { assertEquals, assertInstanceOf, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod.ts'; -import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; - -import { parseArgs } from '../listener/handler.ts'; -import { AppAccessors } from '../../lib/accessors/mod.ts'; -import { Room } from '../../lib/room.ts'; -import { MessageExtender } from '../../lib/accessors/extenders/MessageExtender.ts'; -import { RoomExtender } from '../../lib/accessors/extenders/RoomExtender.ts'; -import { MessageBuilder } from '../../lib/accessors/builders/MessageBuilder.ts'; -import { RoomBuilder } from '../../lib/accessors/builders/RoomBuilder.ts'; - -describe('handlers > listeners', () => { - const mockAppAccessors = { - getReader: () => ({ __type: 'reader' }), - getHttp: () => ({ __type: 'http' }), - getModifier: () => ({ __type: 'modifier' }), - getPersistence: () => ({ __type: 'persistence' }), - getSenderFn: () => (id: string) => Promise.resolve([{ __type: 'bridgeCall' }, { id }]), - } as unknown as AppAccessors; - - it('correctly parses the arguments for a request to trigger the "checkPreMessageSentPrevent" method', () => { - const evtMethod = 'checkPreMessageSentPrevent'; - // For the 'checkPreMessageSentPrevent' method, the context will be a message in a real scenario - const evtArgs = [{ __type: 'context' }]; - - const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - - assertEquals(params.length, 3); - assertEquals(params[0], { __type: 'context' }); - assertEquals(params[1], { __type: 'reader' }); - assertEquals(params[2], { __type: 'http' }); - }); - - it('correctly parses the arguments for a request to trigger the "checkPostMessageDeleted" method', () => { - const evtMethod = 'checkPostMessageDeleted'; - // For the 'checkPostMessageDeleted' method, the context will be a message in a real scenario, - // and the extraContext will provide further information such the user who deleted the message - const evtArgs = [{ __type: 'context' }, { __type: 'extraContext' }]; - - const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - - assertEquals(params.length, 4); - assertEquals(params[0], { __type: 'context' }); - assertEquals(params[1], { __type: 'reader' }); - assertEquals(params[2], { __type: 'http' }); - assertEquals(params[3], { __type: 'extraContext' }); - }); - - it('correctly parses the arguments for a request to trigger the "checkPreRoomCreateExtend" method', () => { - const evtMethod = 'checkPreRoomCreateExtend'; - // For the 'checkPreRoomCreateExtend' method, the context will be a room in a real scenario - const evtArgs = [ - { - id: 'fake', - type: 'fake', - slugifiedName: 'fake', - creator: 'fake', - createdAt: Date.now(), - }, - ]; - - const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - - assertEquals(params.length, 3); - - assertInstanceOf(params[0], Room); - assertEquals(params[1], { __type: 'reader' }); - assertEquals(params[2], { __type: 'http' }); - }); - - it('correctly parses the arguments for a request to trigger the "executePreMessageSentExtend" method', () => { - const evtMethod = 'executePreMessageSentExtend'; - // For the 'executePreMessageSentExtend' method, the context will be a message in a real scenario - const evtArgs = [{ __type: 'context' }]; - - const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - - assertEquals(params.length, 5); - // Instantiating the MessageExtender might modify the original object, so we need to assert it matches instead of equals - assertObjectMatch(params[0] as Record, { - __type: 'context', - }); - assertInstanceOf(params[1], MessageExtender); - assertEquals(params[2], { __type: 'reader' }); - assertEquals(params[3], { __type: 'http' }); - assertEquals(params[4], { __type: 'persistence' }); - }); - - it('correctly parses the arguments for a request to trigger the "executePreRoomCreateExtend" method', () => { - const evtMethod = 'executePreRoomCreateExtend'; - // For the 'executePreRoomCreateExtend' method, the context will be a room in a real scenario - const evtArgs = [{ __type: 'context' }]; - - const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - - assertEquals(params.length, 5); - // Instantiating the RoomExtender might modify the original object, so we need to assert it matches instead of equals - assertObjectMatch(params[0] as Record, { - __type: 'context', - }); - assertInstanceOf(params[1], RoomExtender); - assertEquals(params[2], { __type: 'reader' }); - assertEquals(params[3], { __type: 'http' }); - assertEquals(params[4], { __type: 'persistence' }); - }); - - it('correctly parses the arguments for a request to trigger the "executePreMessageSentModify" method', () => { - const evtMethod = 'executePreMessageSentModify'; - // For the 'executePreMessageSentModify' method, the context will be a message in a real scenario - const evtArgs = [{ __type: 'context' }]; - - const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - - assertEquals(params.length, 5); - // Instantiating the MessageBuilder might modify the original object, so we need to assert it matches instead of equals - assertObjectMatch(params[0] as Record, { - __type: 'context', - }); - assertInstanceOf(params[1], MessageBuilder); - assertEquals(params[2], { __type: 'reader' }); - assertEquals(params[3], { __type: 'http' }); - assertEquals(params[4], { __type: 'persistence' }); - }); - - it('correctly parses the arguments for a request to trigger the "executePreRoomCreateModify" method', () => { - const evtMethod = 'executePreRoomCreateModify'; - // For the 'executePreRoomCreateModify' method, the context will be a room in a real scenario - const evtArgs = [{ __type: 'context' }]; - - const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - - assertEquals(params.length, 5); - // Instantiating the RoomBuilder might modify the original object, so we need to assert it matches instead of equals - assertObjectMatch(params[0] as Record, { - __type: 'context', - }); - assertInstanceOf(params[1], RoomBuilder); - assertEquals(params[2], { __type: 'reader' }); - assertEquals(params[3], { __type: 'http' }); - assertEquals(params[4], { __type: 'persistence' }); - }); - - it('correctly parses the arguments for a request to trigger the "executePostRoomUserJoined" method', () => { - const evtMethod = 'executePostRoomUserJoined'; - // For the 'executePostRoomUserJoined' method, the context will be a room in a real scenario - const room = { - id: 'fake', - type: 'fake', - slugifiedName: 'fake', - creator: 'fake', - createdAt: Date.now(), - }; - - const evtArgs = [{ __type: 'context', room }]; - - const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - - assertEquals(params.length, 5); - assertInstanceOf((params[0] as any).room, Room); - assertEquals(params[1], { __type: 'reader' }); - assertEquals(params[2], { __type: 'http' }); - assertEquals(params[3], { __type: 'persistence' }); - assertEquals(params[4], { __type: 'modifier' }); - }); - - it('correctly parses the arguments for a request to trigger the "executePostRoomUserLeave" method', () => { - const evtMethod = 'executePostRoomUserLeave'; - // For the 'executePostRoomUserLeave' method, the context will be a room in a real scenario - const room = { - id: 'fake', - type: 'fake', - slugifiedName: 'fake', - creator: 'fake', - createdAt: Date.now(), - }; - - const evtArgs = [{ __type: 'context', room }]; - - const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - - assertEquals(params.length, 5); - assertInstanceOf((params[0] as any).room, Room); - assertEquals(params[1], { __type: 'reader' }); - assertEquals(params[2], { __type: 'http' }); - assertEquals(params[3], { __type: 'persistence' }); - assertEquals(params[4], { __type: 'modifier' }); - }); - - it('correctly parses the arguments for a request to trigger the "executePostMessageDeleted" method', () => { - const evtMethod = 'executePostMessageDeleted'; - // For the 'executePostMessageDeleted' method, the context will be a message in a real scenario - const evtArgs = [{ __type: 'context' }, { __type: 'extraContext' }]; - - const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - - assertEquals(params.length, 6); - assertEquals(params[0], { __type: 'context' }); - assertEquals(params[1], { __type: 'reader' }); - assertEquals(params[2], { __type: 'http' }); - assertEquals(params[3], { __type: 'persistence' }); - assertEquals(params[4], { __type: 'modifier' }); - assertEquals(params[5], { __type: 'extraContext' }); - }); - - it('correctly parses the arguments for a request to trigger the "executePostMessageSent" method', () => { - const evtMethod = 'executePostMessageSent'; - // For the 'executePostMessageDeleted' method, the context will be a message in a real scenario - const evtArgs = [ - { - id: 'fake', - sender: 'fake', - createdAt: Date.now(), - room: { - id: 'fake-room', - type: 'fake', - slugifiedName: 'fake', - creator: 'fake', - createdAt: Date.now(), - }, - }, - ]; - - const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - - assertEquals(params.length, 5); - assertObjectMatch(params[0] as Record, { id: 'fake' }); - assertInstanceOf((params[0] as any).room, Room); - assertEquals(params[1], { __type: 'reader' }); - assertEquals(params[2], { __type: 'http' }); - assertEquals(params[3], { __type: 'persistence' }); - assertEquals(params[4], { __type: 'modifier' }); - }); -}); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/scheduler-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/scheduler-handler.test.ts deleted file mode 100644 index 7f5c6eccaf569..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/tests/scheduler-handler.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod.ts'; -import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; - -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessors } from '../../lib/accessors/mod.ts'; -import handleScheduler from '../scheduler-handler.ts'; -import { createMockApp, createMockRequest } from './helpers/mod.ts'; - -describe('handlers > scheduler', () => { - const mockAppAccessors = new AppAccessors(() => - Promise.resolve({ - id: 'mockId', - result: {}, - jsonrpc: '2.0', - serialize: () => '', - }) - ); - - const mockApp = createMockApp(); - - beforeEach(() => { - AppObjectRegistry.clear(); - AppObjectRegistry.set('app', mockApp); - mockAppAccessors.getConfigurationExtend().scheduler.registerProcessors([ - { - id: 'mockId', - processor: () => Promise.resolve('it works!'), - }, - ]); - }); - - afterAll(() => { - AppObjectRegistry.clear(); - }); - - it('correctly executes a request to a processor', async () => { - const result = await handleScheduler(createMockRequest({ method: 'scheduler:mockId', params: [{}] })); - - assertEquals(result, null); - }); -}); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/slashcommand-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/slashcommand-handler.test.ts deleted file mode 100644 index 7114aa1f85bea..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/tests/slashcommand-handler.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -// deno-lint-ignore-file no-explicit-any -import { assertEquals, assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod.ts'; -import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; -import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; - -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessors } from '../../lib/accessors/mod.ts'; -import { handleExecutor, handlePreviewItem } from '../slashcommand-handler.ts'; -import { Room } from '../../lib/room.ts'; -import { createMockRequest } from './helpers/mod.ts'; - -describe('handlers > slashcommand', () => { - const mockAppAccessors = { - getReader: () => ({ __type: 'reader' }), - getHttp: () => ({ __type: 'http' }), - getModifier: () => ({ __type: 'modifier' }), - getPersistence: () => ({ __type: 'persistence' }), - getSenderFn: () => (id: string) => Promise.resolve([{ __type: 'bridgeCall' }, { id }]), - } as unknown as AppAccessors; - - const mockCommandExecutorOnly = { - command: 'executor-only', - i18nParamsExample: 'test', - i18nDescription: 'test', - providesPreview: false, - // deno-lint-ignore no-unused-vars - async executor(context: any, read: any, modify: any, http: any, persis: any): Promise {}, - }; - - const mockCommandExecutorAndPreview = { - command: 'executor-and-preview', - i18nParamsExample: 'test', - i18nDescription: 'test', - providesPreview: true, - // deno-lint-ignore no-unused-vars - async executor(context: any, read: any, modify: any, http: any, persis: any): Promise {}, - // deno-lint-ignore no-unused-vars - async previewer(context: any, read: any, modify: any, http: any, persis: any): Promise {}, - // deno-lint-ignore no-unused-vars - async executePreviewItem(previewItem: any, context: any, read: any, modify: any, http: any, persis: any): Promise {}, - }; - - const mockCommandPreviewWithNoExecutor = { - command: 'preview-with-no-executor', - i18nParamsExample: 'test', - i18nDescription: 'test', - providesPreview: true, - // deno-lint-ignore no-unused-vars - async previewer(context: any, read: any, modify: any, http: any, persis: any): Promise {}, - // deno-lint-ignore no-unused-vars - async executePreviewItem(previewItem: any, context: any, read: any, modify: any, http: any, persis: any): Promise {}, - }; - - beforeEach(() => { - AppObjectRegistry.clear(); - AppObjectRegistry.set('slashcommand:executor-only', mockCommandExecutorOnly); - AppObjectRegistry.set('slashcommand:executor-and-preview', mockCommandExecutorAndPreview); - AppObjectRegistry.set('slashcommand:preview-with-no-executor', mockCommandPreviewWithNoExecutor); - }); - - it('correctly handles execution of a slash command', async () => { - const mockContext = { - sender: { __type: 'sender' }, - room: { __type: 'room' }, - params: { __type: 'params' }, - threadId: 'threadId', - triggerId: 'triggerId', - }; - - const _spy = spy(mockCommandExecutorOnly, 'executor'); - - const mockRequest = createMockRequest({ method: 'slashcommand:executor-only:executor', params: [mockContext] }); - - await handleExecutor({ AppAccessorsInstance: mockAppAccessors, request: mockRequest }, mockCommandExecutorOnly, 'executor', [mockContext]); - - const context = _spy.calls[0].args[0]; - - assertInstanceOf(context.getRoom(), Room); - assertEquals(context.getSender(), { __type: 'sender' }); - assertEquals(context.getArguments(), { __type: 'params' }); - assertEquals(context.getThreadId(), 'threadId'); - assertEquals(context.getTriggerId(), 'triggerId'); - - assertEquals(_spy.calls[0].args[1], mockAppAccessors.getReader()); - assertEquals(_spy.calls[0].args[2], mockAppAccessors.getModifier()); - assertEquals(_spy.calls[0].args[3], mockAppAccessors.getHttp()); - assertEquals(_spy.calls[0].args[4], mockAppAccessors.getPersistence()); - - _spy.restore(); - }); - - it('correctly handles execution of a slash command previewer', async () => { - const mockContext = { - sender: { __type: 'sender' }, - room: { __type: 'room' }, - params: { __type: 'params' }, - threadId: 'threadId', - triggerId: 'triggerId', - }; - - const _spy = spy(mockCommandExecutorAndPreview, 'previewer'); - - const mockRequest = createMockRequest({ method: 'slashcommand:executor-and-preview:previewer', params: [mockContext] }); - - await handleExecutor({ AppAccessorsInstance: mockAppAccessors, request: mockRequest }, mockCommandExecutorAndPreview, 'previewer', [mockContext]); - - const context = _spy.calls[0].args[0]; - - assertInstanceOf(context.getRoom(), Room); - assertEquals(context.getSender(), { __type: 'sender' }); - assertEquals(context.getArguments(), { __type: 'params' }); - assertEquals(context.getThreadId(), 'threadId'); - assertEquals(context.getTriggerId(), 'triggerId'); - - assertEquals(_spy.calls[0].args[1], mockAppAccessors.getReader()); - assertEquals(_spy.calls[0].args[2], mockAppAccessors.getModifier()); - assertEquals(_spy.calls[0].args[3], mockAppAccessors.getHttp()); - assertEquals(_spy.calls[0].args[4], mockAppAccessors.getPersistence()); - - _spy.restore(); - }); - - it('correctly handles execution of a slash command preview item executor', async () => { - const mockContext = { - sender: { __type: 'sender' }, - room: { __type: 'room' }, - params: { __type: 'params' }, - threadId: 'threadId', - triggerId: 'triggerId', - }; - - const mockPreviewItem = { - id: 'previewItemId', - type: 'image', - value: 'https://example.com/image.png', - }; - - const _spy = spy(mockCommandExecutorAndPreview, 'executePreviewItem'); - - const mockRequest = createMockRequest({ method: 'slashcommand:executor-and-preview:executePreviewItem', params: [mockPreviewItem, mockContext] }); - - await handlePreviewItem({ AppAccessorsInstance: mockAppAccessors, request: mockRequest }, mockCommandExecutorAndPreview, [mockPreviewItem, mockContext]); - - const context = _spy.calls[0].args[1]; - - assertInstanceOf(context.getRoom(), Room); - assertEquals(context.getSender(), { __type: 'sender' }); - assertEquals(context.getArguments(), { __type: 'params' }); - assertEquals(context.getThreadId(), 'threadId'); - assertEquals(context.getTriggerId(), 'triggerId'); - - assertEquals(_spy.calls[0].args[2], mockAppAccessors.getReader()); - assertEquals(_spy.calls[0].args[3], mockAppAccessors.getModifier()); - assertEquals(_spy.calls[0].args[4], mockAppAccessors.getHttp()); - assertEquals(_spy.calls[0].args[5], mockAppAccessors.getPersistence()); - - _spy.restore(); - }); -}); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/uikit-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/uikit-handler.test.ts deleted file mode 100644 index b663bd2ae6833..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/tests/uikit-handler.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -// deno-lint-ignore-file no-explicit-any -import { assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod.ts'; -import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; -import jsonrpc from 'jsonrpc-lite'; - -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import handleUIKitInteraction, { - UIKitActionButtonInteractionContext, - UIKitBlockInteractionContext, - UIKitLivechatBlockInteractionContext, - UIKitViewCloseInteractionContext, - UIKitViewSubmitInteractionContext, -} from '../uikit/handler.ts'; - -describe('handlers > uikit', () => { - const mockApp = { - getID: (): string => 'appId', - executeBlockActionHandler: (context: any): Promise => Promise.resolve(context), - executeViewSubmitHandler: (context: any): Promise => Promise.resolve(context), - executeViewClosedHandler: (context: any): Promise => Promise.resolve(context), - executeActionButtonHandler: (context: any): Promise => Promise.resolve(context), - executeLivechatBlockActionHandler: (context: any): Promise => Promise.resolve(context), - }; - - beforeEach(() => { - AppObjectRegistry.set('app', mockApp); - }); - - afterAll(() => { - AppObjectRegistry.clear(); - }); - - it('successfully handles a call for "executeBlockActionHandler"', async () => { - const request = jsonrpc.request(1, 'app:executeBlockActionHandler', [ - { - actionId: 'actionId', - blockId: 'blockId', - value: 'value', - viewId: 'viewId', - }, - ]); - - const result = await handleUIKitInteraction(request); - assertInstanceOf(result, UIKitBlockInteractionContext); - }); - - it('successfully handles a call for "executeViewSubmitHandler"', async () => { - const request = jsonrpc.request(1, 'app:executeViewSubmitHandler', [ - { - viewId: 'viewId', - appId: 'appId', - userId: 'userId', - isAppUser: true, - values: {}, - }, - ]); - - const result = await handleUIKitInteraction(request); - assertInstanceOf(result, UIKitViewSubmitInteractionContext); - }); - - it('successfully handles a call for "executeViewClosedHandler"', async () => { - const request = jsonrpc.request(1, 'app:executeViewClosedHandler', [ - { - viewId: 'viewId', - appId: 'appId', - userId: 'userId', - isAppUser: true, - }, - ]); - - const result = await handleUIKitInteraction(request); - assertInstanceOf(result, UIKitViewCloseInteractionContext); - }); - - it('successfully handles a call for "executeActionButtonHandler"', async () => { - const request = jsonrpc.request(1, 'app:executeActionButtonHandler', [ - { - actionId: 'actionId', - appId: 'appId', - userId: 'userId', - isAppUser: true, - }, - ]); - - const result = await handleUIKitInteraction(request); - assertInstanceOf(result, UIKitActionButtonInteractionContext); - }); - - it('successfully handles a call for "executeLivechatBlockActionHandler"', async () => { - const request = jsonrpc.request(1, 'app:executeLivechatBlockActionHandler', [ - { - actionId: 'actionId', - appId: 'appId', - userId: 'userId', - visitor: {}, - isAppUser: true, - room: {}, - }, - ]); - - const result = await handleUIKitInteraction(request); - assertInstanceOf(result, UIKitLivechatBlockInteractionContext); - }); -}); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/upload-event-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/upload-event-handler.test.ts deleted file mode 100644 index 182d32eda3353..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/tests/upload-event-handler.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -// deno-lint-ignore-file no-explicit-any -import { Buffer } from 'node:buffer'; - -import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; -import type { IPreFileUpload } from '@rocket.chat/apps-engine/definition/uploads/IPreFileUpload.ts'; -import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails.ts'; -import { assertInstanceOf, assertNotInstanceOf, assertEquals, assertStringIncludes } from 'https://deno.land/std@0.203.0/assert/mod.ts'; -import { afterEach, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; -import { assertSpyCalls, spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; -import { JsonRpcError } from 'jsonrpc-lite'; - -import { createMockRequest } from './helpers/mod.ts'; -import handleUploadEvents from '../app/handleUploadEvents.ts'; -import { Errors } from '../lib/assertions.ts'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; - -describe('handlers > upload', () => { - let app: App & IPreFileUpload; - let path: string; - let file: IUploadDetails; - - beforeEach(async () => { - AppObjectRegistry.clear(); - - path = await Deno.makeTempFile(); - - app = { - extendConfiguration: () => {}, - executePreFileUpload: () => Promise.resolve(), - } as unknown as App; - - AppObjectRegistry.set('app', app); - - const content = 'Temp file for testing'; - - await Deno.writeTextFile(path, content); - - file = { - name: 'TempFile.txt', - size: content.length, - type: 'text/plain', - rid: 'RandomRoomId', - userId: 'RandomUserId', - }; - }); - - afterEach(async () => { - await Deno.remove(path).catch((e) => e?.code !== 'ENOENT' && console.warn(`Failed to remove temp file at ${path}`, e)); - }); - - it('correctly handles valid parameters', async () => { - const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); - - assertNotInstanceOf(result, JsonRpcError, 'result is JsonRpcError'); - }); - - it('correctly loads the file contents for IPreFileUpload', async () => { - const _spy = spy(app as any, 'executePreFileUpload'); - - const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); - - assertNotInstanceOf(result, JsonRpcError, 'result is JsonRpcError'); - assertSpyCalls(_spy, 1); - assertInstanceOf((_spy.calls[0].args[0] as any)?.content, Buffer); - }); - - it('fails when app object is not on registry', async () => { - AppObjectRegistry.clear(); - - const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); - - assertInstanceOf(result, JsonRpcError); - assertEquals(result.data.code, Errors.DRT_APP_NOT_AVAILABLE); - }); - - it('fails when the app does not implement the IPreFileUpload event handler', async () => { - delete (app as any)['executePreFileUpload']; - - const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); - - assertInstanceOf(result, JsonRpcError); - assertEquals(result.data.code, Errors.DRT_EVENT_HANDLER_FUNCTION_MISSING); - }); - - it('fails when "file" is not a proper IUploadDetails object', async () => { - const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file: { nope: "bad" }, path }] })); - - assertInstanceOf(result, JsonRpcError); - assertStringIncludes(result.data.err, 'Expected IUploadDetails'); - }); - - it('fails when "path" is not a proper string', async () => { - const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path: {} }] })); - - assertInstanceOf(result, JsonRpcError); - assertStringIncludes(result.data.err, 'Expected string'); - }); - - it('fails when "path" is not a readable file path', async () => { - await Deno.remove(path); - - const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); - - assertInstanceOf(result, JsonRpcError); - assertEquals(result.data.code, "ENOENT"); - }); -}); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/videoconference-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/videoconference-handler.test.ts deleted file mode 100644 index 7632b08c39258..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/tests/videoconference-handler.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -// deno-lint-ignore-file no-explicit-any -import { assertEquals, assertObjectMatch, assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod.ts'; -import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; -import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; -import { JsonRpcError } from 'jsonrpc-lite'; - -import { createMockRequest } from './helpers/mod.ts'; -import videoconfHandler from '../videoconference-handler.ts'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; - -describe('handlers > videoconference', () => { - // deno-lint-ignore no-unused-vars - const mockMethodWithoutParam = (read: any, modify: any, http: any, persis: any): Promise => Promise.resolve('ok none'); - // deno-lint-ignore no-unused-vars - const mockMethodWithOneParam = (call: any, read: any, modify: any, http: any, persis: any): Promise => Promise.resolve('ok one'); - // deno-lint-ignore no-unused-vars - const mockMethodWithTwoParam = (call: any, user: any, read: any, modify: any, http: any, persis: any): Promise => Promise.resolve('ok two'); - // deno-lint-ignore no-unused-vars - const mockMethodWithThreeParam = (call: any, user: any, options: any, read: any, modify: any, http: any, persis: any): Promise => - Promise.resolve('ok three'); - const mockProvider = { - empty: mockMethodWithoutParam, - one: mockMethodWithOneParam, - two: mockMethodWithTwoParam, - three: mockMethodWithThreeParam, - notAFunction: true, - error: () => { - throw new Error('Method execution error example'); - }, - }; - - beforeEach(() => { - AppObjectRegistry.clear(); - AppObjectRegistry.set('videoConfProvider:test-provider', mockProvider); - }); - - it('correctly handles execution of a videoconf method without additional params', async () => { - const _spy = spy(mockProvider, 'empty'); - - const result = await videoconfHandler(createMockRequest({ method: 'videoconference:test-provider:empty', params: [] })); - - assertEquals(result, 'ok none'); - assertEquals(_spy.calls[0].args.length, 4); - - _spy.restore(); - }); - - it('correctly handles execution of a videoconf method with one param', async () => { - const _spy = spy(mockProvider, 'one'); - - const result = await videoconfHandler(createMockRequest({ method: 'videoconference:test-provider:one', params: ['call'] })); - - assertEquals(result, 'ok one'); - assertEquals(_spy.calls[0].args.length, 5); - assertEquals(_spy.calls[0].args[0], 'call'); - - _spy.restore(); - }); - - it('correctly handles execution of a videoconf method with two params', async () => { - const _spy = spy(mockProvider, 'two'); - - const result = await videoconfHandler(createMockRequest({ method: 'videoconference:test-provider:two', params: ['call', 'user'] })); - - assertEquals(result, 'ok two'); - assertEquals(_spy.calls[0].args.length, 6); - assertEquals(_spy.calls[0].args[0], 'call'); - assertEquals(_spy.calls[0].args[1], 'user'); - - _spy.restore(); - }); - - it('correctly handles execution of a videoconf method with three params', async () => { - const _spy = spy(mockProvider, 'three'); - - const result = await videoconfHandler(createMockRequest({ method: 'videoconference:test-provider:three', params: ['call', 'user', 'options'] })); - - assertEquals(result, 'ok three'); - assertEquals(_spy.calls[0].args.length, 7); - assertEquals(_spy.calls[0].args[0], 'call'); - assertEquals(_spy.calls[0].args[1], 'user'); - assertEquals(_spy.calls[0].args[2], 'options'); - - _spy.restore(); - }); - - it('correctly handles an error on execution of a videoconf method', async () => { - const result = await videoconfHandler(createMockRequest({ method: 'videoconference:test-provider:error', params: [] })); - - assertInstanceOf(result, JsonRpcError); - assertObjectMatch(result, { - message: 'Method execution error example', - code: -32000, - }); - }); - - it('correctly handles an error when provider is not found', async () => { - const providerName = 'error-provider'; - const result = await videoconfHandler(createMockRequest({ method: `videoconference:${providerName}:method`, params: [] })); - - assertInstanceOf(result, JsonRpcError); - assertObjectMatch(result, { - message: `Provider ${providerName} not found`, - code: -32000, - }); - }); - - it('correctly handles an error if method is not a function of provider', async () => { - const methodName = 'notAFunction'; - const providerName = 'test-provider'; - const result = await videoconfHandler(createMockRequest({ method: `videoconference:${providerName}:${methodName}`, params: [] })); - - assertInstanceOf(result, JsonRpcError); - assertObjectMatch(result, { - message: 'Method not found', - code: -32601, - data: { - message: `Method ${methodName} not found on provider ${providerName}`, - }, - }); - }); -}); diff --git a/packages/apps-engine/deno-runtime/handlers/uikit/handler.ts b/packages/apps-engine/deno-runtime/handlers/uikit/handler.ts deleted file mode 100644 index 8d352d21927e7..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/uikit/handler.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; -import { Defined, JsonRpcError } from 'jsonrpc-lite'; - -import { require } from '../../lib/require.ts'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { isOneOf } from '../lib/assertions.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; - -export const uikitInteractions = [ - 'executeBlockActionHandler', - 'executeViewSubmitHandler', - 'executeViewClosedHandler', - 'executeActionButtonHandler', - 'executeLivechatBlockActionHandler', -] as const; - -export const { - UIKitBlockInteractionContext, - UIKitViewSubmitInteractionContext, - UIKitViewCloseInteractionContext, - UIKitActionButtonInteractionContext, -} = require('@rocket.chat/apps-engine/definition/uikit/UIKitInteractionContext.js'); - -export const { UIKitLivechatBlockInteractionContext } = require('@rocket.chat/apps-engine/definition/uikit/livechat/UIKitLivechatInteractionContext.js'); - -export default async function handleUIKitInteraction(request: RequestContext): Promise { - const { method: reqMethod, params } = request; - const [, method] = reqMethod.split(':'); - - if (!isOneOf(method, uikitInteractions)) { - return JsonRpcError.methodNotFound(null); - } - - if (!Array.isArray(params)) { - return JsonRpcError.invalidParams(null); - } - - const app = AppObjectRegistry.get('app'); - - const interactionHandler = app?.[method as keyof App] as unknown; - - if (!app || typeof interactionHandler !== 'function') { - return JsonRpcError.methodNotFound({ - message: `App does not implement method "${method}"`, - }); - } - - const [payload] = params as [Record]; - - if (!payload) { - return JsonRpcError.invalidParams(null); - } - - let context; - - switch (method) { - case 'executeBlockActionHandler': - context = new UIKitBlockInteractionContext(payload); - break; - case 'executeViewSubmitHandler': - context = new UIKitViewSubmitInteractionContext(payload); - break; - case 'executeViewClosedHandler': - context = new UIKitViewCloseInteractionContext(payload); - break; - case 'executeActionButtonHandler': - context = new UIKitActionButtonInteractionContext(payload); - break; - case 'executeLivechatBlockActionHandler': - context = new UIKitLivechatBlockInteractionContext(payload); - break; - } - - try { - return await interactionHandler.call( - wrapAppForRequest(app, request), - context, - AppAccessorsInstance.getReader(), - AppAccessorsInstance.getHttp(), - AppAccessorsInstance.getPersistence(), - AppAccessorsInstance.getModifier(), - ); - } catch (e) { - return JsonRpcError.internalError({ message: e.message }); - } -} diff --git a/packages/apps-engine/deno-runtime/handlers/videoconference-handler.ts b/packages/apps-engine/deno-runtime/handlers/videoconference-handler.ts deleted file mode 100644 index 5c82aedd3a63c..0000000000000 --- a/packages/apps-engine/deno-runtime/handlers/videoconference-handler.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders/IVideoConfProvider.ts'; -import { Defined, JsonRpcError } from 'jsonrpc-lite'; - -import { AppObjectRegistry } from '../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; -import { RequestContext } from '../lib/requestContext.ts'; -import { wrapComposedApp } from '../lib/wrapAppForRequest.ts'; - -export default async function videoConferenceHandler(request: RequestContext): Promise { - const { method: call, params } = request; - const { logger } = request.context; - - const [, providerName, methodName] = call.split(':'); - - const provider = AppObjectRegistry.get(`videoConfProvider:${providerName}`); - - if (!provider) { - return new JsonRpcError(`Provider ${providerName} not found`, -32000); - } - - const method = provider[methodName as keyof IVideoConfProvider]; - - if (typeof method !== 'function') { - return JsonRpcError.methodNotFound({ - message: `Method ${methodName} not found on provider ${providerName}`, - }); - } - - const [videoconf, user, options] = params as Array; - - logger.debug(`Executing ${methodName} on video conference provider...`); - - const args = [...(videoconf ? [videoconf] : []), ...(user ? [user] : []), ...(options ? [options] : [])]; - - try { - // deno-lint-ignore ban-types - const result = await (method as Function).apply(wrapComposedApp(provider, request), [ - ...args, - AppAccessorsInstance.getReader(), - AppAccessorsInstance.getModifier(), - AppAccessorsInstance.getHttp(), - AppAccessorsInstance.getPersistence(), - ]); - - logger.debug(`Video Conference Provider's ${methodName} was successfully executed.`); - - return result; - } catch (e) { - logger.debug(`Video Conference Provider's ${methodName} was unsuccessful.`); - return new JsonRpcError(e.message, -32000); - } -} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/builders/BlockBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/BlockBuilder.ts deleted file mode 100644 index de103fe50be02..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/builders/BlockBuilder.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { v1 as uuid } from 'uuid'; - -import type { - BlockType as _BlockType, - IActionsBlock, - IBlock, - IConditionalBlock, - IConditionalBlockFilters, - IContextBlock, - IImageBlock, - IInputBlock, - ISectionBlock, -} from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks.ts'; -import type { - BlockElementType as _BlockElementType, - IBlockElement, - IButtonElement, - IImageElement, - IInputElement, - IInteractiveElement, - IMultiStaticSelectElement, - IOverflowMenuElement, - IPlainTextInputElement, - ISelectElement, - IStaticSelectElement, -} from '@rocket.chat/apps-engine/definition/uikit/blocks/Elements.ts'; -import type { ITextObject, TextObjectType as _TextObjectType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Objects.ts'; - -import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; -import { require } from '../../../lib/require.ts'; - -const { BlockType } = require('@rocket.chat/apps-engine/definition/uikit/blocks/Blocks.js') as { BlockType: typeof _BlockType }; -const { BlockElementType } = require('@rocket.chat/apps-engine/definition/uikit/blocks/Elements.js') as { BlockElementType: typeof _BlockElementType }; -const { TextObjectType } = require('@rocket.chat/apps-engine/definition/uikit/blocks/Objects.js') as { TextObjectType: typeof _TextObjectType }; - -type BlockFunctionParameter = Omit; -type ElementFunctionParameter = T extends IInteractiveElement ? Omit | Partial> - : Omit; - -type SectionBlockParam = BlockFunctionParameter; -type ImageBlockParam = BlockFunctionParameter; -type ActionsBlockParam = BlockFunctionParameter; -type ContextBlockParam = BlockFunctionParameter; -type InputBlockParam = BlockFunctionParameter; - -type ButtonElementParam = ElementFunctionParameter; -type ImageElementParam = ElementFunctionParameter; -type OverflowMenuElementParam = ElementFunctionParameter; -type PlainTextInputElementParam = ElementFunctionParameter; -type StaticSelectElementParam = ElementFunctionParameter; -type MultiStaticSelectElementParam = ElementFunctionParameter; - -/** - * @deprecated please prefer the rocket.chat/ui-kit components - */ -export class BlockBuilder { - private readonly blocks: Array; - private readonly appId: string; - - constructor() { - this.blocks = []; - this.appId = String(AppObjectRegistry.get('id')); - } - - public addSectionBlock(block: SectionBlockParam): BlockBuilder { - this.addBlock({ type: BlockType.SECTION, ...block } as ISectionBlock); - - return this; - } - - public addImageBlock(block: ImageBlockParam): BlockBuilder { - this.addBlock({ type: BlockType.IMAGE, ...block } as IImageBlock); - - return this; - } - - public addDividerBlock(): BlockBuilder { - this.addBlock({ type: BlockType.DIVIDER }); - - return this; - } - - public addActionsBlock(block: ActionsBlockParam): BlockBuilder { - this.addBlock({ type: BlockType.ACTIONS, ...block } as IActionsBlock); - - return this; - } - - public addContextBlock(block: ContextBlockParam): BlockBuilder { - this.addBlock({ type: BlockType.CONTEXT, ...block } as IContextBlock); - - return this; - } - - public addInputBlock(block: InputBlockParam): BlockBuilder { - this.addBlock({ type: BlockType.INPUT, ...block } as IInputBlock); - - return this; - } - - public addConditionalBlock(innerBlocks: BlockBuilder | Array, condition?: IConditionalBlockFilters): BlockBuilder { - const render = innerBlocks instanceof BlockBuilder ? innerBlocks.getBlocks() : innerBlocks; - - this.addBlock({ - type: BlockType.CONDITIONAL, - render, - when: condition, - } as IConditionalBlock); - - return this; - } - - public getBlocks() { - return this.blocks; - } - - public newPlainTextObject(text: string, emoji = false): ITextObject { - return { - type: TextObjectType.PLAINTEXT, - text, - emoji, - }; - } - - public newMarkdownTextObject(text: string): ITextObject { - return { - type: TextObjectType.MARKDOWN, - text, - }; - } - - public newButtonElement(info: ButtonElementParam): IButtonElement { - return this.newInteractiveElement({ - type: BlockElementType.BUTTON, - ...info, - } as IButtonElement); - } - - public newImageElement(info: ImageElementParam): IImageElement { - return { - type: BlockElementType.IMAGE, - ...info, - }; - } - - public newOverflowMenuElement(info: OverflowMenuElementParam): IOverflowMenuElement { - return this.newInteractiveElement({ - type: BlockElementType.OVERFLOW_MENU, - ...info, - } as IOverflowMenuElement); - } - - public newPlainTextInputElement(info: PlainTextInputElementParam): IPlainTextInputElement { - return this.newInputElement({ - type: BlockElementType.PLAIN_TEXT_INPUT, - ...info, - } as IPlainTextInputElement); - } - - public newStaticSelectElement(info: StaticSelectElementParam): IStaticSelectElement { - return this.newSelectElement({ - type: BlockElementType.STATIC_SELECT, - ...info, - } as IStaticSelectElement); - } - - public newMultiStaticElement(info: MultiStaticSelectElementParam): IMultiStaticSelectElement { - return this.newSelectElement({ - type: BlockElementType.MULTI_STATIC_SELECT, - ...info, - } as IMultiStaticSelectElement); - } - - private newInteractiveElement(element: T): T { - if (!element.actionId) { - element.actionId = this.generateActionId(); - } - - return element; - } - - private newInputElement(element: T): T { - if (!element.actionId) { - element.actionId = this.generateActionId(); - } - - return element; - } - - private newSelectElement(element: T): T { - if (!element.actionId) { - element.actionId = this.generateActionId(); - } - - return element; - } - - private addBlock(block: IBlock): void { - if (!block.blockId) { - block.blockId = this.generateBlockId(); - } - - block.appId = this.appId; - - this.blocks.push(block); - } - - private generateBlockId(): string { - return uuid(); - } - - private generateActionId(): string { - return uuid(); - } -} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts deleted file mode 100644 index adbf060182e1d..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { IDiscussionBuilder as _IDiscussionBuilder } from '@rocket.chat/apps-engine/definition/accessors/IDiscussionBuilder.ts'; -import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; -import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; -import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts'; - -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; -import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; - -import { RoomBuilder } from './RoomBuilder.ts'; -import { require } from '../../../lib/require.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - -const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; - -export interface IDiscussionBuilder extends _IDiscussionBuilder, IRoomBuilder {} - -export class DiscussionBuilder extends RoomBuilder implements IDiscussionBuilder { - public kind: _RocketChatAssociationModel.DISCUSSION; - - private reply?: string; - - private parentMessage?: IMessage; - - constructor(data?: Partial) { - super(data); - this.kind = RocketChatAssociationModel.DISCUSSION; - this.room.type = RoomType.PRIVATE_GROUP; - } - - public setParentRoom(parentRoom: IRoom): IDiscussionBuilder { - this.room.parentRoom = parentRoom; - return this; - } - - public getParentRoom(): IRoom { - return this.room.parentRoom!; - } - - public setReply(reply: string): IDiscussionBuilder { - this.reply = reply; - return this; - } - - public getReply(): string { - return this.reply!; - } - - public setParentMessage(parentMessage: IMessage): IDiscussionBuilder { - this.parentMessage = parentMessage; - return this; - } - - public getParentMessage(): IMessage { - return this.parentMessage!; - } -} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts deleted file mode 100644 index b39a418c5aec0..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts +++ /dev/null @@ -1,204 +0,0 @@ -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; -import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; - -import type { ILivechatMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/ILivechatMessageBuilder.ts'; -import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; -import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment.ts'; -import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; -import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; -import type { ILivechatMessage as EngineLivechatMessage } from '@rocket.chat/apps-engine/definition/livechat/ILivechatMessage.ts'; -import type { IVisitor } from '@rocket.chat/apps-engine/definition/livechat/IVisitor.ts'; -import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts'; - -import { MessageBuilder } from './MessageBuilder.ts'; -import { require } from '../../../lib/require.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - -const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; - -export interface ILivechatMessage extends EngineLivechatMessage, IMessage {} - -export class LivechatMessageBuilder implements ILivechatMessageBuilder { - public kind: _RocketChatAssociationModel.LIVECHAT_MESSAGE; - - private msg: ILivechatMessage; - - constructor(message?: ILivechatMessage) { - this.kind = RocketChatAssociationModel.LIVECHAT_MESSAGE; - this.msg = message || ({} as ILivechatMessage); - } - - public setData(data: ILivechatMessage): ILivechatMessageBuilder { - delete data.id; - this.msg = data; - - return this; - } - - public setRoom(room: IRoom): ILivechatMessageBuilder { - this.msg.room = room; - return this; - } - - public getRoom(): IRoom { - return this.msg.room; - } - - public setSender(sender: IUser): ILivechatMessageBuilder { - this.msg.sender = sender; - delete this.msg.visitor; - - return this; - } - - public getSender(): IUser { - return this.msg.sender; - } - - public setText(text: string): ILivechatMessageBuilder { - this.msg.text = text; - return this; - } - - public getText(): string { - return this.msg.text!; - } - - public setEmojiAvatar(emoji: string): ILivechatMessageBuilder { - this.msg.emoji = emoji; - return this; - } - - public getEmojiAvatar(): string { - return this.msg.emoji!; - } - - public setAvatarUrl(avatarUrl: string): ILivechatMessageBuilder { - this.msg.avatarUrl = avatarUrl; - return this; - } - - public getAvatarUrl(): string { - return this.msg.avatarUrl!; - } - - public setUsernameAlias(alias: string): ILivechatMessageBuilder { - this.msg.alias = alias; - return this; - } - - public getUsernameAlias(): string { - return this.msg.alias!; - } - - public addAttachment(attachment: IMessageAttachment): ILivechatMessageBuilder { - if (!this.msg.attachments) { - this.msg.attachments = []; - } - - this.msg.attachments.push(attachment); - return this; - } - - public setAttachments(attachments: Array): ILivechatMessageBuilder { - this.msg.attachments = attachments; - return this; - } - - public getAttachments(): Array { - return this.msg.attachments!; - } - - public replaceAttachment(position: number, attachment: IMessageAttachment): ILivechatMessageBuilder { - if (!this.msg.attachments) { - this.msg.attachments = []; - } - - if (!this.msg.attachments[position]) { - throw new Error(`No attachment found at the index of "${position}" to replace.`); - } - - this.msg.attachments[position] = attachment; - return this; - } - - public removeAttachment(position: number): ILivechatMessageBuilder { - if (!this.msg.attachments) { - this.msg.attachments = []; - } - - if (!this.msg.attachments[position]) { - throw new Error(`No attachment found at the index of "${position}" to remove.`); - } - - this.msg.attachments.splice(position, 1); - - return this; - } - - public setEditor(user: IUser): ILivechatMessageBuilder { - this.msg.editor = user; - return this; - } - - public getEditor(): IUser { - return this.msg.editor; - } - - public setGroupable(groupable: boolean): ILivechatMessageBuilder { - this.msg.groupable = groupable; - return this; - } - - public getGroupable(): boolean { - return this.msg.groupable!; - } - - public setParseUrls(parseUrls: boolean): ILivechatMessageBuilder { - this.msg.parseUrls = parseUrls; - return this; - } - - public getParseUrls(): boolean { - return this.msg.parseUrls!; - } - - public setToken(token: string): ILivechatMessageBuilder { - this.msg.token = token; - return this; - } - - public getToken(): string { - return this.msg.token!; - } - - public setVisitor(visitor: IVisitor): ILivechatMessageBuilder { - this.msg.visitor = visitor; - delete this.msg.sender; - - return this; - } - - public getVisitor(): IVisitor { - return this.msg.visitor; - } - - public getMessage(): ILivechatMessage { - if (!this.msg.room) { - throw new Error('The "room" property is required.'); - } - - if (this.msg.room.type !== RoomType.LIVE_CHAT) { - throw new Error('The room is not a Livechat room'); - } - - return this.msg; - } - - public getMessageBuilder(): IMessageBuilder { - return new MessageBuilder(this.msg as IMessage); - } -} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts deleted file mode 100644 index 032b4ba2552e9..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { LayoutBlock } from '@rocket.chat/ui-kit'; - -import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; -import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; -import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment.ts'; -import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; -import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; -import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks.ts'; - -import { BlockBuilder } from './BlockBuilder.ts'; -import { require } from '../../../lib/require.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - -export class MessageBuilder implements IMessageBuilder { - public kind: _RocketChatAssociationModel.MESSAGE; - - private msg: IMessage; - - private changes: Partial = {}; - private attachmentsChanged = false; - private customFieldsChanged = false; - - constructor(message?: IMessage) { - this.kind = RocketChatAssociationModel.MESSAGE; - this.msg = message || ({} as IMessage); - } - - public setData(data: IMessage): IMessageBuilder { - delete data.id; - this.msg = data; - - return this as IMessageBuilder; - } - - public setUpdateData(data: IMessage, editor: IUser): IMessageBuilder { - this.msg = data; - this.msg.editor = editor; - this.msg.editedAt = new Date(); - - this.changes = structuredClone(this.msg); - - return this as IMessageBuilder; - } - - public setThreadId(threadId: string): IMessageBuilder { - this.msg.threadId = threadId; - this.changes.threadId = threadId; - - return this as IMessageBuilder; - } - - public getThreadId(): string { - return this.msg.threadId!; - } - - public setRoom(room: IRoom): IMessageBuilder { - this.msg.room = room; - this.changes.room = room; - - return this as IMessageBuilder; - } - - public getRoom(): IRoom { - return this.msg.room; - } - - public setSender(sender: IUser): IMessageBuilder { - this.msg.sender = sender; - this.changes.sender = sender; - - return this as IMessageBuilder; - } - - public getSender(): IUser { - return this.msg.sender; - } - - public setText(text: string): IMessageBuilder { - this.msg.text = text; - this.changes.text = text; - - return this as IMessageBuilder; - } - - public getText(): string { - return this.msg.text!; - } - - public setEmojiAvatar(emoji: string): IMessageBuilder { - this.msg.emoji = emoji; - this.changes.emoji = emoji; - - return this as IMessageBuilder; - } - - public getEmojiAvatar(): string { - return this.msg.emoji!; - } - - public setAvatarUrl(avatarUrl: string): IMessageBuilder { - this.msg.avatarUrl = avatarUrl; - this.changes.avatarUrl = avatarUrl; - - return this as IMessageBuilder; - } - - public getAvatarUrl(): string { - return this.msg.avatarUrl!; - } - - public setUsernameAlias(alias: string): IMessageBuilder { - this.msg.alias = alias; - this.changes.alias = alias; - - return this as IMessageBuilder; - } - - public getUsernameAlias(): string { - return this.msg.alias!; - } - - public addAttachment(attachment: IMessageAttachment): IMessageBuilder { - if (!this.msg.attachments) { - this.msg.attachments = []; - } - - this.msg.attachments.push(attachment); - this.attachmentsChanged = true; - - return this as IMessageBuilder; - } - - public setAttachments(attachments: Array): IMessageBuilder { - this.msg.attachments = attachments; - this.attachmentsChanged = true; - - return this as IMessageBuilder; - } - - public getAttachments(): Array { - return this.msg.attachments!; - } - - public replaceAttachment(position: number, attachment: IMessageAttachment): IMessageBuilder { - if (!this.msg.attachments?.[position]) { - throw new Error(`No attachment found at the index of "${position}" to replace.`); - } - - this.msg.attachments[position] = attachment; - this.attachmentsChanged = true; - - return this as IMessageBuilder; - } - - public removeAttachment(position: number): IMessageBuilder { - if (!this.msg.attachments?.[position]) { - throw new Error(`No attachment found at the index of "${position}" to remove.`); - } - - this.msg.attachments.splice(position, 1); - this.attachmentsChanged = true; - - return this as IMessageBuilder; - } - - public setEditor(user: IUser): IMessageBuilder { - this.msg.editor = user; - this.changes.editor = user; - - return this as IMessageBuilder; - } - - public getEditor(): IUser { - return this.msg.editor; - } - - public setGroupable(groupable: boolean): IMessageBuilder { - this.msg.groupable = groupable; - this.changes.groupable = groupable; - - return this as IMessageBuilder; - } - - public getGroupable(): boolean { - return this.msg.groupable!; - } - - public setParseUrls(parseUrls: boolean): IMessageBuilder { - this.msg.parseUrls = parseUrls; - this.changes.parseUrls = parseUrls; - - return this as IMessageBuilder; - } - - public getParseUrls(): boolean { - return this.msg.parseUrls!; - } - - public getMessage(): IMessage { - if (!this.msg.room) { - throw new Error('The "room" property is required.'); - } - - return this.msg; - } - - public addBlocks(blocks: BlockBuilder | Array) { - if (!Array.isArray(this.msg.blocks)) { - this.msg.blocks = []; - } - - if (blocks instanceof BlockBuilder) { - this.msg.blocks.push(...blocks.getBlocks()); - } else { - this.msg.blocks.push(...blocks); - } - - return this as IMessageBuilder; - } - - public setBlocks(blocks: BlockBuilder | Array) { - const blockArray: Array = blocks instanceof BlockBuilder ? blocks.getBlocks() : blocks; - - this.msg.blocks = blockArray; - this.changes.blocks = blockArray; - - return this as IMessageBuilder; - } - - public getBlocks() { - return this.msg.blocks!; - } - - public addCustomField(key: string, value: unknown): IMessageBuilder { - if (!this.msg.customFields) { - this.msg.customFields = {}; - } - - if (this.msg.customFields[key]) { - throw new Error(`The message already contains a custom field by the key: ${key}`); - } - - if (key.includes('.')) { - throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`); - } - - this.msg.customFields[key] = value; - - this.customFieldsChanged = true; - - return this as IMessageBuilder; - } - - public getChanges(): Partial { - const changes: typeof this.changes = structuredClone(this.changes); - - if (this.attachmentsChanged) { - changes.attachments = structuredClone(this.msg.attachments); - } - - if (this.customFieldsChanged) { - changes.customFields = structuredClone(this.msg.customFields); - } - - return changes; - } -} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/builders/RoomBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/RoomBuilder.ts deleted file mode 100644 index 208d476d32162..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/builders/RoomBuilder.ts +++ /dev/null @@ -1,197 +0,0 @@ -import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts'; -import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; -import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; - -import type { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; - -import { require } from '../../../lib/require.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - -export class RoomBuilder implements IRoomBuilder { - public kind: _RocketChatAssociationModel.ROOM | _RocketChatAssociationModel.DISCUSSION; - - protected room: IRoom; - - private members: Array; - - private changes: Partial = {}; - private customFieldsChanged = false; - - constructor(data?: Partial) { - this.kind = RocketChatAssociationModel.ROOM; - this.room = (data || { customFields: {} }) as IRoom; - this.members = []; - } - - public setData(data: Partial): IRoomBuilder { - delete data.id; - this.room = data as IRoom; - - this.changes = structuredClone(this.room); - - return this; - } - - public setDisplayName(name: string): IRoomBuilder { - this.room.displayName = name; - this.changes.displayName = name; - - return this; - } - - public getDisplayName(): string { - return this.room.displayName!; - } - - public setSlugifiedName(name: string): IRoomBuilder { - this.room.slugifiedName = name; - this.changes.slugifiedName = name; - - return this; - } - - public getSlugifiedName(): string { - return this.room.slugifiedName; - } - - public setType(type: RoomType): IRoomBuilder { - this.room.type = type; - this.changes.type = type; - - return this; - } - - public getType(): RoomType { - return this.room.type; - } - - public setCreator(creator: IUser): IRoomBuilder { - this.room.creator = creator; - this.changes.creator = creator; - - return this; - } - - public getCreator(): IUser { - return this.room.creator; - } - - /** - * @deprecated - */ - public addUsername(username: string): IRoomBuilder { - this.addMemberToBeAddedByUsername(username); - return this; - } - - /** - * @deprecated - */ - public setUsernames(usernames: Array): IRoomBuilder { - this.setMembersToBeAddedByUsernames(usernames); - return this; - } - - /** - * @deprecated - */ - public getUsernames(): Array { - const usernames = this.getMembersToBeAddedUsernames(); - if (usernames && usernames.length > 0) { - return usernames; - } - return this.room.usernames || []; - } - - public addMemberToBeAddedByUsername(username: string): IRoomBuilder { - this.members.push(username); - return this; - } - - public setMembersToBeAddedByUsernames(usernames: Array): IRoomBuilder { - this.members = usernames; - return this; - } - - public getMembersToBeAddedUsernames(): Array { - return this.members; - } - - public setDefault(isDefault: boolean): IRoomBuilder { - this.room.isDefault = isDefault; - this.changes.isDefault = isDefault; - - return this; - } - - public getIsDefault(): boolean { - return this.room.isDefault!; - } - - public setReadOnly(isReadOnly: boolean): IRoomBuilder { - this.room.isReadOnly = isReadOnly; - this.changes.isReadOnly = isReadOnly; - - return this; - } - - public getIsReadOnly(): boolean { - return this.room.isReadOnly!; - } - - public setDisplayingOfSystemMessages(displaySystemMessages: boolean): IRoomBuilder { - this.room.displaySystemMessages = displaySystemMessages; - this.changes.displaySystemMessages = displaySystemMessages; - - return this; - } - - public getDisplayingOfSystemMessages(): boolean { - return this.room.displaySystemMessages!; - } - - public addCustomField(key: string, value: object): IRoomBuilder { - if (typeof this.room.customFields !== 'object') { - this.room.customFields = {}; - } - - this.room.customFields[key] = value; - - this.customFieldsChanged = true; - - return this; - } - - public setCustomFields(fields: { [key: string]: object }): IRoomBuilder { - this.room.customFields = fields; - this.customFieldsChanged = true; - - return this; - } - - public getCustomFields(): { [key: string]: object } { - return this.room.customFields!; - } - - public getUserIds(): Array { - return this.room.userIds!; - } - - public getRoom(): IRoom { - return this.room; - } - - public getChanges() { - const changes: Partial = structuredClone(this.changes); - - if (this.customFieldsChanged) { - changes.customFields = structuredClone(this.room.customFields); - } - - return changes; - } -} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/builders/UserBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/UserBuilder.ts deleted file mode 100644 index caaf9a69d5941..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/builders/UserBuilder.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { IUserBuilder } from '@rocket.chat/apps-engine/definition/accessors/IUserBuilder.ts'; -import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; -import type { IUserSettings } from '@rocket.chat/apps-engine/definition/users/IUserSettings.ts'; -import type { IUserEmail } from '@rocket.chat/apps-engine/definition/users/IUserEmail.ts'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; - -import { require } from '../../../lib/require.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - -export class UserBuilder implements IUserBuilder { - public kind: _RocketChatAssociationModel.USER; - - private user: Partial; - - constructor(user?: Partial) { - this.kind = RocketChatAssociationModel.USER; - this.user = user || ({} as Partial); - } - - public setData(data: Partial): IUserBuilder { - delete data.id; - this.user = data; - - return this; - } - - public setEmails(emails: Array): IUserBuilder { - this.user.emails = emails; - return this; - } - - public getEmails(): Array { - return this.user.emails!; - } - - public setDisplayName(name: string): IUserBuilder { - this.user.name = name; - return this; - } - - public getDisplayName(): string { - return this.user.name!; - } - - public setUsername(username: string): IUserBuilder { - this.user.username = username; - return this; - } - - public getUsername(): string { - return this.user.username!; - } - - public setRoles(roles: Array): IUserBuilder { - this.user.roles = roles; - return this; - } - - public getRoles(): Array { - return this.user.roles!; - } - - public getSettings(): Partial { - return this.user.settings; - } - - public getUser(): Partial { - if (!this.user.username) { - throw new Error('The "username" property is required.'); - } - - if (!this.user.name) { - throw new Error('The "name" property is required.'); - } - - return this.user; - } -} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts deleted file mode 100644 index e1bc3f3cf5b24..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceBuilder.ts'; -import type { IGroupVideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference.ts'; - -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; - -import { require } from '../../../lib/require.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - -export type AppVideoConference = Pick & { - createdBy: IGroupVideoConference['createdBy']['_id']; -}; - -export class VideoConferenceBuilder implements IVideoConferenceBuilder { - public kind: _RocketChatAssociationModel.VIDEO_CONFERENCE = RocketChatAssociationModel.VIDEO_CONFERENCE; - - protected call: AppVideoConference; - - constructor(data?: Partial) { - this.call = (data || {}) as AppVideoConference; - } - - public setData(data: Partial): IVideoConferenceBuilder { - this.call = { - rid: data.rid!, - createdBy: data.createdBy, - providerName: data.providerName!, - title: data.title!, - discussionRid: data.discussionRid, - }; - - return this; - } - - public setRoomId(rid: string): IVideoConferenceBuilder { - this.call.rid = rid; - return this; - } - - public getRoomId(): string { - return this.call.rid; - } - - public setCreatedBy(userId: string): IVideoConferenceBuilder { - this.call.createdBy = userId; - return this; - } - - public getCreatedBy(): string { - return this.call.createdBy; - } - - public setProviderName(userId: string): IVideoConferenceBuilder { - this.call.providerName = userId; - return this; - } - - public getProviderName(): string { - return this.call.providerName; - } - - public setProviderData(data: Record | undefined): IVideoConferenceBuilder { - this.call.providerData = data; - return this; - } - - public getProviderData(): Record { - return this.call.providerData!; - } - - public setTitle(userId: string): IVideoConferenceBuilder { - this.call.title = userId; - return this; - } - - public getTitle(): string { - return this.call.title; - } - - public setDiscussionRid(rid: AppVideoConference['discussionRid']): IVideoConferenceBuilder { - this.call.discussionRid = rid; - return this; - } - - public getDiscussionRid(): AppVideoConference['discussionRid'] { - return this.call.discussionRid; - } - - public getVideoConference(): AppVideoConference { - return this.call; - } -} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/extenders/HttpExtender.ts b/packages/apps-engine/deno-runtime/lib/accessors/extenders/HttpExtender.ts deleted file mode 100644 index 8342850975017..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/extenders/HttpExtender.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { IHttpExtend, IHttpPreRequestHandler, IHttpPreResponseHandler } from '@rocket.chat/apps-engine/definition/accessors/IHttp.ts'; - -export class HttpExtend implements IHttpExtend { - private headers: Map; - - private params: Map; - - private requests: Array; - - private responses: Array; - - constructor() { - this.headers = new Map(); - this.params = new Map(); - this.requests = []; - this.responses = []; - } - - public provideDefaultHeader(key: string, value: string): void { - this.headers.set(key, value); - } - - public provideDefaultHeaders(headers: { [key: string]: string }): void { - Object.keys(headers).forEach((key) => this.headers.set(key, headers[key])); - } - - public provideDefaultParam(key: string, value: string): void { - this.params.set(key, value); - } - - public provideDefaultParams(params: { [key: string]: string }): void { - Object.keys(params).forEach((key) => this.params.set(key, params[key])); - } - - public providePreRequestHandler(handler: IHttpPreRequestHandler): void { - this.requests.push(handler); - } - - public providePreResponseHandler(handler: IHttpPreResponseHandler): void { - this.responses.push(handler); - } - - public getDefaultHeaders(): Map { - return new Map(this.headers); - } - - public getDefaultParams(): Map { - return new Map(this.params); - } - - public getPreRequestHandlers(): Array { - return Array.from(this.requests); - } - - public getPreResponseHandlers(): Array { - return Array.from(this.responses); - } -} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/extenders/MessageExtender.ts b/packages/apps-engine/deno-runtime/lib/accessors/extenders/MessageExtender.ts deleted file mode 100644 index 1f45137e15d4b..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/extenders/MessageExtender.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { IMessageExtender } from '@rocket.chat/apps-engine/definition/accessors/IMessageExtender.ts'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; -import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; -import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment.ts'; - -import { require } from '../../../lib/require.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - -export class MessageExtender implements IMessageExtender { - public readonly kind: _RocketChatAssociationModel.MESSAGE; - - constructor(private msg: IMessage) { - this.kind = RocketChatAssociationModel.MESSAGE; - - if (!Array.isArray(msg.attachments)) { - this.msg.attachments = []; - } - } - - public addCustomField(key: string, value: unknown): IMessageExtender { - if (!this.msg.customFields) { - this.msg.customFields = {}; - } - - if (this.msg.customFields[key]) { - throw new Error(`The message already contains a custom field by the key: ${key}`); - } - - if (key.includes('.')) { - throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`); - } - - this.msg.customFields[key] = value; - - return this; - } - - public addAttachment(attachment: IMessageAttachment): IMessageExtender { - this.ensureAttachment(); - - this.msg.attachments!.push(attachment); - - return this; - } - - public addAttachments(attachments: Array): IMessageExtender { - this.ensureAttachment(); - - this.msg.attachments = this.msg.attachments!.concat(attachments); - - return this; - } - - public getMessage(): IMessage { - return structuredClone(this.msg); - } - - private ensureAttachment(): void { - if (!Array.isArray(this.msg.attachments)) { - this.msg.attachments = []; - } - } -} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/extenders/RoomExtender.ts b/packages/apps-engine/deno-runtime/lib/accessors/extenders/RoomExtender.ts deleted file mode 100644 index a138e08d2d28e..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/extenders/RoomExtender.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { IRoomExtender } from '@rocket.chat/apps-engine/definition/accessors/IRoomExtender.ts'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; -import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; -import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; - -import { require } from '../../../lib/require.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - -export class RoomExtender implements IRoomExtender { - public kind: _RocketChatAssociationModel.ROOM; - - private members: Array; - - constructor(private room: IRoom) { - this.kind = RocketChatAssociationModel.ROOM; - this.members = []; - } - - public addCustomField(key: string, value: unknown): IRoomExtender { - if (!this.room.customFields) { - this.room.customFields = {}; - } - - if (this.room.customFields[key]) { - throw new Error(`The room already contains a custom field by the key: ${key}`); - } - - if (key.includes('.')) { - throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`); - } - - this.room.customFields[key] = value; - - return this; - } - - public addMember(user: IUser): IRoomExtender { - if (this.members.find((u) => u.username === user.username)) { - throw new Error('The user is already in the room.'); - } - - this.members.push(user); - - return this; - } - - public getMembersBeingAdded(): Array { - return this.members; - } - - public getUsernamesOfMembersBeingAdded(): Array { - return this.members.map((u) => u.username); - } - - public getRoom(): IRoom { - return structuredClone(this.room); - } -} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts b/packages/apps-engine/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts deleted file mode 100644 index c4b154f46cb32..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { IVideoConferenceExtender } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceExtend.ts'; -import type { VideoConference, VideoConferenceMember } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference.ts'; -import type { IVideoConferenceUser } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConferenceUser.ts'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; - -import { require } from '../../../lib/require.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - -export class VideoConferenceExtender implements IVideoConferenceExtender { - public kind: _RocketChatAssociationModel.VIDEO_CONFERENCE; - - constructor(private videoConference: VideoConference) { - this.kind = RocketChatAssociationModel.VIDEO_CONFERENCE; - } - - public setProviderData(value: Record): IVideoConferenceExtender { - this.videoConference.providerData = value; - - return this; - } - - public setStatus(value: VideoConference['status']): IVideoConferenceExtender { - this.videoConference.status = value; - - return this; - } - - public setEndedBy(value: IVideoConferenceUser['_id']): IVideoConferenceExtender { - this.videoConference.endedBy = { - _id: value, - // Name and username will be loaded automatically by the bridge - username: '', - name: '', - }; - - return this; - } - - public setEndedAt(value: VideoConference['endedAt']): IVideoConferenceExtender { - this.videoConference.endedAt = value; - - return this; - } - - public addUser(userId: VideoConferenceMember['_id'], ts?: VideoConferenceMember['ts']): IVideoConferenceExtender { - this.videoConference.users.push({ - _id: userId, - ts, - // Name and username will be loaded automatically by the bridge - username: '', - name: '', - }); - - return this; - } - - public setDiscussionRid(rid: VideoConference['discussionRid']): IVideoConferenceExtender { - this.videoConference.discussionRid = rid; - - return this; - } - - public getVideoConference(): VideoConference { - return structuredClone(this.videoConference); - } -} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/formatResponseErrorHandler.ts b/packages/apps-engine/deno-runtime/lib/accessors/formatResponseErrorHandler.ts deleted file mode 100644 index 6840c3ab5baa3..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/formatResponseErrorHandler.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ErrorObject } from 'jsonrpc-lite'; - -// deno-lint-ignore no-explicit-any -- that is the type we get from `catch` -export const formatErrorResponse = (error: any): Error => { - if (error instanceof ErrorObject || typeof error?.error?.message === 'string') { - return new Error(error.error.message); - } - - if (error instanceof Error) { - return error; - } - - return new Error('An unknown error occurred', { cause: error }); -}; diff --git a/packages/apps-engine/deno-runtime/lib/accessors/http.ts b/packages/apps-engine/deno-runtime/lib/accessors/http.ts deleted file mode 100644 index 41f1025150fdc..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/http.ts +++ /dev/null @@ -1,92 +0,0 @@ -import type { IHttp, IHttpExtend, IHttpRequest, IHttpResponse } from '@rocket.chat/apps-engine/definition/accessors/IHttp.ts'; -import type { IPersistence } from '@rocket.chat/apps-engine/definition/accessors/IPersistence.ts'; -import type { IRead } from '@rocket.chat/apps-engine/definition/accessors/IRead.ts'; - -import * as Messenger from '../messenger.ts'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { formatErrorResponse } from './formatResponseErrorHandler.ts'; - -type RequestMethod = 'get' | 'post' | 'put' | 'head' | 'delete' | 'patch'; - -export class Http implements IHttp { - private httpExtender: IHttpExtend; - private read: IRead; - private persistence: IPersistence; - private senderFn: typeof Messenger.sendRequest; - - constructor(read: IRead, persistence: IPersistence, httpExtender: IHttpExtend, senderFn: typeof Messenger.sendRequest) { - this.read = read; - this.persistence = persistence; - this.httpExtender = httpExtender; - this.senderFn = senderFn; - // this.httpExtender = new HttpExtend(); - } - - public get(url: string, options?: IHttpRequest): Promise { - return this._processHandler(url, 'get', options); - } - - public put(url: string, options?: IHttpRequest): Promise { - return this._processHandler(url, 'put', options); - } - - public post(url: string, options?: IHttpRequest): Promise { - return this._processHandler(url, 'post', options); - } - - public del(url: string, options?: IHttpRequest): Promise { - return this._processHandler(url, 'delete', options); - } - - public patch(url: string, options?: IHttpRequest): Promise { - return this._processHandler(url, 'patch', options); - } - - private async _processHandler(url: string, method: RequestMethod, options?: IHttpRequest): Promise { - let request = options || {}; - - if (typeof request.headers === 'undefined') { - request.headers = {}; - } - - this.httpExtender.getDefaultHeaders().forEach((value: string, key: string) => { - if (typeof request.headers?.[key] !== 'string') { - request.headers![key] = value; - } - }); - - if (typeof request.params === 'undefined') { - request.params = {}; - } - - this.httpExtender.getDefaultParams().forEach((value: string, key: string) => { - if (typeof request.params?.[key] !== 'string') { - request.params![key] = value; - } - }); - - for (const handler of this.httpExtender.getPreRequestHandlers()) { - request = await handler.executePreHttpRequest(url, request, this.read, this.persistence); - } - - let { result: response } = await this.senderFn({ - method: `bridges:getHttpBridge:doCall`, - params: [ - { - appId: AppObjectRegistry.get('id'), - method, - url, - request, - }, - ], - }).catch((error) => { - throw formatErrorResponse(error); - }); - - for (const handler of this.httpExtender.getPreResponseHandlers()) { - response = await handler.executePreHttpResponse(response as IHttpResponse, this.read, this.persistence); - } - - return response as IHttpResponse; - } -} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/mod.ts b/packages/apps-engine/deno-runtime/lib/accessors/mod.ts deleted file mode 100644 index fc2fb6a3f6669..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/mod.ts +++ /dev/null @@ -1,322 +0,0 @@ -import type { IAppAccessors } from '@rocket.chat/apps-engine/definition/accessors/IAppAccessors.ts'; -import type { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api/IApiEndpointMetadata.ts'; -import type { IEnvironmentWrite } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentWrite.ts'; -import type { IEnvironmentRead } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentRead.ts'; -import type { IConfigurationModify } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationModify.ts'; -import type { IRead } from '@rocket.chat/apps-engine/definition/accessors/IRead.ts'; -import type { IModify } from '@rocket.chat/apps-engine/definition/accessors/IModify.ts'; -import type { INotifier } from '@rocket.chat/apps-engine/definition/accessors/INotifier.ts'; -import type { IPersistence } from '@rocket.chat/apps-engine/definition/accessors/IPersistence.ts'; -import type { IHttp, IHttpExtend } from '@rocket.chat/apps-engine/definition/accessors/IHttp.ts'; -import type { IConfigurationExtend } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationExtend.ts'; -import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands/ISlashCommand.ts'; -import type { IProcessor } from '@rocket.chat/apps-engine/definition/scheduler/IProcessor.ts'; -import type { IApi } from '@rocket.chat/apps-engine/definition/api/IApi.ts'; -import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders/IVideoConfProvider.ts'; -import type { - IOutboundPhoneMessageProvider, - IOutboundEmailMessageProvider, -} from '@rocket.chat/apps-engine/definition/outboundCommunication/IOutboundCommsProvider.ts'; - -import { Http } from './http.ts'; -import { HttpExtend } from './extenders/HttpExtender.ts'; -import * as Messenger from '../messenger.ts'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { ModifyCreator } from './modify/ModifyCreator.ts'; -import { ModifyUpdater } from './modify/ModifyUpdater.ts'; -import { ModifyExtender } from './modify/ModifyExtender.ts'; -import { Notifier } from './notifier.ts'; -import { formatErrorResponse } from './formatResponseErrorHandler.ts'; - -const httpMethods = ['get', 'post', 'put', 'delete', 'head', 'options', 'patch'] as const; - -// We need to create this object first thing, as we'll handle references to it later on -if (!AppObjectRegistry.has('apiEndpoints')) { - AppObjectRegistry.set('apiEndpoints', []); -} - -export class AppAccessors { - private defaultAppAccessors?: IAppAccessors; - private environmentRead?: IEnvironmentRead; - private environmentWriter?: IEnvironmentWrite; - private configModifier?: IConfigurationModify; - private configExtender?: IConfigurationExtend; - private reader?: IRead; - private modifier?: IModify; - private persistence?: IPersistence; - private creator?: ModifyCreator; - private updater?: ModifyUpdater; - private extender?: ModifyExtender; - private httpExtend: IHttpExtend = new HttpExtend(); - private http?: IHttp; - private notifier?: INotifier; - - private proxify: (namespace: string, overrides?: Record unknown>) => T; - - constructor(private readonly senderFn: typeof Messenger.sendRequest) { - this.proxify = (namespace: string, overrides: Record unknown> = {}): T => - new Proxy( - { __kind: `accessor:${namespace}` }, - { - get: - (_target: unknown, prop: string) => - (...params: unknown[]) => { - // We don't want to send a request for this prop - if (prop === 'toJSON') { - return {}; - } - - // If the prop is inteded to be overriden by the caller - if (prop in overrides) { - return overrides[prop].apply(undefined, params); - } - - return senderFn({ - method: `accessor:${namespace}:${prop}`, - params, - }) - .then((response) => response.result) - .catch((err) => { - throw formatErrorResponse(err); - }); - }, - }, - ) as T; - - this.http = new Http(this.getReader(), this.getPersistence(), this.httpExtend, this.getSenderFn()); - this.notifier = new Notifier(this.getSenderFn()); - } - - public getSenderFn() { - return this.senderFn; - } - - public getEnvironmentRead(): IEnvironmentRead { - if (!this.environmentRead) { - this.environmentRead = { - getSettings: () => this.proxify('getEnvironmentRead:getSettings'), - getServerSettings: () => this.proxify('getEnvironmentRead:getServerSettings'), - getEnvironmentVariables: () => this.proxify('getEnvironmentRead:getEnvironmentVariables'), - }; - } - - return this.environmentRead; - } - - public getEnvironmentWrite() { - if (!this.environmentWriter) { - this.environmentWriter = { - getSettings: () => this.proxify('getEnvironmentWrite:getSettings'), - getServerSettings: () => this.proxify('getEnvironmentWrite:getServerSettings'), - }; - } - - return this.environmentWriter; - } - - public getConfigurationModify() { - if (!this.configModifier) { - this.configModifier = { - scheduler: this.proxify('getConfigurationModify:scheduler'), - slashCommands: { - _proxy: this.proxify('getConfigurationModify:slashCommands'), - modifySlashCommand(slashcommand: ISlashCommand) { - // Store the slashcommand instance to use when the Apps-Engine calls the slashcommand - AppObjectRegistry.set(`slashcommand:${slashcommand.command}`, slashcommand); - - return this._proxy.modifySlashCommand(slashcommand); - }, - disableSlashCommand(command: string) { - return this._proxy.disableSlashCommand(command); - }, - enableSlashCommand(command: string) { - return this._proxy.enableSlashCommand(command); - }, - }, - serverSettings: this.proxify('getConfigurationModify:serverSettings'), - }; - } - - return this.configModifier; - } - - public getConfigurationExtend() { - if (!this.configExtender) { - const senderFn = this.senderFn; - - this.configExtender = { - ui: this.proxify('getConfigurationExtend:ui'), - http: this.httpExtend, - settings: this.proxify('getConfigurationExtend:settings'), - externalComponents: this.proxify('getConfigurationExtend:externalComponents'), - api: { - _proxy: this.proxify('getConfigurationExtend:api'), - async provideApi(api: IApi) { - const apiEndpoints = AppObjectRegistry.get('apiEndpoints')!; - - api.endpoints.forEach((endpoint) => { - endpoint._availableMethods = httpMethods.filter((method) => typeof endpoint[method] === 'function'); - - // We need to keep a reference to the endpoint around for us to call the executor later - AppObjectRegistry.set(`api:${endpoint.path}`, endpoint); - }); - - const result = await this._proxy.provideApi(api); - - // Let's call the listApis method to cache the info from the endpoints - // Also, since this is a side-effect, we do it async so we can return to the caller - senderFn({ method: 'accessor:api:listApis' }) - .then((response) => apiEndpoints.push(...(response.result as IApiEndpointMetadata[]))) - .catch((err) => err.error); - - return result; - }, - }, - scheduler: { - _proxy: this.proxify('getConfigurationExtend:scheduler'), - registerProcessors(processors: IProcessor[]) { - // Store the processor instance to use when the Apps-Engine calls the processor - processors.forEach((processor) => { - AppObjectRegistry.set(`scheduler:${processor.id}`, processor); - }); - - return this._proxy.registerProcessors(processors); - }, - }, - videoConfProviders: { - _proxy: this.proxify('getConfigurationExtend:videoConfProviders'), - provideVideoConfProvider(provider: IVideoConfProvider) { - // Store the videoConfProvider instance to use when the Apps-Engine calls the videoConfProvider - AppObjectRegistry.set(`videoConfProvider:${provider.name}`, provider); - - return this._proxy.provideVideoConfProvider(provider); - }, - }, - outboundCommunication: { - _proxy: this.proxify('getConfigurationExtend:outboundCommunication'), - registerEmailProvider(provider: IOutboundEmailMessageProvider) { - AppObjectRegistry.set(`outboundCommunication:${provider.name}-${provider.type}`, provider); - return this._proxy.registerEmailProvider(provider); - }, - registerPhoneProvider(provider: IOutboundPhoneMessageProvider) { - AppObjectRegistry.set(`outboundCommunication:${provider.name}-${provider.type}`, provider); - return this._proxy.registerPhoneProvider(provider); - }, - }, - slashCommands: { - _proxy: this.proxify('getConfigurationExtend:slashCommands'), - provideSlashCommand(slashcommand: ISlashCommand) { - // Store the slashcommand instance to use when the Apps-Engine calls the slashcommand - AppObjectRegistry.set(`slashcommand:${slashcommand.command}`, slashcommand); - - return this._proxy.provideSlashCommand(slashcommand); - }, - }, - }; - } - - return this.configExtender; - } - - public getDefaultAppAccessors() { - if (!this.defaultAppAccessors) { - this.defaultAppAccessors = { - environmentReader: this.getEnvironmentRead(), - environmentWriter: this.getEnvironmentWrite(), - reader: this.getReader(), - http: this.getHttp(), - providedApiEndpoints: AppObjectRegistry.get('apiEndpoints') as IApiEndpointMetadata[], - }; - } - - return this.defaultAppAccessors; - } - - public getReader() { - if (!this.reader) { - this.reader = { - getEnvironmentReader: () => ({ - getSettings: () => this.proxify('getReader:getEnvironmentReader:getSettings'), - getServerSettings: () => this.proxify('getReader:getEnvironmentReader:getServerSettings'), - getEnvironmentVariables: () => this.proxify('getReader:getEnvironmentReader:getEnvironmentVariables'), - }), - getMessageReader: () => this.proxify('getReader:getMessageReader'), - getPersistenceReader: () => this.proxify('getReader:getPersistenceReader'), - getRoomReader: () => this.proxify('getReader:getRoomReader'), - getUserReader: () => this.proxify('getReader:getUserReader'), - getNotifier: () => this.getNotifier(), - getLivechatReader: () => this.proxify('getReader:getLivechatReader'), - getUploadReader: () => this.proxify('getReader:getUploadReader'), - getCloudWorkspaceReader: () => this.proxify('getReader:getCloudWorkspaceReader'), - getVideoConferenceReader: () => this.proxify('getReader:getVideoConferenceReader'), - getOAuthAppsReader: () => this.proxify('getReader:getOAuthAppsReader'), - getThreadReader: () => this.proxify('getReader:getThreadReader'), - getRoleReader: () => this.proxify('getReader:getRoleReader'), - getContactReader: () => this.proxify('getReader:getContactReader'), - getExperimentalReader: () => this.proxify('getReader:getExperimentalReader'), - }; - } - - return this.reader; - } - - public getModifier() { - if (!this.modifier) { - this.modifier = { - getCreator: this.getCreator.bind(this), - getUpdater: this.getUpdater.bind(this), - getExtender: this.getExtender.bind(this), - getDeleter: () => this.proxify('getModifier:getDeleter'), - getNotifier: () => this.getNotifier(), - getUiController: () => this.proxify('getModifier:getUiController'), - getScheduler: () => this.proxify('getModifier:getScheduler'), - getOAuthAppsModifier: () => this.proxify('getModifier:getOAuthAppsModifier'), - getModerationModifier: () => this.proxify('getModifier:getModerationModifier'), - }; - } - - return this.modifier; - } - - public getPersistence() { - if (!this.persistence) { - this.persistence = this.proxify('getPersistence'); - } - - return this.persistence; - } - - public getHttp() { - return this.http; - } - - private getCreator() { - if (!this.creator) { - this.creator = new ModifyCreator(this.senderFn); - } - - return this.creator; - } - - private getUpdater() { - if (!this.updater) { - this.updater = new ModifyUpdater(this.senderFn); - } - - return this.updater; - } - - private getExtender() { - if (!this.extender) { - this.extender = new ModifyExtender(this.senderFn); - } - - return this.extender; - } - - private getNotifier() { - return this.notifier; - } -} - -export const AppAccessorsInstance = new AppAccessors(Messenger.sendRequest); diff --git a/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts deleted file mode 100644 index d30e22c1be182..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts +++ /dev/null @@ -1,383 +0,0 @@ -import type { IModifyCreator } from '@rocket.chat/apps-engine/definition/accessors/IModifyCreator.ts'; -import type { IUploadCreator } from '@rocket.chat/apps-engine/definition/accessors/IUploadCreator.ts'; -import type { IEmailCreator } from '@rocket.chat/apps-engine/definition/accessors/IEmailCreator.ts'; -import type { IContactCreator } from '@rocket.chat/apps-engine/definition/accessors/IContactCreator.ts'; -import type { ILivechatCreator } from '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator.ts'; -import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; -import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; -import type { IBotUser } from '@rocket.chat/apps-engine/definition/users/IBotUser.ts'; -import type { UserType as _UserType } from '@rocket.chat/apps-engine/definition/users/UserType.ts'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; -import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts'; -import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts'; -import type { IUserBuilder } from '@rocket.chat/apps-engine/definition/accessors/IUserBuilder.ts'; -import type { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceBuilder.ts'; -import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; -import type { ILivechatMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/ILivechatMessageBuilder.ts'; -import type { UIHelper as _UIHelper } from '@rocket.chat/apps-engine/server/misc/UIHelper.ts'; - -import * as Messenger from '../../messenger.ts'; -import { randomBytes } from 'node:crypto'; - -import { BlockBuilder } from '../builders/BlockBuilder.ts'; -import { MessageBuilder } from '../builders/MessageBuilder.ts'; -import { DiscussionBuilder, IDiscussionBuilder } from '../builders/DiscussionBuilder.ts'; -import { ILivechatMessage, LivechatMessageBuilder } from '../builders/LivechatMessageBuilder.ts'; -import { RoomBuilder } from '../builders/RoomBuilder.ts'; -import { UserBuilder } from '../builders/UserBuilder.ts'; -import { AppVideoConference, VideoConferenceBuilder } from '../builders/VideoConferenceBuilder.ts'; -import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; -import { require } from '../../../lib/require.ts'; -import { formatErrorResponse } from '../formatResponseErrorHandler.ts'; - -const { UIHelper } = require('@rocket.chat/apps-engine/server/misc/UIHelper.js') as { UIHelper: typeof _UIHelper }; -const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; -const { UserType } = require('@rocket.chat/apps-engine/definition/users/UserType.js') as { UserType: typeof _UserType }; -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - -export class ModifyCreator implements IModifyCreator { - constructor(private readonly senderFn: typeof Messenger.sendRequest) {} - - getLivechatCreator(): ILivechatCreator { - return new Proxy( - { __kind: 'getLivechatCreator' }, - { - get: (_target: unknown, prop: string) => { - // It's not worthwhile to make an asynchronous request for such a simple method - if (prop === 'createToken') { - return () => randomBytes(16).toString('hex'); - } - - if (prop === 'toJSON') { - return () => ({}); - } - - return (...params: unknown[]) => - this.senderFn({ - method: `accessor:getModifier:getCreator:getLivechatCreator:${prop}`, - params, - }) - .then((response) => response.result) - .catch((err) => { - throw formatErrorResponse(err); - }); - }, - }, - ) as ILivechatCreator; - } - - getUploadCreator(): IUploadCreator { - return new Proxy( - { __kind: 'getUploadCreator' }, - { - get: - (_target: unknown, prop: string) => - (...params: unknown[]) => - prop === 'toJSON' - ? {} - : this.senderFn({ - method: `accessor:getModifier:getCreator:getUploadCreator:${prop}`, - params, - }) - .then((response) => response.result) - .catch((err) => { - throw formatErrorResponse(err); - }), - }, - ) as IUploadCreator; - } - - getEmailCreator(): IEmailCreator { - return new Proxy( - { __kind: 'getEmailCreator' }, - { - get: - (_target: unknown, prop: string) => - (...params: unknown[]) => - prop === 'toJSON' - ? {} - : this.senderFn({ - method: `accessor:getModifier:getCreator:getEmailCreator:${prop}`, - params, - }) - .then((response) => response.result) - .catch((err) => { - throw formatErrorResponse(err); - }), - }, - ); - } - - getContactCreator(): IContactCreator { - return new Proxy( - { __kind: 'getContactCreator' }, - { - get: - (_target: unknown, prop: string) => - (...params: unknown[]) => - prop === 'toJSON' - ? {} - : this.senderFn({ - method: `accessor:getModifier:getCreator:getContactCreator:${prop}`, - params, - }) - .then((response) => response.result) - .catch((err) => { - throw formatErrorResponse(err); - }), - }, - ); - } - - getBlockBuilder() { - return new BlockBuilder(); - } - - startMessage(data?: IMessage) { - if (data) { - delete data.id; - } - - return new MessageBuilder(data); - } - - startLivechatMessage(data?: ILivechatMessage) { - if (data) { - delete data.id; - } - - return new LivechatMessageBuilder(data); - } - - startRoom(data?: IRoom) { - if (data) { - // @ts-ignore - this has been imported from the Apps-Engine - delete data.id; - } - - return new RoomBuilder(data); - } - - startDiscussion(data?: Partial) { - if (data) { - delete data.id; - } - - return new DiscussionBuilder(data); - } - - startVideoConference(data?: Partial) { - return new VideoConferenceBuilder(data); - } - - startBotUser(data?: Partial) { - if (data) { - delete data.id; - - const { roles } = data; - - if (roles?.length) { - const hasRole = roles - .map((role: string) => role.toLocaleLowerCase()) - .some((role: string) => role === 'admin' || role === 'owner' || role === 'moderator'); - - if (hasRole) { - throw new Error('Invalid role assigned to the user. Should not be admin, owner or moderator.'); - } - } - - if (!data.type) { - data.type = UserType.BOT; - } - } - - return new UserBuilder(data); - } - - public finish( - builder: IMessageBuilder | ILivechatMessageBuilder | IRoomBuilder | IDiscussionBuilder | IVideoConferenceBuilder | IUserBuilder, - ): Promise { - switch (builder.kind) { - case RocketChatAssociationModel.MESSAGE: - return this._finishMessage(builder as IMessageBuilder); - case RocketChatAssociationModel.LIVECHAT_MESSAGE: - return this._finishLivechatMessage(builder as ILivechatMessageBuilder); - case RocketChatAssociationModel.ROOM: - return this._finishRoom(builder as IRoomBuilder); - case RocketChatAssociationModel.DISCUSSION: - return this._finishDiscussion(builder as IDiscussionBuilder); - case RocketChatAssociationModel.VIDEO_CONFERENCE: - return this._finishVideoConference(builder as IVideoConferenceBuilder); - case RocketChatAssociationModel.USER: - return this._finishUser(builder as IUserBuilder); - default: - throw new Error('Invalid builder passed to the ModifyCreator.finish function.'); - } - } - - private async _finishMessage(builder: IMessageBuilder): Promise { - const result = builder.getMessage(); - delete result.id; - - if (!result.sender || !result.sender.id) { - const response = await this.senderFn({ - method: 'bridges:getUserBridge:doGetAppUser', - params: ['APP_ID'], - }).catch((err) => { - throw formatErrorResponse(err); - }); - - const appUser = response.result; - - if (!appUser) { - throw new Error('Invalid sender assigned to the message.'); - } - - result.sender = appUser; - } - - if (result.blocks?.length) { - // Can we move this elsewhere? This AppObjectRegistry usage doesn't really belong here, but where? - result.blocks = UIHelper.assignIds(result.blocks, AppObjectRegistry.get('id') || ''); - } - - const response = await this.senderFn({ - method: 'bridges:getMessageBridge:doCreate', - params: [result, AppObjectRegistry.get('id')], - }).catch((err) => { - throw formatErrorResponse(err); - }); - - return String(response.result); - } - - private async _finishLivechatMessage(builder: ILivechatMessageBuilder): Promise { - if (builder.getSender() && !builder.getVisitor()) { - return this._finishMessage(builder.getMessageBuilder()); - } - - const result = builder.getMessage(); - delete result.id; - - if (!result.token && (!result.visitor || !result.visitor.token)) { - throw new Error('Invalid visitor sending the message'); - } - - result.token = result.visitor ? result.visitor.token : result.token; - - const response = await this.senderFn({ - method: 'bridges:getLivechatBridge:doCreateMessage', - params: [result, AppObjectRegistry.get('id')], - }).catch((err) => { - throw formatErrorResponse(err); - }); - - return String(response.result); - } - - private async _finishRoom(builder: IRoomBuilder): Promise { - const result = builder.getRoom(); - delete result.id; - - if (!result.type) { - throw new Error('Invalid type assigned to the room.'); - } - - if (result.type !== RoomType.LIVE_CHAT) { - if (!result.creator || !result.creator.id) { - throw new Error('Invalid creator assigned to the room.'); - } - } - - if (result.type !== RoomType.DIRECT_MESSAGE) { - if (result.type !== RoomType.LIVE_CHAT) { - if (!result.slugifiedName || !result.slugifiedName.trim()) { - throw new Error('Invalid slugifiedName assigned to the room.'); - } - } - - if (!result.displayName || !result.displayName.trim()) { - throw new Error('Invalid displayName assigned to the room.'); - } - } - - const response = await this.senderFn({ - method: 'bridges:getRoomBridge:doCreate', - params: [result, builder.getMembersToBeAddedUsernames(), AppObjectRegistry.get('id')], - }).catch((err) => { - throw formatErrorResponse(err); - }); - - return String(response.result); - } - - private async _finishDiscussion(builder: IDiscussionBuilder): Promise { - const room = builder.getRoom(); - delete room.id; - - if (!room.creator || !room.creator.id) { - throw new Error('Invalid creator assigned to the discussion.'); - } - - if (!room.slugifiedName || !room.slugifiedName.trim()) { - throw new Error('Invalid slugifiedName assigned to the discussion.'); - } - - if (!room.displayName || !room.displayName.trim()) { - throw new Error('Invalid displayName assigned to the discussion.'); - } - - if (!room.parentRoom || !room.parentRoom.id) { - throw new Error('Invalid parentRoom assigned to the discussion.'); - } - - const response = await this.senderFn({ - method: 'bridges:getRoomBridge:doCreateDiscussion', - params: [room, builder.getParentMessage(), builder.getReply(), builder.getMembersToBeAddedUsernames(), AppObjectRegistry.get('id')], - }).catch((err) => { - throw formatErrorResponse(err); - }); - - return String(response.result); - } - - private async _finishVideoConference(builder: IVideoConferenceBuilder): Promise { - const videoConference = builder.getVideoConference(); - - if (!videoConference.createdBy) { - throw new Error('Invalid creator assigned to the video conference.'); - } - - if (!videoConference.providerName?.trim()) { - throw new Error('Invalid provider name assigned to the video conference.'); - } - - if (!videoConference.rid) { - throw new Error('Invalid roomId assigned to the video conference.'); - } - - const response = await this.senderFn({ - method: 'bridges:getVideoConferenceBridge:doCreate', - params: [videoConference, AppObjectRegistry.get('id')], - }).catch((err) => { - throw formatErrorResponse(err); - }); - - return String(response.result); - } - - private async _finishUser(builder: IUserBuilder): Promise { - const user = builder.getUser(); - - const response = await this.senderFn({ - method: 'bridges:getUserBridge:doCreate', - params: [user, AppObjectRegistry.get('id')], - }).catch((err) => { - throw formatErrorResponse(err); - }); - - return String(response.result); - } -} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyExtender.ts b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyExtender.ts deleted file mode 100644 index 5f8e0c53ec04a..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyExtender.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { IModifyExtender } from '@rocket.chat/apps-engine/definition/accessors/IModifyExtender.ts'; -import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; -import type { IMessageExtender } from '@rocket.chat/apps-engine/definition/accessors/IMessageExtender.ts'; -import type { IRoomExtender } from '@rocket.chat/apps-engine/definition/accessors/IRoomExtender.ts'; -import type { IVideoConferenceExtender } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceExtend.ts'; -import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; -import type { VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference.ts'; -import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; - -import * as Messenger from '../../messenger.ts'; -import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; -import { MessageExtender } from '../extenders/MessageExtender.ts'; -import { RoomExtender } from '../extenders/RoomExtender.ts'; -import { VideoConferenceExtender } from '../extenders/VideoConferenceExtend.ts'; -import { require } from '../../../lib/require.ts'; -import { formatErrorResponse } from '../formatResponseErrorHandler.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - -export class ModifyExtender implements IModifyExtender { - constructor(private readonly senderFn: typeof Messenger.sendRequest) {} - - public async extendMessage(messageId: string, updater: IUser): Promise { - const result = await this.senderFn({ - method: 'bridges:getMessageBridge:doGetById', - params: [messageId, AppObjectRegistry.get('id')], - }).catch((err) => { - throw formatErrorResponse(err); - }); - - const msg = result.result as IMessage; - - msg.editor = updater; - msg.editedAt = new Date(); - - return new MessageExtender(msg); - } - - public async extendRoom(roomId: string, _updater: IUser): Promise { - const result = await this.senderFn({ - method: 'bridges:getRoomBridge:doGetById', - params: [roomId, AppObjectRegistry.get('id')], - }).catch((err) => { - throw formatErrorResponse(err); - }); - - const room = result.result as IRoom; - - room.updatedAt = new Date(); - - return new RoomExtender(room); - } - - public async extendVideoConference(id: string): Promise { - const result = await this.senderFn({ - method: 'bridges:getVideoConferenceBridge:doGetById', - params: [id, AppObjectRegistry.get('id')], - }).catch((err) => { - throw formatErrorResponse(err); - }); - - const call = result.result as VideoConference; - - call._updatedAt = new Date(); - - return new VideoConferenceExtender(call); - } - - public async finish(extender: IMessageExtender | IRoomExtender | IVideoConferenceExtender): Promise { - switch (extender.kind) { - case RocketChatAssociationModel.MESSAGE: - await this.senderFn({ - method: 'bridges:getMessageBridge:doUpdate', - params: [(extender as IMessageExtender).getMessage(), AppObjectRegistry.get('id')], - }).catch((err) => { - throw formatErrorResponse(err); - }); - break; - case RocketChatAssociationModel.ROOM: - await this.senderFn({ - method: 'bridges:getRoomBridge:doUpdate', - params: [ - (extender as IRoomExtender).getRoom(), - (extender as IRoomExtender).getUsernamesOfMembersBeingAdded(), - AppObjectRegistry.get('id'), - ], - }).catch((err) => { - throw formatErrorResponse(err); - }); - break; - case RocketChatAssociationModel.VIDEO_CONFERENCE: - await this.senderFn({ - method: 'bridges:getVideoConferenceBridge:doUpdate', - params: [(extender as IVideoConferenceExtender).getVideoConference(), AppObjectRegistry.get('id')], - }).catch((err) => { - throw formatErrorResponse(err); - }); - break; - default: - throw new Error('Invalid extender passed to the ModifyExtender.finish function.'); - } - } -} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyUpdater.ts b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyUpdater.ts deleted file mode 100644 index dc9178be1f52d..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyUpdater.ts +++ /dev/null @@ -1,170 +0,0 @@ -import type { IModifyUpdater } from '@rocket.chat/apps-engine/definition/accessors/IModifyUpdater.ts'; -import type { ILivechatUpdater } from '@rocket.chat/apps-engine/definition/accessors/ILivechatUpdater.ts'; -import type { IUserUpdater } from '@rocket.chat/apps-engine/definition/accessors/IUserUpdater.ts'; -import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts'; -import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts'; -import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; -import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; -import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; - -import type { UIHelper as _UIHelper } from '@rocket.chat/apps-engine/server/misc/UIHelper.ts'; -import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; - -import * as Messenger from '../../messenger.ts'; - -import { MessageBuilder } from '../builders/MessageBuilder.ts'; -import { RoomBuilder } from '../builders/RoomBuilder.ts'; -import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; - -import { require } from '../../../lib/require.ts'; -import { formatErrorResponse } from '../formatResponseErrorHandler.ts'; - -const { UIHelper } = require('@rocket.chat/apps-engine/server/misc/UIHelper.js') as { UIHelper: typeof _UIHelper }; -const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - -export class ModifyUpdater implements IModifyUpdater { - constructor(private readonly senderFn: typeof Messenger.sendRequest) {} - - public getLivechatUpdater(): ILivechatUpdater { - return new Proxy( - { __kind: 'getLivechatUpdater' }, - { - get: - (_target: unknown, prop: string) => - (...params: unknown[]) => - prop === 'toJSON' - ? {} - : this.senderFn({ - method: `accessor:getModifier:getUpdater:getLivechatUpdater:${prop}`, - params, - }) - .then((response) => response.result) - .catch((err) => { - throw formatErrorResponse(err); - }), - }, - ) as ILivechatUpdater; - } - - public getUserUpdater(): IUserUpdater { - return new Proxy( - { __kind: 'getUserUpdater' }, - { - get: - (_target: unknown, prop: string) => - (...params: unknown[]) => - prop === 'toJSON' - ? {} - : this.senderFn({ - method: `accessor:getModifier:getUpdater:getUserUpdater:${prop}`, - params, - }) - .then((response) => response.result) - .catch((err) => { - throw formatErrorResponse(err); - }), - }, - ) as IUserUpdater; - } - - public async message(messageId: string, editor: IUser): Promise { - const response = await this.senderFn({ - method: 'bridges:getMessageBridge:doGetById', - params: [messageId, AppObjectRegistry.get('id')], - }).catch((err) => { - throw formatErrorResponse(err); - }); - - const builder = new MessageBuilder(response.result as IMessage); - - builder.setEditor(editor); - - return builder; - } - - public async room(roomId: string, _updater: IUser): Promise { - const response = await this.senderFn({ - method: 'bridges:getRoomBridge:doGetById', - params: [roomId, AppObjectRegistry.get('id')], - }).catch((err) => { - throw formatErrorResponse(err); - }); - - return new RoomBuilder(response.result as IRoom); - } - - public finish(builder: IMessageBuilder | IRoomBuilder): Promise { - switch (builder.kind) { - case RocketChatAssociationModel.MESSAGE: - return this._finishMessage(builder as MessageBuilder); - case RocketChatAssociationModel.ROOM: - return this._finishRoom(builder as RoomBuilder); - default: - throw new Error('Invalid builder passed to the ModifyUpdater.finish function.'); - } - } - - private async _finishMessage(builder: MessageBuilder): Promise { - const result = builder.getMessage(); - - if (!result.id) { - throw new Error("Invalid message, can't update a message without an id."); - } - - if (!result.sender?.id) { - throw new Error('Invalid sender assigned to the message.'); - } - - if (result.blocks?.length) { - result.blocks = UIHelper.assignIds(result.blocks, AppObjectRegistry.get('id') || ''); - } - - const changes = { id: result.id, ...builder.getChanges() }; - - await this.senderFn({ - method: 'bridges:getMessageBridge:doUpdate', - params: [changes, AppObjectRegistry.get('id')], - }).catch((err) => { - throw formatErrorResponse(err); - }); - } - - private async _finishRoom(builder: RoomBuilder): Promise { - const room = builder.getRoom(); - - if (!room.id) { - throw new Error("Invalid room, can't update a room without an id."); - } - - if (!room.type) { - throw new Error('Invalid type assigned to the room.'); - } - - if (room.type !== RoomType.LIVE_CHAT) { - if (!room.creator || !room.creator.id) { - throw new Error('Invalid creator assigned to the room.'); - } - - if (!room.slugifiedName || !room.slugifiedName.trim()) { - throw new Error('Invalid slugifiedName assigned to the room.'); - } - } - - if (!room.displayName || !room.displayName.trim()) { - throw new Error('Invalid displayName assigned to the room.'); - } - - const changes = { id: room.id, ...builder.getChanges() }; - - await this.senderFn({ - method: 'bridges:getRoomBridge:doUpdate', - params: [changes, builder.getMembersToBeAddedUsernames(), AppObjectRegistry.get('id')], - }).catch((err) => { - throw formatErrorResponse(err); - }); - } -} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/notifier.ts b/packages/apps-engine/deno-runtime/lib/accessors/notifier.ts deleted file mode 100644 index 1a85cc12b579f..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/notifier.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { IMessageBuilder, INotifier } from '@rocket.chat/apps-engine/definition/accessors'; -import type { ITypingOptions } from '@rocket.chat/apps-engine/definition/accessors/INotifier.ts'; -import type { _TypingScope } from '@rocket.chat/apps-engine/definition/accessors/INotifier.ts'; -import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; -import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; -import type { IUser } from '@rocket.chat/apps-engine/definition/users'; -import { MessageBuilder } from './builders/MessageBuilder.ts'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import * as Messenger from '../messenger.ts'; -import { require } from '../require.ts'; -import { formatErrorResponse } from './formatResponseErrorHandler.ts'; - -const { TypingScope } = require('@rocket.chat/apps-engine/definition/accessors/INotifier.js') as { - TypingScope: typeof _TypingScope; -}; - -export class Notifier implements INotifier { - private senderFn: typeof Messenger.sendRequest; - - constructor(senderFn: typeof Messenger.sendRequest) { - this.senderFn = senderFn; - } - - public async notifyUser(user: IUser, message: IMessage): Promise { - if (!message.sender || !message.sender.id) { - const appUser = await this.getAppUser(); - - message.sender = appUser; - } - - await this.callMessageBridge('doNotifyUser', [user, message, AppObjectRegistry.get('id')]); - } - - public async notifyRoom(room: IRoom, message: IMessage): Promise { - if (!message.sender || !message.sender.id) { - const appUser = await this.getAppUser(); - - message.sender = appUser; - } - - await this.callMessageBridge('doNotifyRoom', [room, message, AppObjectRegistry.get('id')]); - } - - public async typing(options: ITypingOptions): Promise<() => Promise> { - options.scope = options.scope || TypingScope.Room; - - if (!options.username) { - const appUser = await this.getAppUser(); - options.username = (appUser && appUser.name) || ''; - } - - const appId = AppObjectRegistry.get('id'); - - await this.callMessageBridge('doTyping', [{ ...options, isTyping: true }, appId]); - - return async () => { - await this.callMessageBridge('doTyping', [{ ...options, isTyping: false }, appId]); - }; - } - - public getMessageBuilder(): IMessageBuilder { - return new MessageBuilder(); - } - - private async callMessageBridge(method: string, params: Array): Promise { - await this.senderFn({ - method: `bridges:getMessageBridge:${method}`, - params, - }).catch((err) => { - throw formatErrorResponse(err); - }); - } - - private async getAppUser(): Promise { - const response = await this.senderFn({ - method: 'bridges:getUserBridge:doGetAppUser', - params: [AppObjectRegistry.get('id')], - }).catch((err) => { - throw formatErrorResponse(err); - }); - - return response.result; - } -} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/tests/AppAccessors.test.ts b/packages/apps-engine/deno-runtime/lib/accessors/tests/AppAccessors.test.ts deleted file mode 100644 index ffc77b6904bb7..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/tests/AppAccessors.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; -import { assertEquals } from 'https://deno.land/std@0.203.0/assert/assert_equals.ts'; - -import { AppAccessors } from '../mod.ts'; -import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; - -describe('AppAccessors', () => { - let appAccessors: AppAccessors; - const senderFn = (r: object) => - Promise.resolve({ - id: Math.random().toString(36).substring(2), - jsonrpc: '2.0', - result: r, - serialize() { - return JSON.stringify(this); - }, - }); - - beforeEach(() => { - appAccessors = new AppAccessors(senderFn); - AppObjectRegistry.clear(); - }); - - afterAll(() => { - AppObjectRegistry.clear(); - }); - - it('creates the correct format for IRead calls', async () => { - const roomRead = appAccessors.getReader().getRoomReader(); - const room = await roomRead.getById('123'); - - assertEquals(room, { - params: ['123'], - method: 'accessor:getReader:getRoomReader:getById', - }); - }); - - it('creates the correct format for IEnvironmentRead calls from IRead', async () => { - const reader = appAccessors.getReader().getEnvironmentReader().getEnvironmentVariables(); - const room = await reader.getValueByName('NODE_ENV'); - - assertEquals(room, { - params: ['NODE_ENV'], - method: 'accessor:getReader:getEnvironmentReader:getEnvironmentVariables:getValueByName', - }); - }); - - it('creates the correct format for IEvironmentRead calls', async () => { - const envRead = appAccessors.getEnvironmentRead(); - const env = await envRead.getServerSettings().getValueById('123'); - - assertEquals(env, { - params: ['123'], - method: 'accessor:getEnvironmentRead:getServerSettings:getValueById', - }); - }); - - it('creates the correct format for IEvironmentWrite calls', async () => { - const envRead = appAccessors.getEnvironmentWrite(); - const env = await envRead.getServerSettings().incrementValue('123', 6); - - assertEquals(env, { - params: ['123', 6], - method: 'accessor:getEnvironmentWrite:getServerSettings:incrementValue', - }); - }); - - it('creates the correct format for IConfigurationModify calls', async () => { - const configModify = appAccessors.getConfigurationModify(); - const command = await configModify.slashCommands.modifySlashCommand({ - command: 'test', - i18nDescription: 'test', - i18nParamsExample: 'test', - providesPreview: true, - }); - - assertEquals(command, { - params: [ - { - command: 'test', - i18nDescription: 'test', - i18nParamsExample: 'test', - providesPreview: true, - }, - ], - method: 'accessor:getConfigurationModify:slashCommands:modifySlashCommand', - }); - }); - - it('correctly stores a reference to a slashcommand object and sends a request via proxy', async () => { - const configExtend = appAccessors.getConfigurationExtend(); - - const slashcommand = { - command: 'test', - i18nDescription: 'test', - i18nParamsExample: 'test', - providesPreview: true, - executor() { - return Promise.resolve(); - }, - }; - - const result = await configExtend.slashCommands.provideSlashCommand(slashcommand); - - assertEquals(AppObjectRegistry.get('slashcommand:test'), slashcommand); - - // The function will not be serialized and sent to the main process - delete result.params[0].executor; - - assertEquals(result, { - method: 'accessor:getConfigurationExtend:slashCommands:provideSlashCommand', - params: [ - { - command: 'test', - i18nDescription: 'test', - i18nParamsExample: 'test', - providesPreview: true, - }, - ], - }); - }); -}); diff --git a/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts b/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts deleted file mode 100644 index d88690a77dbfa..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts +++ /dev/null @@ -1,259 +0,0 @@ -// deno-lint-ignore-file no-explicit-any -import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; -import { assertSpyCall, spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; -import { assert, assertEquals, assertNotInstanceOf, assertRejects } from 'https://deno.land/std@0.203.0/assert/mod.ts'; - -import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; -import { ModifyCreator } from '../modify/ModifyCreator.ts'; - -describe('ModifyCreator', () => { - const senderFn = (r: any) => - Promise.resolve({ - id: Math.random().toString(36).substring(2), - jsonrpc: '2.0', - result: r, - serialize() { - return JSON.stringify(this); - }, - }); - - beforeEach(() => { - AppObjectRegistry.clear(); - AppObjectRegistry.set('id', 'deno-test'); - }); - - afterAll(() => { - AppObjectRegistry.clear(); - }); - - it('sends the correct payload in the request to create a message', async () => { - const spying = spy(senderFn); - const modifyCreator = new ModifyCreator(spying); - const messageBuilder = modifyCreator.startMessage(); - - // Importing types from the Apps-Engine is problematic, so we'll go with `any` here - messageBuilder - .setRoom({ id: '123' } as any) - .setSender({ id: '456' } as any) - .setText('Hello World') - .setUsernameAlias('alias') - .setAvatarUrl('https://avatars.com/123'); - - // We can't get a legitimate return value here, so we ignore it - // but we need to know that the request sent was well formed - await modifyCreator.finish(messageBuilder); - - assertSpyCall(spying, 0, { - args: [ - { - method: 'bridges:getMessageBridge:doCreate', - params: [ - { - room: { id: '123' }, - sender: { id: '456' }, - text: 'Hello World', - alias: 'alias', - avatarUrl: 'https://avatars.com/123', - }, - 'deno-test', - ], - }, - ], - }); - }); - - it('sends the correct payload in the request to upload a buffer', async () => { - const modifyCreator = new ModifyCreator(senderFn); - - const result = await modifyCreator.getUploadCreator().uploadBuffer(new Uint8Array([1, 2, 3, 4]), 'text/plain'); - - assertEquals(result, { - method: 'accessor:getModifier:getCreator:getUploadCreator:uploadBuffer', - params: [new Uint8Array([1, 2, 3, 4]), 'text/plain'], - }); - }); - - it('sends the correct payload in the request to create a visitor', async () => { - const modifyCreator = new ModifyCreator(senderFn); - - const result = (await modifyCreator.getLivechatCreator().createVisitor({ - token: 'random token', - username: 'random username for visitor', - name: 'Random Visitor', - })) as any; // We modified the send function so it changed the original return type of the function - - assertEquals(result, { - method: 'accessor:getModifier:getCreator:getLivechatCreator:createVisitor', - params: [ - { - token: 'random token', - username: 'random username for visitor', - name: 'Random Visitor', - }, - ], - }); - }); - - // This test is important because if we return a promise we break API compatibility - it('does not return a promise for calls of the createToken() method of the LivechatCreator', () => { - const modifyCreator = new ModifyCreator(senderFn); - - const result = modifyCreator.getLivechatCreator().createToken(); - - assertNotInstanceOf(result, Promise); - assert(typeof result === 'string', `Expected "${result}" to be of type "string", but got "${typeof result}"`); - }); - - it('throws an error when a proxy method of getLivechatCreator fails', async () => { - const failingSenderFn = () => Promise.reject(new Error('Test error')); - const modifyCreator = new ModifyCreator(failingSenderFn); - const livechatCreator = modifyCreator.getLivechatCreator(); - - await assertRejects( - () => - livechatCreator.createAndReturnVisitor({ - token: 'visitor-token', - username: 'visitor-username', - name: 'Visitor Name', - }), - Error, - 'Test error', - ); - }); - - it('throws an instance of Error when getLivechatCreator fails with a specific error object', async () => { - const failingSenderFn = () => Promise.reject({ error: { message: 'Livechat method error' } }); - const modifyCreator = new ModifyCreator(failingSenderFn); - const livechatCreator = modifyCreator.getLivechatCreator(); - - await assertRejects( - () => - livechatCreator.createVisitor({ - token: 'visitor-token', - username: 'visitor-username', - name: 'Visitor Name', - }), - Error, - 'Livechat method error', - ); - }); - - it('throws a default Error when getLivechatCreator fails with an unknown error object', async () => { - const failingSenderFn = () => Promise.reject({ error: {} }); - const modifyCreator = new ModifyCreator(failingSenderFn); - const livechatCreator = modifyCreator.getLivechatCreator(); - - await assertRejects( - () => - livechatCreator.createVisitor({ - token: 'visitor-token', - username: 'visitor-username', - name: 'Visitor Name', - }), - Error, - 'An unknown error occurred', - ); - }); - - it('throws an error when a proxy method of getUploadCreator fails', async () => { - const failingSenderFn = () => Promise.reject(new Error('Upload error')); - const modifyCreator = new ModifyCreator(failingSenderFn); - const uploadCreator = modifyCreator.getUploadCreator(); - - await assertRejects(() => uploadCreator.uploadBuffer(new Uint8Array([9, 10, 11, 12]), 'image/png'), Error, 'Upload error'); - }); - - it('throws an instance of Error when getUploadCreator fails with a specific error object', async () => { - const failingSenderFn = () => Promise.reject({ error: { message: 'Upload method error' } }); - const modifyCreator = new ModifyCreator(failingSenderFn); - const uploadCreator = modifyCreator.getUploadCreator(); - - await assertRejects(() => uploadCreator.uploadBuffer(new Uint8Array([1, 2, 3]), 'image/png'), Error, 'Upload method error'); - }); - - it('throws a default Error when getUploadCreator fails with an unknown error object', async () => { - const failingSenderFn = () => Promise.reject({ error: {} }); - const modifyCreator = new ModifyCreator(failingSenderFn); - const uploadCreator = modifyCreator.getUploadCreator(); - - await assertRejects(() => uploadCreator.uploadBuffer(new Uint8Array([1, 2, 3]), 'image/png'), Error, 'An unknown error occurred'); - }); - - it('throws an error when a proxy method of getEmailCreator fails', async () => { - const failingSenderFn = () => Promise.reject(new Error('Email error')); - const modifyCreator = new ModifyCreator(failingSenderFn); - const emailCreator = modifyCreator.getEmailCreator(); - - await assertRejects( - () => - emailCreator.send({ - to: 'test@example.com', - from: 'sender@example.com', - subject: 'Test Email', - text: 'This is a test email.', - }), - Error, - 'Email error', - ); - }); - - it('throws an instance of Error when getEmailCreator fails with a specific error object', async () => { - const failingSenderFn = () => Promise.reject({ error: { message: 'Email method error' } }); - const modifyCreator = new ModifyCreator(failingSenderFn); - const emailCreator = modifyCreator.getEmailCreator(); - - await assertRejects( - () => - emailCreator.send({ - to: 'test@example.com', - from: 'sender@example.com', - subject: 'Test Email', - text: 'This is a test email.', - }), - Error, - 'Email method error', - ); - }); - - it('throws a default Error when getEmailCreator fails with an unknown error object', async () => { - const failingSenderFn = () => Promise.reject({ error: {} }); - const modifyCreator = new ModifyCreator(failingSenderFn); - const emailCreator = modifyCreator.getEmailCreator(); - - await assertRejects( - () => - emailCreator.send({ - to: 'test@example.com', - from: 'sender@example.com', - subject: 'Test Email', - text: 'This is a test email.', - }), - Error, - 'An unknown error occurred', - ); - }); - - it('throws an error when a proxy method of getContactCreator fails', async () => { - const failingSenderFn = () => Promise.reject(new Error('Contact creation error')); - const modifyCreator = new ModifyCreator(failingSenderFn); - const contactCreator = modifyCreator.getContactCreator(); - - await assertRejects(() => contactCreator.addContactEmail('test-contact-id', 'test@example.com'), Error, 'Contact creation error'); - }); - - it('throws an instance of Error when getContactCreator fails with a specific error object', async () => { - const failingSenderFn = () => Promise.reject({ error: { message: 'Contact creation error' } }); - const modifyCreator = new ModifyCreator(failingSenderFn); - const contactCreator = modifyCreator.getContactCreator(); - - await assertRejects(() => contactCreator.addContactEmail('test-contact-id', 'test@example.com'), Error, 'Contact creation error'); - }); - - it('throws a default Error when getContactCreator fails with an unknown error object', async () => { - const failingSenderFn = () => Promise.reject({ error: {} }); - const modifyCreator = new ModifyCreator(failingSenderFn); - const contactCreator = modifyCreator.getContactCreator(); - - await assertRejects(() => contactCreator.addContactEmail('test-contact-id', 'test@example.com'), Error, 'An unknown error occurred'); - }); -}); diff --git a/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts b/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts deleted file mode 100644 index de6fd4a7053b3..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts +++ /dev/null @@ -1,244 +0,0 @@ -// deno-lint-ignore-file no-explicit-any -import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; -import { assertSpyCall, spy, stub } from 'https://deno.land/std@0.203.0/testing/mock.ts'; -import { assertRejects } from 'https://deno.land/std@0.203.0/assert/mod.ts'; - -import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; -import { ModifyExtender } from '../modify/ModifyExtender.ts'; -import jsonrpc from 'jsonrpc-lite'; - -describe('ModifyExtender', () => { - let extender: ModifyExtender; - - const senderFn = (r: any) => - Promise.resolve({ - id: Math.random().toString(36).substring(2), - jsonrpc: '2.0', - result: structuredClone(r), - serialize() { - return JSON.stringify(this); - }, - }); - - beforeEach(() => { - AppObjectRegistry.clear(); - AppObjectRegistry.set('id', 'deno-test'); - extender = new ModifyExtender(senderFn); - }); - - afterAll(() => { - AppObjectRegistry.clear(); - }); - - it('correctly formats requests for the extend message requests', async () => { - const _spy = spy(extender, 'senderFn' as keyof ModifyExtender); - - const messageExtender = await extender.extendMessage('message-id', { _id: 'user-id' } as any); - - assertSpyCall(_spy, 0, { - args: [ - { - method: 'bridges:getMessageBridge:doGetById', - params: ['message-id', 'deno-test'], - }, - ], - }); - - messageExtender.addCustomField('key', 'value'); - - await extender.finish(messageExtender); - - assertSpyCall(_spy, 1, { - args: [ - { - method: 'bridges:getMessageBridge:doUpdate', - params: [messageExtender.getMessage(), 'deno-test'], - }, - ], - }); - - _spy.restore(); - }); - - it('correctly formats requests for the extend room requests', async () => { - const _spy = spy(extender, 'senderFn' as keyof ModifyExtender); - - const roomExtender = await extender.extendRoom('room-id', { _id: 'user-id' } as any); - - assertSpyCall(_spy, 0, { - args: [ - { - method: 'bridges:getRoomBridge:doGetById', - params: ['room-id', 'deno-test'], - }, - ], - }); - - roomExtender.addCustomField('key', 'value'); - - await extender.finish(roomExtender); - - assertSpyCall(_spy, 1, { - args: [ - { - method: 'bridges:getRoomBridge:doUpdate', - params: [roomExtender.getRoom(), [], 'deno-test'], - }, - ], - }); - - _spy.restore(); - }); - - it('correctly formats requests for the extend video conference requests', async () => { - const _spy = spy(extender, 'senderFn' as keyof ModifyExtender); - - const videoConferenceExtender = await extender.extendVideoConference('video-conference-id'); - - assertSpyCall(_spy, 0, { - args: [ - { - method: 'bridges:getVideoConferenceBridge:doGetById', - params: ['video-conference-id', 'deno-test'], - }, - ], - }); - - videoConferenceExtender.setStatus(4); - - await extender.finish(videoConferenceExtender); - - assertSpyCall(_spy, 1, { - args: [ - { - method: 'bridges:getVideoConferenceBridge:doUpdate', - params: [videoConferenceExtender.getVideoConference(), 'deno-test'], - }, - ], - }); - - _spy.restore(); - }); - - describe('Error Handling', () => { - describe('extendMessage', () => { - it('throws an instance of Error when senderFn throws an error', async () => { - const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject(new Error('unit-test-error')) as any); - - await assertRejects(() => extender.extendMessage('message-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); - - _stub.restore(); - }); - - it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { - const _stub = stub( - extender, - 'senderFn' as keyof ModifyExtender, - () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, - ); - - await assertRejects(() => extender.extendMessage('message-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); - - _stub.restore(); - }); - - it('throws an instance of Error when senderFn throws an unknown value', async () => { - const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject({}) as any); - - await assertRejects(() => extender.extendMessage('message-id', { _id: 'user-id' } as any), Error, 'An unknown error occurred'); - - _stub.restore(); - }); - }); - - describe('extendRoom', () => { - it('throws an instance of Error when senderFn throws an error', async () => { - const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject(new Error('unit-test-error')) as any); - - await assertRejects(() => extender.extendRoom('room-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); - - _stub.restore(); - }); - - it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { - const _stub = stub( - extender, - 'senderFn' as keyof ModifyExtender, - () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, - ); - - await assertRejects(() => extender.extendRoom('room-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); - - _stub.restore(); - }); - - it('throws an instance of Error when senderFn throws an unknown value', async () => { - const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject({}) as any); - - await assertRejects(() => extender.extendRoom('room-id', { _id: 'user-id' } as any), Error, 'An unknown error occurred'); - - _stub.restore(); - }); - }); - - describe('extendVideoConference', () => { - it('throws an instance of Error when senderFn throws an error', async () => { - const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject(new Error('unit-test-error')) as any); - - await assertRejects(() => extender.extendVideoConference('video-conference-id'), Error, 'unit-test-error'); - - _stub.restore(); - }); - - it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { - const _stub = stub( - extender, - 'senderFn' as keyof ModifyExtender, - () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, - ); - - await assertRejects(() => extender.extendVideoConference('video-conference-id'), Error, 'unit-test-error'); - - _stub.restore(); - }); - - it('throws an instance of Error when senderFn throws an unknown value', async () => { - const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject({}) as any); - - await assertRejects(() => extender.extendVideoConference('video-conference-id'), Error, 'An unknown error occurred'); - - _stub.restore(); - }); - }); - - describe('finish', () => { - it('throws an instance of Error when senderFn throws an error', async () => { - const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject(new Error('unit-test-error')) as any); - - await assertRejects(() => extender.finish({ kind: 'message', getMessage: () => ({}) } as any), Error, 'unit-test-error'); - - _stub.restore(); - }); - - it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { - const _stub = stub( - extender, - 'senderFn' as keyof ModifyExtender, - () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, - ); - - await assertRejects(() => extender.finish({ kind: 'message', getMessage: () => ({}) } as any), Error, 'unit-test-error'); - - _stub.restore(); - }); - - it('throws an instance of Error when senderFn throws an unknown value', async () => { - const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject({}) as any); - - await assertRejects(() => extender.finish({ kind: 'message', getMessage: () => ({}) } as any), Error, 'An unknown error occurred'); - - _stub.restore(); - }); - }); - }); -}); diff --git a/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts b/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts deleted file mode 100644 index 487c10725028a..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts +++ /dev/null @@ -1,234 +0,0 @@ -// deno-lint-ignore-file no-explicit-any -import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; -import { assertSpyCall, spy, stub } from 'https://deno.land/std@0.203.0/testing/mock.ts'; -import { assertEquals, assertRejects } from 'https://deno.land/std@0.203.0/assert/mod.ts'; - -import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; -import { ModifyUpdater } from '../modify/ModifyUpdater.ts'; -import { RoomBuilder } from '../builders/RoomBuilder.ts'; -import jsonrpc from 'jsonrpc-lite'; - -describe('ModifyUpdater', () => { - let modifyUpdater: ModifyUpdater; - - const senderFn = (r: any) => - Promise.resolve({ - id: Math.random().toString(36).substring(2), - jsonrpc: '2.0', - result: structuredClone(r), - serialize() { - return JSON.stringify(this); - }, - }); - - beforeEach(() => { - AppObjectRegistry.clear(); - AppObjectRegistry.set('id', 'deno-test'); - modifyUpdater = new ModifyUpdater(senderFn); - }); - - afterAll(() => { - AppObjectRegistry.clear(); - }); - - it('correctly formats requests for the update message flow', async () => { - const _spy = spy(modifyUpdater, 'senderFn' as keyof ModifyUpdater); - - const messageBuilder = await modifyUpdater.message('123', { id: '456' } as any); - - assertSpyCall(_spy, 0, { - args: [ - { - method: 'bridges:getMessageBridge:doGetById', - params: ['123', 'deno-test'], - }, - ], - }); - - messageBuilder.setUpdateData( - { - id: '123', - room: { id: '123' }, - sender: { id: '456' }, - text: 'Hello World', - }, - { - id: '456', - }, - ); - - await modifyUpdater.finish(messageBuilder); - - assertSpyCall(_spy, 1, { - args: [ - { - method: 'bridges:getMessageBridge:doUpdate', - params: [{ id: '123', ...messageBuilder.getChanges() }, 'deno-test'], - }, - ], - }); - - _spy.restore(); - }); - - it('correctly formats requests for the update room flow', async () => { - const _spy = spy(modifyUpdater, 'senderFn' as keyof ModifyUpdater); - - const roomBuilder = (await modifyUpdater.room('123', { id: '456' } as any)) as RoomBuilder; - - assertSpyCall(_spy, 0, { - args: [ - { - method: 'bridges:getRoomBridge:doGetById', - params: ['123', 'deno-test'], - }, - ], - }); - - roomBuilder.setData({ - id: '123', - type: 'c', - displayName: 'Test Room', - slugifiedName: 'test-room', - creator: { id: '456' }, - }); - - roomBuilder.setMembersToBeAddedByUsernames(['username1', 'username2']); - - // We need to sneak in the id as the `modifyUpdater.room` call won't have legitimate data - roomBuilder.getRoom().id = '123'; - - await modifyUpdater.finish(roomBuilder); - - assertSpyCall(_spy, 1, { - args: [ - { - method: 'bridges:getRoomBridge:doUpdate', - params: [{ id: '123', ...roomBuilder.getChanges() }, roomBuilder.getMembersToBeAddedUsernames(), 'deno-test'], - }, - ], - }); - }); - - it('correctly formats requests to UserUpdater methods', async () => { - const result = (await modifyUpdater.getUserUpdater().updateStatusText({ id: '123' } as any, 'Hello World')) as any; - - assertEquals(result, { - method: 'accessor:getModifier:getUpdater:getUserUpdater:updateStatusText', - params: [{ id: '123' }, 'Hello World'], - }); - }); - - it('correctly formats requests to LivechatUpdater methods', async () => { - const result = (await modifyUpdater.getLivechatUpdater().closeRoom({ id: '123' } as any, 'close it!')) as any; - - assertEquals(result, { - method: 'accessor:getModifier:getUpdater:getLivechatUpdater:closeRoom', - params: [{ id: '123' }, 'close it!'], - }); - }); - - describe('Error Handling', () => { - describe('message', () => { - it('throws an instance of Error when senderFn throws an error', async () => { - const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject(new Error('unit-test-error')) as any); - - await assertRejects(() => modifyUpdater.message('message-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); - - _stub.restore(); - }); - - it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { - const _stub = stub( - modifyUpdater, - 'senderFn' as keyof ModifyUpdater, - () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, - ); - - await assertRejects(() => modifyUpdater.message('message-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); - - _stub.restore(); - }); - - it('throws an instance of Error when senderFn throws an unknown value', async () => { - const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject({}) as any); - - await assertRejects(() => modifyUpdater.message('message-id', { _id: 'user-id' } as any), Error, 'An unknown error occurred'); - - _stub.restore(); - }); - }); - - describe('room', () => { - it('throws an instance of Error when senderFn throws an error', async () => { - const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject(new Error('unit-test-error')) as any); - - await assertRejects(() => modifyUpdater.room('room-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); - - _stub.restore(); - }); - - it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { - const _stub = stub( - modifyUpdater, - 'senderFn' as keyof ModifyUpdater, - () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, - ); - - await assertRejects(() => modifyUpdater.room('room-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); - - _stub.restore(); - }); - - it('throws an instance of Error when senderFn throws an unknown value', async () => { - const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject({}) as any); - - await assertRejects(() => modifyUpdater.room('room-id', { _id: 'user-id' } as any), Error, 'An unknown error occurred'); - - _stub.restore(); - }); - }); - - describe('finish', () => { - const messageUpdater = { - kind: 'message', - getMessage: () => ({ - id: 'message-id', - sender: { id: 'sender-id' }, - }), - getChanges: () => ({ - id: 'message-id', - sender: { id: 'sender-id' }, - }), - } as any; - - it('throws an instance of Error when senderFn throws an error', async () => { - const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject(new Error('unit-test-error')) as any); - - await assertRejects(() => modifyUpdater.finish(messageUpdater), Error, 'unit-test-error'); - - _stub.restore(); - }); - - it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { - const _stub = stub( - modifyUpdater, - 'senderFn' as keyof ModifyUpdater, - () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, - ); - - await assertRejects(() => modifyUpdater.finish(messageUpdater), Error, 'unit-test-error'); - - _stub.restore(); - }); - - it('throws an instance of Error when senderFn throws an unknown value', async () => { - const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject({}) as any); - - await assertRejects(() => modifyUpdater.finish(messageUpdater), Error, 'An unknown error occurred'); - - _stub.restore(); - }); - }); - }); -}); diff --git a/packages/apps-engine/deno-runtime/lib/accessors/tests/formatResponseErrorHandler.test.ts b/packages/apps-engine/deno-runtime/lib/accessors/tests/formatResponseErrorHandler.test.ts deleted file mode 100644 index c909fecdd04a1..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/tests/formatResponseErrorHandler.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -// deno-lint-ignore-file no-explicit-any -import { assertEquals, assertInstanceOf, assertStrictEquals } from 'https://deno.land/std@0.203.0/assert/mod.ts'; -import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; -import * as jsonrpc from 'jsonrpc-lite'; - -import { formatErrorResponse } from '../formatResponseErrorHandler.ts'; - -describe('formatErrorResponse', () => { - describe('JSON-RPC ErrorObject handling', () => { - it('formats ErrorObject instances correctly', () => { - const errorObject = jsonrpc.error('test-id', new jsonrpc.JsonRpcError('Test error message', 1000)); - const result = formatErrorResponse(errorObject); - - assertInstanceOf(result, Error); - assertEquals(result.message, 'Test error message'); - }); - - it('formats objects with error.message structure', () => { - const errorLikeObject = { - error: { - message: 'Custom error message', - code: 404, - }, - }; - const result = formatErrorResponse(errorLikeObject); - - assertInstanceOf(result, Error); - assertEquals(result.message, 'Custom error message'); - }); - - it('handles nested error objects with complex structure', () => { - const complexError = { - error: { - message: 'Database connection failed', - details: { - host: 'localhost', - port: 5432, - }, - }, - id: 'req-123', - }; - const result = formatErrorResponse(complexError); - - assertInstanceOf(result, Error); - assertEquals(result.message, 'Database connection failed'); - }); - - it('handles error objects with empty message', () => { - const emptyMessageError = { - error: { - message: '', - code: 500, - }, - }; - const result = formatErrorResponse(emptyMessageError); - - assertInstanceOf(result, Error); - assertEquals(result.message, ''); - }); - }); - - describe('Error instance passthrough', () => { - it('returns existing Error instances unchanged', () => { - const originalError = new Error('Original error message'); - const result = formatErrorResponse(originalError); - - assertStrictEquals(result, originalError); - assertEquals(result.message, 'Original error message'); - }); - - it('returns custom Error subclasses unchanged', () => { - class CustomError extends Error { - constructor( - message: string, - public code: number, - ) { - super(message); - this.name = 'CustomError'; - } - } - - const customError = new CustomError('Custom error', 404); - const result = formatErrorResponse(customError); - - assertStrictEquals(result, customError); - assertEquals(result.message, 'Custom error'); - assertEquals((result as CustomError).code, 404); - }); - - it('handles Error instances with additional properties', () => { - const errorWithProps = new Error('Error with props') as any; - errorWithProps.statusCode = 500; - errorWithProps.details = { reason: 'timeout' }; - - const result = formatErrorResponse(errorWithProps); - - assertStrictEquals(result, errorWithProps); - assertEquals(result.message, 'Error with props'); - assertEquals((result as any).statusCode, 500); - }); - }); - - describe('Unknown error handling', () => { - it('wraps string errors with default message and cause', () => { - const stringError = 'Simple string error'; - const result = formatErrorResponse(stringError); - - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - assertEquals(result.cause, stringError); - }); - - it('wraps number errors with default message and cause', () => { - const numberError = 404; - const result = formatErrorResponse(numberError); - - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - assertEquals(result.cause, numberError); - }); - - it('wraps boolean errors with default message and cause', () => { - const booleanError = false; - const result = formatErrorResponse(booleanError); - - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - assertEquals(result.cause, booleanError); - }); - - it('wraps null with default message and cause', () => { - const result = formatErrorResponse(null); - - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - assertEquals(result.cause, null); - }); - - it('wraps undefined with default message and cause', () => { - const result = formatErrorResponse(undefined); - - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - assertEquals(result.cause, undefined); - }); - - it('wraps arrays with default message and cause', () => { - const arrayError = ['error', 'details']; - const result = formatErrorResponse(arrayError); - - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - assertEquals(result.cause, arrayError); - }); - - it('wraps functions with default message and cause', () => { - const functionError = () => 'error'; - const result = formatErrorResponse(functionError); - - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - assertEquals(result.cause, functionError); - }); - - it('wraps plain objects without error.message with default message and cause', () => { - const plainObject = { - status: 'failed', - reason: 'timeout', - data: { id: 123 }, - }; - const result = formatErrorResponse(plainObject); - - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - assertEquals(result.cause, plainObject); - }); - - it('wraps objects with error property but no message with default message and cause', () => { - const errorObjectNoMessage = { - error: { - code: 500, - details: 'Internal server error', - }, - }; - const result = formatErrorResponse(errorObjectNoMessage); - - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - assertEquals(result.cause, errorObjectNoMessage); - }); - }); - - it('ensures all returned values are proper Error instances', () => { - const testCases = ['string error', 123, null, undefined, { error: { message: 'test' } }, new Error('test'), { plain: 'object' }]; - - for (const testCase of testCases) { - const result = formatErrorResponse(testCase); - assertInstanceOf(result, Error, `Failed for input: ${JSON.stringify(testCase)}`); - } - }); - - it('prevents "[object Object]" error messages for plain objects', () => { - const plainObject = { status: 'error', code: 500 }; - const result = formatErrorResponse(plainObject); - - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - // Ensure the message is not "[object Object]" - assertEquals(result.message !== '[object Object]', true); - }); -}); diff --git a/packages/apps-engine/deno-runtime/lib/accessors/tests/http.test.ts b/packages/apps-engine/deno-runtime/lib/accessors/tests/http.test.ts deleted file mode 100644 index 88392dec774cc..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/accessors/tests/http.test.ts +++ /dev/null @@ -1,164 +0,0 @@ -// deno-lint-ignore-file no-explicit-any -import { assertRejects } from 'https://deno.land/std@0.203.0/assert/mod.ts'; -import { beforeEach, describe, it, afterAll } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; - -import { Http } from '../http.ts'; -import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; -import { stub } from 'https://deno.land/std@0.203.0/testing/mock.ts'; - -describe('Http accessor error handling integration', () => { - let http: Http; - - beforeEach(() => { - AppObjectRegistry.clear(); - AppObjectRegistry.set('id', 'test-app-id'); - - const mockHttpExtend = { - getDefaultHeaders: () => new Map(), - getDefaultParams: () => new Map(), - getPreRequestHandlers: () => [], - getPreResponseHandlers: () => [], - }; - - const mockRead = {}; - const mockPersistence = {}; - - http = new Http(mockRead as any, mockPersistence as any, mockHttpExtend as any, () => Promise.resolve({}) as any); - }); - - afterAll(() => { - AppObjectRegistry.clear(); - }); - - describe('HTTP method error handling', () => { - it('formats JSON-RPC errors correctly for GET requests', async () => { - const _stub = stub(http, 'senderFn' as keyof Http, () => - Promise.reject({ - error: { - message: 'HTTP GET request failed', - code: 404, - }, - }), - ); - - await assertRejects(() => http.get('https://api.example.com/data'), Error, 'HTTP GET request failed'); - - _stub.restore(); - }); - - it('formats JSON-RPC errors correctly for POST requests', async () => { - const _stub = stub(http, 'senderFn' as keyof Http, () => - Promise.reject({ - error: { - message: 'HTTP POST request validation failed', - code: 400, - }, - }), - ); - - await assertRejects( - () => http.post('https://api.example.com/create', { data: { name: 'test' } }), - Error, - 'HTTP POST request validation failed', - ); - - _stub.restore(); - }); - - it('formats JSON-RPC errors correctly for PUT requests', async () => { - const _stub = stub(http, 'senderFn' as keyof Http, () => - Promise.reject({ - error: { - message: 'HTTP PUT request unauthorized', - code: 401, - }, - }), - ); - - await assertRejects( - () => http.put('https://api.example.com/update/123', { data: { name: 'updated' } }), - Error, - 'HTTP PUT request unauthorized', - ); - - _stub.restore(); - }); - - it('formats JSON-RPC errors correctly for DELETE requests', async () => { - const _stub = stub(http, 'senderFn' as keyof Http, () => - Promise.reject({ - error: { - message: 'HTTP DELETE request forbidden', - code: 403, - }, - }), - ); - - await assertRejects(() => http.del('https://api.example.com/delete/123'), Error, 'HTTP DELETE request forbidden'); - - _stub.restore(); - }); - - it('formats JSON-RPC errors correctly for PATCH requests', async () => { - const _stub = stub(http, 'senderFn' as keyof Http, () => - Promise.reject({ - error: { - message: 'HTTP PATCH request conflict', - code: 409, - }, - }), - ); - - await assertRejects( - () => http.patch('https://api.example.com/patch/123', { data: { status: 'active' } }), - Error, - 'HTTP PATCH request conflict', - ); - - _stub.restore(); - }); - }); - - describe('Error instance passthrough', () => { - it('passes through existing Error instances unchanged for HTTP requests', async () => { - const originalError = new Error('Network timeout error'); - const _stub = stub(http, 'senderFn' as keyof Http, () => Promise.reject(originalError)); - - await assertRejects(() => http.get('https://api.example.com/data'), Error, 'Network timeout error'); - - _stub.restore(); - }); - }); - - describe('Unknown error handling', () => { - it('wraps unknown object errors with default message for HTTP requests', async () => { - const unknownError = { - status: 'failed', - details: 'Something went wrong', - timestamp: Date.now(), - }; - const _stub = stub(http, 'senderFn' as keyof Http, () => Promise.reject(unknownError)); - - await assertRejects(() => http.get('https://api.example.com/data'), Error, 'An unknown error occurred'); - - _stub.restore(); - }); - - it('wraps string errors with default message for HTTP requests', async () => { - const stringError = 'Connection refused'; - const _stub = stub(http, 'senderFn' as keyof Http, () => Promise.reject(stringError)); - - await assertRejects(() => http.get('https://api.example.com/data'), Error, 'An unknown error occurred'); - - _stub.restore(); - }); - - it('wraps null/undefined errors with default message for HTTP requests', async () => { - const _stub = stub(http, 'senderFn' as keyof Http, () => Promise.reject(null)); - - await assertRejects(() => http.get('https://api.example.com/data'), Error, 'An unknown error occurred'); - - _stub.restore(); - }); - }); -}); diff --git a/packages/apps-engine/deno-runtime/lib/ast/mod.ts b/packages/apps-engine/deno-runtime/lib/ast/mod.ts deleted file mode 100644 index 555b4defc36a0..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/ast/mod.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { generate } from 'astring'; -// @deno-types="../../acorn.d.ts" -import { parse, Program } from 'acorn'; -// @deno-types="../../acorn-walk.d.ts" -import { fullAncestor } from 'acorn-walk'; - -import * as operations from './operations.ts'; -import type { WalkerState } from './operations.ts'; - -function fixAst(ast: Program): boolean { - const pendingOperations = [ - operations.fixLivechatIsOnlineCalls, - operations.checkReassignmentOfModifiedIdentifiers, - operations.fixRoomUsernamesCalls, - ]; - - // Have we touched the tree? - let isModified = false; - - while (pendingOperations.length) { - const ops = pendingOperations.splice(0); - const state: WalkerState = { - isModified: false, - functionIdentifiers: new Set(), - }; - - fullAncestor( - ast, - (node, state, ancestors, type) => { - ops.forEach((operation) => operation(node, state, ancestors, type)); - }, - undefined, - state, - ); - - if (state.isModified) { - isModified = true; - } - - if (state.functionIdentifiers.size) { - pendingOperations.push( - operations.buildFixModifiedFunctionsOperation(state.functionIdentifiers), - operations.checkReassignmentOfModifiedIdentifiers, - ); - } - } - - return isModified; -} - -export function fixBrokenSynchronousAPICalls(appSource: string): string { - const astRootNode = parse(appSource, { - // Latest ecma version supported by this version of acorn. - ecmaVersion: "latest", - // Allow everything, we don't want to complain if code is badly written - // Also, since the code itself has been transpiled, the chance of getting - // shenanigans is lower - allowReserved: true, - allowReturnOutsideFunction: true, - allowImportExportEverywhere: true, - allowAwaitOutsideFunction: true, - allowSuperOutsideMethod: true, - }); - - if (fixAst(astRootNode)) { - return generate(astRootNode); - } - - return appSource; -} diff --git a/packages/apps-engine/deno-runtime/lib/ast/operations.ts b/packages/apps-engine/deno-runtime/lib/ast/operations.ts deleted file mode 100644 index 7a5a4993ad297..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/ast/operations.ts +++ /dev/null @@ -1,237 +0,0 @@ -// @deno-types="../../acorn.d.ts" -import { AnyNode, AssignmentExpression, AwaitExpression, Expression, Function, Identifier, MethodDefinition, Property } from 'acorn'; -// @deno-types="../../acorn-walk.d.ts" -import { FullAncestorWalkerCallback } from 'acorn-walk'; - -export type WalkerState = { - isModified: boolean; - functionIdentifiers: Set; -}; - -export function getFunctionIdentifier(ancestors: AnyNode[], functionNodeIndex: number) { - const parent = ancestors[functionNodeIndex - 1]; - - // If there is a parent node and it's not a computed property, we can try to - // extract an identifier for our function from it. This needs to be done first - // because when functions are assigned to named symbols, this will be the only - // way to call it, even if the function itself has an identifier - // Consider the following block: - // - // const foo = function bar() {} - // - // Even though the function itself has a name, the only way to call it in the - // program is wiht `foo()` - if (parent && !(parent as Property | MethodDefinition).computed) { - // Several node types can have an id prop of type Identifier - const { id } = parent as unknown as { id?: Identifier }; - if (id?.type === 'Identifier') { - return id.name; - } - - // Usually assignments to object properties (MethodDefinition, Property) - const { key } = parent as MethodDefinition | Property; - if (key?.type === 'Identifier') { - return key.name; - } - - // Variable assignments have left hand side that can be used as Identifier - const { left } = parent as AssignmentExpression; - - // Simple assignment: `const fn = () => {}` - if (left?.type === 'Identifier') { - return left.name; - } - - // Object property assignment: `obj.fn = () => {}` - if (left?.type === 'MemberExpression' && !left.computed) { - return (left.property as Identifier).name; - } - } - - // nodeIndex needs to be the index of a Function node (either FunctionDeclaration or FunctionExpression) - const currentNode = ancestors[functionNodeIndex] as Function; - - // Function declarations or expressions can be directly named - if (currentNode.id?.type === 'Identifier') { - return currentNode.id.name; - } -} - -export function wrapWithAwait(node: Expression) { - if (!node.type.endsWith('Expression')) { - throw new Error(`Can't wrap "${node.type}" with await`); - } - - const innerNode: Expression = { ...node }; - - node.type = 'AwaitExpression'; - // starting here node has become an AwaitExpression - (node as AwaitExpression).argument = innerNode; - - Object.keys(node).forEach((key) => !['type', 'argument'].includes(key) && delete node[key as keyof AnyNode]); -} - -export function asyncifyScope(ancestors: AnyNode[], state: WalkerState) { - const functionNodeIndex = ancestors.findLastIndex((n) => 'async' in n); - if (functionNodeIndex === -1) return; - - // At this point this is a node with an "async" property, so it has to be - // of type Function - let TS know about that - const functionScopeNode = ancestors[functionNodeIndex] as Function; - - if (functionScopeNode.async) { - return; - } - - functionScopeNode.async = true; - - // If the parent of a function node is a call expression, we're talking about an IIFE - // Should we care about this case as well? - // const parentNode = ancestors[functionScopeIndex-1]; - // if (parentNode?.type === 'CallExpression' && ancestors[functionScopeIndex-2] && ancestors[functionScopeIndex-2].type !== 'AwaitExpression') { - // pendingOperations.push(buildFunctionPredicate(getFunctionIdentifier(ancestors, functionScopeIndex-2))); - // } - - const identifier = getFunctionIdentifier(ancestors, functionNodeIndex); - - // We can't fix calls of functions which name we can't determine at compile time - if (!identifier) return; - - state.functionIdentifiers.add(identifier); -} - -export function buildFixModifiedFunctionsOperation(functionIdentifiers: Set): FullAncestorWalkerCallback { - return function _fixModifiedFunctionsOperation(node, state, ancestors) { - if (node.type !== 'CallExpression') return; - - let isWrappable = false; - - // This node is a simple call to a function, like `fn()` - isWrappable = node.callee.type === 'Identifier' && functionIdentifiers.has(node.callee.name); - - // This node is a call to an object property or instance method, like `obj.fn()`, but not computed like `obj[fn]()` - isWrappable ||= node.callee.type === 'MemberExpression' && - !node.callee.computed && - node.callee.property?.type === 'Identifier' && - functionIdentifiers.has(node.callee.property.name); - - // This is a weird dereferencing technique used by bundlers, and since we'll be dealing with bundled sources we have to check for it - // e.g. `r=(0,fn)(e)` - if (!isWrappable && node.callee.type === 'SequenceExpression') { - const [, secondExpression] = node.callee.expressions; - isWrappable = secondExpression?.type === 'Identifier' && functionIdentifiers.has(secondExpression.name); - isWrappable ||= secondExpression?.type === 'MemberExpression' && - !secondExpression.computed && - secondExpression.property.type === 'Identifier' && - functionIdentifiers.has(secondExpression.property.name); - } - - if (!isWrappable) return; - - // ancestors[ancestors.length-1] === node, so here we're checking for parent node - const parentNode = ancestors[ancestors.length - 2]; - if (!parentNode || parentNode.type === 'AwaitExpression') return; - - wrapWithAwait(node); - asyncifyScope(ancestors, state); - - state.isModified = true; - }; -} - -export const checkReassignmentOfModifiedIdentifiers: FullAncestorWalkerCallback = (node, { functionIdentifiers }, _ancestors) => { - if (node.type === 'AssignmentExpression') { - if (node.operator !== '=') return; - - let identifier = ''; - - if (node.left.type === 'Identifier') identifier = node.left.name; - - if (node.left.type === 'MemberExpression' && !node.left.computed) { - identifier = (node.left.property as Identifier).name; - } - - if (!identifier || node.right.type !== 'Identifier' || !functionIdentifiers.has(node.right.name)) return; - - functionIdentifiers.add(identifier); - - return; - } - - if (node.type === 'VariableDeclarator') { - if (node.id.type !== 'Identifier' || functionIdentifiers.has(node.id.name)) return; - - if (node.init?.type !== 'Identifier' || !functionIdentifiers.has(node.init?.name)) return; - - functionIdentifiers.add(node.id.name); - - return; - } - - // "Property" is for plain objects, "PropertyDefinition" is for classes - // but both share the same structure - if (node.type === 'Property' || node.type === 'PropertyDefinition') { - if (node.key.type !== 'Identifier' || functionIdentifiers.has(node.key.name)) return; - - if (node.value?.type !== 'Identifier' || !functionIdentifiers.has(node.value.name)) return; - - functionIdentifiers.add(node.key.name); - - return; - } -}; - -export const fixLivechatIsOnlineCalls: FullAncestorWalkerCallback = (node, state, ancestors) => { - if (node.type !== 'MemberExpression' || node.computed) return; - - if ((node.property as Identifier).name !== 'isOnline') return; - - if (node.object.type !== 'CallExpression') return; - - if (node.object.callee.type !== 'MemberExpression') return; - - if ((node.object.callee.property as Identifier).name !== 'getLivechatReader') return; - - let parentIndex = ancestors.length - 2; - let targetNode = ancestors[parentIndex]; - - if (targetNode.type !== 'CallExpression') { - targetNode = node; - } else { - parentIndex--; - } - - // If we're already wrapped with an await, nothing to do - if (ancestors[parentIndex].type === 'AwaitExpression') return; - - // If we're in the middle of a chained member access, we can't wrap with await - if (ancestors[parentIndex].type === 'MemberExpression') return; - - wrapWithAwait(targetNode); - asyncifyScope(ancestors, state); - - state.isModified = true; -}; - -export const fixRoomUsernamesCalls: FullAncestorWalkerCallback = (node, state, ancestors) => { - if (node.type !== 'MemberExpression' || node.computed) return; - - if ((node.property as Identifier).name !== 'usernames') return; - - let parentIndex = ancestors.length - 2; - let targetNode = ancestors[parentIndex]; - - if (targetNode.type !== 'CallExpression') { - targetNode = node; - } else { - parentIndex--; - } - - // If we're already wrapped with an await, nothing to do - if (ancestors[parentIndex].type === 'AwaitExpression') return; - - wrapWithAwait(targetNode); - asyncifyScope(ancestors, state); - - state.isModified = true; -}; diff --git a/packages/apps-engine/deno-runtime/lib/ast/tests/data/ast_blocks.ts b/packages/apps-engine/deno-runtime/lib/ast/tests/data/ast_blocks.ts deleted file mode 100644 index 8e750e6eaf587..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/ast/tests/data/ast_blocks.ts +++ /dev/null @@ -1,436 +0,0 @@ -// @deno-types="../../../../acorn.d.ts" -import { AnyNode, ClassDeclaration, ExpressionStatement, FunctionDeclaration, VariableDeclaration } from 'acorn'; - -/** - * Partial AST blocks to support testing. - * `start` and `end` properties are omitted for brevity. - */ - -type TestNodeExcerpt = { - code: string; - node: N; -}; - -export const FunctionDeclarationFoo: TestNodeExcerpt = { - code: 'function foo() {}', - node: { - type: 'FunctionDeclaration', - id: { - type: 'Identifier', - name: 'foo', - }, - expression: false, - generator: false, - async: false, - params: [], - body: { - type: 'BlockStatement', - body: [], - }, - }, -}; - -export const ConstFooAssignedFunctionExpression: TestNodeExcerpt = { - code: 'const foo = function() {}', - node: { - type: 'VariableDeclaration', - kind: 'const', - declarations: [ - { - type: 'VariableDeclarator', - id: { - type: 'Identifier', - name: 'foo', - }, - init: { - type: 'FunctionExpression', - id: null, - expression: false, - generator: false, - async: false, - params: [], - body: { - type: 'BlockStatement', - body: [], - }, - }, - }, - ], - }, -}; - -export const AssignmentExpressionOfArrowFunctionToFooIdentifier: TestNodeExcerpt = { - code: 'foo = () => {}', - node: { - type: 'ExpressionStatement', - expression: { - type: 'AssignmentExpression', - operator: '=', - left: { - type: 'Identifier', - name: 'foo', - }, - right: { - type: 'ArrowFunctionExpression', - id: null, - expression: false, - generator: false, - async: false, - params: [], - body: { - type: 'BlockStatement', - body: [], - }, - }, - }, - }, -}; - -export const AssignmentExpressionOfNamedFunctionToFooMemberExpression: TestNodeExcerpt = { - code: 'obj.foo = function bar() {}', - node: { - type: 'ExpressionStatement', - expression: { - type: 'AssignmentExpression', - operator: '=', - left: { - type: 'MemberExpression', - object: { - type: 'Identifier', - name: 'a', - }, - property: { - type: 'Identifier', - name: 'foo', - }, - computed: false, - optional: false, - }, - right: { - type: 'FunctionExpression', - id: null, - expression: false, - generator: false, - async: false, - params: [], - body: { - type: 'BlockStatement', - body: [], - }, - }, - }, - }, -}; - -export const MethodDefinitionOfFooInClassBar: TestNodeExcerpt = { - code: 'class Bar { foo() {} }', - node: { - type: 'ClassDeclaration', - id: { - type: 'Identifier', - name: 'Bar', - }, - superClass: null, - body: { - type: 'ClassBody', - body: [ - { - type: 'MethodDefinition', - key: { - type: 'Identifier', - name: 'foo', - }, - value: { - type: 'FunctionExpression', - id: null, - expression: false, - generator: false, - async: false, - params: [], - body: { - type: 'BlockStatement', - body: [], - }, - }, - kind: 'method', - computed: false, - static: false, - }, - ], - }, - }, -}; - -export const SimpleCallExpressionOfFoo: TestNodeExcerpt = { - code: 'foo()', - node: { - type: 'ExpressionStatement', - expression: { - type: 'CallExpression', - callee: { - type: 'Identifier', - name: 'foo', - }, - arguments: [], - optional: false, - }, - }, -}; - -export const SyncFunctionDeclarationWithAsyncCallExpression: TestNodeExcerpt = { - // NOTE: this is invalid syntax, it won't be parsed by acorn - // but it can be an intermediary state of the AST after we run - // `wrapWithAwait` on "bar" call expressions, for instance - code: 'function foo() { return () => await bar() }', - node: { - type: 'FunctionDeclaration', - id: { - type: 'Identifier', - name: 'foo', - }, - expression: false, - generator: false, - async: false, - params: [], - body: { - type: 'BlockStatement', - body: [ - { - type: 'ReturnStatement', - argument: { - type: 'ArrowFunctionExpression', - id: null, - expression: true, - generator: false, - async: false, - params: [], - body: { - type: 'AwaitExpression', - argument: { - type: 'CallExpression', - callee: { - type: 'Identifier', - name: 'bar', - }, - arguments: [], - optional: false, - }, - }, - }, - }, - ], - }, - }, -}; - -export const AssignmentOfFooToBar: TestNodeExcerpt = { - code: 'bar = foo', - node: { - type: 'ExpressionStatement', - expression: { - type: 'AssignmentExpression', - operator: '=', - left: { - type: 'Identifier', - name: 'bar', - }, - right: { - type: 'Identifier', - name: 'foo', - }, - }, - }, -}; - -export const AssignmentOfFooToBarMemberExpression: TestNodeExcerpt = { - code: 'obj.bar = foo', - node: { - type: 'ExpressionStatement', - expression: { - type: 'AssignmentExpression', - operator: '=', - left: { - type: 'MemberExpression', - computed: false, - optional: false, - object: { - type: 'Identifier', - name: 'obj', - }, - property: { - type: 'Identifier', - name: 'bar', - }, - }, - right: { - type: 'Identifier', - name: 'foo', - }, - }, - }, -}; - -export const AssignmentOfFooToBarVariableDeclarator: TestNodeExcerpt = { - code: 'const bar = foo', - node: { - type: 'VariableDeclaration', - kind: 'const', - declarations: [ - { - type: 'VariableDeclarator', - id: { - type: 'Identifier', - name: 'bar', - }, - init: { - type: 'Identifier', - name: 'foo', - }, - }, - ], - }, -}; - -export const AssignmentOfFooToBarPropertyDefinition: TestNodeExcerpt = { - code: 'class baz { bar = foo }', - node: { - type: 'ClassDeclaration', - id: { - type: 'Identifier', - name: 'baz', - }, - superClass: null, - body: { - type: 'ClassBody', - body: [ - { - type: 'PropertyDefinition', - static: false, - computed: false, - key: { - type: 'Identifier', - name: 'bar', - }, - value: { - type: 'Identifier', - name: 'foo', - }, - }, - ], - }, - }, -}; - -const fixSimpleCallExpressionCode = ` -function bar() { - const a = foo(); - - return a; -}`; - -export const FixSimpleCallExpression: TestNodeExcerpt = { - code: fixSimpleCallExpressionCode, - node: { - type: 'FunctionDeclaration', - id: { - type: 'Identifier', - name: 'bar', - }, - expression: false, - generator: false, - async: false, - params: [], - body: { - type: 'BlockStatement', - body: [ - { - type: 'VariableDeclaration', - kind: 'const', - declarations: [ - { - type: 'VariableDeclarator', - id: { - type: 'Identifier', - name: 'a', - }, - init: { - type: 'CallExpression', - callee: { - type: 'Identifier', - name: 'foo', - }, - arguments: [], - optional: false, - }, - }, - ], - }, - { - type: 'ReturnStatement', - argument: { - type: 'Identifier', - name: 'a', - }, - }, - ], - }, - }, -}; - -export const ArrowFunctionDerefCallExpression: TestNodeExcerpt = { - // NOTE: this call strategy is widely used by bundlers; it's used to sever the `this` - // reference in the method from the object that contains it. This is mostly because - // the bundler wants to ensure that it does not messes up the bindings in the code it - // generates. - // - // This would be similar to doing `foo.call(undefined)` - code: 'const bar = () => (0, e.foo)();', - node: { - type: 'VariableDeclaration', - kind: 'const', - declarations: [ - { - type: 'VariableDeclarator', - id: { - type: 'Identifier', - name: 'bar', - }, - init: { - type: 'ArrowFunctionExpression', - id: null, - expression: true, - generator: false, - async: false, - params: [], - body: { - type: 'CallExpression', - optional: false, - arguments: [], - callee: { - type: 'SequenceExpression', - expressions: [ - { - type: 'Literal', - value: 0, - }, - { - type: 'MemberExpression', - object: { - type: 'Identifier', - name: 'e', - }, - property: { - type: 'Identifier', - name: 'foo', - }, - computed: false, - optional: false, - }, - ], - }, - }, - }, - }, - ], - }, -}; diff --git a/packages/apps-engine/deno-runtime/lib/ast/tests/operations.test.ts b/packages/apps-engine/deno-runtime/lib/ast/tests/operations.test.ts deleted file mode 100644 index 809de475013c9..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/ast/tests/operations.test.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { assertEquals, assertThrows } from 'https://deno.land/std@0.203.0/assert/mod.ts'; -import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; - -import { - asyncifyScope, - buildFixModifiedFunctionsOperation, - checkReassignmentOfModifiedIdentifiers, - getFunctionIdentifier, - WalkerState, - wrapWithAwait, -} from '../operations.ts'; -import { - ArrowFunctionDerefCallExpression, - AssignmentExpressionOfArrowFunctionToFooIdentifier, - AssignmentExpressionOfNamedFunctionToFooMemberExpression, - AssignmentOfFooToBar, - AssignmentOfFooToBarMemberExpression, - AssignmentOfFooToBarPropertyDefinition, - AssignmentOfFooToBarVariableDeclarator, - ConstFooAssignedFunctionExpression, - FixSimpleCallExpression, - FunctionDeclarationFoo, - MethodDefinitionOfFooInClassBar, - SimpleCallExpressionOfFoo, - SyncFunctionDeclarationWithAsyncCallExpression, -} from './data/ast_blocks.ts'; -import { - AnyNode, - ArrowFunctionExpression, - AssignmentExpression, - AwaitExpression, - Expression, - MethodDefinition, - ReturnStatement, - VariableDeclaration, -} from '../../../acorn.d.ts'; -import { assertNotEquals } from 'https://deno.land/std@0.203.0/assert/assert_not_equals.ts'; - -describe('getFunctionIdentifier', () => { - it(`identifies the name "foo" for the code \`${FunctionDeclarationFoo.code}\``, () => { - // ancestors array is built by the walking lib - const nodeAncestors = [FunctionDeclarationFoo.node]; - const functionNodeIndex = 0; - assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); - }); - - it(`identifies the name "foo" for the code \`${ConstFooAssignedFunctionExpression.code}\``, () => { - // ancestors array is built by the walking lib - const nodeAncestors = [ - ConstFooAssignedFunctionExpression.node, // VariableDeclaration - ConstFooAssignedFunctionExpression.node.declarations[0], // VariableDeclarator - ConstFooAssignedFunctionExpression.node.declarations[0].init!, // FunctionExpression - ]; - const functionNodeIndex = 2; - assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); - }); - - it(`identifies the name "foo" for the code \`${AssignmentExpressionOfArrowFunctionToFooIdentifier.code}\``, () => { - // ancestors array is built by the walking lib - const nodeAncestors = [ - AssignmentExpressionOfArrowFunctionToFooIdentifier.node, // ExpressionStatement - AssignmentExpressionOfArrowFunctionToFooIdentifier.node.expression, // AssignmentExpression - (AssignmentExpressionOfArrowFunctionToFooIdentifier.node.expression as AssignmentExpression).right, // ArrowFunctionExpression - ]; - const functionNodeIndex = 2; - assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); - }); - - it(`identifies the name "foo" for the code \`${AssignmentExpressionOfNamedFunctionToFooMemberExpression.code}\``, () => { - // ancestors array is built by the walking lib - const nodeAncestors = [ - AssignmentExpressionOfNamedFunctionToFooMemberExpression.node, // ExpressionStatement - AssignmentExpressionOfNamedFunctionToFooMemberExpression.node.expression, // AssignmentExpression - (AssignmentExpressionOfNamedFunctionToFooMemberExpression.node.expression as AssignmentExpression).right, // FunctionExpression - ]; - const functionNodeIndex = 2; - assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); - }); - - it(`identifies the name "foo" for the code \`${MethodDefinitionOfFooInClassBar.code}\``, () => { - // ancestors array is built by the walking lib - const nodeAncestors = [ - MethodDefinitionOfFooInClassBar.node, // ClassDeclaration - MethodDefinitionOfFooInClassBar.node.body, // ClassBody - MethodDefinitionOfFooInClassBar.node.body!.body[0], // MethodDefinition - (MethodDefinitionOfFooInClassBar.node.body!.body[0] as MethodDefinition).value, // FunctionExpression - ]; - const functionNodeIndex = 3; - assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); - }); -}); - -describe('wrapWithAwait', () => { - it('wraps a call expression with await', () => { - const node = structuredClone(SimpleCallExpressionOfFoo.node.expression); - wrapWithAwait(node); - - assertEquals('AwaitExpression', node.type); - assertNotEquals(SimpleCallExpressionOfFoo.node.expression.type, node.type); - assertEquals(SimpleCallExpressionOfFoo.node.expression, (node as AwaitExpression).argument); - }); - - it('throws if node is not an expression', () => { - const node = structuredClone(SimpleCallExpressionOfFoo.node); - assertThrows(() => wrapWithAwait(node as unknown as Expression)); - }); -}); - -describe('asyncifyScope', () => { - it('makes only the first function scope async', () => { - const node = structuredClone(SyncFunctionDeclarationWithAsyncCallExpression.node); - const ancestors: AnyNode[] = [ - node, // FunctionDeclaration - node.body, // BlockStatement - node.body!.body[0], // ReturnStatement - (node.body!.body[0] as ReturnStatement).argument!, // ArrowFunctionExpression - ((node.body!.body[0] as ReturnStatement).argument! as ArrowFunctionExpression).body, // AwaitExpression - (((node.body!.body[0] as ReturnStatement).argument! as ArrowFunctionExpression).body as AwaitExpression).argument, // CallExpression - ]; - const state: WalkerState = { - isModified: false, - functionIdentifiers: new Set(), - }; - - asyncifyScope(ancestors, state); - - // Assert the function did indeed change the expression to async - assertEquals(((node.body.body[0] as ReturnStatement).argument as ArrowFunctionExpression).async, true); - - // Assert the function did NOT change all ancestors in the chain - assertEquals(node.async, false); - - // Assert it couldn't find a function identifier - assertEquals(state.functionIdentifiers.size, 0); - }); -}); - -describe('checkReassignmentofModifiedIdentifiers', () => { - it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBar.code}"`, () => { - const node = structuredClone(AssignmentOfFooToBar.node); - const ancestors: AnyNode[] = [ - node, // ExpressionStatement - node.expression, // AssignmentExpression - (node.expression as AssignmentExpression).right, // Identifier - ]; - const state: WalkerState = { - isModified: true, - functionIdentifiers: new Set(['foo']), - }; - - checkReassignmentOfModifiedIdentifiers(node.expression, state, ancestors, ''); - - assertEquals(state.functionIdentifiers.has('bar'), true); - }); - - it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBarMemberExpression.code}"`, () => { - const node = structuredClone(AssignmentOfFooToBarMemberExpression.node); - const ancestors: AnyNode[] = [ - node, // ExpressionStatement - node.expression, // AssignmentExpression - (node.expression as AssignmentExpression).right, // Identifier - ]; - const state: WalkerState = { - isModified: true, - functionIdentifiers: new Set(['foo']), - }; - - checkReassignmentOfModifiedIdentifiers(node.expression, state, ancestors, ''); - - assertEquals(state.functionIdentifiers.has('bar'), true); - }); - - it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBarVariableDeclarator.code}"`, () => { - const node = structuredClone(AssignmentOfFooToBarVariableDeclarator.node); - const ancestors: AnyNode[] = [ - node, // VariableDeclaration - node.declarations[0], // VariableDeclarator - ]; - const state: WalkerState = { - isModified: true, - functionIdentifiers: new Set(['foo']), - }; - - checkReassignmentOfModifiedIdentifiers(node.declarations[0], state, ancestors, ''); - - assertEquals(state.functionIdentifiers.has('bar'), true); - }); - - it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBarPropertyDefinition.code}"`, () => { - const node = structuredClone(AssignmentOfFooToBarPropertyDefinition.node); - const ancestors: AnyNode[] = [ - node, // ClassDeclaration - node.body, // ClassBody - node.body.body[0], // PropertyDefinition - ]; - const state: WalkerState = { - isModified: true, - functionIdentifiers: new Set(['foo']), - }; - - checkReassignmentOfModifiedIdentifiers(node.body.body[0], state, ancestors, ''); - - assertEquals(state.functionIdentifiers.has('bar'), true); - }); -}); - -describe('buildFixModifiedFunctionsOperation', function () { - const state: WalkerState = { - isModified: false, - functionIdentifiers: new Set(['foo']), - }; - - const fixFunction = buildFixModifiedFunctionsOperation(state.functionIdentifiers); - - beforeEach(() => { - state.isModified = false; - state.functionIdentifiers = new Set(['foo']); - }); - - it(`fixes calls of "foo" in the code "${FixSimpleCallExpression.code}"`, () => { - const node = structuredClone(FixSimpleCallExpression.node); - const ancestors: AnyNode[] = [ - node, // FunctionDeclaration - node.body, // BlockStatement - node.body.body[0], // VariableDeclaration - (node.body.body[0] as VariableDeclaration).declarations[0], // VariableDeclarator - (node.body.body[0] as VariableDeclaration).declarations[0].init!, // CallExpression - ]; - - fixFunction(ancestors[4], state, ancestors, ''); - - assertEquals(state.isModified, true); - assertEquals(state.functionIdentifiers.has('bar'), true); - assertNotEquals(FixSimpleCallExpression.node, node); - assertEquals(node.async, true); - assertEquals(ancestors[4].type, 'AwaitExpression'); - }); - - it(`fixes calls of "foo" in the code "${ArrowFunctionDerefCallExpression.code}"`, () => { - const node = structuredClone(ArrowFunctionDerefCallExpression.node); - const ancestors: AnyNode[] = [ - node, // VariableDeclaration - node.declarations[0], // VariableDeclarator - node.declarations[0].init!, // ArrowFunctionExpression - (node.declarations[0].init as ArrowFunctionExpression).body, // CallExpression - ]; - - fixFunction(ancestors[3], state, ancestors, ''); - - // Recorded that a modification has been made - assertEquals(state.isModified, true); - // Recorded that the enclosing scope of the call also requires fixing - assertEquals(state.functionIdentifiers.has('bar'), true); - // Original node and fixed node are different - assertNotEquals(ArrowFunctionDerefCallExpression.node, node); - // The function call is now await'ed - assertEquals(ancestors[3].type, 'AwaitExpression'); - // The parent function of the call is now marked as async - assertEquals((ancestors[2] as ArrowFunctionExpression).async, true); - }); -}); diff --git a/packages/apps-engine/deno-runtime/lib/codec.ts b/packages/apps-engine/deno-runtime/lib/codec.ts deleted file mode 100644 index 95bbfa1a2aa26..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/codec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Buffer } from 'node:buffer'; -import { Decoder, Encoder, ExtensionCodec } from '@msgpack/msgpack'; - -import type { App as _App } from '@rocket.chat/apps-engine/definition/App.ts'; -import { require } from './require.ts'; - -const { App } = require('@rocket.chat/apps-engine/definition/App.js') as { - App: typeof _App; -}; - -const extensionCodec = new ExtensionCodec(); - -extensionCodec.register({ - type: 0, - encode: (object: unknown) => { - // We don't care about functions, but also don't want to throw an error - if (typeof object === 'function' || object instanceof App) { - return new Uint8Array(0); - } - - return null; - }, - decode: (_data: Uint8Array) => undefined, -}); - -// Since Deno doesn't have Buffer by default, we need to use Uint8Array -extensionCodec.register({ - type: 1, - encode: (object: unknown) => { - if (object instanceof Buffer) { - return new Uint8Array(object.buffer, object.byteOffset, object.byteLength); - } - - return null; - }, - // msgpack will reuse the Uint8Array instance, so WE NEED to copy it instead of simply creating a view - decode: (data: Uint8Array) => { - return Buffer.from(data); - }, -}); - -export const encoder = new Encoder({ extensionCodec }); -export const decoder = new Decoder({ extensionCodec }); diff --git a/packages/apps-engine/deno-runtime/lib/logger.ts b/packages/apps-engine/deno-runtime/lib/logger.ts deleted file mode 100644 index 336c420080d37..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/logger.ts +++ /dev/null @@ -1,142 +0,0 @@ -import stackTrace from 'stack-trace'; -import { AppObjectRegistry } from '../AppObjectRegistry.ts'; - -export interface StackFrame { - getTypeName(): string; - getFunctionName(): string; - getMethodName(): string; - getFileName(): string; - getLineNumber(): number; - getColumnNumber(): number; - isNative(): boolean; - isConstructor(): boolean; -} - -enum LogMessageSeverity { - DEBUG = 'debug', - INFORMATION = 'info', - LOG = 'log', - WARNING = 'warning', - ERROR = 'error', - SUCCESS = 'success', -} - -type Entry = { - caller: string; - severity: LogMessageSeverity; - method: string; - timestamp: Date; - args: Array; -}; - -interface ILoggerStorageEntry { - appId: string; - method: string; - entries: Array; - startTime: Date; - endTime: Date; - totalTime: number; - _createdAt: Date; -} - -export class Logger { - private entries: Array; - private start: Date; - private method: string; - - constructor(method: string) { - this.method = method; - this.entries = []; - this.start = new Date(); - } - - public debug(...args: Array): void { - this.addEntry(LogMessageSeverity.DEBUG, this.getStack(stackTrace.get()), ...args); - } - - public info(...args: Array): void { - this.addEntry(LogMessageSeverity.INFORMATION, this.getStack(stackTrace.get()), ...args); - } - - public log(...args: Array): void { - this.addEntry(LogMessageSeverity.LOG, this.getStack(stackTrace.get()), ...args); - } - - public warn(...args: Array): void { - this.addEntry(LogMessageSeverity.WARNING, this.getStack(stackTrace.get()), ...args); - } - - public error(...args: Array): void { - this.addEntry(LogMessageSeverity.ERROR, this.getStack(stackTrace.get()), ...args); - } - - public success(...args: Array): void { - this.addEntry(LogMessageSeverity.SUCCESS, this.getStack(stackTrace.get()), ...args); - } - - private addEntry(severity: LogMessageSeverity, caller: string, ...items: Array): void { - const i = items.map((args) => { - if (args instanceof Error) { - return JSON.stringify(args, Object.getOwnPropertyNames(args)); - } - if (typeof args === 'object' && args !== null && 'stack' in args) { - return JSON.stringify(args, Object.getOwnPropertyNames(args)); - } - if (typeof args === 'object' && args !== null && 'message' in args) { - return JSON.stringify(args, Object.getOwnPropertyNames(args)); - } - const str = JSON.stringify(args, null, 2); - return str ? JSON.parse(str) : str; // force call toJSON to prevent circular references - }); - - this.entries.push({ - caller, - severity, - method: this.method, - timestamp: new Date(), - args: i, - }); - } - - private getStack(stack: Array): string { - let func = 'anonymous'; - - if (stack.length === 1) { - return func; - } - - const frame = stack[1]; - - if (frame.getMethodName() === null) { - func = 'anonymous OR constructor'; - } else { - func = frame.getMethodName(); - } - - if (frame.getFunctionName() !== null) { - func = `${func} -> ${frame.getFunctionName()}`; - } - - return func; - } - - private getTotalTime(): number { - return new Date().getTime() - this.start.getTime(); - } - - public hasEntries(): boolean { - return this.entries.length > 0; - } - - public getLogs(): ILoggerStorageEntry { - return { - appId: AppObjectRegistry.get('id')!, - method: this.method, - entries: this.entries, - startTime: this.start, - endTime: new Date(), - totalTime: this.getTotalTime(), - _createdAt: new Date(), - }; - } -} diff --git a/packages/apps-engine/deno-runtime/lib/messenger.ts b/packages/apps-engine/deno-runtime/lib/messenger.ts deleted file mode 100644 index 3a55aba594f7c..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/messenger.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { writeAll } from '@std/io'; - -import * as jsonrpc from 'jsonrpc-lite'; - -import { encoder } from './codec.ts'; -import { RequestContext } from './requestContext.ts'; - -export type RequestDescriptor = Pick; - -export type NotificationDescriptor = Pick; - -export type SuccessResponseDescriptor = Pick; - -export type ErrorResponseDescriptor = Pick; - -export type JsonRpcRequest = jsonrpc.IParsedObjectRequest | jsonrpc.IParsedObjectNotification; -export type JsonRpcResponse = jsonrpc.IParsedObjectSuccess | jsonrpc.IParsedObjectError; - -export function isRequest(message: jsonrpc.IParsedObject): message is JsonRpcRequest { - return message.type === 'request' || message.type === 'notification'; -} - -export function isResponse(message: jsonrpc.IParsedObject): message is JsonRpcResponse { - return message.type === 'success' || message.type === 'error'; -} - -export function isErrorResponse(message: jsonrpc.JsonRpc): message is jsonrpc.ErrorObject { - return message instanceof jsonrpc.ErrorObject; -} - -const COMMAND_PONG = '_zPONG'; - -export const RPCResponseObserver = new EventTarget(); - -export const Queue = new (class Queue { - private queue: Uint8Array[] = []; - private isProcessing = false; - - private async processQueue() { - if (this.isProcessing) { - return; - } - - this.isProcessing = true; - - while (this.queue.length) { - const message = this.queue.shift(); - - if (message) { - await Transport.send(message); - } - } - - this.isProcessing = false; - } - - public enqueue(message: jsonrpc.JsonRpc | typeof COMMAND_PONG) { - this.queue.push(encoder.encode(message)); - this.processQueue(); - } - - public getCurrentSize() { - return this.queue.length; - } -})(); - -export const Transport = new (class Transporter { - private selectedTransport: Transporter['stdoutTransport'] | Transporter['noopTransport']; - - constructor() { - this.selectedTransport = this.stdoutTransport.bind(this); - } - - private async stdoutTransport(message: Uint8Array): Promise { - await writeAll(Deno.stdout, message); - } - - private async noopTransport(_message: Uint8Array): Promise {} - - public selectTransport(transport: 'stdout' | 'noop'): void { - switch (transport) { - case 'stdout': - this.selectedTransport = this.stdoutTransport.bind(this); - break; - case 'noop': - this.selectedTransport = this.noopTransport.bind(this); - break; - } - } - - public send(message: Uint8Array): Promise { - return this.selectedTransport(message); - } -})(); - -export function parseMessage(message: string | Record) { - let parsed: jsonrpc.IParsedObject | jsonrpc.IParsedObject[]; - - if (typeof message === 'string') { - parsed = jsonrpc.parse(message); - } else { - parsed = jsonrpc.parseObject(message); - } - - if (Array.isArray(parsed)) { - throw jsonrpc.error(null, jsonrpc.JsonRpcError.invalidRequest(null)); - } - - if (parsed.type === 'invalid') { - throw jsonrpc.error(null, parsed.payload); - } - - return parsed; -} - -export async function sendInvalidRequestError(): Promise { - const rpc = jsonrpc.error(null, jsonrpc.JsonRpcError.invalidRequest(null)); - - await Queue.enqueue(rpc); -} - -export async function sendInvalidParamsError(id: jsonrpc.ID): Promise { - const rpc = jsonrpc.error(id, jsonrpc.JsonRpcError.invalidParams(null)); - - await Queue.enqueue(rpc); -} - -export async function sendParseError(): Promise { - const rpc = jsonrpc.error(null, jsonrpc.JsonRpcError.parseError(null)); - - await Queue.enqueue(rpc); -} - -export async function sendMethodNotFound(id: jsonrpc.ID): Promise { - const rpc = jsonrpc.error(id, jsonrpc.JsonRpcError.methodNotFound(null)); - - await Queue.enqueue(rpc); -} - -export async function errorResponse({ error: { message, code = -32000, data = {} }, id }: ErrorResponseDescriptor, req?: RequestContext): Promise { - const { logger } = req?.context || {}; - - if (logger?.hasEntries()) { - data.logs = logger.getLogs(); - } - - const rpc = jsonrpc.error(id, new jsonrpc.JsonRpcError(message, code, data)); - - await Queue.enqueue(rpc); -} - -export async function successResponse({ id, result }: SuccessResponseDescriptor, req: RequestContext): Promise { - const payload = { value: result } as Record; - const { logger } = req.context; - - if (logger.hasEntries()) { - payload.logs = logger.getLogs(); - } - - const rpc = jsonrpc.success(id, payload); - - await Queue.enqueue(rpc); -} - -export function pongResponse(): Promise { - return Promise.resolve(Queue.enqueue(COMMAND_PONG)); -} - -export async function sendRequest(requestDescriptor: RequestDescriptor): Promise { - const request = jsonrpc.request(Math.random().toString(36).slice(2), requestDescriptor.method, requestDescriptor.params); - - // TODO: add timeout to this - const responsePromise = new Promise((resolve, reject) => { - const handler = (event: Event) => { - if (event instanceof ErrorEvent) { - reject(event.error); - } - - if (event instanceof CustomEvent) { - resolve(event.detail); - } - - RPCResponseObserver.removeEventListener(`response:${request.id}`, handler); - }; - - RPCResponseObserver.addEventListener(`response:${request.id}`, handler); - }); - - await Queue.enqueue(request); - - return responsePromise as Promise; -} - -export function sendNotification({ method, params }: NotificationDescriptor) { - const request = jsonrpc.notification(method, params); - - Queue.enqueue(request); -} - -export function log(params: jsonrpc.RpcParams) { - sendNotification({ method: 'log', params }); -} diff --git a/packages/apps-engine/deno-runtime/lib/metricsCollector.ts b/packages/apps-engine/deno-runtime/lib/metricsCollector.ts deleted file mode 100644 index 8484aef826f9b..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/metricsCollector.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { writeAll } from '@std/io'; -import { Queue } from './messenger.ts'; - -export function collectMetrics() { - return { - pid: Deno.pid, - queueSize: Queue.getCurrentSize(), - }; -} - -const encoder = new TextEncoder(); - -/** - * Sends metrics collected from the system via stderr - */ -export async function sendMetrics() { - const metrics = collectMetrics(); - - await writeAll(Deno.stderr, encoder.encode(JSON.stringify(metrics))); -} diff --git a/packages/apps-engine/deno-runtime/lib/parseArgs.ts b/packages/apps-engine/deno-runtime/lib/parseArgs.ts deleted file mode 100644 index a9c4844154990..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/parseArgs.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { parseArgs as $parseArgs } from '@std/cli/parse-args'; - -export type ParsedArgs = { - subprocess: string; - spawnId: number; - metricsReportFrequencyInMs?: number; -}; - -export function parseArgs(args: string[]): ParsedArgs { - return $parseArgs(args); -} diff --git a/packages/apps-engine/deno-runtime/lib/requestContext.ts b/packages/apps-engine/deno-runtime/lib/requestContext.ts deleted file mode 100644 index 91e9346f34bd4..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/requestContext.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { RequestObject } from 'jsonrpc-lite'; - -import { Logger } from './logger.ts'; - -export type RequestContext = RequestObject & { - context: { - logger: Logger; - [key: string]: unknown; - } -}; diff --git a/packages/apps-engine/deno-runtime/lib/require.ts b/packages/apps-engine/deno-runtime/lib/require.ts deleted file mode 100644 index 7d842d829e598..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/require.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createRequire } from 'node:module'; - -const _require = createRequire(import.meta.url); - -export const require = (mod: string) => { - // When we try to import something from the apps-engine, we resolve the path using import maps from Deno - // However, the import maps are configured to look at the source folder for typescript files, but during - // runtime those files are not available - if (mod.startsWith('@rocket.chat/apps-engine')) { - // Only remove "src/" substring when it comes after "apps-engine/" - mod = import.meta.resolve(mod).replace('file://', '').replace('apps-engine/src/', 'apps-engine/'); - } - - return _require(mod); -}; diff --git a/packages/apps-engine/deno-runtime/lib/room.ts b/packages/apps-engine/deno-runtime/lib/room.ts deleted file mode 100644 index 282ded4a90457..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/room.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; -import type { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; -import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; -import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager.ts'; - -const PrivateManager = Symbol('RoomPrivateManager'); - -export class Room { - public id: string | undefined; - - public displayName?: string; - - public slugifiedName: string | undefined; - - public type: RoomType | undefined; - - public creator: IUser | undefined; - - public isDefault?: boolean; - - public isReadOnly?: boolean; - - public displaySystemMessages?: boolean; - - public messageCount?: number; - - public createdAt?: Date; - - public updatedAt?: Date; - - public lastModifiedAt?: Date; - - public customFields?: { [key: string]: unknown }; - - public userIds?: Array; - - private _USERNAMES: Promise> | undefined; - - private [PrivateManager]: AppManager | undefined; - - /** - * @deprecated - */ - public get usernames(): Promise> { - if (!this._USERNAMES) { - this._USERNAMES = this[PrivateManager]?.getBridges().getInternalBridge().doGetUsernamesOfRoomById(this.id); - } - - return this._USERNAMES || Promise.resolve([]); - } - - public set usernames(usernames) {} - - 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 { - return { - id: this.id, - displayName: this.displayName, - slugifiedName: this.slugifiedName, - type: this.type, - creator: this.creator, - isDefault: this.isDefault, - isReadOnly: this.isReadOnly, - displaySystemMessages: this.displaySystemMessages, - messageCount: this.messageCount, - createdAt: this.createdAt, - updatedAt: this.updatedAt, - lastModifiedAt: this.lastModifiedAt, - customFields: this.customFields, - userIds: this.userIds, - }; - } - - public async getUsernames(): Promise> { - // Get usernames - if (!this._USERNAMES) { - this._USERNAMES = await this[PrivateManager]?.getBridges().getInternalBridge().doGetUsernamesOfRoomById(this.id); - } - - return this._USERNAMES || []; - } - - public toJSON() { - return this.value; - } - - public toString() { - return this.value; - } - - public valueOf() { - return this.value; - } -} diff --git a/packages/apps-engine/deno-runtime/lib/roomFactory.ts b/packages/apps-engine/deno-runtime/lib/roomFactory.ts deleted file mode 100644 index e0c2b9f1c4c80..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/roomFactory.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; -import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager.ts'; - -import { AppAccessors } from './accessors/mod.ts'; -import { Room } from './room.ts'; -import { formatErrorResponse } from './accessors/formatResponseErrorHandler.ts'; - -const getMockAppManager = (senderFn: AppAccessors['senderFn']) => ({ - getBridges: () => ({ - getInternalBridge: () => ({ - doGetUsernamesOfRoomById: (roomId: string) => { - return senderFn({ - method: 'bridges:getInternalBridge:doGetUsernamesOfRoomById', - params: [roomId], - }) - .then((result) => result.result) - .catch((err) => { - throw formatErrorResponse(err); - }); - }, - }), - }), -}); - -export default function createRoom(room: IRoom, senderFn: AppAccessors['senderFn']) { - const mockAppManager = getMockAppManager(senderFn); - - return new Room(room, mockAppManager as unknown as AppManager); -} diff --git a/packages/apps-engine/deno-runtime/lib/sanitizeDeprecatedUsage.ts b/packages/apps-engine/deno-runtime/lib/sanitizeDeprecatedUsage.ts deleted file mode 100644 index 4b5838bce12d1..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/sanitizeDeprecatedUsage.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { fixBrokenSynchronousAPICalls } from './ast/mod.ts'; - -function hasPotentialDeprecatedUsage(source: string) { - return ( - // potential usage of Room.usernames getter - source.includes('.usernames') || - // potential usage of LivechatRead.isOnline method - source.includes('.isOnline(') || - // potential usage of LivechatCreator.createToken method - source.includes('.createToken(') - ); -} - -export function sanitizeDeprecatedUsage(source: string) { - if (!hasPotentialDeprecatedUsage(source)) { - return source; - } - - return fixBrokenSynchronousAPICalls(source); -} diff --git a/packages/apps-engine/deno-runtime/lib/tests/logger.test.ts b/packages/apps-engine/deno-runtime/lib/tests/logger.test.ts deleted file mode 100644 index 7ccc49b3b9ca4..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/tests/logger.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod.ts'; -import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; -import { Logger } from '../logger.ts'; - -describe('Logger', () => { - it('getLogs should return an array of entries', () => { - const logger = new Logger('test'); - logger.info('test'); - const logs = logger.getLogs(); - assertEquals(logs.entries.length, 1); - assertEquals(logs.method, 'test'); - }); - - it('should be able to add entries of different severity', () => { - const logger = new Logger('test'); - logger.info('test'); - logger.debug('test'); - logger.error('test'); - const logs = logger.getLogs(); - assertEquals(logs.entries.length, 3); - assertEquals(logs.entries[0].severity, 'info'); - assertEquals(logs.entries[1].severity, 'debug'); - assertEquals(logs.entries[2].severity, 'error'); - }); - - it('should be able to add an info entry', () => { - const logger = new Logger('test'); - logger.info('test'); - const logs = logger.getLogs(); - assertEquals(logs.entries.length, 1); - assertEquals(logs.entries[0].args[0], 'test'); - assertEquals(logs.entries[0].method, 'test'); - assertEquals(logs.entries[0].severity, 'info'); - }); - - it('should be able to add an debug entry', () => { - const logger = new Logger('test'); - logger.debug('test'); - const logs = logger.getLogs(); - assertEquals(logs.entries.length, 1); - assertEquals(logs.entries[0].args[0], 'test'); - assertEquals(logs.entries[0].method, 'test'); - assertEquals(logs.entries[0].severity, 'debug'); - }); - - it('should be able to add an error entry', () => { - const logger = new Logger('test'); - logger.error('test'); - const logs = logger.getLogs(); - assertEquals(logs.entries.length, 1); - assertEquals(logs.entries[0].args[0], 'test'); - assertEquals(logs.entries[0].method, 'test'); - assertEquals(logs.entries[0].severity, 'error'); - }); - - it('should be able to add an success entry', () => { - const logger = new Logger('test'); - logger.success('test'); - const logs = logger.getLogs(); - assertEquals(logs.entries.length, 1); - assertEquals(logs.entries[0].args[0], 'test'); - assertEquals(logs.entries[0].method, 'test'); - assertEquals(logs.entries[0].severity, 'success'); - }); - - it('should be able to add an warning entry', () => { - const logger = new Logger('test'); - logger.warn('test'); - const logs = logger.getLogs(); - assertEquals(logs.entries.length, 1); - assertEquals(logs.entries[0].args[0], 'test'); - assertEquals(logs.entries[0].method, 'test'); - assertEquals(logs.entries[0].severity, 'warning'); - }); - - it('should be able to add an log entry', () => { - const logger = new Logger('test'); - logger.log('test'); - const logs = logger.getLogs(); - assertEquals(logs.entries.length, 1); - assertEquals(logs.entries[0].args[0], 'test'); - assertEquals(logs.entries[0].method, 'test'); - assertEquals(logs.entries[0].severity, 'log'); - }); - - it('should be able to add an entry with multiple arguments', () => { - const logger = new Logger('test'); - logger.log('test', 'test', 'test'); - const logs = logger.getLogs(); - assertEquals(logs.entries.length, 1); - assertEquals(logs.entries[0].args[0], 'test'); - assertEquals(logs.entries[0].args[1], 'test'); - assertEquals(logs.entries[0].args[2], 'test'); - assertEquals(logs.entries[0].method, 'test'); - assertEquals(logs.entries[0].severity, 'log'); - }); - - it('should be able to add an entry with multiple arguments of different types', () => { - const logger = new Logger('test'); - logger.log('test', 1, true, { foo: 'bar' }); - const logs = logger.getLogs(); - assertEquals(logs.entries.length, 1); - assertEquals(logs.entries[0].args[0], 'test'); - assertEquals(logs.entries[0].args[1], 1); - assertEquals(logs.entries[0].args[2], true); - assertEquals(logs.entries[0].args[3], { foo: 'bar' }); - assertEquals(logs.entries[0].method, 'test'); - assertEquals(logs.entries[0].severity, 'log'); - }); -}); diff --git a/packages/apps-engine/deno-runtime/lib/tests/messenger.test.ts b/packages/apps-engine/deno-runtime/lib/tests/messenger.test.ts deleted file mode 100644 index 47b46f0db6e33..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/tests/messenger.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { assertEquals, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod.ts'; -import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; -import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; - -import * as Messenger from '../messenger.ts'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { createMockRequest } from '../../handlers/tests/helpers/mod.ts'; -import { RequestContext } from '../requestContext.ts'; -import { JsonRpc } from 'jsonrpc-lite'; - -describe('Messenger', () => { - let context: RequestContext; - - beforeEach(() => { - AppObjectRegistry.clear(); - AppObjectRegistry.set('id', 'test'); - Messenger.Transport.selectTransport('noop'); - - context = createMockRequest({ method: 'test', params: [] }); - }); - - afterAll(() => { - AppObjectRegistry.clear(); - Messenger.Transport.selectTransport('stdout'); - }); - - it('should add logs to success responses', async () => { - const theSpy = spy(Messenger.Queue, 'enqueue'); - const { logger } = context.context; - - logger.info('test'); - - await Messenger.successResponse({ id: 'test', result: 'test' }, context); - - assertEquals(theSpy.calls.length, 1); - - const [responseArgument] = theSpy.calls[0].args; - - assertObjectMatch(responseArgument as JsonRpc, { - jsonrpc: '2.0', - id: 'test', - result: { - value: 'test', - logs: { - appId: 'test', - method: 'test', - entries: [ - { - severity: 'info', - method: 'test', - args: ['test'], - caller: 'anonymous OR constructor', - }, - ], - }, - }, - }); - - theSpy.restore(); - }); - - it('should add logs to error responses', async () => { - const theSpy = spy(Messenger.Queue, 'enqueue'); - const { logger } = context.context; - - logger.info('test'); - - await Messenger.errorResponse({ id: 'test', error: { code: -32000, message: 'test' } }, context); - - assertEquals(theSpy.calls.length, 1); - - const [responseArgument] = theSpy.calls[0].args; - - assertObjectMatch(responseArgument as JsonRpc, { - jsonrpc: '2.0', - id: 'test', - error: { - code: -32000, - message: 'test', - data: { - logs: { - appId: 'test', - method: 'test', - entries: [ - { - severity: 'info', - method: 'test', - args: ['test'], - caller: 'anonymous OR constructor', - }, - ], - }, - }, - }, - }); - - theSpy.restore(); - }); -}); diff --git a/packages/apps-engine/deno-runtime/lib/wrapAppForRequest.ts b/packages/apps-engine/deno-runtime/lib/wrapAppForRequest.ts deleted file mode 100644 index e9643c2c0274a..0000000000000 --- a/packages/apps-engine/deno-runtime/lib/wrapAppForRequest.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; - -import { RequestContext } from './requestContext.ts'; -import { isApp, isRecord } from '../handlers/lib/assertions.ts'; - -export function wrapAppForRequest(app: App, req: RequestContext): App { - return new Proxy(app, { - get(target, property, receiver) { - if (property === 'logger') { - return req.context.logger; - } - - return Reflect.get(target, property, receiver); - }, - }); -} - -// Instances of objects that have a reference to an App instance won't change throughout the -// lifetime of the runtime, so we can cache the results to avoid iterating the same object multiple times -const composedCache = new WeakMap, ReturnType>(); - -function findAppProperty(v: NonNullable): [string, App] | undefined { - const cachedEntry = composedCache.get(v); - - if (cachedEntry) { - return cachedEntry; - } - - if (!isRecord(v)) { - // Enables us to avoid having to determine whether the value is a record again - composedCache.set(v, undefined); - return undefined; - } - - const entry = Object.entries(v).find(([_,v]) => isApp(v)) as [string, App] | undefined; - - composedCache.set(v, entry); - - return entry; -} - -export function wrapComposedApp>(composed: T, req: RequestContext): T { - const prop = findAppProperty(composed); - - if (!prop) { - return composed; - } - - const proxy = wrapAppForRequest(prop[1], req); - - return new Proxy(composed, { - get(target, property, receiver) { - if (property === prop[0]) { - return proxy; - } - - return Reflect.get(target, property, receiver); - }, - }) -} diff --git a/packages/apps-engine/deno-runtime/main.ts b/packages/apps-engine/deno-runtime/main.ts deleted file mode 100644 index dc7dacbcea7d0..0000000000000 --- a/packages/apps-engine/deno-runtime/main.ts +++ /dev/null @@ -1,132 +0,0 @@ -if (!Deno.args.includes('--subprocess')) { - Deno.stderr.writeSync( - new TextEncoder().encode(` - This is a Deno wrapper for Rocket.Chat Apps. It is not meant to be executed stand-alone; - It is instead meant to be executed as a subprocess by the Apps-Engine framework. - `), - ); - Deno.exit(1001); -} - -import { JsonRpcError } from 'jsonrpc-lite'; - -import * as Messenger from './lib/messenger.ts'; -import { decoder } from './lib/codec.ts'; -import { Logger } from './lib/logger.ts'; - -import slashcommandHandler from './handlers/slashcommand-handler.ts'; -import videoConferenceHandler from './handlers/videoconference-handler.ts'; -import apiHandler from './handlers/api-handler.ts'; -import handleApp from './handlers/app/handler.ts'; -import handleScheduler from './handlers/scheduler-handler.ts'; -import registerErrorListeners from './error-handlers.ts'; -import { sendMetrics } from './lib/metricsCollector.ts'; -import outboundMessageHandler from './handlers/outboundcomms-handler.ts'; -import { RequestContext } from './lib/requestContext.ts'; - -type Handlers = { - app: typeof handleApp; - api: typeof apiHandler; - slashcommand: typeof slashcommandHandler; - videoconference: typeof videoConferenceHandler; - outboundCommunication: typeof outboundMessageHandler; - scheduler: typeof handleScheduler; - ping: (request: RequestContext) => 'pong'; -}; - -const COMMAND_PING = '_zPING'; - -async function requestRouter({ type, payload }: Messenger.JsonRpcRequest): Promise { - const methodHandlers: Handlers = { - app: handleApp, - api: apiHandler, - slashcommand: slashcommandHandler, - videoconference: videoConferenceHandler, - outboundCommunication: outboundMessageHandler, - scheduler: handleScheduler, - ping: (_request) => 'pong', - }; - - // We're not handling notifications at the moment - if (type === 'notification') { - return Messenger.sendInvalidRequestError(); - } - - const { id, method } = payload; - - const logger = new Logger(method); - - const context: RequestContext = Object.assign(payload, { - context: { logger } - }) - - const [methodPrefix] = method.split(':') as [keyof Handlers]; - const handler = methodHandlers[methodPrefix]; - - if (!handler) { - return Messenger.errorResponse({ - error: { message: 'Method not found', code: -32601 }, - id, - }, context); - } - - const result = await handler(context); - - if (result instanceof JsonRpcError) { - return Messenger.errorResponse({ id, error: result }, context); - } - - return Messenger.successResponse({ id, result }, context); -} - -function handleResponse(response: Messenger.JsonRpcResponse): void { - let event: Event; - - if (response.type === 'error') { - event = new ErrorEvent(`response:${response.payload.id}`, { - error: response.payload, - }); - } else { - event = new CustomEvent(`response:${response.payload.id}`, { - detail: response.payload, - }); - } - - Messenger.RPCResponseObserver.dispatchEvent(event); -} - -async function main() { - Messenger.sendNotification({ method: 'ready' }); - - for await (const message of decoder.decodeStream(Deno.stdin.readable)) { - try { - // Process PING command first as it is not JSON RPC - if (message === COMMAND_PING) { - void Messenger.pongResponse(); - void sendMetrics(); - continue; - } - - const JSONRPCMessage = Messenger.parseMessage(message as Record); - - if (Messenger.isRequest(JSONRPCMessage)) { - void requestRouter(JSONRPCMessage); - continue; - } - - if (Messenger.isResponse(JSONRPCMessage)) { - handleResponse(JSONRPCMessage); - } - } catch (error) { - if (Messenger.isErrorResponse(error)) { - await Messenger.errorResponse(error); - } else { - await Messenger.sendParseError(); - } - } - } -} - -registerErrorListeners(); - -main(); diff --git a/packages/apps-engine/package.json b/packages/apps-engine/package.json index 43cf7796660e6..018ad5958e1cd 100644 --- a/packages/apps-engine/package.json +++ b/packages/apps-engine/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/apps-engine", - "version": "1.61.0", - "description": "The engine code for the Rocket.Chat Apps which manages, runs, translates, coordinates and all of that.", + "version": "1.62.0", + "description": "The public API and type definitions for Rocket.Chat App development.", "keywords": [ "rocket.chat", "team chat", @@ -34,76 +34,32 @@ "email": "douglas.gubert@rocket.chat" } ], + "exports": { + "./definition/*": "./definition/*.js", + "./lib/*": "./lib/*.js" + }, "files": [ - "client/**", "definition/**", - "deno-runtime/**", - "lib/**", - "scripts/**", - "server/**" + "lib/**" ], "scripts": { - ".:build:clean": "rimraf client definition server", + "build": "run-s .:build:clean .:build:default", + ".:build:clean": "rimraf definition lib", ".:build:default": "tsc -p tsconfig.json", - ".:build:deno-cache": "node scripts/deno-cache.js", - ".:deno-fmt:fix": "cd deno-runtime && deno fmt", + "dev": "yarn .:build:default --watch", ".:eslint:fix": "eslint --fix .", - ".:lint:deno": "deno lint --ignore=deno-runtime/.deno deno-runtime/", ".:lint:eslint": "eslint .", - ".:test:deno": "cd deno-runtime && deno task test", - ".:test:node": "NODE_ENV=test node --require ts-node/register/transpile-only --test-reporter spec --test \"tests/**/*.test.ts\"", - "build": "run-s .:build:clean .:build:default .:build:deno-cache", - "dev": "yarn .:build:default --watch", - "fix-lint": "yarn .:eslint:fix && yarn .:deno-fmt:fix", + "fix-lint": "yarn .:eslint:fix", "gen-doc": "typedoc", - "lint": "yarn .:lint:eslint && yarn .:lint:deno", - "start": "run-s .:build:clean .:build:watch", - "testunit": "run-p .:test:node .:test:deno", + "lint": "yarn .:lint:eslint", "typecheck": "tsc -p tsconfig.json --noEmit" }, - "nyc": { - "all": true, - "extension": [ - ".ts" - ], - "include": [ - "src/*.ts", - "src/server/**/*.ts" - ], - "reporter": [ - "lcov", - "json", - "html" - ] - }, - "dependencies": { - "@msgpack/msgpack": "3.0.0-beta2", - "adm-zip": "^0.5.16", - "debug": "^4.3.7", - "esbuild": "~0.27.3", - "jose": "^4.15.9", - "jsonrpc-lite": "^2.2.0", - "lodash.clonedeep": "^4.5.0", - "semver": "^7.6.3", - "stack-trace": "0.0.10", - "uuid": "~11.0.5" - }, "devDependencies": { "@rocket.chat/ui-kit": "workspace:~", - "@seald-io/nedb": "^4.1.2", - "@types/adm-zip": "^0.5.7", - "@types/debug": "^4.1.12", - "@types/lodash.clonedeep": "^4.5.9", "@types/node": "~22.16.5", - "@types/semver": "^7.5.8", - "@types/stack-trace": "0.0.33", - "@types/uuid": "~10.0.0", "eslint": "~9.39.3", "npm-run-all": "^4.1.5", - "nyc": "^17.1.0", "rimraf": "^6.0.1", - "tap-bark": "^1.0.0", - "ts-node": "^6.2.0", "typedoc": "~0.28.16", "typescript": "~5.9.3" }, diff --git a/packages/apps-engine/scripts/deno-cache.js b/packages/apps-engine/scripts/deno-cache.js deleted file mode 100644 index c1626e32ff0d2..0000000000000 --- a/packages/apps-engine/scripts/deno-cache.js +++ /dev/null @@ -1,89 +0,0 @@ -const childProcess = require('child_process'); -const fs = require('fs'); -const path = require('path'); - -const SHELL_ERR_CMD_NOT_FOUND = 127; -const { CI } = process.env; - -/** - * Matches 'deno 2.3.1' or 'Deno 2.7.11-alpha3.24' or even 'some deno and-anything in between 1.43.5' (as long as everything is in the same line) - * and extracts the correct version string from those ('2.3.1', '2.7.11' and '1.43.5' respectively). - * - * Doesn't match 'denoing 2.3.1' or 'deno2.3.1' or 'mydeno 2.7.11alpha3.24' or 'deno\n1.43.5' - * - * The expression gets a bit complicated because the word boundary assertion (\b) identifies the dash (-) as a valid word boundary, - * but that is not the case for use, as we don't want to match "make-deno" for instance. So, for correctness, we use a negative lookbehind - * assertion ("(? /(?\d+\.\d+\.\d+)\b/.exec(input)?.groups?.version; - -try { - const toolVersionsPath = path.resolve(__dirname, '..', '..', '..', '.tool-versions'); - const denoToolVersion = extractDenoVersion(fs.readFileSync(toolVersionsPath).toString()); - - if (!denoToolVersion) { - throw new Error(`Invalid Deno version in ${toolVersionsPath}, aborting...`); - } - - const installedVersion = extractDenoVersion(childProcess.execSync('deno --version').toString()); - - if (!installedVersion) { - throw new Error( - `Couldn't determine version of installed Deno. Try validating the version with 'deno --version' and make sure it is a valid Deno installation`, - ); - } - - if (installedVersion !== denoToolVersion) { - const message = `Incorrect Deno version. Required '${denoToolVersion}', found '${installedVersion}'.${CI ? '' : " The server will likely work, but it may cause your deno.lock to change - do not commit it. Make sure your Deno version matches the required one so you don't see this message again."}`; - - if (CI) { - throw new Error(message); - } - - // We don't need to fail if a dev environment doesn't have a matching Deno version, just the warning is enough - console.warn(message); - } -} catch (e) { - if (e.status === SHELL_ERR_CMD_NOT_FOUND) { - console.error( - new Error( - [ - 'Could not execute "deno" in the system. It is now a requirement for the Apps-Engine framework, and Rocket.Chat apps will not work without it.', - 'Make sure to install Deno and run the installation process for the Apps-Engine again. More info on https://docs.deno.com/runtime/manual/getting_started/installation', - ].join('\n'), - { cause: e }, - ), - ); - } else { - console.error(e); - } - - process.exit(1); -} - -const rootPath = path.join(__dirname, '..'); -const denoRuntimePath = path.join(rootPath, 'deno-runtime'); -const DENO_DIR = process.env.DENO_DIR ?? path.join(rootPath, '.deno-cache'); - -// In CI envs, break if lockfile changes; in dev envs, it's alright -const commandLine = CI ? 'deno install --frozen --entrypoint main.ts' : 'deno install --entrypoint main.ts'; - -childProcess.execSync(commandLine, { - cwd: denoRuntimePath, - env: { - ...process.env, - DENO_DIR, - }, - stdio: 'inherit', -}); diff --git a/packages/apps-engine/src/client/AppClientManager.ts b/packages/apps-engine/src/client/AppClientManager.ts deleted file mode 100644 index 8d2cb55420eb3..0000000000000 --- a/packages/apps-engine/src/client/AppClientManager.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { AppServerCommunicator } from './AppServerCommunicator'; -import { AppsEngineUIHost } from './AppsEngineUIHost'; -import type { IAppInfo } from '../definition/metadata'; - -export class AppClientManager { - private apps: Array; - - constructor( - private readonly appsEngineUIHost: AppsEngineUIHost, - private readonly communicator?: AppServerCommunicator, - ) { - if (!(appsEngineUIHost instanceof AppsEngineUIHost)) { - throw new Error('The appClientUIHost must extend appClientUIHost'); - } - - if (communicator && !(communicator instanceof AppServerCommunicator)) { - throw new Error('The communicator must extend AppServerCommunicator'); - } - - this.apps = []; - } - - public async load(): Promise { - this.apps = await this.communicator.getEnabledApps(); - console.log('Enabled apps:', this.apps); - } - - public async initialize(): Promise { - this.appsEngineUIHost.initialize(); - } -} diff --git a/packages/apps-engine/src/client/AppServerCommunicator.ts b/packages/apps-engine/src/client/AppServerCommunicator.ts deleted file mode 100644 index bb4a0538f1d03..0000000000000 --- a/packages/apps-engine/src/client/AppServerCommunicator.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { IAppInfo } from '../definition/metadata'; - -export abstract class AppServerCommunicator { - public abstract getEnabledApps(): Promise>; - - public abstract getDisabledApps(): Promise>; - - // Map> - public abstract getLanguageAdditions(): Promise>>; - - // Map> - public abstract getSlashCommands(): Promise>>; - - // Map> - public abstract getContextualBarButtons(): Promise>>; -} diff --git a/packages/apps-engine/src/client/AppsEngineUIClient.ts b/packages/apps-engine/src/client/AppsEngineUIClient.ts deleted file mode 100644 index 09e87f3da34e7..0000000000000 --- a/packages/apps-engine/src/client/AppsEngineUIClient.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ACTION_ID_LENGTH, MESSAGE_ID } from './constants'; -import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from './definition'; -import { AppsEngineUIMethods } from './definition/AppsEngineUIMethods'; -import { randomString } from './utils'; - -/** - * Represents the SDK provided to the external component. - */ -export class AppsEngineUIClient { - private listener: (this: Window, ev: MessageEvent) => any; - - private callbacks: Map any>; - - constructor() { - this.listener = () => console.log('init'); - this.callbacks = new Map(); - } - - /** - * Get the current user's information. - * - * @return the information of the current user. - */ - public getUserInfo(): Promise { - return this.call(AppsEngineUIMethods.GET_USER_INFO); - } - - /** - * Get the current room's information. - * - * @return the information of the current room. - */ - public getRoomInfo(): Promise { - return this.call(AppsEngineUIMethods.GET_ROOM_INFO); - } - - /** - * Initialize the app SDK for communicating with Rocket.Chat - */ - public init(): void { - this.listener = ({ data }) => { - if (!data?.hasOwnProperty(MESSAGE_ID)) { - return; - } - - const { - [MESSAGE_ID]: { id, payload }, - } = data; - - if (this.callbacks.has(id)) { - const resolve = this.callbacks.get(id); - - if (typeof resolve === 'function') { - resolve(payload); - } - this.callbacks.delete(id); - } - }; - window.addEventListener('message', this.listener); - } - - private call(action: string, payload?: any): Promise { - return new Promise((resolve) => { - const id = randomString(ACTION_ID_LENGTH); - - window.parent.postMessage({ [MESSAGE_ID]: { action, payload, id } }, '*'); - this.callbacks.set(id, resolve); - }); - } -} diff --git a/packages/apps-engine/src/client/AppsEngineUIHost.ts b/packages/apps-engine/src/client/AppsEngineUIHost.ts deleted file mode 100644 index c10067e6624bc..0000000000000 --- a/packages/apps-engine/src/client/AppsEngineUIHost.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { MESSAGE_ID } from './constants'; -import type { IAppsEngineUIResponse, IExternalComponentRoomInfo, IExternalComponentUserInfo } from './definition'; -import { AppsEngineUIMethods } from './definition'; - -type HandleActionData = IExternalComponentUserInfo | IExternalComponentRoomInfo; - -/** - * Represents the host which handles API calls from external components. - */ -export abstract class AppsEngineUIHost { - /** - * The message emitter who calling the API. - */ - private responseDestination!: Window; - - constructor() { - this.initialize(); - } - - /** - * initialize the AppClientUIHost by registering window `message` listener - */ - public initialize() { - window.addEventListener('message', async ({ data, source }) => { - if (!data?.hasOwnProperty(MESSAGE_ID)) { - return; - } - - this.responseDestination = source as Window; - - const { - [MESSAGE_ID]: { action, id }, - } = data; - - switch (action) { - case AppsEngineUIMethods.GET_USER_INFO: - this.handleAction(action, id, await this.getClientUserInfo()); - break; - case AppsEngineUIMethods.GET_ROOM_INFO: - this.handleAction(action, id, await this.getClientRoomInfo()); - break; - } - }); - } - - /** - * Get the current user's information. - */ - public abstract getClientUserInfo(): Promise; - - /** - * Get the opened room's information. - */ - public abstract getClientRoomInfo(): Promise; - - /** - * Handle the action sent from the external component. - * @param action the name of the action - * @param id the unique id of the API call - * @param data The data that will return to the caller - */ - private async handleAction(action: AppsEngineUIMethods, id: string, data: HandleActionData): Promise { - if (this.responseDestination instanceof MessagePort || this.responseDestination instanceof ServiceWorker) { - return; - } - - this.responseDestination.postMessage( - { - [MESSAGE_ID]: { - id, - action, - payload: data, - } as IAppsEngineUIResponse, - }, - '*', - ); - } -} diff --git a/packages/apps-engine/src/client/constants/index.ts b/packages/apps-engine/src/client/constants/index.ts deleted file mode 100644 index bd7f2e779ca1b..0000000000000 --- a/packages/apps-engine/src/client/constants/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * The id length of each action. - */ -export const ACTION_ID_LENGTH = 80; - -export const MESSAGE_ID = 'rc-apps-engine-ui'; diff --git a/packages/apps-engine/src/client/definition/AppsEngineUIMethods.ts b/packages/apps-engine/src/client/definition/AppsEngineUIMethods.ts deleted file mode 100644 index 6eb3fb908e5a1..0000000000000 --- a/packages/apps-engine/src/client/definition/AppsEngineUIMethods.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * The actions provided by the AppClientSDK. - */ -export enum AppsEngineUIMethods { - GET_USER_INFO = 'getUserInfo', - GET_ROOM_INFO = 'getRoomInfo', -} diff --git a/packages/apps-engine/src/client/definition/IAppsEngineUIResponse.ts b/packages/apps-engine/src/client/definition/IAppsEngineUIResponse.ts deleted file mode 100644 index d8690b9b31781..0000000000000 --- a/packages/apps-engine/src/client/definition/IAppsEngineUIResponse.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from './index'; - -/** - * The response to the AppClientSDK's API call. - */ -export interface IAppsEngineUIResponse { - /** - * The name of the action - */ - action: string; - /** - * The unique id of the API call - */ - id: string; - /** - * The data that will return to the caller - */ - payload: IExternalComponentUserInfo | IExternalComponentRoomInfo; -} diff --git a/packages/apps-engine/src/client/definition/IExternalComponentRoomInfo.ts b/packages/apps-engine/src/client/definition/IExternalComponentRoomInfo.ts deleted file mode 100644 index a7743dcae8a47..0000000000000 --- a/packages/apps-engine/src/client/definition/IExternalComponentRoomInfo.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { IExternalComponentUserInfo } from './IExternalComponentUserInfo'; -import type { IRoom } from '../../definition/rooms'; - -type ClientRoomInfo = Pick; - -/** - * Represents the room's information returned to the - * external component. - */ -export interface IExternalComponentRoomInfo extends ClientRoomInfo { - /** - * the list that contains all the users belonging - * to this room. - */ - members: Array; -} diff --git a/packages/apps-engine/src/client/definition/IExternalComponentUserInfo.ts b/packages/apps-engine/src/client/definition/IExternalComponentUserInfo.ts deleted file mode 100644 index 4cd9061347750..0000000000000 --- a/packages/apps-engine/src/client/definition/IExternalComponentUserInfo.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IUser } from '../../definition/users'; - -type ClientUserInfo = Pick; - -/** - * Represents the user's information returned to - * the external component. - */ -export interface IExternalComponentUserInfo extends ClientUserInfo { - /** - * the avatar URL of the Rocket.Chat user - */ - avatarUrl: string; -} diff --git a/packages/apps-engine/src/client/definition/index.ts b/packages/apps-engine/src/client/definition/index.ts deleted file mode 100644 index 70a1fe884a6a9..0000000000000 --- a/packages/apps-engine/src/client/definition/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './AppsEngineUIMethods'; -export * from './IExternalComponentUserInfo'; -export * from './IExternalComponentRoomInfo'; -export * from './IAppsEngineUIResponse'; diff --git a/packages/apps-engine/src/client/index.ts b/packages/apps-engine/src/client/index.ts deleted file mode 100644 index 2ebfee0264d2d..0000000000000 --- a/packages/apps-engine/src/client/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { AppClientManager } from './AppClientManager'; -import { AppServerCommunicator } from './AppServerCommunicator'; - -export { AppClientManager, AppServerCommunicator }; diff --git a/packages/apps-engine/src/client/utils/index.ts b/packages/apps-engine/src/client/utils/index.ts deleted file mode 100644 index ff726ee5934f4..0000000000000 --- a/packages/apps-engine/src/client/utils/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Generate a random string with the specified length. - * @param length the length for the generated random string. - */ -export function randomString(length: number): string { - const buffer: Array = []; - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - - for (let i = 0; i < length; i++) { - buffer.push(chars[getRandomInt(chars.length)]); - } - - return buffer.join(''); -} - -function getRandomInt(max: number): number { - return Math.floor(Math.random() * Math.floor(max)); -} diff --git a/packages/apps-engine/src/server/AppManager.ts b/packages/apps-engine/src/server/AppManager.ts deleted file mode 100644 index 826b2b1b95148..0000000000000 --- a/packages/apps-engine/src/server/AppManager.ts +++ /dev/null @@ -1,1240 +0,0 @@ -import { Buffer } from 'buffer'; - -import type { IGetAppsFilter } from './IGetAppsFilter'; -import { ProxiedApp } from './ProxiedApp'; -import type { PersistenceBridge, UserBridge } from './bridges'; -import { AppBridges } from './bridges'; -import { AppStatus, AppStatusUtils } from '../definition/AppStatus'; -import type { IAppInfo } from '../definition/metadata'; -import { AppMethod } from '../definition/metadata'; -import type { IPermission } from '../definition/permissions/IPermission'; -import type { IUser } from '../definition/users'; -import { UserType } from '../definition/users'; -import type { IInternalPersistenceBridge } from './bridges/IInternalPersistenceBridge'; -import type { IInternalUserBridge } from './bridges/IInternalUserBridge'; -import { AppCompiler, AppFabricationFulfillment, AppPackageParser } from './compiler'; -import { InvalidLicenseError } from './errors'; -import { InvalidInstallationError } from './errors/InvalidInstallationError'; -import { - AppAccessorManager, - AppApiManager, - AppExternalComponentManager, - AppLicenseManager, - AppListenerManager, - AppSchedulerManager, - AppSettingsManager, - AppSlashCommandManager, - AppVideoConfProviderManager, -} from './managers'; -import { AppOutboundCommunicationProviderManager } from './managers/AppOutboundCommunicationProviderManager'; -import { AppRuntimeManager } from './managers/AppRuntimeManager'; -import { AppSignatureManager } from './managers/AppSignatureManager'; -import { UIActionButtonManager } from './managers/UIActionButtonManager'; -import type { IMarketplaceInfo } from './marketplace'; -import { defaultPermissions } from './permissions/AppPermissions'; -import { EmptyRuntime } from './runtime/EmptyRuntime'; -import type { IAppStorageItem } from './storage'; -import { AppLogStorage, AppMetadataStorage } from './storage'; -import { AppSourceStorage } from './storage/AppSourceStorage'; -import { AppInstallationSource } from './storage/IAppStorageItem'; - -export interface IAppInstallParameters { - enable: boolean; - marketplaceInfo?: IMarketplaceInfo[]; - permissionsGranted?: Array; - user: IUser; -} - -export interface IAppUninstallParameters { - user: IUser; -} - -export interface IAppManagerDeps { - metadataStorage: AppMetadataStorage; - logStorage: AppLogStorage; - bridges: AppBridges; - sourceStorage: AppSourceStorage; - /** - * Path to temporary file storage. - * - * Needs to be accessible for reading and writing. - */ - tempFilePath: string; -} - -interface IPurgeAppConfigOpts { - keepScheduledJobs?: boolean; - keepSlashcommands?: boolean; - keepOutboundCommunicationProviders?: boolean; -} - -export class AppManager { - public static Instance: AppManager; - - // apps contains all of the Apps - private readonly apps: Map; - - private readonly appMetadataStorage: AppMetadataStorage; - - private appSourceStorage: AppSourceStorage; - - private readonly logStorage: AppLogStorage; - - private readonly bridges: AppBridges; - - private readonly parser: AppPackageParser; - - private readonly compiler: AppCompiler; - - private readonly accessorManager: AppAccessorManager; - - private readonly listenerManager: AppListenerManager; - - private readonly commandManager: AppSlashCommandManager; - - private readonly apiManager: AppApiManager; - - private readonly externalComponentManager: AppExternalComponentManager; - - private readonly settingsManager: AppSettingsManager; - - private readonly licenseManager: AppLicenseManager; - - private readonly schedulerManager: AppSchedulerManager; - - private readonly uiActionButtonManager: UIActionButtonManager; - - private readonly videoConfProviderManager: AppVideoConfProviderManager; - - private readonly outboundCommunicationProviderManager: AppOutboundCommunicationProviderManager; - - private readonly signatureManager: AppSignatureManager; - - private readonly runtime: AppRuntimeManager; - - private readonly tempFilePath: string; - - private isLoaded: boolean; - - constructor({ metadataStorage, logStorage, bridges, sourceStorage, tempFilePath }: IAppManagerDeps) { - // Singleton style. There can only ever be one AppManager instance - if (typeof AppManager.Instance !== 'undefined') { - throw new Error('There is already a valid AppManager instance'); - } - - if (metadataStorage instanceof AppMetadataStorage) { - this.appMetadataStorage = metadataStorage; - } else { - throw new Error('Invalid instance of the AppMetadataStorage'); - } - - if (logStorage instanceof AppLogStorage) { - this.logStorage = logStorage; - } else { - throw new Error('Invalid instance of the AppLogStorage'); - } - - if (bridges instanceof AppBridges) { - this.bridges = bridges; - } else { - throw new Error('Invalid instance of the AppBridges'); - } - - if (sourceStorage instanceof AppSourceStorage) { - this.appSourceStorage = sourceStorage; - } else { - throw new Error('Invalid instance of the AppSourceStorage'); - } - - this.tempFilePath = tempFilePath; - - this.apps = new Map(); - - this.parser = new AppPackageParser(); - this.compiler = new AppCompiler(); - this.accessorManager = new AppAccessorManager(this); - this.listenerManager = new AppListenerManager(this); - this.commandManager = new AppSlashCommandManager(this); - this.apiManager = new AppApiManager(this); - this.externalComponentManager = new AppExternalComponentManager(); - this.settingsManager = new AppSettingsManager(this); - this.licenseManager = new AppLicenseManager(this); - this.schedulerManager = new AppSchedulerManager(this); - this.uiActionButtonManager = new UIActionButtonManager(this); - this.videoConfProviderManager = new AppVideoConfProviderManager(this); - this.outboundCommunicationProviderManager = new AppOutboundCommunicationProviderManager(this); - this.signatureManager = new AppSignatureManager(this); - this.runtime = new AppRuntimeManager(this); - - this.isLoaded = false; - AppManager.Instance = this; - } - - /** - * Gets the path to the temporary file storage. - * - * Mainly used for upload events - */ - public getTempFilePath(): string { - return this.tempFilePath; - } - - /** Gets the instance of the storage connector. */ - public getStorage(): AppMetadataStorage { - return this.appMetadataStorage; - } - - /** Gets the instance of the log storage connector. */ - public getLogStorage(): AppLogStorage { - return this.logStorage; - } - - /** Gets the instance of the App package parser. */ - public getParser(): AppPackageParser { - return this.parser; - } - - /** Gets the compiler instance. */ - public getCompiler(): AppCompiler { - return this.compiler; - } - - /** Gets the accessor manager instance. */ - public getAccessorManager(): AppAccessorManager { - return this.accessorManager; - } - - /** Gets the instance of the Bridge manager. */ - public getBridges(): AppBridges { - return this.bridges; - } - - /** Gets the instance of the listener manager. */ - public getListenerManager(): AppListenerManager { - return this.listenerManager; - } - - /** Gets the command manager's instance. */ - public getCommandManager(): AppSlashCommandManager { - return this.commandManager; - } - - public getVideoConfProviderManager(): AppVideoConfProviderManager { - return this.videoConfProviderManager; - } - - public getOutboundCommunicationProviderManager(): AppOutboundCommunicationProviderManager { - return this.outboundCommunicationProviderManager; - } - - public getLicenseManager(): AppLicenseManager { - return this.licenseManager; - } - - /** Gets the api manager's instance. */ - public getApiManager(): AppApiManager { - return this.apiManager; - } - - /** Gets the external component manager's instance. */ - public getExternalComponentManager(): AppExternalComponentManager { - return this.externalComponentManager; - } - - /** Gets the manager of the settings, updates and getting. */ - public getSettingsManager(): AppSettingsManager { - return this.settingsManager; - } - - public getSchedulerManager(): AppSchedulerManager { - return this.schedulerManager; - } - - public getUIActionButtonManager(): UIActionButtonManager { - return this.uiActionButtonManager; - } - - public getSignatureManager(): AppSignatureManager { - return this.signatureManager; - } - - public getRuntime(): AppRuntimeManager { - return this.runtime; - } - - /** Gets whether the Apps have been loaded or not. */ - public areAppsLoaded(): boolean { - return this.isLoaded; - } - - public setSourceStorage(storage: AppSourceStorage): void { - this.appSourceStorage = storage; - } - - /** - * Goes through the entire loading up process. - * Expect this to take some time, as it goes through a very - * long process of loading all the Apps up. - */ - public async load(): Promise { - // You can not load the AppManager system again - // if it has already been loaded. - if (this.isLoaded) { - return true; - } - - const items: Map = await this.appMetadataStorage.retrieveAll(); - - for (const item of items.values()) { - try { - const appPackage = await this.appSourceStorage.fetch(item); - const unpackageResult = await this.getParser().unpackageApp(appPackage); - - const app = await this.getCompiler().toSandBox(this, item, unpackageResult); - - this.apps.set(item.id, app); - } catch (e) { - console.warn(`Error while compiling the App "${item.info.name} (${item.id})":`); - console.error(e); - - const prl = new ProxiedApp(this, item, new EmptyRuntime(item.id)); - - this.apps.set(item.id, prl); - } - } - - this.isLoaded = true; - return true; - } - - public async enableAll(): Promise> { - const affs: Array = []; - - // Let's initialize them - for (const rl of this.apps.values()) { - const aff = new AppFabricationFulfillment(); - - aff.setAppInfo(rl.getInfo()); - aff.setImplementedInterfaces(rl.getImplementationList()); - aff.setApp(rl); - affs.push(aff); - - if (AppStatusUtils.isDisabled(await rl.getStatus())) { - // Usually if an App is disabled before it's initialized, - // then something (such as an error) occured while - // it was compiled or something similar. - // We still have to validate its license, though - await rl.validateLicense(); - - continue; - } - - await this.initializeApp(rl, true).catch(console.error); - } - - // Let's ensure the required settings are all set - for (const rl of this.apps.values()) { - if (AppStatusUtils.isDisabled(await rl.getStatus())) { - continue; - } - - if (!this.areRequiredSettingsSet(rl.getStorageItem())) { - await rl.setStatus(AppStatus.INVALID_SETTINGS_DISABLED).catch(console.error); - } - } - - // Now let's enable the apps which were once enabled - // but are not currently disabled. - for (const app of this.apps.values()) { - const status = await app.getStatus(); - if (!AppStatusUtils.isDisabled(status) && AppStatusUtils.isEnabled(app.getPreviousStatus())) { - await this.enableApp(app).catch(console.error); - } else if (!AppStatusUtils.isError(status)) { - this.listenerManager.lockEssentialEvents(app); - this.uiActionButtonManager.clearAppActionButtons(app.getID()); - } - } - - return affs; - } - - public async unload(isManual: boolean): Promise { - // If the AppManager hasn't been loaded yet, then - // there is nothing to unload - if (!this.isLoaded) { - return; - } - - for (const app of this.apps.values()) { - const status = await app.getStatus(); - if (status === AppStatus.INITIALIZED) { - await this.purgeAppConfig(app); - } else if (!AppStatusUtils.isDisabled(status)) { - await this.disable(app.getID(), isManual ? AppStatus.MANUALLY_DISABLED : AppStatus.DISABLED); - } - - this.listenerManager.releaseEssentialEvents(app); - - app.getRuntimeController().stopApp(); - } - - // Remove all the apps from the system now that we have unloaded everything - this.apps.clear(); - - this.isLoaded = false; - } - - /** Gets the Apps which match the filter passed in. */ - public async get(filter?: IGetAppsFilter): Promise { - let rls: Array = []; - - if (typeof filter === 'undefined') { - this.apps.forEach((rl) => rls.push(rl)); - - return rls; - } - - let nothing = true; - - if (typeof filter.enabled === 'boolean' && filter.enabled) { - for (const rl of this.apps.values()) { - if (AppStatusUtils.isEnabled(await rl.getStatus())) { - rls.push(rl); - } - } - - nothing = false; - } - - if (typeof filter.disabled === 'boolean' && filter.disabled) { - for (const rl of this.apps.values()) { - if (AppStatusUtils.isDisabled(await rl.getStatus())) { - rls.push(rl); - } - } - - nothing = false; - } - - if (nothing) { - this.apps.forEach((rl) => rls.push(rl)); - } - - if (typeof filter.ids !== 'undefined') { - rls = rls.filter((rl) => filter.ids.includes(rl.getID())); - } - - if (typeof filter.installationSource !== 'undefined') { - rls = rls.filter((rl) => rl.getInstallationSource() === filter.installationSource); - } - - if (typeof filter.name === 'string') { - rls = rls.filter((rl) => rl.getName() === filter.name); - } else if (filter.name instanceof RegExp) { - rls = rls.filter((rl) => (filter.name as RegExp).test(rl.getName())); - } - - return rls; - } - - /** Gets a single App by the id passed in. */ - public getOneById(appId: string): ProxiedApp { - return this.apps.get(appId); - } - - public getPermissionsById(appId: string): Array { - const app = this.apps.get(appId); - - if (!app) { - return []; - } - const { permissionsGranted } = app.getStorageItem(); - - return permissionsGranted || defaultPermissions; - } - - public async enable(id: string): Promise { - const rl = this.apps.get(id); - - if (!rl) { - throw new Error(`No App by the id "${id}" exists.`); - } - - const status = await rl.getStatus(); - - if (AppStatusUtils.isEnabled(status)) { - return true; - } - - if (status === AppStatus.COMPILER_ERROR_DISABLED) { - throw new Error('The App had compiler errors, can not enable it.'); - } - - const storageItem = await this.appMetadataStorage.retrieveOne(id); - - if (!storageItem) { - throw new Error(`Could not enable an App with the id of "${id}" as it doesn't exist.`); - } - - const isSetup = await this.runStartUpProcess(storageItem, rl, false); - - return isSetup; - } - - public async disable(id: string, status: AppStatus = AppStatus.DISABLED, silent?: boolean): Promise { - if (!AppStatusUtils.isDisabled(status)) { - throw new Error('Invalid disabled status'); - } - - const app = this.apps.get(id); - - if (!app) { - throw new Error(`No App by the id "${id}" exists.`); - } - - if (AppStatusUtils.isEnabled(await app.getStatus())) { - await app.call(AppMethod.ONDISABLE).catch((e) => console.warn('Error while disabling:', e)); - } - - await this.purgeAppConfig(app, { - keepScheduledJobs: true, - keepSlashcommands: true, - keepOutboundCommunicationProviders: true, - }); - - await app.setStatus(status, silent); - - const storageItem = await this.appMetadataStorage.retrieveOne(id); - - app.getStorageItem().marketplaceInfo = storageItem.marketplaceInfo; - await app.validateLicense().catch(() => {}); - - return true; - } - - public async migrate(id: string): Promise { - const app = this.apps.get(id); - - if (!app) { - throw new Error(`No App by the id "${id}" exists.`); - } - - await app.call(AppMethod.ONUPDATE).catch((e) => console.warn('Error while migrating:', e)); - - await this.purgeAppConfig(app, { keepScheduledJobs: true }); - - const storageItem = await this.appMetadataStorage.retrieveOne(id); - - app.getStorageItem().marketplaceInfo = storageItem.marketplaceInfo; - await app.validateLicense().catch(() => {}); - - storageItem.migrated = true; - storageItem.signature = await this.getSignatureManager().signApp(storageItem); - - const { marketplaceInfo, signature, migrated, _id } = storageItem; - const stored = await this.appMetadataStorage.updatePartialAndReturnDocument({ marketplaceInfo, signature, migrated, _id }); - - await this.updateLocal(stored, app); - await this.bridges - .getAppActivationBridge() - .doAppUpdated(app) - .catch(() => {}); - - return true; - } - - public async addLocal(appId: string): Promise { - const storageItem = await this.appMetadataStorage.retrieveOne(appId); - - if (!storageItem) { - throw new Error(`App with id ${appId} couldn't be found`); - } - - const appPackage = await this.appSourceStorage.fetch(storageItem); - - if (!appPackage) { - throw new Error(`Package file for app "${storageItem.info.name}" (${appId}) couldn't be found`); - } - - const parsedPackage = await this.getParser().unpackageApp(appPackage); - const app = await this.getCompiler().toSandBox(this, storageItem, parsedPackage); - - this.apps.set(app.getID(), app); - - await this.loadOne(appId); - } - - public async add(appPackage: Buffer, installationParameters: IAppInstallParameters): Promise { - const { enable = true, marketplaceInfo, permissionsGranted, user } = installationParameters; - - const aff = new AppFabricationFulfillment(); - const result = await this.getParser().unpackageApp(appPackage); - const undoSteps: Array<() => void> = []; - - aff.setAppInfo(result.info); - aff.setImplementedInterfaces(result.implemented.getValues()); - - const descriptor: IAppStorageItem = { - id: result.info.id, - info: result.info, - status: enable ? AppStatus.MANUALLY_ENABLED : AppStatus.MANUALLY_DISABLED, - settings: {}, - implemented: result.implemented.getValues(), - installationSource: marketplaceInfo ? AppInstallationSource.MARKETPLACE : AppInstallationSource.PRIVATE, - marketplaceInfo, - permissionsGranted, - languageContent: result.languageContent, - }; - - try { - descriptor.sourcePath = await this.appSourceStorage.store(descriptor, appPackage); - - undoSteps.push(() => this.appSourceStorage.remove(descriptor)); - } catch (error) { - aff.setStorageError('Failed to store app package'); - - return aff; - } - - let app: ProxiedApp; - - try { - app = await this.getCompiler().toSandBox(this, descriptor, result); - } catch (error) { - await Promise.all(undoSteps.map((undoer) => undoer())); - - throw error; - } - - undoSteps.push(() => - this.getRuntime() - .stopRuntime(app.getRuntimeController()) - .catch(() => {}), - ); - - // Create a user for the app - try { - await this.createAppUser(result.info); - - undoSteps.push(() => this.removeAppUser(app)); - } catch (err) { - aff.setAppUserError({ - username: `${result.info.nameSlug}.bot`, - message: 'Failed to create an app user for this app.', - }); - - await Promise.all(undoSteps.map((undoer) => undoer())); - - return aff; - } - - descriptor.signature = await this.getSignatureManager().signApp(descriptor); - const created = await this.appMetadataStorage.create(descriptor); - - if (!created) { - aff.setStorageError('Failed to create the App, the storage did not return it.'); - - await Promise.all(undoSteps.map((undoer) => undoer())); - - return aff; - } - - app.getStorageItem()._id = created._id; - - this.apps.set(app.getID(), app); - aff.setApp(app); - - // Let everyone know that the App has been added - await this.bridges - .getAppActivationBridge() - .doAppAdded(app) - .catch(() => { - // If an error occurs during this, oh well. - }); - - await this.installApp(app, user); - - // Should enable === true, then we go through the entire start up process - // Otherwise, we only initialize it. - if (enable) { - // Start up the app - await this.runStartUpProcess(created, app, false); - } else { - await this.initializeApp(app); - } - - return aff; - } - - /** - * Uninstalls specified app from the server and remove - * all database records regarding it - * - * @returns the instance of the removed ProxiedApp - */ - public async remove(id: string, uninstallationParameters: IAppUninstallParameters): Promise { - const app = this.apps.get(id); - const { user } = uninstallationParameters; - - // First remove the app - await this.uninstallApp(app, user); - await this.removeLocal(id); - - // Then let everyone know that the App has been removed - await this.bridges.getAppActivationBridge().doAppRemoved(app).catch(); - - return app; - } - - /** - * Removes the app instance from the local Apps container - * and every type of data associated with it - */ - public async removeLocal(id: string): Promise { - const app = this.apps.get(id); - - if (AppStatusUtils.isEnabled(await app.getStatus())) { - await this.disable(id); - } - - await this.purgeAppConfig(app); - this.listenerManager.releaseEssentialEvents(app); - await this.removeAppUser(app); - await (this.bridges.getPersistenceBridge() as IInternalPersistenceBridge & PersistenceBridge).purge(app.getID()); - await this.appMetadataStorage.remove(app.getID()); - await this.appSourceStorage.remove(app.getStorageItem()).catch(() => {}); - - // Errors here don't really prevent the process from dying, so we don't really need to do anything on the catch - await this.getRuntime() - .stopRuntime(app.getRuntimeController()) - .catch(() => {}); - - this.apps.delete(app.getID()); - } - - public async update( - appPackage: Buffer, - permissionsGranted: Array, - updateOptions: { loadApp?: boolean; user?: IUser } = { loadApp: true }, - ): Promise { - const aff = new AppFabricationFulfillment(); - const result = await this.getParser().unpackageApp(appPackage); - - aff.setAppInfo(result.info); - aff.setImplementedInterfaces(result.implemented.getValues()); - - const old = await this.appMetadataStorage.retrieveOne(result.info.id); - - if (!old) { - throw new Error('Can not update an App that does not currently exist.'); - } - - // If there is any error during disabling, it doesn't really matter - await this.disable(old.id).catch(() => {}); - - const descriptor: IAppStorageItem = { - ...old, - id: result.info.id, - info: result.info, - languageContent: result.languageContent, - implemented: result.implemented.getValues(), - }; - - if (!permissionsGranted) { - delete descriptor.permissionsGranted; - } else { - descriptor.permissionsGranted = permissionsGranted; - } - - try { - descriptor.sourcePath = await this.appSourceStorage.update(descriptor, appPackage); - } catch (error) { - aff.setStorageError('Failed to storage app package'); - - return aff; - } - - descriptor.signature = await this.signatureManager.signApp(descriptor); - const stored = await this.appMetadataStorage.updatePartialAndReturnDocument(descriptor, { - unsetPermissionsGranted: typeof permissionsGranted === 'undefined', - }); - - // Errors here don't really prevent the process from dying, so we don't really need to do anything on the catch - await this.getRuntime() - .stopRuntime(this.apps.get(old.id).getRuntimeController()) - .catch(() => {}); - - const app = await this.getCompiler().toSandBox(this, descriptor, result); - - // Ensure there is an user for the app - try { - await this.createAppUser(result.info); - } catch (err) { - aff.setAppUserError({ - username: `${result.info.nameSlug}.bot`, - message: 'Failed to create an app user for this app.', - }); - - return aff; - } - - aff.setApp(app); - - if (updateOptions.loadApp) { - const shouldEnableApp = AppStatusUtils.isEnabled(old.status); - if (shouldEnableApp) { - await this.updateAndStartupLocal(stored, app); - } else { - await this.updateAndInitializeLocal(stored, app); - } - - await this.bridges - .getAppActivationBridge() - .doAppUpdated(app) - .catch(() => {}); - } - - await this.updateApp(app, updateOptions.user, old.info.version); - - return aff; - } - - /** - * Updates the local instance of an app. - * - * If the second parameter is a Buffer of an app package, - * unpackage and instantiate the app's main class - * - * With an instance of a ProxiedApp, start it up and replace - * the reference in the local app collection - */ - async updateLocal(stored: IAppStorageItem, appPackageOrInstance: ProxiedApp | Buffer): Promise { - const app = await (async () => { - if (appPackageOrInstance instanceof Buffer) { - const parseResult = await this.getParser().unpackageApp(appPackageOrInstance); - - // Errors here don't really prevent the process from dying, so we don't really need to do anything on the catch - await this.getRuntime() - .stopRuntime(this.apps.get(stored.id).getRuntimeController()) - .catch(() => {}); - - return this.getCompiler().toSandBox(this, stored, parseResult); - } - - if (appPackageOrInstance instanceof ProxiedApp) { - return appPackageOrInstance; - } - })(); - - // We don't keep slashcommands here as the update could potentially not provide the same list - await this.purgeAppConfig(app, { keepScheduledJobs: true }); - - this.apps.set(app.getID(), app); - return app; - } - - public async updateAndStartupLocal(stored: IAppStorageItem, appPackageOrInstance: ProxiedApp | Buffer) { - const app = await this.updateLocal(stored, appPackageOrInstance); - await this.runStartUpProcess(stored, app, true); - } - - public async updateAndInitializeLocal(stored: IAppStorageItem, appPackageOrInstance: ProxiedApp | Buffer) { - const app = await this.updateLocal(stored, appPackageOrInstance); - await this.initializeApp(app, true); - } - - public getLanguageContent(): { [key: string]: object } { - const langs: { [key: string]: object } = {}; - - this.apps.forEach((rl) => { - const content = rl.getStorageItem().languageContent; - - Object.keys(content).forEach((key) => { - langs[key] = Object.assign(langs[key] || {}, content[key]); - }); - }); - - return langs; - } - - public async changeStatus(appId: string, status: AppStatus): Promise { - switch (status) { - case AppStatus.MANUALLY_DISABLED: - case AppStatus.MANUALLY_ENABLED: - break; - default: - throw new Error('Invalid status to change an App to, must be manually disabled or enabled.'); - } - - const rl = this.apps.get(appId); - - if (!rl) { - throw new Error('Can not change the status of an App which does not currently exist.'); - } - - const storageItem = await rl.getStorageItem(); - - if (AppStatusUtils.isEnabled(status)) { - // Then enable it - if (AppStatusUtils.isEnabled(await rl.getStatus())) { - throw new Error('Can not enable an App which is already enabled.'); - } - - await this.enable(rl.getID()); - - storageItem.status = AppStatus.MANUALLY_ENABLED; - await this.appMetadataStorage.updateStatus(storageItem._id, AppStatus.MANUALLY_ENABLED); - } else { - if (!AppStatusUtils.isEnabled(await rl.getStatus())) { - throw new Error('Can not disable an App which is not enabled.'); - } - - await this.disable(rl.getID(), AppStatus.MANUALLY_DISABLED); - - storageItem.status = AppStatus.MANUALLY_DISABLED; - await this.appMetadataStorage.updateStatus(storageItem._id, AppStatus.MANUALLY_DISABLED); - } - - return rl; - } - - public async updateAppsMarketplaceInfo(appsOverview: Array<{ latest: IMarketplaceInfo }>): Promise { - await Promise.all( - appsOverview.map(async ({ latest: appInfo }) => { - if (!appInfo.subscriptionInfo) { - return; - } - - const app = this.apps.get(appInfo.id); - - if (!app) { - return; - } - - const appStorageItem = app.getStorageItem(); - const { subscriptionInfo } = appStorageItem.marketplaceInfo?.[0] || {}; - - if (subscriptionInfo && subscriptionInfo.license.license === appInfo.subscriptionInfo.license.license) { - return; - } - - appStorageItem.marketplaceInfo[0].subscriptionInfo = appInfo.subscriptionInfo; - appStorageItem.signature = await this.getSignatureManager().signApp(appStorageItem); - - return this.appMetadataStorage.updatePartialAndReturnDocument({ - _id: appStorageItem._id, - marketplaceInfo: appStorageItem.marketplaceInfo, - signature: appStorageItem.signature, - }); - }), - ).catch(() => {}); - - const queue = [] as Array>; - - this.apps.forEach((app) => - queue.push( - app - .validateLicense() - .then(async () => { - if ((await app.getStatus()) !== AppStatus.INVALID_LICENSE_DISABLED) { - return; - } - - return app.setStatus(AppStatus.DISABLED); - }) - .catch(async (error) => { - if (!(error instanceof InvalidLicenseError)) { - console.error(error); - return; - } - - await this.purgeAppConfig(app, { keepScheduledJobs: true }); - - return app.setStatus(AppStatus.INVALID_LICENSE_DISABLED); - }) - .then(async () => { - const status = await app.getStatus(); - if (status === app.getPreviousStatus()) { - return; - } - - const storageItem = app.getStorageItem(); - storageItem.status = status; - - return this.appMetadataStorage.updateStatus(storageItem._id, storageItem.status).catch(console.error) as Promise; - }), - ), - ); - - await Promise.all(queue); - } - - /** - * Goes through the entire loading up process. - * - * @param appId the id of the application to load - */ - public async loadOne(appId: string, silenceStatus = false): Promise { - const rl = this.apps.get(appId); - - if (!rl) { - throw new Error(`No App found by the id of: "${appId}"`); - } - - const item = rl.getStorageItem(); - - await this.initializeApp(rl, silenceStatus); - - if (!this.areRequiredSettingsSet(item)) { - await rl.setStatus(AppStatus.INVALID_SETTINGS_DISABLED); - } - - if (!AppStatusUtils.isDisabled(await rl.getStatus()) && AppStatusUtils.isEnabled(rl.getPreviousStatus())) { - await this.enableApp(rl, silenceStatus); - } - - return this.apps.get(item.id); - } - - private async runStartUpProcess(storageItem: IAppStorageItem, app: ProxiedApp, silenceStatus: boolean): Promise { - if ((await app.getStatus()) !== AppStatus.INITIALIZED) { - const isInitialized = await this.initializeApp(app, silenceStatus); - if (!isInitialized) { - return false; - } - } - - if (!this.areRequiredSettingsSet(storageItem)) { - await app.setStatus(AppStatus.INVALID_SETTINGS_DISABLED, silenceStatus); - return false; - } - - return this.enableApp(app, silenceStatus); - } - - private async installApp(app: ProxiedApp, user: IUser): Promise { - let result: boolean; - const context = { user }; - - try { - await app.call(AppMethod.ONINSTALL, context); - - result = true; - } catch (e) { - const status = AppStatus.ERROR_DISABLED; - - result = false; - - await app.setStatus(status); - } - - return result; - } - - private async updateApp(app: ProxiedApp, user: IUser | null, oldAppVersion: string): Promise { - let result: boolean; - - try { - await app.call(AppMethod.ONUPDATE, { oldAppVersion, user }); - - result = true; - } catch (e) { - const status = AppStatus.ERROR_DISABLED; - - result = false; - - await app.setStatus(status); - } - - return result; - } - - private async initializeApp(app: ProxiedApp, silenceStatus = false): Promise { - let result: boolean; - - try { - await app.validateLicense(); - await app.validateInstallation(); - - await app.call(AppMethod.INITIALIZE); - await app.setStatus(AppStatus.INITIALIZED, silenceStatus); - - await this.commandManager.registerCommands(app.getID()); - - result = true; - } catch (e) { - let status = AppStatus.ERROR_DISABLED; - - if (e instanceof InvalidLicenseError) { - status = AppStatus.INVALID_LICENSE_DISABLED; - } - - if (e instanceof InvalidInstallationError) { - status = AppStatus.INVALID_INSTALLATION_DISABLED; - } - - await this.purgeAppConfig(app); - result = false; - - await app.setStatus(status, silenceStatus); - } - - return result; - } - - private async purgeAppConfig(app: ProxiedApp, opts: IPurgeAppConfigOpts = {}) { - if (!opts.keepScheduledJobs) { - await this.schedulerManager.cleanUp(app.getID()); - } - - if (!opts.keepSlashcommands) { - await this.commandManager.unregisterCommands(app.getID()); - } - - this.listenerManager.unregisterListeners(app); - this.listenerManager.lockEssentialEvents(app); - this.externalComponentManager.unregisterExternalComponents(app.getID()); - await this.apiManager.unregisterApis(app.getID()); - this.accessorManager.purifyApp(app.getID()); - this.uiActionButtonManager.clearAppActionButtons(app.getID()); - this.videoConfProviderManager.unregisterProviders(app.getID()); - await this.outboundCommunicationProviderManager.unregisterProviders(app.getID(), { - keepReferences: opts.keepOutboundCommunicationProviders, - }); - } - - /** - * Determines if the App's required settings are set or not. - * Should a packageValue be provided and not empty, then it's considered set. - */ - private areRequiredSettingsSet(storageItem: IAppStorageItem): boolean { - let result = true; - - for (const setk of Object.keys(storageItem.settings)) { - const sett = storageItem.settings[setk]; - // If it's not required, ignore - if (!sett.required) { - continue; - } - - if (sett.value !== 'undefined' || sett.packageValue !== 'undefined') { - continue; - } - - result = false; - } - - return result; - } - - private async enableApp(app: ProxiedApp, silenceStatus = false): Promise { - let enable: boolean; - let status = AppStatus.ERROR_DISABLED; - - try { - await app.validateLicense(); - await app.validateInstallation(); - - enable = (await app.call(AppMethod.ONENABLE)) as boolean; - - if (enable) { - status = AppStatus.MANUALLY_ENABLED; - } else { - status = AppStatus.DISABLED; - console.warn(`The App (${app.getID()}) disabled itself when being enabled. \nCheck the "onEnable" implementation for details.`); - } - } catch (e) { - enable = false; - - if (e instanceof InvalidLicenseError) { - status = AppStatus.INVALID_LICENSE_DISABLED; - } - - if (e instanceof InvalidInstallationError) { - status = AppStatus.INVALID_INSTALLATION_DISABLED; - } - - console.error(e); - } - - if (enable) { - this.externalComponentManager.registerExternalComponents(app.getID()); - await this.apiManager.registerApis(app.getID()); - this.listenerManager.registerListeners(app); - this.listenerManager.releaseEssentialEvents(app); - this.videoConfProviderManager.registerProviders(app.getID()); - await this.outboundCommunicationProviderManager.registerProviders(app.getID()); - } else { - await this.purgeAppConfig(app, { - keepScheduledJobs: true, - keepSlashcommands: true, - keepOutboundCommunicationProviders: true, - }); - } - - await app.setStatus(status, silenceStatus); - - return enable; - } - - private async createAppUser(appInfo: IAppInfo): Promise { - const appUser = await (this.bridges.getUserBridge() as IInternalUserBridge & UserBridge).getAppUser(appInfo.id); - - if (appUser) { - return appUser.id; - } - - const userData: Partial = { - username: `${appInfo.nameSlug}.bot`, - name: appInfo.name, - roles: ['app'], - appId: appInfo.id, - type: UserType.APP, - status: 'online', - isEnabled: true, - }; - - return (this.bridges.getUserBridge() as IInternalUserBridge & UserBridge).create(userData, appInfo.id, { - avatarUrl: appInfo.iconFileContent || appInfo.iconFile, - joinDefaultChannels: true, - sendWelcomeEmail: false, - }); - } - - private async removeAppUser(app: ProxiedApp): Promise { - const appUser = await (this.bridges.getUserBridge() as IInternalUserBridge & UserBridge).getAppUser(app.getID()); - - if (!appUser) { - return true; - } - - return (this.bridges.getUserBridge() as IInternalUserBridge & UserBridge).remove(appUser, app.getID()); - } - - private async uninstallApp(app: ProxiedApp, user: IUser): Promise { - let result: boolean; - const context = { user }; - - try { - await app.call(AppMethod.ONUNINSTALL, context); - - result = true; - } catch (e) { - const status = AppStatus.ERROR_DISABLED; - - result = false; - - await app.setStatus(status); - } - - return result; - } -} - -export const getPermissionsByAppId = (appId: string) => { - if (!AppManager.Instance) { - console.error('AppManager should be instantiated first'); - return []; - } - return AppManager.Instance.getPermissionsById(appId); -}; diff --git a/packages/apps-engine/src/server/IGetAppsFilter.ts b/packages/apps-engine/src/server/IGetAppsFilter.ts deleted file mode 100644 index 7829725634445..0000000000000 --- a/packages/apps-engine/src/server/IGetAppsFilter.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { AppInstallationSource } from './storage'; - -export interface IGetAppsFilter { - ids?: Array; - name?: string | RegExp; - enabled?: boolean; - disabled?: boolean; - installationSource?: AppInstallationSource; -} diff --git a/packages/apps-engine/src/server/ProxiedApp.ts b/packages/apps-engine/src/server/ProxiedApp.ts deleted file mode 100644 index d4eb00bbb6666..0000000000000 --- a/packages/apps-engine/src/server/ProxiedApp.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { inspect } from 'util'; - -import * as mem from 'mem'; - -import type { AppManager } from './AppManager'; -import { AppStatus } from '../definition/AppStatus'; -import { AppsEngineException } from '../definition/exceptions'; -import type { IAppAuthorInfo, IAppInfo } from '../definition/metadata'; -import { AppMethod } from '../definition/metadata'; -import { InvalidInstallationError } from './errors/InvalidInstallationError'; -import { AppConsole } from './logging'; -import { AppLicenseValidationResult } from './marketplace/license'; -import type { AppsEngineRuntime } from './runtime/AppsEngineRuntime'; -import type { IRuntimeController } from './runtime/IRuntimeController'; -import { JSONRPC_METHOD_NOT_FOUND } from './runtime/deno/AppsEngineDenoRuntime'; -import type { AppInstallationSource, IAppStorageItem } from './storage'; - -export class ProxiedApp { - private previousStatus: AppStatus; - - private latestLicenseValidationResult: AppLicenseValidationResult; - - constructor( - private readonly manager: AppManager, - private storageItem: IAppStorageItem, - private readonly appRuntime: IRuntimeController, - ) { - this.previousStatus = storageItem.status; - - this.appRuntime.on('processExit', () => mem.clear(this.getStatus)); - } - - public getRuntime(): AppsEngineRuntime { - return this.manager.getRuntime(); - } - - public getRuntimeController(): IRuntimeController { - return this.appRuntime; - } - - public getStorageItem(): IAppStorageItem { - return this.storageItem; - } - - public setStorageItem(item: IAppStorageItem): void { - this.storageItem = item; - } - - public getPreviousStatus(): AppStatus { - return this.previousStatus; - } - - public getImplementationList(): { [inter: string]: boolean } { - return this.storageItem.implemented; - } - - public setupLogger(method: `${AppMethod}`): AppConsole { - const logger = new AppConsole(method); - - return logger; - } - - // We'll need to refactor this method to remove the rest parameters so we can pass an options parameter - public async call(method: `${AppMethod}`, ...args: Array): Promise { - let options; - - try { - return await this.appRuntime.sendRequest({ method: `app:${method}`, params: args }, options); - } catch (e) { - if (e.code === AppsEngineException.JSONRPC_ERROR_CODE) { - throw new AppsEngineException(e.message); - } - - if (e.code === JSONRPC_METHOD_NOT_FOUND) { - throw e; - } - - // We cannot throw this error as the previous implementation swallowed those - // and since the server is not prepared to handle those we might crash it if we throw - // Range of JSON-RPC error codes: https://www.jsonrpc.org/specification#error_object - if (e.code >= -32999 || e.code <= -32000) { - // we really need to receive a logger from rocket.chat - console.error('JSON-RPC error received: ', inspect(e, { depth: 10 })); - } - } - } - - public getStatus = mem(() => this.appRuntime.getStatus().catch(() => AppStatus.UNKNOWN), { maxAge: 1000 * 60 * 5 }); - - public async setStatus(status: AppStatus, silent?: boolean): Promise { - await this.call(AppMethod.SETSTATUS, status); - mem.clear(this.getStatus); - if (!silent) { - await this.manager.getBridges().getAppActivationBridge().doAppStatusChanged(this, status); - } - } - - public getName(): string { - return this.storageItem.info.name; - } - - public getNameSlug(): string { - return this.storageItem.info.nameSlug; - } - - // @deprecated This method will be removed in the next major version - public getAppUserUsername(): string { - return `${this.storageItem.info.nameSlug}.bot`; - } - - public getID(): string { - return this.storageItem.id; - } - - public getInstallationSource(): AppInstallationSource { - return this.storageItem.installationSource; - } - - public getVersion(): string { - return this.storageItem.info.version; - } - - public getDescription(): string { - return this.storageItem.info.description; - } - - public getRequiredApiVersion(): string { - return this.storageItem.info.requiredApiVersion; - } - - public getAuthorInfo(): IAppAuthorInfo { - return this.storageItem.info.author; - } - - public getInfo(): IAppInfo { - return this.storageItem.info; - } - - public getEssentials(): IAppInfo['essentials'] { - return this.getInfo().essentials; - } - - public getLatestLicenseValidationResult(): AppLicenseValidationResult { - return this.latestLicenseValidationResult; - } - - public async validateInstallation(): Promise { - try { - await this.manager.getSignatureManager().verifySignedApp(this.getStorageItem()); - } catch (e) { - throw new InvalidInstallationError(e.message); - } - } - - public validateLicense(): Promise { - const { marketplaceInfo } = this.getStorageItem(); - - this.latestLicenseValidationResult = new AppLicenseValidationResult(); - - return this.manager.getLicenseManager().validate(this.latestLicenseValidationResult, marketplaceInfo); - } -} diff --git a/packages/apps-engine/src/server/accessors/ApiExtend.ts b/packages/apps-engine/src/server/accessors/ApiExtend.ts deleted file mode 100644 index 2dc4a9c99c71a..0000000000000 --- a/packages/apps-engine/src/server/accessors/ApiExtend.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IApiExtend } from '../../definition/accessors'; -import type { IApi } from '../../definition/api'; -import type { AppApiManager } from '../managers/AppApiManager'; - -export class ApiExtend implements IApiExtend { - constructor( - private readonly manager: AppApiManager, - private readonly appId: string, - ) {} - - public provideApi(api: IApi): Promise { - return Promise.resolve(this.manager.addApi(this.appId, api)); - } -} diff --git a/packages/apps-engine/src/server/accessors/AppAccessors.ts b/packages/apps-engine/src/server/accessors/AppAccessors.ts deleted file mode 100644 index 965b93c189173..0000000000000 --- a/packages/apps-engine/src/server/accessors/AppAccessors.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { IAppAccessors, IEnvironmentRead, IEnvironmentWrite, IHttp, IRead } from '../../definition/accessors'; -import type { IApiEndpointMetadata } from '../../definition/api'; -import type { AppManager } from '../AppManager'; -import type { AppAccessorManager } from '../managers/AppAccessorManager'; -import type { AppApiManager } from '../managers/AppApiManager'; - -export class AppAccessors implements IAppAccessors { - private accessorManager: AppAccessorManager; - - private apiManager: AppApiManager; - - constructor( - manager: AppManager, - private readonly appId: string, - ) { - this.accessorManager = manager.getAccessorManager(); - this.apiManager = manager.getApiManager(); - } - - public get environmentReader(): IEnvironmentRead { - return this.accessorManager.getEnvironmentRead(this.appId); - } - - public get environmentWriter(): IEnvironmentWrite { - return this.accessorManager.getEnvironmentWrite(this.appId); - } - - public get reader(): IRead { - return this.accessorManager.getReader(this.appId); - } - - public get http(): IHttp { - return this.accessorManager.getHttp(this.appId); - } - - public get providedApiEndpoints(): Array { - return this.apiManager.listApis(this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/CloudWorkspaceRead.ts b/packages/apps-engine/src/server/accessors/CloudWorkspaceRead.ts deleted file mode 100644 index 0ae0648718b88..0000000000000 --- a/packages/apps-engine/src/server/accessors/CloudWorkspaceRead.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ICloudWorkspaceRead } from '../../definition/accessors/ICloudWorkspaceRead'; -import type { IWorkspaceToken } from '../../definition/cloud/IWorkspaceToken'; -import type { CloudWorkspaceBridge } from '../bridges/CloudWorkspaceBridge'; - -export class CloudWorkspaceRead implements ICloudWorkspaceRead { - constructor( - private readonly cloudBridge: CloudWorkspaceBridge, - private readonly appId: string, - ) {} - - public async getWorkspaceToken(scope: string): Promise { - return this.cloudBridge.doGetWorkspaceToken(scope, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/ConfigurationExtend.ts b/packages/apps-engine/src/server/accessors/ConfigurationExtend.ts deleted file mode 100644 index 92bbb2a69acb8..0000000000000 --- a/packages/apps-engine/src/server/accessors/ConfigurationExtend.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { - IApiExtend, - IConfigurationExtend, - IExternalComponentsExtend, - IHttpExtend, - ISchedulerExtend, - ISettingsExtend, - ISlashCommandsExtend, - IUIExtend, - IVideoConfProvidersExtend, - IOutboundCommunicationProviderExtend, -} from '../../definition/accessors'; - -export class ConfigurationExtend implements IConfigurationExtend { - constructor( - public readonly http: IHttpExtend, - public readonly settings: ISettingsExtend, - public readonly slashCommands: ISlashCommandsExtend, - public readonly api: IApiExtend, - public readonly externalComponents: IExternalComponentsExtend, - public readonly scheduler: ISchedulerExtend, - public readonly ui: IUIExtend, - public readonly videoConfProviders: IVideoConfProvidersExtend, - public readonly outboundCommunication: IOutboundCommunicationProviderExtend, - ) {} -} diff --git a/packages/apps-engine/src/server/accessors/ConfigurationModify.ts b/packages/apps-engine/src/server/accessors/ConfigurationModify.ts deleted file mode 100644 index a7f1a4511fd7e..0000000000000 --- a/packages/apps-engine/src/server/accessors/ConfigurationModify.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { IConfigurationModify, ISchedulerModify, IServerSettingsModify, ISlashCommandsModify } from '../../definition/accessors'; - -export class ConfigurationModify implements IConfigurationModify { - constructor( - public readonly serverSettings: IServerSettingsModify, - public readonly slashCommands: ISlashCommandsModify, - public readonly scheduler: ISchedulerModify, - ) {} -} diff --git a/packages/apps-engine/src/server/accessors/ContactCreator.ts b/packages/apps-engine/src/server/accessors/ContactCreator.ts deleted file mode 100644 index 614f0c07d5d4e..0000000000000 --- a/packages/apps-engine/src/server/accessors/ContactCreator.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { IContactCreator } from '../../definition/accessors/IContactCreator'; -import type { ILivechatContact } from '../../definition/livechat'; -import type { AppBridges } from '../bridges'; - -export class ContactCreator implements IContactCreator { - constructor( - private readonly bridges: AppBridges, - private readonly appId: string, - ) {} - - verifyContact(verifyContactChannelParams: { - contactId: string; - field: string; - value: string; - visitorId: string; - roomId: string; - }): Promise { - return this.bridges.getContactBridge().doVerifyContact(verifyContactChannelParams, this.appId); - } - - addContactEmail(contactId: ILivechatContact['_id'], email: string): Promise { - return this.bridges.getContactBridge().doAddContactEmail(contactId, email, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/ContactRead.ts b/packages/apps-engine/src/server/accessors/ContactRead.ts deleted file mode 100644 index 64ec48bf28e74..0000000000000 --- a/packages/apps-engine/src/server/accessors/ContactRead.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IContactRead } from '../../definition/accessors/IContactRead'; -import type { ILivechatContact } from '../../definition/livechat'; -import type { AppBridges } from '../bridges'; - -export class ContactRead implements IContactRead { - constructor( - private readonly bridges: AppBridges, - private readonly appId: string, - ) {} - - public getById(contactId: ILivechatContact['_id']): Promise { - return this.bridges.getContactBridge().doGetById(contactId, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/DiscussionBuilder.ts b/packages/apps-engine/src/server/accessors/DiscussionBuilder.ts deleted file mode 100644 index fa1a903168700..0000000000000 --- a/packages/apps-engine/src/server/accessors/DiscussionBuilder.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { RoomBuilder } from './RoomBuilder'; -import type { IDiscussionBuilder } from '../../definition/accessors'; -import type { IMessage } from '../../definition/messages'; -import { RocketChatAssociationModel } from '../../definition/metadata'; -import { RoomType } from '../../definition/rooms'; -import type { IRoom } from '../../definition/rooms/IRoom'; - -export class DiscussionBuilder extends RoomBuilder implements IDiscussionBuilder { - public kind: RocketChatAssociationModel.DISCUSSION; - - private reply: string; - - private parentMessage: IMessage; - - constructor(data?: Partial) { - super(data); - this.kind = RocketChatAssociationModel.DISCUSSION; - this.room.type = RoomType.PRIVATE_GROUP; - } - - public setParentRoom(parentRoom: IRoom): IDiscussionBuilder { - this.room.parentRoom = parentRoom; - return this; - } - - public getParentRoom(): IRoom { - return this.room.parentRoom; - } - - public setReply(reply: string): IDiscussionBuilder { - this.reply = reply; - return this; - } - - public getReply(): string { - return this.reply; - } - - public setParentMessage(parentMessage: IMessage): IDiscussionBuilder { - this.parentMessage = parentMessage; - return this; - } - - public getParentMessage(): IMessage { - return this.parentMessage; - } -} diff --git a/packages/apps-engine/src/server/accessors/EmailCreator.ts b/packages/apps-engine/src/server/accessors/EmailCreator.ts deleted file mode 100644 index b0d5e136b72f2..0000000000000 --- a/packages/apps-engine/src/server/accessors/EmailCreator.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IEmailCreator } from '../../definition/accessors/IEmailCreator'; -import type { IEmail } from '../../definition/email'; -import type { AppBridges } from '../bridges'; - -export class EmailCreator implements IEmailCreator { - constructor( - private readonly bridges: AppBridges, - private readonly appId: string, - ) {} - - public async send(email: IEmail): Promise { - return this.bridges.getEmailBridge().doSendEmail(email, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/EnvironmentRead.ts b/packages/apps-engine/src/server/accessors/EnvironmentRead.ts deleted file mode 100644 index 563499f9f0a0f..0000000000000 --- a/packages/apps-engine/src/server/accessors/EnvironmentRead.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { IEnvironmentalVariableRead, IEnvironmentRead, IServerSettingRead, ISettingRead } from '../../definition/accessors'; - -export class EnvironmentRead implements IEnvironmentRead { - constructor( - private readonly settings: ISettingRead, - private readonly serverSettings: IServerSettingRead, - private readonly envRead: IEnvironmentalVariableRead, - ) {} - - public getSettings(): ISettingRead { - return this.settings; - } - - public getServerSettings(): IServerSettingRead { - return this.serverSettings; - } - - public getEnvironmentVariables(): IEnvironmentalVariableRead { - return this.envRead; - } -} diff --git a/packages/apps-engine/src/server/accessors/EnvironmentWrite.ts b/packages/apps-engine/src/server/accessors/EnvironmentWrite.ts deleted file mode 100644 index 9f31805c1b543..0000000000000 --- a/packages/apps-engine/src/server/accessors/EnvironmentWrite.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { IEnvironmentWrite, IServerSettingUpdater, ISettingUpdater } from '../../definition/accessors'; - -export class EnvironmentWrite implements IEnvironmentWrite { - constructor( - private readonly settings: ISettingUpdater, - private readonly serverSettings: IServerSettingUpdater, - ) {} - - public getSettings(): ISettingUpdater { - return this.settings; - } - - public getServerSettings(): IServerSettingUpdater { - return this.serverSettings; - } -} diff --git a/packages/apps-engine/src/server/accessors/EnvironmentalVariableRead.ts b/packages/apps-engine/src/server/accessors/EnvironmentalVariableRead.ts deleted file mode 100644 index 4ac5af3f878aa..0000000000000 --- a/packages/apps-engine/src/server/accessors/EnvironmentalVariableRead.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { IEnvironmentalVariableRead } from '../../definition/accessors'; -import type { EnvironmentalVariableBridge } from '../bridges'; - -export class EnvironmentalVariableRead implements IEnvironmentalVariableRead { - constructor( - private readonly bridge: EnvironmentalVariableBridge, - private readonly appId: string, - ) {} - - public getValueByName(envVarName: string): Promise { - return this.bridge.doGetValueByName(envVarName, this.appId); - } - - public isReadable(envVarName: string): Promise { - return this.bridge.doIsReadable(envVarName, this.appId); - } - - public isSet(envVarName: string): Promise { - return this.bridge.doIsSet(envVarName, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/ExperimentalRead.ts b/packages/apps-engine/src/server/accessors/ExperimentalRead.ts deleted file mode 100644 index 20922502163ec..0000000000000 --- a/packages/apps-engine/src/server/accessors/ExperimentalRead.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { IExperimentalRead } from '../../definition/accessors'; -import type { ExperimentalBridge } from '../bridges'; - -export class ExperimentalRead implements IExperimentalRead { - constructor( - protected readonly experimentalBridge: ExperimentalBridge, - protected readonly appId: string, - ) {} -} diff --git a/packages/apps-engine/src/server/accessors/ExternalComponentsExtend.ts b/packages/apps-engine/src/server/accessors/ExternalComponentsExtend.ts deleted file mode 100644 index c4c7c61fe3fde..0000000000000 --- a/packages/apps-engine/src/server/accessors/ExternalComponentsExtend.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IExternalComponentsExtend } from '../../definition/accessors'; -import type { IExternalComponent } from '../../definition/externalComponent/IExternalComponent'; -import type { AppExternalComponentManager } from '../managers/AppExternalComponentManager'; - -export class ExternalComponentsExtend implements IExternalComponentsExtend { - constructor( - private readonly manager: AppExternalComponentManager, - private readonly appId: string, - ) {} - - public async register(externalComponent: IExternalComponent): Promise { - return Promise.resolve(this.manager.addExternalComponent(this.appId, externalComponent)); - } -} diff --git a/packages/apps-engine/src/server/accessors/Http.ts b/packages/apps-engine/src/server/accessors/Http.ts deleted file mode 100644 index a10ac6082417b..0000000000000 --- a/packages/apps-engine/src/server/accessors/Http.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { IHttp, IHttpExtend, IHttpRequest, IHttpResponse } from '../../definition/accessors'; -import { RequestMethod } from '../../definition/accessors'; -import type { AppBridges } from '../bridges/AppBridges'; -import type { AppAccessorManager } from '../managers/AppAccessorManager'; - -export class Http implements IHttp { - constructor( - private readonly accessManager: AppAccessorManager, - private readonly bridges: AppBridges, - private readonly httpExtender: IHttpExtend, - private readonly appId: string, - ) {} - - public get(url: string, options?: IHttpRequest): Promise { - return this._processHandler(url, RequestMethod.GET, options); - } - - public put(url: string, options?: IHttpRequest): Promise { - return this._processHandler(url, RequestMethod.PUT, options); - } - - public post(url: string, options?: IHttpRequest): Promise { - return this._processHandler(url, RequestMethod.POST, options); - } - - public del(url: string, options?: IHttpRequest): Promise { - return this._processHandler(url, RequestMethod.DELETE, options); - } - - public patch(url: string, options?: IHttpRequest): Promise { - return this._processHandler(url, RequestMethod.PATCH, options); - } - - private async _processHandler(url: string, method: RequestMethod, options?: IHttpRequest): Promise { - let request = options || {}; - - if (typeof request.headers === 'undefined') { - request.headers = {}; - } - - this.httpExtender.getDefaultHeaders().forEach((value: string, key: string) => { - if (typeof request.headers[key] !== 'string') { - request.headers[key] = value; - } - }); - - if (typeof request.params === 'undefined') { - request.params = {}; - } - - this.httpExtender.getDefaultParams().forEach((value: string, key: string) => { - if (typeof request.params[key] !== 'string') { - request.params[key] = value; - } - }); - - const reader = this.accessManager.getReader(this.appId); - const persis = this.accessManager.getPersistence(this.appId); - - for (const handler of this.httpExtender.getPreRequestHandlers()) { - request = await handler.executePreHttpRequest(url, request, reader, persis); - } - - let response = await this.bridges.getHttpBridge().doCall({ - appId: this.appId, - method, - url, - request, - }); - - for (const handler of this.httpExtender.getPreResponseHandlers()) { - response = await handler.executePreHttpResponse(response, reader, persis); - } - - return response; - } -} diff --git a/packages/apps-engine/src/server/accessors/HttpExtend.ts b/packages/apps-engine/src/server/accessors/HttpExtend.ts deleted file mode 100644 index d25aa7ba1454c..0000000000000 --- a/packages/apps-engine/src/server/accessors/HttpExtend.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { IHttpExtend, IHttpPreRequestHandler, IHttpPreResponseHandler } from '../../definition/accessors'; - -export class HttpExtend implements IHttpExtend { - private headers: Map; - - private params: Map; - - private requests: Array; - - private responses: Array; - - constructor() { - this.headers = new Map(); - this.params = new Map(); - this.requests = []; - this.responses = []; - } - - public provideDefaultHeader(key: string, value: string): void { - this.headers.set(key, value); - } - - public provideDefaultHeaders(headers: { [key: string]: string }): void { - Object.keys(headers).forEach((key) => this.headers.set(key, headers[key])); - } - - public provideDefaultParam(key: string, value: string): void { - this.params.set(key, value); - } - - public provideDefaultParams(params: { [key: string]: string }): void { - Object.keys(params).forEach((key) => this.params.set(key, params[key])); - } - - public providePreRequestHandler(handler: IHttpPreRequestHandler): void { - this.requests.push(handler); - } - - public providePreResponseHandler(handler: IHttpPreResponseHandler): void { - this.responses.push(handler); - } - - public getDefaultHeaders(): Map { - return new Map(this.headers); - } - - public getDefaultParams(): Map { - return new Map(this.params); - } - - public getPreRequestHandlers(): Array { - return Array.from(this.requests); - } - - public getPreResponseHandlers(): Array { - return Array.from(this.responses); - } -} diff --git a/packages/apps-engine/src/server/accessors/LivechatCreator.ts b/packages/apps-engine/src/server/accessors/LivechatCreator.ts deleted file mode 100644 index d03376d7185b0..0000000000000 --- a/packages/apps-engine/src/server/accessors/LivechatCreator.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { randomBytes } from 'crypto'; - -import type { ILivechatCreator } from '../../definition/accessors'; -import type { IExtraRoomParams } from '../../definition/accessors/ILivechatCreator'; -import type { ILivechatRoom } from '../../definition/livechat/ILivechatRoom'; -import type { IVisitorExternalIdentifier, IVisitor, ResolveVisitorContactData } from '../../definition/livechat/IVisitor'; -import type { IUser } from '../../definition/users'; -import type { AppBridges } from '../bridges'; - -export class LivechatCreator implements ILivechatCreator { - constructor( - private readonly bridges: AppBridges, - private readonly appId: string, - ) {} - - public resolveVisitor(externalId: IVisitorExternalIdentifier, contactData?: ResolveVisitorContactData): Promise { - return this.bridges.getLivechatBridge().doResolveVisitor(externalId, contactData, this.appId); - } - - public createRoom(visitor: IVisitor, agent: IUser, extraParams?: IExtraRoomParams): Promise { - return this.bridges.getLivechatBridge().doCreateRoom(visitor, agent, this.appId, extraParams); - } - - /** - * @deprecated Use `createAndReturnVisitor` instead. - */ - public createVisitor(visitor: IVisitor): Promise { - return this.bridges.getLivechatBridge().doCreateVisitor(visitor, this.appId); - } - - public createAndReturnVisitor(visitor: IVisitor): Promise { - return this.bridges.getLivechatBridge().doCreateAndReturnVisitor(visitor, this.appId); - } - - public createToken(): string { - return randomBytes(16).toString('hex'); // Ensures 128 bits of entropy - } -} diff --git a/packages/apps-engine/src/server/accessors/LivechatMessageBuilder.ts b/packages/apps-engine/src/server/accessors/LivechatMessageBuilder.ts deleted file mode 100644 index d7e68bc698d31..0000000000000 --- a/packages/apps-engine/src/server/accessors/LivechatMessageBuilder.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { MessageBuilder } from './MessageBuilder'; -import type { ILivechatMessageBuilder, IMessageBuilder } from '../../definition/accessors'; -import type { ILivechatMessage } from '../../definition/livechat/ILivechatMessage'; -import type { IVisitor } from '../../definition/livechat/IVisitor'; -import type { IMessage, IMessageAttachment } from '../../definition/messages'; -import { RocketChatAssociationModel } from '../../definition/metadata'; -import type { IRoom } from '../../definition/rooms'; -import { RoomType } from '../../definition/rooms'; -import type { IUser } from '../../definition/users'; - -export class LivechatMessageBuilder implements ILivechatMessageBuilder { - public kind: RocketChatAssociationModel.LIVECHAT_MESSAGE; - - private msg: ILivechatMessage; - - constructor(message?: ILivechatMessage) { - this.kind = RocketChatAssociationModel.LIVECHAT_MESSAGE; - this.msg = message || ({} as ILivechatMessage); - } - - public setData(data: ILivechatMessage): ILivechatMessageBuilder { - delete data.id; - this.msg = data; - - return this; - } - - public setRoom(room: IRoom): ILivechatMessageBuilder { - this.msg.room = room; - return this; - } - - public getRoom(): IRoom { - return this.msg.room; - } - - public setSender(sender: IUser): ILivechatMessageBuilder { - this.msg.sender = sender; - delete this.msg.visitor; - - return this; - } - - public getSender(): IUser { - return this.msg.sender; - } - - public setText(text: string): ILivechatMessageBuilder { - this.msg.text = text; - return this; - } - - public getText(): string { - return this.msg.text; - } - - public setEmojiAvatar(emoji: string): ILivechatMessageBuilder { - this.msg.emoji = emoji; - return this; - } - - public getEmojiAvatar(): string { - return this.msg.emoji; - } - - public setAvatarUrl(avatarUrl: string): ILivechatMessageBuilder { - this.msg.avatarUrl = avatarUrl; - return this; - } - - public getAvatarUrl(): string { - return this.msg.avatarUrl; - } - - public setUsernameAlias(alias: string): ILivechatMessageBuilder { - this.msg.alias = alias; - return this; - } - - public getUsernameAlias(): string { - return this.msg.alias; - } - - public addAttachment(attachment: IMessageAttachment): ILivechatMessageBuilder { - if (!this.msg.attachments) { - this.msg.attachments = []; - } - - this.msg.attachments.push(attachment); - return this; - } - - public setAttachments(attachments: Array): ILivechatMessageBuilder { - this.msg.attachments = attachments; - return this; - } - - public getAttachments(): Array { - return this.msg.attachments; - } - - public replaceAttachment(position: number, attachment: IMessageAttachment): ILivechatMessageBuilder { - if (!this.msg.attachments) { - this.msg.attachments = []; - } - - if (!this.msg.attachments[position]) { - throw new Error(`No attachment found at the index of "${position}" to replace.`); - } - - this.msg.attachments[position] = attachment; - return this; - } - - public removeAttachment(position: number): ILivechatMessageBuilder { - if (!this.msg.attachments) { - this.msg.attachments = []; - } - - if (!this.msg.attachments[position]) { - throw new Error(`No attachment found at the index of "${position}" to remove.`); - } - - this.msg.attachments.splice(position, 1); - - return this; - } - - public setEditor(user: IUser): ILivechatMessageBuilder { - this.msg.editor = user; - return this; - } - - public getEditor(): IUser { - return this.msg.editor; - } - - public setGroupable(groupable: boolean): ILivechatMessageBuilder { - this.msg.groupable = groupable; - return this; - } - - public getGroupable(): boolean { - return this.msg.groupable; - } - - public setParseUrls(parseUrls: boolean): ILivechatMessageBuilder { - this.msg.parseUrls = parseUrls; - return this; - } - - public getParseUrls(): boolean { - return this.msg.parseUrls; - } - - public setToken(token: string): ILivechatMessageBuilder { - this.msg.token = token; - return this; - } - - public getToken(): string { - return this.msg.token; - } - - public setVisitor(visitor: IVisitor): ILivechatMessageBuilder { - this.msg.visitor = visitor; - delete this.msg.sender; - - return this; - } - - public getVisitor(): IVisitor { - return this.msg.visitor; - } - - public getMessage(): ILivechatMessage { - if (!this.msg.room) { - throw new Error('The "room" property is required.'); - } - - if (this.msg.room.type !== RoomType.LIVE_CHAT) { - throw new Error('The room is not a Livechat room'); - } - - return this.msg; - } - - public getMessageBuilder(): IMessageBuilder { - return new MessageBuilder(this.msg as IMessage); - } -} diff --git a/packages/apps-engine/src/server/accessors/LivechatRead.ts b/packages/apps-engine/src/server/accessors/LivechatRead.ts deleted file mode 100644 index 3ee3b6925e154..0000000000000 --- a/packages/apps-engine/src/server/accessors/LivechatRead.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { ILivechatRead } from '../../definition/accessors/ILivechatRead'; -import type { IDepartment } from '../../definition/livechat'; -import type { ILivechatRoom } from '../../definition/livechat/ILivechatRoom'; -import type { IVisitor } from '../../definition/livechat/IVisitor'; -import type { IMessage } from '../../definition/messages'; -import type { LivechatBridge } from '../bridges/LivechatBridge'; - -export class LivechatRead implements ILivechatRead { - constructor( - private readonly livechatBridge: LivechatBridge, - private readonly appId: string, - ) {} - - /** - * @deprecated please use the `isOnlineAsync` method instead. - * In the next major, this method will be `async` - */ - public isOnline(departmentId?: string): boolean { - console.warn( - "The `LivechatRead.isOnline` method is deprecated and won't behave as intended. Please use `LivechatRead.isOnlineAsync` instead", - ); - - return this.livechatBridge.doIsOnline(departmentId, this.appId); - } - - public isOnlineAsync(departmentId?: string): Promise { - return this.livechatBridge.doIsOnlineAsync(departmentId, this.appId); - } - - public getDepartmentsEnabledWithAgents(): Promise> { - return this.livechatBridge.doFindDepartmentsEnabledWithAgents(this.appId); - } - - public getLivechatRooms(visitor: IVisitor, departmentId?: string): Promise> { - return this.livechatBridge.doFindRooms(visitor, departmentId, this.appId); - } - - public getLivechatTotalOpenRoomsByAgentId(agentId: string): Promise { - return this.livechatBridge.doCountOpenRoomsByAgentId(agentId, this.appId); - } - - public getLivechatOpenRoomsByAgentId(agentId: string): Promise> { - return this.livechatBridge.doFindOpenRoomsByAgentId(agentId, this.appId); - } - - /** - * @deprecated This method does not adhere to the conversion practices applied - * elsewhere in the Apps-Engine and will be removed in the next major version. - * Prefer the alternative methods to fetch visitors. - */ - public getLivechatVisitors(query: object): Promise> { - return this.livechatBridge.doFindVisitors(query, this.appId); - } - - public getLivechatVisitorById(id: string): Promise { - return this.livechatBridge.doFindVisitorById(id, this.appId); - } - - public getLivechatVisitorByEmail(email: string): Promise { - return this.livechatBridge.doFindVisitorByEmail(email, this.appId); - } - - public getLivechatVisitorByToken(token: string): Promise { - return this.livechatBridge.doFindVisitorByToken(token, this.appId); - } - - public getLivechatVisitorByPhoneNumber(phoneNumber: string): Promise { - return this.livechatBridge.doFindVisitorByPhoneNumber(phoneNumber, this.appId); - } - - public getLivechatDepartmentByIdOrName(value: string): Promise { - return this.livechatBridge.doFindDepartmentByIdOrName(value, this.appId); - } - - public _fetchLivechatRoomMessages(roomId: string): Promise> { - return this.livechatBridge.do_fetchLivechatRoomMessages(this.appId, roomId); - } -} diff --git a/packages/apps-engine/src/server/accessors/LivechatUpdater.ts b/packages/apps-engine/src/server/accessors/LivechatUpdater.ts deleted file mode 100644 index e7aa686084185..0000000000000 --- a/packages/apps-engine/src/server/accessors/LivechatUpdater.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { ILivechatUpdater } from '../../definition/accessors'; -import type { ILivechatRoom, ILivechatTransferData, IVisitor, IVisitorExternalIdentifier } from '../../definition/livechat'; -import type { IUser } from '../../definition/users'; -import type { AppBridges } from '../bridges'; - -export class LivechatUpdater implements ILivechatUpdater { - constructor( - private readonly bridges: AppBridges, - private readonly appId: string, - ) {} - - public transferVisitor(visitor: IVisitor, transferData: ILivechatTransferData): Promise { - return this.bridges.getLivechatBridge().doTransferVisitor(visitor, transferData, this.appId); - } - - public closeRoom(room: ILivechatRoom, comment: string, closer?: IUser): Promise { - return this.bridges.getLivechatBridge().doCloseRoom(room, comment, closer, this.appId); - } - - public setCustomFields(token: IVisitor['token'], key: string, value: string, overwrite: boolean): Promise { - return this.bridges - .getLivechatBridge() - .doSetCustomFields({ token, key, value, overwrite }, this.appId) - .then((result) => result > 0); - } - - public updateVisitorExternalId( - visitorId: string, - externalId: Omit, - ): Promise { - return this.bridges.getLivechatBridge().doUpdateVisitorExternalId(visitorId, externalId, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/MessageBuilder.ts b/packages/apps-engine/src/server/accessors/MessageBuilder.ts deleted file mode 100644 index ff6cdf69f9164..0000000000000 --- a/packages/apps-engine/src/server/accessors/MessageBuilder.ts +++ /dev/null @@ -1,224 +0,0 @@ -import type { LayoutBlock } from '@rocket.chat/ui-kit'; - -import type { IMessageBuilder } from '../../definition/accessors'; -import type { IMessage, IMessageAttachment } from '../../definition/messages'; -import { RocketChatAssociationModel } from '../../definition/metadata'; -import type { IRoom } from '../../definition/rooms'; -import type { IBlock } from '../../definition/uikit'; -import { BlockBuilder } from '../../definition/uikit'; -import type { IUser } from '../../definition/users'; - -export class MessageBuilder implements IMessageBuilder { - public kind: RocketChatAssociationModel.MESSAGE; - - private msg: IMessage; - - constructor(message?: IMessage) { - this.kind = RocketChatAssociationModel.MESSAGE; - this.msg = message || ({} as IMessage); - } - - public setData(data: IMessage): IMessageBuilder { - delete data.id; - this.msg = data; - - return this; - } - - public setUpdateData(data: IMessage, editor: IUser): IMessageBuilder { - this.msg = data; - this.msg.editor = editor; - this.msg.editedAt = new Date(); - - return this; - } - - public setThreadId(threadId: string): IMessageBuilder { - this.msg.threadId = threadId; - - return this; - } - - public getThreadId(): string { - return this.msg.threadId; - } - - public setRoom(room: IRoom): IMessageBuilder { - this.msg.room = room; - return this; - } - - public getRoom(): IRoom { - return this.msg.room; - } - - public setSender(sender: IUser): IMessageBuilder { - this.msg.sender = sender; - return this; - } - - public getSender(): IUser { - return this.msg.sender; - } - - public setText(text: string): IMessageBuilder { - this.msg.text = text; - return this; - } - - public getText(): string { - return this.msg.text; - } - - public setEmojiAvatar(emoji: string): IMessageBuilder { - this.msg.emoji = emoji; - return this; - } - - public getEmojiAvatar(): string { - return this.msg.emoji; - } - - public setAvatarUrl(avatarUrl: string): IMessageBuilder { - this.msg.avatarUrl = avatarUrl; - return this; - } - - public getAvatarUrl(): string { - return this.msg.avatarUrl; - } - - public setUsernameAlias(alias: string): IMessageBuilder { - this.msg.alias = alias; - return this; - } - - public getUsernameAlias(): string { - return this.msg.alias; - } - - public addAttachment(attachment: IMessageAttachment): IMessageBuilder { - if (!this.msg.attachments) { - this.msg.attachments = []; - } - - this.msg.attachments.push(attachment); - return this; - } - - public setAttachments(attachments: Array): IMessageBuilder { - this.msg.attachments = attachments; - return this; - } - - public getAttachments(): Array { - return this.msg.attachments; - } - - public replaceAttachment(position: number, attachment: IMessageAttachment): IMessageBuilder { - if (!this.msg.attachments) { - this.msg.attachments = []; - } - - if (!this.msg.attachments[position]) { - throw new Error(`No attachment found at the index of "${position}" to replace.`); - } - - this.msg.attachments[position] = attachment; - return this; - } - - public removeAttachment(position: number): IMessageBuilder { - if (!this.msg.attachments) { - this.msg.attachments = []; - } - - if (!this.msg.attachments[position]) { - throw new Error(`No attachment found at the index of "${position}" to remove.`); - } - - this.msg.attachments.splice(position, 1); - - return this; - } - - public setEditor(user: IUser): IMessageBuilder { - this.msg.editor = user; - return this; - } - - public getEditor(): IUser { - return this.msg.editor; - } - - public setGroupable(groupable: boolean): IMessageBuilder { - this.msg.groupable = groupable; - return this; - } - - public getGroupable(): boolean { - return this.msg.groupable; - } - - public setParseUrls(parseUrls: boolean): IMessageBuilder { - this.msg.parseUrls = parseUrls; - return this; - } - - public getParseUrls(): boolean { - return this.msg.parseUrls; - } - - public getMessage(): IMessage { - if (!this.msg.room) { - throw new Error('The "room" property is required.'); - } - - return this.msg; - } - - public addBlocks(blocks: BlockBuilder | Array) { - if (!Array.isArray(this.msg.blocks)) { - this.msg.blocks = []; - } - - if (blocks instanceof BlockBuilder) { - this.msg.blocks.push(...blocks.getBlocks()); - } else { - this.msg.blocks.push(...blocks); - } - - return this; - } - - public setBlocks(blocks: BlockBuilder | Array) { - if (blocks instanceof BlockBuilder) { - this.msg.blocks = blocks.getBlocks(); - } else { - this.msg.blocks = blocks; - } - - return this; - } - - public getBlocks() { - return this.msg.blocks; - } - - public addCustomField(key: string, value: any): IMessageBuilder { - if (!this.msg.customFields) { - this.msg.customFields = {}; - } - - if (this.msg.customFields[key]) { - throw new Error(`The message already contains a custom field by the key: ${key}`); - } - - if (key.includes('.')) { - throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`); - } - - this.msg.customFields[key] = value; - return this; - } -} diff --git a/packages/apps-engine/src/server/accessors/MessageExtender.ts b/packages/apps-engine/src/server/accessors/MessageExtender.ts deleted file mode 100644 index eb1b9a267800c..0000000000000 --- a/packages/apps-engine/src/server/accessors/MessageExtender.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { IMessageExtender } from '../../definition/accessors'; -import type { IMessage, IMessageAttachment } from '../../definition/messages'; -import { RocketChatAssociationModel } from '../../definition/metadata'; -import { Utilities } from '../misc/Utilities'; - -export class MessageExtender implements IMessageExtender { - public readonly kind: RocketChatAssociationModel.MESSAGE; - - constructor(private msg: IMessage) { - this.kind = RocketChatAssociationModel.MESSAGE; - - if (!Array.isArray(msg.attachments)) { - this.msg.attachments = []; - } - } - - public addCustomField(key: string, value: any): IMessageExtender { - if (!this.msg.customFields) { - this.msg.customFields = {}; - } - - if (this.msg.customFields[key]) { - throw new Error(`The message already contains a custom field by the key: ${key}`); - } - - if (key.includes('.')) { - throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`); - } - - this.msg.customFields[key] = value; - - return this; - } - - public addAttachment(attachment: IMessageAttachment): IMessageExtender { - this.msg.attachments.push(attachment); - - return this; - } - - public addAttachments(attachments: Array): IMessageExtender { - this.msg.attachments = this.msg.attachments.concat(attachments); - - return this; - } - - public getMessage(): IMessage { - return Utilities.deepClone(this.msg); - } -} diff --git a/packages/apps-engine/src/server/accessors/MessageRead.ts b/packages/apps-engine/src/server/accessors/MessageRead.ts deleted file mode 100644 index 2a6528d646660..0000000000000 --- a/packages/apps-engine/src/server/accessors/MessageRead.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { IMessageRead } from '../../definition/accessors'; -import type { IMessage } from '../../definition/messages'; -import type { IRoom } from '../../definition/rooms'; -import type { IUser } from '../../definition/users'; -import type { MessageBridge } from '../bridges/MessageBridge'; - -export class MessageRead implements IMessageRead { - constructor( - private messageBridge: MessageBridge, - private appId: string, - ) {} - - public getById(id: string): Promise { - return this.messageBridge.doGetById(id, this.appId); - } - - public async getSenderUser(messageId: string): Promise { - const msg = await this.messageBridge.doGetById(messageId, this.appId); - - if (!msg) { - return undefined; - } - - return msg.sender; - } - - public async getRoom(messageId: string): Promise { - const msg = await this.messageBridge.doGetById(messageId, this.appId); - - if (!msg) { - return undefined; - } - - return msg.room; - } -} diff --git a/packages/apps-engine/src/server/accessors/ModerationModify.ts b/packages/apps-engine/src/server/accessors/ModerationModify.ts deleted file mode 100644 index 6b94cdd70425d..0000000000000 --- a/packages/apps-engine/src/server/accessors/ModerationModify.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { IModerationModify } from '../../definition/accessors'; -import type { IMessage } from '../../definition/messages'; -import type { IUser } from '../../definition/users'; -import type { ModerationBridge } from '../bridges'; - -export class ModerationModify implements IModerationModify { - constructor( - private moderationBridge: ModerationBridge, - appId: string, - ) {} - - public report(messageId: string, description: string, userId: string, appId: string): Promise { - return this.moderationBridge.doReport(messageId, description, userId, appId); - } - - public dismissReportsByMessageId(messageId: IMessage['id'], reason: string, action: string, appId: string): Promise { - return this.moderationBridge.doDismissReportsByMessageId(messageId, reason, action, appId); - } - - public dismissReportsByUserId(userId: IUser['id'], reason: string, action: string, appId: string): Promise { - return this.moderationBridge.doDismissReportsByUserId(userId, reason, action, appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/Modify.ts b/packages/apps-engine/src/server/accessors/Modify.ts deleted file mode 100644 index 604bd5ea325a2..0000000000000 --- a/packages/apps-engine/src/server/accessors/Modify.ts +++ /dev/null @@ -1,92 +0,0 @@ -import type { - IModify, - IModifyCreator, - IModifyDeleter, - IModifyExtender, - IModifyUpdater, - INotifier, - ISchedulerModify, - IUIController, -} from '../../definition/accessors'; -import type { IOAuthAppsModify } from '../../definition/accessors/IOAuthAppsModify'; -import type { AppBridges } from '../bridges'; -import { ModerationModify } from './ModerationModify'; -import { ModifyCreator } from './ModifyCreator'; -import { ModifyDeleter } from './ModifyDeleter'; -import { ModifyExtender } from './ModifyExtender'; -import { ModifyUpdater } from './ModifyUpdater'; -import { Notifier } from './Notifier'; -import { OAuthAppsModify } from './OAuthAppsModify'; -import { SchedulerModify } from './SchedulerModify'; -import { UIController } from './UIController'; - -export class Modify implements IModify { - private creator: IModifyCreator; - - private deleter: IModifyDeleter; - - private updater: IModifyUpdater; - - private extender: IModifyExtender; - - private notifier: INotifier; - - private uiController: IUIController; - - private scheduler: ISchedulerModify; - - private oauthApps: IOAuthAppsModify; - - private moderation: ModerationModify; - - constructor( - private readonly bridges: AppBridges, - private readonly appId: string, - ) { - this.creator = new ModifyCreator(this.bridges, this.appId); - this.deleter = new ModifyDeleter(this.bridges, this.appId); - this.updater = new ModifyUpdater(this.bridges, this.appId); - this.extender = new ModifyExtender(this.bridges, this.appId); - this.notifier = new Notifier(this.bridges.getUserBridge(), this.bridges.getMessageBridge(), this.appId); - this.uiController = new UIController(this.appId, this.bridges); - this.scheduler = new SchedulerModify(this.bridges.getSchedulerBridge(), this.appId); - this.oauthApps = new OAuthAppsModify(this.bridges.getOAuthAppsBridge(), this.appId); - this.moderation = new ModerationModify(this.bridges.getModerationBridge(), this.appId); - } - - public getCreator(): IModifyCreator { - return this.creator; - } - - public getDeleter(): IModifyDeleter { - return this.deleter; - } - - public getUpdater(): IModifyUpdater { - return this.updater; - } - - public getExtender(): IModifyExtender { - return this.extender; - } - - public getNotifier(): INotifier { - return this.notifier; - } - - public getUiController(): IUIController { - return this.uiController; - } - - public getScheduler(): ISchedulerModify { - return this.scheduler; - } - - public getOAuthAppsModifier() { - return this.oauthApps; - } - - public getModerationModifier() { - return this.moderation; - } -} diff --git a/packages/apps-engine/src/server/accessors/ModifyCreator.ts b/packages/apps-engine/src/server/accessors/ModifyCreator.ts deleted file mode 100644 index fe7bbdcb5fc8d..0000000000000 --- a/packages/apps-engine/src/server/accessors/ModifyCreator.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { ContactCreator } from './ContactCreator'; -import { DiscussionBuilder } from './DiscussionBuilder'; -import { EmailCreator } from './EmailCreator'; -import { LivechatCreator } from './LivechatCreator'; -import { LivechatMessageBuilder } from './LivechatMessageBuilder'; -import { MessageBuilder } from './MessageBuilder'; -import { RoomBuilder } from './RoomBuilder'; -import { UploadCreator } from './UploadCreator'; -import { UserBuilder } from './UserBuilder'; -import { VideoConferenceBuilder } from './VideoConferenceBuilder'; -import type { - IDiscussionBuilder, - ILivechatCreator, - ILivechatMessageBuilder, - IMessageBuilder, - IModifyCreator, - IRoomBuilder, - IUploadCreator, - IUserBuilder, - IVideoConferenceBuilder, -} from '../../definition/accessors'; -import type { IContactCreator } from '../../definition/accessors/IContactCreator'; -import type { IEmailCreator } from '../../definition/accessors/IEmailCreator'; -import type { ILivechatMessage } from '../../definition/livechat/ILivechatMessage'; -import type { IMessage } from '../../definition/messages'; -import { RocketChatAssociationModel } from '../../definition/metadata'; -import type { IRoom } from '../../definition/rooms'; -import { RoomType } from '../../definition/rooms'; -import { BlockBuilder } from '../../definition/uikit'; -import type { IBotUser } from '../../definition/users/IBotUser'; -import { UserType } from '../../definition/users/UserType'; -import type { AppVideoConference } from '../../definition/videoConferences'; -import type { AppBridges } from '../bridges'; -import { UIHelper } from '../misc/UIHelper'; - -export class ModifyCreator implements IModifyCreator { - private livechatCreator: LivechatCreator; - - private uploadCreator: UploadCreator; - - private emailCreator: EmailCreator; - - private contactCreator: ContactCreator; - - constructor( - private readonly bridges: AppBridges, - private readonly appId: string, - ) { - this.livechatCreator = new LivechatCreator(bridges, appId); - this.uploadCreator = new UploadCreator(bridges, appId); - this.emailCreator = new EmailCreator(bridges, appId); - this.contactCreator = new ContactCreator(bridges, appId); - } - - public getLivechatCreator(): ILivechatCreator { - return this.livechatCreator; - } - - public getUploadCreator(): IUploadCreator { - return this.uploadCreator; - } - - public getEmailCreator(): IEmailCreator { - return this.emailCreator; - } - - public getContactCreator(): IContactCreator { - return this.contactCreator; - } - - /** - * @deprecated please prefer the rocket.chat/ui-kit components - */ - public getBlockBuilder(): BlockBuilder { - return new BlockBuilder(this.appId); - } - - public startMessage(data?: IMessage): IMessageBuilder { - if (data) { - delete data.id; - } - - return new MessageBuilder(data); - } - - public startLivechatMessage(data?: ILivechatMessage): ILivechatMessageBuilder { - if (data) { - delete data.id; - } - - return new LivechatMessageBuilder(data); - } - - public startRoom(data?: IRoom): IRoomBuilder { - if (data) { - delete data.id; - } - - return new RoomBuilder(data); - } - - public startDiscussion(data?: Partial): IDiscussionBuilder { - if (data) { - delete data.id; - } - - return new DiscussionBuilder(data); - } - - public startVideoConference(data?: Partial): IVideoConferenceBuilder { - return new VideoConferenceBuilder(data); - } - - public startBotUser(data?: Partial): IUserBuilder { - if (data) { - delete data.id; - - const { roles } = data; - - if (roles?.length) { - const hasRole = roles - .map((role) => role.toLocaleLowerCase()) - .some((role) => role === 'admin' || role === 'owner' || role === 'moderator'); - - if (hasRole) { - throw new Error('Invalid role assigned to the user. Should not be admin, owner or moderator.'); - } - } - - if (!data.type) { - data.type = UserType.BOT; - } - } - - return new UserBuilder(data); - } - - public finish( - builder: IMessageBuilder | ILivechatMessageBuilder | IRoomBuilder | IDiscussionBuilder | IVideoConferenceBuilder | IUserBuilder, - ): Promise { - switch (builder.kind) { - case RocketChatAssociationModel.MESSAGE: - return this._finishMessage(builder); - case RocketChatAssociationModel.LIVECHAT_MESSAGE: - return this._finishLivechatMessage(builder); - case RocketChatAssociationModel.ROOM: - return this._finishRoom(builder); - case RocketChatAssociationModel.DISCUSSION: - return this._finishDiscussion(builder as IDiscussionBuilder); - case RocketChatAssociationModel.VIDEO_CONFERENCE: - return this._finishVideoConference(builder); - case RocketChatAssociationModel.USER: - return this._finishUser(builder); - default: - throw new Error('Invalid builder passed to the ModifyCreator.finish function.'); - } - } - - private async _finishMessage(builder: IMessageBuilder): Promise { - const result = builder.getMessage(); - delete result.id; - - if (!result.sender?.id) { - const appUser = await this.bridges.getUserBridge().doGetAppUser(this.appId); - - if (!appUser) { - throw new Error('Invalid sender assigned to the message.'); - } - - result.sender = appUser; - } - - if (result.blocks?.length) { - result.blocks = UIHelper.assignIds(result.blocks, this.appId); - } - - return this.bridges.getMessageBridge().doCreate(result, this.appId); - } - - private _finishLivechatMessage(builder: ILivechatMessageBuilder): Promise { - if (builder.getSender() && !builder.getVisitor()) { - return this._finishMessage(builder.getMessageBuilder()); - } - - const result = builder.getMessage(); - delete result.id; - - if (!result.token && !result.visitor?.token) { - throw new Error('Invalid visitor sending the message'); - } - - result.token = result.visitor ? result.visitor.token : result.token; - - return this.bridges.getLivechatBridge().doCreateMessage(result, this.appId); - } - - private _finishRoom(builder: IRoomBuilder): Promise { - const result = builder.getRoom(); - delete result.id; - - if (!result.type) { - throw new Error('Invalid type assigned to the room.'); - } - - if (result.type !== RoomType.LIVE_CHAT) { - if (!result.creator?.id) { - throw new Error('Invalid creator assigned to the room.'); - } - } - - if (result.type !== RoomType.DIRECT_MESSAGE) { - if (result.type !== RoomType.LIVE_CHAT) { - if (!result.slugifiedName?.trim()) { - throw new Error('Invalid slugifiedName assigned to the room.'); - } - } - - if (!result.displayName?.trim()) { - throw new Error('Invalid displayName assigned to the room.'); - } - } - - return this.bridges.getRoomBridge().doCreate(result, builder.getMembersToBeAddedUsernames(), this.appId); - } - - private _finishDiscussion(builder: IDiscussionBuilder): Promise { - const room = builder.getRoom(); - delete room.id; - - if (!room.creator?.id) { - throw new Error('Invalid creator assigned to the discussion.'); - } - - if (!room.slugifiedName?.trim()) { - throw new Error('Invalid slugifiedName assigned to the discussion.'); - } - - if (!room.displayName?.trim()) { - throw new Error('Invalid displayName assigned to the discussion.'); - } - - if (!room.parentRoom?.id) { - throw new Error('Invalid parentRoom assigned to the discussion.'); - } - - return this.bridges - .getRoomBridge() - .doCreateDiscussion(room, builder.getParentMessage(), builder.getReply(), builder.getMembersToBeAddedUsernames(), this.appId); - } - - private _finishVideoConference(builder: IVideoConferenceBuilder): Promise { - const videoConference = builder.getVideoConference(); - - if (!videoConference.createdBy) { - throw new Error('Invalid creator assigned to the video conference.'); - } - - if (!videoConference.providerName?.trim()) { - throw new Error('Invalid provider name assigned to the video conference.'); - } - - if (!videoConference.rid) { - throw new Error('Invalid roomId assigned to the video conference.'); - } - - return this.bridges.getVideoConferenceBridge().doCreate(videoConference, this.appId); - } - - private _finishUser(builder: IUserBuilder): Promise { - const user = builder.getUser(); - - return this.bridges.getUserBridge().doCreate(user, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/ModifyDeleter.ts b/packages/apps-engine/src/server/accessors/ModifyDeleter.ts deleted file mode 100644 index 51b2778313dbb..0000000000000 --- a/packages/apps-engine/src/server/accessors/ModifyDeleter.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { IModifyDeleter } from '../../definition/accessors'; -import type { IMessage } from '../../definition/messages'; -import type { IUser, UserType } from '../../definition/users'; -import type { AppBridges } from '../bridges'; - -export class ModifyDeleter implements IModifyDeleter { - constructor( - private readonly bridges: AppBridges, - private readonly appId: string, - ) {} - - public async deleteRoom(roomId: string): Promise { - return this.bridges.getRoomBridge().doDelete(roomId, this.appId); - } - - public async deleteUsers(appId: Exclude, userType: UserType.APP | UserType.BOT): Promise { - return this.bridges.getUserBridge().doDeleteUsersCreatedByApp(appId, userType); - } - - public async deleteMessage(message: IMessage, user: IUser): Promise { - return this.bridges.getMessageBridge().doDelete(message, user, this.appId); - } - - /** - * Removes `usernames` from the room's member list - * - * For performance reasons, it is only possible to remove 50 users in one - * call to this method. Removing users is an expensive operation due to the - * amount of entity relationships that need to be modified. - */ - public async removeUsersFromRoom(roomId: string, usernames: Array) { - if (usernames.length > 50) { - throw new Error('A maximum of 50 members can be removed in a single call'); - } - - return this.bridges.getRoomBridge().doRemoveUsers(roomId, usernames, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/ModifyExtender.ts b/packages/apps-engine/src/server/accessors/ModifyExtender.ts deleted file mode 100644 index 1c276e6a01419..0000000000000 --- a/packages/apps-engine/src/server/accessors/ModifyExtender.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { MessageExtender } from './MessageExtender'; -import { RoomExtender } from './RoomExtender'; -import { VideoConferenceExtender } from './VideoConferenceExtend'; -import type { IMessageExtender, IModifyExtender, IRoomExtender, IVideoConferenceExtender } from '../../definition/accessors'; -import { RocketChatAssociationModel } from '../../definition/metadata'; -import type { IUser } from '../../definition/users'; -import type { AppBridges } from '../bridges/AppBridges'; - -export class ModifyExtender implements IModifyExtender { - constructor( - private readonly bridges: AppBridges, - private readonly appId: string, - ) {} - - public async extendMessage(messageId: string, updater: IUser): Promise { - const msg = await this.bridges.getMessageBridge().doGetById(messageId, this.appId); - msg.editor = updater; - msg.editedAt = new Date(); - - return new MessageExtender(msg); - } - - public async extendRoom(roomId: string, updater: IUser): Promise { - const room = await this.bridges.getRoomBridge().doGetById(roomId, this.appId); - room.updatedAt = new Date(); - - return new RoomExtender(room); - } - - public async extendVideoConference(id: string): Promise { - const call = await this.bridges.getVideoConferenceBridge().doGetById(id, this.appId); - call._updatedAt = new Date(); - - return new VideoConferenceExtender(call); - } - - public finish(extender: IMessageExtender | IRoomExtender | IVideoConferenceExtender): Promise { - switch (extender.kind) { - case RocketChatAssociationModel.MESSAGE: - return this.bridges.getMessageBridge().doUpdate(extender.getMessage(), this.appId); - case RocketChatAssociationModel.ROOM: - return this.bridges.getRoomBridge().doUpdate(extender.getRoom(), extender.getUsernamesOfMembersBeingAdded(), this.appId); - case RocketChatAssociationModel.VIDEO_CONFERENCE: - return this.bridges.getVideoConferenceBridge().doUpdate(extender.getVideoConference(), this.appId); - default: - throw new Error('Invalid extender passed to the ModifyExtender.finish function.'); - } - } -} diff --git a/packages/apps-engine/src/server/accessors/ModifyUpdater.ts b/packages/apps-engine/src/server/accessors/ModifyUpdater.ts deleted file mode 100644 index 496e497860690..0000000000000 --- a/packages/apps-engine/src/server/accessors/ModifyUpdater.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { LivechatUpdater } from './LivechatUpdater'; -import { MessageBuilder } from './MessageBuilder'; -import { RoomBuilder } from './RoomBuilder'; -import { UserUpdater } from './UserUpdater'; -import type { ILivechatUpdater, IMessageBuilder, IMessageUpdater, IModifyUpdater, IRoomBuilder } from '../../definition/accessors'; -import type { IUserUpdater } from '../../definition/accessors/IUserUpdater'; -import { RocketChatAssociationModel } from '../../definition/metadata'; -import { RoomType } from '../../definition/rooms'; -import type { IUser } from '../../definition/users'; -import type { AppBridges } from '../bridges'; -import { UIHelper } from '../misc/UIHelper'; - -export class ModifyUpdater implements IModifyUpdater { - private livechatUpdater: ILivechatUpdater; - - private userUpdater: IUserUpdater; - - private messageUpdater: IMessageUpdater; - - constructor( - private readonly bridges: AppBridges, - private readonly appId: string, - ) { - this.livechatUpdater = new LivechatUpdater(this.bridges, this.appId); - this.userUpdater = new UserUpdater(this.bridges, this.appId); - } - - public getLivechatUpdater(): ILivechatUpdater { - return this.livechatUpdater; - } - - public getUserUpdater(): IUserUpdater { - return this.userUpdater; - } - - public getMessageUpdater(): IMessageUpdater { - return this.messageUpdater; - } - - public async message(messageId: string, updater: IUser): Promise { - const msg = await this.bridges.getMessageBridge().doGetById(messageId, this.appId); - - return new MessageBuilder(msg); - } - - public async room(roomId: string, updater: IUser): Promise { - const room = await this.bridges.getRoomBridge().doGetById(roomId, this.appId); - - return new RoomBuilder(room); - } - - public finish(builder: IMessageBuilder | IRoomBuilder): Promise { - switch (builder.kind) { - case RocketChatAssociationModel.MESSAGE: - return this._finishMessage(builder); - case RocketChatAssociationModel.ROOM: - return this._finishRoom(builder); - default: - throw new Error('Invalid builder passed to the ModifyUpdater.finish function.'); - } - } - - private _finishMessage(builder: IMessageBuilder): Promise { - const result = builder.getMessage(); - - if (!result.id) { - throw new Error("Invalid message, can't update a message without an id."); - } - - if (!result.sender?.id) { - throw new Error('Invalid sender assigned to the message.'); - } - - if (result.blocks?.length) { - result.blocks = UIHelper.assignIds(result.blocks, this.appId); - // result.blocks = this._assignIds(result.blocks); - } - - return this.bridges.getMessageBridge().doUpdate(result, this.appId); - } - - private _finishRoom(builder: IRoomBuilder): Promise { - const result = builder.getRoom(); - - if (!result.id) { - throw new Error('Invalid room, can not update a room without an id.'); - } - - if (!result.type) { - throw new Error('Invalid type assigned to the room.'); - } - - if (result.type !== RoomType.LIVE_CHAT) { - if (!result.creator?.id) { - throw new Error('Invalid creator assigned to the room.'); - } - - if (!result.slugifiedName?.trim()) { - throw new Error('Invalid slugifiedName assigned to the room.'); - } - } - - if (!result.displayName?.trim()) { - throw new Error('Invalid displayName assigned to the room.'); - } - - return this.bridges.getRoomBridge().doUpdate(result, builder.getMembersToBeAddedUsernames(), this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/Notifier.ts b/packages/apps-engine/src/server/accessors/Notifier.ts deleted file mode 100644 index ff3d97070df06..0000000000000 --- a/packages/apps-engine/src/server/accessors/Notifier.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { IMessageBuilder, INotifier } from '../../definition/accessors'; -import type { ITypingOptions } from '../../definition/accessors/INotifier'; -import { TypingScope } from '../../definition/accessors/INotifier'; -import type { IMessage } from '../../definition/messages'; -import type { IRoom } from '../../definition/rooms'; -import type { IUser } from '../../definition/users'; -import type { MessageBridge, UserBridge } from '../bridges'; -import { MessageBuilder } from './MessageBuilder'; - -export class Notifier implements INotifier { - constructor( - private readonly userBridge: UserBridge, - private readonly msgBridge: MessageBridge, - private readonly appId: string, - ) {} - - public async notifyUser(user: IUser, message: IMessage): Promise { - if (!message.sender?.id) { - const appUser = await this.userBridge.doGetAppUser(this.appId); - - message.sender = appUser; - } - - await this.msgBridge.doNotifyUser(user, message, this.appId); - } - - public async notifyRoom(room: IRoom, message: IMessage): Promise { - if (!message.sender?.id) { - const appUser = await this.userBridge.doGetAppUser(this.appId); - - message.sender = appUser; - } - - await this.msgBridge.doNotifyRoom(room, message, this.appId); - } - - public async typing(options: ITypingOptions): Promise<() => Promise> { - options.scope = options.scope || TypingScope.Room; - - if (!options.username) { - const appUser = await this.userBridge.doGetAppUser(this.appId); - options.username = appUser?.name || ''; - } - - this.msgBridge.doTyping({ ...options, isTyping: true }, this.appId); - - return () => this.msgBridge.doTyping({ ...options, isTyping: false }, this.appId); - } - - public getMessageBuilder(): IMessageBuilder { - return new MessageBuilder(); - } -} diff --git a/packages/apps-engine/src/server/accessors/OAuthAppsModify.ts b/packages/apps-engine/src/server/accessors/OAuthAppsModify.ts deleted file mode 100644 index 323e706c0fa1b..0000000000000 --- a/packages/apps-engine/src/server/accessors/OAuthAppsModify.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { IOAuthAppParams } from '../../definition/accessors/IOAuthApp'; -import type { IOAuthAppsModify } from '../../definition/accessors/IOAuthAppsModify'; -import type { OAuthAppsBridge } from '../bridges/OAuthAppsBridge'; - -export class OAuthAppsModify implements IOAuthAppsModify { - constructor( - private readonly oauthAppsBridge: OAuthAppsBridge, - private readonly appId: string, - ) {} - - public async createOAuthApp(oAuthApp: IOAuthAppParams): Promise { - return this.oauthAppsBridge.doCreate(oAuthApp, this.appId); - } - - public async updateOAuthApp(oAuthApp: IOAuthAppParams, id: string): Promise { - return this.oauthAppsBridge.doUpdate(oAuthApp, id, this.appId); - } - - public async deleteOAuthApp(id: string): Promise { - return this.oauthAppsBridge.doDelete(id, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/OAuthAppsReader.ts b/packages/apps-engine/src/server/accessors/OAuthAppsReader.ts deleted file mode 100644 index 5d82957274fe1..0000000000000 --- a/packages/apps-engine/src/server/accessors/OAuthAppsReader.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { IOAuthApp } from '../../definition/accessors/IOAuthApp'; -import type { IOAuthAppsReader } from '../../definition/accessors/IOAuthAppsReader'; -import type { OAuthAppsBridge } from '../bridges/OAuthAppsBridge'; - -export class OAuthAppsReader implements IOAuthAppsReader { - constructor( - private readonly oauthAppsBridge: OAuthAppsBridge, - private readonly appId: string, - ) {} - - public async getOAuthAppById(id: string): Promise { - return this.oauthAppsBridge.doGetByid(id, this.appId); - } - - public async getOAuthAppByName(name: string): Promise> { - return this.oauthAppsBridge.doGetByName(name, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/OutboundCommunicationProviderExtend.ts b/packages/apps-engine/src/server/accessors/OutboundCommunicationProviderExtend.ts deleted file mode 100644 index db713a6cdf3e1..0000000000000 --- a/packages/apps-engine/src/server/accessors/OutboundCommunicationProviderExtend.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { IOutboundCommunicationProviderExtend } from '../../definition/accessors/IOutboundCommunicationProviderExtend'; -import type { IOutboundPhoneMessageProvider, IOutboundEmailMessageProvider } from '../../definition/outboundCommunication'; -import type { AppOutboundCommunicationProviderManager } from '../managers/AppOutboundCommunicationProviderManager'; - -export class OutboundMessageProviderExtend implements IOutboundCommunicationProviderExtend { - constructor( - private readonly manager: AppOutboundCommunicationProviderManager, - private readonly appId: string, - ) {} - - public registerPhoneProvider(provider: IOutboundPhoneMessageProvider): Promise { - return Promise.resolve(this.manager.addProvider(this.appId, provider)); - } - - public registerEmailProvider(provider: IOutboundEmailMessageProvider): Promise { - return Promise.resolve(this.manager.addProvider(this.appId, provider)); - } -} diff --git a/packages/apps-engine/src/server/accessors/Persistence.ts b/packages/apps-engine/src/server/accessors/Persistence.ts deleted file mode 100644 index cd783e891c34b..0000000000000 --- a/packages/apps-engine/src/server/accessors/Persistence.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { IPersistence } from '../../definition/accessors'; -import type { RocketChatAssociationRecord } from '../../definition/metadata'; -import type { PersistenceBridge } from '../bridges/PersistenceBridge'; - -export class Persistence implements IPersistence { - constructor( - private persistBridge: PersistenceBridge, - private appId: string, - ) {} - - public create(data: object): Promise { - return this.persistBridge.doCreate(data, this.appId); - } - - public createWithAssociation(data: object, association: RocketChatAssociationRecord): Promise { - return this.persistBridge.doCreateWithAssociations(data, new Array(association), this.appId); - } - - public createWithAssociations(data: object, associations: Array): Promise { - return this.persistBridge.doCreateWithAssociations(data, associations, this.appId); - } - - public update(id: string, data: object, upsert = false): Promise { - return this.persistBridge.doUpdate(id, data, upsert, this.appId); - } - - public updateByAssociation(association: RocketChatAssociationRecord, data: object, upsert = false): Promise { - return this.persistBridge.doUpdateByAssociations(new Array(association), data, upsert, this.appId); - } - - public updateByAssociations(associations: Array, data: object, upsert = false): Promise { - return this.persistBridge.doUpdateByAssociations(associations, data, upsert, this.appId); - } - - public remove(id: string): Promise { - return this.persistBridge.doRemove(id, this.appId); - } - - public removeByAssociation(association: RocketChatAssociationRecord): Promise> { - return this.persistBridge.doRemoveByAssociations(new Array(association), this.appId); - } - - public removeByAssociations(associations: Array): Promise> { - return this.persistBridge.doRemoveByAssociations(associations, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/PersistenceRead.ts b/packages/apps-engine/src/server/accessors/PersistenceRead.ts deleted file mode 100644 index c9f4f39d96596..0000000000000 --- a/packages/apps-engine/src/server/accessors/PersistenceRead.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { IPersistenceRead } from '../../definition/accessors'; -import type { RocketChatAssociationRecord } from '../../definition/metadata'; -import type { PersistenceBridge } from '../bridges'; - -export class PersistenceRead implements IPersistenceRead { - constructor( - private persistBridge: PersistenceBridge, - private appId: string, - ) {} - - public read(id: string): Promise { - return this.persistBridge.doReadById(id, this.appId); - } - - public readByAssociation(association: RocketChatAssociationRecord): Promise> { - return this.persistBridge.doReadByAssociations(new Array(association), this.appId); - } - - public readByAssociations(associations: Array): Promise> { - return this.persistBridge.doReadByAssociations(associations, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/Reader.ts b/packages/apps-engine/src/server/accessors/Reader.ts deleted file mode 100644 index baa05b79ac331..0000000000000 --- a/packages/apps-engine/src/server/accessors/Reader.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { - ICloudWorkspaceRead, - IEnvironmentRead, - IExperimentalRead, - ILivechatRead, - IMessageRead, - INotifier, - IPersistenceRead, - IRead, - IRoomRead, - IUploadRead, - IUserRead, - IVideoConferenceRead, -} from '../../definition/accessors'; -import type { IContactRead } from '../../definition/accessors/IContactRead'; -import type { IOAuthAppsReader } from '../../definition/accessors/IOAuthAppsReader'; -import type { IRoleRead } from '../../definition/accessors/IRoleRead'; -import type { IThreadRead } from '../../definition/accessors/IThreadRead'; - -export class Reader implements IRead { - constructor( - private env: IEnvironmentRead, - private message: IMessageRead, - private persist: IPersistenceRead, - private room: IRoomRead, - private user: IUserRead, - private noti: INotifier, - private livechat: ILivechatRead, - private upload: IUploadRead, - private cloud: ICloudWorkspaceRead, - private videoConf: IVideoConferenceRead, - private contactRead: IContactRead, - private oauthApps: IOAuthAppsReader, - private thread: IThreadRead, - private role: IRoleRead, - private experimental: IExperimentalRead, - ) {} - - public getEnvironmentReader(): IEnvironmentRead { - return this.env; - } - - public getThreadReader(): IThreadRead { - return this.thread; - } - - public getMessageReader(): IMessageRead { - return this.message; - } - - public getPersistenceReader(): IPersistenceRead { - return this.persist; - } - - public getRoomReader(): IRoomRead { - return this.room; - } - - public getUserReader(): IUserRead { - return this.user; - } - - public getNotifier(): INotifier { - return this.noti; - } - - public getLivechatReader(): ILivechatRead { - return this.livechat; - } - - public getUploadReader(): IUploadRead { - return this.upload; - } - - public getCloudWorkspaceReader(): ICloudWorkspaceRead { - return this.cloud; - } - - public getVideoConferenceReader(): IVideoConferenceRead { - return this.videoConf; - } - - public getOAuthAppsReader(): IOAuthAppsReader { - return this.oauthApps; - } - - public getRoleReader(): IRoleRead { - return this.role; - } - - public getContactReader(): IContactRead { - return this.contactRead; - } - - public getExperimentalReader(): IExperimentalRead { - return this.experimental; - } -} diff --git a/packages/apps-engine/src/server/accessors/RoleRead.ts b/packages/apps-engine/src/server/accessors/RoleRead.ts deleted file mode 100644 index 209b22d6c0f68..0000000000000 --- a/packages/apps-engine/src/server/accessors/RoleRead.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { IRoleRead } from '../../definition/accessors/IRoleRead'; -import type { IRole } from '../../definition/roles'; -import type { RoleBridge } from '../bridges'; - -export class RoleRead implements IRoleRead { - constructor( - private roleBridge: RoleBridge, - private appId: string, - ) {} - - public getOneByIdOrName(idOrName: IRole['id'] | IRole['name']): Promise { - return this.roleBridge.doGetOneByIdOrName(idOrName, this.appId); - } - - public getCustomRoles(): Promise> { - return this.roleBridge.doGetCustomRoles(this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/RoomBuilder.ts b/packages/apps-engine/src/server/accessors/RoomBuilder.ts deleted file mode 100644 index abe36eb91d8ad..0000000000000 --- a/packages/apps-engine/src/server/accessors/RoomBuilder.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type { IRoomBuilder } from '../../definition/accessors'; -import { RocketChatAssociationModel } from '../../definition/metadata'; -import type { IRoom, RoomType } from '../../definition/rooms'; -import type { IUser } from '../../definition/users'; - -export class RoomBuilder implements IRoomBuilder { - public kind: RocketChatAssociationModel.ROOM | RocketChatAssociationModel.DISCUSSION; - - protected room: IRoom; - - private members: Array; - - constructor(data?: Partial) { - this.kind = RocketChatAssociationModel.ROOM; - this.room = (data || { customFields: {} }) as IRoom; - this.members = []; - } - - public setData(data: Partial): IRoomBuilder { - delete data.id; - this.room = data as IRoom; - - return this; - } - - public setDisplayName(name: string): IRoomBuilder { - this.room.displayName = name; - return this; - } - - public getDisplayName(): string { - return this.room.displayName; - } - - public setSlugifiedName(name: string): IRoomBuilder { - this.room.slugifiedName = name; - return this; - } - - public getSlugifiedName(): string { - return this.room.slugifiedName; - } - - public setType(type: RoomType): IRoomBuilder { - this.room.type = type; - return this; - } - - public getType(): RoomType { - return this.room.type; - } - - public setCreator(creator: IUser): IRoomBuilder { - this.room.creator = creator; - return this; - } - - public getCreator(): IUser { - return this.room.creator; - } - - /** - * @deprecated - */ - public addUsername(username: string): IRoomBuilder { - this.addMemberToBeAddedByUsername(username); - return this; - } - - /** - * @deprecated - */ - public setUsernames(usernames: Array): IRoomBuilder { - this.setMembersToBeAddedByUsernames(usernames); - return this; - } - - /** - * @deprecated - */ - public getUsernames(): Array { - const usernames = this.getMembersToBeAddedUsernames(); - if (usernames && usernames.length > 0) { - return usernames; - } - return this.room.usernames || []; - } - - public addMemberToBeAddedByUsername(username: string): IRoomBuilder { - this.members.push(username); - return this; - } - - public setMembersToBeAddedByUsernames(usernames: Array): IRoomBuilder { - this.members = usernames; - return this; - } - - public getMembersToBeAddedUsernames(): Array { - return this.members; - } - - public setDefault(isDefault: boolean): IRoomBuilder { - this.room.isDefault = isDefault; - return this; - } - - public getIsDefault(): boolean { - return this.room.isDefault; - } - - public setReadOnly(isReadOnly: boolean): IRoomBuilder { - this.room.isReadOnly = isReadOnly; - return this; - } - - public getIsReadOnly(): boolean { - return this.room.isReadOnly; - } - - public setDisplayingOfSystemMessages(displaySystemMessages: boolean): IRoomBuilder { - this.room.displaySystemMessages = displaySystemMessages; - return this; - } - - public getDisplayingOfSystemMessages(): boolean { - return this.room.displaySystemMessages; - } - - public addCustomField(key: string, value: object): IRoomBuilder { - if (typeof this.room.customFields !== 'object') { - this.room.customFields = {}; - } - - this.room.customFields[key] = value; - return this; - } - - public setCustomFields(fields: { [key: string]: object }): IRoomBuilder { - this.room.customFields = fields; - return this; - } - - public getCustomFields(): { [key: string]: object } { - return this.room.customFields; - } - - public getUserIds(): Array { - return this.room.userIds; - } - - public getRoom(): IRoom { - return this.room; - } -} diff --git a/packages/apps-engine/src/server/accessors/RoomExtender.ts b/packages/apps-engine/src/server/accessors/RoomExtender.ts deleted file mode 100644 index a83f3c9b28dea..0000000000000 --- a/packages/apps-engine/src/server/accessors/RoomExtender.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { IRoomExtender } from '../../definition/accessors'; -import { RocketChatAssociationModel } from '../../definition/metadata'; -import type { IRoom } from '../../definition/rooms'; -import type { IUser } from '../../definition/users'; -import { Utilities } from '../misc/Utilities'; - -export class RoomExtender implements IRoomExtender { - public kind: RocketChatAssociationModel.ROOM; - - private members: Array; - - constructor(private room: IRoom) { - this.kind = RocketChatAssociationModel.ROOM; - this.members = []; - } - - public addCustomField(key: string, value: any): IRoomExtender { - if (!this.room.customFields) { - this.room.customFields = {}; - } - - if (this.room.customFields[key]) { - throw new Error(`The room already contains a custom field by the key: ${key}`); - } - - if (key.includes('.')) { - throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`); - } - - this.room.customFields[key] = value; - - return this; - } - - public addMember(user: IUser): IRoomExtender { - if (this.members.find((u) => u.username === user.username)) { - throw new Error('The user is already in the room.'); - } - - this.members.push(user); - - return this; - } - - public getMembersBeingAdded(): Array { - return this.members; - } - - public getUsernamesOfMembersBeingAdded(): Array { - return this.members.map((u) => u.username); - } - - public getRoom(): IRoom { - return Utilities.deepClone(this.room); - } -} diff --git a/packages/apps-engine/src/server/accessors/RoomRead.ts b/packages/apps-engine/src/server/accessors/RoomRead.ts deleted file mode 100644 index 01e10cee8b950..0000000000000 --- a/packages/apps-engine/src/server/accessors/RoomRead.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { IRoomRead } from '../../definition/accessors'; -import type { IMessageRaw } from '../../definition/messages'; -import type { IRoom, IRoomRaw } from '../../definition/rooms'; -import type { IUser } from '../../definition/users'; -import type { RoomBridge } from '../bridges'; -import { type GetMessagesOptions, type GetRoomsFilters, type GetRoomsOptions, GetMessagesSortableFields } from '../bridges/RoomBridge'; - -export class RoomRead implements IRoomRead { - constructor( - private roomBridge: RoomBridge, - private appId: string, - ) {} - - public getById(id: string): Promise { - return this.roomBridge.doGetById(id, this.appId); - } - - public getCreatorUserById(id: string): Promise { - return this.roomBridge.doGetCreatorById(id, this.appId); - } - - public getByName(name: string): Promise { - return this.roomBridge.doGetByName(name, this.appId); - } - - public getCreatorUserByName(name: string): Promise { - return this.roomBridge.doGetCreatorByName(name, this.appId); - } - - public getMessages(roomId: string, options: Partial = {}): Promise { - if (typeof options.limit !== 'undefined' && (!Number.isFinite(options.limit) || options.limit > 100)) { - throw new Error(`Invalid limit provided. Expected number <= 100, got ${options.limit}`); - } - - options.limit ??= 100; - options.showThreadMessages ??= true; - - if (options.sort) { - this.validateSort(options.sort); - } - - return this.roomBridge.doGetMessages(roomId, options as GetMessagesOptions, this.appId); - } - - public getMembers(roomId: string): Promise> { - return this.roomBridge.doGetMembers(roomId, this.appId); - } - - public getAllRooms(filters: GetRoomsFilters = {}, { limit = 100, skip = 0 }: GetRoomsOptions = {}): Promise | undefined> { - if (!Number.isFinite(limit) || limit <= 0 || limit > 100) { - throw new Error(`Invalid limit provided. Expected number between 1 and 100, got ${limit}`); - } - - if (!Number.isFinite(skip) || skip < 0) { - throw new Error(`Invalid skip provided. Expected number >= 0, got ${skip}`); - } - - return this.roomBridge.doGetAllRooms( - filters, - { limit, skip }, - this.appId, - ); - } - - public getDirectByUsernames(usernames: Array): Promise { - return this.roomBridge.doGetDirectByUsernames(usernames, this.appId); - } - - public getModerators(roomId: string): Promise> { - return this.roomBridge.doGetModerators(roomId, this.appId); - } - - public getOwners(roomId: string): Promise> { - return this.roomBridge.doGetOwners(roomId, this.appId); - } - - public getLeaders(roomId: string): Promise> { - return this.roomBridge.doGetLeaders(roomId, this.appId); - } - - public async getUnreadByUser(roomId: string, uid: string, options: Partial = {}): Promise { - const { limit = 100, sort = { createdAt: 'asc' }, skip = 0, showThreadMessages = true } = options; - - if (typeof roomId !== 'string' || roomId.trim().length === 0) { - throw new Error('Invalid roomId: must be a non-empty string'); - } - - if (!Number.isFinite(limit) || limit <= 0 || limit > 100) { - throw new Error(`Invalid limit provided. Expected number between 1 and 100, got ${limit}`); - } - - this.validateSort(sort); - - const completeOptions: GetMessagesOptions = { limit, sort, skip, showThreadMessages }; - - return this.roomBridge.doGetUnreadByUser(roomId, uid, completeOptions, this.appId); - } - - public getUserUnreadMessageCount(roomId: string, uid: string): Promise { - return this.roomBridge.doGetUserUnreadMessageCount(roomId, uid, this.appId); - } - - // If there are any invalid fields or values, throw - private validateSort(sort: Record) { - Object.entries(sort).forEach(([key, value]) => { - if (!GetMessagesSortableFields.includes(key as (typeof GetMessagesSortableFields)[number])) { - throw new Error(`Invalid key "${key}" used in sort. Available keys for sorting are ${GetMessagesSortableFields.join(', ')}`); - } - - if (value !== 'asc' && value !== 'desc') { - throw new Error(`Invalid sort direction for field "${key}". Expected "asc" or "desc", got ${value}`); - } - }); - } -} diff --git a/packages/apps-engine/src/server/accessors/SchedulerExtend.ts b/packages/apps-engine/src/server/accessors/SchedulerExtend.ts deleted file mode 100644 index 29e2c1aee5704..0000000000000 --- a/packages/apps-engine/src/server/accessors/SchedulerExtend.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ISchedulerExtend } from '../../definition/accessors'; -import type { IProcessor } from '../../definition/scheduler'; -import type { AppSchedulerManager } from '../managers/AppSchedulerManager'; - -export class SchedulerExtend implements ISchedulerExtend { - constructor( - private readonly manager: AppSchedulerManager, - private readonly appId: string, - ) {} - - public async registerProcessors(processors: Array = []): Promise> { - return this.manager.registerProcessors(processors, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/SchedulerModify.ts b/packages/apps-engine/src/server/accessors/SchedulerModify.ts deleted file mode 100644 index a687889759ce9..0000000000000 --- a/packages/apps-engine/src/server/accessors/SchedulerModify.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { ISchedulerModify } from '../../definition/accessors'; -import type { IOnetimeSchedule, IRecurringSchedule } from '../../definition/scheduler'; -import type { SchedulerBridge } from '../bridges'; - -function createProcessorId(jobId: string, appId: string): string { - return jobId.includes(`_${appId}`) ? jobId : `${jobId}_${appId}`; -} - -export class SchedulerModify implements ISchedulerModify { - constructor( - private readonly bridge: SchedulerBridge, - private readonly appId: string, - ) {} - - public async scheduleOnce(job: IOnetimeSchedule): Promise { - return this.bridge.doScheduleOnce({ ...job, id: createProcessorId(job.id, this.appId) }, this.appId); - } - - public async scheduleRecurring(job: IRecurringSchedule): Promise { - return this.bridge.doScheduleRecurring({ ...job, id: createProcessorId(job.id, this.appId) }, this.appId); - } - - public async cancelJob(jobId: string): Promise { - return this.bridge.doCancelJob(createProcessorId(jobId, this.appId), this.appId); - } - - public async cancelAllJobs(): Promise { - return this.bridge.doCancelAllJobs(this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/ServerSettingRead.ts b/packages/apps-engine/src/server/accessors/ServerSettingRead.ts deleted file mode 100644 index 1fbc39c06ce35..0000000000000 --- a/packages/apps-engine/src/server/accessors/ServerSettingRead.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { IServerSettingRead } from '../../definition/accessors'; -import type { ISetting } from '../../definition/settings'; -import type { ServerSettingBridge } from '../bridges/ServerSettingBridge'; - -export class ServerSettingRead implements IServerSettingRead { - constructor( - private readonly settingBridge: ServerSettingBridge, - private readonly appId: string, - ) {} - - public getOneById(id: string): Promise { - return this.settingBridge.doGetOneById(id, this.appId); - } - - public async getValueById(id: string): Promise { - const set = await this.settingBridge.doGetOneById(id, this.appId); - - if (typeof set === 'undefined') { - throw new Error(`No Server Setting found, or it is unaccessible, by the id of "${id}".`); - } - - if (set.value === undefined || set.value === null) { - return set.packageValue; - } - - return set.value; - } - - public getAll(): Promise> { - throw new Error('Method not implemented.'); - // return this.settingBridge.getAll(this.appId); - } - - public isReadableById(id: string): Promise { - return this.settingBridge.doIsReadableById(id, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/ServerSettingUpdater.ts b/packages/apps-engine/src/server/accessors/ServerSettingUpdater.ts deleted file mode 100644 index 87dfff693c3f2..0000000000000 --- a/packages/apps-engine/src/server/accessors/ServerSettingUpdater.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { IServerSettingUpdater } from '../../definition/accessors'; -import type { ISetting } from '../../definition/settings'; -import type { AppBridges } from '../bridges'; - -export class ServerSettingUpdater implements IServerSettingUpdater { - constructor( - private readonly bridges: AppBridges, - private readonly appId: string, - ) {} - - public async updateOne(setting: ISetting): Promise { - await this.bridges.getServerSettingBridge().doUpdateOne(setting, this.appId); - } - - public async incrementValue(id: ISetting['id'], value = 1): Promise { - await this.bridges.getServerSettingBridge().doIncrementValue(id, value, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/ServerSettingsModify.ts b/packages/apps-engine/src/server/accessors/ServerSettingsModify.ts deleted file mode 100644 index 8cdc908009b73..0000000000000 --- a/packages/apps-engine/src/server/accessors/ServerSettingsModify.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { IServerSettingsModify } from '../../definition/accessors'; -import type { ISetting } from '../../definition/settings'; -import type { ServerSettingBridge } from '../bridges/ServerSettingBridge'; - -export class ServerSettingsModify implements IServerSettingsModify { - constructor( - private readonly bridge: ServerSettingBridge, - private readonly appId: string, - ) {} - - public async hideGroup(name: string): Promise { - await this.bridge.doHideGroup(name, this.appId); - } - - public async hideSetting(id: string): Promise { - await this.bridge.doHideSetting(id, this.appId); - } - - public async modifySetting(setting: ISetting): Promise { - await this.bridge.doUpdateOne(setting, this.appId); - } - - public async incrementValue(id: ISetting['id'], value = 1): Promise { - await this.bridge.doIncrementValue(id, value, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/SettingRead.ts b/packages/apps-engine/src/server/accessors/SettingRead.ts deleted file mode 100644 index 7b701c249a49c..0000000000000 --- a/packages/apps-engine/src/server/accessors/SettingRead.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { ISettingRead } from '../../definition/accessors'; -import type { ISetting } from '../../definition/settings'; -import type { ProxiedApp } from '../ProxiedApp'; - -export class SettingRead implements ISettingRead { - constructor(private readonly app: ProxiedApp) {} - - public getById(id: string): Promise { - return Promise.resolve(this.app.getStorageItem().settings[id]); - } - - public async getValueById(id: string): Promise { - const set = await this.getById(id); - - if (typeof set === 'undefined') { - throw new Error(`Setting "${id}" does not exist.`); - } - - if (set.value === undefined || set.value === null) { - return set.packageValue; - } - - return set.value; - } -} diff --git a/packages/apps-engine/src/server/accessors/SettingUpdater.ts b/packages/apps-engine/src/server/accessors/SettingUpdater.ts deleted file mode 100644 index 4dd7850c2a167..0000000000000 --- a/packages/apps-engine/src/server/accessors/SettingUpdater.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { ISettingUpdater } from '../../definition/accessors/ISettingUpdater'; -import type { ISetting } from '../../definition/settings'; -import type { ProxiedApp } from '../ProxiedApp'; -import type { AppSettingsManager } from '../managers'; - -/** - * Implementation of ISettingUpdater that provides methods to update app settings. - */ -export class SettingUpdater implements ISettingUpdater { - constructor( - private readonly app: ProxiedApp, - private readonly manager: AppSettingsManager, - ) {} - - /** - * Updates a single setting value - * @param id The setting ID to update - * @param value The new value to set - * @returns Promise that resolves when the update is complete - * @throws Error if the setting doesn't exist - */ - public async updateValue(id: ISetting['id'], value: ISetting['value']): Promise { - const appId = this.app.getID(); - const storageItem = this.app.getStorageItem(); - - if (!storageItem.settings?.[id]) { - throw new Error(`Setting "${id}" not found for app ${appId}`); - } - - const setting = this.manager.getAppSetting(appId, id); - - this.manager.updateAppSetting(appId, { - ...setting, - updatedAt: new Date(), - value, - }); - } - - /** - * Updates the values for a multi-value setting by overwriting them - * @param id The setting ID to update - * @param values The new values to set - * @returns Promise that resolves when the update is complete - * @throws Error if the setting doesn't exist - */ - public async updateSelectOptions(id: ISetting['id'], values: ISetting['values']): Promise { - const appId = this.app.getID(); - const storageItem = this.app.getStorageItem(); - - if (!storageItem.settings?.[id]) { - throw new Error(`Setting "${id}" not found for app ${appId}`); - } - - const setting = this.manager.getAppSetting(appId, id); - - // TODO: This operation completely overwrites existing values - // which could lead to loss of selected values. Consider: - // Adding warning logs when selected value will be removed - - this.manager.updateAppSetting(appId, { - ...setting, - updatedAt: new Date(), - values, // Overwrite the values instead of merging - }); - } -} diff --git a/packages/apps-engine/src/server/accessors/SettingsExtend.ts b/packages/apps-engine/src/server/accessors/SettingsExtend.ts deleted file mode 100644 index 654c52f385fbf..0000000000000 --- a/packages/apps-engine/src/server/accessors/SettingsExtend.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ISettingsExtend } from '../../definition/accessors'; -import type { ISetting } from '../../definition/settings'; -import type { ProxiedApp } from '../ProxiedApp'; - -export class SettingsExtend implements ISettingsExtend { - constructor(private readonly app: ProxiedApp) {} - - public async provideSetting(setting: ISetting): Promise { - if (this.app.getStorageItem().settings[setting.id]) { - // :see_no_evil: - const old = await Promise.resolve(this.app.getStorageItem().settings[setting.id]); - - setting.createdAt = old.createdAt; - setting.updatedAt = new Date(); - setting.value = old.value; - - this.app.getStorageItem().settings[setting.id] = setting; - - return; - } - - setting.createdAt = new Date(); - setting.updatedAt = new Date(); - this.app.getStorageItem().settings[setting.id] = setting; - } -} diff --git a/packages/apps-engine/src/server/accessors/SlashCommandsExtend.ts b/packages/apps-engine/src/server/accessors/SlashCommandsExtend.ts deleted file mode 100644 index e540fdbee5f1c..0000000000000 --- a/packages/apps-engine/src/server/accessors/SlashCommandsExtend.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ISlashCommandsExtend } from '../../definition/accessors'; -import type { ISlashCommand } from '../../definition/slashcommands'; -import type { AppSlashCommandManager } from '../managers/AppSlashCommandManager'; - -export class SlashCommandsExtend implements ISlashCommandsExtend { - constructor( - private readonly manager: AppSlashCommandManager, - private readonly appId: string, - ) {} - - public async provideSlashCommand(slashCommand: ISlashCommand): Promise { - await this.manager.addCommand(this.appId, slashCommand); - } -} diff --git a/packages/apps-engine/src/server/accessors/SlashCommandsModify.ts b/packages/apps-engine/src/server/accessors/SlashCommandsModify.ts deleted file mode 100644 index f03dba9d13690..0000000000000 --- a/packages/apps-engine/src/server/accessors/SlashCommandsModify.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { ISlashCommandsModify } from '../../definition/accessors'; -import type { ISlashCommand } from '../../definition/slashcommands'; -import type { AppSlashCommandManager } from '../managers'; - -export class SlashCommandsModify implements ISlashCommandsModify { - constructor( - private readonly manager: AppSlashCommandManager, - private readonly appId: string, - ) {} - - public modifySlashCommand(slashCommand: ISlashCommand): Promise { - return Promise.resolve(this.manager.modifyCommand(this.appId, slashCommand)); - } - - public disableSlashCommand(command: string): Promise { - return Promise.resolve(this.manager.disableCommand(this.appId, command)); - } - - public enableSlashCommand(command: string): Promise { - return Promise.resolve(this.manager.enableCommand(this.appId, command)); - } -} diff --git a/packages/apps-engine/src/server/accessors/ThreadRead.ts b/packages/apps-engine/src/server/accessors/ThreadRead.ts deleted file mode 100644 index c979bf9a50d13..0000000000000 --- a/packages/apps-engine/src/server/accessors/ThreadRead.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IThreadRead } from '../../definition/accessors/IThreadRead'; -import type { IMessage } from '../../definition/messages'; -import type { ThreadBridge } from '../bridges/ThreadBridge'; - -export class ThreadRead implements IThreadRead { - constructor( - private threadBridge: ThreadBridge, - private appId: string, - ) {} - - public getThreadById(id: string): Promise> { - return this.threadBridge.doGetById(id, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/UIController.ts b/packages/apps-engine/src/server/accessors/UIController.ts deleted file mode 100644 index 1c3ad58e1d293..0000000000000 --- a/packages/apps-engine/src/server/accessors/UIController.ts +++ /dev/null @@ -1,118 +0,0 @@ -import type { IUIController } from '../../definition/accessors'; -import type { IUIKitErrorInteractionParam, IUIKitInteractionParam, IUIKitSurfaceViewParam } from '../../definition/accessors/IUIController'; -import { UIKitInteractionType, UIKitSurfaceType } from '../../definition/uikit'; -import { - formatContextualBarInteraction, - formatErrorInteraction, - formatModalInteraction, -} from '../../definition/uikit/UIKitInteractionPayloadFormatter'; -import type { IUIKitContextualBarViewParam, IUIKitModalViewParam } from '../../definition/uikit/UIKitInteractionResponder'; -import type { IUser } from '../../definition/users'; -import type { AppBridges, UiInteractionBridge } from '../bridges'; -import { UIHelper } from '../misc/UIHelper'; - -export class UIController implements IUIController { - private readonly uiInteractionBridge: UiInteractionBridge; - - constructor( - private readonly appId: string, - bridges: AppBridges, - ) { - this.uiInteractionBridge = bridges.getUiInteractionBridge(); - } - - /** - * @deprecated please prefer the `openSurfaceView` method - */ - public openModalView(view: IUIKitModalViewParam, context: IUIKitInteractionParam, user: IUser) { - return this.openModal(view, context, user); - } - - /** - * @deprecated please prefer the `updateSurfaceView` method - */ - public updateModalView(view: IUIKitModalViewParam, context: IUIKitInteractionParam, user: IUser) { - return this.openModal(view, context, user, true); - } - - /** - * @deprecated please prefer the `openSurfaceView` method - */ - public openContextualBarView(view: IUIKitContextualBarViewParam, context: IUIKitInteractionParam, user: IUser) { - return this.openContextualBar(view, context, user); - } - - /** - * @deprecated please prefer the `updateSurfaceView` method - */ - public updateContextualBarView(view: IUIKitContextualBarViewParam, context: IUIKitInteractionParam, user: IUser) { - return this.openContextualBar(view, context, user, true); - } - - public openSurfaceView(view: IUIKitSurfaceViewParam, context: IUIKitInteractionParam, user: IUser) { - const blocks = UIHelper.assignIds(view.blocks, this.appId); - const viewWithIds = { ...view, blocks }; - - switch (view.type) { - case UIKitSurfaceType.CONTEXTUAL_BAR: - return this.openContextualBar(viewWithIds, context, user); - case UIKitSurfaceType.MODAL: - return this.openModal(viewWithIds, context, user); - } - } - - public updateSurfaceView(view: IUIKitSurfaceViewParam, context: IUIKitInteractionParam, user: IUser) { - const blocks = UIHelper.assignIds(view.blocks, this.appId); - const viewWithIds = { ...view, blocks }; - - switch (view.type) { - case UIKitSurfaceType.CONTEXTUAL_BAR: - return this.openContextualBar(viewWithIds, context, user, true); - case UIKitSurfaceType.MODAL: - return this.openModal(viewWithIds, context, user, true); - } - } - - public setViewError(errorInteraction: IUIKitErrorInteractionParam, context: IUIKitInteractionParam, user: IUser) { - const interactionContext = { - ...context, - type: UIKitInteractionType.ERRORS, - appId: this.appId, - }; - - return this.uiInteractionBridge.doNotifyUser(user, formatErrorInteraction(errorInteraction, interactionContext), this.appId); - } - - private openContextualBar( - view: IUIKitContextualBarViewParam, - context: IUIKitInteractionParam, - user: IUser, - isUpdate = false, - ): Promise { - let type = UIKitInteractionType.CONTEXTUAL_BAR_OPEN; - if (isUpdate) { - type = UIKitInteractionType.CONTEXTUAL_BAR_UPDATE; - } - const interactionContext = { - ...context, - type, - appId: this.appId, - }; - - return this.uiInteractionBridge.doNotifyUser(user, formatContextualBarInteraction(view, interactionContext), this.appId); - } - - private openModal(view: IUIKitModalViewParam, context: IUIKitInteractionParam, user: IUser, isUpdate = false): Promise { - let type = UIKitInteractionType.MODAL_OPEN; - if (isUpdate) { - type = UIKitInteractionType.MODAL_UPDATE; - } - const interactionContext = { - ...context, - type, - appId: this.appId, - }; - - return this.uiInteractionBridge.doNotifyUser(user, formatModalInteraction(view, interactionContext), this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/UIExtend.ts b/packages/apps-engine/src/server/accessors/UIExtend.ts deleted file mode 100644 index 81062f2cc71f1..0000000000000 --- a/packages/apps-engine/src/server/accessors/UIExtend.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IUIExtend } from '../../definition/accessors'; -import type { IUIActionButtonDescriptor } from '../../definition/ui'; -import type { UIActionButtonManager } from '../managers/UIActionButtonManager'; - -export class UIExtend implements IUIExtend { - constructor( - private readonly manager: UIActionButtonManager, - private readonly appId: string, - ) {} - - public registerButton(button: IUIActionButtonDescriptor): void { - this.manager.registerActionButton(this.appId, button); - } -} diff --git a/packages/apps-engine/src/server/accessors/UploadCreator.ts b/packages/apps-engine/src/server/accessors/UploadCreator.ts deleted file mode 100644 index dbacbdc30354f..0000000000000 --- a/packages/apps-engine/src/server/accessors/UploadCreator.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { IUploadCreator } from '../../definition/accessors'; -import type { IUpload } from '../../definition/uploads'; -import type { IUploadDescriptor } from '../../definition/uploads/IUploadDescriptor'; -import type { IUploadDetails } from '../../definition/uploads/IUploadDetails'; -import type { AppBridges } from '../bridges'; - -export class UploadCreator implements IUploadCreator { - constructor( - private readonly bridges: AppBridges, - private readonly appId: string, - ) {} - - public async uploadBuffer(buffer: Buffer, descriptor: IUploadDescriptor): Promise { - if (!descriptor.hasOwnProperty('user') && !descriptor.visitorToken) { - descriptor.user = await this.bridges.getUserBridge().doGetAppUser(this.appId); - } - - const details = { - name: descriptor.filename, - size: buffer.length, - rid: descriptor.room.id, - userId: descriptor.user?.id, - visitorToken: descriptor.visitorToken, - } as IUploadDetails; - - return this.bridges.getUploadBridge().doCreateUpload(details, buffer, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/UploadRead.ts b/packages/apps-engine/src/server/accessors/UploadRead.ts deleted file mode 100644 index 84ab8c47b84a1..0000000000000 --- a/packages/apps-engine/src/server/accessors/UploadRead.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { IUploadRead } from '../../definition/accessors'; -import type { IUpload } from '../../definition/uploads'; -import type { UploadBridge } from '../bridges/UploadBridge'; - -export class UploadRead implements IUploadRead { - constructor( - private readonly uploadBridge: UploadBridge, - private readonly appId: string, - ) {} - - public getById(id: string): Promise { - return this.uploadBridge.doGetById(id, this.appId); - } - - public getBuffer(upload: IUpload): Promise { - return this.uploadBridge.doGetBuffer(upload, this.appId); - } - - public async getBufferById(id: string): Promise { - const upload = await this.uploadBridge.doGetById(id, this.appId); - - return this.uploadBridge.doGetBuffer(upload, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/UserBuilder.ts b/packages/apps-engine/src/server/accessors/UserBuilder.ts deleted file mode 100644 index f18482663f414..0000000000000 --- a/packages/apps-engine/src/server/accessors/UserBuilder.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { IUserBuilder } from '../../definition/accessors'; -import { RocketChatAssociationModel } from '../../definition/metadata'; -import type { IUser, IUserEmail } from '../../definition/users'; -import type { IUserSettings } from '../../definition/users/IUserSettings'; - -export class UserBuilder implements IUserBuilder { - public kind: RocketChatAssociationModel.USER; - - private user: Partial; - - constructor(user?: Partial) { - this.kind = RocketChatAssociationModel.USER; - this.user = user || ({} as Partial); - } - - public setData(data: Partial): IUserBuilder { - delete data.id; - this.user = data; - - return this; - } - - public setEmails(emails: Array): IUserBuilder { - this.user.emails = emails; - return this; - } - - public getEmails(): Array { - return this.user.emails; - } - - public setDisplayName(name: string): IUserBuilder { - this.user.name = name; - return this; - } - - public getDisplayName(): string { - return this.user.name; - } - - public setUsername(username: string): IUserBuilder { - this.user.username = username; - return this; - } - - public getUsername(): string { - return this.user.username; - } - - public setRoles(roles: Array): IUserBuilder { - this.user.roles = roles; - return this; - } - - public getRoles(): Array { - return this.user.roles; - } - - public getSettings(): Partial { - return this.user.settings; - } - - public getUser(): Partial { - if (!this.user.username) { - throw new Error('The "username" property is required.'); - } - - if (!this.user.name) { - throw new Error('The "name" property is required.'); - } - - return this.user; - } -} diff --git a/packages/apps-engine/src/server/accessors/UserRead.ts b/packages/apps-engine/src/server/accessors/UserRead.ts deleted file mode 100644 index a147a51b3bfaa..0000000000000 --- a/packages/apps-engine/src/server/accessors/UserRead.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { IUserRead } from '../../definition/accessors'; -import type { IUser } from '../../definition/users'; -import type { UserBridge } from '../bridges/UserBridge'; - -export class UserRead implements IUserRead { - constructor( - private userBridge: UserBridge, - private appId: string, - ) {} - - public getById(id: string): Promise { - return this.userBridge.doGetById(id, this.appId); - } - - public getByUsername(username: string): Promise { - return this.userBridge.doGetByUsername(username, this.appId); - } - - public getAppUser(appId: string = this.appId): Promise { - return this.userBridge.doGetAppUser(appId); - } - - public getUserUnreadMessageCount(uid: string): Promise { - return this.userBridge.doGetUserUnreadMessageCount(uid, this.appId); - } - - public getUserRoomIds(userId: string): Promise { - return this.userBridge.doGetUserRoomIds(userId, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/UserUpdater.ts b/packages/apps-engine/src/server/accessors/UserUpdater.ts deleted file mode 100644 index 423a19e0f0044..0000000000000 --- a/packages/apps-engine/src/server/accessors/UserUpdater.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { IUserUpdater } from '../../definition/accessors/IUserUpdater'; -import type { UserStatusConnection } from '../../definition/users'; -import type { IUser } from '../../definition/users/IUser'; -import type { AppBridges } from '../bridges'; - -export class UserUpdater implements IUserUpdater { - constructor( - private readonly bridges: AppBridges, - private readonly appId: string, - ) {} - - public async updateStatusText(user: IUser, statusText: IUser['statusText']) { - return this.bridges.getUserBridge().doUpdate(user, { statusText }, this.appId); - } - - public async updateStatus(user: IUser, statusText: IUser['statusText'], status: UserStatusConnection) { - return this.bridges.getUserBridge().doUpdate(user, { statusText, status }, this.appId); - } - - public async updateBio(user: IUser, bio: IUser['bio']) { - return this.bridges.getUserBridge().doUpdate(user, { bio }, this.appId); - } - - public async updateCustomFields(user: IUser, customFields: IUser['customFields']) { - return this.bridges.getUserBridge().doUpdate(user, { customFields }, this.appId); - } - - public async deactivate(userId: IUser['id'], confirmRelinquish: boolean) { - return this.bridges.getUserBridge().doDeactivate(userId, confirmRelinquish, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/VideoConfProviderExtend.ts b/packages/apps-engine/src/server/accessors/VideoConfProviderExtend.ts deleted file mode 100644 index da9688e8104ea..0000000000000 --- a/packages/apps-engine/src/server/accessors/VideoConfProviderExtend.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IVideoConfProvidersExtend } from '../../definition/accessors'; -import type { IVideoConfProvider } from '../../definition/videoConfProviders'; -import type { AppVideoConfProviderManager } from '../managers/AppVideoConfProviderManager'; - -export class VideoConfProviderExtend implements IVideoConfProvidersExtend { - constructor( - private readonly manager: AppVideoConfProviderManager, - private readonly appId: string, - ) {} - - public provideVideoConfProvider(provider: IVideoConfProvider): Promise { - return Promise.resolve(this.manager.addProvider(this.appId, provider)); - } -} diff --git a/packages/apps-engine/src/server/accessors/VideoConferenceBuilder.ts b/packages/apps-engine/src/server/accessors/VideoConferenceBuilder.ts deleted file mode 100644 index d77254e1bcfdb..0000000000000 --- a/packages/apps-engine/src/server/accessors/VideoConferenceBuilder.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { IVideoConferenceBuilder } from '../../definition/accessors'; -import { RocketChatAssociationModel } from '../../definition/metadata'; -import type { AppVideoConference } from '../../definition/videoConferences'; - -export class VideoConferenceBuilder implements IVideoConferenceBuilder { - public kind: RocketChatAssociationModel.VIDEO_CONFERENCE = RocketChatAssociationModel.VIDEO_CONFERENCE; - - protected call: AppVideoConference; - - constructor(data?: Partial) { - this.call = (data || {}) as AppVideoConference; - } - - public setData(data: Partial): IVideoConferenceBuilder { - this.call = { - rid: data.rid, - createdBy: data.createdBy, - providerName: data.providerName, - title: data.title, - discussionRid: data.discussionRid, - }; - - return this; - } - - public setRoomId(rid: string): IVideoConferenceBuilder { - this.call.rid = rid; - return this; - } - - public getRoomId(): string { - return this.call.rid; - } - - public setCreatedBy(userId: string): IVideoConferenceBuilder { - this.call.createdBy = userId; - return this; - } - - public getCreatedBy(): string { - return this.call.createdBy; - } - - public setProviderName(userId: string): IVideoConferenceBuilder { - this.call.providerName = userId; - return this; - } - - public getProviderName(): string { - return this.call.providerName; - } - - public setProviderData(data: Record | undefined): IVideoConferenceBuilder { - this.call.providerData = data; - return this; - } - - public getProviderData(): Record | undefined { - return this.call.providerData; - } - - public setTitle(userId: string): IVideoConferenceBuilder { - this.call.title = userId; - return this; - } - - public getTitle(): string { - return this.call.title; - } - - public setDiscussionRid(rid: AppVideoConference['discussionRid']): IVideoConferenceBuilder { - this.call.discussionRid = rid; - return this; - } - - public getDiscussionRid(): AppVideoConference['discussionRid'] { - return this.call.discussionRid; - } - - public getVideoConference(): AppVideoConference { - return this.call; - } -} diff --git a/packages/apps-engine/src/server/accessors/VideoConferenceExtend.ts b/packages/apps-engine/src/server/accessors/VideoConferenceExtend.ts deleted file mode 100644 index b95d0275527dc..0000000000000 --- a/packages/apps-engine/src/server/accessors/VideoConferenceExtend.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { IVideoConferenceExtender } from '../../definition/accessors'; -import { RocketChatAssociationModel } from '../../definition/metadata'; -import type { IVideoConferenceUser, VideoConference } from '../../definition/videoConferences'; -import type { VideoConferenceMember } from '../../definition/videoConferences/IVideoConference'; -import { Utilities } from '../misc/Utilities'; - -export class VideoConferenceExtender implements IVideoConferenceExtender { - public kind: RocketChatAssociationModel.VIDEO_CONFERENCE; - - constructor(private videoConference: VideoConference) { - this.kind = RocketChatAssociationModel.VIDEO_CONFERENCE; - } - - public setProviderData(value: Record): IVideoConferenceExtender { - this.videoConference.providerData = value; - - return this; - } - - public setStatus(value: VideoConference['status']): IVideoConferenceExtender { - this.videoConference.status = value; - - return this; - } - - public setEndedBy(value: IVideoConferenceUser['_id']): IVideoConferenceExtender { - this.videoConference.endedBy = { - _id: value, - // Name and username will be loaded automatically by the bridge - username: '', - name: '', - }; - - return this; - } - - public setEndedAt(value: VideoConference['endedAt']): IVideoConferenceExtender { - this.videoConference.endedAt = value; - - return this; - } - - public addUser(userId: VideoConferenceMember['_id'], ts?: VideoConferenceMember['ts']): IVideoConferenceExtender { - this.videoConference.users.push({ - _id: userId, - ts, - // Name and username will be loaded automatically by the bridge - username: '', - name: '', - }); - - return this; - } - - public setDiscussionRid(rid: VideoConference['discussionRid']): IVideoConferenceExtender { - this.videoConference.discussionRid = rid; - - return this; - } - - public getVideoConference(): VideoConference { - return Utilities.deepClone(this.videoConference); - } -} diff --git a/packages/apps-engine/src/server/accessors/VideoConferenceRead.ts b/packages/apps-engine/src/server/accessors/VideoConferenceRead.ts deleted file mode 100644 index 2db2c2ff9908b..0000000000000 --- a/packages/apps-engine/src/server/accessors/VideoConferenceRead.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IVideoConferenceRead } from '../../definition/accessors'; -import type { VideoConference } from '../../definition/videoConferences'; -import type { VideoConferenceBridge } from '../bridges'; - -export class VideoConferenceRead implements IVideoConferenceRead { - constructor( - private videoConfBridge: VideoConferenceBridge, - private appId: string, - ) {} - - public getById(id: string): Promise { - return this.videoConfBridge.doGetById(id, this.appId); - } -} diff --git a/packages/apps-engine/src/server/accessors/index.ts b/packages/apps-engine/src/server/accessors/index.ts deleted file mode 100644 index eb5cbdc0218c7..0000000000000 --- a/packages/apps-engine/src/server/accessors/index.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { ApiExtend } from './ApiExtend'; -import { AppAccessors } from './AppAccessors'; -import { ConfigurationExtend } from './ConfigurationExtend'; -import { ConfigurationModify } from './ConfigurationModify'; -import { EnvironmentRead } from './EnvironmentRead'; -import { EnvironmentWrite } from './EnvironmentWrite'; -import { EnvironmentalVariableRead } from './EnvironmentalVariableRead'; -import { ExternalComponentsExtend } from './ExternalComponentsExtend'; -import { Http } from './Http'; -import { HttpExtend } from './HttpExtend'; -import { LivechatRead } from './LivechatRead'; -import { MessageBuilder } from './MessageBuilder'; -import { MessageExtender } from './MessageExtender'; -import { MessageRead } from './MessageRead'; -import { ModerationModify } from './ModerationModify'; -import { Modify } from './Modify'; -import { ModifyCreator } from './ModifyCreator'; -import { ModifyExtender } from './ModifyExtender'; -import { ModifyUpdater } from './ModifyUpdater'; -import { Notifier } from './Notifier'; -import { OAuthAppsModify } from './OAuthAppsModify'; -import { OAuthAppsReader } from './OAuthAppsReader'; -import { OutboundMessageProviderExtend } from './OutboundCommunicationProviderExtend'; -import { Persistence } from './Persistence'; -import { PersistenceRead } from './PersistenceRead'; -import { Reader } from './Reader'; -import { RoleRead } from './RoleRead'; -import { RoomBuilder } from './RoomBuilder'; -import { RoomExtender } from './RoomExtender'; -import { RoomRead } from './RoomRead'; -import { SchedulerExtend } from './SchedulerExtend'; -import { SchedulerModify } from './SchedulerModify'; -import { ServerSettingRead } from './ServerSettingRead'; -import { ServerSettingUpdater } from './ServerSettingUpdater'; -import { ServerSettingsModify } from './ServerSettingsModify'; -import { SettingRead } from './SettingRead'; -import { SettingUpdater } from './SettingUpdater'; -import { SettingsExtend } from './SettingsExtend'; -import { SlashCommandsExtend } from './SlashCommandsExtend'; -import { SlashCommandsModify } from './SlashCommandsModify'; -import { UploadRead } from './UploadRead'; -import { UserBuilder } from './UserBuilder'; -import { UserRead } from './UserRead'; -import { VideoConfProviderExtend } from './VideoConfProviderExtend'; -import { VideoConferenceBuilder } from './VideoConferenceBuilder'; -import { VideoConferenceExtender } from './VideoConferenceExtend'; -import { VideoConferenceRead } from './VideoConferenceRead'; - -export { - ApiExtend, - AppAccessors, - ConfigurationExtend, - ConfigurationModify, - EnvironmentalVariableRead, - EnvironmentRead, - EnvironmentWrite, - ExternalComponentsExtend, - Http, - HttpExtend, - LivechatRead, - MessageBuilder, - MessageExtender, - MessageRead, - ModerationModify, - Modify, - ModifyCreator, - ModifyExtender, - ModifyUpdater, - Notifier, - Persistence, - PersistenceRead, - Reader, - RoleRead, - RoomBuilder, - RoomExtender, - RoomRead, - ServerSettingRead, - ServerSettingsModify, - ServerSettingUpdater, - SettingRead, - SettingsExtend, - SettingUpdater, - SlashCommandsExtend, - SlashCommandsModify, - UploadRead, - UserBuilder, - UserRead, - SchedulerExtend, - SchedulerModify, - VideoConferenceBuilder, - VideoConferenceExtender, - VideoConferenceRead, - VideoConfProviderExtend, - OAuthAppsModify, - OAuthAppsReader, - OutboundMessageProviderExtend, -}; diff --git a/packages/apps-engine/src/server/bridges/ApiBridge.ts b/packages/apps-engine/src/server/bridges/ApiBridge.ts deleted file mode 100644 index 47be5b27e92e0..0000000000000 --- a/packages/apps-engine/src/server/bridges/ApiBridge.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import type { AppApi } from '../managers/AppApi'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export abstract class ApiBridge extends BaseBridge { - public async doRegisterApi(api: AppApi, appId: string): Promise { - if (this.hasDefaultPermission(appId)) { - return this.registerApi(api, appId); - } - } - - public async doUnregisterApis(appId: string): Promise { - if (this.hasDefaultPermission(appId)) { - return this.unregisterApis(appId); - } - } - - /** - * Registers an api with the system which is being bridged. - * - * @param api the api to register - * @param appId the id of the app calling this - */ - protected abstract registerApi(api: AppApi, appId: string): Promise; - - /** - * Unregisters all provided api's of an app from the bridged system. - * - * @param appId the id of the app calling this - */ - protected abstract unregisterApis(appId: string): Promise; - - private hasDefaultPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.apis.default)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.apis.default], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/AppActivationBridge.ts b/packages/apps-engine/src/server/bridges/AppActivationBridge.ts deleted file mode 100644 index 099cf71800702..0000000000000 --- a/packages/apps-engine/src/server/bridges/AppActivationBridge.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { AppStatus } from '../../definition/AppStatus'; -import type { ProxiedApp } from '../ProxiedApp'; -import { BaseBridge } from './BaseBridge'; - -export abstract class AppActivationBridge extends BaseBridge { - public async doAppAdded(app: ProxiedApp): Promise { - return this.appAdded(app); - } - - public async doAppUpdated(app: ProxiedApp): Promise { - return this.appUpdated(app); - } - - public async doAppRemoved(app: ProxiedApp): Promise { - return this.appRemoved(app); - } - - public async doAppStatusChanged(app: ProxiedApp, status: AppStatus): Promise { - return this.appStatusChanged(app, status); - } - - public async doActionsChanged(): Promise { - return this.actionsChanged(); - } - - protected abstract appAdded(app: ProxiedApp): Promise; - - protected abstract appUpdated(app: ProxiedApp): Promise; - - protected abstract appRemoved(app: ProxiedApp): Promise; - - protected abstract appStatusChanged(app: ProxiedApp, status: AppStatus): Promise; - - protected abstract actionsChanged(): Promise; -} diff --git a/packages/apps-engine/src/server/bridges/AppBridges.ts b/packages/apps-engine/src/server/bridges/AppBridges.ts deleted file mode 100644 index 5e5ef7ca12106..0000000000000 --- a/packages/apps-engine/src/server/bridges/AppBridges.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type { ApiBridge } from './ApiBridge'; -import type { AppActivationBridge } from './AppActivationBridge'; -import type { AppDetailChangesBridge } from './AppDetailChangesBridge'; -import type { CloudWorkspaceBridge } from './CloudWorkspaceBridge'; -import type { CommandBridge } from './CommandBridge'; -import type { ContactBridge } from './ContactBridge'; -import type { EmailBridge } from './EmailBridge'; -import type { EnvironmentalVariableBridge } from './EnvironmentalVariableBridge'; -import type { ExperimentalBridge } from './ExperimentalBridge'; -import type { HttpBridge } from './HttpBridge'; -import type { IInternalBridge } from './IInternalBridge'; -import type { IInternalFederationBridge } from './IInternalFederationBridge'; -import type { IListenerBridge } from './IListenerBridge'; -import type { LivechatBridge } from './LivechatBridge'; -import type { MessageBridge } from './MessageBridge'; -import type { ModerationBridge } from './ModerationBridge'; -import type { OAuthAppsBridge } from './OAuthAppsBridge'; -import type { OutboundMessageBridge } from './OutboundMessagesBridge'; -import type { PersistenceBridge } from './PersistenceBridge'; -import type { RoleBridge } from './RoleBridge'; -import type { RoomBridge } from './RoomBridge'; -import type { SchedulerBridge } from './SchedulerBridge'; -import type { ServerSettingBridge } from './ServerSettingBridge'; -import type { ThreadBridge } from './ThreadBridge'; -import type { UiInteractionBridge } from './UiInteractionBridge'; -import type { UploadBridge } from './UploadBridge'; -import type { UserBridge } from './UserBridge'; -import type { VideoConferenceBridge } from './VideoConferenceBridge'; - -export type Bridge = - | CommandBridge - | ContactBridge - | ApiBridge - | AppDetailChangesBridge - | EnvironmentalVariableBridge - | HttpBridge - | IListenerBridge - | LivechatBridge - | MessageBridge - | PersistenceBridge - | AppActivationBridge - | RoomBridge - | IInternalBridge - | ServerSettingBridge - | EmailBridge - | ExperimentalBridge - | UploadBridge - | UserBridge - | UiInteractionBridge - | SchedulerBridge - | VideoConferenceBridge - | OAuthAppsBridge - | ModerationBridge - | RoleBridge - | OutboundMessageBridge; - -export abstract class AppBridges { - public abstract getCommandBridge(): CommandBridge; - - public abstract getContactBridge(): ContactBridge; - - public abstract getApiBridge(): ApiBridge; - - public abstract getAppDetailChangesBridge(): AppDetailChangesBridge; - - public abstract getEnvironmentalVariableBridge(): EnvironmentalVariableBridge; - - public abstract getHttpBridge(): HttpBridge; - - public abstract getListenerBridge(): IListenerBridge; - - public abstract getLivechatBridge(): LivechatBridge; - - public abstract getMessageBridge(): MessageBridge; - - public abstract getPersistenceBridge(): PersistenceBridge; - - public abstract getAppActivationBridge(): AppActivationBridge; - - public abstract getRoomBridge(): RoomBridge; - - public abstract getInternalBridge(): IInternalBridge; - - public abstract getInternalFederationBridge(): IInternalFederationBridge; - - public abstract getServerSettingBridge(): ServerSettingBridge; - - public abstract getUploadBridge(): UploadBridge; - - public abstract getEmailBridge(): EmailBridge; - - public abstract getUserBridge(): UserBridge; - - public abstract getUiInteractionBridge(): UiInteractionBridge; - - public abstract getSchedulerBridge(): SchedulerBridge; - - public abstract getCloudWorkspaceBridge(): CloudWorkspaceBridge; - - public abstract getVideoConferenceBridge(): VideoConferenceBridge; - - public abstract getOAuthAppsBridge(): OAuthAppsBridge; - - public abstract getModerationBridge(): ModerationBridge; - - public abstract getThreadBridge(): ThreadBridge; - - public abstract getRoleBridge(): RoleBridge; - - public abstract getOutboundMessageBridge(): OutboundMessageBridge; - - public abstract getExperimentalBridge(): ExperimentalBridge; -} diff --git a/packages/apps-engine/src/server/bridges/AppDetailChangesBridge.ts b/packages/apps-engine/src/server/bridges/AppDetailChangesBridge.ts deleted file mode 100644 index 5d1742c55a0d2..0000000000000 --- a/packages/apps-engine/src/server/bridges/AppDetailChangesBridge.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { ISetting } from '../../definition/settings'; - -/** - * An abstract class which will contain various methods related to Apps - * which are called for various inner detail working changes. This - * allows for us to notify various external components of internal - * changes. - */ -export abstract class AppDetailChangesBridge extends BaseBridge { - public doOnAppSettingsChange(appId: string, setting: ISetting): void { - return this.onAppSettingsChange(appId, setting); - } - - protected abstract onAppSettingsChange(appId: string, setting: ISetting): void; -} diff --git a/packages/apps-engine/src/server/bridges/BaseBridge.ts b/packages/apps-engine/src/server/bridges/BaseBridge.ts deleted file mode 100644 index 7085c3d54db78..0000000000000 --- a/packages/apps-engine/src/server/bridges/BaseBridge.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * This class will be used for identification - * of the instances the host sends over to - * the Apps-Engine - */ -export abstract class BaseBridge {} diff --git a/packages/apps-engine/src/server/bridges/CloudWorkspaceBridge.ts b/packages/apps-engine/src/server/bridges/CloudWorkspaceBridge.ts deleted file mode 100644 index ff14f9151aaf8..0000000000000 --- a/packages/apps-engine/src/server/bridges/CloudWorkspaceBridge.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { IWorkspaceToken } from '../../definition/cloud/IWorkspaceToken'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export abstract class CloudWorkspaceBridge extends BaseBridge { - public doGetWorkspaceToken(scope: string, appId: string): Promise { - if (this.hasCloudTokenPermission(appId)) { - return this.getWorkspaceToken(scope, appId); - } - } - - protected abstract getWorkspaceToken(scope: string, appId: string): Promise; - - private hasCloudTokenPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.cloud['workspace-token'])) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.cloud['workspace-token']], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/CommandBridge.ts b/packages/apps-engine/src/server/bridges/CommandBridge.ts deleted file mode 100644 index d5c7feba20c87..0000000000000 --- a/packages/apps-engine/src/server/bridges/CommandBridge.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { ISlashCommand } from '../../definition/slashcommands'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export abstract class CommandBridge extends BaseBridge { - public async doDoesCommandExist(command: string, appId: string): Promise { - if (this.hasDefaultPermission(appId)) { - return this.doesCommandExist(command, appId); - } - } - - public async doEnableCommand(command: string, appId: string): Promise { - if (this.hasDefaultPermission(appId)) { - return this.enableCommand(command, appId); - } - } - - public async doDisableCommand(command: string, appId: string): Promise { - if (this.hasDefaultPermission(appId)) { - return this.disableCommand(command, appId); - } - } - - public async doModifyCommand(command: ISlashCommand, appId: string): Promise { - if (this.hasDefaultPermission(appId)) { - return this.modifyCommand(command, appId); - } - } - - public async doRegisterCommand(command: ISlashCommand, appId: string): Promise { - if (this.hasDefaultPermission(appId)) { - return this.registerCommand(command, appId); - } - } - - public async doUnregisterCommand(command: string, appId: string): Promise { - if (this.hasDefaultPermission(appId)) { - return this.unregisterCommand(command, appId); - } - } - - /** - * Checks if the provided command already exists inside of the - * system which is being bridged. This does not check if the app - * registered it but it will return whether the supplied command is - * already defined by something else or not. - * - * @param command the command to check if it exists - * @param appId the id of the app calling this - * @returns whether the command is already in the system - */ - protected abstract doesCommandExist(command: string, appId: string): Promise; - - /** - * Enables an existing command from the bridged system. The callee - * must ensure that the command that's being enabled is defined by - * the bridged system and not another App since the bridged system - * will not check that. - * - * @param command the command to enable - * @param appId the id of the app calling this - */ - protected abstract enableCommand(command: string, appId: string): Promise; - - /** - * Disables an existing command from the bridged system, the callee must - * ensure the command disabling is defined by the system and not another - * App since the bridged system won't check that. - * - * @param command the command which to disable - * @param appId the id of the app calling this - */ - protected abstract disableCommand(command: string, appId: string): Promise; - - /** - * Changes how a system slash command behaves, allows Apps to provide - * different executors per system commands. - * - * @param command the modified slash command - * @param appId the id of the app calling this - */ - protected abstract modifyCommand(command: ISlashCommand, appId: string): Promise; - - /** - * Registers a command with the system which is being bridged. - * - * @param command the command to register - * @param appId the id of the app calling this - * @param toRun the executor which is called when the command is ran - */ - protected abstract registerCommand(command: ISlashCommand, appId: string): Promise; - - /** - * Unregisters the provided command from the bridged system. - * - * @param command the command to unregister - * @param appId the id of the app calling this - */ - protected abstract unregisterCommand(command: string, appId: string): Promise; - - private hasDefaultPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.command.default)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.command.default], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/ContactBridge.ts b/packages/apps-engine/src/server/bridges/ContactBridge.ts deleted file mode 100644 index 71aa8ddd543fa..0000000000000 --- a/packages/apps-engine/src/server/bridges/ContactBridge.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { ILivechatContact } from '../../definition/livechat/ILivechatContact'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export type VerifyContactChannelParams = { - contactId: string; - field: string; - value: string; - visitorId: string; - roomId: string; -}; - -export abstract class ContactBridge extends BaseBridge { - public async doGetById(contactId: ILivechatContact['_id'], appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getById(contactId, appId); - } - } - - public async doVerifyContact(verifyContactChannelParams: VerifyContactChannelParams, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.verifyContact(verifyContactChannelParams, appId); - } - } - - public async doAddContactEmail(contactId: ILivechatContact['_id'], email: string, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.addContactEmail(contactId, email, appId); - } - } - - protected abstract getById(contactId: ILivechatContact['_id'], appId: string): Promise; - - protected abstract verifyContact(verifyContactChannelParams: VerifyContactChannelParams, appId: string): Promise; - - protected abstract addContactEmail(contactId: ILivechatContact['_id'], email: string, appId: string): Promise; - - private hasReadPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.contact.read)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.contact.read], - }), - ); - - return false; - } - - private hasWritePermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.contact.write)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.contact.write], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/EmailBridge.ts b/packages/apps-engine/src/server/bridges/EmailBridge.ts deleted file mode 100644 index 285e9a0fe52e0..0000000000000 --- a/packages/apps-engine/src/server/bridges/EmailBridge.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { IEmail } from '../../definition/email'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export abstract class EmailBridge extends BaseBridge { - public async doSendEmail(email: IEmail, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.sendEmail(email, appId); - } - } - - protected abstract sendEmail(email: IEmail, appId: string): Promise; - - private hasWritePermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.email.send)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.email.send], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/EnvironmentalVariableBridge.ts b/packages/apps-engine/src/server/bridges/EnvironmentalVariableBridge.ts deleted file mode 100644 index 79b29bf316c41..0000000000000 --- a/packages/apps-engine/src/server/bridges/EnvironmentalVariableBridge.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export abstract class EnvironmentalVariableBridge extends BaseBridge { - public async doGetValueByName(envVarName: string, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getValueByName(envVarName, appId); - } - } - - public async doIsReadable(envVarName: string, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.isReadable(envVarName, appId); - } - } - - public async doIsSet(envVarName: string, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.isSet(envVarName, appId); - } - } - - protected abstract getValueByName(envVarName: string, appId: string): Promise; - - protected abstract isReadable(envVarName: string, appId: string): Promise; - - protected abstract isSet(envVarName: string, appId: string): Promise; - - private hasReadPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.env.read)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.env.read], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/ExperimentalBridge.ts b/packages/apps-engine/src/server/bridges/ExperimentalBridge.ts deleted file mode 100644 index 806b0d66fef37..0000000000000 --- a/packages/apps-engine/src/server/bridges/ExperimentalBridge.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { BaseBridge } from './BaseBridge'; - -/** - * @description - * Experimental bridge for experimental features. - * Methods in this class are not guaranteed to be stable between updates as the - * team evaluates the proper signature, underlying implementation and performance - * impact of candidates for future APIs - */ -export abstract class ExperimentalBridge extends BaseBridge {} diff --git a/packages/apps-engine/src/server/bridges/HttpBridge.ts b/packages/apps-engine/src/server/bridges/HttpBridge.ts deleted file mode 100644 index 7020b21a6317f..0000000000000 --- a/packages/apps-engine/src/server/bridges/HttpBridge.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { IHttpRequest, IHttpResponse, RequestMethod } from '../../definition/accessors'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export interface IHttpBridgeRequestInfo { - appId: string; - method: RequestMethod; - url: string; - request: IHttpRequest; -} - -export abstract class HttpBridge extends BaseBridge { - public async doCall(info: IHttpBridgeRequestInfo): Promise { - if (this.hasDefaultPermission(info.appId)) { - return this.call(info); - } - } - - protected abstract call(info: IHttpBridgeRequestInfo): Promise; - - private hasDefaultPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.networking.default)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.networking.default], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/IInternalBridge.ts b/packages/apps-engine/src/server/bridges/IInternalBridge.ts deleted file mode 100644 index 239fac8610998..0000000000000 --- a/packages/apps-engine/src/server/bridges/IInternalBridge.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { ISetting } from '../../definition/settings'; - -export interface IInternalBridge { - doGetUsernamesOfRoomById(roomId: string): Promise>; - doGetUsernamesOfRoomByIdSync(roomId: string): Array; - doGetWorkspacePublicKey(): Promise; -} diff --git a/packages/apps-engine/src/server/bridges/IInternalFederationBridge.ts b/packages/apps-engine/src/server/bridges/IInternalFederationBridge.ts deleted file mode 100644 index 382f69679b842..0000000000000 --- a/packages/apps-engine/src/server/bridges/IInternalFederationBridge.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface IInternalFederationBridge { - /** - * Get Federation's private key. - * For apps engine's internal use - * - */ - getPrivateKey(): Promise; - - /** - * Get Federation's public key. - * For apps engine's internal use - * - */ - getPublicKey(): Promise; -} diff --git a/packages/apps-engine/src/server/bridges/IInternalPersistenceBridge.ts b/packages/apps-engine/src/server/bridges/IInternalPersistenceBridge.ts deleted file mode 100644 index e1dcd9b247697..0000000000000 --- a/packages/apps-engine/src/server/bridges/IInternalPersistenceBridge.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface IInternalPersistenceBridge { - /** - * Purges the App's persistant storage data from the persistent storage. - * For apps engine's internal use - * - * @argument appId the id of the app's data to remove - */ - purge(appId: string): Promise; -} diff --git a/packages/apps-engine/src/server/bridges/IInternalSchedulerBridge.ts b/packages/apps-engine/src/server/bridges/IInternalSchedulerBridge.ts deleted file mode 100644 index 2064043128a39..0000000000000 --- a/packages/apps-engine/src/server/bridges/IInternalSchedulerBridge.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface IInternalSchedulerBridge { - /** - * Cancels all the running jobs from the app - * For apps-engine's internal use - * @param appId the id of the app calling this - */ - cancelAllJobs(appId: string): Promise; -} diff --git a/packages/apps-engine/src/server/bridges/IInternalUserBridge.ts b/packages/apps-engine/src/server/bridges/IInternalUserBridge.ts deleted file mode 100644 index 2bb4006d01deb..0000000000000 --- a/packages/apps-engine/src/server/bridges/IInternalUserBridge.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { IUser, IUserCreationOptions } from '../../definition/users'; - -export interface IInternalUserBridge { - create(data: Partial, appId: string, options?: IUserCreationOptions): Promise; - getAppUser(appId?: string): Promise; - remove(user: IUser, appId: string): Promise; - getActiveUserCount(): Promise; -} diff --git a/packages/apps-engine/src/server/bridges/IListenerBridge.ts b/packages/apps-engine/src/server/bridges/IListenerBridge.ts deleted file mode 100644 index 7c4b664a4e584..0000000000000 --- a/packages/apps-engine/src/server/bridges/IListenerBridge.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { IMessage } from '../../definition/messages'; -import type { AppInterface } from '../../definition/metadata'; -import type { IRoom } from '../../definition/rooms'; -import type { UIKitIncomingInteraction } from '../../definition/uikit'; - -export interface IListenerBridge { - messageEvent(int: AppInterface, message: IMessage): Promise; - roomEvent(int: AppInterface, room: IRoom): Promise; - uiKitInteractionEvent(int: AppInterface, action: UIKitIncomingInteraction): Promise; -} diff --git a/packages/apps-engine/src/server/bridges/InternalBridge.ts b/packages/apps-engine/src/server/bridges/InternalBridge.ts deleted file mode 100644 index 74188fab2b499..0000000000000 --- a/packages/apps-engine/src/server/bridges/InternalBridge.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { ISetting } from '../../definition/settings'; - -export abstract class InternalBridge extends BaseBridge { - public doGetUsernamesOfRoomById(roomId: string): Promise> { - return this.getUsernamesOfRoomById(roomId); - } - - public doGetUsernamesOfRoomByIdSync(roomId: string): Array { - return this.getUsernamesOfRoomByIdSync(roomId); - } - - public async doGetWorkspacePublicKey(): Promise { - return this.getWorkspacePublicKey(); - } - - protected abstract getUsernamesOfRoomById(roomId: string): Promise>; - - protected abstract getUsernamesOfRoomByIdSync(roomId: string): Array; - - protected abstract getWorkspacePublicKey(): Promise; -} diff --git a/packages/apps-engine/src/server/bridges/ListenerBridge.ts b/packages/apps-engine/src/server/bridges/ListenerBridge.ts deleted file mode 100644 index 19806220e9cbf..0000000000000 --- a/packages/apps-engine/src/server/bridges/ListenerBridge.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { IMessage } from '../../definition/messages'; -import type { AppInterface } from '../../definition/metadata'; -import type { IRoom } from '../../definition/rooms'; - -export abstract class ListenerBridge extends BaseBridge { - public async doMessageEvent(int: AppInterface, message: IMessage): Promise { - return this.messageEvent(int, message); - } - - public async doRoomEvent(int: AppInterface, room: IRoom): Promise { - return this.roomEvent(int, room); - } - - protected abstract messageEvent(int: AppInterface, message: IMessage): Promise; - - protected abstract roomEvent(int: AppInterface, room: IRoom): Promise; -} diff --git a/packages/apps-engine/src/server/bridges/LivechatBridge.ts b/packages/apps-engine/src/server/bridges/LivechatBridge.ts deleted file mode 100644 index 88a7575898049..0000000000000 --- a/packages/apps-engine/src/server/bridges/LivechatBridge.ts +++ /dev/null @@ -1,305 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { IExtraRoomParams } from '../../definition/accessors/ILivechatCreator'; -import type { - IDepartment, - IVisitorExternalIdentifier, - ILivechatMessage, - ILivechatRoom, - ILivechatTransferData, - IVisitor, - ResolveVisitorContactData, -} from '../../definition/livechat'; -import type { IMessage } from '../../definition/messages'; -import type { IUser } from '../../definition/users'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -type LivechatReadPermissions = keyof Pick< - typeof AppPermissions, - 'livechat-department' | 'livechat-message' | 'livechat-room' | 'livechat-status' | 'livechat-visitor' ->; - -type LivechatWritePermissions = keyof Pick< - typeof AppPermissions, - 'livechat-custom-fields' | 'livechat-department' | 'livechat-message' | 'livechat-room' | 'livechat-visitor' ->; - -type LivechatMultiplePermissions = keyof Pick; - -export abstract class LivechatBridge extends BaseBridge { - public doIsOnline(departmentId?: string, appId?: string): boolean { - if (this.hasReadPermission(appId, 'livechat-status')) { - return this.isOnline(departmentId, appId); - } - } - - public async doIsOnlineAsync(departmentId?: string, appId?: string): Promise { - if (this.hasReadPermission(appId, 'livechat-status')) { - return this.isOnlineAsync(departmentId, appId); - } - } - - public async doCreateMessage(message: ILivechatMessage, appId: string): Promise { - if (this.hasWritePermission(appId, 'livechat-message')) { - return this.createMessage(message, appId); - } - } - - public async doGetMessageById(messageId: string, appId: string): Promise { - if (this.hasReadPermission(appId, 'livechat-message')) { - return this.getMessageById(messageId, appId); - } - } - - public async doUpdateMessage(message: ILivechatMessage, appId: string): Promise { - if (this.hasWritePermission(appId, 'livechat-message')) { - return this.updateMessage(message, appId); - } - } - - /** - * @deprecated please use the `doCreateAndReturnVisitor` method instead. - */ - public async doCreateVisitor(visitor: IVisitor, appId: string): Promise { - if (this.hasWritePermission(appId, 'livechat-visitor')) { - return this.createVisitor(visitor, appId); - } - } - - public async doCreateAndReturnVisitor(visitor: IVisitor, appId: string): Promise { - if (this.hasWritePermission(appId, 'livechat-visitor')) { - return this.createAndReturnVisitor(visitor, appId); - } - } - - public async doFindVisitors(query: object, appId: string): Promise> { - if (this.hasReadPermission(appId, 'livechat-visitor')) { - return this.findVisitors(query, appId); - } - } - - public async doFindVisitorById(id: string, appId: string): Promise { - if (this.hasReadPermission(appId, 'livechat-visitor')) { - return this.findVisitorById(id, appId); - } - } - - public async doFindVisitorByEmail(email: string, appId: string): Promise { - if (this.hasReadPermission(appId, 'livechat-visitor')) { - return this.findVisitorByEmail(email, appId); - } - } - - public async doFindVisitorByToken(token: string, appId: string): Promise { - if (this.hasReadPermission(appId, 'livechat-visitor')) { - return this.findVisitorByToken(token, appId); - } - } - - public async doFindVisitorByPhoneNumber(phoneNumber: string, appId: string): Promise { - if (this.hasReadPermission(appId, 'livechat-visitor')) { - return this.findVisitorByPhoneNumber(phoneNumber, appId); - } - } - - public async doResolveVisitor( - externalId: Omit, - contactData: ResolveVisitorContactData | undefined, - appId: string, - ): Promise { - if (this.hasWritePermission(appId, 'livechat-visitor')) { - return this.resolveVisitor(externalId, contactData, appId); - } - } - - public async doTransferVisitor(visitor: IVisitor, transferData: ILivechatTransferData, appId: string): Promise { - if (this.hasWritePermission(appId, 'livechat-visitor')) { - return this.transferVisitor(visitor, transferData, appId); - } - } - - public async doUpdateVisitorExternalId( - visitorId: string, - externalId: Omit, - appId: string, - ): Promise { - if (this.hasWritePermission(appId, 'livechat-visitor')) { - return this.updateVisitorExternalId(visitorId, externalId, appId); - } - } - - public async doCreateRoom(visitor: IVisitor, agent: IUser, appId: string, extraParams?: IExtraRoomParams): Promise { - if (this.hasWritePermission(appId, 'livechat-room')) { - return this.createRoom(visitor, agent, appId, extraParams); - } - } - - public async doCloseRoom(room: ILivechatRoom, comment: string, closer: IUser | undefined, appId: string): Promise { - if (this.hasWritePermission(appId, 'livechat-room')) { - return this.closeRoom(room, comment, closer, appId); - } - } - - public async doCountOpenRoomsByAgentId(agentId: string, appId: string): Promise { - if (this.hasReadPermission(appId, 'livechat-room')) { - return this.countOpenRoomsByAgentId(agentId, appId); - } - } - - public async doFindOpenRoomsByAgentId(agentId: string, appId: string): Promise> { - if (this.hasReadPermission(appId, 'livechat-room')) { - return this.findOpenRoomsByAgentId(agentId, appId); - } - } - - public async doFindRooms(visitor: IVisitor, departmentId: string | null, appId: string): Promise> { - if (this.hasReadPermission(appId, 'livechat-room')) { - return this.findRooms(visitor, departmentId, appId); - } - } - - public async doFindDepartmentByIdOrName(value: string, appId: string): Promise { - if (this.hasReadPermission(appId, 'livechat-department') || this.hasMultiplePermission(appId, 'livechat-department')) { - return this.findDepartmentByIdOrName(value, appId); - } - } - - public async doFindDepartmentsEnabledWithAgents(appId: string): Promise> { - if (this.hasMultiplePermission(appId, 'livechat-department')) { - return this.findDepartmentsEnabledWithAgents(appId); - } - } - - public async do_fetchLivechatRoomMessages(appId: string, roomId: string): Promise> { - if (this.hasMultiplePermission(appId, 'livechat-message')) { - return this._fetchLivechatRoomMessages(appId, roomId); - } - } - - public async doSetCustomFields( - data: { token: IVisitor['token']; key: string; value: string; overwrite: boolean }, - appId: string, - ): Promise { - if (this.hasWritePermission(appId, 'livechat-custom-fields')) { - return this.setCustomFields(data, appId); - } - } - - /** - * @deprecated please use the `isOnlineAsync` method instead. - * In the next major, this method will be `async` - */ - protected abstract isOnline(departmentId?: string, appId?: string): boolean; - - protected abstract isOnlineAsync(departmentId?: string, appId?: string): Promise; - - protected abstract createMessage(message: ILivechatMessage, appId: string): Promise; - - protected abstract getMessageById(messageId: string, appId: string): Promise; - - protected abstract updateMessage(message: ILivechatMessage, appId: string): Promise; - - /** - * @deprecated please use `createAndReturnVisitor` instead. - * It returns the created record rather than the ID. - */ - protected abstract createVisitor(visitor: IVisitor, appId: string): Promise; - - protected abstract createAndReturnVisitor(visitor: IVisitor, appId: string): Promise; - - /** - * @deprecated This method does not adhere to the conversion practices applied - * elsewhere in the Apps-Engine and will be removed in the next major version. - * Prefer other methods that fetch visitors. - */ - protected abstract findVisitors(query: object, appId: string): Promise>; - - protected abstract findVisitorById(id: string, appId: string): Promise; - - protected abstract findVisitorByEmail(email: string, appId: string): Promise; - - protected abstract findVisitorByToken(token: string, appId: string): Promise; - - protected abstract findVisitorByPhoneNumber(phoneNumber: string, appId: string): Promise; - - protected abstract resolveVisitor( - externalId: Omit, - contactData: ResolveVisitorContactData | undefined, - appId: string, - ): Promise; - - protected abstract transferVisitor(visitor: IVisitor, transferData: ILivechatTransferData, appId: string): Promise; - - protected abstract updateVisitorExternalId( - visitorId: string, - externalId: Omit, - appId: string, - ): Promise; - - protected abstract createRoom(visitor: IVisitor, agent: IUser, appId: string, extraParams?: IExtraRoomParams): Promise; - - protected abstract closeRoom(room: ILivechatRoom, comment: string, closer: IUser | undefined, appId: string): Promise; - - protected abstract countOpenRoomsByAgentId(agentId: string, appId: string): Promise; - - protected abstract findOpenRoomsByAgentId(agentId: string, appId: string): Promise>; - - protected abstract findRooms(visitor: IVisitor, departmentId: string | null, appId: string): Promise>; - - protected abstract findDepartmentByIdOrName(value: string, appId: string): Promise; - - protected abstract findDepartmentsEnabledWithAgents(appId: string): Promise>; - - protected abstract _fetchLivechatRoomMessages(appId: string, roomId: string): Promise>; - - protected abstract setCustomFields( - data: { token: IVisitor['token']; key: string; value: string; overwrite: boolean }, - appId: string, - ): Promise; - - private hasReadPermission(appId: string, scope: LivechatReadPermissions): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions[scope].read)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions[scope].read], - }), - ); - - return false; - } - - private hasWritePermission(appId: string, scope: LivechatWritePermissions): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions[scope].write)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions[scope].write], - }), - ); - - return false; - } - - private hasMultiplePermission(appId: string, scope: LivechatMultiplePermissions): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions[scope].multiple)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions[scope].multiple], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/MessageBridge.ts b/packages/apps-engine/src/server/bridges/MessageBridge.ts deleted file mode 100644 index e1d437339cb10..0000000000000 --- a/packages/apps-engine/src/server/bridges/MessageBridge.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { ITypingOptions } from '../../definition/accessors/INotifier'; -import type { IMessage, Reaction } from '../../definition/messages'; -import type { IRoom } from '../../definition/rooms'; -import type { IUser } from '../../definition/users'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export interface ITypingDescriptor extends ITypingOptions { - isTyping: boolean; -} - -export abstract class MessageBridge extends BaseBridge { - public async doCreate(message: IMessage, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.create(message, appId); - } - } - - public async doUpdate(message: IMessage, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.update(message, appId); - } - } - - public async doNotifyUser(user: IUser, message: IMessage, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.notifyUser(user, message, appId); - } - } - - public async doNotifyRoom(room: IRoom, message: IMessage, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.notifyRoom(room, message, appId); - } - } - - public async doTyping(options: ITypingDescriptor, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.typing(options, appId); - } - } - - public async doGetById(messageId: string, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getById(messageId, appId); - } - } - - public async doDelete(message: IMessage, user: IUser, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.delete(message, user, appId); - } - } - - public async doAddReaction(messageId: string, userId: string, reaction: Reaction, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.addReaction(messageId, userId, reaction); - } - } - - public async doRemoveReaction(messageId: string, userId: string, reaction: Reaction, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.removeReaction(messageId, userId, reaction); - } - } - - protected abstract create(message: IMessage, appId: string): Promise; - - protected abstract update(message: IMessage, appId: string): Promise; - - protected abstract notifyUser(user: IUser, message: IMessage, appId: string): Promise; - - protected abstract notifyRoom(room: IRoom, message: IMessage, appId: string): Promise; - - protected abstract typing(options: ITypingDescriptor, appId: string): Promise; - - protected abstract getById(messageId: string, appId: string): Promise; - - protected abstract delete(message: IMessage, user: IUser, appId: string): Promise; - - protected abstract addReaction(messageId: string, userId: string, reaction: Reaction): Promise; - - protected abstract removeReaction(messageId: string, userId: string, reaction: Reaction): Promise; - - private hasReadPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.message.read)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.message.read], - }), - ); - - return false; - } - - private hasWritePermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.message.write)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.message.write], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/ModerationBridge.ts b/packages/apps-engine/src/server/bridges/ModerationBridge.ts deleted file mode 100644 index 39109c2bc2d64..0000000000000 --- a/packages/apps-engine/src/server/bridges/ModerationBridge.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { IMessage } from '../../definition/messages'; -import type { IUser } from '../../definition/users'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export abstract class ModerationBridge extends BaseBridge { - public async doReport(messageId: IMessage['id'], description: string, userId: string, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.report(messageId, description, userId, appId); - } - } - - public async doDismissReportsByMessageId(messageId: IMessage['id'], reason: string, action: string, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.dismissReportsByMessageId(messageId, reason, action, appId); - } - } - - public async doDismissReportsByUserId(userId: IUser['id'], reason: string, action: string, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.dismissReportsByUserId(userId, reason, action, appId); - } - } - - protected abstract report(messageId: string, description: string, userId: string, appId: string): Promise; - - protected abstract dismissReportsByMessageId(messageId: string, reason: string, action: string, appId: string): Promise; - - protected abstract dismissReportsByUserId(userId: string, reason: string, action: string, appId: string): Promise; - - private hasWritePermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.moderation.write)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.moderation.write], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/OAuthAppsBridge.ts b/packages/apps-engine/src/server/bridges/OAuthAppsBridge.ts deleted file mode 100644 index 3e34510953fd1..0000000000000 --- a/packages/apps-engine/src/server/bridges/OAuthAppsBridge.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { IOAuthApp, IOAuthAppParams } from '../../definition/accessors/IOAuthApp'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export abstract class OAuthAppsBridge extends BaseBridge { - public async doCreate(oAuthApp: IOAuthAppParams, appId: string) { - if (this.hasWritePermission(appId)) { - return this.create(oAuthApp, appId); - } - } - - public async doGetByid(id: string, appId: string) { - if (this.hasReadPermission(appId)) { - return this.getById(id, appId); - } - } - - public async doGetByName(name: string, appId: string) { - if (this.hasReadPermission(appId)) { - return this.getByName(name, appId); - } - } - - public async doUpdate(oAuthApp: IOAuthAppParams, id: string, appId: string) { - if (this.hasWritePermission(appId)) { - return this.update(oAuthApp, id, appId); - } - } - - public async doDelete(id: string, appId: string) { - if (this.hasWritePermission(appId)) { - return this.delete(id, appId); - } - } - - public async doPurge(appId: string) { - if (this.hasWritePermission(appId)) { - return this.purge(appId); - } - } - - protected abstract create(oAuthApp: IOAuthAppParams, appId: string): Promise; - - protected abstract getById(id: string, appId: string): Promise; - - protected abstract getByName(name: string, appId: string): Promise>; - - protected abstract update(oAuthApp: IOAuthAppParams, id: string, appId: string): Promise; - - protected abstract delete(id: string, appId: string): Promise; - - protected abstract purge(appId: string): Promise; - - private hasWritePermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions['oauth-app'].write)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions['oauth-app'].write], - }), - ); - - return false; - } - - private hasReadPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions['oauth-app'].read)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions['oauth-app'].read], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/OutboundMessagesBridge.ts b/packages/apps-engine/src/server/bridges/OutboundMessagesBridge.ts deleted file mode 100644 index 91751d1bcafc9..0000000000000 --- a/packages/apps-engine/src/server/bridges/OutboundMessagesBridge.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { - IOutboundEmailMessageProvider, - IOutboundMessageProviders, - IOutboundPhoneMessageProvider, -} from '../../definition/outboundCommunication'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export abstract class OutboundMessageBridge extends BaseBridge { - public async doRegisterPhoneProvider(info: IOutboundPhoneMessageProvider, appId: string): Promise { - if (this.hasProviderPermission(appId)) { - return this.registerPhoneProvider(info, appId); - } - } - - public async doRegisterEmailProvider(info: IOutboundEmailMessageProvider, appId: string): Promise { - if (this.hasProviderPermission(appId)) { - return this.registerEmailProvider(info, appId); - } - } - - public async doUnRegisterProvider(info: IOutboundMessageProviders, appId: string): Promise { - if (this.hasProviderPermission(appId)) { - return this.unRegisterProvider(info, appId); - } - } - - private hasProviderPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.outboundComms.provide)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.outboundComms.provide], - }), - ); - - return false; - } - - protected abstract registerPhoneProvider(info: IOutboundPhoneMessageProvider, appId: string): Promise; - - protected abstract registerEmailProvider(info: IOutboundEmailMessageProvider, appId: string): Promise; - - protected abstract unRegisterProvider(info: IOutboundMessageProviders, appId: string): Promise; -} diff --git a/packages/apps-engine/src/server/bridges/PersistenceBridge.ts b/packages/apps-engine/src/server/bridges/PersistenceBridge.ts deleted file mode 100644 index bb9d6a69dabfe..0000000000000 --- a/packages/apps-engine/src/server/bridges/PersistenceBridge.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { RocketChatAssociationRecord } from '../../definition/metadata'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export abstract class PersistenceBridge extends BaseBridge { - public async doPurge(appId: string): Promise { - if (this.hasDefaultPermission(appId)) { - return this.purge(appId); - } - } - - public async doCreate(data: object, appId: string): Promise { - if (this.hasDefaultPermission(appId)) { - return this.create(data, appId); - } - } - - public async doCreateWithAssociations(data: object, associations: Array, appId: string): Promise { - if (this.hasDefaultPermission(appId)) { - return this.createWithAssociations(data, associations, appId); - } - } - - public async doReadById(id: string, appId: string): Promise { - if (this.hasDefaultPermission(appId)) { - return this.readById(id, appId); - } - } - - public async doReadByAssociations(associations: Array, appId: string): Promise> { - if (this.hasDefaultPermission(appId)) { - return this.readByAssociations(associations, appId); - } - } - - public async doRemove(id: string, appId: string): Promise { - if (this.hasDefaultPermission(appId)) { - return this.remove(id, appId); - } - } - - public async doRemoveByAssociations(associations: Array, appId: string): Promise | undefined> { - if (this.hasDefaultPermission(appId)) { - return this.removeByAssociations(associations, appId); - } - } - - public async doUpdate(id: string, data: object, upsert: boolean, appId: string): Promise { - if (this.hasDefaultPermission(appId)) { - return this.update(id, data, upsert, appId); - } - } - - public async doUpdateByAssociations( - associations: Array, - data: object, - upsert: boolean, - appId: string, - ): Promise { - if (this.hasDefaultPermission(appId)) { - return this.updateByAssociations(associations, data, upsert, appId); - } - } - - /** - * Purges the App's persistant storage data from the persistent storage. - * - * @argument appId the id of the app's data to remove - */ - protected abstract purge(appId: string): Promise; - - /** - * Creates a new persistant record with the provided data attached. - * - * @argument data the data to store in persistent storage - * @argument appId the id of the app which is storing the data - * @returns the id of the stored record - */ - protected abstract create(data: object, appId: string): Promise; - - /** - * Creates a new record in the App's persistent storage with the data being - * associated with at least one Rocket.Chat record. - * - * @argument data the data to store in the persistent storage - * @argument associations the associations records this data is associated with - * @argument appId the id of the app which is storing the data - * @returns the id of the stored record - */ - protected abstract createWithAssociations(data: object, associations: Array, appId: string): Promise; - - /** - * Retrieves from the persistent storage the record by the id provided. - * - * @argument id the record id to read - * @argument appId the id of the app calling this - * @returns the data stored in the persistent storage, or undefined - */ - protected abstract readById(id: string, appId: string): Promise; - - /** - * Retrieves the data which is associated with the provided records. - * - * @argument associations the association records to query about - * @argument appId the id of the app calling this - * @returns an array of records if they exist, an empty array otherwise - */ - protected abstract readByAssociations(associations: Array, appId: string): Promise>; - - /** - * Removes the record which matches the provided id. - * - * @argument id the id of the record - * @argument appId the id of the app calling this - * @returns the data being removed - */ - protected abstract remove(id: string, appId: string): Promise; - - /** - * Removes any data which has been associated with the provided records. - * - * @argument associations the associations which to remove records - * @argument appId the id of the app calling this - * @returns the data of the removed records - */ - protected abstract removeByAssociations( - associations: Array, - appId: string, - ): Promise | undefined>; - - /** - * Updates the record in the database, with the option of creating a new one if it doesn't exist. - * - * @argument id the id of the record to update - * @argument data the updated data to set in the record - * @argument upsert whether to create if the id doesn't exist - * @argument appId the id of the app calling this - * @returns the id, whether the new one or the existing one - */ - protected abstract update(id: string, data: object, upsert: boolean, appId: string): Promise; - - /** - * Updates the record in the database, with the option of creating a new one if it doesn't exist. - * - * @argument associations the association records to update - * @argument data the updated data to set in the record - * @argument upsert whether to create if the id doesn't exist - * @argument appId the id of the app calling this - * @returns the id, whether the new one or the existing one - */ - protected abstract updateByAssociations( - associations: Array, - data: object, - upsert: boolean, - appId: string, - ): Promise; - - private hasDefaultPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.persistence.default)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.persistence.default], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/RoleBridge.ts b/packages/apps-engine/src/server/bridges/RoleBridge.ts deleted file mode 100644 index 46066826fa7c8..0000000000000 --- a/packages/apps-engine/src/server/bridges/RoleBridge.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { IRole } from '../../definition/roles'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export abstract class RoleBridge extends BaseBridge { - public async doGetOneByIdOrName(idOrName: IRole['id'] | IRole['name'], appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getOneByIdOrName(idOrName, appId); - } - } - - public async doGetCustomRoles(appId: string): Promise> { - if (this.hasReadPermission(appId)) { - return this.getCustomRoles(appId); - } - } - - protected abstract getOneByIdOrName(idOrName: IRole['id'] | IRole['name'], appId: string): Promise; - - protected abstract getCustomRoles(appId: string): Promise>; - - private hasReadPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.role.read)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.role.read], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/RoomBridge.ts b/packages/apps-engine/src/server/bridges/RoomBridge.ts deleted file mode 100644 index 66f4ff1a878a2..0000000000000 --- a/packages/apps-engine/src/server/bridges/RoomBridge.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { IMessage, IMessageRaw } from '../../definition/messages'; -import type { IRoom, IRoomRaw, RoomType } from '../../definition/rooms'; -import type { IUser } from '../../definition/users'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export const GetMessagesSortableFields = ['createdAt'] as const; - -export type GetMessagesOptions = { - limit: number; - skip: number; - sort: Record<(typeof GetMessagesSortableFields)[number], 'asc' | 'desc'>; - showThreadMessages: boolean; -}; - -/** - * Filters for querying rooms in the system. - */ -export type GetRoomsFilters = { - /** - * When specified, only rooms matching the provided types will be returned. - */ - types?: Array; - /** - * Filter to include or exclude discussion rooms. - * - * When undefined (default), discussions are included in the result set. - * - * When true, ONLY discussions are included in the result set (remove non-discussions). - * When false, discussion rooms are excluded from the result set. - */ - discussions?: boolean; - /** - * Filter to include or exclude team main rooms. - * - * When undefined (default), team main rooms are included in the result set. - * - * When true, ONLY team main rooms are included in the result set (remove non-teams). - * When false, team main rooms are excluded from the result set. - */ - teams?: boolean; -}; - -export type GetRoomsOptions = { - limit?: number; - skip?: number; -}; - -export abstract class RoomBridge extends BaseBridge { - public async doCreate(room: IRoom, members: Array, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.create(room, members, appId); - } - } - - public async doGetById(roomId: string, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getById(roomId, appId); - } - } - - public async doGetByName(roomName: string, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getByName(roomName, appId); - } - } - - public async doGetCreatorById(roomId: string, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getCreatorById(roomId, appId); - } - } - - public async doGetCreatorByName(roomName: string, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getCreatorByName(roomName, appId); - } - } - - public async doGetDirectByUsernames(usernames: Array, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getDirectByUsernames(usernames, appId); - } - } - - public async doGetMembers(roomId: string, appId: string): Promise> { - if (this.hasReadPermission(appId)) { - return this.getMembers(roomId, appId); - } - } - - public async doGetAllRooms(filters: GetRoomsFilters = {}, options: GetRoomsOptions = {}, appId: string): Promise | undefined> { - if (this.hasViewAllRoomsPermission(appId)) { - return this.getAllRooms(filters, options, appId); - } - } - - public async doUpdate(room: IRoom, members: Array, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.update(room, members, appId); - } - } - - public async doCreateDiscussion( - room: IRoom, - parentMessage: IMessage | undefined, - reply: string | undefined, - members: Array, - appId: string, - ): Promise { - if (this.hasWritePermission(appId)) { - return this.createDiscussion(room, parentMessage, reply, members, appId); - } - } - - public async doDelete(room: string, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.delete(room, appId); - } - } - - public async doGetModerators(roomId: string, appId: string): Promise> { - if (this.hasReadPermission(appId)) { - return this.getModerators(roomId, appId); - } - } - - public async doGetOwners(roomId: string, appId: string): Promise> { - if (this.hasReadPermission(appId)) { - return this.getOwners(roomId, appId); - } - } - - public async doGetLeaders(roomId: string, appId: string): Promise> { - if (this.hasReadPermission(appId)) { - return this.getLeaders(roomId, appId); - } - } - - public async doGetMessages(roomId: string, options: GetMessagesOptions, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getMessages(roomId, options, appId); - } - } - - public async doRemoveUsers(roomId: string, usernames: Array, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.removeUsers(roomId, usernames, appId); - } - } - - public async doGetUnreadByUser(roomId: string, uid: string, options: GetMessagesOptions, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getUnreadByUser(roomId, uid, options, appId); - } - } - - public async doGetUserUnreadMessageCount(roomId: string, uid: string, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getUserUnreadMessageCount(roomId, uid, appId); - } - } - - protected abstract create(room: IRoom, members: Array, appId: string): Promise; - - protected abstract getById(roomId: string, appId: string): Promise; - - protected abstract getByName(roomName: string, appId: string): Promise; - - protected abstract getCreatorById(roomId: string, appId: string): Promise; - - protected abstract getCreatorByName(roomName: string, appId: string): Promise; - - protected abstract getDirectByUsernames(usernames: Array, appId: string): Promise; - - protected abstract getMembers(roomId: string, appId: string): Promise>; - - protected abstract getAllRooms(filters: GetRoomsFilters, options: GetRoomsOptions, appId: string): Promise>; - - protected abstract update(room: IRoom, members: Array, appId: string): Promise; - - protected abstract createDiscussion( - room: IRoom, - parentMessage: IMessage | undefined, - reply: string | undefined, - members: Array, - appId: string, - ): Promise; - - protected abstract delete(room: string, appId: string): Promise; - - protected abstract getModerators(roomId: string, appId: string): Promise>; - - protected abstract getOwners(roomId: string, appId: string): Promise>; - - protected abstract getLeaders(roomId: string, appId: string): Promise>; - - protected abstract getMessages(roomId: string, options: GetMessagesOptions, appId: string): Promise; - - protected abstract removeUsers(roomId: string, usernames: Array, appId: string): Promise; - - protected abstract getUnreadByUser(roomId: string, uid: string, options: GetMessagesOptions, appId: string): Promise; - - protected abstract getUserUnreadMessageCount(roomId: string, uid: string, appId: string): Promise; - - private hasWritePermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.room.write)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.room.write], - }), - ); - - return false; - } - - private hasReadPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.room.read)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.room.read], - }), - ); - - return false; - } - - private hasViewAllRoomsPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.room['system-view-all'])) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.room['system-view-all']], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/SchedulerBridge.ts b/packages/apps-engine/src/server/bridges/SchedulerBridge.ts deleted file mode 100644 index e881c2f08be6a..0000000000000 --- a/packages/apps-engine/src/server/bridges/SchedulerBridge.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { IOnetimeSchedule, IProcessor, IRecurringSchedule } from '../../definition/scheduler'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export abstract class SchedulerBridge extends BaseBridge { - public async doRegisterProcessors(processors: Array = [], appId: string): Promise> { - if (this.hasDefaultPermission(appId)) { - return this.registerProcessors(processors, appId); - } - } - - public async doScheduleOnce(job: IOnetimeSchedule, appId: string): Promise { - if (this.hasDefaultPermission(appId)) { - return this.scheduleOnce(job, appId); - } - } - - public async doScheduleRecurring(job: IRecurringSchedule, appId: string): Promise { - if (this.hasDefaultPermission(appId)) { - return this.scheduleRecurring(job, appId); - } - } - - public async doCancelJob(jobId: string, appId: string): Promise { - if (this.hasDefaultPermission(appId)) { - return this.cancelJob(jobId, appId); - } - } - - public async doCancelAllJobs(appId: string): Promise { - if (this.hasDefaultPermission(appId)) { - return this.cancelAllJobs(appId); - } - } - - protected abstract registerProcessors(processors: Array, appId: string): Promise>; - - protected abstract scheduleOnce(job: IOnetimeSchedule, appId: string): Promise; - - protected abstract scheduleRecurring(job: IRecurringSchedule, appId: string): Promise; - - protected abstract cancelJob(jobId: string, appId: string): Promise; - - protected abstract cancelAllJobs(appId: string): Promise; - - private hasDefaultPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.scheduler.default)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.scheduler.default], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/ServerSettingBridge.ts b/packages/apps-engine/src/server/bridges/ServerSettingBridge.ts deleted file mode 100644 index e108a23d0d268..0000000000000 --- a/packages/apps-engine/src/server/bridges/ServerSettingBridge.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { ISetting } from '../../definition/settings'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export abstract class ServerSettingBridge extends BaseBridge { - public async doGetAll(appId: string): Promise> { - if (this.hasReadPermission(appId)) { - return this.getAll(appId); - } - } - - public async doGetOneById(id: string, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getOneById(id, appId); - } - } - - public async doHideGroup(name: string, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.hideGroup(name, appId); - } - } - - public async doHideSetting(id: string, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.hideSetting(id, appId); - } - } - - public async doIsReadableById(id: string, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.isReadableById(id, appId); - } - } - - public async doUpdateOne(setting: ISetting, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.updateOne(setting, appId); - } - } - - public async doIncrementValue(id: ISetting['id'], value: number, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.incrementValue(id, value, appId); - } - } - - protected abstract getAll(appId: string): Promise>; - - protected abstract getOneById(id: string, appId: string): Promise; - - protected abstract hideGroup(name: string, appId: string): Promise; - - protected abstract hideSetting(id: string, appId: string): Promise; - - protected abstract isReadableById(id: string, appId: string): Promise; - - protected abstract updateOne(setting: ISetting, appId: string): Promise; - - protected abstract incrementValue(id: ISetting['id'], value: number, appId: string): Promise; - - private hasWritePermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.setting.write)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.setting.write], - }), - ); - - return false; - } - - private hasReadPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.setting.read)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.setting.read], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/ThreadBridge.ts b/packages/apps-engine/src/server/bridges/ThreadBridge.ts deleted file mode 100644 index cdbc42035cd0a..0000000000000 --- a/packages/apps-engine/src/server/bridges/ThreadBridge.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { ITypingOptions } from '../../definition/accessors/INotifier'; -import type { IMessage } from '../../definition/messages'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export interface ITypingDescriptor extends ITypingOptions { - isTyping: boolean; -} - -export abstract class ThreadBridge extends BaseBridge { - public async doGetById(messageId: string, appId: string): Promise> { - if (this.hasReadPermission(appId)) { - return this.getById(messageId, appId); - } - } - - protected abstract getById(messageId: string, appId: string): Promise>; - - private hasReadPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.threads.read)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.threads.read], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/UiInteractionBridge.ts b/packages/apps-engine/src/server/bridges/UiInteractionBridge.ts deleted file mode 100644 index e085d389ff4e0..0000000000000 --- a/packages/apps-engine/src/server/bridges/UiInteractionBridge.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { IUIKitInteraction } from '../../definition/uikit'; -import type { IUser } from '../../definition/users'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export abstract class UiInteractionBridge extends BaseBridge { - public async doNotifyUser(user: IUser, interaction: IUIKitInteraction, appId: string): Promise { - if (this.hasInteractionPermission(appId)) { - return this.notifyUser(user, interaction, appId); - } - } - - protected abstract notifyUser(user: IUser, interaction: IUIKitInteraction, appId: string): Promise; - - private hasInteractionPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.ui.interaction)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.ui.interaction], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/UploadBridge.ts b/packages/apps-engine/src/server/bridges/UploadBridge.ts deleted file mode 100644 index 65d0ae0b13449..0000000000000 --- a/packages/apps-engine/src/server/bridges/UploadBridge.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { IUpload } from '../../definition/uploads'; -import type { IUploadDetails } from '../../definition/uploads/IUploadDetails'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export abstract class UploadBridge extends BaseBridge { - public async doGetById(id: string, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getById(id, appId); - } - } - - public async doGetBuffer(upload: IUpload, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getBuffer(upload, appId); - } - } - - public async doCreateUpload(details: IUploadDetails, buffer: Buffer, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.createUpload(details, buffer, appId); - } - } - - protected abstract getById(id: string, appId: string): Promise; - - protected abstract getBuffer(upload: IUpload, appId: string): Promise; - - protected abstract createUpload(details: IUploadDetails, buffer: Buffer, appId: string): Promise; - - private hasReadPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.upload.read)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.upload.read], - }), - ); - - return false; - } - - private hasWritePermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.upload.write)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.upload.write], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/UserBridge.ts b/packages/apps-engine/src/server/bridges/UserBridge.ts deleted file mode 100644 index 72cf52eb17c1d..0000000000000 --- a/packages/apps-engine/src/server/bridges/UserBridge.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { IUser, IUserCreationOptions, UserType } from '../../definition/users'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export abstract class UserBridge extends BaseBridge { - public async doGetById(id: string, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getById(id, appId); - } - } - - public async doGetByUsername(username: string, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getByUsername(username, appId); - } - } - - public async doGetAppUser(appId?: string): Promise { - return this.getAppUser(appId); - } - - public async doCreate(data: Partial, appId: string, options?: IUserCreationOptions): Promise { - if (this.hasWritePermission(appId)) { - return this.create(data, appId, options || {}); - } - } - - public async doRemove(user: IUser, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.remove(user, appId); - } - } - - public async doUpdate(user: IUser, updates: Partial, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.update(user, updates, appId); - } - } - - public async doGetUserUnreadMessageCount(uid: string, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getUserUnreadMessageCount(uid, appId); - } - } - - public async doGetUserRoomIds(userId: string, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getUserRoomIds(userId, appId); - } - } - - public async doDeleteUsersCreatedByApp(appId: string, type: UserType.BOT | UserType.APP): Promise { - if (this.hasWritePermission(appId)) { - return this.deleteUsersCreatedByApp(appId, type); - } - } - - public async doDeactivate(userId: IUser['id'], confirmRelinquish: boolean, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.deactivate(userId, confirmRelinquish, appId); - } - } - - protected abstract getById(id: string, appId: string): Promise; - - protected abstract getByUsername(username: string, appId: string): Promise; - - protected abstract getAppUser(appId?: string): Promise; - - protected abstract getActiveUserCount(): Promise; - - protected abstract getUserUnreadMessageCount(uid: string, appId: string): Promise; - - protected abstract getUserRoomIds(userId: string, appId: string): Promise; - - /** - * Creates a user. - * @param data the essential data for creating a user - * @param appId the id of the app calling this - * @param options options for passing extra data - */ - protected abstract create(data: Partial, appId: string, options?: IUserCreationOptions): Promise; - - /** - * Remove a user. - * - * @param user the user object to be removed - * @param appId the id of the app executing the call - */ - protected abstract remove(user: IUser, appId: string): Promise; - - /** - * Updates a user. - * - * Note: the actual methods used by apps to update - * user properties are much more granular, but at a - * bridge level we can adopt a more practical approach - * since it is only accessible internally by the framework - * - * @param user the user to be updated - * @param updates a map of properties to be updated - * @param appId the id of the app executing the call - */ - protected abstract update(user: IUser, updates: Partial, appId: string): Promise; - - /** - * Deletes all bot or app users created by the App. - * @param appId the App's ID. - * @param type the type of the user to be deleted. - * @returns true if any user was deleted, false otherwise. - */ - protected abstract deleteUsersCreatedByApp(appId: string, type: UserType.APP | UserType.BOT): Promise; - - /** - * Deactivates a user. - * @param userId the user's ID. - * @param confirmRelinquish whether the user confirmed the relinquish of the account. - * @param appId the App's ID. - * @returns true if the user was deactivated, false otherwise. - * @throws {Error} if the user is not found. - * @throws {Error} if the user is the last admin. - * @throws {Error} if the user is the last owner, if confirmRelinquish is false. - */ - protected abstract deactivate(userId: IUser['id'], confirmRelinquish: boolean, appId: string): Promise; - - private hasReadPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.user.read)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.user.read], - }), - ); - - return false; - } - - private hasWritePermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.user.write)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.user.write], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/VideoConferenceBridge.ts b/packages/apps-engine/src/server/bridges/VideoConferenceBridge.ts deleted file mode 100644 index b2d57f3317341..0000000000000 --- a/packages/apps-engine/src/server/bridges/VideoConferenceBridge.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { BaseBridge } from './BaseBridge'; -import type { IVideoConfProvider } from '../../definition/videoConfProviders'; -import type { AppVideoConference } from '../../definition/videoConferences/AppVideoConference'; -import type { VideoConference } from '../../definition/videoConferences/IVideoConference'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export abstract class VideoConferenceBridge extends BaseBridge { - public async doGetById(callId: string, appId: string): Promise { - if (this.hasReadPermission(appId)) { - return this.getById(callId, appId); - } - } - - public async doCreate(call: AppVideoConference, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.create(call, appId); - } - } - - public async doUpdate(call: VideoConference, appId: string): Promise { - if (this.hasWritePermission(appId)) { - return this.update(call, appId); - } - } - - public async doRegisterProvider(info: IVideoConfProvider, appId: string): Promise { - if (this.hasProviderPermission(appId)) { - return this.registerProvider(info, appId); - } - } - - public async doUnRegisterProvider(info: IVideoConfProvider, appId: string): Promise { - if (this.hasProviderPermission(appId)) { - return this.unRegisterProvider(info, appId); - } - } - - protected abstract create(call: AppVideoConference, appId: string): Promise; - - protected abstract getById(callId: string, appId: string): Promise; - - protected abstract update(call: VideoConference, appId: string): Promise; - - protected abstract registerProvider(info: IVideoConfProvider, appId: string): Promise; - - protected abstract unRegisterProvider(info: IVideoConfProvider, appId: string): Promise; - - private hasWritePermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.videoConference.write)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.videoConference.write], - }), - ); - - return false; - } - - private hasReadPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.videoConference.read)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.videoConference.read], - }), - ); - - return false; - } - - private hasProviderPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.videoConference.provider)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.videoConference.provider], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/bridges/index.ts b/packages/apps-engine/src/server/bridges/index.ts deleted file mode 100644 index c472ad2293acb..0000000000000 --- a/packages/apps-engine/src/server/bridges/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { ApiBridge } from './ApiBridge'; -import { AppActivationBridge } from './AppActivationBridge'; -import { AppBridges } from './AppBridges'; -import { AppDetailChangesBridge } from './AppDetailChangesBridge'; -import { CloudWorkspaceBridge } from './CloudWorkspaceBridge'; -import { CommandBridge } from './CommandBridge'; -import { ContactBridge } from './ContactBridge'; -import { EmailBridge } from './EmailBridge'; -import { EnvironmentalVariableBridge } from './EnvironmentalVariableBridge'; -import { ExperimentalBridge } from './ExperimentalBridge'; -import { HttpBridge, IHttpBridgeRequestInfo } from './HttpBridge'; -import { IInternalBridge } from './IInternalBridge'; -import { IInternalFederationBridge } from './IInternalFederationBridge'; -import { IListenerBridge } from './IListenerBridge'; -import { LivechatBridge } from './LivechatBridge'; -import { MessageBridge } from './MessageBridge'; -import { ModerationBridge } from './ModerationBridge'; -import { OutboundMessageBridge } from './OutboundMessagesBridge'; -import { PersistenceBridge } from './PersistenceBridge'; -import { RoleBridge } from './RoleBridge'; -import { RoomBridge } from './RoomBridge'; -import { SchedulerBridge } from './SchedulerBridge'; -import { ServerSettingBridge } from './ServerSettingBridge'; -import { UiInteractionBridge } from './UiInteractionBridge'; -import { UploadBridge } from './UploadBridge'; -import { UserBridge } from './UserBridge'; -import { VideoConferenceBridge } from './VideoConferenceBridge'; - -export { - CloudWorkspaceBridge, - ContactBridge, - EnvironmentalVariableBridge, - HttpBridge, - IHttpBridgeRequestInfo, - IListenerBridge, - LivechatBridge, - MessageBridge, - PersistenceBridge, - AppActivationBridge, - AppDetailChangesBridge, - CommandBridge, - ApiBridge, - RoomBridge, - IInternalBridge, - ServerSettingBridge, - UserBridge, - UploadBridge, - EmailBridge, - ExperimentalBridge, - UiInteractionBridge, - SchedulerBridge, - AppBridges, - VideoConferenceBridge, - IInternalFederationBridge, - ModerationBridge, - RoleBridge, - OutboundMessageBridge, -}; diff --git a/packages/apps-engine/src/server/compiler/AppCompiler.ts b/packages/apps-engine/src/server/compiler/AppCompiler.ts deleted file mode 100644 index 9ae3b12ba67a0..0000000000000 --- a/packages/apps-engine/src/server/compiler/AppCompiler.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as path from 'path'; - -import type { AppManager } from '../AppManager'; -import { ProxiedApp } from '../ProxiedApp'; -import type { IAppStorageItem } from '../storage'; -import type { IParseAppPackageResult } from './IParseAppPackageResult'; - -export class AppCompiler { - public normalizeStorageFiles(files: { [key: string]: string }): { [key: string]: string } { - const result: { [key: string]: string } = {}; - - Object.entries(files).forEach(([name, content]) => { - result[name.replace(/\$/g, '.')] = content; - }); - - return result; - } - - public async toSandBox(manager: AppManager, storage: IAppStorageItem, packageResult: IParseAppPackageResult): Promise { - if (typeof packageResult.files[path.normalize(storage.info.classFile)] === 'undefined') { - throw new Error(`Invalid App package for "${storage.info.name}". Could not find the classFile (${storage.info.classFile}) file.`); - } - - const runtime = await manager.getRuntime().startRuntimeForApp(packageResult, storage); - - const app = new ProxiedApp(manager, storage, runtime); - - return app; - } -} diff --git a/packages/apps-engine/src/server/compiler/AppFabricationFulfillment.ts b/packages/apps-engine/src/server/compiler/AppFabricationFulfillment.ts deleted file mode 100644 index 6fb53a8c52580..0000000000000 --- a/packages/apps-engine/src/server/compiler/AppFabricationFulfillment.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { IAppInfo } from '../../definition/metadata'; -import type { ProxiedApp } from '../ProxiedApp'; -import { AppLicenseValidationResult } from '../marketplace/license'; - -export class AppFabricationFulfillment { - public info: IAppInfo; - - public app: ProxiedApp; - - public implemented: { [int: string]: boolean }; - - public licenseValidationResult: AppLicenseValidationResult; - - public storageError: string; - - public appUserError: object; - - constructor() { - this.licenseValidationResult = new AppLicenseValidationResult(); - } - - public setAppInfo(information: IAppInfo): void { - this.info = structuredClone(information); - - this.licenseValidationResult.setAppId(information.id); - } - - public getAppInfo(): IAppInfo { - return this.info; - } - - public setApp(application: ProxiedApp): void { - this.app = application; - } - - public getApp(): ProxiedApp { - return this.app; - } - - public setImplementedInterfaces(interfaces: { [int: string]: boolean }): void { - this.implemented = structuredClone(interfaces); - } - - public getImplementedInferfaces(): { [int: string]: boolean } { - return this.implemented; - } - - public setStorageError(errorMessage: string): void { - this.storageError = errorMessage; - } - - public setAppUserError(error: object): void { - this.appUserError = error; - } - - public getStorageError(): string { - return this.storageError; - } - - public getAppUserError(): object { - return this.appUserError; - } - - public hasStorageError(): boolean { - return !!this.storageError; - } - - public hasAppUserError(): boolean { - return !!this.appUserError; - } - - public getLicenseValidationResult(): AppLicenseValidationResult { - return this.licenseValidationResult; - } -} diff --git a/packages/apps-engine/src/server/compiler/AppImplements.ts b/packages/apps-engine/src/server/compiler/AppImplements.ts deleted file mode 100644 index ba9be27b678bc..0000000000000 --- a/packages/apps-engine/src/server/compiler/AppImplements.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { AppInterface } from '../../definition/metadata/AppInterface'; -import { Utilities } from '../misc/Utilities'; - -export class AppImplements { - private implemented: Record; - - constructor() { - this.implemented = {} as Record; - - Object.keys(AppInterface).forEach((int: AppInterface) => { - this.implemented[int] = false; - }); - } - - public setImplements(int: AppInterface): void { - if (int in AppInterface) { - this.implemented[int] = true; - } - } - - public doesImplement(int: AppInterface): boolean { - return this.implemented[int]; - } - - public getValues(): Record { - return Utilities.deepCloneAndFreeze(this.implemented); - } - - public toJSON(): Record { - return this.getValues(); - } -} diff --git a/packages/apps-engine/src/server/compiler/AppPackageParser.ts b/packages/apps-engine/src/server/compiler/AppPackageParser.ts deleted file mode 100644 index 3d3af9c210806..0000000000000 --- a/packages/apps-engine/src/server/compiler/AppPackageParser.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as path from 'path'; - -import * as AdmZip from 'adm-zip'; -import * as semver from 'semver'; -import { v4 as uuidv4 } from 'uuid'; - -import { AppImplements } from '.'; -import type { IAppInfo } from '../../definition/metadata/IAppInfo'; -import { ENGINE_VERSION } from '../../definition/version'; -import { RequiredApiVersionError } from '../errors'; -import type { IParseAppPackageResult } from './IParseAppPackageResult'; - -export class AppPackageParser { - public static uuid4Regex = /^[0-9a-fA-f]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/; - - private allowedIconExts: Array = ['.png', '.jpg', '.jpeg', '.gif']; - - private appsEngineVersion: string = ENGINE_VERSION; - - public async unpackageApp(appPackage: Buffer): Promise { - const zip = new AdmZip(appPackage); - const infoZip = zip.getEntry('app.json'); - let info: IAppInfo; - - if (infoZip && !infoZip.isDirectory) { - try { - info = JSON.parse(infoZip.getData().toString()) as IAppInfo; - - if (!AppPackageParser.uuid4Regex.test(info.id)) { - info.id = uuidv4(); - console.warn( - 'WARNING: We automatically generated a uuid v4 id for', - info.name, - 'since it did not provide us an id. This is NOT', - 'recommended as the same App can be installed several times.', - ); - } - } catch (e) { - throw new Error('Invalid App package. The "app.json" file is not valid json.'); - } - } else { - throw new Error('Invalid App package. No "app.json" file.'); - } - - info.classFile = info.classFile.replace('.ts', '.js'); - - if (!semver.satisfies(this.appsEngineVersion, info.requiredApiVersion)) { - throw new RequiredApiVersionError(info, this.appsEngineVersion); - } - - // Load all of the TypeScript only files - const files: { [s: string]: string } = {}; - - zip - .getEntries() - .filter((entry) => !entry.isDirectory && entry.entryName.endsWith('.js')) - .forEach((entry) => { - const norm = path.normalize(entry.entryName); - - // Files which start with `.` are supposed to be hidden - if (norm.startsWith('.')) { - return; - } - - files[norm] = entry.getData().toString(); - }); - - // Ensure that the main class file exists - if (!files[path.normalize(info.classFile)]) { - throw new Error(`Invalid App package. Could not find the classFile (${info.classFile}) file.`); - } - - const languageContent = this.getLanguageContent(zip); - - // Get the icon's content - const iconFile = this.getIconFile(zip, info.iconFile); - if (iconFile) { - info.iconFileContent = iconFile; - } - - const implemented = new AppImplements(); - - if (Array.isArray(info.implements)) { - info.implements.forEach((interfaceName) => implemented.setImplements(interfaceName)); - } - - return { - info, - files, - languageContent, - implemented, - }; - } - - private getLanguageContent(zip: AdmZip): { [key: string]: object } { - const languageContent: { [key: string]: object } = {}; - - zip - .getEntries() - .filter((entry) => !entry.isDirectory && entry.entryName.startsWith('i18n/') && entry.entryName.endsWith('.json')) - .forEach((entry) => { - const entrySplit = entry.entryName.split('/'); - const lang = entrySplit[entrySplit.length - 1].split('.')[0].toLowerCase(); - - let content; - try { - content = JSON.parse(entry.getData().toString()); - } catch (e) { - // Failed to parse it, maybe warn them? idk yet - } - - languageContent[lang] = Object.assign(languageContent[lang] || {}, content); - }); - - return languageContent; - } - - private getIconFile(zip: AdmZip, filePath: string): string { - if (!filePath) { - return undefined; - } - - const ext = path.extname(filePath); - if (!this.allowedIconExts.includes(ext)) { - return undefined; - } - - const entry = zip.getEntry(filePath); - - if (!entry) { - return undefined; - } - - if (entry.isDirectory) { - return undefined; - } - - const base64 = entry.getData().toString('base64'); - - return `data:image/${ext.replace('.', '')};base64,${base64}`; - } - -} diff --git a/packages/apps-engine/src/server/compiler/IParseAppPackageResult.ts b/packages/apps-engine/src/server/compiler/IParseAppPackageResult.ts deleted file mode 100644 index 7c21bcb266d15..0000000000000 --- a/packages/apps-engine/src/server/compiler/IParseAppPackageResult.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { AppImplements } from './AppImplements'; -import type { IAppInfo } from '../../definition/metadata'; - -export interface IParseAppPackageResult { - info: IAppInfo; - files: { [key: string]: string }; - languageContent: { [key: string]: object }; - implemented: AppImplements; -} diff --git a/packages/apps-engine/src/server/compiler/index.ts b/packages/apps-engine/src/server/compiler/index.ts deleted file mode 100644 index 466be8c786bf5..0000000000000 --- a/packages/apps-engine/src/server/compiler/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { AppCompiler } from './AppCompiler'; -import { AppFabricationFulfillment } from './AppFabricationFulfillment'; -import { AppImplements } from './AppImplements'; -import { AppPackageParser } from './AppPackageParser'; -import { IParseAppPackageResult } from './IParseAppPackageResult'; - -export { AppCompiler, AppFabricationFulfillment, AppImplements, AppPackageParser, IParseAppPackageResult }; diff --git a/packages/apps-engine/src/server/compiler/modules/index.ts b/packages/apps-engine/src/server/compiler/modules/index.ts deleted file mode 100644 index 31cbec5af30c9..0000000000000 --- a/packages/apps-engine/src/server/compiler/modules/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { moduleHandlerFactory } from './networking'; - -export enum AllowedInternalModules { - path = 'path', - url = 'url', - crypto = 'crypto', - buffer = 'buffer', - stream = 'stream', - net = 'net', - http = 'http', - https = 'https', - zlib = 'zlib', - util = 'util', - punycode = 'punycode', - os = 'os', - querystring = 'querystring', -} - -export class ForbiddenNativeModuleAccess extends Error { - constructor(module: string, prop: string) { - super(`Access to property ${prop} in module ${module} is forbidden`); - } -} - -const defaultHandler = () => ({}); - -const noopHandler = () => ({ - get: (): undefined => undefined, -}); - -const proxyHandlers = { - path: defaultHandler, - url: defaultHandler, - crypto: defaultHandler, - buffer: defaultHandler, - stream: defaultHandler, - net: moduleHandlerFactory('net'), - http: moduleHandlerFactory('http'), - https: moduleHandlerFactory('https'), - zlib: defaultHandler, - util: defaultHandler, - punycode: defaultHandler, - os: noopHandler, - querystring: defaultHandler, -}; - -export function requireNativeModule(module: AllowedInternalModules, appId: string, requirer: any) { - const requiredModule = requirer(module); - - return new Proxy( - requiredModule, - // Creates a proxy handler that is aware of the appId requiring the module - Reflect.apply(proxyHandlers[module], undefined, [appId]), - ); -} diff --git a/packages/apps-engine/src/server/compiler/modules/networking.ts b/packages/apps-engine/src/server/compiler/modules/networking.ts deleted file mode 100644 index d8ed3c8e744b7..0000000000000 --- a/packages/apps-engine/src/server/compiler/modules/networking.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type * as http from 'http'; -import type * as https from 'https'; -import type * as net from 'net'; - -import { ForbiddenNativeModuleAccess } from '.'; -import { PermissionDeniedError } from '../../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../../managers/AppPermissionManager'; -import { AppPermissions } from '../../permissions/AppPermissions'; - -type IHttp = typeof http; -type IHttps = typeof https; -type INet = typeof net; - -type NetworkingLibs = IHttp | IHttps | INet; - -const networkingModuleBlockList = ['createServer', 'Server']; - -export const moduleHandlerFactory = (module: string) => { - return (appId: string): ProxyHandler => ({ - get(target, prop: string, receiver) { - if (networkingModuleBlockList.includes(prop)) { - throw new ForbiddenNativeModuleAccess(module, prop); - } - - if (!AppPermissionManager.hasPermission(appId, AppPermissions.networking.default)) { - throw new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.networking.default], - methodName: `${module}.${prop}`, - }); - } - - return Reflect.get(target, prop, receiver); - }, - }); -}; diff --git a/packages/apps-engine/src/server/errors/AppOutboundProcessError.ts b/packages/apps-engine/src/server/errors/AppOutboundProcessError.ts deleted file mode 100644 index a7e905ae90b90..0000000000000 --- a/packages/apps-engine/src/server/errors/AppOutboundProcessError.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class AppOutboundProcessError implements Error { - public name = 'OutboundProviderError'; - - public message: string; - - constructor(message: string, where?: string) { - this.message = message; - if (where) { - this.message += ` (${where})`; - } - } -} diff --git a/packages/apps-engine/src/server/errors/CommandAlreadyExistsError.ts b/packages/apps-engine/src/server/errors/CommandAlreadyExistsError.ts deleted file mode 100644 index 8c49b4315e083..0000000000000 --- a/packages/apps-engine/src/server/errors/CommandAlreadyExistsError.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class CommandAlreadyExistsError implements Error { - public name = 'CommandAlreadyExists'; - - public message: string; - - constructor(command: string) { - this.message = `The command "${command}" already exists in the system.`; - } -} diff --git a/packages/apps-engine/src/server/errors/CommandHasAlreadyBeenTouchedError.ts b/packages/apps-engine/src/server/errors/CommandHasAlreadyBeenTouchedError.ts deleted file mode 100644 index bbc0e39c02cea..0000000000000 --- a/packages/apps-engine/src/server/errors/CommandHasAlreadyBeenTouchedError.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class CommandHasAlreadyBeenTouchedError implements Error { - public name = 'CommandHasAlreadyBeenTouched'; - - public message: string; - - constructor(command: string) { - this.message = `The command "${command}" has already been touched by another App.`; - } -} diff --git a/packages/apps-engine/src/server/errors/CompilerError.ts b/packages/apps-engine/src/server/errors/CompilerError.ts deleted file mode 100644 index 3d71ad50d443a..0000000000000 --- a/packages/apps-engine/src/server/errors/CompilerError.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class CompilerError implements Error { - public name = 'CompilerError'; - - public message: string; - - constructor(detail: string) { - this.message = `An error occured while compiling an App: ${detail}`; - } -} diff --git a/packages/apps-engine/src/server/errors/InvalidInstallationError.ts b/packages/apps-engine/src/server/errors/InvalidInstallationError.ts deleted file mode 100644 index 4388261c17c91..0000000000000 --- a/packages/apps-engine/src/server/errors/InvalidInstallationError.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class InvalidInstallationError extends Error { - public constructor(message: string) { - super(`Invalid app installation: ${message}`); - } -} diff --git a/packages/apps-engine/src/server/errors/InvalidLicenseError.ts b/packages/apps-engine/src/server/errors/InvalidLicenseError.ts deleted file mode 100644 index 4ab70cdfc4dcf..0000000000000 --- a/packages/apps-engine/src/server/errors/InvalidLicenseError.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { AppLicenseValidationResult } from '../marketplace/license/AppLicenseValidationResult'; - -export class InvalidLicenseError extends Error { - public constructor(public readonly validationResult: AppLicenseValidationResult) { - super('Invalid app license'); - } -} diff --git a/packages/apps-engine/src/server/errors/MustContainFunctionError.ts b/packages/apps-engine/src/server/errors/MustContainFunctionError.ts deleted file mode 100644 index bbe2d56a3d1cf..0000000000000 --- a/packages/apps-engine/src/server/errors/MustContainFunctionError.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class MustContainFunctionError implements Error { - public name = 'MustContainFunction'; - - public message: string; - - constructor(fileName: string, funcName: string) { - this.message = `The App (${fileName}) doesn't have a "${funcName}" function which is required.`; - } -} diff --git a/packages/apps-engine/src/server/errors/MustExtendAppError.ts b/packages/apps-engine/src/server/errors/MustExtendAppError.ts deleted file mode 100644 index bdb2fb3f21a56..0000000000000 --- a/packages/apps-engine/src/server/errors/MustExtendAppError.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class MustExtendAppError implements Error { - public name = 'MustExtendApp'; - - public message = 'App must extend the "App" abstract class.'; -} diff --git a/packages/apps-engine/src/server/errors/NotEnoughMethodArgumentsError.ts b/packages/apps-engine/src/server/errors/NotEnoughMethodArgumentsError.ts deleted file mode 100644 index 9fc6a01bfca1f..0000000000000 --- a/packages/apps-engine/src/server/errors/NotEnoughMethodArgumentsError.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class NotEnoughMethodArgumentsError implements Error { - public readonly name: string = 'NotEnoughMethodArgumentsError'; - - public readonly message: string; - - constructor(method: string, requiredCount: number, providedCount: number) { - this.message = `The method "${method}" requires ${requiredCount} parameters but was only passed ${providedCount}.`; - } -} diff --git a/packages/apps-engine/src/server/errors/PathAlreadyExistsError.ts b/packages/apps-engine/src/server/errors/PathAlreadyExistsError.ts deleted file mode 100644 index 9f2a7a671b3c5..0000000000000 --- a/packages/apps-engine/src/server/errors/PathAlreadyExistsError.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class PathAlreadyExistsError implements Error { - public name = 'PathAlreadyExists'; - - public message: string; - - constructor(path: string) { - this.message = `The api path "${path}" already exists in the system.`; - } -} diff --git a/packages/apps-engine/src/server/errors/PermissionDeniedError.ts b/packages/apps-engine/src/server/errors/PermissionDeniedError.ts deleted file mode 100644 index 37ca3af77a1d8..0000000000000 --- a/packages/apps-engine/src/server/errors/PermissionDeniedError.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { IPermission } from '../../definition/permissions/IPermission'; - -interface IPermissionDeniedErrorParams { - appId: string; - missingPermissions: Array; - methodName?: string; - reason?: string; - message?: string; -} - -export class PermissionDeniedError extends Error { - constructor({ appId, missingPermissions, methodName, reason, message }: IPermissionDeniedErrorParams) { - if (message) { - super(message); - } else { - const permissions = missingPermissions.map((permission) => `"${permission.name}"`).join(', '); - - super( - `Failed to call the method ${methodName ? `"${methodName}"` : ''} as the app (${appId}) lacks the following permissions:\n` + - `[${permissions}]. Declare them in your app.json to fix the issue.\n` + - `reason: ${reason}`, - ); - } - } -} diff --git a/packages/apps-engine/src/server/errors/RequiredApiVersionError.ts b/packages/apps-engine/src/server/errors/RequiredApiVersionError.ts deleted file mode 100644 index 832386ebfc66b..0000000000000 --- a/packages/apps-engine/src/server/errors/RequiredApiVersionError.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as semver from 'semver'; - -import type { IAppInfo } from '../../definition/metadata'; - -export class RequiredApiVersionError implements Error { - public name = 'RequiredApiVersion'; - - public message: string; - - constructor(info: IAppInfo, versionInstalled: string) { - let moreInfo = ''; - if (semver.gt(versionInstalled, info.requiredApiVersion)) { - moreInfo = ' Please tell the author to update their App as it is out of date.'; - } - - this.message = - `Failed to load the App "${info.name}" (${info.id}) as it requires ` + - `v${info.requiredApiVersion} of the App API however your server comes with ` + - `v${versionInstalled}.${moreInfo}`; - } -} diff --git a/packages/apps-engine/src/server/errors/VideoConfProviderAlreadyExistsError.ts b/packages/apps-engine/src/server/errors/VideoConfProviderAlreadyExistsError.ts deleted file mode 100644 index 934d2a35dd3ca..0000000000000 --- a/packages/apps-engine/src/server/errors/VideoConfProviderAlreadyExistsError.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class VideoConfProviderAlreadyExistsError implements Error { - public name = 'VideoConfProviderAlreadyExists'; - - public message: string; - - constructor(name: string) { - this.message = `The video conference provider "${name}" was already registered by another App.`; - } -} diff --git a/packages/apps-engine/src/server/errors/VideoConfProviderNotRegisteredError.ts b/packages/apps-engine/src/server/errors/VideoConfProviderNotRegisteredError.ts deleted file mode 100644 index 9ba854f9ea74e..0000000000000 --- a/packages/apps-engine/src/server/errors/VideoConfProviderNotRegisteredError.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class VideoConfProviderNotRegisteredError implements Error { - public name = 'VideoConfProviderNotRegistered'; - - public message: string; - - constructor(providerName: string) { - this.message = `The video conference provider "${providerName}" is not registered in the system.`; - } -} diff --git a/packages/apps-engine/src/server/errors/index.ts b/packages/apps-engine/src/server/errors/index.ts deleted file mode 100644 index f93385e5abb9a..0000000000000 --- a/packages/apps-engine/src/server/errors/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { CommandAlreadyExistsError } from './CommandAlreadyExistsError'; -import { CommandHasAlreadyBeenTouchedError } from './CommandHasAlreadyBeenTouchedError'; -import { CompilerError } from './CompilerError'; -import { InvalidLicenseError } from './InvalidLicenseError'; -import { MustContainFunctionError } from './MustContainFunctionError'; -import { MustExtendAppError } from './MustExtendAppError'; -import { NotEnoughMethodArgumentsError } from './NotEnoughMethodArgumentsError'; -import { PathAlreadyExistsError } from './PathAlreadyExistsError'; -import { RequiredApiVersionError } from './RequiredApiVersionError'; -import { VideoConfProviderAlreadyExistsError } from './VideoConfProviderAlreadyExistsError'; -import { VideoConfProviderNotRegisteredError } from './VideoConfProviderNotRegisteredError'; - -export { - CommandAlreadyExistsError, - CommandHasAlreadyBeenTouchedError, - PathAlreadyExistsError, - CompilerError, - MustContainFunctionError, - MustExtendAppError, - NotEnoughMethodArgumentsError, - RequiredApiVersionError, - InvalidLicenseError, - VideoConfProviderAlreadyExistsError, - VideoConfProviderNotRegisteredError, -}; diff --git a/packages/apps-engine/src/server/logging/AppConsole.ts b/packages/apps-engine/src/server/logging/AppConsole.ts deleted file mode 100644 index 2ae4b5cc1595c..0000000000000 --- a/packages/apps-engine/src/server/logging/AppConsole.ts +++ /dev/null @@ -1,121 +0,0 @@ -import * as stackTrace from 'stack-trace'; - -import type { ILoggerStorageEntry } from './ILoggerStorageEntry'; -import type { ILogEntry, ILogger } from '../../definition/accessors'; -import { LogMessageSeverity } from '../../definition/accessors'; -import type { AppMethod } from '../../definition/metadata'; - -export class AppConsole implements ILogger { - public static toStorageEntry(appId: string, logger: AppConsole): ILoggerStorageEntry { - return { - appId, - method: logger.getMethod(), - entries: logger.getEntries(), - startTime: logger.getStartTime(), - endTime: logger.getEndTime(), - totalTime: logger.getTotalTime(), - _createdAt: new Date(), - }; - } - - public method: `${AppMethod}`; - - private entries: Array; - - private start: Date; - - constructor(method: `${AppMethod}`) { - this.method = method; - this.entries = []; - this.start = new Date(); - } - - public debug(...items: Array): void { - this.addEntry(LogMessageSeverity.DEBUG, this.getFunc(stackTrace.get()), ...items); - } - - public info(...items: Array): void { - this.addEntry(LogMessageSeverity.INFORMATION, this.getFunc(stackTrace.get()), ...items); - } - - public log(...items: Array): void { - this.addEntry(LogMessageSeverity.LOG, this.getFunc(stackTrace.get()), ...items); - } - - public warn(...items: Array): void { - this.addEntry(LogMessageSeverity.WARNING, this.getFunc(stackTrace.get()), ...items); - } - - public error(...items: Array): void { - this.addEntry(LogMessageSeverity.ERROR, this.getFunc(stackTrace.get()), ...items); - } - - public success(...items: Array): void { - this.addEntry(LogMessageSeverity.SUCCESS, this.getFunc(stackTrace.get()), ...items); - } - - public getEntries(): Array { - return Array.from(this.entries); - } - - public getMethod(): `${AppMethod}` { - return this.method; - } - - public getStartTime(): Date { - return this.start; - } - - public getEndTime(): Date { - return new Date(); - } - - public getTotalTime(): number { - return this.getEndTime().getTime() - this.getStartTime().getTime(); - } - - private addEntry(severity: LogMessageSeverity, caller: string, ...items: Array): void { - const i = items.map((v) => { - if (v instanceof Error) { - return JSON.stringify(v, Object.getOwnPropertyNames(v)); - } - if (typeof v === 'object' && typeof v.stack === 'string' && typeof v.message === 'string') { - return JSON.stringify(v, Object.getOwnPropertyNames(v)); - } - const str = JSON.stringify(v, null, 2); - return str ? JSON.parse(str) : str; // force call toJSON to prevent circular references - }); - - this.entries.push({ - caller, - severity, - timestamp: new Date(), - args: i, - }); - - // This should be a setting? :thinking: - // console.log(`${ severity.toUpperCase() }:`, i); - } - - private getFunc(stack: Array): string { - let func = 'anonymous'; - - if (stack.length === 1) { - return func; - } - - const frame: stackTrace.StackFrame = stack[1]; - - if (frame.getMethodName() === null) { - func = 'anonymous OR constructor'; - } else { - func = frame.getMethodName(); - } - - if (frame.getFunctionName() !== null) { - func = `${func} -> ${frame.getFunctionName()}`; - } - - return func; - } -} diff --git a/packages/apps-engine/src/server/logging/ILoggerStorageEntry.ts b/packages/apps-engine/src/server/logging/ILoggerStorageEntry.ts deleted file mode 100644 index 6ec91157454b1..0000000000000 --- a/packages/apps-engine/src/server/logging/ILoggerStorageEntry.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ILogEntry } from '../../definition/accessors'; -import type { AppMethod } from '../../definition/metadata'; - -export interface ILoggerStorageEntry { - appId: string; - method: `${AppMethod}`; - entries: Array; - startTime: Date; - endTime: Date; - totalTime: number; - instanceId?: string; - // Internal value to be used for sorting - _createdAt: Date; -} diff --git a/packages/apps-engine/src/server/logging/index.ts b/packages/apps-engine/src/server/logging/index.ts deleted file mode 100644 index e17cdd6e9e6c2..0000000000000 --- a/packages/apps-engine/src/server/logging/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { AppConsole } from './AppConsole'; -import { ILoggerStorageEntry } from './ILoggerStorageEntry'; - -export { AppConsole, ILoggerStorageEntry }; diff --git a/packages/apps-engine/src/server/managers/AppAccessorManager.ts b/packages/apps-engine/src/server/managers/AppAccessorManager.ts deleted file mode 100644 index 9247d83195c86..0000000000000 --- a/packages/apps-engine/src/server/managers/AppAccessorManager.ts +++ /dev/null @@ -1,251 +0,0 @@ -import type { - IConfigurationExtend, - IConfigurationModify, - IEnvironmentRead, - IEnvironmentWrite, - IHttp, - IHttpExtend, - IModify, - IPersistence, - IRead, -} from '../../definition/accessors'; -import type { AppManager } from '../AppManager'; -import { - ApiExtend, - ConfigurationExtend, - ConfigurationModify, - EnvironmentalVariableRead, - EnvironmentRead, - EnvironmentWrite, - ExternalComponentsExtend, - Http, - HttpExtend, - LivechatRead, - MessageRead, - Modify, - Notifier, - OAuthAppsReader, - OutboundMessageProviderExtend, - Persistence, - PersistenceRead, - Reader, - RoleRead, - RoomRead, - SchedulerExtend, - SchedulerModify, - ServerSettingRead, - ServerSettingsModify, - ServerSettingUpdater, - SettingRead, - SettingsExtend, - SettingUpdater, - SlashCommandsExtend, - SlashCommandsModify, - UploadRead, - UserRead, - VideoConferenceRead, - VideoConfProviderExtend, -} from '../accessors'; -import { CloudWorkspaceRead } from '../accessors/CloudWorkspaceRead'; -import { ContactRead } from '../accessors/ContactRead'; -import { ExperimentalRead } from '../accessors/ExperimentalRead'; -import { ThreadRead } from '../accessors/ThreadRead'; -import { UIExtend } from '../accessors/UIExtend'; -import type { AppBridges } from '../bridges/AppBridges'; - -export class AppAccessorManager { - private readonly bridges: AppBridges; - - private readonly configExtenders: Map; - - private readonly envReaders: Map; - - private readonly envWriters: Map; - - private readonly configModifiers: Map; - - private readonly readers: Map; - - private readonly modifiers: Map; - - private readonly persists: Map; - - private readonly https: Map; - - constructor(private readonly manager: AppManager) { - this.bridges = this.manager.getBridges(); - this.configExtenders = new Map(); - this.envReaders = new Map(); - this.envWriters = new Map(); - this.configModifiers = new Map(); - this.readers = new Map(); - this.modifiers = new Map(); - this.persists = new Map(); - this.https = new Map(); - } - - /** - * Purifies the accessors for the provided App. - * - * @param appId The id of the App to purge the accessors for. - */ - public purifyApp(appId: string): void { - this.configExtenders.delete(appId); - this.envReaders.delete(appId); - this.envWriters.delete(appId); - this.configModifiers.delete(appId); - this.readers.delete(appId); - this.modifiers.delete(appId); - this.persists.delete(appId); - this.https.delete(appId); - } - - public getConfigurationExtend(appId: string): IConfigurationExtend { - if (!this.configExtenders.has(appId)) { - const rl = this.manager.getOneById(appId); - - if (!rl) { - throw new Error(`No App found by the provided id: ${appId}`); - } - - const htt = new HttpExtend(); - const cmds = new SlashCommandsExtend(this.manager.getCommandManager(), appId); - const videoConf = new VideoConfProviderExtend(this.manager.getVideoConfProviderManager(), appId); - const apis = new ApiExtend(this.manager.getApiManager(), appId); - const sets = new SettingsExtend(rl); - const excs = new ExternalComponentsExtend(this.manager.getExternalComponentManager(), appId); - const scheduler = new SchedulerExtend(this.manager.getSchedulerManager(), appId); - const ui = new UIExtend(this.manager.getUIActionButtonManager(), appId); - const outboundComms = new OutboundMessageProviderExtend(this.manager.getOutboundCommunicationProviderManager(), appId); - - this.configExtenders.set(appId, new ConfigurationExtend(htt, sets, cmds, apis, excs, scheduler, ui, videoConf, outboundComms)); - } - - return this.configExtenders.get(appId); - } - - public getEnvironmentRead(appId: string): IEnvironmentRead { - if (!this.envReaders.has(appId)) { - const rl = this.manager.getOneById(appId); - - if (!rl) { - throw new Error(`No App found by the provided id: ${appId}`); - } - - const sets = new SettingRead(rl); - const servsets = new ServerSettingRead(this.bridges.getServerSettingBridge(), appId); - const env = new EnvironmentalVariableRead(this.bridges.getEnvironmentalVariableBridge(), appId); - - this.envReaders.set(appId, new EnvironmentRead(sets, servsets, env)); - } - - return this.envReaders.get(appId); - } - - public getEnvironmentWrite(appId: string): IEnvironmentWrite { - if (!this.envWriters.has(appId)) { - const rl = this.manager.getOneById(appId); - - if (!rl) { - throw new Error(`No App found by the provided id: ${appId}`); - } - - const sets = new SettingUpdater(rl, this.manager.getSettingsManager()); - const serverSetting = new ServerSettingUpdater(this.bridges, appId); - - this.envWriters.set(appId, new EnvironmentWrite(sets, serverSetting)); - } - - return this.envWriters.get(appId); - } - - public getConfigurationModify(appId: string): IConfigurationModify { - if (!this.configModifiers.has(appId)) { - this.configModifiers.set( - appId, - new ConfigurationModify( - new ServerSettingsModify(this.bridges.getServerSettingBridge(), appId), - new SlashCommandsModify(this.manager.getCommandManager(), appId), - new SchedulerModify(this.bridges.getSchedulerBridge(), appId), - ), - ); - } - - return this.configModifiers.get(appId); - } - - public getReader(appId: string): IRead { - if (!this.readers.has(appId)) { - const env = this.getEnvironmentRead(appId); - const msg = new MessageRead(this.bridges.getMessageBridge(), appId); - const persist = new PersistenceRead(this.bridges.getPersistenceBridge(), appId); - const room = new RoomRead(this.bridges.getRoomBridge(), appId); - const user = new UserRead(this.bridges.getUserBridge(), appId); - const noti = new Notifier(this.bridges.getUserBridge(), this.bridges.getMessageBridge(), appId); - const livechat = new LivechatRead(this.bridges.getLivechatBridge(), appId); - const upload = new UploadRead(this.bridges.getUploadBridge(), appId); - const cloud = new CloudWorkspaceRead(this.bridges.getCloudWorkspaceBridge(), appId); - const videoConf = new VideoConferenceRead(this.bridges.getVideoConferenceBridge(), appId); - const oauthApps = new OAuthAppsReader(this.bridges.getOAuthAppsBridge(), appId); - const contactReader = new ContactRead(this.bridges, appId); - const thread = new ThreadRead(this.bridges.getThreadBridge(), appId); - const role = new RoleRead(this.bridges.getRoleBridge(), appId); - const experimental = new ExperimentalRead(this.bridges.getExperimentalBridge(), appId); - - this.readers.set( - appId, - new Reader( - env, - msg, - persist, - room, - user, - noti, - livechat, - upload, - cloud, - videoConf, - contactReader, - oauthApps, - thread, - role, - experimental, - ), - ); - } - - return this.readers.get(appId); - } - - public getModifier(appId: string): IModify { - if (!this.modifiers.has(appId)) { - this.modifiers.set(appId, new Modify(this.bridges, appId)); - } - - return this.modifiers.get(appId); - } - - public getPersistence(appId: string): IPersistence { - if (!this.persists.has(appId)) { - this.persists.set(appId, new Persistence(this.bridges.getPersistenceBridge(), appId)); - } - - return this.persists.get(appId); - } - - public getHttp(appId: string): IHttp { - if (!this.https.has(appId)) { - let ext: IHttpExtend; - if (this.configExtenders.has(appId)) { - ext = this.configExtenders.get(appId).http; - } else { - const cf = this.getConfigurationExtend(appId); - ext = cf.http; - } - - this.https.set(appId, new Http(this, this.bridges, ext, appId)); - } - - return this.https.get(appId); - } -} diff --git a/packages/apps-engine/src/server/managers/AppApi.ts b/packages/apps-engine/src/server/managers/AppApi.ts deleted file mode 100644 index 78d551fe0a50e..0000000000000 --- a/packages/apps-engine/src/server/managers/AppApi.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { IApi, IApiRequest, IApiResponse } from '../../definition/api'; -import { ApiSecurity, ApiVisibility } from '../../definition/api'; -import type { IApiEndpoint } from '../../definition/api/IApiEndpoint'; -import type { IApiEndpointInfo } from '../../definition/api/IApiEndpointInfo'; -import type { ProxiedApp } from '../ProxiedApp'; -import type { AppLogStorage } from '../storage'; -import type { AppAccessorManager } from './AppAccessorManager'; - -export class AppApi { - public readonly computedPath: string; - - public readonly basePath: string; - - public readonly appId: string; - - public readonly hash?: string; - - public readonly implementedMethods: Array; - - constructor( - public app: ProxiedApp, - public api: IApi, - public endpoint: IApiEndpoint, - ) { - this.appId = app.getID(); - - switch (this.api.visibility) { - case ApiVisibility.PUBLIC: - this.basePath = `/api/apps/public/${app.getID()}`; - break; - - case ApiVisibility.PRIVATE: - this.basePath = `/api/apps/private/${app.getID()}/${app.getStorageItem()._id}`; - this.hash = app.getStorageItem()._id; - break; - } - - this.computedPath = `${this.basePath}/${endpoint.path}`; - - this.implementedMethods = endpoint._availableMethods; - } - - public async runExecutor(request: IApiRequest, logStorage: AppLogStorage, accessors: AppAccessorManager): Promise { - const { path } = this.endpoint; - - const { method } = request; - - if (!this.validateVisibility(request)) { - return { - status: 404, - }; - } - - if (!this.validateSecurity(request)) { - return { - status: 401, - }; - } - - const endpoint: IApiEndpointInfo = { - basePath: this.basePath, - fullPath: this.computedPath, - appId: this.appId, - hash: this.hash, - }; - - try { - const result = await this.app.getRuntimeController().sendRequest({ - method: `api:${path}:${method}`, - params: [request, endpoint], - }); - - return result as IApiResponse; - } catch (e) { - console.error(e); - throw e; - } - } - - private validateVisibility(request: IApiRequest): boolean { - if (this.api.visibility === ApiVisibility.PUBLIC) { - return true; - } - - if (this.api.visibility === ApiVisibility.PRIVATE) { - return this.app.getStorageItem()._id === request.privateHash; - } - - return false; - } - - private validateSecurity(request: IApiRequest): boolean { - if (this.api.security === ApiSecurity.UNSECURE) { - return true; - } - - return false; - } -} diff --git a/packages/apps-engine/src/server/managers/AppApiManager.ts b/packages/apps-engine/src/server/managers/AppApiManager.ts deleted file mode 100644 index 71cdb569aaec5..0000000000000 --- a/packages/apps-engine/src/server/managers/AppApiManager.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { AppStatusUtils } from '../../definition/AppStatus'; -import { HttpStatusCode } from '../../definition/accessors'; -import type { IApi, IApiEndpointMetadata, IApiRequest, IApiResponse } from '../../definition/api'; -import type { AppManager } from '../AppManager'; -import type { ApiBridge } from '../bridges'; -import { PathAlreadyExistsError } from '../errors'; -import type { AppAccessorManager } from './AppAccessorManager'; -import { AppApi } from './AppApi'; - -/** - * The api manager for the Apps. - * - * An App will add api's during their `initialize` method. - * Then once an App's `onEnable` is called and it returns true, - * only then will that App's api's be enabled. - */ -export class AppApiManager { - private readonly bridge: ApiBridge; - - private readonly accessors: AppAccessorManager; - - // Variable that contains the api's which have been provided by apps. - // The key of the top map is app id and the key of the inner map is the path - private providedApis: Map>; - - constructor(private readonly manager: AppManager) { - this.bridge = this.manager.getBridges().getApiBridge(); - this.accessors = this.manager.getAccessorManager(); - this.providedApis = new Map>(); - } - - /** - * Adds an to *be* registered. This will *not register* it with the - * bridged system yet as this is only called on an App's - * `initialize` method and an App might not get enabled. - * When adding an api, it can *not* already exist in the system. - * - * @param appId the app's id which the api belongs to - * @param api the api to add to the system - */ - public addApi(appId: string, api: IApi): void { - if (api.endpoints.length === 0) { - throw new Error('Invalid Api parameter provided, endpoints must contain, at least, one IApiEndpoint.'); - } - - const app = this.manager.getOneById(appId); - if (!app) { - throw new Error('App must exist in order for an api to be added.'); - } - - // Verify the api's path doesn't exist already - if (this.providedApis.get(appId)) { - api.endpoints.forEach((endpoint) => { - if (this.providedApis.get(appId).has(endpoint.path)) { - throw new PathAlreadyExistsError(endpoint.path); - } - }); - } - - if (!this.providedApis.has(appId)) { - this.providedApis.set(appId, new Map()); - } - - api.endpoints.forEach((endpoint) => { - this.providedApis.get(appId).set(endpoint.path, new AppApi(app, api, endpoint)); - }); - } - - /** - * Registers all of the api's for the provided app inside - * of the bridged system which then enables them. - * - * @param appId The app's id of which to register it's api's with the bridged system - */ - public async registerApis(appId: string): Promise { - if (!this.providedApis.has(appId)) { - return; - } - - await this.bridge.doUnregisterApis(appId); - for await (const [, apiApp] of this.providedApis.get(appId).entries()) { - await this.registerApi(appId, apiApp); - } - } - - /** - * Unregisters the api's from the system. - * - * @param appId the appId for the api's to purge - */ - public async unregisterApis(appId: string): Promise { - if (this.providedApis.has(appId)) { - await this.bridge.doUnregisterApis(appId); - - this.providedApis.delete(appId); - } - } - - /** - * Executes an App's api. - * - * @param appId the app which is providing the api - * @param path the path to be executed in app's api's - * @param request the request data to be evaluated byt the app - */ - public async executeApi(appId: string, path: string, request: IApiRequest): Promise { - const api = this.providedApis.get(appId).get(path); - - if (!api) { - return { - status: HttpStatusCode.NOT_FOUND, - }; - } - - const app = this.manager.getOneById(appId); - - if (!app || AppStatusUtils.isDisabled(await app.getStatus())) { - // Just in case someone decides to do something they shouldn't - // let's ensure the app actually exists - return { - status: HttpStatusCode.NOT_FOUND, - }; - } - - return api.runExecutor(request, this.manager.getLogStorage(), this.accessors); - } - - /** - * Return a list of api's for a certain app - * - * @param appId the app which is providing the api - */ - public listApis(appId: string): Array { - const apis = this.providedApis.get(appId); - - if (!apis) { - return []; - } - - const result = []; - - for (const api of apis.values()) { - const metadata: IApiEndpointMetadata = { - path: api.endpoint.path, - computedPath: api.computedPath, - methods: api.implementedMethods, - examples: api.endpoint.examples || {}, - }; - - result.push(metadata); - } - - return result; - } - - /** - * Actually goes and provide's the bridged system with the api information. - * - * @param appId the app which is providing the api - * @param info the api's registration information - */ - private async registerApi(appId: string, api: AppApi): Promise { - await this.bridge.doRegisterApi(api, appId); - } -} diff --git a/packages/apps-engine/src/server/managers/AppExternalComponentManager.ts b/packages/apps-engine/src/server/managers/AppExternalComponentManager.ts deleted file mode 100644 index cb588cb8b873f..0000000000000 --- a/packages/apps-engine/src/server/managers/AppExternalComponentManager.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { IExternalComponent } from '../../definition/externalComponent'; - -/** - * The external component manager for the apps. - * - * An app will register external components during its `initialize` method. - * Then once an app's `onEnable` method is called and it returns true, - * only then will that app's external components be enabled. - */ -export class AppExternalComponentManager { - /** - * The map that maintains all registered components. - * The key of the top map is app id and the key of inner map is the - * external component name. - */ - private registeredExternalComponents: Map>; - - /** - * Contains the apps and the external components they have touhed. - * The key of the top map is app id and the key of inner map is the - * external component name. - * Doesn't matter whether the app provided, modified, disabled, - * or enabled. As long as an app touched external components, then - * they are listed here. - */ - private appTouchedExternalComponents: Map>; - - constructor() { - this.registeredExternalComponents = new Map>(); - this.appTouchedExternalComponents = new Map>(); - } - - /** - * Get all registered components. - */ - public getRegisteredExternalComponents(): Map> { - return this.registeredExternalComponents; - } - - /** - * Get all external components that apps have registered - * before, including disabled apps' external components. - */ - public getAppTouchedExternalComponents(): Map> { - return this.appTouchedExternalComponents; - } - - /** - * Get all external components of an app by specifying the appId. - * - * @param appId the id of the app - */ - public getExternalComponents(appId: string): Map { - if (this.appTouchedExternalComponents.has(appId)) { - return this.appTouchedExternalComponents.get(appId); - } - - return null; - } - - /** - * Get an array of external components which are enabled and ready for usage. - */ - public getProvidedComponents(): Array { - const registeredExternalComponents = this.getRegisteredExternalComponents(); - const providedComponents: Array = []; - - registeredExternalComponents.forEach((appExternalComponents) => { - Array.from(appExternalComponents.values()).forEach((externalComponent) => { - providedComponents.push(externalComponent); - }); - }); - - return providedComponents; - } - - /** - * Add an external component to the appTouchedExternalComponents. - * If you call this method twice and the component - * has the same name as before, the first one will be - * overwritten as the names provided **must** be unique. - * - * @param appId the id of the app - * @param externalComponent the external component about to be added - */ - public addExternalComponent(appId: string, externalComponent: IExternalComponent): void { - externalComponent.appId = appId; - - if (!this.appTouchedExternalComponents.get(appId)) { - this.appTouchedExternalComponents.set(appId, new Map(Object.entries({ [externalComponent.name]: externalComponent }))); - } else { - const appExternalComponents = this.appTouchedExternalComponents.get(appId); - - appExternalComponents.set(externalComponent.name, externalComponent); - } - } - - /** - * Add enabled apps' external components from the appTouchedExternalComponents - * to the registeredExternalComponents. - * - * @param appId the id of the app - */ - public registerExternalComponents(appId: string): void { - if (!this.appTouchedExternalComponents.has(appId)) { - return; - } - const externalComponents = this.appTouchedExternalComponents.get(appId); - - if (externalComponents.size > 0) { - this.registeredExternalComponents.set(appId, externalComponents); - } - } - - /** - * Remove all external components of an app from the - * registeredExternalComponents by specifying the appId. - * - * @param appId the id of the app - */ - public unregisterExternalComponents(appId: string): void { - if (this.registeredExternalComponents.has(appId)) { - this.registeredExternalComponents.delete(appId); - } - } - - /** - * Remove all external components of an app from both the - * registeredExternalComponents and the appTouchedComponents - * by specifying the appId. - * - * @param appId the id of the app - */ - public purgeExternalComponents(appId: string): void { - if (this.appTouchedExternalComponents.has(appId)) { - this.appTouchedExternalComponents.delete(appId); - } - if (this.registeredExternalComponents.has(appId)) { - this.registeredExternalComponents.delete(appId); - } - } -} diff --git a/packages/apps-engine/src/server/managers/AppLicenseManager.ts b/packages/apps-engine/src/server/managers/AppLicenseManager.ts deleted file mode 100644 index b72de6a5fd69a..0000000000000 --- a/packages/apps-engine/src/server/managers/AppLicenseManager.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { AppManager } from '../AppManager'; -import type { UserBridge } from '../bridges'; -import type { IInternalUserBridge } from '../bridges/IInternalUserBridge'; -import { InvalidLicenseError } from '../errors'; -import type { IMarketplaceInfo } from '../marketplace'; -import { MarketplacePurchaseType } from '../marketplace/MarketplacePurchaseType'; -import { Crypto } from '../marketplace/license'; -import type { AppLicenseValidationResult } from '../marketplace/license'; - -enum LicenseVersion { - v1 = 1, -} - -export class AppLicenseManager { - private readonly crypto: Crypto; - - private readonly userBridge: UserBridge; - - constructor(private readonly manager: AppManager) { - this.crypto = new Crypto(this.manager.getBridges().getInternalBridge()); - this.userBridge = this.manager.getBridges().getUserBridge(); - } - - public async validate(validationResult: AppLicenseValidationResult, appMarketplaceInfo?: IMarketplaceInfo[]): Promise { - const marketplaceInfo = appMarketplaceInfo?.[0]; - if (!marketplaceInfo || marketplaceInfo.purchaseType !== MarketplacePurchaseType.PurchaseTypeSubscription) { - return; - } - - validationResult.setValidated(true); - - const encryptedLicense = marketplaceInfo.subscriptionInfo.license.license; - - if (!encryptedLicense) { - validationResult.addError('license', 'License for app is invalid'); - - throw new InvalidLicenseError(validationResult); - } - - let license; - try { - license = (await this.crypto.decryptLicense(encryptedLicense)) as any; - } catch (err) { - validationResult.addError('publicKey', err.message); - - throw new InvalidLicenseError(validationResult); - } - - switch (license.version) { - case LicenseVersion.v1: - await this.validateV1(marketplaceInfo, license, validationResult); - break; - } - } - - private async validateV1( - appMarketplaceInfo: IMarketplaceInfo, - license: any, - validationResult: AppLicenseValidationResult, - ): Promise { - if (license.isBundle && !appMarketplaceInfo.bundledIn?.find((value) => value.bundleId === license.appId)) { - validationResult.addError('bundle', 'License issued for a bundle that does not contain the app'); - } else if (!license.isBundle && license.appId !== appMarketplaceInfo.id) { - validationResult.addError('appId', `License hasn't been issued for this app`); - } - - const renewal = new Date(license.renewalDate); - const expire = new Date(license.expireDate); - const now = new Date(); - - if (expire < now) { - validationResult.addError('expire', 'License is no longer valid and needs to be renewed'); - } - - const currentActiveUsers = await (this.userBridge as UserBridge & IInternalUserBridge).getActiveUserCount(); - - if (license.maxSeats < currentActiveUsers) { - validationResult.addError( - 'maxSeats', - 'License does not accomodate the current amount of active users. Please increase the number of seats', - ); - } - - if (validationResult.hasErrors) { - throw new InvalidLicenseError(validationResult); - } - - if (renewal < now) { - validationResult.addWarning('renewal', 'License has expired and needs to be renewed'); - } - - if (license.seats < currentActiveUsers) { - validationResult.addWarning( - 'seats', - 'License does not have enough seats to accommodate the current amount of active users. Please increase the number of seats', - ); - } - } -} diff --git a/packages/apps-engine/src/server/managers/AppListenerManager.ts b/packages/apps-engine/src/server/managers/AppListenerManager.ts deleted file mode 100644 index 327bc949ba0ac..0000000000000 --- a/packages/apps-engine/src/server/managers/AppListenerManager.ts +++ /dev/null @@ -1,1283 +0,0 @@ -import type { AppAccessorManager } from './AppAccessorManager'; -import type { IEmailDescriptor, IPreEmailSentContext } from '../../definition/email'; -import { EssentialAppDisabledException } from '../../definition/exceptions'; -import type { IExternalComponent } from '../../definition/externalComponent'; -import type { ILivechatEventContext, ILivechatRoom, ILivechatTransferEventContext, IVisitor } from '../../definition/livechat'; -import type { ILivechatDepartmentEventContext } from '../../definition/livechat/ILivechatEventContext'; -import type { - IMessage, - IMessageDeleteContext, - IMessageFollowContext, - IMessagePinContext, - IMessageReactionContext, - IMessageReportContext, - IMessageStarContext, -} from '../../definition/messages'; -import { AppInterface, AppMethod } from '../../definition/metadata'; -import type { IRoom, IRoomUserJoinedContext, IRoomUserLeaveContext } from '../../definition/rooms'; -import { RoomType } from '../../definition/rooms'; -import { UIActionButtonContext } from '../../definition/ui'; -import type { IUIKitResponse, IUIKitSurface, UIKitIncomingInteraction } from '../../definition/uikit'; -import { UIKitIncomingInteractionType } from '../../definition/uikit'; -import { isUIKitIncomingInteractionActionButtonMessageBox } from '../../definition/uikit/IUIKitIncomingInteractionActionButton'; -import type { - IUIKitIncomingInteractionMessageContainer, - IUIKitIncomingInteractionModalContainer, -} from '../../definition/uikit/UIKitIncomingInteractionContainer'; -import type { IUIKitLivechatBlockIncomingInteraction, IUIKitLivechatIncomingInteraction } from '../../definition/uikit/livechat'; -import type { IFileUploadInternalContext } from '../../definition/uploads/IFileUploadContext'; -import type { IUser, IUserContext, IUserStatusContext, IUserUpdateContext } from '../../definition/users'; -import type { AppManager } from '../AppManager'; -import type { ProxiedApp } from '../ProxiedApp'; -import { Utilities } from '../misc/Utilities'; -import { JSONRPC_METHOD_NOT_FOUND } from '../runtime/deno/AppsEngineDenoRuntime'; - -export interface IListenerExecutor { - [AppInterface.IPreMessageSentPrevent]: { - args: [IMessage]; - result: boolean; - }; - [AppInterface.IPreMessageSentExtend]: { - args: [IMessage]; - result: IMessage; - }; - [AppInterface.IPreMessageSentModify]: { - args: [IMessage]; - result: IMessage; - }; - [AppInterface.IPostSystemMessageSent]: { - args: [IMessage]; - result: void; - }; - [AppInterface.IPostMessageSent]: { - args: [IMessage]; - result: void; - }; - [AppInterface.IPreMessageDeletePrevent]: { - args: [IMessage]; - result: boolean; - }; - [AppInterface.IPostMessageDeleted]: { - args: [IMessageDeleteContext]; - result: void; - }; - [AppInterface.IPreMessageUpdatedPrevent]: { - args: [IMessage]; - result: unknown; - }; - [AppInterface.IPreMessageUpdatedExtend]: { - args: [IMessage]; - result: boolean; - }; - [AppInterface.IPreMessageUpdatedModify]: { - args: [IMessage]; - result: IMessage; - }; - [AppInterface.IPostMessageUpdated]: { - args: [IMessage]; - result: IMessage; - }; - [AppInterface.IPostMessageReacted]: { - args: [IMessageReactionContext]; - result: void; - }; - [AppInterface.IPostMessageFollowed]: { - args: [IMessageFollowContext]; - result: void; - }; - [AppInterface.IPostMessagePinned]: { - args: [IMessagePinContext]; - result: void; - }; - [AppInterface.IPostMessageStarred]: { - args: [IMessageStarContext]; - result: void; - }; - [AppInterface.IPostMessageReported]: { - args: [IMessageReportContext]; - result: void; - }; - // Rooms - [AppInterface.IPreRoomCreatePrevent]: { - args: [IRoom]; - result: boolean; - }; - [AppInterface.IPreRoomCreateExtend]: { - args: [IRoom]; - result: IRoom; - }; - [AppInterface.IPreRoomCreateModify]: { - args: [IRoom]; - result: IRoom; - }; - [AppInterface.IPostRoomCreate]: { - args: [IRoom]; - result: void; - }; - [AppInterface.IPreRoomDeletePrevent]: { - args: [IRoom]; - result: boolean; - }; - [AppInterface.IPostRoomDeleted]: { - args: [IRoom]; - result: void; - }; - [AppInterface.IPreRoomUserJoined]: { - args: [IRoomUserJoinedContext]; - result: void; - }; - [AppInterface.IPostRoomUserJoined]: { - args: [IRoomUserJoinedContext]; - result: void; - }; - [AppInterface.IPreRoomUserLeave]: { - args: [IRoomUserLeaveContext]; - result: void; - }; - [AppInterface.IPostRoomUserLeave]: { - args: [IRoomUserLeaveContext]; - result: void; - }; - // External Components - [AppInterface.IPostExternalComponentOpened]: { - args: [IExternalComponent]; - result: void; - }; - [AppInterface.IPostExternalComponentClosed]: { - args: [IExternalComponent]; - result: void; - }; - [AppInterface.IUIKitInteractionHandler]: { - args: [UIKitIncomingInteraction]; - result: IUIKitResponse; - }; - [AppInterface.IUIKitLivechatInteractionHandler]: { - args: [IUIKitLivechatIncomingInteraction]; - result: IUIKitResponse; - }; - // Livechat - [AppInterface.IPostLivechatRoomStarted]: { - args: [ILivechatRoom]; - result: void; - }; - /** - * @deprecated please prefer the AppInterface.IPostLivechatRoomClosed event - */ - [AppInterface.ILivechatRoomClosedHandler]: { - args: [ILivechatRoom]; - result: void; - }; - [AppInterface.IPreLivechatRoomCreatePrevent]: { - args: [ILivechatRoom]; - result: void; - }; - [AppInterface.IPostLivechatRoomClosed]: { - args: [ILivechatRoom]; - result: void; - }; - [AppInterface.IPostLivechatRoomSaved]: { - args: [ILivechatRoom]; - result: void; - }; - [AppInterface.IPostLivechatAgentAssigned]: { - args: [ILivechatEventContext]; - result: void; - }; - [AppInterface.IPostLivechatAgentUnassigned]: { - args: [ILivechatEventContext]; - result: void; - }; - [AppInterface.IPostLivechatRoomTransferred]: { - args: [ILivechatTransferEventContext]; - result: void; - }; - [AppInterface.IPostLivechatGuestSaved]: { - args: [IVisitor]; - result: void; - }; - [AppInterface.IPostLivechatDepartmentRemoved]: { - args: [ILivechatDepartmentEventContext]; - result: void; - }; - [AppInterface.IPostLivechatDepartmentDisabled]: { - args: [ILivechatDepartmentEventContext]; - result: void; - }; - // FileUpload - [AppInterface.IPreFileUpload]: { - args: [IFileUploadInternalContext]; - result: void; - }; - // Email - [AppInterface.IPreEmailSent]: { - args: [IPreEmailSentContext]; - result: IUIKitResponse; - }; - // User - [AppInterface.IPostUserCreated]: { - args: [IUserContext]; - result: void; - }; - [AppInterface.IPostUserUpdated]: { - args: [IUserContext]; - result: void; - }; - [AppInterface.IPostUserDeleted]: { - args: [IUserContext]; - result: void; - }; - [AppInterface.IPostUserLoggedIn]: { - args: [IUser]; - result: void; - }; - [AppInterface.IPostUserLoggedOut]: { - args: [IUser]; - result: void; - }; - [AppInterface.IPostUserStatusChanged]: { - args: [IUserStatusContext]; - result: void; - }; -} - -// type EventReturn = void | boolean | IMessage | IRoom | IUser | IUIKitResponse | ILivechatRoom | IEmailDescriptor; - -export class AppListenerManager { - private am: AppAccessorManager; - - private listeners: Map>; - - private defaultHandlers = new 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, []); - this.lockedEvents.set(intt, new Set()); - }); - - this.defaultHandlers.set('executeViewClosedHandler', { success: true }); - } - - public registerListeners(app: ProxiedApp): void { - this.unregisterListeners(app); - - Object.entries(app.getImplementationList()).forEach(([event, isImplemented]) => { - if (!isImplemented) { - return; - } - - this.listeners.get(event).push(app.getID()); - }); - } - - public unregisterListeners(app: ProxiedApp): void { - this.listeners.forEach((apps, int) => { - if (apps.includes(app.getID())) { - const where = apps.indexOf(app.getID()); - this.listeners.get(int).splice(where, 1); - } - }); - } - - 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: Array = []; - - for (const appId of this.listeners.get(int)) { - results.push(this.manager.getOneById(appId)); - } - - return results; - } - - public isEventBlocked(event: AppInterface): boolean { - const lockedEventList = this.lockedEvents.get(event); - - return !!lockedEventList?.size; - } - - /* eslint-disable-next-line complexity */ - public async executeListener( - int: I, - data: IListenerExecutor[I]['args'][0], - ): 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: - return this.executePreMessageSentPrevent(data as IMessage); - case AppInterface.IPreMessageSentExtend: - return this.executePreMessageSentExtend(data as IMessage); - case AppInterface.IPreMessageSentModify: - return this.executePreMessageSentModify(data as IMessage); - case AppInterface.IPostMessageSent: - this.executePostMessageSent(data as IMessage); - return; - case AppInterface.IPostSystemMessageSent: - this.executePostSystemMessageSent(data as IMessage); - return; - case AppInterface.IPreMessageDeletePrevent: - return this.executePreMessageDeletePrevent(data as IMessage); - case AppInterface.IPostMessageDeleted: - this.executePostMessageDelete(data as IMessageDeleteContext); - return; - case AppInterface.IPreMessageUpdatedPrevent: - return this.executePreMessageUpdatedPrevent(data as IMessage); - case AppInterface.IPreMessageUpdatedExtend: - return this.executePreMessageUpdatedExtend(data as IMessage); - case AppInterface.IPreMessageUpdatedModify: - return this.executePreMessageUpdatedModify(data as IMessage); - case AppInterface.IPostMessageUpdated: - this.executePostMessageUpdated(data as IMessage); - return; - case AppInterface.IPostMessageReacted: - return this.executePostMessageReacted(data as IMessageReactionContext); - case AppInterface.IPostMessageFollowed: - return this.executePostMessageFollowed(data as IMessageFollowContext); - case AppInterface.IPostMessagePinned: - return this.executePostMessagePinned(data as IMessagePinContext); - case AppInterface.IPostMessageStarred: - return this.executePostMessageStarred(data as IMessageStarContext); - case AppInterface.IPostMessageReported: - return this.executePostMessageReported(data as IMessageReportContext); - // Rooms - case AppInterface.IPreRoomCreatePrevent: - return this.executePreRoomCreatePrevent(data as IRoom); - case AppInterface.IPreRoomCreateExtend: - return this.executePreRoomCreateExtend(data as IRoom); - case AppInterface.IPreRoomCreateModify: - return this.executePreRoomCreateModify(data as IRoom); - case AppInterface.IPostRoomCreate: - this.executePostRoomCreate(data as IRoom); - return; - case AppInterface.IPreRoomDeletePrevent: - return this.executePreRoomDeletePrevent(data as IRoom); - 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); - case AppInterface.IPreRoomUserLeave: - return this.executePreRoomUserLeave(data as IRoomUserLeaveContext); - case AppInterface.IPostRoomUserLeave: - return this.executePostRoomUserLeave(data as IRoomUserLeaveContext); - // External Components - case AppInterface.IPostExternalComponentOpened: - this.executePostExternalComponentOpened(data as IExternalComponent); - return; - case AppInterface.IPostExternalComponentClosed: - this.executePostExternalComponentClosed(data as IExternalComponent); - return; - case AppInterface.IUIKitInteractionHandler: - return this.executeUIKitInteraction(data as UIKitIncomingInteraction); - case AppInterface.IUIKitLivechatInteractionHandler: - return this.executeUIKitLivechatInteraction(data as IUIKitLivechatIncomingInteraction); - // Livechat - case AppInterface.IPostLivechatRoomStarted: - return this.executePostLivechatRoomStarted(data as ILivechatRoom); - /** - * @deprecated please prefer the AppInterface.IPostLivechatRoomClosed event - */ - case AppInterface.ILivechatRoomClosedHandler: - return this.executeLivechatRoomClosedHandler(data as ILivechatRoom); - case AppInterface.IPreLivechatRoomCreatePrevent: - return this.executePreLivechatRoomCreatePrevent(data as ILivechatRoom); - case AppInterface.IPostLivechatRoomClosed: - return this.executePostLivechatRoomClosed(data as ILivechatRoom); - case AppInterface.IPostLivechatRoomSaved: - return this.executePostLivechatRoomSaved(data as ILivechatRoom); - case AppInterface.IPostLivechatAgentAssigned: - return this.executePostLivechatAgentAssigned(data as ILivechatEventContext); - case AppInterface.IPostLivechatAgentUnassigned: - return this.executePostLivechatAgentUnassigned(data as ILivechatEventContext); - case AppInterface.IPostLivechatRoomTransferred: - return this.executePostLivechatRoomTransferred(data as ILivechatTransferEventContext); - case AppInterface.IPostLivechatDepartmentRemoved: - return this.executePostLivechatDepartmentRemoved(data as ILivechatDepartmentEventContext); - case AppInterface.IPostLivechatDepartmentDisabled: - return this.executePostLivechatDepartmentDisabled(data as ILivechatDepartmentEventContext); - case AppInterface.IPostLivechatGuestSaved: - return this.executePostLivechatGuestSaved(data as IVisitor); - // FileUpload - case AppInterface.IPreFileUpload: - return this.executePreFileUpload(data as IFileUploadInternalContext); - // Email - case AppInterface.IPreEmailSent: - return this.executePreEmailSent(data as IPreEmailSentContext); - // User - case AppInterface.IPostUserCreated: - return this.executePostUserCreated(data as IUserContext); - case AppInterface.IPostUserUpdated: - return this.executePostUserUpdated(data as IUserContext); - case AppInterface.IPostUserDeleted: - return this.executePostUserDeleted(data as IUserContext); - case AppInterface.IPostUserLoggedIn: - return this.executePostUserLoggedIn(data as IUser); - case AppInterface.IPostUserLoggedOut: - return this.executePostUserLoggedOut(data as IUser); - case AppInterface.IPostUserStatusChanged: - return this.executePostUserStatusChanged(data as IUserStatusContext); - default: - console.warn('An invalid listener was called'); - } - } - - // Messages - private async executePreMessageSentPrevent(data: IMessage): Promise { - let prevented = false; - - for (const appId of this.listeners.get(AppInterface.IPreMessageSentPrevent)) { - const app = this.manager.getOneById(appId); - - const continueOn = (await app.call(AppMethod.CHECKPREMESSAGESENTPREVENT, data).catch((error) => { - // This method is optional, so if it doesn't exist, we should continue - if (error?.code === JSONRPC_METHOD_NOT_FOUND) { - return true; - } - - throw error; - })) as boolean; - - if (!continueOn) { - continue; - } - - prevented = (await app.call(AppMethod.EXECUTEPREMESSAGESENTPREVENT, data)) as boolean; - - if (prevented) { - return prevented; - } - } - - return prevented; - } - - private async executePreMessageSentExtend(data: IMessage): Promise { - let msg = data; - - for (const appId of this.listeners.get(AppInterface.IPreMessageSentExtend)) { - const app = this.manager.getOneById(appId); - - const continueOn = (await app.call(AppMethod.CHECKPREMESSAGESENTEXTEND, data).catch((error) => { - // This method is optional, so if it doesn't exist, we should continue - if (error?.code === JSONRPC_METHOD_NOT_FOUND) { - return true; - } - - throw error; - })) as boolean; - - if (continueOn) { - msg = await app.call(AppMethod.EXECUTEPREMESSAGESENTEXTEND, msg); - } - } - - return msg; - } - - private async executePreMessageSentModify(data: IMessage): Promise { - let msg = data; - - for (const appId of this.listeners.get(AppInterface.IPreMessageSentModify)) { - const app = this.manager.getOneById(appId); - - const continueOn = (await app.call(AppMethod.CHECKPREMESSAGESENTMODIFY, msg).catch((error) => { - // This method is optional, so if it doesn't exist, we should continue - if (error?.code === JSONRPC_METHOD_NOT_FOUND) { - return true; - } - - throw error; - })) as boolean; - - if (continueOn) { - msg = (await app.call(AppMethod.EXECUTEPREMESSAGESENTMODIFY, msg)) as IMessage; - } - } - - return msg; - } - - private async executePostMessageSent(data: IMessage): Promise { - // First check if the app implements Bot DM handlers and check if the dm contains more than one user - if (data.room.type === RoomType.DIRECT_MESSAGE && data.room.userIds.length > 1) { - for (const appId of this.listeners.get(AppInterface.IPostMessageSentToBot)) { - const app = this.manager.getOneById(appId); - - const reader = this.am.getReader(appId); - const bot = await reader.getUserReader().getAppUser(); - if (!bot) { - continue; - } - - // if the sender is the bot just ignore it - - if (bot.id === data.sender.id) { - continue; - } - // if the user doesnt belong to the room ignore it - if (!data.room.userIds.includes(bot.id)) { - continue; - } - - await app.call(AppMethod.EXECUTEPOSTMESSAGESENTTOBOT, data); - } - } - - for (const appId of this.listeners.get(AppInterface.IPostMessageSent)) { - const app = this.manager.getOneById(appId); - - const continueOn = (await app.call(AppMethod.CHECKPOSTMESSAGESENT, data).catch((error) => { - // This method is optional, so if it doesn't exist, we should continue - if (error?.code === JSONRPC_METHOD_NOT_FOUND) { - return true; - } - - throw error; - })) as boolean; - - if (continueOn) { - await app.call(AppMethod.EXECUTEPOSTMESSAGESENT, data); - } - } - } - - private async executePostSystemMessageSent(data: IMessage): Promise { - for (const appId of this.listeners.get(AppInterface.IPostSystemMessageSent)) { - const app = this.manager.getOneById(appId); - await app.call(AppMethod.EXECUTEPOSTSYSTEMMESSAGESENT, data); - } - } - - private async executePreMessageDeletePrevent(data: IMessage): Promise { - let prevented = false; - - for (const appId of this.listeners.get(AppInterface.IPreMessageDeletePrevent)) { - const app = this.manager.getOneById(appId); - - const continueOn = (await app.call(AppMethod.CHECKPREMESSAGEDELETEPREVENT, data).catch((error) => { - // This method is optional, so if it doesn't exist, we should continue - if (error?.code === JSONRPC_METHOD_NOT_FOUND) { - return true; - } - - throw error; - })) as boolean; - - if (continueOn) { - prevented = (await app.call(AppMethod.EXECUTEPREMESSAGEDELETEPREVENT, data)) as boolean; - - if (prevented) { - return prevented; - } - } - } - - return prevented; - } - - private async executePostMessageDelete(data: IMessageDeleteContext): Promise { - const context = Utilities.deepCloneAndFreeze(data); - const { message } = context; - - for (const appId of this.listeners.get(AppInterface.IPostMessageDeleted)) { - const app = this.manager.getOneById(appId); - - const continueOn = (await app - .call( - AppMethod.CHECKPOSTMESSAGEDELETED, - // `context` has more information about the event, but - // we had to keep this `message` here for compatibility - message, - context, - ) - .catch((error) => { - // This method is optional, so if it doesn't exist, we should continue - if (error?.code === JSONRPC_METHOD_NOT_FOUND) { - return true; - } - - throw error; - })) as boolean; - - if (continueOn) { - await app.call(AppMethod.EXECUTEPOSTMESSAGEDELETED, message, context); - } - } - } - - private async executePreMessageUpdatedPrevent(data: IMessage): Promise { - let prevented = false; - - for (const appId of this.listeners.get(AppInterface.IPreMessageUpdatedPrevent)) { - const app = this.manager.getOneById(appId); - - const continueOn = (await app.call(AppMethod.CHECKPREMESSAGEUPDATEDPREVENT, data).catch((error) => { - // This method is optional, so if it doesn't exist, we should continue - if (error?.code === JSONRPC_METHOD_NOT_FOUND) { - return true; - } - - throw error; - })) as boolean; - - if (continueOn) { - prevented = (await app.call(AppMethod.EXECUTEPREMESSAGEUPDATEDPREVENT, data)) as boolean; - - if (prevented) { - return prevented; - } - } - } - - return prevented; - } - - private async executePreMessageUpdatedExtend(data: IMessage): Promise { - let msg = data; - - for (const appId of this.listeners.get(AppInterface.IPreMessageUpdatedExtend)) { - const app = this.manager.getOneById(appId); - - const continueOn = (await app.call(AppMethod.CHECKPREMESSAGEUPDATEDEXTEND, msg).catch((error) => { - // This method is optional, so if it doesn't exist, we should continue - if (error?.code === JSONRPC_METHOD_NOT_FOUND) { - return true; - } - - throw error; - })) as boolean; - - if (continueOn) { - msg = await app.call(AppMethod.EXECUTEPREMESSAGEUPDATEDEXTEND, msg); - } - } - - return msg; - } - - private async executePreMessageUpdatedModify(data: IMessage): Promise { - let msg = data; - - for (const appId of this.listeners.get(AppInterface.IPreMessageUpdatedModify)) { - const app = this.manager.getOneById(appId); - - const continueOn = (await app.call(AppMethod.CHECKPREMESSAGEUPDATEDMODIFY, msg).catch((error) => { - // This method is optional, so if it doesn't exist, we should continue - if (error?.code === JSONRPC_METHOD_NOT_FOUND) { - return true; - } - - throw error; - })) as boolean; - - if (continueOn) { - msg = (await app.call(AppMethod.EXECUTEPREMESSAGEUPDATEDMODIFY, msg)) as IMessage; - } - } - - return msg; - } - - private async executePostMessageUpdated(data: IMessage): Promise { - for (const appId of this.listeners.get(AppInterface.IPostMessageUpdated)) { - const app = this.manager.getOneById(appId); - - const continueOn = (await app.call(AppMethod.CHECKPOSTMESSAGEUPDATED, data).catch((error) => { - // This method is optional, so if it doesn't exist, we should continue - if (error?.code === JSONRPC_METHOD_NOT_FOUND) { - return true; - } - - throw error; - })) as boolean; - - if (continueOn) { - await app.call(AppMethod.EXECUTEPOSTMESSAGEUPDATED, data); - } - } - } - - // Rooms - private async executePreRoomCreatePrevent(data: IRoom): Promise { - let prevented = false; - - for (const appId of this.listeners.get(AppInterface.IPreRoomCreatePrevent)) { - const app = this.manager.getOneById(appId); - - const continueOn = (await app.call(AppMethod.CHECKPREROOMCREATEPREVENT, data).catch((error) => { - // This method is optional, so if it doesn't exist, we should continue - if (error?.code === JSONRPC_METHOD_NOT_FOUND) { - return true; - } - - throw error; - })) as boolean; - - if (continueOn) { - prevented = (await app.call(AppMethod.EXECUTEPREROOMCREATEPREVENT, data)) as boolean; - - if (prevented) { - return prevented; - } - } - } - - return prevented; - } - - private async executePreRoomCreateExtend(data: IRoom): Promise { - let room = data; - - for (const appId of this.listeners.get(AppInterface.IPreRoomCreateExtend)) { - const app = this.manager.getOneById(appId); - - const continueOn = (await app.call(AppMethod.CHECKPREROOMCREATEEXTEND, room).catch((error) => { - // This method is optional, so if it doesn't exist, we should continue - if (error?.code === JSONRPC_METHOD_NOT_FOUND) { - return true; - } - - throw error; - })) as boolean; - - if (continueOn) { - room = await app.call(AppMethod.EXECUTEPREROOMCREATEEXTEND, room); - } - } - - return room; - } - - private async executePreRoomCreateModify(data: IRoom): Promise { - let room = data; - - for (const appId of this.listeners.get(AppInterface.IPreRoomCreateModify)) { - const app = this.manager.getOneById(appId); - - const continueOn = (await app.call(AppMethod.CHECKPREROOMCREATEMODIFY, room).catch((error) => { - // This method is optional, so if it doesn't exist, we should continue - if (error?.code === JSONRPC_METHOD_NOT_FOUND) { - return true; - } - - throw error; - })) as boolean; - - if (continueOn) { - room = (await app.call(AppMethod.EXECUTEPREROOMCREATEMODIFY, room)) as IRoom; - } - } - - return room; - } - - private async executePostRoomCreate(data: IRoom): Promise { - for (const appId of this.listeners.get(AppInterface.IPostRoomCreate)) { - const app = this.manager.getOneById(appId); - - const continueOn = (await app.call(AppMethod.CHECKPOSTROOMCREATE, data).catch((error) => { - // This method is optional, so if it doesn't exist, we should continue - if (error?.code === JSONRPC_METHOD_NOT_FOUND) { - return true; - } - - throw error; - })) as boolean; - - if (continueOn) { - await app.call(AppMethod.EXECUTEPOSTROOMCREATE, data); - } - } - } - - private async executePreRoomDeletePrevent(data: IRoom): Promise { - let prevented = false; - - for (const appId of this.listeners.get(AppInterface.IPreRoomDeletePrevent)) { - const app = this.manager.getOneById(appId); - - const continueOn = (await app.call(AppMethod.CHECKPREROOMDELETEPREVENT, data).catch((error) => { - // This method is optional, so if it doesn't exist, we should continue - if (error?.code === JSONRPC_METHOD_NOT_FOUND) { - return true; - } - - throw error; - })) as boolean; - - if (continueOn) { - prevented = (await app.call(AppMethod.EXECUTEPREROOMDELETEPREVENT, data)) as boolean; - - if (prevented) { - return prevented; - } - } - } - - return prevented; - } - - private async executePostRoomDeleted(data: IRoom): Promise { - for (const appId of this.listeners.get(AppInterface.IPostRoomDeleted)) { - const app = this.manager.getOneById(appId); - - const continueOn = (await app.call(AppMethod.CHECKPOSTROOMDELETED, data).catch((error) => { - // This method is optional, so if it doesn't exist, we should continue - if (error?.code === JSONRPC_METHOD_NOT_FOUND) { - return true; - } - - throw error; - })) as boolean; - - if (continueOn) { - await app.call(AppMethod.EXECUTEPOSTROOMDELETED, data); - } - } - } - - private async executePreRoomUserJoined(externalData: IRoomUserJoinedContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPreRoomUserJoined)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_PRE_ROOM_USER_JOINED, externalData); - } - } - - private async executePostRoomUserJoined(externalData: IRoomUserJoinedContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostRoomUserJoined)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_ROOM_USER_JOINED, externalData); - } - } - - private async executePreRoomUserLeave(externalData: IRoomUserLeaveContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPreRoomUserLeave)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_PRE_ROOM_USER_LEAVE, externalData); - } - } - - private async executePostRoomUserLeave(externalData: IRoomUserLeaveContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostRoomUserLeave)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_ROOM_USER_LEAVE, externalData); - } - } - - // External Components - private async executePostExternalComponentOpened(data: IExternalComponent): Promise { - for (const appId of this.listeners.get(AppInterface.IPostExternalComponentOpened)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTEPOSTEXTERNALCOMPONENTOPENED, data); - } - } - - private async executePostExternalComponentClosed(data: IExternalComponent): Promise { - for (const appId of this.listeners.get(AppInterface.IPostExternalComponentClosed)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTEPOSTEXTERNALCOMPONENTCLOSED, data); - } - } - - private async executeUIKitInteraction(data: UIKitIncomingInteraction): Promise { - const { appId } = data; - - const app = this.manager.getOneById(appId); - const handleError = (method: string) => (error: unknown) => { - if ((error as Record)?.code === JSONRPC_METHOD_NOT_FOUND) { - if (this.defaultHandlers.has(method)) { - console.warn( - `App ${appId} triggered an interaction but it doesn't exist or doesn't have method ${method}. Falling back to default handler.`, - ); - return this.defaultHandlers.get(method); - } - - console.warn( - `App ${appId} triggered an interaction but it doesn't exist or doesn't have method ${method} and there is no default handler for it.`, - ); - return; - } - - throw error; - }; - - const { actionId, user, triggerId } = data; - - switch (data.type) { - case UIKitIncomingInteractionType.BLOCK: { - const method = 'executeBlockActionHandler'; - const { value, blockId } = data.payload as { value: string; blockId: string }; - - return app - .call(method, { - appId, - actionId, - blockId, - user, - room: data.room, - triggerId, - value, - message: data.message, - container: data.container, - }) - .catch(handleError(method)); - } - case UIKitIncomingInteractionType.VIEW_SUBMIT: { - const method = 'executeViewSubmitHandler'; - const { view } = data.payload as { view: IUIKitSurface }; - - return app - .call(method, { - appId, - actionId, - view, - room: data.room, - triggerId, - user, - }) - .catch(handleError(method)); - } - case UIKitIncomingInteractionType.VIEW_CLOSED: { - const method = 'executeViewClosedHandler'; - const { view, isCleared } = data.payload as { view: IUIKitSurface; isCleared: boolean }; - - return app - .call(method, { - appId, - actionId, - view, - room: data.room, - isCleared, - user, - }) - .catch(handleError(method)); - } - case 'actionButton': { - const method = 'executeActionButtonHandler'; - - if (isUIKitIncomingInteractionActionButtonMessageBox(data)) { - return app - .call(method, { - appId, - actionId, - buttonContext: UIActionButtonContext.MESSAGE_BOX_ACTION, - room: data.room, - triggerId, - user, - threadId: data.tmid, - ...('message' in data.payload && { text: data.payload.message }), - }) - .catch(handleError(method)); - } - - return app - .call(method, { - appId, - actionId, - triggerId, - buttonContext: data.payload.context as UIActionButtonContext, - room: ('room' in data && data.room) || undefined, - user, - ...('message' in data && { message: data.message }), - }) - .catch(handleError(method)); - } - } - } - - private async executeUIKitLivechatInteraction(data: IUIKitLivechatIncomingInteraction): Promise { - const { appId, type } = data; - - const method = ((interactionType: string) => { - switch (interactionType) { - case UIKitIncomingInteractionType.BLOCK: - return AppMethod.UIKIT_LIVECHAT_BLOCK_ACTION; - } - })(type); - - const app = this.manager.getOneById(appId); - - const interactionData = (( - interactionType: UIKitIncomingInteractionType, - interaction: IUIKitLivechatIncomingInteraction, - ): IUIKitLivechatBlockIncomingInteraction => { - const { actionId, message, visitor, room, triggerId, container } = interaction; - - switch (interactionType) { - case UIKitIncomingInteractionType.BLOCK: { - const { value, blockId } = interaction.payload as { value: string; blockId: string }; - - return { - appId, - actionId, - blockId, - visitor, - room, - triggerId, - value, - message, - container: container as IUIKitIncomingInteractionModalContainer | IUIKitIncomingInteractionMessageContainer, - }; - } - } - })(type, data); - - return app.call(method, interactionData); - } - - // Livechat - private async executePreLivechatRoomCreatePrevent(data: ILivechatRoom): Promise { - for (const appId of this.listeners.get(AppInterface.IPreLivechatRoomCreatePrevent)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_PRE_LIVECHAT_ROOM_CREATE_PREVENT, data); - } - } - - private async executePostLivechatRoomStarted(data: ILivechatRoom): Promise { - for (const appId of this.listeners.get(AppInterface.IPostLivechatRoomStarted)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_LIVECHAT_ROOM_STARTED, data); - } - } - - private async executeLivechatRoomClosedHandler(data: ILivechatRoom): Promise { - for (const appId of this.listeners.get(AppInterface.ILivechatRoomClosedHandler)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_LIVECHAT_ROOM_CLOSED_HANDLER, data); - } - } - - private async executePostLivechatRoomClosed(data: ILivechatRoom): Promise { - for (const appId of this.listeners.get(AppInterface.IPostLivechatRoomClosed)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_LIVECHAT_ROOM_CLOSED, data); - } - } - - private async executePostLivechatAgentAssigned(data: ILivechatEventContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostLivechatAgentAssigned)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_LIVECHAT_AGENT_ASSIGNED, data); - } - } - - private async executePostLivechatAgentUnassigned(data: ILivechatEventContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostLivechatAgentUnassigned)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_LIVECHAT_AGENT_UNASSIGNED, data); - } - } - - private async executePostLivechatRoomTransferred(data: ILivechatTransferEventContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostLivechatRoomTransferred)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_LIVECHAT_ROOM_TRANSFERRED, data); - } - } - - private async executePostLivechatGuestSaved(data: IVisitor): Promise { - for (const appId of this.listeners.get(AppInterface.IPostLivechatGuestSaved)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_LIVECHAT_GUEST_SAVED, data); - } - } - - private async executePostLivechatRoomSaved(data: ILivechatRoom): Promise { - for (const appId of this.listeners.get(AppInterface.IPostLivechatRoomSaved)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_LIVECHAT_ROOM_SAVED, data); - } - } - - private async executePostLivechatDepartmentRemoved(data: ILivechatDepartmentEventContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostLivechatDepartmentRemoved)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_LIVECHAT_DEPARTMENT_REMOVED, data); - } - } - - private async executePostLivechatDepartmentDisabled(data: ILivechatDepartmentEventContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostLivechatDepartmentDisabled)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_LIVECHAT_DEPARTMENT_DISABLED, data); - } - } - - // FileUpload - private async executePreFileUpload(data: IFileUploadInternalContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPreFileUpload)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_PRE_FILE_UPLOAD, data); - } - } - - private async executePreEmailSent(data: IPreEmailSentContext): Promise { - let descriptor = data.email; - - for (const appId of this.listeners.get(AppInterface.IPreEmailSent)) { - const app = this.manager.getOneById(appId); - - descriptor = await app.call(AppMethod.EXECUTE_PRE_EMAIL_SENT, { - context: data.context, - email: descriptor, - }); - } - - return descriptor; - } - - private async executePostMessageReacted(data: IMessageReactionContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostMessageReacted)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_MESSAGE_REACTED, data); - } - } - - private async executePostMessageFollowed(data: IMessageFollowContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostMessageFollowed)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_MESSAGE_FOLLOWED, data); - } - } - - private async executePostMessagePinned(data: IMessagePinContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostMessagePinned)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_MESSAGE_PINNED, data); - } - } - - private async executePostMessageStarred(data: IMessageStarContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostMessageStarred)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_MESSAGE_STARRED, data); - } - } - - private async executePostMessageReported(data: IMessageReportContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostMessageReported)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_MESSAGE_REPORTED, data); - } - } - - private async executePostUserCreated(data: IUserContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostUserCreated)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_USER_CREATED, data); - } - } - - private async executePostUserUpdated(data: IUserUpdateContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostUserUpdated)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_USER_UPDATED, data); - } - } - - private async executePostUserDeleted(data: IUserContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostUserDeleted)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_USER_DELETED, data); - } - } - - private async executePostUserLoggedIn(data: IUser): Promise { - for (const appId of this.listeners.get(AppInterface.IPostUserLoggedIn)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_USER_LOGGED_IN, data); - } - } - - private async executePostUserLoggedOut(data: IUser): Promise { - for (const appId of this.listeners.get(AppInterface.IPostUserLoggedOut)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_USER_LOGGED_OUT, data); - } - } - - private async executePostUserStatusChanged(data: IUserStatusContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostUserStatusChanged)) { - const app = this.manager.getOneById(appId); - - await app.call(AppMethod.EXECUTE_POST_USER_STATUS_CHANGED, data); - } - } -} diff --git a/packages/apps-engine/src/server/managers/AppOutboundCommunicationProvider.ts b/packages/apps-engine/src/server/managers/AppOutboundCommunicationProvider.ts deleted file mode 100644 index d868eda63d262..0000000000000 --- a/packages/apps-engine/src/server/managers/AppOutboundCommunicationProvider.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { AppAccessorManager } from '.'; -import { AppMethod } from '../../definition/metadata'; -import type { IOutboundMessage, IOutboundMessageProviders, ProviderMetadata } from '../../definition/outboundCommunication'; -import type { ProxiedApp } from '../ProxiedApp'; -import { AppOutboundProcessError } from '../errors/AppOutboundProcessError'; -import type { AppLogStorage } from '../storage'; - -export class OutboundMessageProvider { - public isRegistered: boolean; - - constructor( - public app: ProxiedApp, - public provider: IOutboundMessageProviders, - ) { - this.isRegistered = false; - } - - public async runGetProviderMetadata(logStorage: AppLogStorage, accessors: AppAccessorManager): Promise { - return this.runTheCode(AppMethod._OUTBOUND_GET_PROVIDER_METADATA, logStorage, accessors, []); - } - - public async runSendOutboundMessage(logStorage: AppLogStorage, accessors: AppAccessorManager, body: IOutboundMessage): Promise { - await this.runTheCode(AppMethod._OUTBOUND_SEND_MESSAGE, logStorage, accessors, [body]); - } - - private async runTheCode( - method: AppMethod._OUTBOUND_GET_PROVIDER_METADATA | AppMethod._OUTBOUND_SEND_MESSAGE, - logStorage: AppLogStorage, - accessors: AppAccessorManager, - runContextArgs: Array, - ): Promise { - const provider = `${this.provider.name}-${this.provider.type}`; - - try { - const result = await this.app.getRuntimeController().sendRequest({ - method: `outboundCommunication:${provider}:${method}`, - params: runContextArgs, - }); - - return result as T; - } catch (e) { - if (e?.message === 'error-invalid-provider') { - throw new Error('error-provider-not-registered'); - } - throw new AppOutboundProcessError(e.message, method); - } - } - - public setRegistered(registered: boolean): void { - this.isRegistered = registered; - } -} diff --git a/packages/apps-engine/src/server/managers/AppOutboundCommunicationProviderManager.ts b/packages/apps-engine/src/server/managers/AppOutboundCommunicationProviderManager.ts deleted file mode 100644 index cd1b045e951b4..0000000000000 --- a/packages/apps-engine/src/server/managers/AppOutboundCommunicationProviderManager.ts +++ /dev/null @@ -1,138 +0,0 @@ -import type { AppAccessorManager } from '.'; -import type { - IOutboundMessageProviders, - IOutboundEmailMessageProvider, - IOutboundPhoneMessageProvider, - ValidOutboundProvider, - IOutboundMessage, -} from '../../definition/outboundCommunication'; -import type { AppManager } from '../AppManager'; -import type { OutboundMessageBridge } from '../bridges'; -import { OutboundMessageProvider } from './AppOutboundCommunicationProvider'; -import { AppPermissionManager } from './AppPermissionManager'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export class AppOutboundCommunicationProviderManager { - private readonly accessors: AppAccessorManager; - - private readonly bridge: OutboundMessageBridge; - - private outboundMessageProviders: Map>; - - constructor(private readonly manager: AppManager) { - this.bridge = this.manager.getBridges().getOutboundMessageBridge(); - this.accessors = this.manager.getAccessorManager(); - - this.outboundMessageProviders = new Map>(); - } - - public isAlreadyDefined(providerId: string, providerType: ValidOutboundProvider): boolean { - const providersByApp = this.outboundMessageProviders.get(providerId); - if (!providersByApp) { - return false; - } - if (!providersByApp.get(providerType)) { - return false; - } - return true; - } - - public addProvider(appId: string, provider: IOutboundMessageProviders): void { - const app = this.manager.getOneById(appId); - if (!app) { - throw new Error('App must exist in order for an outbound provider to be added.'); - } - - if (!AppPermissionManager.hasPermission(appId, AppPermissions.outboundComms.provide)) { - throw new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.outboundComms.provide], - }); - } - - if (!this.outboundMessageProviders.has(appId)) { - this.outboundMessageProviders.set(appId, new Map()); - } - - this.outboundMessageProviders.get(appId).set(provider.type, new OutboundMessageProvider(app, provider)); - } - - public async registerProviders(appId: string): Promise { - if (!this.outboundMessageProviders.has(appId)) { - return; - } - - const appProviders = this.outboundMessageProviders.get(appId); - if (!appProviders) { - return; - } - - for await (const [, providerInfo] of appProviders) { - if (providerInfo.isRegistered) { - continue; - } - - if (providerInfo.provider.type === 'phone') { - await this.registerPhoneProvider(appId, providerInfo.provider); - providerInfo.setRegistered(true); - } else if (providerInfo.provider.type === 'email') { - await this.registerEmailProvider(appId, providerInfo.provider); - providerInfo.setRegistered(true); - } - } - } - - public async unregisterProviders(appId: string, opts?: { keepReferences: boolean }): Promise { - if (!this.outboundMessageProviders.has(appId)) { - return; - } - - const appProviders = this.outboundMessageProviders.get(appId); - for await (const [, providerInfo] of appProviders) { - await this.unregisterProvider(appId, providerInfo, opts); - } - - if (!opts?.keepReferences) { - this.outboundMessageProviders.delete(appId); - } - } - - private async registerPhoneProvider(appId: string, provider: IOutboundPhoneMessageProvider): Promise { - await this.bridge.doRegisterPhoneProvider(provider, appId); - } - - private async registerEmailProvider(appId: string, provider: IOutboundEmailMessageProvider): Promise { - await this.bridge.doRegisterEmailProvider(provider, appId); - } - - private async unregisterProvider(appId: string, info: OutboundMessageProvider, opts?: { keepReferences: boolean }): Promise { - const key = info.provider.type; - - await this.bridge.doUnRegisterProvider(info.provider, appId); - - info.setRegistered(false); - - if (!opts?.keepReferences) { - this.outboundMessageProviders.get(appId)?.delete(key); - } - } - - public getProviderMetadata(appId: string, providerType: ValidOutboundProvider) { - const providerInfo = this.outboundMessageProviders.get(appId)?.get(providerType); - if (!providerInfo) { - throw new Error('provider-not-registered'); - } - - return providerInfo.runGetProviderMetadata(this.manager.getLogStorage(), this.accessors); - } - - public sendOutboundMessage(appId: string, providerType: ValidOutboundProvider, body: IOutboundMessage) { - const providerInfo = this.outboundMessageProviders.get(appId)?.get(providerType); - if (!providerInfo) { - throw new Error('provider-not-registered'); - } - - return providerInfo.runSendOutboundMessage(this.manager.getLogStorage(), this.accessors, body); - } -} diff --git a/packages/apps-engine/src/server/managers/AppPermissionManager.ts b/packages/apps-engine/src/server/managers/AppPermissionManager.ts deleted file mode 100644 index 310672d0ff81e..0000000000000 --- a/packages/apps-engine/src/server/managers/AppPermissionManager.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { IPermission } from '../../definition/permissions/IPermission'; -import { getPermissionsByAppId } from '../AppManager'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { APPS_ENGINE_RUNTIME_FILE_PREFIX } from '../runtime/AppsEngineRuntime'; - -export class AppPermissionManager { - /** - * It returns the declaration of the permission if the app declared, or it returns `undefined`. - */ - public static hasPermission

(appId: string, permission: P): P | undefined { - if (process.env.NODE_ENV === 'test') { - return permission; - } - - const grantedPermission = getPermissionsByAppId(appId).find(({ name }) => name === permission.name) as unknown; - - if (!grantedPermission) { - return undefined; - } - - return grantedPermission as P; - } - - public static notifyAboutError(err: Error): void { - if (err instanceof PermissionDeniedError) { - const { name, message } = err; - - console.error(`${name}: ${message}\n${this.getCallStack()}`); - } else { - console.error(err); - } - } - - private static getCallStack(): string { - const stack = new Error().stack.toString().split('\n'); - const appStackIndex = stack.findIndex((position) => position.includes(APPS_ENGINE_RUNTIME_FILE_PREFIX)); - - return stack.slice(4, appStackIndex).join('\n'); - } -} diff --git a/packages/apps-engine/src/server/managers/AppRuntimeManager.ts b/packages/apps-engine/src/server/managers/AppRuntimeManager.ts deleted file mode 100644 index dd631bc8eebd0..0000000000000 --- a/packages/apps-engine/src/server/managers/AppRuntimeManager.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { AppManager } from '../AppManager'; -import type { IParseAppPackageResult } from '../compiler'; -import type { IRuntimeController } from '../runtime/IRuntimeController'; -import { DenoRuntimeSubprocessController } from '../runtime/deno/AppsEngineDenoRuntime'; -import type { IAppStorageItem } from '../storage'; - -export type AppRuntimeParams = { - appId: string; - appSource: string; -}; - -export type ExecRequestContext = { - method: string; - params: unknown[]; -}; - -export type ExecRequestOptions = { - timeout?: number; -}; - -const defaultRuntimeFactory = (manager: AppManager, appPackage: IParseAppPackageResult, storageItem: IAppStorageItem) => - new DenoRuntimeSubprocessController(manager, appPackage, storageItem); - -export class AppRuntimeManager { - private readonly subprocesses: Record = {}; - - constructor( - private readonly manager: AppManager, - private readonly runtimeFactory = defaultRuntimeFactory, - ) {} - - public async startRuntimeForApp( - appPackage: IParseAppPackageResult, - storageItem: IAppStorageItem, - options = { force: false }, - ): Promise { - const { id: appId } = appPackage.info; - - if (appId in this.subprocesses && !options.force) { - throw new Error('App already has an associated runtime'); - } - - this.subprocesses[appId] = this.runtimeFactory(this.manager, appPackage, storageItem); - - try { - await this.subprocesses[appId].setupApp(); - } catch (error) { - const subprocess = this.subprocesses[appId]; - delete this.subprocesses[appId]; - await subprocess.stopApp(); - throw error; - } - - return this.subprocesses[appId]; - } - - public async runInSandbox(appId: string, execRequest: ExecRequestContext, options?: ExecRequestOptions): Promise { - const subprocess = this.subprocesses[appId]; - - if (!subprocess) { - throw new Error('App does not have an associated runtime'); - } - - return subprocess.sendRequest(execRequest); - } - - public async stopRuntime(controller: IRuntimeController): Promise { - await controller.stopApp(); - - const appId = controller.getAppId(); - - if (appId in this.subprocesses) { - delete this.subprocesses[appId]; - } - } -} diff --git a/packages/apps-engine/src/server/managers/AppSchedulerManager.ts b/packages/apps-engine/src/server/managers/AppSchedulerManager.ts deleted file mode 100644 index 2e8535b5dd115..0000000000000 --- a/packages/apps-engine/src/server/managers/AppSchedulerManager.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { AppStatus } from '../../definition/AppStatus'; -import type { IJobContext, IOnetimeSchedule, IProcessor, IRecurringSchedule } from '../../definition/scheduler'; -import type { AppManager } from '../AppManager'; -import type { IInternalSchedulerBridge } from '../bridges/IInternalSchedulerBridge'; -import type { SchedulerBridge } from '../bridges/SchedulerBridge'; - -function createProcessorId(jobId: string, appId: string): string { - return jobId.includes(`_${appId}`) ? jobId : `${jobId}_${appId}`; -} - -export class AppSchedulerManager { - private readonly bridge: SchedulerBridge; - - private registeredProcessors: Map; - - constructor(private readonly manager: AppManager) { - this.bridge = this.manager.getBridges().getSchedulerBridge(); - this.registeredProcessors = new Map(); - } - - public async registerProcessors(processors: Array = [], appId: string): Promise> { - if (!this.registeredProcessors.get(appId)) { - this.registeredProcessors.set(appId, {}); - } - - return this.bridge.doRegisterProcessors( - processors.map((processor) => { - const processorId = createProcessorId(processor.id, appId); - - this.registeredProcessors.get(appId)[processorId] = processor; - - return { - id: processorId, - processor: this.wrapProcessor(appId, processorId).bind(this), - startupSetting: processor.startupSetting, - }; - }), - appId, - ); - } - - public wrapProcessor(appId: string, processorId: string): IProcessor['processor'] { - return async (jobContext: IJobContext) => { - const processor = this.registeredProcessors.get(appId)[processorId]; - - if (!processor) { - throw new Error(`Processor ${processorId} not available`); - } - - const app = this.manager.getOneById(appId); - const status = await app.getStatus(); - const previousStatus = app.getPreviousStatus(); - - const isNotToRunJob = this.isNotToRunJob(status, previousStatus); - - if (isNotToRunJob) { - return; - } - - try { - await app.getRuntimeController().sendRequest({ - method: `scheduler:${processor.id}`, - params: [jobContext], - }); - } catch (e) { - console.error(e); - throw e; - } - }; - } - - public async scheduleOnce(job: IOnetimeSchedule, appId: string): Promise { - return this.bridge.doScheduleOnce({ ...job, id: createProcessorId(job.id, appId) }, appId); - } - - public async scheduleRecurring(job: IRecurringSchedule, appId: string): Promise { - return this.bridge.doScheduleRecurring({ ...job, id: createProcessorId(job.id, appId) }, appId); - } - - public async cancelJob(jobId: string, appId: string): Promise { - return this.bridge.doCancelJob(createProcessorId(jobId, appId), appId); - } - - public async cancelAllJobs(appId: string): Promise { - return this.bridge.doCancelAllJobs(appId); - } - - public async cleanUp(appId: string): Promise { - await (this.bridge as IInternalSchedulerBridge & SchedulerBridge).cancelAllJobs(appId); - } - - private isNotToRunJob(status: AppStatus, previousStatus: AppStatus): boolean { - const isAppCurrentDisabled = status === AppStatus.DISABLED || status === AppStatus.MANUALLY_DISABLED; - const wasAppDisabled = previousStatus === AppStatus.DISABLED || previousStatus === AppStatus.MANUALLY_DISABLED; - - return (status === AppStatus.INITIALIZED && wasAppDisabled) || isAppCurrentDisabled; - } -} diff --git a/packages/apps-engine/src/server/managers/AppSettingsManager.ts b/packages/apps-engine/src/server/managers/AppSettingsManager.ts deleted file mode 100644 index 64fb7b4cc1053..0000000000000 --- a/packages/apps-engine/src/server/managers/AppSettingsManager.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { AppMethod } from '../../definition/metadata'; -import type { ISetting } from '../../definition/settings'; -import type { ISettingUpdateContext } from '../../definition/settings/ISettingUpdateContext'; -import type { AppManager } from '../AppManager'; -import { Utilities } from '../misc/Utilities'; - -export class AppSettingsManager { - constructor(private manager: AppManager) {} - - public getAppSettings(appId: string): { [key: string]: ISetting } { - const rl = this.manager.getOneById(appId); - - if (!rl) { - throw new Error('No App found by the provided id.'); - } - - return Utilities.deepCloneAndFreeze(rl.getStorageItem().settings); - } - - public getAppSetting(appId: string, settingId: string): ISetting { - const settings = this.getAppSettings(appId); - - if (!settings[settingId]) { - throw new Error('No setting found for the App by the provided id.'); - } - - return Utilities.deepCloneAndFreeze(settings[settingId]); - } - - public async updateAppSetting(appId: string, setting: ISetting): Promise { - const rl = this.manager.getOneById(appId); - - if (!rl) { - throw new Error('No App found by the provided id.'); - } - - const storageItem = rl.getStorageItem(); - - const oldSetting = storageItem.settings[setting.id]; - if (!oldSetting) { - throw new Error('No setting found for the App by the provided id.'); - } - - const decoratedSetting = - (await rl.call(AppMethod.ON_PRE_SETTING_UPDATE, { oldSetting, newSetting: setting } as ISettingUpdateContext)) || setting; - - decoratedSetting.updatedAt = new Date(); - storageItem.settings[decoratedSetting.id] = decoratedSetting; - - await this.manager.getStorage().updateSetting(storageItem._id, decoratedSetting); - - this.manager.getBridges().getAppDetailChangesBridge().doOnAppSettingsChange(appId, decoratedSetting); - - await rl.call(AppMethod.ONSETTINGUPDATED, decoratedSetting); - } -} diff --git a/packages/apps-engine/src/server/managers/AppSignatureManager.ts b/packages/apps-engine/src/server/managers/AppSignatureManager.ts deleted file mode 100644 index 1b24cd9d7d4da..0000000000000 --- a/packages/apps-engine/src/server/managers/AppSignatureManager.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { createHash } from 'crypto'; - -import * as jose from 'jose'; - -import type { AppManager } from '../AppManager'; -import type { IInternalFederationBridge } from '../bridges'; -import type { IAppStorageItem } from '../storage'; - -export class AppSignatureManager { - private readonly federationBridge: IInternalFederationBridge; - - private readonly checksumAlgorithm = 'SHA256'; - - private readonly signingAlgorithm = 'RS512'; - - private privateKey: string; - - private publicKey: string; - - constructor(private readonly manager: AppManager) { - this.federationBridge = this.manager.getBridges().getInternalFederationBridge(); - } - - public async verifySignedApp(app: IAppStorageItem): Promise { - const publicKey = await jose.importSPKI(await this.getPublicKey(), 'pem'); - const { payload } = await jose.jwtVerify(app.signature, publicKey); - - const checksum = this.calculateChecksumForApp(app); - - if (payload.checksum !== checksum) { - throw new Error('Invalid checksum'); - } - } - - public async signApp(app: IAppStorageItem): Promise { - const checksum = this.calculateChecksumForApp(app); - const privateKey = await jose.importPKCS8(await this.getPrivateKey(), this.signingAlgorithm); - const signature = await new jose.SignJWT({ checksum, calg: this.checksumAlgorithm }) - .setProtectedHeader({ alg: this.signingAlgorithm }) - .setIssuedAt() - .sign(privateKey); - - return signature; - } - - private async getPrivateKey(): Promise { - if (!this.privateKey) { - this.privateKey = await this.federationBridge.getPrivateKey(); - } - return this.privateKey; - } - - private async getPublicKey(): Promise { - if (!this.publicKey) { - this.publicKey = await this.federationBridge.getPublicKey(); - } - return this.publicKey; - } - - private calculateChecksumForApp(app: IAppStorageItem, alg = this.checksumAlgorithm): string { - return createHash(alg).update(this.getFieldsForChecksum(app)).digest('hex'); - } - - private getFieldsForChecksum(obj: IAppStorageItem): string { - // These fields don't hold valuable information and should NOT invalidate - // the checksum - const fieldsToIgnore = ['_id', 'status', 'signature', 'updatedAt', 'createdAt', '_updatedAt', '_createdAt', 'settings']; - - // TODO revisit algorithm - const allKeys: Array = []; - const seen: Record = {}; - - JSON.stringify(obj, (key, value) => { - if (!(key in seen)) { - allKeys.push(key); - seen[key] = null; - } - return value; - }); - - const filteredKeys = allKeys.sort().filter((key) => !fieldsToIgnore.includes(key)); - - return JSON.stringify(obj, filteredKeys); - } -} diff --git a/packages/apps-engine/src/server/managers/AppSlashCommand.ts b/packages/apps-engine/src/server/managers/AppSlashCommand.ts deleted file mode 100644 index 29ea0a5c3c635..0000000000000 --- a/packages/apps-engine/src/server/managers/AppSlashCommand.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { AppMethod } from '../../definition/metadata'; -import type { ISlashCommand, ISlashCommandPreview, ISlashCommandPreviewItem, SlashCommandContext } from '../../definition/slashcommands'; -import type { ProxiedApp } from '../ProxiedApp'; -import type { AppLogStorage } from '../storage'; -import type { AppAccessorManager } from './AppAccessorManager'; - -export class AppSlashCommand { - /** - * States whether this command has been registered into the Rocket.Chat system or not. - */ - public isRegistered: boolean; - - /** - * Declares whether this command has been enabled or not, - * does not have to be inside of the Rocket.Chat system if `isRegistered` is false. - */ - public isEnabled: boolean; - - /** - * Proclaims whether this command has been disabled or not, - * does not have to be inside the Rocket.Chat system if `isRegistered` is false. - */ - public isDisabled: boolean; - - constructor( - public app: ProxiedApp, - public slashCommand: ISlashCommand, - ) { - this.isRegistered = false; - this.isEnabled = false; - this.isDisabled = false; - } - - public hasBeenRegistered(): void { - this.isDisabled = false; - this.isEnabled = true; - this.isRegistered = true; - } - - public async runExecutorOrPreviewer( - method: AppMethod._COMMAND_EXECUTOR | AppMethod._COMMAND_PREVIEWER, - context: SlashCommandContext, - logStorage: AppLogStorage, - accessors: AppAccessorManager, - ): Promise { - return this.runTheCode(method, logStorage, accessors, context, []); - } - - public async runPreviewExecutor( - previewItem: ISlashCommandPreviewItem, - context: SlashCommandContext, - logStorage: AppLogStorage, - accessors: AppAccessorManager, - ): Promise { - await this.runTheCode(AppMethod._COMMAND_PREVIEW_EXECUTOR, logStorage, accessors, context, [previewItem]); - } - - private async runTheCode( - method: AppMethod._COMMAND_EXECUTOR | AppMethod._COMMAND_PREVIEWER | AppMethod._COMMAND_PREVIEW_EXECUTOR, - _logStorage: AppLogStorage, - _accessors: AppAccessorManager, - context: SlashCommandContext, - runContextArgs: Array, - ): Promise { - const { command } = this.slashCommand; - - try { - const result = await this.app.getRuntimeController().sendRequest({ - method: `slashcommand:${command}:${method}`, - params: [...runContextArgs, context], - }); - - return result as void | ISlashCommandPreview; - } catch (e) { - // @TODO this needs to be revisited - console.error(e); - throw e; - } - } -} diff --git a/packages/apps-engine/src/server/managers/AppSlashCommandManager.ts b/packages/apps-engine/src/server/managers/AppSlashCommandManager.ts deleted file mode 100644 index b5c5662adf2a7..0000000000000 --- a/packages/apps-engine/src/server/managers/AppSlashCommandManager.ts +++ /dev/null @@ -1,477 +0,0 @@ -import { AppStatusUtils } from '../../definition/AppStatus'; -import { AppMethod } from '../../definition/metadata'; -import type { ISlashCommand, ISlashCommandPreview, ISlashCommandPreviewItem } from '../../definition/slashcommands'; -import { SlashCommandContext } from '../../definition/slashcommands'; -import type { AppManager } from '../AppManager'; -import type { CommandBridge } from '../bridges'; -import { CommandAlreadyExistsError, CommandHasAlreadyBeenTouchedError } from '../errors'; -import type { AppAccessorManager } from './AppAccessorManager'; -import { AppSlashCommand } from './AppSlashCommand'; -import { Room } from '../rooms/Room'; - -/** - * The command manager for the Apps. - * - * An App will add commands during their `initialize` method. - * Then once an App's `onEnable` is called and it returns true, - * only then will that App's commands be enabled. - * - * Registered means the command has been provided to the bridged system. - */ -export class AppSlashCommandManager { - private readonly bridge: CommandBridge; - - private readonly accessors: AppAccessorManager; - - /** - * Variable that contains the commands which have been provided by apps. - * The key of the top map is app id and the key of the inner map is the command - */ - private providedCommands: Map>; - - /** - * Contains the commands which have modified the system commands - */ - private modifiedCommands: Map; - - /** - * Contains the commands as keys and appId that touched it. - * Doesn't matter whether the app provided, modified, disabled, or enabled. - * As long as an app touched the command (besides to see if it exists), then it is listed here. - */ - private touchedCommandsToApps: Map; - - /** - * Contains the apps and the commands they have touched. The key is the appId and value is the commands. - * Doesn't matter whether the app provided, modified, disabled, or enabled. - * As long as an app touched the command (besides to see if it exists), then it is listed here. - */ - private appsTouchedCommands: Map>; - - constructor(private readonly manager: AppManager) { - this.bridge = this.manager.getBridges().getCommandBridge(); - this.accessors = this.manager.getAccessorManager(); - this.touchedCommandsToApps = new Map(); - this.appsTouchedCommands = new Map>(); - this.providedCommands = new Map>(); - this.modifiedCommands = new Map(); - } - - /** - * Checks whether an App can touch a command or not. There are only two ways an App can touch - * a command: - * 1. The command has yet to be touched - * 2. The app has already touched the command - * - * When do we consider an App touching a command? Whenever it adds, modifies, - * or removes one that it didn't provide. - * - * @param appId the app's id which to check for - * @param command the command to check about - * @returns whether or not the app can touch the command - */ - public canCommandBeTouchedBy(appId: string, command: string): boolean { - const cmd = command.toLowerCase().trim(); - return cmd && (!this.touchedCommandsToApps.has(cmd) || this.touchedCommandsToApps.get(cmd) === appId); - } - - /** - * Determines whether the command is already provided by an App or not. - * It is case insensitive. - * - * @param command the command to check if it exists or not - * @returns whether or not it is already provided - */ - public isAlreadyDefined(command: string): boolean { - const search = command.toLowerCase().trim(); - let exists = false; - - this.providedCommands.forEach((cmds) => { - if (cmds.has(search)) { - exists = true; - } - }); - - return exists; - } - - /** - * Adds a command to *be* registered. This will *not register* it with the - * bridged system yet as this is only called on an App's - * `initialize` method and an App might not get enabled. - * When adding a command, it can *not* already exist in the system - * (to overwrite) and another App can *not* have already touched or provided it. - * Apps are on a first come first serve basis for providing and modifying commands. - * - * @param appId the app's id which the command belongs to - * @param command the command to add to the system - */ - public async addCommand(appId: string, command: ISlashCommand): Promise { - command.command = command.command.toLowerCase().trim(); - - // Ensure the app can touch this command - if (!this.canCommandBeTouchedBy(appId, command.command)) { - throw new CommandHasAlreadyBeenTouchedError(command.command); - } - - // Verify the command doesn't exist already - if ((await this.bridge.doDoesCommandExist(command.command, appId)) || this.isAlreadyDefined(command.command)) { - throw new CommandAlreadyExistsError(command.command); - } - - const app = this.manager.getOneById(appId); - if (!app) { - throw new Error('App must exist in order for a command to be added.'); - } - - if (!this.providedCommands.has(appId)) { - this.providedCommands.set(appId, new Map()); - } - - this.providedCommands.get(appId).set(command.command, new AppSlashCommand(app, command)); - - // The app has now touched the command, so let's set it - this.setAsTouched(appId, command.command); - } - - /** - * Modifies an existing command. The command must either be the App's - * own command or a system command. One App can not modify another - * App's command. Apps are on a first come first serve basis as to whether - * or not they can touch or provide a command. If App "A" first provides, - * or overwrites, a command then App "B" can not touch that command. - * - * @param appId the app's id of the command to modify - * @param command the modified command to replace the current one with - */ - public async modifyCommand(appId: string, command: ISlashCommand): Promise { - command.command = command.command.toLowerCase().trim(); - - // Ensure the app can touch this command - if (!this.canCommandBeTouchedBy(appId, command.command)) { - throw new CommandHasAlreadyBeenTouchedError(command.command); - } - - const app = this.manager.getOneById(appId); - if (!app) { - throw new Error('App must exist in order to modify a command.'); - } - - const hasNotProvidedIt = !this.providedCommands.has(appId) || !this.providedCommands.get(appId).has(command.command); - - // They haven't provided (added) it and the bridged system doesn't have it, error out - if (hasNotProvidedIt && !(await this.bridge.doDoesCommandExist(command.command, appId))) { - throw new Error('You must first register a command before you can modify it.'); - } - - if (hasNotProvidedIt) { - await this.bridge.doModifyCommand(command, appId); - const regInfo = new AppSlashCommand(app, command); - regInfo.isDisabled = false; - regInfo.isEnabled = true; - regInfo.isRegistered = true; - this.modifiedCommands.set(command.command, regInfo); - } else { - this.providedCommands.get(appId).get(command.command).slashCommand = command; - } - - this.setAsTouched(appId, command.command); - } - - /** - * Goes and enables a command in the bridged system. The command - * which is being enabled must either be the App's or a system - * command which has yet to be touched by an App. - * - * @param appId the id of the app enabling the command - * @param command the command which is being enabled - */ - public async enableCommand(appId: string, command: string): Promise { - const cmd = command.toLowerCase().trim(); - - // Ensure the app can touch this command - if (!this.canCommandBeTouchedBy(appId, cmd)) { - throw new CommandHasAlreadyBeenTouchedError(cmd); - } - - // Handle if the App provided the command fist - if (this.providedCommands.has(appId) && this.providedCommands.get(appId).has(cmd)) { - const cmdInfo = this.providedCommands.get(appId).get(cmd); - - // A command marked as disabled can then be "enabled" but not be registered. - // This happens when an App is not enabled and they change the status of - // command based upon a setting they provide which a User can change. - if (!cmdInfo.isRegistered) { - cmdInfo.isDisabled = false; - cmdInfo.isEnabled = true; - } - - return; - } - - if (!(await this.bridge.doDoesCommandExist(cmd, appId))) { - throw new Error(`The command "${cmd}" does not exist to enable.`); - } - - await this.bridge.doEnableCommand(cmd, appId); - this.setAsTouched(appId, cmd); - } - - /** - * Renders an existing slash command un-usable. Whether that command is provided - * by the App calling this or a command provided by the bridged system, we don't care. - * However, an App can not disable a command which has already been touched - * by another App in some way. - * - * @param appId the app's id which is disabling the command - * @param command the command to disable in the bridged system - */ - public async disableCommand(appId: string, command: string): Promise { - const cmd = command.toLowerCase().trim(); - - // Ensure the app can touch this command - if (!this.canCommandBeTouchedBy(appId, cmd)) { - throw new CommandHasAlreadyBeenTouchedError(cmd); - } - - // Handle if the App provided the command fist - if (this.providedCommands.has(appId) && this.providedCommands.get(appId).has(cmd)) { - const cmdInfo = this.providedCommands.get(appId).get(cmd); - - // A command marked as enabled can then be "disabled" but not yet be registered. - // This happens when an App is not enabled and they change the status of - // command based upon a setting they provide which a User can change. - if (!cmdInfo.isRegistered) { - cmdInfo.isDisabled = true; - cmdInfo.isEnabled = false; - } - - return; - } - - if (!(await this.bridge.doDoesCommandExist(cmd, appId))) { - throw new Error(`The command "${cmd}" does not exist to disable.`); - } - - await this.bridge.doDisableCommand(cmd, appId); - this.setAsTouched(appId, cmd); - } - - /** - * Registers all of the commands for the provided app inside - * of the bridged system which then enables them. - * - * @param appId The app's id of which to register it's commands with the bridged system - */ - public async registerCommands(appId: string): Promise { - if (!this.providedCommands.has(appId)) { - return; - } - - const commands = this.providedCommands.get(appId); - for await (const [, appSlashCommand] of commands) { - if (appSlashCommand.isDisabled) { - continue; - } - await this.registerCommand(appId, appSlashCommand); - } - } - - /** - * Unregisters the commands from the system and restores the commands - * which the app modified in the system. - * - * @param appId the appId for the commands to purge - */ - public async unregisterCommands(appId: string): Promise { - if (this.providedCommands.has(appId)) { - const commands = this.providedCommands.get(appId); - for await (const [, appSlashCommand] of commands) { - const cmd = appSlashCommand.slashCommand.command; - await this.bridge.doUnregisterCommand(cmd, appId); - this.touchedCommandsToApps.delete(cmd); - if (!this.appsTouchedCommands.has(appId)) { - continue; - } - const ind = this.appsTouchedCommands.get(appId).indexOf(cmd); - this.appsTouchedCommands.get(appId).splice(ind, 1); - appSlashCommand.isRegistered = true; - } - - this.providedCommands.delete(appId); - } - - if (this.appsTouchedCommands.has(appId)) { - // The commands inside the appsTouchedCommands should now - // only be the ones which the App has enabled, disabled, or modified. - // We call restore to enable the commands provided by the bridged system - // or unmodify the commands modified by the App - this.appsTouchedCommands.get(appId).forEach((cmd) => { - // @NOTE this "restore" method isn't present in the bridge - // this.bridge.doRestoreCommand(cmd, appId); - this.modifiedCommands.get(cmd).isRegistered = false; - this.modifiedCommands.delete(cmd); - this.touchedCommandsToApps.delete(cmd); - }); - - this.appsTouchedCommands.delete(appId); - } - } - - /** - * Executes an App's command. - * - * @param command the command to execute - * @param context the context in which the command was entered - */ - public async executeCommand(command: string, context: SlashCommandContext): Promise { - const cmd = command.toLowerCase().trim(); - - if (!this.shouldCommandFunctionsRun(cmd)) { - return; - } - - const app = this.manager.getOneById(this.touchedCommandsToApps.get(cmd)); - - if (!app) { - throw new Error('App not found'); - } - - if (!AppStatusUtils.isEnabled(await app.getStatus())) { - throw new Error('App not enabled'); - } - - const appCmd = this.retrieveCommandInfo(cmd, app.getID()); - await appCmd.runExecutorOrPreviewer( - AppMethod._COMMAND_EXECUTOR, - this.ensureContext(context), - this.manager.getLogStorage(), - this.accessors, - ); - } - - public async getPreviews(command: string, context: SlashCommandContext): Promise { - const cmd = command.toLowerCase().trim(); - - if (!this.shouldCommandFunctionsRun(cmd)) { - return; - } - - const app = this.manager.getOneById(this.touchedCommandsToApps.get(cmd)); - - if (!app || AppStatusUtils.isDisabled(await app.getStatus())) { - // Just in case someone decides to do something they shouldn't - // let's ensure the app actually exists - return; - } - - const appCmd = this.retrieveCommandInfo(cmd, app.getID()); - - const result = await appCmd.runExecutorOrPreviewer( - AppMethod._COMMAND_PREVIEWER, - this.ensureContext(context), - this.manager.getLogStorage(), - this.accessors, - ); - - if (!result) { - // Failed to get the preview, thus returning is fine - return; - } - - return result; - } - - public async executePreview(command: string, previewItem: ISlashCommandPreviewItem, context: SlashCommandContext): Promise { - const cmd = command.toLowerCase().trim(); - - if (!this.shouldCommandFunctionsRun(cmd)) { - return; - } - - const app = this.manager.getOneById(this.touchedCommandsToApps.get(cmd)); - - if (!app || AppStatusUtils.isDisabled(await app.getStatus())) { - // Just in case someone decides to do something they shouldn't - // let's ensure the app actually exists - return; - } - - const appCmd = this.retrieveCommandInfo(cmd, app.getID()); - await appCmd.runPreviewExecutor(previewItem, this.ensureContext(context), this.manager.getLogStorage(), this.accessors); - } - - private ensureContext(context: SlashCommandContext): SlashCommandContext { - // Due to the internal changes for the usernames property, we need to ensure the room - // is a class and not just an interface - let room: Room; - if (context.getRoom() instanceof Room) { - room = context.getRoom() as Room; - } else { - room = new Room(context.getRoom(), this.manager); - } - - return new SlashCommandContext(context.getSender(), room, context.getArguments(), context.getThreadId(), context.getTriggerId()); - } - - /** - * Determines if the command's functions should run, - * this way the code isn't duplicated three times. - * - * @param command the lowercase and trimmed command - * @returns whether or not to continue - */ - private shouldCommandFunctionsRun(command: string): boolean { - // None of the Apps have touched the command to execute, - // thus we don't care so exit out - if (!this.touchedCommandsToApps.has(command)) { - return false; - } - - const appId = this.touchedCommandsToApps.get(command); - const cmdInfo = this.retrieveCommandInfo(command, appId); - - // Should the command information really not exist - // Or if the command hasn't been registered - // Or the command is disabled on our side - // then let's not execute it, as the App probably doesn't want it yet - if (!cmdInfo?.isRegistered || cmdInfo?.isDisabled) { - return false; - } - - return true; - } - - private retrieveCommandInfo(command: string, appId: string): AppSlashCommand { - return this.modifiedCommands.get(command) || this.providedCommands.get(appId).get(command); - } - - /** - * Sets that an App has been touched. - * - * @param appId the app's id which has touched the command - * @param command the command, lowercase and trimmed, which has been touched - */ - private setAsTouched(appId: string, command: string): void { - if (!this.appsTouchedCommands.has(appId)) { - this.appsTouchedCommands.set(appId, []); - } - - if (!this.appsTouchedCommands.get(appId).includes(command)) { - this.appsTouchedCommands.get(appId).push(command); - } - - this.touchedCommandsToApps.set(command, appId); - } - - /** - * Actually goes and provide's the bridged system with the command information. - * - * @param appId the app which is providing the command - * @param info the command's registration information - */ - private async registerCommand(appId: string, info: AppSlashCommand): Promise { - await this.bridge.doRegisterCommand(info.slashCommand, appId); - info.hasBeenRegistered(); - } -} diff --git a/packages/apps-engine/src/server/managers/AppVideoConfProvider.ts b/packages/apps-engine/src/server/managers/AppVideoConfProvider.ts deleted file mode 100644 index 2d1b13f3da9b7..0000000000000 --- a/packages/apps-engine/src/server/managers/AppVideoConfProvider.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { AppMethod } from '../../definition/metadata'; -import type { IBlock } from '../../definition/uikit'; -import type { - IVideoConferenceOptions, - IVideoConfProvider, - VideoConfData, - VideoConfDataExtended, -} from '../../definition/videoConfProviders'; -import type { VideoConference } from '../../definition/videoConferences'; -import type { IVideoConferenceUser } from '../../definition/videoConferences/IVideoConferenceUser'; -import type { ProxiedApp } from '../ProxiedApp'; -import { JSONRPC_METHOD_NOT_FOUND } from '../runtime/deno/AppsEngineDenoRuntime'; -import type { AppLogStorage } from '../storage'; -import type { AppAccessorManager } from './AppAccessorManager'; - -export class AppVideoConfProvider { - /** - * States whether this provider has been registered into the Rocket.Chat system or not. - */ - public isRegistered: boolean; - - constructor( - public app: ProxiedApp, - public provider: IVideoConfProvider, - ) { - this.isRegistered = false; - } - - public hasBeenRegistered(): void { - this.isRegistered = true; - } - - public async runIsFullyConfigured(logStorage: AppLogStorage, accessors: AppAccessorManager): Promise { - return !!(await this.runTheCode(AppMethod._VIDEOCONF_IS_CONFIGURED, logStorage, accessors, [])) as boolean; - } - - public async runGenerateUrl(call: VideoConfData, logStorage: AppLogStorage, accessors: AppAccessorManager): Promise { - return (await this.runTheCode(AppMethod._VIDEOCONF_GENERATE_URL, logStorage, accessors, [call])) as string; - } - - public async runCustomizeUrl( - call: VideoConfDataExtended, - user: IVideoConferenceUser | undefined, - options: IVideoConferenceOptions = {}, - logStorage: AppLogStorage, - accessors: AppAccessorManager, - ): Promise { - return (await this.runTheCode(AppMethod._VIDEOCONF_CUSTOMIZE_URL, logStorage, accessors, [call, user, options])) as string; - } - - public async runOnNewVideoConference(call: VideoConference, logStorage: AppLogStorage, accessors: AppAccessorManager): Promise { - await this.runTheCode(AppMethod._VIDEOCONF_NEW, logStorage, accessors, [call]); - } - - public async runOnVideoConferenceChanged(call: VideoConference, logStorage: AppLogStorage, accessors: AppAccessorManager): Promise { - await this.runTheCode(AppMethod._VIDEOCONF_CHANGED, logStorage, accessors, [call]); - } - - public async runOnUserJoin( - call: VideoConference, - user: IVideoConferenceUser | undefined, - logStorage: AppLogStorage, - accessors: AppAccessorManager, - ): Promise { - await this.runTheCode(AppMethod._VIDEOCONF_USER_JOINED, logStorage, accessors, [call, user]); - } - - public async runGetVideoConferenceInfo( - call: VideoConference, - user: IVideoConferenceUser | undefined, - logStorage: AppLogStorage, - accessors: AppAccessorManager, - ): Promise | undefined> { - return (await this.runTheCode(AppMethod._VIDEOCONF_GET_INFO, logStorage, accessors, [call, user])) as Array | undefined; - } - - private async runTheCode( - method: - | AppMethod._VIDEOCONF_GENERATE_URL - | AppMethod._VIDEOCONF_CUSTOMIZE_URL - | AppMethod._VIDEOCONF_IS_CONFIGURED - | AppMethod._VIDEOCONF_NEW - | AppMethod._VIDEOCONF_CHANGED - | AppMethod._VIDEOCONF_GET_INFO - | AppMethod._VIDEOCONF_USER_JOINED, - _logStorage: AppLogStorage, - _accessors: AppAccessorManager, - runContextArgs: Array, - ): Promise | undefined> { - const provider = this.provider.name; - - try { - const result = await this.app.getRuntimeController().sendRequest({ - method: `videoconference:${provider}:${method}`, - params: runContextArgs, - }); - - return result as string | boolean | Array | undefined; - } catch (e) { - if (e?.code === JSONRPC_METHOD_NOT_FOUND) { - if (method === AppMethod._VIDEOCONF_IS_CONFIGURED) { - return true; - } - if (![AppMethod._VIDEOCONF_GENERATE_URL, AppMethod._VIDEOCONF_CUSTOMIZE_URL].includes(method)) { - return undefined; - } - } - - // @TODO add error handling - console.log(e); - } - } -} diff --git a/packages/apps-engine/src/server/managers/AppVideoConfProviderManager.ts b/packages/apps-engine/src/server/managers/AppVideoConfProviderManager.ts deleted file mode 100644 index 7a77cadc768cf..0000000000000 --- a/packages/apps-engine/src/server/managers/AppVideoConfProviderManager.ts +++ /dev/null @@ -1,216 +0,0 @@ -import type { IBlock } from '../../definition/uikit'; -import type { - IVideoConferenceOptions, - IVideoConfProvider, - VideoConfData, - VideoConfDataExtended, -} from '../../definition/videoConfProviders'; -import type { VideoConference } from '../../definition/videoConferences'; -import type { IVideoConferenceUser } from '../../definition/videoConferences/IVideoConferenceUser'; -import type { AppManager } from '../AppManager'; -import type { VideoConferenceBridge } from '../bridges'; -import { VideoConfProviderAlreadyExistsError, VideoConfProviderNotRegisteredError } from '../errors'; -import type { AppAccessorManager } from './AppAccessorManager'; -import { AppPermissionManager } from './AppPermissionManager'; -import { AppVideoConfProvider } from './AppVideoConfProvider'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export class AppVideoConfProviderManager { - private readonly accessors: AppAccessorManager; - - private readonly bridge: VideoConferenceBridge; - - private videoConfProviders: Map>; - - private providerApps: Map; - - constructor(private readonly manager: AppManager) { - this.bridge = this.manager.getBridges().getVideoConferenceBridge(); - this.accessors = this.manager.getAccessorManager(); - - this.videoConfProviders = new Map>(); - this.providerApps = new Map(); - } - - public canProviderBeTouchedBy(appId: string, providerName: string): boolean { - const key = providerName.toLowerCase().trim(); - return (key && (!this.providerApps.has(key) || this.providerApps.get(key) === appId)) || false; - } - - public isAlreadyDefined(providerName: string): boolean { - const search = providerName.toLowerCase().trim(); - - for (const [, providers] of this.videoConfProviders) { - if (providers.has(search)) { - return true; - } - } - - return false; - } - - public addProvider(appId: string, provider: IVideoConfProvider): void { - const app = this.manager.getOneById(appId); - if (!app) { - throw new Error('App must exist in order for a video conference provider to be added.'); - } - - if (!AppPermissionManager.hasPermission(appId, AppPermissions.videoConference.provider)) { - throw new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.videoConference.provider], - }); - } - - const providerName = provider.name.toLowerCase().trim(); - if (!this.canProviderBeTouchedBy(appId, providerName)) { - throw new VideoConfProviderAlreadyExistsError(provider.name); - } - - if (!this.videoConfProviders.has(appId)) { - this.videoConfProviders.set(appId, new Map()); - } - - this.videoConfProviders.get(appId).set(providerName, new AppVideoConfProvider(app, provider)); - this.linkAppProvider(appId, providerName); - } - - public registerProviders(appId: string): void { - if (!this.videoConfProviders.has(appId)) { - return; - } - - const appProviders = this.videoConfProviders.get(appId); - if (!appProviders) { - return; - } - - for (const [, providerInfo] of appProviders) { - this.registerProvider(appId, providerInfo); - } - } - - public unregisterProviders(appId: string): void { - if (!this.videoConfProviders.has(appId)) { - return; - } - - const appProviders = this.videoConfProviders.get(appId); - for (const [, providerInfo] of appProviders) { - this.unregisterProvider(appId, providerInfo); - } - - this.videoConfProviders.delete(appId); - } - - public async isFullyConfigured(providerName: string): Promise { - const providerInfo = this.retrieveProviderInfo(providerName); - if (!providerInfo) { - throw new VideoConfProviderNotRegisteredError(providerName); - } - - return providerInfo.runIsFullyConfigured(this.manager.getLogStorage(), this.accessors); - } - - public async onNewVideoConference(providerName: string, call: VideoConference): Promise { - const providerInfo = this.retrieveProviderInfo(providerName); - if (!providerInfo) { - throw new VideoConfProviderNotRegisteredError(providerName); - } - - return providerInfo.runOnNewVideoConference(call, this.manager.getLogStorage(), this.accessors); - } - - public async onVideoConferenceChanged(providerName: string, call: VideoConference): Promise { - const providerInfo = this.retrieveProviderInfo(providerName); - if (!providerInfo) { - throw new VideoConfProviderNotRegisteredError(providerName); - } - - return providerInfo.runOnVideoConferenceChanged(call, this.manager.getLogStorage(), this.accessors); - } - - public async onUserJoin(providerName: string, call: VideoConference, user?: IVideoConferenceUser): Promise { - const providerInfo = this.retrieveProviderInfo(providerName); - if (!providerInfo) { - throw new VideoConfProviderNotRegisteredError(providerName); - } - - return providerInfo.runOnUserJoin(call, user, this.manager.getLogStorage(), this.accessors); - } - - public async getVideoConferenceInfo( - providerName: string, - call: VideoConference, - user?: IVideoConferenceUser, - ): Promise | undefined> { - const providerInfo = this.retrieveProviderInfo(providerName); - if (!providerInfo) { - throw new VideoConfProviderNotRegisteredError(providerName); - } - - return providerInfo.runGetVideoConferenceInfo(call, user, this.manager.getLogStorage(), this.accessors); - } - - public async generateUrl(providerName: string, call: VideoConfData): Promise { - const providerInfo = this.retrieveProviderInfo(providerName); - if (!providerInfo) { - throw new VideoConfProviderNotRegisteredError(providerName); - } - - return providerInfo.runGenerateUrl(call, this.manager.getLogStorage(), this.accessors); - } - - public async customizeUrl( - providerName: string, - call: VideoConfDataExtended, - user?: IVideoConferenceUser, - options?: IVideoConferenceOptions, - ): Promise { - const providerInfo = this.retrieveProviderInfo(providerName); - if (!providerInfo) { - throw new VideoConfProviderNotRegisteredError(providerName); - } - - return providerInfo.runCustomizeUrl(call, user, options, this.manager.getLogStorage(), this.accessors); - } - - private retrieveProviderInfo(providerName: string): AppVideoConfProvider | undefined { - const key = providerName.toLowerCase().trim(); - - for (const [, providers] of this.videoConfProviders) { - if (!providers.has(key)) { - continue; - } - - const provider = providers.get(key); - if (provider.isRegistered) { - return provider; - } - } - } - - private linkAppProvider(appId: string, providerName: string): void { - this.providerApps.set(providerName, appId); - } - - private registerProvider(appId: string, info: AppVideoConfProvider): void { - this.bridge.doRegisterProvider(info.provider, appId); - info.hasBeenRegistered(); - } - - private unregisterProvider(appId: string, info: AppVideoConfProvider): void { - const key = info.provider.name.toLowerCase().trim(); - - this.bridge.doUnRegisterProvider(info.provider, appId); - this.providerApps.delete(key); - - info.isRegistered = false; - - const map = this.videoConfProviders.get(appId); - if (map) { - map.delete(key); - } - } -} diff --git a/packages/apps-engine/src/server/managers/UIActionButtonManager.ts b/packages/apps-engine/src/server/managers/UIActionButtonManager.ts deleted file mode 100644 index b062017db9b24..0000000000000 --- a/packages/apps-engine/src/server/managers/UIActionButtonManager.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { AppStatusUtils } from '../../definition/AppStatus'; -import type { IUIActionButton, IUIActionButtonDescriptor } from '../../definition/ui'; -import type { AppManager } from '../AppManager'; -import type { AppActivationBridge } from '../bridges'; -import { AppPermissionManager } from './AppPermissionManager'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissions } from '../permissions/AppPermissions'; - -export class UIActionButtonManager { - private readonly activationBridge: AppActivationBridge; - - private readonly manager: AppManager; - - private registeredActionButtons = new Map>(); - - constructor(manager: AppManager) { - this.manager = manager; - this.activationBridge = manager.getBridges().getAppActivationBridge(); - } - - public registerActionButton(appId: string, button: IUIActionButtonDescriptor) { - if (!this.hasPermission(appId)) { - return false; - } - - if (!this.registeredActionButtons.has(appId)) { - this.registeredActionButtons.set(appId, new Map()); - } - - this.registeredActionButtons.get(appId).set(button.actionId, button); - - this.activationBridge.doActionsChanged(); - - return true; - } - - public clearAppActionButtons(appId: string) { - this.registeredActionButtons.set(appId, new Map()); - this.activationBridge.doActionsChanged(); - } - - public getAppActionButtons(appId: string) { - return this.registeredActionButtons.get(appId); - } - - public async getAllActionButtons(): Promise> { - const buttonList: Array = []; - - // Flatten map to a simple list of buttons from enabled apps only - for (const [appId, appButtons] of this.registeredActionButtons) { - const app = this.manager.getOneById(appId); - - // Skip if app doesn't exist - if (!app) { - continue; - } - - // or if it is not enabled - try { - const appStatus = await app.getStatus(); - if (!AppStatusUtils.isEnabled(appStatus)) { - continue; - } - } catch (error) { - // If we can't get the app status, skip this app's buttons - continue; - } - - // Add buttons from this enabled app - appButtons.forEach((button) => - buttonList.push({ - ...button, - appId, - }), - ); - } - - return buttonList; - } - - private hasPermission(appId: string) { - if (AppPermissionManager.hasPermission(appId, AppPermissions.ui.registerButtons)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.ui.registerButtons], - }), - ); - - return false; - } -} diff --git a/packages/apps-engine/src/server/managers/index.ts b/packages/apps-engine/src/server/managers/index.ts deleted file mode 100644 index 9d7b22c79bc53..0000000000000 --- a/packages/apps-engine/src/server/managers/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { AppAccessorManager } from './AppAccessorManager'; -import { AppApiManager } from './AppApiManager'; -import { AppExternalComponentManager } from './AppExternalComponentManager'; -import { AppLicenseManager } from './AppLicenseManager'; -import { AppListenerManager } from './AppListenerManager'; -import { AppOutboundCommunicationProviderManager } from './AppOutboundCommunicationProviderManager'; -import { AppSchedulerManager } from './AppSchedulerManager'; -import { AppSettingsManager } from './AppSettingsManager'; -import { AppSlashCommandManager } from './AppSlashCommandManager'; -import { AppVideoConfProviderManager } from './AppVideoConfProviderManager'; - -export { - AppAccessorManager, - AppLicenseManager, - AppListenerManager, - AppExternalComponentManager, - AppSettingsManager, - AppSlashCommandManager, - AppApiManager, - AppSchedulerManager, - AppVideoConfProviderManager, - AppOutboundCommunicationProviderManager, -}; diff --git a/packages/apps-engine/src/server/marketplace/IAppLicenseMetadata.ts b/packages/apps-engine/src/server/marketplace/IAppLicenseMetadata.ts deleted file mode 100644 index 0c8ceb9411979..0000000000000 --- a/packages/apps-engine/src/server/marketplace/IAppLicenseMetadata.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface IAppLicenseMetadata { - license: string; - version: number; - expireDate: Date; -} diff --git a/packages/apps-engine/src/server/marketplace/IMarketplaceInfo.ts b/packages/apps-engine/src/server/marketplace/IMarketplaceInfo.ts deleted file mode 100644 index 40e8928d9df4f..0000000000000 --- a/packages/apps-engine/src/server/marketplace/IMarketplaceInfo.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { IMarketplacePricingPlan } from './IMarketplacePricingPlan'; -import type { IMarketplaceSimpleBundleInfo } from './IMarketplaceSimpleBundleInfo'; -import type { IMarketplaceSubscriptionInfo } from './IMarketplaceSubscriptionInfo'; -import type { MarketplacePurchaseType } from './MarketplacePurchaseType'; -import type { IAppInfo } from '../../definition/metadata'; - -export interface IMarketplaceInfo extends IAppInfo { - categories: Array; - status: string; - reviewedNote?: string; - rejectionNote?: string; - isVisible: boolean; - isPurchased: boolean; - isSubscribed: boolean; - isBundled: boolean; - createdDate: string; - modifiedDate: string; - price: number; - subscriptionInfo?: IMarketplaceSubscriptionInfo; - purchaseType: MarketplacePurchaseType; - pricingPlans?: Array; - bundledIn?: Array; - isEnterpriseOnly?: boolean; -} diff --git a/packages/apps-engine/src/server/marketplace/IMarketplacePricingPlan.ts b/packages/apps-engine/src/server/marketplace/IMarketplacePricingPlan.ts deleted file mode 100644 index 7d3a7f5958c11..0000000000000 --- a/packages/apps-engine/src/server/marketplace/IMarketplacePricingPlan.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { IMarketplacePricingTier } from './IMarketplacePricingTier'; -import type { MarketplacePricingStrategy } from './MarketplacePricingStrategy'; - -export interface IMarketplacePricingPlan { - id: string; - enabled: boolean; - price: number; - isPerSeat: boolean; - strategy: MarketplacePricingStrategy; - tiers?: Array; -} diff --git a/packages/apps-engine/src/server/marketplace/IMarketplacePricingTier.ts b/packages/apps-engine/src/server/marketplace/IMarketplacePricingTier.ts deleted file mode 100644 index 65d3f593f7bff..0000000000000 --- a/packages/apps-engine/src/server/marketplace/IMarketplacePricingTier.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface IMarketplacePricingTier { - perUnit: boolean; - minimum: number; - maximum: number; - price: number; -} diff --git a/packages/apps-engine/src/server/marketplace/IMarketplaceSimpleBundleInfo.ts b/packages/apps-engine/src/server/marketplace/IMarketplaceSimpleBundleInfo.ts deleted file mode 100644 index 4fc68e39afc9b..0000000000000 --- a/packages/apps-engine/src/server/marketplace/IMarketplaceSimpleBundleInfo.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IMarketplaceSimpleBundleInfo { - bundleId: string; - bundleName: string; -} diff --git a/packages/apps-engine/src/server/marketplace/IMarketplaceSubscriptionInfo.ts b/packages/apps-engine/src/server/marketplace/IMarketplaceSubscriptionInfo.ts deleted file mode 100644 index 4be076b629ad9..0000000000000 --- a/packages/apps-engine/src/server/marketplace/IMarketplaceSubscriptionInfo.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { IAppLicenseMetadata } from './IAppLicenseMetadata'; -import type { MarketplaceSubscriptionStatus } from './MarketplaceSubscriptionStatus'; -import type { MarketplaceSubscriptionType } from './MarketplaceSubscriptionType'; - -export interface IMarketplaceSubscriptionInfo { - seats: number; - maxSeats: number; - startDate: string; - periodEnd: string; - isSubscripbedViaBundle: boolean; - endDate?: string; - typeOf: MarketplaceSubscriptionType; - status: MarketplaceSubscriptionStatus; - license: IAppLicenseMetadata; -} diff --git a/packages/apps-engine/src/server/marketplace/MarketplacePricingStrategy.ts b/packages/apps-engine/src/server/marketplace/MarketplacePricingStrategy.ts deleted file mode 100644 index 473f4428430c0..0000000000000 --- a/packages/apps-engine/src/server/marketplace/MarketplacePricingStrategy.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum MarketplacePricingStrategy { - PricingStrategyOnce = 'once', - PricingStrategyMonthly = 'monthly', - PricingStrategyYearly = 'yearly', -} diff --git a/packages/apps-engine/src/server/marketplace/MarketplacePurchaseType.ts b/packages/apps-engine/src/server/marketplace/MarketplacePurchaseType.ts deleted file mode 100644 index e4deef5b7f9c3..0000000000000 --- a/packages/apps-engine/src/server/marketplace/MarketplacePurchaseType.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum MarketplacePurchaseType { - PurchaseTypeBuy = 'buy', - PurchaseTypeSubscription = 'subscription', -} diff --git a/packages/apps-engine/src/server/marketplace/MarketplaceSubscriptionStatus.ts b/packages/apps-engine/src/server/marketplace/MarketplaceSubscriptionStatus.ts deleted file mode 100644 index c57bc59440cee..0000000000000 --- a/packages/apps-engine/src/server/marketplace/MarketplaceSubscriptionStatus.ts +++ /dev/null @@ -1,10 +0,0 @@ -export enum MarketplaceSubscriptionStatus { - // PurchaseSubscriptionStatusTrialing is when the subscription is in the trial phase - PurchaseSubscriptionStatusTrialing = 'trialing', - // PurchaseSubscriptionStatusActive is when the subscription is active and being billed for - PurchaseSubscriptionStatusActive = 'active', - // PurchaseSubscriptionStatusCanceled is when the subscription is inactive due to being canceled - PurchaseSubscriptionStatusCanceled = 'canceled', - // PurchaseSubscriptionStatusPastDue is when the subscription was active but is now past due as a result of incorrect billing information - PurchaseSubscriptionStatusPastDue = 'pastDue', -} diff --git a/packages/apps-engine/src/server/marketplace/MarketplaceSubscriptionType.ts b/packages/apps-engine/src/server/marketplace/MarketplaceSubscriptionType.ts deleted file mode 100644 index 30256fc48cf1e..0000000000000 --- a/packages/apps-engine/src/server/marketplace/MarketplaceSubscriptionType.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum MarketplaceSubscriptionType { - SubscriptionTypeApp = 'app', - SubscriptionTypeService = 'service', -} diff --git a/packages/apps-engine/src/server/marketplace/index.ts b/packages/apps-engine/src/server/marketplace/index.ts deleted file mode 100644 index d06ea988cc43d..0000000000000 --- a/packages/apps-engine/src/server/marketplace/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IAppLicenseMetadata } from './IAppLicenseMetadata'; -import { IMarketplaceInfo } from './IMarketplaceInfo'; -import { IMarketplacePricingPlan } from './IMarketplacePricingPlan'; -import { IMarketplacePricingTier } from './IMarketplacePricingTier'; -import { IMarketplaceSimpleBundleInfo } from './IMarketplaceSimpleBundleInfo'; -import { IMarketplaceSubscriptionInfo } from './IMarketplaceSubscriptionInfo'; - -export { - IAppLicenseMetadata, - IMarketplaceInfo, - IMarketplacePricingPlan, - IMarketplacePricingTier, - IMarketplaceSimpleBundleInfo, - IMarketplaceSubscriptionInfo, -}; diff --git a/packages/apps-engine/src/server/marketplace/license/AppLicenseValidationResult.ts b/packages/apps-engine/src/server/marketplace/license/AppLicenseValidationResult.ts deleted file mode 100644 index 2946d40111f54..0000000000000 --- a/packages/apps-engine/src/server/marketplace/license/AppLicenseValidationResult.ts +++ /dev/null @@ -1,56 +0,0 @@ -export class AppLicenseValidationResult { - private errors: { [key: string]: string } = {}; - - private warnings: { [key: string]: string } = {}; - - private validated = false; - - private appId: string; - - public addError(field: string, message: string): void { - this.errors[field] = message; - } - - public addWarning(field: string, message: string): void { - this.warnings[field] = message; - } - - public get hasErrors(): boolean { - return !!Object.keys(this.errors).length; - } - - public get hasWarnings(): boolean { - return !!Object.keys(this.warnings).length; - } - - public get hasBeenValidated(): boolean { - return this.validated; - } - - public setValidated(validated: boolean): void { - this.validated = validated; - } - - public setAppId(appId: string): void { - this.appId = appId; - } - - public getAppId(): string { - return this.appId; - } - - public getErrors(): object { - return this.errors; - } - - public getWarnings(): object { - return this.warnings; - } - - public toJSON(): object { - return { - errors: this.errors, - warnings: this.warnings, - }; - } -} diff --git a/packages/apps-engine/src/server/marketplace/license/Crypto.ts b/packages/apps-engine/src/server/marketplace/license/Crypto.ts deleted file mode 100644 index 95e1db449fcd9..0000000000000 --- a/packages/apps-engine/src/server/marketplace/license/Crypto.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { publicDecrypt } from 'crypto'; - -import type { IInternalBridge } from '../../bridges'; - -export class Crypto { - constructor(private readonly internalBridge: IInternalBridge) {} - - public async decryptLicense(content: string): Promise { - const publicKeySetting = await this.internalBridge.doGetWorkspacePublicKey(); - - if (!publicKeySetting?.value) { - throw new Error('Public key not available, cannot decrypt'); // TODO: add custom error? - } - - const decoded = publicDecrypt(publicKeySetting.value, Buffer.from(content, 'base64')); - - let license; - try { - license = JSON.parse(decoded.toString()); - } catch (error) { - throw new Error('Invalid license provided'); - } - - return license; - } -} diff --git a/packages/apps-engine/src/server/marketplace/license/index.ts b/packages/apps-engine/src/server/marketplace/license/index.ts deleted file mode 100644 index 573b1e7fbae52..0000000000000 --- a/packages/apps-engine/src/server/marketplace/license/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { AppLicenseValidationResult } from './AppLicenseValidationResult'; -import { Crypto } from './Crypto'; - -export { AppLicenseValidationResult, Crypto }; diff --git a/packages/apps-engine/src/server/messages/Message.ts b/packages/apps-engine/src/server/messages/Message.ts deleted file mode 100644 index 60a18d07745eb..0000000000000 --- a/packages/apps-engine/src/server/messages/Message.ts +++ /dev/null @@ -1,109 +0,0 @@ -import type { LayoutBlock } from '@rocket.chat/ui-kit'; - -import type { IMessage, IMessageAttachment, IMessageFile, IMessageReactions } from '../../definition/messages'; -import type { IUser, IUserLookup } from '../../definition/users'; -import type { AppManager } from '../AppManager'; -import { Room } from '../rooms/Room'; - -export class Message implements IMessage { - public id?: string; - - public sender: IUser; - - public text?: string; - - public createdAt?: Date; - - public updatedAt?: Date; - - public editor?: IUser; - - public editedAt?: Date; - - public emoji?: string; - - public avatarUrl?: string; - - public alias?: string; - - public attachments?: Array; - - public reactions?: IMessageReactions; - - public groupable?: boolean; - - public parseUrls?: boolean; - - public customFields?: { [key: string]: any }; - - public threadId?: string; - - public file?: IMessageFile; - - public blocks?: Array; - - public starred?: Array<{ _id: string }>; - - public pinned?: boolean; - - public pinnedAt?: Date; - - public pinnedBy?: IUserLookup; - - private _ROOM: Room; - - public get room(): Room { - return this._ROOM; - } - - public set room(room) { - this._ROOM = new Room(room, this.manager); - } - - public constructor( - message: IMessage, - private manager: AppManager, - ) { - Object.assign(this, message); - } - - get value(): object { - return { - id: this.id, - sender: this.sender, - text: this.text, - createdAt: this.createdAt, - updatedAt: this.updatedAt, - editor: this.editor, - editedAt: this.editedAt, - emoji: this.emoji, - avatarUrl: this.avatarUrl, - alias: this.alias, - attachments: this.attachments, - reactions: this.reactions, - groupable: this.groupable, - parseUrls: this.parseUrls, - customFields: this.customFields, - threadId: this.threadId, - room: this.room, - file: this.file, - blocks: this.blocks, - starred: this.starred, - pinned: this.pinned, - pinnedAt: this.pinnedAt, - pinnedBy: this.pinnedBy, - }; - } - - public toJSON() { - return this.value; - } - - public toString() { - return this.value; - } - - public valueOf() { - return this.value; - } -} diff --git a/packages/apps-engine/src/server/misc/UIHelper.ts b/packages/apps-engine/src/server/misc/UIHelper.ts deleted file mode 100644 index e71feb6c71b78..0000000000000 --- a/packages/apps-engine/src/server/misc/UIHelper.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { LayoutBlock } from '@rocket.chat/ui-kit'; -import { v4 as uuid } from 'uuid'; - -import type { IBlock } from '../../definition/uikit'; - -export class UIHelper { - /** - * Assign blockId, appId and actionId to every block/element inside the array - * @param blocks the blocks that will be iterated and assigned the ids - * @param appId the appId that will be assigned to - * @returns the array of block with the ids properties assigned - */ - public static assignIds(blocks: Array, appId: string): Array { - blocks.forEach((block: (IBlock | LayoutBlock) & { appId?: string; blockId?: string; elements?: Array }) => { - if (!block.appId) { - block.appId = appId; - } - if (!block.blockId) { - block.blockId = uuid(); - } - if (block.elements) { - block.elements.forEach((element) => { - if (!element.actionId) { - element.actionId = uuid(); - } - }); - } - }); - - return blocks; - } -} diff --git a/packages/apps-engine/src/server/misc/Utilities.ts b/packages/apps-engine/src/server/misc/Utilities.ts deleted file mode 100644 index f126d379752e8..0000000000000 --- a/packages/apps-engine/src/server/misc/Utilities.ts +++ /dev/null @@ -1,36 +0,0 @@ -import cloneDeep = require('lodash.clonedeep'); - -export class Utilities { - public static deepClone(item: T): T { - return cloneDeep(item); - } - - public static deepFreeze(item: any): T { - Object.freeze(item); - - Object.getOwnPropertyNames(item).forEach((prop: string) => { - if ( - item.hasOwnProperty(prop) && - item[prop] !== null && - (typeof item[prop] === 'object' || typeof item[prop] === 'function') && - !Object.isFrozen(item[prop]) - ) { - Utilities.deepFreeze(item[prop]); - } - }); - - return item; - } - - public static deepCloneAndFreeze(item: T): T { - return Utilities.deepFreeze(Utilities.deepClone(item)); - } - - public static omit(object: { [key: string]: any }, keys: Array) { - const cloned = this.deepClone(object); - for (const key of keys) { - delete cloned[key]; - } - return cloned; - } -} diff --git a/packages/apps-engine/src/server/oauth2/OAuth2Client.ts b/packages/apps-engine/src/server/oauth2/OAuth2Client.ts deleted file mode 100644 index ccb0d23d5d5e7..0000000000000 --- a/packages/apps-engine/src/server/oauth2/OAuth2Client.ts +++ /dev/null @@ -1,337 +0,0 @@ -import { URL } from 'url'; - -import type { App } from '../../definition/App'; -import type { IConfigurationExtend, IHttp, IModify, IPersistence, IRead } from '../../definition/accessors'; -import { HttpStatusCode } from '../../definition/accessors'; -import type { IApiEndpointInfo, IApiRequest, IApiResponse } from '../../definition/api'; -import { ApiSecurity, ApiVisibility } from '../../definition/api'; -import { RocketChatAssociationModel, RocketChatAssociationRecord } from '../../definition/metadata'; -import type { IAuthData, IOAuth2Client, IOAuth2ClientOptions } from '../../definition/oauth2/IOAuth2'; -import { SettingType } from '../../definition/settings'; -import type { IUser } from '../../definition/users'; - -export enum GrantType { - RefreshToken = 'refresh_token', - AuthorizationCode = 'authorization_code', -} - -export class OAuth2Client implements IOAuth2Client { - private defaultContents = { - success: `
\ -

\ - Authorization went successfully
\ - You can close this tab now
\ -

\ -
`, - failed: `
\ -

\ - Oops, something went wrong, please try again or in case it still does not work, contact the administrator.\ -

\ -
`, - }; - - constructor( - private readonly app: App, - private readonly config: IOAuth2ClientOptions, - ) {} - - public async setup(configuration: IConfigurationExtend): Promise { - configuration.api.provideApi({ - security: ApiSecurity.UNSECURE, - visibility: ApiVisibility.PUBLIC, - endpoints: [ - { - path: `${this.config.alias}-callback`, - get: this.handleOAuthCallback.bind(this), - }, - ], - }); - - await Promise.all([ - configuration.settings.provideSetting({ - id: `${this.config.alias}-oauth-client-id`, - type: SettingType.STRING, - public: true, - required: true, - packageValue: '', - i18nLabel: `${this.config.alias}-oauth-client-id`, - }), - - configuration.settings.provideSetting({ - id: `${this.config.alias}-oauth-clientsecret`, - type: SettingType.STRING, - public: true, - required: true, - packageValue: '', - i18nLabel: `${this.config.alias}-oauth-client-secret`, - }), - ]); - } - - public async getUserAuthorizationUrl(user: IUser, scopes?: Array): Promise { - const redirectUri = this.app.getAccessors().providedApiEndpoints[0].computedPath.substring(1); - - const siteUrl = await this.getBaseURLWithoutTrailingSlash(); - - const finalScopes = ([] as Array).concat(this.config.defaultScopes || [], scopes || []); - - const { authUri } = this.config; - - const clientId = await this.app - .getAccessors() - .reader.getEnvironmentReader() - .getSettings() - .getValueById(`${this.config.alias}-oauth-client-id`); - - const url = new URL(authUri, siteUrl); - - url.searchParams.set('response_type', 'code'); - url.searchParams.set('redirect_uri', `${siteUrl}/${redirectUri}`); - url.searchParams.set('state', user.id); - url.searchParams.set('client_id', clientId); - url.searchParams.set('access_type', 'offline'); - - if (finalScopes.length > 0) { - url.searchParams.set('scope', finalScopes.join(' ')); - } - - return url; - } - - public async getAccessTokenForUser(user: IUser): Promise { - const associations = [ - new RocketChatAssociationRecord(RocketChatAssociationModel.USER, user.id), - new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, `${this.config.alias}-oauth-connection`), - ]; - - const [result] = (await this.app.getAccessors().reader.getPersistenceReader().readByAssociations(associations)) as unknown as Array< - IAuthData | undefined - >; - - return result; - } - - public async refreshUserAccessToken(user: IUser, persis: IPersistence): Promise { - try { - const tokenInfo = await this.getAccessTokenForUser(user); - - if (!tokenInfo) { - throw new Error('User has no access token information'); - } - - if (!tokenInfo.refreshToken) { - throw new Error('User token information has no refresh token available'); - } - - const { - config: { refreshTokenUri }, - } = this; - - const clientId = await this.app - .getAccessors() - .reader.getEnvironmentReader() - .getSettings() - .getValueById(`${this.config.alias}-oauth-client-id`); - - const clientSecret = await this.app - .getAccessors() - .reader.getEnvironmentReader() - .getSettings() - .getValueById(`${this.config.alias}-oauth-clientsecret`); - - const siteUrl = await this.getBaseURLWithoutTrailingSlash(); - - const redirectUri = this.app.getAccessors().providedApiEndpoints[0].computedPath.substring(1); - - const url = new URL(refreshTokenUri); - - url.searchParams.set('client_id', clientId); - url.searchParams.set('client_secret', clientSecret); - url.searchParams.set('redirect_uri', `${siteUrl}/${redirectUri}`); - url.searchParams.set('refresh_token', tokenInfo.refreshToken); - url.searchParams.set('grant_type', GrantType.RefreshToken); - - const { content, statusCode } = await this.app.getAccessors().http.post(url.href); - - if (statusCode !== 200) { - throw new Error('Request to provider was unsuccessful. Check logs for more information'); - } - - const { access_token, expires_in, refresh_token, scope } = JSON.parse(content as string); - - if (!access_token) { - throw new Error('No access token returned by the provider'); - } - - const authData: IAuthData = { - scope, - token: access_token, - expiresAt: expires_in, - refreshToken: refresh_token || tokenInfo.refreshToken, - }; - - await this.saveToken(authData, user.id, persis); - - return authData; - } catch (error) { - this.app.getLogger().error(error); - throw error; - } - } - - public async revokeUserAccessToken(user: IUser, persis: IPersistence): Promise { - try { - const tokenInfo = await this.getAccessTokenForUser(user); - - if (!tokenInfo?.token) { - throw new Error('No access token available for this user.'); - } - - const url = new URL(this.config.revokeTokenUri); - - url.searchParams.set('token', tokenInfo?.token); - - const result = await this.app.getAccessors().http.post(url.href); - - if (result.statusCode !== 200) { - throw new Error('Provider did not allow token to be revoked'); - } - - await this.removeToken({ userId: user.id, persis }); - - return true; - } catch (error) { - this.app.getLogger().error(error); - return false; - } - } - - private async getBaseURLWithoutTrailingSlash(): Promise { - const SITE_URL = 'Site_Url'; - const url = await this.app.getAccessors().environmentReader.getServerSettings().getValueById(SITE_URL); - - if (url.endsWith('/')) { - return url.substr(0, url.length - 1); - } - return url; - } - - private async handleOAuthCallback( - request: IApiRequest, - endpoint: IApiEndpointInfo, - read: IRead, - modify: IModify, - http: IHttp, - persis: IPersistence, - ): Promise { - try { - const { - query: { code, state }, - } = request; - - const user = await this.app.getAccessors().reader.getUserReader().getById(state); - - if (!user) { - throw new Error('User could not be determined.'); - } - - // User chose not to authorize the access - if (!code) { - const failedResult = await this.config.authorizationCallback?.(undefined, user, read, modify, http, persis); - - return { - status: HttpStatusCode.UNAUTHORIZED, - content: failedResult?.responseContent || this.defaultContents.failed, - }; - } - - const siteUrl = await this.getBaseURLWithoutTrailingSlash(); - - const accessTokenUrl = this.config.accessTokenUri; - - const redirectUri = this.app.getAccessors().providedApiEndpoints[0].computedPath.substring(1); - - const clientId = await this.app - .getAccessors() - .reader.getEnvironmentReader() - .getSettings() - .getValueById(`${this.config.alias}-oauth-client-id`); - - const clientSecret = await this.app - .getAccessors() - .reader.getEnvironmentReader() - .getSettings() - .getValueById(`${this.config.alias}-oauth-clientsecret`); - - const url = new URL(accessTokenUrl, siteUrl); - - url.searchParams.set('client_id', clientId); - url.searchParams.set('redirect_uri', `${siteUrl}/${redirectUri}`); - url.searchParams.set('code', code); - url.searchParams.set('client_secret', clientSecret); - url.searchParams.set('access_type', 'offline'); - url.searchParams.set('grant_type', GrantType.AuthorizationCode); - - const { content, statusCode } = await http.post(url.href, { - headers: { Accept: 'application/json' }, - }); - - // If provider had a server error, nothing we can do - if (statusCode >= 500) { - throw new Error('Request for access token failed. Check logs for more information'); - } - - const response = JSON.parse(content as string); - const { access_token, expires_in, refresh_token, scope } = response; - - const authData: IAuthData = { - scope, - token: access_token, - expiresAt: expires_in, - refreshToken: refresh_token, - }; - - const result = await this.config.authorizationCallback?.(authData, user, read, modify, http, persis); - - await this.saveToken(authData, user.id, persis); - - return { - status: statusCode, - content: result?.responseContent || this.defaultContents.success, - }; - } catch (error) { - this.app.getLogger().error(error); - return { - status: HttpStatusCode.INTERNAL_SERVER_ERROR, - content: this.defaultContents.failed, - }; - } - } - - private async saveToken(authData: IAuthData, userId: string, persis: IPersistence): Promise { - const { scope, token, expiresAt, refreshToken } = authData; - - return persis.updateByAssociations( - [ - new RocketChatAssociationRecord(RocketChatAssociationModel.USER, userId), - new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, `${this.config.alias}-oauth-connection`), - ], - { - scope, - token, - expiresAt: expiresAt || '', - refreshToken: refreshToken || '', - }, - true, // we want to create the record if it doesn't exist - ); - } - - private async removeToken({ userId, persis }: { userId: string; persis: IPersistence }): Promise { - const [result] = (await persis.removeByAssociations([ - new RocketChatAssociationRecord(RocketChatAssociationModel.USER, userId), - new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, `${this.config.alias}-oauth-connection`), - ])) as unknown as Array; - - return result; - } -} diff --git a/packages/apps-engine/src/server/permissions/AppPermissions.ts b/packages/apps-engine/src/server/permissions/AppPermissions.ts deleted file mode 100644 index 138b057858503..0000000000000 --- a/packages/apps-engine/src/server/permissions/AppPermissions.ts +++ /dev/null @@ -1,168 +0,0 @@ -import type { - INetworkingPermission, - IPermission, - IReadSettingPermission, - IWorkspaceTokenPermission, -} from '../../definition/permissions/IPermission'; - -/** - * @description - * - * App Permission naming rules: - * - * 'scope-name': { - * 'permission-name': { name: 'scope-name.permission-name' } - * } - * - * You can retrive this permission by using: - * AppPermissions['scope-name']['permission-name'] -> { name: 'scope-name.permission-name' } - * - * @example - * - * AppPermissions.upload.read // { name: 'upload.read', domains: [] } - */ -export const AppPermissions = { - 'user': { - read: { name: 'user.read' }, - write: { name: 'user.write' }, - }, - 'upload': { - read: { name: 'upload.read' }, - write: { name: 'upload.write' }, - }, - 'email': { - send: { name: 'email.send' }, - }, - 'ui': { - interaction: { name: 'ui.interact' }, - registerButtons: { name: 'ui.registerButtons' }, - }, - 'setting': { - read: { name: 'server-setting.read', hiddenSettings: [] } as IReadSettingPermission, - write: { name: 'server-setting.write' }, - }, - 'room': { - read: { name: 'room.read' }, - write: { name: 'room.write' }, - 'system-view-all': { name: 'room.system.view-all' }, - }, - 'role': { - read: { name: 'role.read' }, - write: { name: 'role.write' }, - }, - 'message': { - read: { name: 'message.read' }, - write: { name: 'message.write' }, - }, - 'moderation': { - read: { name: 'moderation.read' }, - write: { name: 'moderation.write' }, - }, - 'contact': { - read: { name: 'contact.read' }, - write: { name: 'contact.write' }, - }, - 'threads': { - read: { name: 'threads.read' }, - }, - 'livechat-status': { - read: { name: 'livechat-status.read' }, - }, - 'livechat-custom-fields': { - write: { name: 'livechat-custom-fields.write' }, - }, - 'livechat-visitor': { - read: { name: 'livechat-visitor.read' }, - write: { name: 'livechat-visitor.write' }, - }, - 'livechat-message': { - read: { name: 'livechat-message.read' }, - write: { name: 'livechat-message.write' }, - multiple: { name: 'livechat-message.multiple' }, - }, - 'livechat-room': { - read: { name: 'livechat-room.read' }, - write: { name: 'livechat-room.write' }, - }, - 'livechat-department': { - read: { name: 'livechat-department.read' }, - write: { name: 'livechat-department.write' }, - multiple: { name: 'livechat-department.multiple' }, - }, - 'env': { - read: { name: 'env.read' }, - }, - 'cloud': { - 'workspace-token': { name: 'cloud.workspace-token', scopes: [] } as IWorkspaceTokenPermission, - }, - // Internal permissions - 'scheduler': { - default: { name: 'scheduler' }, - }, - 'networking': { - default: { name: 'networking', domains: [] } as INetworkingPermission, - }, - 'persistence': { - default: { name: 'persistence' }, - }, - 'command': { - default: { name: 'slashcommand' }, - }, - 'videoConference': { - read: { name: 'video-conference.read' }, - write: { name: 'video-conference.write' }, - provider: { name: 'video-conference-provider' }, - }, - 'apis': { - default: { name: 'api' }, - }, - 'oauth-app': { - read: { name: 'oauth-app.read' }, - write: { name: 'oauth-app.write' }, - }, - 'outboundComms': { - provide: { name: 'outbound-communication.provide' }, - }, - 'experimental': { - default: { name: 'experimental.default' }, - }, -}; - -/** - * @description - * Default permissions for apps - * Used to ensure backward compatibility with apps - * that were developed before the permission system was introduced. - */ -export const defaultPermissions: Array = [ - AppPermissions.user.read, - AppPermissions.user.write, - AppPermissions.upload.read, - AppPermissions.upload.write, - AppPermissions.ui.interaction, - AppPermissions.setting.read, - AppPermissions.setting.write, - AppPermissions.room.read, - AppPermissions.room.write, - AppPermissions.message.read, - AppPermissions.message.write, - AppPermissions['livechat-department'].read, - AppPermissions['livechat-department'].write, - AppPermissions['livechat-room'].read, - AppPermissions['livechat-room'].write, - AppPermissions['livechat-message'].read, - AppPermissions['livechat-message'].write, - AppPermissions['livechat-visitor'].read, - AppPermissions['livechat-visitor'].write, - AppPermissions['livechat-status'].read, - AppPermissions['livechat-custom-fields'].write, - AppPermissions.scheduler.default, - AppPermissions.networking.default, - AppPermissions.persistence.default, - AppPermissions.env.read, - AppPermissions.command.default, - AppPermissions.videoConference.provider, - AppPermissions.videoConference.read, - AppPermissions.videoConference.write, - AppPermissions.apis.default, -]; diff --git a/packages/apps-engine/src/server/rooms/Room.ts b/packages/apps-engine/src/server/rooms/Room.ts deleted file mode 100644 index 83568d0eae907..0000000000000 --- a/packages/apps-engine/src/server/rooms/Room.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type { IRoom, RoomType } from '../../definition/rooms'; -import type { IUser } from '../../definition/users'; -import type { AppManager } from '../AppManager'; - -const PrivateManager = Symbol('RoomPrivateManager'); - -export class Room implements IRoom { - public id: string; - - public displayName?: string; - - public slugifiedName: string; - - public type: RoomType; - - public creator: IUser; - - public isDefault?: boolean; - - public isReadOnly?: boolean; - - public displaySystemMessages?: boolean; - - public messageCount?: number; - - public createdAt?: Date; - - public updatedAt?: Date; - - public lastModifiedAt?: Date; - - public customFields?: { [key: string]: any }; - - public userIds?: Array; - - private _USERNAMES: Array; - - private [PrivateManager]: AppManager; - - /** - * @deprecated - */ - public get usernames(): Array { - // Get usernames - if (!this._USERNAMES) { - this._USERNAMES = this[PrivateManager].getBridges().getInternalBridge().doGetUsernamesOfRoomByIdSync(this.id); - } - - return this._USERNAMES; - } - - public set usernames(usernames) {} - - 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 { - return { - id: this.id, - displayName: this.displayName, - slugifiedName: this.slugifiedName, - type: this.type, - creator: this.creator, - isDefault: this.isDefault, - isReadOnly: this.isReadOnly, - displaySystemMessages: this.displaySystemMessages, - messageCount: this.messageCount, - createdAt: this.createdAt, - updatedAt: this.updatedAt, - lastModifiedAt: this.lastModifiedAt, - customFields: this.customFields, - userIds: this.userIds, - }; - } - - public async getUsernames(): Promise> { - // Get usernames - if (!this._USERNAMES) { - this._USERNAMES = await this[PrivateManager].getBridges().getInternalBridge().doGetUsernamesOfRoomById(this.id); - } - - return this._USERNAMES; - } - - public toJSON() { - return this.value; - } - - public toString() { - return this.value; - } - - public valueOf() { - return this.value; - } -} diff --git a/packages/apps-engine/src/server/runtime/AppsEngineEmptyRuntime.ts b/packages/apps-engine/src/server/runtime/AppsEngineEmptyRuntime.ts deleted file mode 100644 index cfd2b86a9fa23..0000000000000 --- a/packages/apps-engine/src/server/runtime/AppsEngineEmptyRuntime.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { IAppsEngineRuntimeOptions } from './AppsEngineRuntime'; -import { AppsEngineRuntime } from './AppsEngineRuntime'; -import type { App } from '../../definition/App'; - -export class AppsEngineEmptyRuntime extends AppsEngineRuntime { - public static async runCode(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { - throw new Error('Empty runtime does not support code execution'); - } - - public static runCodeSync(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): any { - throw new Error('Empty runtime does not support code execution'); - } - - constructor(readonly app: App) { - super(app, () => {}); - } - - public async runInSandbox(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { - return Promise.reject(new Error('Empty runtime does not support execution')); - } -} diff --git a/packages/apps-engine/src/server/runtime/AppsEngineNodeRuntime.ts b/packages/apps-engine/src/server/runtime/AppsEngineNodeRuntime.ts deleted file mode 100644 index a686e154547af..0000000000000 --- a/packages/apps-engine/src/server/runtime/AppsEngineNodeRuntime.ts +++ /dev/null @@ -1,74 +0,0 @@ -import * as timers from 'timers'; -import * as vm from 'vm'; - -import type { IAppsEngineRuntimeOptions } from './AppsEngineRuntime'; -import { APPS_ENGINE_RUNTIME_DEFAULT_TIMEOUT, AppsEngineRuntime, getFilenameForApp } from './AppsEngineRuntime'; -import type { App } from '../../definition/App'; - -export class AppsEngineNodeRuntime extends AppsEngineRuntime { - public static defaultRuntimeOptions = { - timeout: APPS_ENGINE_RUNTIME_DEFAULT_TIMEOUT, - }; - - public static defaultContext = { - ...timers, - Buffer, - console, - process: {}, - exports: {}, - }; - - public static async runCode(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { - return new Promise((resolve, reject) => { - process.nextTick(() => { - try { - resolve(this.runCodeSync(code, sandbox, options)); - } catch (e) { - reject(e); - } - }); - }); - } - - public static runCodeSync(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): any { - return vm.runInNewContext( - code, - { ...AppsEngineNodeRuntime.defaultContext, ...sandbox }, - { ...AppsEngineNodeRuntime.defaultRuntimeOptions, ...(options || {}) }, - ); - } - - constructor( - private readonly app: App, - private readonly customRequire: (mod: string) => any, - ) { - super(app, customRequire); - } - - public async runInSandbox(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { - return new Promise((resolve, reject) => { - process.nextTick(async () => { - try { - sandbox ??= {}; - - const result = await vm.runInNewContext( - code, - { - ...AppsEngineNodeRuntime.defaultContext, - ...sandbox, - require: this.customRequire, - }, - { - ...AppsEngineNodeRuntime.defaultRuntimeOptions, - filename: getFilenameForApp(options?.filename || this.app.getName()), - }, - ); - - resolve(result); - } catch (e) { - reject(e); - } - }); - }); - } -} diff --git a/packages/apps-engine/src/server/runtime/AppsEngineRuntime.ts b/packages/apps-engine/src/server/runtime/AppsEngineRuntime.ts deleted file mode 100644 index 0b1c0e8a58ef0..0000000000000 --- a/packages/apps-engine/src/server/runtime/AppsEngineRuntime.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { App } from '../../definition/App'; - -export const APPS_ENGINE_RUNTIME_DEFAULT_TIMEOUT = 1000; - -export const APPS_ENGINE_RUNTIME_FILE_PREFIX = '$RocketChat_App$'; - -export function getFilenameForApp(filename: string): string { - return `${APPS_ENGINE_RUNTIME_FILE_PREFIX}_${filename}`; -} - -export abstract class AppsEngineRuntime { - public static async runCode(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { - throw new Error(`Can't call this method on abstract class. Override it in a proper runtime class.`); - } - - public static runCodeSync(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): any { - throw new Error(`Can't call this method on abstract class. Override it in a proper runtime class.`); - } - - constructor(app: App, customRequire: (module: string) => any) {} - - public abstract runInSandbox(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise; -} - -export interface IAppsEngineRuntimeOptions { - timeout?: number; - filename?: string; - returnAllExports?: boolean; -} diff --git a/packages/apps-engine/src/server/runtime/EmptyRuntime.ts b/packages/apps-engine/src/server/runtime/EmptyRuntime.ts deleted file mode 100644 index e19f40e6d8165..0000000000000 --- a/packages/apps-engine/src/server/runtime/EmptyRuntime.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { EventEmitter } from 'events'; - -import type { IRuntimeController, RuntimeRequestOptions } from './IRuntimeController'; -import { AppStatus } from '../../definition/AppStatus'; - -export class EmptyRuntime extends EventEmitter implements IRuntimeController { - private readonly appId: string; - - constructor(appId: string) { - super(); - this.appId = appId; - } - - /** - * Returns a disabled status since this is an empty runtime - */ - public async getStatus(): Promise { - return Promise.resolve(AppStatus.COMPILER_ERROR_DISABLED); - } - - /** - * Stub implementation that throws an error since this runtime cannot handle requests - */ - public async sendRequest(message: { method: string; params: any[] }, options?: RuntimeRequestOptions): Promise { - throw new Error(`EmptyRuntime cannot handle requests. Method: ${message.method}`); - } - - /** - * Stub implementation for setting up the runtime - */ - public async setupApp(): Promise { - // Nothing to setup in an empty runtime - return Promise.resolve(); - } - - /** - * Stub implementation for stopping the runtime - */ - public async stopApp(): Promise { - // Nothing to stop in an empty runtime - return Promise.resolve(); - } - - /** - * Get the app ID associated with this runtime - */ - public getAppId(): string { - return this.appId; - } -} diff --git a/packages/apps-engine/src/server/runtime/IRuntimeController.ts b/packages/apps-engine/src/server/runtime/IRuntimeController.ts deleted file mode 100644 index 37d2270220795..0000000000000 --- a/packages/apps-engine/src/server/runtime/IRuntimeController.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { EventEmitter } from 'events'; - -import type { AppStatus } from '../../definition/AppStatus'; - -export type RuntimeRequestOptions = { - timeout: number; -}; - -export interface IRuntimeController extends EventEmitter { - /** - * Get the current status of the app runtime - */ - getStatus(): Promise; - - /** - * Send a request to the app runtime - */ - sendRequest(message: { method: string; params: any[] }, options?: RuntimeRequestOptions): Promise; - - /** - * Setup the app runtime - */ - setupApp(): Promise; - - /** - * Stop the app runtime - */ - stopApp(): Promise; - - /** - * Get the app ID associated with this runtime - */ - getAppId(): string; -} diff --git a/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts b/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts deleted file mode 100644 index 69ecd286ef3fb..0000000000000 --- a/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts +++ /dev/null @@ -1,739 +0,0 @@ -import * as child_process from 'child_process'; -import * as fs from 'fs'; -import * as path from 'path'; -import { type Readable, EventEmitter } from 'stream'; -import { inspect as utilInspect } from 'util'; - -import debugFactory from 'debug'; -import * as jsonrpc from 'jsonrpc-lite'; - -import { LivenessManager } from './LivenessManager'; -import { ProcessMessenger } from './ProcessMessenger'; -import { bundleLegacyApp } from './bundler'; -import { newDecoder } from './codec'; -import { AppStatus, AppStatusUtils } from '../../../definition/AppStatus'; -import { AppMethod } from '../../../definition/metadata'; -import type { AppManager } from '../../AppManager'; -import type { AppBridges } from '../../bridges'; -import type { IParseAppPackageResult } from '../../compiler'; -import { AppConsole, type ILoggerStorageEntry } from '../../logging'; -import type { AppAccessorManager, AppApiManager } from '../../managers'; -import type { AppLogStorage, IAppStorageItem } from '../../storage'; -import type { IRuntimeController } from '../IRuntimeController'; - -const baseDebug = debugFactory('appsEngine:runtime:deno'); - -const inspect = (value: unknown) => utilInspect(value, { depth: 10, compact: true, breakLength: Infinity }); - -export const ALLOWED_ACCESSOR_METHODS = [ - 'getConfigurationExtend', - 'getEnvironmentRead', - 'getEnvironmentWrite', - 'getConfigurationModify', - 'getReader', - 'getPersistence', - 'getHttp', - 'getModifier', -] as Array< - keyof Pick< - AppAccessorManager, - | 'getConfigurationExtend' - | 'getEnvironmentRead' - | 'getEnvironmentWrite' - | 'getConfigurationModify' - | 'getReader' - | 'getPersistence' - | 'getHttp' - | 'getModifier' - > ->; - -// Trying to access environment variables in Deno throws an error where in vm2 it simply returned `undefined` -// So here we define the allowed envvars to prevent the process (and the compatibility) from breaking -export const ALLOWED_ENVIRONMENT_VARIABLES = [ - 'NODE_EXTRA_CA_CERTS', // Accessed by the `https` node module -]; - -const COMMAND_PONG = '_zPONG'; - -export const JSONRPC_METHOD_NOT_FOUND = -32601; - -export function getRuntimeTimeout() { - const defaultTimeout = 30000; - const envValue = isFinite(process.env.APPS_ENGINE_RUNTIME_TIMEOUT as any) - ? Number(process.env.APPS_ENGINE_RUNTIME_TIMEOUT) - : defaultTimeout; - - if (envValue < 0) { - console.log('Environment variable APPS_ENGINE_RUNTIME_TIMEOUT has a negative value, ignoring...'); - return defaultTimeout; - } - - return envValue; -} - -export function isValidOrigin(accessor: string): accessor is (typeof ALLOWED_ACCESSOR_METHODS)[number] { - return ALLOWED_ACCESSOR_METHODS.includes(accessor as any); -} - -export function getDenoConfigPath(): string { - try { - // This path is relative to the compiled version of the Apps-Engine source - return require.resolve('../../../deno-runtime/deno.jsonc'); - } catch { - // This path is relative to the original Apps-Engine files - used during tests - return require.resolve('../../../../deno-runtime/deno.jsonc'); - } -} - -type AbortFunction = (reason?: any) => void; - -export class DenoRuntimeSubprocessController extends EventEmitter implements IRuntimeController { - private deno: child_process.ChildProcess | undefined; - - private state: 'uninitialized' | 'ready' | 'invalid' | 'restarting' | 'unknown' | 'stopped'; - - /** - * Incremental id that keeps track of how many times we've spawned a process for this app - */ - private spawnId = 0; - - private readonly debug: debug.Debugger; - - private readonly options = { - timeout: getRuntimeTimeout(), - }; - - private readonly accessors: AppAccessorManager; - - private readonly api: AppApiManager; - - private readonly logStorage: AppLogStorage; - - private readonly bridges: AppBridges; - - private readonly messenger: ProcessMessenger; - - private readonly livenessManager: LivenessManager; - - private readonly tempFilePath: string; - - private readonly denoRuntimePath: string; - - private readonly denoConfigPath: string; - - constructor( - manager: AppManager, - // We need to keep the appSource around in case the Deno process needs to be restarted - private readonly appPackage: IParseAppPackageResult, - private readonly storageItem: IAppStorageItem, - ) { - super(); - - this.tempFilePath = manager.getTempFilePath(); - this.denoRuntimePath = path.join(this.tempFilePath, 'deno-runtime', 'main.ts'); - this.denoConfigPath = getDenoConfigPath(); - - /** - * Deno 2.x refuses to run scripts inside the node_modules, so we create a symlink to the deno runtime files in the temp directory - * The temp directory is the same we are given by the host to store temporary upload files - */ - try { - fs.symlinkSync( - path.dirname(this.denoConfigPath), - path.dirname(this.denoRuntimePath), - 'dir' - ); - } catch (reason: unknown) { - if ((reason as NodeJS.ErrnoException).code !== 'EEXIST') { - throw reason; - } - } - - this.debug = baseDebug.extend(appPackage.info.id); - this.messenger = new ProcessMessenger(); - this.livenessManager = new LivenessManager({ - controller: this, - messenger: this.messenger, - debug: this.debug, - }); - - this.state = 'uninitialized'; - - this.accessors = manager.getAccessorManager(); - this.api = manager.getApiManager(); - this.logStorage = manager.getLogStorage(); - this.bridges = manager.getBridges(); - } - - public spawnProcess(): void { - try { - const denoExePath = 'deno'; - - const denoWrapperPath = this.denoRuntimePath; - // During development, the appsEngineDir is enough to run the deno process - const appsEngineDir = path.dirname(path.join(this.denoConfigPath, '..')); - const DENO_DIR = process.env.DENO_DIR ?? path.join(appsEngineDir, '.deno-cache'); - // When running in production, we're likely inside a node_modules which the Deno - // process must be able to read in order to include files that use NPM packages - const parentNodeModulesDir = path.dirname(path.join(appsEngineDir, '..')); - - const allowedDirs = [appsEngineDir, parentNodeModulesDir, this.tempFilePath]; - - const options = [ - 'run', - '--cached-only', - `--config=${this.denoConfigPath}`, - `--allow-read=${allowedDirs.join(',')}`, - `--allow-env=${ALLOWED_ENVIRONMENT_VARIABLES.join(',')}`, - denoWrapperPath, - '--subprocess', - this.appPackage.info.id, - '--spawnId', - String(this.spawnId++), - ]; - - // If the app doesn't request any permissions, it gets the default set of permissions, which includes "networking" - // If the app requests specific permissions, we need to check whether it requests "networking" or not - if (!this.appPackage.info.permissions || this.appPackage.info.permissions.findIndex((p) => p.name === 'networking') !== -1) { - options.splice(1, 0, '--allow-net'); - } - - const environment = { - env: { - // We need to pass the PATH, otherwise the shell won't find the deno executable - // But the runtime itself won't have access to the env var because of the parameters - PATH: process.env.PATH, - DENO_DIR, - }, - }; - - // SECURITY: We control the command, the arguments and the script that will be executed. - this.deno = child_process.spawn(denoExePath, options, environment); - this.messenger.setReceiver(this.deno); - this.livenessManager.attach(this.deno); - - this.debug('Started subprocess %d with options %s and env %s', this.deno.pid, inspect(options), inspect(environment)); - - this.setupListeners(); - } catch (e) { - this.state = 'invalid'; - console.error(`Failed to start Deno subprocess for app ${this.getAppId()}`, e); - } - } - - /** - * Attempts to kill the process currently controlled by this.deno - * - * @returns boolean - if a process has been killed or not - */ - public async killProcess(): Promise { - if (!this.deno) { - this.debug('No child process reference'); - return false; - } - - let { killed } = this.deno; - - // This field is not populated if the process is killed by the OS - if (killed) { - this.debug('App process was already killed'); - return killed; - } - - // What else should we do? - if (this.deno.kill('SIGKILL')) { - // Let's wait until we get confirmation the process exited - await new Promise((r) => this.deno.on('exit', r)); - killed = true; - } else { - this.debug('Tried killing the process but failed. Was it already dead?'); - killed = false; - } - - delete this.deno; - this.messenger.clearReceiver(); - return killed; - } - - // Debug purposes, could be deleted later - emit(eventName: string | symbol, ...args: any[]): boolean { - const hadListeners = super.emit(eventName, ...args); - - if (!hadListeners) { - this.debug('Emitted but no one listened: ', eventName, args); - } - - return hadListeners; - } - - public getProcessState() { - return this.state; - } - - public async getStatus(): Promise { - // If the process has been terminated, we can't get the status - if (!this.deno || this.deno.exitCode !== null) { - return AppStatus.UNKNOWN; - } - - return this.sendRequest({ method: 'app:getStatus', params: [] }) as Promise; - } - - public async setupApp() { - this.debug('Setting up app subprocess'); - this.spawnProcess(); - - // If there is more than one file in the package, then it is a legacy app that has not been bundled - if (Object.keys(this.appPackage.files).length > 1) { - await bundleLegacyApp(this.appPackage); - } - - await this.waitUntilReady(); - - await this.sendRequest({ method: 'app:construct', params: [this.appPackage] }); - - this.emit('constructed'); - } - - public async stopApp() { - this.debug('Stopping app subprocess'); - - this.state = 'stopped'; - - await this.killProcess(); - } - - public async restartApp() { - this.debug('Restarting app subprocess'); - const logger = new AppConsole('runtime:restart'); - - logger.info({ msg: 'Starting restart procedure for app subprocess...', runtimeData: this.livenessManager.getRuntimeData() }); - - this.state = 'restarting'; - - try { - const pid = this.deno?.pid; - - const hasKilled = await this.killProcess(); - - if (hasKilled) { - logger.debug({ msg: 'Process successfully terminated', pid }); - } else { - logger.warn({ msg: 'Could not terminate process. Maybe it was already dead?', pid }); - } - - await this.setupApp(); - logger.info({ msg: 'New subprocess successfully spawned', pid: this.deno.pid }); - - // setupApp() changes the state to 'ready' - we'll need to workaround that for now - this.state = 'restarting'; - - await this.sendRequest({ method: 'app:initialize' }); - await this.sendRequest({ method: 'app:setStatus', params: [this.storageItem.status] }); - - if (AppStatusUtils.isEnabled(this.storageItem.status)) { - await this.sendRequest({ method: 'app:onEnable' }); - } - - this.state = 'ready'; - - logger.info('Successfully restarted app subprocess'); - } catch (e) { - logger.error({ msg: "Failed to restart app's subprocess", err: e }); - throw e; - } finally { - await this.logStorage.storeEntries(AppConsole.toStorageEntry(this.getAppId(), logger)); - } - } - - public getAppId(): string { - return this.appPackage.info.id; - } - - public async sendRequest(message: Pick, options = this.options): Promise { - const id = String(Math.random().toString(36)).substring(2); - - const start = Date.now(); - - const request = jsonrpc.request(id, message.method, message.params); - - const { promise, abort } = this.waitForResponse(request, options); - - try { - this.debug('Sending message to subprocess %s', inspect(message)); - this.messenger.send(request); - } catch (e) { - abort(e); - } - - return promise.finally(() => { - this.debug('Request %s for method %s took %dms', id, message.method, Date.now() - start); - }); - } - - private waitUntilReady(): Promise { - if (this.state === 'ready') { - return; - } - - return new Promise((resolve, reject) => { - let timeoutId: NodeJS.Timeout; - - const handler = () => { - clearTimeout(timeoutId); - resolve(); - }; - - timeoutId = setTimeout(() => { - this.off('ready', handler); - reject(new Error(`[${this.getAppId()}] Timeout: app process not ready`)); - }, this.options.timeout); - - this.once('ready', handler); - }); - } - - private waitForResponse(req: jsonrpc.RequestObject, options = this.options): { abort: AbortFunction; promise: Promise } { - const controller = new AbortController(); - const { abort, signal } = controller; - - return { - abort: abort.bind(controller), - promise: new Promise((resolve, reject) => { - const eventName = `result:${req.id}`; - - const responseCallback = (result: unknown, error: jsonrpc.IParsedObjectError['payload']['error'] | Error) => { - this.off(eventName, responseCallback); - clearTimeout(timeoutId); - - if (error) { - reject(error); - } - - resolve(result); - }; - - const timeoutId = setTimeout( - () => - responseCallback( - undefined, - new Error(`[${this.getAppId()}] Request "${req.id}" for method "${req.method}" timed out after ${options.timeout}ms`), - ), - options.timeout, - ); - - signal.onabort = () => - responseCallback(undefined, signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason))); - - this.once(eventName, responseCallback); - }), - }; - } - - private onReady(): void { - this.state = 'ready'; - } - - /** - * Listeners need to be setup every time the reference - * in `this.deno` changes, i.e. every time the subprocess - * is restarted - */ - private setupListeners(): void { - if (!this.deno) { - return; - } - - this.deno.stderr.on('data', this.parseError.bind(this)); - this.deno.on('error', (err) => { - this.state = 'invalid'; - console.error(`Failed to startup Deno subprocess for app ${this.getAppId()}`, err); - }); - - this.deno.once('exit', (code) => this.emit('processExit', code)); - - this.once('ready', this.onReady.bind(this)); - - this.parseStdout(this.deno.stdout); - } - - // Probable should extract this to a separate file - private async handleAccessorMessage({ payload: { method, id, params } }: jsonrpc.IParsedObjectRequest): Promise { - const accessorMethods = method.substring(9).split(':'); // First 9 characters are always 'accessor:' - - this.debug('Handling accessor message %s with params %s', inspect(accessorMethods), inspect(params)); - - const managerOrigin = accessorMethods.shift(); - const tailMethodName = accessorMethods.pop(); - - // If we're restarting the app, we can't register resources again, so we - // hijack requests for the `ConfigurationExtend` accessor and don't let them through - // This needs to be refactored ASAP - if (this.state === 'restarting' && managerOrigin === 'getConfigurationExtend') { - return jsonrpc.success(id, null); - } - - if (managerOrigin === 'api' && tailMethodName === 'listApis') { - const result = this.api.listApis(this.appPackage.info.id); - - return jsonrpc.success(id, result); - } - - /** - * At this point, the accessorMethods array will contain the path to the accessor from the origin (AppAccessorManager) - * The accessor is the one that contains the actual method the app wants to call - * - * Most of the times, it will take one step from origin to accessor - * For example, for the call AppAccessorManager.getEnvironmentRead().getServerSettings().getValueById() we'll have - * the following: - * - * ``` - * const managerOrigin = 'getEnvironmentRead' - * const tailMethod = 'getValueById' - * const accessorMethods = ['getServerSettings'] - * ``` - * - * But sometimes there can be more steps, like in the following example: - * AppAccessorManager.getReader().getEnvironmentReader().getEnvironmentVariables().getValueByName() - * In this case, we'll have: - * - * ``` - * const managerOrigin = 'getReader' - * const tailMethod = 'getValueByName' - * const accessorMethods = ['getEnvironmentReader', 'getEnvironmentVariables'] - * ``` - **/ - // Prevent app from trying to get properties from the manager that - // are not intended for public access - if (!isValidOrigin(managerOrigin)) { - throw new Error(`Invalid accessor namespace "${managerOrigin}"`); - } - - // Need to fix typing of return value - const getAccessorForOrigin = ( - accessorMethods: string[], - managerOrigin: (typeof ALLOWED_ACCESSOR_METHODS)[number], - accessorManager: AppAccessorManager, - ) => { - const origin = accessorManager[managerOrigin](this.appPackage.info.id); - - if (managerOrigin === 'getHttp' || managerOrigin === 'getPersistence') { - return origin; - } - - if (managerOrigin === 'getConfigurationExtend' || managerOrigin === 'getConfigurationModify') { - return origin[accessorMethods[0] as keyof typeof origin]; - } - - let accessor = origin; - - // Call all intermediary objects to "resolve" the accessor - accessorMethods.forEach((methodName) => { - const method = accessor[methodName as keyof typeof accessor] as unknown; - - if (typeof method !== 'function') { - throw new Error(`Invalid accessor method "${methodName}"`); - } - - accessor = method.apply(accessor); - }); - - return accessor; - }; - - const accessor = getAccessorForOrigin(accessorMethods, managerOrigin, this.accessors); - - const tailMethod = accessor[tailMethodName as keyof typeof accessor] as unknown; - - if (typeof tailMethod !== 'function') { - throw new Error(`Invalid accessor method "${tailMethodName}"`); - } - - const result = await tailMethod.apply(accessor, params); - - return jsonrpc.success(id, typeof result === 'undefined' ? null : result); - } - - private async handleBridgeMessage({ - payload: { method, id, params }, - }: jsonrpc.IParsedObjectRequest): Promise { - const [bridgeName, bridgeMethod] = method.substring(8).split(':'); - - this.debug('Handling bridge message %s().%s() with params %s', bridgeName, bridgeMethod, inspect(params)); - - const bridge = this.bridges[bridgeName as keyof typeof this.bridges]; - - if (!bridgeMethod.startsWith('do') || typeof bridge !== 'function' || !Array.isArray(params)) { - throw new Error('Invalid bridge request'); - } - - const bridgeInstance = bridge.call(this.bridges); - - const methodRef = bridgeInstance[bridgeMethod as keyof typeof bridge] as unknown; - - if (typeof methodRef !== 'function') { - throw new Error('Invalid bridge request'); - } - - let result; - try { - result = await methodRef.apply( - bridgeInstance, - // Should the protocol expect the placeholder APP_ID value or should the Deno process send the actual appId? - // If we do not expect the APP_ID, the Deno process will be able to impersonate other apps, potentially - params.map((value: unknown) => (value === 'APP_ID' ? this.appPackage.info.id : value)), - ); - } catch (error) { - this.debug('Error executing bridge method %s().%s() %s', bridgeName, bridgeMethod, inspect(error.message)); - const jsonRpcError = new jsonrpc.JsonRpcError(error.message, -32000, error); - return jsonrpc.error(id, jsonRpcError); - } - - return jsonrpc.success(id, typeof result === 'undefined' ? null : result); - } - - private async handleIncomingMessage(message: jsonrpc.IParsedObjectNotification | jsonrpc.IParsedObjectRequest): Promise { - const { method } = message.payload; - - if (method.startsWith('accessor:')) { - let result: jsonrpc.SuccessObject | jsonrpc.ErrorObject; - - try { - result = await this.handleAccessorMessage(message as jsonrpc.IParsedObjectRequest); - } catch (e) { - result = jsonrpc.error((message.payload as jsonrpc.RequestObject).id, new jsonrpc.JsonRpcError(e.message, 1000)); - } - - this.messenger.send(result); - - return; - } - - if (method.startsWith('bridges:')) { - let result: jsonrpc.SuccessObject | jsonrpc.ErrorObject; - - try { - result = await this.handleBridgeMessage(message as jsonrpc.IParsedObjectRequest); - } catch (e) { - result = jsonrpc.error((message.payload as jsonrpc.RequestObject).id, new jsonrpc.JsonRpcError(e.message, 1000)); - } - - this.messenger.send(result); - - return; - } - - switch (method) { - case 'ready': - this.emit('ready'); - break; - case 'log': - console.log('SUBPROCESS LOG', message); - break; - case 'unhandledRejection': - case 'uncaughtException': - await this.logUnhandledError(`runtime:${method}`, message); - break; - default: - console.warn('Unrecognized method from sub process'); - break; - } - } - - private async logUnhandledError( - method: `${AppMethod.RUNTIME_UNCAUGHT_EXCEPTION | AppMethod.RUNTIME_UNHANDLED_REJECTION}`, - message: jsonrpc.IParsedObjectRequest | jsonrpc.IParsedObjectNotification, - ) { - this.debug('Unhandled error of type "%s" caught in subprocess', method); - - const logger = new AppConsole(method); - logger.error(message.payload); - - await this.logStorage.storeEntries(AppConsole.toStorageEntry(this.getAppId(), logger)); - } - - private async handleResultMessage(message: jsonrpc.IParsedObjectError | jsonrpc.IParsedObjectSuccess): Promise { - const { id } = message.payload; - - let result: unknown; - let error: jsonrpc.IParsedObjectError['payload']['error'] | undefined; - let logs: ILoggerStorageEntry; - - if (message.type === 'success') { - const params = message.payload.result as { value: unknown; logs?: ILoggerStorageEntry }; - result = params.value; - logs = params.logs; - } else { - error = message.payload.error; - logs = message.payload.error.data?.logs as ILoggerStorageEntry; - } - - // Should we try to make sure all result messages have logs? - if (logs) { - await this.logStorage.storeEntries(logs); - } - - this.emit(`result:${id}`, result, error); - } - - private async parseStdout(stream: Readable): Promise { - try { - for await (const message of newDecoder().decodeStream(stream)) { - this.debug('Received message from subprocess %s', inspect(message)); - try { - // Process PONG resonse first as it is not JSON RPC - if (message === COMMAND_PONG) { - this.emit('pong'); - continue; - } - - const JSONRPCMessage = jsonrpc.parseObject(message); - - if (Array.isArray(JSONRPCMessage)) { - throw new Error('Invalid message format'); - } - - this.emit('heartbeat'); - - if (JSONRPCMessage.type === 'request' || JSONRPCMessage.type === 'notification') { - this.handleIncomingMessage(JSONRPCMessage).catch((reason) => - console.error(`[${this.getAppId()}] Error executing handler`, reason, message), - ); - continue; - } - - if (JSONRPCMessage.type === 'success' || JSONRPCMessage.type === 'error') { - this.handleResultMessage(JSONRPCMessage).catch((reason) => - console.error(`[${this.getAppId()}] Error executing handler`, reason, message), - ); - continue; - } - - console.error('Unrecognized message type', JSONRPCMessage); - } catch (e) { - // SyntaxError is thrown when the message is not a valid JSON - if (e instanceof SyntaxError) { - console.error(`[${this.getAppId()}] Failed to parse message`); - continue; - } - - console.error(`[${this.getAppId()}] Error executing handler`, e, message); - } - } - } catch (e) { - console.error(`[${this.getAppId()}]`, e); - this.emit('error', new Error('DECODE_ERROR')); - } - } - - private async parseError(chunk: Buffer): Promise { - try { - const data = JSON.parse(chunk.toString()); - - this.debug('Metrics received from subprocess (via stderr): %s', inspect(data)); - } catch (e) { - console.error('Subprocess stderr', chunk.toString()); - } - } -} diff --git a/packages/apps-engine/src/server/runtime/deno/LivenessManager.ts b/packages/apps-engine/src/server/runtime/deno/LivenessManager.ts deleted file mode 100644 index f7ba66ed37c04..0000000000000 --- a/packages/apps-engine/src/server/runtime/deno/LivenessManager.ts +++ /dev/null @@ -1,254 +0,0 @@ -import type { ChildProcess } from 'child_process'; -import { EventEmitter } from 'stream'; - -import type { DenoRuntimeSubprocessController } from './AppsEngineDenoRuntime'; -import type { ProcessMessenger } from './ProcessMessenger'; - -export const COMMAND_PING = '_zPING'; - -const defaultOptions: LivenessManager['options'] = { - pingTimeoutInMS: 1000, - pingIntervalInMS: 10000, - consecutiveTimeoutLimit: 4, - maxRestarts: Infinity, - restartAttemptDelayInMS: 1000, -}; - -/** - * Responsible for pinging the Deno subprocess and for restarting it - * if something doesn't look right - */ -export class LivenessManager { - private readonly controller: DenoRuntimeSubprocessController; - - private readonly messenger: ProcessMessenger; - - private readonly debug: debug.Debugger; - - private readonly options: { - // How long should we wait for a response to the ping request - pingTimeoutInMS: number; - - // How long is the delay between ping messages - pingIntervalInMS: number; - - // Limit of times the process can timeout the ping response before we consider it as unresponsive - consecutiveTimeoutLimit: number; - - // Limit of times we can try to restart a process - maxRestarts: number; - - // Time to delay the next restart attempt after a failed one - restartAttemptDelayInMS: number; - }; - - private subprocess: ChildProcess; - - private watchdogTimeout: NodeJS.Timeout | null = null; - - private lastHeartbeatTimestamp = NaN; - - // A promise tracking the current ping process - used mostly for testing - private pendingPing: Promise | null; - - // This is the perfect use-case for an AbortController, but it's experimental in Node 14.x - private pingAbortController: EventEmitter; - - private pingTimeoutConsecutiveCount = 0; - - private restartCount = 0; - - private restartLog: Record[] = []; - - constructor( - deps: { - controller: DenoRuntimeSubprocessController; - messenger: ProcessMessenger; - debug: debug.Debugger; - }, - options: Partial = {}, - ) { - this.controller = deps.controller; - this.messenger = deps.messenger; - this.debug = deps.debug; - this.pingAbortController = new EventEmitter(); - - this.options = Object.assign({}, defaultOptions, options); - - this.controller.on('heartbeat', () => { - this.lastHeartbeatTimestamp = Date.now(); - this.pingTimeoutConsecutiveCount = 0; - }); - - this.controller.on('error', async (reason) => { - if (reason instanceof Error && reason.message.startsWith('DECODE_ERROR')) { - await this.restartProcess('Decode error', 'controller'); - } - }); - } - - public getRuntimeData() { - const { lastHeartbeatTimestamp, restartCount, pingTimeoutConsecutiveCount, restartLog } = this; - - return { - lastHeartbeatTimestamp, - restartCount, - pingTimeoutConsecutiveCount, - restartLog, - }; - } - - public attach(deno: ChildProcess) { - this.subprocess = deno; - - this.pingTimeoutConsecutiveCount = 0; - - this.subprocess.once('exit', this.handleExit.bind(this)); - this.subprocess.once('error', this.handleError.bind(this)); - - this.controller.once('constructed', this.start.bind(this)); - } - - public start() { - this.lastHeartbeatTimestamp = Date.now(); - - this.watchdogTimeout = setInterval(() => { - if (Date.now() - this.lastHeartbeatTimestamp < this.options.pingIntervalInMS) { - return; - } - - try { - this.ping(); - } catch { - // If the ping call fails synchronously, it's because we couldn't send the ping message - // then likely the process isn't running, so we stop everything - this.debug('[LivenessManager] Failed to send ping to subprocess, stopping watchdog...'); - this.stop(); - } - }, this.options.pingIntervalInMS); - - this.watchdogTimeout.unref(); - } - - public stop() { - this.pingAbortController.emit('abort'); - clearInterval(this.watchdogTimeout); - this.watchdogTimeout = null; - this.pendingPing = null; - } - - public getPendingPing() { - return this.pendingPing; - } - - /** - * Start up the process of ping/pong for liveness check - * - * The message exchange does not use JSON RPC as it adds a lot of overhead - * with the creation and encoding of a full object for transfer. By using a - * string the process is less intensive. - */ - private ping() { - const start = Date.now(); - - this.pendingPing = new Promise((resolve, reject) => { - const onceCallback = () => { - const now = Date.now(); - this.debug('Ping successful in %d ms', now - start); - clearTimeout(timeoutId); - this.pingTimeoutConsecutiveCount = 0; - this.lastHeartbeatTimestamp = now; - resolve(true); - }; - - const timeoutCallback = () => { - this.debug('Ping failed in %d ms (consecutive failure #%d)', Date.now() - start, this.pingTimeoutConsecutiveCount); - this.controller.off('pong', onceCallback); - this.pingTimeoutConsecutiveCount++; - reject('timeout'); - }; - - this.pingAbortController.once('abort', () => { - this.debug('Ping aborted'); - reject('abort'); - }); - - const timeoutId = setTimeout(timeoutCallback, this.options.pingTimeoutInMS); - - this.controller.once('pong', onceCallback); - }) - .catch((reason) => { - if (reason === 'abort') { - return false; - } - - if (reason === 'timeout' && this.pingTimeoutConsecutiveCount >= this.options.consecutiveTimeoutLimit) { - this.debug( - 'Subprocess failed to respond to pings %d consecutive times. Attempting restart...', - this.options.consecutiveTimeoutLimit, - ); - this.restartProcess('Too many pings timed out'); - return false; - } - - return true; - }) - .finally(() => { - this.pingAbortController.removeAllListeners('abort'); - }); - - this.messenger.send(COMMAND_PING); - } - - private handleError(err: Error) { - this.debug('App has failed to start.`', err); - this.restartProcess(err.message); - } - - private handleExit(exitCode: number, signal: string) { - const processState = this.controller.getProcessState(); - // If the we're restarting the process, or want to stop the process, or it exited cleanly, nothing else for us to do - if (processState === 'restarting' || processState === 'stopped' || (exitCode === 0 && !signal)) { - return; - } - - let reason: string; - - // Otherwise we attempt to restart the process - if (signal) { - this.debug('App has been killed (%s). Attempting restart #%d...', signal, this.restartCount + 1); - reason = `App has been killed with signal ${signal}`; - } else { - this.debug('App has exited with code %d. Attempting restart #%d...', exitCode, this.restartCount + 1); - reason = `App has exited with code ${exitCode}`; - } - - this.restartProcess(reason); - } - - private async restartProcess(reason: string, source = 'liveness-manager') { - this.stop(); - - if (this.restartCount >= this.options.maxRestarts) { - this.debug('Limit of restarts reached (%d). Aborting restart...', this.options.maxRestarts); - this.controller.stopApp(); - return; - } - - this.restartLog.push({ - reason, - source, - restartedAt: new Date(), - pid: this.subprocess.pid, - }); - - try { - await this.controller.restartApp(); - } catch (e) { - this.debug('Restart attempt failed. Retrying in %dms', this.options.restartAttemptDelayInMS); - setTimeout(() => this.restartProcess('Failed restart attempt'), this.options.restartAttemptDelayInMS); - } - - this.restartCount++; - } -} diff --git a/packages/apps-engine/src/server/runtime/deno/ProcessMessenger.ts b/packages/apps-engine/src/server/runtime/deno/ProcessMessenger.ts deleted file mode 100644 index c5c2394e56dfa..0000000000000 --- a/packages/apps-engine/src/server/runtime/deno/ProcessMessenger.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { ChildProcess } from 'child_process'; - -import type { JsonRpc } from 'jsonrpc-lite'; - -import type { COMMAND_PING } from './LivenessManager'; -import type { Encoder } from './codec'; -import { newEncoder } from './codec'; - -type Message = JsonRpc | typeof COMMAND_PING; - -export class ProcessMessenger { - private deno: ChildProcess | undefined; - - private encoder: Encoder | undefined; - - private _sendStrategy: (message: Message) => void; - - constructor() { - this._sendStrategy = this.strategyError; - } - - public send(message: Message) { - this._sendStrategy(message); - } - - public setReceiver(deno: ChildProcess) { - this.deno = deno; - - this.switchStrategy(); - } - - public clearReceiver() { - delete this.deno; - delete this.encoder; - - this.switchStrategy(); - } - - private switchStrategy() { - if (this.deno?.stdin?.writable) { - this._sendStrategy = this.strategySend.bind(this); - - // Get a clean encoder - this.encoder = newEncoder(); - } else { - this._sendStrategy = this.strategyError.bind(this); - } - } - - private strategyError(_message: Message) { - throw new Error('No process configured to receive a message'); - } - - private strategySend(message: Message) { - this.deno.stdin.write(this.encoder.encode(message)); - } -} diff --git a/packages/apps-engine/src/server/runtime/deno/bundler.ts b/packages/apps-engine/src/server/runtime/deno/bundler.ts deleted file mode 100644 index e6442a336b83c..0000000000000 --- a/packages/apps-engine/src/server/runtime/deno/bundler.ts +++ /dev/null @@ -1,90 +0,0 @@ -import * as path from 'path'; - -import { build, type PluginBuild, type OnLoadArgs, type OnResolveArgs } from 'esbuild'; - -import type { IParseAppPackageResult } from '../../compiler'; - -/** - * Some legacy apps that might be installed in workspaces have not been bundled after compilation, - * leading to multiple files being sent to the subprocess and requiring further logic to require one another. - * This makes running the app in the Deno Runtime much more difficult, so instead we bundle the files at runtime. - */ -export async function bundleLegacyApp(appPackage: IParseAppPackageResult) { - const buildResult = await build({ - write: false, - bundle: true, - minify: true, - platform: 'node', - target: ['node10'], - define: { - 'global.Promise': 'Promise', - }, - external: ['@rocket.chat/apps-engine/*'], - stdin: { - contents: appPackage.files[appPackage.info.classFile], - sourcefile: appPackage.info.classFile, - loader: 'js', - }, - plugins: [ - { - name: 'legacy-app', - setup(build: PluginBuild) { - build.onResolve({ filter: /.*/ }, (args: OnResolveArgs) => { - if (args.namespace === 'file') { - return; - } - - const modulePath = path.join(path.dirname(args.importer), args.path).concat('.js'); - - const hasFile = !!appPackage.files[modulePath]; - - if (hasFile) { - return { - namespace: 'app-source', - path: modulePath, - }; - } - - // require('../') or require('./') are both valid, but aren't included in the files record in the same way - // we need to treat those differently - if (/\.\.?\//.test(args.path)) { - const indexModulePath = modulePath.replace(/\.js$/, `${path.sep}index.js`); - - if (appPackage.files[indexModulePath]) { - return { - namespace: 'app-source', - path: indexModulePath, - }; - } - } - - return { - path: args.path, - external: true, - }; - }); - - build.onLoad({ filter: /.*/, namespace: 'app-source' }, (args: OnLoadArgs) => { - if (!appPackage.files[args.path]) { - return { - errors: [ - { - text: `File ${args.path} could not be found`, - }, - ], - }; - } - - return { - contents: appPackage.files[args.path], - }; - }); - }, - }, - ], - }); - - const [{ text: bundle }] = buildResult.outputFiles; - - appPackage.files = { [appPackage.info.classFile]: bundle }; -} diff --git a/packages/apps-engine/src/server/runtime/deno/codec.ts b/packages/apps-engine/src/server/runtime/deno/codec.ts deleted file mode 100644 index 53b05846565ee..0000000000000 --- a/packages/apps-engine/src/server/runtime/deno/codec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Decoder as _Decoder, Encoder as _Encoder, ExtensionCodec } from '@msgpack/msgpack'; - -const extensionCodec = new ExtensionCodec(); - -extensionCodec.register({ - type: 0, - encode: (object: unknown) => { - // We don't care about functions, but also don't want to throw an error - if (typeof object === 'function') { - return new Uint8Array([0]); - } - }, - - decode: (_data: Uint8Array) => undefined, -}); - -// We need to handle Buffers because Deno needs its own decoding -extensionCodec.register({ - type: 1, - encode: (object: unknown) => { - if (object instanceof Buffer) { - return new Uint8Array(object.buffer, object.byteOffset, object.byteLength); - } - }, - - // msgpack will reuse the Uint8Array instance, so WE NEED to copy it instead of simply creating a view - decode: (data: Uint8Array) => Buffer.from(data), -}); - -/** - * The Encoder and Decoder classes perform "stateful" operations, i.e. they read from a - * stream, store the data locally and decode it from its buffer. - * - * In practice, this affects the decoder when there is decode error. After an error, the decoder - * keeps the malformed data in its buffer, and even if we try to decode from another source (e.g. different stream) - * it will fail again as there's still data in the buffer. - * - * For that reason, we can't have a singleton instance of Encoder and Decoder, but rather one - * instance for each time we create a new subprocess - */ -export const newEncoder = () => new _Encoder({ extensionCodec }); -export const newDecoder = () => new _Decoder({ extensionCodec }); - -export type Encoder = _Encoder; -export type Decoder = _Decoder; diff --git a/packages/apps-engine/src/server/storage/AppLogStorage.ts b/packages/apps-engine/src/server/storage/AppLogStorage.ts deleted file mode 100644 index 8a3487a5587be..0000000000000 --- a/packages/apps-engine/src/server/storage/AppLogStorage.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { ILoggerStorageEntry } from '../logging'; - -export interface IAppLogStorageFindOptions { - sort?: Record; - skip?: number; - limit?: number; - projection?: Record; -} - -export abstract class AppLogStorage { - constructor(private readonly engine: string) {} - - public getEngine() { - return this.engine; - } - - public abstract findPaginated( - query: { [field: string]: any }, - options?: IAppLogStorageFindOptions, - ): Promise<{ logs: ILoggerStorageEntry[]; total: number }>; - - public abstract storeEntries(logEntry: ILoggerStorageEntry): Promise; - - public abstract getEntriesFor(appId: string): Promise>; - - public abstract removeEntriesFor(appId: string): Promise; -} diff --git a/packages/apps-engine/src/server/storage/AppMetadataStorage.ts b/packages/apps-engine/src/server/storage/AppMetadataStorage.ts deleted file mode 100644 index 6eab3603da6b4..0000000000000 --- a/packages/apps-engine/src/server/storage/AppMetadataStorage.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { IAppStorageItem } from './IAppStorageItem'; -import type { AppStatus } from '../../definition/AppStatus'; -import type { IAppInfo } from '../../definition/metadata/IAppInfo'; -import type { ISetting } from '../../definition/settings'; -import type { IMarketplaceInfo } from '../marketplace'; - -export abstract class AppMetadataStorage { - constructor(private readonly engine: string) {} - - public getEngine() { - return this.engine; - } - - public abstract create(item: IAppStorageItem): Promise; - - public abstract retrieveOne(id: string): Promise; - - public abstract retrieveAll(): Promise>; - - public abstract retrieveAllPrivate(): Promise>; - - public abstract remove(id: string): Promise<{ success: boolean }>; - - public abstract updatePartialAndReturnDocument( - item: Partial, - options?: { unsetPermissionsGranted?: boolean }, - ): Promise; - - public abstract updateStatus(_id: string, status: AppStatus): Promise; - - public abstract updateSetting(_id: string, setting: ISetting): Promise; - - public abstract updateAppInfo(_id: string, info: IAppInfo): Promise; - - public abstract updateMarketplaceInfo(_id: string, marketplaceInfo: IMarketplaceInfo[]): Promise; -} diff --git a/packages/apps-engine/src/server/storage/AppSourceStorage.ts b/packages/apps-engine/src/server/storage/AppSourceStorage.ts deleted file mode 100644 index c0c0f8ea61971..0000000000000 --- a/packages/apps-engine/src/server/storage/AppSourceStorage.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { IAppStorageItem } from './IAppStorageItem'; - -export abstract class AppSourceStorage { - /** - * Stores an app package (zip file) in the underlying - * storage provided by the host - * - * @param item descriptor of the App - * @param zip the app package file contents - * - * @returns the path in which the pacakge has been stored - */ - public abstract store(item: IAppStorageItem, zip: Buffer): Promise; - - /** - * Fetches an app's package file contents - * - * @param item descriptor of the App - * - * @returns buffer containing the file contents of the app's package - */ - public abstract fetch(item: IAppStorageItem): Promise; - - /** - * Updates an app package (zip file) in the underlying - * storage provided by the host - * - * @param item descriptor of the App - * @param zip the app package file contents - * - * @returns the path in which the pacakge has been stored - */ - public abstract update(item: IAppStorageItem, zip: Buffer): Promise; - - /** - * - * @param item descriptor of the App - */ - public abstract remove(item: IAppStorageItem): Promise; -} diff --git a/packages/apps-engine/src/server/storage/IAppStorageItem.ts b/packages/apps-engine/src/server/storage/IAppStorageItem.ts deleted file mode 100644 index 2ac4469b6f51c..0000000000000 --- a/packages/apps-engine/src/server/storage/IAppStorageItem.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { AppStatus } from '../../definition/AppStatus'; -import type { IAppInfo } from '../../definition/metadata'; -import type { IPermission } from '../../definition/permissions/IPermission'; -import type { ISetting } from '../../definition/settings'; -import type { IMarketplaceInfo } from '../marketplace'; - -export interface IAppStorageItem { - _id?: string; - id: string; - createdAt?: Date; - updatedAt?: Date; - status: AppStatus; - info: IAppInfo; - installationSource: AppInstallationSource; - /** - * The path that represents where the source of the app storaged. - */ - sourcePath?: string; - languageContent: { [key: string]: object }; - settings: { [id: string]: ISetting }; - implemented: { [int: string]: boolean }; - marketplaceInfo?: IMarketplaceInfo[]; - permissionsGranted?: Array; - signature?: string; - migrated?: boolean; -} - -export enum AppInstallationSource { - MARKETPLACE = 'marketplace', - PRIVATE = 'private', -} diff --git a/packages/apps-engine/src/server/storage/index.ts b/packages/apps-engine/src/server/storage/index.ts deleted file mode 100644 index 5f0993129bf3a..0000000000000 --- a/packages/apps-engine/src/server/storage/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { AppLogStorage, IAppLogStorageFindOptions } from './AppLogStorage'; -export { AppMetadataStorage } from './AppMetadataStorage'; -export { IAppStorageItem, AppInstallationSource } from './IAppStorageItem'; -export { AppSourceStorage } from './AppSourceStorage'; diff --git a/packages/apps-engine/tsconfig.json b/packages/apps-engine/tsconfig.json index 2d70ec6e00a34..7e564876a10d8 100644 --- a/packages/apps-engine/tsconfig.json +++ b/packages/apps-engine/tsconfig.json @@ -7,8 +7,6 @@ "noImplicitAny": true, "noUnusedLocals": true, "removeComments": false, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, "moduleResolution": "node", "resolveJsonModule": true, "types": ["node"], @@ -17,6 +15,6 @@ "outDir": ".", "skipLibCheck": true }, - "exclude": ["deno-runtime"], + "exclude": ["node_modules"], "include": ["./src/**/*"] } diff --git a/packages/apps-engine/turbo.json b/packages/apps-engine/turbo.json index a07ebf0886a2b..81cbf5f669fd8 100644 --- a/packages/apps-engine/turbo.json +++ b/packages/apps-engine/turbo.json @@ -3,7 +3,7 @@ "tasks": { "build": { "dependsOn": ["^build"], - "outputs": ["client/**", "definition/**", "deno-runtime/**", "lib/**", "scripts/**", "server/**", ".deno-cache/**"] + "outputs": ["definition/**", "lib/**"] } } } diff --git a/packages/apps/src/server/compiler/AppPackageParser.ts b/packages/apps/src/server/compiler/AppPackageParser.ts index 525ad48407390..fdd01980d5e63 100644 --- a/packages/apps/src/server/compiler/AppPackageParser.ts +++ b/packages/apps/src/server/compiler/AppPackageParser.ts @@ -1,4 +1,3 @@ -import * as fs from 'fs'; import * as path from 'path'; import * as AdmZip from 'adm-zip'; @@ -7,6 +6,7 @@ import { v4 as uuidv4 } from 'uuid'; import { AppImplements } from '.'; import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata/IAppInfo'; +import { ENGINE_VERSION } from '@rocket.chat/apps-engine/definition/version'; import { RequiredApiVersionError } from '../errors'; import type { IParseAppPackageResult } from './IParseAppPackageResult'; @@ -15,11 +15,7 @@ export class AppPackageParser { private allowedIconExts: Array = ['.png', '.jpg', '.jpeg', '.gif']; - private appsEngineVersion: string; - - constructor() { - this.appsEngineVersion = this.getEngineVersion(); - } + private appsEngineVersion: string = ENGINE_VERSION; public async unpackageApp(appPackage: Buffer): Promise { const zip = new AdmZip(appPackage); @@ -143,21 +139,4 @@ export class AppPackageParser { return `data:image/${ext.replace('.', '')};base64,${base64}`; } - - private getEngineVersion(): string { - const devLocation = path.join(__dirname, '../../../package.json'); - const prodLocation = path.join(__dirname, '../../package.json'); - - let info: { version: string }; - - if (fs.existsSync(devLocation)) { - info = JSON.parse(fs.readFileSync(devLocation, 'utf8')); - } else if (fs.existsSync(prodLocation)) { - info = JSON.parse(fs.readFileSync(prodLocation, 'utf8')); - } else { - throw new Error('Could not find the Apps TypeScript Definition Package Version!'); - } - - return info.version.replace(/^[^0-9]/, '').split('-')[0]; - } } From c9af208a6004816638c5ef7dfdfb7a7044875065 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Apr 2026 14:11:04 -0300 Subject: [PATCH 18/19] refactor(apps): source room options and OAuth2Client from apps-engine definition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GetMessagesOptions, GetRoomsFilters, GetRoomsOptions, and GetMessagesSortableFields are now canonical in @rocket.chat/apps-engine/definition/rooms/IGetMessagesOptions — import them from there and re-export for downstream consumers. OAuth2Client is now canonical in @rocket.chat/apps-engine/definition/oauth2/OAuth2Client — replace the duplicate copy with a re-export. Also add typesVersions to @rocket.chat/apps/package.json so that consumers using moduleResolution: "node" can resolve ./server/* and ./client/* subpath imports without needing node16/bundler resolution. Co-Authored-By: Claude Sonnet 4.6 --- packages/apps/package.json | 22 +- .../apps/src/server/bridges/RoomBridge.ts | 46 +-- .../apps/src/server/oauth2/OAuth2Client.ts | 338 +----------------- 3 files changed, 26 insertions(+), 380 deletions(-) diff --git a/packages/apps/package.json b/packages/apps/package.json index 6950e17f2b97b..ed96487088815 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -4,6 +4,26 @@ "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", + "exports": { + ".": { + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./server/*": { + "require": "./dist/server/*.js", + "types": "./dist/server/*.d.ts" + }, + "./client/*": { + "require": "./dist/client/*.js", + "types": "./dist/client/*.d.ts" + } + }, + "typesVersions": { + "*": { + "server/*": ["./dist/server/*"], + "client/*": ["./dist/client/*"] + } + }, "files": [ "/dist", "/deno-runtime", @@ -21,10 +41,10 @@ "test:node": "NODE_ENV=test node --require ts-node/register/transpile-only --test-reporter spec --test \"tests/**/*.test.ts\"" }, "dependencies": { + "@msgpack/msgpack": "3.0.0-beta2", "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/model-typings": "workspace:^", - "@msgpack/msgpack": "3.0.0-beta2", "adm-zip": "^0.5.16", "debug": "^4.3.7", "esbuild": "~0.27.3", diff --git a/packages/apps/src/server/bridges/RoomBridge.ts b/packages/apps/src/server/bridges/RoomBridge.ts index 5cef99d5ed08a..00bd2b942a58a 100644 --- a/packages/apps/src/server/bridges/RoomBridge.ts +++ b/packages/apps/src/server/bridges/RoomBridge.ts @@ -1,52 +1,14 @@ import { BaseBridge } from './BaseBridge'; import type { IMessage, IMessageRaw } from '@rocket.chat/apps-engine/definition/messages'; -import type { IRoom, IRoomRaw, RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IRoom, IRoomRaw } from '@rocket.chat/apps-engine/definition/rooms'; +import { GetMessagesSortableFields } from '@rocket.chat/apps-engine/definition/rooms/IGetMessagesOptions'; +import type { GetMessagesOptions, GetRoomsFilters, GetRoomsOptions } from '@rocket.chat/apps-engine/definition/rooms/IGetMessagesOptions'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; import { PermissionDeniedError } from '../errors/PermissionDeniedError'; import { AppPermissionManager } from '../managers/AppPermissionManager'; import { AppPermissions } from '../permissions/AppPermissions'; -export const GetMessagesSortableFields = ['createdAt'] as const; - -export type GetMessagesOptions = { - limit: number; - skip: number; - sort: Record<(typeof GetMessagesSortableFields)[number], 'asc' | 'desc'>; - showThreadMessages: boolean; -}; - -/** - * Filters for querying rooms in the system. - */ -export type GetRoomsFilters = { - /** - * When specified, only rooms matching the provided types will be returned. - */ - types?: Array; - /** - * Filter to include or exclude discussion rooms. - * - * When undefined (default), discussions are included in the result set. - * - * When true, ONLY discussions are included in the result set (remove non-discussions). - * When false, discussion rooms are excluded from the result set. - */ - discussions?: boolean; - /** - * Filter to include or exclude team main rooms. - * - * When undefined (default), team main rooms are included in the result set. - * - * When true, ONLY team main rooms are included in the result set (remove non-teams). - * When false, team main rooms are excluded from the result set. - */ - teams?: boolean; -}; - -export type GetRoomsOptions = { - limit?: number; - skip?: number; -}; +export { GetMessagesSortableFields, GetMessagesOptions, GetRoomsFilters, GetRoomsOptions }; export abstract class RoomBridge extends BaseBridge { public async doCreate(room: IRoom, members: Array, appId: string): Promise { diff --git a/packages/apps/src/server/oauth2/OAuth2Client.ts b/packages/apps/src/server/oauth2/OAuth2Client.ts index 7a6ee989ef8b6..d6277bca86622 100644 --- a/packages/apps/src/server/oauth2/OAuth2Client.ts +++ b/packages/apps/src/server/oauth2/OAuth2Client.ts @@ -1,337 +1 @@ -import { URL } from 'url'; - -import type { App } from '@rocket.chat/apps-engine/definition/App'; -import type { IConfigurationExtend, IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors'; -import { HttpStatusCode } from '@rocket.chat/apps-engine/definition/accessors'; -import type { IApiEndpointInfo, IApiRequest, IApiResponse } from '@rocket.chat/apps-engine/definition/api'; -import { ApiSecurity, ApiVisibility } from '@rocket.chat/apps-engine/definition/api'; -import { RocketChatAssociationModel, RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata'; -import type { IAuthData, IOAuth2Client, IOAuth2ClientOptions } from '@rocket.chat/apps-engine/definition/oauth2/IOAuth2'; -import { SettingType } from '@rocket.chat/apps-engine/definition/settings'; -import type { IUser } from '@rocket.chat/apps-engine/definition/users'; - -export enum GrantType { - RefreshToken = 'refresh_token', - AuthorizationCode = 'authorization_code', -} - -export class OAuth2Client implements IOAuth2Client { - private defaultContents = { - success: `
\ -

\ - Authorization went successfully
\ - You can close this tab now
\ -

\ -
`, - failed: `
\ -

\ - Oops, something went wrong, please try again or in case it still does not work, contact the administrator.\ -

\ -
`, - }; - - constructor( - private readonly app: App, - private readonly config: IOAuth2ClientOptions, - ) {} - - public async setup(configuration: IConfigurationExtend): Promise { - configuration.api.provideApi({ - security: ApiSecurity.UNSECURE, - visibility: ApiVisibility.PUBLIC, - endpoints: [ - { - path: `${this.config.alias}-callback`, - get: this.handleOAuthCallback.bind(this), - }, - ], - }); - - await Promise.all([ - configuration.settings.provideSetting({ - id: `${this.config.alias}-oauth-client-id`, - type: SettingType.STRING, - public: true, - required: true, - packageValue: '', - i18nLabel: `${this.config.alias}-oauth-client-id`, - }), - - configuration.settings.provideSetting({ - id: `${this.config.alias}-oauth-clientsecret`, - type: SettingType.STRING, - public: true, - required: true, - packageValue: '', - i18nLabel: `${this.config.alias}-oauth-client-secret`, - }), - ]); - } - - public async getUserAuthorizationUrl(user: IUser, scopes?: Array): Promise { - const redirectUri = this.app.getAccessors().providedApiEndpoints[0].computedPath.substring(1); - - const siteUrl = await this.getBaseURLWithoutTrailingSlash(); - - const finalScopes = ([] as Array).concat(this.config.defaultScopes || [], scopes || []); - - const { authUri } = this.config; - - const clientId = await this.app - .getAccessors() - .reader.getEnvironmentReader() - .getSettings() - .getValueById(`${this.config.alias}-oauth-client-id`); - - const url = new URL(authUri, siteUrl); - - url.searchParams.set('response_type', 'code'); - url.searchParams.set('redirect_uri', `${siteUrl}/${redirectUri}`); - url.searchParams.set('state', user.id); - url.searchParams.set('client_id', clientId); - url.searchParams.set('access_type', 'offline'); - - if (finalScopes.length > 0) { - url.searchParams.set('scope', finalScopes.join(' ')); - } - - return url; - } - - public async getAccessTokenForUser(user: IUser): Promise { - const associations = [ - new RocketChatAssociationRecord(RocketChatAssociationModel.USER, user.id), - new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, `${this.config.alias}-oauth-connection`), - ]; - - const [result] = (await this.app.getAccessors().reader.getPersistenceReader().readByAssociations(associations)) as unknown as Array< - IAuthData | undefined - >; - - return result; - } - - public async refreshUserAccessToken(user: IUser, persis: IPersistence): Promise { - try { - const tokenInfo = await this.getAccessTokenForUser(user); - - if (!tokenInfo) { - throw new Error('User has no access token information'); - } - - if (!tokenInfo.refreshToken) { - throw new Error('User token information has no refresh token available'); - } - - const { - config: { refreshTokenUri }, - } = this; - - const clientId = await this.app - .getAccessors() - .reader.getEnvironmentReader() - .getSettings() - .getValueById(`${this.config.alias}-oauth-client-id`); - - const clientSecret = await this.app - .getAccessors() - .reader.getEnvironmentReader() - .getSettings() - .getValueById(`${this.config.alias}-oauth-clientsecret`); - - const siteUrl = await this.getBaseURLWithoutTrailingSlash(); - - const redirectUri = this.app.getAccessors().providedApiEndpoints[0].computedPath.substring(1); - - const url = new URL(refreshTokenUri); - - url.searchParams.set('client_id', clientId); - url.searchParams.set('client_secret', clientSecret); - url.searchParams.set('redirect_uri', `${siteUrl}/${redirectUri}`); - url.searchParams.set('refresh_token', tokenInfo.refreshToken); - url.searchParams.set('grant_type', GrantType.RefreshToken); - - const { content, statusCode } = await this.app.getAccessors().http.post(url.href); - - if (statusCode !== 200) { - throw new Error('Request to provider was unsuccessful. Check logs for more information'); - } - - const { access_token, expires_in, refresh_token, scope } = JSON.parse(content as string); - - if (!access_token) { - throw new Error('No access token returned by the provider'); - } - - const authData: IAuthData = { - scope, - token: access_token, - expiresAt: expires_in, - refreshToken: refresh_token || tokenInfo.refreshToken, - }; - - await this.saveToken(authData, user.id, persis); - - return authData; - } catch (error) { - this.app.getLogger().error(error); - throw error; - } - } - - public async revokeUserAccessToken(user: IUser, persis: IPersistence): Promise { - try { - const tokenInfo = await this.getAccessTokenForUser(user); - - if (!tokenInfo?.token) { - throw new Error('No access token available for this user.'); - } - - const url = new URL(this.config.revokeTokenUri); - - url.searchParams.set('token', tokenInfo?.token); - - const result = await this.app.getAccessors().http.post(url.href); - - if (result.statusCode !== 200) { - throw new Error('Provider did not allow token to be revoked'); - } - - await this.removeToken({ userId: user.id, persis }); - - return true; - } catch (error) { - this.app.getLogger().error(error); - return false; - } - } - - private async getBaseURLWithoutTrailingSlash(): Promise { - const SITE_URL = 'Site_Url'; - const url = await this.app.getAccessors().environmentReader.getServerSettings().getValueById(SITE_URL); - - if (url.endsWith('/')) { - return url.substr(0, url.length - 1); - } - return url; - } - - private async handleOAuthCallback( - request: IApiRequest, - endpoint: IApiEndpointInfo, - read: IRead, - modify: IModify, - http: IHttp, - persis: IPersistence, - ): Promise { - try { - const { - query: { code, state }, - } = request; - - const user = await this.app.getAccessors().reader.getUserReader().getById(state); - - if (!user) { - throw new Error('User could not be determined.'); - } - - // User chose not to authorize the access - if (!code) { - const failedResult = await this.config.authorizationCallback?.(undefined, user, read, modify, http, persis); - - return { - status: HttpStatusCode.UNAUTHORIZED, - content: failedResult?.responseContent || this.defaultContents.failed, - }; - } - - const siteUrl = await this.getBaseURLWithoutTrailingSlash(); - - const accessTokenUrl = this.config.accessTokenUri; - - const redirectUri = this.app.getAccessors().providedApiEndpoints[0].computedPath.substring(1); - - const clientId = await this.app - .getAccessors() - .reader.getEnvironmentReader() - .getSettings() - .getValueById(`${this.config.alias}-oauth-client-id`); - - const clientSecret = await this.app - .getAccessors() - .reader.getEnvironmentReader() - .getSettings() - .getValueById(`${this.config.alias}-oauth-clientsecret`); - - const url = new URL(accessTokenUrl, siteUrl); - - url.searchParams.set('client_id', clientId); - url.searchParams.set('redirect_uri', `${siteUrl}/${redirectUri}`); - url.searchParams.set('code', code); - url.searchParams.set('client_secret', clientSecret); - url.searchParams.set('access_type', 'offline'); - url.searchParams.set('grant_type', GrantType.AuthorizationCode); - - const { content, statusCode } = await http.post(url.href, { - headers: { Accept: 'application/json' }, - }); - - // If provider had a server error, nothing we can do - if (statusCode >= 500) { - throw new Error('Request for access token failed. Check logs for more information'); - } - - const response = JSON.parse(content as string); - const { access_token, expires_in, refresh_token, scope } = response; - - const authData: IAuthData = { - scope, - token: access_token, - expiresAt: expires_in, - refreshToken: refresh_token, - }; - - const result = await this.config.authorizationCallback?.(authData, user, read, modify, http, persis); - - await this.saveToken(authData, user.id, persis); - - return { - status: statusCode, - content: result?.responseContent || this.defaultContents.success, - }; - } catch (error) { - this.app.getLogger().error(error); - return { - status: HttpStatusCode.INTERNAL_SERVER_ERROR, - content: this.defaultContents.failed, - }; - } - } - - private async saveToken(authData: IAuthData, userId: string, persis: IPersistence): Promise { - const { scope, token, expiresAt, refreshToken } = authData; - - return persis.updateByAssociations( - [ - new RocketChatAssociationRecord(RocketChatAssociationModel.USER, userId), - new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, `${this.config.alias}-oauth-connection`), - ], - { - scope, - token, - expiresAt: expiresAt || '', - refreshToken: refreshToken || '', - }, - true, // we want to create the record if it doesn't exist - ); - } - - private async removeToken({ userId, persis }: { userId: string; persis: IPersistence }): Promise { - const [result] = (await persis.removeByAssociations([ - new RocketChatAssociationRecord(RocketChatAssociationModel.USER, userId), - new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, `${this.config.alias}-oauth-connection`), - ])) as unknown as Array; - - return result; - } -} +export { OAuth2Client, GrantType } from '@rocket.chat/apps-engine/definition/oauth2/OAuth2Client'; From 83f9262b5fdfca5a76ec8b12874149916bfb0fb7 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Apr 2026 16:35:59 -0300 Subject: [PATCH 19/19] fix(apps): resolve strict-mode TypeScript errors in packages/apps/src Fix all TypeScript strict-mode errors introduced when the packages/apps package was configured with strict: true, noImplicitReturns, and noUnusedParameters. Errors included: - Map.get() used without non-null assertion after has()/set() guards - Catch clause variables typed as unknown (now cast explicitly) - Missing return statements in all code paths (TS7030) - Unused parameters renamed with _ prefix - Optional chaining / nullish coalescing for possibly-undefined values - Bridge do* methods returning undefined as unknown as T for denied perms Co-Authored-By: Claude Sonnet 4.6 --- packages/apps/src/client/AppClientManager.ts | 2 +- packages/apps/src/server/AppManager.ts | 35 ++- packages/apps/src/server/ProxiedApp.ts | 15 +- .../apps/src/server/accessors/ContactRead.ts | 4 +- .../src/server/accessors/DiscussionBuilder.ts | 8 +- .../accessors/EnvironmentalVariableRead.ts | 2 +- packages/apps/src/server/accessors/Http.ts | 8 +- .../accessors/LivechatMessageBuilder.ts | 24 +- .../apps/src/server/accessors/LivechatRead.ts | 2 +- .../src/server/accessors/MessageBuilder.ts | 66 ++--- .../src/server/accessors/MessageExtender.ts | 4 +- .../apps/src/server/accessors/MessageRead.ts | 6 +- .../src/server/accessors/ModerationModify.ts | 2 +- .../src/server/accessors/ModifyCreator.ts | 8 +- .../src/server/accessors/ModifyExtender.ts | 6 +- .../src/server/accessors/ModifyUpdater.ts | 6 +- .../apps/src/server/accessors/Notifier.ts | 4 +- .../src/server/accessors/OAuthAppsModify.ts | 2 +- .../src/server/accessors/OAuthAppsReader.ts | 4 +- .../apps/src/server/accessors/Persistence.ts | 6 +- .../apps/src/server/accessors/RoomBuilder.ts | 14 +- .../apps/src/server/accessors/RoomRead.ts | 6 +- .../apps/src/server/accessors/UIController.ts | 8 +- .../apps/src/server/accessors/UserBuilder.ts | 10 +- .../accessors/VideoConferenceBuilder.ts | 28 +-- .../server/accessors/VideoConferenceExtend.ts | 2 +- .../server/bridges/CloudWorkspaceBridge.ts | 2 + .../apps/src/server/bridges/CommandBridge.ts | 2 + .../apps/src/server/bridges/ContactBridge.ts | 4 + .../bridges/EnvironmentalVariableBridge.ts | 6 + .../apps/src/server/bridges/HttpBridge.ts | 2 + .../apps/src/server/bridges/LivechatBridge.ts | 50 +++- .../apps/src/server/bridges/MessageBridge.ts | 4 + .../src/server/bridges/ModerationBridge.ts | 4 +- .../src/server/bridges/OAuthAppsBridge.ts | 12 +- .../src/server/bridges/PersistenceBridge.ts | 16 ++ .../apps/src/server/bridges/RoleBridge.ts | 4 + .../apps/src/server/bridges/RoomBridge.ts | 30 +++ .../src/server/bridges/ServerSettingBridge.ts | 6 + .../apps/src/server/bridges/ThreadBridge.ts | 2 + .../apps/src/server/bridges/UploadBridge.ts | 6 + .../apps/src/server/bridges/UserBridge.ts | 18 ++ .../server/bridges/VideoConferenceBridge.ts | 4 + .../compiler/AppFabricationFulfillment.ts | 10 +- .../apps/src/server/compiler/AppImplements.ts | 4 +- .../src/server/compiler/AppPackageParser.ts | 14 +- .../src/server/managers/AppAccessorManager.ts | 18 +- packages/apps/src/server/managers/AppApi.ts | 6 +- .../apps/src/server/managers/AppApiManager.ts | 8 +- .../managers/AppExternalComponentManager.ts | 8 +- .../src/server/managers/AppLicenseManager.ts | 4 +- .../src/server/managers/AppListenerManager.ts | 227 +++++++++--------- .../AppOutboundCommunicationProvider.ts | 17 +- ...AppOutboundCommunicationProviderManager.ts | 4 +- .../server/managers/AppPermissionManager.ts | 2 +- .../src/server/managers/AppRuntimeManager.ts | 2 +- .../server/managers/AppSchedulerManager.ts | 6 +- .../src/server/managers/AppSettingsManager.ts | 2 +- .../server/managers/AppSignatureManager.ts | 10 +- .../server/managers/AppSlashCommandManager.ts | 58 ++--- .../server/managers/AppVideoConfProvider.ts | 5 +- .../managers/AppVideoConfProviderManager.ts | 8 +- .../server/managers/UIActionButtonManager.ts | 2 +- .../license/AppLicenseValidationResult.ts | 2 +- packages/apps/src/server/messages/Message.ts | 4 +- packages/apps/src/server/misc/UIHelper.ts | 15 +- packages/apps/src/server/rooms/Room.ts | 14 +- .../server/runtime/AppsEngineEmptyRuntime.ts | 6 +- .../server/runtime/AppsEngineNodeRuntime.ts | 4 +- .../src/server/runtime/AppsEngineRuntime.ts | 6 +- .../apps/src/server/runtime/EmptyRuntime.ts | 2 +- .../runtime/deno/AppsEngineDenoRuntime.ts | 25 +- .../server/runtime/deno/LivenessManager.ts | 6 +- .../server/runtime/deno/ProcessMessenger.ts | 2 +- .../apps/src/server/runtime/deno/codec.ts | 6 +- 75 files changed, 575 insertions(+), 386 deletions(-) diff --git a/packages/apps/src/client/AppClientManager.ts b/packages/apps/src/client/AppClientManager.ts index bba33e76f25a9..8427aa80e11ee 100644 --- a/packages/apps/src/client/AppClientManager.ts +++ b/packages/apps/src/client/AppClientManager.ts @@ -21,7 +21,7 @@ export class AppClientManager { } public async load(): Promise { - this.apps = await this.communicator.getEnabledApps(); + this.apps = await this.communicator!.getEnabledApps(); console.log('Enabled apps:', this.apps); } diff --git a/packages/apps/src/server/AppManager.ts b/packages/apps/src/server/AppManager.ts index c7ddc42b63623..f3a39d65932ee 100644 --- a/packages/apps/src/server/AppManager.ts +++ b/packages/apps/src/server/AppManager.ts @@ -421,7 +421,7 @@ export class AppManager { } if (typeof filter.ids !== 'undefined') { - rls = rls.filter((rl) => filter.ids.includes(rl.getID())); + rls = rls.filter((rl) => filter.ids!.includes(rl.getID())); } if (typeof filter.installationSource !== 'undefined') { @@ -438,7 +438,7 @@ export class AppManager { } /** Gets a single App by the id passed in. */ - public getOneById(appId: string): ProxiedApp { + public getOneById(appId: string): ProxiedApp | undefined { return this.apps.get(appId); } @@ -504,7 +504,7 @@ export class AppManager { await app.setStatus(status, silent); - const storageItem = await this.appMetadataStorage.retrieveOne(id); + const storageItem = (await this.appMetadataStorage.retrieveOne(id))!; app.getStorageItem().marketplaceInfo = storageItem.marketplaceInfo; await app.validateLicense().catch(() => {}); @@ -523,7 +523,7 @@ export class AppManager { await this.purgeAppConfig(app, { keepScheduledJobs: true }); - const storageItem = await this.appMetadataStorage.retrieveOne(id); + const storageItem = (await this.appMetadataStorage.retrieveOne(id))!; app.getStorageItem().marketplaceInfo = storageItem.marketplaceInfo; await app.validateLicense().catch(() => {}); @@ -674,6 +674,9 @@ export class AppManager { */ public async remove(id: string, uninstallationParameters: IAppUninstallParameters): Promise { const app = this.apps.get(id); + if (!app) { + throw new Error(`No App by the id "${id}" exists.`); + } const { user } = uninstallationParameters; // First remove the app @@ -693,6 +696,10 @@ export class AppManager { public async removeLocal(id: string): Promise { const app = this.apps.get(id); + if (!app) { + return; + } + if (AppStatusUtils.isEnabled(await app.getStatus())) { await this.disable(id); } @@ -761,7 +768,7 @@ export class AppManager { // Errors here don't really prevent the process from dying, so we don't really need to do anything on the catch await this.getRuntime() - .stopRuntime(this.apps.get(old.id).getRuntimeController()) + .stopRuntime(this.apps.get(old.id)!.getRuntimeController()) .catch(() => {}); const app = await this.getCompiler().toSandBox(this, descriptor, result); @@ -794,7 +801,7 @@ export class AppManager { .catch(() => {}); } - await this.updateApp(app, updateOptions.user, old.info.version); + await this.updateApp(app, updateOptions.user ?? null, old.info.version); return aff; } @@ -815,7 +822,7 @@ export class AppManager { // Errors here don't really prevent the process from dying, so we don't really need to do anything on the catch await this.getRuntime() - .stopRuntime(this.apps.get(stored.id).getRuntimeController()) + .stopRuntime(this.apps.get(stored.id)!.getRuntimeController()) .catch(() => {}); return this.getCompiler().toSandBox(this, stored, parseResult); @@ -824,6 +831,8 @@ export class AppManager { if (appPackageOrInstance instanceof ProxiedApp) { return appPackageOrInstance; } + + throw new Error('Invalid app package or instance provided to updateLocal'); })(); // We don't keep slashcommands here as the update could potentially not provide the same list @@ -883,7 +892,7 @@ export class AppManager { await this.enable(rl.getID()); storageItem.status = AppStatus.MANUALLY_ENABLED; - await this.appMetadataStorage.updateStatus(storageItem._id, AppStatus.MANUALLY_ENABLED); + await this.appMetadataStorage.updateStatus(storageItem._id!, AppStatus.MANUALLY_ENABLED); } else { if (!AppStatusUtils.isEnabled(await rl.getStatus())) { throw new Error('Can not disable an App which is not enabled.'); @@ -892,7 +901,7 @@ export class AppManager { await this.disable(rl.getID(), AppStatus.MANUALLY_DISABLED); storageItem.status = AppStatus.MANUALLY_DISABLED; - await this.appMetadataStorage.updateStatus(storageItem._id, AppStatus.MANUALLY_DISABLED); + await this.appMetadataStorage.updateStatus(storageItem._id!, AppStatus.MANUALLY_DISABLED); } return rl; @@ -918,7 +927,7 @@ export class AppManager { return; } - appStorageItem.marketplaceInfo[0].subscriptionInfo = appInfo.subscriptionInfo; + appStorageItem.marketplaceInfo![0].subscriptionInfo = appInfo.subscriptionInfo; appStorageItem.signature = await this.getSignatureManager().signApp(appStorageItem); return this.appMetadataStorage.updatePartialAndReturnDocument({ @@ -961,7 +970,7 @@ export class AppManager { const storageItem = app.getStorageItem(); storageItem.status = status; - return this.appMetadataStorage.updateStatus(storageItem._id, storageItem.status).catch(console.error) as Promise; + return this.appMetadataStorage.updateStatus(storageItem._id!, storageItem.status).catch(console.error) as Promise; }), ), ); @@ -993,7 +1002,7 @@ export class AppManager { await this.enableApp(rl, silenceStatus); } - return this.apps.get(item.id); + return this.apps.get(item.id)!; } private async runStartUpProcess(storageItem: IAppStorageItem, app: ProxiedApp, silenceStatus: boolean): Promise { @@ -1099,7 +1108,7 @@ export class AppManager { this.uiActionButtonManager.clearAppActionButtons(app.getID()); this.videoConfProviderManager.unregisterProviders(app.getID()); await this.outboundCommunicationProviderManager.unregisterProviders(app.getID(), { - keepReferences: opts.keepOutboundCommunicationProviders, + keepReferences: opts.keepOutboundCommunicationProviders ?? false, }); } diff --git a/packages/apps/src/server/ProxiedApp.ts b/packages/apps/src/server/ProxiedApp.ts index 8523975db2973..4711913bf06b0 100644 --- a/packages/apps/src/server/ProxiedApp.ts +++ b/packages/apps/src/server/ProxiedApp.ts @@ -1,6 +1,6 @@ import { inspect } from 'util'; -import * as mem from 'mem'; +import mem = require('mem'); import type { AppManager } from './AppManager'; import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; @@ -18,7 +18,7 @@ import type { AppInstallationSource, IAppStorageItem } from './storage'; export class ProxiedApp { private previousStatus: AppStatus; - private latestLicenseValidationResult: AppLicenseValidationResult; + private latestLicenseValidationResult!: AppLicenseValidationResult; constructor( private readonly manager: AppManager, @@ -67,18 +67,19 @@ export class ProxiedApp { try { return await this.appRuntime.sendRequest({ method: `app:${method}`, params: args }, options); } catch (e) { - if (e.code === AppsEngineException.JSONRPC_ERROR_CODE) { - throw new AppsEngineException(e.message); + const err = e as { code: number; message: string }; + if (err.code === AppsEngineException.JSONRPC_ERROR_CODE) { + throw new AppsEngineException(err.message); } - if (e.code === JSONRPC_METHOD_NOT_FOUND) { + if (err.code === JSONRPC_METHOD_NOT_FOUND) { throw e; } // We cannot throw this error as the previous implementation swallowed those // and since the server is not prepared to handle those we might crash it if we throw // Range of JSON-RPC error codes: https://www.jsonrpc.org/specification#error_object - if (e.code >= -32999 || e.code <= -32000) { + if (err.code >= -32999 || err.code <= -32000) { // we really need to receive a logger from rocket.chat console.error('JSON-RPC error received: ', inspect(e, { depth: 10 })); } @@ -148,7 +149,7 @@ export class ProxiedApp { try { await this.manager.getSignatureManager().verifySignedApp(this.getStorageItem()); } catch (e) { - throw new InvalidInstallationError(e.message); + throw new InvalidInstallationError((e as Error).message); } } diff --git a/packages/apps/src/server/accessors/ContactRead.ts b/packages/apps/src/server/accessors/ContactRead.ts index ddabcf064e22f..49032cb53564b 100644 --- a/packages/apps/src/server/accessors/ContactRead.ts +++ b/packages/apps/src/server/accessors/ContactRead.ts @@ -8,7 +8,7 @@ export class ContactRead implements IContactRead { private readonly appId: string, ) {} - public getById(contactId: ILivechatContact['_id']): Promise { - return this.bridges.getContactBridge().doGetById(contactId, this.appId); + public getById(contactId: ILivechatContact['_id']): Promise { + return this.bridges.getContactBridge().doGetById(contactId, this.appId) as Promise; } } diff --git a/packages/apps/src/server/accessors/DiscussionBuilder.ts b/packages/apps/src/server/accessors/DiscussionBuilder.ts index 55723eaadeccb..d357cd60758cc 100644 --- a/packages/apps/src/server/accessors/DiscussionBuilder.ts +++ b/packages/apps/src/server/accessors/DiscussionBuilder.ts @@ -6,11 +6,11 @@ import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; export class DiscussionBuilder extends RoomBuilder implements IDiscussionBuilder { - public kind: RocketChatAssociationModel.DISCUSSION; + public override kind: RocketChatAssociationModel.DISCUSSION; - private reply: string; + private reply!: string; - private parentMessage: IMessage; + private parentMessage!: IMessage; constructor(data?: Partial) { super(data); @@ -24,7 +24,7 @@ export class DiscussionBuilder extends RoomBuilder implements IDiscussionBuilder } public getParentRoom(): IRoom { - return this.room.parentRoom; + return this.room.parentRoom as IRoom; } public setReply(reply: string): IDiscussionBuilder { diff --git a/packages/apps/src/server/accessors/EnvironmentalVariableRead.ts b/packages/apps/src/server/accessors/EnvironmentalVariableRead.ts index 11277832f4d46..0916c6512a88e 100644 --- a/packages/apps/src/server/accessors/EnvironmentalVariableRead.ts +++ b/packages/apps/src/server/accessors/EnvironmentalVariableRead.ts @@ -8,7 +8,7 @@ export class EnvironmentalVariableRead implements IEnvironmentalVariableRead { ) {} public getValueByName(envVarName: string): Promise { - return this.bridge.doGetValueByName(envVarName, this.appId); + return this.bridge.doGetValueByName(envVarName, this.appId) as Promise; } public isReadable(envVarName: string): Promise { diff --git a/packages/apps/src/server/accessors/Http.ts b/packages/apps/src/server/accessors/Http.ts index 3016e9903c74d..b6061f0ac7824 100644 --- a/packages/apps/src/server/accessors/Http.ts +++ b/packages/apps/src/server/accessors/Http.ts @@ -39,8 +39,8 @@ export class Http implements IHttp { } this.httpExtender.getDefaultHeaders().forEach((value: string, key: string) => { - if (typeof request.headers[key] !== 'string') { - request.headers[key] = value; + if (typeof request.headers![key] !== 'string') { + request.headers![key] = value; } }); @@ -49,8 +49,8 @@ export class Http implements IHttp { } this.httpExtender.getDefaultParams().forEach((value: string, key: string) => { - if (typeof request.params[key] !== 'string') { - request.params[key] = value; + if (typeof request.params![key] !== 'string') { + request.params![key] = value; } }); diff --git a/packages/apps/src/server/accessors/LivechatMessageBuilder.ts b/packages/apps/src/server/accessors/LivechatMessageBuilder.ts index d83b0ec3455eb..d1d2ab97a5a93 100644 --- a/packages/apps/src/server/accessors/LivechatMessageBuilder.ts +++ b/packages/apps/src/server/accessors/LivechatMessageBuilder.ts @@ -42,7 +42,7 @@ export class LivechatMessageBuilder implements ILivechatMessageBuilder { } public getSender(): IUser { - return this.msg.sender; + return this.msg.sender as IUser; } public setText(text: string): ILivechatMessageBuilder { @@ -51,7 +51,7 @@ export class LivechatMessageBuilder implements ILivechatMessageBuilder { } public getText(): string { - return this.msg.text; + return this.msg.text as string; } public setEmojiAvatar(emoji: string): ILivechatMessageBuilder { @@ -60,7 +60,7 @@ export class LivechatMessageBuilder implements ILivechatMessageBuilder { } public getEmojiAvatar(): string { - return this.msg.emoji; + return this.msg.emoji as string; } public setAvatarUrl(avatarUrl: string): ILivechatMessageBuilder { @@ -69,7 +69,7 @@ export class LivechatMessageBuilder implements ILivechatMessageBuilder { } public getAvatarUrl(): string { - return this.msg.avatarUrl; + return this.msg.avatarUrl as string; } public setUsernameAlias(alias: string): ILivechatMessageBuilder { @@ -78,7 +78,7 @@ export class LivechatMessageBuilder implements ILivechatMessageBuilder { } public getUsernameAlias(): string { - return this.msg.alias; + return this.msg.alias as string; } public addAttachment(attachment: IMessageAttachment): ILivechatMessageBuilder { @@ -96,7 +96,7 @@ export class LivechatMessageBuilder implements ILivechatMessageBuilder { } public getAttachments(): Array { - return this.msg.attachments; + return this.msg.attachments as Array; } public replaceAttachment(position: number, attachment: IMessageAttachment): ILivechatMessageBuilder { @@ -132,7 +132,7 @@ export class LivechatMessageBuilder implements ILivechatMessageBuilder { } public getEditor(): IUser { - return this.msg.editor; + return this.msg.editor as IUser; } public setGroupable(groupable: boolean): ILivechatMessageBuilder { @@ -141,7 +141,7 @@ export class LivechatMessageBuilder implements ILivechatMessageBuilder { } public getGroupable(): boolean { - return this.msg.groupable; + return this.msg.groupable as boolean; } public setParseUrls(parseUrls: boolean): ILivechatMessageBuilder { @@ -150,7 +150,7 @@ export class LivechatMessageBuilder implements ILivechatMessageBuilder { } public getParseUrls(): boolean { - return this.msg.parseUrls; + return this.msg.parseUrls as boolean; } public setToken(token: string): ILivechatMessageBuilder { @@ -159,18 +159,18 @@ export class LivechatMessageBuilder implements ILivechatMessageBuilder { } public getToken(): string { - return this.msg.token; + return this.msg.token as string; } public setVisitor(visitor: IVisitor): ILivechatMessageBuilder { this.msg.visitor = visitor; - delete this.msg.sender; + this.msg.sender = undefined as unknown as IUser; return this; } public getVisitor(): IVisitor { - return this.msg.visitor; + return this.msg.visitor as IVisitor; } public getMessage(): ILivechatMessage { diff --git a/packages/apps/src/server/accessors/LivechatRead.ts b/packages/apps/src/server/accessors/LivechatRead.ts index 5db4b480c79e0..5d934102a0198 100644 --- a/packages/apps/src/server/accessors/LivechatRead.ts +++ b/packages/apps/src/server/accessors/LivechatRead.ts @@ -32,7 +32,7 @@ export class LivechatRead implements ILivechatRead { } public getLivechatRooms(visitor: IVisitor, departmentId?: string): Promise> { - return this.livechatBridge.doFindRooms(visitor, departmentId, this.appId); + return this.livechatBridge.doFindRooms(visitor, departmentId ?? null, this.appId); } public getLivechatTotalOpenRoomsByAgentId(agentId: string): Promise { diff --git a/packages/apps/src/server/accessors/MessageBuilder.ts b/packages/apps/src/server/accessors/MessageBuilder.ts index 1d30ad35f9410..d3760359fe189 100644 --- a/packages/apps/src/server/accessors/MessageBuilder.ts +++ b/packages/apps/src/server/accessors/MessageBuilder.ts @@ -22,7 +22,7 @@ export class MessageBuilder implements IMessageBuilder { delete data.id; this.msg = data; - return this; + return this as IMessageBuilder; } public setUpdateData(data: IMessage, editor: IUser): IMessageBuilder { @@ -30,22 +30,22 @@ export class MessageBuilder implements IMessageBuilder { this.msg.editor = editor; this.msg.editedAt = new Date(); - return this; + return this as IMessageBuilder; } public setThreadId(threadId: string): IMessageBuilder { this.msg.threadId = threadId; - return this; + return this as IMessageBuilder; } public getThreadId(): string { - return this.msg.threadId; + return this.msg.threadId as string; } public setRoom(room: IRoom): IMessageBuilder { this.msg.room = room; - return this; + return this as IMessageBuilder; } public getRoom(): IRoom { @@ -54,47 +54,47 @@ export class MessageBuilder implements IMessageBuilder { public setSender(sender: IUser): IMessageBuilder { this.msg.sender = sender; - return this; + return this as IMessageBuilder; } public getSender(): IUser { - return this.msg.sender; + return this.msg.sender as IUser; } public setText(text: string): IMessageBuilder { this.msg.text = text; - return this; + return this as IMessageBuilder; } public getText(): string { - return this.msg.text; + return this.msg.text as string; } public setEmojiAvatar(emoji: string): IMessageBuilder { this.msg.emoji = emoji; - return this; + return this as IMessageBuilder; } public getEmojiAvatar(): string { - return this.msg.emoji; + return this.msg.emoji as string; } public setAvatarUrl(avatarUrl: string): IMessageBuilder { this.msg.avatarUrl = avatarUrl; - return this; + return this as IMessageBuilder; } public getAvatarUrl(): string { - return this.msg.avatarUrl; + return this.msg.avatarUrl as string; } public setUsernameAlias(alias: string): IMessageBuilder { this.msg.alias = alias; - return this; + return this as IMessageBuilder; } public getUsernameAlias(): string { - return this.msg.alias; + return this.msg.alias as string; } public addAttachment(attachment: IMessageAttachment): IMessageBuilder { @@ -103,16 +103,16 @@ export class MessageBuilder implements IMessageBuilder { } this.msg.attachments.push(attachment); - return this; + return this as IMessageBuilder; } public setAttachments(attachments: Array): IMessageBuilder { this.msg.attachments = attachments; - return this; + return this as IMessageBuilder; } public getAttachments(): Array { - return this.msg.attachments; + return this.msg.attachments as Array; } public replaceAttachment(position: number, attachment: IMessageAttachment): IMessageBuilder { @@ -125,7 +125,7 @@ export class MessageBuilder implements IMessageBuilder { } this.msg.attachments[position] = attachment; - return this; + return this as IMessageBuilder; } public removeAttachment(position: number): IMessageBuilder { @@ -139,34 +139,34 @@ export class MessageBuilder implements IMessageBuilder { this.msg.attachments.splice(position, 1); - return this; + return this as IMessageBuilder; } public setEditor(user: IUser): IMessageBuilder { this.msg.editor = user; - return this; + return this as IMessageBuilder; } public getEditor(): IUser { - return this.msg.editor; + return this.msg.editor as IUser; } public setGroupable(groupable: boolean): IMessageBuilder { this.msg.groupable = groupable; - return this; + return this as IMessageBuilder; } public getGroupable(): boolean { - return this.msg.groupable; + return this.msg.groupable as boolean; } public setParseUrls(parseUrls: boolean): IMessageBuilder { this.msg.parseUrls = parseUrls; - return this; + return this as IMessageBuilder; } public getParseUrls(): boolean { - return this.msg.parseUrls; + return this.msg.parseUrls as boolean; } public getMessage(): IMessage { @@ -177,7 +177,7 @@ export class MessageBuilder implements IMessageBuilder { return this.msg; } - public addBlocks(blocks: BlockBuilder | Array) { + public addBlocks(blocks: BlockBuilder | Array): IMessageBuilder { if (!Array.isArray(this.msg.blocks)) { this.msg.blocks = []; } @@ -188,21 +188,21 @@ export class MessageBuilder implements IMessageBuilder { this.msg.blocks.push(...blocks); } - return this; + return this as IMessageBuilder; } - public setBlocks(blocks: BlockBuilder | Array) { + public setBlocks(blocks: BlockBuilder | Array): IMessageBuilder { if (blocks instanceof BlockBuilder) { this.msg.blocks = blocks.getBlocks(); } else { this.msg.blocks = blocks; } - return this; + return this as IMessageBuilder; } - public getBlocks() { - return this.msg.blocks; + public getBlocks(): Array { + return this.msg.blocks as Array; } public addCustomField(key: string, value: any): IMessageBuilder { @@ -219,6 +219,6 @@ export class MessageBuilder implements IMessageBuilder { } this.msg.customFields[key] = value; - return this; + return this as IMessageBuilder; } } diff --git a/packages/apps/src/server/accessors/MessageExtender.ts b/packages/apps/src/server/accessors/MessageExtender.ts index e569ccf7efbcc..c43039c2f669e 100644 --- a/packages/apps/src/server/accessors/MessageExtender.ts +++ b/packages/apps/src/server/accessors/MessageExtender.ts @@ -33,13 +33,13 @@ export class MessageExtender implements IMessageExtender { } public addAttachment(attachment: IMessageAttachment): IMessageExtender { - this.msg.attachments.push(attachment); + this.msg.attachments!.push(attachment); return this; } public addAttachments(attachments: Array): IMessageExtender { - this.msg.attachments = this.msg.attachments.concat(attachments); + this.msg.attachments = this.msg.attachments!.concat(attachments); return this; } diff --git a/packages/apps/src/server/accessors/MessageRead.ts b/packages/apps/src/server/accessors/MessageRead.ts index f3bd44fc0dce4..c965f46d7ccfb 100644 --- a/packages/apps/src/server/accessors/MessageRead.ts +++ b/packages/apps/src/server/accessors/MessageRead.ts @@ -18,17 +18,17 @@ export class MessageRead implements IMessageRead { const msg = await this.messageBridge.doGetById(messageId, this.appId); if (!msg) { - return undefined; + return undefined as unknown as IUser; } - return msg.sender; + return msg.sender as IUser; } public async getRoom(messageId: string): Promise { const msg = await this.messageBridge.doGetById(messageId, this.appId); if (!msg) { - return undefined; + return undefined as unknown as IRoom; } return msg.room; diff --git a/packages/apps/src/server/accessors/ModerationModify.ts b/packages/apps/src/server/accessors/ModerationModify.ts index a8a308053876b..f5d21eaf5d773 100644 --- a/packages/apps/src/server/accessors/ModerationModify.ts +++ b/packages/apps/src/server/accessors/ModerationModify.ts @@ -6,7 +6,7 @@ import type { ModerationBridge } from '../bridges'; export class ModerationModify implements IModerationModify { constructor( private moderationBridge: ModerationBridge, - appId: string, + _appId: string, ) {} public report(messageId: string, description: string, userId: string, appId: string): Promise { diff --git a/packages/apps/src/server/accessors/ModifyCreator.ts b/packages/apps/src/server/accessors/ModifyCreator.ts index 03ff764d032d5..3b80485618f1a 100644 --- a/packages/apps/src/server/accessors/ModifyCreator.ts +++ b/packages/apps/src/server/accessors/ModifyCreator.ts @@ -93,7 +93,7 @@ export class ModifyCreator implements IModifyCreator { public startRoom(data?: IRoom): IRoomBuilder { if (data) { - delete data.id; + (data as Partial).id = undefined; } return new RoomBuilder(data); @@ -101,7 +101,7 @@ export class ModifyCreator implements IModifyCreator { public startDiscussion(data?: Partial): IDiscussionBuilder { if (data) { - delete data.id; + data.id = undefined; } return new DiscussionBuilder(data); @@ -196,7 +196,7 @@ export class ModifyCreator implements IModifyCreator { private _finishRoom(builder: IRoomBuilder): Promise { const result = builder.getRoom(); - delete result.id; + (result as Partial).id = undefined; if (!result.type) { throw new Error('Invalid type assigned to the room.'); @@ -225,7 +225,7 @@ export class ModifyCreator implements IModifyCreator { private _finishDiscussion(builder: IDiscussionBuilder): Promise { const room = builder.getRoom(); - delete room.id; + (room as Partial).id = undefined; if (!room.creator?.id) { throw new Error('Invalid creator assigned to the discussion.'); diff --git a/packages/apps/src/server/accessors/ModifyExtender.ts b/packages/apps/src/server/accessors/ModifyExtender.ts index 0b324b3bdb1af..95a81abc2e307 100644 --- a/packages/apps/src/server/accessors/ModifyExtender.ts +++ b/packages/apps/src/server/accessors/ModifyExtender.ts @@ -12,15 +12,15 @@ export class ModifyExtender implements IModifyExtender { private readonly appId: string, ) {} - public async extendMessage(messageId: string, updater: IUser): Promise { + public async extendMessage(messageId: string, _updater: IUser): Promise { const msg = await this.bridges.getMessageBridge().doGetById(messageId, this.appId); - msg.editor = updater; + msg.editor = _updater; msg.editedAt = new Date(); return new MessageExtender(msg); } - public async extendRoom(roomId: string, updater: IUser): Promise { + public async extendRoom(roomId: string, _updater: IUser): Promise { const room = await this.bridges.getRoomBridge().doGetById(roomId, this.appId); room.updatedAt = new Date(); diff --git a/packages/apps/src/server/accessors/ModifyUpdater.ts b/packages/apps/src/server/accessors/ModifyUpdater.ts index 1133738b6e743..d030967019dc9 100644 --- a/packages/apps/src/server/accessors/ModifyUpdater.ts +++ b/packages/apps/src/server/accessors/ModifyUpdater.ts @@ -15,7 +15,7 @@ export class ModifyUpdater implements IModifyUpdater { private userUpdater: IUserUpdater; - private messageUpdater: IMessageUpdater; + private messageUpdater!: IMessageUpdater; constructor( private readonly bridges: AppBridges, @@ -37,13 +37,13 @@ export class ModifyUpdater implements IModifyUpdater { return this.messageUpdater; } - public async message(messageId: string, updater: IUser): Promise { + public async message(messageId: string, _updater: IUser): Promise { const msg = await this.bridges.getMessageBridge().doGetById(messageId, this.appId); return new MessageBuilder(msg); } - public async room(roomId: string, updater: IUser): Promise { + public async room(roomId: string, _updater: IUser): Promise { const room = await this.bridges.getRoomBridge().doGetById(roomId, this.appId); return new RoomBuilder(room); diff --git a/packages/apps/src/server/accessors/Notifier.ts b/packages/apps/src/server/accessors/Notifier.ts index 142fde36dcf82..4dd7c53ed6bd5 100644 --- a/packages/apps/src/server/accessors/Notifier.ts +++ b/packages/apps/src/server/accessors/Notifier.ts @@ -18,7 +18,7 @@ export class Notifier implements INotifier { if (!message.sender?.id) { const appUser = await this.userBridge.doGetAppUser(this.appId); - message.sender = appUser; + message.sender = appUser as IUser; } await this.msgBridge.doNotifyUser(user, message, this.appId); @@ -28,7 +28,7 @@ export class Notifier implements INotifier { if (!message.sender?.id) { const appUser = await this.userBridge.doGetAppUser(this.appId); - message.sender = appUser; + message.sender = appUser as IUser; } await this.msgBridge.doNotifyRoom(room, message, this.appId); diff --git a/packages/apps/src/server/accessors/OAuthAppsModify.ts b/packages/apps/src/server/accessors/OAuthAppsModify.ts index 7c78ea45e75c0..4649b047bea88 100644 --- a/packages/apps/src/server/accessors/OAuthAppsModify.ts +++ b/packages/apps/src/server/accessors/OAuthAppsModify.ts @@ -9,7 +9,7 @@ export class OAuthAppsModify implements IOAuthAppsModify { ) {} public async createOAuthApp(oAuthApp: IOAuthAppParams): Promise { - return this.oauthAppsBridge.doCreate(oAuthApp, this.appId); + return this.oauthAppsBridge.doCreate(oAuthApp, this.appId) as Promise; } public async updateOAuthApp(oAuthApp: IOAuthAppParams, id: string): Promise { diff --git a/packages/apps/src/server/accessors/OAuthAppsReader.ts b/packages/apps/src/server/accessors/OAuthAppsReader.ts index 18c375e168054..057c22377f1fa 100644 --- a/packages/apps/src/server/accessors/OAuthAppsReader.ts +++ b/packages/apps/src/server/accessors/OAuthAppsReader.ts @@ -9,10 +9,10 @@ export class OAuthAppsReader implements IOAuthAppsReader { ) {} public async getOAuthAppById(id: string): Promise { - return this.oauthAppsBridge.doGetByid(id, this.appId); + return this.oauthAppsBridge.doGetByid(id, this.appId) as Promise; } public async getOAuthAppByName(name: string): Promise> { - return this.oauthAppsBridge.doGetByName(name, this.appId); + return this.oauthAppsBridge.doGetByName(name, this.appId) as Promise>; } } diff --git a/packages/apps/src/server/accessors/Persistence.ts b/packages/apps/src/server/accessors/Persistence.ts index 6106cf16c7fb2..6eddbd04c9509 100644 --- a/packages/apps/src/server/accessors/Persistence.ts +++ b/packages/apps/src/server/accessors/Persistence.ts @@ -33,14 +33,14 @@ export class Persistence implements IPersistence { } public remove(id: string): Promise { - return this.persistBridge.doRemove(id, this.appId); + return this.persistBridge.doRemove(id, this.appId) as Promise; } public removeByAssociation(association: RocketChatAssociationRecord): Promise> { - return this.persistBridge.doRemoveByAssociations(new Array(association), this.appId); + return this.persistBridge.doRemoveByAssociations(new Array(association), this.appId) as Promise>; } public removeByAssociations(associations: Array): Promise> { - return this.persistBridge.doRemoveByAssociations(associations, this.appId); + return this.persistBridge.doRemoveByAssociations(associations, this.appId) as Promise>; } } diff --git a/packages/apps/src/server/accessors/RoomBuilder.ts b/packages/apps/src/server/accessors/RoomBuilder.ts index aa1e4c31d43b7..ba01f916db2cb 100644 --- a/packages/apps/src/server/accessors/RoomBuilder.ts +++ b/packages/apps/src/server/accessors/RoomBuilder.ts @@ -29,7 +29,7 @@ export class RoomBuilder implements IRoomBuilder { } public getDisplayName(): string { - return this.room.displayName; + return this.room.displayName as string; } public setSlugifiedName(name: string): IRoomBuilder { @@ -38,7 +38,7 @@ export class RoomBuilder implements IRoomBuilder { } public getSlugifiedName(): string { - return this.room.slugifiedName; + return this.room.slugifiedName as string; } public setType(type: RoomType): IRoomBuilder { @@ -106,7 +106,7 @@ export class RoomBuilder implements IRoomBuilder { } public getIsDefault(): boolean { - return this.room.isDefault; + return this.room.isDefault as boolean; } public setReadOnly(isReadOnly: boolean): IRoomBuilder { @@ -115,7 +115,7 @@ export class RoomBuilder implements IRoomBuilder { } public getIsReadOnly(): boolean { - return this.room.isReadOnly; + return this.room.isReadOnly as boolean; } public setDisplayingOfSystemMessages(displaySystemMessages: boolean): IRoomBuilder { @@ -124,7 +124,7 @@ export class RoomBuilder implements IRoomBuilder { } public getDisplayingOfSystemMessages(): boolean { - return this.room.displaySystemMessages; + return this.room.displaySystemMessages as boolean; } public addCustomField(key: string, value: object): IRoomBuilder { @@ -142,11 +142,11 @@ export class RoomBuilder implements IRoomBuilder { } public getCustomFields(): { [key: string]: object } { - return this.room.customFields; + return this.room.customFields as { [key: string]: object }; } public getUserIds(): Array { - return this.room.userIds; + return this.room.userIds as Array; } public getRoom(): IRoom { diff --git a/packages/apps/src/server/accessors/RoomRead.ts b/packages/apps/src/server/accessors/RoomRead.ts index 858a599befc2b..68f1dd28a78ad 100644 --- a/packages/apps/src/server/accessors/RoomRead.ts +++ b/packages/apps/src/server/accessors/RoomRead.ts @@ -16,7 +16,7 @@ export class RoomRead implements IRoomRead { } public getCreatorUserById(id: string): Promise { - return this.roomBridge.doGetCreatorById(id, this.appId); + return this.roomBridge.doGetCreatorById(id, this.appId) as Promise; } public getByName(name: string): Promise { @@ -24,7 +24,7 @@ export class RoomRead implements IRoomRead { } public getCreatorUserByName(name: string): Promise { - return this.roomBridge.doGetCreatorByName(name, this.appId); + return this.roomBridge.doGetCreatorByName(name, this.appId) as Promise; } public getMessages(roomId: string, options: Partial = {}): Promise { @@ -63,7 +63,7 @@ export class RoomRead implements IRoomRead { } public getDirectByUsernames(usernames: Array): Promise { - return this.roomBridge.doGetDirectByUsernames(usernames, this.appId); + return this.roomBridge.doGetDirectByUsernames(usernames, this.appId) as Promise; } public getModerators(roomId: string): Promise> { diff --git a/packages/apps/src/server/accessors/UIController.ts b/packages/apps/src/server/accessors/UIController.ts index 97d4f1d99b63d..cd38db999bf27 100644 --- a/packages/apps/src/server/accessors/UIController.ts +++ b/packages/apps/src/server/accessors/UIController.ts @@ -49,7 +49,7 @@ export class UIController implements IUIController { return this.openContextualBar(view, context, user, true); } - public openSurfaceView(view: IUIKitSurfaceViewParam, context: IUIKitInteractionParam, user: IUser) { + public openSurfaceView(view: IUIKitSurfaceViewParam, context: IUIKitInteractionParam, user: IUser): Promise { const blocks = UIHelper.assignIds(view.blocks, this.appId); const viewWithIds = { ...view, blocks }; @@ -58,10 +58,12 @@ export class UIController implements IUIController { return this.openContextualBar(viewWithIds, context, user); case UIKitSurfaceType.MODAL: return this.openModal(viewWithIds, context, user); + default: + return Promise.resolve(); } } - public updateSurfaceView(view: IUIKitSurfaceViewParam, context: IUIKitInteractionParam, user: IUser) { + public updateSurfaceView(view: IUIKitSurfaceViewParam, context: IUIKitInteractionParam, user: IUser): Promise { const blocks = UIHelper.assignIds(view.blocks, this.appId); const viewWithIds = { ...view, blocks }; @@ -70,6 +72,8 @@ export class UIController implements IUIController { return this.openContextualBar(viewWithIds, context, user, true); case UIKitSurfaceType.MODAL: return this.openModal(viewWithIds, context, user, true); + default: + return Promise.resolve(); } } diff --git a/packages/apps/src/server/accessors/UserBuilder.ts b/packages/apps/src/server/accessors/UserBuilder.ts index f1c891ec73bd6..bfadc9d22e2de 100644 --- a/packages/apps/src/server/accessors/UserBuilder.ts +++ b/packages/apps/src/server/accessors/UserBuilder.ts @@ -26,7 +26,7 @@ export class UserBuilder implements IUserBuilder { } public getEmails(): Array { - return this.user.emails; + return this.user.emails as Array; } public setDisplayName(name: string): IUserBuilder { @@ -35,7 +35,7 @@ export class UserBuilder implements IUserBuilder { } public getDisplayName(): string { - return this.user.name; + return this.user.name as string; } public setUsername(username: string): IUserBuilder { @@ -44,7 +44,7 @@ export class UserBuilder implements IUserBuilder { } public getUsername(): string { - return this.user.username; + return this.user.username as string; } public setRoles(roles: Array): IUserBuilder { @@ -53,11 +53,11 @@ export class UserBuilder implements IUserBuilder { } public getRoles(): Array { - return this.user.roles; + return this.user.roles as Array; } public getSettings(): Partial { - return this.user.settings; + return this.user.settings as Partial; } public getUser(): Partial { diff --git a/packages/apps/src/server/accessors/VideoConferenceBuilder.ts b/packages/apps/src/server/accessors/VideoConferenceBuilder.ts index a7c101b9851e7..1a5cce7fdd9da 100644 --- a/packages/apps/src/server/accessors/VideoConferenceBuilder.ts +++ b/packages/apps/src/server/accessors/VideoConferenceBuilder.ts @@ -13,19 +13,19 @@ export class VideoConferenceBuilder implements IVideoConferenceBuilder { public setData(data: Partial): IVideoConferenceBuilder { this.call = { - rid: data.rid, - createdBy: data.createdBy, - providerName: data.providerName, - title: data.title, + rid: data.rid as string, + createdBy: data.createdBy as string, + providerName: data.providerName as string, + title: data.title as string, discussionRid: data.discussionRid, }; - return this; + return this as IVideoConferenceBuilder; } public setRoomId(rid: string): IVideoConferenceBuilder { this.call.rid = rid; - return this; + return this as IVideoConferenceBuilder; } public getRoomId(): string { @@ -34,7 +34,7 @@ export class VideoConferenceBuilder implements IVideoConferenceBuilder { public setCreatedBy(userId: string): IVideoConferenceBuilder { this.call.createdBy = userId; - return this; + return this as IVideoConferenceBuilder; } public getCreatedBy(): string { @@ -43,25 +43,25 @@ export class VideoConferenceBuilder implements IVideoConferenceBuilder { public setProviderName(userId: string): IVideoConferenceBuilder { this.call.providerName = userId; - return this; + return this as IVideoConferenceBuilder; } public getProviderName(): string { return this.call.providerName; } - public setProviderData(data: Record | undefined): IVideoConferenceBuilder { + public setProviderData(data: Record): IVideoConferenceBuilder { this.call.providerData = data; - return this; + return this as IVideoConferenceBuilder; } - public getProviderData(): Record | undefined { - return this.call.providerData; + public getProviderData(): Record { + return this.call.providerData as Record; } public setTitle(userId: string): IVideoConferenceBuilder { this.call.title = userId; - return this; + return this as IVideoConferenceBuilder; } public getTitle(): string { @@ -70,7 +70,7 @@ export class VideoConferenceBuilder implements IVideoConferenceBuilder { public setDiscussionRid(rid: AppVideoConference['discussionRid']): IVideoConferenceBuilder { this.call.discussionRid = rid; - return this; + return this as IVideoConferenceBuilder; } public getDiscussionRid(): AppVideoConference['discussionRid'] { diff --git a/packages/apps/src/server/accessors/VideoConferenceExtend.ts b/packages/apps/src/server/accessors/VideoConferenceExtend.ts index 1a6a252b89e65..d8da5c1c20e9b 100644 --- a/packages/apps/src/server/accessors/VideoConferenceExtend.ts +++ b/packages/apps/src/server/accessors/VideoConferenceExtend.ts @@ -43,7 +43,7 @@ export class VideoConferenceExtender implements IVideoConferenceExtender { public addUser(userId: VideoConferenceMember['_id'], ts?: VideoConferenceMember['ts']): IVideoConferenceExtender { this.videoConference.users.push({ _id: userId, - ts, + ts: ts as Date, // Name and username will be loaded automatically by the bridge username: '', name: '', diff --git a/packages/apps/src/server/bridges/CloudWorkspaceBridge.ts b/packages/apps/src/server/bridges/CloudWorkspaceBridge.ts index abccdea2bc3bf..85f722fa6c0e6 100644 --- a/packages/apps/src/server/bridges/CloudWorkspaceBridge.ts +++ b/packages/apps/src/server/bridges/CloudWorkspaceBridge.ts @@ -9,6 +9,8 @@ export abstract class CloudWorkspaceBridge extends BaseBridge { if (this.hasCloudTokenPermission(appId)) { return this.getWorkspaceToken(scope, appId); } + + return undefined as unknown as Promise; } protected abstract getWorkspaceToken(scope: string, appId: string): Promise; diff --git a/packages/apps/src/server/bridges/CommandBridge.ts b/packages/apps/src/server/bridges/CommandBridge.ts index 2a7c2d78197c0..83f4452fc2a57 100644 --- a/packages/apps/src/server/bridges/CommandBridge.ts +++ b/packages/apps/src/server/bridges/CommandBridge.ts @@ -9,6 +9,8 @@ export abstract class CommandBridge extends BaseBridge { if (this.hasDefaultPermission(appId)) { return this.doesCommandExist(command, appId); } + + return undefined as unknown as boolean; } public async doEnableCommand(command: string, appId: string): Promise { diff --git a/packages/apps/src/server/bridges/ContactBridge.ts b/packages/apps/src/server/bridges/ContactBridge.ts index dd45d67f4afec..090069e4f7750 100644 --- a/packages/apps/src/server/bridges/ContactBridge.ts +++ b/packages/apps/src/server/bridges/ContactBridge.ts @@ -17,6 +17,8 @@ export abstract class ContactBridge extends BaseBridge { if (this.hasReadPermission(appId)) { return this.getById(contactId, appId); } + + return undefined; } public async doVerifyContact(verifyContactChannelParams: VerifyContactChannelParams, appId: string): Promise { @@ -29,6 +31,8 @@ export abstract class ContactBridge extends BaseBridge { if (this.hasWritePermission(appId)) { return this.addContactEmail(contactId, email, appId); } + + return undefined as unknown as ILivechatContact; } protected abstract getById(contactId: ILivechatContact['_id'], appId: string): Promise; diff --git a/packages/apps/src/server/bridges/EnvironmentalVariableBridge.ts b/packages/apps/src/server/bridges/EnvironmentalVariableBridge.ts index 79b29bf316c41..73fdc44fafa42 100644 --- a/packages/apps/src/server/bridges/EnvironmentalVariableBridge.ts +++ b/packages/apps/src/server/bridges/EnvironmentalVariableBridge.ts @@ -8,18 +8,24 @@ export abstract class EnvironmentalVariableBridge extends BaseBridge { if (this.hasReadPermission(appId)) { return this.getValueByName(envVarName, appId); } + + return undefined; } public async doIsReadable(envVarName: string, appId: string): Promise { if (this.hasReadPermission(appId)) { return this.isReadable(envVarName, appId); } + + return undefined as unknown as boolean; } public async doIsSet(envVarName: string, appId: string): Promise { if (this.hasReadPermission(appId)) { return this.isSet(envVarName, appId); } + + return undefined as unknown as boolean; } protected abstract getValueByName(envVarName: string, appId: string): Promise; diff --git a/packages/apps/src/server/bridges/HttpBridge.ts b/packages/apps/src/server/bridges/HttpBridge.ts index 20d25e50d7063..5a3ddb205cc90 100644 --- a/packages/apps/src/server/bridges/HttpBridge.ts +++ b/packages/apps/src/server/bridges/HttpBridge.ts @@ -16,6 +16,8 @@ export abstract class HttpBridge extends BaseBridge { if (this.hasDefaultPermission(info.appId)) { return this.call(info); } + + return undefined as unknown as IHttpResponse; } protected abstract call(info: IHttpBridgeRequestInfo): Promise; diff --git a/packages/apps/src/server/bridges/LivechatBridge.ts b/packages/apps/src/server/bridges/LivechatBridge.ts index 37d7b7623df8a..bc8d479b3e7b7 100644 --- a/packages/apps/src/server/bridges/LivechatBridge.ts +++ b/packages/apps/src/server/bridges/LivechatBridge.ts @@ -29,27 +29,35 @@ type LivechatMultiplePermissions = keyof Pick { - if (this.hasReadPermission(appId, 'livechat-status')) { + if (this.hasReadPermission(appId!, 'livechat-status')) { return this.isOnlineAsync(departmentId, appId); } + + return undefined as unknown as boolean; } public async doCreateMessage(message: ILivechatMessage, appId: string): Promise { if (this.hasWritePermission(appId, 'livechat-message')) { return this.createMessage(message, appId); } + + return undefined as unknown as string; } public async doGetMessageById(messageId: string, appId: string): Promise { if (this.hasReadPermission(appId, 'livechat-message')) { return this.getMessageById(messageId, appId); } + + return undefined as unknown as ILivechatMessage; } public async doUpdateMessage(message: ILivechatMessage, appId: string): Promise { @@ -65,42 +73,56 @@ export abstract class LivechatBridge extends BaseBridge { if (this.hasWritePermission(appId, 'livechat-visitor')) { return this.createVisitor(visitor, appId); } + + return undefined as unknown as string; } public async doCreateAndReturnVisitor(visitor: IVisitor, appId: string): Promise { if (this.hasWritePermission(appId, 'livechat-visitor')) { return this.createAndReturnVisitor(visitor, appId); } + + return undefined; } public async doFindVisitors(query: object, appId: string): Promise> { if (this.hasReadPermission(appId, 'livechat-visitor')) { return this.findVisitors(query, appId); } + + return undefined as unknown as Array; } public async doFindVisitorById(id: string, appId: string): Promise { if (this.hasReadPermission(appId, 'livechat-visitor')) { return this.findVisitorById(id, appId); } + + return undefined; } public async doFindVisitorByEmail(email: string, appId: string): Promise { if (this.hasReadPermission(appId, 'livechat-visitor')) { return this.findVisitorByEmail(email, appId); } + + return undefined; } public async doFindVisitorByToken(token: string, appId: string): Promise { if (this.hasReadPermission(appId, 'livechat-visitor')) { return this.findVisitorByToken(token, appId); } + + return undefined; } public async doFindVisitorByPhoneNumber(phoneNumber: string, appId: string): Promise { if (this.hasReadPermission(appId, 'livechat-visitor')) { return this.findVisitorByPhoneNumber(phoneNumber, appId); } + + return undefined; } public async doResolveVisitor( @@ -111,12 +133,16 @@ export abstract class LivechatBridge extends BaseBridge { if (this.hasWritePermission(appId, 'livechat-visitor')) { return this.resolveVisitor(externalId, contactData, appId); } + + return undefined; } public async doTransferVisitor(visitor: IVisitor, transferData: ILivechatTransferData, appId: string): Promise { if (this.hasWritePermission(appId, 'livechat-visitor')) { return this.transferVisitor(visitor, transferData, appId); } + + return undefined as unknown as boolean; } public async doUpdateVisitorExternalId( @@ -127,54 +153,72 @@ export abstract class LivechatBridge extends BaseBridge { if (this.hasWritePermission(appId, 'livechat-visitor')) { return this.updateVisitorExternalId(visitorId, externalId, appId); } + + return undefined; } public async doCreateRoom(visitor: IVisitor, agent: IUser, appId: string, extraParams?: IExtraRoomParams): Promise { if (this.hasWritePermission(appId, 'livechat-room')) { return this.createRoom(visitor, agent, appId, extraParams); } + + return undefined as unknown as ILivechatRoom; } public async doCloseRoom(room: ILivechatRoom, comment: string, closer: IUser | undefined, appId: string): Promise { if (this.hasWritePermission(appId, 'livechat-room')) { return this.closeRoom(room, comment, closer, appId); } + + return undefined as unknown as boolean; } public async doCountOpenRoomsByAgentId(agentId: string, appId: string): Promise { if (this.hasReadPermission(appId, 'livechat-room')) { return this.countOpenRoomsByAgentId(agentId, appId); } + + return undefined as unknown as number; } public async doFindOpenRoomsByAgentId(agentId: string, appId: string): Promise> { if (this.hasReadPermission(appId, 'livechat-room')) { return this.findOpenRoomsByAgentId(agentId, appId); } + + return undefined as unknown as Array; } public async doFindRooms(visitor: IVisitor, departmentId: string | null, appId: string): Promise> { if (this.hasReadPermission(appId, 'livechat-room')) { return this.findRooms(visitor, departmentId, appId); } + + return undefined as unknown as Array; } public async doFindDepartmentByIdOrName(value: string, appId: string): Promise { if (this.hasReadPermission(appId, 'livechat-department') || this.hasMultiplePermission(appId, 'livechat-department')) { return this.findDepartmentByIdOrName(value, appId); } + + return undefined; } public async doFindDepartmentsEnabledWithAgents(appId: string): Promise> { if (this.hasMultiplePermission(appId, 'livechat-department')) { return this.findDepartmentsEnabledWithAgents(appId); } + + return undefined as unknown as Array; } public async do_fetchLivechatRoomMessages(appId: string, roomId: string): Promise> { if (this.hasMultiplePermission(appId, 'livechat-message')) { return this._fetchLivechatRoomMessages(appId, roomId); } + + return undefined as unknown as Array; } public async doSetCustomFields( @@ -184,6 +228,8 @@ export abstract class LivechatBridge extends BaseBridge { if (this.hasWritePermission(appId, 'livechat-custom-fields')) { return this.setCustomFields(data, appId); } + + return undefined as unknown as number; } /** diff --git a/packages/apps/src/server/bridges/MessageBridge.ts b/packages/apps/src/server/bridges/MessageBridge.ts index 9d714f1339bcd..ae0a788c16193 100644 --- a/packages/apps/src/server/bridges/MessageBridge.ts +++ b/packages/apps/src/server/bridges/MessageBridge.ts @@ -16,6 +16,8 @@ export abstract class MessageBridge extends BaseBridge { if (this.hasWritePermission(appId)) { return this.create(message, appId); } + + return undefined as unknown as string; } public async doUpdate(message: IMessage, appId: string): Promise { @@ -46,6 +48,8 @@ export abstract class MessageBridge extends BaseBridge { if (this.hasReadPermission(appId)) { return this.getById(messageId, appId); } + + return undefined as unknown as IMessage; } public async doDelete(message: IMessage, user: IUser, appId: string): Promise { diff --git a/packages/apps/src/server/bridges/ModerationBridge.ts b/packages/apps/src/server/bridges/ModerationBridge.ts index 1ad81ece722cf..1685fba7d1992 100644 --- a/packages/apps/src/server/bridges/ModerationBridge.ts +++ b/packages/apps/src/server/bridges/ModerationBridge.ts @@ -8,13 +8,13 @@ import { AppPermissions } from '../permissions/AppPermissions'; export abstract class ModerationBridge extends BaseBridge { public async doReport(messageId: IMessage['id'], description: string, userId: string, appId: string): Promise { if (this.hasWritePermission(appId)) { - return this.report(messageId, description, userId, appId); + return this.report(messageId!, description, userId, appId); } } public async doDismissReportsByMessageId(messageId: IMessage['id'], reason: string, action: string, appId: string): Promise { if (this.hasWritePermission(appId)) { - return this.dismissReportsByMessageId(messageId, reason, action, appId); + return this.dismissReportsByMessageId(messageId!, reason, action, appId); } } diff --git a/packages/apps/src/server/bridges/OAuthAppsBridge.ts b/packages/apps/src/server/bridges/OAuthAppsBridge.ts index d8b193b6f649b..214f08a0f1865 100644 --- a/packages/apps/src/server/bridges/OAuthAppsBridge.ts +++ b/packages/apps/src/server/bridges/OAuthAppsBridge.ts @@ -5,22 +5,28 @@ import { AppPermissionManager } from '../managers/AppPermissionManager'; import { AppPermissions } from '../permissions/AppPermissions'; export abstract class OAuthAppsBridge extends BaseBridge { - public async doCreate(oAuthApp: IOAuthAppParams, appId: string) { + public async doCreate(oAuthApp: IOAuthAppParams, appId: string): Promise { if (this.hasWritePermission(appId)) { return this.create(oAuthApp, appId); } + + return undefined; } - public async doGetByid(id: string, appId: string) { + public async doGetByid(id: string, appId: string): Promise { if (this.hasReadPermission(appId)) { return this.getById(id, appId); } + + return undefined; } - public async doGetByName(name: string, appId: string) { + public async doGetByName(name: string, appId: string): Promise | undefined> { if (this.hasReadPermission(appId)) { return this.getByName(name, appId); } + + return undefined; } public async doUpdate(oAuthApp: IOAuthAppParams, id: string, appId: string) { diff --git a/packages/apps/src/server/bridges/PersistenceBridge.ts b/packages/apps/src/server/bridges/PersistenceBridge.ts index 7380cfdac46ba..1dc64023b95e1 100644 --- a/packages/apps/src/server/bridges/PersistenceBridge.ts +++ b/packages/apps/src/server/bridges/PersistenceBridge.ts @@ -15,42 +15,56 @@ export abstract class PersistenceBridge extends BaseBridge { if (this.hasDefaultPermission(appId)) { return this.create(data, appId); } + + return undefined as unknown as string; } public async doCreateWithAssociations(data: object, associations: Array, appId: string): Promise { if (this.hasDefaultPermission(appId)) { return this.createWithAssociations(data, associations, appId); } + + return undefined as unknown as string; } public async doReadById(id: string, appId: string): Promise { if (this.hasDefaultPermission(appId)) { return this.readById(id, appId); } + + return undefined as unknown as object; } public async doReadByAssociations(associations: Array, appId: string): Promise> { if (this.hasDefaultPermission(appId)) { return this.readByAssociations(associations, appId); } + + return undefined as unknown as Array; } public async doRemove(id: string, appId: string): Promise { if (this.hasDefaultPermission(appId)) { return this.remove(id, appId); } + + return undefined; } public async doRemoveByAssociations(associations: Array, appId: string): Promise | undefined> { if (this.hasDefaultPermission(appId)) { return this.removeByAssociations(associations, appId); } + + return undefined; } public async doUpdate(id: string, data: object, upsert: boolean, appId: string): Promise { if (this.hasDefaultPermission(appId)) { return this.update(id, data, upsert, appId); } + + return undefined as unknown as string; } public async doUpdateByAssociations( @@ -62,6 +76,8 @@ export abstract class PersistenceBridge extends BaseBridge { if (this.hasDefaultPermission(appId)) { return this.updateByAssociations(associations, data, upsert, appId); } + + return undefined as unknown as string; } /** diff --git a/packages/apps/src/server/bridges/RoleBridge.ts b/packages/apps/src/server/bridges/RoleBridge.ts index 14fcbe2a1032d..c8e0f9699e022 100644 --- a/packages/apps/src/server/bridges/RoleBridge.ts +++ b/packages/apps/src/server/bridges/RoleBridge.ts @@ -9,12 +9,16 @@ export abstract class RoleBridge extends BaseBridge { if (this.hasReadPermission(appId)) { return this.getOneByIdOrName(idOrName, appId); } + + return undefined as unknown as IRole | null; } public async doGetCustomRoles(appId: string): Promise> { if (this.hasReadPermission(appId)) { return this.getCustomRoles(appId); } + + return undefined as unknown as Array; } protected abstract getOneByIdOrName(idOrName: IRole['id'] | IRole['name'], appId: string): Promise; diff --git a/packages/apps/src/server/bridges/RoomBridge.ts b/packages/apps/src/server/bridges/RoomBridge.ts index 00bd2b942a58a..16a9e6859e687 100644 --- a/packages/apps/src/server/bridges/RoomBridge.ts +++ b/packages/apps/src/server/bridges/RoomBridge.ts @@ -15,48 +15,64 @@ export abstract class RoomBridge extends BaseBridge { if (this.hasWritePermission(appId)) { return this.create(room, members, appId); } + + return undefined as unknown as string; } public async doGetById(roomId: string, appId: string): Promise { if (this.hasReadPermission(appId)) { return this.getById(roomId, appId); } + + return undefined as unknown as IRoom; } public async doGetByName(roomName: string, appId: string): Promise { if (this.hasReadPermission(appId)) { return this.getByName(roomName, appId); } + + return undefined as unknown as IRoom; } public async doGetCreatorById(roomId: string, appId: string): Promise { if (this.hasReadPermission(appId)) { return this.getCreatorById(roomId, appId); } + + return undefined; } public async doGetCreatorByName(roomName: string, appId: string): Promise { if (this.hasReadPermission(appId)) { return this.getCreatorByName(roomName, appId); } + + return undefined; } public async doGetDirectByUsernames(usernames: Array, appId: string): Promise { if (this.hasReadPermission(appId)) { return this.getDirectByUsernames(usernames, appId); } + + return undefined; } public async doGetMembers(roomId: string, appId: string): Promise> { if (this.hasReadPermission(appId)) { return this.getMembers(roomId, appId); } + + return undefined as unknown as Array; } public async doGetAllRooms(filters: GetRoomsFilters = {}, options: GetRoomsOptions = {}, appId: string): Promise | undefined> { if (this.hasViewAllRoomsPermission(appId)) { return this.getAllRooms(filters, options, appId); } + + return undefined; } public async doUpdate(room: IRoom, members: Array, appId: string): Promise { @@ -75,6 +91,8 @@ export abstract class RoomBridge extends BaseBridge { if (this.hasWritePermission(appId)) { return this.createDiscussion(room, parentMessage, reply, members, appId); } + + return undefined as unknown as string; } public async doDelete(room: string, appId: string): Promise { @@ -87,24 +105,32 @@ export abstract class RoomBridge extends BaseBridge { if (this.hasReadPermission(appId)) { return this.getModerators(roomId, appId); } + + return undefined as unknown as Array; } public async doGetOwners(roomId: string, appId: string): Promise> { if (this.hasReadPermission(appId)) { return this.getOwners(roomId, appId); } + + return undefined as unknown as Array; } public async doGetLeaders(roomId: string, appId: string): Promise> { if (this.hasReadPermission(appId)) { return this.getLeaders(roomId, appId); } + + return undefined as unknown as Array; } public async doGetMessages(roomId: string, options: GetMessagesOptions, appId: string): Promise { if (this.hasReadPermission(appId)) { return this.getMessages(roomId, options, appId); } + + return undefined as unknown as IMessageRaw[]; } public async doRemoveUsers(roomId: string, usernames: Array, appId: string): Promise { @@ -117,12 +143,16 @@ export abstract class RoomBridge extends BaseBridge { if (this.hasReadPermission(appId)) { return this.getUnreadByUser(roomId, uid, options, appId); } + + return undefined as unknown as IMessageRaw[]; } public async doGetUserUnreadMessageCount(roomId: string, uid: string, appId: string): Promise { if (this.hasReadPermission(appId)) { return this.getUserUnreadMessageCount(roomId, uid, appId); } + + return undefined as unknown as number; } protected abstract create(room: IRoom, members: Array, appId: string): Promise; diff --git a/packages/apps/src/server/bridges/ServerSettingBridge.ts b/packages/apps/src/server/bridges/ServerSettingBridge.ts index 0508937371206..97dfedbb5524f 100644 --- a/packages/apps/src/server/bridges/ServerSettingBridge.ts +++ b/packages/apps/src/server/bridges/ServerSettingBridge.ts @@ -9,12 +9,16 @@ export abstract class ServerSettingBridge extends BaseBridge { if (this.hasReadPermission(appId)) { return this.getAll(appId); } + + return undefined as unknown as Array; } public async doGetOneById(id: string, appId: string): Promise { if (this.hasReadPermission(appId)) { return this.getOneById(id, appId); } + + return undefined as unknown as ISetting; } public async doHideGroup(name: string, appId: string): Promise { @@ -33,6 +37,8 @@ export abstract class ServerSettingBridge extends BaseBridge { if (this.hasReadPermission(appId)) { return this.isReadableById(id, appId); } + + return undefined as unknown as boolean; } public async doUpdateOne(setting: ISetting, appId: string): Promise { diff --git a/packages/apps/src/server/bridges/ThreadBridge.ts b/packages/apps/src/server/bridges/ThreadBridge.ts index da812fc01bbd8..48b32f99919e8 100644 --- a/packages/apps/src/server/bridges/ThreadBridge.ts +++ b/packages/apps/src/server/bridges/ThreadBridge.ts @@ -14,6 +14,8 @@ export abstract class ThreadBridge extends BaseBridge { if (this.hasReadPermission(appId)) { return this.getById(messageId, appId); } + + return undefined as unknown as Array; } protected abstract getById(messageId: string, appId: string): Promise>; diff --git a/packages/apps/src/server/bridges/UploadBridge.ts b/packages/apps/src/server/bridges/UploadBridge.ts index 68806a2c5b121..fbda9c0f34a38 100644 --- a/packages/apps/src/server/bridges/UploadBridge.ts +++ b/packages/apps/src/server/bridges/UploadBridge.ts @@ -10,18 +10,24 @@ export abstract class UploadBridge extends BaseBridge { if (this.hasReadPermission(appId)) { return this.getById(id, appId); } + + return undefined as unknown as IUpload; } public async doGetBuffer(upload: IUpload, appId: string): Promise { if (this.hasReadPermission(appId)) { return this.getBuffer(upload, appId); } + + return undefined as unknown as Buffer; } public async doCreateUpload(details: IUploadDetails, buffer: Buffer, appId: string): Promise { if (this.hasWritePermission(appId)) { return this.createUpload(details, buffer, appId); } + + return undefined as unknown as IUpload; } protected abstract getById(id: string, appId: string): Promise; diff --git a/packages/apps/src/server/bridges/UserBridge.ts b/packages/apps/src/server/bridges/UserBridge.ts index 0d31c556da382..8844b77ad210d 100644 --- a/packages/apps/src/server/bridges/UserBridge.ts +++ b/packages/apps/src/server/bridges/UserBridge.ts @@ -9,12 +9,16 @@ export abstract class UserBridge extends BaseBridge { if (this.hasReadPermission(appId)) { return this.getById(id, appId); } + + return undefined as unknown as IUser; } public async doGetByUsername(username: string, appId: string): Promise { if (this.hasReadPermission(appId)) { return this.getByUsername(username, appId); } + + return undefined as unknown as IUser; } public async doGetAppUser(appId?: string): Promise { @@ -25,42 +29,56 @@ export abstract class UserBridge extends BaseBridge { if (this.hasWritePermission(appId)) { return this.create(data, appId, options || {}); } + + return undefined as unknown as string; } public async doRemove(user: IUser, appId: string): Promise { if (this.hasWritePermission(appId)) { return this.remove(user, appId); } + + return undefined as unknown as boolean; } public async doUpdate(user: IUser, updates: Partial, appId: string): Promise { if (this.hasWritePermission(appId)) { return this.update(user, updates, appId); } + + return undefined as unknown as boolean; } public async doGetUserUnreadMessageCount(uid: string, appId: string): Promise { if (this.hasReadPermission(appId)) { return this.getUserUnreadMessageCount(uid, appId); } + + return undefined as unknown as number; } public async doGetUserRoomIds(userId: string, appId: string): Promise { if (this.hasReadPermission(appId)) { return this.getUserRoomIds(userId, appId); } + + return undefined as unknown as string[]; } public async doDeleteUsersCreatedByApp(appId: string, type: UserType.BOT | UserType.APP): Promise { if (this.hasWritePermission(appId)) { return this.deleteUsersCreatedByApp(appId, type); } + + return undefined as unknown as boolean; } public async doDeactivate(userId: IUser['id'], confirmRelinquish: boolean, appId: string): Promise { if (this.hasWritePermission(appId)) { return this.deactivate(userId, confirmRelinquish, appId); } + + return undefined as unknown as boolean; } protected abstract getById(id: string, appId: string): Promise; diff --git a/packages/apps/src/server/bridges/VideoConferenceBridge.ts b/packages/apps/src/server/bridges/VideoConferenceBridge.ts index d65a5e664980e..f4f1d8446aa0b 100644 --- a/packages/apps/src/server/bridges/VideoConferenceBridge.ts +++ b/packages/apps/src/server/bridges/VideoConferenceBridge.ts @@ -11,12 +11,16 @@ export abstract class VideoConferenceBridge extends BaseBridge { if (this.hasReadPermission(appId)) { return this.getById(callId, appId); } + + return undefined as unknown as VideoConference; } public async doCreate(call: AppVideoConference, appId: string): Promise { if (this.hasWritePermission(appId)) { return this.create(call, appId); } + + return undefined as unknown as string; } public async doUpdate(call: VideoConference, appId: string): Promise { diff --git a/packages/apps/src/server/compiler/AppFabricationFulfillment.ts b/packages/apps/src/server/compiler/AppFabricationFulfillment.ts index bc528080bf6df..4b579ba1e7b89 100644 --- a/packages/apps/src/server/compiler/AppFabricationFulfillment.ts +++ b/packages/apps/src/server/compiler/AppFabricationFulfillment.ts @@ -3,17 +3,17 @@ import type { ProxiedApp } from '../ProxiedApp'; import { AppLicenseValidationResult } from '../marketplace/license'; export class AppFabricationFulfillment { - public info: IAppInfo; + public info!: IAppInfo; - public app: ProxiedApp; + public app!: ProxiedApp; - public implemented: { [int: string]: boolean }; + public implemented!: { [int: string]: boolean }; public licenseValidationResult: AppLicenseValidationResult; - public storageError: string; + public storageError!: string; - public appUserError: object; + public appUserError!: object; constructor() { this.licenseValidationResult = new AppLicenseValidationResult(); diff --git a/packages/apps/src/server/compiler/AppImplements.ts b/packages/apps/src/server/compiler/AppImplements.ts index 5b13e648c3269..83c455908be45 100644 --- a/packages/apps/src/server/compiler/AppImplements.ts +++ b/packages/apps/src/server/compiler/AppImplements.ts @@ -7,8 +7,8 @@ export class AppImplements { constructor() { this.implemented = {} as Record; - Object.keys(AppInterface).forEach((int: AppInterface) => { - this.implemented[int] = false; + Object.keys(AppInterface).forEach((int: string) => { + this.implemented[int as AppInterface] = false; }); } diff --git a/packages/apps/src/server/compiler/AppPackageParser.ts b/packages/apps/src/server/compiler/AppPackageParser.ts index fdd01980d5e63..3a69512e5a70c 100644 --- a/packages/apps/src/server/compiler/AppPackageParser.ts +++ b/packages/apps/src/server/compiler/AppPackageParser.ts @@ -1,6 +1,6 @@ import * as path from 'path'; -import * as AdmZip from 'adm-zip'; +import AdmZip from 'adm-zip'; import * as semver from 'semver'; import { v4 as uuidv4 } from 'uuid'; @@ -53,8 +53,8 @@ export class AppPackageParser { zip .getEntries() - .filter((entry) => !entry.isDirectory && entry.entryName.endsWith('.js')) - .forEach((entry) => { + .filter((entry: AdmZip.IZipEntry) => !entry.isDirectory && entry.entryName.endsWith('.js')) + .forEach((entry: AdmZip.IZipEntry) => { const norm = path.normalize(entry.entryName); // Files which start with `.` are supposed to be hidden @@ -117,22 +117,22 @@ export class AppPackageParser { private getIconFile(zip: AdmZip, filePath: string): string { if (!filePath) { - return undefined; + return undefined as unknown as string; } const ext = path.extname(filePath); if (!this.allowedIconExts.includes(ext)) { - return undefined; + return undefined as unknown as string; } const entry = zip.getEntry(filePath); if (!entry) { - return undefined; + return undefined as unknown as string; } if (entry.isDirectory) { - return undefined; + return undefined as unknown as string; } const base64 = entry.getData().toString('base64'); diff --git a/packages/apps/src/server/managers/AppAccessorManager.ts b/packages/apps/src/server/managers/AppAccessorManager.ts index b93d5d6043053..1b1ed0eedfd63 100644 --- a/packages/apps/src/server/managers/AppAccessorManager.ts +++ b/packages/apps/src/server/managers/AppAccessorManager.ts @@ -121,7 +121,7 @@ export class AppAccessorManager { this.configExtenders.set(appId, new ConfigurationExtend(htt, sets, cmds, apis, excs, scheduler, ui, videoConf, outboundComms)); } - return this.configExtenders.get(appId); + return this.configExtenders.get(appId)!; } public getEnvironmentRead(appId: string): IEnvironmentRead { @@ -139,7 +139,7 @@ export class AppAccessorManager { this.envReaders.set(appId, new EnvironmentRead(sets, servsets, env)); } - return this.envReaders.get(appId); + return this.envReaders.get(appId)!; } public getEnvironmentWrite(appId: string): IEnvironmentWrite { @@ -156,7 +156,7 @@ export class AppAccessorManager { this.envWriters.set(appId, new EnvironmentWrite(sets, serverSetting)); } - return this.envWriters.get(appId); + return this.envWriters.get(appId)!; } public getConfigurationModify(appId: string): IConfigurationModify { @@ -171,7 +171,7 @@ export class AppAccessorManager { ); } - return this.configModifiers.get(appId); + return this.configModifiers.get(appId)!; } public getReader(appId: string): IRead { @@ -214,7 +214,7 @@ export class AppAccessorManager { ); } - return this.readers.get(appId); + return this.readers.get(appId)!; } public getModifier(appId: string): IModify { @@ -222,7 +222,7 @@ export class AppAccessorManager { this.modifiers.set(appId, new Modify(this.bridges, appId)); } - return this.modifiers.get(appId); + return this.modifiers.get(appId)!; } public getPersistence(appId: string): IPersistence { @@ -230,14 +230,14 @@ export class AppAccessorManager { this.persists.set(appId, new Persistence(this.bridges.getPersistenceBridge(), appId)); } - return this.persists.get(appId); + return this.persists.get(appId)!; } public getHttp(appId: string): IHttp { if (!this.https.has(appId)) { let ext: IHttpExtend; if (this.configExtenders.has(appId)) { - ext = this.configExtenders.get(appId).http; + ext = this.configExtenders.get(appId)!.http; } else { const cf = this.getConfigurationExtend(appId); ext = cf.http; @@ -246,6 +246,6 @@ export class AppAccessorManager { this.https.set(appId, new Http(this, this.bridges, ext, appId)); } - return this.https.get(appId); + return this.https.get(appId)!; } } diff --git a/packages/apps/src/server/managers/AppApi.ts b/packages/apps/src/server/managers/AppApi.ts index 826f8023ff590..d90ab628c3e03 100644 --- a/packages/apps/src/server/managers/AppApi.ts +++ b/packages/apps/src/server/managers/AppApi.ts @@ -37,10 +37,10 @@ export class AppApi { this.computedPath = `${this.basePath}/${endpoint.path}`; - this.implementedMethods = endpoint._availableMethods; + this.implementedMethods = endpoint._availableMethods ?? []; } - public async runExecutor(request: IApiRequest, logStorage: AppLogStorage, accessors: AppAccessorManager): Promise { + public async runExecutor(request: IApiRequest, _logStorage: AppLogStorage, _accessors: AppAccessorManager): Promise { const { path } = this.endpoint; const { method } = request; @@ -89,7 +89,7 @@ export class AppApi { return false; } - private validateSecurity(request: IApiRequest): boolean { + private validateSecurity(_request: IApiRequest): boolean { if (this.api.security === ApiSecurity.UNSECURE) { return true; } diff --git a/packages/apps/src/server/managers/AppApiManager.ts b/packages/apps/src/server/managers/AppApiManager.ts index 2a742faedbf8c..f1298f2013e7d 100644 --- a/packages/apps/src/server/managers/AppApiManager.ts +++ b/packages/apps/src/server/managers/AppApiManager.ts @@ -51,7 +51,7 @@ export class AppApiManager { // Verify the api's path doesn't exist already if (this.providedApis.get(appId)) { api.endpoints.forEach((endpoint) => { - if (this.providedApis.get(appId).has(endpoint.path)) { + if (this.providedApis.get(appId)!.has(endpoint.path)) { throw new PathAlreadyExistsError(endpoint.path); } }); @@ -62,7 +62,7 @@ export class AppApiManager { } api.endpoints.forEach((endpoint) => { - this.providedApis.get(appId).set(endpoint.path, new AppApi(app, api, endpoint)); + this.providedApis.get(appId)!.set(endpoint.path, new AppApi(app, api, endpoint)); }); } @@ -78,7 +78,7 @@ export class AppApiManager { } await this.bridge.doUnregisterApis(appId); - for await (const [, apiApp] of this.providedApis.get(appId).entries()) { + for await (const [, apiApp] of this.providedApis.get(appId)!.entries()) { await this.registerApi(appId, apiApp); } } @@ -104,7 +104,7 @@ export class AppApiManager { * @param request the request data to be evaluated byt the app */ public async executeApi(appId: string, path: string, request: IApiRequest): Promise { - const api = this.providedApis.get(appId).get(path); + const api = this.providedApis.get(appId)!.get(path); if (!api) { return { diff --git a/packages/apps/src/server/managers/AppExternalComponentManager.ts b/packages/apps/src/server/managers/AppExternalComponentManager.ts index a03c984173863..0c34e42648610 100644 --- a/packages/apps/src/server/managers/AppExternalComponentManager.ts +++ b/packages/apps/src/server/managers/AppExternalComponentManager.ts @@ -50,9 +50,9 @@ export class AppExternalComponentManager { * * @param appId the id of the app */ - public getExternalComponents(appId: string): Map { + public getExternalComponents(appId: string): Map | null { if (this.appTouchedExternalComponents.has(appId)) { - return this.appTouchedExternalComponents.get(appId); + return this.appTouchedExternalComponents.get(appId)!; } return null; @@ -89,7 +89,7 @@ export class AppExternalComponentManager { if (!this.appTouchedExternalComponents.get(appId)) { this.appTouchedExternalComponents.set(appId, new Map(Object.entries({ [externalComponent.name]: externalComponent }))); } else { - const appExternalComponents = this.appTouchedExternalComponents.get(appId); + const appExternalComponents = this.appTouchedExternalComponents.get(appId)!; appExternalComponents.set(externalComponent.name, externalComponent); } @@ -105,7 +105,7 @@ export class AppExternalComponentManager { if (!this.appTouchedExternalComponents.has(appId)) { return; } - const externalComponents = this.appTouchedExternalComponents.get(appId); + const externalComponents = this.appTouchedExternalComponents.get(appId)!; if (externalComponents.size > 0) { this.registeredExternalComponents.set(appId, externalComponents); diff --git a/packages/apps/src/server/managers/AppLicenseManager.ts b/packages/apps/src/server/managers/AppLicenseManager.ts index b72de6a5fd69a..65dd983b184bc 100644 --- a/packages/apps/src/server/managers/AppLicenseManager.ts +++ b/packages/apps/src/server/managers/AppLicenseManager.ts @@ -29,7 +29,7 @@ export class AppLicenseManager { validationResult.setValidated(true); - const encryptedLicense = marketplaceInfo.subscriptionInfo.license.license; + const encryptedLicense = marketplaceInfo.subscriptionInfo!.license.license; if (!encryptedLicense) { validationResult.addError('license', 'License for app is invalid'); @@ -41,7 +41,7 @@ export class AppLicenseManager { try { license = (await this.crypto.decryptLicense(encryptedLicense)) as any; } catch (err) { - validationResult.addError('publicKey', err.message); + validationResult.addError('publicKey', (err as Error).message); throw new InvalidLicenseError(validationResult); } diff --git a/packages/apps/src/server/managers/AppListenerManager.ts b/packages/apps/src/server/managers/AppListenerManager.ts index 46d8651c85b3a..198801d2fe286 100644 --- a/packages/apps/src/server/managers/AppListenerManager.ts +++ b/packages/apps/src/server/managers/AppListenerManager.ts @@ -278,7 +278,7 @@ export class AppListenerManager { return; } - this.listeners.get(event).push(app.getID()); + this.listeners.get(event)!.push(app.getID()); }); } @@ -286,7 +286,7 @@ export class AppListenerManager { this.listeners.forEach((apps, int) => { if (apps.includes(app.getID())) { const where = apps.indexOf(app.getID()); - this.listeners.get(int).splice(where, 1); + this.listeners.get(int)!.splice(where, 1); } }); } @@ -296,7 +296,7 @@ export class AppListenerManager { return; } - app.getEssentials().forEach((event) => { + app.getEssentials()!.forEach((event) => { const lockedEvent = this.lockedEvents.get(event); if (!lockedEvent) { @@ -312,7 +312,7 @@ export class AppListenerManager { return; } - app.getEssentials().forEach((event) => { + app.getEssentials()!.forEach((event) => { const lockedEvent = this.lockedEvents.get(event); if (!lockedEvent) { @@ -326,8 +326,8 @@ export class AppListenerManager { public getListeners(int: AppInterface): Array { const results: Array = []; - for (const appId of this.listeners.get(int)) { - results.push(this.manager.getOneById(appId)); + for (const appId of this.listeners.get(int)!) { + results.push(this.manager.getOneById(appId)!); } return results; @@ -467,6 +467,7 @@ export class AppListenerManager { return this.executePostUserStatusChanged(data as IUserStatusContext); default: console.warn('An invalid listener was called'); + return undefined as unknown as Promise; } } @@ -474,8 +475,8 @@ export class AppListenerManager { private async executePreMessageSentPrevent(data: IMessage): Promise { let prevented = false; - for (const appId of this.listeners.get(AppInterface.IPreMessageSentPrevent)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPreMessageSentPrevent)!) { + const app = this.manager.getOneById(appId)!; const continueOn = (await app.call(AppMethod.CHECKPREMESSAGESENTPREVENT, data).catch((error) => { // This method is optional, so if it doesn't exist, we should continue @@ -503,8 +504,8 @@ export class AppListenerManager { private async executePreMessageSentExtend(data: IMessage): Promise { let msg = data; - for (const appId of this.listeners.get(AppInterface.IPreMessageSentExtend)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPreMessageSentExtend)!) { + const app = this.manager.getOneById(appId)!; const continueOn = (await app.call(AppMethod.CHECKPREMESSAGESENTEXTEND, data).catch((error) => { // This method is optional, so if it doesn't exist, we should continue @@ -526,8 +527,8 @@ export class AppListenerManager { private async executePreMessageSentModify(data: IMessage): Promise { let msg = data; - for (const appId of this.listeners.get(AppInterface.IPreMessageSentModify)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPreMessageSentModify)!) { + const app = this.manager.getOneById(appId)!; const continueOn = (await app.call(AppMethod.CHECKPREMESSAGESENTMODIFY, msg).catch((error) => { // This method is optional, so if it doesn't exist, we should continue @@ -548,9 +549,9 @@ export class AppListenerManager { private async executePostMessageSent(data: IMessage): Promise { // First check if the app implements Bot DM handlers and check if the dm contains more than one user - if (data.room.type === RoomType.DIRECT_MESSAGE && data.room.userIds.length > 1) { - for (const appId of this.listeners.get(AppInterface.IPostMessageSentToBot)) { - const app = this.manager.getOneById(appId); + if (data.room.type === RoomType.DIRECT_MESSAGE && data.room.userIds!.length > 1) { + for (const appId of this.listeners.get(AppInterface.IPostMessageSentToBot)!) { + const app = this.manager.getOneById(appId)!; const reader = this.am.getReader(appId); const bot = await reader.getUserReader().getAppUser(); @@ -564,7 +565,7 @@ export class AppListenerManager { continue; } // if the user doesnt belong to the room ignore it - if (!data.room.userIds.includes(bot.id)) { + if (!data.room.userIds!.includes(bot.id)) { continue; } @@ -572,8 +573,8 @@ export class AppListenerManager { } } - for (const appId of this.listeners.get(AppInterface.IPostMessageSent)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostMessageSent)!) { + const app = this.manager.getOneById(appId)!; const continueOn = (await app.call(AppMethod.CHECKPOSTMESSAGESENT, data).catch((error) => { // This method is optional, so if it doesn't exist, we should continue @@ -591,8 +592,8 @@ export class AppListenerManager { } private async executePostSystemMessageSent(data: IMessage): Promise { - for (const appId of this.listeners.get(AppInterface.IPostSystemMessageSent)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostSystemMessageSent)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTEPOSTSYSTEMMESSAGESENT, data); } } @@ -600,8 +601,8 @@ export class AppListenerManager { private async executePreMessageDeletePrevent(data: IMessage): Promise { let prevented = false; - for (const appId of this.listeners.get(AppInterface.IPreMessageDeletePrevent)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPreMessageDeletePrevent)!) { + const app = this.manager.getOneById(appId)!; const continueOn = (await app.call(AppMethod.CHECKPREMESSAGEDELETEPREVENT, data).catch((error) => { // This method is optional, so if it doesn't exist, we should continue @@ -628,8 +629,8 @@ export class AppListenerManager { const context = Utilities.deepCloneAndFreeze(data); const { message } = context; - for (const appId of this.listeners.get(AppInterface.IPostMessageDeleted)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostMessageDeleted)!) { + const app = this.manager.getOneById(appId)!; const continueOn = (await app .call( @@ -657,8 +658,8 @@ export class AppListenerManager { private async executePreMessageUpdatedPrevent(data: IMessage): Promise { let prevented = false; - for (const appId of this.listeners.get(AppInterface.IPreMessageUpdatedPrevent)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPreMessageUpdatedPrevent)!) { + const app = this.manager.getOneById(appId)!; const continueOn = (await app.call(AppMethod.CHECKPREMESSAGEUPDATEDPREVENT, data).catch((error) => { // This method is optional, so if it doesn't exist, we should continue @@ -684,8 +685,8 @@ export class AppListenerManager { private async executePreMessageUpdatedExtend(data: IMessage): Promise { let msg = data; - for (const appId of this.listeners.get(AppInterface.IPreMessageUpdatedExtend)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPreMessageUpdatedExtend)!) { + const app = this.manager.getOneById(appId)!; const continueOn = (await app.call(AppMethod.CHECKPREMESSAGEUPDATEDEXTEND, msg).catch((error) => { // This method is optional, so if it doesn't exist, we should continue @@ -707,8 +708,8 @@ export class AppListenerManager { private async executePreMessageUpdatedModify(data: IMessage): Promise { let msg = data; - for (const appId of this.listeners.get(AppInterface.IPreMessageUpdatedModify)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPreMessageUpdatedModify)!) { + const app = this.manager.getOneById(appId)!; const continueOn = (await app.call(AppMethod.CHECKPREMESSAGEUPDATEDMODIFY, msg).catch((error) => { // This method is optional, so if it doesn't exist, we should continue @@ -728,8 +729,8 @@ export class AppListenerManager { } private async executePostMessageUpdated(data: IMessage): Promise { - for (const appId of this.listeners.get(AppInterface.IPostMessageUpdated)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostMessageUpdated)!) { + const app = this.manager.getOneById(appId)!; const continueOn = (await app.call(AppMethod.CHECKPOSTMESSAGEUPDATED, data).catch((error) => { // This method is optional, so if it doesn't exist, we should continue @@ -750,8 +751,8 @@ export class AppListenerManager { private async executePreRoomCreatePrevent(data: IRoom): Promise { let prevented = false; - for (const appId of this.listeners.get(AppInterface.IPreRoomCreatePrevent)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPreRoomCreatePrevent)!) { + const app = this.manager.getOneById(appId)!; const continueOn = (await app.call(AppMethod.CHECKPREROOMCREATEPREVENT, data).catch((error) => { // This method is optional, so if it doesn't exist, we should continue @@ -777,8 +778,8 @@ export class AppListenerManager { private async executePreRoomCreateExtend(data: IRoom): Promise { let room = data; - for (const appId of this.listeners.get(AppInterface.IPreRoomCreateExtend)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPreRoomCreateExtend)!) { + const app = this.manager.getOneById(appId)!; const continueOn = (await app.call(AppMethod.CHECKPREROOMCREATEEXTEND, room).catch((error) => { // This method is optional, so if it doesn't exist, we should continue @@ -800,8 +801,8 @@ export class AppListenerManager { private async executePreRoomCreateModify(data: IRoom): Promise { let room = data; - for (const appId of this.listeners.get(AppInterface.IPreRoomCreateModify)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPreRoomCreateModify)!) { + const app = this.manager.getOneById(appId)!; const continueOn = (await app.call(AppMethod.CHECKPREROOMCREATEMODIFY, room).catch((error) => { // This method is optional, so if it doesn't exist, we should continue @@ -821,8 +822,8 @@ export class AppListenerManager { } private async executePostRoomCreate(data: IRoom): Promise { - for (const appId of this.listeners.get(AppInterface.IPostRoomCreate)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostRoomCreate)!) { + const app = this.manager.getOneById(appId)!; const continueOn = (await app.call(AppMethod.CHECKPOSTROOMCREATE, data).catch((error) => { // This method is optional, so if it doesn't exist, we should continue @@ -842,8 +843,8 @@ export class AppListenerManager { private async executePreRoomDeletePrevent(data: IRoom): Promise { let prevented = false; - for (const appId of this.listeners.get(AppInterface.IPreRoomDeletePrevent)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPreRoomDeletePrevent)!) { + const app = this.manager.getOneById(appId)!; const continueOn = (await app.call(AppMethod.CHECKPREROOMDELETEPREVENT, data).catch((error) => { // This method is optional, so if it doesn't exist, we should continue @@ -867,8 +868,8 @@ export class AppListenerManager { } private async executePostRoomDeleted(data: IRoom): Promise { - for (const appId of this.listeners.get(AppInterface.IPostRoomDeleted)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostRoomDeleted)!) { + const app = this.manager.getOneById(appId)!; const continueOn = (await app.call(AppMethod.CHECKPOSTROOMDELETED, data).catch((error) => { // This method is optional, so if it doesn't exist, we should continue @@ -886,32 +887,32 @@ export class AppListenerManager { } private async executePreRoomUserJoined(externalData: IRoomUserJoinedContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPreRoomUserJoined)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPreRoomUserJoined)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_PRE_ROOM_USER_JOINED, externalData); } } private async executePostRoomUserJoined(externalData: IRoomUserJoinedContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostRoomUserJoined)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostRoomUserJoined)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_ROOM_USER_JOINED, externalData); } } private async executePreRoomUserLeave(externalData: IRoomUserLeaveContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPreRoomUserLeave)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPreRoomUserLeave)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_PRE_ROOM_USER_LEAVE, externalData); } } private async executePostRoomUserLeave(externalData: IRoomUserLeaveContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostRoomUserLeave)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostRoomUserLeave)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_ROOM_USER_LEAVE, externalData); } @@ -919,16 +920,16 @@ export class AppListenerManager { // External Components private async executePostExternalComponentOpened(data: IExternalComponent): Promise { - for (const appId of this.listeners.get(AppInterface.IPostExternalComponentOpened)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostExternalComponentOpened)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTEPOSTEXTERNALCOMPONENTOPENED, data); } } private async executePostExternalComponentClosed(data: IExternalComponent): Promise { - for (const appId of this.listeners.get(AppInterface.IPostExternalComponentClosed)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostExternalComponentClosed)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTEPOSTEXTERNALCOMPONENTCLOSED, data); } @@ -937,7 +938,7 @@ export class AppListenerManager { private async executeUIKitInteraction(data: UIKitIncomingInteraction): Promise { const { appId } = data; - const app = this.manager.getOneById(appId); + const app = this.manager.getOneById(appId)!; const handleError = (method: string) => (error: unknown) => { if ((error as Record)?.code === JSONRPC_METHOD_NOT_FOUND) { if (this.defaultHandlers.has(method)) { @@ -1037,20 +1038,24 @@ export class AppListenerManager { }) .catch(handleError(method)); } + default: + return undefined as unknown as Promise; } } private async executeUIKitLivechatInteraction(data: IUIKitLivechatIncomingInteraction): Promise { const { appId, type } = data; - const method = ((interactionType: string) => { + const method = ((interactionType: string): AppMethod => { switch (interactionType) { case UIKitIncomingInteractionType.BLOCK: return AppMethod.UIKIT_LIVECHAT_BLOCK_ACTION; + default: + throw new Error(`Unknown UIKit livechat interaction type: ${interactionType}`); } })(type); - const app = this.manager.getOneById(appId); + const app = this.manager.getOneById(appId)!; const interactionData = (( interactionType: UIKitIncomingInteractionType, @@ -1064,106 +1069,108 @@ export class AppListenerManager { return { appId, - actionId, + actionId: actionId!, blockId, visitor, room, - triggerId, + triggerId: triggerId!, value, message, container: container as IUIKitIncomingInteractionModalContainer | IUIKitIncomingInteractionMessageContainer, }; } + default: + return undefined as unknown as IUIKitLivechatBlockIncomingInteraction; } })(type, data); - return app.call(method, interactionData); + return app!.call(method, interactionData); } // Livechat private async executePreLivechatRoomCreatePrevent(data: ILivechatRoom): Promise { - for (const appId of this.listeners.get(AppInterface.IPreLivechatRoomCreatePrevent)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPreLivechatRoomCreatePrevent)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_PRE_LIVECHAT_ROOM_CREATE_PREVENT, data); } } private async executePostLivechatRoomStarted(data: ILivechatRoom): Promise { - for (const appId of this.listeners.get(AppInterface.IPostLivechatRoomStarted)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostLivechatRoomStarted)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_LIVECHAT_ROOM_STARTED, data); } } private async executeLivechatRoomClosedHandler(data: ILivechatRoom): Promise { - for (const appId of this.listeners.get(AppInterface.ILivechatRoomClosedHandler)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.ILivechatRoomClosedHandler)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_LIVECHAT_ROOM_CLOSED_HANDLER, data); } } private async executePostLivechatRoomClosed(data: ILivechatRoom): Promise { - for (const appId of this.listeners.get(AppInterface.IPostLivechatRoomClosed)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostLivechatRoomClosed)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_LIVECHAT_ROOM_CLOSED, data); } } private async executePostLivechatAgentAssigned(data: ILivechatEventContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostLivechatAgentAssigned)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostLivechatAgentAssigned)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_LIVECHAT_AGENT_ASSIGNED, data); } } private async executePostLivechatAgentUnassigned(data: ILivechatEventContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostLivechatAgentUnassigned)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostLivechatAgentUnassigned)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_LIVECHAT_AGENT_UNASSIGNED, data); } } private async executePostLivechatRoomTransferred(data: ILivechatTransferEventContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostLivechatRoomTransferred)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostLivechatRoomTransferred)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_LIVECHAT_ROOM_TRANSFERRED, data); } } private async executePostLivechatGuestSaved(data: IVisitor): Promise { - for (const appId of this.listeners.get(AppInterface.IPostLivechatGuestSaved)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostLivechatGuestSaved)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_LIVECHAT_GUEST_SAVED, data); } } private async executePostLivechatRoomSaved(data: ILivechatRoom): Promise { - for (const appId of this.listeners.get(AppInterface.IPostLivechatRoomSaved)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostLivechatRoomSaved)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_LIVECHAT_ROOM_SAVED, data); } } private async executePostLivechatDepartmentRemoved(data: ILivechatDepartmentEventContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostLivechatDepartmentRemoved)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostLivechatDepartmentRemoved)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_LIVECHAT_DEPARTMENT_REMOVED, data); } } private async executePostLivechatDepartmentDisabled(data: ILivechatDepartmentEventContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostLivechatDepartmentDisabled)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostLivechatDepartmentDisabled)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_LIVECHAT_DEPARTMENT_DISABLED, data); } @@ -1171,8 +1178,8 @@ export class AppListenerManager { // FileUpload private async executePreFileUpload(data: IFileUploadInternalContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPreFileUpload)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPreFileUpload)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_PRE_FILE_UPLOAD, data); } @@ -1181,8 +1188,8 @@ export class AppListenerManager { private async executePreEmailSent(data: IPreEmailSentContext): Promise { let descriptor = data.email; - for (const appId of this.listeners.get(AppInterface.IPreEmailSent)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPreEmailSent)!) { + const app = this.manager.getOneById(appId)!; descriptor = await app.call(AppMethod.EXECUTE_PRE_EMAIL_SENT, { context: data.context, @@ -1194,88 +1201,88 @@ export class AppListenerManager { } private async executePostMessageReacted(data: IMessageReactionContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostMessageReacted)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostMessageReacted)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_MESSAGE_REACTED, data); } } private async executePostMessageFollowed(data: IMessageFollowContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostMessageFollowed)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostMessageFollowed)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_MESSAGE_FOLLOWED, data); } } private async executePostMessagePinned(data: IMessagePinContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostMessagePinned)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostMessagePinned)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_MESSAGE_PINNED, data); } } private async executePostMessageStarred(data: IMessageStarContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostMessageStarred)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostMessageStarred)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_MESSAGE_STARRED, data); } } private async executePostMessageReported(data: IMessageReportContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostMessageReported)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostMessageReported)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_MESSAGE_REPORTED, data); } } private async executePostUserCreated(data: IUserContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostUserCreated)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostUserCreated)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_USER_CREATED, data); } } private async executePostUserUpdated(data: IUserUpdateContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostUserUpdated)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostUserUpdated)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_USER_UPDATED, data); } } private async executePostUserDeleted(data: IUserContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostUserDeleted)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostUserDeleted)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_USER_DELETED, data); } } private async executePostUserLoggedIn(data: IUser): Promise { - for (const appId of this.listeners.get(AppInterface.IPostUserLoggedIn)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostUserLoggedIn)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_USER_LOGGED_IN, data); } } private async executePostUserLoggedOut(data: IUser): Promise { - for (const appId of this.listeners.get(AppInterface.IPostUserLoggedOut)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostUserLoggedOut)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_USER_LOGGED_OUT, data); } } private async executePostUserStatusChanged(data: IUserStatusContext): Promise { - for (const appId of this.listeners.get(AppInterface.IPostUserStatusChanged)) { - const app = this.manager.getOneById(appId); + for (const appId of this.listeners.get(AppInterface.IPostUserStatusChanged)!) { + const app = this.manager.getOneById(appId)!; await app.call(AppMethod.EXECUTE_POST_USER_STATUS_CHANGED, data); } diff --git a/packages/apps/src/server/managers/AppOutboundCommunicationProvider.ts b/packages/apps/src/server/managers/AppOutboundCommunicationProvider.ts index 7239f84161f67..ff5b3c88290be 100644 --- a/packages/apps/src/server/managers/AppOutboundCommunicationProvider.ts +++ b/packages/apps/src/server/managers/AppOutboundCommunicationProvider.ts @@ -15,18 +15,18 @@ export class OutboundMessageProvider { this.isRegistered = false; } - public async runGetProviderMetadata(logStorage: AppLogStorage, accessors: AppAccessorManager): Promise { - return this.runTheCode(AppMethod._OUTBOUND_GET_PROVIDER_METADATA, logStorage, accessors, []); + public async runGetProviderMetadata(_logStorage: AppLogStorage, _accessors: AppAccessorManager): Promise { + return this.runTheCode(AppMethod._OUTBOUND_GET_PROVIDER_METADATA, _logStorage, _accessors, []); } - public async runSendOutboundMessage(logStorage: AppLogStorage, accessors: AppAccessorManager, body: IOutboundMessage): Promise { - await this.runTheCode(AppMethod._OUTBOUND_SEND_MESSAGE, logStorage, accessors, [body]); + public async runSendOutboundMessage(_logStorage: AppLogStorage, _accessors: AppAccessorManager, body: IOutboundMessage): Promise { + await this.runTheCode(AppMethod._OUTBOUND_SEND_MESSAGE, _logStorage, _accessors, [body]); } private async runTheCode( method: AppMethod._OUTBOUND_GET_PROVIDER_METADATA | AppMethod._OUTBOUND_SEND_MESSAGE, - logStorage: AppLogStorage, - accessors: AppAccessorManager, + _logStorage: AppLogStorage, + _accessors: AppAccessorManager, runContextArgs: Array, ): Promise { const provider = `${this.provider.name}-${this.provider.type}`; @@ -39,10 +39,11 @@ export class OutboundMessageProvider { return result as T; } catch (e) { - if (e?.message === 'error-invalid-provider') { + const err = e as { message?: string }; + if (err.message === 'error-invalid-provider') { throw new Error('error-provider-not-registered'); } - throw new AppOutboundProcessError(e.message, method); + throw new AppOutboundProcessError(err.message ?? String(e), method); } } diff --git a/packages/apps/src/server/managers/AppOutboundCommunicationProviderManager.ts b/packages/apps/src/server/managers/AppOutboundCommunicationProviderManager.ts index 996692715cfb2..b73903ec4b589 100644 --- a/packages/apps/src/server/managers/AppOutboundCommunicationProviderManager.ts +++ b/packages/apps/src/server/managers/AppOutboundCommunicationProviderManager.ts @@ -55,7 +55,7 @@ export class AppOutboundCommunicationProviderManager { this.outboundMessageProviders.set(appId, new Map()); } - this.outboundMessageProviders.get(appId).set(provider.type, new OutboundMessageProvider(app, provider)); + this.outboundMessageProviders.get(appId)!.set(provider.type, new OutboundMessageProvider(app, provider)); } public async registerProviders(appId: string): Promise { @@ -89,7 +89,7 @@ export class AppOutboundCommunicationProviderManager { } const appProviders = this.outboundMessageProviders.get(appId); - for await (const [, providerInfo] of appProviders) { + for await (const [, providerInfo] of appProviders!) { await this.unregisterProvider(appId, providerInfo, opts); } diff --git a/packages/apps/src/server/managers/AppPermissionManager.ts b/packages/apps/src/server/managers/AppPermissionManager.ts index 915ef35ea8992..d0fb1561685c4 100644 --- a/packages/apps/src/server/managers/AppPermissionManager.ts +++ b/packages/apps/src/server/managers/AppPermissionManager.ts @@ -32,7 +32,7 @@ export class AppPermissionManager { } private static getCallStack(): string { - const stack = new Error().stack.toString().split('\n'); + const stack = (new Error().stack ?? '').split('\n'); const appStackIndex = stack.findIndex((position) => position.includes(APPS_ENGINE_RUNTIME_FILE_PREFIX)); return stack.slice(4, appStackIndex).join('\n'); diff --git a/packages/apps/src/server/managers/AppRuntimeManager.ts b/packages/apps/src/server/managers/AppRuntimeManager.ts index dd631bc8eebd0..bbec840d2d671 100644 --- a/packages/apps/src/server/managers/AppRuntimeManager.ts +++ b/packages/apps/src/server/managers/AppRuntimeManager.ts @@ -54,7 +54,7 @@ export class AppRuntimeManager { return this.subprocesses[appId]; } - public async runInSandbox(appId: string, execRequest: ExecRequestContext, options?: ExecRequestOptions): Promise { + public async runInSandbox(appId: string, execRequest: ExecRequestContext, _options?: ExecRequestOptions): Promise { const subprocess = this.subprocesses[appId]; if (!subprocess) { diff --git a/packages/apps/src/server/managers/AppSchedulerManager.ts b/packages/apps/src/server/managers/AppSchedulerManager.ts index b6a7ba0e4ee29..ab1e1e749de8c 100644 --- a/packages/apps/src/server/managers/AppSchedulerManager.ts +++ b/packages/apps/src/server/managers/AppSchedulerManager.ts @@ -27,7 +27,7 @@ export class AppSchedulerManager { processors.map((processor) => { const processorId = createProcessorId(processor.id, appId); - this.registeredProcessors.get(appId)[processorId] = processor; + this.registeredProcessors.get(appId)![processorId] = processor; return { id: processorId, @@ -41,13 +41,13 @@ export class AppSchedulerManager { public wrapProcessor(appId: string, processorId: string): IProcessor['processor'] { return async (jobContext: IJobContext) => { - const processor = this.registeredProcessors.get(appId)[processorId]; + const processor = this.registeredProcessors.get(appId)![processorId]; if (!processor) { throw new Error(`Processor ${processorId} not available`); } - const app = this.manager.getOneById(appId); + const app = this.manager.getOneById(appId)!; const status = await app.getStatus(); const previousStatus = app.getPreviousStatus(); diff --git a/packages/apps/src/server/managers/AppSettingsManager.ts b/packages/apps/src/server/managers/AppSettingsManager.ts index a039008ba57e7..3dfb072c4e10f 100644 --- a/packages/apps/src/server/managers/AppSettingsManager.ts +++ b/packages/apps/src/server/managers/AppSettingsManager.ts @@ -47,7 +47,7 @@ export class AppSettingsManager { decoratedSetting.updatedAt = new Date(); storageItem.settings[decoratedSetting.id] = decoratedSetting; - await this.manager.getStorage().updateSetting(storageItem._id, decoratedSetting); + await this.manager.getStorage().updateSetting(storageItem._id!, decoratedSetting); this.manager.getBridges().getAppDetailChangesBridge().doOnAppSettingsChange(appId, decoratedSetting); diff --git a/packages/apps/src/server/managers/AppSignatureManager.ts b/packages/apps/src/server/managers/AppSignatureManager.ts index 1b24cd9d7d4da..ae2df98f2b026 100644 --- a/packages/apps/src/server/managers/AppSignatureManager.ts +++ b/packages/apps/src/server/managers/AppSignatureManager.ts @@ -13,9 +13,9 @@ export class AppSignatureManager { private readonly signingAlgorithm = 'RS512'; - private privateKey: string; + private privateKey!: string; - private publicKey: string; + private publicKey!: string; constructor(private readonly manager: AppManager) { this.federationBridge = this.manager.getBridges().getInternalFederationBridge(); @@ -23,7 +23,7 @@ export class AppSignatureManager { public async verifySignedApp(app: IAppStorageItem): Promise { const publicKey = await jose.importSPKI(await this.getPublicKey(), 'pem'); - const { payload } = await jose.jwtVerify(app.signature, publicKey); + const { payload } = await jose.jwtVerify(app.signature as string, publicKey); const checksum = this.calculateChecksumForApp(app); @@ -45,14 +45,14 @@ export class AppSignatureManager { private async getPrivateKey(): Promise { if (!this.privateKey) { - this.privateKey = await this.federationBridge.getPrivateKey(); + this.privateKey = (await this.federationBridge.getPrivateKey()) as string; } return this.privateKey; } private async getPublicKey(): Promise { if (!this.publicKey) { - this.publicKey = await this.federationBridge.getPublicKey(); + this.publicKey = (await this.federationBridge.getPublicKey()) as string; } return this.publicKey; } diff --git a/packages/apps/src/server/managers/AppSlashCommandManager.ts b/packages/apps/src/server/managers/AppSlashCommandManager.ts index fcb32f8ad6f90..f7f3ae4e3d547 100644 --- a/packages/apps/src/server/managers/AppSlashCommandManager.ts +++ b/packages/apps/src/server/managers/AppSlashCommandManager.ts @@ -72,7 +72,7 @@ export class AppSlashCommandManager { */ public canCommandBeTouchedBy(appId: string, command: string): boolean { const cmd = command.toLowerCase().trim(); - return cmd && (!this.touchedCommandsToApps.has(cmd) || this.touchedCommandsToApps.get(cmd) === appId); + return !!cmd && (!this.touchedCommandsToApps.has(cmd) || this.touchedCommandsToApps.get(cmd) === appId); } /** @@ -128,7 +128,7 @@ export class AppSlashCommandManager { this.providedCommands.set(appId, new Map()); } - this.providedCommands.get(appId).set(command.command, new AppSlashCommand(app, command)); + this.providedCommands.get(appId)!.set(command.command, new AppSlashCommand(app, command)); // The app has now touched the command, so let's set it this.setAsTouched(appId, command.command); @@ -157,7 +157,7 @@ export class AppSlashCommandManager { throw new Error('App must exist in order to modify a command.'); } - const hasNotProvidedIt = !this.providedCommands.has(appId) || !this.providedCommands.get(appId).has(command.command); + const hasNotProvidedIt = !this.providedCommands.has(appId) || !this.providedCommands.get(appId)!.has(command.command); // They haven't provided (added) it and the bridged system doesn't have it, error out if (hasNotProvidedIt && !(await this.bridge.doDoesCommandExist(command.command, appId))) { @@ -172,7 +172,7 @@ export class AppSlashCommandManager { regInfo.isRegistered = true; this.modifiedCommands.set(command.command, regInfo); } else { - this.providedCommands.get(appId).get(command.command).slashCommand = command; + this.providedCommands.get(appId)!.get(command.command)!.slashCommand = command; } this.setAsTouched(appId, command.command); @@ -195,8 +195,8 @@ export class AppSlashCommandManager { } // Handle if the App provided the command fist - if (this.providedCommands.has(appId) && this.providedCommands.get(appId).has(cmd)) { - const cmdInfo = this.providedCommands.get(appId).get(cmd); + if (this.providedCommands.has(appId) && this.providedCommands.get(appId)!.has(cmd)) { + const cmdInfo = this.providedCommands.get(appId)!.get(cmd)!; // A command marked as disabled can then be "enabled" but not be registered. // This happens when an App is not enabled and they change the status of @@ -235,8 +235,8 @@ export class AppSlashCommandManager { } // Handle if the App provided the command fist - if (this.providedCommands.has(appId) && this.providedCommands.get(appId).has(cmd)) { - const cmdInfo = this.providedCommands.get(appId).get(cmd); + if (this.providedCommands.has(appId) && this.providedCommands.get(appId)!.has(cmd)) { + const cmdInfo = this.providedCommands.get(appId)!.get(cmd)!; // A command marked as enabled can then be "disabled" but not yet be registered. // This happens when an App is not enabled and they change the status of @@ -268,7 +268,7 @@ export class AppSlashCommandManager { return; } - const commands = this.providedCommands.get(appId); + const commands = this.providedCommands.get(appId)!; for await (const [, appSlashCommand] of commands) { if (appSlashCommand.isDisabled) { continue; @@ -285,7 +285,7 @@ export class AppSlashCommandManager { */ public async unregisterCommands(appId: string): Promise { if (this.providedCommands.has(appId)) { - const commands = this.providedCommands.get(appId); + const commands = this.providedCommands.get(appId)!; for await (const [, appSlashCommand] of commands) { const cmd = appSlashCommand.slashCommand.command; await this.bridge.doUnregisterCommand(cmd, appId); @@ -293,8 +293,8 @@ export class AppSlashCommandManager { if (!this.appsTouchedCommands.has(appId)) { continue; } - const ind = this.appsTouchedCommands.get(appId).indexOf(cmd); - this.appsTouchedCommands.get(appId).splice(ind, 1); + const ind = this.appsTouchedCommands.get(appId)!.indexOf(cmd); + this.appsTouchedCommands.get(appId)!.splice(ind, 1); appSlashCommand.isRegistered = true; } @@ -306,10 +306,10 @@ export class AppSlashCommandManager { // only be the ones which the App has enabled, disabled, or modified. // We call restore to enable the commands provided by the bridged system // or unmodify the commands modified by the App - this.appsTouchedCommands.get(appId).forEach((cmd) => { + this.appsTouchedCommands.get(appId)!.forEach((cmd) => { // @NOTE this "restore" method isn't present in the bridge // this.bridge.doRestoreCommand(cmd, appId); - this.modifiedCommands.get(cmd).isRegistered = false; + this.modifiedCommands.get(cmd)!.isRegistered = false; this.modifiedCommands.delete(cmd); this.touchedCommandsToApps.delete(cmd); }); @@ -331,7 +331,7 @@ export class AppSlashCommandManager { return; } - const app = this.manager.getOneById(this.touchedCommandsToApps.get(cmd)); + const app = this.manager.getOneById(this.touchedCommandsToApps.get(cmd)!); if (!app) { throw new Error('App not found'); @@ -341,7 +341,7 @@ export class AppSlashCommandManager { throw new Error('App not enabled'); } - const appCmd = this.retrieveCommandInfo(cmd, app.getID()); + const appCmd = this.retrieveCommandInfo(cmd, app.getID())!; await appCmd.runExecutorOrPreviewer( AppMethod._COMMAND_EXECUTOR, this.ensureContext(context), @@ -350,22 +350,22 @@ export class AppSlashCommandManager { ); } - public async getPreviews(command: string, context: SlashCommandContext): Promise { + public async getPreviews(command: string, context: SlashCommandContext): Promise { const cmd = command.toLowerCase().trim(); if (!this.shouldCommandFunctionsRun(cmd)) { - return; + return undefined; } - const app = this.manager.getOneById(this.touchedCommandsToApps.get(cmd)); + const app = this.manager.getOneById(this.touchedCommandsToApps.get(cmd)!); if (!app || AppStatusUtils.isDisabled(await app.getStatus())) { // Just in case someone decides to do something they shouldn't // let's ensure the app actually exists - return; + return undefined; } - const appCmd = this.retrieveCommandInfo(cmd, app.getID()); + const appCmd = this.retrieveCommandInfo(cmd, app.getID())!; const result = await appCmd.runExecutorOrPreviewer( AppMethod._COMMAND_PREVIEWER, @@ -376,7 +376,7 @@ export class AppSlashCommandManager { if (!result) { // Failed to get the preview, thus returning is fine - return; + return undefined; } return result; @@ -389,7 +389,7 @@ export class AppSlashCommandManager { return; } - const app = this.manager.getOneById(this.touchedCommandsToApps.get(cmd)); + const app = this.manager.getOneById(this.touchedCommandsToApps.get(cmd)!); if (!app || AppStatusUtils.isDisabled(await app.getStatus())) { // Just in case someone decides to do something they shouldn't @@ -397,7 +397,7 @@ export class AppSlashCommandManager { return; } - const appCmd = this.retrieveCommandInfo(cmd, app.getID()); + const appCmd = this.retrieveCommandInfo(cmd, app.getID())!; await appCmd.runPreviewExecutor(previewItem, this.ensureContext(context), this.manager.getLogStorage(), this.accessors); } @@ -428,7 +428,7 @@ export class AppSlashCommandManager { return false; } - const appId = this.touchedCommandsToApps.get(command); + const appId = this.touchedCommandsToApps.get(command)!; const cmdInfo = this.retrieveCommandInfo(command, appId); // Should the command information really not exist @@ -442,8 +442,8 @@ export class AppSlashCommandManager { return true; } - private retrieveCommandInfo(command: string, appId: string): AppSlashCommand { - return this.modifiedCommands.get(command) || this.providedCommands.get(appId).get(command); + private retrieveCommandInfo(command: string, appId: string): AppSlashCommand | undefined { + return this.modifiedCommands.get(command) || this.providedCommands.get(appId)?.get(command); } /** @@ -457,8 +457,8 @@ export class AppSlashCommandManager { this.appsTouchedCommands.set(appId, []); } - if (!this.appsTouchedCommands.get(appId).includes(command)) { - this.appsTouchedCommands.get(appId).push(command); + if (!this.appsTouchedCommands.get(appId)!.includes(command)) { + this.appsTouchedCommands.get(appId)!.push(command); } this.touchedCommandsToApps.set(command, appId); diff --git a/packages/apps/src/server/managers/AppVideoConfProvider.ts b/packages/apps/src/server/managers/AppVideoConfProvider.ts index b0e76f4073aa3..82418ec1d8486 100644 --- a/packages/apps/src/server/managers/AppVideoConfProvider.ts +++ b/packages/apps/src/server/managers/AppVideoConfProvider.ts @@ -97,7 +97,8 @@ export class AppVideoConfProvider { return result as string | boolean | Array | undefined; } catch (e) { - if (e?.code === JSONRPC_METHOD_NOT_FOUND) { + const err = e as { code?: number }; + if (err.code === JSONRPC_METHOD_NOT_FOUND) { if (method === AppMethod._VIDEOCONF_IS_CONFIGURED) { return true; } @@ -109,5 +110,7 @@ export class AppVideoConfProvider { // @TODO add error handling console.log(e); } + + return undefined; } } diff --git a/packages/apps/src/server/managers/AppVideoConfProviderManager.ts b/packages/apps/src/server/managers/AppVideoConfProviderManager.ts index c961b5b1ccf79..c9c6d299af0c0 100644 --- a/packages/apps/src/server/managers/AppVideoConfProviderManager.ts +++ b/packages/apps/src/server/managers/AppVideoConfProviderManager.ts @@ -72,7 +72,7 @@ export class AppVideoConfProviderManager { this.videoConfProviders.set(appId, new Map()); } - this.videoConfProviders.get(appId).set(providerName, new AppVideoConfProvider(app, provider)); + this.videoConfProviders.get(appId)!.set(providerName, new AppVideoConfProvider(app, provider)); this.linkAppProvider(appId, providerName); } @@ -97,7 +97,7 @@ export class AppVideoConfProviderManager { } const appProviders = this.videoConfProviders.get(appId); - for (const [, providerInfo] of appProviders) { + for (const [, providerInfo] of appProviders!) { this.unregisterProvider(appId, providerInfo); } @@ -184,11 +184,13 @@ export class AppVideoConfProviderManager { continue; } - const provider = providers.get(key); + const provider = providers.get(key)!; if (provider.isRegistered) { return provider; } } + + return undefined; } private linkAppProvider(appId: string, providerName: string): void { diff --git a/packages/apps/src/server/managers/UIActionButtonManager.ts b/packages/apps/src/server/managers/UIActionButtonManager.ts index 6b47448c3b09d..ee75d17f1060b 100644 --- a/packages/apps/src/server/managers/UIActionButtonManager.ts +++ b/packages/apps/src/server/managers/UIActionButtonManager.ts @@ -27,7 +27,7 @@ export class UIActionButtonManager { this.registeredActionButtons.set(appId, new Map()); } - this.registeredActionButtons.get(appId).set(button.actionId, button); + this.registeredActionButtons.get(appId)!.set(button.actionId, button); this.activationBridge.doActionsChanged(); diff --git a/packages/apps/src/server/marketplace/license/AppLicenseValidationResult.ts b/packages/apps/src/server/marketplace/license/AppLicenseValidationResult.ts index 2946d40111f54..cf16937041ae6 100644 --- a/packages/apps/src/server/marketplace/license/AppLicenseValidationResult.ts +++ b/packages/apps/src/server/marketplace/license/AppLicenseValidationResult.ts @@ -5,7 +5,7 @@ export class AppLicenseValidationResult { private validated = false; - private appId: string; + private appId!: string; public addError(field: string, message: string): void { this.errors[field] = message; diff --git a/packages/apps/src/server/messages/Message.ts b/packages/apps/src/server/messages/Message.ts index af429593f3002..e15f80c863663 100644 --- a/packages/apps/src/server/messages/Message.ts +++ b/packages/apps/src/server/messages/Message.ts @@ -8,7 +8,7 @@ import { Room } from '../rooms/Room'; export class Message implements IMessage { public id?: string; - public sender: IUser; + public sender!: IUser; public text?: string; @@ -50,7 +50,7 @@ export class Message implements IMessage { public pinnedBy?: IUserLookup; - private _ROOM: Room; + private _ROOM!: Room; public get room(): Room { return this._ROOM; diff --git a/packages/apps/src/server/misc/UIHelper.ts b/packages/apps/src/server/misc/UIHelper.ts index 70989cd6665fe..e4403fa5f7256 100644 --- a/packages/apps/src/server/misc/UIHelper.ts +++ b/packages/apps/src/server/misc/UIHelper.ts @@ -11,15 +11,16 @@ export class UIHelper { * @returns the array of block with the ids properties assigned */ public static assignIds(blocks: Array, appId: string): Array { - blocks.forEach((block: (IBlock | LayoutBlock) & { appId?: string; blockId?: string; elements?: Array }) => { - if (!block.appId) { - block.appId = appId; + blocks.forEach((block) => { + const b = block as (IBlock | LayoutBlock) & { appId?: string; blockId?: string; elements?: Array }; + if (!b.appId) { + b.appId = appId; } - if (!block.blockId) { - block.blockId = uuid(); + if (!b.blockId) { + b.blockId = uuid(); } - if (block.elements) { - block.elements.forEach((element) => { + if (b.elements) { + b.elements.forEach((element) => { if (!element.actionId) { element.actionId = uuid(); } diff --git a/packages/apps/src/server/rooms/Room.ts b/packages/apps/src/server/rooms/Room.ts index c0ea200c470e7..a76471837d3e0 100644 --- a/packages/apps/src/server/rooms/Room.ts +++ b/packages/apps/src/server/rooms/Room.ts @@ -5,15 +5,15 @@ import type { AppManager } from '../AppManager'; const PrivateManager = Symbol('RoomPrivateManager'); export class Room implements IRoom { - public id: string; + public id!: string; public displayName?: string; - public slugifiedName: string; + public slugifiedName!: string; - public type: RoomType; + public type!: RoomType; - public creator: IUser; + public creator!: IUser; public isDefault?: boolean; @@ -33,9 +33,9 @@ export class Room implements IRoom { public userIds?: Array; - private _USERNAMES: Array; + private _USERNAMES!: Array; - private [PrivateManager]: AppManager; + private [PrivateManager]!: AppManager; /** * @deprecated @@ -49,7 +49,7 @@ export class Room implements IRoom { return this._USERNAMES; } - public set usernames(usernames) {} + public set usernames(_usernames: Array) {} public constructor(room: IRoom, manager: AppManager) { Object.assign(this, room); diff --git a/packages/apps/src/server/runtime/AppsEngineEmptyRuntime.ts b/packages/apps/src/server/runtime/AppsEngineEmptyRuntime.ts index 3b0a6f90bbefb..1a1cfcfe17580 100644 --- a/packages/apps/src/server/runtime/AppsEngineEmptyRuntime.ts +++ b/packages/apps/src/server/runtime/AppsEngineEmptyRuntime.ts @@ -3,11 +3,11 @@ import { AppsEngineRuntime } from './AppsEngineRuntime'; import type { App } from '@rocket.chat/apps-engine/definition/App'; export class AppsEngineEmptyRuntime extends AppsEngineRuntime { - public static async runCode(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { + public static override async runCode(_code: string, _sandbox?: Record, _options?: IAppsEngineRuntimeOptions): Promise { throw new Error('Empty runtime does not support code execution'); } - public static runCodeSync(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): any { + public static override runCodeSync(_code: string, _sandbox?: Record, _options?: IAppsEngineRuntimeOptions): any { throw new Error('Empty runtime does not support code execution'); } @@ -15,7 +15,7 @@ export class AppsEngineEmptyRuntime extends AppsEngineRuntime { super(app, () => {}); } - public async runInSandbox(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { + public async runInSandbox(_code: string, _sandbox?: Record, _options?: IAppsEngineRuntimeOptions): Promise { return Promise.reject(new Error('Empty runtime does not support execution')); } } diff --git a/packages/apps/src/server/runtime/AppsEngineNodeRuntime.ts b/packages/apps/src/server/runtime/AppsEngineNodeRuntime.ts index a9cf20b1d8fa4..71ad32c0f13f8 100644 --- a/packages/apps/src/server/runtime/AppsEngineNodeRuntime.ts +++ b/packages/apps/src/server/runtime/AppsEngineNodeRuntime.ts @@ -18,7 +18,7 @@ export class AppsEngineNodeRuntime extends AppsEngineRuntime { exports: {}, }; - public static async runCode(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { + public static override async runCode(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { return new Promise((resolve, reject) => { process.nextTick(() => { try { @@ -30,7 +30,7 @@ export class AppsEngineNodeRuntime extends AppsEngineRuntime { }); } - public static runCodeSync(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): any { + public static override runCodeSync(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): any { return vm.runInNewContext( code, { ...AppsEngineNodeRuntime.defaultContext, ...sandbox }, diff --git a/packages/apps/src/server/runtime/AppsEngineRuntime.ts b/packages/apps/src/server/runtime/AppsEngineRuntime.ts index 69f32092a7401..d8e419b0ff3c8 100644 --- a/packages/apps/src/server/runtime/AppsEngineRuntime.ts +++ b/packages/apps/src/server/runtime/AppsEngineRuntime.ts @@ -9,15 +9,15 @@ export function getFilenameForApp(filename: string): string { } export abstract class AppsEngineRuntime { - public static async runCode(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { + public static async runCode(_code: string, _sandbox?: Record, _options?: IAppsEngineRuntimeOptions): Promise { throw new Error(`Can't call this method on abstract class. Override it in a proper runtime class.`); } - public static runCodeSync(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): any { + public static runCodeSync(_code: string, _sandbox?: Record, _options?: IAppsEngineRuntimeOptions): any { throw new Error(`Can't call this method on abstract class. Override it in a proper runtime class.`); } - constructor(app: App, customRequire: (module: string) => any) {} + constructor(_app: App, _customRequire: (module: string) => any) {} public abstract runInSandbox(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise; } diff --git a/packages/apps/src/server/runtime/EmptyRuntime.ts b/packages/apps/src/server/runtime/EmptyRuntime.ts index f8de8482ac4b4..c4ec2b5a091cb 100644 --- a/packages/apps/src/server/runtime/EmptyRuntime.ts +++ b/packages/apps/src/server/runtime/EmptyRuntime.ts @@ -21,7 +21,7 @@ export class EmptyRuntime extends EventEmitter implements IRuntimeController { /** * Stub implementation that throws an error since this runtime cannot handle requests */ - public async sendRequest(message: { method: string; params: any[] }, options?: RuntimeRequestOptions): Promise { + public async sendRequest(message: { method: string; params: any[] }, _options?: RuntimeRequestOptions): Promise { throw new Error(`EmptyRuntime cannot handle requests. Method: ${message.method}`); } diff --git a/packages/apps/src/server/runtime/deno/AppsEngineDenoRuntime.ts b/packages/apps/src/server/runtime/deno/AppsEngineDenoRuntime.ts index c74f22d9f7a01..0fff9d1894d83 100644 --- a/packages/apps/src/server/runtime/deno/AppsEngineDenoRuntime.ts +++ b/packages/apps/src/server/runtime/deno/AppsEngineDenoRuntime.ts @@ -285,7 +285,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu // What else should we do? if (this.deno.kill('SIGKILL')) { // Let's wait until we get confirmation the process exited - await new Promise((r) => this.deno.on('exit', r)); + await new Promise((r) => this.deno!.on('exit', r)); killed = true; } else { this.debug('Tried killing the process but failed. Was it already dead?'); @@ -298,7 +298,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu } // Debug purposes, could be deleted later - emit(eventName: string | symbol, ...args: any[]): boolean { + override emit(eventName: string | symbol, ...args: any[]): boolean { const hadListeners = super.emit(eventName, ...args); if (!hadListeners) { @@ -365,7 +365,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu } await this.setupApp(); - logger.info({ msg: 'New subprocess successfully spawned', pid: this.deno.pid }); + logger.info({ msg: 'New subprocess successfully spawned', pid: this.deno!.pid }); // setupApp() changes the state to 'ready' - we'll need to workaround that for now this.state = 'restarting'; @@ -415,7 +415,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu private waitUntilReady(): Promise { if (this.state === 'ready') { - return; + return Promise.resolve(); } return new Promise((resolve, reject) => { @@ -486,7 +486,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu return; } - this.deno.stderr.on('data', this.parseError.bind(this)); + this.deno.stderr!.on('data', this.parseError.bind(this)); this.deno.on('error', (err) => { this.state = 'invalid'; console.error(`Failed to startup Deno subprocess for app ${this.getAppId()}`, err); @@ -496,7 +496,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu this.once('ready', this.onReady.bind(this)); - this.parseStdout(this.deno.stdout); + this.parseStdout(this.deno.stdout!); } // Probable should extract this to a separate file @@ -547,7 +547,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu **/ // Prevent app from trying to get properties from the manager that // are not intended for public access - if (!isValidOrigin(managerOrigin)) { + if (!managerOrigin || !isValidOrigin(managerOrigin)) { throw new Error(`Invalid accessor namespace "${managerOrigin}"`); } @@ -626,8 +626,9 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu params.map((value: unknown) => (value === 'APP_ID' ? this.appPackage.info.id : value)), ); } catch (error) { - this.debug('Error executing bridge method %s().%s() %s', bridgeName, bridgeMethod, inspect(error.message)); - const jsonRpcError = new jsonrpc.JsonRpcError(error.message, -32000, error); + const err = error as Error; + this.debug('Error executing bridge method %s().%s() %s', bridgeName, bridgeMethod, inspect(err.message)); + const jsonRpcError = new jsonrpc.JsonRpcError(err.message, -32000, err); return jsonrpc.error(id, jsonRpcError); } @@ -643,7 +644,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu try { result = await this.handleAccessorMessage(message as jsonrpc.IParsedObjectRequest); } catch (e) { - result = jsonrpc.error((message.payload as jsonrpc.RequestObject).id, new jsonrpc.JsonRpcError(e.message, 1000)); + result = jsonrpc.error((message.payload as jsonrpc.RequestObject).id, new jsonrpc.JsonRpcError((e as Error).message, 1000)); } this.messenger.send(result); @@ -657,7 +658,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu try { result = await this.handleBridgeMessage(message as jsonrpc.IParsedObjectRequest); } catch (e) { - result = jsonrpc.error((message.payload as jsonrpc.RequestObject).id, new jsonrpc.JsonRpcError(e.message, 1000)); + result = jsonrpc.error((message.payload as jsonrpc.RequestObject).id, new jsonrpc.JsonRpcError((e as Error).message, 1000)); } this.messenger.send(result); @@ -699,7 +700,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu let result: unknown; let error: jsonrpc.IParsedObjectError['payload']['error'] | undefined; - let logs: ILoggerStorageEntry; + let logs: ILoggerStorageEntry | undefined; if (message.type === 'success') { const params = message.payload.result as { value: unknown; logs?: ILoggerStorageEntry }; diff --git a/packages/apps/src/server/runtime/deno/LivenessManager.ts b/packages/apps/src/server/runtime/deno/LivenessManager.ts index f7ba66ed37c04..0da5e4009082c 100644 --- a/packages/apps/src/server/runtime/deno/LivenessManager.ts +++ b/packages/apps/src/server/runtime/deno/LivenessManager.ts @@ -42,14 +42,14 @@ export class LivenessManager { restartAttemptDelayInMS: number; }; - private subprocess: ChildProcess; + private subprocess!: ChildProcess; private watchdogTimeout: NodeJS.Timeout | null = null; private lastHeartbeatTimestamp = NaN; // A promise tracking the current ping process - used mostly for testing - private pendingPing: Promise | null; + private pendingPing: Promise | null = null; // This is the perfect use-case for an AbortController, but it's experimental in Node 14.x private pingAbortController: EventEmitter; @@ -132,7 +132,7 @@ export class LivenessManager { public stop() { this.pingAbortController.emit('abort'); - clearInterval(this.watchdogTimeout); + clearInterval(this.watchdogTimeout ?? undefined); this.watchdogTimeout = null; this.pendingPing = null; } diff --git a/packages/apps/src/server/runtime/deno/ProcessMessenger.ts b/packages/apps/src/server/runtime/deno/ProcessMessenger.ts index c5c2394e56dfa..7da5cdbc6f400 100644 --- a/packages/apps/src/server/runtime/deno/ProcessMessenger.ts +++ b/packages/apps/src/server/runtime/deno/ProcessMessenger.ts @@ -52,6 +52,6 @@ export class ProcessMessenger { } private strategySend(message: Message) { - this.deno.stdin.write(this.encoder.encode(message)); + this.deno!.stdin!.write(this.encoder!.encode(message)); } } diff --git a/packages/apps/src/server/runtime/deno/codec.ts b/packages/apps/src/server/runtime/deno/codec.ts index 53b05846565ee..37ac42f9f04f2 100644 --- a/packages/apps/src/server/runtime/deno/codec.ts +++ b/packages/apps/src/server/runtime/deno/codec.ts @@ -4,11 +4,12 @@ const extensionCodec = new ExtensionCodec(); extensionCodec.register({ type: 0, - encode: (object: unknown) => { + encode: (object: unknown): Uint8Array | null => { // We don't care about functions, but also don't want to throw an error if (typeof object === 'function') { return new Uint8Array([0]); } + return null; }, decode: (_data: Uint8Array) => undefined, @@ -17,10 +18,11 @@ extensionCodec.register({ // We need to handle Buffers because Deno needs its own decoding extensionCodec.register({ type: 1, - encode: (object: unknown) => { + encode: (object: unknown): Uint8Array | null => { if (object instanceof Buffer) { return new Uint8Array(object.buffer, object.byteOffset, object.byteLength); } + return null; }, // msgpack will reuse the Uint8Array instance, so WE NEED to copy it instead of simply creating a view