Skip to content

Commit

Permalink
feat: videoconf handler (#691)
Browse files Browse the repository at this point in the history
Co-authored-by: Douglas Gubert <[email protected]>
  • Loading branch information
tapiarafael and d-gubert authored Dec 29, 2023
1 parent 4a7a913 commit f09427e
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 59 deletions.
117 changes: 117 additions & 0 deletions deno-runtime/handlers/tests/videoconference-handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// deno-lint-ignore-file no-explicit-any
import { assertEquals, assertObjectMatch } from 'https://deno.land/[email protected]/assert/mod.ts';
import { beforeEach, describe, it } from 'https://deno.land/[email protected]/testing/bdd.ts';
import { spy } from "https://deno.land/[email protected]/testing/mock.ts";

import { AppObjectRegistry } from '../../AppObjectRegistry.ts';
import videoconfHandler from '../videoconference-handler.ts';
import { assertInstanceOf } from "https://deno.land/[email protected]/assert/assert_instance_of.ts";
import { JsonRpcError } from "jsonrpc-lite";

describe('handlers > videoconference', () => {
// deno-lint-ignore no-unused-vars
const mockMethodWithoutParam = (read: any, modify: any, http: any, persis: any): Promise<string> => Promise.resolve('ok none');
// deno-lint-ignore no-unused-vars
const mockMethodWithOneParam = (call: any, read: any, modify: any, http: any, persis: any): Promise<string> => Promise.resolve('ok one');
// deno-lint-ignore no-unused-vars
const mockMethodWithTwoParam = (call: any, user: any, read: any, modify: any, http: any, persis: any): Promise<string> => Promise.resolve('ok two');
// deno-lint-ignore no-unused-vars
const mockMethodWithThreeParam = (call: any, user: any, options: any, read: any, modify: any, http: any, persis: any): Promise<string> => Promise.resolve('ok three');
const mockProvider = {
empty: mockMethodWithoutParam,
one: mockMethodWithOneParam,
two: mockMethodWithTwoParam,
three: mockMethodWithThreeParam,
notAFunction: true,
error: () => { throw new Error('Method execution error example') }
}

beforeEach(() => {
AppObjectRegistry.clear();
AppObjectRegistry.set('videoConfProvider:test-provider', mockProvider);
});

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', []);

assertEquals(result, 'ok none')
assertEquals(_spy.calls[0].args.length, 4);

_spy.restore();
});

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']);

assertEquals(result, 'ok one')
assertEquals(_spy.calls[0].args.length, 5);
assertEquals(_spy.calls[0].args[0], 'call');

_spy.restore();
});

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']);

assertEquals(result, 'ok two')
assertEquals(_spy.calls[0].args.length, 6);
assertEquals(_spy.calls[0].args[0], 'call');
assertEquals(_spy.calls[0].args[1], 'user');

_spy.restore();
});

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']);

assertEquals(result, 'ok three')
assertEquals(_spy.calls[0].args.length, 7);
assertEquals(_spy.calls[0].args[0], 'call');
assertEquals(_spy.calls[0].args[1], 'user');
assertEquals(_spy.calls[0].args[2], 'options');

_spy.restore();
});

it('correctly handles an error on execution of a videoconf method', async () => {
const result = await videoconfHandler('videoconference:test-provider:error', []);

assertInstanceOf(result, JsonRpcError)
assertObjectMatch(result, {
message: 'Method execution error example',
code: -32000
})
})

it('correctly handles an error when provider is not found', async () => {
const providerName = 'error-provider'
const result = await videoconfHandler(`videoconference:${providerName}:method`, []);

assertInstanceOf(result, JsonRpcError)
assertObjectMatch(result, {
message: `Provider ${providerName} not found`,
code: -32000
})
})

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}`, []);

assertInstanceOf(result, JsonRpcError)
assertObjectMatch(result, {
message: `Method ${methodName} not found on provider ${providerName}`,
code: -32000
})
})

});
50 changes: 50 additions & 0 deletions deno-runtime/handlers/videoconference-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Defined, JsonRpcError } from "jsonrpc-lite";
import { AppObjectRegistry } from "../AppObjectRegistry.ts";
import { IVideoConfProvider } from "@rocket.chat/apps-engine/definition/videoConfProviders/IVideoConfProvider.ts";
import { AppAccessorsInstance } from "../lib/accessors/mod.ts";
import { Logger } from "../lib/logger.ts";

export default async function videoConferenceHandler(call: string, params: unknown): Promise<JsonRpcError | Defined> {
const [, providerName, methodName] = call.split(':');

const provider = AppObjectRegistry.get<IVideoConfProvider>(`videoConfProvider:${providerName}`);
const logger = AppObjectRegistry.get<Logger>('logger');

if (!provider) {
return new JsonRpcError(`Provider ${providerName} not found`, -32000);
}

const method = provider[methodName as keyof IVideoConfProvider];

if (typeof method !== 'function') {
return new JsonRpcError(`Method ${methodName} not found on provider ${providerName}`, -32000);
}

const [videoconf, user, options] = params as Array<unknown>;

logger?.debug(`Executing ${methodName} on video conference provider...`);

const args = [
...(videoconf ? [videoconf] : []),
...(user ? [user] : []),
...(options ? [options] : []),
];

try {
// deno-lint-ignore ban-types
const result = await(method as Function).apply(provider, [
...args,
AppAccessorsInstance.getReader(),
AppAccessorsInstance.getModifier(),
AppAccessorsInstance.getHttp(),
AppAccessorsInstance.getPersistence()
]);

logger?.debug(`Video Conference Provider's ${methodName} was successfully executed.`);

return result;
} catch (e) {
logger?.debug(`Video Conference Provider's ${methodName} was unsuccessful.`);
return new JsonRpcError(e.message, -32000);
}
}
50 changes: 26 additions & 24 deletions deno-runtime/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,24 @@ import { AppObjectRegistry } from './AppObjectRegistry.ts';
import { Logger } from './lib/logger.ts';

import slashcommandHandler from './handlers/slashcommand-handler.ts';
import videoConferenceHandler from './handlers/videoconference-handler.ts';
import handleApp from './handlers/app/handler.ts';

AppObjectRegistry.set('MESSAGE_SEPARATOR', Deno.args.at(-1));

type Handlers = {
'app': typeof handleApp,
'slashcommand': typeof slashcommandHandler
'videoconference': typeof videoConferenceHandler
}

async function requestRouter({ type, payload }: Messenger.JsonRpcRequest): Promise<void> {
const methodHandlers: Handlers = {
'app': handleApp,
'slashcommand': slashcommandHandler,
'videoconference': videoConferenceHandler
}

// We're not handling notifications at the moment
if (type === 'notification') {
return Messenger.sendInvalidRequestError();
Expand All @@ -38,34 +51,23 @@ async function requestRouter({ type, payload }: Messenger.JsonRpcRequest): Promi
(app as unknown as Record<string, unknown>).logger = logger;
}

switch (true) {
case method.startsWith('app:'): {
const result = await handleApp(method, params);
const [methodPrefix] = method.split(':') as [keyof Handlers];
const handler = methodHandlers[methodPrefix]

if (result instanceof JsonRpcError) {
return Messenger.errorResponse({ id, error: result });
}

Messenger.successResponse({ id, result });
break;
}
case method.startsWith('slashcommand:'): {
const result = await slashcommandHandler(method, params);
if (!handler) {
return Messenger.errorResponse({
error: { message: 'Method not found', code: -32601 },
id,
});
}

if (result instanceof JsonRpcError) {
return Messenger.errorResponse({ id, error: result });
}
const result = await handler(method, params);

return Messenger.successResponse({ id, result });
}
default: {
Messenger.errorResponse({
error: { message: 'Method not found', code: -32601 },
id,
});
break;
}
if (result instanceof JsonRpcError) {
return Messenger.errorResponse({ id, error: result });
}

return Messenger.successResponse({ id, result });
}

function handleResponse(response: Messenger.JsonRpcResponse): void {
Expand Down
45 changes: 10 additions & 35 deletions src/server/managers/AppVideoConfProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { IBlock } from '../../definition/uikit';
import type { VideoConference } from '../../definition/videoConferences';
import type { IVideoConferenceUser } from '../../definition/videoConferences/IVideoConferenceUser';
import type { IVideoConferenceOptions, IVideoConfProvider, VideoConfData, VideoConfDataExtended } from '../../definition/videoConfProviders';
import { AppConsole } from '../logging';
import type { ProxiedApp } from '../ProxiedApp';
import type { AppLogStorage } from '../storage';
import type { AppAccessorManager } from './AppAccessorManager';
Expand Down Expand Up @@ -83,46 +82,22 @@ export class AppVideoConfProvider {
| AppMethod._VIDEOCONF_CHANGED
| AppMethod._VIDEOCONF_GET_INFO
| AppMethod._VIDEOCONF_USER_JOINED,
logStorage: AppLogStorage,
accessors: AppAccessorManager,
_logStorage: AppLogStorage,
_accessors: AppAccessorManager,
runContextArgs: Array<any>,
): Promise<string | boolean | Array<IBlock> | undefined> {
// Ensure the provider has the property before going on
if (typeof this.provider[method] !== 'function') {
return;
}

const runContext = {
provider: this.provider,
args: [
...runContextArgs,
accessors.getReader(this.app.getID()),
accessors.getModifier(this.app.getID()),
accessors.getHttp(this.app.getID()),
accessors.getPersistence(this.app.getID()),
],
};

const logger = this.app.setupLogger(method);
logger.debug(`Executing ${method} on video conference provider...`);
const provider = this.provider.name;

let result: string | undefined;
try {
const runCode = `module.exports = provider.${method}.apply(provider, args)`;
result = await this.app.getRuntime().runInSandbox(runCode, runContext);
logger.debug(`Video Conference Provider's ${method} was successfully executed.`);
} catch (e) {
logger.error(e);
logger.debug(`Video Conference Provider's ${method} was unsuccessful.`);
}
const result = await this.app.getDenoRuntime().sendRequest({
method: `videoconference.${provider}.${method}`,
params: [runContextArgs],
});

try {
await logStorage.storeEntries(AppConsole.toStorageEntry(this.app.getID(), logger));
return result as string;
} catch (e) {
// Don't care, at the moment.
// TODO: Evaluate to determine if we do care
// @TODO add error handling
console.log(e);
}

return result;
}
}

0 comments on commit f09427e

Please sign in to comment.