From b574afbd08b2a064d45ad75f6e625a052c163d4a Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 5 Dec 2023 10:26:32 -0300 Subject: [PATCH] Add ModifyUpdater accessors (#681) --- deno-runtime/lib/accessors/mod.ts | 14 +- .../lib/accessors/modify/ModifyUpdater.ts | 137 ++++++++++++++++++ .../lib/accessors/tests/ModifyUpdater.test.ts | 128 ++++++++++++++++ 3 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 deno-runtime/lib/accessors/modify/ModifyUpdater.ts create mode 100644 deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts diff --git a/deno-runtime/lib/accessors/mod.ts b/deno-runtime/lib/accessors/mod.ts index 78b970cb4..5418e776f 100644 --- a/deno-runtime/lib/accessors/mod.ts +++ b/deno-runtime/lib/accessors/mod.ts @@ -1,4 +1,3 @@ -// @ts-ignore - this is a hack to make the tests work import type { IAppAccessors } from '@rocket.chat/apps-engine/definition/accessors/IAppAccessors.ts'; import type { IEnvironmentWrite } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentWrite.ts'; import type { IEnvironmentRead } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentRead.ts'; @@ -16,6 +15,7 @@ import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/vid import * as Messenger from '../messenger.ts'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import { ModifyCreator } from "./modify/ModifyCreator.ts"; +import { ModifyUpdater } from "./modify/ModifyUpdater.ts"; const httpMethods = ['get', 'post', 'put', 'delete', 'head', 'options', 'patch'] as const; @@ -30,6 +30,7 @@ export class AppAccessors { private persistence?: IPersistence; private http?: IHttp; private creator?: ModifyCreator; + private updater?: ModifyUpdater; private proxify: (namespace: string) => T; @@ -195,9 +196,8 @@ export class AppAccessors { public getModifier() { if (!this.modifier) { this.modifier = { - // getCreator: () => this.proxify('getModifier:getCreator'), // can't be proxy getCreator: this.getCreator.bind(this), - getUpdater: () => this.proxify('getModifier:getUpdater'), // can't be proxy + getUpdater: this.getUpdater.bind(this), getDeleter: () => this.proxify('getModifier:getDeleter'), getExtender: () => this.proxify('getModifier:getExtender'), // can't be proxy getNotifier: () => this.proxify('getModifier:getNotifier'), @@ -234,6 +234,14 @@ export class AppAccessors { return this.creator; } + + private getUpdater() { + if (!this.updater) { + this.updater = new ModifyUpdater(this.senderFn); + } + + return this.updater; + } } export const AppAccessorsInstance = new AppAccessors(Messenger.sendRequest); diff --git a/deno-runtime/lib/accessors/modify/ModifyUpdater.ts b/deno-runtime/lib/accessors/modify/ModifyUpdater.ts new file mode 100644 index 000000000..5cd23d86d --- /dev/null +++ b/deno-runtime/lib/accessors/modify/ModifyUpdater.ts @@ -0,0 +1,137 @@ +import { createRequire } from 'node:module'; + +import type { IModifyUpdater } from '@rocket.chat/apps-engine/definition/accessors/IModifyUpdater.ts'; +import type { ILivechatUpdater } from '@rocket.chat/apps-engine/definition/accessors/ILivechatUpdater.ts'; +import type { IUserUpdater } from '@rocket.chat/apps-engine/definition/accessors/IUserUpdater.ts'; +import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts'; +import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.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 { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; + +import * as Messenger from '../../messenger.ts'; +import { MessageBuilder } from '../MessageBuilder.ts'; +import { RoomBuilder } from '../RoomBuilder.ts'; +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; + +const require = createRequire(import.meta.url); + +const UIHelper = require(import.meta.resolve('@rocket.chat/apps-engine/server/misc/UIHelper.js').replace('file://', '').replace('src/', '')); + +export class ModifyUpdater implements IModifyUpdater { + constructor(private readonly senderFn: typeof Messenger.sendRequest) {} + + public getLivechatUpdater(): ILivechatUpdater { + return new Proxy( + { __kind: 'getLivechatUpdater' }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + this.senderFn({ + method: `accessor:getModifier:getUpdater:getLivechatUpdater:${prop}`, + params, + }), + }, + ) as ILivechatUpdater; + } + + public getUserUpdater(): IUserUpdater { + return new Proxy( + { __kind: 'getUserUpdater' }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + this.senderFn({ + method: `accessor:getModifier:getUpdater:getUserUpdater:${prop}`, + params, + }), + }, + ) as IUserUpdater; + } + + public async message(messageId: string, _updater: IUser): Promise { + const response = await this.senderFn({ + method: 'bridges:getMessageBridge:doGetById', + params: [messageId, AppObjectRegistry.get('appId')], + }); + + return new MessageBuilder(response.result as IMessage); + } + + public async room(roomId: string, _updater: IUser): Promise { + const response = await this.senderFn({ + method: 'bridges:getRoomBridge:doGetById', + params: [roomId, AppObjectRegistry.get('appId')], + }); + + return new RoomBuilder(response.result as IRoom); + } + + public finish(builder: IMessageBuilder | IRoomBuilder): Promise { + switch (builder.kind) { + case RocketChatAssociationModel.MESSAGE: + return this._finishMessage(builder as IMessageBuilder); + case RocketChatAssociationModel.ROOM: + return this._finishRoom(builder as IRoomBuilder); + default: + throw new Error('Invalid builder passed to the ModifyUpdater.finish function.'); + } + } + + private async _finishMessage(builder: IMessageBuilder): Promise { + const result = builder.getMessage(); + + if (!result.id) { + throw new Error("Invalid message, can't update a message without an id."); + } + + if (!result.sender?.id) { + throw new Error('Invalid sender assigned to the message.'); + } + + if (result.blocks?.length) { + result.blocks = UIHelper.assignIds(result.blocks, AppObjectRegistry.get('appId')); + } + + await this.senderFn({ + method: 'bridges:getMessageBridge:doUpdate', + params: [result, AppObjectRegistry.get('appId')], + }); + } + + private async _finishRoom(builder: IRoomBuilder): Promise { + const result = builder.getRoom(); + + if (!result.id) { + throw new Error("Invalid room, can't update a room without an id."); + } + + if (!result.type) { + throw new Error('Invalid type assigned to the room.'); + } + + if (result.type !== RoomType.LIVE_CHAT) { + if (!result.creator || !result.creator.id) { + throw new Error('Invalid creator assigned to the room.'); + } + + if (!result.slugifiedName || !result.slugifiedName.trim()) { + throw new Error('Invalid slugifiedName assigned to the room.'); + } + } + + if (!result.displayName || !result.displayName.trim()) { + throw new Error('Invalid displayName assigned to the room.'); + } + + await this.senderFn({ + method: 'bridges:getRoomBridge:doUpdate', + params: [result, builder.getMembersToBeAddedUsernames(), AppObjectRegistry.get('appId')], + }); + } +} diff --git a/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts b/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts new file mode 100644 index 000000000..99ef7e556 --- /dev/null +++ b/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts @@ -0,0 +1,128 @@ +// deno-lint-ignore-file no-explicit-any +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { assertSpyCall, spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; +import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod.ts'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { ModifyUpdater } from '../modify/ModifyUpdater.ts'; + +describe('ModifyUpdater', () => { + let modifyUpdater: ModifyUpdater; + + const senderFn = (r: any) => + Promise.resolve({ + id: Math.random().toString(36).substring(2), + jsonrpc: '2.0', + result: r, + serialize() { + return JSON.stringify(this); + }, + }); + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('appId', 'deno-test'); + modifyUpdater = new ModifyUpdater(senderFn); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('correctly formats requests for the update message flow', async () => { + const _spy = spy(modifyUpdater, 'senderFn' as keyof ModifyUpdater); + + const messageBuilder = await modifyUpdater.message('123', { id: '456' } as any); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getMessageBridge:doGetById', + params: ['123', 'deno-test'], + }, + ], + }); + + messageBuilder.setUpdateData( + { + id: '123', + room: { id: '123' }, + sender: { id: '456' }, + text: 'Hello World', + }, + { + id: '456', + }, + ); + + await modifyUpdater.finish(messageBuilder); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getMessageBridge:doUpdate', + params: [messageBuilder.getMessage(), 'deno-test'], + }, + ], + }); + + _spy.restore(); + }); + + it('correctly formats requests for the update room flow', async () => { + const _spy = spy(modifyUpdater, 'senderFn' as keyof ModifyUpdater); + + const roomBuilder = await modifyUpdater.room('123', { id: '456' } as any); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getRoomBridge:doGetById', + params: ['123', 'deno-test'], + }, + ], + }); + + roomBuilder.setData({ + id: '123', + type: 'c', + displayName: 'Test Room', + slugifiedName: 'test-room', + creator: { id: '456' }, + }); + + roomBuilder.setMembersToBeAddedByUsernames(['username1', 'username2']); + + // We need to sneak in the id as the `modifyUpdater.room` call won't have legitimate data + roomBuilder.getRoom().id = '123'; + + await modifyUpdater.finish(roomBuilder); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getRoomBridge:doUpdate', + params: [roomBuilder.getRoom(), roomBuilder.getMembersToBeAddedUsernames(), 'deno-test'], + }, + ], + }); + }); + + it('correctly formats requests to UserUpdater methods', async () => { + const result = await modifyUpdater.getUserUpdater().updateStatusText({ id: '123' } as any, 'Hello World') as any; + + assertEquals(result.result, { + method: 'accessor:getModifier:getUpdater:getUserUpdater:updateStatusText', + params: [{ id: '123' }, 'Hello World'], + }); + }); + + it('correctly formats requests to LivechatUpdater methods', async () => { + const result = await modifyUpdater.getLivechatUpdater().closeRoom({ id: '123' } as any, 'close it!') as any; + + assertEquals(result.result, { + method: 'accessor:getModifier:getUpdater:getLivechatUpdater:closeRoom', + params: [{ id: '123' }, 'close it!'], + }); + }); +});