@rocket.chat/apps-engine
Version:
The engine code for the Rocket.Chat Apps which manages, runs, translates, coordinates and all of that.
151 lines (119 loc) • 6.02 kB
text/typescript
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 { AppObjectRegistry } from '../../AppObjectRegistry.ts';
import { MessageExtender } from '../../lib/accessors/extenders/MessageExtender.ts';
import { RoomExtender } from '../../lib/accessors/extenders/RoomExtender.ts';
import { MessageBuilder } from '../../lib/accessors/builders/MessageBuilder.ts';
import { RoomBuilder } from '../../lib/accessors/builders/RoomBuilder.ts';
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";
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<Defined | JsonRpcError> {
const app = AppObjectRegistry.get<App>('app');
const eventExecutor = app?.[evtInterface as keyof App];
if (typeof eventExecutor !== 'function') {
return JsonRpcError.methodNotFound({
message: 'Invalid event interface called on app',
});
}
if (!Array.isArray(params) || params.length < 1 || params.length > 2) {
return JsonRpcError.invalidParams(null);
}
try {
const args = parseArgs({ AppAccessorsInstance }, evtInterface, params);
return await (eventExecutor as (...args: unknown[]) => Promise<Defined>).apply(app, args);
} catch (e) {
if (e instanceof JsonRpcError) {
return e;
}
if (e instanceof AppsEngineException) {
return new JsonRpcError(e.message, AppsEngineException.JSONRPC_ERROR_CODE, { name: e.name });
}
return JsonRpcError.internalError({ message: e.message });
}
}
export function parseArgs(deps: { AppAccessorsInstance: AppAccessors }, evtMethod: string, params: unknown[]): unknown[] {
const { AppAccessorsInstance } = deps;
/**
* param1 is the context for the event handler execution
* param2 is an optional extra content that some hanlers require
*/
const [param1, param2] = params as [unknown, unknown];
if (!param1) {
throw JsonRpcError.invalidParams(null);
}
let context = param1;
if (evtMethod.includes('Message')) {
context = hydrateMessageObjects(context) as Record<string, unknown>;
} else if (evtMethod.endsWith('RoomUserJoined') || evtMethod.endsWith('RoomUserLeave')) {
(context as Record<string, unknown>).room = createRoom((context as Record<string, unknown>).room as IRoom, AppAccessorsInstance.getSenderFn());
} else if (evtMethod.includes('PreRoom')) {
context = createRoom(context as IRoom, AppAccessorsInstance.getSenderFn());
}
const args: unknown[] = [context, AppAccessorsInstance.getReader(), AppAccessorsInstance.getHttp()];
// "check" events will only go this far - (context, reader, http)
if (evtMethod.startsWith('check')) {
// "checkPostMessageDeleted" has an extra param - (context, reader, http, extraContext)
if (param2) {
args.push(hydrateMessageObjects(param2));
}
return args;
}
// From this point on, all events will require (reader, http, persistence) injected
args.push(AppAccessorsInstance.getPersistence());
// "extend" events have an additional "Extender" param - (context, extender, reader, http, persistence)
if (evtMethod.endsWith('Extend')) {
if (evtMethod.includes('Message')) {
args.splice(1, 0, new MessageExtender(param1 as IMessage));
} else if (evtMethod.includes('Room')) {
args.splice(1, 0, new RoomExtender(param1 as IRoom));
}
return args;
}
// "Modify" events have an additional "Builder" param - (context, builder, reader, http, persistence)
if (evtMethod.endsWith('Modify')) {
if (evtMethod.includes('Message')) {
args.splice(1, 0, new MessageBuilder(param1 as IMessage));
} else if (evtMethod.includes('Room')) {
args.splice(1, 0, new RoomBuilder(param1 as IRoom));
}
return args;
}
// From this point on, all events will require (reader, http, persistence, modifier) injected
args.push(AppAccessorsInstance.getModifier());
// This guy gets an extra one
if (evtMethod === 'executePostMessageDeleted') {
if (!param2) {
throw JsonRpcError.invalidParams(null);
}
args.push(hydrateMessageObjects(param2));
}
return args;
}
/**
* Hydrate the context object with the correct IMessage
*
* Some information is lost upon serializing the data from listeners through the pipes,
* so here we hydrate the complete object as necessary
*/
function hydrateMessageObjects(context: unknown): unknown {
if (objectIsRawMessage(context)) {
context.room = createRoom(context.room as IRoom, AppAccessorsInstance.getSenderFn());
} else if ((context as Record<string, unknown>)?.message) {
(context as Record<string, unknown>).message = hydrateMessageObjects((context as Record<string, unknown>).message);
}
return context;
}
function objectIsRawMessage(value: unknown): value is IMessage {
if (!value) return false;
const { id, room, sender, createdAt } = value as Record<string, unknown>;
// Check if we have the fields of a message and the room hasn't already been hydrated
return !!(id && room && sender && createdAt) && !(room instanceof Room);
}