diff --git a/packages/apps-engine/deno-runtime/deno.jsonc b/packages/apps-engine/deno-runtime/deno.jsonc index 525eda492feb4..f9a45c4814951 100644 --- a/packages/apps-engine/deno-runtime/deno.jsonc +++ b/packages/apps-engine/deno-runtime/deno.jsonc @@ -13,7 +13,7 @@ "uuid": "npm:uuid@8.3.2" }, "tasks": { - "test": "deno test --no-check --allow-read=../../../" + "test": "deno test --no-check --allow-read=../../../,/tmp --allow-write=/tmp" }, "fmt": { "lineWidth": 160, diff --git a/packages/apps-engine/deno-runtime/handlers/api-handler.ts b/packages/apps-engine/deno-runtime/handlers/api-handler.ts index 0b3c5c375b89b..ed82e027745b3 100644 --- a/packages/apps-engine/deno-runtime/handlers/api-handler.ts +++ b/packages/apps-engine/deno-runtime/handlers/api-handler.ts @@ -1,11 +1,13 @@ -import { Defined, JsonRpcError } from 'jsonrpc-lite'; import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api/IApiEndpoint.ts'; +import { Defined, JsonRpcError } from 'jsonrpc-lite'; import { AppObjectRegistry } from '../AppObjectRegistry.ts'; import { Logger } from '../lib/logger.ts'; import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; +import { RequestContext } from '../lib/requestContext.ts'; -export default async function apiHandler(call: string, params: unknown): Promise { +export default async function apiHandler(request: RequestContext): Promise { + const { method: call, params } = request; const [, path, httpMethod] = call.split(':'); const endpoint = AppObjectRegistry.get(`api:${path}`); @@ -21,14 +23,14 @@ export default async function apiHandler(call: string, params: unknown): Promise return new JsonRpcError(`${path}'s ${httpMethod} not exists`, -32000); } - const [request, endpointInfo] = params as Array; + const [requestData, endpointInfo] = params as Array; - logger?.debug(`${path}'s ${call} is being executed...`, request); + logger?.debug(`${path}'s ${call} is being executed...`, requestData); try { // deno-lint-ignore ban-types const result = await (method as Function).apply(endpoint, [ - request, + requestData, endpointInfo, AppAccessorsInstance.getReader(), AppAccessorsInstance.getModifier(), diff --git a/packages/apps-engine/deno-runtime/handlers/app/construct.ts b/packages/apps-engine/deno-runtime/handlers/app/construct.ts index f0e7140490330..2710331618d9a 100644 --- a/packages/apps-engine/deno-runtime/handlers/app/construct.ts +++ b/packages/apps-engine/deno-runtime/handlers/app/construct.ts @@ -1,10 +1,12 @@ +import { Socket } from 'node:net'; + import type { IParseAppPackageResult } from '@rocket.chat/apps-engine/server/compiler/IParseAppPackageResult.ts'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import { require } from '../../lib/require.ts'; import { sanitizeDeprecatedUsage } from '../../lib/sanitizeDeprecatedUsage.ts'; import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { Socket } from 'node:net'; +import { RequestContext } from '../../lib/requestContext.ts'; const ALLOWED_NATIVE_MODULES = ['path', 'url', 'crypto', 'buffer', 'stream', 'net', 'http', 'https', 'zlib', 'util', 'punycode', 'os', 'querystring', 'fs']; const ALLOWED_EXTERNAL_MODULES = ['uuid']; @@ -13,7 +15,8 @@ function prepareEnvironment() { // Deno does not behave equally to Node when it comes to piping content to a socket // So we intervene here const originalFinal = Socket.prototype._final; - Socket.prototype._final = function _final(cb) { + // deno-lint-ignore no-explicit-any + Socket.prototype._final = function _final(cb: any) { // Deno closes the readable stream in the Socket earlier than Node // The exact reason for that is yet unknown, so we'll need to simply delay the execution // which allows data to be read in a response @@ -71,7 +74,9 @@ function wrapAppCode(code: string): (require: (module: string) => unknown) => Pr ) as (require: (module: string) => unknown) => Promise>; } -export default async function handleConstructApp(params: unknown): Promise { +export default async function handleConstructApp(request: RequestContext): Promise { + const { params } = request; + if (!Array.isArray(params)) { throw new Error('Invalid params', { cause: 'invalid_param_type' }); } diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleInitialize.ts b/packages/apps-engine/deno-runtime/handlers/app/handleInitialize.ts index e8d12ff47452e..3dbe746a2d602 100644 --- a/packages/apps-engine/deno-runtime/handlers/app/handleInitialize.ts +++ b/packages/apps-engine/deno-runtime/handlers/app/handleInitialize.ts @@ -2,8 +2,9 @@ import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; -export default async function handleInitialize(): Promise { +export default async function handleInitialize(_request: RequestContext): Promise { const app = AppObjectRegistry.get('app'); if (typeof app?.initialize !== 'function') { diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnDisable.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnDisable.ts index 3e7d14b618793..070daa20fbe5e 100644 --- a/packages/apps-engine/deno-runtime/handlers/app/handleOnDisable.ts +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnDisable.ts @@ -2,8 +2,9 @@ import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; -export default async function handleOnDisable(): Promise { +export default async function handleOnDisable(_request: RequestContext): Promise { const app = AppObjectRegistry.get('app'); if (typeof app?.onDisable !== 'function') { diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnEnable.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnEnable.ts index 6bbdbbe7a575b..6cafc5db7d30c 100644 --- a/packages/apps-engine/deno-runtime/handlers/app/handleOnEnable.ts +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnEnable.ts @@ -2,8 +2,9 @@ import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; -export default function handleOnEnable(): Promise { +export default function handleOnEnable(_request: RequestContext): Promise { const app = AppObjectRegistry.get('app'); if (typeof app?.onEnable !== 'function') { diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnInstall.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnInstall.ts index 95cb2be3f909f..801ec59eee2fc 100644 --- a/packages/apps-engine/deno-runtime/handlers/app/handleOnInstall.ts +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnInstall.ts @@ -2,8 +2,10 @@ import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; -export default async function handleOnInstall(params: unknown): Promise { +export default async function handleOnInstall(request: RequestContext): Promise { + const { params } = request; const app = AppObjectRegistry.get('app'); if (typeof app?.onInstall !== 'function') { diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts index 8209a93791016..3bda835d8ebd4 100644 --- a/packages/apps-engine/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts @@ -2,8 +2,10 @@ import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; -export default function handleOnPreSettingUpdate(params: unknown): Promise { +export default function handleOnPreSettingUpdate(request: RequestContext): Promise { + const { params } = request; const app = AppObjectRegistry.get('app'); if (typeof app?.onPreSettingUpdate !== 'function') { diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnSettingUpdated.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnSettingUpdated.ts index 16150be39be24..42688242fb170 100644 --- a/packages/apps-engine/deno-runtime/handlers/app/handleOnSettingUpdated.ts +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnSettingUpdated.ts @@ -2,8 +2,10 @@ import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; -export default async function handleOnSettingUpdated(params: unknown): Promise { +export default async function handleOnSettingUpdated(request: RequestContext): Promise { + const { params } = request; const app = AppObjectRegistry.get('app'); if (typeof app?.onSettingUpdated !== 'function') { diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnUninstall.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnUninstall.ts index cb7e50c4f0b67..244b6dc16e529 100644 --- a/packages/apps-engine/deno-runtime/handlers/app/handleOnUninstall.ts +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnUninstall.ts @@ -2,8 +2,10 @@ import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; -export default async function handleOnUninstall(params: unknown): Promise { +export default async function handleOnUninstall(request: RequestContext): Promise { + const { params } = request; const app = AppObjectRegistry.get('app'); if (typeof app?.onUninstall !== 'function') { diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnUpdate.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnUpdate.ts index 2f27247d8af88..8a71e39dc3692 100644 --- a/packages/apps-engine/deno-runtime/handlers/app/handleOnUpdate.ts +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnUpdate.ts @@ -2,8 +2,10 @@ import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; -export default async function handleOnUpdate(params: unknown): Promise { +export default async function handleOnUpdate(request: RequestContext): Promise { + const { params } = request; const app = AppObjectRegistry.get('app'); if (typeof app?.onUpdate !== 'function') { diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleSetStatus.ts b/packages/apps-engine/deno-runtime/handlers/app/handleSetStatus.ts index d95f4f17e1c7a..9c7f4650cf8d1 100644 --- a/packages/apps-engine/deno-runtime/handlers/app/handleSetStatus.ts +++ b/packages/apps-engine/deno-runtime/handlers/app/handleSetStatus.ts @@ -3,12 +3,15 @@ import type { AppStatus as _AppStatus } from '@rocket.chat/apps-engine/definitio import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import { require } from '../../lib/require.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; const { AppStatus } = require('@rocket.chat/apps-engine/definition/AppStatus.js') as { AppStatus: typeof _AppStatus; }; -export default async function handleSetStatus(params: unknown): Promise { +export default async function handleSetStatus(request: RequestContext): Promise { + const { params } = request; + if (!Array.isArray(params) || !Object.values(AppStatus).includes(params[0])) { throw new Error('Invalid params', { cause: 'invalid_param_type' }); } diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleUploadEvents.ts b/packages/apps-engine/deno-runtime/handlers/app/handleUploadEvents.ts index bb39c75e3d236..42d83320d0841 100644 --- a/packages/apps-engine/deno-runtime/handlers/app/handleUploadEvents.ts +++ b/packages/apps-engine/deno-runtime/handlers/app/handleUploadEvents.ts @@ -10,6 +10,7 @@ import { Defined, JsonRpcError } from 'jsonrpc-lite'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import { assertAppAvailable, assertHandlerFunction, isRecord } from '../lib/assertions.ts'; import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; export const uploadEvents = ['executePreFileUpload'] as const; @@ -25,13 +26,16 @@ function assertString(v: unknown): asserts v is string { throw JsonRpcError.invalidParams({ err: `Invalid 'path' parameter. Expected string, got`, value: v }); } -export default async function handleUploadEvents(method: typeof uploadEvents[number], params: unknown): Promise { - const [{ file, path }] = params as [{ file?: IUpload, path?: string }]; - - const app = AppObjectRegistry.get('app'); - const handlerFunction = app?.[method as keyof App] as unknown; +export default async function handleUploadEvents(request: RequestContext): Promise { + const { method: rawMethod, params } = request as { method: `app:${typeof uploadEvents[number]}`; params: [{ file?: IUploadDetails, path?: string }]}; + const [, method] = rawMethod.split(':') as ['app', typeof uploadEvents[number]]; try { + const [{ file, path }] = params; + + const app = AppObjectRegistry.get('app'); + const handlerFunction = app?.[method as keyof App] as unknown; + assertAppAvailable(app); assertHandlerFunction(handlerFunction); assertIsUpload(file); diff --git a/packages/apps-engine/deno-runtime/handlers/app/handler.ts b/packages/apps-engine/deno-runtime/handlers/app/handler.ts index cfb2df08cfb69..8bebe15dd3360 100644 --- a/packages/apps-engine/deno-runtime/handlers/app/handler.ts +++ b/packages/apps-engine/deno-runtime/handlers/app/handler.ts @@ -11,14 +11,16 @@ import handleOnDisable from './handleOnDisable.ts'; import handleOnUninstall from './handleOnUninstall.ts'; import handleOnPreSettingUpdate from './handleOnPreSettingUpdate.ts'; import handleOnSettingUpdated from './handleOnSettingUpdated.ts'; -import handleListener from '../listener/handler.ts'; -import handleUIKitInteraction, { uikitInteractions } from '../uikit/handler.ts'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import handleOnUpdate from './handleOnUpdate.ts'; import handleUploadEvents, { uploadEvents } from './handleUploadEvents.ts'; +import handleListener from '../listener/handler.ts'; +import handleUIKitInteraction, { uikitInteractions } from '../uikit/handler.ts'; import { isOneOf } from '../lib/assertions.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; -export default async function handleApp(method: string, params: unknown): Promise { +export default async function handleApp(request: RequestContext): Promise { + const { method } = request; const [, appMethod] = method.split(':'); try { @@ -52,49 +54,49 @@ export default async function handleApp(method: string, params: unknown): Promis }; if (app && isOneOf(appMethod, uploadEvents)) { - return handleUploadEvents(appMethod, params).then(formatResult); + return handleUploadEvents(request).then(formatResult); } if (app && isOneOf(appMethod, uikitInteractions)) { - return handleUIKitInteraction(appMethod, params).then(formatResult); + return handleUIKitInteraction(request).then(formatResult); } if (app && (appMethod.startsWith('check') || appMethod.startsWith('execute'))) { - return handleListener(appMethod, params).then(formatResult); + return handleListener(request).then(formatResult); } let result: Defined | JsonRpcError; switch (appMethod) { case 'construct': - result = await handleConstructApp(params); + result = await handleConstructApp(request); break; case 'initialize': - result = await handleInitialize(); + result = await handleInitialize(request); break; case 'setStatus': - result = await handleSetStatus(params); + result = await handleSetStatus(request); break; case 'onEnable': - result = await handleOnEnable(); + result = await handleOnEnable(request); break; case 'onDisable': - result = await handleOnDisable(); + result = await handleOnDisable(request); break; case 'onInstall': - result = await handleOnInstall(params); + result = await handleOnInstall(request); break; case 'onUninstall': - result = await handleOnUninstall(params); + result = await handleOnUninstall(request); break; case 'onPreSettingUpdate': - result = await handleOnPreSettingUpdate(params); + result = await handleOnPreSettingUpdate(request); break; case 'onSettingUpdated': - result = await handleOnSettingUpdated(params); + result = await handleOnSettingUpdated(request); break; case 'onUpdate': - result = await handleOnUpdate(params); + result = await handleOnUpdate(request); break; default: throw new JsonRpcError('Method not found', -32601); diff --git a/packages/apps-engine/deno-runtime/handlers/lib/assertions.ts b/packages/apps-engine/deno-runtime/handlers/lib/assertions.ts index c154015d24b02..046f2c575463d 100644 --- a/packages/apps-engine/deno-runtime/handlers/lib/assertions.ts +++ b/packages/apps-engine/deno-runtime/handlers/lib/assertions.ts @@ -1,6 +1,16 @@ import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; import { JsonRpcError } from 'jsonrpc-lite'; +/** + * Known failures that can happen in the runtime. + * + * DRT = Deno RunTime + */ +export const Errors = { + DRT_APP_NOT_AVAILABLE: 'DRT_APP_NOT_AVAILABLE', + DRT_EVENT_HANDLER_FUNCTION_MISSING: 'DRT_EVENT_HANDLER_FUNCTION_MISSING', +} + export function isRecord(v: unknown): v is Record { if (!v || typeof v !== 'object') { return false; @@ -22,12 +32,12 @@ export function isOneOf(value: unknown, array: readonly T[]): value is T { export function assertAppAvailable(v: unknown): asserts v is App { if (v && typeof (v as App)['extendConfiguration'] === 'function') return; - throw JsonRpcError.internalError({ err: 'App object not available' }); + throw JsonRpcError.internalError({ err: 'App object not available', code: Errors.DRT_APP_NOT_AVAILABLE }); } // deno-lint-ignore ban-types -- Function is the best we can do at this time export function assertHandlerFunction(v: unknown): asserts v is Function { if (v instanceof Function) return; - throw JsonRpcError.internalError({ err: `Expected handler function, got ${v}` }); + throw JsonRpcError.internalError({ err: `Expected handler function, got ${v}`, code: Errors.DRT_EVENT_HANDLER_FUNCTION_MISSING }); } diff --git a/packages/apps-engine/deno-runtime/handlers/listener/handler.ts b/packages/apps-engine/deno-runtime/handlers/listener/handler.ts index df6fc81a188db..f0d43104d8f8e 100644 --- a/packages/apps-engine/deno-runtime/handlers/listener/handler.ts +++ b/packages/apps-engine/deno-runtime/handlers/listener/handler.ts @@ -1,8 +1,8 @@ -import { Defined, JsonRpcError } from 'jsonrpc-lite'; import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; import type { AppsEngineException as _AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions/AppsEngineException.ts'; +import { Defined, JsonRpcError } from 'jsonrpc-lite'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import { MessageExtender } from '../../lib/accessors/extenders/MessageExtender.ts'; @@ -13,12 +13,15 @@ import { AppAccessors, AppAccessorsInstance } from '../../lib/accessors/mod.ts'; import { require } from '../../lib/require.ts'; import createRoom from '../../lib/roomFactory.ts'; import { Room } from '../../lib/room.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; const { AppsEngineException } = require('@rocket.chat/apps-engine/definition/exceptions/AppsEngineException.js') as { AppsEngineException: typeof _AppsEngineException; }; -export default async function handleListener(evtInterface: string, params: unknown): Promise { +export default async function handleListener(request: RequestContext): Promise { + const { method, params } = request; + const [, evtInterface] = method.split(':'); const app = AppObjectRegistry.get('app'); const eventExecutor = app?.[evtInterface as keyof App]; diff --git a/packages/apps-engine/deno-runtime/handlers/outboundcomms-handler.ts b/packages/apps-engine/deno-runtime/handlers/outboundcomms-handler.ts index b701eb25ca679..573f87d376d20 100644 --- a/packages/apps-engine/deno-runtime/handlers/outboundcomms-handler.ts +++ b/packages/apps-engine/deno-runtime/handlers/outboundcomms-handler.ts @@ -1,12 +1,15 @@ -import { JsonRpcError, Defined } from 'jsonrpc-lite'; import type { IOutboundMessageProviders } from '@rocket.chat/apps-engine/definition/outboundCommunication/IOutboundCommsProvider.ts'; +import { JsonRpcError, Defined } from 'jsonrpc-lite'; import { AppObjectRegistry } from '../AppObjectRegistry.ts'; import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; import { Logger } from '../lib/logger.ts'; +import { RequestContext } from '../lib/requestContext.ts'; -export default async function outboundMessageHandler(call: string, params: unknown): Promise { +export default async function outboundMessageHandler(request: RequestContext): Promise { + const { method: call, params } = request; const [, providerName, methodName] = call.split(':'); + const provider = AppObjectRegistry.get(`outboundCommunication:${providerName}`); if (!provider) { return new JsonRpcError('error-invalid-provider', -32000); diff --git a/packages/apps-engine/deno-runtime/handlers/scheduler-handler.ts b/packages/apps-engine/deno-runtime/handlers/scheduler-handler.ts index 30d69ed7046e0..8a9cb359a6883 100644 --- a/packages/apps-engine/deno-runtime/handlers/scheduler-handler.ts +++ b/packages/apps-engine/deno-runtime/handlers/scheduler-handler.ts @@ -4,8 +4,10 @@ import type { IProcessor } from '@rocket.chat/apps-engine/definition/scheduler/I import { AppObjectRegistry } from '../AppObjectRegistry.ts'; import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; +import { RequestContext } from '../lib/requestContext.ts'; -export default async function handleScheduler(method: string, params: unknown): Promise { +export default async function handleScheduler(request: RequestContext): Promise { + const { method, params } = request; const [, processorId] = method.split(':'); if (!Array.isArray(params)) { return JsonRpcError.invalidParams({ message: 'Invalid params' }); diff --git a/packages/apps-engine/deno-runtime/handlers/slashcommand-handler.ts b/packages/apps-engine/deno-runtime/handlers/slashcommand-handler.ts index c317f891bebf4..9fa262b16cf7c 100644 --- a/packages/apps-engine/deno-runtime/handlers/slashcommand-handler.ts +++ b/packages/apps-engine/deno-runtime/handlers/slashcommand-handler.ts @@ -1,22 +1,23 @@ -import { Defined, JsonRpcError } from 'jsonrpc-lite'; - import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands/ISlashCommand.ts'; import type { SlashCommandContext as _SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands/SlashCommandContext.ts'; import type { Room as _Room } from '@rocket.chat/apps-engine/server/rooms/Room.ts'; +import { Defined, JsonRpcError } from 'jsonrpc-lite'; import { AppObjectRegistry } from '../AppObjectRegistry.ts'; import { AppAccessors, AppAccessorsInstance } from '../lib/accessors/mod.ts'; import { require } from '../lib/require.ts'; import createRoom from '../lib/roomFactory.ts'; +import { RequestContext } from '../lib/requestContext.ts'; // For some reason Deno couldn't understand the typecast to the original interfaces and said it wasn't a constructor type const { SlashCommandContext } = require('@rocket.chat/apps-engine/definition/slashcommands/SlashCommandContext.js') as { SlashCommandContext: typeof _SlashCommandContext; }; -export default async function slashCommandHandler(call: string, params: unknown): Promise { +export default async function slashCommandHandler(request: RequestContext): Promise { + const { method: call, params } = request; const [, commandName, method] = call.split(':'); const command = AppObjectRegistry.get(`slashcommand:${commandName}`); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/api-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/api-handler.test.ts index 0212fcea3a541..e0a50670a66da 100644 --- a/packages/apps-engine/deno-runtime/handlers/tests/api-handler.test.ts +++ b/packages/apps-engine/deno-runtime/handlers/tests/api-handler.test.ts @@ -1,12 +1,12 @@ // deno-lint-ignore-file no-explicit-any +import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api/IApiEndpoint.ts'; import { assertEquals, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod.ts'; import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; +import { assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/assert_instance_of.ts'; +import jsonrpc, { JsonRpcError } from 'jsonrpc-lite'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/assert_instance_of.ts'; -import { JsonRpcError } from 'jsonrpc-lite'; -import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api/IApiEndpoint.ts'; import apiHandler from '../api-handler.ts'; describe('handlers > api', () => { @@ -30,7 +30,7 @@ describe('handlers > api', () => { it('correctly handles execution of an api endpoint method GET', async () => { const _spy = spy(mockEndpoint, 'get'); - const result = await apiHandler('api:/test:get', ['request', 'endpointInfo']); + const result = await apiHandler(jsonrpc.request(1, 'api:/test:get', ['request', 'endpointInfo'])); assertEquals(result, 'ok'); assertEquals(_spy.calls[0].args.length, 6); @@ -41,7 +41,7 @@ describe('handlers > api', () => { it('correctly handles execution of an api endpoint method POST', async () => { const _spy = spy(mockEndpoint, 'post'); - const result = await apiHandler('api:/test:post', ['request', 'endpointInfo']); + const result = await apiHandler(jsonrpc.request(1, 'api:/test:post', ['request', 'endpointInfo'])); assertEquals(result, 'ok'); assertEquals(_spy.calls[0].args.length, 6); @@ -50,7 +50,7 @@ describe('handlers > api', () => { }); it('correctly handles an error if the method not exists for the selected endpoint', async () => { - const result = await apiHandler(`api:/test:delete`, ['request', 'endpointInfo']); + const result = await apiHandler(jsonrpc.request(1, `api:/test:delete`, ['request', 'endpointInfo'])); assertInstanceOf(result, JsonRpcError); assertObjectMatch(result, { @@ -60,7 +60,7 @@ describe('handlers > api', () => { }); it('correctly handles an error if endpoint not exists', async () => { - const result = await apiHandler(`api:/error:get`, ['request', 'endpointInfo']); + const result = await apiHandler(jsonrpc.request(1, `api:/error:get`, ['request', 'endpointInfo'])); assertInstanceOf(result, JsonRpcError); assertObjectMatch(result, { @@ -70,7 +70,7 @@ describe('handlers > api', () => { }); it('correctly handles an error if the method execution fails', async () => { - const result = await apiHandler(`api:/test:put`, ['request', 'endpointInfo']); + const result = await apiHandler(jsonrpc.request(1, `api:/test:put`, ['request', 'endpointInfo'])); assertInstanceOf(result, JsonRpcError); assertObjectMatch(result, { diff --git a/packages/apps-engine/deno-runtime/handlers/tests/scheduler-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/scheduler-handler.test.ts index a18e7cdeb0309..58c2e703a2961 100644 --- a/packages/apps-engine/deno-runtime/handlers/tests/scheduler-handler.test.ts +++ b/packages/apps-engine/deno-runtime/handlers/tests/scheduler-handler.test.ts @@ -1,5 +1,6 @@ import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod.ts'; import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import jsonrpc from 'jsonrpc-lite'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import { AppAccessors } from '../../lib/accessors/mod.ts'; @@ -39,7 +40,7 @@ describe('handlers > scheduler', () => { }); it('correctly executes a request to a processor', async () => { - const result = await handleScheduler('scheduler:mockId', [{}]); + const result = await handleScheduler(jsonrpc.request(1, 'scheduler:mockId', [{}])); assertEquals(result, null); }); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/uikit-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/uikit-handler.test.ts index 066cdc5ee13a1..b663bd2ae6833 100644 --- a/packages/apps-engine/deno-runtime/handlers/tests/uikit-handler.test.ts +++ b/packages/apps-engine/deno-runtime/handlers/tests/uikit-handler.test.ts @@ -1,6 +1,7 @@ // deno-lint-ignore-file no-explicit-any import { assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod.ts'; import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import jsonrpc from 'jsonrpc-lite'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import handleUIKitInteraction, { @@ -30,7 +31,7 @@ describe('handlers > uikit', () => { }); it('successfully handles a call for "executeBlockActionHandler"', async () => { - const result = await handleUIKitInteraction('executeBlockActionHandler', [ + const request = jsonrpc.request(1, 'app:executeBlockActionHandler', [ { actionId: 'actionId', blockId: 'blockId', @@ -39,11 +40,12 @@ describe('handlers > uikit', () => { }, ]); + const result = await handleUIKitInteraction(request); assertInstanceOf(result, UIKitBlockInteractionContext); }); it('successfully handles a call for "executeViewSubmitHandler"', async () => { - const result = await handleUIKitInteraction('executeViewSubmitHandler', [ + const request = jsonrpc.request(1, 'app:executeViewSubmitHandler', [ { viewId: 'viewId', appId: 'appId', @@ -53,11 +55,12 @@ describe('handlers > uikit', () => { }, ]); + const result = await handleUIKitInteraction(request); assertInstanceOf(result, UIKitViewSubmitInteractionContext); }); it('successfully handles a call for "executeViewClosedHandler"', async () => { - const result = await handleUIKitInteraction('executeViewClosedHandler', [ + const request = jsonrpc.request(1, 'app:executeViewClosedHandler', [ { viewId: 'viewId', appId: 'appId', @@ -66,11 +69,12 @@ describe('handlers > uikit', () => { }, ]); + const result = await handleUIKitInteraction(request); assertInstanceOf(result, UIKitViewCloseInteractionContext); }); it('successfully handles a call for "executeActionButtonHandler"', async () => { - const result = await handleUIKitInteraction('executeActionButtonHandler', [ + const request = jsonrpc.request(1, 'app:executeActionButtonHandler', [ { actionId: 'actionId', appId: 'appId', @@ -79,11 +83,12 @@ describe('handlers > uikit', () => { }, ]); + const result = await handleUIKitInteraction(request); assertInstanceOf(result, UIKitActionButtonInteractionContext); }); it('successfully handles a call for "executeLivechatBlockActionHandler"', async () => { - const result = await handleUIKitInteraction('executeLivechatBlockActionHandler', [ + const request = jsonrpc.request(1, 'app:executeLivechatBlockActionHandler', [ { actionId: 'actionId', appId: 'appId', @@ -94,6 +99,7 @@ describe('handlers > uikit', () => { }, ]); + const result = await handleUIKitInteraction(request); assertInstanceOf(result, UIKitLivechatBlockInteractionContext); }); }); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/upload-event-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/upload-event-handler.test.ts new file mode 100644 index 0000000000000..db8aec4d3c34c --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/tests/upload-event-handler.test.ts @@ -0,0 +1,106 @@ +// deno-lint-ignore-file no-explicit-any +import { Buffer } from 'node:buffer'; + +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; +import type { IPreFileUpload } from '@rocket.chat/apps-engine/definition/uploads/IPreFileUpload.ts'; +import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails.ts'; +import { assertInstanceOf, assertNotInstanceOf, assertEquals, assertStringIncludes } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { afterEach, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { assertSpyCalls, spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; +import jsonrpc, { JsonRpcError } from 'jsonrpc-lite'; + +import handleUploadEvents from '../app/handleUploadEvents.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { Errors } from '../lib/assertions.ts'; + +describe('handlers > upload', () => { + let app: App & IPreFileUpload; + let path: string; + let file: IUploadDetails; + + beforeEach(async () => { + AppObjectRegistry.clear(); + + path = await Deno.makeTempFile(); + + app = { + extendConfiguration: () => {}, + executePreFileUpload: () => Promise.resolve(), + } as unknown as App; + + AppObjectRegistry.set('app', app); + + const content = 'Temp file for testing'; + + await Deno.writeTextFile(path, content); + + file = { + name: 'TempFile.txt', + size: content.length, + type: 'text/plain', + rid: 'RandomRoomId', + userId: 'RandomUserId', + }; + }); + + afterEach(async () => { + await Deno.remove(path).catch((e) => e?.code !== 'ENOENT' && console.warn(`Failed to remove temp file at ${path}`, e)); + }); + + it('correctly handles valid parameters', async () => { + const result = await handleUploadEvents(jsonrpc.request(1, 'app:executePreFileUpload', [{ file, path }])); + + assertNotInstanceOf(result, JsonRpcError, 'result is JsonRpcError'); + }); + + it('correctly loads the file contents for IPreFileUpload', async () => { + const _spy = spy(app as any, 'executePreFileUpload'); + + const result = await handleUploadEvents(jsonrpc.request(1, 'app:executePreFileUpload', [{ file, path }])); + + assertNotInstanceOf(result, JsonRpcError, 'result is JsonRpcError'); + assertSpyCalls(_spy, 1); + assertInstanceOf((_spy.calls[0].args[0] as any)?.content, Buffer); + }); + + it('fails when app object is not on registry', async () => { + AppObjectRegistry.clear(); + + const result = await handleUploadEvents(jsonrpc.request(1, 'app:executePreFileUpload', [{ file, path }])); + + assertInstanceOf(result, JsonRpcError); + assertEquals(result.data.code, Errors.DRT_APP_NOT_AVAILABLE); + }); + + it('fails when the app does not implement the IPreFileUpload event handler', async () => { + delete (app as any)['executePreFileUpload']; + + const result = await handleUploadEvents(jsonrpc.request(1, 'app:executePreFileUpload', [{ file, path }])); + + assertInstanceOf(result, JsonRpcError); + assertEquals(result.data.code, Errors.DRT_EVENT_HANDLER_FUNCTION_MISSING); + }); + + it('fails when "file" is not a proper IUploadDetails object', async () => { + const result = await handleUploadEvents(jsonrpc.request(1, 'app:executePreFileUpload', [{ file: { nope: "bad" }, path }])); + + assertInstanceOf(result, JsonRpcError); + assertStringIncludes(result.data.err, 'Expected IUploadDetails'); + }); + + it('fails when "path" is not a proper string', async () => { + const result = await handleUploadEvents(jsonrpc.request(1, 'app:executePreFileUpload', [{ file, path: {} }])); + + assertInstanceOf(result, JsonRpcError); + assertStringIncludes(result.data.err, 'Expected string'); + }); + + it('fails when "path" is not a readable file path', async () => { + await Deno.remove(path); + + const result = await handleUploadEvents(jsonrpc.request(1, 'app:executePreFileUpload', [{ file, path }])); + + assertInstanceOf(result, JsonRpcError); + assertEquals(result.data.code, "ENOENT"); + }); +}); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/videoconference-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/videoconference-handler.test.ts index 01f545ce6853e..55980ec2bda16 100644 --- a/packages/apps-engine/deno-runtime/handlers/tests/videoconference-handler.test.ts +++ b/packages/apps-engine/deno-runtime/handlers/tests/videoconference-handler.test.ts @@ -6,7 +6,7 @@ import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import videoconfHandler from '../videoconference-handler.ts'; import { assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/assert_instance_of.ts'; -import { JsonRpcError } from 'jsonrpc-lite'; +import jsonrpc, { JsonRpcError } from 'jsonrpc-lite'; describe('handlers > videoconference', () => { // deno-lint-ignore no-unused-vars @@ -37,7 +37,7 @@ describe('handlers > videoconference', () => { it('correctly handles execution of a videoconf method without additional params', async () => { const _spy = spy(mockProvider, 'empty'); - const result = await videoconfHandler('videoconference:test-provider:empty', []); + const result = await videoconfHandler(jsonrpc.request(1, 'videoconference:test-provider:empty', [])); assertEquals(result, 'ok none'); assertEquals(_spy.calls[0].args.length, 4); @@ -48,7 +48,7 @@ describe('handlers > videoconference', () => { it('correctly handles execution of a videoconf method with one param', async () => { const _spy = spy(mockProvider, 'one'); - const result = await videoconfHandler('videoconference:test-provider:one', ['call']); + const result = await videoconfHandler(jsonrpc.request(1, 'videoconference:test-provider:one', ['call'])); assertEquals(result, 'ok one'); assertEquals(_spy.calls[0].args.length, 5); @@ -60,7 +60,7 @@ describe('handlers > videoconference', () => { it('correctly handles execution of a videoconf method with two params', async () => { const _spy = spy(mockProvider, 'two'); - const result = await videoconfHandler('videoconference:test-provider:two', ['call', 'user']); + const result = await videoconfHandler(jsonrpc.request(1, 'videoconference:test-provider:two', ['call', 'user'])); assertEquals(result, 'ok two'); assertEquals(_spy.calls[0].args.length, 6); @@ -73,7 +73,7 @@ describe('handlers > videoconference', () => { it('correctly handles execution of a videoconf method with three params', async () => { const _spy = spy(mockProvider, 'three'); - const result = await videoconfHandler('videoconference:test-provider:three', ['call', 'user', 'options']); + const result = await videoconfHandler(jsonrpc.request(1, 'videoconference:test-provider:three', ['call', 'user', 'options'])); assertEquals(result, 'ok three'); assertEquals(_spy.calls[0].args.length, 7); @@ -85,7 +85,7 @@ describe('handlers > videoconference', () => { }); it('correctly handles an error on execution of a videoconf method', async () => { - const result = await videoconfHandler('videoconference:test-provider:error', []); + const result = await videoconfHandler(jsonrpc.request(1, 'videoconference:test-provider:error', [])); assertInstanceOf(result, JsonRpcError); assertObjectMatch(result, { @@ -96,7 +96,7 @@ describe('handlers > videoconference', () => { it('correctly handles an error when provider is not found', async () => { const providerName = 'error-provider'; - const result = await videoconfHandler(`videoconference:${providerName}:method`, []); + const result = await videoconfHandler(jsonrpc.request(1, `videoconference:${providerName}:method`, [])); assertInstanceOf(result, JsonRpcError); assertObjectMatch(result, { @@ -108,7 +108,7 @@ describe('handlers > videoconference', () => { it('correctly handles an error if method is not a function of provider', async () => { const methodName = 'notAFunction'; const providerName = 'test-provider'; - const result = await videoconfHandler(`videoconference:${providerName}:${methodName}`, []); + const result = await videoconfHandler(jsonrpc.request(1, `videoconference:${providerName}:${methodName}`, [])); assertInstanceOf(result, JsonRpcError); assertObjectMatch(result, { diff --git a/packages/apps-engine/deno-runtime/handlers/uikit/handler.ts b/packages/apps-engine/deno-runtime/handlers/uikit/handler.ts index 25e08a85e648b..c79fea6fe763c 100644 --- a/packages/apps-engine/deno-runtime/handlers/uikit/handler.ts +++ b/packages/apps-engine/deno-runtime/handlers/uikit/handler.ts @@ -1,9 +1,10 @@ -import { Defined, JsonRpcError } from 'jsonrpc-lite'; import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; +import { Defined, JsonRpcError } from 'jsonrpc-lite'; import { require } from '../../lib/require.ts'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { RequestContext } from '../../lib/requestContext.ts'; export const uikitInteractions = [ 'executeBlockActionHandler', @@ -22,7 +23,10 @@ export const { export const { UIKitLivechatBlockInteractionContext } = require('@rocket.chat/apps-engine/definition/uikit/livechat/UIKitLivechatInteractionContext.js'); -export default async function handleUIKitInteraction(method: string, params: unknown): Promise { +export default async function handleUIKitInteraction(request: RequestContext): Promise { + const { method: reqMethod, params } = request; + const [, method] = reqMethod.split(':'); + if (!uikitInteractions.includes(method)) { return JsonRpcError.methodNotFound(null); } diff --git a/packages/apps-engine/deno-runtime/handlers/videoconference-handler.ts b/packages/apps-engine/deno-runtime/handlers/videoconference-handler.ts index dc897ee337b22..c6caf75f10c10 100644 --- a/packages/apps-engine/deno-runtime/handlers/videoconference-handler.ts +++ b/packages/apps-engine/deno-runtime/handlers/videoconference-handler.ts @@ -1,11 +1,13 @@ -import { Defined, JsonRpcError } from 'jsonrpc-lite'; import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders/IVideoConfProvider.ts'; +import { Defined, JsonRpcError } from 'jsonrpc-lite'; import { AppObjectRegistry } from '../AppObjectRegistry.ts'; import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; import { Logger } from '../lib/logger.ts'; +import { RequestContext } from '../lib/requestContext.ts'; -export default async function videoConferenceHandler(call: string, params: unknown): Promise { +export default async function videoConferenceHandler(request: RequestContext): Promise { + const { method: call, params } = request; const [, providerName, methodName] = call.split(':'); const provider = AppObjectRegistry.get(`videoConfProvider:${providerName}`); diff --git a/packages/apps-engine/deno-runtime/lib/requestContext.ts b/packages/apps-engine/deno-runtime/lib/requestContext.ts new file mode 100644 index 0000000000000..91e9346f34bd4 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/requestContext.ts @@ -0,0 +1,10 @@ +import { RequestObject } from 'jsonrpc-lite'; + +import { Logger } from './logger.ts'; + +export type RequestContext = RequestObject & { + context: { + logger: Logger; + [key: string]: unknown; + } +}; diff --git a/packages/apps-engine/deno-runtime/main.ts b/packages/apps-engine/deno-runtime/main.ts index 25a8228066cc4..3f49e15809109 100644 --- a/packages/apps-engine/deno-runtime/main.ts +++ b/packages/apps-engine/deno-runtime/main.ts @@ -8,8 +8,8 @@ if (!Deno.args.includes('--subprocess')) { Deno.exit(1001); } -import { JsonRpcError } from 'jsonrpc-lite'; import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; +import { JsonRpcError } from 'jsonrpc-lite'; import * as Messenger from './lib/messenger.ts'; import { decoder } from './lib/codec.ts'; @@ -24,6 +24,7 @@ import handleScheduler from './handlers/scheduler-handler.ts'; import registerErrorListeners from './error-handlers.ts'; import { sendMetrics } from './lib/metricsCollector.ts'; import outboundMessageHandler from './handlers/outboundcomms-handler.ts'; +import { RequestContext } from './lib/requestContext.ts'; type Handlers = { app: typeof handleApp; @@ -32,7 +33,7 @@ type Handlers = { videoconference: typeof videoConferenceHandler; outboundCommunication: typeof outboundMessageHandler; scheduler: typeof handleScheduler; - ping: (method: string, params: unknown) => 'pong'; + ping: (request: RequestContext) => 'pong'; }; const COMMAND_PING = '_zPING'; @@ -45,7 +46,7 @@ async function requestRouter({ type, payload }: Messenger.JsonRpcRequest): Promi videoconference: videoConferenceHandler, outboundCommunication: outboundMessageHandler, scheduler: handleScheduler, - ping: (_method, _params) => 'pong', + ping: (_request) => 'pong', }; // We're not handling notifications at the moment @@ -53,11 +54,15 @@ async function requestRouter({ type, payload }: Messenger.JsonRpcRequest): Promi return Messenger.sendInvalidRequestError(); } - const { id, method, params } = payload; + const { id, method } = payload; const logger = new Logger(method); AppObjectRegistry.set('logger', logger); + const context: RequestContext = Object.assign(payload, { + context: { logger } + }) + const app = AppObjectRegistry.get('app'); if (app) { @@ -75,7 +80,7 @@ async function requestRouter({ type, payload }: Messenger.JsonRpcRequest): Promi }); } - const result = await handler(method, params); + const result = await handler(context); if (result instanceof JsonRpcError) { return Messenger.errorResponse({ id, error: result });