Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a3cc5c9
Import services from apps-to-service branch
d-gubert Apr 18, 2023
5b84b57
Merge remote-tracking branch 'origin/develop' into feat/apps-engine-s…
d-gubert Apr 18, 2023
fe7df73
Merge AppsService into AppsEngineService
d-gubert Apr 18, 2023
0f33e60
Adapt api service to microservice serialization
d-gubert Apr 19, 2023
255e54a
Fix types and usages
d-gubert Apr 19, 2023
170b2d3
Merge branch 'develop' into feat/apps-engine-services
d-gubert Apr 19, 2023
68f4d71
Fix startup lock
d-gubert Apr 19, 2023
af4f5a7
Improve apps statistics service types
d-gubert Apr 20, 2023
ecfdc3a
Deserializing AppFabricationFulfillment
d-gubert Apr 20, 2023
4eae25a
Improve comments in AppApiService
d-gubert Apr 20, 2023
6f803ae
Rename apps services
d-gubert Apr 25, 2023
f004770
Register services
d-gubert Apr 25, 2023
9d0cfe4
Merge remote-tracking branch 'origin/develop' into feat/apps-engine-s…
d-gubert Apr 25, 2023
4d3557b
Merge branch 'develop' into feat/apps-engine-services
d-gubert Apr 26, 2023
fbf92cb
Merge branch 'develop' into feat/apps-engine-services
d-gubert Apr 26, 2023
44d733b
Merge branch 'develop' into feat/apps-engine-services
d-gubert Apr 28, 2023
f91052a
Merge branch 'develop' into feat/apps-engine-services
d-gubert May 2, 2023
e3c5183
Merge branch 'develop' into feat/apps-engine-services
d-gubert May 10, 2023
6af3553
Merge branch 'develop' into feat/apps-engine-services
d-gubert May 10, 2023
92abdc6
Merge branch 'develop' into feat/apps-engine-services
d-gubert May 12, 2023
06607df
Merge branch 'develop' into feat/apps-engine-services
May 26, 2023
534748a
Merge branch 'develop' into feat/apps-engine-services
May 31, 2023
74e4de1
Merge branch 'develop' into feat/apps-engine-services
May 31, 2023
7557790
Merge branch 'develop' into feat/apps-engine-services
Jun 5, 2023
ba404cb
Merge branch 'develop' into feat/apps-engine-services
d-gubert Jun 5, 2023
9c1ba65
Merge branch 'develop' into feat/apps-engine-services
Jun 6, 2023
052d76c
Merge branch 'develop' into feat/apps-engine-services
Jun 7, 2023
1673d94
feat: register apps engine services
Jun 14, 2023
c9a7a02
Merge branch 'develop' into feat/apps-engine-services
Jun 14, 2023
98d7c4c
feat: get statistics from apps-engine service
Jun 16, 2023
2075a78
Merge remote-tracking branch 'origin/develop' into feat/apps-engine-s…
d-gubert Jun 25, 2023
0ebe968
Apply change request
d-gubert Jun 25, 2023
8b0cc7b
Merge remote-tracking branch 'origin/develop' into feat/apps-engine-s…
d-gubert Jun 29, 2023
7c694fe
Merge remote-tracking branch 'origin/develop' into feat/apps-engine-s…
d-gubert Jul 26, 2023
2a2f1db
Fix linting errors
d-gubert Jul 27, 2023
b630bb3
Typecheck hiccup
d-gubert Jul 27, 2023
fccb9cf
Further fix linting
d-gubert Jul 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/meteor/app/metrics/server/lib/collectMetrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const setPrometheusData = async (): Promise<void> => {
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);
Expand Down
17 changes: 7 additions & 10 deletions apps/meteor/app/statistics/server/lib/getAppsStatistics.js
Original file line number Diff line number Diff line change
@@ -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/rocketchat.info';

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,
};
}
2 changes: 1 addition & 1 deletion apps/meteor/app/statistics/server/lib/statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ export const statistics = {
}),
);

statistics.apps = getAppsStatistics();
statistics.apps = await getAppsStatistics();
statistics.services = await getServicesStatistics();
statistics.importer = getImporterStatistics();
statistics.videoConf = await VideoConf.getStatistics();
Expand Down
3 changes: 1 addition & 2 deletions apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ export async function isUnderAppLimits(licenseAppsConfig: NonNullable<ILicense['
return true;
}

const storageItems = await Promise.all(apps.map((app) => 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];
Expand Down
8 changes: 6 additions & 2 deletions apps/meteor/ee/server/apps/orchestrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ export class AppServerOrchestrator {
}

getProvidedComponents() {
if (!this.isLoaded()) {
return [];
}

return this._manager.getExternalComponentManager().getProvidedComponents();
}

Expand All @@ -130,7 +134,7 @@ export class AppServerOrchestrator {
}

isLoaded() {
return this.getManager().areAppsLoaded();
return this.isInitialized() && this.getManager().areAppsLoaded();
}

isDebugging() {
Expand All @@ -157,7 +161,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;
}

Expand Down
184 changes: 184 additions & 0 deletions apps/meteor/ee/server/apps/services/apiService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
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 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 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
* 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;
};

type IAppsApiRequestHandler = (req: IRequestWithPrivateHash, res: PromiseResponse, next: NextFunction) => void;

interface IAppsApiRouter {
(req: Serialized<Request> | IRequestWithPrivateHash, res: PromiseResponse, next: NextFunction): void;
all(path: string, ...handlers: IAppsApiRequestHandler[]): IAppsApiRouter;
}

export class AppsApiService extends ServiceClass implements IAppsApiService {
protected name = 'apps-api';

private apps: AppServerOrchestrator;

protected appRouters: Map<string, IAppsApiRouter>;

constructor() {
super();
this.appRouters = new Map();
this.apps = OrchestratorFactory.getOrchestrator();
}

/* ---- 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 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
* 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
*/
public async handlePublicRequest(req: Serialized<Request>): Promise<AppsApiServiceResponse> {
return new Promise<AppsApiServiceResponse>((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();
});
}

/**
* 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 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
* 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
*/
public async handlePrivateRequest(req: IRequestWithPrivateHash): Promise<AppsApiServiceResponse> {
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();
});
}

/* ---- BRIDGE METHODS ---- */

public async registerApi(endpoint: IApiEndpoint, appId: string): Promise<void> {
let router = this.appRouters.get(appId);

if (!router) {
// eslint-disable-next-line new-cap
router = Router() as unknown as IAppsApiRouter;
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));
}
}

public async unregisterApi(appId: string): Promise<void> {
this.appRouters.delete(appId);
}

/* ---- PRIVATE METHODS ---- */

private authMiddleware(authRequired: boolean) {
return (req: IRequestWithPrivateHash, res: PromiseResponse, next: NextFunction): void => {
if (!req.user && authRequired) {
return res.resolve({
statusCode: 401,
body: 'Unauthorized',
});
}

next();
};
}

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 || {},
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 }) => {
resolve({
statusCode: status,
headers,
body: content,
});
})
.catch((reason) => {
// Should we handle this as an error?
resolve({
statusCode: 500,
body: reason.message,
});
});
};
}
}
41 changes: 41 additions & 0 deletions apps/meteor/ee/server/apps/services/converterService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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 { ServiceClass } from '@rocket.chat/core-services';
import type { IAppsConverterService } from '@rocket.chat/core-services';

import type { AppServerOrchestrator } from '../orchestrator';
import { OrchestratorFactory } from './orchestratorFactory';

export class AppsConverterService extends ServiceClass implements IAppsConverterService {
protected name = 'apps-converter';

private apps: AppServerOrchestrator;

constructor() {
super();

this.apps = OrchestratorFactory.getOrchestrator();
}

async convertRoomById(id: string): Promise<IRoom> {
return this.apps.getConverters()?.get('rooms').convertById(id);
}

async convertMessageById(id: string): Promise<IMessage> {
return this.apps.getConverters()?.get('messages').convertById(id);
}

async convertVistitorByToken(token: string): Promise<IVisitor> {
return this.apps.getConverters()?.get('visitors').convertByToken(token);
}

async convertUserToApp(user: any): Promise<IUser> {
return this.apps.getConverters()?.get('users').convertToApp(user);
}

async convertUserById(id: string): Promise<IUser> {
return this.apps.getConverters()?.get('users').convertById(id);
}
}
6 changes: 6 additions & 0 deletions apps/meteor/ee/server/apps/services/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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<string, string>,
warnings: fulfillment.getLicenseValidationResult().getWarnings() as Record<string, string>,
},
storageError: fulfillment.getStorageError(),
appUserError: fulfillment.getAppUserError() as { username: string; message: string },
};
}
Original file line number Diff line number Diff line change
@@ -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(),
};
}
Loading