diff --git a/deno-runtime/lib/accessors/builders/BlockBuilder.ts b/deno-runtime/lib/accessors/builders/BlockBuilder.ts new file mode 100644 index 000000000..d643792fa --- /dev/null +++ b/deno-runtime/lib/accessors/builders/BlockBuilder.ts @@ -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 = Omit; +type ElementFunctionParameter = T extends IInteractiveElement + ? Omit | Partial> + : Omit; + +type SectionBlockParam = BlockFunctionParameter; +type ImageBlockParam = BlockFunctionParameter; +type ActionsBlockParam = BlockFunctionParameter; +type ContextBlockParam = BlockFunctionParameter; +type InputBlockParam = BlockFunctionParameter; + +type ButtonElementParam = ElementFunctionParameter; +type ImageElementParam = ElementFunctionParameter; +type OverflowMenuElementParam = ElementFunctionParameter; +type PlainTextInputElementParam = ElementFunctionParameter; +type StaticSelectElementParam = ElementFunctionParameter; +type MultiStaticSelectElementParam = ElementFunctionParameter; + +/** + * @deprecated please prefer the rocket.chat/ui-kit components + */ +export class BlockBuilder { + private readonly blocks: Array; + 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, 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(element: T): T { + if (!element.actionId) { + element.actionId = this.generateActionId(); + } + + return element; + } + + private newInputElement(element: T): T { + if (!element.actionId) { + element.actionId = this.generateActionId(); + } + + return element; + } + + private newSelectElement(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(); + } + +} diff --git a/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts b/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts new file mode 100644 index 000000000..b69142eff --- /dev/null +++ b/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts @@ -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 type { IRoomBuilder } from "@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.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"; + +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) { + 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!; + } +} + diff --git a/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts b/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts new file mode 100644 index 000000000..b90081e23 --- /dev/null +++ b/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts @@ -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): ILivechatMessageBuilder { + this.msg.attachments = attachments; + return this; + } + + public getAttachments(): Array { + 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); + } +} + diff --git a/deno-runtime/lib/accessors/builders/MessageBuilder.ts b/deno-runtime/lib/accessors/builders/MessageBuilder.ts new file mode 100644 index 000000000..f8bebbcc4 --- /dev/null +++ b/deno-runtime/lib/accessors/builders/MessageBuilder.ts @@ -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): IMessageBuilder { + this.msg.attachments = attachments; + return this as IMessageBuilder; + } + + public getAttachments(): Array { + 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) { + 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) { + 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; + } +} diff --git a/deno-runtime/lib/accessors/builders/RoomBuilder.ts b/deno-runtime/lib/accessors/builders/RoomBuilder.ts new file mode 100644 index 000000000..19b634b37 --- /dev/null +++ b/deno-runtime/lib/accessors/builders/RoomBuilder.ts @@ -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; + + constructor(data?: Partial) { + this.kind = RocketChatAssociationModel.ROOM; + this.room = (data || { customFields: {} }) as IRoom; + this.members = []; + } + + public setData(data: Partial): 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): IRoomBuilder { + this.setMembersToBeAddedByUsernames(usernames); + return this; + } + + /** + * @deprecated + */ + public getUsernames(): Array { + 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): IRoomBuilder { + this.members = usernames; + return this; + } + + public getMembersToBeAddedUsernames(): Array { + 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 { + return this.room.userIds!; + } + + public getRoom(): IRoom { + return this.room; + } +} + diff --git a/deno-runtime/lib/accessors/builders/UserBuilder.ts b/deno-runtime/lib/accessors/builders/UserBuilder.ts new file mode 100644 index 000000000..05fcc3d1f --- /dev/null +++ b/deno-runtime/lib/accessors/builders/UserBuilder.ts @@ -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; + + constructor(user?: Partial) { + this.kind = RocketChatAssociationModel.USER; + this.user = user || ({} as Partial); + } + + public setData(data: Partial): IUserBuilder { + delete data.id; + this.user = data; + + return this; + } + + public setEmails(emails: Array): IUserBuilder { + this.user.emails = emails; + return this; + } + + public getEmails(): Array { + 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): IUserBuilder { + this.user.roles = roles; + return this; + } + + public getRoles(): Array { + return this.user.roles!; + } + + public getSettings(): Partial { + return this.user.settings; + } + + public getUser(): Partial { + 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; + } +} diff --git a/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts b/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts new file mode 100644 index 000000000..fad79aa3f --- /dev/null +++ b/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts @@ -0,0 +1,78 @@ +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 & { + createdBy: IGroupVideoConference['createdBy']['_id']; +}; + +export class VideoConferenceBuilder implements IVideoConferenceBuilder { + public kind: RocketChatAssociationModel.VIDEO_CONFERENCE = RocketChatAssociationModel.VIDEO_CONFERENCE; + + protected call: AppVideoConference; + + constructor(data?: Partial) { + this.call = (data || {}) as AppVideoConference; + } + + public setData(data: Partial): 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 | undefined): IVideoConferenceBuilder { + this.call.providerData = data; + return this; + } + + public getProviderData(): Record { + 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; + } +} + diff --git a/deno-runtime/lib/accessors/extenders/MessageExtender.ts b/deno-runtime/lib/accessors/extenders/MessageExtender.ts new file mode 100644 index 000000000..12eae78df --- /dev/null +++ b/deno-runtime/lib/accessors/extenders/MessageExtender.ts @@ -0,0 +1,60 @@ +import { IMessageExtender } from '@rocket.chat/apps-engine/definition/accessors/IMessageExtender.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'; + +export class MessageExtender implements IMessageExtender { + public readonly kind: RocketChatAssociationModel.MESSAGE; + + constructor(private msg: IMessage) { + this.kind = RocketChatAssociationModel.MESSAGE; + + if (!Array.isArray(msg.attachments)) { + this.msg.attachments = []; + } + } + + public addCustomField(key: string, value: unknown): IMessageExtender { + 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; + } + + public addAttachment(attachment: IMessageAttachment): IMessageExtender { + this.ensureAttachment(); + + this.msg.attachments!.push(attachment); + + return this; + } + + public addAttachments(attachments: Array): IMessageExtender { + this.ensureAttachment(); + + this.msg.attachments = this.msg.attachments!.concat(attachments); + + return this; + } + + public getMessage(): IMessage { + return structuredClone(this.msg); + } + + private ensureAttachment(): void { + if (!Array.isArray(this.msg.attachments)) { + this.msg.attachments = []; + } + } +} diff --git a/deno-runtime/lib/accessors/extenders/RoomExtender.ts b/deno-runtime/lib/accessors/extenders/RoomExtender.ts new file mode 100644 index 000000000..7063bf90e --- /dev/null +++ b/deno-runtime/lib/accessors/extenders/RoomExtender.ts @@ -0,0 +1,55 @@ +import { IRoomExtender } from "@rocket.chat/apps-engine/definition/accessors/IRoomExtender.ts"; +import { RocketChatAssociationModel } from "@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts"; +import { IRoom } from "@rocket.chat/apps-engine/definition/rooms/IRoom.ts"; +import { IUser } from "@rocket.chat/apps-engine/definition/users/IUser.ts"; + +export class RoomExtender implements IRoomExtender { + public kind: RocketChatAssociationModel.ROOM; + + private members: Array; + + constructor(private room: IRoom) { + this.kind = RocketChatAssociationModel.ROOM; + this.members = []; + } + + public addCustomField(key: string, value: unknown): IRoomExtender { + if (!this.room.customFields) { + this.room.customFields = {}; + } + + if (this.room.customFields[key]) { + throw new Error(`The room 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.room.customFields[key] = value; + + return this; + } + + public addMember(user: IUser): IRoomExtender { + if (this.members.find((u) => u.username === user.username)) { + throw new Error('The user is already in the room.'); + } + + this.members.push(user); + + return this; + } + + public getMembersBeingAdded(): Array { + return this.members; + } + + public getUsernamesOfMembersBeingAdded(): Array { + return this.members.map((u) => u.username); + } + + public getRoom(): IRoom { + return structuredClone(this.room); + } +} diff --git a/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts b/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts new file mode 100644 index 000000000..5e1027541 --- /dev/null +++ b/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts @@ -0,0 +1,57 @@ +import type { IVideoConferenceExtender } from "@rocket.chat/apps-engine/definition/accessors/IVideoConferenceExtend.ts"; +import type { VideoConference, VideoConferenceMember } from "@rocket.chat/apps-engine/definition/videoConferences/IVideoConference.ts"; +import type { IVideoConferenceUser } from "@rocket.chat/apps-engine/definition/videoConferences/IVideoConferenceUser.ts"; +import { RocketChatAssociationModel } from "@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts"; + +export class VideoConferenceExtender implements IVideoConferenceExtender { + public kind: RocketChatAssociationModel.VIDEO_CONFERENCE; + + constructor(private videoConference: VideoConference) { + this.kind = RocketChatAssociationModel.VIDEO_CONFERENCE; + } + + public setProviderData(value: Record): IVideoConferenceExtender { + this.videoConference.providerData = value; + + return this; + } + + public setStatus(value: VideoConference['status']): IVideoConferenceExtender { + this.videoConference.status = value; + + return this; + } + + public setEndedBy(value: IVideoConferenceUser['_id']): IVideoConferenceExtender { + this.videoConference.endedBy = { + _id: value, + // Name and username will be loaded automatically by the bridge + username: '', + name: '', + }; + + return this; + } + + public setEndedAt(value: VideoConference['endedAt']): IVideoConferenceExtender { + this.videoConference.endedAt = value; + + return this; + } + + public addUser(userId: VideoConferenceMember['_id'], ts?: VideoConferenceMember['ts']): IVideoConferenceExtender { + this.videoConference.users.push({ + _id: userId, + ts, + // Name and username will be loaded automatically by the bridge + username: '', + name: '', + }); + + return this; + } + + public getVideoConference(): VideoConference { + return structuredClone(this.videoConference); + } +} diff --git a/deno-runtime/lib/accessors/mod.ts b/deno-runtime/lib/accessors/mod.ts index 5418e776f..95ac7f7d6 100644 --- a/deno-runtime/lib/accessors/mod.ts +++ b/deno-runtime/lib/accessors/mod.ts @@ -16,6 +16,7 @@ import * as Messenger from '../messenger.ts'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import { ModifyCreator } from "./modify/ModifyCreator.ts"; import { ModifyUpdater } from "./modify/ModifyUpdater.ts"; +import { ModifyExtender } from "./modify/ModifyExtender.ts"; const httpMethods = ['get', 'post', 'put', 'delete', 'head', 'options', 'patch'] as const; @@ -31,6 +32,7 @@ export class AppAccessors { private http?: IHttp; private creator?: ModifyCreator; private updater?: ModifyUpdater; + private extender?: ModifyExtender; private proxify: (namespace: string) => T; @@ -198,8 +200,8 @@ export class AppAccessors { this.modifier = { getCreator: this.getCreator.bind(this), getUpdater: this.getUpdater.bind(this), + getExtender: this.getExtender.bind(this), getDeleter: () => this.proxify('getModifier:getDeleter'), - getExtender: () => this.proxify('getModifier:getExtender'), // can't be proxy getNotifier: () => this.proxify('getModifier:getNotifier'), getUiController: () => this.proxify('getModifier:getUiController'), getScheduler: () => this.proxify('getModifier:getScheduler'), @@ -242,6 +244,14 @@ export class AppAccessors { return this.updater; } + + private getExtender() { + if (!this.extender) { + this.extender = new ModifyExtender(this.senderFn); + } + + return this.extender; + } } export const AppAccessorsInstance = new AppAccessors(Messenger.sendRequest); diff --git a/deno-runtime/lib/accessors/modify/ModifyCreator.ts b/deno-runtime/lib/accessors/modify/ModifyCreator.ts index 42614f228..69f909327 100644 --- a/deno-runtime/lib/accessors/modify/ModifyCreator.ts +++ b/deno-runtime/lib/accessors/modify/ModifyCreator.ts @@ -16,14 +16,15 @@ import { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/acc 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 { BlockBuilder } from '../builders/BlockBuilder.ts'; +import { MessageBuilder } from '../builders/MessageBuilder.ts'; +import { DiscussionBuilder, IDiscussionBuilder } from '../builders/DiscussionBuilder.ts'; +import { ILivechatMessage, LivechatMessageBuilder } from '../builders/LivechatMessageBuilder.ts'; +import { RoomBuilder } from '../builders/RoomBuilder.ts'; +import { UserBuilder } from '../builders/UserBuilder.ts'; +import { AppVideoConference, VideoConferenceBuilder } from '../builders/VideoConferenceBuilder.ts'; import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; const require = createRequire(import.meta.url); diff --git a/deno-runtime/lib/accessors/modify/ModifyExtender.ts b/deno-runtime/lib/accessors/modify/ModifyExtender.ts new file mode 100644 index 000000000..e380b53e5 --- /dev/null +++ b/deno-runtime/lib/accessors/modify/ModifyExtender.ts @@ -0,0 +1,84 @@ +import type { IModifyExtender } from '@rocket.chat/apps-engine/definition/accessors/IModifyExtender.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; +import type { IMessageExtender } from "@rocket.chat/apps-engine/definition/accessors/IMessageExtender.ts"; +import type { IRoomExtender } from "@rocket.chat/apps-engine/definition/accessors/IRoomExtender.ts"; +import type { IVideoConferenceExtender } from "@rocket.chat/apps-engine/definition/accessors/IVideoConferenceExtend.ts"; +import type { IUser } from "@rocket.chat/apps-engine/definition/users/IUser.ts"; +import type { VideoConference } from "@rocket.chat/apps-engine/definition/videoConferences/IVideoConference.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 * as Messenger from '../../messenger.ts'; +import { AppObjectRegistry } from "../../../AppObjectRegistry.ts"; +import { MessageExtender } from "../extenders/MessageExtender.ts"; +import { RoomExtender } from "../extenders/RoomExtender.ts"; +import { VideoConferenceExtender } from "../extenders/VideoConferenceExtend.ts"; + +export class ModifyExtender implements IModifyExtender { + constructor(private readonly senderFn: typeof Messenger.sendRequest) {} + + public async extendMessage(messageId: string, updater: IUser): Promise { + const result = await this.senderFn({ + method: 'bridges:getMessageBridge:doGetById', + params: [messageId, AppObjectRegistry.get('appId')], + }); + + const msg = result.result as IMessage; + + msg.editor = updater; + msg.editedAt = new Date(); + + return new MessageExtender(msg); + } + + public async extendRoom(roomId: string, _updater: IUser): Promise { + const result = await this.senderFn({ + method: 'bridges:getRoomBridge:doGetById', + params: [roomId, AppObjectRegistry.get('appId')], + }); + + const room = result.result as IRoom; + + room.updatedAt = new Date(); + + return new RoomExtender(room); + } + + public async extendVideoConference(id: string): Promise { + const result = await this.senderFn({ + method: 'bridges:getVideoConferenceBridge:doGetById', + params: [id, AppObjectRegistry.get('appId')], + }); + + const call = result.result as VideoConference; + + call._updatedAt = new Date(); + + return new VideoConferenceExtender(call); + } + + public async finish(extender: IMessageExtender | IRoomExtender | IVideoConferenceExtender): Promise { + switch (extender.kind) { + case RocketChatAssociationModel.MESSAGE: + await this.senderFn({ + method: 'bridges:getMessageBridge:doUpdate', + params: [(extender as IMessageExtender).getMessage(), AppObjectRegistry.get('appId')], + }); + break; + case RocketChatAssociationModel.ROOM: + await this.senderFn({ + method: 'bridges:getRoomBridge:doUpdate', + params: [(extender as IRoomExtender).getRoom(), (extender as IRoomExtender).getUsernamesOfMembersBeingAdded(), AppObjectRegistry.get('appId')], + }); + break; + case RocketChatAssociationModel.VIDEO_CONFERENCE: + await this.senderFn({ + method: 'bridges:getVideoConferenceBridge:doUpdate', + params: [(extender as IVideoConferenceExtender).getVideoConference(), AppObjectRegistry.get('appId')], + }); + break; + default: + throw new Error('Invalid extender passed to the ModifyExtender.finish function.'); + } + } +} diff --git a/deno-runtime/lib/accessors/modify/ModifyUpdater.ts b/deno-runtime/lib/accessors/modify/ModifyUpdater.ts index 5cd23d86d..11188a673 100644 --- a/deno-runtime/lib/accessors/modify/ModifyUpdater.ts +++ b/deno-runtime/lib/accessors/modify/ModifyUpdater.ts @@ -13,8 +13,9 @@ 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 { MessageBuilder } from '../builders/MessageBuilder.ts'; +import { RoomBuilder } from '../builders/RoomBuilder.ts'; import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; const require = createRequire(import.meta.url); diff --git a/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts b/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts new file mode 100644 index 000000000..c318b9f33 --- /dev/null +++ b/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts @@ -0,0 +1,120 @@ +// 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 { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { ModifyExtender } from '../modify/ModifyExtender.ts'; + +describe('ModifyExtender', () => { + let extender: ModifyExtender; + + const senderFn = (r: any) => + Promise.resolve({ + id: Math.random().toString(36).substring(2), + jsonrpc: '2.0', + result: structuredClone(r), + serialize() { + return JSON.stringify(this); + }, + }); + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('appId', 'deno-test'); + extender = new ModifyExtender(senderFn); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('correctly formats requests for the extend message requests', async () => { + const _spy = spy(extender, 'senderFn' as keyof ModifyExtender); + + const messageExtender = await extender.extendMessage('message-id', { _id: 'user-id' } as any); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getMessageBridge:doGetById', + params: ['message-id', 'deno-test'], + }, + ], + }); + + messageExtender.addCustomField('key', 'value'); + + await extender.finish(messageExtender); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getMessageBridge:doUpdate', + params: [messageExtender.getMessage(), 'deno-test'], + }, + ], + }); + + _spy.restore(); + }); + + it('correctly formats requests for the extend room requests', async () => { + const _spy = spy(extender, 'senderFn' as keyof ModifyExtender); + + const roomExtender = await extender.extendRoom('room-id', { _id: 'user-id' } as any); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getRoomBridge:doGetById', + params: ['room-id', 'deno-test'], + }, + ], + }); + + roomExtender.addCustomField('key', 'value'); + + await extender.finish(roomExtender); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getRoomBridge:doUpdate', + params: [roomExtender.getRoom(), [], 'deno-test'], + }, + ], + }); + + _spy.restore(); + }); + + it('correctly formats requests for the extend video conference requests', async () => { + const _spy = spy(extender, 'senderFn' as keyof ModifyExtender); + + const videoConferenceExtender = await extender.extendVideoConference('video-conference-id'); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getVideoConferenceBridge:doGetById', + params: ['video-conference-id', 'deno-test'], + }, + ], + }); + + videoConferenceExtender.setStatus(4); + + await extender.finish(videoConferenceExtender); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getVideoConferenceBridge:doUpdate', + params: [videoConferenceExtender.getVideoConference(), 'deno-test'], + }, + ], + }); + + _spy.restore(); + }); +}); diff --git a/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts b/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts index 99ef7e556..712b9cb8f 100644 --- a/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts +++ b/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts @@ -13,7 +13,7 @@ describe('ModifyUpdater', () => { Promise.resolve({ id: Math.random().toString(36).substring(2), jsonrpc: '2.0', - result: r, + result: structuredClone(r), serialize() { return JSON.stringify(this); },