diff --git a/deno-runtime/AppObjectRegistry.ts b/deno-runtime/AppObjectRegistry.ts new file mode 100644 index 000000000..d75e78460 --- /dev/null +++ b/deno-runtime/AppObjectRegistry.ts @@ -0,0 +1,24 @@ +export const AppObjectRegistry = new class { + registry: Record = {}; + + public get(key: string): unknown { + return this.registry[key]; + } + + public set(key: string, value: unknown): void { + this.registry[key] = value; + } + + public has(key: string): boolean { + return key in this.registry; + } + + public delete(key: string): void { + delete this.registry[key]; + } + + public clear(): void { + this.registry = {}; + } +} + diff --git a/deno-runtime/lib/accessors/_test.ts b/deno-runtime/lib/accessors/_test.ts index 903cc99a4..0960bb7a1 100644 --- a/deno-runtime/lib/accessors/_test.ts +++ b/deno-runtime/lib/accessors/_test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/ import { assertEquals } from "https://deno.land/std@0.203.0/assert/assert_equals.ts"; import { AppAccessors, getProxify } from "./mod.ts"; +import { AppObjectRegistry } from "../../AppObjectRegistry.ts"; describe('AppAccessors', () => { let appAccessors: AppAccessors; @@ -16,6 +17,7 @@ describe('AppAccessors', () => { beforeEach(() => { appAccessors = new AppAccessors(proxify); + AppObjectRegistry.clear(); }); it('creates the correct format for IRead calls', async () => { @@ -77,4 +79,35 @@ describe('AppAccessors', () => { method: 'accessor:getConfigurationModify:slashCommands:modifySlashCommand', }); }); + + it('correctly stores a reference to a slashcommand object and sends a request via proxy', async () => { + const configExtend = appAccessors.getConfigurationExtend(); + + const slashcommand = { + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + executor() { + return Promise.resolve(); + } + }; + + const result = await configExtend.slashCommands.provideSlashCommand(slashcommand); + + assertEquals(AppObjectRegistry.get('slashcommand:test'), slashcommand); + + // The function will not be serialized and sent to the main process + delete result.result.params[0].executor; + + assertEquals(result.result, { + method: 'accessor:getConfigurationExtend:slashCommands:provideSlashCommand', + params: [{ + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + }], + }); + }); }); diff --git a/deno-runtime/lib/accessors/mod.ts b/deno-runtime/lib/accessors/mod.ts index 8f313a1a1..5c26aadcc 100644 --- a/deno-runtime/lib/accessors/mod.ts +++ b/deno-runtime/lib/accessors/mod.ts @@ -8,23 +8,29 @@ import type { IModify } from '@rocket.chat/apps-engine/definition/accessors/IMod import type { IPersistence } from '@rocket.chat/apps-engine/definition/accessors/IPersistence.ts'; import type { IHttp } from '@rocket.chat/apps-engine/definition/accessors/IHttp.ts'; import type { IConfigurationExtend } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationExtend.ts'; +import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands/ISlashCommand.ts'; +import type { IProcessor } from '@rocket.chat/apps-engine/definition/scheduler/IProcessor.ts'; +import type { IApi } from '@rocket.chat/apps-engine/definition/api/IApi.ts'; +import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders/IVideoConfProvider.ts'; import * as Messenger from '../messenger.ts'; - -export const getProxify = (call: typeof Messenger.sendRequest) => function proxify(namespace: string): T { - return new Proxy( - { __kind: namespace }, // debugging purposes - { - get: - (_target: unknown, prop: string) => - (...params: unknown[]) => - call({ - method: `accessor:${namespace}:${prop}`, - params, - }), - }, - ) as T; -} +import { AppObjectRegistry } from "../../AppObjectRegistry.ts"; + +export const getProxify = (call: typeof Messenger.sendRequest) => + function proxify(namespace: string): T { + return new Proxy( + { __kind: namespace }, // debugging purposes + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + call({ + method: `accessor:${namespace}:${prop}`, + params, + }), + }, + ) as T; + }; export class AppAccessors { private defaultAppAccessors?: IAppAccessors; @@ -43,8 +49,8 @@ export class AppAccessors { if (!this.environmentRead) { this.environmentRead = { getSettings: () => this.proxify('getEnvironmentRead:getSettings'), - getServerSettings: () => this.proxify('getEnvironmentRead:getServerSettings'), - getEnvironmentVariables: () => this.proxify('getEnvironmentRead:getEnvironmentVariables'), + getServerSettings: () => this.proxify('getEnvironmentRead:getServerSettings'), + getEnvironmentVariables: () => this.proxify('getEnvironmentRead:getEnvironmentVariables'), }; } @@ -74,18 +80,53 @@ export class AppAccessors { return this.configModifier; } - public getConifgurationExtend() { + public getConfigurationExtend() { if (!this.configExtender) { this.configExtender = { ui: this.proxify('getConfigurationExtend:ui'), - api: this.proxify('getConfigurationExtend:api'), http: this.proxify('getConfigurationExtend:http'), settings: this.proxify('getConfigurationExtend:settings'), - scheduler: this.proxify('getConfigurationExtend:scheduler'), - slashCommands: this.proxify('getConfigurationExtend:slashCommands'), externalComponents: this.proxify('getConfigurationExtend:externalComponents'), - videoConfProviders: this.proxify('getConfigurationExtend:videoConfProviders'), - } + api: { + _proxy: this.proxify('getConfigurationExtend:api'), + provideApi(api: IApi) { + api.endpoints.forEach((endpoint) => { + AppObjectRegistry.set(`api:${endpoint.path}`, endpoint); + }); + + return this._proxy.provideApi(api); + }, + }, + scheduler: { + _proxy: this.proxify('getConfigurationExtend:scheduler'), + registerProcessors(processors: IProcessor[]) { + // Store the processor instance to use when the Apps-Engine calls the processor + processors.forEach((processor) => { + AppObjectRegistry.set(`scheduler:${processor.id}`, processor); + }); + + return this._proxy.registerProcessors(processors); + }, + }, + videoConfProviders: { + _proxy: this.proxify('getConfigurationExtend:videoConfProviders'), + provideVideoConfProvider(provider: IVideoConfProvider) { + // Store the videoConfProvider instance to use when the Apps-Engine calls the videoConfProvider + AppObjectRegistry.set(`videoConfProvider:${provider.name}`, provider); + + return this._proxy.provideVideoConfProvider(provider); + }, + }, + slashCommands: { + _proxy: this.proxify('getConfigurationExtend:slashCommands'), + provideSlashCommand(slashcommand: ISlashCommand) { + // Store the slashcommand instance to use when the Apps-Engine calls the slashcommand + AppObjectRegistry.set(`slashcommand:${slashcommand.command}`, slashcommand); + + return this._proxy.provideSlashCommand(slashcommand); + } + } + }; } return this.configExtender; @@ -143,7 +184,7 @@ export class AppAccessors { getScheduler: () => this.proxify('getModifier:getScheduler'), getOAuthAppsModifier: () => this.proxify('getModifier:getOAuthAppsModifier'), getModerationModifier: () => this.proxify('getModifier:getModerationModifier'), - } + }; } return this.modifier; diff --git a/deno-runtime/main.ts b/deno-runtime/main.ts index e19a361e3..192be3276 100644 --- a/deno-runtime/main.ts +++ b/deno-runtime/main.ts @@ -12,6 +12,7 @@ import { createRequire } from 'node:module'; import { sanitizeDeprecatedUsage } from "./lib/sanitizeDeprecatedUsage.ts"; import { AppAccessorsInstance } from "./lib/accessors/mod.ts"; import * as Messenger from "./lib/messenger.ts"; +import { AppObjectRegistry } from "./AppObjectRegistry.ts"; const require = createRequire(import.meta.url); @@ -53,13 +54,13 @@ function wrapAppCode(code: string): (require: (module: string) => unknown) => Pr ) as (require: (module: string) => unknown) => Promise>; } -async function handlInitializeApp({ id, source }: { id: string; source: string }): Promise> { +async function handlInitializeApp({ id, source }: { id: string; source: string }): Promise { source = sanitizeDeprecatedUsage(source); const require = buildRequire(); const exports = await wrapAppCode(source)(require); // This is the same naive logic we've been using in the App Compiler const appClass = Object.values(exports)[0] as typeof App; - const app = new appClass({ author: {} }, proxify('logger'), AppAccessorsInstance.getDefaultAppAccessors()); + const app = new appClass({ author: {} }, console, AppAccessorsInstance.getDefaultAppAccessors()); if (typeof app.getName !== 'function') { throw new Error('App must contain a getName function'); @@ -85,7 +86,8 @@ async function handlInitializeApp({ id, source }: { id: string; source: string } throw new Error('App must contain a getRequiredApiVersion function'); } - return app; + AppObjectRegistry.set('app', app); + AppObjectRegistry.set('id', id); } async function handleRequest({ type, payload }: Messenger.JsonRpcRequest): Promise { @@ -104,7 +106,8 @@ async function handleRequest({ type, payload }: Messenger.JsonRpcRequest): Promi return Messenger.sendInvalidParamsError(id); } - const app = await handlInitializeApp({ id: appId, source }) + await handlInitializeApp({ id: appId, source }) + Messenger.successResponse({ id, result: 'hooray' }); break; }