Skip to content
This repository was archived by the owner on Nov 5, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions deno-runtime/AppObjectRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const AppObjectRegistry = new class {
registry: Record<string, unknown> = {};

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 = {};
}
}

33 changes: 33 additions & 0 deletions deno-runtime/lib/accessors/_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,6 +17,7 @@ describe('AppAccessors', () => {

beforeEach(() => {
appAccessors = new AppAccessors(proxify);
AppObjectRegistry.clear();
});

it('creates the correct format for IRead calls', async () => {
Expand Down Expand Up @@ -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,
}],
});
});
});
89 changes: 65 additions & 24 deletions deno-runtime/lib/accessors/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(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<T>(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;
Expand All @@ -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'),
};
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 7 additions & 4 deletions deno-runtime/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -53,13 +54,13 @@ function wrapAppCode(code: string): (require: (module: string) => unknown) => Pr
) as (require: (module: string) => unknown) => Promise<Record<string, unknown>>;
}

async function handlInitializeApp({ id, source }: { id: string; source: string }): Promise<Record<string, unknown>> {
async function handlInitializeApp({ id, source }: { id: string; source: string }): Promise<void> {
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');
Expand All @@ -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<void> {
Expand All @@ -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;
}
Expand Down