Skip to content

Commit

Permalink
Modify Accessors (#680)
Browse files Browse the repository at this point in the history
d-gubert authored Dec 5, 2023
1 parent 632c285 commit 092f19b
Showing 17 changed files with 1,671 additions and 79 deletions.
4 changes: 3 additions & 1 deletion deno-runtime/deno.jsonc
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
{
"imports": {
"@rocket.chat/apps-engine/": "./../src/",
"@rocket.chat/ui-kit": "npm:@rocket.chat/ui-kit@^0.31.22",
"acorn": "npm:acorn@8.10.0",
"acorn-walk": "npm:acorn-walk@8.2.0",
"astring": "npm:astring@1.8.6"
"astring": "npm:astring@1.8.6",
"jsonrpc-lite": "npm:jsonrpc-lite@2.2.0",
"uuid": "npm:uuid@8.3.2",
}
}
73 changes: 45 additions & 28 deletions deno-runtime/deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

208 changes: 208 additions & 0 deletions deno-runtime/lib/accessors/BlockBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { v1 as uuid } from 'uuid';

import {
BlockType,
IActionsBlock,
IBlock,
IConditionalBlock,
IConditionalBlockFilters,
IContextBlock,
IImageBlock,
IInputBlock,
ISectionBlock,
} from "@rocket.chat/apps-engine/definition/uikit/blocks/Blocks.ts";
import type {
IBlockElement,
IButtonElement,
IImageElement,
IInputElement,
IInteractiveElement,
IMultiStaticSelectElement,
IOverflowMenuElement,
IPlainTextInputElement,
ISelectElement,
IStaticSelectElement,
} from '@rocket.chat/apps-engine/definition/uikit/blocks/Elements.ts';
import { BlockElementType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Elements.ts';
import type { ITextObject } from '@rocket.chat/apps-engine/definition/uikit/blocks/Objects.ts';
import { TextObjectType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Objects.ts';
import { AppObjectRegistry } from "../../AppObjectRegistry.ts";

type BlockFunctionParameter<T extends IBlock> = Omit<T, 'type'>;
type ElementFunctionParameter<T extends IBlockElement> = T extends IInteractiveElement
? Omit<T, 'type' | 'actionId'> | Partial<Pick<T, 'actionId'>>
: Omit<T, 'type'>;

type SectionBlockParam = BlockFunctionParameter<ISectionBlock>;
type ImageBlockParam = BlockFunctionParameter<IImageBlock>;
type ActionsBlockParam = BlockFunctionParameter<IActionsBlock>;
type ContextBlockParam = BlockFunctionParameter<IContextBlock>;
type InputBlockParam = BlockFunctionParameter<IInputBlock>;

type ButtonElementParam = ElementFunctionParameter<IButtonElement>;
type ImageElementParam = ElementFunctionParameter<IImageElement>;
type OverflowMenuElementParam = ElementFunctionParameter<IOverflowMenuElement>;
type PlainTextInputElementParam = ElementFunctionParameter<IPlainTextInputElement>;
type StaticSelectElementParam = ElementFunctionParameter<IStaticSelectElement>;
type MultiStaticSelectElementParam = ElementFunctionParameter<IMultiStaticSelectElement>;

/**
* @deprecated please prefer the rocket.chat/ui-kit components
*/
export class BlockBuilder {
private readonly blocks: Array<IBlock>;
private readonly appId: string;

constructor() {
this.blocks = [];
this.appId = String(AppObjectRegistry.get('appId'));
}

public addSectionBlock(block: SectionBlockParam): BlockBuilder {
this.addBlock({ type: BlockType.SECTION, ...block } as ISectionBlock);

return this;
}

public addImageBlock(block: ImageBlockParam): BlockBuilder {
this.addBlock({ type: BlockType.IMAGE, ...block } as IImageBlock);

return this;
}

public addDividerBlock(): BlockBuilder {
this.addBlock({ type: BlockType.DIVIDER });

return this;
}

public addActionsBlock(block: ActionsBlockParam): BlockBuilder {
this.addBlock({ type: BlockType.ACTIONS, ...block } as IActionsBlock);

return this;
}

public addContextBlock(block: ContextBlockParam): BlockBuilder {
this.addBlock({ type: BlockType.CONTEXT, ...block } as IContextBlock);

return this;
}

public addInputBlock(block: InputBlockParam): BlockBuilder {
this.addBlock({ type: BlockType.INPUT, ...block } as IInputBlock);

return this;
}

public addConditionalBlock(innerBlocks: BlockBuilder | Array<IBlock>, condition?: IConditionalBlockFilters): BlockBuilder {
const render = innerBlocks instanceof BlockBuilder ? innerBlocks.getBlocks() : innerBlocks;

this.addBlock({ type: BlockType.CONDITIONAL, render, when: condition } as IConditionalBlock);

return this;
}

public getBlocks() {
return this.blocks;
}

public newPlainTextObject(text: string, emoji = false): ITextObject {
return {
type: TextObjectType.PLAINTEXT,
text,
emoji,
};
}

public newMarkdownTextObject(text: string): ITextObject {
return {
type: TextObjectType.MARKDOWN,
text,
};
}

public newButtonElement(info: ButtonElementParam): IButtonElement {
return this.newInteractiveElement({
type: BlockElementType.BUTTON,
...info,
} as IButtonElement);
}

public newImageElement(info: ImageElementParam): IImageElement {
return {
type: BlockElementType.IMAGE,
...info,
};
}

public newOverflowMenuElement(info: OverflowMenuElementParam): IOverflowMenuElement {
return this.newInteractiveElement({
type: BlockElementType.OVERFLOW_MENU,
...info,
} as IOverflowMenuElement);
}

public newPlainTextInputElement(info: PlainTextInputElementParam): IPlainTextInputElement {
return this.newInputElement({
type: BlockElementType.PLAIN_TEXT_INPUT,
...info,
} as IPlainTextInputElement);
}

public newStaticSelectElement(info: StaticSelectElementParam): IStaticSelectElement {
return this.newSelectElement({
type: BlockElementType.STATIC_SELECT,
...info,
} as IStaticSelectElement);
}

public newMultiStaticElement(info: MultiStaticSelectElementParam): IMultiStaticSelectElement {
return this.newSelectElement({
type: BlockElementType.MULTI_STATIC_SELECT,
...info,
} as IMultiStaticSelectElement);
}

private newInteractiveElement<T extends IInteractiveElement>(element: T): T {
if (!element.actionId) {
element.actionId = this.generateActionId();
}

return element;
}

private newInputElement<T extends IInputElement>(element: T): T {
if (!element.actionId) {
element.actionId = this.generateActionId();
}

return element;
}

private newSelectElement<T extends ISelectElement>(element: T): T {
if (!element.actionId) {
element.actionId = this.generateActionId();
}

return element;
}

private addBlock(block: IBlock): void {
if (!block.blockId) {
block.blockId = this.generateBlockId();
}

block.appId = this.appId;

this.blocks.push(block);
}

private generateBlockId(): string {
return uuid();
}

private generateActionId(): string {
return uuid();
}

}
53 changes: 53 additions & 0 deletions deno-runtime/lib/accessors/DiscussionBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { IDiscussionBuilder as _IDiscussionBuilder } from "@rocket.chat/apps-engine/definition/accessors/IDiscussionBuilder.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 { RocketChatAssociationModel } from "@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts";
import { RoomType } from "@rocket.chat/apps-engine/definition/rooms/RoomType.ts";

import { RoomBuilder } from "./RoomBuilder.ts";
import { IRoomBuilder } from "@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts";

export interface IDiscussionBuilder extends _IDiscussionBuilder, IRoomBuilder {}

export class DiscussionBuilder extends RoomBuilder implements IDiscussionBuilder {
public kind: RocketChatAssociationModel.DISCUSSION;

private reply?: string;

private parentMessage?: IMessage;

constructor(data?: Partial<IRoom>) {
super(data);
this.kind = RocketChatAssociationModel.DISCUSSION;
this.room.type = RoomType.PRIVATE_GROUP;
}

public setParentRoom(parentRoom: IRoom): IDiscussionBuilder {
this.room.parentRoom = parentRoom;
return this;
}

public getParentRoom(): IRoom {
return this.room.parentRoom!;
}

public setReply(reply: string): IDiscussionBuilder {
this.reply = reply;
return this;
}

public getReply(): string {
return this.reply!;
}

public setParentMessage(parentMessage: IMessage): IDiscussionBuilder {
this.parentMessage = parentMessage;
return this;
}

public getParentMessage(): IMessage {
return this.parentMessage!;
}
}

198 changes: 198 additions & 0 deletions deno-runtime/lib/accessors/LivechatMessageBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { RocketChatAssociationModel } from "@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts";
import { RoomType } from "@rocket.chat/apps-engine/definition/rooms/RoomType.ts";

import type { ILivechatMessageBuilder } from "@rocket.chat/apps-engine/definition/accessors/ILivechatMessageBuilder.ts";
import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts';
import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment.ts';
import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts';
import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts';
import type { ILivechatMessage as EngineLivechatMessage } from '@rocket.chat/apps-engine/definition/livechat/ILivechatMessage.ts';
import type { IVisitor } from "@rocket.chat/apps-engine/definition/livechat/IVisitor.ts";
import type { IMessageBuilder } from "@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts";

import { MessageBuilder } from "./MessageBuilder.ts";

export interface ILivechatMessage extends EngineLivechatMessage, IMessage {}

export class LivechatMessageBuilder implements ILivechatMessageBuilder {
public kind: RocketChatAssociationModel.LIVECHAT_MESSAGE;

private msg: ILivechatMessage;

constructor(message?: ILivechatMessage) {
this.kind = RocketChatAssociationModel.LIVECHAT_MESSAGE;
this.msg = message || ({} as ILivechatMessage);
}

public setData(data: ILivechatMessage): ILivechatMessageBuilder {
delete data.id;
this.msg = data;

return this;
}

public setRoom(room: IRoom): ILivechatMessageBuilder {
this.msg.room = room;
return this;
}

public getRoom(): IRoom {
return this.msg.room;
}

public setSender(sender: IUser): ILivechatMessageBuilder {
this.msg.sender = sender;
delete this.msg.visitor;

return this;
}

public getSender(): IUser {
return this.msg.sender;
}

public setText(text: string): ILivechatMessageBuilder {
this.msg.text = text;
return this;
}

public getText(): string {
return this.msg.text!;
}

public setEmojiAvatar(emoji: string): ILivechatMessageBuilder {
this.msg.emoji = emoji;
return this;
}

public getEmojiAvatar(): string {
return this.msg.emoji!;
}

public setAvatarUrl(avatarUrl: string): ILivechatMessageBuilder {
this.msg.avatarUrl = avatarUrl;
return this;
}

public getAvatarUrl(): string {
return this.msg.avatarUrl!;
}

public setUsernameAlias(alias: string): ILivechatMessageBuilder {
this.msg.alias = alias;
return this;
}

public getUsernameAlias(): string {
return this.msg.alias!;
}

public addAttachment(attachment: IMessageAttachment): ILivechatMessageBuilder {
if (!this.msg.attachments) {
this.msg.attachments = [];
}

this.msg.attachments.push(attachment);
return this;
}

public setAttachments(attachments: Array<IMessageAttachment>): ILivechatMessageBuilder {
this.msg.attachments = attachments;
return this;
}

public getAttachments(): Array<IMessageAttachment> {
return this.msg.attachments!;
}

public replaceAttachment(position: number, attachment: IMessageAttachment): ILivechatMessageBuilder {
if (!this.msg.attachments) {
this.msg.attachments = [];
}

if (!this.msg.attachments[position]) {
throw new Error(`No attachment found at the index of "${position}" to replace.`);
}

this.msg.attachments[position] = attachment;
return this;
}

public removeAttachment(position: number): ILivechatMessageBuilder {
if (!this.msg.attachments) {
this.msg.attachments = [];
}

if (!this.msg.attachments[position]) {
throw new Error(`No attachment found at the index of "${position}" to remove.`);
}

this.msg.attachments.splice(position, 1);

return this;
}

public setEditor(user: IUser): ILivechatMessageBuilder {
this.msg.editor = user;
return this;
}

public getEditor(): IUser {
return this.msg.editor;
}

public setGroupable(groupable: boolean): ILivechatMessageBuilder {
this.msg.groupable = groupable;
return this;
}

public getGroupable(): boolean {
return this.msg.groupable!;
}

public setParseUrls(parseUrls: boolean): ILivechatMessageBuilder {
this.msg.parseUrls = parseUrls;
return this;
}

public getParseUrls(): boolean {
return this.msg.parseUrls!;
}

public setToken(token: string): ILivechatMessageBuilder {
this.msg.token = token;
return this;
}

public getToken(): string {
return this.msg.token!;
}

public setVisitor(visitor: IVisitor): ILivechatMessageBuilder {
this.msg.visitor = visitor;
delete this.msg.sender;

return this;
}

public getVisitor(): IVisitor {
return this.msg.visitor;
}

public getMessage(): ILivechatMessage {
if (!this.msg.room) {
throw new Error('The "room" property is required.');
}

if (this.msg.room.type !== RoomType.LIVE_CHAT) {
throw new Error('The room is not a Livechat room');
}

return this.msg;
}

public getMessageBuilder(): IMessageBuilder {
return new MessageBuilder(this.msg as IMessage);
}
}

226 changes: 226 additions & 0 deletions deno-runtime/lib/accessors/MessageBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { Block } from '@rocket.chat/ui-kit';

import { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts';
import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts';
import { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts';
import { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment.ts';
import { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts';
import { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts';
import { IBlock } from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks.ts';
import { BlockBuilder } from "./BlockBuilder.ts";

export class MessageBuilder implements IMessageBuilder {
public kind: RocketChatAssociationModel.MESSAGE;

private msg: IMessage;

constructor(message?: IMessage) {
this.kind = RocketChatAssociationModel.MESSAGE;
this.msg = message || ({} as IMessage);
}

public setData(data: IMessage): IMessageBuilder {
delete data.id;
this.msg = data;

return this as IMessageBuilder;
}

public setUpdateData(data: IMessage, editor: IUser): IMessageBuilder {
this.msg = data;
this.msg.editor = editor;
this.msg.editedAt = new Date();

return this as IMessageBuilder;
}

public setThreadId(threadId: string): IMessageBuilder {
this.msg.threadId = threadId;

return this as IMessageBuilder;
}

public getThreadId(): string {
return this.msg.threadId!;
}

public setRoom(room: IRoom): IMessageBuilder {
this.msg.room = room;
return this as IMessageBuilder;
}

public getRoom(): IRoom {
return this.msg.room;
}

public setSender(sender: IUser): IMessageBuilder {
this.msg.sender = sender;
return this as IMessageBuilder;
}

public getSender(): IUser {
return this.msg.sender;
}

public setText(text: string): IMessageBuilder {
this.msg.text = text;
return this as IMessageBuilder;
}

public getText(): string {
return this.msg.text!;
}

public setEmojiAvatar(emoji: string): IMessageBuilder {
this.msg.emoji = emoji;
return this as IMessageBuilder;
}

public getEmojiAvatar(): string {
return this.msg.emoji!;
}

public setAvatarUrl(avatarUrl: string): IMessageBuilder {
this.msg.avatarUrl = avatarUrl;
return this as IMessageBuilder;
}

public getAvatarUrl(): string {
return this.msg.avatarUrl!;
}

public setUsernameAlias(alias: string): IMessageBuilder {
this.msg.alias = alias;
return this as IMessageBuilder;
}

public getUsernameAlias(): string {
return this.msg.alias!;
}

public addAttachment(attachment: IMessageAttachment): IMessageBuilder {
if (!this.msg.attachments) {
this.msg.attachments = [];
}

this.msg.attachments.push(attachment);
return this as IMessageBuilder;
}

public setAttachments(attachments: Array<IMessageAttachment>): IMessageBuilder {
this.msg.attachments = attachments;
return this as IMessageBuilder;
}

public getAttachments(): Array<IMessageAttachment> {
return this.msg.attachments!;
}

public replaceAttachment(position: number, attachment: IMessageAttachment): IMessageBuilder {
if (!this.msg.attachments) {
this.msg.attachments = [];
}

if (!this.msg.attachments[position]) {
throw new Error(`No attachment found at the index of "${position}" to replace.`);
}

this.msg.attachments[position] = attachment;
return this as IMessageBuilder;
}

public removeAttachment(position: number): IMessageBuilder {
if (!this.msg.attachments) {
this.msg.attachments = [];
}

if (!this.msg.attachments[position]) {
throw new Error(`No attachment found at the index of "${position}" to remove.`);
}

this.msg.attachments.splice(position, 1);

return this as IMessageBuilder;
}

public setEditor(user: IUser): IMessageBuilder {
this.msg.editor = user;
return this as IMessageBuilder;
}

public getEditor(): IUser {
return this.msg.editor;
}

public setGroupable(groupable: boolean): IMessageBuilder {
this.msg.groupable = groupable;
return this as IMessageBuilder;
}

public getGroupable(): boolean {
return this.msg.groupable!;
}

public setParseUrls(parseUrls: boolean): IMessageBuilder {
this.msg.parseUrls = parseUrls;
return this as IMessageBuilder;
}

public getParseUrls(): boolean {
return this.msg.parseUrls!;
}

public getMessage(): IMessage {
if (!this.msg.room) {
throw new Error('The "room" property is required.');
}

return this.msg;
}

public addBlocks(blocks: BlockBuilder | Array<IBlock | Block>) {
if (!Array.isArray(this.msg.blocks)) {
this.msg.blocks = [];
}

if (blocks instanceof BlockBuilder) {
this.msg.blocks.push(...blocks.getBlocks());
} else {
this.msg.blocks.push(...blocks);
}

return this as IMessageBuilder;
}

public setBlocks(blocks: BlockBuilder | Array<IBlock | Block>) {
if (blocks instanceof BlockBuilder) {
this.msg.blocks = blocks.getBlocks();
} else {
this.msg.blocks = blocks;
}

return this as IMessageBuilder;
}

public getBlocks() {
return this.msg.blocks!;
}

public addCustomField(key: string, value: unknown): IMessageBuilder {
if (!this.msg.customFields) {
this.msg.customFields = {};
}

if (this.msg.customFields[key]) {
throw new Error(`The message already contains a custom field by the key: ${key}`);
}

if (key.includes('.')) {
throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`);
}

this.msg.customFields[key] = value;

return this as IMessageBuilder;
}
}
157 changes: 157 additions & 0 deletions deno-runtime/lib/accessors/RoomBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { IRoomBuilder } from "@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts";
import { RocketChatAssociationModel } from "@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts";
import { IRoom } from "@rocket.chat/apps-engine/definition/rooms/IRoom.ts";
import { RoomType } from "@rocket.chat/apps-engine/definition/rooms/RoomType.ts";
import { IUser } from "@rocket.chat/apps-engine/definition/users/IUser.ts";

export class RoomBuilder implements IRoomBuilder {
public kind: RocketChatAssociationModel.ROOM | RocketChatAssociationModel.DISCUSSION;

protected room: IRoom;

private members: Array<string>;

constructor(data?: Partial<IRoom>) {
this.kind = RocketChatAssociationModel.ROOM;
this.room = (data || { customFields: {} }) as IRoom;
this.members = [];
}

public setData(data: Partial<IRoom>): IRoomBuilder {
delete data.id;
this.room = data as IRoom;

return this;
}

public setDisplayName(name: string): IRoomBuilder {
this.room.displayName = name;
return this;
}

public getDisplayName(): string {
return this.room.displayName!;
}

public setSlugifiedName(name: string): IRoomBuilder {
this.room.slugifiedName = name;
return this;
}

public getSlugifiedName(): string {
return this.room.slugifiedName;
}

public setType(type: RoomType): IRoomBuilder {
this.room.type = type;
return this;
}

public getType(): RoomType {
return this.room.type;
}

public setCreator(creator: IUser): IRoomBuilder {
this.room.creator = creator;
return this;
}

public getCreator(): IUser {
return this.room.creator;
}

/**
* @deprecated
*/
public addUsername(username: string): IRoomBuilder {
this.addMemberToBeAddedByUsername(username);
return this;
}

/**
* @deprecated
*/
public setUsernames(usernames: Array<string>): IRoomBuilder {
this.setMembersToBeAddedByUsernames(usernames);
return this;
}

/**
* @deprecated
*/
public getUsernames(): Array<string> {
const usernames = this.getMembersToBeAddedUsernames();
if (usernames && usernames.length > 0) {
return usernames;
}
return this.room.usernames || [];
}

public addMemberToBeAddedByUsername(username: string): IRoomBuilder {
this.members.push(username);
return this;
}

public setMembersToBeAddedByUsernames(usernames: Array<string>): IRoomBuilder {
this.members = usernames;
return this;
}

public getMembersToBeAddedUsernames(): Array<string> {
return this.members;
}

public setDefault(isDefault: boolean): IRoomBuilder {
this.room.isDefault = isDefault;
return this;
}

public getIsDefault(): boolean {
return this.room.isDefault!;
}

public setReadOnly(isReadOnly: boolean): IRoomBuilder {
this.room.isReadOnly = isReadOnly;
return this;
}

public getIsReadOnly(): boolean {
return this.room.isReadOnly!;
}

public setDisplayingOfSystemMessages(displaySystemMessages: boolean): IRoomBuilder {
this.room.displaySystemMessages = displaySystemMessages;
return this;
}

public getDisplayingOfSystemMessages(): boolean {
return this.room.displaySystemMessages!;
}

public addCustomField(key: string, value: object): IRoomBuilder {
if (typeof this.room.customFields !== 'object') {
this.room.customFields = {};
}

this.room.customFields[key] = value;
return this;
}

public setCustomFields(fields: { [key: string]: object }): IRoomBuilder {
this.room.customFields = fields;
return this;
}

public getCustomFields(): { [key: string]: object } {
return this.room.customFields!;
}

public getUserIds(): Array<string> {
return this.room.userIds!;
}

public getRoom(): IRoom {
return this.room;
}
}

75 changes: 75 additions & 0 deletions deno-runtime/lib/accessors/UserBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { IUserBuilder } from '@rocket.chat/apps-engine/definition/accessors/IUserBuilder.ts';
import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts';
import { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts';
import { IUserSettings } from '@rocket.chat/apps-engine/definition/users/IUserSettings.ts';
import { IUserEmail } from '@rocket.chat/apps-engine/definition/users/IUserEmail.ts';

export class UserBuilder implements IUserBuilder {
public kind: RocketChatAssociationModel.USER;

private user: Partial<IUser>;

constructor(user?: Partial<IUser>) {
this.kind = RocketChatAssociationModel.USER;
this.user = user || ({} as Partial<IUser>);
}

public setData(data: Partial<IUser>): IUserBuilder {
delete data.id;
this.user = data;

return this;
}

public setEmails(emails: Array<IUserEmail>): IUserBuilder {
this.user.emails = emails;
return this;
}

public getEmails(): Array<IUserEmail> {
return this.user.emails!;
}

public setDisplayName(name: string): IUserBuilder {
this.user.name = name;
return this;
}

public getDisplayName(): string {
return this.user.name!;
}

public setUsername(username: string): IUserBuilder {
this.user.username = username;
return this;
}

public getUsername(): string {
return this.user.username!;
}

public setRoles(roles: Array<string>): IUserBuilder {
this.user.roles = roles;
return this;
}

public getRoles(): Array<string> {
return this.user.roles!;
}

public getSettings(): Partial<IUserSettings> {
return this.user.settings;
}

public getUser(): Partial<IUser> {
if (!this.user.username) {
throw new Error('The "username" property is required.');
}

if (!this.user.name) {
throw new Error('The "name" property is required.');
}

return this.user;
}
}
79 changes: 79 additions & 0 deletions deno-runtime/lib/accessors/VideoConferenceBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { IVideoConferenceBuilder } from "@rocket.chat/apps-engine/definition/accessors/IVideoConferenceBuilder.ts";
import { RocketChatAssociationModel } from "@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts";
import type { IGroupVideoConference } from "@rocket.chat/apps-engine/definition/videoConferences/IVideoConference.ts";

export type AppVideoConference = Pick<IGroupVideoConference, 'rid' | 'providerName' | 'providerData' | 'title'> & {
createdBy: IGroupVideoConference['createdBy']['_id'];
};


export class VideoConferenceBuilder implements IVideoConferenceBuilder {
public kind: RocketChatAssociationModel.VIDEO_CONFERENCE = RocketChatAssociationModel.VIDEO_CONFERENCE;

protected call: AppVideoConference;

constructor(data?: Partial<AppVideoConference>) {
this.call = (data || {}) as AppVideoConference;
}

public setData(data: Partial<AppVideoConference>): IVideoConferenceBuilder {
this.call = {
rid: data.rid!,
createdBy: data.createdBy,
providerName: data.providerName!,
title: data.title!,
};

return this;
}

public setRoomId(rid: string): IVideoConferenceBuilder {
this.call.rid = rid;
return this;
}

public getRoomId(): string {
return this.call.rid;
}

public setCreatedBy(userId: string): IVideoConferenceBuilder {
this.call.createdBy = userId;
return this;
}

public getCreatedBy(): string {
return this.call.createdBy;
}

public setProviderName(userId: string): IVideoConferenceBuilder {
this.call.providerName = userId;
return this;
}

public getProviderName(): string {
return this.call.providerName;
}

public setProviderData(data: Record<string, unknown> | undefined): IVideoConferenceBuilder {
this.call.providerData = data;
return this;
}

public getProviderData(): Record<string, unknown> {
return this.call.providerData!;
}

public setTitle(userId: string): IVideoConferenceBuilder {
this.call.title = userId;
return this;
}

public getTitle(): string {
return this.call.title;
}

public getVideoConference(): AppVideoConference {
return this.call;
}
}

17 changes: 14 additions & 3 deletions deno-runtime/lib/accessors/mod.ts
Original file line number Diff line number Diff line change
@@ -15,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";

const httpMethods = ['get', 'post', 'put', 'delete', 'head', 'options', 'patch'] as const;

@@ -28,13 +29,14 @@ export class AppAccessors {
private modifier?: IModify;
private persistence?: IPersistence;
private http?: IHttp;
private creator?: ModifyCreator;

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

constructor(senderFn: typeof Messenger.sendRequest) {
constructor(private readonly senderFn: typeof Messenger.sendRequest) {
this.proxify = <T>(namespace: string): T =>
new Proxy(
{ __kind: namespace },
{ __kind: `accessor:${namespace}` },
{
get:
(_target: unknown, prop: string) =>
@@ -193,7 +195,8 @@ export class AppAccessors {
public getModifier() {
if (!this.modifier) {
this.modifier = {
getCreator: () => this.proxify('getModifier:getCreator'), // can't be proxy
// getCreator: () => this.proxify('getModifier:getCreator'), // can't be proxy
getCreator: this.getCreator.bind(this),
getUpdater: () => this.proxify('getModifier:getUpdater'), // can't be proxy
getDeleter: () => this.proxify('getModifier:getDeleter'),
getExtender: () => this.proxify('getModifier:getExtender'), // can't be proxy
@@ -223,6 +226,14 @@ export class AppAccessors {

return this.http;
}

private getCreator() {
if (!this.creator) {
this.creator = new ModifyCreator(this.senderFn);
}

return this.creator;
}
}

export const AppAccessorsInstance = new AppAccessors(Messenger.sendRequest);
306 changes: 306 additions & 0 deletions deno-runtime/lib/accessors/modify/ModifyCreator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
import { createRequire } from 'node:module';

import type { IModifyCreator } from '@rocket.chat/apps-engine/definition/accessors/IModifyCreator.ts';
import type { IUploadCreator } from '@rocket.chat/apps-engine/definition/accessors/IUploadCreator.ts';
import type { ILivechatCreator } from '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator.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 { IBotUser } from '@rocket.chat/apps-engine/definition/users/IBotUser.ts';

import { UserType } from '@rocket.chat/apps-engine/definition/users/UserType.ts';
import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts';
import { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts';
import { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts';
import { IUserBuilder } from '@rocket.chat/apps-engine/definition/accessors/IUserBuilder.ts';
import { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceBuilder.ts';
import { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts';
import { ILivechatMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/ILivechatMessageBuilder.ts';

import { BlockBuilder } from '../BlockBuilder.ts';
import { MessageBuilder } from '../MessageBuilder.ts';
import { DiscussionBuilder, IDiscussionBuilder } from '../DiscussionBuilder.ts';
import { ILivechatMessage, LivechatMessageBuilder } from '../LivechatMessageBuilder.ts';
import { RoomBuilder } from '../RoomBuilder.ts';
import { UserBuilder } from '../UserBuilder.ts';
import { AppVideoConference, VideoConferenceBuilder } from '../VideoConferenceBuilder.ts';
import * as Messenger from '../../messenger.ts';
import { AppObjectRegistry } from '../../../AppObjectRegistry.ts';

const require = createRequire(import.meta.url);

// @deno-types="../../../../server/misc/UIHelper.d.ts"
const UIHelper = require(import.meta.resolve('@rocket.chat/apps-engine/server/misc/UIHelper.js').replace('file://', '').replace('src/', ''));

export class ModifyCreator implements IModifyCreator {
constructor(private readonly senderFn: typeof Messenger.sendRequest) {}

getLivechatCreator(): ILivechatCreator {
return new Proxy(
{ __kind: 'getLivechatCreator' },
{
get: (_target: unknown, prop: string) => {
if (prop === 'createToken') {
return () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}

return (...params: unknown[]) =>
this.senderFn({
method: `accessor:getModifier:getCreator:getLivechatCreator:${prop}`,
params,
});
},
},
) as ILivechatCreator;
}

getUploadCreator(): IUploadCreator {
return new Proxy(
{ __kind: 'getUploadCreator' },
{
get:
(_target: unknown, prop: string) =>
(...params: unknown[]) =>
this.senderFn({
method: `accessor:getModifier:getCreator:getUploadCreator:${prop}`,
params,
}),
},
) as IUploadCreator;
}

getBlockBuilder() {
return new BlockBuilder();
}

startMessage(data?: IMessage) {
if (data) {
delete data.id;
}

return new MessageBuilder(data);
}

startLivechatMessage(data?: ILivechatMessage) {
if (data) {
delete data.id;
}

return new LivechatMessageBuilder(data);
}

startRoom(data?: IRoom) {
if (data) {
// @ts-ignore - this has been imported from the Apps-Engine
delete data.id;
}

return new RoomBuilder(data);
}

startDiscussion(data?: Partial<IRoom>) {
if (data) {
delete data.id;
}

return new DiscussionBuilder(data);
}

startVideoConference(data?: Partial<AppVideoConference>) {
return new VideoConferenceBuilder(data);
}

startBotUser(data?: Partial<IBotUser>) {
if (data) {
delete data.id;

const { roles } = data;

if (roles?.length) {
const hasRole = roles
.map((role: string) => role.toLocaleLowerCase())
.some((role: string) => role === 'admin' || role === 'owner' || role === 'moderator');

if (hasRole) {
throw new Error('Invalid role assigned to the user. Should not be admin, owner or moderator.');
}
}

if (!data.type) {
data.type = UserType.BOT;
}
}

return new UserBuilder(data);
}

public finish(
builder: IMessageBuilder | ILivechatMessageBuilder | IRoomBuilder | IDiscussionBuilder | IVideoConferenceBuilder | IUserBuilder,
): Promise<string> {
switch (builder.kind) {
case RocketChatAssociationModel.MESSAGE:
return this._finishMessage(builder as IMessageBuilder);
case RocketChatAssociationModel.LIVECHAT_MESSAGE:
return this._finishLivechatMessage(builder as ILivechatMessageBuilder);
case RocketChatAssociationModel.ROOM:
return this._finishRoom(builder as IRoomBuilder);
case RocketChatAssociationModel.DISCUSSION:
return this._finishDiscussion(builder as IDiscussionBuilder);
case RocketChatAssociationModel.VIDEO_CONFERENCE:
return this._finishVideoConference(builder as IVideoConferenceBuilder);
case RocketChatAssociationModel.USER:
return this._finishUser(builder as IUserBuilder);
default:
throw new Error('Invalid builder passed to the ModifyCreator.finish function.');
}
}

private async _finishMessage(builder: IMessageBuilder): Promise<string> {
const result = builder.getMessage();
delete result.id;

if (!result.sender || !result.sender.id) {
const response = await this.senderFn({
method: 'bridges:getUserBridge:doGetAppUser',
params: ['APP_ID'],
});

const appUser = response.result;

if (!appUser) {
throw new Error('Invalid sender assigned to the message.');
}

result.sender = appUser;
}

if (result.blocks?.length) {
// Can we move this elsewhere? This AppObjectRegistry usage doesn't really belong here, but where?
result.blocks = UIHelper.assignIds(result.blocks, AppObjectRegistry.get('appId'));
}

const response = await this.senderFn({
method: 'bridges:getMessageBridge:doCreate',
params: [result, AppObjectRegistry.get('appId')],
});

return String(response.result);
}

private async _finishLivechatMessage(builder: ILivechatMessageBuilder): Promise<string> {
if (builder.getSender() && !builder.getVisitor()) {
return this._finishMessage(builder.getMessageBuilder());
}

const result = builder.getMessage();
delete result.id;

if (!result.token && (!result.visitor || !result.visitor.token)) {
throw new Error('Invalid visitor sending the message');
}

result.token = result.visitor ? result.visitor.token : result.token;

const response = await this.senderFn({
method: 'bridges:getLivechatBridge:doCreateMessage',
params: [result, AppObjectRegistry.get('appId')],
});

return String(response.result);
}

private async _finishRoom(builder: IRoomBuilder): Promise<string> {
const result = builder.getRoom();
delete result.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.type !== RoomType.DIRECT_MESSAGE) {
if (result.type !== RoomType.LIVE_CHAT) {
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.');
}
}

const response = await this.senderFn({
method: 'bridges:getRoomBridge:doCreate',
params: [result, builder.getMembersToBeAddedUsernames(), AppObjectRegistry.get('appId')],
});

return String(response.result);
}

private async _finishDiscussion(builder: IDiscussionBuilder): Promise<string> {
const room = builder.getRoom();
delete room.id;

if (!room.creator || !room.creator.id) {
throw new Error('Invalid creator assigned to the discussion.');
}

if (!room.slugifiedName || !room.slugifiedName.trim()) {
throw new Error('Invalid slugifiedName assigned to the discussion.');
}

if (!room.displayName || !room.displayName.trim()) {
throw new Error('Invalid displayName assigned to the discussion.');
}

if (!room.parentRoom || !room.parentRoom.id) {
throw new Error('Invalid parentRoom assigned to the discussion.');
}

const response = await this.senderFn({
method: 'bridges:getRoomBridge:doCreateDiscussion',
params: [room, builder.getParentMessage(), builder.getReply(), builder.getMembersToBeAddedUsernames(), AppObjectRegistry.get('appId')],
});

return String(response.result);
}

private async _finishVideoConference(builder: IVideoConferenceBuilder): Promise<string> {
const videoConference = builder.getVideoConference();

if (!videoConference.createdBy) {
throw new Error('Invalid creator assigned to the video conference.');
}

if (!videoConference.providerName?.trim()) {
throw new Error('Invalid provider name assigned to the video conference.');
}

if (!videoConference.rid) {
throw new Error('Invalid roomId assigned to the video conference.');
}

const response = await this.senderFn({
method: 'bridges:getVideoConferenceBridge:doCreate',
params: [videoConference, AppObjectRegistry.get('appId')],
});

return String(response.result);
}

private async _finishUser(builder: IUserBuilder): Promise<string> {
const user = builder.getUser();

const response = await this.senderFn({
method: 'bridges:getUserBridge:doCreate',
params: [user, AppObjectRegistry.get('appId')],
});

return String(response.result);
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts';
import { assertEquals } from "https://deno.land/std@0.203.0/assert/assert_equals.ts";
import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts';
import { assertEquals } from 'https://deno.land/std@0.203.0/assert/assert_equals.ts';

import { AppAccessors } from "./mod.ts";
import { AppObjectRegistry } from "../../AppObjectRegistry.ts";
import { AppAccessors } from '../mod.ts';
import { AppObjectRegistry } from '../../../AppObjectRegistry.ts';

describe('AppAccessors', () => {
let appAccessors: AppAccessors;
const senderFn = (r: object) => Promise.resolve({
id: Math.random().toString(36).substring(2),
jsonrpc: '2.0',
result: r,
serialize() {
return JSON.stringify(this);
}
});
const senderFn = (r: object) =>
Promise.resolve({
id: Math.random().toString(36).substring(2),
jsonrpc: '2.0',
result: r,
serialize() {
return JSON.stringify(this);
},
});

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

afterAll(() => {
AppObjectRegistry.clear();
});

it('creates the correct format for IRead calls', async () => {
const roomRead = appAccessors.getReader().getRoomReader();
const room = await roomRead.getById('123');
@@ -70,12 +75,14 @@ describe('AppAccessors', () => {
});

assertEquals(command.result, {
params: [{
command: 'test',
i18nDescription: 'test',
i18nParamsExample: 'test',
providesPreview: true,
}],
params: [
{
command: 'test',
i18nDescription: 'test',
i18nParamsExample: 'test',
providesPreview: true,
},
],
method: 'accessor:getConfigurationModify:slashCommands:modifySlashCommand',
});
});
@@ -90,7 +97,7 @@ describe('AppAccessors', () => {
providesPreview: true,
executor() {
return Promise.resolve();
}
},
};

const result = await configExtend.slashCommands.provideSlashCommand(slashcommand);
@@ -102,12 +109,14 @@ describe('AppAccessors', () => {

assertEquals(result.result, {
method: 'accessor:getConfigurationExtend:slashCommands:provideSlashCommand',
params: [{
command: 'test',
i18nDescription: 'test',
i18nParamsExample: 'test',
providesPreview: true,
}],
params: [
{
command: 'test',
i18nDescription: 'test',
i18nParamsExample: 'test',
providesPreview: true,
},
],
});
});
});
106 changes: 106 additions & 0 deletions deno-runtime/lib/accessors/tests/ModifyCreator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// 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 { assert, assertEquals, assertNotInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod.ts';

import { AppObjectRegistry } from '../../../AppObjectRegistry.ts';
import { ModifyCreator } from '../modify/ModifyCreator.ts';

describe('ModifyCreator', () => {
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');
});

afterAll(() => {
AppObjectRegistry.clear();
});

it('sends the correct payload in the request to create a message', async () => {
const spying = spy(senderFn);
const modifyCreator = new ModifyCreator(spying);
const messageBuilder = modifyCreator.startMessage();

// Importing types from the Apps-Engine is problematic, so we'll go with `any` here
messageBuilder
.setRoom({ id: '123' } as any)
.setSender({ id: '456' } as any)
.setText('Hello World')
.setUsernameAlias('alias')
.setAvatarUrl('https://avatars.com/123');

// We can't get a legitimate return value here, so we ignore it
// but we need to know that the request sent was well formed
await modifyCreator.finish(messageBuilder);

assertSpyCall(spying, 0, {
args: [
{
method: 'bridges:getMessageBridge:doCreate',
params: [
{
room: { id: '123' },
sender: { id: '456' },
text: 'Hello World',
alias: 'alias',
avatarUrl: 'https://avatars.com/123',
},
'deno-test',
],
},
],
});
});

it('sends the correct payload in the request to upload a buffer', async () => {
const modifyCreator = new ModifyCreator(senderFn);

const result = await modifyCreator.getUploadCreator().uploadBuffer(new Uint8Array([1, 2, 3, 4]), 'text/plain');

assertEquals(result.result, {
method: 'accessor:getModifier:getCreator:getUploadCreator:uploadBuffer',
params: [new Uint8Array([1, 2, 3, 4]), 'text/plain'],
});
});

it('sends the correct payload in the request to create a visitor', async () => {
const modifyCreator = new ModifyCreator(senderFn);

const result = (await modifyCreator.getLivechatCreator().createVisitor({
token: 'random token',
username: 'random username for visitor',
name: 'Random Visitor',
})) as any; // We modified the send function so it changed the original return type of the function

assertEquals(result.result, {
method: 'accessor:getModifier:getCreator:getLivechatCreator:createVisitor',
params: [
{
token: 'random token',
username: 'random username for visitor',
name: 'Random Visitor',
},
],
});
});

// This test is important because if we return a promise we break API compatibility
it('does not return a promise for calls of the createToken() method of the LivechatCreator', () => {
const modifyCreator = new ModifyCreator(senderFn);

const result = modifyCreator.getLivechatCreator().createToken();

assertNotInstanceOf(result, Promise);
assert(typeof result === 'string', `Expected "${result}" to be of type "string", but got "${typeof result}"`);
});
});
1 change: 1 addition & 0 deletions deno-runtime/lib/messenger.ts
Original file line number Diff line number Diff line change
@@ -86,6 +86,7 @@ export async function sendRequest(requestDescriptor: RequestDescriptor): Promise

await send(request);

// TODO: add timeout to this
return new Promise((resolve, reject) => {
const handler = (event: Event) => {
if (event instanceof ErrorEvent) {
51 changes: 50 additions & 1 deletion src/server/runtime/AppsEngineDenoRuntime.ts
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import * as jsonrpc from 'jsonrpc-lite';

import type { AppAccessorManager, AppApiManager } from '../managers';
import type { AppManager } from '../AppManager';
import type { AppBridges } from '../bridges';

export type AppRuntimeParams = {
appId: string;
@@ -20,10 +21,18 @@ const ALLOWED_ACCESSOR_METHODS = [
'getReader',
'getPersistence',
'getHttp',
'getModifier',
] as Array<
keyof Pick<
AppAccessorManager,
'getConfigurationExtend' | 'getEnvironmentRead' | 'getEnvironmentWrite' | 'getConfigurationModify' | 'getReader' | 'getPersistence' | 'getHttp'
| 'getConfigurationExtend'
| 'getEnvironmentRead'
| 'getEnvironmentWrite'
| 'getConfigurationModify'
| 'getReader'
| 'getPersistence'
| 'getHttp'
| 'getModifier'
>
>;

@@ -63,6 +72,8 @@ export class DenoRuntimeSubprocessController extends EventEmitter {

private readonly api: AppApiManager;

private readonly bridges: AppBridges;

// We need to keep the appSource around in case the Deno process needs to be restarted
constructor(private readonly appId: string, private readonly appSource: string, manager: AppManager) {
super();
@@ -83,6 +94,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter {

this.accessors = manager.getAccessorManager();
this.api = manager.getApiManager();
this.bridges = manager.getBridges();
}

emit(eventName: string | symbol, ...args: any[]): boolean {
@@ -234,13 +246,50 @@ export class DenoRuntimeSubprocessController extends EventEmitter {
return jsonrpc.success(id, result);
}

private async handleBridgeMessage({ payload: { method, id, params } }: jsonrpc.IParsedObjectRequest): Promise<jsonrpc.SuccessObject> {
const [bridgeName, bridgeMethod] = method.substring(8).split(':');

const bridge = this.bridges[bridgeName as keyof typeof this.bridges];

if (!bridgeMethod.startsWith('do') || typeof bridge !== 'function' || !Array.isArray(params)) {
throw new Error('Invalid bridge request');
}

const bridgeInstance = bridge.call(this.bridges);

const methodRef = bridgeInstance[bridgeMethod as keyof typeof bridge] as unknown;

if (typeof methodRef !== 'function') {
throw new Error('Invalid bridge request');
}

const result = await methodRef.apply(
bridgeInstance,
// Should the protocol expect the placeholder APP_ID value or should the Deno process send the actual appId?
// If we do not expect the APP_ID, the Deno process will be able to impersonate other apps, potentially
params.map((value: unknown) => (value === 'APP_ID' ? this.appId : value)),
);

return jsonrpc.success(id, result);
}

private async handleIncomingMessage(message: jsonrpc.IParsedObjectNotification | jsonrpc.IParsedObjectRequest): Promise<void> {
const { method } = message.payload;

if (method.startsWith('accessor:')) {
const result = await this.handleAccessorMessage(message as jsonrpc.IParsedObjectRequest);

this.deno.stdin.write(result.serialize());

return;
}

if (method.startsWith('bridge:')) {
const result = await this.handleBridgeMessage(message as jsonrpc.IParsedObjectRequest);

this.deno.stdin.write(result.serialize());

return;
}

switch (method) {
116 changes: 114 additions & 2 deletions tests/server/runtime/DenoRuntimeSubprocessController.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { TestFixture, Setup, SetupFixture, Expect, AsyncTest } from 'alsatian';
import { TestFixture, Setup, SetupFixture, Expect, AsyncTest, SpyOn, Any } from 'alsatian';

import { AppAccessorManager, AppApiManager } from '../../../src/server/managers';
import { TestData, TestInfastructureSetup } from '../../test-data/utilities';
import { DenoRuntimeSubprocessController } from '../../../src/server/runtime/AppsEngineDenoRuntime';
import type { AppManager } from '../../../src/server/AppManager';
import { UserStatusConnection, UserType } from '../../../src/definition/users';

@TestFixture('DenoRuntimeSubprocessController')
export class DenuRuntimeSubprocessControllerTestFixture {
@@ -36,6 +37,8 @@ export class DenuRuntimeSubprocessControllerTestFixture {

@AsyncTest('correctly identifies a call to the HTTP accessor')
public async testHttpAccessor() {
const spy = SpyOn(this.manager.getBridges().getHttpBridge(), 'doCall');

// eslint-disable-next-line
const r = await this.controller['handleAccessorMessage']({
type: 'request' as any,
@@ -48,17 +51,48 @@ export class DenuRuntimeSubprocessControllerTestFixture {
},
});

Expect(this.manager.getBridges().getHttpBridge().doCall).toHaveBeenCalledWith(
Any(Object).thatMatches({
appId: 'deno-controller',
method: 'get',
url: 'https://google.com',
}),
);

Expect(r.result).toEqual({
method: 'get',
url: 'https://google.com',
content: "{ test: 'test' }",
statusCode: 200,
headers: {},
});

spy.restore();
}

@AsyncTest('correctly identifies a call to the IRead accessor')
public async testIReadAccessor() {
const spy = SpyOn(this.manager.getBridges().getUserBridge(), 'doGetByUsername');

spy.andReturn(
Promise.resolve({
id: 'id',
username: 'rocket.cat',
isEnabled: true,
emails: [],
name: 'name',
roles: [],
type: UserType.USER,
active: true,
utcOffset: 0,
status: 'offline',
statusConnection: UserStatusConnection.OFFLINE,
lastLoginAt: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
}),
);

// eslint-disable-next-line
const { id, result } = await this.controller['handleAccessorMessage']({
type: 'request' as any,
@@ -71,9 +105,10 @@ export class DenuRuntimeSubprocessControllerTestFixture {
},
});

Expect(this.manager.getBridges().getUserBridge().doGetByUsername).toHaveBeenCalledWith('rocket.cat', 'deno-controller');

Expect(id).toBe('test');
Expect((result as any).username).toEqual('rocket.cat');
Expect((result as any).appId).toEqual('deno-controller');
}

@AsyncTest('correctly identifies a call to the IEnvironmentReader accessor via IRead')
@@ -93,4 +128,81 @@ export class DenuRuntimeSubprocessControllerTestFixture {
Expect(id).toBe('requestId');
Expect((result as any).id).toEqual('setting test id');
}

@AsyncTest('correctly identifies a call to create a visitor via the LivechatCreator')
public async testLivechatCreator() {
const spy = SpyOn(this.manager.getBridges().getLivechatBridge(), 'doCreateVisitor');

spy.andReturn(Promise.resolve('random id'));

// eslint-disable-next-line
const { id, result } = await this.controller['handleAccessorMessage']({
type: 'request' as any,
payload: {
jsonrpc: '2.0',
id: 'requestId',
method: 'accessor:getModifier:getCreator:getLivechatCreator:createVisitor',
params: [
{
id: 'random id',
token: 'random token',
username: 'random username for visitor',
name: 'Random Visitor',
},
],
serialize: () => '',
},
});

// Making sure `handleAccessorMessage` correctly identified which accessor it should resolve to
// and that it passed the correct arguments to the bridge method
Expect(this.manager.getBridges().getLivechatBridge().doCreateVisitor).toHaveBeenCalledWith(
Any(Object).thatMatches({
id: 'random id',
token: 'random token',
username: 'random username for visitor',
name: 'Random Visitor',
}),
'deno-controller',
);

Expect(id).toBe('requestId');
Expect(result).toEqual('random id');

spy.restore();
}

@AsyncTest('correctly identifies a call to the message bridge')
public async testMessageBridge() {
const spy = SpyOn(this.manager.getBridges().getMessageBridge(), 'doCreate');

spy.andReturn(Promise.resolve('random-message-id'));

const messageParam = {
room: { id: '123' },
sender: { id: '456' },
text: 'Hello World',
alias: 'alias',
avatarUrl: 'https://avatars.com/123',
};

// eslint-disable-next-line
const { id, result } = await this.controller['handleBridgeMessage']({
type: 'request' as any,
payload: {
jsonrpc: '2.0',
id: 'requestId',
method: 'bridges:getMessageBridge:doCreate',
params: [messageParam, 'APP_ID'],
serialize: () => '',
},
});

Expect(this.manager.getBridges().getMessageBridge().doCreate).toHaveBeenCalledWith(messageParam, 'deno-controller');

Expect(id).toBe('requestId');
Expect(result).toEqual('random-message-id');

spy.restore();
}
}
21 changes: 2 additions & 19 deletions tests/test-data/bridges/userBridge.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { IUser } from '../../../src/definition/users';
import { UserStatusConnection, UserType } from '../../../src/definition/users';
import type { IUser, UserType } from '../../../src/definition/users';
import { UserBridge } from '../../../src/server/bridges';

export class TestsUserBridge extends UserBridge {
@@ -8,23 +7,7 @@ export class TestsUserBridge extends UserBridge {
}

public getByUsername(username: string, appId: string): Promise<IUser> {
return Promise.resolve({
id: 'id',
username,
isEnabled: true,
emails: [],
name: 'name',
roles: [],
type: UserType.USER,
active: true,
appId,
utcOffset: 0,
status: 'offline',
statusConnection: UserStatusConnection.OFFLINE,
lastLoginAt: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
});
throw new Error('Method not implemented.');
}

public create(user: Partial<IUser>): Promise<string> {

0 comments on commit 092f19b

Please sign in to comment.