From a3cc5c9da179865b964c16579fd839e2959e0fd8 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 18 Apr 2023 15:02:24 -0300 Subject: [PATCH 01/16] Import services from apps-to-service branch --- .../ee/server/apps/services/apiService.ts | 112 ++++++++++++++++++ .../server/apps/services/converterService.ts | 40 +++++++ .../ee/server/apps/services/managerService.ts | 111 +++++++++++++++++ .../apps/services/orchestratorFactory.ts | 40 +++++++ .../ee/server/apps/services/orchservice.ts | 91 ++++++++++++++ .../server/apps/services}/service.ts | 59 +++++---- .../server/apps/services/statisticsService.ts | 40 +++++++ .../apps/services/videoManagerService.ts | 70 +++++++++++ apps/meteor/server/services/startup.ts | 4 +- packages/core-services/src/index.ts | 18 +++ .../src/types/IAppsApiService.ts | 14 +++ .../src/types/IAppsConverterService.ts | 12 ++ .../src/types/IAppsManagerService.ts | 40 +++++++ .../core-services/src/types/IAppsService.ts | 25 ++++ .../src/types/IAppsStatisticsService.ts | 8 ++ .../src/types/IAppsVideoManagerService.ts | 18 +++ 16 files changed, 677 insertions(+), 25 deletions(-) create mode 100644 apps/meteor/ee/server/apps/services/apiService.ts create mode 100644 apps/meteor/ee/server/apps/services/converterService.ts create mode 100644 apps/meteor/ee/server/apps/services/managerService.ts create mode 100644 apps/meteor/ee/server/apps/services/orchestratorFactory.ts create mode 100644 apps/meteor/ee/server/apps/services/orchservice.ts rename apps/meteor/{server/services/apps-engine => ee/server/apps/services}/service.ts (85%) create mode 100644 apps/meteor/ee/server/apps/services/statisticsService.ts create mode 100644 apps/meteor/ee/server/apps/services/videoManagerService.ts create mode 100644 packages/core-services/src/types/IAppsApiService.ts create mode 100644 packages/core-services/src/types/IAppsConverterService.ts create mode 100644 packages/core-services/src/types/IAppsManagerService.ts create mode 100644 packages/core-services/src/types/IAppsService.ts create mode 100644 packages/core-services/src/types/IAppsStatisticsService.ts create mode 100644 packages/core-services/src/types/IAppsVideoManagerService.ts diff --git a/apps/meteor/ee/server/apps/services/apiService.ts b/apps/meteor/ee/server/apps/services/apiService.ts new file mode 100644 index 0000000000000..71b9585a36916 --- /dev/null +++ b/apps/meteor/ee/server/apps/services/apiService.ts @@ -0,0 +1,112 @@ +import type { RequestMethod } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IApiEndpoint, IApiRequest } from '@rocket.chat/apps-engine/definition/api'; +import { Router } from 'express'; +import type { Request, Response, IRouter, RequestHandler, NextFunction } from 'express'; +import type { IAppsApiService, IRequestWithPrivateHash } from '@rocket.chat/core-services'; +import { ServiceClass } from '@rocket.chat/core-services'; + +import { OrchestratorFactory } from './orchestratorFactory'; +import type { AppServerOrchestrator } from '../orchestrator'; + +export class AppsApiService extends ServiceClass implements IAppsApiService { + protected name = 'apps'; + + private apps: AppServerOrchestrator; + + protected appRouters: Map; + + constructor() { + super(); + this.appRouters = new Map(); + this.apps = OrchestratorFactory.getOrchestrator(); + } + + async handlePublicRequest(req: Request, res: Response): Promise { + const notFound = (): Response => res.sendStatus(404); + + const router = this.appRouters.get(req.params.appId); + + if (router) { + return router(req, res, notFound); + } + + notFound(); + } + + async handlePrivateRequest(req: IRequestWithPrivateHash, res: Response): Promise { + const notFound = (): Response => res.sendStatus(404); + + const router = this.appRouters.get(req.params.appId); + + if (router) { + req._privateHash = req.params.hash; + return router(req, res, notFound); + } + + notFound(); + } + + registerApi(endpoint: IApiEndpoint, appId: string): void { + let router = this.appRouters.get(appId); + + if (!router) { + // eslint-disable-next-line new-cap + router = Router(); + this.appRouters.set(appId, router); + } + + const method = 'all'; + + let routePath = endpoint.path.trim(); + if (!routePath.startsWith('/')) { + routePath = `/${routePath}`; + } + + if (router[method] instanceof Function) { + router[method](routePath, this.authMiddleware(!!endpoint.authRequired), this._appApiExecutor(endpoint, appId)); + } + } + + private authMiddleware(authRequired: boolean) { + return (req: Request, res: Response, next: NextFunction): void => { + if (!req.user && authRequired) { + res.status(401).send('Unauthorized'); + return; + } + next(); + }; + } + + unregisterApi(appId: string): void { + if (this.appRouters.get(appId)) { + this.appRouters.delete(appId); + } + } + + private _appApiExecutor(endpoint: IApiEndpoint, appId: string): RequestHandler { + return (req: IRequestWithPrivateHash, res: Response): void => { + const request: IApiRequest = { + method: req.method.toLowerCase() as RequestMethod, + headers: req.headers as { [key: string]: string }, + query: (req.query as { [key: string]: string }) || {}, + params: req.params || {}, + content: req.body, + privateHash: req._privateHash, + user: req.user && this.apps.getConverters()?.get('users')?.convertToApp(req.user), + }; + this.apps + .getManager() + ?.getApiManager() + .executeApi(appId, endpoint.path, request) + .then(({ status, headers = {}, content }) => { + res.set(headers); + res.status(status); + res.send(content); + }) + .catch((reason) => { + // Should we handle this as an error? + res.status(500).send(reason.message); + }); + }; + } +} diff --git a/apps/meteor/ee/server/apps/services/converterService.ts b/apps/meteor/ee/server/apps/services/converterService.ts new file mode 100644 index 0000000000000..e176a76abc34c --- /dev/null +++ b/apps/meteor/ee/server/apps/services/converterService.ts @@ -0,0 +1,40 @@ +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import type { IVisitor } from '@rocket.chat/apps-engine/definition/livechat'; +import { ServiceClass } from '@rocket.chat/core-services'; +import type { IAppsConverterService } from '@rocket.chat/core-services'; + +import { OrchestratorFactory } from './orchestratorFactory'; +import type { AppServerOrchestrator } from '../orchestrator'; + +export class AppsConverterService extends ServiceClass implements IAppsConverterService { + protected name = 'apps'; + + private apps: AppServerOrchestrator; + + constructor() { + super(); + this.apps = OrchestratorFactory.getOrchestrator(); + } + + async convertRoomById(id: string): Promise { + return this.apps.getConverters()?.get('rooms').convertById(id); + } + + async convertMessageById(id: string): Promise { + return this.apps.getConverters()?.get('messages').convertById(id); + } + + async convertVistitorByToken(token: string): Promise { + return this.apps.getConverters()?.get('visitors').convertByToken(token); + } + + async convertUserToApp(user: any): Promise { + return this.apps.getConverters()?.get('users').convertToApp(user); + } + + async convertUserById(id: string): Promise { + return this.apps.getConverters()?.get('users').convertById(id); + } +} diff --git a/apps/meteor/ee/server/apps/services/managerService.ts b/apps/meteor/ee/server/apps/services/managerService.ts new file mode 100644 index 0000000000000..6851ba5a15584 --- /dev/null +++ b/apps/meteor/ee/server/apps/services/managerService.ts @@ -0,0 +1,111 @@ +import type { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api'; +import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; +import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; +import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; +import type { AppFabricationFulfillment } from '@rocket.chat/apps-engine/server/compiler'; +import type { IAppInstallParameters, IAppUninstallParameters } from '@rocket.chat/apps-engine/server/AppManager'; +import type { IGetAppsFilter } from '@rocket.chat/apps-engine/server/IGetAppsFilter'; +import type { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; +import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import type { + SlashCommandContext, + ISlashCommandPreview, + ISlashCommandPreviewItem, +} from '@rocket.chat/apps-engine/definition/slashcommands'; +import { ServiceClass } from '@rocket.chat/core-services'; +import type { IAppsManagerService } from '@rocket.chat/core-services'; + +import { OrchestratorFactory } from './orchestratorFactory'; +import type { AppServerOrchestrator } from '../orchestrator'; + +export class AppsManagerService extends ServiceClass implements IAppsManagerService { + protected name = 'apps'; + + private apps: AppServerOrchestrator; + + constructor() { + super(); + this.apps = OrchestratorFactory.getOrchestrator(); + } + + async loadOne(appId: string): Promise { + return (this.apps.getManager() as any).loadOne(appId); // TO-DO: fix type + } + + async enable(appId: string): Promise { + return this.apps.getManager()?.enable(appId); + } + + async disable(appId: string): Promise { + return this.apps.getManager()?.disable(appId); + } + + get(filter?: IGetAppsFilter | undefined): ProxiedApp[] { + return this.apps.getManager()?.get(filter) ?? []; + } + + async add(appPackage: Buffer, installationParameters: IAppInstallParameters): Promise { + return this.apps.getManager()?.add(appPackage, installationParameters); + } + + async remove(id: string, uninstallationParameters: IAppUninstallParameters): Promise { + return this.apps.getManager()?.remove(id, uninstallationParameters); + } + + async removeLocal(id: string): Promise { + return this.apps.getManager()?.removeLocal(id); + } + + async update( + appPackage: Buffer, + permissionsGranted: IPermission[], + updateOptions = { loadApp: true }, + ): Promise { + return this.apps.getManager()?.update(appPackage, permissionsGranted, updateOptions); + } + + async updateLocal(stored: IAppStorageItem, appPackageOrInstance: ProxiedApp | Buffer): Promise { + this.apps.getManager()?.updateLocal(stored, appPackageOrInstance); + } + + getOneById(appId: string): ProxiedApp | undefined { + return this.apps.getManager()?.getOneById(appId); + } + + async updateAppSetting(appId: string, setting: ISetting): Promise { + return this.apps.getManager()?.getSettingsManager().updateAppSetting(appId, setting); + } + + getAppSettings(appId: string): { [key: string]: ISetting } | undefined { + return this.apps.getManager()?.getSettingsManager().getAppSettings(appId); + } + + listApis(appId: string): IApiEndpointMetadata[] | undefined { + return this.apps.getManager()?.getApiManager().listApis(appId); + } + + async changeStatus(appId: string, status: AppStatus): Promise { + return this.apps.getManager()?.changeStatus(appId, status); + } + + getAllActionButtons(): IUIActionButton[] { + return this.apps.getManager()?.getUIActionButtonManager().getAllActionButtons() ?? []; + } + + async getCommandPreviews(command: string, context: SlashCommandContext): Promise { + return this.apps.getManager()?.getCommandManager().getPreviews(command, context); + } + + async commandExecutePreview( + command: string, + previewItem: ISlashCommandPreviewItem, + context: SlashCommandContext, + ): Promise { + return this.apps.getManager()?.getCommandManager().executePreview(command, previewItem, context); + } + + async commandExecuteCommand(command: string, context: SlashCommandContext): Promise { + return this.apps.getManager()?.getCommandManager().executeCommand(command, context); + } +} diff --git a/apps/meteor/ee/server/apps/services/orchestratorFactory.ts b/apps/meteor/ee/server/apps/services/orchestratorFactory.ts new file mode 100644 index 0000000000000..4dc4828da8aa3 --- /dev/null +++ b/apps/meteor/ee/server/apps/services/orchestratorFactory.ts @@ -0,0 +1,40 @@ +import type { Db } from 'mongodb'; + +import { settings } from '../../../../app/settings/server'; +import { AppServerOrchestrator } from '../orchestrator'; + +type AppsInitParams = { + appsSourceStorageFilesystemPath: any; + appsSourceStorageType: any; + marketplaceUrl?: string | undefined; +}; + +export class OrchestratorFactory { + private static orchestrator: AppServerOrchestrator; + + private static createOrchestrator(db?: Db) { + const appsInitParams: AppsInitParams = { + appsSourceStorageType: settings.get('Apps_Framework_Source_Package_Storage_Type'), + appsSourceStorageFilesystemPath: settings.get('Apps_Framework_Source_Package_Storage_FileSystem_Path'), + marketplaceUrl: 'https://marketplace.rocket.chat', + }; + + this.orchestrator = new AppServerOrchestrator(db); + + const { OVERWRITE_INTERNAL_MARKETPLACE_URL } = process.env || {}; + + if (typeof OVERWRITE_INTERNAL_MARKETPLACE_URL === 'string' && OVERWRITE_INTERNAL_MARKETPLACE_URL.length > 0) { + appsInitParams.marketplaceUrl = OVERWRITE_INTERNAL_MARKETPLACE_URL; + } + + this.orchestrator.initialize(appsInitParams); + } + + public static getOrchestrator(db?: Db) { + if (!this.orchestrator) { + this.createOrchestrator(db); + } + + return this.orchestrator; + } +} diff --git a/apps/meteor/ee/server/apps/services/orchservice.ts b/apps/meteor/ee/server/apps/services/orchservice.ts new file mode 100644 index 0000000000000..90807b0004b56 --- /dev/null +++ b/apps/meteor/ee/server/apps/services/orchservice.ts @@ -0,0 +1,91 @@ +import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; +import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; +import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import type { Db } from 'mongodb'; +import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/externalComponent'; +import type { IAppsPersistenceModel } from '@rocket.chat/model-typings'; +import type { IAppsService } from '@rocket.chat/core-services'; +import { ServiceClass } from '@rocket.chat/core-services'; + +import { OrchestratorFactory } from './orchestratorFactory'; +import type { AppServerOrchestrator } from '../orchestrator'; + +export class AppsOrchestratorService extends ServiceClass implements IAppsService { + protected name = 'apps'; + + private apps: AppServerOrchestrator; + + constructor(db: Db) { + super(); + + this.apps = OrchestratorFactory.getOrchestrator(db); + } + + async started(): Promise { + return this.apps.load(); + } + + async triggerEvent(event: string, ...payload: any): Promise { + return this.apps.triggerEvent(event, ...payload); + } + + async updateAppsMarketplaceInfo(apps: Array): Promise { + return this.apps.updateAppsMarketplaceInfo(apps); + } + + initialize(): void { + // Do we need this? + } + + async load(): Promise { + return this.apps.load(); + } + + async unload(): Promise { + return this.apps.unload(); + } + + isLoaded(): boolean { + return this.apps.isLoaded(); + } + + isInitialized(): boolean { + return this.apps.isInitialized(); + } + + getPersistenceModel(): IAppsPersistenceModel { + return this.apps.getPersistenceModel(); + } + + getMarketplaceUrl(): string { + return this.apps.getMarketplaceUrl() as string; + } + + rocketChatLoggerWarn(obj: T, args: any[]) { + return this.apps.getRocketChatLogger()?.warn(obj, args); + } + + getProvidedComponents(): IExternalComponent[] { + return this.apps.getProvidedComponents(); + } + + rocketChatLoggerError(obj: T, args: any[]) { + return this.apps.getRocketChatLogger()?.error(obj, args); + } + + retrieveOneFromStorage(appId: string): Promise { + return this.apps.getStorage()!.retrieveOne(appId); + } + + fetchAppSourceStorage(storageItem: IAppStorageItem): Promise | undefined { + return this.apps.getAppSourceStorage()?.fetch(storageItem); + } + + setStorage(value: string): void { + return this.apps.getAppSourceStorage()?.setStorage(value); + } + + setFileSystemStoragePath(value: string): void { + return this.apps.getAppSourceStorage()?.setFileSystemStoragePath(value); + } +} diff --git a/apps/meteor/server/services/apps-engine/service.ts b/apps/meteor/ee/server/apps/services/service.ts similarity index 85% rename from apps/meteor/server/services/apps-engine/service.ts rename to apps/meteor/ee/server/apps/services/service.ts index 0c481c1684526..4c72f9ca98907 100644 --- a/apps/meteor/server/services/apps-engine/service.ts +++ b/apps/meteor/ee/server/apps/services/service.ts @@ -6,16 +6,49 @@ import type { ISetting } from '@rocket.chat/core-typings'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import type { IGetAppsFilter } from '@rocket.chat/apps-engine/server/IGetAppsFilter'; +import type { Db } from 'mongodb'; -import { Apps, AppEvents } from '../../../ee/server/apps/orchestrator'; -import { SystemLogger } from '../../lib/logger/system'; +import type { AppServerOrchestrator } from '../orchestrator'; +import { Apps, AppEvents } from '../orchestrator'; +import { SystemLogger } from '../../../../server/lib/logger/system'; +import { OrchestratorFactory } from './orchestratorFactory'; export class AppsEngineService extends ServiceClassInternal implements IAppsEngineService { protected name = 'apps-engine'; - constructor() { + private apps: AppServerOrchestrator; + + constructor(db: Db) { super(); + this.apps = OrchestratorFactory.getOrchestrator(db); + } + + async started(): Promise { + this.initializeEventListeners(); + } + + async isInitialized(): Promise { + return Apps.isInitialized(); + } + + async getApps(query: IGetAppsFilter): Promise { + return Apps.getManager() + ?.get(query) + .map((app) => app.getApp().getInfo()); + } + + async getAppStorageItemById(appId: string): Promise { + const app = Apps.getManager()?.getOneById(appId); + + if (!app) { + return; + } + + return app.getStorageItem(); + } + + private initializeEventListeners() { this.onEvent('presence.status', async ({ user, previousStatus }): Promise => { await Apps.triggerEvent(AppEvents.IPostUserStatusChanged, { user, @@ -89,24 +122,4 @@ export class AppsEngineService extends ServiceClassInternal implements IAppsEngi await appManager.getSettingsManager().updateAppSetting(appId, setting as any); }); } - - isInitialized(): boolean { - return Apps.isInitialized(); - } - - async getApps(query: IGetAppsFilter): Promise { - return Apps.getManager() - ?.get(query) - .map((app) => app.getApp().getInfo()); - } - - async getAppStorageItemById(appId: string): Promise { - const app = Apps.getManager()?.getOneById(appId); - - if (!app) { - return; - } - - return app.getStorageItem(); - } } diff --git a/apps/meteor/ee/server/apps/services/statisticsService.ts b/apps/meteor/ee/server/apps/services/statisticsService.ts new file mode 100644 index 0000000000000..92578378e97d4 --- /dev/null +++ b/apps/meteor/ee/server/apps/services/statisticsService.ts @@ -0,0 +1,40 @@ +import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import { ServiceClass } from '@rocket.chat/core-services'; +import type { IAppsStatisticsService } from '@rocket.chat/core-services'; + +import { OrchestratorFactory } from './orchestratorFactory'; +import type { AppServerOrchestrator } from '../orchestrator'; + +export type AppStatistics = { + totalInstalled: number | false; + totalActive: number | false; + totalFailed: number | false; +}; + +export class AppsStatisticsService extends ServiceClass implements IAppsStatisticsService { + protected name = 'apps'; + + private apps: AppServerOrchestrator; + + constructor() { + super(); + + this.apps = OrchestratorFactory.getOrchestrator(); + } + + getStatistics(): AppStatistics { + const isInitialized = this.apps.isInitialized(); + const manager = this.apps.getManager(); + + const totalInstalled = isInitialized && manager?.get().length; + const totalActive = isInitialized && manager?.get({ enabled: true }).length; + const totalFailed = + isInitialized && manager?.get({ disabled: true }).filter((app: any) => app.status !== AppStatus.MANUALLY_DISABLED).length; + + return { + totalInstalled: totalInstalled ?? false, + totalActive: totalActive ?? false, + totalFailed: totalFailed ?? false, + }; + } +} diff --git a/apps/meteor/ee/server/apps/services/videoManagerService.ts b/apps/meteor/ee/server/apps/services/videoManagerService.ts new file mode 100644 index 0000000000000..75a6846fefe3a --- /dev/null +++ b/apps/meteor/ee/server/apps/services/videoManagerService.ts @@ -0,0 +1,70 @@ +import type { IVideoConferenceUser, VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences'; +import type { VideoConfData, VideoConfDataExtended, IVideoConferenceOptions } from '@rocket.chat/apps-engine/definition/videoConfProviders'; +import type { AppVideoConfProviderManager } from '@rocket.chat/apps-engine/server/managers'; +import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit'; +import { ServiceClass } from '@rocket.chat/core-services'; +import type { IAppsVideoManagerService } from '@rocket.chat/core-services'; + +import { OrchestratorFactory } from './orchestratorFactory'; +import type { AppServerOrchestrator } from '../orchestrator'; + +export class AppsVideoManagerService extends ServiceClass implements IAppsVideoManagerService { + protected name = 'apps'; + + private apps: AppServerOrchestrator; + + constructor() { + super(); + this.apps = OrchestratorFactory.getOrchestrator(); + } + + private getVideoConfProviderManager(): AppVideoConfProviderManager { + if (!this.apps.isLoaded()) { + throw new Error('apps-engine-not-loaded'); + } + + const manager = this.apps.getManager()?.getVideoConfProviderManager(); + if (!manager) { + throw new Error('no-videoconf-provider-app'); + } + + return manager; + } + + async isFullyConfigured(providerName: string): Promise { + return this.getVideoConfProviderManager().isFullyConfigured(providerName); + } + + async generateUrl(providerName: string, call: VideoConfData): Promise { + return this.getVideoConfProviderManager().generateUrl(providerName, call); + } + + async customizeUrl( + providerName: string, + call: VideoConfDataExtended, + user?: IVideoConferenceUser | undefined, + options?: IVideoConferenceOptions | undefined, + ): Promise { + return this.getVideoConfProviderManager().customizeUrl(providerName, call, user, options); + } + + async onUserJoin(providerName: string, call: VideoConference, user?: IVideoConferenceUser | undefined): Promise { + return this.getVideoConfProviderManager().onUserJoin(providerName, call, user); + } + + async onNewVideoConference(providerName: string, call: VideoConference): Promise { + return this.getVideoConfProviderManager().onNewVideoConference(providerName, call); + } + + async onVideoConferenceChanged(providerName: string, call: VideoConference): Promise { + return this.getVideoConfProviderManager().onVideoConferenceChanged(providerName, call); + } + + async getVideoConferenceInfo( + providerName: string, + call: VideoConference, + user?: IVideoConferenceUser | undefined, + ): Promise { + return this.getVideoConfProviderManager().getVideoConferenceInfo(providerName, call, user); + } +} diff --git a/apps/meteor/server/services/startup.ts b/apps/meteor/server/services/startup.ts index 004d4f25a4b41..ef64059c5d6c8 100644 --- a/apps/meteor/server/services/startup.ts +++ b/apps/meteor/server/services/startup.ts @@ -3,7 +3,7 @@ import { api } from '@rocket.chat/core-services'; import { OmnichannelTranscript, QueueWorker } from '@rocket.chat/omnichannel-services'; import { AnalyticsService } from './analytics/service'; -import { AppsEngineService } from './apps-engine/service'; +import { AppsEngineService } from '../../ee/server/apps/services/service'; import { AuthorizationLivechat } from '../../app/livechat/server/roomAccessValidator.internalService'; import { BannerService } from './banner/service'; import { LDAPService } from './ldap/service'; @@ -30,7 +30,7 @@ import { Logger } from '../lib/logger/Logger'; const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; -api.registerService(new AppsEngineService()); +api.registerService(new AppsEngineService(db)); api.registerService(new AnalyticsService()); api.registerService(new AuthorizationLivechat()); api.registerService(new BannerService()); diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index 82a3f92598ad2..db94a7f196759 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -42,6 +42,12 @@ import type { ITranslationService } from './types/ITranslationService'; import type { IMessageService } from './types/IMessageService'; import type { ISettingsService } from './types/ISettingsService'; import type { IOmnichannelIntegrationService } from './types/IOmnichannelIntegrationService'; +import { IAppsApiService } from './types/IAppsApiService'; +import { IAppsManagerService } from './types/IAppsManagerService'; +import { IAppsService } from './types/IAppsService'; +import { IAppsStatisticsService } from './types/IAppsStatisticsService'; +import { IAppsVideoManagerService } from './types/IAppsVideoManagerService'; +import { IAppsConverterService } from './types/IAppsConverterService'; export { asyncLocalStorage } from './lib/asyncLocalStorage'; export { MeteorError, isMeteorError } from './MeteorError'; @@ -58,7 +64,13 @@ export { FindVoipRoomsParams, IAccount, IAnalyticsService, + IAppsApiService, + IAppsConverterService, IAppsEngineService, + IAppsManagerService, + IAppsService, + IAppsStatisticsService, + IAppsVideoManagerService, IAuthorization, IAuthorizationLivechat, IAuthorizationVoip, @@ -119,6 +131,12 @@ export { // TODO think in a way to not have to pass the service name to proxify here as well export const Authorization = proxifyWithWait('authorization'); export const Apps = proxifyWithWait('apps-engine'); +// export const Apps = proxifyWithWait('apps'); +export const AppsStatistics = proxifyWithWait('apps'); +export const AppsConverter = proxifyWithWait('apps'); +export const AppsManager = proxifyWithWait('apps'); +export const AppsVideoManager = proxifyWithWait('apps'); +export const AppsApiService = proxifyWithWait('apps'); export const Presence = proxifyWithWait('presence'); export const Account = proxifyWithWait('accounts'); export const License = proxifyWithWait('license'); diff --git a/packages/core-services/src/types/IAppsApiService.ts b/packages/core-services/src/types/IAppsApiService.ts new file mode 100644 index 0000000000000..a4a97bda97f4c --- /dev/null +++ b/packages/core-services/src/types/IAppsApiService.ts @@ -0,0 +1,14 @@ +import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api'; +import type { Request, Response } from 'express'; + +export interface IRequestWithPrivateHash extends Request { + _privateHash?: string; + content?: any; +} + +export interface IAppsApiService { + handlePublicRequest(req: Request, res: Response): Promise; + handlePrivateRequest(req: IRequestWithPrivateHash, res: Response): Promise; + registerApi(endpoint: IApiEndpoint, appId: string): Promise; + unregisterApi(appId: string): Promise; +} diff --git a/packages/core-services/src/types/IAppsConverterService.ts b/packages/core-services/src/types/IAppsConverterService.ts new file mode 100644 index 0000000000000..87d0603007581 --- /dev/null +++ b/packages/core-services/src/types/IAppsConverterService.ts @@ -0,0 +1,12 @@ +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 { IVisitor } from '@rocket.chat/apps-engine/definition/livechat'; + +export interface IAppsConverterService { + convertRoomById(id: string): Promise; + convertMessageById(id: string): Promise; + convertVistitorByToken(id: string): Promise; + convertUserToApp(user: any): Promise; + convertUserById(id: string): Promise; +} diff --git a/packages/core-services/src/types/IAppsManagerService.ts b/packages/core-services/src/types/IAppsManagerService.ts new file mode 100644 index 0000000000000..7e92b739e80bb --- /dev/null +++ b/packages/core-services/src/types/IAppsManagerService.ts @@ -0,0 +1,40 @@ +import type { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api'; +import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; +import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; +import type { + ISlashCommandPreview, + ISlashCommandPreviewItem, + SlashCommandContext, +} from '@rocket.chat/apps-engine/definition/slashcommands'; +import type { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; +import type { IAppInstallParameters, IAppUninstallParameters } from '@rocket.chat/apps-engine/server/AppManager'; +import type { AppFabricationFulfillment } from '@rocket.chat/apps-engine/server/compiler'; +import type { IGetAppsFilter } from '@rocket.chat/apps-engine/server/IGetAppsFilter'; +import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; +import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; + +export interface IAppsManagerService { + get(filter?: IGetAppsFilter): Promise; + add(appPackage: Buffer, installationParameters: IAppInstallParameters): Promise; + remove(id: string, uninstallationParameters: IAppUninstallParameters): Promise; + removeLocal(id: string): Promise; + update(appPackage: Buffer, permissionsGranted: Array, updateOptions?: any): Promise; + updateLocal(stored: IAppStorageItem, appPackageOrInstance: ProxiedApp | Buffer): Promise; + enable(appId: string): Promise; + disable(appId: string): Promise; + loadOne(appId: string): Promise; + getOneById(appId: string): Promise; + getAllActionButtons(): Promise; + updateAppSetting(appId: string, setting: ISetting): Promise; + getAppSettings(appId: string): Promise<{ [key: string]: ISetting } | undefined>; + listApis(appId: string): Promise | undefined>; + changeStatus(appId: string, status: AppStatus): Promise; + getCommandPreviews(command: string, context: SlashCommandContext): Promise; + commandExecutePreview( + command: string, + previewItem: ISlashCommandPreviewItem, + context: SlashCommandContext, + ): Promise; + commandExecuteCommand(command: string, context: SlashCommandContext): Promise; +} diff --git a/packages/core-services/src/types/IAppsService.ts b/packages/core-services/src/types/IAppsService.ts new file mode 100644 index 0000000000000..65d41df8d3b8f --- /dev/null +++ b/packages/core-services/src/types/IAppsService.ts @@ -0,0 +1,25 @@ +import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/externalComponent'; +import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; +import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; +import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import type { IAppsPersistenceModel } from '@rocket.chat/model-typings'; + +export interface IAppsService { + triggerEvent: (event: string, ...payload: any) => Promise; + updateAppsMarketplaceInfo: (apps: Array) => Promise; + load: () => Promise; + unload: () => Promise; + isLoaded: () => Promise; + isInitialized: () => Promise; + getPersistenceModel: () => Promise; + getMarketplaceUrl: () => Promise; + getProvidedComponents: () => Promise; + rocketChatLoggerWarn(obj: T, args: any[]): Promise; + rocketChatLoggerError(obj: T, args: any[]): Promise; + retrieveOneFromStorage(appId: string): Promise; + fetchAppSourceStorage(storageItem: IAppStorageItem): Promise; + setFrameworkEnabled: (value: boolean) => Promise; + setDevelopmentMode: (value: boolean) => Promise; + setStorage(value: string): Promise; + setFileSystemStoragePath(value: string): Promise; +} diff --git a/packages/core-services/src/types/IAppsStatisticsService.ts b/packages/core-services/src/types/IAppsStatisticsService.ts new file mode 100644 index 0000000000000..04bdfaa4446b2 --- /dev/null +++ b/packages/core-services/src/types/IAppsStatisticsService.ts @@ -0,0 +1,8 @@ +export type AppStatistics = { + totalInstalled: number | false; + totalActive: number | false; + totalFailed: number | false; +}; +export interface IAppsStatisticsService { + getStatistics: () => Promise; +} diff --git a/packages/core-services/src/types/IAppsVideoManagerService.ts b/packages/core-services/src/types/IAppsVideoManagerService.ts new file mode 100644 index 0000000000000..1558a6ca44a66 --- /dev/null +++ b/packages/core-services/src/types/IAppsVideoManagerService.ts @@ -0,0 +1,18 @@ +import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit'; +import type { IVideoConferenceUser, VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences'; +import type { VideoConfData, VideoConfDataExtended, IVideoConferenceOptions } from '@rocket.chat/apps-engine/definition/videoConfProviders'; + +export interface IAppsVideoManagerService { + isFullyConfigured(providerName: string): Promise; + generateUrl(providerName: string, call: VideoConfData): Promise; + customizeUrl( + providerName: string, + call: VideoConfDataExtended, + user?: IVideoConferenceUser, + options?: IVideoConferenceOptions, + ): Promise; + onUserJoin(providerName: string, call: VideoConference, user?: IVideoConferenceUser): Promise; + onNewVideoConference(providerName: string, call: VideoConference): Promise; + onVideoConferenceChanged(providerName: string, call: VideoConference): Promise; + getVideoConferenceInfo(providerName: string, call: VideoConference, user?: IVideoConferenceUser): Promise; +} From fe7df73eb42f71a131685a2dcd7153a16b8eab7c Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 18 Apr 2023 17:08:19 -0300 Subject: [PATCH 02/16] Merge AppsService into AppsEngineService --- .../server/apps/services/converterService.ts | 1 + .../lib/transformProxiedAppToAppResult.ts | 14 +++ .../ee/server/apps/services/managerService.ts | 36 ++++---- .../apps/services/orchestratorFactory.ts | 12 ++- .../ee/server/apps/services/orchservice.ts | 91 ------------------- .../meteor/ee/server/apps/services/service.ts | 54 ++++++++++- packages/core-services/src/index.ts | 6 +- .../src/types/IAppsEngineService.ts | 25 ++++- .../src/types/IAppsManagerService.ts | 15 ++- .../core-services/src/types/IAppsService.ts | 25 ----- 10 files changed, 120 insertions(+), 159 deletions(-) create mode 100644 apps/meteor/ee/server/apps/services/lib/transformProxiedAppToAppResult.ts delete mode 100644 apps/meteor/ee/server/apps/services/orchservice.ts delete mode 100644 packages/core-services/src/types/IAppsService.ts diff --git a/apps/meteor/ee/server/apps/services/converterService.ts b/apps/meteor/ee/server/apps/services/converterService.ts index e176a76abc34c..aaed20823b418 100644 --- a/apps/meteor/ee/server/apps/services/converterService.ts +++ b/apps/meteor/ee/server/apps/services/converterService.ts @@ -15,6 +15,7 @@ export class AppsConverterService extends ServiceClass implements IAppsConverter constructor() { super(); + this.apps = OrchestratorFactory.getOrchestrator(); } diff --git a/apps/meteor/ee/server/apps/services/lib/transformProxiedAppToAppResult.ts b/apps/meteor/ee/server/apps/services/lib/transformProxiedAppToAppResult.ts new file mode 100644 index 0000000000000..26093d28ebdab --- /dev/null +++ b/apps/meteor/ee/server/apps/services/lib/transformProxiedAppToAppResult.ts @@ -0,0 +1,14 @@ +import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; +import type { AppsEngineAppResult } from '@rocket.chat/core-services'; + +export function transformProxiedAppToAppResult(app?: ProxiedApp): AppsEngineAppResult | undefined { + if (!app) { + return; + } + + return { + appId: app.getID(), + currentStatus: app.getStatus(), + storageItem: app.getStorageItem(), + }; +} diff --git a/apps/meteor/ee/server/apps/services/managerService.ts b/apps/meteor/ee/server/apps/services/managerService.ts index 6851ba5a15584..40fe5bd0c3964 100644 --- a/apps/meteor/ee/server/apps/services/managerService.ts +++ b/apps/meteor/ee/server/apps/services/managerService.ts @@ -1,11 +1,9 @@ import type { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api'; import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; -import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; import type { AppFabricationFulfillment } from '@rocket.chat/apps-engine/server/compiler'; import type { IAppInstallParameters, IAppUninstallParameters } from '@rocket.chat/apps-engine/server/AppManager'; -import type { IGetAppsFilter } from '@rocket.chat/apps-engine/server/IGetAppsFilter'; import type { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; import type { @@ -14,10 +12,11 @@ import type { ISlashCommandPreviewItem, } from '@rocket.chat/apps-engine/definition/slashcommands'; import { ServiceClass } from '@rocket.chat/core-services'; -import type { IAppsManagerService } from '@rocket.chat/core-services'; +import type { AppsEngineAppResult, IAppsManagerService } from '@rocket.chat/core-services'; import { OrchestratorFactory } from './orchestratorFactory'; import type { AppServerOrchestrator } from '../orchestrator'; +import { transformProxiedAppToAppResult } from './lib/transformProxiedAppToAppResult'; export class AppsManagerService extends ServiceClass implements IAppsManagerService { protected name = 'apps'; @@ -26,11 +25,12 @@ export class AppsManagerService extends ServiceClass implements IAppsManagerServ constructor() { super(); + this.apps = OrchestratorFactory.getOrchestrator(); } - async loadOne(appId: string): Promise { - return (this.apps.getManager() as any).loadOne(appId); // TO-DO: fix type + async loadOne(appId: string): Promise { + return this.apps.getManager()?.loadOne(appId).then(transformProxiedAppToAppResult); } async enable(appId: string): Promise { @@ -41,16 +41,12 @@ export class AppsManagerService extends ServiceClass implements IAppsManagerServ return this.apps.getManager()?.disable(appId); } - get(filter?: IGetAppsFilter | undefined): ProxiedApp[] { - return this.apps.getManager()?.get(filter) ?? []; - } - async add(appPackage: Buffer, installationParameters: IAppInstallParameters): Promise { return this.apps.getManager()?.add(appPackage, installationParameters); } - async remove(id: string, uninstallationParameters: IAppUninstallParameters): Promise { - return this.apps.getManager()?.remove(id, uninstallationParameters); + async remove(id: string, uninstallationParameters: IAppUninstallParameters): Promise { + return this.apps.getManager()?.remove(id, uninstallationParameters).then(transformProxiedAppToAppResult); } async removeLocal(id: string): Promise { @@ -65,31 +61,31 @@ export class AppsManagerService extends ServiceClass implements IAppsManagerServ return this.apps.getManager()?.update(appPackage, permissionsGranted, updateOptions); } - async updateLocal(stored: IAppStorageItem, appPackageOrInstance: ProxiedApp | Buffer): Promise { - this.apps.getManager()?.updateLocal(stored, appPackageOrInstance); + async updateLocal(stored: IAppStorageItem, appPackageOrInstance: Buffer): Promise { + return this.apps.getManager()?.updateLocal(stored, appPackageOrInstance); } - getOneById(appId: string): ProxiedApp | undefined { - return this.apps.getManager()?.getOneById(appId); + async getOneById(appId: string): Promise { + return transformProxiedAppToAppResult(this.apps.getManager()?.getOneById(appId)); } async updateAppSetting(appId: string, setting: ISetting): Promise { return this.apps.getManager()?.getSettingsManager().updateAppSetting(appId, setting); } - getAppSettings(appId: string): { [key: string]: ISetting } | undefined { + async getAppSettings(appId: string): Promise | undefined> { return this.apps.getManager()?.getSettingsManager().getAppSettings(appId); } - listApis(appId: string): IApiEndpointMetadata[] | undefined { + async listApis(appId: string): Promise { return this.apps.getManager()?.getApiManager().listApis(appId); } - async changeStatus(appId: string, status: AppStatus): Promise { - return this.apps.getManager()?.changeStatus(appId, status); + async changeStatus(appId: string, status: AppStatus): Promise { + return this.apps.getManager()?.changeStatus(appId, status).then(transformProxiedAppToAppResult); } - getAllActionButtons(): IUIActionButton[] { + async getAllActionButtons(): Promise { return this.apps.getManager()?.getUIActionButtonManager().getAllActionButtons() ?? []; } diff --git a/apps/meteor/ee/server/apps/services/orchestratorFactory.ts b/apps/meteor/ee/server/apps/services/orchestratorFactory.ts index 4dc4828da8aa3..b3833e3a96f7c 100644 --- a/apps/meteor/ee/server/apps/services/orchestratorFactory.ts +++ b/apps/meteor/ee/server/apps/services/orchestratorFactory.ts @@ -12,14 +12,15 @@ type AppsInitParams = { export class OrchestratorFactory { private static orchestrator: AppServerOrchestrator; - private static createOrchestrator(db?: Db) { + private static createOrchestrator(_db: Db) { const appsInitParams: AppsInitParams = { appsSourceStorageType: settings.get('Apps_Framework_Source_Package_Storage_Type'), appsSourceStorageFilesystemPath: settings.get('Apps_Framework_Source_Package_Storage_FileSystem_Path'), marketplaceUrl: 'https://marketplace.rocket.chat', }; - this.orchestrator = new AppServerOrchestrator(db); + this.orchestrator = new AppServerOrchestrator(); + // this.orchestrator = new AppServerOrchestrator(db); const { OVERWRITE_INTERNAL_MARKETPLACE_URL } = process.env || {}; @@ -27,11 +28,16 @@ export class OrchestratorFactory { appsInitParams.marketplaceUrl = OVERWRITE_INTERNAL_MARKETPLACE_URL; } - this.orchestrator.initialize(appsInitParams); + this.orchestrator.initialize(); + // this.orchestrator.initialize(appsInitParams); } public static getOrchestrator(db?: Db) { if (!this.orchestrator) { + if (!db) { + throw new Error('The database connection is required to initialize the Apps Engine Orchestrator.'); + } + this.createOrchestrator(db); } diff --git a/apps/meteor/ee/server/apps/services/orchservice.ts b/apps/meteor/ee/server/apps/services/orchservice.ts deleted file mode 100644 index 90807b0004b56..0000000000000 --- a/apps/meteor/ee/server/apps/services/orchservice.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; -import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; -import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; -import type { Db } from 'mongodb'; -import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/externalComponent'; -import type { IAppsPersistenceModel } from '@rocket.chat/model-typings'; -import type { IAppsService } from '@rocket.chat/core-services'; -import { ServiceClass } from '@rocket.chat/core-services'; - -import { OrchestratorFactory } from './orchestratorFactory'; -import type { AppServerOrchestrator } from '../orchestrator'; - -export class AppsOrchestratorService extends ServiceClass implements IAppsService { - protected name = 'apps'; - - private apps: AppServerOrchestrator; - - constructor(db: Db) { - super(); - - this.apps = OrchestratorFactory.getOrchestrator(db); - } - - async started(): Promise { - return this.apps.load(); - } - - async triggerEvent(event: string, ...payload: any): Promise { - return this.apps.triggerEvent(event, ...payload); - } - - async updateAppsMarketplaceInfo(apps: Array): Promise { - return this.apps.updateAppsMarketplaceInfo(apps); - } - - initialize(): void { - // Do we need this? - } - - async load(): Promise { - return this.apps.load(); - } - - async unload(): Promise { - return this.apps.unload(); - } - - isLoaded(): boolean { - return this.apps.isLoaded(); - } - - isInitialized(): boolean { - return this.apps.isInitialized(); - } - - getPersistenceModel(): IAppsPersistenceModel { - return this.apps.getPersistenceModel(); - } - - getMarketplaceUrl(): string { - return this.apps.getMarketplaceUrl() as string; - } - - rocketChatLoggerWarn(obj: T, args: any[]) { - return this.apps.getRocketChatLogger()?.warn(obj, args); - } - - getProvidedComponents(): IExternalComponent[] { - return this.apps.getProvidedComponents(); - } - - rocketChatLoggerError(obj: T, args: any[]) { - return this.apps.getRocketChatLogger()?.error(obj, args); - } - - retrieveOneFromStorage(appId: string): Promise { - return this.apps.getStorage()!.retrieveOne(appId); - } - - fetchAppSourceStorage(storageItem: IAppStorageItem): Promise | undefined { - return this.apps.getAppSourceStorage()?.fetch(storageItem); - } - - setStorage(value: string): void { - return this.apps.getAppSourceStorage()?.setStorage(value); - } - - setFileSystemStoragePath(value: string): void { - return this.apps.getAppSourceStorage()?.setFileSystemStoragePath(value); - } -} diff --git a/apps/meteor/ee/server/apps/services/service.ts b/apps/meteor/ee/server/apps/services/service.ts index 4c72f9ca98907..b6e7d62538900 100644 --- a/apps/meteor/ee/server/apps/services/service.ts +++ b/apps/meteor/ee/server/apps/services/service.ts @@ -1,5 +1,5 @@ import { ServiceClassInternal } from '@rocket.chat/core-services'; -import type { IAppsEngineService } from '@rocket.chat/core-services'; +import type { AppsEngineAppResult, IAppsEngineService } from '@rocket.chat/core-services'; import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { ISetting } from '@rocket.chat/core-typings'; @@ -7,11 +7,13 @@ import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import type { IGetAppsFilter } from '@rocket.chat/apps-engine/server/IGetAppsFilter'; import type { Db } from 'mongodb'; +import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/externalComponent'; import type { AppServerOrchestrator } from '../orchestrator'; import { Apps, AppEvents } from '../orchestrator'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { OrchestratorFactory } from './orchestratorFactory'; +import { transformProxiedAppToAppResult } from './lib/transformProxiedAppToAppResult'; export class AppsEngineService extends ServiceClassInternal implements IAppsEngineService { protected name = 'apps-engine'; @@ -26,16 +28,16 @@ export class AppsEngineService extends ServiceClassInternal implements IAppsEngi async started(): Promise { this.initializeEventListeners(); + + return this.apps.load(); } async isInitialized(): Promise { return Apps.isInitialized(); } - async getApps(query: IGetAppsFilter): Promise { - return Apps.getManager() - ?.get(query) - .map((app) => app.getApp().getInfo()); + async getApps(query?: IGetAppsFilter): Promise { + return Apps.getManager()?.get(query).map(transformProxiedAppToAppResult) as AppsEngineAppResult[]; } async getAppStorageItemById(appId: string): Promise { @@ -48,6 +50,48 @@ export class AppsEngineService extends ServiceClassInternal implements IAppsEngi return app.getStorageItem(); } + async triggerEvent(event: string, ...payload: any): Promise { + return this.apps.triggerEvent(event, ...payload); + } + + async updateAppsMarketplaceInfo(apps: Array): Promise { + return this.apps + .updateAppsMarketplaceInfo(apps) + .then((apps) => (apps ? (apps.map(transformProxiedAppToAppResult) as AppsEngineAppResult[]) : undefined)); + } + + async load(): Promise { + return this.apps.load(); + } + + async unload(): Promise { + return this.apps.unload(); + } + + async isLoaded(): Promise { + return this.apps.isLoaded(); + } + + async getMarketplaceUrl(): Promise { + return this.apps.getMarketplaceUrl() as string; + } + + async getProvidedComponents(): Promise { + return this.apps.getProvidedComponents(); + } + + async fetchAppSourceStorage(storageItem: IAppStorageItem): Promise { + return this.apps.getAppSourceStorage()?.fetch(storageItem); + } + + async setStorage(value: string): Promise { + return this.apps.getAppSourceStorage()?.setStorage(value); + } + + async setFileSystemStoragePath(value: string): Promise { + return this.apps.getAppSourceStorage()?.setFileSystemStoragePath(value); + } + private initializeEventListeners() { this.onEvent('presence.status', async ({ user, previousStatus }): Promise => { await Apps.triggerEvent(AppEvents.IPostUserStatusChanged, { diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index db94a7f196759..7b99a98af997a 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -3,7 +3,7 @@ import type { ISendFileLivechatMessageParams, ISendFileMessageParams, IUploadFil import type { IAuthorization, RoomAccessValidator } from './types/IAuthorization'; import type { IAuthorizationLivechat } from './types/IAuthorizationLivechat'; import type { IAuthorizationVoip } from './types/IAuthorizationVoip'; -import type { IAppsEngineService } from './types/IAppsEngineService'; +import type { IAppsEngineService, AppsEngineAppResult } from './types/IAppsEngineService'; import type { IPresence } from './types/IPresence'; import type { IAccount, ILoginResult } from './types/IAccount'; import type { ILicense } from './types/ILicense'; @@ -44,7 +44,6 @@ import type { ISettingsService } from './types/ISettingsService'; import type { IOmnichannelIntegrationService } from './types/IOmnichannelIntegrationService'; import { IAppsApiService } from './types/IAppsApiService'; import { IAppsManagerService } from './types/IAppsManagerService'; -import { IAppsService } from './types/IAppsService'; import { IAppsStatisticsService } from './types/IAppsStatisticsService'; import { IAppsVideoManagerService } from './types/IAppsVideoManagerService'; import { IAppsConverterService } from './types/IAppsConverterService'; @@ -67,8 +66,8 @@ export { IAppsApiService, IAppsConverterService, IAppsEngineService, + AppsEngineAppResult, IAppsManagerService, - IAppsService, IAppsStatisticsService, IAppsVideoManagerService, IAuthorization, @@ -131,7 +130,6 @@ export { // TODO think in a way to not have to pass the service name to proxify here as well export const Authorization = proxifyWithWait('authorization'); export const Apps = proxifyWithWait('apps-engine'); -// export const Apps = proxifyWithWait('apps'); export const AppsStatistics = proxifyWithWait('apps'); export const AppsConverter = proxifyWithWait('apps'); export const AppsManager = proxifyWithWait('apps'); diff --git a/packages/core-services/src/types/IAppsEngineService.ts b/packages/core-services/src/types/IAppsEngineService.ts index 5c67c27833ab9..3a7f8b8696743 100644 --- a/packages/core-services/src/types/IAppsEngineService.ts +++ b/packages/core-services/src/types/IAppsEngineService.ts @@ -1,9 +1,28 @@ -import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; import type { IGetAppsFilter } from '@rocket.chat/apps-engine/server/IGetAppsFilter'; +import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/externalComponent'; +import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; +// import { AppLicenseValidationResult } from '@rocket.chat/apps-engine/server/marketplace/license'; +export type AppsEngineAppResult = { + appId: string; + currentStatus: AppStatus; + storageItem: IAppStorageItem; + // latestValidationResult?: Pick; +}; export interface IAppsEngineService { - isInitialized(): boolean; - getApps(query: IGetAppsFilter): Promise; + getApps(query: IGetAppsFilter): Promise; getAppStorageItemById(appId: string): Promise; + triggerEvent: (event: string, ...payload: any) => Promise; + updateAppsMarketplaceInfo: (apps: Array) => Promise; + load: () => Promise; + unload: () => Promise; + isLoaded: () => Promise; + isInitialized: () => Promise; + getMarketplaceUrl: () => Promise; + getProvidedComponents: () => Promise; + fetchAppSourceStorage(storageItem: IAppStorageItem): Promise; + setStorage(value: string): Promise; + setFileSystemStoragePath(value: string): Promise; } diff --git a/packages/core-services/src/types/IAppsManagerService.ts b/packages/core-services/src/types/IAppsManagerService.ts index 7e92b739e80bb..417c6e3b92bbd 100644 --- a/packages/core-services/src/types/IAppsManagerService.ts +++ b/packages/core-services/src/types/IAppsManagerService.ts @@ -10,26 +10,25 @@ import type { import type { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; import type { IAppInstallParameters, IAppUninstallParameters } from '@rocket.chat/apps-engine/server/AppManager'; import type { AppFabricationFulfillment } from '@rocket.chat/apps-engine/server/compiler'; -import type { IGetAppsFilter } from '@rocket.chat/apps-engine/server/IGetAppsFilter'; -import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import type { AppsEngineAppResult } from './IAppsEngineService'; + export interface IAppsManagerService { - get(filter?: IGetAppsFilter): Promise; add(appPackage: Buffer, installationParameters: IAppInstallParameters): Promise; - remove(id: string, uninstallationParameters: IAppUninstallParameters): Promise; + remove(id: string, uninstallationParameters: IAppUninstallParameters): Promise; removeLocal(id: string): Promise; update(appPackage: Buffer, permissionsGranted: Array, updateOptions?: any): Promise; - updateLocal(stored: IAppStorageItem, appPackageOrInstance: ProxiedApp | Buffer): Promise; + updateLocal(stored: IAppStorageItem, appPackageOrInstance: Buffer): Promise; enable(appId: string): Promise; disable(appId: string): Promise; - loadOne(appId: string): Promise; - getOneById(appId: string): Promise; + loadOne(appId: string): Promise; + getOneById(appId: string): Promise; getAllActionButtons(): Promise; updateAppSetting(appId: string, setting: ISetting): Promise; getAppSettings(appId: string): Promise<{ [key: string]: ISetting } | undefined>; listApis(appId: string): Promise | undefined>; - changeStatus(appId: string, status: AppStatus): Promise; + changeStatus(appId: string, status: AppStatus): Promise; getCommandPreviews(command: string, context: SlashCommandContext): Promise; commandExecutePreview( command: string, diff --git a/packages/core-services/src/types/IAppsService.ts b/packages/core-services/src/types/IAppsService.ts deleted file mode 100644 index 65d41df8d3b8f..0000000000000 --- a/packages/core-services/src/types/IAppsService.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/externalComponent'; -import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; -import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; -import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; -import type { IAppsPersistenceModel } from '@rocket.chat/model-typings'; - -export interface IAppsService { - triggerEvent: (event: string, ...payload: any) => Promise; - updateAppsMarketplaceInfo: (apps: Array) => Promise; - load: () => Promise; - unload: () => Promise; - isLoaded: () => Promise; - isInitialized: () => Promise; - getPersistenceModel: () => Promise; - getMarketplaceUrl: () => Promise; - getProvidedComponents: () => Promise; - rocketChatLoggerWarn(obj: T, args: any[]): Promise; - rocketChatLoggerError(obj: T, args: any[]): Promise; - retrieveOneFromStorage(appId: string): Promise; - fetchAppSourceStorage(storageItem: IAppStorageItem): Promise; - setFrameworkEnabled: (value: boolean) => Promise; - setDevelopmentMode: (value: boolean) => Promise; - setStorage(value: string): Promise; - setFileSystemStoragePath(value: string): Promise; -} From 0f33e60ce3554c0d156228f280a95b5c14fd945e Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 18 Apr 2023 22:17:52 -0300 Subject: [PATCH 03/16] Adapt api service to microservice serialization --- .../ee/server/apps/services/apiService.ts | 148 +++++++++++++----- packages/core-services/src/index.ts | 4 +- .../src/types/IAppsApiService.ts | 14 +- 3 files changed, 122 insertions(+), 44 deletions(-) diff --git a/apps/meteor/ee/server/apps/services/apiService.ts b/apps/meteor/ee/server/apps/services/apiService.ts index 71b9585a36916..785a0949a0910 100644 --- a/apps/meteor/ee/server/apps/services/apiService.ts +++ b/apps/meteor/ee/server/apps/services/apiService.ts @@ -1,19 +1,40 @@ import type { RequestMethod } from '@rocket.chat/apps-engine/definition/accessors'; import type { IApiEndpoint, IApiRequest } from '@rocket.chat/apps-engine/definition/api'; import { Router } from 'express'; -import type { Request, Response, IRouter, RequestHandler, NextFunction } from 'express'; -import type { IAppsApiService, IRequestWithPrivateHash } from '@rocket.chat/core-services'; +import type { Request, NextFunction } from 'express'; +import type { AppsApiServiceResponse, IAppsApiService, IRequestWithPrivateHash } from '@rocket.chat/core-services'; import { ServiceClass } from '@rocket.chat/core-services'; +import type { Serialized } from '@rocket.chat/core-typings'; import { OrchestratorFactory } from './orchestratorFactory'; import type { AppServerOrchestrator } from '../orchestrator'; +/* eslint-disable @typescript-eslint/ban-types -- The details of the function are not important here */ + +/** + * This type is used to replace the Express Response object, as in the service it won't be + * possible to get an instance of the original Response generated by Express. + * + * We use the `resolve` function to return the response to the caller. + */ +type PromiseResponse = { + resolve: (response: AppsApiServiceResponse) => void; +}; +/* eslint-enable @typescript-eslint/ban-types */ + +type IAppsApiRequestHandler = (req: IRequestWithPrivateHash, res: PromiseResponse, next: NextFunction) => void; + +interface IAppsApiRouter { + (req: Serialized | IRequestWithPrivateHash, res: PromiseResponse, next: NextFunction): void; + all(path: string, ...handlers: IAppsApiRequestHandler[]): IAppsApiRouter; +} + export class AppsApiService extends ServiceClass implements IAppsApiService { protected name = 'apps'; private apps: AppServerOrchestrator; - protected appRouters: Map; + protected appRouters: Map; constructor() { super(); @@ -21,37 +42,75 @@ export class AppsApiService extends ServiceClass implements IAppsApiService { this.apps = OrchestratorFactory.getOrchestrator(); } - async handlePublicRequest(req: Request, res: Response): Promise { - const notFound = (): Response => res.sendStatus(404); - - const router = this.appRouters.get(req.params.appId); - - if (router) { - return router(req, res, notFound); - } + /* ---- ENDPOINT COMMUNICATION METHODS ---- */ + + /** + * This method triggers the execution of a public route registered by an app. + * + * It is supposed to be called by the ENDPOINT COMMUNICATOR in the core, as it is + * the component that interfaces directly with the Express server. + * + * The returning promise will ALWAYS resolve, even if the route is not found. + * + * The way we indicate an error to the caller is by returning a status code. + * + * We expect the caller to appropriately respond to their HTTP request based on + * the status code, headers and body returned + * + * @param req A dry request object, containing only information and no functions + * @returns A promise that resolve to AppsApiServiceResponse type + */ + async handlePublicRequest(req: Serialized): Promise { + return new Promise((resolve) => { + const notFound = () => resolve({ statusCode: 404, body: 'Not found' }); + + const router = this.appRouters.get(req.params.appId); + + if (router) { + return router(req, { resolve }, notFound); + } - notFound(); + notFound(); + }); } - async handlePrivateRequest(req: IRequestWithPrivateHash, res: Response): Promise { - const notFound = (): Response => res.sendStatus(404); - - const router = this.appRouters.get(req.params.appId); - - if (router) { - req._privateHash = req.params.hash; - return router(req, res, notFound); - } + /** + * This method triggers the execution of a private route registered by an app. + * + * It is supposed to be called by the ENDPOINT COMMUNICATOR in the core, as it is + * the component that interfaces directly with the Express server. + * + * The way we indicate an error to the caller is by returning a status code. + * + * We expect the caller to appropriately respond to their HTTP request based on + * the status code, headers and body returned + * + * @param req A dry request object, containing only information and no functions + * @returns A promise that resolves when the request is done + */ + handlePrivateRequest(req: IRequestWithPrivateHash): Promise { + return new Promise((resolve) => { + const notFound = () => resolve({ statusCode: 404, body: 'Not found' }); + + const router = this.appRouters.get(req.params.appId); + + if (router) { + req._privateHash = req.params.hash; + return router(req, { resolve }, notFound); + } - notFound(); + notFound(); + }); } - registerApi(endpoint: IApiEndpoint, appId: string): void { + /* ---- BRIDGE METHODS ---- */ + + async registerApi(endpoint: IApiEndpoint, appId: string): Promise { let router = this.appRouters.get(appId); if (!router) { // eslint-disable-next-line new-cap - router = Router(); + router = Router() as unknown as IAppsApiRouter; this.appRouters.set(appId, router); } @@ -67,24 +126,29 @@ export class AppsApiService extends ServiceClass implements IAppsApiService { } } + async unregisterApi(appId: string): Promise { + if (this.appRouters.get(appId)) { + this.appRouters.delete(appId); + } + } + + /* ---- PRIVATE METHODS ---- */ + private authMiddleware(authRequired: boolean) { - return (req: Request, res: Response, next: NextFunction): void => { + return (req: IRequestWithPrivateHash, res: PromiseResponse, next: NextFunction): void => { if (!req.user && authRequired) { - res.status(401).send('Unauthorized'); - return; + return res.resolve({ + statusCode: 401, + body: 'Unauthorized', + }); } + next(); }; } - unregisterApi(appId: string): void { - if (this.appRouters.get(appId)) { - this.appRouters.delete(appId); - } - } - - private _appApiExecutor(endpoint: IApiEndpoint, appId: string): RequestHandler { - return (req: IRequestWithPrivateHash, res: Response): void => { + private _appApiExecutor(endpoint: IApiEndpoint, appId: string) { + return (req: IRequestWithPrivateHash, { resolve }: PromiseResponse): void => { const request: IApiRequest = { method: req.method.toLowerCase() as RequestMethod, headers: req.headers as { [key: string]: string }, @@ -94,18 +158,24 @@ export class AppsApiService extends ServiceClass implements IAppsApiService { privateHash: req._privateHash, user: req.user && this.apps.getConverters()?.get('users')?.convertToApp(req.user), }; + this.apps .getManager() ?.getApiManager() .executeApi(appId, endpoint.path, request) .then(({ status, headers = {}, content }) => { - res.set(headers); - res.status(status); - res.send(content); + resolve({ + statusCode: status, + headers, + body: content, + }); }) .catch((reason) => { // Should we handle this as an error? - res.status(500).send(reason.message); + resolve({ + statusCode: 500, + body: reason.message, + }); }); }; } diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index 7b99a98af997a..b7de734ca23b7 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -42,7 +42,7 @@ import type { ITranslationService } from './types/ITranslationService'; import type { IMessageService } from './types/IMessageService'; import type { ISettingsService } from './types/ISettingsService'; import type { IOmnichannelIntegrationService } from './types/IOmnichannelIntegrationService'; -import { IAppsApiService } from './types/IAppsApiService'; +import { AppsApiServiceResponse, IAppsApiService, IRequestWithPrivateHash } from './types/IAppsApiService'; import { IAppsManagerService } from './types/IAppsManagerService'; import { IAppsStatisticsService } from './types/IAppsStatisticsService'; import { IAppsVideoManagerService } from './types/IAppsVideoManagerService'; @@ -60,6 +60,7 @@ export { IServiceContext, ServiceClass, IServiceClass, ServiceClassInternal } fr export { AutoUpdateRecord, + AppsApiServiceResponse, FindVoipRoomsParams, IAccount, IAnalyticsService, @@ -125,6 +126,7 @@ export { IMessageService, ISettingsService, IOmnichannelIntegrationService, + IRequestWithPrivateHash, }; // TODO think in a way to not have to pass the service name to proxify here as well diff --git a/packages/core-services/src/types/IAppsApiService.ts b/packages/core-services/src/types/IAppsApiService.ts index a4a97bda97f4c..0c2edd2ce1352 100644 --- a/packages/core-services/src/types/IAppsApiService.ts +++ b/packages/core-services/src/types/IAppsApiService.ts @@ -1,14 +1,20 @@ import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api'; +import type { Serialized } from '@rocket.chat/core-typings'; import type { Request, Response } from 'express'; -export interface IRequestWithPrivateHash extends Request { +export interface IRequestWithPrivateHash extends Serialized { _privateHash?: string; - content?: any; } +export type AppsApiServiceResponse = { + statusCode: number; + headers?: Record; + body?: string; +}; + export interface IAppsApiService { - handlePublicRequest(req: Request, res: Response): Promise; - handlePrivateRequest(req: IRequestWithPrivateHash, res: Response): Promise; + handlePublicRequest(req: Serialized, res: Response): Promise; + handlePrivateRequest(req: IRequestWithPrivateHash, res: Response): Promise; registerApi(endpoint: IApiEndpoint, appId: string): Promise; unregisterApi(appId: string): Promise; } From 255e54aa9797c83acc93ec1e782718c75063d723 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 18 Apr 2023 22:55:04 -0300 Subject: [PATCH 04/16] Fix types and usages --- apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts | 3 +-- apps/meteor/ee/server/apps/services/statisticsService.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts b/apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts index da81cf6f9c2ca..dbc61188e3a87 100644 --- a/apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts +++ b/apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts @@ -10,8 +10,7 @@ export async function isUnderAppLimits(licenseAppsConfig: NonNullable Apps.getAppStorageItemById(app.id))); - const activeAppsFromSameSource = storageItems.filter((item) => item && getInstallationSourceFromAppStorageItem(item) === source); + const activeAppsFromSameSource = apps.filter((item) => item && getInstallationSourceFromAppStorageItem(item.storageItem) === source); const configKey = `max${source.charAt(0).toUpperCase()}${source.slice(1)}Apps` as keyof typeof licenseAppsConfig; const configLimit = licenseAppsConfig[configKey]; diff --git a/apps/meteor/ee/server/apps/services/statisticsService.ts b/apps/meteor/ee/server/apps/services/statisticsService.ts index 92578378e97d4..1500d4f2ea8d9 100644 --- a/apps/meteor/ee/server/apps/services/statisticsService.ts +++ b/apps/meteor/ee/server/apps/services/statisticsService.ts @@ -22,7 +22,7 @@ export class AppsStatisticsService extends ServiceClass implements IAppsStatisti this.apps = OrchestratorFactory.getOrchestrator(); } - getStatistics(): AppStatistics { + async getStatistics(): Promise { const isInitialized = this.apps.isInitialized(); const manager = this.apps.getManager(); From 68f4d71f0d82a21a9cdb3a93441a0965d98109b8 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Wed, 19 Apr 2023 16:21:40 -0300 Subject: [PATCH 05/16] Fix startup lock --- apps/meteor/ee/server/apps/orchestrator.js | 8 ++++++-- .../meteor/ee/server/apps/services/orchestratorFactory.ts | 7 +++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/meteor/ee/server/apps/orchestrator.js b/apps/meteor/ee/server/apps/orchestrator.js index c6fdf7fb0aba3..a5ae3578dbaa4 100644 --- a/apps/meteor/ee/server/apps/orchestrator.js +++ b/apps/meteor/ee/server/apps/orchestrator.js @@ -122,6 +122,10 @@ export class AppServerOrchestrator { } getProvidedComponents() { + if (!this.isLoaded()) { + return []; + } + return this._manager.getExternalComponentManager().getProvidedComponents(); } @@ -134,7 +138,7 @@ export class AppServerOrchestrator { } isLoaded() { - return this.getManager().areAppsLoaded(); + return this.isInitialized() && this.getManager().areAppsLoaded(); } isDebugging() { @@ -161,7 +165,7 @@ export class AppServerOrchestrator { async load() { // Don't try to load it again if it has // already been loaded - if (this.isLoaded()) { + if (!this.isInitialized() || this.isLoaded()) { return; } diff --git a/apps/meteor/ee/server/apps/services/orchestratorFactory.ts b/apps/meteor/ee/server/apps/services/orchestratorFactory.ts index b3833e3a96f7c..0b18c14ff081b 100644 --- a/apps/meteor/ee/server/apps/services/orchestratorFactory.ts +++ b/apps/meteor/ee/server/apps/services/orchestratorFactory.ts @@ -1,7 +1,8 @@ import type { Db } from 'mongodb'; import { settings } from '../../../../app/settings/server'; -import { AppServerOrchestrator } from '../orchestrator'; +import type { AppServerOrchestrator } from '../orchestrator'; +import { Apps } from '../orchestrator'; type AppsInitParams = { appsSourceStorageFilesystemPath: any; @@ -10,7 +11,7 @@ type AppsInitParams = { }; export class OrchestratorFactory { - private static orchestrator: AppServerOrchestrator; + private static orchestrator: AppServerOrchestrator = Apps; private static createOrchestrator(_db: Db) { const appsInitParams: AppsInitParams = { @@ -19,7 +20,6 @@ export class OrchestratorFactory { marketplaceUrl: 'https://marketplace.rocket.chat', }; - this.orchestrator = new AppServerOrchestrator(); // this.orchestrator = new AppServerOrchestrator(db); const { OVERWRITE_INTERNAL_MARKETPLACE_URL } = process.env || {}; @@ -28,7 +28,6 @@ export class OrchestratorFactory { appsInitParams.marketplaceUrl = OVERWRITE_INTERNAL_MARKETPLACE_URL; } - this.orchestrator.initialize(); // this.orchestrator.initialize(appsInitParams); } From af4f5a739f1c34a5b553378b76aa4fe800f5c738 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Wed, 19 Apr 2023 22:03:27 -0300 Subject: [PATCH 06/16] Improve apps statistics service types --- .../ee/server/apps/services/statisticsService.ts | 10 ++-------- packages/core-services/src/index.ts | 3 ++- .../core-services/src/types/IAppsStatisticsService.ts | 5 +++-- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/apps/meteor/ee/server/apps/services/statisticsService.ts b/apps/meteor/ee/server/apps/services/statisticsService.ts index 1500d4f2ea8d9..6d2906aec42ee 100644 --- a/apps/meteor/ee/server/apps/services/statisticsService.ts +++ b/apps/meteor/ee/server/apps/services/statisticsService.ts @@ -1,16 +1,10 @@ import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import { ServiceClass } from '@rocket.chat/core-services'; -import type { IAppsStatisticsService } from '@rocket.chat/core-services'; +import type { AppsStatisticsResult, IAppsStatisticsService } from '@rocket.chat/core-services'; import { OrchestratorFactory } from './orchestratorFactory'; import type { AppServerOrchestrator } from '../orchestrator'; -export type AppStatistics = { - totalInstalled: number | false; - totalActive: number | false; - totalFailed: number | false; -}; - export class AppsStatisticsService extends ServiceClass implements IAppsStatisticsService { protected name = 'apps'; @@ -22,7 +16,7 @@ export class AppsStatisticsService extends ServiceClass implements IAppsStatisti this.apps = OrchestratorFactory.getOrchestrator(); } - async getStatistics(): Promise { + async getStatistics(): Promise { const isInitialized = this.apps.isInitialized(); const manager = this.apps.getManager(); diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index b7de734ca23b7..86e36004c422c 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -44,7 +44,7 @@ import type { ISettingsService } from './types/ISettingsService'; import type { IOmnichannelIntegrationService } from './types/IOmnichannelIntegrationService'; import { AppsApiServiceResponse, IAppsApiService, IRequestWithPrivateHash } from './types/IAppsApiService'; import { IAppsManagerService } from './types/IAppsManagerService'; -import { IAppsStatisticsService } from './types/IAppsStatisticsService'; +import { IAppsStatisticsService, AppsStatisticsResult } from './types/IAppsStatisticsService'; import { IAppsVideoManagerService } from './types/IAppsVideoManagerService'; import { IAppsConverterService } from './types/IAppsConverterService'; @@ -61,6 +61,7 @@ export { IServiceContext, ServiceClass, IServiceClass, ServiceClassInternal } fr export { AutoUpdateRecord, AppsApiServiceResponse, + AppsStatisticsResult, FindVoipRoomsParams, IAccount, IAnalyticsService, diff --git a/packages/core-services/src/types/IAppsStatisticsService.ts b/packages/core-services/src/types/IAppsStatisticsService.ts index 04bdfaa4446b2..1c7ca29bf37bf 100644 --- a/packages/core-services/src/types/IAppsStatisticsService.ts +++ b/packages/core-services/src/types/IAppsStatisticsService.ts @@ -1,8 +1,9 @@ -export type AppStatistics = { +export type AppsStatisticsResult = { totalInstalled: number | false; totalActive: number | false; totalFailed: number | false; }; + export interface IAppsStatisticsService { - getStatistics: () => Promise; + getStatistics: () => Promise; } From ecfdc3a147de20d6bd6543844107b13d64745295 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Wed, 19 Apr 2023 22:04:25 -0300 Subject: [PATCH 07/16] Deserializing AppFabricationFulfillment --- .../lib/transformAppFabricationFulfillment.ts | 17 +++++++++++++++++ .../ee/server/apps/services/managerService.ts | 8 ++++---- packages/core-services/src/index.ts | 3 ++- .../src/types/IAppsManagerService.ts | 15 ++++++++++++++- 4 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 apps/meteor/ee/server/apps/services/lib/transformAppFabricationFulfillment.ts diff --git a/apps/meteor/ee/server/apps/services/lib/transformAppFabricationFulfillment.ts b/apps/meteor/ee/server/apps/services/lib/transformAppFabricationFulfillment.ts new file mode 100644 index 0000000000000..779cfd80d7ff0 --- /dev/null +++ b/apps/meteor/ee/server/apps/services/lib/transformAppFabricationFulfillment.ts @@ -0,0 +1,17 @@ +import type { AppFabricationFulfillment as AppsEngineAppFabricationFulfillment } from '@rocket.chat/apps-engine/server/compiler'; +import type { AppFabricationFulfillment, AppsEngineAppResult } from '@rocket.chat/core-services'; + +import { transformProxiedAppToAppResult } from './transformProxiedAppToAppResult'; + +export function transformAppFabricationFulfillment(fulfillment: AppsEngineAppFabricationFulfillment): AppFabricationFulfillment { + return { + appId: fulfillment.getApp().getID(), + appsEngineResult: transformProxiedAppToAppResult(fulfillment.getApp()) as AppsEngineAppResult, + licenseValidationResult: { + errors: fulfillment.getLicenseValidationResult().getErrors() as Record, + warnings: fulfillment.getLicenseValidationResult().getWarnings() as Record, + }, + storageError: fulfillment.getStorageError(), + appUserError: fulfillment.getAppUserError() as { username: string; message: string }, + }; +} diff --git a/apps/meteor/ee/server/apps/services/managerService.ts b/apps/meteor/ee/server/apps/services/managerService.ts index 40fe5bd0c3964..ac779c0bf0157 100644 --- a/apps/meteor/ee/server/apps/services/managerService.ts +++ b/apps/meteor/ee/server/apps/services/managerService.ts @@ -2,7 +2,6 @@ import type { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/a import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; -import type { AppFabricationFulfillment } from '@rocket.chat/apps-engine/server/compiler'; import type { IAppInstallParameters, IAppUninstallParameters } from '@rocket.chat/apps-engine/server/AppManager'; import type { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; @@ -12,11 +11,12 @@ import type { ISlashCommandPreviewItem, } from '@rocket.chat/apps-engine/definition/slashcommands'; import { ServiceClass } from '@rocket.chat/core-services'; -import type { AppsEngineAppResult, IAppsManagerService } from '@rocket.chat/core-services'; +import type { AppFabricationFulfillment, AppsEngineAppResult, IAppsManagerService } from '@rocket.chat/core-services'; import { OrchestratorFactory } from './orchestratorFactory'; import type { AppServerOrchestrator } from '../orchestrator'; import { transformProxiedAppToAppResult } from './lib/transformProxiedAppToAppResult'; +import { transformAppFabricationFulfillment } from './lib/transformAppFabricationFulfillment'; export class AppsManagerService extends ServiceClass implements IAppsManagerService { protected name = 'apps'; @@ -42,7 +42,7 @@ export class AppsManagerService extends ServiceClass implements IAppsManagerServ } async add(appPackage: Buffer, installationParameters: IAppInstallParameters): Promise { - return this.apps.getManager()?.add(appPackage, installationParameters); + return this.apps.getManager()?.add(appPackage, installationParameters).then(transformAppFabricationFulfillment); } async remove(id: string, uninstallationParameters: IAppUninstallParameters): Promise { @@ -58,7 +58,7 @@ export class AppsManagerService extends ServiceClass implements IAppsManagerServ permissionsGranted: IPermission[], updateOptions = { loadApp: true }, ): Promise { - return this.apps.getManager()?.update(appPackage, permissionsGranted, updateOptions); + return this.apps.getManager()?.update(appPackage, permissionsGranted, updateOptions).then(transformAppFabricationFulfillment); } async updateLocal(stored: IAppStorageItem, appPackageOrInstance: Buffer): Promise { diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index 86e36004c422c..76d59a50efe4e 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -43,7 +43,7 @@ import type { IMessageService } from './types/IMessageService'; import type { ISettingsService } from './types/ISettingsService'; import type { IOmnichannelIntegrationService } from './types/IOmnichannelIntegrationService'; import { AppsApiServiceResponse, IAppsApiService, IRequestWithPrivateHash } from './types/IAppsApiService'; -import { IAppsManagerService } from './types/IAppsManagerService'; +import { AppFabricationFulfillment, IAppsManagerService } from './types/IAppsManagerService'; import { IAppsStatisticsService, AppsStatisticsResult } from './types/IAppsStatisticsService'; import { IAppsVideoManagerService } from './types/IAppsVideoManagerService'; import { IAppsConverterService } from './types/IAppsConverterService'; @@ -62,6 +62,7 @@ export { AutoUpdateRecord, AppsApiServiceResponse, AppsStatisticsResult, + AppFabricationFulfillment, FindVoipRoomsParams, IAccount, IAnalyticsService, diff --git a/packages/core-services/src/types/IAppsManagerService.ts b/packages/core-services/src/types/IAppsManagerService.ts index 417c6e3b92bbd..29ca818f995b9 100644 --- a/packages/core-services/src/types/IAppsManagerService.ts +++ b/packages/core-services/src/types/IAppsManagerService.ts @@ -9,11 +9,24 @@ import type { } from '@rocket.chat/apps-engine/definition/slashcommands'; import type { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; import type { IAppInstallParameters, IAppUninstallParameters } from '@rocket.chat/apps-engine/server/AppManager'; -import type { AppFabricationFulfillment } from '@rocket.chat/apps-engine/server/compiler'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; import type { AppsEngineAppResult } from './IAppsEngineService'; +export type AppFabricationFulfillment = { + appId: string; + appsEngineResult: AppsEngineAppResult; + storageError: string; + licenseValidationResult: { + errors: Record; + warnings: Record; + }; + appUserError: { + username: string; + message: string; + }; +}; + export interface IAppsManagerService { add(appPackage: Buffer, installationParameters: IAppInstallParameters): Promise; remove(id: string, uninstallationParameters: IAppUninstallParameters): Promise; From 4eae25ac5e903578545a5a9d9191becd2ccb15ef Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Wed, 19 Apr 2023 22:07:52 -0300 Subject: [PATCH 08/16] Improve comments in AppApiService --- apps/meteor/ee/server/apps/services/apiService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/meteor/ee/server/apps/services/apiService.ts b/apps/meteor/ee/server/apps/services/apiService.ts index 785a0949a0910..6a90e65329cf3 100644 --- a/apps/meteor/ee/server/apps/services/apiService.ts +++ b/apps/meteor/ee/server/apps/services/apiService.ts @@ -50,7 +50,7 @@ export class AppsApiService extends ServiceClass implements IAppsApiService { * It is supposed to be called by the ENDPOINT COMMUNICATOR in the core, as it is * the component that interfaces directly with the Express server. * - * The returning promise will ALWAYS resolve, even if the route is not found. + * The returning promise will ALWAYS resolve, even if there's an error in the handler. * * The way we indicate an error to the caller is by returning a status code. * @@ -80,6 +80,8 @@ export class AppsApiService extends ServiceClass implements IAppsApiService { * It is supposed to be called by the ENDPOINT COMMUNICATOR in the core, as it is * the component that interfaces directly with the Express server. * + * The returning promise will ALWAYS resolve, even if there's an error in the handler. + * * The way we indicate an error to the caller is by returning a status code. * * We expect the caller to appropriately respond to their HTTP request based on From 6f803aed516f2104a0df4e7a942143dd7e9807c0 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 25 Apr 2023 10:30:37 -0300 Subject: [PATCH 09/16] Rename apps services --- apps/meteor/ee/server/apps/services/apiService.ts | 2 +- .../meteor/ee/server/apps/services/converterService.ts | 2 +- apps/meteor/ee/server/apps/services/managerService.ts | 2 +- apps/meteor/ee/server/apps/services/service.ts | 4 ++-- .../ee/server/apps/services/statisticsService.ts | 2 +- .../ee/server/apps/services/videoManagerService.ts | 2 +- packages/core-services/src/index.ts | 10 +++++----- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/meteor/ee/server/apps/services/apiService.ts b/apps/meteor/ee/server/apps/services/apiService.ts index 6a90e65329cf3..670854761c1e0 100644 --- a/apps/meteor/ee/server/apps/services/apiService.ts +++ b/apps/meteor/ee/server/apps/services/apiService.ts @@ -30,7 +30,7 @@ interface IAppsApiRouter { } export class AppsApiService extends ServiceClass implements IAppsApiService { - protected name = 'apps'; + protected name = 'apps-api'; private apps: AppServerOrchestrator; diff --git a/apps/meteor/ee/server/apps/services/converterService.ts b/apps/meteor/ee/server/apps/services/converterService.ts index aaed20823b418..d8143ddae1b4c 100644 --- a/apps/meteor/ee/server/apps/services/converterService.ts +++ b/apps/meteor/ee/server/apps/services/converterService.ts @@ -9,7 +9,7 @@ import { OrchestratorFactory } from './orchestratorFactory'; import type { AppServerOrchestrator } from '../orchestrator'; export class AppsConverterService extends ServiceClass implements IAppsConverterService { - protected name = 'apps'; + protected name = 'apps-converter'; private apps: AppServerOrchestrator; diff --git a/apps/meteor/ee/server/apps/services/managerService.ts b/apps/meteor/ee/server/apps/services/managerService.ts index ac779c0bf0157..658b8263d003a 100644 --- a/apps/meteor/ee/server/apps/services/managerService.ts +++ b/apps/meteor/ee/server/apps/services/managerService.ts @@ -19,7 +19,7 @@ import { transformProxiedAppToAppResult } from './lib/transformProxiedAppToAppRe import { transformAppFabricationFulfillment } from './lib/transformAppFabricationFulfillment'; export class AppsManagerService extends ServiceClass implements IAppsManagerService { - protected name = 'apps'; + protected name = 'apps-manager'; private apps: AppServerOrchestrator; diff --git a/apps/meteor/ee/server/apps/services/service.ts b/apps/meteor/ee/server/apps/services/service.ts index b6e7d62538900..c5feab3e5be65 100644 --- a/apps/meteor/ee/server/apps/services/service.ts +++ b/apps/meteor/ee/server/apps/services/service.ts @@ -1,4 +1,4 @@ -import { ServiceClassInternal } from '@rocket.chat/core-services'; +import { ServiceClass } from '@rocket.chat/core-services'; import type { AppsEngineAppResult, IAppsEngineService } from '@rocket.chat/core-services'; import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; @@ -15,7 +15,7 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import { OrchestratorFactory } from './orchestratorFactory'; import { transformProxiedAppToAppResult } from './lib/transformProxiedAppToAppResult'; -export class AppsEngineService extends ServiceClassInternal implements IAppsEngineService { +export class AppsEngineService extends ServiceClass implements IAppsEngineService { protected name = 'apps-engine'; private apps: AppServerOrchestrator; diff --git a/apps/meteor/ee/server/apps/services/statisticsService.ts b/apps/meteor/ee/server/apps/services/statisticsService.ts index 6d2906aec42ee..a1d07915a7109 100644 --- a/apps/meteor/ee/server/apps/services/statisticsService.ts +++ b/apps/meteor/ee/server/apps/services/statisticsService.ts @@ -6,7 +6,7 @@ import { OrchestratorFactory } from './orchestratorFactory'; import type { AppServerOrchestrator } from '../orchestrator'; export class AppsStatisticsService extends ServiceClass implements IAppsStatisticsService { - protected name = 'apps'; + protected name = 'apps-statistics'; private apps: AppServerOrchestrator; diff --git a/apps/meteor/ee/server/apps/services/videoManagerService.ts b/apps/meteor/ee/server/apps/services/videoManagerService.ts index 75a6846fefe3a..cd449a43f9d7e 100644 --- a/apps/meteor/ee/server/apps/services/videoManagerService.ts +++ b/apps/meteor/ee/server/apps/services/videoManagerService.ts @@ -9,7 +9,7 @@ import { OrchestratorFactory } from './orchestratorFactory'; import type { AppServerOrchestrator } from '../orchestrator'; export class AppsVideoManagerService extends ServiceClass implements IAppsVideoManagerService { - protected name = 'apps'; + protected name = 'apps-video-manager'; private apps: AppServerOrchestrator; diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index 76d59a50efe4e..0a3bf24f83be7 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -134,11 +134,11 @@ export { // TODO think in a way to not have to pass the service name to proxify here as well export const Authorization = proxifyWithWait('authorization'); export const Apps = proxifyWithWait('apps-engine'); -export const AppsStatistics = proxifyWithWait('apps'); -export const AppsConverter = proxifyWithWait('apps'); -export const AppsManager = proxifyWithWait('apps'); -export const AppsVideoManager = proxifyWithWait('apps'); -export const AppsApiService = proxifyWithWait('apps'); +export const AppsStatistics = proxifyWithWait('apps-statistics'); +export const AppsConverter = proxifyWithWait('apps-converter'); +export const AppsManager = proxifyWithWait('apps-manager'); +export const AppsVideoManager = proxifyWithWait('apps-video-manager'); +export const AppsApiService = proxifyWithWait('apps-api'); export const Presence = proxifyWithWait('presence'); export const Account = proxifyWithWait('accounts'); export const License = proxifyWithWait('license'); From f004770bcfb69926f915a09f921c05a848b0a82b Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 25 Apr 2023 11:25:06 -0300 Subject: [PATCH 10/16] Register services --- apps/meteor/ee/server/apps/services/index.ts | 6 ++++++ apps/meteor/ee/server/startup/services.ts | 20 ++++++++++++++++++++ apps/meteor/server/services/startup.ts | 2 -- 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 apps/meteor/ee/server/apps/services/index.ts diff --git a/apps/meteor/ee/server/apps/services/index.ts b/apps/meteor/ee/server/apps/services/index.ts new file mode 100644 index 0000000000000..79d3bc3ad605d --- /dev/null +++ b/apps/meteor/ee/server/apps/services/index.ts @@ -0,0 +1,6 @@ +export { AppsApiService } from './apiService'; +export { AppsConverterService } from './converterService'; +export { AppsEngineService } from './service'; +export { AppsManagerService } from './managerService'; +export { AppsStatisticsService } from './statisticsService'; +export { AppsVideoManagerService } from './videoManagerService'; diff --git a/apps/meteor/ee/server/startup/services.ts b/apps/meteor/ee/server/startup/services.ts index 6aec5347f9807..8b3aad0154019 100644 --- a/apps/meteor/ee/server/startup/services.ts +++ b/apps/meteor/ee/server/startup/services.ts @@ -1,4 +1,5 @@ import { api } from '@rocket.chat/core-services'; +import { MongoInternals } from 'meteor/mongo'; import { EnterpriseSettings } from '../../app/settings/server/settings.internalService'; import { LDAPEEService } from '../local-services/ldap/service'; @@ -9,6 +10,14 @@ import { isRunningMs } from '../../../server/lib/isRunningMs'; import { FederationService } from '../../../server/services/federation/service'; import { FederationServiceEE } from '../local-services/federation/service'; import { isEnterprise, onLicense } from '../../app/license/server'; +import { + AppsApiService, + AppsConverterService, + AppsEngineService, + AppsManagerService, + AppsStatisticsService, + AppsVideoManagerService, +} from '../apps/services'; // TODO consider registering these services only after a valid license is added api.registerService(new EnterpriseSettings()); @@ -16,9 +25,20 @@ api.registerService(new LDAPEEService()); api.registerService(new LicenseService()); api.registerService(new MessageReadsService()); +const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; + +// This will need to be moved when we create the microservices +api.registerService(new AppsEngineService(db)); + // when not running micro services we want to start up the instance intercom if (!isRunningMs()) { api.registerService(new InstanceService()); + + api.registerService(new AppsConverterService()); + api.registerService(new AppsApiService()); + api.registerService(new AppsManagerService()); + api.registerService(new AppsStatisticsService()); + api.registerService(new AppsVideoManagerService()); } let federationService: FederationService; diff --git a/apps/meteor/server/services/startup.ts b/apps/meteor/server/services/startup.ts index ef64059c5d6c8..054fd23020b17 100644 --- a/apps/meteor/server/services/startup.ts +++ b/apps/meteor/server/services/startup.ts @@ -3,7 +3,6 @@ import { api } from '@rocket.chat/core-services'; import { OmnichannelTranscript, QueueWorker } from '@rocket.chat/omnichannel-services'; import { AnalyticsService } from './analytics/service'; -import { AppsEngineService } from '../../ee/server/apps/services/service'; import { AuthorizationLivechat } from '../../app/livechat/server/roomAccessValidator.internalService'; import { BannerService } from './banner/service'; import { LDAPService } from './ldap/service'; @@ -30,7 +29,6 @@ import { Logger } from '../lib/logger/Logger'; const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; -api.registerService(new AppsEngineService(db)); api.registerService(new AnalyticsService()); api.registerService(new AuthorizationLivechat()); api.registerService(new BannerService()); From 1673d9461f39b5fa48a2780128c3aacd93e346b7 Mon Sep 17 00:00:00 2001 From: Rafael Tapia Date: Wed, 14 Jun 2023 12:05:39 -0300 Subject: [PATCH 11/16] feat: register apps engine services --- apps/meteor/ee/server/startup/services.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/meteor/ee/server/startup/services.ts b/apps/meteor/ee/server/startup/services.ts index 8b3aad0154019..2f9658aea7c38 100644 --- a/apps/meteor/ee/server/startup/services.ts +++ b/apps/meteor/ee/server/startup/services.ts @@ -29,16 +29,15 @@ const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; // This will need to be moved when we create the microservices api.registerService(new AppsEngineService(db)); +api.registerService(new AppsConverterService()); +api.registerService(new AppsApiService()); +api.registerService(new AppsManagerService()); +api.registerService(new AppsStatisticsService()); +api.registerService(new AppsVideoManagerService()); // when not running micro services we want to start up the instance intercom if (!isRunningMs()) { api.registerService(new InstanceService()); - - api.registerService(new AppsConverterService()); - api.registerService(new AppsApiService()); - api.registerService(new AppsManagerService()); - api.registerService(new AppsStatisticsService()); - api.registerService(new AppsVideoManagerService()); } let federationService: FederationService; From 98d7c4cbd8c642f074557133fb7c4f4b9f1850d6 Mon Sep 17 00:00:00 2001 From: Rafael Tapia Date: Fri, 16 Jun 2023 14:56:18 -0300 Subject: [PATCH 12/16] feat: get statistics from apps-engine service --- .../app/metrics/server/lib/collectMetrics.ts | 2 +- .../statistics/server/lib/getAppsStatistics.js | 17 +++++++---------- .../app/statistics/server/lib/statistics.ts | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/apps/meteor/app/metrics/server/lib/collectMetrics.ts b/apps/meteor/app/metrics/server/lib/collectMetrics.ts index 44e193fa2fe55..d35a9a578d082 100644 --- a/apps/meteor/app/metrics/server/lib/collectMetrics.ts +++ b/apps/meteor/app/metrics/server/lib/collectMetrics.ts @@ -39,7 +39,7 @@ const setPrometheusData = async (): Promise => { metrics.ddpConnectedUsers.set(_.unique(authenticatedSessions.map((s) => s.userId)).length); // Apps metrics - const { totalInstalled, totalActive, totalFailed } = getAppsStatistics(); + const { totalInstalled, totalActive, totalFailed } = await getAppsStatistics(); metrics.totalAppsInstalled.set(totalInstalled || 0); metrics.totalAppsEnabled.set(totalActive || 0); diff --git a/apps/meteor/app/statistics/server/lib/getAppsStatistics.js b/apps/meteor/app/statistics/server/lib/getAppsStatistics.js index e44af22aa166b..955a8566f5c34 100644 --- a/apps/meteor/app/statistics/server/lib/getAppsStatistics.js +++ b/apps/meteor/app/statistics/server/lib/getAppsStatistics.js @@ -1,17 +1,14 @@ -import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import { AppsStatistics } from '@rocket.chat/core-services'; -import { Apps } from '../../../../ee/server/apps'; import { Info } from '../../../utils/server'; -export function getAppsStatistics() { +export async function getAppsStatistics() { + const { totalActive, totalFailed, totalInstalled } = await AppsStatistics.getStatistics(); + return { engineVersion: Info.marketplaceApiVersion, - totalInstalled: Apps.isInitialized() && Apps.getManager().get().length, - totalActive: Apps.isInitialized() && Apps.getManager().get({ enabled: true }).length, - totalFailed: - Apps.isInitialized() && - Apps.getManager() - .get({ disabled: true }) - .filter(({ app: { status } }) => status !== AppStatus.MANUALLY_DISABLED).length, + totalInstalled, + totalActive, + totalFailed, }; } diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 47c0b5b225575..197c61b8f4bb8 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -409,7 +409,7 @@ export const statistics = { }), ); - statistics.apps = getAppsStatistics(); + statistics.apps = await getAppsStatistics(); statistics.services = await getServicesStatistics(); statistics.importer = getImporterStatistics(); statistics.videoConf = await VideoConf.getStatistics(); From 0ebe9684a3f50e6356151f33572e54c9b7c0f20a Mon Sep 17 00:00:00 2001 From: Douglas Date: Sun, 25 Jun 2023 20:41:10 -0300 Subject: [PATCH 13/16] Apply change request --- .../ee/server/apps/services/apiService.ts | 22 +++++++++---------- packages/core-typings/src/Serialized.ts | 5 +++++ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/apps/meteor/ee/server/apps/services/apiService.ts b/apps/meteor/ee/server/apps/services/apiService.ts index 670854761c1e0..e026763841579 100644 --- a/apps/meteor/ee/server/apps/services/apiService.ts +++ b/apps/meteor/ee/server/apps/services/apiService.ts @@ -1,3 +1,5 @@ +import * as util from 'util'; + import type { RequestMethod } from '@rocket.chat/apps-engine/definition/accessors'; import type { IApiEndpoint, IApiRequest } from '@rocket.chat/apps-engine/definition/api'; import { Router } from 'express'; @@ -9,8 +11,6 @@ import type { Serialized } from '@rocket.chat/core-typings'; import { OrchestratorFactory } from './orchestratorFactory'; import type { AppServerOrchestrator } from '../orchestrator'; -/* eslint-disable @typescript-eslint/ban-types -- The details of the function are not important here */ - /** * This type is used to replace the Express Response object, as in the service it won't be * possible to get an instance of the original Response generated by Express. @@ -20,7 +20,6 @@ import type { AppServerOrchestrator } from '../orchestrator'; type PromiseResponse = { resolve: (response: AppsApiServiceResponse) => void; }; -/* eslint-enable @typescript-eslint/ban-types */ type IAppsApiRequestHandler = (req: IRequestWithPrivateHash, res: PromiseResponse, next: NextFunction) => void; @@ -60,7 +59,7 @@ export class AppsApiService extends ServiceClass implements IAppsApiService { * @param req A dry request object, containing only information and no functions * @returns A promise that resolve to AppsApiServiceResponse type */ - async handlePublicRequest(req: Serialized): Promise { + public async handlePublicRequest(req: Serialized): Promise { return new Promise((resolve) => { const notFound = () => resolve({ statusCode: 404, body: 'Not found' }); @@ -90,7 +89,7 @@ export class AppsApiService extends ServiceClass implements IAppsApiService { * @param req A dry request object, containing only information and no functions * @returns A promise that resolves when the request is done */ - handlePrivateRequest(req: IRequestWithPrivateHash): Promise { + public async handlePrivateRequest(req: IRequestWithPrivateHash): Promise { return new Promise((resolve) => { const notFound = () => resolve({ statusCode: 404, body: 'Not found' }); @@ -107,7 +106,7 @@ export class AppsApiService extends ServiceClass implements IAppsApiService { /* ---- BRIDGE METHODS ---- */ - async registerApi(endpoint: IApiEndpoint, appId: string): Promise { + public async registerApi(endpoint: IApiEndpoint, appId: string): Promise { let router = this.appRouters.get(appId); if (!router) { @@ -128,10 +127,8 @@ export class AppsApiService extends ServiceClass implements IAppsApiService { } } - async unregisterApi(appId: string): Promise { - if (this.appRouters.get(appId)) { - this.appRouters.delete(appId); - } + public async unregisterApi(appId: string): Promise { + this.appRouters.delete(appId); } /* ---- PRIVATE METHODS ---- */ @@ -151,12 +148,15 @@ export class AppsApiService extends ServiceClass implements IAppsApiService { private _appApiExecutor(endpoint: IApiEndpoint, appId: string) { return (req: IRequestWithPrivateHash, { resolve }: PromiseResponse): void => { + // Microservice serializer converts Buffers to Uint8Arrays, so we need to convert it back + const content = util.types.isTypedArray(req.body) ? Buffer.from(req.body) : req.body; + const request: IApiRequest = { + content, method: req.method.toLowerCase() as RequestMethod, headers: req.headers as { [key: string]: string }, query: (req.query as { [key: string]: string }) || {}, params: req.params || {}, - content: req.body, privateHash: req._privateHash, user: req.user && this.apps.getConverters()?.get('users')?.convertToApp(req.user), }; diff --git a/packages/core-typings/src/Serialized.ts b/packages/core-typings/src/Serialized.ts index c84077610ee8b..77ca11ff525a4 100644 --- a/packages/core-typings/src/Serialized.ts +++ b/packages/core-typings/src/Serialized.ts @@ -1,5 +1,10 @@ +// We need to use the `Function` type directly here +/* eslint-disable @typescript-eslint/ban-types */ + export type Serialized = T extends Date ? Exclude | string + : T extends Function + ? never : T extends boolean | number | string | null | undefined ? T : T extends {} From 2a2f1db7ff9ee06458fb8d17a98eaa5b57f1ae18 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Wed, 26 Jul 2023 21:01:00 -0300 Subject: [PATCH 14/16] Fix linting errors --- packages/core-services/src/types/IAppsConverterService.ts | 2 +- packages/core-services/src/types/IAppsManagerService.ts | 2 +- packages/core-services/src/types/IAppsVideoManagerService.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core-services/src/types/IAppsConverterService.ts b/packages/core-services/src/types/IAppsConverterService.ts index 87d0603007581..8acc0bde2259a 100644 --- a/packages/core-services/src/types/IAppsConverterService.ts +++ b/packages/core-services/src/types/IAppsConverterService.ts @@ -1,7 +1,7 @@ +import type { IVisitor } from '@rocket.chat/apps-engine/definition/livechat'; 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 { IVisitor } from '@rocket.chat/apps-engine/definition/livechat'; export interface IAppsConverterService { convertRoomById(id: string): Promise; diff --git a/packages/core-services/src/types/IAppsManagerService.ts b/packages/core-services/src/types/IAppsManagerService.ts index 29ca818f995b9..62a2016bd49e8 100644 --- a/packages/core-services/src/types/IAppsManagerService.ts +++ b/packages/core-services/src/types/IAppsManagerService.ts @@ -1,5 +1,5 @@ -import type { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api'; import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import type { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api'; import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import type { diff --git a/packages/core-services/src/types/IAppsVideoManagerService.ts b/packages/core-services/src/types/IAppsVideoManagerService.ts index 1558a6ca44a66..8f7b7617eb4c7 100644 --- a/packages/core-services/src/types/IAppsVideoManagerService.ts +++ b/packages/core-services/src/types/IAppsVideoManagerService.ts @@ -1,6 +1,6 @@ import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit'; -import type { IVideoConferenceUser, VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences'; import type { VideoConfData, VideoConfDataExtended, IVideoConferenceOptions } from '@rocket.chat/apps-engine/definition/videoConfProviders'; +import type { IVideoConferenceUser, VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences'; export interface IAppsVideoManagerService { isFullyConfigured(providerName: string): Promise; From b630bb3aff9c5328ea42989e6fe7c7f6af0c9f1e Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Wed, 26 Jul 2023 21:06:05 -0300 Subject: [PATCH 15/16] Typecheck hiccup --- apps/meteor/ee/server/apps/services/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/ee/server/apps/services/service.ts b/apps/meteor/ee/server/apps/services/service.ts index 91e065e8b059f..caed7fdf5ffa8 100644 --- a/apps/meteor/ee/server/apps/services/service.ts +++ b/apps/meteor/ee/server/apps/services/service.ts @@ -31,7 +31,7 @@ export class AppsEngineService extends ServiceClass implements IAppsEngineServic return this.apps.load(); } - isInitialized(): boolean { + async isInitialized(): Promise { return Apps.isInitialized(); } From fccb9cf708d473d02b18cbf3bc4f7bd9e55f2e12 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Wed, 26 Jul 2023 21:19:11 -0300 Subject: [PATCH 16/16] Further fix linting --- apps/meteor/ee/server/apps/services/apiService.ts | 6 +++--- .../ee/server/apps/services/converterService.ts | 6 +++--- .../ee/server/apps/services/managerService.ts | 14 +++++++------- .../ee/server/apps/services/statisticsService.ts | 2 +- .../ee/server/apps/services/videoManagerService.ts | 6 +++--- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/apps/meteor/ee/server/apps/services/apiService.ts b/apps/meteor/ee/server/apps/services/apiService.ts index e026763841579..c262d7dfe482d 100644 --- a/apps/meteor/ee/server/apps/services/apiService.ts +++ b/apps/meteor/ee/server/apps/services/apiService.ts @@ -2,14 +2,14 @@ import * as util from 'util'; import type { RequestMethod } from '@rocket.chat/apps-engine/definition/accessors'; import type { IApiEndpoint, IApiRequest } from '@rocket.chat/apps-engine/definition/api'; -import { Router } from 'express'; -import type { Request, NextFunction } from 'express'; import type { AppsApiServiceResponse, IAppsApiService, IRequestWithPrivateHash } from '@rocket.chat/core-services'; import { ServiceClass } from '@rocket.chat/core-services'; import type { Serialized } from '@rocket.chat/core-typings'; +import type { Request, NextFunction } from 'express'; +import { Router } from 'express'; -import { OrchestratorFactory } from './orchestratorFactory'; import type { AppServerOrchestrator } from '../orchestrator'; +import { OrchestratorFactory } from './orchestratorFactory'; /** * This type is used to replace the Express Response object, as in the service it won't be diff --git a/apps/meteor/ee/server/apps/services/converterService.ts b/apps/meteor/ee/server/apps/services/converterService.ts index d8143ddae1b4c..4058fee6f2079 100644 --- a/apps/meteor/ee/server/apps/services/converterService.ts +++ b/apps/meteor/ee/server/apps/services/converterService.ts @@ -1,12 +1,12 @@ -import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IVisitor } from '@rocket.chat/apps-engine/definition/livechat'; 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 { IVisitor } from '@rocket.chat/apps-engine/definition/livechat'; import { ServiceClass } from '@rocket.chat/core-services'; import type { IAppsConverterService } from '@rocket.chat/core-services'; -import { OrchestratorFactory } from './orchestratorFactory'; import type { AppServerOrchestrator } from '../orchestrator'; +import { OrchestratorFactory } from './orchestratorFactory'; export class AppsConverterService extends ServiceClass implements IAppsConverterService { protected name = 'apps-converter'; diff --git a/apps/meteor/ee/server/apps/services/managerService.ts b/apps/meteor/ee/server/apps/services/managerService.ts index 658b8263d003a..5ee4caea1e0c0 100644 --- a/apps/meteor/ee/server/apps/services/managerService.ts +++ b/apps/meteor/ee/server/apps/services/managerService.ts @@ -1,22 +1,22 @@ -import type { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api'; import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; -import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; +import type { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api'; import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; -import type { IAppInstallParameters, IAppUninstallParameters } from '@rocket.chat/apps-engine/server/AppManager'; -import type { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; -import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import type { SlashCommandContext, ISlashCommandPreview, ISlashCommandPreviewItem, } from '@rocket.chat/apps-engine/definition/slashcommands'; +import type { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; +import type { IAppInstallParameters, IAppUninstallParameters } from '@rocket.chat/apps-engine/server/AppManager'; +import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; import { ServiceClass } from '@rocket.chat/core-services'; import type { AppFabricationFulfillment, AppsEngineAppResult, IAppsManagerService } from '@rocket.chat/core-services'; -import { OrchestratorFactory } from './orchestratorFactory'; import type { AppServerOrchestrator } from '../orchestrator'; -import { transformProxiedAppToAppResult } from './lib/transformProxiedAppToAppResult'; import { transformAppFabricationFulfillment } from './lib/transformAppFabricationFulfillment'; +import { transformProxiedAppToAppResult } from './lib/transformProxiedAppToAppResult'; +import { OrchestratorFactory } from './orchestratorFactory'; export class AppsManagerService extends ServiceClass implements IAppsManagerService { protected name = 'apps-manager'; diff --git a/apps/meteor/ee/server/apps/services/statisticsService.ts b/apps/meteor/ee/server/apps/services/statisticsService.ts index a1d07915a7109..a325fbbf67c8f 100644 --- a/apps/meteor/ee/server/apps/services/statisticsService.ts +++ b/apps/meteor/ee/server/apps/services/statisticsService.ts @@ -2,8 +2,8 @@ import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import { ServiceClass } from '@rocket.chat/core-services'; import type { AppsStatisticsResult, IAppsStatisticsService } from '@rocket.chat/core-services'; -import { OrchestratorFactory } from './orchestratorFactory'; import type { AppServerOrchestrator } from '../orchestrator'; +import { OrchestratorFactory } from './orchestratorFactory'; export class AppsStatisticsService extends ServiceClass implements IAppsStatisticsService { protected name = 'apps-statistics'; diff --git a/apps/meteor/ee/server/apps/services/videoManagerService.ts b/apps/meteor/ee/server/apps/services/videoManagerService.ts index cd449a43f9d7e..bf442577d4414 100644 --- a/apps/meteor/ee/server/apps/services/videoManagerService.ts +++ b/apps/meteor/ee/server/apps/services/videoManagerService.ts @@ -1,12 +1,12 @@ -import type { IVideoConferenceUser, VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences'; +import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit'; import type { VideoConfData, VideoConfDataExtended, IVideoConferenceOptions } from '@rocket.chat/apps-engine/definition/videoConfProviders'; +import type { IVideoConferenceUser, VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences'; import type { AppVideoConfProviderManager } from '@rocket.chat/apps-engine/server/managers'; -import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit'; import { ServiceClass } from '@rocket.chat/core-services'; import type { IAppsVideoManagerService } from '@rocket.chat/core-services'; -import { OrchestratorFactory } from './orchestratorFactory'; import type { AppServerOrchestrator } from '../orchestrator'; +import { OrchestratorFactory } from './orchestratorFactory'; export class AppsVideoManagerService extends ServiceClass implements IAppsVideoManagerService { protected name = 'apps-video-manager';