diff --git a/apps/meteor/app/apps/server/api/index.ts b/apps/meteor/app/apps/server/api/index.ts new file mode 100644 index 0000000000000..83e6d357d58d8 --- /dev/null +++ b/apps/meteor/app/apps/server/api/index.ts @@ -0,0 +1,21 @@ +import express from 'express'; +import { WebApp } from 'meteor/webapp'; +import { AppsApiService } from '@rocket.chat/core-services'; + +import { authenticationMiddleware } from '../../../api/server/middlewares/authentication'; + +const apiServer = express(); + +apiServer.disable('x-powered-by'); + +WebApp.connectHandlers.use(apiServer); + +class AppsApiRoutes { + constructor() { + const rejectUnauthorized = false; + apiServer.use('/api/apps/private/:appId/:hash', authenticationMiddleware({ rejectUnauthorized }), AppsApiService.handlePrivateRequest); + apiServer.use('/api/apps/public/:appId', authenticationMiddleware({ rejectUnauthorized }), AppsApiService.handlePublicRequest); + } +} + +export const AppsApiRoutesInstance = new AppsApiRoutes(); diff --git a/apps/meteor/app/apps/server/bridges/activation.ts b/apps/meteor/app/apps/server/bridges/activation.ts index dce9d0011ec2a..dd3c8cd917718 100644 --- a/apps/meteor/app/apps/server/bridges/activation.ts +++ b/apps/meteor/app/apps/server/bridges/activation.ts @@ -4,6 +4,7 @@ import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import { Users } from '@rocket.chat/models'; import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; +import { AppEvents } from '../../../../ee/server/apps/communication'; export class AppActivationBridge extends ActivationBridge { // eslint-disable-next-line no-empty-function @@ -12,15 +13,15 @@ export class AppActivationBridge extends ActivationBridge { } protected async appAdded(app: ProxiedApp): Promise { - await this.orch.getNotifier().appAdded(app.getID()); + this.orch.notifyAppEvent(AppEvents.APP_ADDED, app.getID()); } protected async appUpdated(app: ProxiedApp): Promise { - await this.orch.getNotifier().appUpdated(app.getID()); + this.orch.notifyAppEvent(AppEvents.APP_UPDATED, app.getID()); } protected async appRemoved(app: ProxiedApp): Promise { - await this.orch.getNotifier().appRemoved(app.getID()); + this.orch.notifyAppEvent(AppEvents.APP_REMOVED, app.getID()); } protected async appStatusChanged(app: ProxiedApp, status: AppStatus): Promise { @@ -28,10 +29,10 @@ export class AppActivationBridge extends ActivationBridge { await Users.updateStatusByAppId(app.getID(), userStatus); - await this.orch.getNotifier().appStatusUpdated(app.getID(), status); + this.orch.notifyAppEvent(AppEvents.APP_STATUS_CHANGE, app.getID(), status); } protected async actionsChanged(): Promise { - await this.orch.getNotifier().actionsChanged(); + this.orch.notifyAppEvent(AppEvents.APP_STATUS_CHANGE); } } diff --git a/apps/meteor/app/apps/server/bridges/api.ts b/apps/meteor/app/apps/server/bridges/api.ts index 485110799cbbd..37b073d316d65 100644 --- a/apps/meteor/app/apps/server/bridges/api.ts +++ b/apps/meteor/app/apps/server/bridges/api.ts @@ -1,94 +1,27 @@ -import { Meteor } from 'meteor/meteor'; -import type { Response, Request, IRouter, RequestHandler } from 'express'; -import express from 'express'; -import { WebApp } from 'meteor/webapp'; -import { ApiBridge } from '@rocket.chat/apps-engine/server/bridges/ApiBridge'; -import type { IApiRequest, IApiEndpoint, IApi } from '@rocket.chat/apps-engine/definition/api'; +import type { IApi, IApiEndpoint } from '@rocket.chat/apps-engine/definition/api'; +import { ApiBridge } from '@rocket.chat/apps-engine/server/bridges'; import type { AppApi } from '@rocket.chat/apps-engine/server/managers/AppApi'; -import type { RequestMethod } from '@rocket.chat/apps-engine/definition/accessors'; +import { AppsApiService } from '@rocket.chat/core-services'; import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; -import { authenticationMiddleware } from '../../../api/server/middlewares/authentication'; - -const apiServer = express(); - -apiServer.disable('x-powered-by'); - -WebApp.connectHandlers.use(apiServer); - -interface IRequestWithPrivateHash extends Request { - _privateHash?: string; - content?: any; -} export class AppApisBridge extends ApiBridge { - appRouters: Map; - - // eslint-disable-next-line no-empty-function constructor(private readonly orch: AppServerOrchestrator) { super(); - this.appRouters = new Map(); - - apiServer.use('/api/apps/private/:appId/:hash', (req: IRequestWithPrivateHash, res: Response) => { - 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(); - }); - - apiServer.use('/api/apps/public/:appId', (req: Request, res: Response) => { - const notFound = (): Response => res.sendStatus(404); - - const router = this.appRouters.get(req.params.appId); - - if (router) { - return router(req, res, notFound); - } - - notFound(); - }); } - public async registerApi({ api, computedPath, endpoint }: AppApi, appId: string): Promise { + protected async registerApi({ api, computedPath, endpoint }: AppApi, appId: string): Promise { this.orch.debugLog(`The App ${appId} is registering the api: "${endpoint.path}" (${computedPath})`); this._verifyApi(api, endpoint); - let router = this.appRouters.get(appId); - - if (!router) { - router = express.Router(); // eslint-disable-line new-cap - 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, - authenticationMiddleware({ rejectUnauthorized: !!endpoint.authRequired }), - Meteor.bindEnvironment(this._appApiExecutor(endpoint, appId)), - ); - } + await AppsApiService.registerApi(endpoint, appId); } - public async unregisterApis(appId: string): Promise { + protected async unregisterApis(appId: string): Promise { this.orch.debugLog(`The App ${appId} is unregistering all apis`); - if (this.appRouters.get(appId)) { - this.appRouters.delete(appId); - } + await AppsApiService.unregisterApi(appId); } private _verifyApi(api: IApi, endpoint: IApiEndpoint): void { @@ -100,32 +33,4 @@ export class AppApisBridge extends ApiBridge { throw new Error('Invalid Api parameter provided, it must be a valid IApi object.'); } } - - 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.orch.getConverters()?.get('users')?.convertToApp(req.user), - }; - - this.orch - .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/app/apps/server/bridges/cloud.ts b/apps/meteor/app/apps/server/bridges/cloud.ts index 9dcb9025044b4..7b4b0378789e4 100644 --- a/apps/meteor/app/apps/server/bridges/cloud.ts +++ b/apps/meteor/app/apps/server/bridges/cloud.ts @@ -1,8 +1,8 @@ import { CloudWorkspaceBridge } from '@rocket.chat/apps-engine/server/bridges/CloudWorkspaceBridge'; import type { IWorkspaceToken } from '@rocket.chat/apps-engine/definition/cloud/IWorkspaceToken'; -import { getWorkspaceAccessTokenWithScope } from '../../../cloud/server'; import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; +import { getWorkspaceAccessTokenWithScope } from '../../../cloud/server'; export class AppCloudBridge extends CloudWorkspaceBridge { // eslint-disable-next-line no-empty-function diff --git a/apps/meteor/app/apps/server/bridges/commands.ts b/apps/meteor/app/apps/server/bridges/commands.ts index bf52fb3577ef3..fd2d00c9e9222 100644 --- a/apps/meteor/app/apps/server/bridges/commands.ts +++ b/apps/meteor/app/apps/server/bridges/commands.ts @@ -1,18 +1,15 @@ -import { Meteor } from 'meteor/meteor'; -import type { ISlashCommand, ISlashCommandPreview, ISlashCommandPreviewItem } from '@rocket.chat/apps-engine/definition/slashcommands'; -import { SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands'; +import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands'; import { CommandBridge } from '@rocket.chat/apps-engine/server/bridges/CommandBridge'; -import type { IMessage, RequiredField, SlashCommand } from '@rocket.chat/core-typings'; +import type { SlashCommand } from '@rocket.chat/core-typings'; +import { SlashCommandService } from '@rocket.chat/core-services'; -import { slashCommands } from '../../../utils/server'; -import { Utilities } from '../../../../ee/lib/misc/Utilities'; import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; -import { parseParameters } from '../../../../lib/utils/parseParameters'; +import { AppEvents } from '../../../../ee/server/apps'; +import { Utilities } from '../../../../ee/lib/misc/Utilities'; export class AppCommandsBridge extends CommandBridge { - disabledCommands: Map; + disabledCommands: Map; - // eslint-disable-next-line no-empty-function constructor(private readonly orch: AppServerOrchestrator) { super(); this.disabledCommands = new Map(); @@ -27,7 +24,7 @@ export class AppCommandsBridge extends CommandBridge { const cmd = command.toLowerCase(); - return typeof slashCommands.commands[cmd] === 'object' || this.disabledCommands.has(cmd); + return typeof (await SlashCommandService.getCommand(cmd)) === 'object' || this.disabledCommands.has(cmd); } protected async enableCommand(command: string, appId: string): Promise { @@ -42,10 +39,10 @@ export class AppCommandsBridge extends CommandBridge { throw new Error(`The command is not currently disabled: "${cmd}"`); } - slashCommands.commands[cmd] = this.disabledCommands.get(cmd) as (typeof slashCommands.commands)[string]; + await SlashCommandService.setAppCommand(this.disabledCommands.get(cmd) as SlashCommand); this.disabledCommands.delete(cmd); - this.orch.getNotifier().commandUpdated(cmd); + this.orch.notifyAppEvent(AppEvents.COMMAND_UPDATED, cmd); } protected async disableCommand(command: string, appId: string): Promise { @@ -61,16 +58,16 @@ export class AppCommandsBridge extends CommandBridge { return; } - const commandObj = slashCommands.commands[cmd]; + const commandObj = await SlashCommandService.getCommand(cmd); if (typeof commandObj === 'undefined') { throw new Error(`Command does not exist in the system currently: "${cmd}"`); } this.disabledCommands.set(cmd, commandObj); - delete slashCommands.commands[cmd]; + await SlashCommandService.removeCommand(cmd); - this.orch.getNotifier().commandDisabled(cmd); + this.orch.notifyAppEvent(AppEvents.COMMAND_DISABLED, cmd); } // command: { command, paramsExample, i18nDescription, executor: function } @@ -80,23 +77,21 @@ export class AppCommandsBridge extends CommandBridge { this._verifyCommand(command); const cmd = command.command.toLowerCase(); - if (typeof slashCommands.commands[cmd] === 'undefined') { + const item = await SlashCommandService.getCommand(cmd); + const typeofCommand = typeof item; + + if (typeofCommand === 'undefined') { throw new Error(`Command does not exist in the system currently (or it is disabled): "${cmd}"`); } - const item = slashCommands.commands[cmd]; - item.params = command.i18nParamsExample ? command.i18nParamsExample : item.params; item.description = command.i18nDescription ? command.i18nDescription : item.params; - item.callback = this._appCommandExecutor.bind(this); item.providesPreview = command.providesPreview; - item.previewer = command.previewer ? this._appCommandPreviewer.bind(this) : item.previewer; - item.previewCallback = ( - command.executePreviewItem ? this._appCommandPreviewExecutor.bind(this) : item.previewCallback - ) as (typeof slashCommands.commands)[string]['previewCallback']; + item.previewer = !command.previewer ? undefined : ({} as any); + item.previewCallback = !command.executePreviewItem ? undefined : ({} as any); + await SlashCommandService.setAppCommand(item); - slashCommands.commands[cmd] = item; - this.orch.getNotifier().commandUpdated(cmd); + this.orch.notifyAppEvent(AppEvents.COMMAND_UPDATED, cmd); } protected async registerCommand(command: ISlashCommand, appId: string): Promise { @@ -110,16 +105,13 @@ export class AppCommandsBridge extends CommandBridge { params: Utilities.getI18nKeyForApp(command.i18nParamsExample, appId), description: Utilities.getI18nKeyForApp(command.i18nDescription, appId), permission: command.permission, - callback: this._appCommandExecutor.bind(this), providesPreview: command.providesPreview, - previewer: !command.previewer ? undefined : this._appCommandPreviewer.bind(this), - previewCallback: (!command.executePreviewItem ? undefined : this._appCommandPreviewExecutor.bind(this)) as - | (typeof slashCommands.commands)[string]['previewCallback'] - | undefined, + previewer: !command.previewer ? undefined : {}, + previewCallback: !command.executePreviewItem ? undefined : {}, } as SlashCommand; - slashCommands.commands[command.command.toLowerCase()] = item; - this.orch.getNotifier().commandAdded(command.command.toLowerCase()); + await SlashCommandService.setAppCommand(item); + this.orch.notifyAppEvent(AppEvents.COMMAND_ADDED, command.command.toLowerCase()); } protected async unregisterCommand(command: string, appId: string): Promise { @@ -131,9 +123,9 @@ export class AppCommandsBridge extends CommandBridge { const cmd = command.toLowerCase(); this.disabledCommands.delete(cmd); - delete slashCommands.commands[cmd]; + await SlashCommandService.removeCommand(cmd); - this.orch.getNotifier().commandRemoved(cmd); + this.orch.notifyAppEvent(AppEvents.COMMAND_REMOVED, cmd); } private _verifyCommand(command: ISlashCommand): void { @@ -161,63 +153,4 @@ export class AppCommandsBridge extends CommandBridge { throw new Error('Invalid Slash Command parameter provided, it must be a valid ISlashCommand object.'); } } - - private async _appCommandExecutor( - command: string, - parameters: any, - message: RequiredField, 'rid'>, - triggerId?: string, - ): Promise { - const user = await this.orch.getConverters()?.get('users').convertById(Meteor.userId()); - const room = await this.orch.getConverters()?.get('rooms').convertById(message.rid); - const threadId = message.tmid; - const params = parseParameters(parameters); - - const context = new SlashCommandContext( - Object.freeze(user), - Object.freeze(room), - Object.freeze(params) as string[], - threadId, - triggerId, - ); - - await this.orch.getManager()?.getCommandManager().executeCommand(command, context); - } - - private async _appCommandPreviewer( - command: string, - parameters: any, - message: RequiredField, 'rid'>, - ): Promise { - const user = await this.orch.getConverters()?.get('users').convertById(Meteor.userId()); - const room = await this.orch.getConverters()?.get('rooms').convertById(message.rid); - const threadId = message.tmid; - const params = parseParameters(parameters); - - const context = new SlashCommandContext(Object.freeze(user), Object.freeze(room), Object.freeze(params) as string[], threadId); - return this.orch.getManager()?.getCommandManager().getPreviews(command, context); - } - - private async _appCommandPreviewExecutor( - command: string, - parameters: any, - message: IMessage, - preview: ISlashCommandPreviewItem, - triggerId: string, - ): Promise { - const user = await this.orch.getConverters()?.get('users').convertById(Meteor.userId()); - const room = await this.orch.getConverters()?.get('rooms').convertById(message.rid); - const threadId = message.tmid; - const params = parseParameters(parameters); - - const context = new SlashCommandContext( - Object.freeze(user), - Object.freeze(room), - Object.freeze(params) as string[], - threadId, - triggerId, - ); - - await this.orch.getManager()?.getCommandManager().executePreview(command, preview, context); - } } diff --git a/apps/meteor/app/apps/server/bridges/details.ts b/apps/meteor/app/apps/server/bridges/details.ts index cb60cc56f3aab..00d06c4292989 100644 --- a/apps/meteor/app/apps/server/bridges/details.ts +++ b/apps/meteor/app/apps/server/bridges/details.ts @@ -2,6 +2,7 @@ import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import { AppDetailChangesBridge as DetailChangesBridge } from '@rocket.chat/apps-engine/server/bridges/AppDetailChangesBridge'; import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; +import { AppEvents } from '../../../../ee/server/apps/communication'; export class AppDetailChangesBridge extends DetailChangesBridge { // eslint-disable-next-line no-empty-function @@ -11,7 +12,7 @@ export class AppDetailChangesBridge extends DetailChangesBridge { protected onAppSettingsChange(appId: string, setting: ISetting): void { try { - this.orch.getNotifier().appSettingsChange(appId, setting); + this.orch.notifyAppEvent(AppEvents.APP_SETTING_UPDATED, appId, setting); } catch (e) { console.warn('failed to notify about the setting change.', appId); } diff --git a/apps/meteor/app/apps/server/bridges/http.ts b/apps/meteor/app/apps/server/bridges/http.ts index b783e79d6243d..c676a76691fa4 100644 --- a/apps/meteor/app/apps/server/bridges/http.ts +++ b/apps/meteor/app/apps/server/bridges/http.ts @@ -68,6 +68,9 @@ export class AppHttpBridge extends HttpBridge { this.orch.debugLog(`The App ${info.appId} is requesting from the outter webs:`, info); try { + const allowSelfSignedCerts = + (request.hasOwnProperty('strictSSL') && !request.strictSSL) || + (request.hasOwnProperty('rejectUnauthorized') && request.rejectUnauthorized); const response = await fetch( url.href, { @@ -75,8 +78,7 @@ export class AppHttpBridge extends HttpBridge { body: content, headers, }, - (request.hasOwnProperty('strictSSL') && !request.strictSSL) || - (request.hasOwnProperty('rejectUnauthorized') && request.rejectUnauthorized), + allowSelfSignedCerts, ); const result: IHttpResponse = { diff --git a/apps/meteor/app/apps/server/bridges/internal.ts b/apps/meteor/app/apps/server/bridges/internal.ts index 36b909a3d7011..1035edbf107e7 100644 --- a/apps/meteor/app/apps/server/bridges/internal.ts +++ b/apps/meteor/app/apps/server/bridges/internal.ts @@ -2,6 +2,7 @@ import { InternalBridge } from '@rocket.chat/apps-engine/server/bridges/Internal import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import type { ISubscription } from '@rocket.chat/core-typings'; import { Settings, Subscriptions } from '@rocket.chat/models'; +import Future from 'fibers/future'; import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; import { isTruthy } from '../../../../lib/isTruthy'; @@ -17,14 +18,13 @@ export class AppInternalBridge extends InternalBridge { return []; } - // Depends on apps engine separation to microservices - const records = Promise.await( + const records = Future.fromPromise( Subscriptions.findByRoomIdWhenUsernameExists(roomId, { projection: { 'u.username': 1, }, }).toArray(), - ); + ).wait(); if (!records || records.length === 0) { return []; diff --git a/apps/meteor/app/apps/server/bridges/listeners.js b/apps/meteor/app/apps/server/bridges/listeners.js index d81832c7fdf9d..83ae8f28d760e 100644 --- a/apps/meteor/app/apps/server/bridges/listeners.js +++ b/apps/meteor/app/apps/server/bridges/listeners.js @@ -71,19 +71,19 @@ export class AppListenerBridge { async messageEvent(inte, message, ...payload) { const msg = await this.orch.getConverters().get('messages').convertMessage(message); - const params = (() => { + const params = await (async () => { switch (inte) { case AppInterface.IPostMessageDeleted: const [userDeleted] = payload; return { + user: await this.orch.getConverters().get('users').convertToApp(userDeleted), message: msg, - user: this.orch.getConverters().get('users').convertToApp(userDeleted), }; case AppInterface.IPostMessageReacted: const [userReacted, reaction, isReacted] = payload; return { message: msg, - user: this.orch.getConverters().get('users').convertToApp(userReacted), + user: await this.orch.getConverters().get('users').convertToApp(userReacted), reaction, isReacted, }; @@ -91,28 +91,28 @@ export class AppListenerBridge { const [userFollowed, isUnfollow] = payload; return { message: msg, - user: this.orch.getConverters().get('users').convertToApp(userFollowed), + user: await this.orch.getConverters().get('users').convertToApp(userFollowed), isUnfollow, }; case AppInterface.IPostMessagePinned: const [userPinned, isUnpinned] = payload; return { message: msg, - user: this.orch.getConverters().get('users').convertToApp(userPinned), + user: await this.orch.getConverters().get('users').convertToApp(userPinned), isUnpinned, }; case AppInterface.IPostMessageStarred: const [userStarred, isStarred] = payload; return { message: msg, - user: this.orch.getConverters().get('users').convertToApp(userStarred), + user: await this.orch.getConverters().get('users').convertToApp(userStarred), isStarred, }; case AppInterface.IPostMessageReported: const [userReported, reason] = payload; return { message: msg, - user: this.orch.getConverters().get('users').convertToApp(userReported), + user: await this.orch.getConverters().get('users').convertToApp(userReported), reason, }; default: @@ -131,22 +131,22 @@ export class AppListenerBridge { async roomEvent(inte, room, ...payload) { const rm = await this.orch.getConverters().get('rooms').convertRoom(room); - const params = (() => { + const params = await (async () => { switch (inte) { case AppInterface.IPreRoomUserJoined: case AppInterface.IPostRoomUserJoined: const [joiningUser, invitingUser] = payload; return { room: rm, - joiningUser: this.orch.getConverters().get('users').convertToApp(joiningUser), - invitingUser: this.orch.getConverters().get('users').convertToApp(invitingUser), + joiningUser: await this.orch.getConverters().get('users').convertToApp(joiningUser), + invitingUser: await this.orch.getConverters().get('users').convertToApp(invitingUser), }; case AppInterface.IPreRoomUserLeave: case AppInterface.IPostRoomUserLeave: const [leavingUser] = payload; return { room: rm, - leavingUser: this.orch.getConverters().get('users').convertToApp(leavingUser), + leavingUser: await this.orch.getConverters().get('users').convertToApp(leavingUser), }; default: return rm; @@ -170,7 +170,7 @@ export class AppListenerBridge { .getListenerManager() .executeListener(inte, { room: await this.orch.getConverters().get('rooms').convertRoom(data.room), - agent: this.orch.getConverters().get('users').convertToApp(data.user), + agent: await this.orch.getConverters().get('users').convertToApp(data.user), }); case AppInterface.IPostLivechatRoomTransferred: const converter = data.type === LivechatTransferEventType.AGENT ? 'users' : 'departments'; @@ -206,12 +206,12 @@ export class AppListenerBridge { switch (inte) { case AppInterface.IPostUserLoggedIn: case AppInterface.IPostUserLogout: - context = this.orch.getConverters().get('users').convertToApp(data.user); + context = await this.orch.getConverters().get('users').convertToApp(data.user); return this.orch.getManager().getListenerManager().executeListener(inte, context); case AppInterface.IPostUserStatusChanged: const { currentStatus, previousStatus } = data; context = { - user: this.orch.getConverters().get('users').convertToApp(data.user), + user: await this.orch.getConverters().get('users').convertToApp(data.user), currentStatus, previousStatus, }; @@ -221,11 +221,11 @@ export class AppListenerBridge { case AppInterface.IPostUserUpdated: case AppInterface.IPostUserDeleted: context = { - user: this.orch.getConverters().get('users').convertToApp(data.user), - performedBy: this.orch.getConverters().get('users').convertToApp(data.performedBy), + user: await this.orch.getConverters().get('users').convertToApp(data.user), + performedBy: await this.orch.getConverters().get('users').convertToApp(data.performedBy), }; if (inte === AppInterface.IPostUserUpdated) { - context.previousData = this.orch.getConverters().get('users').convertToApp(data.previousUser); + context.previousData = await this.orch.getConverters().get('users').convertToApp(data.previousUser); } return this.orch.getManager().getListenerManager().executeListener(inte, context); } diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index b5db8804a2777..6b04a9c34a1a2 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -1,4 +1,5 @@ -import { Random } from '@rocket.chat/random'; +import { v4 as uuid } from 'uuid'; +import Future from 'fibers/future'; import { LivechatBridge } from '@rocket.chat/apps-engine/server/bridges/LivechatBridge'; import type { ILivechatMessage, @@ -11,26 +12,22 @@ import type { IUser } from '@rocket.chat/apps-engine/definition/users'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; import type { IExtraRoomParams } from '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator'; import { OmnichannelSourceType } from '@rocket.chat/core-typings'; -import { LivechatVisitors, LivechatRooms, LivechatDepartment, Users } from '@rocket.chat/models'; +import { LivechatDepartment, LivechatVisitors, LivechatRooms, Users } from '@rocket.chat/models'; +import { LivechatService } from '@rocket.chat/core-services'; -import { getRoom } from '../../../livechat/server/api/lib/livechat'; -import { Livechat } from '../../../livechat/server/lib/Livechat'; import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; -import { Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped'; export class AppLivechatBridge extends LivechatBridge { - // eslint-disable-next-line no-empty-function constructor(private readonly orch: AppServerOrchestrator) { super(); } protected isOnline(departmentId?: string): boolean { - // Depends on apps engine separation to microservices - return Promise.await(Livechat.online(departmentId)); + return Future.fromPromise(LivechatService.isOnline(departmentId)).wait() as boolean; } protected async isOnlineAsync(departmentId?: string): Promise { - return Livechat.online(departmentId); + return LivechatService.isOnline(departmentId); } protected async createMessage(message: ILivechatMessage, appId: string): Promise { @@ -40,8 +37,8 @@ export class AppLivechatBridge extends LivechatBridge { throw new Error('Invalid token for livechat message'); } - const msg = await Livechat.sendMessage({ - guest: this.orch.getConverters()?.get('visitors').convertAppVisitor(message.visitor), + const msg = await LivechatService.sendMessage({ + guest: await this.orch.getConverters()?.get('visitors').convertAppVisitor(message.visitor), message: await this.orch.getConverters()?.get('messages').convertAppMessage(message), agent: undefined, roomInfo: { @@ -66,11 +63,11 @@ export class AppLivechatBridge extends LivechatBridge { this.orch.debugLog(`The App ${appId} is updating a message.`); const data = { - guest: message.visitor, + guest: message.visitor as IVisitor, message: await this.orch.getConverters()?.get('messages').convertAppMessage(message), }; - await Livechat.updateMessage(data); + await LivechatService.updateMessage(data); } protected async createRoom(visitor: IVisitor, agent: IUser, appId: string, extraParams?: IExtraRoomParams): Promise { @@ -90,10 +87,10 @@ export class AppLivechatBridge extends LivechatBridge { agentRoom = Object.assign({}, { agentId: user?._id, username: user?.username }); } - const result = await getRoom({ - guest: this.orch.getConverters()?.get('visitors').convertAppVisitor(visitor), + const result = await LivechatService.getRoom({ + guest: await this.orch.getConverters()?.get('visitors').convertAppVisitor(visitor), agent: agentRoom, - rid: Random.id(), + rid: uuid(), roomInfo: { source: { type: OmnichannelSourceType.APP, @@ -113,8 +110,8 @@ export class AppLivechatBridge extends LivechatBridge { protected async closeRoom(room: ILivechatRoom, comment: string, closer: IUser | undefined, appId: string): Promise { this.orch.debugLog(`The App ${appId} is closing a livechat room.`); - const user = closer && this.orch.getConverters()?.get('users').convertToRocketChat(closer); - const visitor = this.orch.getConverters()?.get('visitors').convertAppVisitor(room.visitor); + const user = closer && (await this.orch.getConverters()?.get('users').convertToRocketChat(closer)); + const visitor = await this.orch.getConverters()?.get('visitors').convertAppVisitor(room.visitor); const closeData: any = { room: await this.orch.getConverters()?.get('rooms').convertAppRoom(room), @@ -123,7 +120,7 @@ export class AppLivechatBridge extends LivechatBridge { ...(visitor && { visitor }), }; - await LivechatTyped.closeRoom(closeData); + await LivechatService.closeRoom(closeData); return true; } @@ -143,7 +140,8 @@ export class AppLivechatBridge extends LivechatBridge { result = await LivechatRooms.findOpenByVisitorToken(visitor.token, {}).toArray(); } - return Promise.all((result as unknown as ILivechatRoom[]).map((room) => this.orch.getConverters()?.get('rooms').convertRoom(room))); + const promisedRooms = result.map(async (room: ILivechatRoom) => this.orch.getConverters()?.get('rooms').convertRoom(room)); + return Promise.all(promisedRooms); } protected async createVisitor(visitor: IVisitor, appId: string): Promise { @@ -156,7 +154,7 @@ export class AppLivechatBridge extends LivechatBridge { token: visitor.token, email: '', connectionData: undefined, - phone: {}, + phone: { number: '' }, id: visitor.id, }; @@ -165,10 +163,10 @@ export class AppLivechatBridge extends LivechatBridge { } if (visitor.phone?.length) { - (registerData as any).phone = { number: visitor.phone[0].phoneNumber }; + registerData.phone = { number: visitor.phone[0].phoneNumber }; } - return Livechat.registerGuest(registerData); + return LivechatService.registerGuest(registerData); } protected async transferVisitor(visitor: IVisitor, transferData: ILivechatTransferData, appId: string): Promise { @@ -206,11 +204,9 @@ export class AppLivechatBridge extends LivechatBridge { userId = transferredTo._id; } - return Livechat.transfer( - await this.orch.getConverters()?.get('rooms').convertAppRoom(currentRoom), - this.orch.getConverters()?.get('visitors').convertAppVisitor(visitor), - { userId, departmentId, transferredBy, transferredTo }, - ); + const room = await this.orch.getConverters()?.get('rooms').convertAppRoom(currentRoom); + const guest = await this.orch.getConverters()?.get('visitors').convertAppVisitor(visitor); + return LivechatService.transferVisitor(room, guest, { userId, departmentId, transferredBy, transferredTo } as any); } protected async findVisitors(query: object, appId: string): Promise> { @@ -220,11 +216,12 @@ export class AppLivechatBridge extends LivechatBridge { console.warn('The method AppLivechatBridge.findVisitors is deprecated. Please consider using its alternatives'); } - return Promise.all( - (await LivechatVisitors.find(query).toArray()).map( - async (visitor) => visitor && this.orch.getConverters()?.get('visitors').convertVisitor(visitor), - ), + const livechatVisitors = await LivechatVisitors.find(query).toArray(); + const promisedVisitors = livechatVisitors.map( + async (visitor) => visitor && this.orch.getConverters()?.get('visitors').convertVisitor(visitor), ); + + return Promise.all(promisedVisitors); } protected async findVisitorById(id: string, appId: string): Promise { @@ -263,10 +260,9 @@ export class AppLivechatBridge extends LivechatBridge { protected async findDepartmentByIdOrName(value: string, appId: string): Promise { this.orch.debugLog(`The App ${appId} is looking for livechat departments.`); - return this.orch - .getConverters() - ?.get('departments') - .convertDepartment(await LivechatDepartment.findOneByIdOrName(value, {})); + const department = await LivechatDepartment.findOneByIdOrName(value, {}); + + return this.orch.getConverters()?.get('departments').convertDepartment(department); } protected async findDepartmentsEnabledWithAgents(appId: string): Promise> { @@ -275,7 +271,7 @@ export class AppLivechatBridge extends LivechatBridge { const converter = this.orch.getConverters()?.get('departments'); const boundConverter = converter.convertDepartment.bind(converter); - return Promise.all((await LivechatDepartment.findEnabledWithAgents().toArray()).map(boundConverter)); + return (await LivechatDepartment.findEnabledWithAgents().toArray()).map(boundConverter); } protected async _fetchLivechatRoomMessages(appId: string, roomId: string): Promise> { @@ -288,7 +284,7 @@ export class AppLivechatBridge extends LivechatBridge { const boundMessageConverter = messageConverter.convertMessage.bind(messageConverter); - return (await Livechat.getRoomMessages({ rid: roomId })).map(boundMessageConverter); + return (await LivechatService.getRoomMessages(roomId)).map(boundMessageConverter); } protected async setCustomFields( @@ -297,6 +293,6 @@ export class AppLivechatBridge extends LivechatBridge { ): Promise { this.orch.debugLog(`The App ${appId} is setting livechat visitor's custom fields.`); - return Livechat.setCustomFields(data); + return LivechatService.setCustomFields(data); } } diff --git a/apps/meteor/app/apps/server/bridges/messages.ts b/apps/meteor/app/apps/server/bridges/messages.ts index 28797d262e37f..4724c6513a6f8 100644 --- a/apps/meteor/app/apps/server/bridges/messages.ts +++ b/apps/meteor/app/apps/server/bridges/messages.ts @@ -3,12 +3,9 @@ import { MessageBridge } from '@rocket.chat/apps-engine/server/bridges/MessageBr import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; -import { api } from '@rocket.chat/core-services'; -import { Users, Subscriptions, Messages } from '@rocket.chat/models'; +import { Messages, Users, Subscriptions } from '@rocket.chat/models'; +import { api, MessageService, NotificationService } from '@rocket.chat/core-services'; -import { updateMessage } from '../../../lib/server/functions/updateMessage'; -import { executeSendMessage } from '../../../lib/server/methods/sendMessage'; -import notifications from '../../../notifications/server/lib/Notifications'; import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; export class AppMessageBridge extends MessageBridge { @@ -22,7 +19,7 @@ export class AppMessageBridge extends MessageBridge { const convertedMessage = await this.orch.getConverters()?.get('messages').convertAppMessage(message); - const sentMessage = await executeSendMessage(convertedMessage.u._id, convertedMessage); + const sentMessage = await MessageService.sendMessage(convertedMessage.u._id, convertedMessage); return sentMessage._id; } @@ -48,10 +45,10 @@ export class AppMessageBridge extends MessageBridge { const editor = await Users.findOneById(message.editor.id); if (!editor) { - throw new Error('Invalid editor assigned to the message for the update.'); + throw new Error('Could not find message editor'); } - await updateMessage(msg, editor); + await MessageService.updateMessage(msg, editor); } protected async notifyUser(user: IUser, message: IMessage, appId: string): Promise { @@ -90,7 +87,7 @@ export class AppMessageBridge extends MessageBridge { protected async typing({ scope, id, username, isTyping }: ITypingDescriptor): Promise { switch (scope) { case 'room': - notifications.notifyRoom(id, 'typing', username, isTyping); + await NotificationService.notifyRoom(id, 'typing', username, isTyping); return; default: throw new Error('Unrecognized typing scope provided'); diff --git a/apps/meteor/app/apps/server/bridges/oauthApps.ts b/apps/meteor/app/apps/server/bridges/oauthApps.ts index 943c082ba85aa..fe467da91e1f1 100644 --- a/apps/meteor/app/apps/server/bridges/oauthApps.ts +++ b/apps/meteor/app/apps/server/bridges/oauthApps.ts @@ -2,7 +2,6 @@ import type { IOAuthApp, IOAuthAppParams } from '@rocket.chat/apps-engine/defini import { OAuthAppsBridge } from '@rocket.chat/apps-engine/server/bridges/OAuthAppsBridge'; import type { IOAuthApps } from '@rocket.chat/core-typings'; import { OAuthApps, Users } from '@rocket.chat/models'; -import { Random } from '@rocket.chat/random'; import { v4 as uuidv4 } from 'uuid'; import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; @@ -28,8 +27,8 @@ export class AppOAuthAppsBridge extends OAuthAppsBridge { ...oAuthApp, _id: uuidv4(), appId, - clientId: clientId ?? Random.id(), - clientSecret: clientSecret ?? Random.secret(), + clientId: clientId ?? uuidv4(), + clientSecret: clientSecret ?? uuidv4(), _createdAt: new Date(), _createdBy: { _id, diff --git a/apps/meteor/app/apps/server/bridges/persistence.ts b/apps/meteor/app/apps/server/bridges/persistence.ts index 3206a04708b16..e7bdff7bfa469 100644 --- a/apps/meteor/app/apps/server/bridges/persistence.ts +++ b/apps/meteor/app/apps/server/bridges/persistence.ts @@ -15,7 +15,7 @@ export class AppPersistenceBridge extends PersistenceBridge { await this.orch.getPersistenceModel().remove({ appId }); } - protected async create(data: object, appId: string): Promise { + protected async create(data: Record, appId: string): Promise { this.orch.debugLog(`The App ${appId} is storing a new object in their persistence.`, data); if (typeof data !== 'object') { @@ -25,7 +25,11 @@ export class AppPersistenceBridge extends PersistenceBridge { return this.orch.getPersistenceModel().insertOne({ appId, data }); } - protected async createWithAssociations(data: object, associations: Array, appId: string): Promise { + protected async createWithAssociations( + data: Record, + associations: Array, + appId: string, + ): Promise { this.orch.debugLog( `The App ${appId} is storing a new object in their persistence that is associated with some models.`, data, diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index 49495b9d76b94..e7694cec25116 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -1,15 +1,13 @@ import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import type { ISubscription, IUser as ICoreUser, RoomType as CoreRoomType } from '@rocket.chat/core-typings'; import { RoomBridge } from '@rocket.chat/apps-engine/server/bridges/RoomBridge'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; -import { Meteor } from 'meteor/meteor'; -import type { ISubscription, IUser as ICoreUser } from '@rocket.chat/core-typings'; -import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; +import { Users, Subscriptions, Rooms } from '@rocket.chat/models'; +import { Room } from '@rocket.chat/core-services'; import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; -import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom'; -import { deleteRoom } from '../../../lib/server/functions/deleteRoom'; export class AppRoomBridge extends RoomBridge { // eslint-disable-next-line no-empty-function @@ -21,39 +19,31 @@ export class AppRoomBridge extends RoomBridge { this.orch.debugLog(`The App ${appId} is creating a new room.`, room); const rcRoom = await this.orch.getConverters()?.get('rooms').convertAppRoom(room); - let method: string; + let roomType: CoreRoomType; switch (room.type) { case RoomType.CHANNEL: - method = 'createChannel'; + roomType = 'c'; break; case RoomType.PRIVATE_GROUP: - method = 'createPrivateGroup'; + roomType = 'p'; break; case RoomType.DIRECT_MESSAGE: - method = 'createDirectMessage'; + roomType = 'd'; break; default: throw new Error('Only channels, private groups and direct messages can be created.'); } - let rid = ''; - await Meteor.runAsUser(room.creator.id, async () => { - const extraData = Object.assign({}, rcRoom); - delete extraData.name; - delete extraData.t; - delete extraData.ro; - delete extraData.customFields; - let info; - if (room.type === RoomType.DIRECT_MESSAGE) { - info = await Meteor.callAsync(method, ...members); - } else { - info = await Meteor.callAsync(method, rcRoom.name, members, rcRoom.ro, rcRoom.customFields, extraData); - } - rid = info.rid; - }); + const extraData = Object.assign({}, rcRoom); + delete extraData.name; + delete extraData.t; + delete extraData.ro; + delete extraData.customFields; + + const { _id } = await Room.create(room.creator.id, { name: rcRoom.name, type: roomType, readOnly: rcRoom.ro, extraData, members }); - return rid; + return _id; } protected async getById(roomId: string, appId: string): Promise { @@ -94,10 +84,11 @@ export class AppRoomBridge extends RoomBridge { protected async getMembers(roomId: string, appId: string): Promise> { this.orch.debugLog(`The App ${appId} is getting the room's members by room id: "${roomId}"`); - const subscriptions = await Subscriptions.findByRoomId(roomId, {}); - return Promise.all( - (await subscriptions.toArray()).map((sub: ISubscription) => this.orch.getConverters()?.get('users').convertById(sub.u?._id)), + const subscriptions = await Subscriptions.findByRoomId(roomId, {}).toArray(); + const promisedMembers = subscriptions.map(async (sub: ISubscription) => + this.orch.getConverters()?.get('users').convertById(sub.u?._id), ); + return Promise.all(promisedMembers); } protected async getDirectByUsernames(usernames: Array, appId: string): Promise { @@ -118,22 +109,23 @@ export class AppRoomBridge extends RoomBridge { const rm = await this.orch.getConverters()?.get('rooms').convertAppRoom(room); - await Rooms.updateOne({ _id: rm._id }, { $set: rm }); + // @ts-ignore Circular reference on field 'value' + await Rooms.updateOne(rm._id, rm); - for await (const username of members) { + const promisedAddedUsers = members.map(async (username: string) => { const member = await Users.findOneByUsername(username, {}); - if (!member) { - continue; + if (member) { + return Room.addUserToRoom(rm._id, member); } + }); - await addUserToRoom(rm._id, member); - } + await Promise.all(promisedAddedUsers); } protected async delete(roomId: string, appId: string): Promise { this.orch.debugLog(`The App ${appId} is deleting a room.`); - await deleteRoom(roomId); + await Rooms.removeById(roomId); } protected async createDiscussion( @@ -157,20 +149,17 @@ export class AppRoomBridge extends RoomBridge { } const discussion = { - prid: rcRoom.prid, - t_name: rcRoom.fname, - pmid: rcMessage ? rcMessage._id : undefined, + parentRoomId: rcRoom.prid, + parentMessageId: rcMessage ? rcMessage._id : undefined, + creatorId: room.creator.id, + name: rcRoom.fname, + members: members.length > 0 ? members : [], reply: reply && reply.trim() !== '' ? reply : undefined, - users: members.length > 0 ? members : [], }; - let rid = ''; - await Meteor.runAsUser(room.creator.id, async () => { - const info = await Meteor.callAsync('createDiscussion', discussion); - rid = info.rid; - }); + const { _id } = await Room.createDiscussion(discussion); - return rid; + return _id; } protected getModerators(roomId: string, appId: string): Promise { @@ -189,14 +178,11 @@ export class AppRoomBridge extends RoomBridge { } private async getUsersByRoomIdAndSubscriptionRole(roomId: string, role: string): Promise { - const subs = (await Subscriptions.findByRoomIdAndRoles(roomId, [role], { - projection: { uid: '$u._id', _id: 0 }, - }).toArray()) as unknown as { - uid: string; - }[]; - // Was this a bug? - const users = await Users.findByIds(subs.map((user: { uid: string }) => user.uid)).toArray(); - const userConverter = this.orch.getConverters()!.get('users'); - return users.map((user: ICoreUser) => userConverter!.convertToApp(user)); + const subs = await Subscriptions.findByRoomIdAndRoles(roomId, [role], { projection: { uid: '$u._id', _id: 0 } }); + const subsUids = subs.map((user: { uid: string }) => user.uid); + const users = await Users.findByIds(subsUids).toArray(); + const userConverter = this.orch.getConverters()?.get('users'); + const promisedUsers = users.map(async (user: ICoreUser) => userConverter.convertToApp(user)); + return Promise.all(promisedUsers); } } diff --git a/apps/meteor/app/apps/server/bridges/scheduler.ts b/apps/meteor/app/apps/server/bridges/scheduler.ts index 8349f5764b41f..c16b6b58e835d 100644 --- a/apps/meteor/app/apps/server/bridges/scheduler.ts +++ b/apps/meteor/app/apps/server/bridges/scheduler.ts @@ -1,7 +1,6 @@ import type { Job } from '@rocket.chat/agenda'; import { Agenda } from '@rocket.chat/agenda'; import { ObjectID } from 'bson'; -import { MongoInternals } from 'meteor/mongo'; import type { IProcessor, IOnetimeSchedule, IRecurringSchedule, IJobContext } from '@rocket.chat/apps-engine/definition/scheduler'; import { StartupType } from '@rocket.chat/apps-engine/definition/scheduler'; import { SchedulerBridge } from '@rocket.chat/apps-engine/server/bridges/SchedulerBridge'; @@ -40,7 +39,7 @@ export class AppSchedulerBridge extends SchedulerBridge { constructor(private readonly orch: AppServerOrchestrator) { super(); this.scheduler = new Agenda({ - mongo: (MongoInternals.defaultRemoteCollectionDriver().mongo as any).client.db(), + mongo: this.orch.db, db: { collection: 'rocketchat_apps_scheduler' }, // this ensures the same job doesn't get executed multiple times in a cluster defaultConcurrency: 1, diff --git a/apps/meteor/app/apps/server/bridges/settings.ts b/apps/meteor/app/apps/server/bridges/settings.ts index 14e4834c36328..fe67352fa90fb 100644 --- a/apps/meteor/app/apps/server/bridges/settings.ts +++ b/apps/meteor/app/apps/server/bridges/settings.ts @@ -14,7 +14,8 @@ export class AppSettingBridge extends ServerSettingBridge { this.orch.debugLog(`The App ${appId} is getting all the settings.`); const settings = await Settings.find({ secret: false }).toArray(); - return settings.map((s) => this.orch.getConverters()?.get('settings').convertToApp(s)); + const promisedSettings = settings.map(async (s) => this.orch.getConverters()?.get('settings').convertToApp(s)); + return Promise.all(promisedSettings); } protected async getOneById(id: string, appId: string): Promise { diff --git a/apps/meteor/app/apps/server/bridges/uploads.ts b/apps/meteor/app/apps/server/bridges/uploads.ts index 4368b1dba6dff..2d4b56ada13db 100644 --- a/apps/meteor/app/apps/server/bridges/uploads.ts +++ b/apps/meteor/app/apps/server/bridges/uploads.ts @@ -1,11 +1,10 @@ -import { Meteor } from 'meteor/meteor'; import { UploadBridge } from '@rocket.chat/apps-engine/server/bridges/UploadBridge'; -import type { IUpload } from '@rocket.chat/apps-engine/definition/uploads'; import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails'; +import type { IUpload } from '@rocket.chat/apps-engine/definition/uploads'; +import { Upload } from '@rocket.chat/core-services'; -import { FileUpload } from '../../../file-upload/server'; -import { determineFileType } from '../../../../ee/lib/misc/determineFileType'; import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; +import { determineFileType } from '../../../../ee/lib/misc/determineFileType'; const getUploadDetails = (details: IUploadDetails): Partial => { if (details.visitorToken) { @@ -31,19 +30,7 @@ export class AppUploadBridge extends UploadBridge { const rocketChatUpload = this.orch.getConverters()?.get('uploads').convertToRocketChat(upload); - return new Promise((resolve, reject) => { - FileUpload.getBuffer(rocketChatUpload, (error?: Error, result?: Buffer | false) => { - if (error) { - return reject(error); - } - - if (!(result instanceof Buffer)) { - return reject(new Error('Unknown error')); - } - - resolve(result); - }); - }); + return Upload.getBuffer(rocketChatUpload); } protected async createUpload(details: IUploadDetails, buffer: Buffer, appId: string): Promise { @@ -53,19 +40,27 @@ export class AppUploadBridge extends UploadBridge { throw new Error('Missing user to perform the upload operation'); } - const fileStore = FileUpload.getStore('Uploads'); - details.type = determineFileType(buffer, details.name); - return Meteor.runAsUser(details.userId, async () => { - const uploadedFile = await fileStore.insert(getUploadDetails(details), buffer); - this.orch.debugLog(`The App ${appId} has created an upload`, uploadedFile); - if (details.visitorToken) { - await Meteor.callAsync('sendFileLivechatMessage', details.rid, details.visitorToken, uploadedFile); - } else { - await Meteor.callAsync('sendFileMessage', details.rid, null, uploadedFile); - } - return this.orch.getConverters()?.get('uploads').convertToApp(uploadedFile); - }); + const uploadDetails = getUploadDetails(details); + const uploadedFile = await Upload.uploadFile({ buffer, details: uploadDetails, userId: details.userId }); + + if (details.visitorToken) { + await Upload.sendFileLivechatMessage({ + file: uploadedFile, + roomId: details.rid, + visitorToken: details.visitorToken, + }); + } else { + await Upload.sendFileMessage({ + roomId: details.rid, + file: uploadedFile, + userId: details.userId, + }); + } + + this.orch.debugLog(`The App ${appId} has created an upload`, uploadedFile); + + return this.orch.getConverters()?.get('uploads').convertToApp(uploadedFile); } } diff --git a/apps/meteor/app/apps/server/bridges/users.ts b/apps/meteor/app/apps/server/bridges/users.ts index 14861588d422e..cb4e8f2f3d8d1 100644 --- a/apps/meteor/app/apps/server/bridges/users.ts +++ b/apps/meteor/app/apps/server/bridges/users.ts @@ -1,13 +1,12 @@ -import { Random } from '@rocket.chat/random'; +import { v4 as uuid } from 'uuid'; import { UserBridge } from '@rocket.chat/apps-engine/server/bridges/UserBridge'; import type { IUserCreationOptions, IUser, UserType } from '@rocket.chat/apps-engine/definition/users'; import { Subscriptions, Users } from '@rocket.chat/models'; -import { Presence } from '@rocket.chat/core-services'; +import { User as UserService, Presence } from '@rocket.chat/core-services'; import type { UserStatus } from '@rocket.chat/core-typings'; -import { setUserAvatar, deleteUser, getUserCreatedByApp } from '../../../lib/server/functions'; -import { checkUsernameAvailability } from '../../../lib/server/functions/checkUsernameAvailability'; import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; +import { getUserCreatedByApp, deleteUser } from '../../../lib/server'; export class AppUserBridge extends UserBridge { // eslint-disable-next-line no-empty-function @@ -31,7 +30,7 @@ export class AppUserBridge extends UserBridge { this.orch.debugLog(`The App ${appId} is getting its assigned user`); if (!appId) { - return; + throw new Error('No appId provided'); } const user = await Users.findOneByAppId(appId, {}); @@ -59,10 +58,10 @@ export class AppUserBridge extends UserBridge { protected async create(userDescriptor: Partial, appId: string, options?: IUserCreationOptions): Promise { this.orch.debugLog(`The App ${appId} is requesting to create a new user.`); - const user = this.orch.getConverters()?.get('users').convertToRocketChat(userDescriptor); + const user = await this.orch.getConverters()?.get('users').convertToRocketChat(userDescriptor); if (!user._id) { - user._id = Random.id(); + user._id = uuid(); } if (!user.createdAt) { @@ -72,14 +71,14 @@ export class AppUserBridge extends UserBridge { switch (user.type) { case 'bot': case 'app': - if (!(await checkUsernameAvailability(user.username))) { + if (!(await UserService.checkUsernameAvailability(user.username))) { throw new Error(`The username "${user.username}" is already being used. Rename or remove the user using it to install this App`); } await Users.insertOne(user); if (options?.avatarUrl) { - await setUserAvatar(user, options.avatarUrl, '', 'local'); + await UserService.setUserAvatar({ user, dataURI: options.avatarUrl, contentType: '', service: 'local' }); } break; @@ -100,7 +99,7 @@ export class AppUserBridge extends UserBridge { } try { - await deleteUser(user.id); + await UserService.deleteUser(user.id); } catch (err) { throw new Error(`Errors occurred while deleting an app user: ${err}`); } diff --git a/apps/meteor/app/apps/server/converters/messages.js b/apps/meteor/app/apps/server/converters/messages.js index 8eae8eb5954d3..7f5e3d052db98 100644 --- a/apps/meteor/app/apps/server/converters/messages.js +++ b/apps/meteor/app/apps/server/converters/messages.js @@ -1,5 +1,5 @@ -import { Random } from '@rocket.chat/random'; import { Messages, Rooms, Users } from '@rocket.chat/models'; +import { v4 as uuid } from 'uuid'; import { transformMappedData } from '../../../../ee/lib/misc/transformMappedData'; @@ -65,7 +65,7 @@ export class AppMessagesConverter { // When the sender of the message is a Guest (livechat) and not a user if (!user) { - user = this.orch.getConverters().get('users').convertToApp(message.u); + user = await this.orch.getConverters().get('users').convertToApp(message.u); } delete message.u; @@ -119,7 +119,7 @@ export class AppMessagesConverter { const attachments = this._convertAppAttachments(message.attachments); const newMessage = { - _id: message.id || Random.id(), + _id: message.id || uuid(), ...('threadId' in message && { tmid: message.threadId }), rid: room._id, u, @@ -237,6 +237,8 @@ export class AppMessagesConverter { }, }; - return Promise.all(attachments.map(async (attachment) => transformMappedData(attachment, map))); + const promisedAttachments = attachments.map(async (attachment) => transformMappedData(attachment, map)); + + return Promise.all(promisedAttachments); } } diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js index 8fc74ac21c9a1..9f861562a47a7 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.js @@ -1,5 +1,5 @@ import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; -import { LivechatVisitors, Rooms, LivechatDepartment, Users } from '@rocket.chat/models'; +import { LivechatDepartment, LivechatVisitors, Rooms, Users } from '@rocket.chat/models'; import { transformMappedData } from '../../../../ee/lib/misc/transformMappedData'; diff --git a/apps/meteor/app/apps/server/converters/visitors.js b/apps/meteor/app/apps/server/converters/visitors.js index ba288c96d7b81..442b3dd1b3c30 100644 --- a/apps/meteor/app/apps/server/converters/visitors.js +++ b/apps/meteor/app/apps/server/converters/visitors.js @@ -2,7 +2,6 @@ import { LivechatVisitors } from '@rocket.chat/models'; import { transformMappedData } from '../../../../ee/lib/misc/transformMappedData'; -// TODO: check if functions from this converter can be async export class AppVisitorsConverter { constructor(orch) { this.orch = orch; diff --git a/apps/meteor/app/apps/server/settings.ts b/apps/meteor/app/apps/server/settings.ts new file mode 100644 index 0000000000000..8daac8832d294 --- /dev/null +++ b/apps/meteor/app/apps/server/settings.ts @@ -0,0 +1,91 @@ +import type { SettingValue } from '@rocket.chat/core-typings'; +import { AppsLogs } from '@rocket.chat/models'; +import { Apps } from '@rocket.chat/core-services'; + +import { settings, settingsRegistry } from '../../settings/server'; + +export function addAppsSettings() { + void settingsRegistry.addGroup('General', async function () { + await this.section('Apps', async function () { + await this.add('Apps_Logs_TTL', '30_days', { + type: 'select', + values: [ + { + key: '7_days', + i18nLabel: 'Apps_Logs_TTL_7days', + }, + { + key: '14_days', + i18nLabel: 'Apps_Logs_TTL_14days', + }, + { + key: '30_days', + i18nLabel: 'Apps_Logs_TTL_30days', + }, + ], + public: true, + hidden: false, + alert: 'Apps_Logs_TTL_Alert', + }); + + await this.add('Apps_Framework_Source_Package_Storage_Type', 'gridfs', { + type: 'select', + values: [ + { + key: 'gridfs', + i18nLabel: 'GridFS', + }, + { + key: 'filesystem', + i18nLabel: 'FileSystem', + }, + ], + public: true, + hidden: false, + alert: 'Apps_Framework_Source_Package_Storage_Type_Alert', + }); + + await this.add('Apps_Framework_Source_Package_Storage_FileSystem_Path', '', { + type: 'string', + public: true, + enableQuery: { + _id: 'Apps_Framework_Source_Package_Storage_Type', + value: 'filesystem', + }, + alert: 'Apps_Framework_Source_Package_Storage_FileSystem_Alert', + }); + }); + }); +} + +export function watchAppsSettingsChanges() { + settings.watch('Apps_Framework_Source_Package_Storage_Type', async (value: SettingValue) => { + await Apps.setStorage(value as string); + }); + + settings.watch('Apps_Framework_Source_Package_Storage_FileSystem_Path', async (value: SettingValue) => { + await Apps.setFileSystemStoragePath(value as string); + }); + + settings.watch('Apps_Logs_TTL', async (value: SettingValue) => { + let expireAfterSeconds = 0; + + switch (value) { + case '7_days': + expireAfterSeconds = 604800; + break; + case '14_days': + expireAfterSeconds = 1209600; + break; + case '30_days': + expireAfterSeconds = 2592000; + break; + } + + if (!expireAfterSeconds) { + return; + } + + await AppsLogs.resetTTLIndex(expireAfterSeconds); + }); +} diff --git a/apps/meteor/app/apps/server/startup.ts b/apps/meteor/app/apps/server/startup.ts new file mode 100644 index 0000000000000..45c9879489421 --- /dev/null +++ b/apps/meteor/app/apps/server/startup.ts @@ -0,0 +1,5 @@ +import { addAppsSettings, watchAppsSettingsChanges } from './settings'; +import './api'; + +addAppsSettings(); +watchAppsSettingsChanges(); diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index f14bce0bc527d..587685e3afdc3 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -5,6 +5,8 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import _ from 'underscore'; import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; import { Roles, Settings, Users } from '@rocket.chat/models'; +import { AppInterface as AppEvents } from '@rocket.chat/apps-engine/definition/metadata'; +import { Apps } from '@rocket.chat/core-services'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; @@ -15,7 +17,6 @@ import { parseCSV } from '../../../../lib/utils/parseCSV'; import { isValidAttemptByUser, isValidLoginAttemptByIp } from '../lib/restrictLoginAttempts'; import { getClientAddress } from '../../../../server/lib/getClientAddress'; import { getNewUserRoles } from '../../../../server/services/user/lib/getNewUserRoles'; -import { AppEvents, Apps } from '../../../../ee/server/apps/orchestrator'; import { safeGetMeteorUser } from '../../../utils/server/functions/safeGetMeteorUser'; import { safeHtmlDots } from '../../../../lib/utils/safeHtmlDots'; diff --git a/apps/meteor/app/discussion/server/methods/createDiscussion.ts b/apps/meteor/app/discussion/server/methods/createDiscussion.ts index 1e0fa8142fa12..16924b0f612fe 100644 --- a/apps/meteor/app/discussion/server/methods/createDiscussion.ts +++ b/apps/meteor/app/discussion/server/methods/createDiscussion.ts @@ -61,7 +61,7 @@ type CreateDiscussionProperties = { encrypted?: boolean; }; -const create = async ({ prid, pmid, t_name: discussionName, reply, users, user, encrypted }: CreateDiscussionProperties) => { +export const create = async ({ prid, pmid, t_name: discussionName, reply, users, user, encrypted }: CreateDiscussionProperties) => { // if you set both, prid and pmid, and the rooms dont match... should throw an error) let message: null | IMessage = null; if (pmid) { diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.ts b/apps/meteor/app/file-upload/server/lib/FileUpload.ts index dd67b3e5275da..6d601ac85c442 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.ts +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.ts @@ -21,6 +21,7 @@ import { hashLoginToken } from '@rocket.chat/account-utils'; import type { IUpload } from '@rocket.chat/core-typings'; import type { NextFunction } from 'connect'; import type { OptionalId } from 'mongodb'; +import { Apps } from '@rocket.chat/core-services'; import { UploadFS } from '../../../../server/ufs'; import { settings } from '../../../settings/server'; @@ -28,12 +29,12 @@ import { mime } from '../../../utils/lib/mimeTypes'; import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; import { fileUploadIsValidContentType } from '../../../utils/lib/fileUploadRestrictions'; import { isValidJWT, generateJWT } from '../../../utils/server/lib/JWTHelper'; -import { AppEvents, Apps } from '../../../../ee/server/apps'; import { streamToBuffer } from './streamToBuffer'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import type { Store, StoreOptions } from '../../../../server/ufs/ufs-store'; import { ufsComplete } from '../../../../server/ufs/ufs-methods'; +import { AppEvents } from '../../../../ee/server/apps'; const cookie = new Cookies(); let maxFileSize = 0; diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index 914faf775c431..70a94763abe48 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -1,10 +1,10 @@ import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import { Meteor } from 'meteor/meteor'; +import { AppInterface as AppEvents } from '@rocket.chat/apps-engine/definition/metadata'; import type { IUser } from '@rocket.chat/core-typings'; import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; -import { Message, Team } from '@rocket.chat/core-services'; +import { Apps, Message, Team } from '@rocket.chat/core-services'; -import { AppEvents, Apps } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; diff --git a/apps/meteor/app/lib/server/functions/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts index 68553909c6caa..f890137ab1c66 100644 --- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts +++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts @@ -5,8 +5,8 @@ import type { ICreatedRoom, ISubscription, IUser } from '@rocket.chat/core-typin import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; import type { MatchKeysAndValues } from 'mongodb'; import type { ISubscriptionExtraData } from '@rocket.chat/core-services'; +import { Apps } from '@rocket.chat/core-services'; -import { Apps } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { settings } from '../../../settings/server'; import { getDefaultSubscriptionPref } from '../../../utils/server'; diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 9793f3cfaa3ba..6203488445bff 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -1,11 +1,10 @@ import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import { Meteor } from 'meteor/meteor'; import type { ICreatedRoom, IUser, IRoom, RoomType } from '@rocket.chat/core-typings'; -import { Message, Team } from '@rocket.chat/core-services'; +import { Message, Team, Apps } from '@rocket.chat/core-services'; import type { ICreateRoomParams, ISubscriptionExtraData } from '@rocket.chat/core-services'; import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; -import { Apps } from '../../../../ee/server/apps'; import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; import { callbacks } from '../../../../lib/callbacks'; import { getValidRoomName } from '../../../utils/server'; diff --git a/apps/meteor/app/lib/server/functions/deleteMessage.ts b/apps/meteor/app/lib/server/functions/deleteMessage.ts index 0a1dcd85ed3d7..d12f415d8d150 100644 --- a/apps/meteor/app/lib/server/functions/deleteMessage.ts +++ b/apps/meteor/app/lib/server/functions/deleteMessage.ts @@ -1,22 +1,21 @@ import { Meteor } from 'meteor/meteor'; import type { IMessage, IUser } from '@rocket.chat/core-typings'; import { Messages, Rooms, Uploads } from '@rocket.chat/models'; -import { api } from '@rocket.chat/core-services'; +import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata'; +import { api, Apps } from '@rocket.chat/core-services'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; import { callbacks } from '../../../../lib/callbacks'; -import { Apps } from '../../../../ee/server/apps'; export async function deleteMessage(message: IMessage, user: IUser): Promise { const deletedMsg = await Messages.findOneById(message._id); const isThread = (deletedMsg?.tcount || 0) > 0; const keepHistory = settings.get('Message_KeepHistory') || isThread; const showDeletedStatus = settings.get('Message_ShowDeletedStatus') || isThread; - const bridges = Apps?.isLoaded() && Apps.getBridges(); - if (deletedMsg && bridges) { - const prevent = await bridges.getListenerBridge().messageEvent('IPreMessageDeletePrevent', deletedMsg); + if (deletedMsg) { + const prevent = await Apps.triggerEvent(AppInterface.IPreMessageDeletePrevent, deletedMsg); if (prevent) { throw new Meteor.Error('error-app-prevented-deleting', 'A Rocket.Chat App prevented the message deleting.'); } @@ -69,7 +68,5 @@ export async function deleteMessage(message: IMessage, user: IUser): Promise { const originalMessage = originalMsg || (await Messages.findOneById(message._id)); // For the Rocket.Chat Apps :) - if (message && Apps && Apps.isLoaded()) { + if (message) { const appMessage = Object.assign({}, originalMessage, message); - const prevent = await Apps.getBridges()?.getListenerBridge().messageEvent('IPreMessageUpdatedPrevent', appMessage); + const prevent = Promise.await(Apps.triggerEvent(AppInterface.IPreMessageUpdatedPrevent, appMessage)); if (prevent) { throw new Meteor.Error('error-app-prevented-updating', 'A Rocket.Chat App prevented the message updating.'); } let result; - result = await Apps.getBridges()?.getListenerBridge().messageEvent('IPreMessageUpdatedExtend', appMessage); - result = await Apps.getBridges()?.getListenerBridge().messageEvent('IPreMessageUpdatedModify', result); + result = Promise.await(Apps.triggerEvent(AppInterface.IPreMessageUpdatedExtend, appMessage)); + result = Promise.await(Apps.triggerEvent(AppInterface.IPreMessageUpdatedModify, result)); if (typeof result === 'object') { message = Object.assign(appMessage, result); @@ -68,11 +69,9 @@ export const updateMessage = async function (message: IMessage, user: IUser, ori return; } - if (Apps?.isLoaded()) { - // This returns a promise, but it won't mutate anything about the message - // so, we don't really care if it is successful or fails - void Apps.getBridges()?.getListenerBridge().messageEvent('IPostMessageUpdated', message); - } + // This returns a promise, but it won't mutate anything about the message + // so, we don't really care if it is successful or fails + void Apps.triggerEvent(AppInterface.IPostMessageUpdated, message); setImmediate(async function () { const msg = await Messages.findOneById(_id); diff --git a/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts b/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts index 73c104468135e..346f0aa8439bc 100644 --- a/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts +++ b/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts @@ -4,10 +4,11 @@ import { Accounts } from 'meteor/accounts-base'; import { SHA256 } from '@rocket.chat/sha256'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Users } from '@rocket.chat/models'; +import { AppInterface as AppEvents } from '@rocket.chat/apps-engine/definition/metadata'; +import { Apps } from '@rocket.chat/core-services'; import { settings } from '../../../settings/server'; import { deleteUser } from '../functions'; -import { AppEvents, Apps } from '../../../../ee/server/apps/orchestrator'; import { trim } from '../../../../lib/utils/stringUtils'; declare module '@rocket.chat/ui-contexts' { diff --git a/apps/meteor/app/lib/server/methods/executeSlashCommandPreview.ts b/apps/meteor/app/lib/server/methods/executeSlashCommandPreview.ts index de7102e69886c..3d7e4188d6175 100644 --- a/apps/meteor/app/lib/server/methods/executeSlashCommandPreview.ts +++ b/apps/meteor/app/lib/server/methods/executeSlashCommandPreview.ts @@ -21,7 +21,8 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ executeSlashCommandPreview(command, preview) { - if (!Meteor.userId()) { + const userId = Meteor.userId(); + if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getSlashCommandPreview', }); @@ -46,6 +47,6 @@ Meteor.methods({ }); } - return slashCommands.executePreview(command.cmd, command.params, command.msg, preview, command.triggerId); + return slashCommands.executePreview(command.cmd, command.params, command.msg, preview, command.triggerId, userId); }, }); diff --git a/apps/meteor/app/lib/server/methods/getSlashCommandPreviews.ts b/apps/meteor/app/lib/server/methods/getSlashCommandPreviews.ts index 5a0cf2f736a2b..7cdb27f57c6d9 100644 --- a/apps/meteor/app/lib/server/methods/getSlashCommandPreviews.ts +++ b/apps/meteor/app/lib/server/methods/getSlashCommandPreviews.ts @@ -17,7 +17,8 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async getSlashCommandPreviews(command) { - if (!Meteor.userId()) { + const userId = Meteor.userId(); + if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getSlashCommandPreview', }); @@ -36,6 +37,6 @@ Meteor.methods({ }); } - return slashCommands.getPreviews(command.cmd, command.params, command.msg); + return slashCommands.getPreviews(command.cmd, command.params, command.msg, userId); }, }); diff --git a/apps/meteor/app/livechat/server/lib/Helper.js b/apps/meteor/app/livechat/server/lib/Helper.js index 51626275a4553..4807feba36d94 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.js +++ b/apps/meteor/app/livechat/server/lib/Helper.js @@ -2,9 +2,10 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { Match, check } from 'meteor/check'; import { LivechatTransferEventType } from '@rocket.chat/apps-engine/definition/livechat'; +import { AppInterface as AppEvents } from '@rocket.chat/apps-engine/definition/metadata'; +import { api, Apps, Message } from '@rocket.chat/core-services'; import { OmnichannelSourceType, DEFAULT_SLA_CONFIG } from '@rocket.chat/core-typings'; import { LivechatPriorityWeight } from '@rocket.chat/core-typings/src/ILivechatPriority'; -import { api, Message } from '@rocket.chat/core-services'; import { LivechatDepartmentAgents, Users as UsersRaw, @@ -22,7 +23,6 @@ import { RoutingManager } from './RoutingManager'; import { callbacks } from '../../../../lib/callbacks'; import { Logger } from '../../../logger/server'; import { settings } from '../../../settings/server'; -import { Apps, AppEvents } from '../../../../ee/server/apps'; import { sendNotification } from '../../../lib/server'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; import { queueInquiry, saveQueueInquiry } from './QueueManager'; diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 04841972f68b9..30bce524dd460 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -8,6 +8,7 @@ import { Match, check } from 'meteor/check'; import { Random } from '@rocket.chat/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import UAParser from 'ua-parser-js'; +import { AppInterface as AppEvents } from '@rocket.chat/apps-engine/definition/metadata'; import { LivechatVisitors, LivechatCustomField, @@ -21,7 +22,7 @@ import { Rooms, Users, } from '@rocket.chat/models'; -import { Message, VideoConf, api } from '@rocket.chat/core-services'; +import { Apps, Message, VideoConf, api } from '@rocket.chat/core-services'; import { QueueManager } from './QueueManager'; import { RoutingManager } from './RoutingManager'; @@ -38,7 +39,6 @@ import { updateMessage } from '../../../lib/server/functions/updateMessage'; import { deleteMessage } from '../../../lib/server/functions/deleteMessage'; import { FileUpload } from '../../../file-upload/server'; import { normalizeTransferredByData, parseAgentCustomFields, updateDepartmentAgents, validateEmail } from './Helper'; -import { Apps, AppEvents } from '../../../../ee/server/apps'; import { businessHourManager } from '../business-hour'; import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles'; diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.js b/apps/meteor/app/livechat/server/lib/RoutingManager.js index 0a79b06474744..9456104ffc6dd 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.js +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.js @@ -1,7 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; +import { AppInterface as AppEvents } from '@rocket.chat/apps-engine/definition/metadata'; +import { Apps, Message } from '@rocket.chat/core-services'; import { LivechatInquiry, LivechatRooms, Subscriptions, Rooms, Users } from '@rocket.chat/models'; -import { Message } from '@rocket.chat/core-services'; import { createLivechatSubscription, @@ -15,7 +16,6 @@ import { } from './Helper'; import { callbacks } from '../../../../lib/callbacks'; import { Logger } from '../../../../server/lib/logger/Logger'; -import { Apps, AppEvents } from '../../../../ee/server/apps'; const logger = new Logger('RoutingManager'); @@ -111,7 +111,7 @@ export const RoutingManager = { await dispatchAgentDelegated(rid, agent.agentId); logger.debug(`Agent ${agent.agentId} assigned to inquriy ${inquiry._id}. Instances notified`); - Apps.getBridges().getListenerBridge().livechatEvent(AppEvents.IPostLivechatAgentAssigned, { room, user }); + Apps.triggerEvent(AppEvents.IPostLivechatAgentAssigned, { room, user }); return inquiry; }, diff --git a/apps/meteor/app/mailer/server/api.ts b/apps/meteor/app/mailer/server/api.ts index cfc8bde01dcf2..08cce3b805398 100644 --- a/apps/meteor/app/mailer/server/api.ts +++ b/apps/meteor/app/mailer/server/api.ts @@ -7,10 +7,10 @@ import stripHtml from 'string-strip-html'; import { escapeHTML } from '@rocket.chat/string-helpers'; import type { ISetting } from '@rocket.chat/core-typings'; import { Settings } from '@rocket.chat/models'; +import { Apps } from '@rocket.chat/core-services'; import { settings } from '../../settings/server'; import { replaceVariables } from './replaceVariables'; -import { Apps } from '../../../ee/server/apps'; import { validateEmail } from '../../../lib/emailValidator'; import { strLeft, strRightBack } from '../../../lib/utils/stringUtils'; diff --git a/apps/meteor/app/message-pin/server/pinMessage.ts b/apps/meteor/app/message-pin/server/pinMessage.ts index b7819b304106e..5a9d6125b17c8 100644 --- a/apps/meteor/app/message-pin/server/pinMessage.ts +++ b/apps/meteor/app/message-pin/server/pinMessage.ts @@ -3,7 +3,7 @@ import { check } from 'meteor/check'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import type { IMessage, IUser, MessageAttachment, MessageQuoteAttachment } from '@rocket.chat/core-typings'; import { isQuoteAttachment } from '@rocket.chat/core-typings'; -import { Message } from '@rocket.chat/core-services'; +import { Apps, Message } from '@rocket.chat/core-services'; import { Messages, Rooms, Subscriptions, Users } from '@rocket.chat/models'; import { settings } from '../../settings/server'; @@ -12,8 +12,8 @@ import { isTheLastMessage } from '../../lib/server'; import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL'; import { canAccessRoomAsync, roomAccessAttributes } from '../../authorization/server'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; -import { Apps, AppEvents } from '../../../ee/server/apps/orchestrator'; import { isTruthy } from '../../../lib/isTruthy'; +import { AppEvents } from '../../../ee/server/apps'; const recursiveRemove = (msg: MessageAttachment, deep = 1) => { if (!msg || !isQuoteAttachment(msg)) { diff --git a/apps/meteor/app/message-star/server/starMessage.ts b/apps/meteor/app/message-star/server/starMessage.ts index 61317f9a5ab9a..057fe2667aa65 100644 --- a/apps/meteor/app/message-star/server/starMessage.ts +++ b/apps/meteor/app/message-star/server/starMessage.ts @@ -2,11 +2,12 @@ import { Meteor } from 'meteor/meteor'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import type { IMessage } from '@rocket.chat/core-typings'; import { Messages, Subscriptions, Rooms } from '@rocket.chat/models'; +import { Apps } from '@rocket.chat/core-services'; import { settings } from '../../settings/server'; import { isTheLastMessage } from '../../lib/server'; import { canAccessRoomAsync, roomAccessAttributes } from '../../authorization/server'; -import { Apps, AppEvents } from '../../../ee/server/apps/orchestrator'; +import { AppEvents } from '../../../ee/server/apps'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention 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/reactions/server/setReaction.ts b/apps/meteor/app/reactions/server/setReaction.ts index 19178a7cd5754..785c5987de005 100644 --- a/apps/meteor/app/reactions/server/setReaction.ts +++ b/apps/meteor/app/reactions/server/setReaction.ts @@ -1,8 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import _ from 'underscore'; +import { AppInterface as AppEvents } from '@rocket.chat/apps-engine/definition/metadata'; +import { api, Apps } from '@rocket.chat/core-services'; import { Messages, EmojiCustom, Rooms } from '@rocket.chat/models'; -import { api } from '@rocket.chat/core-services'; import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; @@ -11,7 +12,6 @@ import { emoji } from '../../emoji/server'; import { isTheLastMessage } from '../../lib/server'; import { canAccessRoomAsync } from '../../authorization/server'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; -import { AppEvents, Apps } from '../../../ee/server/apps/orchestrator'; const removeUserReaction = (message: IMessage, reaction: string, username: string) => { if (!message.reactions) { diff --git a/apps/meteor/app/statistics/server/lib/getAppsStatistics.js b/apps/meteor/app/statistics/server/lib/getAppsStatistics.js index e44af22aa166b..d7459fab74f05 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 { totalInstalled, totalActive, totalFailed } = 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 abcbde7169f20..88ede65302f61 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(); diff --git a/apps/meteor/app/threads/server/methods/followMessage.ts b/apps/meteor/app/threads/server/methods/followMessage.ts index 18eb795c31c56..b38cb113a5994 100644 --- a/apps/meteor/app/threads/server/methods/followMessage.ts +++ b/apps/meteor/app/threads/server/methods/followMessage.ts @@ -1,5 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; +import { AppInterface as AppEvents } from '@rocket.chat/apps-engine/definition/metadata'; +import { Apps } from '@rocket.chat/core-services'; import type { IMessage } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Messages } from '@rocket.chat/models'; @@ -8,7 +10,6 @@ import { RateLimiter } from '../../../lib/server'; import { settings } from '../../../settings/server'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { follow } from '../functions'; -import { Apps, AppEvents } from '../../../../ee/server/apps/orchestrator'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/apps/meteor/app/threads/server/methods/unfollowMessage.ts b/apps/meteor/app/threads/server/methods/unfollowMessage.ts index d2b337634bf13..eff9ad0db9a3a 100644 --- a/apps/meteor/app/threads/server/methods/unfollowMessage.ts +++ b/apps/meteor/app/threads/server/methods/unfollowMessage.ts @@ -1,5 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; +import { AppInterface as AppEvents } from '@rocket.chat/apps-engine/definition/metadata'; +import { Apps } from '@rocket.chat/core-services'; import type { IMessage } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Messages } from '@rocket.chat/models'; @@ -8,7 +10,6 @@ import { RateLimiter } from '../../../lib/server'; import { settings } from '../../../settings/server'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { unfollow } from '../functions'; -import { Apps, AppEvents } from '../../../../ee/server/apps/orchestrator'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/apps/meteor/app/utils/lib/slashCommand.ts b/apps/meteor/app/utils/lib/slashCommand.ts index 9acc70c4896f0..37bb1c046355c 100644 --- a/apps/meteor/app/utils/lib/slashCommand.ts +++ b/apps/meteor/app/utils/lib/slashCommand.ts @@ -53,6 +53,7 @@ export const slashCommands = { params: string, message: RequiredField, 'rid'>, triggerId?: string | undefined, + userId?: string, ): Promise { const cmd = this.commands[command]; if (typeof cmd?.callback !== 'function') { @@ -63,12 +64,13 @@ export const slashCommands = { throw new Meteor.Error('invalid-command-usage', 'Executing a command requires at least a message with a room id.'); } - return cmd.callback(command, params, message, triggerId); + return cmd.callback(command, params, message, triggerId, userId); }, async getPreviews( command: string, params: string, message: RequiredField, 'rid'>, + userId?: string, ): Promise { const cmd = this.commands[command]; if (typeof cmd?.previewer !== 'function') { @@ -79,7 +81,7 @@ export const slashCommands = { throw new Meteor.Error('invalid-command-usage', 'Executing a command requires at least a message with a room id.'); } - const previewInfo = await cmd.previewer(command, params, message); + const previewInfo = await cmd.previewer(command, params, message, userId); if (!previewInfo?.items?.length) { return; @@ -98,6 +100,7 @@ export const slashCommands = { message: Pick & Partial>, preview: SlashCommandPreviewItem, triggerId?: string, + userId?: string, ) { const cmd = this.commands[command]; if (typeof cmd?.previewCallback !== 'function') { @@ -113,7 +116,7 @@ export const slashCommands = { throw new Meteor.Error('error-invalid-preview', 'Preview Item must have an id, type, and value.'); } - return cmd.previewCallback(command, params, message, preview, triggerId); + return cmd.previewCallback(command, params, message, preview, triggerId, userId); }, }; @@ -126,7 +129,8 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async slashCommand(command) { - if (!Meteor.userId()) { + const userId = Meteor.userId(); + if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'slashCommand', }); @@ -137,6 +141,6 @@ Meteor.methods({ method: 'executeSlashCommandPreview', }); } - return slashCommands.run(command.cmd, command.params, command.msg, command.triggerId); + return slashCommands.run(command.cmd, command.params, command.msg, command.triggerId, userId); }, }); diff --git a/apps/meteor/ee/app/apps/apiService.ts b/apps/meteor/ee/app/apps/apiService.ts new file mode 100644 index 0000000000000..1ef9660ccca7b --- /dev/null +++ b/apps/meteor/ee/app/apps/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 type { AppServerOrchestrator } from '../../server/apps/orchestrator'; +import { OrchestratorFactory } from './orchestratorFactory'; + +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/app/apps/converterService.ts b/apps/meteor/ee/app/apps/converterService.ts new file mode 100644 index 0000000000000..87720b0f21e94 --- /dev/null +++ b/apps/meteor/ee/app/apps/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 type { AppServerOrchestrator } from '../../server/apps/orchestrator'; +import { OrchestratorFactory } from './orchestratorFactory'; + +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/app/apps/managerService.ts b/apps/meteor/ee/app/apps/managerService.ts new file mode 100644 index 0000000000000..da25d080c47c7 --- /dev/null +++ b/apps/meteor/ee/app/apps/managerService.ts @@ -0,0 +1,120 @@ +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 { IAppLogStorageFindOptions, 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 type { ILoggerStorageEntry } from '@rocket.chat/apps-engine/server/logging'; + +import type { AppServerOrchestrator } from '../../server/apps/orchestrator'; +import { OrchestratorFactory } from './orchestratorFactory'; + +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, status?: AppStatus, silent?: boolean): Promise { + return this.apps.getManager()?.disable(appId, status, silent); + } + + 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: boolean }, + ): Promise { + return this.apps.getManager()?.update(appPackage, permissionsGranted, updateOptions); + } + + async updateLocal(stored: IAppStorageItem, appPackageOrInstance: ProxiedApp | Buffer): Promise { + await 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); + } + + getAppSetting(appId: string, settingId: string): ISetting | undefined { + return this.apps.getManager()?.getSettingsManager().getAppSetting(appId, settingId); + } + + 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); + } + + async findLogs(query: { [field: string]: any }, options?: IAppLogStorageFindOptions): Promise | undefined> { + return this.apps.getManager()?.getLogStorage().find(query, options); + } + + async getAppStorageItemById(id: string): Promise { + return this.apps.getManager()?.getStorage().retrieveOne(id) ?? null; + } +} diff --git a/apps/meteor/ee/app/apps/orchestratorFactory.ts b/apps/meteor/ee/app/apps/orchestratorFactory.ts new file mode 100644 index 0000000000000..5f0f39338668f --- /dev/null +++ b/apps/meteor/ee/app/apps/orchestratorFactory.ts @@ -0,0 +1,40 @@ +import type { Db } from 'mongodb'; + +import { AppServerOrchestrator } from '../../server/apps/orchestrator'; +import { settings } from '../../../app/settings/server'; + +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/app/apps/service.ts b/apps/meteor/ee/app/apps/service.ts new file mode 100644 index 0000000000000..331777a1c4021 --- /dev/null +++ b/apps/meteor/ee/app/apps/service.ts @@ -0,0 +1,128 @@ +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 { settings } from '../../../app/settings/server'; +import type { AppServerOrchestrator } from '../../server/apps/orchestrator'; +import { OrchestratorFactory } from './orchestratorFactory'; + +type AppsInitParams = { + appsSourceStorageFilesystemPath: any; + appsSourceStorageType: any; + marketplaceUrl?: string | undefined; +}; + +export class AppsOrchestratorService extends ServiceClass implements IAppsService { + protected name = 'apps'; + + private apps: AppServerOrchestrator; + + private 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', + }; + + constructor(db: Db) { + super(); + + this.apps = OrchestratorFactory.getOrchestrator(db); + } + + async started(): Promise { + if (this.apps.isLoaded()) { + return; + } + + void 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 { + return this.apps.initialize(this.appsInitParams); + } + + 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); + } + + rocketChatLoggerDebug(args?: any) { + return this.apps.debugLog(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); + } + + // runOnAppEvent(listener: AppServerNotifier): void { + // Object.entries(AppEvents).forEach(([key, value]) => { + // this.apps.appEventsSink.on(value, (...args) => { + // const method = + // key.toLowerCase().split('_')[0] + + // key + // .toLowerCase() + // .split('_') + // .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + // .flat() + // .join(''); + // listener[method](...args); + // }); + // }); + // } +} diff --git a/apps/meteor/ee/app/apps/statisticsService.ts b/apps/meteor/ee/app/apps/statisticsService.ts new file mode 100644 index 0000000000000..3877faf504710 --- /dev/null +++ b/apps/meteor/ee/app/apps/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 type { AppServerOrchestrator } from '../../server/apps/orchestrator'; +import { OrchestratorFactory } from './orchestratorFactory'; + +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/storage/AppFileSystemSourceStorage.ts b/apps/meteor/ee/app/apps/storage/AppFileSystemSourceStorage.ts similarity index 100% rename from apps/meteor/ee/server/apps/storage/AppFileSystemSourceStorage.ts rename to apps/meteor/ee/app/apps/storage/AppFileSystemSourceStorage.ts diff --git a/apps/meteor/ee/server/apps/storage/AppGridFSSourceStorage.ts b/apps/meteor/ee/app/apps/storage/AppGridFSSourceStorage.ts similarity index 93% rename from apps/meteor/ee/server/apps/storage/AppGridFSSourceStorage.ts rename to apps/meteor/ee/app/apps/storage/AppGridFSSourceStorage.ts index 3adfaf6d46cbe..c34889cacdef6 100644 --- a/apps/meteor/ee/server/apps/storage/AppGridFSSourceStorage.ts +++ b/apps/meteor/ee/app/apps/storage/AppGridFSSourceStorage.ts @@ -1,5 +1,4 @@ -import { MongoInternals } from 'meteor/mongo'; -import type { GridFSBucketWriteStream } from 'mongodb'; +import type { Db, GridFSBucketWriteStream } from 'mongodb'; import { ObjectId, GridFSBucket } from 'mongodb'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; import { AppSourceStorage } from '@rocket.chat/apps-engine/server/storage'; @@ -11,11 +10,9 @@ export class AppGridFSSourceStorage extends AppSourceStorage { private bucket: GridFSBucket; - constructor() { + constructor(db: Db) { super(); - const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; - this.bucket = new GridFSBucket(db, { bucketName: 'rocketchat_apps_packages', chunkSizeBytes: 1024 * 255, diff --git a/apps/meteor/ee/server/apps/storage/AppRealStorage.ts b/apps/meteor/ee/app/apps/storage/AppRealStorage.ts similarity index 99% rename from apps/meteor/ee/server/apps/storage/AppRealStorage.ts rename to apps/meteor/ee/app/apps/storage/AppRealStorage.ts index cceb70da9e960..38930f0fcc20a 100644 --- a/apps/meteor/ee/server/apps/storage/AppRealStorage.ts +++ b/apps/meteor/ee/app/apps/storage/AppRealStorage.ts @@ -29,6 +29,7 @@ export class AppRealStorage extends AppMetadataStorage { public async retrieveAll(): Promise> { const docs = await this.db.find({}).toArray(); + const items = new Map(); docs.forEach((i) => items.set(i.id, i)); @@ -38,11 +39,13 @@ export class AppRealStorage extends AppMetadataStorage { public async update(item: IAppStorageItem): Promise { await this.db.updateOne({ id: item.id }, { $set: item }); + return this.retrieveOne(item.id); } public async remove(id: string): Promise<{ success: boolean }> { await this.db.deleteOne({ id }); + return { success: true }; } } diff --git a/apps/meteor/ee/server/apps/storage/ConfigurableAppSourceStorage.ts b/apps/meteor/ee/app/apps/storage/ConfigurableAppSourceStorage.ts similarity index 93% rename from apps/meteor/ee/server/apps/storage/ConfigurableAppSourceStorage.ts rename to apps/meteor/ee/app/apps/storage/ConfigurableAppSourceStorage.ts index 42a0b5d3e40ca..e2fd0db596241 100644 --- a/apps/meteor/ee/server/apps/storage/ConfigurableAppSourceStorage.ts +++ b/apps/meteor/ee/app/apps/storage/ConfigurableAppSourceStorage.ts @@ -1,5 +1,6 @@ import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; import { AppSourceStorage } from '@rocket.chat/apps-engine/server/storage'; +import type { Db } from 'mongodb'; import { AppFileSystemSourceStorage } from './AppFileSystemSourceStorage'; import { AppGridFSSourceStorage } from './AppGridFSSourceStorage'; @@ -11,11 +12,11 @@ export class ConfigurableAppSourceStorage extends AppSourceStorage { private storage: AppSourceStorage; - constructor(readonly storageType: string, filesystemStoragePath: string) { + constructor(readonly storageType: string, filesystemStoragePath: string, db: Db) { super(); this.filesystem = new AppFileSystemSourceStorage(); - this.gridfs = new AppGridFSSourceStorage(); + this.gridfs = new AppGridFSSourceStorage(db); this.setStorage(storageType); this.setFileSystemStoragePath(filesystemStoragePath); diff --git a/apps/meteor/ee/app/apps/storage/LogsStorage.ts b/apps/meteor/ee/app/apps/storage/LogsStorage.ts new file mode 100644 index 0000000000000..0e7033b6deca6 --- /dev/null +++ b/apps/meteor/ee/app/apps/storage/LogsStorage.ts @@ -0,0 +1,39 @@ +import type { ILoggerStorageEntry } from '@rocket.chat/apps-engine/server/logging'; +import { AppConsole } from '@rocket.chat/apps-engine/server/logging'; +import type { IAppLogStorageFindOptions } from '@rocket.chat/apps-engine/server/storage'; +import { AppLogStorage } from '@rocket.chat/apps-engine/server/storage'; +import type { IAppsLogsModel } from '@rocket.chat/model-typings'; +import { InstanceStatus } from '@rocket.chat/instance-status'; + +export class AppRealLogsStorage extends AppLogStorage { + constructor(private db: IAppsLogsModel) { + super('mongodb'); + } + + public async find( + query: { + [field: string]: any; + }, + options?: IAppLogStorageFindOptions, + ): Promise> { + return this.db.find(query, { projection: options?.fields || {} }).toArray(); + } + + public async storeEntries(appId: string, logger: AppConsole): Promise { + const item = AppConsole.toStorageEntry(appId, logger); + + item.instanceId = InstanceStatus.id(); + + const id = (await this.db.insertOne(item)).insertedId; + + return this.db.findOneById(id); + } + + public async getEntriesFor(appId: string): Promise> { + return this.db.find({ appId }).toArray(); + } + + public async removeEntriesFor(appId: string): Promise { + await this.db.remove({ appId }); + } +} diff --git a/apps/meteor/ee/server/apps/storage/index.js b/apps/meteor/ee/app/apps/storage/index.js similarity index 50% rename from apps/meteor/ee/server/apps/storage/index.js rename to apps/meteor/ee/app/apps/storage/index.js index 7f8d90715a963..3d3913d81fc8f 100644 --- a/apps/meteor/ee/server/apps/storage/index.js +++ b/apps/meteor/ee/app/apps/storage/index.js @@ -1,6 +1,3 @@ -import './AppFileSystemSourceStorage'; -import './AppGridFSSourceStorage'; - -export { AppRealLogsStorage } from './logs-storage'; +export { AppRealLogsStorage } from './LogsStorage'; export { AppRealStorage } from './AppRealStorage'; export { ConfigurableAppSourceStorage } from './ConfigurableAppSourceStorage'; diff --git a/apps/meteor/ee/app/apps/videoManagerService.ts b/apps/meteor/ee/app/apps/videoManagerService.ts new file mode 100644 index 0000000000000..79c1aed00efe6 --- /dev/null +++ b/apps/meteor/ee/app/apps/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 type { AppServerOrchestrator } from '../../server/apps/orchestrator'; +import { OrchestratorFactory } from './orchestratorFactory'; + +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 { + this.getVideoConfProviderManager().onUserJoin(providerName, call, user); + } + + async onNewVideoConference(providerName: string, call: VideoConference): Promise { + this.getVideoConfProviderManager().onNewVideoConference(providerName, call); + } + + async onVideoConferenceChanged(providerName: string, call: VideoConference): Promise { + 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/ee/server/apps/appRequestsCron.ts b/apps/meteor/ee/server/apps/appRequestsCron.ts index d1b556209785d..9f06518cb3611 100644 --- a/apps/meteor/ee/server/apps/appRequestsCron.ts +++ b/apps/meteor/ee/server/apps/appRequestsCron.ts @@ -1,14 +1,14 @@ import { SyncedCron } from 'meteor/littledata:synced-cron'; +import { Apps, AppsManager } from '@rocket.chat/core-services'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { settings } from '../../../app/settings/server'; -import { Apps } from './orchestrator'; import { getWorkspaceAccessToken } from '../../../app/cloud/server'; import { appRequestNotififyForUsers } from './marketplace/appRequestNotifyUsers'; const appsNotifyAppRequests = async function _appsNotifyAppRequests() { try { - const installedApps = await Apps.installedApps({ enabled: true }); + const installedApps = await AppsManager.get({ enabled: true }); if (!installedApps || installedApps.length === 0) { return; } @@ -17,13 +17,13 @@ const appsNotifyAppRequests = async function _appsNotifyAppRequests() { const token = await getWorkspaceAccessToken(); if (!token) { - Apps.debugLog(`could not load workspace token to send app requests notifications`); + await Apps.rocketChatLoggerDebug(`could not load workspace token to send app requests notifications`); return; } - const baseUrl = Apps.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); if (!baseUrl) { - Apps.debugLog(`could not load marketplace base url to send app requests notifications`); + await Apps.rocketChatLoggerDebug(`could not load marketplace base url to send app requests notifications`); return; } @@ -35,10 +35,12 @@ const appsNotifyAppRequests = async function _appsNotifyAppRequests() { const pendingSentUrl = `${baseUrl}/v1/app-request/sent/pending`; const result = await fetch(pendingSentUrl, options); - const { data } = await result.json(); - const filtered = installedApps.filter((app) => data.indexOf(app.getID()) !== -1); + const data = (await result.json()).data?.data; + const filtered = installedApps.filter((app) => data.indexOf(app?.getID()) !== -1); for await (const app of filtered) { + if (!app) continue; + const appId = app.getID(); const appName = app.getName(); @@ -48,18 +50,18 @@ const appsNotifyAppRequests = async function _appsNotifyAppRequests() { await fetch(`${baseUrl}/v1/app-request/markAsSent/${appId}`, { ...options, method: 'POST' }); return response; }) - .catch((err) => { - Apps.debugLog(`could not send app request notifications for app ${appId}. Error: ${err}`); + .catch(async (err) => { + await Apps.rocketChatLoggerDebug(`could not send app request notifications for app ${appId}. Error: ${err}`); return err; }); const errors = (usersNotified as (string | Error)[]).filter((batch) => batch instanceof Error); if (errors.length > 0) { - Apps.debugLog(`Some batches of users could not be notified for app ${appId}. Errors: ${errors}`); + await Apps.rocketChatLoggerDebug(`Some batches of users could not be notified for app ${appId}. Errors: ${errors}`); } } } catch (err) { - Apps.debugLog(err); + await Apps.rocketChatLoggerDebug(err); } }; diff --git a/apps/meteor/ee/server/apps/communication/endpoints/actionButtonsHandler.ts b/apps/meteor/ee/server/apps/communication/endpoints/actionButtonsHandler.ts index ab9a8feffb405..7d1da7ac41c79 100644 --- a/apps/meteor/ee/server/apps/communication/endpoints/actionButtonsHandler.ts +++ b/apps/meteor/ee/server/apps/communication/endpoints/actionButtonsHandler.ts @@ -1,18 +1,15 @@ -import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; +import { AppsManager } from '@rocket.chat/core-services'; import { API } from '../../../../../app/api/server'; -import type { AppsRestApi } from '../rest'; -export const actionButtonsHandler = (apiManager: AppsRestApi) => +export const actionButtonsHandler = () => [ { authRequired: false, }, { get(): any { - const manager = apiManager._manager as AppManager; - - const buttons = manager.getUIActionButtonManager().getAllActionButtons(); + const buttons = AppsManager.getAllActionButtons(); return API.v1.success(buttons); }, diff --git a/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts b/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts index e0e79c27b6c56..806ae0e06c95d 100644 --- a/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts +++ b/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts @@ -1,7 +1,7 @@ -import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; +import { AppsManager } from '@rocket.chat/core-services'; +import type { IAppStorageItem } from '@rocket.chat/core-typings'; import { API } from '../../../../../app/api/server'; -import type { AppsRestApi } from '../rest'; import { getAppsConfig } from '../../../../app/license/server/license'; import type { SuccessResult } from '../../../../../app/api/server/definition'; import { getInstallationSourceFromAppStorageItem } from '../../../../../lib/apps/getInstallationSourceFromAppStorageItem'; @@ -13,22 +13,23 @@ type AppsCountResult = { maxPrivateApps: number; }; -export const appsCountHandler = (apiManager: AppsRestApi) => +export const appsCountHandler = () => [ { authRequired: false, }, { - get(): SuccessResult { - const manager = apiManager._manager as AppManager; - - const apps = manager.get({ enabled: true }); + async get(): Promise> { + const apps = await AppsManager.get({ enabled: true }); const { maxMarketplaceApps, maxPrivateApps } = getAppsConfig(); return API.v1.success({ - totalMarketplaceEnabled: apps.filter((app) => getInstallationSourceFromAppStorageItem(app.getStorageItem()) === 'marketplace') - .length, - totalPrivateEnabled: apps.filter((app) => getInstallationSourceFromAppStorageItem(app.getStorageItem()) === 'private').length, + totalMarketplaceEnabled: apps.filter( + (app) => getInstallationSourceFromAppStorageItem(app?.getStorageItem() as IAppStorageItem) === 'marketplace', + ).length, + totalPrivateEnabled: apps.filter( + (app) => getInstallationSourceFromAppStorageItem(app?.getStorageItem() as IAppStorageItem) === 'private', + ).length, maxMarketplaceApps, maxPrivateApps, }); diff --git a/apps/meteor/ee/server/apps/communication/index.ts b/apps/meteor/ee/server/apps/communication/index.ts index 73405ca1a2d8c..a16bc073eacee 100644 --- a/apps/meteor/ee/server/apps/communication/index.ts +++ b/apps/meteor/ee/server/apps/communication/index.ts @@ -1,5 +1,7 @@ import { AppsRestApi } from './rest'; import { AppUIKitInteractionApi } from './uikit'; -import { AppServerNotifier } from './websockets'; +import { AppServerListener, AppServerNotifier } from './websockets'; +import { AppEvents } from './events'; +import startup from './startup'; -export { AppUIKitInteractionApi, AppsRestApi, AppServerNotifier }; +export { AppUIKitInteractionApi, AppsRestApi, AppEvents, AppServerNotifier, AppServerListener, startup }; diff --git a/apps/meteor/ee/server/apps/communication/rest.ts b/apps/meteor/ee/server/apps/communication/rest.ts index a8ec38334fc4c..cb6752c395e74 100644 --- a/apps/meteor/ee/server/apps/communication/rest.ts +++ b/apps/meteor/ee/server/apps/communication/rest.ts @@ -1,16 +1,18 @@ -import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; import { Meteor } from 'meteor/meteor'; import { Settings, Users } from '@rocket.chat/models'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { Apps, AppsConverter, AppsManager } from '@rocket.chat/core-services'; import { AppStatus, AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { IUser, IMessage } from '@rocket.chat/core-typings'; import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; +import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; +import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; -import { getUploadFormData } from '../../../../app/api/server/lib/getUploadFormData'; -import { getWorkspaceAccessToken, getWorkspaceAccessTokenWithScope } from '../../../../app/cloud/server'; import { settings } from '../../../../app/settings/server'; +import { Info } from '../../../../app/utils/server'; import { formatAppInstanceForRest } from '../../../lib/misc/formatAppInstanceForRest'; +import { getUploadFormData } from '../../../../app/api/server/lib/getUploadFormData'; import { actionButtonsHandler } from './endpoints/actionButtonsHandler'; import { apiDeprecationLogger } from '../../../../app/lib/server/lib/deprecationWarningLogger'; import { notifyAppInstall } from '../marketplace/appInstall'; @@ -20,9 +22,7 @@ import { sendMessagesToAdmins } from '../../../../server/lib/sendMessagesToAdmin import { getPaginationItems } from '../../../../app/api/server/helpers/getPaginationItems'; import type { APIClass } from '../../../../app/api/server'; import { API } from '../../../../app/api/server'; -import { Info } from '../../../../app/utils/server'; -import type { AppServerOrchestrator } from '../orchestrator'; -import { Apps } from '../orchestrator'; +import { getWorkspaceAccessToken, getWorkspaceAccessTokenWithScope } from '../../../../app/cloud/server'; const rocketChatVersion = Info.version; const appsEngineVersionForMarketplace = Info.marketplaceApiVersion.replace(/-.*/g, ''); @@ -35,13 +35,7 @@ const purchaseTypes = new Set(['buy', 'subscription']); export class AppsRestApi { public api: APIClass<'/apps'>; - public _orch: AppServerOrchestrator; - - public _manager: AppManager; - - constructor(orch: AppServerOrchestrator, manager: AppManager) { - this._orch = orch; - this._manager = manager; + constructor() { void this.loadAPI(); } @@ -53,22 +47,19 @@ export class AppsRestApi { enableCors: false, auth: API.getUserAuth(), }); - this.addManagementRoutes(); + await this.addManagementRoutes(); } - addManagementRoutes() { - const orchestrator = this._orch; - const manager = this._manager; - - const handleError = (message: string, e: any) => { + async addManagementRoutes() { + const handleError = async (message: string, e: any) => { // when there is no `response` field in the error, it means the request // couldn't even make it to the server if (!e.hasOwnProperty('response')) { - orchestrator.getRocketChatLogger().warn(message, e.message); + await Apps.rocketChatLoggerWarn(message, e.message); return API.v1.internalError('Could not reach the Marketplace'); } - orchestrator.getRocketChatLogger().error(message, e.response.data); + await Apps.rocketChatLoggerError(message, e.response.data); if (e.response.statusCode >= 500 && e.response.statusCode <= 599) { return API.v1.internalError(); @@ -81,15 +72,15 @@ export class AppsRestApi { return API.v1.failure(); }; - this.api.addRoute('actionButtons', ...actionButtonsHandler(this)); - this.api.addRoute('count', ...appsCountHandler(this)); + this.api.addRoute('actionButtons', ...actionButtonsHandler()); + this.api.addRoute('count', ...appsCountHandler()); this.api.addRoute( 'incompatibleModal', { authRequired: true }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); const workspaceId = settings.get('Cloud_Workspace_Id'); const { action, appId, appVersion } = this.queryParams; @@ -105,7 +96,7 @@ export class AppsRestApi { { authRequired: true }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); // Gets the Apps from the marketplace const headers = getDefaultHeaders(); @@ -123,7 +114,7 @@ export class AppsRestApi { }, }); if (request.status !== 200) { - orchestrator.getRocketChatLogger().error('Error getting the Apps:', await request.json()); + await Apps.rocketChatLoggerError('Error getting the Apps:', await request.json()); return API.v1.failure(); } result = await request.json(); @@ -145,7 +136,7 @@ export class AppsRestApi { { authRequired: true }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(); @@ -157,12 +148,12 @@ export class AppsRestApi { try { const request = await fetch(`${baseUrl}/v1/categories`, { headers }); if (request.status !== 200) { - orchestrator.getRocketChatLogger().error('Error getting the Apps:', await request.json()); + await Apps.rocketChatLoggerError('Error getting the Apps:', await request.json()); return API.v1.failure(); } result = await request.json(); } catch (e: any) { - orchestrator.getRocketChatLogger().error('Error getting the categories from the Marketplace:', e.response.data); + await Apps.rocketChatLoggerError('Error getting the categories from the Marketplace:', e.response.data); return API.v1.internalError(); } @@ -176,7 +167,7 @@ export class AppsRestApi { { authRequired: true }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); const workspaceId = settings.get('Cloud_Workspace_Id'); @@ -207,7 +198,10 @@ export class AppsRestApi { { authRequired: true }, { async get() { - const apps = manager.get().map(formatAppInstanceForRest); + const apps = (await AppsManager.get()).map((proxiedApp) => { + if (!proxiedApp) return; + return formatAppInstanceForRest(proxiedApp); + }); return API.v1.success({ apps }); }, }, @@ -219,7 +213,7 @@ export class AppsRestApi { { authRequired: true, permissionsRequired: ['manage-apps'] }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); // Gets the Apps from the marketplace if ('marketplace' in this.queryParams && this.queryParams.marketplace) { @@ -237,7 +231,7 @@ export class AppsRestApi { try { const request = await fetch(`${baseUrl}/v1/apps`, { headers }); if (request.status !== 200) { - orchestrator.getRocketChatLogger().error('Error getting the Apps:', await request.json()); + await Apps.rocketChatLoggerError('Error getting the Apps:', await request.json()); return API.v1.failure(); } result = await request.json(); @@ -262,12 +256,12 @@ export class AppsRestApi { try { const request = await fetch(`${baseUrl}/v1/categories`, { headers }); if (request.status !== 200) { - orchestrator.getRocketChatLogger().error('Error getting the Apps:', await request.json()); + await Apps.rocketChatLoggerError('Error getting the Apps:', await request.json()); return API.v1.failure(); } result = await request.json(); } catch (e: any) { - orchestrator.getRocketChatLogger().error('Error getting the categories from the Marketplace:', e); + await Apps.rocketChatLoggerError('Error getting the categories from the Marketplace:', e); return API.v1.internalError(); } @@ -305,10 +299,10 @@ export class AppsRestApi { }); } - apiDeprecationLogger.warn( - 'This endpoint has been deprecated and will be removed in the future. Use /apps/installed to get the installed apps list.', - ); - const apps = manager.get().map(formatAppInstanceForRest); + const apps = (await AppsManager.get()).map((proxiedApp) => { + if (!proxiedApp) return; + return formatAppInstanceForRest(proxiedApp); + }); return API.v1.success({ apps }); }, @@ -329,7 +323,7 @@ export class AppsRestApi { buff = Buffer.from(await response.arrayBuffer()); } catch (e: any) { - orchestrator.getRocketChatLogger().error('Error getting the app from url:', e.response.data); + await Apps.rocketChatLoggerError('Error getting the app from url:', e.response.data); return API.v1.internalError(); } @@ -337,7 +331,7 @@ export class AppsRestApi { return API.v1.success({ buff }); } } else if (this.bodyParams.appId && this.bodyParams.marketplace && this.bodyParams.version) { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); const headers = getDefaultHeaders(); try { @@ -391,12 +385,13 @@ export class AppsRestApi { return API.v1.failure({ error: 'Failed to get a file to install for the App. ' }); } - const user = orchestrator - ?.getConverters() - ?.get('users') - ?.convertToApp(await Meteor.userAsync()); + const user = await AppsConverter.convertUserToApp(Meteor.user()); + + const aff = await AppsManager.add(buff, { marketplaceInfo, permissionsGranted, enable: true, user }); + if (!aff) { + return API.v1.failure({ error: 'Failed to install the App. ' }); + } - const aff = await manager.add(buff, { marketplaceInfo, permissionsGranted, enable: false, user }); const info: IAppInfo & { status?: AppStatus } = aff.getAppInfo(); if (aff.hasStorageError()) { @@ -413,10 +408,11 @@ export class AppsRestApi { info.status = aff.getApp().getStatus(); - void notifyAppInstall(orchestrator.getMarketplaceUrl() as string, 'install', info); + const marketplaceURL = await Apps.getMarketplaceUrl(); + void notifyAppInstall(marketplaceURL, 'install', info); if (await canEnableApp(aff.getApp().getStorageItem())) { - const success = await manager.enable(info.id); + const success = await AppsManager.enable(info.id); info.status = success ? AppStatus.AUTO_ENABLED : info.status; } @@ -438,7 +434,7 @@ export class AppsRestApi { return API.v1.failure({ error: 'Invalid request. Please ensure an appId is attached to the request.' }); } - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); const workspaceId = settings.get('Cloud_Workspace_Id'); const requester = { @@ -472,8 +468,8 @@ export class AppsRestApi { nickname: a.nickname, }; }); - } catch (e) { - orchestrator.getRocketChatLogger().error('Error getting the admins to request an app be installed:', e); + } catch (e: any) { + await Apps.rocketChatLoggerError('Error getting the admins to request an app be installed:', e); } const queryParams = new URLSearchParams(); @@ -493,8 +489,8 @@ export class AppsRestApi { 'externalComponents', { authRequired: false }, { - get() { - const externalComponents = orchestrator.getProvidedComponents(); + async get() { + const externalComponents = await Apps.getProvidedComponents(); return API.v1.success({ externalComponents }); }, @@ -505,11 +501,14 @@ export class AppsRestApi { 'languages', { authRequired: false }, { - get() { - const apps = manager.get().map((prl) => ({ - id: prl.getID(), - languages: prl.getStorageItem().languageContent, - })); + async get() { + const apps = (await AppsManager.get()).map((proxiedApp) => { + if (!proxiedApp) return; + return { + id: proxiedApp.getID(), + languages: proxiedApp.getStorageItem().languageContent, + }; + }); return API.v1.success({ apps }); }, @@ -520,7 +519,7 @@ export class AppsRestApi { 'externalComponentEvent', { authRequired: true }, { - post() { + async post() { if ( !this.bodyParams.externalComponent || !this.bodyParams.event || @@ -531,14 +530,11 @@ export class AppsRestApi { try { const { event, externalComponent } = this.bodyParams; - const result = (Apps?.getBridges()?.getListenerBridge() as Record).externalComponentEvent( - event, - externalComponent, - ); + const result = await Apps.triggerEvent(event, externalComponent); return API.v1.success({ result }); } catch (e: any) { - orchestrator.getRocketChatLogger().error(`Error triggering external components' events ${e.response.data}`); + await Apps.rocketChatLoggerError(`Error triggering external components' events`, e.response.data); return API.v1.internalError(); } }, @@ -550,7 +546,7 @@ export class AppsRestApi { { authRequired: true, permissionsRequired: ['manage-apps'] }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); const headers: Record = {}; const token = await getWorkspaceAccessToken(); @@ -562,12 +558,12 @@ export class AppsRestApi { try { const request = await fetch(`${baseUrl}/v1/bundles/${this.urlParams.id}/apps`, { headers }); if (request.status !== 200) { - orchestrator.getRocketChatLogger().error("Error getting the Bundle's Apps from the Marketplace:", await request.json()); + await Apps.rocketChatLoggerError("Error getting the Bundle's Apps from the Marketplace:", await request.json()); return API.v1.failure(); } result = await request.json(); } catch (e: any) { - orchestrator.getRocketChatLogger().error("Error getting the Bundle's Apps from the Marketplace:", e.response.data); + await Apps.rocketChatLoggerError("Error getting the Bundle's Apps from the Marketplace:", e.response.data); return API.v1.internalError(); } @@ -581,7 +577,7 @@ export class AppsRestApi { { authRequired: true }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(); @@ -593,7 +589,7 @@ export class AppsRestApi { try { const request = await fetch(`${baseUrl}/v1/featured-apps`, { headers }); if (request.status !== 200) { - orchestrator.getRocketChatLogger().error('Error getting the Featured Apps from the Marketplace:', await request.json()); + await Apps.rocketChatLoggerError('Error getting the Featured Apps from the Marketplace:', await request.json()); return API.v1.failure(); } result = await request.json(); @@ -612,7 +608,7 @@ export class AppsRestApi { { async get() { if (this.queryParams.marketplace && this.queryParams.version) { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); const headers: Record = {}; // DO NOT ATTACH THE FRAMEWORK/ENGINE VERSION HERE. const token = await getWorkspaceAccessToken(); @@ -624,7 +620,7 @@ export class AppsRestApi { try { const request = await fetch(`${baseUrl}/v1/apps/${this.urlParams.id}?appVersion=${this.queryParams.version}`, { headers }); if (request.status !== 200) { - orchestrator.getRocketChatLogger().error('Error getting the App information from the Marketplace:', await request.json()); + await Apps.rocketChatLoggerError('Error getting the App information from the Marketplace:', await request.json()); return API.v1.failure(); } result = await request.json(); @@ -636,7 +632,7 @@ export class AppsRestApi { } if (this.queryParams.marketplace && this.queryParams.update && this.queryParams.appVersion) { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(); @@ -650,7 +646,7 @@ export class AppsRestApi { headers, }); if (request.status !== 200) { - orchestrator.getRocketChatLogger().error('Error getting the App update info from the Marketplace:', await request.json()); + await Apps.rocketChatLoggerError('Error getting the App update info from the Marketplace:', await request.json()); return API.v1.failure(); } result = await request.json(); @@ -660,7 +656,7 @@ export class AppsRestApi { return API.v1.success({ app: result }); } - const app = manager.getOneById(this.urlParams.id); + const app = await AppsManager.getOneById(this.urlParams.id); if (!app) { return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`); } @@ -684,7 +680,7 @@ export class AppsRestApi { buff = Buffer.from(await response.arrayBuffer()); } else if (this.bodyParams.appId && this.bodyParams.marketplace && this.bodyParams.version) { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(true, 'marketplace:download', false); @@ -698,7 +694,7 @@ export class AppsRestApi { ); if (response.status !== 200) { - orchestrator.getRocketChatLogger().error('Error getting the App from the Marketplace:', await response.text()); + await Apps.rocketChatLoggerError('Error getting the App from the Marketplace:', await response.text()); return API.v1.failure(); } @@ -710,7 +706,7 @@ export class AppsRestApi { buff = Buffer.from(await response.arrayBuffer()); } catch (e: any) { - orchestrator.getRocketChatLogger().error('Error getting the App from the Marketplace:', e.response.data); + await Apps.rocketChatLoggerError('Error getting the App from the Marketplace:', e.response.data); return API.v1.internalError(); } } else { @@ -738,7 +734,11 @@ export class AppsRestApi { return API.v1.failure({ error: 'Failed to get a file to install for the App. ' }); } - const aff = await manager.update(buff, permissionsGranted); + const aff = await AppsManager.update(buff, permissionsGranted); + if (!aff) { + return API.v1.failure({ error: 'Failed to update the App. ' }); + } + const info: IAppInfo & { status?: AppStatus } = aff.getAppInfo(); if (aff.hasStorageError()) { @@ -755,7 +755,8 @@ export class AppsRestApi { info.status = aff.getApp().getStatus(); - void notifyAppInstall(orchestrator.getMarketplaceUrl() as string, 'update', info); + const marketplaceURL = await Apps.getMarketplaceUrl(); + void notifyAppInstall(marketplaceURL, 'update', info); return API.v1.success({ app: info, @@ -764,23 +765,21 @@ export class AppsRestApi { }); }, async delete() { - const prl = manager.getOneById(this.urlParams.id); + const prl = await AppsManager.getOneById(this.urlParams.id); if (!prl) { return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`); } - const user = orchestrator - ?.getConverters() - ?.get('users') - .convertToApp(await Meteor.userAsync()); + const user = await AppsConverter.convertUserToApp(Meteor.user()); - await manager.remove(prl.getID(), { user }); + await AppsManager.remove(prl.getID(), { user }); const info: IAppInfo & { status?: AppStatus } = prl.getInfo(); info.status = prl.getStatus(); - void notifyAppInstall(orchestrator.getMarketplaceUrl() as string, 'uninstall', info); + const marketplaceURL = await Apps.getMarketplaceUrl(); + void notifyAppInstall(marketplaceURL, 'uninstall', info); return API.v1.success({ app: info }); }, @@ -792,7 +791,7 @@ export class AppsRestApi { { authRequired: true }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); const headers: Record = {}; // DO NOT ATTACH THE FRAMEWORK/ENGINE VERSION HERE. const token = await getWorkspaceAccessToken(); @@ -815,7 +814,7 @@ export class AppsRestApi { } if (!result || statusCode !== 200) { - orchestrator.getRocketChatLogger().error('Error getting the App versions from the Marketplace:', result); + await Apps.rocketChatLoggerError('Error getting the App versions from the Marketplace:', result); return API.v1.failure(); } @@ -853,7 +852,7 @@ export class AppsRestApi { return API.v1.success(); } catch (e) { - orchestrator.getRocketChatLogger().error('Error when notifying admins that an user requested an app:', e); + await Apps.rocketChatLoggerError('Error when notifying admins that an user requested an app:', e); return API.v1.failure(); } }, @@ -865,7 +864,7 @@ export class AppsRestApi { { authRequired: true, permissionsRequired: ['manage-apps'] }, { async post() { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(); @@ -889,12 +888,12 @@ export class AppsRestApi { throw new Error(result.error); } } catch (e: any) { - orchestrator.getRocketChatLogger().error('Error syncing the App from the Marketplace:', e); + await Apps.rocketChatLoggerError('Error syncing the App from the Marketplace:', e); return API.v1.internalError(); } if (statusCode !== 200) { - orchestrator.getRocketChatLogger().error('Error syncing the App from the Marketplace:', result); + await Apps.rocketChatLoggerError('Error syncing the App from the Marketplace:', result); return API.v1.failure(); } @@ -909,8 +908,8 @@ export class AppsRestApi { ':id/icon', { authRequired: false }, { - get() { - const prl = manager.getOneById(this.urlParams.id); + async get() { + const prl = await AppsManager.getOneById(this.urlParams.id); if (!prl) { return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`); } @@ -941,7 +940,7 @@ export class AppsRestApi { { authRequired: false }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); const appId = this.urlParams.id; const headers = getDefaultHeaders(); @@ -953,7 +952,7 @@ export class AppsRestApi { screenshots: data, }); } catch (e: any) { - orchestrator.getRocketChatLogger().error('Error getting the screenshots from the Marketplace:', e.message); + await Apps.rocketChatLoggerError('Error getting the screenshots from the Marketplace:', e.message); return API.v1.failure(e.message); } }, @@ -964,8 +963,8 @@ export class AppsRestApi { ':id/languages', { authRequired: false }, { - get() { - const prl = manager.getOneById(this.urlParams.id); + async get() { + const prl = await AppsManager.getOneById(this.urlParams.id); if (prl) { const languages = prl.getStorageItem().languageContent || {}; @@ -982,7 +981,7 @@ export class AppsRestApi { { authRequired: true, permissionsRequired: ['manage-apps'] }, { async get() { - const prl = manager.getOneById(this.urlParams.id); + const prl = await AppsManager.getOneById(this.urlParams.id); if (prl) { const { offset, count } = await getPaginationItems(this.queryParams); @@ -996,7 +995,7 @@ export class AppsRestApi { fields, }; - const logs = await orchestrator?.getLogStorage()?.find(ourQuery, options); + const logs = await AppsManager.findLogs(ourQuery, options); return API.v1.success({ logs }); } @@ -1009,8 +1008,8 @@ export class AppsRestApi { ':id/settings', { authRequired: true, permissionsRequired: ['manage-apps'] }, { - get() { - const prl = manager.getOneById(this.urlParams.id); + async get() { + const prl = await AppsManager.getOneById(this.urlParams.id); if (prl) { const settings = Object.assign({}, prl.getStorageItem().settings); @@ -1030,7 +1029,7 @@ export class AppsRestApi { return API.v1.failure('The settings to update must be present.'); } - const prl = manager.getOneById(this.urlParams.id); + const prl = await AppsManager.getOneById(this.urlParams.id); if (!prl) { return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`); @@ -1038,11 +1037,11 @@ export class AppsRestApi { const { settings } = prl.getStorageItem(); - const updated = []; + const updated: Array = []; for await (const s of this.bodyParams.settings) { if (settings[s.id] && settings[s.id].value !== s.value) { - await manager.getSettingsManager().updateAppSetting(this.urlParams.id, s); + await AppsManager.updateAppSetting(this.urlParams.id, s); // Updating? updated.push(s); } @@ -1057,9 +1056,9 @@ export class AppsRestApi { ':id/settings/:settingId', { authRequired: true, permissionsRequired: ['manage-apps'] }, { - get() { + async get() { try { - const setting = manager.getSettingsManager().getAppSetting(this.urlParams.id, this.urlParams.settingId); + const setting = await AppsManager.getAppSetting(this.urlParams.id, this.urlParams.settingId); return API.v1.success({ setting }); } catch (e: any) { @@ -1078,7 +1077,7 @@ export class AppsRestApi { } try { - await manager.getSettingsManager().updateAppSetting(this.urlParams.id, this.bodyParams.setting); + await AppsManager.updateAppSetting(this.urlParams.id, this.bodyParams.setting); return API.v1.success(); } catch (e: any) { @@ -1098,12 +1097,12 @@ export class AppsRestApi { ':id/apis', { authRequired: true, permissionsRequired: ['manage-apps'] }, { - get() { - const prl = manager.getOneById(this.urlParams.id); + async get() { + const prl = await AppsManager.getOneById(this.urlParams.id); if (prl) { return API.v1.success({ - apis: (manager as Record).apiManager.listApis(this.urlParams.id), // TODO: this is accessing a private property from the manager, we should expose a method to get the list of APIs + apis: await AppsManager.listApis(this.urlParams.id), }); } return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`); @@ -1115,8 +1114,8 @@ export class AppsRestApi { ':id/status', { authRequired: true, permissionsRequired: ['manage-apps'] }, { - get() { - const prl = manager.getOneById(this.urlParams.id); + async get() { + const prl = await AppsManager.getOneById(this.urlParams.id); if (prl) { return API.v1.success({ status: prl.getStatus() }); @@ -1128,7 +1127,7 @@ export class AppsRestApi { return API.v1.failure('Invalid status provided, it must be "status" field and a string.'); } - const prl = manager.getOneById(this.urlParams.id); + const prl = await AppsManager.getOneById(this.urlParams.id); if (!prl) { return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`); @@ -1140,7 +1139,7 @@ export class AppsRestApi { } } - const result = await manager.changeStatus(prl.getID(), this.bodyParams.status); + const result = (await AppsManager.changeStatus(prl.getID(), this.bodyParams.status)) as ProxiedApp; return API.v1.success({ status: result.getStatus() }); }, @@ -1152,7 +1151,7 @@ export class AppsRestApi { { authRequired: true }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); const { appId, q = '', sort = '', limit = 25, offset = 0 } = this.queryParams; const headers = getDefaultHeaders(); @@ -1172,7 +1171,7 @@ export class AppsRestApi { } return API.v1.success(result); } catch (e: any) { - orchestrator.getRocketChatLogger().error('Error getting all non sent app requests from the Marketplace:', e.message); + await Apps.rocketChatLoggerError('Error getting all non sent app requests from the Marketplace:', e.message); return API.v1.failure(e.message); } @@ -1185,7 +1184,7 @@ export class AppsRestApi { { authRequired: true }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(); @@ -1201,7 +1200,7 @@ export class AppsRestApi { } return API.v1.success(result); } catch (e: any) { - orchestrator.getRocketChatLogger().error('Error getting the app requests stats from marketplace', e.message); + await Apps.rocketChatLoggerError('Error getting the app requests stats from marketplace', e.message); return API.v1.failure(e.message); } @@ -1214,7 +1213,7 @@ export class AppsRestApi { { authRequired: true }, { async post() { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(); @@ -1238,7 +1237,7 @@ export class AppsRestApi { return API.v1.success(result); } catch (e: any) { - orchestrator.getRocketChatLogger().error('Error marking app requests as seen', e.message); + await Apps.rocketChatLoggerError('Error marking app requests as seen', e.message); return API.v1.failure(e.message); } diff --git a/apps/meteor/ee/server/apps/communication/startup.ts b/apps/meteor/ee/server/apps/communication/startup.ts new file mode 100644 index 0000000000000..cdf401d4aef3d --- /dev/null +++ b/apps/meteor/ee/server/apps/communication/startup.ts @@ -0,0 +1,13 @@ +import { AppServerNotifier, AppsRestApi, AppUIKitInteractionApi } from '.'; + +export default (function communicatorsStartup() { + const notifier = new AppServerNotifier(); + const restapi = new AppsRestApi(); + const uikit = new AppUIKitInteractionApi(); + + return { + notifier, + restapi, + uikit, + }; +})(); diff --git a/apps/meteor/ee/server/apps/communication/uikit.ts b/apps/meteor/ee/server/apps/communication/uikit.ts index c2eb74713aae5..fa7b823d68e30 100644 --- a/apps/meteor/ee/server/apps/communication/uikit.ts +++ b/apps/meteor/ee/server/apps/communication/uikit.ts @@ -6,11 +6,9 @@ import { Meteor } from 'meteor/meteor'; import { WebApp } from 'meteor/webapp'; import { UIKitIncomingInteractionType } from '@rocket.chat/apps-engine/definition/uikit'; import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata'; -import { UiKitCoreApp } from '@rocket.chat/core-services'; +import { UiKitCoreApp, Apps, AppsConverter } from '@rocket.chat/core-services'; import { settings } from '../../../../app/settings/server'; -import type { AppServerOrchestrator } from '../orchestrator'; -import { Apps } from '../orchestrator'; import { authenticationMiddleware } from '../../../../app/api/server/middlewares/authentication'; const apiServer = express(); @@ -63,7 +61,7 @@ router.use(async (req: Request, res, next) => { const { 'x-visitor-token': visitorToken } = req.headers; if (visitorToken) { - req.body.visitor = await Apps.getConverters()?.get('visitors').convertByToken(visitorToken); + req.body.visitor = await AppsConverter.convertVistitorByToken(visitorToken as string); } if (!req.user && !req.body.visitor) { @@ -98,7 +96,7 @@ const getPayloadForType = (type: UIKitIncomingInteractionType, req: Request) => const { visitor } = req.body; const { user } = req; - const room = rid; // orch.getConverters().get('rooms').convertById(rid); + const room = rid; const message = mid; return { @@ -179,7 +177,7 @@ router.post('/:appId', async (req, res, next) => { }); const appsRoutes = - (orch: AppServerOrchestrator) => + () => async (req: Request, res: Response): Promise => { const { appId } = req.params; @@ -190,9 +188,9 @@ const appsRoutes = const { type, actionId, triggerId, mid, rid, payload, container } = req.body; const { visitor } = req.body; - const room = await orch.getConverters()?.get('rooms').convertById(rid); - const user = orch.getConverters()?.get('users').convertToApp(req.user); - const message = mid && (await orch.getConverters()?.get('messages').convertById(mid)); + const room = await AppsConverter.convertRoomById(rid); + const user = await AppsConverter.convertUserToApp(req.user); + const message = await AppsConverter.convertMessageById(mid); const action = { type, @@ -210,8 +208,7 @@ const appsRoutes = try { const eventInterface = !visitor ? AppInterface.IUIKitInteractionHandler : AppInterface.IUIKitLivechatInteractionHandler; - const result = await orch.triggerEvent(eventInterface, action); - + const result = await Apps.triggerEvent(eventInterface, action); res.send(result); } catch (e) { const error = e instanceof Error ? e.message : e; @@ -227,7 +224,7 @@ const appsRoutes = payload: { view, isCleared }, } = req.body; - const user = orch.getConverters()?.get('users').convertToApp(req.user); + const user = await AppsConverter.convertUserToApp(req.user); const action = { type, @@ -241,7 +238,7 @@ const appsRoutes = }; try { - const result = await orch.triggerEvent('IUIKitInteractionHandler', action); + const result = await Apps.triggerEvent('IUIKitInteractionHandler', action); res.send(result); } catch (e) { @@ -254,7 +251,7 @@ const appsRoutes = case UIKitIncomingInteractionType.VIEW_SUBMIT: { const { type, actionId, triggerId, payload } = req.body; - const user = orch.getConverters()?.get('users').convertToApp(req.user); + const user = await AppsConverter.convertUserToApp(req.user); const action = { type, @@ -266,7 +263,7 @@ const appsRoutes = }; try { - const result = await orch.triggerEvent('IUIKitInteractionHandler', action); + const result = await Apps.triggerEvent('IUIKitInteractionHandler', action); res.send(result); } catch (e) { @@ -286,10 +283,9 @@ const appsRoutes = payload: { context }, } = req.body; - const room = await orch.getConverters()?.get('rooms').convertById(rid); - const user = orch.getConverters()?.get('users').convertToApp(req.user); - const message = mid && (await orch.getConverters()?.get('messages').convertById(mid)); - + const room = await AppsConverter.convertRoomById(rid); + const user = await AppsConverter.convertUserToApp(req.user); + const message = mid && (await AppsConverter.convertMessageById(mid)); const action = { type, appId, @@ -304,7 +300,7 @@ const appsRoutes = }; try { - const result = await orch.triggerEvent('IUIKitInteractionHandler', action); + const result = await Apps.triggerEvent('IUIKitInteractionHandler', action); res.send(result); } catch (e) { @@ -323,11 +319,7 @@ const appsRoutes = }; export class AppUIKitInteractionApi { - orch: AppServerOrchestrator; - - constructor(orch: AppServerOrchestrator) { - this.orch = orch; - - router.post('/:appId', appsRoutes(orch)); + constructor() { + router.post('/:appId', appsRoutes()); } } diff --git a/apps/meteor/ee/server/apps/communication/websockets.ts b/apps/meteor/ee/server/apps/communication/websockets.ts index 58df570e43fc6..443d4124b604d 100644 --- a/apps/meteor/ee/server/apps/communication/websockets.ts +++ b/apps/meteor/ee/server/apps/communication/websockets.ts @@ -3,25 +3,21 @@ 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'; import type { IStreamer } from 'meteor/rocketchat:streamer'; -import { api } from '@rocket.chat/core-services'; +import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import { Apps, AppsManager, api } from '@rocket.chat/core-services'; import { SystemLogger } from '../../../../server/lib/logger/system'; import notifications from '../../../../app/notifications/server/lib/Notifications'; -import type { AppServerOrchestrator } from '../orchestrator'; import { AppEvents } from './events'; -export { AppEvents }; export class AppServerListener { - private orch: AppServerOrchestrator; - engineStreamer: IStreamer; clientStreamer: IStreamer; received; - constructor(orch: AppServerOrchestrator, engineStreamer: IStreamer, clientStreamer: IStreamer, received: Map) { - this.orch = orch; + constructor(engineStreamer: IStreamer, clientStreamer: IStreamer, received: Map) { this.engineStreamer = engineStreamer; this.clientStreamer = clientStreamer; this.received = received; @@ -40,12 +36,12 @@ export class AppServerListener { } async onAppAdded(appId: string): Promise { - await (this.orch.getManager()! as any).loadOne(appId); // TO-DO: fix type + await AppsManager.loadOne(appId); this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_ADDED, appId); } async onAppStatusUpdated({ appId, status }: { appId: string; status: AppStatus }): Promise { - const app = this.orch.getManager()?.getOneById(appId); + const app = await AppsManager.getOneById(appId); if (!app || app.getStatus() === status) { return; @@ -58,10 +54,10 @@ export class AppServerListener { }); if (AppStatusUtils.isEnabled(status)) { - await this.orch.getManager()?.enable(appId).catch(SystemLogger.error); + await AppsManager.enable(appId).catch(SystemLogger.error); this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_STATUS_CHANGE, { appId, status }); } else if (AppStatusUtils.isDisabled(status)) { - await this.orch.getManager()?.disable(appId, status, true).catch(SystemLogger.error); + await AppsManager.disable(appId).catch(SystemLogger.error); this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_STATUS_CHANGE, { appId, status }); } } @@ -72,33 +68,30 @@ export class AppServerListener { setting, when: new Date(), }); - await this.orch - .getManager()! - .getSettingsManager() - .updateAppSetting(appId, setting as any); // TO-DO: fix type of `setting` + await AppsManager.updateAppSetting(appId, setting as any); this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_SETTING_UPDATED, { appId }); } async onAppUpdated(appId: string): Promise { this.received.set(`${AppEvents.APP_UPDATED}_${appId}`, { appId, when: new Date() }); - const storageItem = await this.orch.getStorage()!.retrieveOne(appId); + const storageItem = (await Apps.retrieveOneFromStorage(appId)) as IAppStorageItem; // maybe we should verify if items exists? - const appPackage = await this.orch.getAppSourceStorage()!.fetch(storageItem); + const appPackage = (await Apps.fetchAppSourceStorage(storageItem)) as Buffer; // maybe we should verify if items exists? - await this.orch.getManager()!.updateLocal(storageItem, appPackage); + await AppsManager.updateLocal(storageItem, appPackage); this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_UPDATED, appId); } async onAppRemoved(appId: string): Promise { - const app = this.orch.getManager()!.getOneById(appId); + const app = await AppsManager.getOneById(appId); if (!app) { return; } - await this.orch.getManager()!.removeLocal(appId); + await AppsManager.removeLocal(appId); this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_REMOVED, appId); } @@ -132,14 +125,16 @@ export class AppServerNotifier { listener: AppServerListener; - constructor(orch: AppServerOrchestrator) { + constructor() { this.engineStreamer = notifications.streamAppsEngine; // This is used to broadcast to the web clients this.clientStreamer = notifications.streamApps; this.received = new Map(); - this.listener = new AppServerListener(orch, this.engineStreamer, this.clientStreamer, this.received); + this.listener = new AppServerListener(this.engineStreamer, this.clientStreamer, this.received); + + // void Apps.runOnAppEvent(this); } async appAdded(appId: string): Promise { @@ -159,7 +154,7 @@ export class AppServerNotifier { void api.broadcast('apps.updated', appId); } - async appStatusUpdated(appId: string, status: AppStatus): Promise { + async appStatusChange(appId: string, status: AppStatus): Promise { if (this.received.has(`${AppEvents.APP_STATUS_CHANGE}_${appId}`)) { const details = this.received.get(`${AppEvents.APP_STATUS_CHANGE}_${appId}`); if (details.status === status) { @@ -171,7 +166,7 @@ export class AppServerNotifier { void api.broadcast('apps.statusUpdate', appId, status); } - async appSettingsChange(appId: string, setting: ISetting): Promise { + async appSettingUpdated(appId: string, setting: ISetting): Promise { if (this.received.has(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting._id}`)) { this.received.delete(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting._id}`); return; diff --git a/apps/meteor/ee/server/apps/cron.js b/apps/meteor/ee/server/apps/cron.js index 46537f6618358..b80681ca43b74 100644 --- a/apps/meteor/ee/server/apps/cron.js +++ b/apps/meteor/ee/server/apps/cron.js @@ -2,9 +2,9 @@ import { SyncedCron } from 'meteor/littledata:synced-cron'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import { Settings, Users } from '@rocket.chat/models'; +import { Apps } from '@rocket.chat/core-services'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; -import { Apps } from './orchestrator'; import { getWorkspaceAccessToken } from '../../../app/cloud/server'; import { sendMessagesToAdmins } from '../../../server/lib/sendMessagesToAdmins'; @@ -70,7 +70,7 @@ const notifyAdminsAboutRenewedApps = async function _notifyAdminsAboutRenewedApp const appsUpdateMarketplaceInfo = async function _appsUpdateMarketplaceInfo() { const token = await getWorkspaceAccessToken(); - const baseUrl = Apps.getMarketplaceUrl(); + const baseUrl = await Apps.getMarketplaceUrl(); const workspaceIdSetting = await Settings.getValueById('Cloud_Workspace_Id'); const currentSeats = await Users.getActiveLocalUserCount(); diff --git a/apps/meteor/ee/server/apps/index.ts b/apps/meteor/ee/server/apps/index.ts index 35f7c2cc041fd..7b844408d4a64 100644 --- a/apps/meteor/ee/server/apps/index.ts +++ b/apps/meteor/ee/server/apps/index.ts @@ -1,4 +1,4 @@ import './cron'; import './appRequestsCron'; -export { Apps, AppEvents } from './orchestrator'; +export { AppEvents } from './orchestrator'; diff --git a/apps/meteor/ee/server/apps/orchestrator.js b/apps/meteor/ee/server/apps/orchestrator.js index c6fdf7fb0aba3..2b694e17ad3e7 100644 --- a/apps/meteor/ee/server/apps/orchestrator.js +++ b/apps/meteor/ee/server/apps/orchestrator.js @@ -1,13 +1,13 @@ +import EventEmitter from 'events'; + import { EssentialAppDisabledException } from '@rocket.chat/apps-engine/definition/exceptions'; import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata'; import { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; -import { Meteor } from 'meteor/meteor'; -import { AppLogs, Apps as AppsModel, AppsPersistence } from '@rocket.chat/models'; +import { Apps as AppsModel, AppsLogs as AppsLogsModel, AppsPersistence as AppsPersistenceModel } from '@rocket.chat/models'; +import { MeteorError } from '@rocket.chat/core-services'; import { Logger } from '../../../server/lib/logger/Logger'; -import { settings, settingsRegistry } from '../../../app/settings/server'; import { RealAppBridges } from '../../../app/apps/server/bridges'; -import { AppServerNotifier, AppsRestApi, AppUIKitInteractionApi } from './communication'; import { AppMessagesConverter, AppRoomsConverter, @@ -18,46 +18,39 @@ import { AppUploadsConverter, AppVisitorsConverter, } from '../../../app/apps/server/converters'; -import { AppRealLogsStorage, AppRealStorage, ConfigurableAppSourceStorage } from './storage'; +import { AppRealLogsStorage, AppRealStorage, ConfigurableAppSourceStorage } from '../../app/apps/storage'; import { canEnableApp } from '../../app/license/server/license'; function isTesting() { return process.env.TEST_MODE === 'true'; } -let appsSourceStorageType; -let appsSourceStorageFilesystemPath; - export class AppServerOrchestrator { - constructor() { + constructor(db) { + this.db = db; this._isInitialized = false; + this.appEventsSink = new EventEmitter(); } - initialize() { + initialize({ marketplaceUrl = 'https://marketplace.rocket.chat', appsSourceStorageType, appsSourceStorageFilesystemPath }) { if (this._isInitialized) { return; } this._rocketchatLogger = new Logger('Rocket.Chat Apps'); - if (typeof process.env.OVERWRITE_INTERNAL_MARKETPLACE_URL === 'string' && process.env.OVERWRITE_INTERNAL_MARKETPLACE_URL !== '') { - this._marketplaceUrl = process.env.OVERWRITE_INTERNAL_MARKETPLACE_URL; - } else { - this._marketplaceUrl = 'https://marketplace.rocket.chat'; - } + this._marketplaceUrl = marketplaceUrl; this._model = AppsModel; - this._logModel = AppLogs; - this._persistModel = AppsPersistence; + this._logModel = AppsLogsModel; + this._persistModel = AppsPersistenceModel; this._storage = new AppRealStorage(this._model); this._logStorage = new AppRealLogsStorage(this._logModel); - // TODO: Remove it when fixed the race condition // This enforce Fibers for a method not waited on apps-engine preventing a race condition const { storeEntries } = this._logStorage; this._logStorage.storeEntries = (...args) => Promise.await(storeEntries.call(this._logStorage, ...args)); - - this._appSourceStorage = new ConfigurableAppSourceStorage(appsSourceStorageType, appsSourceStorageFilesystemPath); + this._appSourceStorage = new ConfigurableAppSourceStorage(appsSourceStorageType, appsSourceStorageFilesystemPath, this.db); this._converters = new Map(); this._converters.set('messages', new AppMessagesConverter(this)); @@ -78,11 +71,6 @@ export class AppServerOrchestrator { sourceStorage: this._appSourceStorage, }); - this._communicators = new Map(); - this._communicators.set('notifier', new AppServerNotifier(this)); - this._communicators.set('restapi', new AppsRestApi(this, this._manager)); - this._communicators.set('uikit', new AppUIKitInteractionApi(this)); - this._isInitialized = true; } @@ -113,8 +101,8 @@ export class AppServerOrchestrator { return this._bridges; } - getNotifier() { - return this._communicators.get('notifier'); + notifyAppEvent(event, ...payload) { + this.appEventsSink.emit(event, ...payload); } getManager() { @@ -229,7 +217,7 @@ export class AppServerOrchestrator { .handleEvent(event, ...payload) .catch((error) => { if (error instanceof EssentialAppDisabledException) { - throw new Meteor.Error('error-essential-app-disabled'); + throw new MeteorError('error-app-essential-disabled'); } throw error; @@ -238,100 +226,3 @@ export class AppServerOrchestrator { } export const AppEvents = AppInterface; -export const Apps = new AppServerOrchestrator(); - -void settingsRegistry.addGroup('General', async function () { - await this.section('Apps', async function () { - await this.add('Apps_Logs_TTL', '30_days', { - type: 'select', - values: [ - { - key: '7_days', - i18nLabel: 'Apps_Logs_TTL_7days', - }, - { - key: '14_days', - i18nLabel: 'Apps_Logs_TTL_14days', - }, - { - key: '30_days', - i18nLabel: 'Apps_Logs_TTL_30days', - }, - ], - public: true, - hidden: false, - alert: 'Apps_Logs_TTL_Alert', - }); - - await this.add('Apps_Framework_Source_Package_Storage_Type', 'gridfs', { - type: 'select', - values: [ - { - key: 'gridfs', - i18nLabel: 'GridFS', - }, - { - key: 'filesystem', - i18nLabel: 'FileSystem', - }, - ], - public: true, - hidden: false, - alert: 'Apps_Framework_Source_Package_Storage_Type_Alert', - }); - - await this.add('Apps_Framework_Source_Package_Storage_FileSystem_Path', '', { - type: 'string', - public: true, - enableQuery: { - _id: 'Apps_Framework_Source_Package_Storage_Type', - value: 'filesystem', - }, - alert: 'Apps_Framework_Source_Package_Storage_FileSystem_Alert', - }); - }); -}); - -settings.watch('Apps_Framework_Source_Package_Storage_Type', (value) => { - if (!Apps.isInitialized()) { - appsSourceStorageType = value; - } else { - Apps.getAppSourceStorage().setStorage(value); - } -}); - -settings.watch('Apps_Framework_Source_Package_Storage_FileSystem_Path', (value) => { - if (!Apps.isInitialized()) { - appsSourceStorageFilesystemPath = value; - } else { - Apps.getAppSourceStorage().setFileSystemStoragePath(value); - } -}); - -settings.watch('Apps_Logs_TTL', async (value) => { - if (!Apps.isInitialized()) { - return; - } - - let expireAfterSeconds = 0; - - switch (value) { - case '7_days': - expireAfterSeconds = 604800; - break; - case '14_days': - expireAfterSeconds = 1209600; - break; - case '30_days': - expireAfterSeconds = 2592000; - break; - } - - if (!expireAfterSeconds) { - return; - } - - const model = Apps._logModel; - - await model.resetTTLIndex(expireAfterSeconds); -}); diff --git a/apps/meteor/ee/server/apps/startup.ts b/apps/meteor/ee/server/apps/startup.ts index 2a63e11aa48bc..cb4633f8db478 100644 --- a/apps/meteor/ee/server/apps/startup.ts +++ b/apps/meteor/ee/server/apps/startup.ts @@ -1,9 +1 @@ -import { Meteor } from 'meteor/meteor'; - -import { Apps } from './orchestrator'; - -Meteor.startup(function _appServerOrchestrator() { - Apps.initialize(); - - void Apps.load(); -}); +import './communication/startup'; diff --git a/apps/meteor/ee/server/apps/storage/logs-storage.js b/apps/meteor/ee/server/apps/storage/logs-storage.js deleted file mode 100644 index b48599ca2d385..0000000000000 --- a/apps/meteor/ee/server/apps/storage/logs-storage.js +++ /dev/null @@ -1,32 +0,0 @@ -import { AppConsole } from '@rocket.chat/apps-engine/server/logging'; -import { AppLogStorage } from '@rocket.chat/apps-engine/server/storage'; -import { InstanceStatus } from '@rocket.chat/instance-status'; - -export class AppRealLogsStorage extends AppLogStorage { - constructor(model) { - super('mongodb'); - this.db = model; - } - - async find(...args) { - return this.db.find(...args).toArray(); - } - - async storeEntries(appId, logger) { - const item = AppConsole.toStorageEntry(appId, logger); - - item.instanceId = InstanceStatus.id(); - - const id = (await this.db.insertOne(item)).insertedId; - - return this.db.findOneById(id); - } - - async getEntriesFor(appId) { - return this.db.find({ appId }).toArray(); - } - - async removeEntriesFor(appId) { - await this.db.remove({ appId }); - } -} diff --git a/apps/meteor/ee/server/lib/registerServiceModels.ts b/apps/meteor/ee/server/lib/registerServiceModels.ts index 88d33143c1157..16d5203e420f1 100644 --- a/apps/meteor/ee/server/lib/registerServiceModels.ts +++ b/apps/meteor/ee/server/lib/registerServiceModels.ts @@ -26,6 +26,9 @@ import { IntegrationHistoryRaw } from '../../../server/models/raw/IntegrationHis import { IntegrationsRaw } from '../../../server/models/raw/Integrations'; import { EmailInboxRaw } from '../../../server/models/raw/EmailInbox'; import { PbxEventsRaw } from '../../../server/models/raw/PbxEvents'; +import { AppsModel as AppsRaw } from '../../../server/models/raw/Apps'; +import { AppsLogsRaw } from '../../../server/models/raw/AppsLogs'; +import { AppsPersistenceModel as AppsPersistenceRaw } from '../../../server/models/raw/AppsPersistence'; import { LivechatPriorityRaw } from '../models/raw/LivechatPriority'; import { LivechatRoomsRaw } from '../../../server/models/raw/LivechatRooms'; import { UploadsRaw } from '../../../server/models/raw/Uploads'; @@ -59,6 +62,10 @@ export function registerServiceModels(db: Db, trash?: Collection new IntegrationsRaw(db)); registerModel('IEmailInboxModel', () => new EmailInboxRaw(db)); registerModel('IPbxEventsModel', () => new PbxEventsRaw(db)); + + registerModel('IAppsModel', new AppsRaw(db)); + registerModel('IAppsLogsModel', new AppsLogsRaw(db)); + registerModel('IAppsPersistenceModel', new AppsPersistenceRaw(db)); registerModel('ILivechatPriorityModel', new LivechatPriorityRaw(db)); registerModel('ILivechatRoomsModel', () => new LivechatRoomsRaw(db)); registerModel('IUploadsModel', () => new UploadsRaw(db)); diff --git a/apps/meteor/server/main.ts b/apps/meteor/server/main.ts index 8bf06426228ae..a0b9339c8fc5f 100644 --- a/apps/meteor/server/main.ts +++ b/apps/meteor/server/main.ts @@ -9,6 +9,7 @@ import './lib/logger/startup'; import './importPackages'; import '../imports/startup/server'; import '../app/lib/server/startup'; +import '../app/apps/server/startup'; import '../ee/server'; import './lib/pushConfig'; diff --git a/apps/meteor/server/methods/deleteUser.ts b/apps/meteor/server/methods/deleteUser.ts index d83c002d92c04..43800b32c66b5 100644 --- a/apps/meteor/server/methods/deleteUser.ts +++ b/apps/meteor/server/methods/deleteUser.ts @@ -1,5 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; +import { AppInterface as AppEvents } from '@rocket.chat/apps-engine/definition/metadata'; +import { Apps } from '@rocket.chat/core-services'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import type { IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; @@ -7,7 +9,6 @@ import { Users } from '@rocket.chat/models'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; import { callbacks } from '../../lib/callbacks'; import { deleteUser } from '../../app/lib/server'; -import { AppEvents, Apps } from '../../ee/server/apps/orchestrator'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/apps/meteor/server/methods/eraseRoom.ts b/apps/meteor/server/methods/eraseRoom.ts index 8f4c1f33ea31e..e963f52d0be24 100644 --- a/apps/meteor/server/methods/eraseRoom.ts +++ b/apps/meteor/server/methods/eraseRoom.ts @@ -1,13 +1,12 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { Message, Team } from '@rocket.chat/core-services'; +import { Message, Team, Apps } from '@rocket.chat/core-services'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Rooms } from '@rocket.chat/models'; import { methodDeprecationLogger } from '../../app/lib/server/lib/deprecationWarningLogger'; import { deleteRoom } from '../../app/lib/server/functions/deleteRoom'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; -import { Apps } from '../../ee/server/apps'; import { roomCoordinator } from '../lib/rooms/roomCoordinator'; export async function eraseRoom(rid: string, uid: string): Promise { @@ -35,11 +34,9 @@ export async function eraseRoom(rid: string, uid: string): Promise { }); } - if (Apps?.isLoaded()) { - const prevent = await Apps.getBridges()?.getListenerBridge().roomEvent('IPreRoomDeletePrevent', room); - if (prevent) { - throw new Meteor.Error('error-app-prevented-deleting', 'A Rocket.Chat App prevented the room erasing.'); - } + const prevent = await Apps.triggerEvent('IPreRoomDeletePrevent', room); + if (prevent) { + throw new Meteor.Error('error-app-prevented-deleting', 'A Rocket.Chat App prevented the room erasing.'); } await deleteRoom(rid); @@ -53,9 +50,7 @@ export async function eraseRoom(rid: string, uid: string): Promise { } } - if (Apps?.isLoaded()) { - void Apps.getBridges()?.getListenerBridge().roomEvent('IPostRoomDeleted', room); - } + void Apps.triggerEvent('IPostRoomDeleted', room); } declare module '@rocket.chat/ui-contexts' { diff --git a/apps/meteor/server/methods/logoutCleanUp.ts b/apps/meteor/server/methods/logoutCleanUp.ts index 6e14b52c71ee8..76673d90a198c 100644 --- a/apps/meteor/server/methods/logoutCleanUp.ts +++ b/apps/meteor/server/methods/logoutCleanUp.ts @@ -2,9 +2,10 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import type { IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; +import { Apps } from '@rocket.chat/core-services'; import { callbacks } from '../../lib/callbacks'; -import { AppEvents, Apps } from '../../ee/server/apps/orchestrator'; +import { AppEvents } from '../../ee/server/apps/orchestrator'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/apps/meteor/server/methods/reportMessage.ts b/apps/meteor/server/methods/reportMessage.ts index 46628299d1ff3..36c6bcb56ede6 100644 --- a/apps/meteor/server/methods/reportMessage.ts +++ b/apps/meteor/server/methods/reportMessage.ts @@ -1,11 +1,12 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import { Reports, Rooms, Messages } from '@rocket.chat/models'; +import { AppInterface as AppEvents } from '@rocket.chat/apps-engine/definition/metadata'; +import { Apps } from '@rocket.chat/core-services'; import type { IMessage } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { canAccessRoomAsync } from '../../app/authorization/server/functions/canAccessRoom'; -import { AppEvents, Apps } from '../../ee/server/apps'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/apps/meteor/server/methods/saveUserProfile.ts b/apps/meteor/server/methods/saveUserProfile.ts index 7f1f05861c812..b170b559e87e4 100644 --- a/apps/meteor/server/methods/saveUserProfile.ts +++ b/apps/meteor/server/methods/saveUserProfile.ts @@ -1,6 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { Accounts } from 'meteor/accounts-base'; +import { AppInterface as AppEvents } from '@rocket.chat/apps-engine/definition/metadata'; +import { Apps } from '@rocket.chat/core-services'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Users } from '@rocket.chat/models'; @@ -11,7 +13,6 @@ import { twoFactorRequired } from '../../app/2fa/server/twoFactorRequired'; import { saveUserIdentity } from '../../app/lib/server/functions/saveUserIdentity'; import { compareUserPassword } from '../lib/compareUserPassword'; import { compareUserPasswordHistory } from '../lib/compareUserPasswordHistory'; -import { AppEvents, Apps } from '../../ee/server/apps/orchestrator'; async function saveUserProfile( this: Meteor.MethodThisType, diff --git a/apps/meteor/server/models/AppsLogs.ts b/apps/meteor/server/models/AppsLogs.ts new file mode 100644 index 0000000000000..73eeda790810f --- /dev/null +++ b/apps/meteor/server/models/AppsLogs.ts @@ -0,0 +1,6 @@ +import { registerModel } from '@rocket.chat/models'; + +import { db } from '../database/utils'; +import { AppsLogsRaw } from './raw/AppsLogs'; + +registerModel('IAppsLogsModel', new AppsLogsRaw(db)); diff --git a/apps/meteor/server/models/raw/AppsLogs.ts b/apps/meteor/server/models/raw/AppsLogs.ts new file mode 100644 index 0000000000000..413e3b87dc37a --- /dev/null +++ b/apps/meteor/server/models/raw/AppsLogs.ts @@ -0,0 +1,22 @@ +import type { ILoggerStorageEntry } from '@rocket.chat/apps-engine/server/logging'; +import type { IAppsLogsModel } from '@rocket.chat/model-typings'; +import type { Db, IndexDescription } from 'mongodb'; + +import { BaseRaw } from './BaseRaw'; + +export class AppsLogsRaw extends BaseRaw implements IAppsLogsModel { + constructor(db: Db) { + super(db, 'apps_logs'); + } + + protected modelIndexes(): IndexDescription[] { + return [{ key: { _updatedAt: 1 }, expireAfterSeconds: 60 * 60 * 24 * 30 }]; + } + + async resetTTLIndex(expireAfterSeconds: number): Promise { + this.tryDropIndex('_updatedAt').catch((e) => console.error(`Could not drop _updatedAt index on apps_logs collection: ${e}`)); + this.tryEnsureIndex({ _updatedAt: 1 }, { expireAfterSeconds }).catch((e) => + console.error(`Could not create _updatedAt index on apps_logs collection: ${e}`), + ); + } +} diff --git a/apps/meteor/server/models/raw/BaseRaw.ts b/apps/meteor/server/models/raw/BaseRaw.ts index fdbcd406139bd..01321d50e34ae 100644 --- a/apps/meteor/server/models/raw/BaseRaw.ts +++ b/apps/meteor/server/models/raw/BaseRaw.ts @@ -20,6 +20,8 @@ import type { InsertManyResult, InsertOneResult, DeleteResult, + CreateIndexesOptions, + IndexSpecification, DeleteOptions, } from 'mongodb'; import { ObjectId } from 'mongodb'; @@ -89,6 +91,14 @@ export abstract class BaseRaw< return undefined; } + tryEnsureIndex(index: IndexSpecification, options: CreateIndexesOptions): Promise { + return this.col.createIndex(index, options); + } + + tryDropIndex(index: string): Promise { + return this.col.dropIndex(index); + } + getCollectionName(): string { return this.collectionName; } diff --git a/apps/meteor/server/models/raw/LivechatDepartment.ts b/apps/meteor/server/models/raw/LivechatDepartment.ts index 98dad5e14f74e..e205df0116161 100644 --- a/apps/meteor/server/models/raw/LivechatDepartment.ts +++ b/apps/meteor/server/models/raw/LivechatDepartment.ts @@ -145,6 +145,37 @@ export class LivechatDepartmentRaw extends BaseRaw implemen return this.find(query, options); } + findOneByIdOrName( + term: ILivechatDepartmentRecord['_id' | 'name'], + options: FindOptions, + ): Promise { + const query = { + $or: [ + { + _id: term, + }, + { + name: term, + }, + ], + }; + + return this.findOne(query, options); + } + + findEnabledWithAgents(projection?: FindOptions): FindCursor { + const query = { + numAgents: { $gt: 0 }, + enabled: true, + }; + + if (projection) { + return this.find(query, { projection }); + } + + return this.find(query); + } + addBusinessHourToDepartmentsByIds(ids: string[] = [], businessHourId: string): Promise { const query = { _id: { $in: ids }, diff --git a/apps/meteor/server/models/raw/LivechatRooms.js b/apps/meteor/server/models/raw/LivechatRooms.js index 2aaf22d9e12b5..314afe14fa228 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.js +++ b/apps/meteor/server/models/raw/LivechatRooms.js @@ -1241,6 +1241,27 @@ export class LivechatRoomsRaw extends BaseRaw { return this.find({ t: 'l', open: true }); } + findOpenByVisitorTokenAndDepartmentId(visitorToken, departmentId, options) { + const query = { + 't': 'l', + 'open': true, + 'v.token': visitorToken, + departmentId, + }; + + return this.find(query, options); + } + + findOpenByVisitorToken(visitorToken, options) { + const query = { + 't': 'l', + 'open': true, + 'v.token': visitorToken, + }; + + return this.find(query, options); + } + setAutoTransferOngoingById(roomId) { const query = { _id: roomId, diff --git a/apps/meteor/server/models/raw/Rooms.js b/apps/meteor/server/models/raw/Rooms.js index f264ad7844184..6a6fbe5b56800 100644 --- a/apps/meteor/server/models/raw/Rooms.js +++ b/apps/meteor/server/models/raw/Rooms.js @@ -735,6 +735,16 @@ export class RoomsRaw extends BaseRaw { return this.findOne(query, options); } + findDirectRoomContainingAllUsernames(usernames, options) { + const query = { + t: 'd', + usernames: { $size: usernames.length, $all: usernames }, + usersCount: usernames.length, + }; + + return this.findOne(query, options); + } + findFederatedRooms(options) { const query = { federated: true, diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 5cbc8017ea54f..7155139160b46 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -2,6 +2,7 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Subscriptions } from '@rocket.chat/models'; import { BaseRaw } from './BaseRaw'; +import { settings } from '../../../app/settings/server'; const queryStatusAgentOnline = (extraFilters = {}, isLivechatEnabledWhenAgentIdle) => ({ statusLivechat: 'available', @@ -296,6 +297,21 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } + getActiveLocalUserCount() { + return this.findActive().toArray().length - this.findActiveRemote().toArray().length; + } + + findActiveRemote(options = {}) { + return this.find( + { + active: true, + isRemote: true, + roles: { $ne: ['guest'] }, + }, + options, + ); + } + findByIds(userIds, options = {}) { const query = { _id: { $in: userIds }, @@ -539,6 +555,29 @@ export class UsersRaw extends BaseRaw { return agent; } + async getAgentInfo(agentId) { + const query = { + _id: agentId, + }; + + const options = { + projection: { + name: 1, + username: 1, + phone: 1, + customFields: 1, + status: 1, + livechat: 1, + }, + }; + + if (settings.get('Livechat_show_agent_email')) { + options.fields.emails = 1; + } + + return this.findOne(query, options); + } + findAllResumeTokensByUserId(userId) { return this.col .aggregate([ @@ -1077,6 +1116,16 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } + removeBannerById(_id, banner) { + const update = { + $unset: { + [`banners.${banner.id}`]: true, + }, + }; + + return this.updateOne({ _id }, update); + } + async isUserInRoleScope(uid) { const query = { _id: uid, diff --git a/apps/meteor/server/models/startup.ts b/apps/meteor/server/models/startup.ts index 2ba08ce0d2f8d..8d9fac7b4ba0f 100644 --- a/apps/meteor/server/models/startup.ts +++ b/apps/meteor/server/models/startup.ts @@ -1,6 +1,6 @@ import './Analytics'; import './Apps'; -import './AppLogs'; +import './AppsLogs'; import './AppsPersistence'; import './Avatars'; import './Banners'; diff --git a/apps/meteor/server/services/apps-engine/service.ts b/apps/meteor/server/services/apps-engine/service.ts index 0c481c1684526..100728c5f4389 100644 --- a/apps/meteor/server/services/apps-engine/service.ts +++ b/apps/meteor/server/services/apps-engine/service.ts @@ -1,4 +1,5 @@ -import { ServiceClassInternal } from '@rocket.chat/core-services'; +import { AppInterface as AppEvents } from '@rocket.chat/apps-engine/definition/metadata'; +import { ServiceClassInternal, Apps, AppsManager } from '@rocket.chat/core-services'; import type { 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'; @@ -7,7 +8,6 @@ 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 { Apps, AppEvents } from '../../../ee/server/apps/orchestrator'; import { SystemLogger } from '../../lib/logger/system'; export class AppsEngineService extends ServiceClassInternal implements IAppsEngineService { @@ -26,53 +26,53 @@ export class AppsEngineService extends ServiceClassInternal implements IAppsEngi this.onEvent('apps.added', async (appId: string): Promise => { // if the app already exists in this instance, don't load it again - const app = Apps.getManager()?.getOneById(appId); + const app = await AppsManager.getOneById(appId); if (app) { return; } - await (Apps.getManager() as any)?.loadOne(appId); + await AppsManager.loadOne(appId); }); this.onEvent('apps.removed', async (appId: string): Promise => { - const app = Apps.getManager()?.getOneById(appId); + const app = await AppsManager.getOneById(appId); if (!app) { return; } - await Apps.getManager()?.removeLocal(appId); + await await AppsManager.removeLocal(appId); }); this.onEvent('apps.updated', async (appId: string): Promise => { - const storageItem = await Apps.getStorage()?.retrieveOne(appId); + const storageItem = await AppsManager.getAppStorageItemById(appId); if (!storageItem) { return; } - const appPackage = await Apps.getAppSourceStorage()?.fetch(storageItem); + const appPackage = await Apps.fetchAppSourceStorage(storageItem); if (!appPackage) { return; } - await Apps.getManager()?.updateLocal(storageItem, appPackage); + await await AppsManager.updateLocal(storageItem, appPackage); }); this.onEvent('apps.statusUpdate', async (appId: string, status: AppStatus): Promise => { - const app = Apps.getManager()?.getOneById(appId); + const app = await AppsManager.getOneById(appId); if (!app || app.getStatus() === status) { return; } if (AppStatusUtils.isEnabled(status)) { - await Apps.getManager()?.enable(appId).catch(SystemLogger.error); + await AppsManager.enable(appId).catch(SystemLogger.error); } else if (AppStatusUtils.isDisabled(status)) { - await Apps.getManager()?.disable(appId, status, true).catch(SystemLogger.error); + await AppsManager.disable(appId, status, true).catch(SystemLogger.error); } }); this.onEvent('apps.settingUpdated', async (appId: string, setting: ISetting & { id: string }): Promise => { - const app = Apps.getManager()?.getOneById(appId); + const app = await AppsManager.getOneById(appId); const oldSetting = app?.getStorageItem().settings[setting.id].value; // avoid updating the setting if the value is the same, @@ -81,27 +81,22 @@ export class AppsEngineService extends ServiceClassInternal implements IAppsEngi return; } - const appManager = Apps.getManager(); - if (!appManager) { - return; - } - - await appManager.getSettingsManager().updateAppSetting(appId, setting as any); + await AppsManager.updateAppSetting(appId, setting as any); }); } - isInitialized(): boolean { + 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> { + const proxiedApps = await AppsManager.get(query); + + return proxiedApps.map((app) => app?.getApp()?.getInfo()); } async getAppStorageItemById(appId: string): Promise { - const app = Apps.getManager()?.getOneById(appId); + const app = await AppsManager.getOneById(appId); if (!app) { return; diff --git a/apps/meteor/server/services/cloud/service.ts b/apps/meteor/server/services/cloud/service.ts new file mode 100644 index 0000000000000..c79fb49583b66 --- /dev/null +++ b/apps/meteor/server/services/cloud/service.ts @@ -0,0 +1,14 @@ +import { Meteor } from 'meteor/meteor'; +import { ServiceClassInternal } from '@rocket.chat/core-services'; +import type { IAccessToken, ICloudService } from '@rocket.chat/core-services'; + +import { getWorkspaceAccessTokenWithScope } from '../../../app/cloud/server'; + +export class CloudService extends ServiceClassInternal implements ICloudService { + protected name = 'cloud'; + + getWorkspaceAccessTokenWithScope(scope?: string): IAccessToken { + const boundGetWorkspaceAccessToken = Meteor.bindEnvironment(getWorkspaceAccessTokenWithScope); + return boundGetWorkspaceAccessToken(scope); + } +} diff --git a/apps/meteor/server/services/livechat/service.ts b/apps/meteor/server/services/livechat/service.ts new file mode 100644 index 0000000000000..fc2624b054357 --- /dev/null +++ b/apps/meteor/server/services/livechat/service.ts @@ -0,0 +1,79 @@ +import type { IVisitor } from '@rocket.chat/apps-engine/definition/livechat'; +import type { IMessage, ILivechatVisitor, OmnichannelSourceType, IOmnichannelRoom, IRoom, ILivechatAgent } from '@rocket.chat/core-typings'; +import type { ILivechatService, CloseRoomParams } from '@rocket.chat/core-services'; +import { ServiceClassInternal } from '@rocket.chat/core-services'; + +import { Livechat as LivechatTyped } from '../../../app/livechat/server/lib/LivechatTyped'; +import { Livechat } from '../../../app/livechat/server'; + +export class LivechatService extends ServiceClassInternal implements ILivechatService { + async isOnline(department?: string, skipNoAgentSetting?: boolean, skipFallbackCheck?: boolean): Promise { + return Livechat.online(department, skipNoAgentSetting, skipFallbackCheck); + } + + async sendMessage(props: { guest: IVisitor; message: IMessage; roomInfo: Record; agent: string }): Promise { + return Livechat.sendMessage(props); + } + + async updateMessage(props: { guest: IVisitor; message: IMessage }): Promise { + return Livechat.updateMessage(props); + } + + async getRoom(props: { + guest: ILivechatVisitor; + rid?: string; + roomInfo?: { + source?: { + type: OmnichannelSourceType; + id?: string; + alias?: string; + label?: string; + sidebarIcon?: string; + defaultIcon?: string; + }; + }; + agent?: { agentId?: string; username?: string }; + extraParams?: Record; + }): Promise<{ room: IOmnichannelRoom; newRoom: boolean }> { + return Livechat.getRoom(props); + } + + async closeRoom(props: CloseRoomParams): Promise { + await LivechatTyped.closeRoom(props); + } + + registerGuest(props: { + id?: string; + token: string; + name: string; + email: string; + department?: string; + phone?: { number: string }; + username: string; + connectionData?: string; + status?: string; + }): Promise { + return Livechat.registerGuest(props as any); + } + + transferVisitor( + room: IRoom, + visitor: IVisitor, + transferData: { + userId?: string; + departmentId?: string; + transferredTo: ILivechatAgent; + transferredBy: { _id: string; username?: string; name?: string; type: string }; + }, + ): Promise { + return Livechat.transfer(room, visitor, transferData); + } + + getRoomMessages(roomId: string): Promise { + return Livechat.getRoomMessages({ rid: roomId }); + } + + setCustomFields(props: { token: string; key: string; value: string; overwrite: boolean }): Promise { + return Livechat.setCustomFields(props); + } +} diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/message/service.ts similarity index 63% rename from apps/meteor/server/services/messages/service.ts rename to apps/meteor/server/services/message/service.ts index 2f15cdbadb515..c761f459a19d8 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/message/service.ts @@ -1,16 +1,21 @@ -import type { IMessage, MessageTypesValues, IUser } from '@rocket.chat/core-typings'; +import type { AtLeast, IMessage, IUser, MessageTypesValues } from '@rocket.chat/core-typings'; import type { IMessageService } from '@rocket.chat/core-services'; import { ServiceClassInternal } from '@rocket.chat/core-services'; import { Messages } from '@rocket.chat/models'; +import { updateMessage } from '../../../app/lib/server'; import { executeSendMessage } from '../../../app/lib/server/methods/sendMessage'; -import { settings } from '../../../app/settings/server'; +// import { settings } from '../../../app/settings/client'; export class MessageService extends ServiceClassInternal implements IMessageService { protected name = 'message'; - async sendMessage({ fromId, rid, msg }: { fromId: string; rid: string; msg: string }): Promise { - return executeSendMessage(fromId, { rid, msg }); + async sendMessage(userId: string, message: AtLeast): Promise { + return executeSendMessage(userId, message); + } + + async updateMessage(message: IMessage, editor: IUser): Promise { + return updateMessage(message, editor); } async saveSystemMessage( @@ -29,7 +34,8 @@ export class MessageService extends ServiceClassInternal implements IMessageServ rid, message, { _id: userId, username, name }, - settings.get('Message_Read_Receipt_Enabled'), + // settings.get('Message_Read_Receipt_Enabled'), + false, extraData, ); diff --git a/apps/meteor/server/services/notification/service.ts b/apps/meteor/server/services/notification/service.ts new file mode 100644 index 0000000000000..baa619a154336 --- /dev/null +++ b/apps/meteor/server/services/notification/service.ts @@ -0,0 +1,12 @@ +import type { INotificationService } from '@rocket.chat/core-services'; +import { ServiceClassInternal } from '@rocket.chat/core-services'; + +import notifications from '../../../app/notifications/server/lib/Notifications'; + +export class NotificationService extends ServiceClassInternal implements INotificationService { + protected name = 'notification'; + + notifyRoom(room: string, eventName: string, ...args: any[]): void { + notifications.notifyRoom(room, eventName, ...args); + } +} diff --git a/apps/meteor/server/services/room/service.ts b/apps/meteor/server/services/room/service.ts index 668934c643ca4..8f7feb514ac3f 100644 --- a/apps/meteor/server/services/room/service.ts +++ b/apps/meteor/server/services/room/service.ts @@ -1,9 +1,10 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import { ServiceClassInternal, Authorization } from '@rocket.chat/core-services'; -import type { ICreateRoomParams, IRoomService } from '@rocket.chat/core-services'; +import type { ICreateRoomParams, IRoomService, ICreateDiscussionParams } from '@rocket.chat/core-services'; -import { createRoom } from '../../../app/lib/server/functions/createRoom'; // TODO remove this import +import { createRoom, addUserToRoom as meteorAddUserToRoom } from '../../../app/lib/server/functions'; // TODO remove this import +import { create as createDiscussion } from '../../../app/discussion/server/methods/createDiscussion'; import { createDirectMessage } from '../../methods/createDirectMessage'; export class RoomService extends ServiceClassInternal implements IRoomService { @@ -48,4 +49,36 @@ export class RoomService extends ServiceClassInternal implements IRoomService { return true; } + + async addUserToRoom( + rid: string, + user: Pick | string, + inviter?: Pick, + silenced?: boolean, + ): Promise { + return meteorAddUserToRoom(rid, user, inviter, silenced); + } + + async createDiscussion(params: ICreateDiscussionParams): Promise { + const { parentRoomId, parentMessageId, creatorId, name, members = [], encrypted, reply } = params; + + const user = await Users.findOneById>(creatorId, { + projection: { username: 1 }, + }); + + if (!user || !user.username) { + throw new Error('User not found'); + } + + // TODO: convert `createDiscussion` function to "raw" and move to here + return createDiscussion({ + prid: parentRoomId, + pmid: parentMessageId, + t_name: name, + users: members, + user, + encrypted, + reply, + }); + } } diff --git a/apps/meteor/server/services/slashcommand/service.ts b/apps/meteor/server/services/slashcommand/service.ts new file mode 100644 index 0000000000000..3608ba47379ee --- /dev/null +++ b/apps/meteor/server/services/slashcommand/service.ts @@ -0,0 +1,102 @@ +import type { IMessage, RequiredField, SlashCommand, SlashCommandPreviews } from '@rocket.chat/core-typings'; +import type { ISlashCommandPreviewItem } from '@rocket.chat/apps-engine/definition/slashcommands'; +import { SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands'; +import type { ISlashCommandService } from '@rocket.chat/core-services'; +import { ServiceClassInternal, AppsConverter, AppsManager } from '@rocket.chat/core-services'; + +import { slashCommands } from '../../../app/utils/server'; +import { parseParameters } from '../../../lib/utils/parseParameters'; + +export class SlashCommandService extends ServiceClassInternal implements ISlashCommandService { + protected name = 'slashcommand'; + + getCommand(cmd: string): SlashCommand { + return slashCommands.commands[cmd]; + } + + setCommand(command: SlashCommand): void { + const cmd = command.command.toLocaleLowerCase(); + + slashCommands.commands[cmd] = command; + } + + setAppCommand(command: SlashCommand): void { + const cmd = command.command.toLocaleLowerCase(); + + command.callback = this._appCommandExecutor.bind(this); + command.previewer = command.previewer ? this._appCommandPreviewer.bind(this) : undefined; + command.previewCallback = command.previewCallback + ? (this._appCommandPreviewExecutor.bind(this) as typeof slashCommands.commands[string]['previewCallback']) + : undefined; + + slashCommands.commands[cmd] = command; + } + + removeCommand(command: string): void { + delete slashCommands.commands[command]; + } + + private async _appCommandExecutor( + command: string, + parameters: any, + message: RequiredField, 'rid'>, + triggerId?: string, + userId?: string, + ): Promise { + const user = await AppsConverter.convertUserById(userId as string); + const room = await AppsConverter.convertRoomById(message.rid); + const threadId = message.tmid; + const params = parseParameters(parameters); + + const context = new SlashCommandContext( + Object.freeze(user), + Object.freeze(room), + Object.freeze(params) as string[], + threadId, + triggerId, + ); + + await AppsManager.commandExecuteCommand(command, context); + } + + private async _appCommandPreviewer( + command: string, + parameters: string, + message: IMessage, + userId?: string, + ): Promise { + const user = await AppsConverter.convertUserById(userId as string); + const room = await AppsConverter.convertRoomById(message.rid); + const threadId = message.tmid; + const params = parseParameters(parameters); + + const context = new SlashCommandContext(Object.freeze(user), Object.freeze(room), Object.freeze(params) as string[], threadId); + const preview = await AppsManager.getCommandPreviews(command, context); + + return preview as SlashCommandPreviews; + } + + private async _appCommandPreviewExecutor( + command: string, + parameters: any, + message: IMessage, + preview: ISlashCommandPreviewItem, + triggerId: string, + userId?: string, + ): Promise { + const user = await AppsConverter.convertUserById(userId as string); + const room = await AppsConverter.convertRoomById(message.rid); + const threadId = message.tmid; + const params = parseParameters(parameters); + + const context = new SlashCommandContext( + Object.freeze(user), + Object.freeze(room), + Object.freeze(params) as string[], + threadId, + triggerId, + ); + + await AppsManager.commandExecutePreview(command, preview, context); + } +} diff --git a/apps/meteor/server/services/startup.ts b/apps/meteor/server/services/startup.ts index 004d4f25a4b41..03da8aa78690d 100644 --- a/apps/meteor/server/services/startup.ts +++ b/apps/meteor/server/services/startup.ts @@ -22,7 +22,13 @@ import { isRunningMs } from '../lib/isRunningMs'; import { PushService } from './push/service'; import { DeviceManagementService } from './device-management/service'; import { UploadService } from './upload/service'; -import { MessageService } from './messages/service'; +import { CloudService } from './cloud/service'; +import { UserService } from './user/service'; +import { FederationService } from './federation/service'; +import { LivechatService } from './livechat/service'; +import { NotificationService } from './notification/service'; +import { SlashCommandService } from './slashcommand/service'; +import { MessageService } from './message/service'; import { TranslationService } from './translation/service'; import { SettingsService } from './settings/service'; import { OmnichannelIntegrationService } from './omnichannel-integrations/service'; @@ -34,20 +40,27 @@ api.registerService(new AppsEngineService()); api.registerService(new AnalyticsService()); api.registerService(new AuthorizationLivechat()); api.registerService(new BannerService()); +api.registerService(new CloudService()); api.registerService(new LDAPService()); api.registerService(new MediaService()); api.registerService(new MeteorService()); api.registerService(new NPSService()); api.registerService(new RoomService()); api.registerService(new SAUMonitorService()); +api.registerService(new UploadService()); api.registerService(new VoipService(db)); api.registerService(new OmnichannelService()); api.registerService(new OmnichannelVoipService()); api.registerService(new TeamService()); api.registerService(new UiKitCoreApp()); +api.registerService(new UserService()); api.registerService(new PushService()); api.registerService(new DeviceManagementService()); api.registerService(new VideoConfService()); +api.registerService(new FederationService()); +api.registerService(new LivechatService()); +api.registerService(new NotificationService()); +api.registerService(new SlashCommandService()); api.registerService(new UploadService()); api.registerService(new MessageService()); api.registerService(new TranslationService()); @@ -61,9 +74,22 @@ if (!isRunningMs()) { const { Authorization } = await import('./authorization/service'); + const { AppsOrchestratorService } = await import('../../ee/app/apps/service'); + const { AppsStatisticsService } = await import('../../ee/app/apps/statisticsService'); + const { AppsConverterService } = await import('../../ee/app/apps/converterService'); + const { AppsManagerService } = await import('../../ee/app/apps/managerService'); + const { AppsVideoManagerService } = await import('../../ee/app/apps/videoManagerService'); + const { AppsApiService } = await import('../../ee/app/apps/apiService'); + api.registerService(new Presence()); api.registerService(new Authorization()); + api.registerService(new AppsOrchestratorService(db)); + api.registerService(new AppsStatisticsService()); + api.registerService(new AppsConverterService()); + api.registerService(new AppsManagerService()); + api.registerService(new AppsVideoManagerService()); + api.registerService(new AppsApiService()); // Run EE services defined outside of the main repo // Otherwise, monolith would ignore them :( // Always register the service and manage licensing inside the service (tbd) diff --git a/apps/meteor/server/services/upload/service.ts b/apps/meteor/server/services/upload/service.ts index 016b04dfe46d6..8b96ecc843b7e 100644 --- a/apps/meteor/server/services/upload/service.ts +++ b/apps/meteor/server/services/upload/service.ts @@ -1,6 +1,6 @@ -import { ServiceClassInternal } from '@rocket.chat/core-services'; -import { Meteor } from 'meteor/meteor'; import type { IMessage, IUpload } from '@rocket.chat/core-typings'; +import { Meteor } from 'meteor/meteor'; +import { ServiceClassInternal } from '@rocket.chat/core-services'; import type { ISendFileLivechatMessageParams, ISendFileMessageParams, IUploadFileParams, IUploadService } from '@rocket.chat/core-services'; import { FileUpload } from '../../../app/file-upload/server'; @@ -23,6 +23,18 @@ export class UploadService extends ServiceClassInternal implements IUploadServic return Meteor.callAsync('sendFileLivechatMessage', roomId, visitorToken, file, message); } + async getBuffer(cb: Function): Promise { + return new Promise((resolve, reject) => { + FileUpload.getBuffer(cb, (error: Error, result: Buffer) => { + if (error) { + return reject(error); + } + + resolve(result); + }); + }); + } + async getFileBuffer({ userId, file }: { userId: string; file: IUpload }): Promise { return Meteor.runAsUser(userId, () => { return new Promise((resolve, reject) => { diff --git a/apps/meteor/server/services/user/service.ts b/apps/meteor/server/services/user/service.ts new file mode 100644 index 0000000000000..cad703d7415d5 --- /dev/null +++ b/apps/meteor/server/services/user/service.ts @@ -0,0 +1,29 @@ +import { Meteor } from 'meteor/meteor'; +import type { ISetUserAvatarParams, IUserService } from '@rocket.chat/core-services'; +import { ServiceClassInternal } from '@rocket.chat/core-services'; + +import { setUserAvatar } from '../../../app/lib/server'; +import { deleteUser as meteorDeleteUser } from '../../../app/lib/server/functions'; +import { checkUsernameAvailability } from '../../../app/lib/server/functions/checkUsernameAvailability'; + +export class UserService extends ServiceClassInternal implements IUserService { + protected name = 'user'; + + constructor() { + super(); + } + + async setUserAvatar({ user, dataURI, contentType, service, etag }: ISetUserAvatarParams): Promise { + await Meteor.runAsUser(user._id, async () => { + await setUserAvatar(user, dataURI, contentType, service, etag); + }); + } + + async deleteUser(userId: string, confirmRelinquish = false): Promise { + return meteorDeleteUser(userId, confirmRelinquish); + } + + async checkUsernameAvailability(username: string): Promise { + return checkUsernameAvailability(username); + } +} diff --git a/apps/meteor/server/services/video-conference/service.ts b/apps/meteor/server/services/video-conference/service.ts index 7c2ca7a427b06..92d65d44a18c8 100644 --- a/apps/meteor/server/services/video-conference/service.ts +++ b/apps/meteor/server/services/video-conference/service.ts @@ -25,15 +25,13 @@ import { isLivechatVideoConference, } from '@rocket.chat/core-typings'; import type { MessageSurfaceLayout } from '@rocket.chat/ui-kit'; -import type { AppVideoConfProviderManager } from '@rocket.chat/apps-engine/server/managers'; import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import type { PaginatedResult } from '@rocket.chat/rest-typings'; import { Users, VideoConference as VideoConferenceModel, Rooms, Messages, Subscriptions } from '@rocket.chat/models'; import type { IVideoConfService, VideoConferenceJoinOptions } from '@rocket.chat/core-services'; -import { api, ServiceClassInternal } from '@rocket.chat/core-services'; +import { api, ServiceClassInternal, AppsVideoManager } from '@rocket.chat/core-services'; -import { Apps } from '../../../ee/server/apps'; import { sendMessage } from '../../../app/lib/server/functions/sendMessage'; import { settings } from '../../../app/settings/server'; import { videoConfProviders } from '../../lib/videoConfProviders'; @@ -152,7 +150,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf } } - const blocks = await (await this.getProviderManager()).getVideoConferenceInfo(call.providerName, call, user || undefined).catch((e) => { + const blocks = await AppsVideoManager.getVideoConferenceInfo(call.providerName, call, user || undefined).catch((e) => { throw new Error(e); }); @@ -502,8 +500,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf } private async validateProvider(providerName: string): Promise { - const manager = await this.getProviderManager(); - const configured = await manager.isFullyConfigured(providerName).catch(() => false); + const configured = await AppsVideoManager.isFullyConfigured(providerName).catch(() => false); if (!configured) { throw new Error(availabilityErrors.NOT_CONFIGURED); } @@ -748,19 +745,6 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf return this.getUrl(call, user, options); } - private async getProviderManager(): Promise { - if (!Apps?.isLoaded()) { - throw new Error('apps-engine-not-loaded'); - } - - const manager = Apps.getManager()?.getVideoConfProviderManager(); - if (!manager) { - throw new Error(availabilityErrors.NO_APP); - } - - return manager; - } - private async getRoomName(rid: string): Promise { const room = await Rooms.findOneById>(rid, { projection: { name: 1, fname: 1 } }); @@ -774,18 +758,16 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf const title = isGroupVideoConference(call) ? call.title || (await this.getRoomName(call.rid)) : ''; - return (await this.getProviderManager()) - .generateUrl(call.providerName, { - _id: call._id, - type: call.type, - rid: call.rid, - createdBy: call.createdBy as Required, - title, - providerData: call.providerData, - }) - .catch((e) => { - throw new Error(e); - }); + return AppsVideoManager.generateUrl(call.providerName, { + _id: call._id, + type: call.type, + rid: call.rid, + createdBy: call.createdBy as Required, + title, + providerData: call.providerData, + }).catch((e) => { + throw new Error(e); + }); } private async getCallTitleForUser(call: VideoConference, userId?: IUser['_id']): Promise { @@ -860,7 +842,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf name: user.name as string, }; - return (await this.getProviderManager()).customizeUrl(call.providerName, callData, userData, options).catch((e) => { + return AppsVideoManager.customizeUrl(call.providerName, callData, userData, options).catch((e) => { throw new Error(e); }); } @@ -876,7 +858,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf throw new Error('video-conf-provider-unavailable'); } - (await this.getProviderManager()).onNewVideoConference(call.providerName, call).catch((e) => { + AppsVideoManager.onNewVideoConference(call.providerName, call).catch((e) => { throw new Error(e); }); } @@ -892,7 +874,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf throw new Error('video-conf-provider-unavailable'); } - (await this.getProviderManager()).onVideoConferenceChanged(call.providerName, call).catch((e) => { + AppsVideoManager.onVideoConferenceChanged(call.providerName, call).catch((e) => { throw new Error(e); }); } @@ -908,7 +890,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf throw new Error('video-conf-provider-unavailable'); } - (await this.getProviderManager()).onUserJoin(call.providerName, call, user).catch((e) => { + AppsVideoManager.onUserJoin(call.providerName, call, user).catch((e) => { throw new Error(e); }); } diff --git a/ee/apps/apps-engine/.eslintrc b/ee/apps/apps-engine/.eslintrc new file mode 100644 index 0000000000000..4d3f4a7d4d544 --- /dev/null +++ b/ee/apps/apps-engine/.eslintrc @@ -0,0 +1,16 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "overrides": [ + { + "files": ["**/*.spec.js", "**/*.spec.jsx"], + "env": { + "jest": true + } + } + ], + "ignorePatterns": ["**/dist"], + "plugins": ["jest"], + "env": { + "jest/globals": true + } +} diff --git a/ee/apps/apps-engine/Dockerfile b/ee/apps/apps-engine/Dockerfile new file mode 100644 index 0000000000000..c27bf31f9e58b --- /dev/null +++ b/ee/apps/apps-engine/Dockerfile @@ -0,0 +1,34 @@ +FROM node:14.19.3-alpine + +ARG SERVICE + +WORKDIR /app + +COPY ./packages/core-typings/package.json packages/core-typings/package.json +COPY ./packages/core-typings/dist packages/core-typings/dist +COPY ./packages/rest-typings/package.json packages/rest-typings/package.json +COPY ./packages/rest-typings/dist packages/rest-typings/dist +COPY ./packages/model-typings/package.json packages/model-typings/package.json +COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/models/package.json packages/models/package.json +COPY ./packages/models/dist packages/models/dist + +COPY ./ee/apps/${SERVICE}/dist . + +COPY ./package.json . +COPY ./yarn.lock . +COPY ./.yarnrc.yml . +COPY ./.yarn/plugins .yarn/plugins +COPY ./.yarn/releases .yarn/releases +COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json + +ENV NODE_ENV=production \ + PORT=3000 + +WORKDIR /app/ee/apps/${SERVICE} + +RUN yarn workspaces focus --production + +EXPOSE 3000 9458 + +CMD ["node", "src/service.js"] diff --git a/ee/apps/apps-engine/package.json b/ee/apps/apps-engine/package.json new file mode 100644 index 0000000000000..343865e87d0ae --- /dev/null +++ b/ee/apps/apps-engine/package.json @@ -0,0 +1,47 @@ +{ + "name": "@rocket.chat/apps-engine-service", + "private": true, + "version": "0.1.0", + "description": "Rocket.Chat's Apps Engine service", + "scripts": { + "build": "tsc -p tsconfig.json", + "ms": "TRANSPORTER=${TRANSPORTER:-TCP} MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts", + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint src", + "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" + }, + "keywords": [ + "rocketchat" + ], + "author": "Rocket.Chat", + "dependencies": { + "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/emitter": "next", + "@rocket.chat/model-typings": "workspace:^", + "@rocket.chat/models": "workspace:^", + "@rocket.chat/rest-typings": "workspace:^", + "@rocket.chat/string-helpers": "next", + "@types/node": "^14.18.21", + "ejson": "^2.2.2", + "eventemitter3": "^4.0.7", + "fibers": "^5.0.3", + "mem": "^8.1.1", + "moleculer": "^0.14.21", + "mongodb": "^4.3.1", + "nats": "^2.4.0", + "pino": "^8.4.2", + "polka": "^0.5.2" + }, + "devDependencies": { + "@rocket.chat/eslint-config": "workspace:^", + "@types/eslint": "^8", + "@types/polka": "^0.5.4", + "eslint": "^8.21.0", + "ts-node": "^10.9.1", + "typescript": "~4.5.5" + }, + "main": "./dist/ee/apps/apps-engine/src/service.js", + "files": [ + "/dist" + ] +} diff --git a/ee/apps/apps-engine/src/service.ts b/ee/apps/apps-engine/src/service.ts new file mode 100755 index 0000000000000..f27b480c9a9e8 --- /dev/null +++ b/ee/apps/apps-engine/src/service.ts @@ -0,0 +1,50 @@ +import type { Document } from 'mongodb'; +import polka from 'polka'; +import { api } from '@rocket.chat/core-services'; + +import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; +import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; +import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; + +const PORT = process.env.PORT || 3034; + +(async () => { + const db = await getConnection(); + + const trash = await getCollection(Collections.Trash); + + registerServiceModels(db, trash); + + api.setBroker(broker); + + // need to import service after models are registered + const { AppsOrchestratorService } = await import('../../../../apps/meteor/ee/app/apps/service'); + const { AppsStatisticsService } = await import('../../../../apps/meteor/ee/app/apps/statisticsService'); + const { AppsConverterService } = await import('../../../../apps/meteor/ee/app/apps/converterService'); + const { AppsManagerService } = await import('../../../../apps/meteor/ee/app/apps/managerService'); + const { AppsVideoManagerService } = await import('../../../../apps/meteor/ee/app/apps/videoManagerService'); + const { AppsApiService } = await import('../../../../apps/meteor/ee/app/apps/apiService'); + + api.registerService(new AppsOrchestratorService(db)); + api.registerService(new AppsStatisticsService()); + api.registerService(new AppsConverterService()); + api.registerService(new AppsManagerService()); + api.registerService(new AppsVideoManagerService()); + api.registerService(new AppsApiService()); + + await api.start(); + + polka() + .get('/health', async function (_req, res) { + try { + await api.nodeList(); + res.end('ok'); + } catch (err) { + console.error('Service not healthy', err); + + res.writeHead(500); + res.end('not healthy'); + } + }) + .listen(PORT); +})(); diff --git a/ee/apps/apps-engine/tsconfig.json b/ee/apps/apps-engine/tsconfig.json new file mode 100644 index 0000000000000..fd62af76f0710 --- /dev/null +++ b/ee/apps/apps-engine/tsconfig.json @@ -0,0 +1,31 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "target": "es2018", + "lib": ["esnext", "dom"], + "allowJs": true, + "checkJs": false, + "incremental": true, + + /* Strict Type-Checking Options */ + "noImplicitAny": true, + "strictNullChecks": true, + "strictPropertyInitialization": false, + "strictFunctionTypes": false, + + /* Additional Checks */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": false, + "noFallthroughCasesInSwitch": false, + + /* Module Resolution Options */ + "outDir": "./dist", + "importsNotUsedAsValues": "preserve", + "declaration": false, + "declarationMap": false + }, + "files": ["./src/service.ts"], + "include": ["../../../apps/meteor/definition/externals/meteor"], + "exclude": ["./dist"] +} diff --git a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts index 81c9ef11b72f3..a403b9e402a50 100644 --- a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts +++ b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts @@ -380,8 +380,7 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT const { rid } = await roomService.createDirectMessage({ to: details.userId, from: 'rocket.cat' }); this.log.info(`Transcript for room ${details.rid} by user ${details.userId} - Sending error message to user`); - await messageService.sendMessage({ - fromId: 'rocket.cat', + await messageService.sendMessage('rocket.cat', { rid, msg: `${await translationService.translate('pdf_error_message', user)}: ${e.message}`, }); diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index 82a3f92598ad2..cee5ef01fff93 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -24,7 +24,13 @@ import type { IListRoomsFilter, } from './types/ITeamService'; import type { IMessageReadsService } from './types/IMessageReadsService'; -import type { IRoomService, ICreateRoomParams, ISubscriptionExtraData } from './types/IRoomService'; +import type { + IRoomService, + ICreateRoomParams, + ISubscriptionExtraData, + ICreateDiscussionParams, + ICreateRoomExtraData, +} from './types/IRoomService'; import type { IMediaService, ResizeResult } from './types/IMediaService'; import type { IVoipService } from './types/IVoipService'; import type { IOmnichannelVoipService, FindVoipRoomsParams } from './types/IOmnichannelVoipService'; @@ -36,10 +42,21 @@ import type { IDeviceManagementService } from './types/IDeviceManagementService' import type { IPushService } from './types/IPushService'; import type { IOmnichannelService } from './types/IOmnichannelService'; import type { ITelemetryEvent, TelemetryMap, TelemetryEvents } from './types/ITelemetryEvent'; +import type { IAppsApiService, IRequestWithPrivateHash } from './types/IAppsApiService'; +import type { IAppsConverterService } from './types/IAppsConverterService'; +import type { IAppsManagerService } from './types/IAppsManagerService'; +import type { IAppsService } from './types/IAppsService'; +import type { IAppsStatisticsService } from './types/IAppsStatisticsService'; +import type { IAppsVideoManagerService } from './types/IAppsVideoManagerService'; +import type { ILivechatService, CloseRoomParams } from './types/ILivechatService'; +import type { IMessageService } from './types/IMessageService'; +import type { INotificationService } from './types/INotificationService'; +import type { ISlashCommandService } from './types/ISlashCommandService'; +import type { ICloudService, IAccessToken } from './types/ICloudService'; +import type { IUserService, ISetUserAvatarParams } from './types/IUserService'; import type { IOmnichannelTranscriptService } from './types/IOmnichannelTranscriptService'; import type { IQueueWorkerService, HealthAggResult } from './types/IQueueWorkerService'; import type { ITranslationService } from './types/ITranslationService'; -import type { IMessageService } from './types/IMessageService'; import type { ISettingsService } from './types/ISettingsService'; import type { IOmnichannelIntegrationService } from './types/IOmnichannelIntegrationService'; @@ -81,6 +98,8 @@ export { IPushService, IMessageReadsService, IRoomService, + ICreateDiscussionParams, + ICreateRoomExtraData, ISAUMonitorService, ISubscriptionExtraData, ITeamAutocompleteResult, @@ -107,18 +126,32 @@ export { ISendFileMessageParams, IUploadFileParams, IUploadService, + IAppsService, + IAppsStatisticsService, + IAppsConverterService, + IAppsManagerService, + IAppsVideoManagerService, + IAppsApiService, + IRequestWithPrivateHash, + ILivechatService, + CloseRoomParams, + IMessageService, + INotificationService, + ISlashCommandService, + ICloudService, + IAccessToken, + IUserService, + ISetUserAvatarParams, IOmnichannelTranscriptService, IQueueWorkerService, HealthAggResult, ITranslationService, - IMessageService, ISettingsService, IOmnichannelIntegrationService, }; // 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 Presence = proxifyWithWait('presence'); export const Account = proxifyWithWait('accounts'); export const License = proxifyWithWait('license'); @@ -138,6 +171,18 @@ export const SAUMonitor = proxifyWithWait('sau-monitor'); export const DeviceManagement = proxifyWithWait('device-management'); export const VideoConf = proxifyWithWait('video-conference'); export const Upload = proxifyWithWait('upload'); +export const Cloud = proxifyWithWait('cloud'); +export const User = proxifyWithWait('user'); +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 LivechatService = proxifyWithWait('livechat'); +export const MessageService = proxifyWithWait('message'); +export const NotificationService = proxifyWithWait('notification'); +export const SlashCommandService = proxifyWithWait('slashcommand'); export const QueueWorker = proxifyWithWait('queue-worker'); export const OmnichannelTranscript = proxifyWithWait('omnichannel-transcript'); export const Message = proxifyWithWait('message'); diff --git a/packages/core-services/src/types/IAppsApiService.ts b/packages/core-services/src/types/IAppsApiService.ts new file mode 100644 index 0000000000000..3500c5e895708 --- /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): void; + unregisterApi(appId: string): void; +} 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/IAppsEngineService.ts b/packages/core-services/src/types/IAppsEngineService.ts index 5c67c27833ab9..b8d94fc0ec618 100644 --- a/packages/core-services/src/types/IAppsEngineService.ts +++ b/packages/core-services/src/types/IAppsEngineService.ts @@ -3,7 +3,7 @@ import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; import type { IGetAppsFilter } from '@rocket.chat/apps-engine/server/IGetAppsFilter'; export interface IAppsEngineService { - isInitialized(): boolean; - getApps(query: IGetAppsFilter): Promise; + isInitialized(): Promise; + getApps(query: IGetAppsFilter): Promise>; getAppStorageItemById(appId: 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..82cc367083645 --- /dev/null +++ b/packages/core-services/src/types/IAppsManagerService.ts @@ -0,0 +1,47 @@ +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 { ILoggerStorageEntry } from '@rocket.chat/apps-engine/server/logging'; +import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; +import type { IAppLogStorageFindOptions, IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; + +export interface IAppsManagerService { + get(filter?: IGetAppsFilter): Array; + add(appPackage: Buffer, installationParameters: IAppInstallParameters): Promise; + remove(id: string, uninstallationParameters: IAppUninstallParameters): Promise; + removeLocal(id: string): Promise; + update( + appPackage: Buffer, + permissionsGranted: Array, + updateOptions?: { loadApp: boolean }, + ): Promise; + updateLocal(stored: IAppStorageItem, appPackageOrInstance: ProxiedApp | Buffer): Promise; + enable(appId: string): Promise; + disable(appId: string, status?: AppStatus, silent?: boolean): Promise; + loadOne(appId: string): Promise; + getOneById(appId: string): ProxiedApp | undefined; + getAllActionButtons(): IUIActionButton[]; + updateAppSetting(appId: string, setting: ISetting): Promise; + getAppSetting(appId: string, settingId: string): ISetting | undefined; + listApis(appId: string): Array | 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; + findLogs(query: { [field: string]: any }, options?: IAppLogStorageFindOptions): Promise | undefined>; + getAppStorageItemById(appId: string): 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..e503ef72e00ab --- /dev/null +++ b/packages/core-services/src/types/IAppsService.ts @@ -0,0 +1,26 @@ +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; + initialize: () => void; + load: () => Promise; + unload: () => Promise; + isLoaded: () => boolean; + isInitialized: () => boolean; + getPersistenceModel: () => IAppsPersistenceModel; + getMarketplaceUrl: () => string; + getProvidedComponents: () => IExternalComponent[]; + rocketChatLoggerWarn(obj: T, args?: any): void; + rocketChatLoggerError(obj: T, args?: any): void; + rocketChatLoggerDebug(args?: any): void; + retrieveOneFromStorage(appId: string): Promise; + fetchAppSourceStorage(storageItem: IAppStorageItem): Promise | undefined; + setStorage(value: string): void; + setFileSystemStoragePath(value: string): void; + // runOnAppEvent(listener: AppServerNotifier): void; +} diff --git a/packages/core-services/src/types/IAppsStatisticsService.ts b/packages/core-services/src/types/IAppsStatisticsService.ts new file mode 100644 index 0000000000000..3cc68e5516583 --- /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: () => AppStatistics; +} diff --git a/packages/core-services/src/types/IAppsVideoManagerService.ts b/packages/core-services/src/types/IAppsVideoManagerService.ts new file mode 100644 index 0000000000000..b72697abe8f85 --- /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 | undefined>; +} diff --git a/packages/core-services/src/types/ICloudService.ts b/packages/core-services/src/types/ICloudService.ts new file mode 100644 index 0000000000000..35a66ecfe26a2 --- /dev/null +++ b/packages/core-services/src/types/ICloudService.ts @@ -0,0 +1,8 @@ +export interface IAccessToken { + token: string; + expiresAt: Date; +} + +export interface ICloudService { + getWorkspaceAccessTokenWithScope(scope?: string): IAccessToken; +} diff --git a/packages/core-services/src/types/ILivechatService.ts b/packages/core-services/src/types/ILivechatService.ts new file mode 100644 index 0000000000000..b1e5b2580fc71 --- /dev/null +++ b/packages/core-services/src/types/ILivechatService.ts @@ -0,0 +1,86 @@ +import type { IVisitor } from '@rocket.chat/apps-engine/definition/livechat'; +import type { + IMessage, + ILivechatVisitor, + OmnichannelSourceType, + IOmnichannelRoom, + IRoom, + ILivechatAgent, + IUser, +} from '@rocket.chat/core-typings'; + +import type { IServiceClass } from './ServiceClass'; + +type GenericCloseRoomParams = { + room: IOmnichannelRoom; + comment?: string; + options?: { + clientAction?: boolean; + tags?: string[]; + emailTranscript?: + | { + sendToVisitor: false; + } + | { + sendToVisitor: true; + requestData: NonNullable; + }; + pdfTranscript?: { + requestedBy: string; + }; + }; +}; + +export type CloseRoomParamsByUser = { + user: IUser; +} & GenericCloseRoomParams; + +export type CloseRoomParamsByVisitor = { + visitor: ILivechatVisitor; +} & GenericCloseRoomParams; + +export type CloseRoomParams = CloseRoomParamsByUser | CloseRoomParamsByVisitor; + +export interface ILivechatService extends IServiceClass { + isOnline(department?: string, skipNoAgentSetting?: boolean, skipFallbackCheck?: boolean): Promise; + sendMessage(props: { guest: IVisitor; message: IMessage; roomInfo: Record; agent: string | undefined }): Promise; + updateMessage(props: { guest: IVisitor; message: IMessage }): Promise; + getRoom(props: { + guest: ILivechatVisitor; + rid?: string; + roomInfo?: { + source?: { type: OmnichannelSourceType; id?: string; alias?: string; label?: string; sidebarIcon?: string; defaultIcon?: string }; + }; + agent?: { agentId?: string; username?: string }; + extraParams?: Record; + }): Promise<{ room: IOmnichannelRoom; newRoom: boolean }>; + closeRoom(props: CloseRoomParams): Promise; + registerGuest(props: { + id?: string; + token: string; + name: string; + email: string; + department?: string; + phone?: { number: string }; + username: string; + connectionData?: string; + status?: string; + }): Promise; + transferVisitor( + room: IRoom, + visitor: IVisitor, + transferData: { + userId?: string; + departmentId?: string; + transferredTo: ILivechatAgent; + transferredBy: { + _id: string; + username?: string; + name?: string; + type: string; + }; + }, + ): Promise; + getRoomMessages(roomId: string): Promise>; + setCustomFields(props: { token: string; key: string; value: string; overwrite: boolean }): Promise; +} diff --git a/packages/core-services/src/types/IMessageService.ts b/packages/core-services/src/types/IMessageService.ts index cf02b9e664bee..b3f1a48a8b210 100644 --- a/packages/core-services/src/types/IMessageService.ts +++ b/packages/core-services/src/types/IMessageService.ts @@ -1,7 +1,10 @@ -import type { IMessage, MessageTypesValues, IUser } from '@rocket.chat/core-typings'; +import type { AtLeast, IMessage, IUser, MessageTypesValues } from '@rocket.chat/core-typings'; -export interface IMessageService { - sendMessage({ fromId, rid, msg }: { fromId: string; rid: string; msg: string }): Promise; +import type { IServiceClass } from './ServiceClass'; + +export interface IMessageService extends IServiceClass { + sendMessage(userId: string, message: AtLeast): Promise; + updateMessage(message: IMessage, editor: IUser): Promise; saveSystemMessage( type: MessageTypesValues, rid: string, diff --git a/packages/core-services/src/types/INotificationService.ts b/packages/core-services/src/types/INotificationService.ts new file mode 100644 index 0000000000000..24ae096ad88cb --- /dev/null +++ b/packages/core-services/src/types/INotificationService.ts @@ -0,0 +1,5 @@ +import type { IServiceClass } from './ServiceClass'; + +export interface INotificationService extends IServiceClass { + notifyRoom(room: string, eventName: string, ...args: any[]): void; +} diff --git a/packages/core-services/src/types/IRoomService.ts b/packages/core-services/src/types/IRoomService.ts index b8f4ccde0ec24..826f9272fa49b 100644 --- a/packages/core-services/src/types/IRoomService.ts +++ b/packages/core-services/src/types/IRoomService.ts @@ -1,4 +1,4 @@ -import type { IRoom } from '@rocket.chat/core-typings'; +import type { IRoom, IUser } from '@rocket.chat/core-typings'; export interface ISubscriptionExtraData { open: boolean; @@ -25,8 +25,26 @@ export interface ICreateRoomParams { extraData?: Partial; options?: ICreateRoomOptions; } + +export interface ICreateDiscussionParams { + parentRoomId: string; + parentMessageId: string; + creatorId: string; + name: string; + members: Array; + encrypted?: boolean; + reply?: string; +} + export interface IRoomService { addMember(uid: string, rid: string): Promise; create(uid: string, params: ICreateRoomParams): Promise; + createDiscussion(params: ICreateDiscussionParams): Promise; + addUserToRoom( + rid: string, + user: Pick | string, + inviter?: Pick, + silenced?: boolean, + ): Promise; createDirectMessage(data: { to: string; from: string }): Promise<{ rid: string }>; } diff --git a/packages/core-services/src/types/ISlashCommandService.ts b/packages/core-services/src/types/ISlashCommandService.ts new file mode 100644 index 0000000000000..694cf01b99821 --- /dev/null +++ b/packages/core-services/src/types/ISlashCommandService.ts @@ -0,0 +1,10 @@ +import type { SlashCommand } from '@rocket.chat/core-typings'; + +import type { IServiceClass } from './ServiceClass'; + +export interface ISlashCommandService extends IServiceClass { + getCommand(command: string): SlashCommand; + setCommand(command: SlashCommand): void; + setAppCommand(command: SlashCommand): void; + removeCommand(command: string): void; +} diff --git a/packages/core-services/src/types/IUploadService.ts b/packages/core-services/src/types/IUploadService.ts index c816612c03487..25ae368e3dcdf 100644 --- a/packages/core-services/src/types/IUploadService.ts +++ b/packages/core-services/src/types/IUploadService.ts @@ -24,5 +24,6 @@ export interface IUploadService { uploadFile(params: IUploadFileParams): Promise; sendFileMessage(params: ISendFileMessageParams): Promise; sendFileLivechatMessage(params: ISendFileLivechatMessageParams): Promise; + getBuffer(cb: Function): Promise; getFileBuffer({ file }: { userId: string; file: IUpload }): Promise; } diff --git a/packages/core-services/src/types/IUserService.ts b/packages/core-services/src/types/IUserService.ts new file mode 100644 index 0000000000000..d6d5f3a5f8035 --- /dev/null +++ b/packages/core-services/src/types/IUserService.ts @@ -0,0 +1,15 @@ +import type { IUser } from '@rocket.chat/core-typings'; + +export interface ISetUserAvatarParams { + user: Pick; + dataURI: string; + contentType: string; + service: 'initials' | 'url' | 'rest' | string; + etag?: string; +} + +export interface IUserService { + setUserAvatar(param: ISetUserAvatarParams): Promise; + deleteUser(userId: string, confirmRelinquish: boolean): Promise; + checkUsernameAvailability(username: string): Promise; +} diff --git a/packages/core-typings/src/Apps.ts b/packages/core-typings/src/Apps.ts index 3d0afce9d12c5..c2051d04f17ff 100644 --- a/packages/core-typings/src/Apps.ts +++ b/packages/core-typings/src/Apps.ts @@ -1,5 +1,8 @@ +import type { IAppStorageItem as IAppStorageItemType } from '@rocket.chat/apps-engine/server/storage'; import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import type { IRocketChatRecord } from '.'; + export type AppScreenshot = { id: string; appId: string; @@ -126,3 +129,5 @@ export type App = { documentationUrl: string; migrated: boolean; }; + +export interface IAppStorageItem extends IRocketChatRecord, Omit {} diff --git a/packages/core-typings/src/SlashCommands/index.ts b/packages/core-typings/src/SlashCommands/index.ts index fc35ce52e02ea..b584ae8f2227f 100644 --- a/packages/core-typings/src/SlashCommands/index.ts +++ b/packages/core-typings/src/SlashCommands/index.ts @@ -6,6 +6,7 @@ type SlashCommandCallback = ( params: string, message: RequiredField, 'rid'>, triggerId?: string, + userId?: string, ) => Promise | unknown; export type SlashCommandPreviewItem = { @@ -23,6 +24,7 @@ type SlashCommandPreviewer = ( command: string, params: string, message: RequiredField, 'rid'>, + userId?: string, ) => Promise; type SlashCommandPreviewCallback = ( @@ -30,7 +32,8 @@ type SlashCommandPreviewCallback = ( params: string, message: RequiredField, 'rid'>, preview: SlashCommandPreviewItem, - triggerId?: string, + triggerId: string, + userId?: string, ) => void; export type SlashCommandOptions = { diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index b6d39af679515..d9463ab8c7b24 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -23,6 +23,7 @@ "/dist" ], "dependencies": { + "@rocket.chat/apps-engine": "1.38.1", "@rocket.chat/core-typings": "workspace:^" } } diff --git a/packages/model-typings/src/index.ts b/packages/model-typings/src/index.ts index 98a7f01b724d6..bce28129144fc 100644 --- a/packages/model-typings/src/index.ts +++ b/packages/model-typings/src/index.ts @@ -1,4 +1,7 @@ export * from './models/IAnalyticsModel'; +export * from './models/IAppsLogsModel'; +export * from './models/IAppsModel'; +export * from './models/IAppsPersistenceModel'; export * from './models/IAvatarsModel'; export * from './models/IBannersDismissModel'; export * from './models/IBannersModel'; diff --git a/packages/model-typings/src/models/IAppsLogsModel.ts b/packages/model-typings/src/models/IAppsLogsModel.ts new file mode 100644 index 0000000000000..a3af0bb0235f1 --- /dev/null +++ b/packages/model-typings/src/models/IAppsLogsModel.ts @@ -0,0 +1,7 @@ +import type { ILoggerStorageEntry } from '@rocket.chat/apps-engine/server/logging'; + +import type { IBaseModel } from './IBaseModel'; + +export interface IAppsLogsModel extends IBaseModel { + resetTTLIndex(expireAfterSeconds: number): Promise; +} diff --git a/packages/model-typings/src/models/IAppsModel.ts b/packages/model-typings/src/models/IAppsModel.ts index 7378e1f7bbe88..529fb4c97ed9b 100644 --- a/packages/model-typings/src/models/IAppsModel.ts +++ b/packages/model-typings/src/models/IAppsModel.ts @@ -1,4 +1,8 @@ +import type { IAppStorageItem } from '@rocket.chat/core-typings'; + import type { IBaseModel } from './IBaseModel'; -// TODO: type for AppLogs -export type IAppsModel = IBaseModel; +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface IAppsModel extends IBaseModel { + // +} diff --git a/packages/model-typings/src/models/IAppsPersistenceModel.ts b/packages/model-typings/src/models/IAppsPersistenceModel.ts index 0fbab033c6c90..a9b5bf810cb26 100644 --- a/packages/model-typings/src/models/IAppsPersistenceModel.ts +++ b/packages/model-typings/src/models/IAppsPersistenceModel.ts @@ -1,8 +1,9 @@ +import type { IPersistenceItem } from '@rocket.chat/apps-engine/definition/persistence'; import type { DeleteResult, Filter } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; -// TODO: type for appspersistence -export interface IAppsPersistenceModel extends IBaseModel { +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface IAppsPersistenceModel extends IBaseModel { remove(query: Filter): Promise; } diff --git a/packages/model-typings/src/models/IBaseModel.ts b/packages/model-typings/src/models/IBaseModel.ts index b1d098245bd6f..4bd0fe56b1d83 100644 --- a/packages/model-typings/src/models/IBaseModel.ts +++ b/packages/model-typings/src/models/IBaseModel.ts @@ -2,6 +2,7 @@ import type { BulkWriteOptions, ChangeStream, Collection, + CreateIndexesOptions, DeleteOptions, DeleteResult, Document, @@ -10,6 +11,7 @@ import type { FindCursor, FindOneAndUpdateOptions, FindOptions, + IndexSpecification, InsertManyResult, InsertOneOptions, InsertOneResult, @@ -45,6 +47,10 @@ export interface IBaseModel< > { col: Collection; + tryEnsureIndex(index: IndexSpecification, options: CreateIndexesOptions): Promise; + + tryDropIndex(index: string): Promise; + getCollectionName(): string; findOneAndUpdate(query: Filter, update: UpdateFilter | T, options?: FindOneAndUpdateOptions): Promise>; diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index 9f5d2466453f8..5b2f885006a88 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -100,6 +100,10 @@ export interface ILivechatRoomsModel extends IBaseModel { setDepartmentByRoomId(roomId: any, departmentId: any): any; + findOpenByVisitorTokenAndDepartmentId(visitorToken: string, departmentId: string, options?: any): any; + + findOpenByVisitorToken(visitorToken: string, options?: any): any; + findOpen(): FindCursor; setAutoTransferOngoingById(roomId: string): Promise; diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index 68faa0897d046..7851839062550 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -91,6 +91,8 @@ export interface IRoomsModel extends IBaseModel { findOneDirectRoomContainingAllUserIDs(uids: string[], options?: FindOptions): Promise; + findDirectRoomContainingAllUsernames(usernames: string[], options?: FindOptions): Promise; + countByType(t: IRoom['t']): Promise; findPaginatedByNameOrFNameAndRoomIdsIncludingTeamRooms( diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index 80619d3942389..a97d7d09b92ab 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -95,6 +95,8 @@ export interface IUsersModel extends IBaseModel { userId: any, ): Promise<{ agentId: string; username: string; lastAssignTime: Date; lastRoutingTime: Date; queueInfo: { chats: number } }>; + getAgentInfo(agentId: string): Promise; + findAllResumeTokensByUserId(userId: any): any; findActiveByUsernameOrNameRegexWithExceptionsAndConditions( @@ -156,6 +158,8 @@ export interface IUsersModel extends IBaseModel { removeRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][]): Promise; + removeBannerById(_id: string, banner: any): Promise; + isUserInRoleScope(uid: IUser['_id']): Promise; addBannerById(_id: any, banner: any): any; @@ -184,6 +188,10 @@ export interface IUsersModel extends IBaseModel { findActiveByIdsOrUsernames(userIds: string[], options?: any): FindCursor; + getActiveLocalUserCount(): number; + + findActiveRemote(options: any): any; + setAsFederated(userId: string): any; removeRoomByRoomId(rid: any): any; diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 582f9d568e003..7e68b6038f995 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -1,5 +1,6 @@ import type { IAnalyticsModel, + IAppsLogsModel, IAvatarsModel, IBannersDismissModel, IBannersModel, @@ -87,11 +88,12 @@ export function getCollectionName(name: string): string { export { registerModel } from './proxify'; -export const Apps = proxify('IAppsModel'); export const AppsTokens = proxify('IAppsTokensModel'); -export const AppsPersistence = proxify('IAppsPersistenceModel'); export const AppLogs = proxify('IAppLogsModel'); export const Analytics = proxify('IAnalyticsModel'); +export const Apps = proxify('IAppsModel'); +export const AppsLogs = proxify('IAppsLogsModel'); +export const AppsPersistence = proxify('IAppsPersistenceModel'); export const Avatars = proxify('IAvatarsModel'); export const BannersDismiss = proxify('IBannersDismissModel'); export const Banners = proxify('IBannersModel'); diff --git a/yarn.lock b/yarn.lock index 698650bb0288e..188f33ebf4080 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2919,6 +2919,24 @@ __metadata: languageName: node linkType: hard +"@eslint-community/eslint-utils@npm:^4.2.0": + version: 4.4.0 + resolution: "@eslint-community/eslint-utils@npm:4.4.0" + dependencies: + eslint-visitor-keys: ^3.3.0 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: cdfe3ae42b4f572cbfb46d20edafe6f36fc5fb52bf2d90875c58aefe226892b9677fef60820e2832caf864a326fe4fc225714c46e8389ccca04d5f9288aabd22 + languageName: node + linkType: hard + +"@eslint-community/regexpp@npm:^4.4.0": + version: 4.5.0 + resolution: "@eslint-community/regexpp@npm:4.5.0" + checksum: 99c01335947dbd7f2129e954413067e217ccaa4e219fe0917b7d2bd96135789384b8fedbfb8eb09584d5130b27a7b876a7150ab7376f51b3a0c377d5ce026a10 + languageName: node + linkType: hard + "@eslint/eslintrc@npm:^0.4.3": version: 0.4.3 resolution: "@eslint/eslintrc@npm:0.4.3" @@ -2970,6 +2988,30 @@ __metadata: languageName: node linkType: hard +"@eslint/eslintrc@npm:^2.0.2": + version: 2.0.2 + resolution: "@eslint/eslintrc@npm:2.0.2" + dependencies: + ajv: ^6.12.4 + debug: ^4.3.2 + espree: ^9.5.1 + globals: ^13.19.0 + ignore: ^5.2.0 + import-fresh: ^3.2.1 + js-yaml: ^4.1.0 + minimatch: ^3.1.2 + strip-json-comments: ^3.1.1 + checksum: cfcf5e12c7b2c4476482e7f12434e76eae16fcd163ee627309adb10b761e5caa4a4e52ed7be464423320ff3d11eca5b50de5bf8be3e25834222470835dd5c801 + languageName: node + linkType: hard + +"@eslint/js@npm:8.39.0": + version: 8.39.0 + resolution: "@eslint/js@npm:8.39.0" + checksum: 63fe36e2bfb5ff5705d1c1a8ccecd8eb2f81d9af239713489e767b0e398759c0177fcc75ad62581d02942f2776903a8496d5fae48dc2d883dff1b96fcb19e9e2 + languageName: node + linkType: hard + "@faker-js/faker@npm:^6.3.1": version: 6.3.1 resolution: "@faker-js/faker@npm:6.3.1" @@ -6399,6 +6441,53 @@ __metadata: languageName: unknown linkType: soft +"@rocket.chat/apps-engine-service@workspace:ee/apps/apps-engine": + version: 0.0.0-use.local + resolution: "@rocket.chat/apps-engine-service@workspace:ee/apps/apps-engine" + dependencies: + "@rocket.chat/core-typings": "workspace:^" + "@rocket.chat/emitter": next + "@rocket.chat/eslint-config": "workspace:^" + "@rocket.chat/model-typings": "workspace:^" + "@rocket.chat/models": "workspace:^" + "@rocket.chat/rest-typings": "workspace:^" + "@rocket.chat/string-helpers": next + "@types/eslint": ^8 + "@types/node": ^14.18.21 + "@types/polka": ^0.5.4 + ejson: ^2.2.2 + eslint: ^8.21.0 + eventemitter3: ^4.0.7 + fibers: ^5.0.3 + mem: ^8.1.1 + moleculer: ^0.14.21 + mongodb: ^4.3.1 + nats: ^2.4.0 + pino: ^8.4.2 + polka: ^0.5.2 + ts-node: ^10.9.1 + typescript: ~4.5.5 + languageName: unknown + linkType: soft + +"@rocket.chat/apps-engine@file:/home/tapia/projects/Rocket.Chat.Apps-engine::locator=%40rocket.chat%2Fmodel-typings%40workspace%3Apackages%2Fmodel-typings": + version: 1.39.0-alpha + resolution: "@rocket.chat/apps-engine@file:/home/tapia/projects/Rocket.Chat.Apps-engine#/home/tapia/projects/Rocket.Chat.Apps-engine::hash=fa19cd&locator=%40rocket.chat%2Fmodel-typings%40workspace%3Apackages%2Fmodel-typings" + dependencies: + adm-zip: ^0.5.9 + cryptiles: ^4.1.3 + jose: ^4.11.1 + lodash.clonedeep: ^4.5.0 + semver: ^5.7.1 + stack-trace: 0.0.10 + uuid: ^3.4.0 + vm2: ^3.9.17 + peerDependencies: + "@rocket.chat/ui-kit": "*" + checksum: f36cf14ecffd638e6f4dfa0544a1f02f5c84cc1776b4a7a85ce07b62e12621a8d39c9e0997639f5ee36d3db899e2c072764a4c6cb4b311aca07d9a8226858f61 + languageName: node + linkType: hard + "@rocket.chat/apps-engine@npm:1.38.2": version: 1.38.2 resolution: "@rocket.chat/apps-engine@npm:1.38.2" @@ -7423,6 +7512,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/model-typings@workspace:packages/model-typings" dependencies: + "@rocket.chat/apps-engine": /home/tapia/projects/Rocket.Chat.Apps-engine "@rocket.chat/core-typings": "workspace:^" "@types/jest": ~29.5.0 "@types/node-rsa": ^1.1.1 @@ -11497,6 +11587,16 @@ __metadata: languageName: node linkType: hard +"@types/eslint@npm:^8": + version: 8.37.0 + resolution: "@types/eslint@npm:8.37.0" + dependencies: + "@types/estree": "*" + "@types/json-schema": "*" + checksum: 06d3b3fba12004294591b5c7a52e3cec439472195da54e096076b1f2ddfbb8a445973b9681046dd530a6ac31eca502f635abc1e3ce37d03513089358e6f822ee + languageName: node + linkType: hard + "@types/eslint@npm:^8.4.10": version: 8.4.10 resolution: "@types/eslint@npm:8.4.10" @@ -15112,6 +15212,13 @@ __metadata: languageName: node linkType: hard +"bluebird@npm:^2.10.0": + version: 2.11.0 + resolution: "bluebird@npm:2.11.0" + checksum: f1c6cbec64100bca65c88e5de0d9ee9bbb435f7c74c68a16a9466a8b40daf64346805c2fe04af821564ce6d4199085e7855d2e272282a065eb723344815cc354 + languageName: node + linkType: hard + "blueimp-md5@npm:^2.16.0": version: 2.19.0 resolution: "blueimp-md5@npm:2.19.0" @@ -19780,6 +19887,16 @@ __metadata: languageName: node linkType: hard +"eslint-scope@npm:^7.2.0": + version: 7.2.0 + resolution: "eslint-scope@npm:7.2.0" + dependencies: + esrecurse: ^4.3.0 + estraverse: ^5.2.0 + checksum: 64591a2d8b244ade9c690b59ef238a11d5c721a98bcee9e9f445454f442d03d3e04eda88e95a4daec558220a99fa384309d9faae3d459bd40e7a81b4063980ae + languageName: node + linkType: hard + "eslint-utils@npm:^2.1.0": version: 2.1.0 resolution: "eslint-utils@npm:2.1.0" @@ -19821,6 +19938,13 @@ __metadata: languageName: node linkType: hard +"eslint-visitor-keys@npm:^3.4.0": + version: 3.4.0 + resolution: "eslint-visitor-keys@npm:3.4.0" + checksum: 33159169462d3989321a1ec1e9aaaf6a24cc403d5d347e9886d1b5bfe18ffa1be73bdc6203143a28a606b142b1af49787f33cff0d6d0813eb5f2e8d2e1a6043c + languageName: node + linkType: hard + "eslint@npm:^7.32.0": version: 7.32.0 resolution: "eslint@npm:7.32.0" @@ -19920,6 +20044,56 @@ __metadata: languageName: node linkType: hard +"eslint@npm:^8.21.0": + version: 8.39.0 + resolution: "eslint@npm:8.39.0" + dependencies: + "@eslint-community/eslint-utils": ^4.2.0 + "@eslint-community/regexpp": ^4.4.0 + "@eslint/eslintrc": ^2.0.2 + "@eslint/js": 8.39.0 + "@humanwhocodes/config-array": ^0.11.8 + "@humanwhocodes/module-importer": ^1.0.1 + "@nodelib/fs.walk": ^1.2.8 + ajv: ^6.10.0 + chalk: ^4.0.0 + cross-spawn: ^7.0.2 + debug: ^4.3.2 + doctrine: ^3.0.0 + escape-string-regexp: ^4.0.0 + eslint-scope: ^7.2.0 + eslint-visitor-keys: ^3.4.0 + espree: ^9.5.1 + esquery: ^1.4.2 + esutils: ^2.0.2 + fast-deep-equal: ^3.1.3 + file-entry-cache: ^6.0.1 + find-up: ^5.0.0 + glob-parent: ^6.0.2 + globals: ^13.19.0 + grapheme-splitter: ^1.0.4 + ignore: ^5.2.0 + import-fresh: ^3.0.0 + imurmurhash: ^0.1.4 + is-glob: ^4.0.0 + is-path-inside: ^3.0.3 + js-sdsl: ^4.1.4 + js-yaml: ^4.1.0 + json-stable-stringify-without-jsonify: ^1.0.1 + levn: ^0.4.1 + lodash.merge: ^4.6.2 + minimatch: ^3.1.2 + natural-compare: ^1.4.0 + optionator: ^0.9.1 + strip-ansi: ^6.0.1 + strip-json-comments: ^3.1.0 + text-table: ^0.2.0 + bin: + eslint: bin/eslint.js + checksum: d7a074ff326e7ea482500dc0427a7d4b0260460f0f812d19b46b1cca681806b67309f23da9d17cd3de8eb74dd3c14cb549c4d58b05b140564d14cc1a391122a0 + languageName: node + linkType: hard + "eslint@npm:^8.29.0, eslint@npm:~8.29.0": version: 8.29.0 resolution: "eslint@npm:8.29.0" @@ -19991,6 +20165,17 @@ __metadata: languageName: node linkType: hard +"espree@npm:^9.5.1": + version: 9.5.1 + resolution: "espree@npm:9.5.1" + dependencies: + acorn: ^8.8.0 + acorn-jsx: ^5.3.2 + eslint-visitor-keys: ^3.4.0 + checksum: cdf6e43540433d917c4f2ee087c6e987b2063baa85a1d9cdaf51533d78275ebd5910c42154e7baf8e3e89804b386da0a2f7fad2264d8f04420e7506bf87b3b88 + languageName: node + linkType: hard + "esprima@npm:^4.0.0, esprima@npm:^4.0.1": version: 4.0.1 resolution: "esprima@npm:4.0.1" @@ -20010,6 +20195,15 @@ __metadata: languageName: node linkType: hard +"esquery@npm:^1.4.2": + version: 1.5.0 + resolution: "esquery@npm:1.5.0" + dependencies: + estraverse: ^5.1.0 + checksum: aefb0d2596c230118656cd4ec7532d447333a410a48834d80ea648b1e7b5c9bc9ed8b5e33a89cb04e487b60d622f44cf5713bf4abed7c97343edefdc84a35900 + languageName: node + linkType: hard + "esrecurse@npm:^4.1.0, esrecurse@npm:^4.3.0": version: 4.3.0 resolution: "esrecurse@npm:4.3.0" @@ -37335,6 +37529,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:~4.5.5": + version: 4.5.5 + resolution: "typescript@npm:4.5.5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 506f4c919dc8aeaafa92068c997f1d213b9df4d9756d0fae1a1e7ab66b585ab3498050e236113a1c9e57ee08c21ec6814ca7a7f61378c058d79af50a4b1f5a5e + languageName: node + linkType: hard + "typescript@npm:~5.0.2": version: 5.0.2 resolution: "typescript@npm:5.0.2" @@ -37345,6 +37549,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@~4.5.5#~builtin": + version: 4.5.5 + resolution: "typescript@patch:typescript@npm%3A4.5.5#~builtin::version=4.5.5&hash=f456af" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 858c61fa63f7274ca4aaaffeced854d550bf416cff6e558c4884041b3311fb662f476f167cf5c9f8680c607239797e26a2ee0bcc6467fbc05bfcb218e1c6c671 + languageName: node + linkType: hard + "typescript@patch:typescript@~5.0.2#~builtin": version: 5.0.2 resolution: "typescript@patch:typescript@npm%3A5.0.2#~builtin::version=5.0.2&hash=f456af"