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
14 changes: 11 additions & 3 deletions deno-runtime/lib/accessors/mod.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;

Expand All @@ -30,6 +30,7 @@ export class AppAccessors {
private persistence?: IPersistence;
private http?: IHttp;
private creator?: ModifyCreator;
private updater?: ModifyUpdater;

private proxify: <T>(namespace: string) => T;

Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -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);
137 changes: 137 additions & 0 deletions deno-runtime/lib/accessors/modify/ModifyUpdater.ts
Original file line number Diff line number Diff line change
@@ -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<IMessageBuilder> {
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<IRoomBuilder> {
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<void> {
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<void> {
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<void> {
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')],
});
}
}
128 changes: 128 additions & 0 deletions deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts
Original file line number Diff line number Diff line change
@@ -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!'],
});
});
});