From 65d2a453f0f0a5236d710ae0ef0587e6aabcb19d Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Fri, 20 Sep 2024 10:56:48 -0300 Subject: [PATCH 001/291] chore: update E2EE setting text (#33226) --- packages/i18n/src/locales/en.i18n.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 8ce6bea2e117..9f37642263da 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1788,8 +1788,8 @@ "Duplicated_Email_address_will_be_ignored": "Duplicated email address will be ignored.", "Markdown_Marked_Tables": "Enable Marked Tables", "duplicated-account": "Duplicated account", - "E2E_Allow_Unencrypted_Messages": "Unencrypted messages in encrypted rooms", - "E2E_Allow_Unencrypted_Messages_Description": "Allow plain text messages to be sent in encrypted rooms. These messages will not be encrypted.", + "E2E_Allow_Unencrypted_Messages": "Access unencrypted content in encrypted rooms", + "E2E_Allow_Unencrypted_Messages_Description": "Allow access to encrypted rooms to people without room encryption keys. They'll be able to see and send unencrypted messages.", "E2E Encryption": "E2E Encryption", "E2E_Encryption_enabled_for_room": "End-to-end encryption enabled for #{{roomName}}", "E2E_Encryption_disabled_for_room": "End-to-end encryption disabled for #{{roomName}}", From 9bcb802fdcd2a458ae8f8cd69d94173b4f40861d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= <43561537+rique223@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:58:34 -0300 Subject: [PATCH 002/291] feat: Implement proper accessbility for report user modal (#33294) Co-authored-by: Tasso Evangelista <2263066+tassoevan@users.noreply.github.com> --- .changeset/late-hats-carry.md | 6 ++++ .../UserInfo/ReportUserModal.tsx | 31 +++++++++++-------- packages/i18n/src/locales/en.i18n.json | 2 ++ 3 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 .changeset/late-hats-carry.md diff --git a/.changeset/late-hats-carry.md b/.changeset/late-hats-carry.md new file mode 100644 index 000000000000..ec24c7cd5376 --- /dev/null +++ b/.changeset/late-hats-carry.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Improves the accessibility of the report user modal by adding an appropriate label, description, and ARIA attributes. diff --git a/apps/meteor/client/views/room/contextualBar/UserInfo/ReportUserModal.tsx b/apps/meteor/client/views/room/contextualBar/UserInfo/ReportUserModal.tsx index 5f94f7c407b0..86b4571d88d1 100644 --- a/apps/meteor/client/views/room/contextualBar/UserInfo/ReportUserModal.tsx +++ b/apps/meteor/client/views/room/contextualBar/UserInfo/ReportUserModal.tsx @@ -1,4 +1,4 @@ -import { Box, FieldGroup, Field, FieldLabel, FieldRow, FieldError, TextAreaInput } from '@rocket.chat/fuselage'; +import { Box, FieldGroup, Field, FieldLabel, FieldRow, FieldError, TextAreaInput, FieldDescription } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { UserAvatar } from '@rocket.chat/ui-avatar'; import type { ComponentProps } from 'react'; @@ -45,27 +45,32 @@ const ReportUserModal = ({ username, displayName, onConfirm, onClose }: ReportUs onCancel={onClose} confirmText={t('Report')} > + + + + {displayName} + + - - - - - {displayName} - - - + {t('Report_reason')} + {t('Let_moderators_know_what_the_issue_is')} - {errors.reasonForReport && {errors.reasonForReport.message}} + {errors.reasonForReport && ( + + {errors.reasonForReport.message} + + )} diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 9f37642263da..0e99c1bdc1d8 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -3199,6 +3199,7 @@ "leave-p": "Leave Private Groups", "leave-p_description": "Permission to leave private groups", "Lets_get_you_new_one_": "Let's get you a new one!", + "Let_moderators_know_what_the_issue_is": "Let moderators know what the issue is", "Let_them_know": "Let them know", "Left": "Left", "License": "License", @@ -4490,6 +4491,7 @@ "Report_exclamation_mark": "Report!", "Report_has_been_sent": "Report has been sent", "Report_Number": "Report Number", + "Report_reason": "Report reason", "Report_this_message_question_mark": "Report this message?", "Report_User": "Report user", "Reporting": "Reporting", From 827850d545043896722b53d2ed81af3e4d0738b7 Mon Sep 17 00:00:00 2001 From: "Julio A." <52619625+julio-cfa@users.noreply.github.com> Date: Mon, 23 Sep 2024 21:36:58 +0200 Subject: [PATCH 003/291] fix: imported fixes (#33330) --- .changeset/little-bottles-peel.md | 5 +++ apps/meteor/app/api/server/v1/rooms.ts | 9 +++- .../lib/server/methods/cleanRoomHistory.ts | 8 ++++ .../content/urlPreviews/OEmbedHtmlPreview.tsx | 11 ++++- apps/meteor/tests/end-to-end/api/methods.ts | 43 ++++++++++++++++++- apps/meteor/tests/end-to-end/api/rooms.ts | 28 ++++++++++++ 6 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 .changeset/little-bottles-peel.md diff --git a/.changeset/little-bottles-peel.md b/.changeset/little-bottles-peel.md new file mode 100644 index 000000000000..eacb88108a0f --- /dev/null +++ b/.changeset/little-bottles-peel.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 3dc62e462ddf..117ae3851c43 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -40,7 +40,7 @@ import { findRoomsAvailableForTeams, } from '../lib/rooms'; -async function findRoomByIdOrName({ +export async function findRoomByIdOrName({ params, checkedArchived = true, }: { @@ -365,7 +365,12 @@ API.v1.addRoute( { authRequired: true, validateParams: isRoomsCleanHistoryProps }, { async post() { - const { _id } = await findRoomByIdOrName({ params: this.bodyParams }); + const room = await findRoomByIdOrName({ params: this.bodyParams }); + const { _id } = room; + + if (!room || !(await canAccessRoomAsync(room, { _id: this.userId }))) { + return API.v1.failure('User does not have access to the room [error-not-allowed]', 'error-not-allowed'); + } const { latest, diff --git a/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts b/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts index d6136eee9131..c804128d27bd 100644 --- a/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts +++ b/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts @@ -2,6 +2,8 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { findRoomByIdOrName } from '../../../api/server/v1/rooms'; +import { canAccessRoomAsync } from '../../../authorization/server'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { cleanRoomHistory } from '../functions/cleanRoomHistory'; @@ -56,6 +58,12 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'cleanRoomHistory' }); } + const room = await findRoomByIdOrName({ params: { roomId } }); + + if (!room || !(await canAccessRoomAsync(room, { _id: userId }))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'cleanRoomHistory' }); + } + return cleanRoomHistory({ rid: roomId, latest, diff --git a/apps/meteor/client/components/message/content/urlPreviews/OEmbedHtmlPreview.tsx b/apps/meteor/client/components/message/content/urlPreviews/OEmbedHtmlPreview.tsx index e8dd4e1ddcc2..518f1ebad203 100644 --- a/apps/meteor/client/components/message/content/urlPreviews/OEmbedHtmlPreview.tsx +++ b/apps/meteor/client/components/message/content/urlPreviews/OEmbedHtmlPreview.tsx @@ -1,12 +1,21 @@ import { Box } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import React from 'react'; import OEmbedCollapsible from './OEmbedCollapsible'; import type { OEmbedPreviewMetadata } from './OEmbedPreviewMetadata'; +const purifyOptions = { + ADD_TAGS: ['iframe'], + ADD_ATTR: ['frameborder', 'allow', 'allowfullscreen', 'scrolling', 'src', 'style', 'referrerpolicy'], + ALLOW_UNKNOWN_PROTOCOLS: true, +}; + const OEmbedHtmlPreview = ({ html, ...props }: OEmbedPreviewMetadata): ReactElement => ( - {html && } + + {html && } + ); export default OEmbedHtmlPreview; diff --git a/apps/meteor/tests/end-to-end/api/methods.ts b/apps/meteor/tests/end-to-end/api/methods.ts index 08945994e438..e3c42389e506 100644 --- a/apps/meteor/tests/end-to-end/api/methods.ts +++ b/apps/meteor/tests/end-to-end/api/methods.ts @@ -616,9 +616,19 @@ describe('Meteor.methods', () => { describe('[@cleanRoomHistory]', () => { let rid: IRoom['_id']; - + let testUser: IUser; + let testUserCredentials: Credentials; let channelName: string; + before('update permissions', async () => { + await updatePermission('clean-channel-history', ['admin', 'user']); + }); + + before('create test user', async () => { + testUser = await createUser(); + testUserCredentials = await login(testUser.username, password); + }); + before('create room', (done) => { channelName = `methods-test-channel-${Date.now()}`; void request @@ -676,7 +686,36 @@ describe('Meteor.methods', () => { .end(done); }); - after(() => deleteRoom({ type: 'p', roomId: rid })); + after(() => + Promise.all([deleteRoom({ type: 'p', roomId: rid }), deleteUser(testUser), updatePermission('clean-channel-history', ['admin'])]), + ); + + it('should throw an error if user is not part of the room', async () => { + await request + .post(methodCall('cleanRoomHistory')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'cleanRoomHistory', + params: [ + { + roomId: rid, + oldest: { $date: new Date().getTime() }, + latest: { $date: new Date().getTime() }, + }, + ], + id: 'id', + msg: 'method', + }), + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error').that.is.an('object'); + expect(data.error).to.have.a.property('error', 'error-not-allowed'); + }); + }); it('should not change the _updatedAt value when nothing is changed on the room', async () => { const roomBefore = await request.get(api('groups.info')).set(credentials).query({ diff --git a/apps/meteor/tests/end-to-end/api/rooms.ts b/apps/meteor/tests/end-to-end/api/rooms.ts index 15f85964ffff..5047af7956d8 100644 --- a/apps/meteor/tests/end-to-end/api/rooms.ts +++ b/apps/meteor/tests/end-to-end/api/rooms.ts @@ -1133,6 +1133,34 @@ describe('[Rooms]', () => { }) .end(done); }); + describe('test user is not part of room', async () => { + beforeEach(async () => { + await updatePermission('clean-channel-history', ['admin', 'user']); + }); + + afterEach(async () => { + await updatePermission('clean-channel-history', ['admin']); + }); + + it('should return an error when the user with right privileges is not part of the room', async () => { + await request + .post(api('rooms.cleanHistory')) + .set(userCredentials) + .send({ + roomId: privateChannel._id, + latest: '9999-12-31T23:59:59.000Z', + oldest: '0001-01-01T00:00:00.000Z', + limit: 2000, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-not-allowed'); + expect(res.body).to.have.property('error', 'User does not have access to the room [error-not-allowed]'); + }); + }); + }); }); describe('[/rooms.info]', () => { let testChannel: IRoom; From a6b91525a8c28a640a6e378a912abc06c4ef6bae Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Mon, 23 Sep 2024 19:00:02 -0300 Subject: [PATCH 004/291] chore: create network broker package (#33338) --- _templates/service/new/package.json.ejs.t | 1 + _templates/service/new/service.ejs.t | 4 +- .../ee/server/services/ecdh-proxy/service.ts | 3 +- apps/meteor/ee/server/services/package.json | 1 + apps/meteor/ee/server/startup/index.ts | 2 +- apps/meteor/package.json | 1 + ee/apps/account-service/Dockerfile | 3 ++ ee/apps/account-service/package.json | 1 + ee/apps/account-service/src/service.ts | 2 +- ee/apps/authorization-service/Dockerfile | 3 ++ ee/apps/authorization-service/package.json | 1 + ee/apps/authorization-service/src/service.ts | 2 +- ee/apps/ddp-streamer/Dockerfile | 3 ++ ee/apps/ddp-streamer/package.json | 1 + ee/apps/ddp-streamer/src/service.ts | 2 +- ee/apps/omnichannel-transcript/Dockerfile | 3 ++ ee/apps/omnichannel-transcript/package.json | 1 + ee/apps/omnichannel-transcript/src/service.ts | 2 +- ee/apps/presence-service/Dockerfile | 3 ++ ee/apps/presence-service/package.json | 1 + ee/apps/presence-service/src/service.ts | 2 +- ee/apps/queue-worker/Dockerfile | 3 ++ ee/apps/queue-worker/package.json | 1 + ee/apps/queue-worker/src/service.ts | 2 +- ee/apps/stream-hub-service/Dockerfile | 3 ++ ee/apps/stream-hub-service/package.json | 1 + ee/apps/stream-hub-service/src/service.ts | 2 +- ee/packages/network-broker/.eslintrc.json | 4 ++ ee/packages/network-broker/jest.config.ts | 6 +++ ee/packages/network-broker/package.json | 39 +++++++++++++++++++ .../network-broker/src}/EnterpriseCheck.ts | 0 .../network-broker/src/NetworkBroker.test.ts | 4 +- .../network-broker/src}/NetworkBroker.ts | 16 +++++--- .../packages/network-broker/src/index.ts | 2 +- ee/packages/network-broker/tsconfig.json | 9 +++++ yarn.lock | 31 +++++++++++++++ 36 files changed, 145 insertions(+), 20 deletions(-) create mode 100644 ee/packages/network-broker/.eslintrc.json create mode 100644 ee/packages/network-broker/jest.config.ts create mode 100644 ee/packages/network-broker/package.json rename {apps/meteor/ee/server/lib => ee/packages/network-broker/src}/EnterpriseCheck.ts (100%) rename apps/meteor/ee/tests/unit/server/NetworkBroker.tests.ts => ee/packages/network-broker/src/NetworkBroker.test.ts (85%) rename {apps/meteor/ee/server => ee/packages/network-broker/src}/NetworkBroker.ts (94%) rename apps/meteor/ee/server/startup/broker.ts => ee/packages/network-broker/src/index.ts (98%) create mode 100644 ee/packages/network-broker/tsconfig.json diff --git a/_templates/service/new/package.json.ejs.t b/_templates/service/new/package.json.ejs.t index 2c74278d1ced..0aa1bd69e995 100644 --- a/_templates/service/new/package.json.ejs.t +++ b/_templates/service/new/package.json.ejs.t @@ -20,6 +20,7 @@ to: ee/apps/<%= name %>/package.json "dependencies": { "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/emitter": "next", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", diff --git a/_templates/service/new/service.ejs.t b/_templates/service/new/service.ejs.t index 54080d94cf08..77f02d2b7769 100644 --- a/_templates/service/new/service.ejs.t +++ b/_templates/service/new/service.ejs.t @@ -1,11 +1,11 @@ --- to: ee/apps/<%= name %>/src/service.ts --- +import { api } from '@rocket.chat/core-services'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; -import { api } from '@rocket.chat/core-services'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; diff --git a/apps/meteor/ee/server/services/ecdh-proxy/service.ts b/apps/meteor/ee/server/services/ecdh-proxy/service.ts index 7ef3e8d26dcc..a795f157c9a5 100755 --- a/apps/meteor/ee/server/services/ecdh-proxy/service.ts +++ b/apps/meteor/ee/server/services/ecdh-proxy/service.ts @@ -1,5 +1,4 @@ -import '../../startup/broker'; - +import '@rocket.chat/network-broker'; import { api } from '@rocket.chat/core-services'; import { ECDHProxy } from './ECDHProxy'; diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 43659382eb67..a2adb5872ad2 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -25,6 +25,7 @@ "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@rocket.chat/ui-kit": "workspace:~", diff --git a/apps/meteor/ee/server/startup/index.ts b/apps/meteor/ee/server/startup/index.ts index a8091f0e9a37..eb09ca6ed30f 100644 --- a/apps/meteor/ee/server/startup/index.ts +++ b/apps/meteor/ee/server/startup/index.ts @@ -13,7 +13,7 @@ import { isRunningMs } from '../../../server/lib/isRunningMs'; export const registerEEBroker = async (): Promise => { // only starts network broker if running in micro services mode if (isRunningMs()) { - const { broker } = await import('./broker'); + const { broker } = await import('@rocket.chat/network-broker'); api.setBroker(broker); void api.start(); diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 9ee7e48d1794..c7da95059475 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -262,6 +262,7 @@ "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/mp3-encoder": "0.24.0", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/omnichannel-services": "workspace:^", "@rocket.chat/onboarding-ui": "~0.33.3", "@rocket.chat/password-policies": "workspace:^", diff --git a/ee/apps/account-service/Dockerfile b/ee/apps/account-service/Dockerfile index acbc5b0371d2..c80d4f2eb376 100644 --- a/ee/apps/account-service/Dockerfile +++ b/ee/apps/account-service/Dockerfile @@ -37,6 +37,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index b9e45ed14eda..6f92b8eb9078 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -20,6 +20,7 @@ "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@rocket.chat/tools": "workspace:^", diff --git a/ee/apps/account-service/src/service.ts b/ee/apps/account-service/src/service.ts index f166233ca137..07ca30ed748f 100755 --- a/ee/apps/account-service/src/service.ts +++ b/ee/apps/account-service/src/service.ts @@ -1,10 +1,10 @@ import { api } from '@rocket.chat/core-services'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; const PORT = process.env.PORT || 3033; diff --git a/ee/apps/authorization-service/Dockerfile b/ee/apps/authorization-service/Dockerfile index c662d8765300..9a9e8ded922c 100644 --- a/ee/apps/authorization-service/Dockerfile +++ b/ee/apps/authorization-service/Dockerfile @@ -37,6 +37,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index b7912b8bc94d..262fb6789f9d 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -20,6 +20,7 @@ "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@types/node": "^14.18.63", diff --git a/ee/apps/authorization-service/src/service.ts b/ee/apps/authorization-service/src/service.ts index 29162b636229..4dcd466afa60 100755 --- a/ee/apps/authorization-service/src/service.ts +++ b/ee/apps/authorization-service/src/service.ts @@ -1,10 +1,10 @@ import { api } from '@rocket.chat/core-services'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; const PORT = process.env.PORT || 3034; diff --git a/ee/apps/ddp-streamer/Dockerfile b/ee/apps/ddp-streamer/Dockerfile index f556cbde6752..32103dc3528b 100644 --- a/ee/apps/ddp-streamer/Dockerfile +++ b/ee/apps/ddp-streamer/Dockerfile @@ -40,6 +40,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 87029e4b8993..84859add8d7b 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -23,6 +23,7 @@ "@rocket.chat/logger": "workspace:^", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "colorette": "^1.4.0", diff --git a/ee/apps/ddp-streamer/src/service.ts b/ee/apps/ddp-streamer/src/service.ts index b5cd20f7ec02..07666a265dbe 100755 --- a/ee/apps/ddp-streamer/src/service.ts +++ b/ee/apps/ddp-streamer/src/service.ts @@ -1,9 +1,9 @@ import { api } from '@rocket.chat/core-services'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; (async () => { const db = await getConnection(); diff --git a/ee/apps/omnichannel-transcript/Dockerfile b/ee/apps/omnichannel-transcript/Dockerfile index 6a93a8e5e8be..9b7e47968e68 100644 --- a/ee/apps/omnichannel-transcript/Dockerfile +++ b/ee/apps/omnichannel-transcript/Dockerfile @@ -37,6 +37,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 27290070b653..ced114bfb589 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -22,6 +22,7 @@ "@rocket.chat/logger": "workspace:^", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/omnichannel-services": "workspace:^", "@rocket.chat/pdf-worker": "workspace:^", "@rocket.chat/tools": "workspace:^", diff --git a/ee/apps/omnichannel-transcript/src/service.ts b/ee/apps/omnichannel-transcript/src/service.ts index 14cbc5b8438a..66456456fb74 100644 --- a/ee/apps/omnichannel-transcript/src/service.ts +++ b/ee/apps/omnichannel-transcript/src/service.ts @@ -1,11 +1,11 @@ import { api } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; const PORT = process.env.PORT || 3036; diff --git a/ee/apps/presence-service/Dockerfile b/ee/apps/presence-service/Dockerfile index aa9c1c0bd6c9..430880d29606 100644 --- a/ee/apps/presence-service/Dockerfile +++ b/ee/apps/presence-service/Dockerfile @@ -40,6 +40,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index 5968631341b0..d8e47cc5c5ea 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -20,6 +20,7 @@ "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/presence": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@types/node": "^14.18.63", diff --git a/ee/apps/presence-service/src/service.ts b/ee/apps/presence-service/src/service.ts index b7275a29106f..0e1c97f2daa2 100755 --- a/ee/apps/presence-service/src/service.ts +++ b/ee/apps/presence-service/src/service.ts @@ -1,10 +1,10 @@ import { api } from '@rocket.chat/core-services'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; const PORT = process.env.PORT || 3031; diff --git a/ee/apps/queue-worker/Dockerfile b/ee/apps/queue-worker/Dockerfile index 6a93a8e5e8be..9b7e47968e68 100644 --- a/ee/apps/queue-worker/Dockerfile +++ b/ee/apps/queue-worker/Dockerfile @@ -37,6 +37,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index b3d8b0aff94e..4270818a6eb3 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -22,6 +22,7 @@ "@rocket.chat/logger": "workspace:^", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/omnichannel-services": "workspace:^", "@types/node": "^14.18.63", "ejson": "^2.2.3", diff --git a/ee/apps/queue-worker/src/service.ts b/ee/apps/queue-worker/src/service.ts index 8583257a6860..4bc6c9642913 100644 --- a/ee/apps/queue-worker/src/service.ts +++ b/ee/apps/queue-worker/src/service.ts @@ -1,11 +1,11 @@ import { api } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; const PORT = process.env.PORT || 3038; diff --git a/ee/apps/stream-hub-service/Dockerfile b/ee/apps/stream-hub-service/Dockerfile index c662d8765300..9a9e8ded922c 100644 --- a/ee/apps/stream-hub-service/Dockerfile +++ b/ee/apps/stream-hub-service/Dockerfile @@ -37,6 +37,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 9fd5cf0586f6..66443ebeec68 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -21,6 +21,7 @@ "@rocket.chat/logger": "workspace:^", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@types/node": "^14.18.63", "ejson": "^2.2.3", diff --git a/ee/apps/stream-hub-service/src/service.ts b/ee/apps/stream-hub-service/src/service.ts index 4975b5b306be..eade703321d2 100755 --- a/ee/apps/stream-hub-service/src/service.ts +++ b/ee/apps/stream-hub-service/src/service.ts @@ -1,11 +1,11 @@ import { api } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; import { DatabaseWatcher } from '../../../../apps/meteor/server/database/DatabaseWatcher'; import { StreamHub } from './StreamHub'; diff --git a/ee/packages/network-broker/.eslintrc.json b/ee/packages/network-broker/.eslintrc.json new file mode 100644 index 000000000000..a83aeda48e66 --- /dev/null +++ b/ee/packages/network-broker/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/ee/packages/network-broker/jest.config.ts b/ee/packages/network-broker/jest.config.ts new file mode 100644 index 000000000000..c18c8ae02465 --- /dev/null +++ b/ee/packages/network-broker/jest.config.ts @@ -0,0 +1,6 @@ +import server from '@rocket.chat/jest-presets/server'; +import type { Config } from 'jest'; + +export default { + preset: server.preset, +} satisfies Config; diff --git a/ee/packages/network-broker/package.json b/ee/packages/network-broker/package.json new file mode 100644 index 000000000000..c4d3ea1b284b --- /dev/null +++ b/ee/packages/network-broker/package.json @@ -0,0 +1,39 @@ +{ + "name": "@rocket.chat/network-broker", + "version": "0.1.0", + "private": true, + "devDependencies": { + "@rocket.chat/eslint-config": "workspace:^", + "@types/chai": "~4.3.19", + "@types/ejson": "^2.2.2", + "@types/node": "^14.18.63", + "@types/sinon": "^10.0.20", + "chai": "^4.3.10", + "eslint": "~8.45.0", + "jest": "~29.7.0", + "sinon": "^14.0.2", + "typescript": "~5.5.4" + }, + "scripts": { + "lint": "eslint src", + "lint:fix": "eslint src --fix", + "test": "jest", + "build": "tsc", + "testunit": "jest", + "typecheck": "tsc --noEmit --skipLibCheck" + }, + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "/dist" + ], + "volta": { + "extends": "../../../package.json" + }, + "dependencies": { + "@rocket.chat/core-services": "workspace:^", + "ejson": "^2.2.3", + "moleculer": "^0.14.34", + "pino": "^8.15.0" + } +} diff --git a/apps/meteor/ee/server/lib/EnterpriseCheck.ts b/ee/packages/network-broker/src/EnterpriseCheck.ts similarity index 100% rename from apps/meteor/ee/server/lib/EnterpriseCheck.ts rename to ee/packages/network-broker/src/EnterpriseCheck.ts diff --git a/apps/meteor/ee/tests/unit/server/NetworkBroker.tests.ts b/ee/packages/network-broker/src/NetworkBroker.test.ts similarity index 85% rename from apps/meteor/ee/tests/unit/server/NetworkBroker.tests.ts rename to ee/packages/network-broker/src/NetworkBroker.test.ts index 1aac9c33fc33..c79fb12e7049 100644 --- a/apps/meteor/ee/tests/unit/server/NetworkBroker.tests.ts +++ b/ee/packages/network-broker/src/NetworkBroker.test.ts @@ -2,8 +2,8 @@ import { ServiceClass } from '@rocket.chat/core-services'; import { expect } from 'chai'; import sinon from 'sinon'; -import { BrokerMocked } from '../../../../tests/mocks/server/BrokerMocked'; -import { NetworkBroker } from '../../../server/NetworkBroker'; +import { BrokerMocked } from '../../../../apps/meteor/tests/mocks/server/BrokerMocked'; +import { NetworkBroker } from './NetworkBroker'; class DelayedStopBroker extends BrokerMocked { async destroyService(name: string) { diff --git a/apps/meteor/ee/server/NetworkBroker.ts b/ee/packages/network-broker/src/NetworkBroker.ts similarity index 94% rename from apps/meteor/ee/server/NetworkBroker.ts rename to ee/packages/network-broker/src/NetworkBroker.ts index 0fed6fca542d..e326357cba0a 100644 --- a/apps/meteor/ee/server/NetworkBroker.ts +++ b/ee/packages/network-broker/src/NetworkBroker.ts @@ -2,7 +2,7 @@ import { asyncLocalStorage } from '@rocket.chat/core-services'; import type { IBroker, IBrokerNode, IServiceMetrics, IServiceClass, EventSignatures } from '@rocket.chat/core-services'; import type { ServiceBroker, Context, ServiceSchema } from 'moleculer'; -import { EnterpriseCheck } from './lib/EnterpriseCheck'; +import { EnterpriseCheck } from './EnterpriseCheck'; const events: { [k: string]: string } = { onNodeConnected: '$node.connected', @@ -25,7 +25,7 @@ const waitForServicesTimeout = parseInt(WAIT_FOR_SERVICES_TIMEOUT, 10) || 10000; export class NetworkBroker implements IBroker { private broker: ServiceBroker; - private started: Promise; + private started: Promise = Promise.resolve(false); metrics: IServiceMetrics; @@ -36,7 +36,9 @@ export class NetworkBroker implements IBroker { } async call(method: string, data: any): Promise { - await this.started; + if (!(await this.started)) { + return; + } const context = asyncLocalStorage.getStore(); @@ -54,7 +56,9 @@ export class NetworkBroker implements IBroker { } async waitAndCall(method: string, data: any): Promise { - await this.started; + if (!(await this.started)) { + return; + } try { await this.broker.waitForServices(method.split('.')[0], waitForServicesTimeout); @@ -182,6 +186,8 @@ export class NetworkBroker implements IBroker { } async start(): Promise { - this.started = this.broker.start(); + await this.broker.start(); + + this.started = Promise.resolve(true); } } diff --git a/apps/meteor/ee/server/startup/broker.ts b/ee/packages/network-broker/src/index.ts similarity index 98% rename from apps/meteor/ee/server/startup/broker.ts rename to ee/packages/network-broker/src/index.ts index daae4ace4e05..caa12890b514 100644 --- a/apps/meteor/ee/server/startup/broker.ts +++ b/ee/packages/network-broker/src/index.ts @@ -3,7 +3,7 @@ import EJSON from 'ejson'; import { Errors, Serializers, ServiceBroker } from 'moleculer'; import { pino } from 'pino'; -import { NetworkBroker } from '../NetworkBroker'; +import { NetworkBroker } from './NetworkBroker'; const { MS_NAMESPACE = '', diff --git a/ee/packages/network-broker/tsconfig.json b/ee/packages/network-broker/tsconfig.json new file mode 100644 index 000000000000..ada83b80ff89 --- /dev/null +++ b/ee/packages/network-broker/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.server.json", + "compilerOptions": { + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + }, + "files": ["./src/index.ts"] +} diff --git a/yarn.lock b/yarn.lock index 6b1123701996..a7d363694243 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8441,6 +8441,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@rocket.chat/tools": "workspace:^" @@ -8556,6 +8557,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@types/gc-stats": ^1.4.3 @@ -8719,6 +8721,7 @@ __metadata: "@rocket.chat/logger": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@types/ejson": ^2.2.2 @@ -9396,6 +9399,7 @@ __metadata: "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/mp3-encoder": 0.24.0 + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/omnichannel-services": "workspace:^" "@rocket.chat/onboarding-ui": ~0.33.3 "@rocket.chat/password-policies": "workspace:^" @@ -9771,6 +9775,27 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/network-broker@workspace:^, @rocket.chat/network-broker@workspace:ee/packages/network-broker": + version: 0.0.0-use.local + resolution: "@rocket.chat/network-broker@workspace:ee/packages/network-broker" + dependencies: + "@rocket.chat/core-services": "workspace:^" + "@rocket.chat/eslint-config": "workspace:^" + "@types/chai": ~4.3.19 + "@types/ejson": ^2.2.2 + "@types/node": ^14.18.63 + "@types/sinon": ^10.0.20 + chai: ^4.3.10 + ejson: ^2.2.3 + eslint: ~8.45.0 + jest: ~29.7.0 + moleculer: ^0.14.34 + pino: ^8.15.0 + sinon: ^14.0.2 + typescript: ~5.5.4 + languageName: unknown + linkType: soft + "@rocket.chat/omnichannel-services@workspace:^, @rocket.chat/omnichannel-services@workspace:ee/packages/omnichannel-services": version: 0.0.0-use.local resolution: "@rocket.chat/omnichannel-services@workspace:ee/packages/omnichannel-services" @@ -9817,6 +9842,7 @@ __metadata: "@rocket.chat/logger": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/omnichannel-services": "workspace:^" "@rocket.chat/pdf-worker": "workspace:^" "@rocket.chat/tools": "workspace:^" @@ -9954,6 +9980,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/presence": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@types/gc-stats": ^1.4.3 @@ -10019,6 +10046,7 @@ __metadata: "@rocket.chat/logger": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/omnichannel-services": "workspace:^" "@types/gc-stats": ^1.4.3 "@types/node": ^14.18.63 @@ -10176,6 +10204,7 @@ __metadata: "@rocket.chat/logger": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@types/bcrypt": ^5.0.2 @@ -10493,6 +10522,7 @@ __metadata: "@codemirror/lang-json": ^6.0.1 "@codemirror/tooltip": ^0.19.16 "@lezer/highlight": ^1.1.6 + "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": ~0.31.25 "@rocket.chat/fuselage": ^0.59.0 "@rocket.chat/fuselage-hooks": ^0.33.1 @@ -37207,6 +37237,7 @@ __metadata: "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@rocket.chat/ui-kit": "workspace:~" From 1f89f780bd205d36b572689e1103c860f787a13e Mon Sep 17 00:00:00 2001 From: Lucas Pelegrino Date: Tue, 24 Sep 2024 09:54:31 -0300 Subject: [PATCH 005/291] feat: Adds new admin feature preview setting management (#33212) Co-authored-by: Guilherme Gazzo --- .changeset/quick-rings-wave.md | 7 + .../UserMenu/hooks/useAccountItems.tsx | 4 +- .../hooks/useFeaturePreviewEnableQuery.ts | 28 ++++ .../sidebar/header/hooks/useAccountItems.tsx | 4 +- .../AccountFeaturePreviewBadge.tsx | 21 --- .../AccountFeaturePreviewPage.tsx | 44 ++---- .../client/views/account/sidebarItems.tsx | 5 +- .../AdminFeaturePreviewPage.tsx | 127 ++++++++++++++++++ .../AdminFeaturePreviewRoute.tsx | 26 ++++ apps/meteor/client/views/admin/routes.tsx | 9 ++ .../meteor/client/views/admin/sidebarItems.ts | 8 ++ .../featurePreview/enhanced-navigation.png | Bin 0 -> 2372 bytes .../resizable-contextual-bar.png | Bin 0 -> 4776 bytes .../images/featurePreview/timestamp.png | Bin 0 -> 51432 bytes apps/meteor/server/settings/accounts.ts | 5 + .../tests/end-to-end/api/miscellaneous.ts | 1 + packages/i18n/src/locales/en.i18n.json | 16 ++- packages/i18n/src/locales/hi-IN.i18n.json | 6 +- .../FeaturePreview/FeaturePreviewBadge.tsx | 21 +++ .../src/components/FeaturePreview/index.ts | 2 + packages/ui-client/src/components/index.ts | 2 +- .../useDefaultSettingFeaturePreviewList.ts | 12 ++ .../ui-client/src/hooks/useFeaturePreview.ts | 5 +- .../src/hooks/useFeaturePreviewList.ts | 32 +++-- ... usePreferenceFeaturePreviewList.spec.tsx} | 13 +- .../hooks/usePreferenceFeaturePreviewList.ts | 16 +++ packages/ui-client/src/index.ts | 2 + 27 files changed, 324 insertions(+), 92 deletions(-) create mode 100644 .changeset/quick-rings-wave.md create mode 100644 apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts delete mode 100644 apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx create mode 100644 apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx create mode 100644 apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx create mode 100644 apps/meteor/public/images/featurePreview/enhanced-navigation.png create mode 100644 apps/meteor/public/images/featurePreview/resizable-contextual-bar.png create mode 100644 apps/meteor/public/images/featurePreview/timestamp.png create mode 100644 packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx create mode 100644 packages/ui-client/src/components/FeaturePreview/index.ts create mode 100644 packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts rename packages/ui-client/src/hooks/{useFeaturePreviewList.spec.tsx => usePreferenceFeaturePreviewList.spec.tsx} (79%) create mode 100644 packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts diff --git a/.changeset/quick-rings-wave.md b/.changeset/quick-rings-wave.md new file mode 100644 index 000000000000..0ea22897ff45 --- /dev/null +++ b/.changeset/quick-rings-wave.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +"@rocket.chat/ui-client": minor +--- + +Added new Admin Feature Preview management view, this will allow the workspace admins to both enable feature previewing in the workspace as well as define which feature previews are enabled by default for the users in the workspace. diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx index 82c39c5c1b10..e54e2b72d675 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx @@ -1,7 +1,7 @@ import { Badge } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { defaultFeaturesPreview, useFeaturePreviewList } from '@rocket.chat/ui-client'; import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { defaultFeaturesPreview, usePreferenceFeaturePreviewList } from '@rocket.chat/ui-client'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -9,7 +9,7 @@ export const useAccountItems = (): GenericMenuItemProps[] => { const t = useTranslation(); const router = useRouter(); - const { unseenFeatures, featurePreviewEnabled } = useFeaturePreviewList(); + const { unseenFeatures, featurePreviewEnabled } = usePreferenceFeaturePreviewList(); const handleMyAccount = useEffectEvent(() => { router.navigate('/account'); diff --git a/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts b/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts new file mode 100644 index 000000000000..fd88f0237d29 --- /dev/null +++ b/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts @@ -0,0 +1,28 @@ +import type { FeaturePreviewProps } from '@rocket.chat/ui-client'; +import { useMemo } from 'react'; + +const handleFeaturePreviewEnableQuery = (item: FeaturePreviewProps, _: any, features: FeaturePreviewProps[]) => { + if (item.enableQuery) { + const expected = item.enableQuery.value; + const received = features.find((el) => el.name === item.enableQuery?.name)?.value; + if (expected !== received) { + item.disabled = true; + item.value = false; + } else { + item.disabled = false; + } + } + return item; +}; + +const groupFeaturePreview = (features: FeaturePreviewProps[]) => + Object.entries( + features.reduce((result, currentValue) => { + (result[currentValue.group] = result[currentValue.group] || []).push(currentValue); + return result; + }, {} as Record), + ); + +export const useFeaturePreviewEnableQuery = (features: FeaturePreviewProps[]) => { + return useMemo(() => groupFeaturePreview(features.map(handleFeaturePreviewEnableQuery)), [features]); +}; diff --git a/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx b/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx index 2be6b2b1dea2..51ab7a198a67 100644 --- a/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx +++ b/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx @@ -1,7 +1,7 @@ import { Badge } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { defaultFeaturesPreview, useFeaturePreviewList } from '@rocket.chat/ui-client'; import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { defaultFeaturesPreview, usePreferenceFeaturePreviewList } from '@rocket.chat/ui-client'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -9,7 +9,7 @@ export const useAccountItems = (): GenericMenuItemProps[] => { const t = useTranslation(); const router = useRouter(); - const { unseenFeatures, featurePreviewEnabled } = useFeaturePreviewList(); + const { unseenFeatures, featurePreviewEnabled } = usePreferenceFeaturePreviewList(); const handleMyAccount = useMutableCallback(() => { router.navigate('/account'); diff --git a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx deleted file mode 100644 index c109ca1aefb5..000000000000 --- a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Badge } from '@rocket.chat/fuselage'; -import { useFeaturePreviewList } from '@rocket.chat/ui-client'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -const AccountFeaturePreviewBadge = () => { - const { t } = useTranslation(); - const { unseenFeatures } = useFeaturePreviewList(); - - if (!unseenFeatures) { - return null; - } - - return ( - - {unseenFeatures} - - ); -}; - -export default AccountFeaturePreviewBadge; diff --git a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx index dd9ab6a90959..358d2394003b 100644 --- a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx +++ b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx @@ -1,4 +1,3 @@ -import { css } from '@rocket.chat/css-in-js'; import { ButtonGroup, Button, @@ -13,9 +12,10 @@ import { FieldLabel, FieldRow, FieldHint, + Callout, + Margins, } from '@rocket.chat/fuselage'; -import type { FeaturePreviewProps } from '@rocket.chat/ui-client'; -import { useFeaturePreviewList } from '@rocket.chat/ui-client'; +import { usePreferenceFeaturePreviewList } from '@rocket.chat/ui-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useToastMessageDispatch, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import type { ChangeEvent } from 'react'; @@ -23,26 +23,12 @@ import React, { useEffect, Fragment } from 'react'; import { useForm } from 'react-hook-form'; import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; +import { useFeaturePreviewEnableQuery } from '../../../hooks/useFeaturePreviewEnableQuery'; -const handleEnableQuery = (features: FeaturePreviewProps[]) => { - return features.map((item) => { - if (item.enableQuery) { - const expected = item.enableQuery.value; - const received = features.find((el) => el.name === item.enableQuery?.name)?.value; - if (expected !== received) { - item.disabled = true; - item.value = false; - } else { - item.disabled = false; - } - } - return item; - }); -}; const AccountFeaturePreviewPage = () => { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const { features, unseenFeatures } = useFeaturePreviewList(); + const { features, unseenFeatures } = usePreferenceFeaturePreviewList(); const setUserPreferences = useEndpoint('POST', '/v1/users.setPreferences'); @@ -85,12 +71,7 @@ const AccountFeaturePreviewPage = () => { setValue('featuresPreview', updated, { shouldDirty: true }); }; - const grouppedFeaturesPreview = Object.entries( - handleEnableQuery(featuresPreview).reduce((result, currentValue) => { - (result[currentValue.group] = result[currentValue.group] || []).push(currentValue); - return result; - }, {} as Record), - ); + const grouppedFeaturesPreview = useFeaturePreviewEnableQuery(featuresPreview); return ( @@ -105,14 +86,11 @@ const AccountFeaturePreviewPage = () => { )} {featuresPreview.length > 0 && ( <> - - {t('Feature_preview_page_description')} + + + {t('Feature_preview_page_description')} + {t('Feature_preview_page_callout')} + {grouppedFeaturesPreview?.map(([group, features], index) => ( diff --git a/apps/meteor/client/views/account/sidebarItems.tsx b/apps/meteor/client/views/account/sidebarItems.tsx index ca0376be329d..fa2ab8bd5e40 100644 --- a/apps/meteor/client/views/account/sidebarItems.tsx +++ b/apps/meteor/client/views/account/sidebarItems.tsx @@ -1,10 +1,9 @@ -import { defaultFeaturesPreview } from '@rocket.chat/ui-client'; +import { defaultFeaturesPreview, FeaturePreviewBadge } from '@rocket.chat/ui-client'; import React from 'react'; import { hasPermission, hasAtLeastOnePermission } from '../../../app/authorization/client'; import { settings } from '../../../app/settings/client'; import { createSidebarItems } from '../../lib/createSidebarItems'; -import AccountFeaturePreviewBadge from './featurePreview/AccountFeaturePreviewBadge'; export const { registerSidebarItem: registerAccountSidebarItem, @@ -54,7 +53,7 @@ export const { href: '/account/feature-preview', i18nLabel: 'Feature_preview', icon: 'flask', - badge: () => , + badge: () => , permissionGranted: () => settings.get('Accounts_AllowFeaturePreview') && defaultFeaturesPreview?.length > 0, }, { diff --git a/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx new file mode 100644 index 000000000000..615fd20cf5a6 --- /dev/null +++ b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx @@ -0,0 +1,127 @@ +import { + ButtonGroup, + Button, + Box, + ToggleSwitch, + Accordion, + Field, + FieldGroup, + FieldLabel, + FieldRow, + FieldHint, + Callout, + Margins, +} from '@rocket.chat/fuselage'; +import { useDefaultSettingFeaturePreviewList } from '@rocket.chat/ui-client'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useTranslation, useSettingsDispatch } from '@rocket.chat/ui-contexts'; +import type { ChangeEvent } from 'react'; +import React, { Fragment } from 'react'; +import { useForm } from 'react-hook-form'; + +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; +import { useFeaturePreviewEnableQuery } from '../../../hooks/useFeaturePreviewEnableQuery'; +import { useEditableSetting } from '../EditableSettingsContext'; +import Setting from '../settings/Setting'; +import SettingsGroupPageSkeleton from '../settings/SettingsGroupPage/SettingsGroupPageSkeleton'; + +const AdminFeaturePreviewPage = () => { + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + const allowFeaturePreviewSetting = useEditableSetting('Accounts_AllowFeaturePreview'); + const { features } = useDefaultSettingFeaturePreviewList(); + + const { + watch, + formState: { isDirty }, + setValue, + handleSubmit, + reset, + } = useForm({ + defaultValues: { featuresPreview: features }, + }); + const { featuresPreview } = watch(); + const dispatch = useSettingsDispatch(); + + const handleSave = async () => { + try { + const featuresToBeSaved = featuresPreview.map((feature) => ({ name: feature.name, value: feature.value })); + + await dispatch([ + { _id: allowFeaturePreviewSetting!._id, value: allowFeaturePreviewSetting!.value }, + { _id: 'Accounts_Default_User_Preferences_featuresPreview', value: JSON.stringify(featuresToBeSaved) }, + ]); + dispatchToastMessage({ type: 'success', message: t('Preferences_saved') }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } finally { + reset({ featuresPreview }); + } + }; + + const handleFeatures = (e: ChangeEvent) => { + const updated = featuresPreview.map((item) => (item.name === e.target.name ? { ...item, value: e.target.checked } : item)); + setValue('featuresPreview', updated, { shouldDirty: true }); + }; + + const grouppedFeaturesPreview = useFeaturePreviewEnableQuery(featuresPreview); + + if (!allowFeaturePreviewSetting) { + // TODO: Implement FeaturePreviewSkeleton component + return ; + } + + return ( + + + + + + + {t('Feature_preview_admin_page_description')} + {t('Feature_preview_page_callout')} + {t('Feature_preview_admin_page_callout')} + + + + + {grouppedFeaturesPreview?.map(([group, features], index) => ( + + + {features.map((feature) => ( + + + + {t(feature.i18n)} + + + {feature.description && {t(feature.description)}} + + {feature.imageUrl && } + + ))} + + + ))} + + + + + + + + + + + ); +}; + +export default AdminFeaturePreviewPage; diff --git a/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx new file mode 100644 index 000000000000..a7d6bd77d136 --- /dev/null +++ b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx @@ -0,0 +1,26 @@ +import { usePermission } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React, { memo } from 'react'; + +import SettingsProvider from '../../../providers/SettingsProvider'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; +import EditableSettingsProvider from '../settings/EditableSettingsProvider'; +import AdminFeaturePreviewPage from './AdminFeaturePreviewPage'; + +const AdminFeaturePreviewRoute = (): ReactElement => { + const canViewFeaturesPreview = usePermission('manage-cloud'); + + if (!canViewFeaturesPreview) { + return ; + } + + return ( + + + + + + ); +}; + +export default memo(AdminFeaturePreviewRoute); diff --git a/apps/meteor/client/views/admin/routes.tsx b/apps/meteor/client/views/admin/routes.tsx index f70df1625871..d244d5e2f19b 100644 --- a/apps/meteor/client/views/admin/routes.tsx +++ b/apps/meteor/client/views/admin/routes.tsx @@ -104,6 +104,10 @@ declare module '@rocket.chat/ui-contexts' { pathname: `/admin/subscription`; pattern: '/admin/subscription'; }; + 'admin-feature-preview': { + pathname: '/admin/feature-preview'; + pattern: '/admin/feature-preview'; + }; } } @@ -237,3 +241,8 @@ registerAdminRoute('/subscription', { name: 'subscription', component: lazy(() => import('./subscription/SubscriptionRoute')), }); + +registerAdminRoute('/feature-preview', { + name: 'admin-feature-preview', + component: lazy(() => import('./featurePreview/AdminFeaturePreviewRoute')), +}); diff --git a/apps/meteor/client/views/admin/sidebarItems.ts b/apps/meteor/client/views/admin/sidebarItems.ts index 013206d9e9a8..fc7d307396d4 100644 --- a/apps/meteor/client/views/admin/sidebarItems.ts +++ b/apps/meteor/client/views/admin/sidebarItems.ts @@ -1,3 +1,5 @@ +import { defaultFeaturesPreview } from '@rocket.chat/ui-client'; + import { hasPermission, hasAtLeastOnePermission, hasAllPermission } from '../../../app/authorization/client'; import { createSidebarItems } from '../../lib/createSidebarItems'; @@ -129,6 +131,12 @@ export const { icon: 'emoji', permissionGranted: (): boolean => hasPermission('manage-emoji'), }, + { + href: '/admin/feature-preview', + i18nLabel: 'Feature_preview', + icon: 'flask', + permissionGranted: () => defaultFeaturesPreview?.length > 0, + }, { href: '/admin/settings', i18nLabel: 'Settings', diff --git a/apps/meteor/public/images/featurePreview/enhanced-navigation.png b/apps/meteor/public/images/featurePreview/enhanced-navigation.png new file mode 100644 index 0000000000000000000000000000000000000000..4240326ba985d0a054ae5d9c8521b24c8d93e6f7 GIT binary patch literal 2372 zcmeHIYgCg*8lKz;k(*0VynsLp8%mX01@Ra$34#GB2w8)n2Lz6kXazzALMRv_YP;xG zVrfx?0*kHdhC)CtVh|D##7hAYg%TP_P1$fufB*?0CfSL{AKkOvo<04szxKyBGw<`h z^UgExH}gzEL^#D}&89T~0Gp8DKq>$zF&wv8TEKSeq4Rv$;nISm(*aoTVg3*yRQGuZ zBGRdp08rcKGzASbgB(T%puW)RWda5O%aM>k@?kbYDJ7;mXg7)%7}|_U^u0g_?-vIh z3PLLno>;U^yRy;Ip#U%U`gz?3Q9dbXM|jd~=gzm4M_kLikKsE!!V_rWmyy}L8oj)= zS2fb?Im$rG8QwM!!;_UE8Rx|HP0%OX2>m!4{!$M9@ zmzycKXS|vvx3jAFOuN;#LOx%@xz-bPQB~L4+B$H!?W8hJu;wTX4KMQ=yEwA+byHJQ z4Srntp0Hr<_>yuo%x4&B_O`IRa^*|1XD(Z^^k)!1hMgXbR43&u9)Lcv`iBcipXe%2 zq_vq)1Omaa`(22J@|`C&yL#jc-3GtCI}p2cuqKsjiPzA?P2zh4jxQVm;07eyMCq?+ zYV8fOwUa-ksVNIq0FcPLSd^mM+Od*DToGaEjy4EpBb~T5rBg*r>d@@<%gYepB7JF0 zqq_ZhTTbt4e99^Fl9eP{muC7SQ?As-T7ZUt1HXW7`egSV>XAs%dT2+~9d#BA(P*)j z(D0-Wc6F=Xq;Ez*19MWIbzS0osT7X6ozKk>&N3dh)U{&Re&fo8P%s&w0N~{VcrIKq z2x~}d4RQmV;DFu^gWmrCgjZEc@Ze_b(=Aoh-_d=XJZ8+v)lKA~z%BN~S86rpYMt?0u zFkC3)tSau`mnY25=B{g)dM3owSbf=Vw0-aZz2T~fTHkJ@F6pcLmqLRS&;Cv>BWR!I z+RPN!dq?ck6I|axSa+4|B7lNzG$S`NkbWu8%wj3c6P@vb)pL!5@#dmB^TW{?vN zBGfH=G3iH<==4p-aBGihHuu9{(zqJ-xB51NXv62_%8>>T@awGan@0K^3i((iN+xo%#ouHt&I7j7;gA~ zaM|kYft59-NVWnweEfy~obuR!X;>4?FtKhWk9LQc+ATc4!u|H0t|p=C4#)UkIs1b* zU~l>r;^|xj-Rs_fd+|eK`kzi4=TVAO6y`vwsAMd7!IrRlB4EwT%Irr}B_*-~AjVn(;{-)H)yq<^yYV&jM#%$9>QVh%U z%UJHItG`bwI}ZY}o0&klMYUn~qrAe2#ymoOtW#gr9Dz%HofNmdj`t8&#p7n_pOvuZ z=F?L2tj|bH_{0E*sl4L|c!c+jV` zeXy-WWIHdBNTLXs_UIBMDy?(d=pS%;{K~$(^7C(zfUTgQC0m#>rkYzG!&0srmo+zP z%uIJAf4!wl*sSe43silWOG&I~w3z|7=J<^(E=H1Jo*@0}lR+k4a4{Z+7t=?Z<-^16 z#HH**nNR$)vabV9KteLt5UV>qWh13{~`%a65CIDvwi8xxILa6 z!t*MO@WlMq>6uJgtPOPTCQBxzspEI!dI! CCsuy| literal 0 HcmV?d00001 diff --git a/apps/meteor/public/images/featurePreview/resizable-contextual-bar.png b/apps/meteor/public/images/featurePreview/resizable-contextual-bar.png new file mode 100644 index 0000000000000000000000000000000000000000..c36c7e44a29d8c27c9c035a77026f1cd7e7dabd0 GIT binary patch literal 4776 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 a2K(3rHj$^VY%~U(889)lFep3k9Q`kU29J{f literal 0 HcmV?d00001 diff --git a/apps/meteor/public/images/featurePreview/timestamp.png b/apps/meteor/public/images/featurePreview/timestamp.png new file mode 100644 index 0000000000000000000000000000000000000000..7573f97db55b73e2d880af67e6705e3c4a2040c7 GIT binary patch literal 51432 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

&c?(AAUaFYNC38B5NhzYmJ_RR zL>SGF>SN$vDeeqMfUz1WeRyOj;Ilr%cMsNSwW*xQok;wu-W~Tik;lp>_`PH^@n4S@ zXw=7}@NQjEv>_%k*v;YbU*w%TtT+7+wmo;YUEI<%0sv)jAOjZT)wo=d6=sickEvhuX7{PxVy2T$f-kR=!SU=hVj!2k5|7`y#qK1k1)5N7xjMVQ-H4!NL*;UVF=FMdD!@J-yLk|D&~XB+;jk$v9i#aqNbKKbu^*4BVwni?4S&ceH%)78Vo;3We3z>0}G(1W=Te^H+ zc3x`TUiBlCK8kjO`)Jlb4l8GuDG**g!r+l8S7}78R6^WFWuoTL;6SIIbdQ7FC0>|Aqq~|&r=2%&UIMNuFHXGipg$|j6gaGa~R9GyX%`#=8mz& z3F%O4WFF_ttHHFgIZ+N&G2gE5GZpX*q=o-WEO)j_tD^PBBG=-7;r+*Q4Z2` zd)&d#IICHLqscicjx#W19KMHf0lpyLuck>DOZN<{4le$@2=v?aU+Q|4qM*k>-V3l4 zhz_qPiX%|Uj6NG1EbPn~E(IF^otw_5K0Zsnh!>(6!|GbJjcFu}_E&iooVwdg4L;Tg zgvRB3UbgDZ2{|RieK*w){5%dEz&s_U(5s7*4#wg50$1<-3H($4{;Ym7^$!&E6_lYf z*4BfZ5_qKWaWv4dWpk430t3i^55*3c(8Ukv_bVAV7a(#z84gq0an27F(A2H*4Db75 ziQ2P=`Vd`>>uP4kvi=Bs0lN|rw~3n*-QSY4=d8uY8|)@WgqMZq>2;;Cyz(o}R#s7|?Xb*2FxuR(TNhozRkI6;i zKV-ND5n7cS)KuR3=PGsvO%nVDlMhnUVk_Qz_>=HX7A6@#RmmB@|!RG9Y_ovVfRS7{@8n5Q6 zTwP2>0ytm%d`Sl~0RE+Z(~gIW&gOJ>$Zdl~yl|qPxCQ6YifqjhC}lzq-x`*MbDnxY z6X%@`=;opCoAxl|$!i}hty9``$(#9N2ch%%%8E?D0?^4*pA@1%!39QxJE4!H8Ym%X zLnYCKCu1c1-fZ)636;P86s0(JX}%CBXxfe_!TRN{FVItBs!p&af`C@3lG(C>Axw?8 z#WI65W$*}y2`%agC-7q7YkGmWpTpuoM#UvS4d3E>2umvtH*JA*g znd19VV?1e9@2OtR1~luQ!q#hzCKGejMFjj3_P`StljZ_yvDu~RnGF;wip%36cjm0# z^i3%$wt^!^+D;#A!_lRBY6#(Mb5r`;Zu`?+MP=-h00D&kQHPY=SC1M`uU0H3G%LD5 zGE>OQDaJS_maeT}vRz>!lmduAeeszNL1||4yV~8)Lg#}Se z<^M?=o8`9llaqG0di12T@@OCNmOMisOHhF8sFzLjD)*+ro!G7}DqkUv^FO{-GJA^j z-1;()hM&|Q5p_5dI!`o*ZgQMf%R0v+M0+@GI?4tpz^o0gEyvNsk*Dx0RFWV<&)xL| zBY_1)HeDcr)3p0>>}0qQAu-S$L)SlssV^~nQ*viu62k?@6&=T(d+244z|2w`r8ua!_t-e)Vw z0G?bZOYb0Z>f&?%7sgn3_v6QRgkji#fsAD$GHN<#kiOGaI1}g}uq_l8v`nyvdr6>7 zzAktxI{={0brzX+yig3qk?vDaa+x$5{=++v{>mFZ36cWHInd`P*p{A^ooC zdtt=0+$!gdR%*J&LlpI?U|&?9K{uG3&zUA|2k$m6DK_9OcHkdFJP0edE84t3f z87S=QiEo&HDD@z~d8Zb>-~1LD=A@bc@zA4&uI~E9cx{cLRo6iLG4~&gQWWTeH-n86 zVyL~(SP9{$h1*yiH`>weD92Vi`?R+Qk}()=fkp2{r|tDGJz68!M_Ju+$*xfQd9U9d zBr%QoLTWQ@!)aQYt-KFT=a|$K_8_v+sNZ%B%*ddN2wvCQonH|+p)fJN7fF-lR{P0) z@t(tlo8X6nAVSHo-Ai)O!N(AHwShk_Jw~Is-1+4u`$FY|_;wPiYy@JPYLuGPt&W5~ zjfJv>J$(lPaEW5Fp@H<&&ShceaQl?-;qbe6ds?;WC*hX(zyxT6tWn3Icmb%&8vH7U zq}cr6KT~41)4r%BcRr4wAL_kK z``AB~{bl+YQ6l*5f088l7kw7)(xO%?bGsbb6vqn9dPj{We|Aw5d~A8<`^L@Kqr5Pm zn@?#Ua|0q_+XPG+d~syb%T21hUZyOeAf!4c1!!E_9^HK{Iq$YVa!_oij2=;TT p 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 i18nLabel: 'Sidebar_Sections_Order', i18nDescription: 'Sidebar_Sections_Order_Description', }); + + await this.add('Accounts_Default_User_Preferences_featuresPreview', '[]', { + type: 'string', + public: true, + }); }); await this.section('Avatar', async function () { diff --git a/apps/meteor/tests/end-to-end/api/miscellaneous.ts b/apps/meteor/tests/end-to-end/api/miscellaneous.ts index b8341f7c0994..d933f1f3c4b3 100644 --- a/apps/meteor/tests/end-to-end/api/miscellaneous.ts +++ b/apps/meteor/tests/end-to-end/api/miscellaneous.ts @@ -186,6 +186,7 @@ describe('miscellaneous', () => { 'muteFocusedConversations', 'notifyCalendarEvents', 'enableMobileRinging', + 'featuresPreview', ].filter((p) => Boolean(p)); expect(res.body).to.have.property('success', true); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 0e99c1bdc1d8..47d2034e002e 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -85,6 +85,7 @@ "Accounts_AllowEmailChange": "Allow Email Change", "Accounts_AllowEmailNotifications": "Allow Email Notifications", "Accounts_AllowFeaturePreview": "Allow Feature Preview", + "Accounts_AllowFeaturePreview_Description": "Make feature preview available to all workspace members.", "Accounts_AllowPasswordChange": "Allow Password Change", "Accounts_AllowPasswordChangeForOAuthUsers": "Allow Password Change for OAuth Users", "Accounts_AllowRealNameChange": "Allow Name Change", @@ -1125,8 +1126,8 @@ "Common_Access": "Common Access", "Commit": "Commit", "Community": "Community", - "Contextualbar_resizable": "Contextual bar resizable", - "Contextualbar_resizable_description": "Allows you to adjust the size of the contextual bar by simply dragging, giving you instant customization and flexibility", + "Contextualbar_resizable": "Resizable contextual bar", + "Contextualbar_resizable_description": "Adjust the size of the contextual bar by clicking and dragging the edge, giving you instant customization and flexibility.", "Free_Edition": "Free edition", "Composer_not_available_phone_calls": "Messages are not available on phone calls", "Condensed": "Condensed", @@ -1949,8 +1950,8 @@ "Enable_Password_History": "Enable Password History", "Enable_Password_History_Description": "When enabled, users won't be able to update their passwords to some of their most recently used passwords.", "Enable_Svg_Favicon": "Enable SVG favicon", - "Enable_timestamp": "Enable timestamp parsing in messages", - "Enable_timestamp_description": "Enable timestamps to be parsed in messages", + "Enable_timestamp": "Timestamp in messages", + "Enable_timestamp_description": "Render Unix timestamps inside messages in your local (system) timezone.", "Enable_to_bypass_email_verification": "Enable to bypass email verification", "Enable_two-factor_authentication": "Enable two-factor authentication via TOTP", "Enable_two-factor_authentication_email": "Enable two-factor authentication via Email", @@ -2284,7 +2285,10 @@ "Favorite_Rooms": "Enable Favorite Rooms", "Favorites": "Favorites", "Feature_preview": "Feature preview", - "Feature_preview_page_description": "Welcome to the features preview page! Here, you can enable the latest cutting-edge features that are currently under development and not yet officially released.\n\nPlease note that these configurations are still in the testing phase and may not be stable or fully functional.", + "Feature_preview_page_description": "Enable the latest features that are currently under development.", + "Feature_preview_page_callout": "Feature previews are being tested and may not be stable or fully functional. Features may become premium capabilities once officially released.", + "Feature_preview_admin_page_description": "Choose what feature previews to make available to workspace members.", + "Feature_preview_admin_page_callout": "Features enabled here will be enabled to each user in their feature preview preferences.", "featured": "featured", "Featured": "Featured", "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "This feature depends on the above selected call provider to be enabled from the administration settings (Admin -> Video Conference).", @@ -4364,7 +4368,7 @@ "Queue_Time": "Queue Time", "Queue_management": "Queue Management", "Quick_reactions": "Quick reactions", - "Quick_reactions_description": "The three most used reactions get an easy access while your mouse is over the message", + "Quick_reactions_description": "Easily access your most used and most recent emoji message reactions by hovering on a message.", "quote": "quote", "Quote": "Quote", "Random": "Random", diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index 090e081e83fa..3110d6e82a67 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -2169,7 +2169,6 @@ "Favorite_Rooms": "पसंदीदा कमरे सक्षम करें", "Favorites": "पसंदीदा", "Feature_preview": "फ़ीचर पूर्वावलोकन", - "Feature_preview_page_description": "फीचर पूर्वावलोकन पृष्ठ पर आपका स्वागत है! यहां, आप नवीनतम अत्याधुनिक सुविधाओं को सक्षम कर सकते हैं जो वर्तमान में विकास के अधीन हैं और अभी तक आधिकारिक तौर पर जारी नहीं की गई हैं।\n\nकृपया ध्यान दें कि ये कॉन्फ़िगरेशन अभी भी परीक्षण चरण में हैं और स्थिर या पूरी तरह कार्यात्मक नहीं हो सकते हैं।", "featured": "प्रदर्शित", "Featured": "प्रदर्शित", "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "यह सुविधा प्रशासन सेटिंग्स (एडमिन -> वीडियो कॉन्फ्रेंस) से सक्षम होने के लिए उपरोक्त चयनित कॉल प्रदाता पर निर्भर करती है।", @@ -4128,7 +4127,6 @@ "Queue_Time": "कतार समय", "Queue_management": "कतार प्रबंधन", "Quick_reactions": "त्वरित प्रतिक्रियाएँ", - "Quick_reactions_description": "जब आपका माउस संदेश पर होता है तो सबसे अधिक उपयोग की जाने वाली तीन प्रतिक्रियाओं तक आसान पहुंच मिलती है", "quote": "उद्धरण", "Quote": "उद्धरण", "Random": "Random", @@ -4987,7 +4985,7 @@ "The_application_will_be_able_to": "<1>{{appName}} यह करने में सक्षम होगा:", "The_channel_name_is_required": "चैनल का नाम आवश्यक है", "The_emails_are_being_sent": "ईमेल भेजे जा रहे हैं.", - "The_empty_room__roomName__will_be_removed_automatically": "खाली कमरा {{roomName}} स्वचालित रूप से हटा दिया जाएगा।", + "The_empty_room__roomName__will_be_removed_automatically": "खाली कमरा {{roomName}} स्वचालित रूप से हटा दिया जाएगा।", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "छवि का आकार बदलना काम नहीं करेगा क्योंकि हम आपके सर्वर पर स्थापित ImageMagick या ग्राफ़िक्सMagick का पता नहीं लगा सकते हैं।", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "संदेश एक चर्चा है आप संदेशों को पुनर्प्राप्त नहीं कर पाएंगे!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "मोबाइल सूचनाएं सभी उपयोगकर्ताओं के लिए अक्षम कर दी गई थीं, पुश गेटवे को फिर से सक्षम करने के लिए \"एडमिन > पुश\" पर जाएं", @@ -6134,4 +6132,4 @@ "Unlimited_seats": "असीमित सीटें", "Unlimited_MACs": "असीमित एमएसी", "Unlimited_seats_MACs": "असीमित सीटें और एमएसी" -} \ No newline at end of file +} diff --git a/packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx b/packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx new file mode 100644 index 000000000000..eece30cc7280 --- /dev/null +++ b/packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx @@ -0,0 +1,21 @@ +import { Badge } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; + +import { usePreferenceFeaturePreviewList } from '../../hooks/usePreferenceFeaturePreviewList'; + +const FeaturePreviewBadge = () => { + const t = useTranslation(); + const { unseenFeatures } = usePreferenceFeaturePreviewList(); + + if (!unseenFeatures) { + return null; + } + + return ( + + {unseenFeatures} + + ); +}; + +export default FeaturePreviewBadge; diff --git a/packages/ui-client/src/components/FeaturePreview/index.ts b/packages/ui-client/src/components/FeaturePreview/index.ts new file mode 100644 index 000000000000..f6b8e5f2071e --- /dev/null +++ b/packages/ui-client/src/components/FeaturePreview/index.ts @@ -0,0 +1,2 @@ +export { FeaturePreview, FeaturePreviewOn, FeaturePreviewOff } from './FeaturePreview'; +export { default as FeaturePreviewBadge } from './FeaturePreviewBadge'; diff --git a/packages/ui-client/src/components/index.ts b/packages/ui-client/src/components/index.ts index 8642983229aa..7308c8e75431 100644 --- a/packages/ui-client/src/components/index.ts +++ b/packages/ui-client/src/components/index.ts @@ -11,7 +11,7 @@ export * as UserStatus from './UserStatus'; export * from './Header'; export * from './HeaderV2'; export * from './MultiSelectCustom/MultiSelectCustom'; -export * from './FeaturePreview/FeaturePreview'; +export * from './FeaturePreview'; export * from './RoomBanner'; export { default as UserAutoComplete } from './UserAutoComplete'; export * from './GenericMenu'; diff --git a/packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts b/packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts new file mode 100644 index 000000000000..373862379cc1 --- /dev/null +++ b/packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts @@ -0,0 +1,12 @@ +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { parseSetting, useFeaturePreviewList } from './useFeaturePreviewList'; + +export const useDefaultSettingFeaturePreviewList = () => { + const featurePreviewSettingJSON = useSetting('Accounts_Default_User_Preferences_featuresPreview'); + + const settingFeaturePreview = useMemo(() => parseSetting(featurePreviewSettingJSON), [featurePreviewSettingJSON]); + + return useFeaturePreviewList(settingFeaturePreview ?? []); +}; diff --git a/packages/ui-client/src/hooks/useFeaturePreview.ts b/packages/ui-client/src/hooks/useFeaturePreview.ts index 4bdda9c9251a..bd46adfdefff 100644 --- a/packages/ui-client/src/hooks/useFeaturePreview.ts +++ b/packages/ui-client/src/hooks/useFeaturePreview.ts @@ -1,7 +1,8 @@ -import { type FeaturesAvailable, useFeaturePreviewList } from './useFeaturePreviewList'; +import { type FeaturesAvailable } from './useFeaturePreviewList'; +import { usePreferenceFeaturePreviewList } from './usePreferenceFeaturePreviewList'; export const useFeaturePreview = (featureName: FeaturesAvailable) => { - const { features } = useFeaturePreviewList(); + const { features } = usePreferenceFeaturePreviewList(); const currentFeature = features?.find((feature) => feature.name === featureName); diff --git a/packages/ui-client/src/hooks/useFeaturePreviewList.ts b/packages/ui-client/src/hooks/useFeaturePreviewList.ts index ff103a8d84ef..08bda4ff81ff 100644 --- a/packages/ui-client/src/hooks/useFeaturePreviewList.ts +++ b/packages/ui-client/src/hooks/useFeaturePreviewList.ts @@ -1,5 +1,4 @@ import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useUserPreference, useSetting } from '@rocket.chat/ui-contexts'; export type FeaturesAvailable = | 'quickReactions' @@ -24,6 +23,7 @@ export type FeaturePreviewProps = { }; }; +// TODO: Move the features preview array to another directory to be accessed from both BE and FE. export const defaultFeaturesPreview: FeaturePreviewProps[] = [ { name: 'quickReactions', @@ -47,6 +47,7 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ i18n: 'Enable_timestamp', description: 'Enable_timestamp_description', group: 'Message', + imageUrl: 'images/featurePreview/timestamp.png', value: false, enabled: true, }, @@ -55,6 +56,7 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ i18n: 'Contextualbar_resizable', description: 'Contextualbar_resizable_description', group: 'Navigation', + imageUrl: 'images/featurePreview/resizable-contextual-bar.png', value: false, enabled: true, }, @@ -63,6 +65,7 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ i18n: 'New_navigation', description: 'New_navigation_description', group: 'Navigation', + imageUrl: 'images/featurePreview/enhanced-navigation.png', value: false, enabled: true, }, @@ -82,22 +85,27 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ export const enabledDefaultFeatures = defaultFeaturesPreview.filter((feature) => feature.enabled); -export const useFeaturePreviewList = () => { - const featurePreviewEnabled = useSetting('Accounts_AllowFeaturePreview'); - const userFeaturesPreview = useUserPreference('featuresPreview'); - - if (!featurePreviewEnabled) { - return { unseenFeatures: 0, features: [] as FeaturePreviewProps[], featurePreviewEnabled }; +// TODO: Remove this logic after we have a way to store object settings. +export const parseSetting = (setting?: FeaturePreviewProps[] | string) => { + if (typeof setting === 'string') { + try { + return JSON.parse(setting) as FeaturePreviewProps[]; + } catch (_) { + return; + } } + return setting; +}; +export const useFeaturePreviewList = (featuresList: Pick[]) => { const unseenFeatures = enabledDefaultFeatures.filter( - (feature) => !userFeaturesPreview?.find((userFeature) => userFeature.name === feature.name), + (defaultFeature) => !featuresList?.find((feature) => feature.name === defaultFeature.name), ).length; - const mergedFeatures = enabledDefaultFeatures.map((feature) => { - const userFeature = userFeaturesPreview?.find((userFeature) => userFeature.name === feature.name); - return { ...feature, ...userFeature }; + const mergedFeatures = enabledDefaultFeatures.map((defaultFeature) => { + const features = featuresList?.find((feature) => feature.name === defaultFeature.name); + return { ...defaultFeature, ...features }; }); - return { unseenFeatures, features: mergedFeatures, featurePreviewEnabled }; + return { unseenFeatures, features: mergedFeatures }; }; diff --git a/packages/ui-client/src/hooks/useFeaturePreviewList.spec.tsx b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.spec.tsx similarity index 79% rename from packages/ui-client/src/hooks/useFeaturePreviewList.spec.tsx rename to packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.spec.tsx index e348cfb6a864..ac3d6f92d51a 100644 --- a/packages/ui-client/src/hooks/useFeaturePreviewList.spec.tsx +++ b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.spec.tsx @@ -1,10 +1,11 @@ import { mockAppRoot } from '@rocket.chat/mock-providers'; import { renderHook } from '@testing-library/react'; -import { useFeaturePreviewList, enabledDefaultFeatures } from './useFeaturePreviewList'; +import { enabledDefaultFeatures } from './useFeaturePreviewList'; +import { usePreferenceFeaturePreviewList } from './usePreferenceFeaturePreviewList'; it('should return the number of unseen features and Accounts_AllowFeaturePreview enabled ', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot().withSetting('Accounts_AllowFeaturePreview', true).build(), }); @@ -18,7 +19,7 @@ it('should return the number of unseen features and Accounts_AllowFeaturePreview }); it('should return the number of unseen features and Accounts_AllowFeaturePreview disabled ', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot().withSetting('Accounts_AllowFeaturePreview', false).build(), }); @@ -32,7 +33,7 @@ it('should return the number of unseen features and Accounts_AllowFeaturePreview }); it('should return 0 unseen features', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot() .withSetting('Accounts_AllowFeaturePreview', true) @@ -49,7 +50,7 @@ it('should return 0 unseen features', () => { }); it('should ignore removed feature previews', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot() .withSetting('Accounts_AllowFeaturePreview', true) @@ -72,7 +73,7 @@ it('should ignore removed feature previews', () => { }); it('should turn off ignored feature previews', async () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot() .withSetting('Accounts_AllowFeaturePreview', true) diff --git a/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts new file mode 100644 index 000000000000..d7c4c13417d2 --- /dev/null +++ b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts @@ -0,0 +1,16 @@ +import { useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { FeaturePreviewProps, parseSetting, useFeaturePreviewList } from './useFeaturePreviewList'; + +export const usePreferenceFeaturePreviewList = () => { + const featurePreviewEnabled = useSetting('Accounts_AllowFeaturePreview'); + const userFeaturesPreviewPreference = useUserPreference('featuresPreview'); + const userFeaturesPreview = useMemo(() => parseSetting(userFeaturesPreviewPreference), [userFeaturesPreviewPreference]); + const { unseenFeatures, features } = useFeaturePreviewList(userFeaturesPreview ?? []); + + if (!featurePreviewEnabled) { + return { unseenFeatures: 0, features: [] as FeaturePreviewProps[], featurePreviewEnabled }; + } + return { unseenFeatures, features, featurePreviewEnabled }; +}; diff --git a/packages/ui-client/src/index.ts b/packages/ui-client/src/index.ts index 3e640343da5b..a96ef265aadc 100644 --- a/packages/ui-client/src/index.ts +++ b/packages/ui-client/src/index.ts @@ -1,5 +1,7 @@ export * from './components'; export * from './hooks/useFeaturePreview'; +export * from './hooks/useDefaultSettingFeaturePreviewList'; export * from './hooks/useFeaturePreviewList'; +export * from './hooks/usePreferenceFeaturePreviewList'; export * from './hooks/useDocumentTitle'; export * from './helpers'; From a4fcd99e3006f28abb0b3f06985809b447365fc4 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 24 Sep 2024 12:45:44 -0300 Subject: [PATCH 006/291] chore: move common files to core-services (#33341) --- _templates/service/new/service.ejs.t | 8 ++------ ee/apps/account-service/src/service.ts | 8 ++------ ee/apps/authorization-service/src/service.ts | 8 ++------ ee/apps/ddp-streamer/src/service.ts | 8 ++------ ee/apps/omnichannel-transcript/src/service.ts | 8 ++------ ee/apps/presence-service/src/service.ts | 8 ++------ ee/apps/queue-worker/src/service.ts | 8 ++------ ee/apps/stream-hub-service/src/service.ts | 8 ++------ packages/core-services/src/index.ts | 2 ++ .../core-services/src/lib}/mongo.ts | 14 ++------------ 10 files changed, 20 insertions(+), 60 deletions(-) rename {apps/meteor/ee/server/services => packages/core-services/src/lib}/mongo.ts (72%) diff --git a/_templates/service/new/service.ejs.t b/_templates/service/new/service.ejs.t index 77f02d2b7769..699539365259 100644 --- a/_templates/service/new/service.ejs.t +++ b/_templates/service/new/service.ejs.t @@ -1,12 +1,10 @@ --- to: ee/apps/<%= name %>/src/service.ts --- -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; const PORT = process.env.PORT || <%= h.random() %>; @@ -14,9 +12,7 @@ const PORT = process.env.PORT || <%= h.random() %>; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/account-service/src/service.ts b/ee/apps/account-service/src/service.ts index 07ca30ed748f..c2f64e37bde3 100755 --- a/ee/apps/account-service/src/service.ts +++ b/ee/apps/account-service/src/service.ts @@ -1,19 +1,15 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; const PORT = process.env.PORT || 3033; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/authorization-service/src/service.ts b/ee/apps/authorization-service/src/service.ts index 4dcd466afa60..1698ef7a115c 100755 --- a/ee/apps/authorization-service/src/service.ts +++ b/ee/apps/authorization-service/src/service.ts @@ -1,19 +1,15 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; const PORT = process.env.PORT || 3034; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/ddp-streamer/src/service.ts b/ee/apps/ddp-streamer/src/service.ts index 07666a265dbe..58552240cadd 100755 --- a/ee/apps/ddp-streamer/src/service.ts +++ b/ee/apps/ddp-streamer/src/service.ts @@ -1,16 +1,12 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/omnichannel-transcript/src/service.ts b/ee/apps/omnichannel-transcript/src/service.ts index 66456456fb74..ad60687d5ba4 100644 --- a/ee/apps/omnichannel-transcript/src/service.ts +++ b/ee/apps/omnichannel-transcript/src/service.ts @@ -1,20 +1,16 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; const PORT = process.env.PORT || 3036; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/presence-service/src/service.ts b/ee/apps/presence-service/src/service.ts index 0e1c97f2daa2..0c51c30dc577 100755 --- a/ee/apps/presence-service/src/service.ts +++ b/ee/apps/presence-service/src/service.ts @@ -1,19 +1,15 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; const PORT = process.env.PORT || 3031; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/queue-worker/src/service.ts b/ee/apps/queue-worker/src/service.ts index 4bc6c9642913..c11376d56534 100644 --- a/ee/apps/queue-worker/src/service.ts +++ b/ee/apps/queue-worker/src/service.ts @@ -1,20 +1,16 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; const PORT = process.env.PORT || 3038; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/stream-hub-service/src/service.ts b/ee/apps/stream-hub-service/src/service.ts index eade703321d2..5e035548dc38 100755 --- a/ee/apps/stream-hub-service/src/service.ts +++ b/ee/apps/stream-hub-service/src/service.ts @@ -1,11 +1,9 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; import { DatabaseWatcher } from '../../../../apps/meteor/server/database/DatabaseWatcher'; import { StreamHub } from './StreamHub'; @@ -14,9 +12,7 @@ const PORT = process.env.PORT || 3035; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index 8eea19ea7405..85722c98839f 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -75,6 +75,8 @@ export { AnalyticsOverviewDataResult, } from './types/IOmnichannelAnalyticsService'; +export { getConnection, getTrashCollection } from './lib/mongo'; + export { AutoUpdateRecord, FindVoipRoomsParams, diff --git a/apps/meteor/ee/server/services/mongo.ts b/packages/core-services/src/lib/mongo.ts similarity index 72% rename from apps/meteor/ee/server/services/mongo.ts rename to packages/core-services/src/lib/mongo.ts index 27e9e931c039..fab1fd108d99 100644 --- a/apps/meteor/ee/server/services/mongo.ts +++ b/packages/core-services/src/lib/mongo.ts @@ -5,16 +5,6 @@ const { MONGO_URL = 'mongodb://localhost:27017/rocketchat' } = process.env; const name = /^mongodb:\/\/.*?(?::[0-9]+)?\/([^?]*)/.exec(MONGO_URL)?.[1]; -export enum Collections { - Subscriptions = 'rocketchat_subscription', - UserSession = 'usersSessions', - User = 'users', - Trash = 'rocketchat__trash', - Messages = 'rocketchat_message', - Rooms = 'rocketchat_room', - Settings = 'rocketchat_settings', -} - function connectDb(options?: MongoClientOptions): Promise { const client = new MongoClient(MONGO_URL, options); @@ -44,9 +34,9 @@ export const getConnection = ((): ((options?: MongoClientOptions) => Promise }; })(); -export async function getCollection(name: Collections): Promise> { +export async function getTrashCollection(): Promise> { if (!db) { db = await getConnection(); } - return db.collection(name); + return db.collection('rocketchat__trash'); } From 7dc9c4154f672c26f046e8936f4cb02e86814355 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 25 Sep 2024 13:14:54 -0300 Subject: [PATCH 007/291] chore: replace Meteor._localStorage -> Accounts.storageLocation (#33356) --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 27 ++++++++++--------- apps/meteor/app/ui-master/server/scripts.ts | 1 + .../client/messageBox/createComposerAPI.ts | 8 +++--- apps/meteor/app/ui-utils/server/Message.ts | 4 +-- .../app/utils/client/lib/RestApiClient.ts | 6 +++-- .../client/meteorOverrides/login/saml.ts | 2 +- .../providers/UserProvider/UserProvider.tsx | 6 ++--- apps/meteor/client/startup/accounts.ts | 16 +---------- .../components/AuthorizationFormPage.tsx | 3 +-- .../externals/meteor/accounts-base.d.ts | 1 + 10 files changed, 31 insertions(+), 43 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index bbd6f208f35a..50224cb89dbb 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -6,6 +6,7 @@ import { isE2EEMessage } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import EJSON from 'ejson'; import _ from 'lodash'; +import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; @@ -308,8 +309,8 @@ class E2E extends Emitter { getKeysFromLocalStorage(): KeyPair { return { - public_key: Meteor._localStorage.getItem('public_key'), - private_key: Meteor._localStorage.getItem('private_key'), + public_key: Accounts.storageLocation.getItem('public_key'), + private_key: Accounts.storageLocation.getItem('private_key'), }; } @@ -332,7 +333,7 @@ class E2E extends Emitter { imperativeModal.close(); }, onConfirm: () => { - Meteor._localStorage.removeItem('e2e.randomPassword'); + Accounts.storageLocation.removeItem('e2e.randomPassword'); this.setState(E2EEState.READY); dispatchToastMessage({ type: 'success', message: t('End_To_End_Encryption_Enabled') }); this.closeAlert(); @@ -394,7 +395,7 @@ class E2E extends Emitter { await this.persistKeys(this.getKeysFromLocalStorage(), await this.createRandomPassword()); } - const randomPassword = Meteor._localStorage.getItem('e2e.randomPassword'); + const randomPassword = Accounts.storageLocation.getItem('e2e.randomPassword'); if (randomPassword) { this.setState(E2EEState.SAVE_PASSWORD); this.openAlert({ @@ -412,8 +413,8 @@ class E2E extends Emitter { this.log('-> Stop Client'); this.closeAlert(); - Meteor._localStorage.removeItem('public_key'); - Meteor._localStorage.removeItem('private_key'); + Accounts.storageLocation.removeItem('public_key'); + Accounts.storageLocation.removeItem('private_key'); this.instancesByRoomId = {}; this.privateKey = undefined; this.started = false; @@ -425,8 +426,8 @@ class E2E extends Emitter { async changePassword(newPassword: string): Promise { await this.persistKeys(this.getKeysFromLocalStorage(), newPassword, { force: true }); - if (Meteor._localStorage.getItem('e2e.randomPassword')) { - Meteor._localStorage.setItem('e2e.randomPassword', newPassword); + if (Accounts.storageLocation.getItem('e2e.randomPassword')) { + Accounts.storageLocation.setItem('e2e.randomPassword', newPassword); } } @@ -447,12 +448,12 @@ class E2E extends Emitter { } async loadKeys({ public_key, private_key }: { public_key: string; private_key: string }): Promise { - Meteor._localStorage.setItem('public_key', public_key); + Accounts.storageLocation.setItem('public_key', public_key); try { this.privateKey = await importRSAKey(EJSON.parse(private_key), ['decrypt']); - Meteor._localStorage.setItem('private_key', private_key); + Accounts.storageLocation.setItem('private_key', private_key); } catch (error) { this.setState(E2EEState.ERROR); return this.error('Error importing private key: ', error); @@ -474,7 +475,7 @@ class E2E extends Emitter { try { const publicKey = await exportJWKKey(key.publicKey); - Meteor._localStorage.setItem('public_key', JSON.stringify(publicKey)); + Accounts.storageLocation.setItem('public_key', JSON.stringify(publicKey)); } catch (error) { this.setState(E2EEState.ERROR); return this.error('Error exporting public key: ', error); @@ -483,7 +484,7 @@ class E2E extends Emitter { try { const privateKey = await exportJWKKey(key.privateKey); - Meteor._localStorage.setItem('private_key', JSON.stringify(privateKey)); + Accounts.storageLocation.setItem('private_key', JSON.stringify(privateKey)); } catch (error) { this.setState(E2EEState.ERROR); return this.error('Error exporting private key: ', error); @@ -498,7 +499,7 @@ class E2E extends Emitter { async createRandomPassword(): Promise { const randomPassword = await generateMnemonicPhrase(5); - Meteor._localStorage.setItem('e2e.randomPassword', randomPassword); + Accounts.storageLocation.setItem('e2e.randomPassword', randomPassword); return randomPassword; } diff --git a/apps/meteor/app/ui-master/server/scripts.ts b/apps/meteor/app/ui-master/server/scripts.ts index 9edadb021d32..3e84a6e39c90 100644 --- a/apps/meteor/app/ui-master/server/scripts.ts +++ b/apps/meteor/app/ui-master/server/scripts.ts @@ -45,6 +45,7 @@ window.addEventListener('load', function() { }); window.localStorage.clear(); Meteor._localStorage = window.sessionStorage; + Accounts.config({ clientStorage: 'session' }); } }); ` diff --git a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts index a926f8540d27..741f7959fa90 100644 --- a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts +++ b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts @@ -1,6 +1,6 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; -import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI'; import { withDebouncing } from '../../../../lib/utils/highOrderFunctions'; @@ -31,11 +31,11 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) const persist = withDebouncing({ wait: 300 })(() => { if (input.value) { - Meteor._localStorage.setItem(storageID, input.value); + Accounts.storageLocation.setItem(storageID, input.value); return; } - Meteor._localStorage.removeItem(storageID); + Accounts.storageLocation.removeItem(storageID); }); const notifyQuotedMessagesUpdate = (): void => { @@ -262,7 +262,7 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) const insertNewLine = (): void => insertText('\n'); - setText(Meteor._localStorage.getItem(storageID) ?? '', { + setText(Accounts.storageLocation.getItem(storageID) ?? '', { skipFocus: true, }); diff --git a/apps/meteor/app/ui-utils/server/Message.ts b/apps/meteor/app/ui-utils/server/Message.ts index 06ae59238b42..21d8886c70bc 100644 --- a/apps/meteor/app/ui-utils/server/Message.ts +++ b/apps/meteor/app/ui-utils/server/Message.ts @@ -1,6 +1,6 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { escapeHTML } from '@rocket.chat/string-helpers'; -import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; import { trim } from '../../../lib/utils/stringUtils'; import { i18n } from '../../../server/lib/i18n'; @@ -17,7 +17,7 @@ export const Message = { } if (messageType.message) { if (!language) { - language = Meteor._localStorage.getItem('userLanguage') || 'en'; + language = Accounts.storageLocation.getItem('userLanguage') || 'en'; } const data = (typeof messageType.data === 'function' && messageType.data(msg)) || {}; return i18n.t(messageType.message, { ...data, lng: language }); diff --git a/apps/meteor/app/utils/client/lib/RestApiClient.ts b/apps/meteor/app/utils/client/lib/RestApiClient.ts index c5e12250b441..53c95ee3e4fa 100644 --- a/apps/meteor/app/utils/client/lib/RestApiClient.ts +++ b/apps/meteor/app/utils/client/lib/RestApiClient.ts @@ -1,6 +1,5 @@ import { RestClient } from '@rocket.chat/api-client'; import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; import { invokeTwoFactorModal } from '../../../../client/lib/2fa/process2faReturn'; import { baseURI } from '../../../../client/lib/baseURI'; @@ -12,7 +11,10 @@ class RestApiClient extends RestClient { 'X-Auth-Token': string; } | undefined { - const [uid, token] = [Meteor._localStorage.getItem(Accounts.USER_ID_KEY), Meteor._localStorage.getItem(Accounts.LOGIN_TOKEN_KEY)]; + const [uid, token] = [ + Accounts.storageLocation.getItem(Accounts.USER_ID_KEY), + Accounts.storageLocation.getItem(Accounts.LOGIN_TOKEN_KEY), + ]; if (!uid || !token) { return; diff --git a/apps/meteor/client/meteorOverrides/login/saml.ts b/apps/meteor/client/meteorOverrides/login/saml.ts index 14dfcc694e5c..f2199af5c0c7 100644 --- a/apps/meteor/client/meteorOverrides/login/saml.ts +++ b/apps/meteor/client/meteorOverrides/login/saml.ts @@ -72,7 +72,7 @@ Meteor.logout = async function (...args) { // Remove the userId from the client to prevent calls to the server while the logout is processed. // If the logout fails, the userId will be reloaded on the resume call - Meteor._localStorage.removeItem(Accounts.USER_ID_KEY); + Accounts.storageLocation.removeItem(Accounts.USER_ID_KEY); // A nasty bounce: 'result' has the SAML LogoutRequest but we need a proper 302 to redirected from the server. window.location.replace(Meteor.absoluteUrl(`_saml/sloRedirect/${provider}/?redirect=${encodeURIComponent(result)}`)); diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index 27bba21eae95..53761fbef4e7 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -19,8 +19,6 @@ import { useDeleteUser } from './hooks/useDeleteUser'; import { useEmailVerificationWarning } from './hooks/useEmailVerificationWarning'; import { useUpdateAvatar } from './hooks/useUpdateAvatar'; -const getUserId = (): string | null => Meteor.userId(); - const getUser = (): IUser | null => Meteor.user() as IUser | null; const logout = (): Promise => @@ -42,9 +40,9 @@ type UserProviderProps = { }; const UserProvider = ({ children }: UserProviderProps): ReactElement => { - const userId = useReactiveValue(getUserId); - const previousUserId = useRef(userId); const user = useReactiveValue(getUser); + const userId = user?._id ?? null; + const previousUserId = useRef(userId); const [userLanguage, setUserLanguage] = useLocalStorage('userLanguage', ''); const [preferedLanguage, setPreferedLanguage] = useLocalStorage('preferedLanguage', ''); diff --git a/apps/meteor/client/startup/accounts.ts b/apps/meteor/client/startup/accounts.ts index 88008a606656..50c033dc0596 100644 --- a/apps/meteor/client/startup/accounts.ts +++ b/apps/meteor/client/startup/accounts.ts @@ -2,7 +2,7 @@ import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { settings } from '../../app/settings/client'; +// import { settings } from '../../app/settings/client'; import { mainReady } from '../../app/ui-utils/client'; import { sdk } from '../../app/utils/client/lib/SDKClient'; import { t } from '../../app/utils/lib/i18n'; @@ -25,17 +25,3 @@ Accounts.onEmailVerificationLink((token: string) => { }); }); }); - -Meteor.startup(() => { - Tracker.autorun((computation) => { - const forgetUserSessionOnWindowClose = settings.get('Accounts_ForgetUserSessionOnWindowClose'); - - if (forgetUserSessionOnWindowClose === undefined) { - return; - } - - computation.stop(); - - Accounts.config({ clientStorage: forgetUserSessionOnWindowClose ? 'session' : 'local' }); - }); -}); diff --git a/apps/meteor/client/views/oauth/components/AuthorizationFormPage.tsx b/apps/meteor/client/views/oauth/components/AuthorizationFormPage.tsx index 14f251042abc..623214352372 100644 --- a/apps/meteor/client/views/oauth/components/AuthorizationFormPage.tsx +++ b/apps/meteor/client/views/oauth/components/AuthorizationFormPage.tsx @@ -4,7 +4,6 @@ import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { Form } from '@rocket.chat/layout'; import { useLogout, useRoute } from '@rocket.chat/ui-contexts'; import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; import React, { useEffect, useMemo, useRef } from 'react'; import { Trans, useTranslation } from 'react-i18next'; @@ -19,7 +18,7 @@ type AuthorizationFormPageProps = { }; const AuthorizationFormPage = ({ oauthApp, redirectUri, user }: AuthorizationFormPageProps) => { - const token = useMemo(() => Meteor._localStorage.getItem(Accounts.LOGIN_TOKEN_KEY) ?? undefined, []); + const token = useMemo(() => Accounts.storageLocation.getItem(Accounts.LOGIN_TOKEN_KEY) ?? undefined, []); const formLabelId = useUniqueId(); diff --git a/apps/meteor/definition/externals/meteor/accounts-base.d.ts b/apps/meteor/definition/externals/meteor/accounts-base.d.ts index 31b70f7b7154..0d30eed0430d 100644 --- a/apps/meteor/definition/externals/meteor/accounts-base.d.ts +++ b/apps/meteor/definition/externals/meteor/accounts-base.d.ts @@ -1,5 +1,6 @@ declare module 'meteor/accounts-base' { namespace Accounts { + const storageLocation: Window['localStorage']; function createUser( options: { username?: string; From b4c3e5cfee9f5a778b26c374b8b89a5a1ae79a26 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 25 Sep 2024 19:43:42 -0300 Subject: [PATCH 008/291] Bump rocket.chat to 6.14.0-develop (#33366) --- apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/package.json | 2 +- package.json | 2 +- packages/core-typings/package.json | 2 +- packages/rest-typings/package.json | 2 +- yarn.lock | 22 +++++++++++----------- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index cfec7fa33824..de3eb50fa5d1 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "6.13.0-rc.1" + "version": "6.14.0-develop" } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 766770427e0a..3767939a7e3c 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "6.13.0-rc.1", + "version": "6.14.0-develop", "private": true, "author": { "name": "Rocket.Chat", diff --git a/package.json b/package.json index 1a87fa6034bf..f06deed440ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "6.13.0-rc.1", + "version": "6.14.0-develop", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 6fcd5a43d20d..0db23760066b 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", - "version": "6.13.0-rc.1", + "version": "6.14.0-develop", "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", "eslint": "~8.45.0", diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 6d435a93ccfc..30a448980c9b 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "6.13.0-rc.1", + "version": "6.14.0-develop", "devDependencies": { "@rocket.chat/eslint-config": "workspace:~", "@types/jest": "~29.5.13", diff --git a/yarn.lock b/yarn.lock index 161420f46399..f6c5de3e9a44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8962,10 +8962,10 @@ __metadata: "@rocket.chat/icons": "*" "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 7.0.0-rc.0 - "@rocket.chat/ui-contexts": 11.0.0-rc.0 + "@rocket.chat/ui-avatar": 7.0.0-rc.1 + "@rocket.chat/ui-contexts": 11.0.0-rc.1 "@rocket.chat/ui-kit": "*" - "@rocket.chat/ui-video-conf": 11.0.0-rc.0 + "@rocket.chat/ui-video-conf": 11.0.0-rc.1 "@tanstack/react-query": "*" react: "*" react-dom: "*" @@ -9052,8 +9052,8 @@ __metadata: "@rocket.chat/fuselage-tokens": "*" "@rocket.chat/message-parser": 0.31.31-rc.0 "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": 11.0.0-rc.0 - "@rocket.chat/ui-contexts": 11.0.0-rc.0 + "@rocket.chat/ui-client": 11.0.0-rc.1 + "@rocket.chat/ui-contexts": 11.0.0-rc.1 katex: "*" react: "*" languageName: unknown @@ -10286,7 +10286,7 @@ __metadata: typescript: ~5.5.4 peerDependencies: "@rocket.chat/fuselage": "*" - "@rocket.chat/ui-contexts": 11.0.0-rc.0 + "@rocket.chat/ui-contexts": 11.0.0-rc.1 react: ~17.0.2 languageName: unknown linkType: soft @@ -10337,8 +10337,8 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-avatar": 7.0.0-rc.0 - "@rocket.chat/ui-contexts": 11.0.0-rc.0 + "@rocket.chat/ui-avatar": 7.0.0-rc.1 + "@rocket.chat/ui-contexts": 11.0.0-rc.1 react: "*" react-i18next: "*" languageName: unknown @@ -10507,8 +10507,8 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 7.0.0-rc.0 - "@rocket.chat/ui-contexts": 11.0.0-rc.0 + "@rocket.chat/ui-avatar": 7.0.0-rc.1 + "@rocket.chat/ui-contexts": 11.0.0-rc.1 react: ^17.0.2 react-dom: ^17.0.2 languageName: unknown @@ -10596,7 +10596,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": 0.2.2 - "@rocket.chat/ui-contexts": 11.0.0-rc.0 + "@rocket.chat/ui-contexts": 11.0.0-rc.1 "@tanstack/react-query": "*" react: "*" react-hook-form: "*" From 6bee2a11a5ca3ed282311d0dd7f97d9c0a9392a0 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:54:59 -0700 Subject: [PATCH 009/291] ci: use node20 for release action (#33343) --- packages/release-action/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/release-action/action.yml b/packages/release-action/action.yml index 2fc8fe35d565..23d7382aab6d 100644 --- a/packages/release-action/action.yml +++ b/packages/release-action/action.yml @@ -10,7 +10,7 @@ inputs: required: false runs: - using: "node16" + using: "node20" main: "dist/index.js" branding: From fc26d85b287e9f3c7b8b8f714316cfb72470048e Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Sep 2024 18:47:39 -0300 Subject: [PATCH 010/291] regression: `Sidepanel` sort requires refresh after room update (#33370) --- .../views/room/Sidepanel/hooks/useTeamslistChildren.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts b/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts index 5791a6e5d547..772c29509608 100644 --- a/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts +++ b/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts @@ -1,6 +1,7 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import type { Mongo } from 'meteor/mongo'; import { useEffect, useMemo } from 'react'; import { ChatRoom } from '../../../../../app/models/client'; @@ -12,7 +13,7 @@ const sortRoomByLastMessage = (a: IRoom, b: IRoom) => { if (!b.lm) { return -1; } - return new Date(b.lm).toUTCString().localeCompare(new Date(a.lm).toUTCString()); + return b.lm.getTime() - a.lm.getTime(); }; export const useTeamsListChildrenUpdate = ( @@ -23,7 +24,7 @@ export const useTeamsListChildrenUpdate = ( const queryClient = useQueryClient(); const query = useMemo(() => { - const query: Parameters[0] = { + const query: Mongo.Selector = { $or: [ { _id: parentRid, From fa226e4fcc3f1e4f785dd45f0e615e13b7775100 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 27 Sep 2024 01:49:52 -0300 Subject: [PATCH 011/291] chore: add ui-composer to storybook (#33383) --- .gitignore | 1 + packages/ui-composer/package.json | 102 +++++++++++++++--------------- 2 files changed, 53 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index dbad2c29a22c..03e74631957b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ yarn-error.log* .env.test.local .env.production.local +storybook-static # turbo .turbo diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index c43f3485880e..562865bfd2d1 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -1,52 +1,54 @@ { - "name": "@rocket.chat/ui-composer", - "version": "0.3.0-rc.0", - "private": true, - "main": "./dist/index.js", - "typings": "./dist/index.d.ts", - "files": [ - "/dist" - ], - "scripts": { - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", - "build": "rm -rf dist && tsc -p tsconfig.build.json", - "typecheck": "tsc --noEmit", - "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", - "storybook": "start-storybook -p 6006" - }, - "devDependencies": { - "@babel/core": "~7.22.20", - "@react-aria/toolbar": "^3.0.0-beta.1", - "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.59.1", - "@rocket.chat/icons": "~0.38.0", - "@storybook/addon-actions": "~6.5.16", - "@storybook/addon-docs": "~6.5.16", - "@storybook/addon-essentials": "~6.5.16", - "@storybook/builder-webpack4": "~6.5.16", - "@storybook/manager-webpack4": "~6.5.16", - "@storybook/react": "~6.5.16", - "@storybook/testing-library": "~0.0.13", - "@types/react": "~17.0.80", - "@types/react-dom": "~17.0.25", - "eslint": "~8.45.0", - "eslint-plugin-react": "~7.32.2", - "eslint-plugin-react-hooks": "~4.6.2", - "eslint-plugin-storybook": "~0.6.15", - "react": "~17.0.2", - "react-docgen-typescript-plugin": "~1.0.8", - "react-dom": "~17.0.2", - "typescript": "~5.5.4" - }, - "peerDependencies": { - "@react-aria/toolbar": "*", - "@rocket.chat/fuselage": "*", - "@rocket.chat/icons": "*", - "react": "^17.0.2", - "react-dom": "^17.0.2" - }, - "volta": { - "extends": "../../package.json" - } + "name": "@rocket.chat/ui-composer", + "version": "0.3.0-rc.0", + "private": true, + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "/dist" + ], + "scripts": { + "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "build": "rm -rf dist && tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit", + "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", + "storybook": "start-storybook -p 6006", + "build-preview": "build-storybook", + ".:build-preview-move": "mkdir -p ../../.preview/ && cp -r ./storybook-static ../../.preview/ui-composer" + }, + "devDependencies": { + "@babel/core": "~7.22.20", + "@react-aria/toolbar": "^3.0.0-beta.1", + "@rocket.chat/eslint-config": "workspace:^", + "@rocket.chat/fuselage": "^0.59.1", + "@rocket.chat/icons": "~0.38.0", + "@storybook/addon-actions": "~6.5.16", + "@storybook/addon-docs": "~6.5.16", + "@storybook/addon-essentials": "~6.5.16", + "@storybook/builder-webpack4": "~6.5.16", + "@storybook/manager-webpack4": "~6.5.16", + "@storybook/react": "~6.5.16", + "@storybook/testing-library": "~0.0.13", + "@types/react": "~17.0.80", + "@types/react-dom": "~17.0.25", + "eslint": "~8.45.0", + "eslint-plugin-react": "~7.32.2", + "eslint-plugin-react-hooks": "~4.6.2", + "eslint-plugin-storybook": "~0.6.15", + "react": "~17.0.2", + "react-docgen-typescript-plugin": "~1.0.8", + "react-dom": "~17.0.2", + "typescript": "~5.5.4" + }, + "peerDependencies": { + "@react-aria/toolbar": "*", + "@rocket.chat/fuselage": "*", + "@rocket.chat/icons": "*", + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "volta": { + "extends": "../../package.json" + } } From 34087b04728c52038cfd3dc7c5412c1fa35d58e3 Mon Sep 17 00:00:00 2001 From: "Julio A." <52619625+julio-cfa@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:46:04 +0200 Subject: [PATCH 012/291] ci: remove Jira-GitHub security integration (#33384) --- .../vulnerabilities-jira-integration.yml | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 .github/workflows/vulnerabilities-jira-integration.yml diff --git a/.github/workflows/vulnerabilities-jira-integration.yml b/.github/workflows/vulnerabilities-jira-integration.yml deleted file mode 100644 index 2daeb533937d..000000000000 --- a/.github/workflows/vulnerabilities-jira-integration.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Github vulnerabilities and jira board integration - -on: - schedule: - - cron: '0 1 * * *' - -jobs: - IntegrateSecurityVulnerabilities: - runs-on: ubuntu-latest - steps: - - name: "Github vulnerabilities and jira board integration" - uses: RocketChat/github-vulnerabilities-jira-integration@v0.3 - env: - JIRA_URL: https://rocketchat.atlassian.net/ - JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }} - GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} - JIRA_EMAIL: security-team-accounts@rocket.chat - JIRA_PROJECT_ID: GJIT - UID_CUSTOMFIELD_ID: customfield_10059 - JIRA_COMPLETE_PHASE_ID: 31 - JIRA_START_PHASE_ID: 11 - From 5965a1dbfe568f3379f53202a888932e6ffd3e52 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:47:09 -0300 Subject: [PATCH 013/291] chore: Single Contact ID: improved typings and removed some duplicated code (#33324) --- .../app/livechat/server/api/v1/contact.ts | 14 +-- .../app/livechat/server/lib/Contacts.ts | 87 ++++++++----------- .../app/livechat/server/lib/LivechatTyped.ts | 4 +- .../server/models/raw/LivechatCustomField.ts | 6 +- 4 files changed, 50 insertions(+), 61 deletions(-) diff --git a/apps/meteor/app/livechat/server/api/v1/contact.ts b/apps/meteor/app/livechat/server/api/v1/contact.ts index f3fec80b23fe..ec0559d5f191 100644 --- a/apps/meteor/app/livechat/server/api/v1/contact.ts +++ b/apps/meteor/app/livechat/server/api/v1/contact.ts @@ -9,7 +9,7 @@ import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { API } from '../../../../api/server'; -import { Contacts, createContact, updateContact } from '../../lib/Contacts'; +import { Contacts, createContact, updateContact, isSingleContactEnabled } from '../../lib/Contacts'; API.v1.addRoute( 'omnichannel/contact', @@ -96,8 +96,8 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['create-livechat-contact'], validateParams: isPOSTOmnichannelContactsProps }, { async post() { - if (process.env.TEST_MODE?.toUpperCase() !== 'TRUE') { - throw new Meteor.Error('error-not-allowed', 'This endpoint is only allowed in test mode'); + if (!isSingleContactEnabled()) { + return API.v1.unauthorized(); } const contactId = await createContact({ ...this.bodyParams, unknown: false }); @@ -111,8 +111,8 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['update-livechat-contact'], validateParams: isPOSTUpdateOmnichannelContactsProps }, { async post() { - if (process.env.TEST_MODE?.toUpperCase() !== 'TRUE') { - throw new Meteor.Error('error-not-allowed', 'This endpoint is only allowed in test mode'); + if (!isSingleContactEnabled()) { + return API.v1.unauthorized(); } const contact = await updateContact({ ...this.bodyParams }); @@ -127,8 +127,8 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-livechat-contact'], validateParams: isGETOmnichannelContactsProps }, { async get() { - if (process.env.TEST_MODE?.toUpperCase() !== 'TRUE') { - throw new Meteor.Error('error-not-allowed', 'This endpoint is only allowed in test mode'); + if (!isSingleContactEnabled()) { + return API.v1.unauthorized(); } const contact = await LivechatContacts.findOneById(this.queryParams.contactId); diff --git a/apps/meteor/app/livechat/server/lib/Contacts.ts b/apps/meteor/app/livechat/server/lib/Contacts.ts index f6f812ce8af8..e9be40aa942b 100644 --- a/apps/meteor/app/livechat/server/lib/Contacts.ts +++ b/apps/meteor/app/livechat/server/lib/Contacts.ts @@ -1,4 +1,5 @@ import type { + AtLeast, ILivechatContact, ILivechatContactChannel, ILivechatCustomField, @@ -113,41 +114,8 @@ export const Contacts = { } } - const allowedCF = LivechatCustomField.findByScope>( - 'visitor', - { - projection: { _id: 1, label: 1, regexp: 1, required: 1 }, - }, - false, - ); - - const livechatData: Record = {}; - - for await (const cf of allowedCF) { - if (!customFields.hasOwnProperty(cf._id)) { - if (cf.required) { - throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); - } - continue; - } - const cfValue: string = trim(customFields[cf._id]); - - if (!cfValue || typeof cfValue !== 'string') { - if (cf.required) { - throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); - } - continue; - } - - if (cf.regexp) { - const regex = new RegExp(cf.regexp); - if (!regex.test(cfValue)) { - throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); - } - } - - livechatData[cf._id] = cfValue; - } + const allowedCF = await getAllowedCustomFields(); + const livechatData: Record = validateCustomFields(allowedCF, customFields, { ignoreAdditionalFields: true }); const fieldsToRemove = { // if field is explicitely set to empty string, remove @@ -202,15 +170,20 @@ export const Contacts = { }, }; +export function isSingleContactEnabled(): boolean { + // The Single Contact feature is not yet available in production, but can already be partially used in test environments. + return process.env.TEST_MODE?.toUpperCase() === 'TRUE'; +} + export async function createContact(params: CreateContactParams): Promise { - const { name, emails, phones, customFields = {}, contactManager, channels, unknown } = params; + const { name, emails, phones, customFields: receivedCustomFields = {}, contactManager, channels, unknown } = params; if (contactManager) { await validateContactManager(contactManager); } const allowedCustomFields = await getAllowedCustomFields(); - validateCustomFields(allowedCustomFields, customFields); + const customFields = validateCustomFields(allowedCustomFields, receivedCustomFields); const { insertedId } = await LivechatContacts.insertOne({ name, @@ -226,7 +199,7 @@ export async function createContact(params: CreateContactParams): Promise { - const { contactId, name, emails, phones, customFields, contactManager, channels } = params; + const { contactId, name, emails, phones, customFields: receivedCustomFields, contactManager, channels } = params; const contact = await LivechatContacts.findOneById>(contactId, { projection: { _id: 1 } }); @@ -238,17 +211,21 @@ export async function updateContact(params: UpdateContactParams): Promise { +async function getAllowedCustomFields(): Promise[]> { return LivechatCustomField.findByScope( 'visitor', { @@ -258,7 +235,13 @@ async function getAllowedCustomFields(): Promise { ).toArray(); } -export function validateCustomFields(allowedCustomFields: ILivechatCustomField[], customFields: Record) { +export function validateCustomFields( + allowedCustomFields: AtLeast[], + customFields: Record, + options?: { ignoreAdditionalFields?: boolean }, +): Record { + const validValues: Record = {}; + for (const cf of allowedCustomFields) { if (!customFields.hasOwnProperty(cf._id)) { if (cf.required) { @@ -281,14 +264,20 @@ export function validateCustomFields(allowedCustomFields: ILivechatCustomField[] throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); } } + + validValues[cf._id] = cfValue; } - const allowedCustomFieldIds = new Set(allowedCustomFields.map((cf) => cf._id)); - for (const key in customFields) { - if (!allowedCustomFieldIds.has(key)) { - throw new Error(i18n.t('error-custom-field-not-allowed', { key })); + if (!options?.ignoreAdditionalFields) { + const allowedCustomFieldIds = new Set(allowedCustomFields.map((cf) => cf._id)); + for (const key in customFields) { + if (!allowedCustomFieldIds.has(key)) { + throw new Error(i18n.t('error-custom-field-not-allowed', { key })); + } } } + + return validValues; } export async function validateContactManager(contactManagerUserId: string) { diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 6c2d655f4c95..44ee46f04418 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -71,7 +71,7 @@ import * as Mailer from '../../../mailer/server/api'; import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; import { businessHourManager } from '../business-hour'; -import { createContact } from './Contacts'; +import { createContact, isSingleContactEnabled } from './Contacts'; import { parseAgentCustomFields, updateDepartmentAgents, validateEmail, normalizeTransferredByData } from './Helper'; import { QueueManager } from './QueueManager'; import { RoutingManager } from './RoutingManager'; @@ -669,7 +669,7 @@ class LivechatClass { } } - if (process.env.TEST_MODE?.toUpperCase() === 'TRUE') { + if (isSingleContactEnabled()) { const contactId = await createContact({ name: name ?? (visitorDataToUpdate.username as string), emails: email ? [email] : [], diff --git a/apps/meteor/server/models/raw/LivechatCustomField.ts b/apps/meteor/server/models/raw/LivechatCustomField.ts index 71228f55069d..38a93f6439b4 100644 --- a/apps/meteor/server/models/raw/LivechatCustomField.ts +++ b/apps/meteor/server/models/raw/LivechatCustomField.ts @@ -13,12 +13,12 @@ export class LivechatCustomFieldRaw extends BaseRaw implem return [{ key: { scope: 1 } }]; } - findByScope( + findByScope( scope: ILivechatCustomField['scope'], options?: FindOptions, includeHidden = true, - ): FindCursor { - return this.find({ scope, ...(includeHidden === true ? {} : { visibility: { $ne: 'hidden' } }) }, options); + ): FindCursor { + return this.find({ scope, ...(includeHidden === true ? {} : { visibility: { $ne: 'hidden' } }) }, options); } findMatchingCustomFields( From 92e366ee285a2f6adec4bafa45c3bc21994cf1e3 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Fri, 27 Sep 2024 14:45:31 -0300 Subject: [PATCH 014/291] fix: race condition when forwarding livechat by splitting subscription removal (#33381) --- .changeset/dry-taxis-cry.md | 5 +++++ apps/meteor/server/models/raw/BaseRaw.ts | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 .changeset/dry-taxis-cry.md diff --git a/.changeset/dry-taxis-cry.md b/.changeset/dry-taxis-cry.md new file mode 100644 index 000000000000..ae8244087d9e --- /dev/null +++ b/.changeset/dry-taxis-cry.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes a race condition that causes livechat conversations to get stuck in the agent's sidebar panel after being forwarded. diff --git a/apps/meteor/server/models/raw/BaseRaw.ts b/apps/meteor/server/models/raw/BaseRaw.ts index d822038b177e..3e763017bbd3 100644 --- a/apps/meteor/server/models/raw/BaseRaw.ts +++ b/apps/meteor/server/models/raw/BaseRaw.ts @@ -318,33 +318,33 @@ export abstract class BaseRaw< async findOneAndDelete(filter: Filter, options?: FindOneAndDeleteOptions): Promise> { if (!this.trash) { - if (options) { - return this.col.findOneAndDelete(filter, options); - } - return this.col.findOneAndDelete(filter); + return this.col.findOneAndDelete(filter, options || {}); } - const result = await this.col.findOneAndDelete(filter); - - const { value: doc } = result; + const doc = await this.col.findOne(filter); if (!doc) { - return result; + return { ok: 1, value: null }; } const { _id, ...record } = doc; - const trash: TDeleted = { ...record, _deletedAt: new Date(), __collection__: this.name, } as unknown as TDeleted; - // since the operation is not atomic, we need to make sure that the record is not already deleted/inserted await this.trash?.updateOne({ _id } as Filter, { $set: trash } as UpdateFilter, { upsert: true, }); - return result; + try { + await this.col.deleteOne({ _id } as Filter); + } catch (e) { + await this.trash?.deleteOne({ _id } as Filter); + throw e; + } + + return { ok: 1, value: doc }; } async deleteMany(filter: Filter, options?: DeleteOptions & { onTrash?: (record: ResultFields) => void }): Promise { From b17b3befdf860eeb532d9dfd78903ab15a709b2f Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 27 Sep 2024 14:11:21 -0600 Subject: [PATCH 015/291] fix: Avoid notifying `watch.settings` on uncaught errors (#33376) --- .../server/lib/RocketChat.ErrorHandler.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts b/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts index 264443a1378b..984561fe13cd 100644 --- a/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts +++ b/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts @@ -3,14 +3,13 @@ import { Meteor } from 'meteor/meteor'; import { throttledCounter } from '../../../../lib/utils/throttledCounter'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; -import { notifyOnSettingChanged } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; const incException = throttledCounter((counter) => { Settings.incrementValueById('Uncaught_Exceptions_Count', counter, { returnDocument: 'after' }) .then(({ value }) => { if (value) { - void notifyOnSettingChanged(value); + settings.set(value); } }) .catch(console.error); @@ -118,5 +117,12 @@ process.on('unhandledRejection', (error) => { process.on('uncaughtException', async (error) => { incException(); + + console.error('=== UnCaughtException ==='); + console.error(error); + console.error('-------------------------'); + console.error('Errors like this can cause oplog processing errors.'); + console.error('==========================='); + void errorHandler.trackError(error.message, error.stack); }); From a430faf72bb160355bf7cb92549e27d341e35d7c Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 27 Sep 2024 18:52:13 -0300 Subject: [PATCH 016/291] chore: add gazzodown preview (#33279) --- .gitignore | 2 ++ packages/gazzodown/.storybook/main.js | 8 ++++++++ packages/gazzodown/package.json | 2 ++ 3 files changed, 12 insertions(+) diff --git a/.gitignore b/.gitignore index 03e74631957b..8ca2d018f92e 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,5 @@ storybook-static data/ registration.yaml + +storybook-static diff --git a/packages/gazzodown/.storybook/main.js b/packages/gazzodown/.storybook/main.js index de5a951bbded..f12b58cf856f 100644 --- a/packages/gazzodown/.storybook/main.js +++ b/packages/gazzodown/.storybook/main.js @@ -15,6 +15,14 @@ module.exports = { include: /node_modules/, loader: 'babel-loader', }); + config.module.rules.push({ + test: /\.m?js$/, + include: /node_modules/, + type: 'javascript/auto', + use: { + loader: require.resolve('babel-loader'), + }, + }); return config; }, }; diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index cf4a9209d649..ca64df5ec913 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -10,6 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.build.json", "build-storybook": "build-storybook", + "build-preview": "build-storybook --quiet", + ".:build-preview-move": "mkdir -p ../../.preview && cp -r ./storybook-static ../../.preview/gazzodown", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", "lint": "eslint --ext .js,.jsx,.ts,.tsx .", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", From 214d1b348681bb43f6c1b859b31efe5773bb2cce Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 27 Sep 2024 19:11:27 -0300 Subject: [PATCH 017/291] chore(Sidepanel): uses only local channels and discussions (#33387) --- .../views/room/Sidepanel/RoomSidepanel.tsx | 4 +- .../SidepanelItem/RoomSidepanelItem.tsx | 4 +- .../Sidepanel/hooks/useTeamslistChildren.ts | 79 ++++--------------- 3 files changed, 20 insertions(+), 67 deletions(-) diff --git a/apps/meteor/client/views/room/Sidepanel/RoomSidepanel.tsx b/apps/meteor/client/views/room/Sidepanel/RoomSidepanel.tsx index 27c45e2774e8..6ee16a850202 100644 --- a/apps/meteor/client/views/room/Sidepanel/RoomSidepanel.tsx +++ b/apps/meteor/client/views/room/Sidepanel/RoomSidepanel.tsx @@ -51,8 +51,8 @@ const RoomSidepanelWithData = ({ parentRid, openedRoom }: { parentRid: string; o ( diff --git a/apps/meteor/client/views/room/Sidepanel/SidepanelItem/RoomSidepanelItem.tsx b/apps/meteor/client/views/room/Sidepanel/SidepanelItem/RoomSidepanelItem.tsx index dceb69e1aba3..8bb4d84eaebe 100644 --- a/apps/meteor/client/views/room/Sidepanel/SidepanelItem/RoomSidepanelItem.tsx +++ b/apps/meteor/client/views/room/Sidepanel/SidepanelItem/RoomSidepanelItem.tsx @@ -1,4 +1,4 @@ -import type { IRoom, ISubscription, Serialized } from '@rocket.chat/core-typings'; +import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; import { useUserSubscription } from '@rocket.chat/ui-contexts'; import React, { memo } from 'react'; @@ -8,7 +8,7 @@ import { useItemData } from '../hooks/useItemData'; export type RoomSidepanelItemProps = { openedRoom?: string; - room: Serialized; + room: IRoom; parentRid: string; viewMode?: 'extended' | 'medium' | 'condensed'; }; diff --git a/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts b/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts index 772c29509608..de7645ae2a30 100644 --- a/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts +++ b/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts @@ -1,107 +1,60 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; import type { Mongo } from 'meteor/mongo'; import { useEffect, useMemo } from 'react'; import { ChatRoom } from '../../../../../app/models/client'; -const sortRoomByLastMessage = (a: IRoom, b: IRoom) => { - if (!a.lm) { - return 1; - } - if (!b.lm) { - return -1; - } - return b.lm.getTime() - a.lm.getTime(); -}; - export const useTeamsListChildrenUpdate = ( parentRid: string, teamId?: string | null, sidepanelItems?: 'channels' | 'discussions' | null, ) => { - const queryClient = useQueryClient(); - const query = useMemo(() => { const query: Mongo.Selector = { $or: [ { _id: parentRid, }, - { - prid: parentRid, - }, ], }; - if (teamId && query.$or) { + if ((!sidepanelItems || sidepanelItems === 'discussions') && query.$or) { + query.$or.push({ + prid: parentRid, + }); + } + + if ((!sidepanelItems || sidepanelItems === 'channels') && teamId && query.$or) { query.$or.push({ teamId, }); } return query; - }, [parentRid, teamId]); + }, [parentRid, teamId, sidepanelItems]); - const teamList = useEndpoint('GET', '/v1/teams.listChildren'); - - const listRoomsAndDiscussions = useEndpoint('GET', '/v1/teams.listChildren'); const result = useQuery({ queryKey: ['sidepanel', 'list', parentRid, sidepanelItems], queryFn: () => - listRoomsAndDiscussions({ - roomId: parentRid, - sort: JSON.stringify({ lm: -1 }), - type: sidepanelItems || undefined, - }), + ChatRoom.find(query, { + sort: { lm: -1 }, + }).fetch(), enabled: sidepanelItems !== null && teamId !== null, refetchInterval: 5 * 60 * 1000, keepPreviousData: true, }); - const { mutate: update } = useMutation({ - mutationFn: async (params?: { action: 'add' | 'remove' | 'update'; data: IRoom }) => { - queryClient.setQueryData(['sidepanel', 'list', parentRid, sidepanelItems], (data: Awaited> | void) => { - if (!data) { - return; - } - - if (params?.action === 'add') { - data.data = [JSON.parse(JSON.stringify(params.data)), ...data.data].sort(sortRoomByLastMessage); - } - - if (params?.action === 'remove') { - data.data = data.data.filter((item) => item._id !== params.data?._id); - } - - if (params?.action === 'update') { - data.data = data.data - .map((item) => (item._id === params.data?._id ? JSON.parse(JSON.stringify(params.data)) : item)) - .sort(sortRoomByLastMessage); - } - - return { ...data }; - }); - }, - }); - useEffect(() => { const liveQueryHandle = ChatRoom.find(query).observe({ - added: (item) => { - queueMicrotask(() => update({ action: 'add', data: item })); - }, - changed: (item) => { - queueMicrotask(() => update({ action: 'update', data: item })); - }, - removed: (item) => { - queueMicrotask(() => update({ action: 'remove', data: item })); - }, + added: () => queueMicrotask(() => result.refetch({ exact: false })), + changed: () => queueMicrotask(() => result.refetch({ exact: false })), + removed: () => queueMicrotask(() => result.refetch({ exact: false })), }); return () => { liveQueryHandle.stop(); }; - }, [update, query]); + }, [query, result]); return result; }; From bc1e6eec3be194a017624f73449c5974d0e296a1 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Fri, 27 Sep 2024 19:36:57 -0300 Subject: [PATCH 018/291] chore: Move Apps-Engine to monorepo (#32951) --- .github/CODEOWNERS | 1 + .github/actions/build-docker-image/action.yml | 5 +- .github/actions/build-docker/action.yml | 7 + .github/actions/meteor-build/action.yml | 5 + .github/actions/setup-node/action.yml | 25 +- .github/workflows/ci-code-check.yml | 4 + .../workflows/ci-deploy-gh-pages-preview.yml | 1 + .github/workflows/ci-deploy-gh-pages.yml | 1 + .github/workflows/ci-test-e2e.yml | 4 + .github/workflows/ci-test-unit.yml | 4 + .github/workflows/ci.yml | 19 + .github/workflows/new-release.yml | 1 + .github/workflows/pr-update-description.yml | 1 + .github/workflows/publish-release.yml | 1 + .github/workflows/release-candidate.yml | 1 + .tool-versions | 1 + README.md | 1 + apps/meteor/.docker/Dockerfile | 11 +- apps/meteor/.docker/Dockerfile.alpine | 27 +- apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- docker-compose-ci.yml | 4 + ee/apps/account-service/Dockerfile | 4 + ee/apps/authorization-service/Dockerfile | 4 + ee/apps/ddp-streamer/Dockerfile | 4 + ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/Dockerfile | 4 + ee/apps/presence-service/Dockerfile | 4 + ee/apps/queue-worker/Dockerfile | 4 + ee/apps/stream-hub-service/Dockerfile | 4 + ee/packages/presence/package.json | 2 +- packages/apps-engine/.eslintignore | 8 + packages/apps-engine/.eslintrc.json | 58 + packages/apps-engine/.gitignore | 61 + packages/apps-engine/.prettierrc | 7 + packages/apps-engine/README.md | 125 ++ packages/apps-engine/deno-runtime/.gitignore | 1 + .../deno-runtime/AppObjectRegistry.ts | 26 + .../apps-engine/deno-runtime/acorn-walk.d.ts | 170 ++ packages/apps-engine/deno-runtime/acorn.d.ts | 857 ++++++++++ packages/apps-engine/deno-runtime/deno.jsonc | 16 + packages/apps-engine/deno-runtime/deno.lock | 107 ++ .../deno-runtime/handlers/api-handler.ts | 46 + .../deno-runtime/handlers/app/construct.ts | 126 ++ .../handlers/app/handleGetStatus.ts | 15 + .../handlers/app/handleInitialize.ts | 19 + .../handlers/app/handleOnDisable.ts | 19 + .../handlers/app/handleOnEnable.ts | 16 + .../handlers/app/handleOnInstall.ts | 30 + .../handlers/app/handleOnPreSettingUpdate.ts | 22 + .../handlers/app/handleOnSettingUpdated.ts | 24 + .../handlers/app/handleOnUninstall.ts | 30 + .../handlers/app/handleOnUpdate.ts | 30 + .../handlers/app/handleSetStatus.ts | 29 + .../deno-runtime/handlers/app/handler.ts | 112 ++ .../deno-runtime/handlers/listener/handler.ts | 150 ++ .../handlers/scheduler-handler.ts | 51 + .../handlers/slashcommand-handler.ts | 122 ++ .../handlers/tests/api-handler.test.ts | 79 + .../handlers/tests/listener-handler.test.ts | 234 +++ .../handlers/tests/scheduler-handler.test.ts | 46 + .../tests/slashcommand-handler.test.ts | 152 ++ .../handlers/tests/uikit-handler.test.ts | 99 ++ .../tests/videoconference-handler.test.ts | 122 ++ .../deno-runtime/handlers/uikit/handler.ts | 82 + .../handlers/videoconference-handler.ts | 49 + .../lib/accessors/builders/BlockBuilder.ts | 216 +++ .../accessors/builders/DiscussionBuilder.ts | 59 + .../builders/LivechatMessageBuilder.ts | 204 +++ .../lib/accessors/builders/MessageBuilder.ts | 232 +++ .../lib/accessors/builders/RoomBuilder.ts | 163 ++ .../lib/accessors/builders/UserBuilder.ts | 81 + .../builders/VideoConferenceBuilder.ts | 94 ++ .../lib/accessors/extenders/HttpExtender.ts | 62 + .../accessors/extenders/MessageExtender.ts | 66 + .../lib/accessors/extenders/RoomExtender.ts | 61 + .../extenders/VideoConferenceExtend.ts | 69 + .../deno-runtime/lib/accessors/http.ts | 92 ++ .../deno-runtime/lib/accessors/mod.ts | 302 ++++ .../lib/accessors/modify/ModifyCreator.ts | 344 ++++ .../lib/accessors/modify/ModifyExtender.ts | 93 ++ .../lib/accessors/modify/ModifyUpdater.ts | 153 ++ .../deno-runtime/lib/accessors/notifier.ts | 75 + .../lib/accessors/tests/AppAccessors.test.ts | 122 ++ .../lib/accessors/tests/ModifyCreator.test.ts | 106 ++ .../accessors/tests/ModifyExtender.test.ts | 120 ++ .../lib/accessors/tests/ModifyUpdater.test.ts | 128 ++ .../apps-engine/deno-runtime/lib/ast/mod.ts | 64 + .../deno-runtime/lib/ast/operations.ts | 239 +++ .../lib/ast/tests/data/ast_blocks.ts | 436 +++++ .../lib/ast/tests/operations.test.ts | 245 +++ .../apps-engine/deno-runtime/lib/codec.ts | 43 + .../apps-engine/deno-runtime/lib/logger.ts | 142 ++ .../apps-engine/deno-runtime/lib/messenger.ts | 199 +++ .../apps-engine/deno-runtime/lib/require.ts | 14 + packages/apps-engine/deno-runtime/lib/room.ts | 104 ++ .../deno-runtime/lib/roomFactory.ts | 27 + .../lib/sanitizeDeprecatedUsage.ts | 20 + .../deno-runtime/lib/tests/logger.test.ts | 111 ++ .../deno-runtime/lib/tests/messenger.test.ts | 96 ++ packages/apps-engine/deno-runtime/main.ts | 129 ++ packages/apps-engine/package.json | 132 ++ packages/apps-engine/scripts/bundle.js | 35 + packages/apps-engine/scripts/deno-cache.js | 25 + .../src/client/AppClientManager.ts | 28 + .../src/client/AppServerCommunicator.ts | 16 + .../src/client/AppsEngineUIClient.ts | 70 + .../src/client/AppsEngineUIHost.ts | 78 + .../apps-engine/src/client/constants/index.ts | 6 + .../client/definition/AppsEngineUIMethods.ts | 7 + .../definition/IAppsEngineUIResponse.ts | 19 + .../definition/IExternalComponentRoomInfo.ts | 16 + .../definition/IExternalComponentUserInfo.ts | 14 + .../src/client/definition/index.ts | 4 + packages/apps-engine/src/client/index.ts | 4 + .../apps-engine/src/client/utils/index.ts | 18 + packages/apps-engine/src/definition/App.ts | 236 +++ .../apps-engine/src/definition/AppStatus.ts | 65 + packages/apps-engine/src/definition/IApp.ts | 90 + packages/apps-engine/src/definition/LICENSE | 21 + .../src/definition/accessors/IApiExtend.ts | 16 + .../src/definition/accessors/IAppAccessors.ts | 11 + .../accessors/IAppInstallationContext.ts | 5 + .../accessors/IAppUninstallationContext.ts | 5 + .../definition/accessors/IAppUpdateContext.ts | 6 + .../accessors/ICloudWorkspaceRead.ts | 24 + .../accessors/IConfigurationExtend.ts | 36 + .../accessors/IConfigurationModify.ts | 18 + .../accessors/IDiscussionBuilder.ts | 25 + .../src/definition/accessors/IEmailCreator.ts | 10 + .../definition/accessors/IEnvironmentRead.ts | 27 + .../definition/accessors/IEnvironmentWrite.ts | 10 + .../accessors/IEnvironmentalVariableRead.ts | 11 + .../accessors/IExternalComponentsExtend.ts | 17 + .../src/definition/accessors/IHttp.ts | 202 +++ .../definition/accessors/ILivechatCreator.ts | 43 + .../accessors/ILivechatMessageBuilder.ts | 219 +++ .../src/definition/accessors/ILivechatRead.ts | 38 + .../definition/accessors/ILivechatUpdater.ts | 33 + .../src/definition/accessors/ILogEntry.ts | 22 + .../src/definition/accessors/ILogger.ts | 29 + .../definition/accessors/IMessageBuilder.ts | 236 +++ .../definition/accessors/IMessageExtender.ts | 36 + .../src/definition/accessors/IMessageRead.ts | 15 + .../definition/accessors/IMessageUpdater.ts | 21 + .../definition/accessors/IModerationModify.ts | 27 + .../src/definition/accessors/IModify.ts | 45 + .../definition/accessors/IModifyCreator.ts | 100 ++ .../definition/accessors/IModifyDeleter.ts | 12 + .../definition/accessors/IModifyExtender.ts | 40 + .../definition/accessors/IModifyUpdater.ts | 52 + .../src/definition/accessors/INotifier.ts | 63 + .../src/definition/accessors/IOAuthApp.ts | 13 + .../definition/accessors/IOAuthAppsModify.ts | 23 + .../definition/accessors/IOAuthAppsReader.ts | 16 + .../src/definition/accessors/IPersistence.ts | 97 ++ .../definition/accessors/IPersistenceRead.ts | 40 + .../src/definition/accessors/IRead.ts | 52 + .../src/definition/accessors/IRoleRead.ts | 27 + .../src/definition/accessors/IRoomBuilder.ts | 186 +++ .../src/definition/accessors/IRoomExtender.ts | 40 + .../src/definition/accessors/IRoomRead.ts | 93 ++ .../definition/accessors/ISchedulerExtend.ts | 11 + .../definition/accessors/ISchedulerModify.ts | 31 + .../accessors/IServerSettingRead.ts | 43 + .../accessors/IServerSettingUpdater.ts | 6 + .../accessors/IServerSettingsModify.ts | 40 + .../src/definition/accessors/ISettingRead.ts | 23 + .../definition/accessors/ISettingUpdater.ts | 5 + .../definition/accessors/ISettingsExtend.ts | 17 + .../accessors/ISlashCommandsExtend.ts | 16 + .../accessors/ISlashCommandsModify.ts | 30 + .../src/definition/accessors/IThreadRead.ts | 9 + .../src/definition/accessors/IUIController.ts | 31 + .../src/definition/accessors/IUIExtend.ts | 5 + .../definition/accessors/IUploadCreator.ts | 12 + .../src/definition/accessors/IUploadRead.ts | 7 + .../src/definition/accessors/IUserBuilder.ts | 60 + .../src/definition/accessors/IUserRead.ts | 22 + .../src/definition/accessors/IUserUpdater.ts | 18 + .../accessors/IVideoConfProvidersExtend.ts | 15 + .../accessors/IVideoConferenceBuilder.ts | 34 + .../accessors/IVideoConferenceExtend.ts | 21 + .../accessors/IVideoConferenceRead.ts | 15 + .../src/definition/accessors/index.ts | 58 + .../src/definition/api/ApiEndpoint.ts | 40 + .../apps-engine/src/definition/api/IApi.ts | 58 + .../src/definition/api/IApiEndpoint.ts | 47 + .../src/definition/api/IApiEndpointInfo.ts | 6 + .../definition/api/IApiEndpointMetadata.ts | 10 + .../src/definition/api/IApiExample.ts | 19 + .../src/definition/api/IRequest.ts | 16 + .../src/definition/api/IResponse.ts | 13 + .../apps-engine/src/definition/api/index.ts | 8 + .../src/definition/app-schema.json | 75 + .../src/definition/assets/IAsset.ts | 6 + .../src/definition/assets/IAssetProvider.ts | 5 + .../src/definition/assets/index.ts | 4 + .../src/definition/cloud/IWorkspaceToken.ts | 4 + .../src/definition/email/IEmail.ts | 9 + .../src/definition/email/IEmailDescriptor.ts | 11 + .../src/definition/email/IPreEmailSent.ts | 25 + .../definition/email/IPreEmailSentContext.ts | 6 + .../apps-engine/src/definition/email/index.ts | 4 + .../src/definition/example-app.json | 13 + .../exceptions/AppsEngineException.ts | 32 + .../EssentialAppDisabledException.ts | 16 + .../FileUploadNotAllowedException.ts | 12 + .../InvalidSettingValueException.ts | 8 + .../exceptions/UserNotAllowedException.ts | 14 + .../src/definition/exceptions/index.ts | 5 + .../externalComponent/IExternalComponent.ts | 51 + .../IExternalComponentOptions.ts | 10 + .../IExternalComponentState.ts | 16 + .../IPostExternalComponentClosed.ts | 16 + .../IPostExternalComponentOpened.ts | 16 + .../src/definition/externalComponent/index.ts | 5 + .../src/definition/livechat/IDepartment.ts | 17 + .../livechat/ILivechatEventContext.ts | 7 + .../definition/livechat/ILivechatMessage.ts | 7 + .../src/definition/livechat/ILivechatRoom.ts | 55 + .../livechat/ILivechatRoomClosedHandler.ts | 19 + .../livechat/ILivechatTransferData.ts | 8 + .../livechat/ILivechatTransferEventContext.ts | 15 + .../livechat/IPostLivechatAgentAssigned.ts | 25 + .../livechat/IPostLivechatAgentUnassigned.ts | 25 + .../livechat/IPostLivechatGuestSaved.ts | 19 + .../livechat/IPostLivechatRoomClosed.ts | 19 + .../livechat/IPostLivechatRoomSaved.ts | 19 + .../livechat/IPostLivechatRoomStarted.ts | 19 + .../livechat/IPostLivechatRoomTransferred.ts | 13 + .../src/definition/livechat/IVisitor.ts | 16 + .../src/definition/livechat/IVisitorEmail.ts | 3 + .../src/definition/livechat/IVisitorPhone.ts | 3 + .../src/definition/livechat/index.ts | 38 + .../src/definition/messages/IMessage.ts | 34 + .../src/definition/messages/IMessageAction.ts | 17 + .../definition/messages/IMessageAttachment.ts | 43 + .../messages/IMessageAttachmentAuthor.ts | 11 + .../messages/IMessageAttachmentField.ts | 11 + .../messages/IMessageAttachmentTitle.ts | 8 + .../messages/IMessageDeleteContext.ts | 17 + .../src/definition/messages/IMessageFile.ts | 5 + .../messages/IMessageFollowContext.ts | 21 + .../definition/messages/IMessagePinContext.ts | 21 + .../src/definition/messages/IMessageRaw.ts | 40 + .../definition/messages/IMessageReaction.ts | 13 + .../messages/IMessageReactionContext.ts | 25 + .../messages/IMessageReportContext.ts | 21 + .../messages/IMessageStarContext.ts | 21 + .../messages/IPostMessageDeleted.ts | 37 + .../messages/IPostMessageFollowed.ts | 18 + .../definition/messages/IPostMessagePinned.ts | 18 + .../messages/IPostMessageReacted.ts | 19 + .../messages/IPostMessageReported.ts | 18 + .../definition/messages/IPostMessageSent.ts | 27 + .../messages/IPostMessageSentToBot.ts | 15 + .../messages/IPostMessageStarred.ts | 18 + .../messages/IPostMessageUpdated.ts | 27 + .../messages/IPreMessageDeletePrevent.ts | 28 + .../messages/IPreMessageSentExtend.ts | 29 + .../messages/IPreMessageSentModify.ts | 29 + .../messages/IPreMessageSentPrevent.ts | 28 + .../messages/IPreMessageUpdatedExtend.ts | 29 + .../messages/IPreMessageUpdatedModify.ts | 29 + .../messages/IPreMessageUpdatedPrevent.ts | 28 + .../messages/MessageActionButtonsAlignment.ts | 4 + .../definition/messages/MessageActionType.ts | 3 + .../messages/MessageProcessingType.ts | 4 + .../src/definition/messages/index.ts | 71 + .../src/definition/metadata/AppInterface.ts | 60 + .../src/definition/metadata/AppMethod.ts | 102 ++ .../src/definition/metadata/IAppAuthorInfo.ts | 5 + .../src/definition/metadata/IAppInfo.ts | 20 + .../metadata/RocketChatAssociations.ts | 22 + .../src/definition/metadata/index.ts | 8 + .../src/definition/oauth2/IOAuth2.ts | 136 ++ .../src/definition/oauth2/OAuth2.ts | 15 + .../src/definition/permissions/IPermission.ts | 12 + .../persistence/IPersistenceItem.ts | 7 + .../src/definition/persistence/index.ts | 1 + .../apps-engine/src/definition/roles/IRole.ts | 8 + .../apps-engine/src/definition/roles/index.ts | 5 + .../src/definition/rooms/IPostRoomCreate.ts | 27 + .../src/definition/rooms/IPostRoomDeleted.ts | 27 + .../definition/rooms/IPostRoomUserJoined.ts | 19 + .../definition/rooms/IPostRoomUserLeave.ts | 19 + .../definition/rooms/IPreRoomCreateExtend.ts | 28 + .../definition/rooms/IPreRoomCreateModify.ts | 28 + .../definition/rooms/IPreRoomCreatePrevent.ts | 26 + .../definition/rooms/IPreRoomDeletePrevent.ts | 26 + .../definition/rooms/IPreRoomUserJoined.ts | 18 + .../src/definition/rooms/IPreRoomUserLeave.ts | 18 + .../apps-engine/src/definition/rooms/IRoom.ts | 26 + .../rooms/IRoomUserJoinedContext.ts | 23 + .../definition/rooms/IRoomUserLeaveContext.ts | 23 + .../src/definition/rooms/RoomType.ts | 6 + .../apps-engine/src/definition/rooms/index.ts | 17 + .../definition/scheduler/IOnetimeSchedule.ts | 13 + .../src/definition/scheduler/IProcessor.ts | 45 + .../scheduler/IRecurringSchedule.ts | 17 + .../src/definition/scheduler/index.ts | 3 + .../src/definition/settings/ISetting.ts | 47 + .../settings/ISettingUpdateContext.ts | 6 + .../src/definition/settings/SettingType.ts | 14 + .../src/definition/settings/index.ts | 4 + .../definition/slashcommands/ISlashCommand.ts | 41 + .../slashcommands/ISlashCommandPreview.ts | 28 + .../slashcommands/SlashCommandContext.ts | 33 + .../src/definition/slashcommands/index.ts | 5 + .../ui/IUIActionButtonDescriptor.ts | 41 + .../definition/ui/UIActionButtonContext.ts | 7 + .../apps-engine/src/definition/ui/index.ts | 2 + .../definition/uikit/IUIKitActionHandler.ts | 77 + .../uikit/IUIKitIncomingInteraction.ts | 28 + .../IUIKitIncomingInteractionActionButton.ts | 78 + .../definition/uikit/IUIKitInteractionType.ts | 41 + .../src/definition/uikit/IUIKitSurface.ts | 22 + .../src/definition/uikit/IUIKitView.ts | 7 + .../UIKitIncomingInteractionContainer.ts | 18 + .../uikit/UIKitIncomingInteractionTypes.ts | 60 + .../uikit/UIKitInteractionContext.ts | 69 + .../uikit/UIKitInteractionPayloadFormatter.ts | 68 + .../uikit/UIKitInteractionResponder.ts | 75 + .../definition/uikit/blocks/BlockBuilder.ts | 196 +++ .../src/definition/uikit/blocks/Blocks.ts | 108 ++ .../src/definition/uikit/blocks/Elements.ts | 115 ++ .../src/definition/uikit/blocks/Objects.ts | 33 + .../src/definition/uikit/blocks/index.ts | 4 + .../apps-engine/src/definition/uikit/index.ts | 7 + .../livechat/IUIKitLivechatActionHandler.ts | 23 + .../IUIKitLivechatIncomingInteraction.ts | 17 + .../UIKitLivechatIncomingInteractionType.ts | 22 + .../UIKitLivechatInteractionContext.ts | 33 + .../src/definition/uikit/livechat/index.ts | 4 + .../definition/uploads/IFileUploadContext.ts | 6 + .../src/definition/uploads/IPreFileUpload.ts | 20 + .../src/definition/uploads/IUpload.ts | 25 + .../definition/uploads/IUploadDescriptor.ts | 24 + .../src/definition/uploads/IUploadDetails.ts | 26 + .../src/definition/uploads/StoreType.ts | 7 + .../src/definition/uploads/index.ts | 6 + .../src/definition/users/IBotUser.ts | 6 + .../src/definition/users/IPostUserCreated.ts | 15 + .../src/definition/users/IPostUserDeleted.ts | 15 + .../src/definition/users/IPostUserLoggedIn.ts | 15 + .../definition/users/IPostUserLoggedOut.ts | 15 + .../users/IPostUserStatusChanged.ts | 17 + .../src/definition/users/IPostUserUpdated.ts | 15 + .../apps-engine/src/definition/users/IUser.ts | 25 + .../src/definition/users/IUserContext.ts | 19 + .../definition/users/IUserCreationOptions.ts | 7 + .../src/definition/users/IUserEmail.ts | 4 + .../src/definition/users/IUserLookup.ts | 5 + .../src/definition/users/IUserSettings.ts | 5 + .../definition/users/IUserStatusContext.ts | 23 + .../src/definition/users/IUserUpdateContex.ts | 24 + .../definition/users/IUserUpdateContext.ts | 24 + .../definition/users/UserStatusConnection.ts | 9 + .../src/definition/users/UserType.ts | 10 + .../apps-engine/src/definition/users/index.ts | 33 + .../videoConfProviders/IVideoConfProvider.ts | 63 + .../IVideoConferenceOptions.ts | 4 + .../videoConfProviders/VideoConfData.ts | 7 + .../definition/videoConfProviders/index.ts | 5 + .../videoConferences/AppVideoConference.ts | 6 + .../videoConferences/IVideoConference.ts | 56 + .../videoConferences/IVideoConferenceUser.ts | 5 + .../src/definition/videoConferences/index.ts | 5 + packages/apps-engine/src/lib/utils.ts | 1 + packages/apps-engine/src/server/AppManager.ts | 1150 +++++++++++++ .../apps-engine/src/server/IGetAppsFilter.ts | 6 + packages/apps-engine/src/server/ProxiedApp.ts | 154 ++ .../src/server/accessors/ApiExtend.ts | 11 + .../src/server/accessors/AppAccessors.ts | 36 + .../server/accessors/CloudWorkspaceRead.ts | 11 + .../server/accessors/ConfigurationExtend.ts | 24 + .../server/accessors/ConfigurationModify.ts | 9 + .../src/server/accessors/DiscussionBuilder.ts | 47 + .../src/server/accessors/EmailCreator.ts | 11 + .../src/server/accessors/EnvironmentRead.ts | 21 + .../src/server/accessors/EnvironmentWrite.ts | 13 + .../accessors/EnvironmentalVariableRead.ts | 18 + .../accessors/ExternalComponentsExtend.ts | 11 + .../apps-engine/src/server/accessors/Http.ts | 77 + .../src/server/accessors/HttpExtend.ts | 58 + .../src/server/accessors/LivechatCreator.ts | 29 + .../accessors/LivechatMessageBuilder.ts | 191 +++ .../src/server/accessors/LivechatRead.ts | 73 + .../src/server/accessors/LivechatUpdater.ts | 23 + .../src/server/accessors/MessageBuilder.ts | 224 +++ .../src/server/accessors/MessageExtender.ts | 50 + .../src/server/accessors/MessageRead.ts | 33 + .../src/server/accessors/ModerationModify.ts | 20 + .../src/server/accessors/Modify.ts | 89 + .../src/server/accessors/ModifyCreator.ts | 260 +++ .../src/server/accessors/ModifyDeleter.ts | 35 + .../src/server/accessors/ModifyExtender.ts | 46 + .../src/server/accessors/ModifyUpdater.ts | 106 ++ .../src/server/accessors/Notifier.ts | 49 + .../src/server/accessors/OAuthAppsModify.ts | 19 + .../src/server/accessors/OAuthAppsReader.ts | 15 + .../src/server/accessors/Persistence.ts | 43 + .../src/server/accessors/PersistenceRead.ts | 19 + .../src/server/accessors/Reader.ts | 87 + .../src/server/accessors/RoleRead.ts | 15 + .../src/server/accessors/RoomBuilder.ts | 155 ++ .../src/server/accessors/RoomExtender.ts | 56 + .../src/server/accessors/RoomRead.ts | 73 + .../src/server/accessors/SchedulerExtend.ts | 11 + .../src/server/accessors/SchedulerModify.ts | 27 + .../src/server/accessors/ServerSettingRead.ts | 34 + .../server/accessors/ServerSettingUpdater.ts | 15 + .../server/accessors/ServerSettingsModify.ts | 23 + .../src/server/accessors/SettingRead.ts | 25 + .../src/server/accessors/SettingUpdater.ts | 22 + .../src/server/accessors/SettingsExtend.ts | 26 + .../server/accessors/SlashCommandsExtend.ts | 11 + .../server/accessors/SlashCommandsModify.ts | 19 + .../src/server/accessors/ThreadRead.ts | 11 + .../src/server/accessors/UIController.ts | 106 ++ .../src/server/accessors/UIExtend.ts | 11 + .../src/server/accessors/UploadCreator.ts | 25 + .../src/server/accessors/UploadRead.ts | 21 + .../src/server/accessors/UserBuilder.ts | 74 + .../src/server/accessors/UserRead.ts | 23 + .../src/server/accessors/UserUpdater.ts | 28 + .../accessors/VideoConfProviderExtend.ts | 11 + .../accessors/VideoConferenceBuilder.ts | 83 + .../server/accessors/VideoConferenceExtend.ts | 64 + .../server/accessors/VideoConferenceRead.ts | 11 + .../apps-engine/src/server/accessors/index.ts | 95 ++ .../src/server/bridges/ApiBridge.ts | 49 + .../src/server/bridges/AppActivationBridge.ts | 35 + .../src/server/bridges/AppBridges.ts | 101 ++ .../server/bridges/AppDetailChangesBridge.ts | 16 + .../src/server/bridges/BaseBridge.ts | 6 + .../server/bridges/CloudWorkspaceBridge.ts | 30 + .../src/server/bridges/CommandBridge.ts | 117 ++ .../src/server/bridges/EmailBridge.ts | 30 + .../bridges/EnvironmentalVariableBridge.ts | 45 + .../src/server/bridges/HttpBridge.ts | 37 + .../src/server/bridges/IInternalBridge.ts | 7 + .../bridges/IInternalFederationBridge.ts | 15 + .../bridges/IInternalPersistenceBridge.ts | 9 + .../bridges/IInternalSchedulerBridge.ts | 8 + .../src/server/bridges/IInternalUserBridge.ts | 8 + .../src/server/bridges/IListenerBridge.ts | 10 + .../src/server/bridges/InternalBridge.ts | 22 + .../src/server/bridges/ListenerBridge.ts | 18 + .../src/server/bridges/LivechatBridge.ts | 259 +++ .../src/server/bridges/MessageBridge.ts | 116 ++ .../src/server/bridges/ModerationBridge.ts | 47 + .../src/server/bridges/OAuthAppsBridge.ts | 85 + .../src/server/bridges/PersistenceBridge.ts | 161 ++ .../src/server/bridges/RoleBridge.ts | 38 + .../src/server/bridges/RoomBridge.ts | 179 ++ .../src/server/bridges/SchedulerBridge.ts | 62 + .../src/server/bridges/ServerSettingBridge.ts | 93 ++ .../src/server/bridges/ThreadBridge.ts | 35 + .../src/server/bridges/UiInteractionBridge.ts | 31 + .../src/server/bridges/UploadBridge.ts | 62 + .../src/server/bridges/UserBridge.ts | 149 ++ .../server/bridges/VideoConferenceBridge.ts | 94 ++ .../apps-engine/src/server/bridges/index.ts | 52 + .../src/server/compiler/AppCompiler.ts | 30 + .../compiler/AppFabricationFulfillment.ts | 74 + .../src/server/compiler/AppImplements.ts | 27 + .../src/server/compiler/AppPackageParser.ts | 161 ++ .../server/compiler/IParseAppPackageResult.ts | 9 + .../apps-engine/src/server/compiler/index.ts | 7 + .../src/server/compiler/modules/index.ts | 55 + .../src/server/compiler/modules/networking.ts | 36 + .../errors/CommandAlreadyExistsError.ts | 9 + .../CommandHasAlreadyBeenTouchedError.ts | 9 + .../src/server/errors/CompilerError.ts | 9 + .../server/errors/InvalidInstallationError.ts | 5 + .../src/server/errors/InvalidLicenseError.ts | 7 + .../server/errors/MustContainFunctionError.ts | 9 + .../src/server/errors/MustExtendAppError.ts | 5 + .../errors/NotEnoughMethodArgumentsError.ts | 9 + .../server/errors/PathAlreadyExistsError.ts | 9 + .../server/errors/PermissionDeniedError.ts | 25 + .../server/errors/RequiredApiVersionError.ts | 21 + .../VideoConfProviderAlreadyExistsError.ts | 9 + .../VideoConfProviderNotRegisteredError.ts | 9 + .../apps-engine/src/server/errors/index.ts | 25 + .../src/server/logging/AppConsole.ts | 121 ++ .../src/server/logging/ILoggerStorageEntry.ts | 14 + .../apps-engine/src/server/logging/index.ts | 4 + .../src/server/managers/AppAccessorManager.ts | 228 +++ .../apps-engine/src/server/managers/AppApi.ts | 95 ++ .../src/server/managers/AppApiManager.ts | 165 ++ .../managers/AppExternalComponentManager.ts | 142 ++ .../src/server/managers/AppLicenseManager.ts | 91 + .../src/server/managers/AppListenerManager.ts | 1220 ++++++++++++++ .../server/managers/AppPermissionManager.ts | 40 + .../src/server/managers/AppRuntimeManager.ts | 57 + .../server/managers/AppSchedulerManager.ts | 98 ++ .../src/server/managers/AppSettingsManager.ts | 55 + .../server/managers/AppSignatureManager.ts | 85 + .../src/server/managers/AppSlashCommand.ts | 77 + .../server/managers/AppSlashCommandManager.ts | 470 ++++++ .../server/managers/AppVideoConfProvider.ts | 105 ++ .../managers/AppVideoConfProviderManager.ts | 207 +++ .../server/managers/UIActionButtonManager.ts | 72 + .../apps-engine/src/server/managers/index.ts | 21 + .../server/marketplace/IAppLicenseMetadata.ts | 5 + .../server/marketplace/IMarketplaceInfo.ts | 24 + .../marketplace/IMarketplacePricingPlan.ts | 11 + .../marketplace/IMarketplacePricingTier.ts | 6 + .../IMarketplaceSimpleBundleInfo.ts | 4 + .../IMarketplaceSubscriptionInfo.ts | 15 + .../marketplace/MarketplacePricingStrategy.ts | 5 + .../marketplace/MarketplacePurchaseType.ts | 4 + .../MarketplaceSubscriptionStatus.ts | 10 + .../MarketplaceSubscriptionType.ts | 4 + .../src/server/marketplace/index.ts | 8 + .../license/AppLicenseValidationResult.ts | 56 + .../src/server/marketplace/license/Crypto.ts | 26 + .../src/server/marketplace/license/index.ts | 4 + .../src/server/messages/Message.ts | 106 ++ .../apps-engine/src/server/misc/UIHelper.ts | 32 + .../apps-engine/src/server/misc/Utilities.ts | 36 + .../src/server/oauth2/OAuth2Client.ts | 322 ++++ .../src/server/permissions/AppPermissions.ts | 152 ++ packages/apps-engine/src/server/rooms/Room.ts | 104 ++ .../server/runtime/AppsEngineEmptyRuntime.ts | 21 + .../server/runtime/AppsEngineNodeRuntime.ts | 71 + .../src/server/runtime/AppsEngineRuntime.ts | 29 + .../runtime/deno/AppsEngineDenoRuntime.ts | 568 +++++++ .../server/runtime/deno/LivenessManager.ts | 184 +++ .../server/runtime/deno/ProcessMessenger.ts | 48 + .../src/server/runtime/deno/bundler.ts | 90 + .../src/server/runtime/deno/codec.ts | 29 + .../src/server/storage/AppLogStorage.ts | 24 + .../src/server/storage/AppMetadataStorage.ts | 19 + .../src/server/storage/AppSourceStorage.ts | 40 + .../src/server/storage/IAppStorageItem.ts | 31 + .../apps-engine/src/server/storage/index.ts | 4 + packages/apps-engine/tests/runner.ts | 20 + .../tests/server/AppManager.spec.ts | 122 ++ .../server/accessors/AppAccessors.spec.ts | 134 ++ .../accessors/ConfigurationExtend.spec.ts | 76 + .../accessors/ConfigurationModify.spec.ts | 28 + .../server/accessors/EnvironmentRead.spec.ts | 29 + .../server/accessors/EnvironmentWrite.spec.ts | 25 + .../EnvironmentalVariableRead.spec.ts | 33 + .../tests/server/accessors/Http.spec.ts | 128 ++ .../tests/server/accessors/HttpExtend.spec.ts | 83 + .../server/accessors/MessageBuilder.spec.ts | 125 ++ .../server/accessors/MessageExtender.spec.ts | 35 + .../server/accessors/MessageRead.spec.ts | 58 + .../tests/server/accessors/Modify.spec.ts | 44 + .../server/accessors/ModifyCreator.spec.ts | 129 ++ .../server/accessors/ModifyExtender.spec.ts | 74 + .../server/accessors/ModifyUpdater.spec.ts | 139 ++ .../tests/server/accessors/Notifier.spec.ts | 37 + .../server/accessors/Persistence.spec.ts | 82 + .../server/accessors/PersistenceRead.spec.ts | 31 + .../tests/server/accessors/Reader.spec.ts | 112 ++ .../server/accessors/RoomBuilder.spec.ts | 90 + .../server/accessors/RoomExtender.spec.ts | 38 + .../tests/server/accessors/RoomRead.spec.ts | 83 + .../accessors/ServerSettingRead.spec.ts | 43 + .../accessors/ServerSettingsModify.spec.ts | 60 + .../server/accessors/SettingRead.spec.ts | 40 + .../server/accessors/SettingsExtend.spec.ts | 54 + .../accessors/SlashCommandsExtend.spec.ts | 47 + .../accessors/SlashCommandsModify.spec.ts | 47 + .../server/accessors/UserBuilder.spec.ts | 58 + .../tests/server/accessors/UserRead.spec.ts | 49 + .../accessors/VideoConfProviderExtend.spec.ts | 38 + .../accessors/VideoConferenceBuilder.spec.ts | 108 ++ .../accessors/VideoConferenceExtend.spec.ts | 81 + .../accessors/VideoConferenceRead.spec.ts | 34 + .../tests/server/compiler/AppCompiler.spec.ts | 24 + .../AppFabricationFulfillment.spec.ts | 48 + .../server/compiler/AppImplements.spec.ts | 19 + .../errors/CommandAlreadyExistsError.spec.ts | 13 + .../CommandHasAlreadyBeenTouchedError.spec.ts | 13 + .../tests/server/errors/CompilerError.spec.ts | 13 + .../errors/MustContainFunctionError.spec.ts | 13 + .../server/errors/MustExtendAppError.spec.ts | 13 + .../NotEnoughMethodArgumentsError.spec.ts | 13 + .../errors/RequiredApiVersionError.spec.ts | 26 + .../tests/server/logging/AppConsole.spec.ts | 79 + .../managers/AppAccessorManager.spec.ts | 166 ++ .../tests/server/managers/AppApi.spec.ts | 28 + .../server/managers/AppApiManager.spec.ts | 233 +++ .../AppExternalComponentManager.spec.ts | 154 ++ .../managers/AppListenerManager.spec.ts | 44 + .../managers/AppSettingsManager.spec.ts | 148 ++ .../server/managers/AppSlashCommand.spec.ts | 29 + .../managers/AppSlashCommandManager.spec.ts | 469 ++++++ .../managers/AppVideoConfProvider.spec.ts | 25 + .../AppVideoConfProviderManager.spec.ts | 435 +++++ .../tests/server/misc/Utilities.spec.ts | 69 + .../DenoRuntimeSubprocessController.spec.ts | 222 +++ .../apps-engine/tests/test-data/README.md | 2 + .../test-data/apps/hello-world-test_0.0.1.zip | Bin 0 -> 10309 bytes .../test-data/apps/testing-app_0.0.8.zip | Bin 0 -> 37318 bytes .../test-data/bridges/OAuthAppsBridge.ts | 28 + .../test-data/bridges/activationBridge.ts | 25 + .../tests/test-data/bridges/apiBridge.ts | 37 + .../tests/test-data/bridges/appBridges.ts | 228 +++ .../test-data/bridges/appDetailChanges.ts | 6 + .../tests/test-data/bridges/cloudBridge.ts | 15 + .../tests/test-data/bridges/commandBridge.ts | 38 + .../tests/test-data/bridges/emailBridge.ts | 8 + .../bridges/environmentalVariableBridge.ts | 15 + .../tests/test-data/bridges/httpBridge.ts | 15 + .../tests/test-data/bridges/internalBridge.ts | 16 + .../bridges/internalFederationBridge.ts | 11 + .../tests/test-data/bridges/livechatBridge.ts | 96 ++ .../tests/test-data/bridges/messageBridge.ts | 43 + .../test-data/bridges/moderationBridge.ts | 17 + .../tests/test-data/bridges/persisBridge.ts | 40 + .../tests/test-data/bridges/roleBridge.ts | 12 + .../tests/test-data/bridges/roomBridge.ts | 67 + .../test-data/bridges/schedulerBridge.ts | 24 + .../test-data/bridges/serverSettingBridge.ts | 41 + .../tests/test-data/bridges/threadBridge.ts | 8 + .../test-data/bridges/uiIntegrationBridge.ts | 9 + .../tests/test-data/bridges/uploadBridge.ts | 17 + .../tests/test-data/bridges/userBridge.ts | 44 + .../bridges/videoConferenceBridge.ts | 25 + .../test-data/misc/fake-library-file.d.ts | 5 + .../test-data/storage/TestSourceStorage.ts | 20 + .../tests/test-data/storage/logStorage.ts | 25 + .../tests/test-data/storage/storage.ts | 93 ++ .../apps-engine/tests/test-data/utilities.ts | 492 ++++++ packages/apps-engine/tsconfig-lint.json | 7 + packages/apps-engine/tsconfig.json | 21 + packages/apps-engine/turbo.json | 9 + packages/apps-engine/typedoc.json | 9 + packages/apps/package.json | 2 +- packages/core-services/package.json | 2 +- packages/core-typings/package.json | 2 +- packages/fuselage-ui-kit/package.json | 2 +- packages/jwt/package.json | 3 + packages/rest-typings/package.json | 2 +- yarn.lock | 1461 +++++++++++++++-- 643 files changed, 35661 insertions(+), 132 deletions(-) create mode 100644 .tool-versions create mode 100644 packages/apps-engine/.eslintignore create mode 100644 packages/apps-engine/.eslintrc.json create mode 100644 packages/apps-engine/.gitignore create mode 100644 packages/apps-engine/.prettierrc create mode 100644 packages/apps-engine/README.md create mode 100644 packages/apps-engine/deno-runtime/.gitignore create mode 100644 packages/apps-engine/deno-runtime/AppObjectRegistry.ts create mode 100644 packages/apps-engine/deno-runtime/acorn-walk.d.ts create mode 100644 packages/apps-engine/deno-runtime/acorn.d.ts create mode 100644 packages/apps-engine/deno-runtime/deno.jsonc create mode 100644 packages/apps-engine/deno-runtime/deno.lock create mode 100644 packages/apps-engine/deno-runtime/handlers/api-handler.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/construct.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleGetStatus.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleInitialize.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnDisable.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnEnable.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnInstall.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnSettingUpdated.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnUninstall.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnUpdate.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleSetStatus.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handler.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/listener/handler.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/scheduler-handler.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/slashcommand-handler.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/tests/api-handler.test.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/tests/listener-handler.test.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/tests/scheduler-handler.test.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/tests/slashcommand-handler.test.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/tests/uikit-handler.test.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/tests/videoconference-handler.test.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/uikit/handler.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/videoconference-handler.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/BlockBuilder.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/RoomBuilder.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/UserBuilder.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/extenders/HttpExtender.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/extenders/MessageExtender.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/extenders/RoomExtender.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/http.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/mod.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyExtender.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyUpdater.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/notifier.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/tests/AppAccessors.test.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts create mode 100644 packages/apps-engine/deno-runtime/lib/ast/mod.ts create mode 100644 packages/apps-engine/deno-runtime/lib/ast/operations.ts create mode 100644 packages/apps-engine/deno-runtime/lib/ast/tests/data/ast_blocks.ts create mode 100644 packages/apps-engine/deno-runtime/lib/ast/tests/operations.test.ts create mode 100644 packages/apps-engine/deno-runtime/lib/codec.ts create mode 100644 packages/apps-engine/deno-runtime/lib/logger.ts create mode 100644 packages/apps-engine/deno-runtime/lib/messenger.ts create mode 100644 packages/apps-engine/deno-runtime/lib/require.ts create mode 100644 packages/apps-engine/deno-runtime/lib/room.ts create mode 100644 packages/apps-engine/deno-runtime/lib/roomFactory.ts create mode 100644 packages/apps-engine/deno-runtime/lib/sanitizeDeprecatedUsage.ts create mode 100644 packages/apps-engine/deno-runtime/lib/tests/logger.test.ts create mode 100644 packages/apps-engine/deno-runtime/lib/tests/messenger.test.ts create mode 100644 packages/apps-engine/deno-runtime/main.ts create mode 100644 packages/apps-engine/package.json create mode 100644 packages/apps-engine/scripts/bundle.js create mode 100644 packages/apps-engine/scripts/deno-cache.js create mode 100644 packages/apps-engine/src/client/AppClientManager.ts create mode 100644 packages/apps-engine/src/client/AppServerCommunicator.ts create mode 100644 packages/apps-engine/src/client/AppsEngineUIClient.ts create mode 100644 packages/apps-engine/src/client/AppsEngineUIHost.ts create mode 100644 packages/apps-engine/src/client/constants/index.ts create mode 100644 packages/apps-engine/src/client/definition/AppsEngineUIMethods.ts create mode 100644 packages/apps-engine/src/client/definition/IAppsEngineUIResponse.ts create mode 100644 packages/apps-engine/src/client/definition/IExternalComponentRoomInfo.ts create mode 100644 packages/apps-engine/src/client/definition/IExternalComponentUserInfo.ts create mode 100644 packages/apps-engine/src/client/definition/index.ts create mode 100644 packages/apps-engine/src/client/index.ts create mode 100644 packages/apps-engine/src/client/utils/index.ts create mode 100644 packages/apps-engine/src/definition/App.ts create mode 100644 packages/apps-engine/src/definition/AppStatus.ts create mode 100644 packages/apps-engine/src/definition/IApp.ts create mode 100644 packages/apps-engine/src/definition/LICENSE create mode 100644 packages/apps-engine/src/definition/accessors/IApiExtend.ts create mode 100644 packages/apps-engine/src/definition/accessors/IAppAccessors.ts create mode 100644 packages/apps-engine/src/definition/accessors/IAppInstallationContext.ts create mode 100644 packages/apps-engine/src/definition/accessors/IAppUninstallationContext.ts create mode 100644 packages/apps-engine/src/definition/accessors/IAppUpdateContext.ts create mode 100644 packages/apps-engine/src/definition/accessors/ICloudWorkspaceRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IConfigurationExtend.ts create mode 100644 packages/apps-engine/src/definition/accessors/IConfigurationModify.ts create mode 100644 packages/apps-engine/src/definition/accessors/IDiscussionBuilder.ts create mode 100644 packages/apps-engine/src/definition/accessors/IEmailCreator.ts create mode 100644 packages/apps-engine/src/definition/accessors/IEnvironmentRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IEnvironmentWrite.ts create mode 100644 packages/apps-engine/src/definition/accessors/IEnvironmentalVariableRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IExternalComponentsExtend.ts create mode 100644 packages/apps-engine/src/definition/accessors/IHttp.ts create mode 100644 packages/apps-engine/src/definition/accessors/ILivechatCreator.ts create mode 100644 packages/apps-engine/src/definition/accessors/ILivechatMessageBuilder.ts create mode 100644 packages/apps-engine/src/definition/accessors/ILivechatRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/ILivechatUpdater.ts create mode 100644 packages/apps-engine/src/definition/accessors/ILogEntry.ts create mode 100644 packages/apps-engine/src/definition/accessors/ILogger.ts create mode 100644 packages/apps-engine/src/definition/accessors/IMessageBuilder.ts create mode 100644 packages/apps-engine/src/definition/accessors/IMessageExtender.ts create mode 100644 packages/apps-engine/src/definition/accessors/IMessageRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IMessageUpdater.ts create mode 100644 packages/apps-engine/src/definition/accessors/IModerationModify.ts create mode 100644 packages/apps-engine/src/definition/accessors/IModify.ts create mode 100644 packages/apps-engine/src/definition/accessors/IModifyCreator.ts create mode 100644 packages/apps-engine/src/definition/accessors/IModifyDeleter.ts create mode 100644 packages/apps-engine/src/definition/accessors/IModifyExtender.ts create mode 100644 packages/apps-engine/src/definition/accessors/IModifyUpdater.ts create mode 100644 packages/apps-engine/src/definition/accessors/INotifier.ts create mode 100644 packages/apps-engine/src/definition/accessors/IOAuthApp.ts create mode 100644 packages/apps-engine/src/definition/accessors/IOAuthAppsModify.ts create mode 100644 packages/apps-engine/src/definition/accessors/IOAuthAppsReader.ts create mode 100644 packages/apps-engine/src/definition/accessors/IPersistence.ts create mode 100644 packages/apps-engine/src/definition/accessors/IPersistenceRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IRoleRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IRoomBuilder.ts create mode 100644 packages/apps-engine/src/definition/accessors/IRoomExtender.ts create mode 100644 packages/apps-engine/src/definition/accessors/IRoomRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/ISchedulerExtend.ts create mode 100644 packages/apps-engine/src/definition/accessors/ISchedulerModify.ts create mode 100644 packages/apps-engine/src/definition/accessors/IServerSettingRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IServerSettingUpdater.ts create mode 100644 packages/apps-engine/src/definition/accessors/IServerSettingsModify.ts create mode 100644 packages/apps-engine/src/definition/accessors/ISettingRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/ISettingUpdater.ts create mode 100644 packages/apps-engine/src/definition/accessors/ISettingsExtend.ts create mode 100644 packages/apps-engine/src/definition/accessors/ISlashCommandsExtend.ts create mode 100644 packages/apps-engine/src/definition/accessors/ISlashCommandsModify.ts create mode 100644 packages/apps-engine/src/definition/accessors/IThreadRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IUIController.ts create mode 100644 packages/apps-engine/src/definition/accessors/IUIExtend.ts create mode 100644 packages/apps-engine/src/definition/accessors/IUploadCreator.ts create mode 100644 packages/apps-engine/src/definition/accessors/IUploadRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IUserBuilder.ts create mode 100644 packages/apps-engine/src/definition/accessors/IUserRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IUserUpdater.ts create mode 100644 packages/apps-engine/src/definition/accessors/IVideoConfProvidersExtend.ts create mode 100644 packages/apps-engine/src/definition/accessors/IVideoConferenceBuilder.ts create mode 100644 packages/apps-engine/src/definition/accessors/IVideoConferenceExtend.ts create mode 100644 packages/apps-engine/src/definition/accessors/IVideoConferenceRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/index.ts create mode 100644 packages/apps-engine/src/definition/api/ApiEndpoint.ts create mode 100644 packages/apps-engine/src/definition/api/IApi.ts create mode 100644 packages/apps-engine/src/definition/api/IApiEndpoint.ts create mode 100644 packages/apps-engine/src/definition/api/IApiEndpointInfo.ts create mode 100644 packages/apps-engine/src/definition/api/IApiEndpointMetadata.ts create mode 100644 packages/apps-engine/src/definition/api/IApiExample.ts create mode 100644 packages/apps-engine/src/definition/api/IRequest.ts create mode 100644 packages/apps-engine/src/definition/api/IResponse.ts create mode 100644 packages/apps-engine/src/definition/api/index.ts create mode 100644 packages/apps-engine/src/definition/app-schema.json create mode 100644 packages/apps-engine/src/definition/assets/IAsset.ts create mode 100644 packages/apps-engine/src/definition/assets/IAssetProvider.ts create mode 100644 packages/apps-engine/src/definition/assets/index.ts create mode 100644 packages/apps-engine/src/definition/cloud/IWorkspaceToken.ts create mode 100644 packages/apps-engine/src/definition/email/IEmail.ts create mode 100644 packages/apps-engine/src/definition/email/IEmailDescriptor.ts create mode 100644 packages/apps-engine/src/definition/email/IPreEmailSent.ts create mode 100644 packages/apps-engine/src/definition/email/IPreEmailSentContext.ts create mode 100644 packages/apps-engine/src/definition/email/index.ts create mode 100644 packages/apps-engine/src/definition/example-app.json create mode 100644 packages/apps-engine/src/definition/exceptions/AppsEngineException.ts create mode 100644 packages/apps-engine/src/definition/exceptions/EssentialAppDisabledException.ts create mode 100644 packages/apps-engine/src/definition/exceptions/FileUploadNotAllowedException.ts create mode 100644 packages/apps-engine/src/definition/exceptions/InvalidSettingValueException.ts create mode 100644 packages/apps-engine/src/definition/exceptions/UserNotAllowedException.ts create mode 100644 packages/apps-engine/src/definition/exceptions/index.ts create mode 100644 packages/apps-engine/src/definition/externalComponent/IExternalComponent.ts create mode 100644 packages/apps-engine/src/definition/externalComponent/IExternalComponentOptions.ts create mode 100644 packages/apps-engine/src/definition/externalComponent/IExternalComponentState.ts create mode 100644 packages/apps-engine/src/definition/externalComponent/IPostExternalComponentClosed.ts create mode 100644 packages/apps-engine/src/definition/externalComponent/IPostExternalComponentOpened.ts create mode 100644 packages/apps-engine/src/definition/externalComponent/index.ts create mode 100644 packages/apps-engine/src/definition/livechat/IDepartment.ts create mode 100644 packages/apps-engine/src/definition/livechat/ILivechatEventContext.ts create mode 100644 packages/apps-engine/src/definition/livechat/ILivechatMessage.ts create mode 100644 packages/apps-engine/src/definition/livechat/ILivechatRoom.ts create mode 100644 packages/apps-engine/src/definition/livechat/ILivechatRoomClosedHandler.ts create mode 100644 packages/apps-engine/src/definition/livechat/ILivechatTransferData.ts create mode 100644 packages/apps-engine/src/definition/livechat/ILivechatTransferEventContext.ts create mode 100644 packages/apps-engine/src/definition/livechat/IPostLivechatAgentAssigned.ts create mode 100644 packages/apps-engine/src/definition/livechat/IPostLivechatAgentUnassigned.ts create mode 100644 packages/apps-engine/src/definition/livechat/IPostLivechatGuestSaved.ts create mode 100644 packages/apps-engine/src/definition/livechat/IPostLivechatRoomClosed.ts create mode 100644 packages/apps-engine/src/definition/livechat/IPostLivechatRoomSaved.ts create mode 100644 packages/apps-engine/src/definition/livechat/IPostLivechatRoomStarted.ts create mode 100644 packages/apps-engine/src/definition/livechat/IPostLivechatRoomTransferred.ts create mode 100644 packages/apps-engine/src/definition/livechat/IVisitor.ts create mode 100644 packages/apps-engine/src/definition/livechat/IVisitorEmail.ts create mode 100644 packages/apps-engine/src/definition/livechat/IVisitorPhone.ts create mode 100644 packages/apps-engine/src/definition/livechat/index.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessage.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageAction.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageAttachment.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageAttachmentAuthor.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageAttachmentField.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageAttachmentTitle.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageDeleteContext.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageFile.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageFollowContext.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessagePinContext.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageRaw.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageReaction.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageReactionContext.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageReportContext.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageStarContext.ts create mode 100644 packages/apps-engine/src/definition/messages/IPostMessageDeleted.ts create mode 100644 packages/apps-engine/src/definition/messages/IPostMessageFollowed.ts create mode 100644 packages/apps-engine/src/definition/messages/IPostMessagePinned.ts create mode 100644 packages/apps-engine/src/definition/messages/IPostMessageReacted.ts create mode 100644 packages/apps-engine/src/definition/messages/IPostMessageReported.ts create mode 100644 packages/apps-engine/src/definition/messages/IPostMessageSent.ts create mode 100644 packages/apps-engine/src/definition/messages/IPostMessageSentToBot.ts create mode 100644 packages/apps-engine/src/definition/messages/IPostMessageStarred.ts create mode 100644 packages/apps-engine/src/definition/messages/IPostMessageUpdated.ts create mode 100644 packages/apps-engine/src/definition/messages/IPreMessageDeletePrevent.ts create mode 100644 packages/apps-engine/src/definition/messages/IPreMessageSentExtend.ts create mode 100644 packages/apps-engine/src/definition/messages/IPreMessageSentModify.ts create mode 100644 packages/apps-engine/src/definition/messages/IPreMessageSentPrevent.ts create mode 100644 packages/apps-engine/src/definition/messages/IPreMessageUpdatedExtend.ts create mode 100644 packages/apps-engine/src/definition/messages/IPreMessageUpdatedModify.ts create mode 100644 packages/apps-engine/src/definition/messages/IPreMessageUpdatedPrevent.ts create mode 100644 packages/apps-engine/src/definition/messages/MessageActionButtonsAlignment.ts create mode 100644 packages/apps-engine/src/definition/messages/MessageActionType.ts create mode 100644 packages/apps-engine/src/definition/messages/MessageProcessingType.ts create mode 100644 packages/apps-engine/src/definition/messages/index.ts create mode 100644 packages/apps-engine/src/definition/metadata/AppInterface.ts create mode 100644 packages/apps-engine/src/definition/metadata/AppMethod.ts create mode 100644 packages/apps-engine/src/definition/metadata/IAppAuthorInfo.ts create mode 100644 packages/apps-engine/src/definition/metadata/IAppInfo.ts create mode 100644 packages/apps-engine/src/definition/metadata/RocketChatAssociations.ts create mode 100644 packages/apps-engine/src/definition/metadata/index.ts create mode 100644 packages/apps-engine/src/definition/oauth2/IOAuth2.ts create mode 100644 packages/apps-engine/src/definition/oauth2/OAuth2.ts create mode 100644 packages/apps-engine/src/definition/permissions/IPermission.ts create mode 100644 packages/apps-engine/src/definition/persistence/IPersistenceItem.ts create mode 100644 packages/apps-engine/src/definition/persistence/index.ts create mode 100644 packages/apps-engine/src/definition/roles/IRole.ts create mode 100644 packages/apps-engine/src/definition/roles/index.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPostRoomCreate.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPostRoomDeleted.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPostRoomUserJoined.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPostRoomUserLeave.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPreRoomCreateExtend.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPreRoomCreateModify.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPreRoomCreatePrevent.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPreRoomDeletePrevent.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPreRoomUserJoined.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPreRoomUserLeave.ts create mode 100644 packages/apps-engine/src/definition/rooms/IRoom.ts create mode 100644 packages/apps-engine/src/definition/rooms/IRoomUserJoinedContext.ts create mode 100644 packages/apps-engine/src/definition/rooms/IRoomUserLeaveContext.ts create mode 100644 packages/apps-engine/src/definition/rooms/RoomType.ts create mode 100644 packages/apps-engine/src/definition/rooms/index.ts create mode 100644 packages/apps-engine/src/definition/scheduler/IOnetimeSchedule.ts create mode 100644 packages/apps-engine/src/definition/scheduler/IProcessor.ts create mode 100644 packages/apps-engine/src/definition/scheduler/IRecurringSchedule.ts create mode 100644 packages/apps-engine/src/definition/scheduler/index.ts create mode 100644 packages/apps-engine/src/definition/settings/ISetting.ts create mode 100644 packages/apps-engine/src/definition/settings/ISettingUpdateContext.ts create mode 100644 packages/apps-engine/src/definition/settings/SettingType.ts create mode 100644 packages/apps-engine/src/definition/settings/index.ts create mode 100644 packages/apps-engine/src/definition/slashcommands/ISlashCommand.ts create mode 100644 packages/apps-engine/src/definition/slashcommands/ISlashCommandPreview.ts create mode 100644 packages/apps-engine/src/definition/slashcommands/SlashCommandContext.ts create mode 100644 packages/apps-engine/src/definition/slashcommands/index.ts create mode 100644 packages/apps-engine/src/definition/ui/IUIActionButtonDescriptor.ts create mode 100644 packages/apps-engine/src/definition/ui/UIActionButtonContext.ts create mode 100644 packages/apps-engine/src/definition/ui/index.ts create mode 100644 packages/apps-engine/src/definition/uikit/IUIKitActionHandler.ts create mode 100644 packages/apps-engine/src/definition/uikit/IUIKitIncomingInteraction.ts create mode 100644 packages/apps-engine/src/definition/uikit/IUIKitIncomingInteractionActionButton.ts create mode 100644 packages/apps-engine/src/definition/uikit/IUIKitInteractionType.ts create mode 100644 packages/apps-engine/src/definition/uikit/IUIKitSurface.ts create mode 100644 packages/apps-engine/src/definition/uikit/IUIKitView.ts create mode 100644 packages/apps-engine/src/definition/uikit/UIKitIncomingInteractionContainer.ts create mode 100644 packages/apps-engine/src/definition/uikit/UIKitIncomingInteractionTypes.ts create mode 100644 packages/apps-engine/src/definition/uikit/UIKitInteractionContext.ts create mode 100644 packages/apps-engine/src/definition/uikit/UIKitInteractionPayloadFormatter.ts create mode 100644 packages/apps-engine/src/definition/uikit/UIKitInteractionResponder.ts create mode 100644 packages/apps-engine/src/definition/uikit/blocks/BlockBuilder.ts create mode 100644 packages/apps-engine/src/definition/uikit/blocks/Blocks.ts create mode 100644 packages/apps-engine/src/definition/uikit/blocks/Elements.ts create mode 100644 packages/apps-engine/src/definition/uikit/blocks/Objects.ts create mode 100644 packages/apps-engine/src/definition/uikit/blocks/index.ts create mode 100644 packages/apps-engine/src/definition/uikit/index.ts create mode 100644 packages/apps-engine/src/definition/uikit/livechat/IUIKitLivechatActionHandler.ts create mode 100644 packages/apps-engine/src/definition/uikit/livechat/IUIKitLivechatIncomingInteraction.ts create mode 100644 packages/apps-engine/src/definition/uikit/livechat/UIKitLivechatIncomingInteractionType.ts create mode 100644 packages/apps-engine/src/definition/uikit/livechat/UIKitLivechatInteractionContext.ts create mode 100644 packages/apps-engine/src/definition/uikit/livechat/index.ts create mode 100644 packages/apps-engine/src/definition/uploads/IFileUploadContext.ts create mode 100644 packages/apps-engine/src/definition/uploads/IPreFileUpload.ts create mode 100644 packages/apps-engine/src/definition/uploads/IUpload.ts create mode 100644 packages/apps-engine/src/definition/uploads/IUploadDescriptor.ts create mode 100644 packages/apps-engine/src/definition/uploads/IUploadDetails.ts create mode 100644 packages/apps-engine/src/definition/uploads/StoreType.ts create mode 100644 packages/apps-engine/src/definition/uploads/index.ts create mode 100644 packages/apps-engine/src/definition/users/IBotUser.ts create mode 100644 packages/apps-engine/src/definition/users/IPostUserCreated.ts create mode 100644 packages/apps-engine/src/definition/users/IPostUserDeleted.ts create mode 100644 packages/apps-engine/src/definition/users/IPostUserLoggedIn.ts create mode 100644 packages/apps-engine/src/definition/users/IPostUserLoggedOut.ts create mode 100644 packages/apps-engine/src/definition/users/IPostUserStatusChanged.ts create mode 100644 packages/apps-engine/src/definition/users/IPostUserUpdated.ts create mode 100644 packages/apps-engine/src/definition/users/IUser.ts create mode 100644 packages/apps-engine/src/definition/users/IUserContext.ts create mode 100644 packages/apps-engine/src/definition/users/IUserCreationOptions.ts create mode 100644 packages/apps-engine/src/definition/users/IUserEmail.ts create mode 100644 packages/apps-engine/src/definition/users/IUserLookup.ts create mode 100644 packages/apps-engine/src/definition/users/IUserSettings.ts create mode 100644 packages/apps-engine/src/definition/users/IUserStatusContext.ts create mode 100644 packages/apps-engine/src/definition/users/IUserUpdateContex.ts create mode 100644 packages/apps-engine/src/definition/users/IUserUpdateContext.ts create mode 100644 packages/apps-engine/src/definition/users/UserStatusConnection.ts create mode 100644 packages/apps-engine/src/definition/users/UserType.ts create mode 100644 packages/apps-engine/src/definition/users/index.ts create mode 100644 packages/apps-engine/src/definition/videoConfProviders/IVideoConfProvider.ts create mode 100644 packages/apps-engine/src/definition/videoConfProviders/IVideoConferenceOptions.ts create mode 100644 packages/apps-engine/src/definition/videoConfProviders/VideoConfData.ts create mode 100644 packages/apps-engine/src/definition/videoConfProviders/index.ts create mode 100644 packages/apps-engine/src/definition/videoConferences/AppVideoConference.ts create mode 100644 packages/apps-engine/src/definition/videoConferences/IVideoConference.ts create mode 100644 packages/apps-engine/src/definition/videoConferences/IVideoConferenceUser.ts create mode 100644 packages/apps-engine/src/definition/videoConferences/index.ts create mode 100644 packages/apps-engine/src/lib/utils.ts create mode 100644 packages/apps-engine/src/server/AppManager.ts create mode 100644 packages/apps-engine/src/server/IGetAppsFilter.ts create mode 100644 packages/apps-engine/src/server/ProxiedApp.ts create mode 100644 packages/apps-engine/src/server/accessors/ApiExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/AppAccessors.ts create mode 100644 packages/apps-engine/src/server/accessors/CloudWorkspaceRead.ts create mode 100644 packages/apps-engine/src/server/accessors/ConfigurationExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/ConfigurationModify.ts create mode 100644 packages/apps-engine/src/server/accessors/DiscussionBuilder.ts create mode 100644 packages/apps-engine/src/server/accessors/EmailCreator.ts create mode 100644 packages/apps-engine/src/server/accessors/EnvironmentRead.ts create mode 100644 packages/apps-engine/src/server/accessors/EnvironmentWrite.ts create mode 100644 packages/apps-engine/src/server/accessors/EnvironmentalVariableRead.ts create mode 100644 packages/apps-engine/src/server/accessors/ExternalComponentsExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/Http.ts create mode 100644 packages/apps-engine/src/server/accessors/HttpExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/LivechatCreator.ts create mode 100644 packages/apps-engine/src/server/accessors/LivechatMessageBuilder.ts create mode 100644 packages/apps-engine/src/server/accessors/LivechatRead.ts create mode 100644 packages/apps-engine/src/server/accessors/LivechatUpdater.ts create mode 100644 packages/apps-engine/src/server/accessors/MessageBuilder.ts create mode 100644 packages/apps-engine/src/server/accessors/MessageExtender.ts create mode 100644 packages/apps-engine/src/server/accessors/MessageRead.ts create mode 100644 packages/apps-engine/src/server/accessors/ModerationModify.ts create mode 100644 packages/apps-engine/src/server/accessors/Modify.ts create mode 100644 packages/apps-engine/src/server/accessors/ModifyCreator.ts create mode 100644 packages/apps-engine/src/server/accessors/ModifyDeleter.ts create mode 100644 packages/apps-engine/src/server/accessors/ModifyExtender.ts create mode 100644 packages/apps-engine/src/server/accessors/ModifyUpdater.ts create mode 100644 packages/apps-engine/src/server/accessors/Notifier.ts create mode 100644 packages/apps-engine/src/server/accessors/OAuthAppsModify.ts create mode 100644 packages/apps-engine/src/server/accessors/OAuthAppsReader.ts create mode 100644 packages/apps-engine/src/server/accessors/Persistence.ts create mode 100644 packages/apps-engine/src/server/accessors/PersistenceRead.ts create mode 100644 packages/apps-engine/src/server/accessors/Reader.ts create mode 100644 packages/apps-engine/src/server/accessors/RoleRead.ts create mode 100644 packages/apps-engine/src/server/accessors/RoomBuilder.ts create mode 100644 packages/apps-engine/src/server/accessors/RoomExtender.ts create mode 100644 packages/apps-engine/src/server/accessors/RoomRead.ts create mode 100644 packages/apps-engine/src/server/accessors/SchedulerExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/SchedulerModify.ts create mode 100644 packages/apps-engine/src/server/accessors/ServerSettingRead.ts create mode 100644 packages/apps-engine/src/server/accessors/ServerSettingUpdater.ts create mode 100644 packages/apps-engine/src/server/accessors/ServerSettingsModify.ts create mode 100644 packages/apps-engine/src/server/accessors/SettingRead.ts create mode 100644 packages/apps-engine/src/server/accessors/SettingUpdater.ts create mode 100644 packages/apps-engine/src/server/accessors/SettingsExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/SlashCommandsExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/SlashCommandsModify.ts create mode 100644 packages/apps-engine/src/server/accessors/ThreadRead.ts create mode 100644 packages/apps-engine/src/server/accessors/UIController.ts create mode 100644 packages/apps-engine/src/server/accessors/UIExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/UploadCreator.ts create mode 100644 packages/apps-engine/src/server/accessors/UploadRead.ts create mode 100644 packages/apps-engine/src/server/accessors/UserBuilder.ts create mode 100644 packages/apps-engine/src/server/accessors/UserRead.ts create mode 100644 packages/apps-engine/src/server/accessors/UserUpdater.ts create mode 100644 packages/apps-engine/src/server/accessors/VideoConfProviderExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/VideoConferenceBuilder.ts create mode 100644 packages/apps-engine/src/server/accessors/VideoConferenceExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/VideoConferenceRead.ts create mode 100644 packages/apps-engine/src/server/accessors/index.ts create mode 100644 packages/apps-engine/src/server/bridges/ApiBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/AppActivationBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/AppBridges.ts create mode 100644 packages/apps-engine/src/server/bridges/AppDetailChangesBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/BaseBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/CloudWorkspaceBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/CommandBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/EmailBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/EnvironmentalVariableBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/HttpBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/IInternalBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/IInternalFederationBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/IInternalPersistenceBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/IInternalSchedulerBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/IInternalUserBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/IListenerBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/InternalBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/ListenerBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/LivechatBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/MessageBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/ModerationBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/OAuthAppsBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/PersistenceBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/RoleBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/RoomBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/SchedulerBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/ServerSettingBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/ThreadBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/UiInteractionBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/UploadBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/UserBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/VideoConferenceBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/index.ts create mode 100644 packages/apps-engine/src/server/compiler/AppCompiler.ts create mode 100644 packages/apps-engine/src/server/compiler/AppFabricationFulfillment.ts create mode 100644 packages/apps-engine/src/server/compiler/AppImplements.ts create mode 100644 packages/apps-engine/src/server/compiler/AppPackageParser.ts create mode 100644 packages/apps-engine/src/server/compiler/IParseAppPackageResult.ts create mode 100644 packages/apps-engine/src/server/compiler/index.ts create mode 100644 packages/apps-engine/src/server/compiler/modules/index.ts create mode 100644 packages/apps-engine/src/server/compiler/modules/networking.ts create mode 100644 packages/apps-engine/src/server/errors/CommandAlreadyExistsError.ts create mode 100644 packages/apps-engine/src/server/errors/CommandHasAlreadyBeenTouchedError.ts create mode 100644 packages/apps-engine/src/server/errors/CompilerError.ts create mode 100644 packages/apps-engine/src/server/errors/InvalidInstallationError.ts create mode 100644 packages/apps-engine/src/server/errors/InvalidLicenseError.ts create mode 100644 packages/apps-engine/src/server/errors/MustContainFunctionError.ts create mode 100644 packages/apps-engine/src/server/errors/MustExtendAppError.ts create mode 100644 packages/apps-engine/src/server/errors/NotEnoughMethodArgumentsError.ts create mode 100644 packages/apps-engine/src/server/errors/PathAlreadyExistsError.ts create mode 100644 packages/apps-engine/src/server/errors/PermissionDeniedError.ts create mode 100644 packages/apps-engine/src/server/errors/RequiredApiVersionError.ts create mode 100644 packages/apps-engine/src/server/errors/VideoConfProviderAlreadyExistsError.ts create mode 100644 packages/apps-engine/src/server/errors/VideoConfProviderNotRegisteredError.ts create mode 100644 packages/apps-engine/src/server/errors/index.ts create mode 100644 packages/apps-engine/src/server/logging/AppConsole.ts create mode 100644 packages/apps-engine/src/server/logging/ILoggerStorageEntry.ts create mode 100644 packages/apps-engine/src/server/logging/index.ts create mode 100644 packages/apps-engine/src/server/managers/AppAccessorManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppApi.ts create mode 100644 packages/apps-engine/src/server/managers/AppApiManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppExternalComponentManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppLicenseManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppListenerManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppPermissionManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppRuntimeManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppSchedulerManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppSettingsManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppSignatureManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppSlashCommand.ts create mode 100644 packages/apps-engine/src/server/managers/AppSlashCommandManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppVideoConfProvider.ts create mode 100644 packages/apps-engine/src/server/managers/AppVideoConfProviderManager.ts create mode 100644 packages/apps-engine/src/server/managers/UIActionButtonManager.ts create mode 100644 packages/apps-engine/src/server/managers/index.ts create mode 100644 packages/apps-engine/src/server/marketplace/IAppLicenseMetadata.ts create mode 100644 packages/apps-engine/src/server/marketplace/IMarketplaceInfo.ts create mode 100644 packages/apps-engine/src/server/marketplace/IMarketplacePricingPlan.ts create mode 100644 packages/apps-engine/src/server/marketplace/IMarketplacePricingTier.ts create mode 100644 packages/apps-engine/src/server/marketplace/IMarketplaceSimpleBundleInfo.ts create mode 100644 packages/apps-engine/src/server/marketplace/IMarketplaceSubscriptionInfo.ts create mode 100644 packages/apps-engine/src/server/marketplace/MarketplacePricingStrategy.ts create mode 100644 packages/apps-engine/src/server/marketplace/MarketplacePurchaseType.ts create mode 100644 packages/apps-engine/src/server/marketplace/MarketplaceSubscriptionStatus.ts create mode 100644 packages/apps-engine/src/server/marketplace/MarketplaceSubscriptionType.ts create mode 100644 packages/apps-engine/src/server/marketplace/index.ts create mode 100644 packages/apps-engine/src/server/marketplace/license/AppLicenseValidationResult.ts create mode 100644 packages/apps-engine/src/server/marketplace/license/Crypto.ts create mode 100644 packages/apps-engine/src/server/marketplace/license/index.ts create mode 100644 packages/apps-engine/src/server/messages/Message.ts create mode 100644 packages/apps-engine/src/server/misc/UIHelper.ts create mode 100644 packages/apps-engine/src/server/misc/Utilities.ts create mode 100644 packages/apps-engine/src/server/oauth2/OAuth2Client.ts create mode 100644 packages/apps-engine/src/server/permissions/AppPermissions.ts create mode 100644 packages/apps-engine/src/server/rooms/Room.ts create mode 100644 packages/apps-engine/src/server/runtime/AppsEngineEmptyRuntime.ts create mode 100644 packages/apps-engine/src/server/runtime/AppsEngineNodeRuntime.ts create mode 100644 packages/apps-engine/src/server/runtime/AppsEngineRuntime.ts create mode 100644 packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts create mode 100644 packages/apps-engine/src/server/runtime/deno/LivenessManager.ts create mode 100644 packages/apps-engine/src/server/runtime/deno/ProcessMessenger.ts create mode 100644 packages/apps-engine/src/server/runtime/deno/bundler.ts create mode 100644 packages/apps-engine/src/server/runtime/deno/codec.ts create mode 100644 packages/apps-engine/src/server/storage/AppLogStorage.ts create mode 100644 packages/apps-engine/src/server/storage/AppMetadataStorage.ts create mode 100644 packages/apps-engine/src/server/storage/AppSourceStorage.ts create mode 100644 packages/apps-engine/src/server/storage/IAppStorageItem.ts create mode 100644 packages/apps-engine/src/server/storage/index.ts create mode 100644 packages/apps-engine/tests/runner.ts create mode 100644 packages/apps-engine/tests/server/AppManager.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/AppAccessors.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/ConfigurationExtend.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/ConfigurationModify.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/EnvironmentRead.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/EnvironmentWrite.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/EnvironmentalVariableRead.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/Http.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/HttpExtend.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/MessageBuilder.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/MessageExtender.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/MessageRead.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/Modify.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/ModifyCreator.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/ModifyExtender.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/ModifyUpdater.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/Notifier.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/Persistence.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/PersistenceRead.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/Reader.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/RoomBuilder.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/RoomExtender.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/RoomRead.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/ServerSettingRead.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/ServerSettingsModify.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/SettingRead.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/SettingsExtend.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/SlashCommandsExtend.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/SlashCommandsModify.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/UserBuilder.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/UserRead.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/VideoConfProviderExtend.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/VideoConferenceBuilder.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/VideoConferenceExtend.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/VideoConferenceRead.spec.ts create mode 100644 packages/apps-engine/tests/server/compiler/AppCompiler.spec.ts create mode 100644 packages/apps-engine/tests/server/compiler/AppFabricationFulfillment.spec.ts create mode 100644 packages/apps-engine/tests/server/compiler/AppImplements.spec.ts create mode 100644 packages/apps-engine/tests/server/errors/CommandAlreadyExistsError.spec.ts create mode 100644 packages/apps-engine/tests/server/errors/CommandHasAlreadyBeenTouchedError.spec.ts create mode 100644 packages/apps-engine/tests/server/errors/CompilerError.spec.ts create mode 100644 packages/apps-engine/tests/server/errors/MustContainFunctionError.spec.ts create mode 100644 packages/apps-engine/tests/server/errors/MustExtendAppError.spec.ts create mode 100644 packages/apps-engine/tests/server/errors/NotEnoughMethodArgumentsError.spec.ts create mode 100644 packages/apps-engine/tests/server/errors/RequiredApiVersionError.spec.ts create mode 100644 packages/apps-engine/tests/server/logging/AppConsole.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppAccessorManager.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppApi.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppApiManager.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppExternalComponentManager.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppListenerManager.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppSettingsManager.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppSlashCommand.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppSlashCommandManager.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppVideoConfProvider.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppVideoConfProviderManager.spec.ts create mode 100644 packages/apps-engine/tests/server/misc/Utilities.spec.ts create mode 100644 packages/apps-engine/tests/server/runtime/DenoRuntimeSubprocessController.spec.ts create mode 100644 packages/apps-engine/tests/test-data/README.md create mode 100644 packages/apps-engine/tests/test-data/apps/hello-world-test_0.0.1.zip create mode 100644 packages/apps-engine/tests/test-data/apps/testing-app_0.0.8.zip create mode 100644 packages/apps-engine/tests/test-data/bridges/OAuthAppsBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/activationBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/apiBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/appBridges.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/appDetailChanges.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/cloudBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/commandBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/emailBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/environmentalVariableBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/httpBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/internalBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/internalFederationBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/livechatBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/messageBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/moderationBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/persisBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/roleBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/roomBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/schedulerBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/serverSettingBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/threadBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/uiIntegrationBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/uploadBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/userBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/videoConferenceBridge.ts create mode 100644 packages/apps-engine/tests/test-data/misc/fake-library-file.d.ts create mode 100644 packages/apps-engine/tests/test-data/storage/TestSourceStorage.ts create mode 100644 packages/apps-engine/tests/test-data/storage/logStorage.ts create mode 100644 packages/apps-engine/tests/test-data/storage/storage.ts create mode 100644 packages/apps-engine/tests/test-data/utilities.ts create mode 100644 packages/apps-engine/tsconfig-lint.json create mode 100644 packages/apps-engine/tsconfig.json create mode 100644 packages/apps-engine/turbo.json create mode 100644 packages/apps-engine/typedoc.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a834776aeff5..f66c5d29de5b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,5 @@ /packages/* @RocketChat/Architecture +/packages/apps-engine/ @RocketChat/apps /packages/core-typings/ @RocketChat/Architecture /packages/rest-typings/ @RocketChat/Architecture @RocketChat/backend /packages/ui-contexts/ @RocketChat/frontend diff --git a/.github/actions/build-docker-image/action.yml b/.github/actions/build-docker-image/action.yml index 378f6bdb01b9..02a05d9605a7 100644 --- a/.github/actions/build-docker-image/action.yml +++ b/.github/actions/build-docker-image/action.yml @@ -12,6 +12,9 @@ inputs: required: false password: required: false + deno-version: + required: true + type: string outputs: image-name: @@ -59,7 +62,7 @@ runs: fi; echo "Build ${{ inputs.release }} Docker image" - docker build -t $IMAGE_NAME . + docker build --build-arg DENO_VERSION=${{ inputs.deno-version }} -t $IMAGE_NAME . echo "image-name-base=${IMAGE_NAME_BASE}" >> $GITHUB_OUTPUT echo "image-name=${IMAGE_NAME}" >> $GITHUB_OUTPUT diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index 5af39b924057..ae84e376a0d9 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -9,6 +9,10 @@ inputs: required: true description: 'Node version' type: string + deno-version: + required: true + description: 'Deno version' + type: string platform: required: false description: 'Platform' @@ -66,6 +70,7 @@ runs: if: inputs.setup == 'true' with: node-version: ${{ inputs.node-version }} + deno-version: ${{ inputs.deno-version }} cache-modules: true install: true NPM_TOKEN: ${{ inputs.NPM_TOKEN }} @@ -79,6 +84,8 @@ runs: run: | args=(rocketchat ${{ inputs.build-containers }}) + export DENO_VERSION="${{ inputs.deno-version }}" + docker compose -f docker-compose-ci.yml build "${args[@]}" - name: Publish Docker images to GitHub Container Registry diff --git a/.github/actions/meteor-build/action.yml b/.github/actions/meteor-build/action.yml index 551a57d28a7c..bfd4ae7f5c20 100644 --- a/.github/actions/meteor-build/action.yml +++ b/.github/actions/meteor-build/action.yml @@ -16,6 +16,10 @@ inputs: NPM_TOKEN: required: false description: 'NPM token' + deno-version: + required: true + description: 'Deno version' + type: string runs: using: composite @@ -30,6 +34,7 @@ runs: uses: ./.github/actions/setup-node with: node-version: ${{ inputs.node-version }} + deno-version: ${{ inputs.deno-version }} cache-modules: true install: true NPM_TOKEN: ${{ inputs.NPM_TOKEN }} diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index 1035e2835792..120797d2ba3c 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -11,10 +11,11 @@ inputs: install: required: false description: 'Install dependencies' - deno-dir: - required: false - description: 'Deno directory' - default: ~/.deno-cache + type: boolean + deno-version: + required: true + description: 'Deno version' + type: string NPM_TOKEN: required: false description: 'NPM token' @@ -28,21 +29,20 @@ runs: using: composite steps: - - run: echo 'DENO_DIR=${{ inputs.deno-dir }}' >> $GITHUB_ENV - shell: bash - - - name: Cache Node Modules + - name: Cache Node Modules & Deno if: inputs.cache-modules id: cache-node-modules uses: actions/cache@v3 with: + # We need to cache node_modules for all workspaces with "hoistingLimits" defined path: | .turbo/cache node_modules - ${{ env.DENO_DIR }} apps/meteor/node_modules apps/meteor/ee/server/services/node_modules - key: node-modules-${{ hashFiles('yarn.lock') }} + packages/apps-engine/node_modules + packages/apps-engine/.deno-cache + key: node-modules-${{ hashFiles('yarn.lock') }}-deno-${{ hashFiles('packages/apps-engine/deno-runtime/deno.lock') }} # # Could use this command to list all paths to save: # find . -name 'node_modules' -prune | grep -v "/\.meteor/" | grep -v "/meteor/packages/" @@ -54,6 +54,11 @@ runs: node-version: ${{ inputs.node-version }} cache: 'yarn' + - name: Use Deno ${{ inputs.deno-version }} + uses: denoland/setup-deno@v1 + with: + deno-version: ${{ inputs.deno-version }} + - name: yarn login shell: bash if: inputs.NPM_TOKEN diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml index af50b3230ba7..41facad89a03 100644 --- a/.github/workflows/ci-code-check.yml +++ b/.github/workflows/ci-code-check.yml @@ -6,6 +6,9 @@ on: node-version: required: true type: string + deno-version: + required: true + type: string env: TOOL_NODE_FLAGS: ${{ vars.TOOL_NODE_FLAGS }} @@ -33,6 +36,7 @@ jobs: uses: ./.github/actions/setup-node with: node-version: ${{ inputs.node-version }} + deno-version: ${{ inputs.deno-version }} cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/ci-deploy-gh-pages-preview.yml b/.github/workflows/ci-deploy-gh-pages-preview.yml index 17f247ddad94..8a0905a174bb 100644 --- a/.github/workflows/ci-deploy-gh-pages-preview.yml +++ b/.github/workflows/ci-deploy-gh-pages-preview.yml @@ -23,6 +23,7 @@ jobs: if: github.event.action != 'closed' with: node-version: 14.21.3 + deno-version: 1.37.1 cache-modules: true install: true diff --git a/.github/workflows/ci-deploy-gh-pages.yml b/.github/workflows/ci-deploy-gh-pages.yml index b381e05ae5d8..0aab8022c7e6 100644 --- a/.github/workflows/ci-deploy-gh-pages.yml +++ b/.github/workflows/ci-deploy-gh-pages.yml @@ -18,6 +18,7 @@ jobs: uses: ./.github/actions/setup-node with: node-version: 14.21.3 + deno-version: 1.37.1 cache-modules: true install: true diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index a80a40419e9f..f219f39c0614 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -6,6 +6,9 @@ on: node-version: required: true type: string + deno-version: + required: true + type: string lowercase-repo: required: true type: string @@ -128,6 +131,7 @@ jobs: uses: ./.github/actions/setup-node with: node-version: ${{ inputs.node-version }} + deno-version: ${{ inputs.deno-version }} cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index 840808ff5e31..883212d0cf3d 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -6,6 +6,9 @@ on: node-version: required: true type: string + deno-version: + required: true + type: string enterprise-license: required: false type: string @@ -37,6 +40,7 @@ jobs: uses: ./.github/actions/setup-node with: node-version: ${{ inputs.node-version }} + deno-version: ${{ inputs.deno-version }} cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40260f71d21f..6b6fa426ca96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,7 @@ jobs: rc-dockerfile-alpine: '${{ github.workspace }}/apps/meteor/.docker/Dockerfile.alpine' rc-docker-tag-alpine: '${{ steps.docker.outputs.gh-docker-tag }}.alpine' node-version: ${{ steps.var.outputs.node-version }} + deno-version: ${{ steps.var.outputs.deno-version }} # this is 100% intentional, secrets are not available for forks, so ee-tests will always fail # to avoid this, we are using a dummy license, expiring at 2025-06-31 enterprise-license: X/XumwIkgwQuld0alWKt37lVA90XjKOrfiMvMZ0/RtqsMtrdL9GoAk+4jXnaY1b2ePoG7XSzGhuxEDxFKIWJK3hIKGNTvrd980LgH5sM5+1T4P42ivSpd8UZi0bwjJkCFLIu9RozzYwslGG0IehMxe0S6VjcO0UYlUJtbMCBHuR2WmTAmO6YVU3ln+pZCbrPFaTPSS1RovhKaNCNkZwIx/CLWW8UTXUuFV/ML4PbKKVoa5nvvJwPeatgL7UCnlSD90lfCiiuikpzj/Y/JLkIL6velFbwNxsrxg9iRJ2k0sKheMMSmlTiGzSvZUm+na5WQq91aKGncih+DmaEZA7QGrjp4eoA0dqTk6OmItsy0fHmQhvZIOKNMeO7vNQiLbaSV6rqibrzu7WPpeIvsvL57T1h37USoCSB6+jDqkzdfoqIpz8BxTiJDj1d8xGPJFVrgxoqQqkj9qIP/gCaEz5DF39QFv5sovk4yK2O8fEQYod2d14V9yECYl4szZPMk1IBfCAC2w7czWGHHFonhL+CQGT403y5wmDmnsnjlCqMKF72odqfTPTI8XnCvJDriPMWohnQEAGtTTyciAhNokx/mjAVJ4NeZPcsbm4BjhvJvnjxx/BhYhBBTNWPaCSZzocfrGUj9Z+ZA7BEz+xAFQyGDx3xRzqIXfT0G7w8fvgYJMU= @@ -39,6 +40,7 @@ jobs: with: sparse-checkout: | package.json + .tool-versions sparse-checkout-cone-mode: false ref: ${{ github.ref }} @@ -53,6 +55,10 @@ jobs: echo "NODE_VERSION: ${NODE_VERSION}" echo "node-version=${NODE_VERSION}" >> $GITHUB_OUTPUT + DENO_VERSION=$(awk '$1=="deno"{ print $2 }' .tool-versions) + echo "DENO_VERSION: ${DENO_VERSION}" + echo "deno-version=${DENO_VERSION}" >> $GITHUB_OUTPUT + - id: by-tag run: | if echo "$GITHUB_REF_NAME" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' ; then @@ -150,6 +156,7 @@ jobs: uses: ./.github/actions/setup-node with: node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} @@ -194,6 +201,7 @@ jobs: - uses: ./.github/actions/meteor-build with: node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} coverage: ${{ github.event_name != 'release' }} build-prod: @@ -224,6 +232,7 @@ jobs: - uses: ./.github/actions/meteor-build with: node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} coverage: ${{ github.event_name != 'release' }} build-gh-docker-coverage: @@ -252,6 +261,7 @@ jobs: CR_USER: ${{ secrets.CR_USER }} CR_PAT: ${{ secrets.CR_PAT }} node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} platform: ${{ matrix.platform }} build-containers: ${{ matrix.platform == 'alpine' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} @@ -280,6 +290,7 @@ jobs: CR_USER: ${{ secrets.CR_USER }} CR_PAT: ${{ secrets.CR_PAT }} node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} platform: ${{ matrix.platform }} build-containers: ${{ matrix.platform == 'alpine' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} @@ -300,6 +311,7 @@ jobs: uses: ./.github/workflows/ci-code-check.yml with: node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} test-unit: name: 🔨 Test Unit @@ -308,6 +320,7 @@ jobs: uses: ./.github/workflows/ci-test-unit.yml with: node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} enterprise-license: ${{ needs.release-versions.outputs.enterprise-license }} secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -321,6 +334,7 @@ jobs: type: api release: ce node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} @@ -344,6 +358,7 @@ jobs: shard: '[1, 2, 3, 4]' total-shard: 4 node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} @@ -371,6 +386,7 @@ jobs: enterprise-license: ${{ needs.release-versions.outputs.enterprise-license }} mongodb-version: "['4.4']" node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} @@ -395,6 +411,7 @@ jobs: total-shard: 5 mongodb-version: "['4.4']" node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} @@ -425,6 +442,7 @@ jobs: total-shard: 5 mongodb-version: "['6.0']" node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} @@ -564,6 +582,7 @@ jobs: username: ${{ secrets.CR_USER }} password: ${{ secrets.CR_PAT }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} docker-image-publish: name: 🚀 Publish Docker Image (main) diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml index b2eae5d90b92..70e9eb354a06 100644 --- a/.github/workflows/new-release.yml +++ b/.github/workflows/new-release.yml @@ -35,6 +35,7 @@ jobs: uses: ./.github/actions/setup-node with: node-version: 14.21.3 + deno-version: 1.37.1 cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/pr-update-description.yml b/.github/workflows/pr-update-description.yml index 084f2a383480..26ffffc6c86f 100644 --- a/.github/workflows/pr-update-description.yml +++ b/.github/workflows/pr-update-description.yml @@ -22,6 +22,7 @@ jobs: uses: ./.github/actions/setup-node with: node-version: 14.21.3 + deno-version: 1.37.1 cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 3f2067ac7ec3..fe049f6a8369 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -25,6 +25,7 @@ jobs: uses: ./.github/actions/setup-node with: node-version: 14.21.3 + deno-version: 1.37.1 cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 4a1e67fca33a..8c9048710dd2 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -16,6 +16,7 @@ jobs: uses: ./.github/actions/setup-node with: node-version: 14.21.3 + deno-version: 1.37.1 cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000000..bc89cc40f83e --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +deno 1.37.1 diff --git a/README.md b/README.md index 6461ad602516..564ca75d2b11 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ You can follow these instructions to setup a dev environment: - Install **Node 14.x (LTS)** either [manually](https://nodejs.org/dist/latest-v14.x/) or using a tool like [nvm](https://github.com/creationix/nvm) or [volta](https://volta.sh/) (recommended) - Install **Meteor** ([version here](apps/meteor/.meteor/release)): https://docs.meteor.com/about/install.html - Install **yarn**: https://yarnpkg.com/getting-started/install +- Install **Deno 1.x**: https://docs.deno.com/runtime/fundamentals/installation/ - Clone this repo: `git clone https://github.com/RocketChat/Rocket.Chat.git` - Run `yarn` to install dependencies diff --git a/apps/meteor/.docker/Dockerfile b/apps/meteor/.docker/Dockerfile index 1e9ed3f5e592..75df2cb90678 100644 --- a/apps/meteor/.docker/Dockerfile +++ b/apps/meteor/.docker/Dockerfile @@ -1,3 +1,7 @@ +ARG DENO_VERSION="1.37.1" + +FROM denoland/deno:bin-${DENO_VERSION} as deno + FROM node:14.21.3-bullseye-slim LABEL maintainer="buildmaster@rocket.chat" @@ -10,6 +14,8 @@ RUN groupadd -g 65533 -r rocketchat \ && apt-get update \ && apt-get install -y --no-install-recommends fontconfig +COPY --from=deno /deno /bin/deno + # --chown requires Docker 17.12 and works only on Linux ADD --chown=rocketchat:rocketchat . /app @@ -20,8 +26,7 @@ ENV DEPLOY_METHOD=docker \ HOME=/tmp \ PORT=3000 \ ROOT_URL=http://localhost:3000 \ - Accounts_AvatarStorePath=/app/uploads \ - DENO_DIR=/usr/share/deno + Accounts_AvatarStorePath=/app/uploads RUN aptMark="$(apt-mark showmanual)" \ && apt-get install -y --no-install-recommends g++ make python3 ca-certificates \ @@ -29,8 +34,6 @@ RUN aptMark="$(apt-mark showmanual)" \ && npm install \ && cd npm/node_modules/isolated-vm \ && npm install \ - && cd /app/bundle/programs/server/npm/node_modules/@rocket.chat/apps-engine/deno-runtime \ - && ../../../deno-bin/bin/deno cache main.ts \ && apt-mark auto '.*' > /dev/null \ && apt-mark manual $aptMark > /dev/null \ && find /usr/local -type f -executable -exec ldd '{}' ';' \ diff --git a/apps/meteor/.docker/Dockerfile.alpine b/apps/meteor/.docker/Dockerfile.alpine index feebf76a03e7..aaa2d2552ab3 100644 --- a/apps/meteor/.docker/Dockerfile.alpine +++ b/apps/meteor/.docker/Dockerfile.alpine @@ -1,7 +1,17 @@ +ARG DENO_VERSION="1.37.1" + +FROM denoland/deno:bin-${DENO_VERSION} as deno + FROM node:14.21.3-alpine3.16 +LABEL maintainer="buildmaster@rocket.chat" + ENV LANG=C.UTF-8 +## Alpine 3.16 does not have a deno package, but newer versions have it +## So as soon as we can update the Alpine version, we can replace the following +## GLIBC installation part by an `apk add deno` + # Installing glibc deps required by Deno # This replaces libc6-compat # Copied from https://github.com/Docker-Hub-frolvlad/docker-alpine-glibc, which denoland/deno:alpine-1.37.1 uses @@ -11,7 +21,7 @@ RUN ALPINE_GLIBC_BASE_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases ALPINE_GLIBC_BASE_PACKAGE_FILENAME="glibc-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \ ALPINE_GLIBC_BIN_PACKAGE_FILENAME="glibc-bin-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \ ALPINE_GLIBC_I18N_PACKAGE_FILENAME="glibc-i18n-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \ - apk add --no-cache --virtual=.build-dependencies wget ca-certificates && \ + apk add --no-cache --virtual=.build-dependencies wget ca-certificates ttf-dejavu && \ echo \ "-----BEGIN PUBLIC KEY-----\ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZ2u1KJKUu/fW4A25y9m\ @@ -44,12 +54,11 @@ RUN ALPINE_GLIBC_BASE_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases rm \ "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \ "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \ - "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \ - apk add --no-cache ttf-dejavu + "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" -ADD . /app +COPY --from=deno /deno /bin/deno -LABEL maintainer="buildmaster@rocket.chat" +ADD . /app # needs a mongo instance - defaults to container linking with alias 'mongo' ENV DEPLOY_METHOD=docker \ @@ -58,13 +67,12 @@ ENV DEPLOY_METHOD=docker \ HOME=/tmp \ PORT=3000 \ ROOT_URL=http://localhost:3000 \ - Accounts_AvatarStorePath=/app/uploads \ - DENO_DIR=/usr/share/deno + Accounts_AvatarStorePath=/app/uploads RUN set -x \ && apk add --no-cache --virtual .fetch-deps python3 make g++ \ && cd /app/bundle/programs/server \ - && npm install --production \ + && npm install --omit=dev --unsafe-perm \ # Start hack for sharp... && rm -rf npm/node_modules/sharp \ && npm install sharp@0.32.6 \ @@ -75,9 +83,6 @@ RUN set -x \ && npm install isolated-vm@4.4.2 \ && mv node_modules/isolated-vm npm/node_modules/isolated-vm \ # End hack for isolated-vm - # Cache Deno dependencies for Apps-Engine - && cd npm/node_modules/@rocket.chat/apps-engine/deno-runtime \ - && /app/bundle/programs/server/npm/node_modules/deno-bin/bin/deno cache main.ts \ && cd /app/bundle/programs/server/npm \ && npm rebuild bcrypt --build-from-source \ && npm cache clear --force \ diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 390b2c646cd1..9eb2e917819d 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -18,7 +18,7 @@ "author": "Rocket.Chat", "license": "MIT", "dependencies": { - "@rocket.chat/apps-engine": "1.45.0-alpha.868", + "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "~0.31.25", diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 3767939a7e3c..b5c9003b6974 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -230,7 +230,7 @@ "@rocket.chat/agenda": "workspace:^", "@rocket.chat/api-client": "workspace:^", "@rocket.chat/apps": "workspace:^", - "@rocket.chat/apps-engine": "1.45.0-alpha.868", + "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/base64": "workspace:^", "@rocket.chat/cas-validate": "workspace:^", "@rocket.chat/core-services": "workspace:^", diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index 23ee3b125524..0fe101c8fab1 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -8,6 +8,8 @@ services: build: dockerfile: ${RC_DOCKERFILE} context: /tmp/build + args: + DENO_VERSION: ${DENO_VERSION} image: ghcr.io/${LOWERCASE_REPOSITORY}/rocket.chat:${RC_DOCKER_TAG} environment: - TEST_MODE=true @@ -39,6 +41,7 @@ services: context: . args: SERVICE: authorization-service + DENO_VERSION: ${DENO_VERSION} image: ghcr.io/${LOWERCASE_REPOSITORY}/authorization-service:${DOCKER_TAG} environment: - 'MONGO_URL=${MONGO_URL}' @@ -73,6 +76,7 @@ services: context: . args: SERVICE: presence-service + DENO_VERSION: ${DENO_VERSION} image: ghcr.io/${LOWERCASE_REPOSITORY}/presence-service:${DOCKER_TAG} environment: - MONGO_URL=${MONGO_URL} diff --git a/ee/apps/account-service/Dockerfile b/ee/apps/account-service/Dockerfile index c80d4f2eb376..a97290a43394 100644 --- a/ee/apps/account-service/Dockerfile +++ b/ee/apps/account-service/Dockerfile @@ -7,6 +7,10 @@ WORKDIR /app COPY ./packages/core-services/package.json packages/core-services/package.json COPY ./packages/core-services/dist packages/core-services/dist +COPY ./packages/apps-engine/package.json packages/apps-engine/package.json +COPY ./packages/apps-engine/client packages/apps-engine/client +COPY ./packages/apps-engine/definition packages/apps-engine/definition + COPY ./packages/agenda/package.json packages/agenda/package.json COPY ./packages/agenda/dist packages/agenda/dist diff --git a/ee/apps/authorization-service/Dockerfile b/ee/apps/authorization-service/Dockerfile index 9a9e8ded922c..9ddeadd380fe 100644 --- a/ee/apps/authorization-service/Dockerfile +++ b/ee/apps/authorization-service/Dockerfile @@ -7,6 +7,10 @@ WORKDIR /app COPY ./packages/core-services/package.json packages/core-services/package.json COPY ./packages/core-services/dist packages/core-services/dist +COPY ./packages/apps-engine/package.json packages/apps-engine/package.json +COPY ./packages/apps-engine/client packages/apps-engine/client +COPY ./packages/apps-engine/definition packages/apps-engine/definition + COPY ./packages/agenda/package.json packages/agenda/package.json COPY ./packages/agenda/dist packages/agenda/dist diff --git a/ee/apps/ddp-streamer/Dockerfile b/ee/apps/ddp-streamer/Dockerfile index 32103dc3528b..dea2bc3790a1 100644 --- a/ee/apps/ddp-streamer/Dockerfile +++ b/ee/apps/ddp-streamer/Dockerfile @@ -7,6 +7,10 @@ WORKDIR /app COPY ./packages/core-services/package.json packages/core-services/package.json COPY ./packages/core-services/dist packages/core-services/dist +COPY ./packages/apps-engine/package.json packages/apps-engine/package.json +COPY ./packages/apps-engine/client packages/apps-engine/client +COPY ./packages/apps-engine/definition packages/apps-engine/definition + COPY ./packages/agenda/package.json packages/agenda/package.json COPY ./packages/agenda/dist packages/agenda/dist diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 36c244f41180..f250b9e33106 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -15,7 +15,6 @@ ], "author": "Rocket.Chat", "dependencies": { - "@rocket.chat/apps-engine": "1.45.0-alpha.868", "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "~0.31.25", @@ -45,6 +44,7 @@ "ws": "^8.8.1" }, "devDependencies": { + "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/ddp-client": "workspace:~", "@rocket.chat/eslint-config": "workspace:^", "@types/ejson": "^2.2.2", diff --git a/ee/apps/omnichannel-transcript/Dockerfile b/ee/apps/omnichannel-transcript/Dockerfile index 9b7e47968e68..0f18534e1453 100644 --- a/ee/apps/omnichannel-transcript/Dockerfile +++ b/ee/apps/omnichannel-transcript/Dockerfile @@ -7,6 +7,10 @@ WORKDIR /app COPY ./packages/core-services/package.json packages/core-services/package.json COPY ./packages/core-services/dist packages/core-services/dist +COPY ./packages/apps-engine/package.json packages/apps-engine/package.json +COPY ./packages/apps-engine/client packages/apps-engine/client +COPY ./packages/apps-engine/definition packages/apps-engine/definition + COPY ./packages/agenda/package.json packages/agenda/package.json COPY ./packages/agenda/dist packages/agenda/dist diff --git a/ee/apps/presence-service/Dockerfile b/ee/apps/presence-service/Dockerfile index 430880d29606..78c6a98f809a 100644 --- a/ee/apps/presence-service/Dockerfile +++ b/ee/apps/presence-service/Dockerfile @@ -13,6 +13,10 @@ COPY ./packages/agenda/dist packages/agenda/dist COPY ./packages/core-services/package.json packages/core-services/package.json COPY ./packages/core-services/dist packages/core-services/dist +COPY ./packages/apps-engine/package.json packages/apps-engine/package.json +COPY ./packages/apps-engine/client packages/apps-engine/client +COPY ./packages/apps-engine/definition packages/apps-engine/definition + COPY ./packages/core-typings/package.json packages/core-typings/package.json COPY ./packages/core-typings/dist packages/core-typings/dist diff --git a/ee/apps/queue-worker/Dockerfile b/ee/apps/queue-worker/Dockerfile index 9b7e47968e68..0f18534e1453 100644 --- a/ee/apps/queue-worker/Dockerfile +++ b/ee/apps/queue-worker/Dockerfile @@ -7,6 +7,10 @@ WORKDIR /app COPY ./packages/core-services/package.json packages/core-services/package.json COPY ./packages/core-services/dist packages/core-services/dist +COPY ./packages/apps-engine/package.json packages/apps-engine/package.json +COPY ./packages/apps-engine/client packages/apps-engine/client +COPY ./packages/apps-engine/definition packages/apps-engine/definition + COPY ./packages/agenda/package.json packages/agenda/package.json COPY ./packages/agenda/dist packages/agenda/dist diff --git a/ee/apps/stream-hub-service/Dockerfile b/ee/apps/stream-hub-service/Dockerfile index 9a9e8ded922c..9ddeadd380fe 100644 --- a/ee/apps/stream-hub-service/Dockerfile +++ b/ee/apps/stream-hub-service/Dockerfile @@ -7,6 +7,10 @@ WORKDIR /app COPY ./packages/core-services/package.json packages/core-services/package.json COPY ./packages/core-services/dist packages/core-services/dist +COPY ./packages/apps-engine/package.json packages/apps-engine/package.json +COPY ./packages/apps-engine/client packages/apps-engine/client +COPY ./packages/apps-engine/definition packages/apps-engine/definition + COPY ./packages/agenda/package.json packages/agenda/package.json COPY ./packages/agenda/dist packages/agenda/dist diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index 912b4bf453fd..06bbc244fd48 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@babel/preset-env": "~7.22.20", "@babel/preset-typescript": "~7.22.15", - "@rocket.chat/apps-engine": "1.45.0-alpha.868", + "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@types/node": "^14.18.63", diff --git a/packages/apps-engine/.eslintignore b/packages/apps-engine/.eslintignore new file mode 100644 index 000000000000..f7e4e0b38e59 --- /dev/null +++ b/packages/apps-engine/.eslintignore @@ -0,0 +1,8 @@ +!/gulpfile.js +/client +/definition +/docs +/server +/lib +/deno-runtime +/.deno diff --git a/packages/apps-engine/.eslintrc.json b/packages/apps-engine/.eslintrc.json new file mode 100644 index 000000000000..8e42d7cfa9f6 --- /dev/null +++ b/packages/apps-engine/.eslintrc.json @@ -0,0 +1,58 @@ +{ + "extends": "@rocket.chat/eslint-config", + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig-lint.json" + }, + "rules": { + "@typescript-eslint/ban-types": [ + "error", + { + "types": { + "{}": false + } + } + ], + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": ["function", "parameter", "variable"], + "modifiers": ["destructured"], + "format": null + }, + { + "selector": ["variable"], + "format": ["camelCase", "UPPER_CASE", "PascalCase"], + "leadingUnderscore": "allowSingleOrDouble" + }, + { + "selector": ["function"], + "format": ["camelCase", "PascalCase"], + "leadingUnderscore": "allowSingleOrDouble" + }, + { + "selector": ["parameter"], + "format": ["camelCase"], + "leadingUnderscore": "allow" + }, + { + "selector": ["parameter"], + "format": ["camelCase"], + "modifiers": ["unused"], + "leadingUnderscore": "allow" + }, + { + "selector": ["interface"], + "format": ["PascalCase"], + "custom": { + "regex": "^I[A-Z]", + "match": true + } + } + ], + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], + "new-cap": "off", + "no-await-in-loop": "off" + } +} diff --git a/packages/apps-engine/.gitignore b/packages/apps-engine/.gitignore new file mode 100644 index 000000000000..c4568f9c7430 --- /dev/null +++ b/packages/apps-engine/.gitignore @@ -0,0 +1,61 @@ +# Created by https://www.gitignore.io/api/node + +### Node ### +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +.deno +.deno-cache + +# Optional REPL history +.node_repl_history + +### Typings ### +## Ignore downloaded typings +/typings + +## dev environment stuff +/examples/ +/dev-dist/ +/data/ +/tests/test-data/dbs +/client +/definition +/server +/lib + +.DS_Store +.idea/ diff --git a/packages/apps-engine/.prettierrc b/packages/apps-engine/.prettierrc new file mode 100644 index 000000000000..9b77117b35c0 --- /dev/null +++ b/packages/apps-engine/.prettierrc @@ -0,0 +1,7 @@ +{ + "tabWidth": 4, + "useTabs": false, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 160 +} diff --git a/packages/apps-engine/README.md b/packages/apps-engine/README.md new file mode 100644 index 000000000000..d73e09f1aed7 --- /dev/null +++ b/packages/apps-engine/README.md @@ -0,0 +1,125 @@ +## Thoughts While Working (for docs) +- Apps which don't provide a valid uuid4 id will be assigned one, but this is not recommended and your App should provide an id +- The language strings are only done on the clients (`TAPi18next.addResourceBundle(lang, projectName, translations);`) +- The implementer of this should restrict the server setting access and environmental variables. Idea is to allow the implementer to have a default set of restricted ones while letting the admin/owner of the server to restrict it even further or lift the restriction on some more. Simple interface with settings and checkbox to allow/disallow them. :thinking: + +## What does the Apps-Engine enable you to do? +The Apps-Engine is Rocket.Chat's _plugin framework_ - it provides the APIs for Rocket.Chat Apps to interact with the host system. + +Currently, a Rocket.Chat App can: +- Listen to message events + - before/after sent + - before/after updated + - before/after deleted +- Listen to room events + - before/after created + - before/after deleted +- Send messages to users and livechat visitors +- Register new slash commands +- Register new HTTP endpoints + +Some features the Engine allows Apps to use: +- Key-Value Storage system +- App specific settings + +## Development environment with Rocket.Chat +When developing new functionalities, you need to integrate the local version of the Apps-Engine with your local version of Rocket.Chat. + +First of all, make sure you've installed all required packages and compiled the changes you've made to the Apps-Engine, since that is what Rocket.Chat will execute: +```sh +npm install +npm run compile +``` + +Now, you need to setup a local Rocket.Chat server, [so head to the project's README for instructions on getting started](https://github.com/RocketChat/Rocket.Chat#development) (if you haven't already). Make sure to actually clone the repo, since you will probably need to add some code to it in order to make your new functionality work. + +After that, `cd` into Rocket.Chat folder and run: +```sh +meteor npm install PATH_TO_APPS_ENGINE +``` + +Where `PATH_TO_APPS_ENGINE` is the path to the Apps-Engine repo you've cloned. + +That's it! Now when you start Rocket.Chat with the `meteor` command, it will use your local Apps-Engine instead of the one on NPM :) + +Whenever you make changes to the engine, run `npm run compile` again - meteor will take care of restarting the server due to the changes. + +## Troubleshooting +1. Sometimes, when you update the Apps-Engine code and compile it while Rocket.Chat is running, you might run on errors similar to these: + +``` +Unable to resolve some modules: + + "@rocket.chat/apps-engine/definition/AppStatus" in +/Users/dev/rocket.chat/Rocket.Chat/app/apps/client/admin/helpers.js (web.browser) + +If you notice problems related to these missing modules, consider running: + + meteor npm install --save @rocket.chat/apps-engine +``` + +Simply restart the meteor process and it should be fixed. + +2. Sometimes when using `meteor npm install PATH_TO_APPS_ENGINE` will cause the following error :- + +``` +npm ERR! code ENOENT +npm ERR! syscall rename +npm ERR! path PATH_TO_ROCKETCHAT/node_modules/.staging/@rocket.chat/apps-engine-c7135600/node_modules/@babel/code-frame +npm ERR! dest PATH_TO_ROCKETCHAT/node_modules/.staging/@babel/code-frame-f3697825 +npm ERR! errno -2 +npm ERR! enoent ENOENT: no such file or directory, rename 'PATH_TO_ROCKETCHAT/node_modules/.staging/@rocket.chat/apps-engine-c7135600/node_modules/@babel/code-frame' -> 'PATH_TO_ROCKETCHAT/node_modules/.staging/@babel/code-frame-f3697825' +npm ERR! enoent This is related to npm not being able to find a file. +npm ERR! enoent +``` +Here `PATH_TO_ROCKETCHAT` is the path to the main rocketchat server repo in your system +To correct this we reinstall the package once again deleting the previous package +``` +~/Rocket.Chat$ rm -rf node_modules/@rocket.chat/apps-engine +~/Rocket.Chat$ cd PATH_TO_APP_ENGINE +~/Rocket.Chat.Apps-engine$ npm install +~/Rocket.Chat.Apps-engine$ cd PATH_TO_ROCKETCHAT +~/Rocket.Chat$ meteor npm install ../Rocket.Chat.Apps-engine +``` + +## Implementer Needs to Implement: +- `src/server/storage/AppStorage` +- `src/server/storage/AppLogStorage` +- `src/server/bridges/*` + +## Testing Framework: +Makes great usage of TypeScript and decorators: https://github.com/alsatian-test/alsatian/wiki +* To run the tests do: `npm run unit-tests` +* To generate the coverage information: `npm run check-coverage` +* To view the coverage: `npm run view-coverage` + +# Rocket.Chat Apps TypeScript Definitions + +## Handlers +Handlers are essentially "listeners" for different events, except there are various ways to handle an event. +When something happens there is `pre` and `post` handlers. +The set of `pre` handlers happens before the event is finalized. +The set of `post` handlers happens after the event is finalized. +With that said, the rule of thumb is that if you are going to modify, extend, or change the data backing the event then that should be done in the `pre` handlers. If you are simply wanting to listen for when something happens and not modify anything, then the `post` is the way to go. + +The order in which they happen is: +* Pre**Event**Prevent +* Pre**Event**Extend +* Pre**Event**Modify +* Post**Event** + +Here is an explanation of what each of them means: +* **Prevent**: This is ran to determine whether the event should be prevented or not. +* **Extend**: This is ran to allow extending the data without being destructive of the data (adding an attachment to a message for example). +* **Modify**: This is ran and allows for destructive changes to the data (change any and everything). +* Post**Event**: Is mostly for simple listening and no changes can be made to the data. + +## Generating/Updating Documentation +To update or generate the documentation, please commit your changes first and then in a second commit provide the updated documentation. + +# Engage with us +## Share your story +We’d love to hear about [your experience](https://survey.zohopublic.com/zs/e4BUFG) and potentially feature it on our [Blog](https://rocket.chat/case-studies/?utm_source=github&utm_medium=readme&utm_campaign=community). + +## Subscribe for Updates +Once a month our marketing team releases an email update with news about product releases, company related topics, events and use cases. [Sign Up!](https://rocket.chat/newsletter/?utm_source=github&utm_medium=readme&utm_campaign=community) diff --git a/packages/apps-engine/deno-runtime/.gitignore b/packages/apps-engine/deno-runtime/.gitignore new file mode 100644 index 000000000000..5942ea3a153e --- /dev/null +++ b/packages/apps-engine/deno-runtime/.gitignore @@ -0,0 +1 @@ +.deno/ diff --git a/packages/apps-engine/deno-runtime/AppObjectRegistry.ts b/packages/apps-engine/deno-runtime/AppObjectRegistry.ts new file mode 100644 index 000000000000..9069c17eaac5 --- /dev/null +++ b/packages/apps-engine/deno-runtime/AppObjectRegistry.ts @@ -0,0 +1,26 @@ +export type Maybe = T | null | undefined; + +export const AppObjectRegistry = new class { + registry: Record = {}; + + public get(key: string): Maybe { + return this.registry[key] as Maybe; + } + + public set(key: string, value: unknown): void { + this.registry[key] = value; + } + + public has(key: string): boolean { + return key in this.registry; + } + + public delete(key: string): void { + delete this.registry[key]; + } + + public clear(): void { + this.registry = {}; + } +} + diff --git a/packages/apps-engine/deno-runtime/acorn-walk.d.ts b/packages/apps-engine/deno-runtime/acorn-walk.d.ts new file mode 100644 index 000000000000..25861f3bce0f --- /dev/null +++ b/packages/apps-engine/deno-runtime/acorn-walk.d.ts @@ -0,0 +1,170 @@ +import type acorn from "./acorn.d.ts"; + +export type FullWalkerCallback = ( + node: acorn.AnyNode, + state: TState, + type: string +) => void + +export type FullAncestorWalkerCallback = ( + node: acorn.AnyNode, + state: TState, + ancestors: acorn.AnyNode[], + type: string +) => void + +type AggregateType = { + Expression: acorn.Expression, + Statement: acorn.Statement, + Pattern: acorn.Pattern, + ForInit: acorn.VariableDeclaration | acorn.Expression +} + +export type SimpleVisitors = { + [type in acorn.AnyNode["type"]]?: (node: Extract, state: TState) => void +} & { + [type in keyof AggregateType]?: (node: AggregateType[type], state: TState) => void +} + +export type AncestorVisitors = { + [type in acorn.AnyNode["type"]]?: ( node: Extract, state: TState, ancestors: acorn.Node[] +) => void +} & { + [type in keyof AggregateType]?: (node: AggregateType[type], state: TState, ancestors: acorn.Node[]) => void +} + +export type WalkerCallback = (node: acorn.Node, state: TState) => void + +export type RecursiveVisitors = { + [type in acorn.AnyNode["type"]]?: ( node: Extract, state: TState, callback: WalkerCallback) => void +} & { + [type in keyof AggregateType]?: (node: AggregateType[type], state: TState, callback: WalkerCallback) => void +} + +export type FindPredicate = (type: string, node: acorn.Node) => boolean + +export interface Found { + node: acorn.Node, + state: TState +} + +/** + * does a 'simple' walk over a tree + * @param node the AST node to walk + * @param visitors an object with properties whose names correspond to node types in the {@link https://github.com/estree/estree | ESTree spec}. The properties should contain functions that will be called with the node object and, if applicable the state at that point. + * @param base a walker algorithm + * @param state a start state. The default walker will simply visit all statements and expressions and not produce a meaningful state. (An example of a use of state is to track scope at each point in the tree.) + */ +export function simple( + node: acorn.Node, + visitors: SimpleVisitors, + base?: RecursiveVisitors, + state?: TState +): void + +/** + * does a 'simple' walk over a tree, building up an array of ancestor nodes (including the current node) and passing the array to the callbacks as a third parameter. + * @param node + * @param visitors + * @param base + * @param state + */ +export function ancestor( + node: acorn.Node, + visitors: AncestorVisitors, + base?: RecursiveVisitors, + state?: TState + ): void + +/** + * does a 'recursive' walk, where the walker functions are responsible for continuing the walk on the child nodes of their target node. + * @param node + * @param state the start state + * @param functions contain an object that maps node types to walker functions + * @param base provides the fallback walker functions for node types that aren't handled in the {@link functions} object. If not given, the default walkers will be used. + */ +export function recursive( + node: acorn.Node, + state: TState, + functions: RecursiveVisitors, + base?: RecursiveVisitors +): void + +/** + * does a 'full' walk over a tree, calling the {@link callback} with the arguments (node, state, type) for each node + * @param node + * @param callback + * @param base + * @param state + */ +export function full( + node: acorn.Node, + callback: FullWalkerCallback, + base?: RecursiveVisitors, + state?: TState +): void + +/** + * does a 'full' walk over a tree, building up an array of ancestor nodes (including the current node) and passing the array to the callbacks as a third parameter. + * @param node + * @param callback + * @param base + * @param state + */ +export function fullAncestor( + node: acorn.AnyNode, + callback: FullAncestorWalkerCallback, + base?: RecursiveVisitors, + state?: TState +): void + +/** + * builds a new walker object by using the walker functions in {@link functions} and filling in the missing ones by taking defaults from {@link base}. + * @param functions + * @param base + */ +export function make( + functions: RecursiveVisitors, + base?: RecursiveVisitors +): RecursiveVisitors + +/** + * tries to locate a node in a tree at the given start and/or end offsets, which satisfies the predicate test. {@link start} and {@link end} can be either `null` (as wildcard) or a `number`. {@link test} may be a string (indicating a node type) or a function that takes (nodeType, node) arguments and returns a boolean indicating whether this node is interesting. {@link base} and {@link state} are optional, and can be used to specify a custom walker. Nodes are tested from inner to outer, so if two nodes match the boundaries, the inner one will be preferred. + * @param node + * @param start + * @param end + * @param type + * @param base + * @param state + */ +export function findNodeAt( + node: acorn.AnyNode, + start: number | undefined, + end?: number | undefined, + type?: FindPredicate | string, + base?: RecursiveVisitors, + state?: TState +): Found | undefined + +/** + * like {@link findNodeAt}, but will match any node that exists 'around' (spanning) the given position. + * @param node + * @param start + * @param type + * @param base + * @param state + */ +export function findNodeAround( + node: acorn.AnyNode, + start: number | undefined, + type?: FindPredicate | string, + base?: RecursiveVisitors, + state?: TState +): Found | undefined + +/** + * similar to {@link findNodeAround}, but will match all nodes after the given position (testing outer nodes before inner nodes). + */ +export const findNodeAfter: typeof findNodeAround + +export const base: RecursiveVisitors diff --git a/packages/apps-engine/deno-runtime/acorn.d.ts b/packages/apps-engine/deno-runtime/acorn.d.ts new file mode 100644 index 000000000000..0b5bc6b407b2 --- /dev/null +++ b/packages/apps-engine/deno-runtime/acorn.d.ts @@ -0,0 +1,857 @@ +export interface Node { + start?: number + end?: number + type: string + range?: [number, number] + loc?: SourceLocation | null +} + +export interface SourceLocation { + source?: string | null + start: Position + end: Position +} + +export interface Position { + /** 1-based */ + line: number + /** 0-based */ + column: number +} + +export interface Identifier extends Node { + type: "Identifier" + name: string +} + +export interface Literal extends Node { + type: "Literal" + value?: string | boolean | null | number | RegExp | bigint + raw?: string + regex?: { + pattern: string + flags: string + } + bigint?: string +} + +export interface Program extends Node { + type: "Program" + body: Array + sourceType: "script" | "module" +} + +export interface Function extends Node { + id?: Identifier | null + params: Array + body: BlockStatement | Expression + generator: boolean + expression: boolean + async: boolean +} + +export interface ExpressionStatement extends Node { + type: "ExpressionStatement" + expression: Expression | Literal + directive?: string +} + +export interface BlockStatement extends Node { + type: "BlockStatement" + body: Array +} + +export interface EmptyStatement extends Node { + type: "EmptyStatement" +} + +export interface DebuggerStatement extends Node { + type: "DebuggerStatement" +} + +export interface WithStatement extends Node { + type: "WithStatement" + object: Expression + body: Statement +} + +export interface ReturnStatement extends Node { + type: "ReturnStatement" + argument?: Expression | null +} + +export interface LabeledStatement extends Node { + type: "LabeledStatement" + label: Identifier + body: Statement +} + +export interface BreakStatement extends Node { + type: "BreakStatement" + label?: Identifier | null +} + +export interface ContinueStatement extends Node { + type: "ContinueStatement" + label?: Identifier | null +} + +export interface IfStatement extends Node { + type: "IfStatement" + test: Expression + consequent: Statement + alternate?: Statement | null +} + +export interface SwitchStatement extends Node { + type: "SwitchStatement" + discriminant: Expression + cases: Array +} + +export interface SwitchCase extends Node { + type: "SwitchCase" + test?: Expression | null + consequent: Array +} + +export interface ThrowStatement extends Node { + type: "ThrowStatement" + argument: Expression +} + +export interface TryStatement extends Node { + type: "TryStatement" + block: BlockStatement + handler?: CatchClause | null + finalizer?: BlockStatement | null +} + +export interface CatchClause extends Node { + type: "CatchClause" + param?: Pattern | null + body: BlockStatement +} + +export interface WhileStatement extends Node { + type: "WhileStatement" + test: Expression + body: Statement +} + +export interface DoWhileStatement extends Node { + type: "DoWhileStatement" + body: Statement + test: Expression +} + +export interface ForStatement extends Node { + type: "ForStatement" + init?: VariableDeclaration | Expression | null + test?: Expression | null + update?: Expression | null + body: Statement +} + +export interface ForInStatement extends Node { + type: "ForInStatement" + left: VariableDeclaration | Pattern + right: Expression + body: Statement +} + +export interface FunctionDeclaration extends Function { + type: "FunctionDeclaration" + id: Identifier + body: BlockStatement +} + +export interface VariableDeclaration extends Node { + type: "VariableDeclaration" + declarations: Array + kind: "var" | "let" | "const" +} + +export interface VariableDeclarator extends Node { + type: "VariableDeclarator" + id: Pattern + init?: Expression | null +} + +export interface ThisExpression extends Node { + type: "ThisExpression" +} + +export interface ArrayExpression extends Node { + type: "ArrayExpression" + elements: Array +} + +export interface ObjectExpression extends Node { + type: "ObjectExpression" + properties: Array +} + +export interface Property extends Node { + type: "Property" + key: Expression + value: Expression + kind: "init" | "get" | "set" + method: boolean + shorthand: boolean + computed: boolean +} + +export interface FunctionExpression extends Function { + type: "FunctionExpression" + body: BlockStatement +} + +export interface UnaryExpression extends Node { + type: "UnaryExpression" + operator: UnaryOperator + prefix: boolean + argument: Expression +} + +export type UnaryOperator = "-" | "+" | "!" | "~" | "typeof" | "void" | "delete" + +export interface UpdateExpression extends Node { + type: "UpdateExpression" + operator: UpdateOperator + argument: Expression + prefix: boolean +} + +export type UpdateOperator = "++" | "--" + +export interface BinaryExpression extends Node { + type: "BinaryExpression" + operator: BinaryOperator + left: Expression | PrivateIdentifier + right: Expression +} + +export type BinaryOperator = "==" | "!=" | "===" | "!==" | "<" | "<=" | ">" | ">=" | "<<" | ">>" | ">>>" | "+" | "-" | "*" | "/" | "%" | "|" | "^" | "&" | "in" | "instanceof" | "**" + +export interface AssignmentExpression extends Node { + type: "AssignmentExpression" + operator: AssignmentOperator + left: Pattern + right: Expression +} + +export type AssignmentOperator = "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "<<=" | ">>=" | ">>>=" | "|=" | "^=" | "&=" | "**=" | "||=" | "&&=" | "??=" + +export interface LogicalExpression extends Node { + type: "LogicalExpression" + operator: LogicalOperator + left: Expression + right: Expression +} + +export type LogicalOperator = "||" | "&&" | "??" + +export interface MemberExpression extends Node { + type: "MemberExpression" + object: Expression | Super + property: Expression | PrivateIdentifier + computed: boolean + optional: boolean +} + +export interface ConditionalExpression extends Node { + type: "ConditionalExpression" + test: Expression + alternate: Expression + consequent: Expression +} + +export interface CallExpression extends Node { + type: "CallExpression" + callee: Expression | Super + arguments: Array + optional: boolean +} + +export interface NewExpression extends Node { + type: "NewExpression" + callee: Expression + arguments: Array +} + +export interface SequenceExpression extends Node { + type: "SequenceExpression" + expressions: Array +} + +export interface ForOfStatement extends Node { + type: "ForOfStatement" + left: VariableDeclaration | Pattern + right: Expression + body: Statement + await: boolean +} + +export interface Super extends Node { + type: "Super" +} + +export interface SpreadElement extends Node { + type: "SpreadElement" + argument: Expression +} + +export interface ArrowFunctionExpression extends Function { + type: "ArrowFunctionExpression" +} + +export interface YieldExpression extends Node { + type: "YieldExpression" + argument?: Expression | null + delegate: boolean +} + +export interface TemplateLiteral extends Node { + type: "TemplateLiteral" + quasis: Array + expressions: Array +} + +export interface TaggedTemplateExpression extends Node { + type: "TaggedTemplateExpression" + tag: Expression + quasi: TemplateLiteral +} + +export interface TemplateElement extends Node { + type: "TemplateElement" + tail: boolean + value: { + cooked?: string | null + raw: string + } +} + +export interface AssignmentProperty extends Node { + type: "Property" + key: Expression + value: Pattern + kind: "init" + method: false + shorthand: boolean + computed: boolean +} + +export interface ObjectPattern extends Node { + type: "ObjectPattern" + properties: Array +} + +export interface ArrayPattern extends Node { + type: "ArrayPattern" + elements: Array +} + +export interface RestElement extends Node { + type: "RestElement" + argument: Pattern +} + +export interface AssignmentPattern extends Node { + type: "AssignmentPattern" + left: Pattern + right: Expression +} + +export interface Class extends Node { + id?: Identifier | null + superClass?: Expression | null + body: ClassBody +} + +export interface ClassBody extends Node { + type: "ClassBody" + body: Array +} + +export interface MethodDefinition extends Node { + type: "MethodDefinition" + key: Expression | PrivateIdentifier + value: FunctionExpression + kind: "constructor" | "method" | "get" | "set" + computed: boolean + static: boolean +} + +export interface ClassDeclaration extends Class { + type: "ClassDeclaration" + id: Identifier +} + +export interface ClassExpression extends Class { + type: "ClassExpression" +} + +export interface MetaProperty extends Node { + type: "MetaProperty" + meta: Identifier + property: Identifier +} + +export interface ImportDeclaration extends Node { + type: "ImportDeclaration" + specifiers: Array + source: Literal +} + +export interface ImportSpecifier extends Node { + type: "ImportSpecifier" + imported: Identifier | Literal + local: Identifier +} + +export interface ImportDefaultSpecifier extends Node { + type: "ImportDefaultSpecifier" + local: Identifier +} + +export interface ImportNamespaceSpecifier extends Node { + type: "ImportNamespaceSpecifier" + local: Identifier +} + +export interface ExportNamedDeclaration extends Node { + type: "ExportNamedDeclaration" + declaration?: Declaration | null + specifiers: Array + source?: Literal | null +} + +export interface ExportSpecifier extends Node { + type: "ExportSpecifier" + exported: Identifier | Literal + local: Identifier | Literal +} + +export interface AnonymousFunctionDeclaration extends Function { + type: "FunctionDeclaration" + id: null + body: BlockStatement +} + +export interface AnonymousClassDeclaration extends Class { + type: "ClassDeclaration" + id: null +} + +export interface ExportDefaultDeclaration extends Node { + type: "ExportDefaultDeclaration" + declaration: AnonymousFunctionDeclaration | FunctionDeclaration | AnonymousClassDeclaration | ClassDeclaration | Expression +} + +export interface ExportAllDeclaration extends Node { + type: "ExportAllDeclaration" + source: Literal + exported?: Identifier | Literal | null +} + +export interface AwaitExpression extends Node { + type: "AwaitExpression" + argument: Expression +} + +export interface ChainExpression extends Node { + type: "ChainExpression" + expression: MemberExpression | CallExpression +} + +export interface ImportExpression extends Node { + type: "ImportExpression" + source: Expression +} + +export interface ParenthesizedExpression extends Node { + type: "ParenthesizedExpression" + expression: Expression +} + +export interface PropertyDefinition extends Node { + type: "PropertyDefinition" + key: Expression | PrivateIdentifier + value?: Expression | null + computed: boolean + static: boolean +} + +export interface PrivateIdentifier extends Node { + type: "PrivateIdentifier" + name: string +} + +export interface StaticBlock extends Node { + type: "StaticBlock" + body: Array +} + +export type Statement = +| ExpressionStatement +| BlockStatement +| EmptyStatement +| DebuggerStatement +| WithStatement +| ReturnStatement +| LabeledStatement +| BreakStatement +| ContinueStatement +| IfStatement +| SwitchStatement +| ThrowStatement +| TryStatement +| WhileStatement +| DoWhileStatement +| ForStatement +| ForInStatement +| ForOfStatement +| Declaration + +export type Declaration = +| FunctionDeclaration +| VariableDeclaration +| ClassDeclaration + +export type Expression = +| Identifier +| Literal +| ThisExpression +| ArrayExpression +| ObjectExpression +| FunctionExpression +| UnaryExpression +| UpdateExpression +| BinaryExpression +| AssignmentExpression +| LogicalExpression +| MemberExpression +| ConditionalExpression +| CallExpression +| NewExpression +| SequenceExpression +| ArrowFunctionExpression +| YieldExpression +| TemplateLiteral +| TaggedTemplateExpression +| ClassExpression +| MetaProperty +| AwaitExpression +| ChainExpression +| ImportExpression +| ParenthesizedExpression + +export type Pattern = +| Identifier +| MemberExpression +| ObjectPattern +| ArrayPattern +| RestElement +| AssignmentPattern + +export type ModuleDeclaration = +| ImportDeclaration +| ExportNamedDeclaration +| ExportDefaultDeclaration +| ExportAllDeclaration + +export type AnyNode = Statement | Expression | Declaration | ModuleDeclaration | Literal | Program | SwitchCase | CatchClause | Property | Super | SpreadElement | TemplateElement | AssignmentProperty | ObjectPattern | ArrayPattern | RestElement | AssignmentPattern | ClassBody | MethodDefinition | MetaProperty | ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier | ExportSpecifier | AnonymousFunctionDeclaration | AnonymousClassDeclaration | PropertyDefinition | PrivateIdentifier | StaticBlock | VariableDeclaration | VariableDeclarator + +export function parse(input: string, options: Options): Program + +export function parseExpressionAt(input: string, pos: number, options: Options): Expression + +export function tokenizer(input: string, options: Options): { + getToken(): Token + [Symbol.iterator](): Iterator +} + +export type ecmaVersion = 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | "latest" + +export interface Options { + /** + * `ecmaVersion` indicates the ECMAScript version to parse. Must be + * either 3, 5, 6 (or 2015), 7 (2016), 8 (2017), 9 (2018), 10 + * (2019), 11 (2020), 12 (2021), 13 (2022), 14 (2023), or `"latest"` + * (the latest version the library supports). This influences + * support for strict mode, the set of reserved words, and support + * for new syntax features. + */ + ecmaVersion: ecmaVersion + + /** + * `sourceType` indicates the mode the code should be parsed in. + * Can be either `"script"` or `"module"`. This influences global + * strict mode and parsing of `import` and `export` declarations. + */ + sourceType?: "script" | "module" + + /** + * a callback that will be called when a semicolon is automatically inserted. + * @param lastTokEnd the position of the comma as an offset + * @param lastTokEndLoc location if {@link locations} is enabled + */ + onInsertedSemicolon?: (lastTokEnd: number, lastTokEndLoc?: Position) => void + + /** + * similar to `onInsertedSemicolon`, but for trailing commas + * @param lastTokEnd the position of the comma as an offset + * @param lastTokEndLoc location if `locations` is enabled + */ + onTrailingComma?: (lastTokEnd: number, lastTokEndLoc?: Position) => void + + /** + * By default, reserved words are only enforced if ecmaVersion >= 5. + * Set `allowReserved` to a boolean value to explicitly turn this on + * an off. When this option has the value "never", reserved words + * and keywords can also not be used as property names. + */ + allowReserved?: boolean | "never" + + /** + * When enabled, a return at the top level is not considered an error. + */ + allowReturnOutsideFunction?: boolean + + /** + * When enabled, import/export statements are not constrained to + * appearing at the top of the program, and an import.meta expression + * in a script isn't considered an error. + */ + allowImportExportEverywhere?: boolean + + /** + * By default, `await` identifiers are allowed to appear at the top-level scope only if {@link ecmaVersion} >= 2022. + * When enabled, await identifiers are allowed to appear at the top-level scope, + * but they are still not allowed in non-async functions. + */ + allowAwaitOutsideFunction?: boolean + + /** + * When enabled, super identifiers are not constrained to + * appearing in methods and do not raise an error when they appear elsewhere. + */ + allowSuperOutsideMethod?: boolean + + /** + * When enabled, hashbang directive in the beginning of file is + * allowed and treated as a line comment. Enabled by default when + * {@link ecmaVersion} >= 2023. + */ + allowHashBang?: boolean + + /** + * By default, the parser will verify that private properties are + * only used in places where they are valid and have been declared. + * Set this to false to turn such checks off. + */ + checkPrivateFields?: boolean + + /** + * When `locations` is on, `loc` properties holding objects with + * `start` and `end` properties as {@link Position} objects will be attached to the + * nodes. + */ + locations?: boolean + + /** + * a callback that will cause Acorn to call that export function with object in the same + * format as tokens returned from `tokenizer().getToken()`. Note + * that you are not allowed to call the parser from the + * callback—that will corrupt its internal state. + */ + onToken?: ((token: Token) => void) | Token[] + + + /** + * This takes a export function or an array. + * + * When a export function is passed, Acorn will call that export function with `(block, text, start, + * end)` parameters whenever a comment is skipped. `block` is a + * boolean indicating whether this is a block (`/* *\/`) comment, + * `text` is the content of the comment, and `start` and `end` are + * character offsets that denote the start and end of the comment. + * When the {@link locations} option is on, two more parameters are + * passed, the full locations of {@link Position} export type of the start and + * end of the comments. + * + * When a array is passed, each found comment of {@link Comment} export type is pushed to the array. + * + * Note that you are not allowed to call the + * parser from the callback—that will corrupt its internal state. + */ + onComment?: (( + isBlock: boolean, text: string, start: number, end: number, startLoc?: Position, + endLoc?: Position + ) => void) | Comment[] + + /** + * Nodes have their start and end characters offsets recorded in + * `start` and `end` properties (directly on the node, rather than + * the `loc` object, which holds line/column data. To also add a + * [semi-standardized][range] `range` property holding a `[start, + * end]` array with the same numbers, set the `ranges` option to + * `true`. + */ + ranges?: boolean + + /** + * It is possible to parse multiple files into a single AST by + * passing the tree produced by parsing the first file as + * `program` option in subsequent parses. This will add the + * toplevel forms of the parsed file to the `Program` (top) node + * of an existing parse tree. + */ + program?: Node + + /** + * When {@link locations} is on, you can pass this to record the source + * file in every node's `loc` object. + */ + sourceFile?: string + + /** + * This value, if given, is stored in every node, whether {@link locations} is on or off. + */ + directSourceFile?: string + + /** + * When enabled, parenthesized expressions are represented by + * (non-standard) ParenthesizedExpression nodes + */ + preserveParens?: boolean +} + +export class Parser { + options: Options + input: string + + constructor(options: Options, input: string, startPos?: number) + parse(): Program + + static parse(input: string, options: Options): Program + static parseExpressionAt(input: string, pos: number, options: Options): Expression + static tokenizer(input: string, options: Options): { + getToken(): Token + [Symbol.iterator](): Iterator + } + static extend(...plugins: ((BaseParser: typeof Parser) => typeof Parser)[]): typeof Parser +} + +export const defaultOptions: Options + +export function getLineInfo(input: string, offset: number): Position + +export class TokenType { + label: string + keyword: string | undefined +} + +export const tokTypes: { + num: TokenType + regexp: TokenType + string: TokenType + name: TokenType + privateId: TokenType + eof: TokenType + + bracketL: TokenType + bracketR: TokenType + braceL: TokenType + braceR: TokenType + parenL: TokenType + parenR: TokenType + comma: TokenType + semi: TokenType + colon: TokenType + dot: TokenType + question: TokenType + questionDot: TokenType + arrow: TokenType + template: TokenType + invalidTemplate: TokenType + ellipsis: TokenType + backQuote: TokenType + dollarBraceL: TokenType + + eq: TokenType + assign: TokenType + incDec: TokenType + prefix: TokenType + logicalOR: TokenType + logicalAND: TokenType + bitwiseOR: TokenType + bitwiseXOR: TokenType + bitwiseAND: TokenType + equality: TokenType + relational: TokenType + bitShift: TokenType + plusMin: TokenType + modulo: TokenType + star: TokenType + slash: TokenType + starstar: TokenType + coalesce: TokenType + + _break: TokenType + _case: TokenType + _catch: TokenType + _continue: TokenType + _debugger: TokenType + _default: TokenType + _do: TokenType + _else: TokenType + _finally: TokenType + _for: TokenType + _function: TokenType + _if: TokenType + _return: TokenType + _switch: TokenType + _throw: TokenType + _try: TokenType + _var: TokenType + _const: TokenType + _while: TokenType + _with: TokenType + _new: TokenType + _this: TokenType + _super: TokenType + _class: TokenType + _extends: TokenType + _export: TokenType + _import: TokenType + _null: TokenType + _true: TokenType + _false: TokenType + _in: TokenType + _instanceof: TokenType + _typeof: TokenType + _void: TokenType + _delete: TokenType +} + +export interface Comment { + type: "Line" | "Block" + value: string + start: number + end: number + loc?: SourceLocation + range?: [number, number] +} + +export class Token { + type: TokenType + start: number + end: number + loc?: SourceLocation + range?: [number, number] +} + +export const version: string diff --git a/packages/apps-engine/deno-runtime/deno.jsonc b/packages/apps-engine/deno-runtime/deno.jsonc new file mode 100644 index 000000000000..231d0924237a --- /dev/null +++ b/packages/apps-engine/deno-runtime/deno.jsonc @@ -0,0 +1,16 @@ +{ + "imports": { + "@rocket.chat/apps-engine/": "./../src/", + "@rocket.chat/ui-kit": "npm:@rocket.chat/ui-kit@^0.31.22", + "@msgpack/msgpack": "npm:@msgpack/msgpack@3.0.0-beta2", + "acorn": "npm:acorn@8.10.0", + "acorn-walk": "npm:acorn-walk@8.2.0", + "astring": "npm:astring@1.8.6", + "jsonrpc-lite": "npm:jsonrpc-lite@2.2.0", + "stack-trace": "npm:stack-trace@0.0.10", + "uuid": "npm:uuid@8.3.2" + }, + "tasks": { + "test": "deno test --no-check --allow-read=../../../" + } +} diff --git a/packages/apps-engine/deno-runtime/deno.lock b/packages/apps-engine/deno-runtime/deno.lock new file mode 100644 index 000000000000..86cebf98f63a --- /dev/null +++ b/packages/apps-engine/deno-runtime/deno.lock @@ -0,0 +1,107 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "npm:@msgpack/msgpack@3.0.0-beta2": "npm:@msgpack/msgpack@3.0.0-beta2", + "npm:@rocket.chat/ui-kit@^0.31.22": "npm:@rocket.chat/ui-kit@0.31.25_@rocket.chat+icons@0.32.0", + "npm:acorn-walk@8.2.0": "npm:acorn-walk@8.2.0", + "npm:acorn@8.10.0": "npm:acorn@8.10.0", + "npm:astring@1.8.6": "npm:astring@1.8.6", + "npm:jsonrpc-lite@2.2.0": "npm:jsonrpc-lite@2.2.0", + "npm:stack-trace": "npm:stack-trace@0.0.10", + "npm:stack-trace@0.0.10": "npm:stack-trace@0.0.10", + "npm:uuid@8.3.2": "npm:uuid@8.3.2" + }, + "npm": { + "@msgpack/msgpack@3.0.0-beta2": { + "integrity": "sha512-y+l1PNV0XDyY8sM3YtuMLK5vE3/hkfId+Do8pLo/OPxfxuFAUwcGz3oiiUuV46/aBpwTzZ+mRWVMtlSKbradhw==", + "dependencies": {} + }, + "@rocket.chat/icons@0.32.0": { + "integrity": "sha512-7yhhELKNLb9kUtXCvau0V+iMXraV2bOsxcPjc/ZtLR5VeeIDTeaflqRWGtLroX6f3bE+J1n5qB5zi8A4YXuH2g==", + "dependencies": {} + }, + "@rocket.chat/ui-kit@0.31.25_@rocket.chat+icons@0.32.0": { + "integrity": "sha512-yTgTKDw9SMlJ6p8n0PDO6zSvox/nHYUrwCIvILQeAK6PvTrgSe/u9CvU7ATTYjnQiQ603yEGR6dxjF4euCGdNA==", + "dependencies": { + "@rocket.chat/icons": "@rocket.chat/icons@0.32.0" + } + }, + "acorn-walk@8.2.0": { + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dependencies": {} + }, + "acorn@8.10.0": { + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dependencies": {} + }, + "astring@1.8.6": { + "integrity": "sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==", + "dependencies": {} + }, + "jsonrpc-lite@2.2.0": { + "integrity": "sha512-/cbbSxtZWs1O7R4tWqabrCM/t3N8qKUZMAg9IUqpPvUs6UyRvm6pCNYkskyKN/XU0UgffW+NY2ZRr8t0AknX7g==", + "dependencies": {} + }, + "stack-trace@0.0.10": { + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dependencies": {} + }, + "uuid@8.3.2": { + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dependencies": {} + } + } + }, + "remote": { + "https://deno.land/std@0.203.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9", + "https://deno.land/std@0.203.0/assert/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea", + "https://deno.land/std@0.203.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", + "https://deno.land/std@0.203.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", + "https://deno.land/std@0.203.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c", + "https://deno.land/std@0.203.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9", + "https://deno.land/std@0.203.0/assert/assert_equals.ts": "d8ec8a22447fbaf2fc9d7c3ed2e66790fdb74beae3e482855d75782218d68227", + "https://deno.land/std@0.203.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7", + "https://deno.land/std@0.203.0/assert/assert_false.ts": "0ccbcaae910f52c857192ff16ea08bda40fdc79de80846c206bfc061e8c851c6", + "https://deno.land/std@0.203.0/assert/assert_greater.ts": "ae2158a2d19313bf675bf7251d31c6dc52973edb12ac64ac8fc7064152af3e63", + "https://deno.land/std@0.203.0/assert/assert_greater_or_equal.ts": "1439da5ebbe20855446cac50097ac78b9742abe8e9a43e7de1ce1426d556e89c", + "https://deno.land/std@0.203.0/assert/assert_instance_of.ts": "3aedb3d8186e120812d2b3a5dea66a6e42bf8c57a8bd927645770bd21eea554c", + "https://deno.land/std@0.203.0/assert/assert_is_error.ts": "c21113094a51a296ffaf036767d616a78a2ae5f9f7bbd464cd0197476498b94b", + "https://deno.land/std@0.203.0/assert/assert_less.ts": "aec695db57db42ec3e2b62e97e1e93db0063f5a6ec133326cc290ff4b71b47e4", + "https://deno.land/std@0.203.0/assert/assert_less_or_equal.ts": "5fa8b6a3ffa20fd0a05032fe7257bf985d207b85685fdbcd23651b70f928c848", + "https://deno.land/std@0.203.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b", + "https://deno.land/std@0.203.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754", + "https://deno.land/std@0.203.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22", + "https://deno.land/std@0.203.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0", + "https://deno.land/std@0.203.0/assert/assert_not_strict_equals.ts": "ca6c6d645e95fbc873d25320efeb8c4c6089a9a5e09f92d7c1c4b6e935c2a6ad", + "https://deno.land/std@0.203.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54", + "https://deno.land/std@0.203.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057", + "https://deno.land/std@0.203.0/assert/assert_strict_equals.ts": "b1f538a7ea5f8348aeca261d4f9ca603127c665e0f2bbfeb91fa272787c87265", + "https://deno.land/std@0.203.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c", + "https://deno.land/std@0.203.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd", + "https://deno.land/std@0.203.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", + "https://deno.land/std@0.203.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece", + "https://deno.land/std@0.203.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278", + "https://deno.land/std@0.203.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085", + "https://deno.land/std@0.203.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a", + "https://deno.land/std@0.203.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536", + "https://deno.land/std@0.203.0/fmt/colors.ts": "c51c4642678eb690dcf5ffee5918b675bf01a33fba82acf303701ae1a4f8c8d9", + "https://deno.land/std@0.203.0/testing/_test_suite.ts": "30f018feeb3835f12ab198d8a518f9089b1bcb2e8c838a8b615ab10d5005465c", + "https://deno.land/std@0.203.0/testing/bdd.ts": "3f446df5ef8e856a869e8eec54c8482590415741ff0b6358a00c43486cc15769", + "https://deno.land/std@0.203.0/testing/mock.ts": "6576b4aa55ee20b1990d656a78fff83599e190948c00e9f25a7f3ac5e9d6492d", + "https://deno.land/std@0.216.0/io/types.ts": "748bbb3ac96abda03594ef5a0db15ce5450dcc6c0d841c8906f8b10ac8d32c96", + "https://deno.land/std@0.216.0/io/write_all.ts": "24aac2312bb21096ae3ae0b102b22c26164d3249dff96dbac130958aa736f038" + }, + "workspace": { + "dependencies": [ + "npm:@msgpack/msgpack@3.0.0-beta2", + "npm:@rocket.chat/ui-kit@^0.31.22", + "npm:acorn-walk@8.2.0", + "npm:acorn@8.10.0", + "npm:astring@1.8.6", + "npm:jsonrpc-lite@2.2.0", + "npm:stack-trace@0.0.10", + "npm:uuid@8.3.2" + ] + } +} diff --git a/packages/apps-engine/deno-runtime/handlers/api-handler.ts b/packages/apps-engine/deno-runtime/handlers/api-handler.ts new file mode 100644 index 000000000000..32d30e532fd3 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/api-handler.ts @@ -0,0 +1,46 @@ +import { Defined, JsonRpcError } from 'jsonrpc-lite'; +import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api/IApiEndpoint.ts'; + +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; +import { Logger } from '../lib/logger.ts'; +import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; + +export default async function apiHandler(call: string, params: unknown): Promise { + const [, path, httpMethod] = call.split(':'); + + const endpoint = AppObjectRegistry.get(`api:${path}`); + const logger = AppObjectRegistry.get('logger'); + + if (!endpoint) { + return new JsonRpcError(`Endpoint ${path} not found`, -32000); + } + + const method = endpoint[httpMethod as keyof IApiEndpoint]; + + if (typeof method !== 'function') { + return new JsonRpcError(`${path}'s ${httpMethod} not exists`, -32000); + } + + const [request, endpointInfo] = params as Array; + + logger?.debug(`${path}'s ${call} is being executed...`, request); + + try { + // deno-lint-ignore ban-types + const result = await (method as Function).apply(endpoint, [ + request, + endpointInfo, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getModifier(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + ]); + + logger?.debug(`${path}'s ${call} was successfully executed.`); + + return result; + } catch (e) { + logger?.debug(`${path}'s ${call} was unsuccessful.`); + return new JsonRpcError(e.message || "Internal server error", -32000); + } +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/construct.ts b/packages/apps-engine/deno-runtime/handlers/app/construct.ts new file mode 100644 index 000000000000..798a83d0923c --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/construct.ts @@ -0,0 +1,126 @@ +import type { IParseAppPackageResult } from '@rocket.chat/apps-engine/server/compiler/IParseAppPackageResult.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { require } from '../../lib/require.ts'; +import { sanitizeDeprecatedUsage } from '../../lib/sanitizeDeprecatedUsage.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { Socket } from 'node:net'; + +const ALLOWED_NATIVE_MODULES = ['path', 'url', 'crypto', 'buffer', 'stream', 'net', 'http', 'https', 'zlib', 'util', 'punycode', 'os', 'querystring', 'fs']; +const ALLOWED_EXTERNAL_MODULES = ['uuid']; + + +function prepareEnvironment() { + // Deno does not behave equally to Node when it comes to piping content to a socket + // So we intervene here + const originalFinal = Socket.prototype._final; + Socket.prototype._final = function _final(cb) { + // Deno closes the readable stream in the Socket earlier than Node + // The exact reason for that is yet unknown, so we'll need to simply delay the execution + // which allows data to be read in a response + setTimeout(() => originalFinal.call(this, cb), 1); + }; +} + +// As the apps are bundled, the only times they will call require are +// 1. To require native modules +// 2. To require external npm packages we may provide +// 3. To require apps-engine files +function buildRequire(): (module: string) => unknown { + return (module: string): unknown => { + if (ALLOWED_NATIVE_MODULES.includes(module)) { + return require(`node:${module}`); + } + + if (ALLOWED_EXTERNAL_MODULES.includes(module)) { + return require(`npm:${module}`); + } + + if (module.startsWith('@rocket.chat/apps-engine')) { + // Our `require` function knows how to handle these + return require(module); + } + + throw new Error(`Module ${module} is not allowed`); + }; +} + +function wrapAppCode(code: string): (require: (module: string) => unknown) => Promise> { + return new Function( + 'require', + ` + const { Buffer } = require('buffer'); + const exports = {}; + const module = { exports }; + const _error = console.error.bind(console); + const _console = { + log: _error, + error: _error, + debug: _error, + info: _error, + warn: _error, + }; + + const result = (async (exports,module,require,Buffer,console,globalThis,Deno) => { + ${code}; + })(exports,module,require,Buffer,_console,undefined,undefined); + + return result.then(() => module.exports);`, + ) as (require: (module: string) => unknown) => Promise>; +} + +export default async function handleConstructApp(params: unknown): Promise { + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [appPackage] = params as [IParseAppPackageResult]; + + if (!appPackage?.info?.id || !appPackage?.info?.classFile || !appPackage?.files) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + prepareEnvironment(); + + AppObjectRegistry.set('id', appPackage.info.id); + const source = sanitizeDeprecatedUsage(appPackage.files[appPackage.info.classFile]); + + const require = buildRequire(); + const exports = await wrapAppCode(source)(require); + + // This is the same naive logic we've been using in the App Compiler + // Applying the correct type here is quite difficult because of the dynamic nature of the code + // deno-lint-ignore no-explicit-any + const appClass = Object.values(exports)[0] as any; + const logger = AppObjectRegistry.get('logger'); + + const app = new appClass(appPackage.info, logger, AppAccessorsInstance.getDefaultAppAccessors()); + + if (typeof app.getName !== 'function') { + throw new Error('App must contain a getName function'); + } + + if (typeof app.getNameSlug !== 'function') { + throw new Error('App must contain a getNameSlug function'); + } + + if (typeof app.getVersion !== 'function') { + throw new Error('App must contain a getVersion function'); + } + + if (typeof app.getID !== 'function') { + throw new Error('App must contain a getID function'); + } + + if (typeof app.getDescription !== 'function') { + throw new Error('App must contain a getDescription function'); + } + + if (typeof app.getRequiredApiVersion !== 'function') { + throw new Error('App must contain a getRequiredApiVersion function'); + } + + AppObjectRegistry.set('app', app); + + return true; +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleGetStatus.ts b/packages/apps-engine/deno-runtime/handlers/app/handleGetStatus.ts new file mode 100644 index 000000000000..5428d989812e --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleGetStatus.ts @@ -0,0 +1,15 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; + +export default function handleGetStatus(): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.getStatus !== 'function') { + throw new Error('App must contain a getStatus function', { + cause: 'invalid_app', + }); + } + + return app.getStatus(); +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleInitialize.ts b/packages/apps-engine/deno-runtime/handlers/app/handleInitialize.ts new file mode 100644 index 000000000000..ad90d3b01e25 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleInitialize.ts @@ -0,0 +1,19 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; + +export default async function handleInitialize(): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.initialize !== 'function') { + throw new Error('App must contain an initialize function', { + cause: 'invalid_app', + }); + } + + await app.initialize(AppAccessorsInstance.getConfigurationExtend(), AppAccessorsInstance.getEnvironmentRead()); + + return true; +} + diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnDisable.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnDisable.ts new file mode 100644 index 000000000000..e66c2414fd0a --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnDisable.ts @@ -0,0 +1,19 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; + +export default async function handleOnDisable(): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onDisable !== 'function') { + throw new Error('App must contain an onDisable function', { + cause: 'invalid_app', + }); + } + + await app.onDisable(AppAccessorsInstance.getConfigurationModify()); + + return true; +} + diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnEnable.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnEnable.ts new file mode 100644 index 000000000000..1bdf84476422 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnEnable.ts @@ -0,0 +1,16 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; + +export default function handleOnEnable(): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onEnable !== 'function') { + throw new Error('App must contain an onEnable function', { + cause: 'invalid_app', + }); + } + + return app.onEnable(AppAccessorsInstance.getEnvironmentRead(), AppAccessorsInstance.getConfigurationModify()); +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnInstall.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnInstall.ts new file mode 100644 index 000000000000..aebf7628a914 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnInstall.ts @@ -0,0 +1,30 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; + +export default async function handleOnInstall(params: unknown): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onInstall !== 'function') { + throw new Error('App must contain an onInstall function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [context] = params as [Record]; + + await app.onInstall( + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + AppAccessorsInstance.getModifier(), + ); + + return true; +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts new file mode 100644 index 000000000000..19646fa6704f --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts @@ -0,0 +1,22 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; + +export default function handleOnPreSettingUpdate(params: unknown): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onPreSettingUpdate !== 'function') { + throw new Error('App must contain an onPreSettingUpdate function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [setting] = params as [Record]; + + return app.onPreSettingUpdate(setting, AppAccessorsInstance.getConfigurationModify(), AppAccessorsInstance.getReader(), AppAccessorsInstance.getHttp()); +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnSettingUpdated.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnSettingUpdated.ts new file mode 100644 index 000000000000..07084bc22425 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnSettingUpdated.ts @@ -0,0 +1,24 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; + +export default async function handleOnSettingUpdated(params: unknown): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onSettingUpdated !== 'function') { + throw new Error('App must contain an onSettingUpdated function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [setting] = params as [Record]; + + await app.onSettingUpdated(setting, AppAccessorsInstance.getConfigurationModify(), AppAccessorsInstance.getReader(), AppAccessorsInstance.getHttp()); + + return true; +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnUninstall.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnUninstall.ts new file mode 100644 index 000000000000..865819728ca4 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnUninstall.ts @@ -0,0 +1,30 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; + +export default async function handleOnUninstall(params: unknown): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onUninstall !== 'function') { + throw new Error('App must contain an onUninstall function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [context] = params as [Record]; + + await app.onUninstall( + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + AppAccessorsInstance.getModifier(), + ); + + return true; +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnUpdate.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnUpdate.ts new file mode 100644 index 000000000000..f21e4f947d5d --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnUpdate.ts @@ -0,0 +1,30 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; + +export default async function handleOnUpdate(params: unknown): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onUpdate !== 'function') { + throw new Error('App must contain an onUpdate function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [context] = params as [Record]; + + await app.onUpdate( + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + AppAccessorsInstance.getModifier(), + ); + + return true; +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleSetStatus.ts b/packages/apps-engine/deno-runtime/handlers/app/handleSetStatus.ts new file mode 100644 index 000000000000..c39ab2a16d62 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleSetStatus.ts @@ -0,0 +1,29 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; +import type { AppStatus as _AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { require } from '../../lib/require.ts'; + +const { AppStatus } = require('@rocket.chat/apps-engine/definition/AppStatus.js') as { + AppStatus: typeof _AppStatus; +}; + +export default async function handleSetStatus(params: unknown): Promise { + if (!Array.isArray(params) || !Object.values(AppStatus).includes(params[0])) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [status] = params as [typeof AppStatus]; + + const app = AppObjectRegistry.get('app'); + + if (!app || typeof app['setStatus'] !== 'function') { + throw new Error('App must contain a setStatus function', { + cause: 'invalid_app', + }); + } + + await app['setStatus'](status); + + return null; +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handler.ts b/packages/apps-engine/deno-runtime/handlers/app/handler.ts new file mode 100644 index 000000000000..2a44f34cb7fe --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handler.ts @@ -0,0 +1,112 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; +import { Defined, JsonRpcError } from 'jsonrpc-lite'; + +import handleConstructApp from './construct.ts'; +import handleInitialize from './handleInitialize.ts'; +import handleGetStatus from './handleGetStatus.ts'; +import handleSetStatus from './handleSetStatus.ts'; +import handleOnEnable from './handleOnEnable.ts'; +import handleOnInstall from './handleOnInstall.ts'; +import handleOnDisable from './handleOnDisable.ts'; +import handleOnUninstall from './handleOnUninstall.ts'; +import handleOnPreSettingUpdate from './handleOnPreSettingUpdate.ts'; +import handleOnSettingUpdated from './handleOnSettingUpdated.ts'; +import handleListener from '../listener/handler.ts'; +import handleUIKitInteraction, { uikitInteractions } from '../uikit/handler.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import handleOnUpdate from './handleOnUpdate.ts'; + +export default async function handleApp(method: string, params: unknown): Promise { + const [, appMethod] = method.split(':'); + + // We don't want the getStatus method to generate logs, so we handle it separately + if (appMethod === 'getStatus') { + return handleGetStatus(); + } + + // `app` will be undefined if the method here is "app:construct" + const app = AppObjectRegistry.get('app'); + + app?.getLogger().debug(`'${appMethod}' is being called...`); + + if (uikitInteractions.includes(appMethod)) { + return handleUIKitInteraction(appMethod, params).then((result) => { + if (result instanceof JsonRpcError) { + app?.getLogger().debug(`'${appMethod}' was unsuccessful.`, result.message); + } else { + app?.getLogger().debug(`'${appMethod}' was successfully called! The result is:`, result); + } + + return result; + }); + } + + if (appMethod.startsWith('check') || appMethod.startsWith('execute')) { + return handleListener(appMethod, params).then((result) => { + if (result instanceof JsonRpcError) { + app?.getLogger().debug(`'${appMethod}' was unsuccessful.`, result.message); + } else { + app?.getLogger().debug(`'${appMethod}' was successfully called! The result is:`, result); + } + + return result; + }); + } + + try { + let result: Defined | JsonRpcError; + + switch (appMethod) { + case 'construct': + result = await handleConstructApp(params); + break; + case 'initialize': + result = await handleInitialize(); + break; + case 'setStatus': + result = await handleSetStatus(params); + break; + case 'onEnable': + result = await handleOnEnable(); + break; + case 'onDisable': + result = await handleOnDisable(); + break; + case 'onInstall': + result = await handleOnInstall(params); + break; + case 'onUninstall': + result = await handleOnUninstall(params); + break; + case 'onPreSettingUpdate': + result = await handleOnPreSettingUpdate(params); + break; + case 'onSettingUpdated': + result = await handleOnSettingUpdated(params); + break; + case 'onUpdate': + result = await handleOnUpdate(params); + break; + default: + throw new JsonRpcError('Method not found', -32601); + } + + app?.getLogger().debug(`'${appMethod}' was successfully called! The result is:`, result); + + return result; + } catch (e: unknown) { + if (!(e instanceof Error)) { + return new JsonRpcError('Unknown error', -32000, e); + } + + if ((e.cause as string)?.includes('invalid_param_type')) { + return JsonRpcError.invalidParams(null); + } + + if ((e.cause as string)?.includes('invalid_app')) { + return JsonRpcError.internalError({ message: 'App unavailable' }); + } + + return new JsonRpcError(e.message, -32000, e); + } +} diff --git a/packages/apps-engine/deno-runtime/handlers/listener/handler.ts b/packages/apps-engine/deno-runtime/handlers/listener/handler.ts new file mode 100644 index 000000000000..1e6de20538fc --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/listener/handler.ts @@ -0,0 +1,150 @@ +import { Defined, JsonRpcError } from 'jsonrpc-lite'; +import type { App } from '@rocket.chat/apps-engine/definition/App.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 { AppsEngineException as _AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions/AppsEngineException.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { MessageExtender } from '../../lib/accessors/extenders/MessageExtender.ts'; +import { RoomExtender } from '../../lib/accessors/extenders/RoomExtender.ts'; +import { MessageBuilder } from '../../lib/accessors/builders/MessageBuilder.ts'; +import { RoomBuilder } from '../../lib/accessors/builders/RoomBuilder.ts'; +import { AppAccessors, AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { require } from '../../lib/require.ts'; +import createRoom from '../../lib/roomFactory.ts'; +import { Room } from "../../lib/room.ts"; + +const { AppsEngineException } = require('@rocket.chat/apps-engine/definition/exceptions/AppsEngineException.js') as { + AppsEngineException: typeof _AppsEngineException; +}; + +export default async function handleListener(evtInterface: string, params: unknown): Promise { + const app = AppObjectRegistry.get('app'); + + const eventExecutor = app?.[evtInterface as keyof App]; + + if (typeof eventExecutor !== 'function') { + return JsonRpcError.methodNotFound({ + message: 'Invalid event interface called on app', + }); + } + + if (!Array.isArray(params) || params.length < 1 || params.length > 2) { + return JsonRpcError.invalidParams(null); + } + + try { + const args = parseArgs({ AppAccessorsInstance }, evtInterface, params); + return await (eventExecutor as (...args: unknown[]) => Promise).apply(app, args); + } catch (e) { + if (e instanceof JsonRpcError) { + return e; + } + + if (e instanceof AppsEngineException) { + return new JsonRpcError(e.message, AppsEngineException.JSONRPC_ERROR_CODE, { name: e.name }); + } + + return JsonRpcError.internalError({ message: e.message }); + } + +} + +export function parseArgs(deps: { AppAccessorsInstance: AppAccessors }, evtMethod: string, params: unknown[]): unknown[] { + const { AppAccessorsInstance } = deps; + /** + * param1 is the context for the event handler execution + * param2 is an optional extra content that some hanlers require + */ + const [param1, param2] = params as [unknown, unknown]; + + if (!param1) { + throw JsonRpcError.invalidParams(null); + } + + let context = param1; + + if (evtMethod.includes('Message')) { + context = hydrateMessageObjects(context) as Record; + } else if (evtMethod.endsWith('RoomUserJoined') || evtMethod.endsWith('RoomUserLeave')) { + (context as Record).room = createRoom((context as Record).room as IRoom, AppAccessorsInstance.getSenderFn()); + } else if (evtMethod.includes('PreRoom')) { + context = createRoom(context as IRoom, AppAccessorsInstance.getSenderFn()); + } + + const args: unknown[] = [context, AppAccessorsInstance.getReader(), AppAccessorsInstance.getHttp()]; + + // "check" events will only go this far - (context, reader, http) + if (evtMethod.startsWith('check')) { + // "checkPostMessageDeleted" has an extra param - (context, reader, http, extraContext) + if (param2) { + args.push(hydrateMessageObjects(param2)); + } + + return args; + } + + // From this point on, all events will require (reader, http, persistence) injected + args.push(AppAccessorsInstance.getPersistence()); + + // "extend" events have an additional "Extender" param - (context, extender, reader, http, persistence) + if (evtMethod.endsWith('Extend')) { + if (evtMethod.includes('Message')) { + args.splice(1, 0, new MessageExtender(param1 as IMessage)); + } else if (evtMethod.includes('Room')) { + args.splice(1, 0, new RoomExtender(param1 as IRoom)); + } + + return args; + } + + // "Modify" events have an additional "Builder" param - (context, builder, reader, http, persistence) + if (evtMethod.endsWith('Modify')) { + if (evtMethod.includes('Message')) { + args.splice(1, 0, new MessageBuilder(param1 as IMessage)); + } else if (evtMethod.includes('Room')) { + args.splice(1, 0, new RoomBuilder(param1 as IRoom)); + } + + return args; + } + + // From this point on, all events will require (reader, http, persistence, modifier) injected + args.push(AppAccessorsInstance.getModifier()); + + // This guy gets an extra one + if (evtMethod === 'executePostMessageDeleted') { + if (!param2) { + throw JsonRpcError.invalidParams(null); + } + + args.push(hydrateMessageObjects(param2)); + } + + return args; +} + +/** + * Hydrate the context object with the correct IMessage + * + * Some information is lost upon serializing the data from listeners through the pipes, + * so here we hydrate the complete object as necessary + */ +function hydrateMessageObjects(context: unknown): unknown { + if (objectIsRawMessage(context)) { + context.room = createRoom(context.room as IRoom, AppAccessorsInstance.getSenderFn()); + } else if ((context as Record)?.message) { + (context as Record).message = hydrateMessageObjects((context as Record).message); + } + + return context; +} + +function objectIsRawMessage(value: unknown): value is IMessage { + if (!value) return false; + + const { id, room, sender, createdAt } = value as Record; + + // Check if we have the fields of a message and the room hasn't already been hydrated + return !!(id && room && sender && createdAt) && !(room instanceof Room); +} diff --git a/packages/apps-engine/deno-runtime/handlers/scheduler-handler.ts b/packages/apps-engine/deno-runtime/handlers/scheduler-handler.ts new file mode 100644 index 000000000000..0145034957f2 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/scheduler-handler.ts @@ -0,0 +1,51 @@ +import { Defined, JsonRpcError } from 'jsonrpc-lite'; +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; +import type { IProcessor } from '@rocket.chat/apps-engine/definition/scheduler/IProcessor.ts'; + +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; + +export default async function handleScheduler(method: string, params: unknown): Promise { + const [, processorId] = method.split(':'); + if (!Array.isArray(params)) { + return JsonRpcError.invalidParams({ message: 'Invalid params' }); + } + + const [context] = params as [Record]; + + const app = AppObjectRegistry.get('app'); + + if (!app) { + return JsonRpcError.internalError({ message: 'App not found' }); + } + + // AppSchedulerManager will append the appId to the processor name to avoid conflicts + const processor = AppObjectRegistry.get(`scheduler:${processorId}`); + + if (!processor) { + return JsonRpcError.methodNotFound({ + message: `Could not find processor for method ${method}`, + }); + } + + app.getLogger().debug(`Job processor ${processor.id} is being executed...`); + + try { + await processor.processor( + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getModifier(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + ); + + app.getLogger().debug(`Job processor ${processor.id} was successfully executed`); + + return null; + } catch (e) { + app.getLogger().error(e); + app.getLogger().error(`Job processor ${processor.id} was unsuccessful`); + + return JsonRpcError.internalError({ message: e.message }); + } +} diff --git a/packages/apps-engine/deno-runtime/handlers/slashcommand-handler.ts b/packages/apps-engine/deno-runtime/handlers/slashcommand-handler.ts new file mode 100644 index 000000000000..cfebf0d1460e --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/slashcommand-handler.ts @@ -0,0 +1,122 @@ +import { Defined, JsonRpcError } from 'jsonrpc-lite'; + +import type { App } from "@rocket.chat/apps-engine/definition/App.ts"; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands/ISlashCommand.ts'; +import type { SlashCommandContext as _SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands/SlashCommandContext.ts'; +import type { Room as _Room } from '@rocket.chat/apps-engine/server/rooms/Room.ts'; + +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; +import { AppAccessors, AppAccessorsInstance } from '../lib/accessors/mod.ts'; +import { require } from '../lib/require.ts'; +import createRoom from '../lib/roomFactory.ts'; + +// For some reason Deno couldn't understand the typecast to the original interfaces and said it wasn't a constructor type +const { SlashCommandContext } = require('@rocket.chat/apps-engine/definition/slashcommands/SlashCommandContext.js') as { + SlashCommandContext: typeof _SlashCommandContext; +}; + +export default async function slashCommandHandler(call: string, params: unknown): Promise { + const [, commandName, method] = call.split(':'); + + const command = AppObjectRegistry.get(`slashcommand:${commandName}`); + + if (!command) { + return new JsonRpcError(`Slashcommand ${commandName} not found`, -32000); + } + + let result: Awaited> | Awaited>; + + // If the command is registered, we're pretty safe to assume the app is not undefined + const app = AppObjectRegistry.get('app')!; + + app.getLogger().debug(`${commandName}'s ${method} is being executed...`, params); + + try { + if (method === 'executor' || method === 'previewer') { + result = await handleExecutor({ AppAccessorsInstance }, command, method, params); + } else if (method === 'executePreviewItem') { + result = await handlePreviewItem({ AppAccessorsInstance }, command, params); + } else { + return new JsonRpcError(`Method ${method} not found on slashcommand ${commandName}`, -32000); + } + + app.getLogger().debug(`${commandName}'s ${method} was successfully executed.`); + } catch (error) { + app.getLogger().debug(`${commandName}'s ${method} was unsuccessful.`); + + return new JsonRpcError(error.message, -32000); + } + + return result; +} + +/** + * @param deps Dependencies that need to be injected into the slashcommand + * @param command The slashcommand that is being executed + * @param method The method that is being executed + * @param params The parameters that are being passed to the method + */ +export function handleExecutor(deps: { AppAccessorsInstance: AppAccessors }, command: ISlashCommand, method: 'executor' | 'previewer', params: unknown) { + const executor = command[method]; + + if (typeof executor !== 'function') { + throw new Error(`Method ${method} not found on slashcommand ${command.command}`); + } + + if (!Array.isArray(params) || typeof params[0] !== 'object' || !params[0]) { + throw new Error(`First parameter must be an object`); + } + + const { sender, room, params: args, threadId, triggerId } = params[0] as Record; + + const context = new SlashCommandContext( + sender as _SlashCommandContext['sender'], + createRoom(room as IRoom, deps.AppAccessorsInstance.getSenderFn()), + args as _SlashCommandContext['params'], + threadId as _SlashCommandContext['threadId'], + triggerId as _SlashCommandContext['triggerId'], + ); + + return executor.apply(command, [ + context, + deps.AppAccessorsInstance.getReader(), + deps.AppAccessorsInstance.getModifier(), + deps.AppAccessorsInstance.getHttp(), + deps.AppAccessorsInstance.getPersistence(), + ]); +} + +/** + * @param deps Dependencies that need to be injected into the slashcommand + * @param command The slashcommand that is being executed + * @param params The parameters that are being passed to the method + */ +export function handlePreviewItem(deps: { AppAccessorsInstance: AppAccessors }, command: ISlashCommand, params: unknown) { + if (typeof command.executePreviewItem !== 'function') { + throw new Error(`Method not found on slashcommand ${command.command}`); + } + + if (!Array.isArray(params) || typeof params[0] !== 'object' || !params[0]) { + throw new Error(`First parameter must be an object`); + } + + const [previewItem, { sender, room, params: args, threadId, triggerId }] = params as [Record, Record]; + + const context = new SlashCommandContext( + sender as _SlashCommandContext['sender'], + createRoom(room as IRoom, deps.AppAccessorsInstance.getSenderFn()), + args as _SlashCommandContext['params'], + threadId as _SlashCommandContext['threadId'], + triggerId as _SlashCommandContext['triggerId'], + ); + + return command.executePreviewItem( + previewItem, + context, + deps.AppAccessorsInstance.getReader(), + deps.AppAccessorsInstance.getModifier(), + deps.AppAccessorsInstance.getHttp(), + deps.AppAccessorsInstance.getPersistence(), + ); +} diff --git a/packages/apps-engine/deno-runtime/handlers/tests/api-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/api-handler.test.ts new file mode 100644 index 000000000000..a3789f755542 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/tests/api-handler.test.ts @@ -0,0 +1,79 @@ +// deno-lint-ignore-file no-explicit-any +import { assertEquals, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { spy } from "https://deno.land/std@0.203.0/testing/mock.ts"; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { assertInstanceOf } from "https://deno.land/std@0.203.0/assert/assert_instance_of.ts"; +import { JsonRpcError } from "jsonrpc-lite"; +import type { IApiEndpoint } from "@rocket.chat/apps-engine/definition/api/IApiEndpoint.ts"; +import apiHandler from "../api-handler.ts"; + +describe('handlers > api', () => { + const mockEndpoint: IApiEndpoint = { + path: '/test', + // deno-lint-ignore no-unused-vars + get: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('ok'), + // deno-lint-ignore no-unused-vars + post: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('ok'), + // deno-lint-ignore no-unused-vars + put: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => { throw new Error('Method execution error example') }, + } + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('api:/test', mockEndpoint); + }); + + it('correctly handles execution of an api endpoint method GET', async () => { + const _spy = spy(mockEndpoint, 'get'); + + const result = await apiHandler('api:/test:get', ['request', 'endpointInfo']); + + assertEquals(result, 'ok'); + assertEquals(_spy.calls[0].args.length, 6); + assertEquals(_spy.calls[0].args[0], 'request'); + assertEquals(_spy.calls[0].args[1], 'endpointInfo'); + }); + + it('correctly handles execution of an api endpoint method POST', async () => { + const _spy = spy(mockEndpoint, 'post'); + + const result = await apiHandler('api:/test:post', ['request', 'endpointInfo']); + + assertEquals(result, 'ok'); + assertEquals(_spy.calls[0].args.length, 6); + assertEquals(_spy.calls[0].args[0], 'request'); + assertEquals(_spy.calls[0].args[1], 'endpointInfo'); + }); + + it('correctly handles an error if the method not exists for the selected endpoint', async () => { + const result = await apiHandler(`api:/test:delete`, ['request', 'endpointInfo']); + + assertInstanceOf(result, JsonRpcError) + assertObjectMatch(result, { + message: `/test's delete not exists`, + code: -32000 + }) + }); + + it('correctly handles an error if endpoint not exists', async () => { + const result = await apiHandler(`api:/error:get`, ['request', 'endpointInfo']); + + assertInstanceOf(result, JsonRpcError) + assertObjectMatch(result, { + message: `Endpoint /error not found`, + code: -32000 + }) + }); + + it('correctly handles an error if the method execution fails', async () => { + const result = await apiHandler(`api:/test:put`, ['request', 'endpointInfo']); + + assertInstanceOf(result, JsonRpcError) + assertObjectMatch(result, { + message: `Method execution error example`, + code: -32000 + }) + }); +}); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/listener-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/listener-handler.test.ts new file mode 100644 index 000000000000..3e3663b06d22 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/tests/listener-handler.test.ts @@ -0,0 +1,234 @@ +// deno-lint-ignore-file no-explicit-any +import { assertEquals, assertInstanceOf, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; + +import { parseArgs } from '../listener/handler.ts'; +import { AppAccessors } from '../../lib/accessors/mod.ts'; +import { Room } from '../../lib/room.ts'; +import { MessageExtender } from '../../lib/accessors/extenders/MessageExtender.ts'; +import { RoomExtender } from '../../lib/accessors/extenders/RoomExtender.ts'; +import { MessageBuilder } from '../../lib/accessors/builders/MessageBuilder.ts'; +import { RoomBuilder } from '../../lib/accessors/builders/RoomBuilder.ts'; + +describe('handlers > listeners', () => { + const mockAppAccessors = { + getReader: () => ({ __type: 'reader' }), + getHttp: () => ({ __type: 'http' }), + getModifier: () => ({ __type: 'modifier' }), + getPersistence: () => ({ __type: 'persistence' }), + getSenderFn: () => (id: string) => Promise.resolve([{ __type: 'bridgeCall' }, { id }]), + } as unknown as AppAccessors; + + it('correctly parses the arguments for a request to trigger the "checkPreMessageSentPrevent" method', () => { + const evtMethod = 'checkPreMessageSentPrevent'; + // For the 'checkPreMessageSentPrevent' method, the context will be a message in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 3); + assertEquals(params[0], { __type: 'context' }); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + }); + + it('correctly parses the arguments for a request to trigger the "checkPostMessageDeleted" method', () => { + const evtMethod = 'checkPostMessageDeleted'; + // For the 'checkPostMessageDeleted' method, the context will be a message in a real scenario, + // and the extraContext will provide further information such the user who deleted the message + const evtArgs = [{ __type: 'context' }, { __type: 'extraContext' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 4); + assertEquals(params[0], { __type: 'context' }); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'extraContext' }); + }); + + it('correctly parses the arguments for a request to trigger the "checkPreRoomCreateExtend" method', () => { + const evtMethod = 'checkPreRoomCreateExtend'; + // For the 'checkPreRoomCreateExtend' method, the context will be a room in a real scenario + const evtArgs = [ + { + id: 'fake', + type: 'fake', + slugifiedName: 'fake', + creator: 'fake', + createdAt: Date.now(), + }, + ]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 3); + + assertInstanceOf(params[0], Room); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePreMessageSentExtend" method', () => { + const evtMethod = 'executePreMessageSentExtend'; + // For the 'executePreMessageSentExtend' method, the context will be a message in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + // Instantiating the MessageExtender might modify the original object, so we need to assert it matches instead of equals + assertObjectMatch(params[0] as Record, { + __type: 'context', + }); + assertInstanceOf(params[1], MessageExtender); + assertEquals(params[2], { __type: 'reader' }); + assertEquals(params[3], { __type: 'http' }); + assertEquals(params[4], { __type: 'persistence' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePreRoomCreateExtend" method', () => { + const evtMethod = 'executePreRoomCreateExtend'; + // For the 'executePreRoomCreateExtend' method, the context will be a room in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + // Instantiating the RoomExtender might modify the original object, so we need to assert it matches instead of equals + assertObjectMatch(params[0] as Record, { + __type: 'context', + }); + assertInstanceOf(params[1], RoomExtender); + assertEquals(params[2], { __type: 'reader' }); + assertEquals(params[3], { __type: 'http' }); + assertEquals(params[4], { __type: 'persistence' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePreMessageSentModify" method', () => { + const evtMethod = 'executePreMessageSentModify'; + // For the 'executePreMessageSentModify' method, the context will be a message in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + // Instantiating the MessageBuilder might modify the original object, so we need to assert it matches instead of equals + assertObjectMatch(params[0] as Record, { + __type: 'context', + }); + assertInstanceOf(params[1], MessageBuilder); + assertEquals(params[2], { __type: 'reader' }); + assertEquals(params[3], { __type: 'http' }); + assertEquals(params[4], { __type: 'persistence' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePreRoomCreateModify" method', () => { + const evtMethod = 'executePreRoomCreateModify'; + // For the 'executePreRoomCreateModify' method, the context will be a room in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + // Instantiating the RoomBuilder might modify the original object, so we need to assert it matches instead of equals + assertObjectMatch(params[0] as Record, { + __type: 'context', + }); + assertInstanceOf(params[1], RoomBuilder); + assertEquals(params[2], { __type: 'reader' }); + assertEquals(params[3], { __type: 'http' }); + assertEquals(params[4], { __type: 'persistence' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePostRoomUserJoined" method', () => { + const evtMethod = 'executePostRoomUserJoined'; + // For the 'executePostRoomUserJoined' method, the context will be a room in a real scenario + const room = { + id: 'fake', + type: 'fake', + slugifiedName: 'fake', + creator: 'fake', + createdAt: Date.now(), + }; + + const evtArgs = [{ __type: 'context', room }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + assertInstanceOf((params[0] as any).room, Room); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'persistence' }); + assertEquals(params[4], { __type: 'modifier' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePostRoomUserLeave" method', () => { + const evtMethod = 'executePostRoomUserLeave'; + // For the 'executePostRoomUserLeave' method, the context will be a room in a real scenario + const room = { + id: 'fake', + type: 'fake', + slugifiedName: 'fake', + creator: 'fake', + createdAt: Date.now(), + }; + + const evtArgs = [{ __type: 'context', room }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + assertInstanceOf((params[0] as any).room, Room); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'persistence' }); + assertEquals(params[4], { __type: 'modifier' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePostMessageDeleted" method', () => { + const evtMethod = 'executePostMessageDeleted'; + // For the 'executePostMessageDeleted' method, the context will be a message in a real scenario + const evtArgs = [{ __type: 'context' }, { __type: 'extraContext' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 6); + assertEquals(params[0], { __type: 'context' }); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'persistence' }); + assertEquals(params[4], { __type: 'modifier' }); + assertEquals(params[5], { __type: 'extraContext' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePostMessageSent" method', () => { + const evtMethod = 'executePostMessageSent'; + // For the 'executePostMessageDeleted' method, the context will be a message in a real scenario + const evtArgs = [ + { + id: 'fake', + sender: 'fake', + createdAt: Date.now(), + room: { + id: 'fake-room', + type: 'fake', + slugifiedName: 'fake', + creator: 'fake', + createdAt: Date.now(), + }, + }, + ]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + assertObjectMatch((params[0] as Record), { id: 'fake' }); + assertInstanceOf((params[0] as any).room, Room); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'persistence' }); + assertEquals(params[4], { __type: 'modifier' }); + }); +}); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/scheduler-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/scheduler-handler.test.ts new file mode 100644 index 000000000000..681f228bdb3e --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/tests/scheduler-handler.test.ts @@ -0,0 +1,46 @@ +import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessors } from '../../lib/accessors/mod.ts'; +import handleScheduler from '../scheduler-handler.ts'; + +describe('handlers > scheduler', () => { + const mockAppAccessors = new AppAccessors(() => + Promise.resolve({ + id: 'mockId', + result: {}, + jsonrpc: '2.0', + serialize: () => '', + }), + ); + + const mockApp = { + getID: () => 'mockApp', + getLogger: () => ({ + debug: () => {}, + error: () => {}, + }), + }; + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('app', mockApp); + mockAppAccessors.getConfigurationExtend().scheduler.registerProcessors([ + { + id: 'mockId', + processor: () => Promise.resolve('it works!'), + }, + ]); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('correctly executes a request to a processor', async () => { + const result = await handleScheduler('scheduler:mockId', [{}]); + + assertEquals(result, null); + }); +}); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/slashcommand-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/slashcommand-handler.test.ts new file mode 100644 index 000000000000..d3da4b132d66 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/tests/slashcommand-handler.test.ts @@ -0,0 +1,152 @@ +// deno-lint-ignore-file no-explicit-any +import { assertEquals, assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessors } from '../../lib/accessors/mod.ts'; +import { handleExecutor, handlePreviewItem } from '../slashcommand-handler.ts'; +import { Room } from "../../lib/room.ts"; + +describe('handlers > slashcommand', () => { + const mockAppAccessors = { + getReader: () => ({ __type: 'reader' }), + getHttp: () => ({ __type: 'http' }), + getModifier: () => ({ __type: 'modifier' }), + getPersistence: () => ({ __type: 'persistence' }), + getSenderFn: () => (id: string) => Promise.resolve([{ __type: 'bridgeCall' }, { id }]), + } as unknown as AppAccessors; + + const mockCommandExecutorOnly = { + command: 'executor-only', + i18nParamsExample: 'test', + i18nDescription: 'test', + providesPreview: false, + // deno-lint-ignore no-unused-vars + async executor(context: any, read: any, modify: any, http: any, persis: any): Promise {}, + }; + + const mockCommandExecutorAndPreview = { + command: 'executor-and-preview', + i18nParamsExample: 'test', + i18nDescription: 'test', + providesPreview: true, + // deno-lint-ignore no-unused-vars + async executor(context: any, read: any, modify: any, http: any, persis: any): Promise {}, + // deno-lint-ignore no-unused-vars + async previewer(context: any, read: any, modify: any, http: any, persis: any): Promise {}, + // deno-lint-ignore no-unused-vars + async executePreviewItem(previewItem: any, context: any, read: any, modify: any, http: any, persis: any): Promise {}, + }; + + const mockCommandPreviewWithNoExecutor = { + command: 'preview-with-no-executor', + i18nParamsExample: 'test', + i18nDescription: 'test', + providesPreview: true, + // deno-lint-ignore no-unused-vars + async previewer(context: any, read: any, modify: any, http: any, persis: any): Promise {}, + // deno-lint-ignore no-unused-vars + async executePreviewItem(previewItem: any, context: any, read: any, modify: any, http: any, persis: any): Promise {}, + }; + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('slashcommand:executor-only', mockCommandExecutorOnly); + AppObjectRegistry.set('slashcommand:executor-and-preview', mockCommandExecutorAndPreview); + AppObjectRegistry.set('slashcommand:preview-with-no-executor', mockCommandPreviewWithNoExecutor); + }); + + it('correctly handles execution of a slash command', async () => { + const mockContext = { + sender: { __type: 'sender' }, + room: { __type: 'room' }, + params: { __type: 'params' }, + threadId: 'threadId', + triggerId: 'triggerId', + }; + + const _spy = spy(mockCommandExecutorOnly, 'executor'); + + await handleExecutor({ AppAccessorsInstance: mockAppAccessors }, mockCommandExecutorOnly, 'executor', [mockContext]); + + const context = _spy.calls[0].args[0]; + + assertInstanceOf(context.getRoom(), Room); + assertEquals(context.getSender(), { __type: 'sender' }); + assertEquals(context.getArguments(), { __type: 'params' }); + assertEquals(context.getThreadId(), 'threadId'); + assertEquals(context.getTriggerId(), 'triggerId'); + + assertEquals(_spy.calls[0].args[1], mockAppAccessors.getReader()); + assertEquals(_spy.calls[0].args[2], mockAppAccessors.getModifier()); + assertEquals(_spy.calls[0].args[3], mockAppAccessors.getHttp()); + assertEquals(_spy.calls[0].args[4], mockAppAccessors.getPersistence()); + + _spy.restore(); + }); + + it('correctly handles execution of a slash command previewer', async () => { + const mockContext = { + sender: { __type: 'sender' }, + room: { __type: 'room' }, + params: { __type: 'params' }, + threadId: 'threadId', + triggerId: 'triggerId', + }; + + const _spy = spy(mockCommandExecutorAndPreview, 'previewer'); + + await handleExecutor({ AppAccessorsInstance: mockAppAccessors }, mockCommandExecutorAndPreview, 'previewer', [mockContext]); + + const context = _spy.calls[0].args[0]; + + assertInstanceOf(context.getRoom(), Room); + assertEquals(context.getSender(), { __type: 'sender' }); + assertEquals(context.getArguments(), { __type: 'params' }); + assertEquals(context.getThreadId(), 'threadId'); + assertEquals(context.getTriggerId(), 'triggerId'); + + assertEquals(_spy.calls[0].args[1], mockAppAccessors.getReader()); + assertEquals(_spy.calls[0].args[2], mockAppAccessors.getModifier()); + assertEquals(_spy.calls[0].args[3], mockAppAccessors.getHttp()); + assertEquals(_spy.calls[0].args[4], mockAppAccessors.getPersistence()); + + _spy.restore(); + }); + + it('correctly handles execution of a slash command preview item executor', async () => { + const mockContext = { + sender: { __type: 'sender' }, + room: { __type: 'room' }, + params: { __type: 'params' }, + threadId: 'threadId', + triggerId: 'triggerId', + }; + + const mockPreviewItem = { + id: 'previewItemId', + type: 'image', + value: 'https://example.com/image.png', + }; + + const _spy = spy(mockCommandExecutorAndPreview, 'executePreviewItem'); + + await handlePreviewItem({ AppAccessorsInstance: mockAppAccessors }, mockCommandExecutorAndPreview, [mockPreviewItem, mockContext]); + + const context = _spy.calls[0].args[1]; + + assertInstanceOf(context.getRoom(), Room); + assertEquals(context.getSender(), { __type: 'sender' }); + assertEquals(context.getArguments(), { __type: 'params' }); + assertEquals(context.getThreadId(), 'threadId'); + assertEquals(context.getTriggerId(), 'triggerId'); + + assertEquals(_spy.calls[0].args[2], mockAppAccessors.getReader()); + assertEquals(_spy.calls[0].args[3], mockAppAccessors.getModifier()); + assertEquals(_spy.calls[0].args[4], mockAppAccessors.getHttp()); + assertEquals(_spy.calls[0].args[5], mockAppAccessors.getPersistence()); + + _spy.restore(); + }); +}); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/uikit-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/uikit-handler.test.ts new file mode 100644 index 000000000000..f2293d6c98e0 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/tests/uikit-handler.test.ts @@ -0,0 +1,99 @@ +// deno-lint-ignore-file no-explicit-any +import { assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import handleUIKitInteraction, { + UIKitActionButtonInteractionContext, + UIKitBlockInteractionContext, + UIKitLivechatBlockInteractionContext, + UIKitViewCloseInteractionContext, + UIKitViewSubmitInteractionContext, +} from '../uikit/handler.ts'; + +describe('handlers > uikit', () => { + const mockApp = { + getID: (): string => 'appId', + executeBlockActionHandler: (context: any): Promise => Promise.resolve(context), + executeViewSubmitHandler: (context: any): Promise => Promise.resolve(context), + executeViewClosedHandler: (context: any): Promise => Promise.resolve(context), + executeActionButtonHandler: (context: any): Promise => Promise.resolve(context), + executeLivechatBlockActionHandler: (context: any): Promise => Promise.resolve(context), + }; + + beforeEach(() => { + AppObjectRegistry.set('app', mockApp); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('successfully handles a call for "executeBlockActionHandler"', async () => { + const result = await handleUIKitInteraction('executeBlockActionHandler', [ + { + actionId: 'actionId', + blockId: 'blockId', + value: 'value', + viewId: 'viewId', + }, + ]); + + assertInstanceOf(result, UIKitBlockInteractionContext); + }); + + it('successfully handles a call for "executeViewSubmitHandler"', async () => { + const result = await handleUIKitInteraction('executeViewSubmitHandler', [ + { + viewId: 'viewId', + appId: 'appId', + userId: 'userId', + isAppUser: true, + values: {}, + }, + ]); + + assertInstanceOf(result, UIKitViewSubmitInteractionContext); + }); + + it('successfully handles a call for "executeViewClosedHandler"', async () => { + const result = await handleUIKitInteraction('executeViewClosedHandler', [ + { + viewId: 'viewId', + appId: 'appId', + userId: 'userId', + isAppUser: true, + }, + ]); + + assertInstanceOf(result, UIKitViewCloseInteractionContext); + }); + + it('successfully handles a call for "executeActionButtonHandler"', async () => { + const result = await handleUIKitInteraction('executeActionButtonHandler', [ + { + actionId: 'actionId', + appId: 'appId', + userId: 'userId', + isAppUser: true, + }, + ]); + + assertInstanceOf(result, UIKitActionButtonInteractionContext); + }); + + it('successfully handles a call for "executeLivechatBlockActionHandler"', async () => { + const result = await handleUIKitInteraction('executeLivechatBlockActionHandler', [ + { + actionId: 'actionId', + appId: 'appId', + userId: 'userId', + visitor: {}, + isAppUser: true, + room: {}, + }, + ]); + + assertInstanceOf(result, UIKitLivechatBlockInteractionContext); + }); +}); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/videoconference-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/videoconference-handler.test.ts new file mode 100644 index 000000000000..a32d3175e24d --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/tests/videoconference-handler.test.ts @@ -0,0 +1,122 @@ +// deno-lint-ignore-file no-explicit-any +import { assertEquals, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import videoconfHandler from '../videoconference-handler.ts'; +import { assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/assert_instance_of.ts'; +import { JsonRpcError } from 'jsonrpc-lite'; + +describe('handlers > videoconference', () => { + // deno-lint-ignore no-unused-vars + const mockMethodWithoutParam = (read: any, modify: any, http: any, persis: any): Promise => Promise.resolve('ok none'); + // deno-lint-ignore no-unused-vars + const mockMethodWithOneParam = (call: any, read: any, modify: any, http: any, persis: any): Promise => Promise.resolve('ok one'); + // deno-lint-ignore no-unused-vars + const mockMethodWithTwoParam = (call: any, user: any, read: any, modify: any, http: any, persis: any): Promise => Promise.resolve('ok two'); + // deno-lint-ignore no-unused-vars + const mockMethodWithThreeParam = (call: any, user: any, options: any, read: any, modify: any, http: any, persis: any): Promise => + Promise.resolve('ok three'); + const mockProvider = { + empty: mockMethodWithoutParam, + one: mockMethodWithOneParam, + two: mockMethodWithTwoParam, + three: mockMethodWithThreeParam, + notAFunction: true, + error: () => { + throw new Error('Method execution error example'); + }, + }; + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('videoConfProvider:test-provider', mockProvider); + }); + + it('correctly handles execution of a videoconf method without additional params', async () => { + const _spy = spy(mockProvider, 'empty'); + + const result = await videoconfHandler('videoconference:test-provider:empty', []); + + assertEquals(result, 'ok none'); + assertEquals(_spy.calls[0].args.length, 4); + + _spy.restore(); + }); + + it('correctly handles execution of a videoconf method with one param', async () => { + const _spy = spy(mockProvider, 'one'); + + const result = await videoconfHandler('videoconference:test-provider:one', ['call']); + + assertEquals(result, 'ok one'); + assertEquals(_spy.calls[0].args.length, 5); + assertEquals(_spy.calls[0].args[0], 'call'); + + _spy.restore(); + }); + + it('correctly handles execution of a videoconf method with two params', async () => { + const _spy = spy(mockProvider, 'two'); + + const result = await videoconfHandler('videoconference:test-provider:two', ['call', 'user']); + + assertEquals(result, 'ok two'); + assertEquals(_spy.calls[0].args.length, 6); + assertEquals(_spy.calls[0].args[0], 'call'); + assertEquals(_spy.calls[0].args[1], 'user'); + + _spy.restore(); + }); + + it('correctly handles execution of a videoconf method with three params', async () => { + const _spy = spy(mockProvider, 'three'); + + const result = await videoconfHandler('videoconference:test-provider:three', ['call', 'user', 'options']); + + assertEquals(result, 'ok three'); + assertEquals(_spy.calls[0].args.length, 7); + assertEquals(_spy.calls[0].args[0], 'call'); + assertEquals(_spy.calls[0].args[1], 'user'); + assertEquals(_spy.calls[0].args[2], 'options'); + + _spy.restore(); + }); + + it('correctly handles an error on execution of a videoconf method', async () => { + const result = await videoconfHandler('videoconference:test-provider:error', []); + + assertInstanceOf(result, JsonRpcError); + assertObjectMatch(result, { + message: 'Method execution error example', + code: -32000, + }); + }); + + it('correctly handles an error when provider is not found', async () => { + const providerName = 'error-provider'; + const result = await videoconfHandler(`videoconference:${providerName}:method`, []); + + assertInstanceOf(result, JsonRpcError); + assertObjectMatch(result, { + message: `Provider ${providerName} not found`, + code: -32000, + }); + }); + + it('correctly handles an error if method is not a function of provider', async () => { + const methodName = 'notAFunction'; + const providerName = 'test-provider'; + const result = await videoconfHandler(`videoconference:${providerName}:${methodName}`, []); + + assertInstanceOf(result, JsonRpcError); + assertObjectMatch(result, { + message: 'Method not found', + code: -32601, + data: { + message: `Method ${methodName} not found on provider ${providerName}`, + }, + }); + }); +}); diff --git a/packages/apps-engine/deno-runtime/handlers/uikit/handler.ts b/packages/apps-engine/deno-runtime/handlers/uikit/handler.ts new file mode 100644 index 000000000000..5a418f242ad9 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/uikit/handler.ts @@ -0,0 +1,82 @@ +import { Defined, JsonRpcError } from 'jsonrpc-lite'; +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { require } from '../../lib/require.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; + +export const uikitInteractions = [ + 'executeBlockActionHandler', + 'executeViewSubmitHandler', + 'executeViewClosedHandler', + 'executeActionButtonHandler', + 'executeLivechatBlockActionHandler', +]; + +export const { + UIKitBlockInteractionContext, + UIKitViewSubmitInteractionContext, + UIKitViewCloseInteractionContext, + UIKitActionButtonInteractionContext, +} = require('@rocket.chat/apps-engine/definition/uikit/UIKitInteractionContext.js'); + +export const { UIKitLivechatBlockInteractionContext } = require('@rocket.chat/apps-engine/definition/uikit/livechat/UIKitLivechatInteractionContext.js'); + +export default async function handleUIKitInteraction(method: string, params: unknown): Promise { + if (!uikitInteractions.includes(method)) { + return JsonRpcError.methodNotFound(null); + } + + if (!Array.isArray(params)) { + return JsonRpcError.invalidParams(null); + } + + const app = AppObjectRegistry.get('app'); + + const interactionHandler = app?.[method as keyof App] as unknown; + + if (!app || typeof interactionHandler !== 'function') { + return JsonRpcError.methodNotFound({ + message: `App does not implement method "${method}"`, + }); + } + + const [payload] = params as [Record]; + + if (!payload) { + return JsonRpcError.invalidParams(null); + } + + let context; + + switch (method) { + case 'executeBlockActionHandler': + context = new UIKitBlockInteractionContext(payload); + break; + case 'executeViewSubmitHandler': + context = new UIKitViewSubmitInteractionContext(payload); + break; + case 'executeViewClosedHandler': + context = new UIKitViewCloseInteractionContext(payload); + break; + case 'executeActionButtonHandler': + context = new UIKitActionButtonInteractionContext(payload); + break; + case 'executeLivechatBlockActionHandler': + context = new UIKitLivechatBlockInteractionContext(payload); + break; + } + + try { + return await interactionHandler.call( + app, + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + AppAccessorsInstance.getModifier(), + ); + } catch (e) { + return JsonRpcError.internalError({ message: e.message }); + } +} diff --git a/packages/apps-engine/deno-runtime/handlers/videoconference-handler.ts b/packages/apps-engine/deno-runtime/handlers/videoconference-handler.ts new file mode 100644 index 000000000000..0347519db2e6 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/videoconference-handler.ts @@ -0,0 +1,49 @@ +import { Defined, JsonRpcError } from 'jsonrpc-lite'; +import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders/IVideoConfProvider.ts'; + +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; +import { Logger } from '../lib/logger.ts'; + +export default async function videoConferenceHandler(call: string, params: unknown): Promise { + const [, providerName, methodName] = call.split(':'); + + const provider = AppObjectRegistry.get(`videoConfProvider:${providerName}`); + const logger = AppObjectRegistry.get('logger'); + + if (!provider) { + return new JsonRpcError(`Provider ${providerName} not found`, -32000); + } + + const method = provider[methodName as keyof IVideoConfProvider]; + + if (typeof method !== 'function') { + return JsonRpcError.methodNotFound({ + message: `Method ${methodName} not found on provider ${providerName}`, + }); + } + + const [videoconf, user, options] = params as Array; + + logger?.debug(`Executing ${methodName} on video conference provider...`); + + const args = [...(videoconf ? [videoconf] : []), ...(user ? [user] : []), ...(options ? [options] : [])]; + + try { + // deno-lint-ignore ban-types + const result = await (method as Function).apply(provider, [ + ...args, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getModifier(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + ]); + + logger?.debug(`Video Conference Provider's ${methodName} was successfully executed.`); + + return result; + } catch (e) { + logger?.debug(`Video Conference Provider's ${methodName} was unsuccessful.`); + return new JsonRpcError(e.message, -32000); + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/builders/BlockBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/BlockBuilder.ts new file mode 100644 index 000000000000..e1602860fe97 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/builders/BlockBuilder.ts @@ -0,0 +1,216 @@ +import { v1 as uuid } from 'uuid'; + +import type { + BlockType as _BlockType, + IActionsBlock, + IBlock, + IConditionalBlock, + IConditionalBlockFilters, + IContextBlock, + IImageBlock, + IInputBlock, + ISectionBlock, +} from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks.ts'; +import type { + BlockElementType as _BlockElementType, + IBlockElement, + IButtonElement, + IImageElement, + IInputElement, + IInteractiveElement, + IMultiStaticSelectElement, + IOverflowMenuElement, + IPlainTextInputElement, + ISelectElement, + IStaticSelectElement, +} from '@rocket.chat/apps-engine/definition/uikit/blocks/Elements.ts'; +import type { ITextObject, TextObjectType as _TextObjectType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Objects.ts'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { require } from '../../../lib/require.ts'; + +const { BlockType } = require('@rocket.chat/apps-engine/definition/uikit/blocks/Blocks.js') as { BlockType: typeof _BlockType }; +const { BlockElementType } = require('@rocket.chat/apps-engine/definition/uikit/blocks/Elements.js') as { BlockElementType: typeof _BlockElementType }; +const { TextObjectType } = require('@rocket.chat/apps-engine/definition/uikit/blocks/Objects.js') as { TextObjectType: typeof _TextObjectType }; + +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('id')); + } + + 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/packages/apps-engine/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts new file mode 100644 index 000000000000..e2c2dc021438 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts @@ -0,0 +1,59 @@ +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 type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; +import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; + +import { RoomBuilder } from './RoomBuilder.ts'; +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; + +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/packages/apps-engine/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts new file mode 100644 index 000000000000..a12024ab7b5d --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts @@ -0,0 +1,204 @@ +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; +import type { RoomType as _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'; +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; + +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/packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts new file mode 100644 index 000000000000..98cd919f7b00 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts @@ -0,0 +1,232 @@ +import { LayoutBlock } from '@rocket.chat/ui-kit'; + +import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.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 { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks.ts'; + +import { BlockBuilder } from './BlockBuilder.ts'; +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +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/packages/apps-engine/deno-runtime/lib/accessors/builders/RoomBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/RoomBuilder.ts new file mode 100644 index 000000000000..38983475162d --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/builders/RoomBuilder.ts @@ -0,0 +1,163 @@ +import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.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 { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; + +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +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/packages/apps-engine/deno-runtime/lib/accessors/builders/UserBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/UserBuilder.ts new file mode 100644 index 000000000000..01c11a13f7a3 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/builders/UserBuilder.ts @@ -0,0 +1,81 @@ +import type { IUserBuilder } from '@rocket.chat/apps-engine/definition/accessors/IUserBuilder.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; +import type { IUserSettings } from '@rocket.chat/apps-engine/definition/users/IUserSettings.ts'; +import type { IUserEmail } from '@rocket.chat/apps-engine/definition/users/IUserEmail.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; + +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +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/packages/apps-engine/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts new file mode 100644 index 000000000000..e617cdddf154 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts @@ -0,0 +1,94 @@ +import type { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceBuilder.ts'; +import type { IGroupVideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference.ts'; + +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; + +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +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!, + discussionRid: data.discussionRid, + }; + + 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 setDiscussionRid(rid: AppVideoConference['discussionRid']): IVideoConferenceBuilder { + this.call.discussionRid = rid; + return this; + } + + public getDiscussionRid(): AppVideoConference['discussionRid'] { + return this.call.discussionRid; + } + + public getVideoConference(): AppVideoConference { + return this.call; + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/extenders/HttpExtender.ts b/packages/apps-engine/deno-runtime/lib/accessors/extenders/HttpExtender.ts new file mode 100644 index 000000000000..c323d385de9d --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/extenders/HttpExtender.ts @@ -0,0 +1,62 @@ +import type { + IHttpExtend, + IHttpPreRequestHandler, + IHttpPreResponseHandler +} from "@rocket.chat/apps-engine/definition/accessors/IHttp.ts"; + +export class HttpExtend implements IHttpExtend { + private headers: Map; + + private params: Map; + + private requests: Array; + + private responses: Array; + + constructor() { + this.headers = new Map(); + this.params = new Map(); + this.requests = []; + this.responses = []; + } + + public provideDefaultHeader(key: string, value: string): void { + this.headers.set(key, value); + } + + public provideDefaultHeaders(headers: { [key: string]: string }): void { + Object.keys(headers).forEach((key) => this.headers.set(key, headers[key])); + } + + public provideDefaultParam(key: string, value: string): void { + this.params.set(key, value); + } + + public provideDefaultParams(params: { [key: string]: string }): void { + Object.keys(params).forEach((key) => this.params.set(key, params[key])); + } + + public providePreRequestHandler(handler: IHttpPreRequestHandler): void { + this.requests.push(handler); + } + + public providePreResponseHandler(handler: IHttpPreResponseHandler): void { + this.responses.push(handler); + } + + public getDefaultHeaders(): Map { + return new Map(this.headers); + } + + public getDefaultParams(): Map { + return new Map(this.params); + } + + public getPreRequestHandlers(): Array { + return Array.from(this.requests); + } + + public getPreResponseHandlers(): Array { + return Array.from(this.responses); + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/extenders/MessageExtender.ts b/packages/apps-engine/deno-runtime/lib/accessors/extenders/MessageExtender.ts new file mode 100644 index 000000000000..abf1629c760a --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/extenders/MessageExtender.ts @@ -0,0 +1,66 @@ +import type { IMessageExtender } from '@rocket.chat/apps-engine/definition/accessors/IMessageExtender.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.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 { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +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/packages/apps-engine/deno-runtime/lib/accessors/extenders/RoomExtender.ts b/packages/apps-engine/deno-runtime/lib/accessors/extenders/RoomExtender.ts new file mode 100644 index 000000000000..6509d5dae90e --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/extenders/RoomExtender.ts @@ -0,0 +1,61 @@ +import type { IRoomExtender } from '@rocket.chat/apps-engine/definition/accessors/IRoomExtender.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.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 { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +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/packages/apps-engine/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts b/packages/apps-engine/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts new file mode 100644 index 000000000000..9616bf619067 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts @@ -0,0 +1,69 @@ +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 type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; + +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +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 setDiscussionRid(rid: VideoConference['discussionRid']): IVideoConferenceExtender { + this.videoConference.discussionRid = rid; + + return this; + } + + public getVideoConference(): VideoConference { + return structuredClone(this.videoConference); + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/http.ts b/packages/apps-engine/deno-runtime/lib/accessors/http.ts new file mode 100644 index 000000000000..f55838e60186 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/http.ts @@ -0,0 +1,92 @@ +import type { + IHttp, + IHttpExtend, + IHttpRequest, + IHttpResponse, +} from "@rocket.chat/apps-engine/definition/accessors/IHttp.ts"; +import type { IPersistence } from "@rocket.chat/apps-engine/definition/accessors/IPersistence.ts"; +import type { IRead } from "@rocket.chat/apps-engine/definition/accessors/IRead.ts"; + +import * as Messenger from '../messenger.ts'; +import { AppObjectRegistry } from "../../AppObjectRegistry.ts"; + +type RequestMethod = 'get' | 'post' | 'put' | 'head' | 'delete' | 'patch'; + +export class Http implements IHttp { + private httpExtender: IHttpExtend; + private read: IRead; + private persistence: IPersistence; + private senderFn: typeof Messenger.sendRequest; + + constructor(read: IRead, persistence: IPersistence, httpExtender: IHttpExtend, senderFn: typeof Messenger.sendRequest) { + this.read = read; + this.persistence = persistence; + this.httpExtender = httpExtender; + this.senderFn = senderFn; + // this.httpExtender = new HttpExtend(); + } + + public get(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'get', options); + } + + public put(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'put', options); + } + + public post(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'post', options); + } + + public del(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'delete', options); + } + + public patch(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'patch', options); + } + + private async _processHandler(url: string, method: RequestMethod, options?: IHttpRequest): Promise { + let request = options || {}; + + if (typeof request.headers === 'undefined') { + request.headers = {}; + } + + this.httpExtender.getDefaultHeaders().forEach((value: string, key: string) => { + if (typeof request.headers?.[key] !== 'string') { + request.headers![key] = value; + } + }); + + if (typeof request.params === 'undefined') { + request.params = {}; + } + + this.httpExtender.getDefaultParams().forEach((value: string, key: string) => { + if (typeof request.params?.[key] !== 'string') { + request.params![key] = value; + } + }); + + for (const handler of this.httpExtender.getPreRequestHandlers()) { + request = await handler.executePreHttpRequest(url, request, this.read, this.persistence); + } + + let { result: response } = await this.senderFn({ + method: `bridges:getHttpBridge:doCall`, + params: [{ + appId: AppObjectRegistry.get('id'), + method, + url, + request, + }], + }) + + for (const handler of this.httpExtender.getPreResponseHandlers()) { + response = await handler.executePreHttpResponse(response as IHttpResponse, this.read, this.persistence); + } + + return response as IHttpResponse; + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/mod.ts b/packages/apps-engine/deno-runtime/lib/accessors/mod.ts new file mode 100644 index 000000000000..e71f014421ab --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/mod.ts @@ -0,0 +1,302 @@ +import type { IAppAccessors } from '@rocket.chat/apps-engine/definition/accessors/IAppAccessors.ts'; +import type { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api/IApiEndpointMetadata.ts'; +import type { IEnvironmentWrite } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentWrite.ts'; +import type { IEnvironmentRead } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentRead.ts'; +import type { IConfigurationModify } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationModify.ts'; +import type { IRead } from '@rocket.chat/apps-engine/definition/accessors/IRead.ts'; +import type { IModify } from '@rocket.chat/apps-engine/definition/accessors/IModify.ts'; +import type { INotifier } from '@rocket.chat/apps-engine/definition/accessors/INotifier.ts'; +import type { IPersistence } from '@rocket.chat/apps-engine/definition/accessors/IPersistence.ts'; +import type { IHttp, IHttpExtend } from '@rocket.chat/apps-engine/definition/accessors/IHttp.ts'; +import type { IConfigurationExtend } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationExtend.ts'; +import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands/ISlashCommand.ts'; +import type { IProcessor } from '@rocket.chat/apps-engine/definition/scheduler/IProcessor.ts'; +import type { IApi } from '@rocket.chat/apps-engine/definition/api/IApi.ts'; +import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders/IVideoConfProvider.ts'; + +import { Http } from './http.ts'; +import { HttpExtend } from './extenders/HttpExtender.ts'; +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'; +import { Notifier } from './notifier.ts'; + +const httpMethods = ['get', 'post', 'put', 'delete', 'head', 'options', 'patch'] as const; + +// We need to create this object first thing, as we'll handle references to it later on +if (!AppObjectRegistry.has('apiEndpoints')) { + AppObjectRegistry.set('apiEndpoints', []); +} + +export class AppAccessors { + private defaultAppAccessors?: IAppAccessors; + private environmentRead?: IEnvironmentRead; + private environmentWriter?: IEnvironmentWrite; + private configModifier?: IConfigurationModify; + private configExtender?: IConfigurationExtend; + private reader?: IRead; + private modifier?: IModify; + private persistence?: IPersistence; + private creator?: ModifyCreator; + private updater?: ModifyUpdater; + private extender?: ModifyExtender; + private httpExtend: IHttpExtend = new HttpExtend(); + private http?: IHttp; + private notifier?: INotifier; + + private proxify: (namespace: string, overrides?: Record unknown>) => T; + + constructor(private readonly senderFn: typeof Messenger.sendRequest) { + this.proxify = (namespace: string, overrides: Record unknown> = {}): T => + new Proxy( + { __kind: `accessor:${namespace}` }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => { + // We don't want to send a request for this prop + if (prop === 'toJSON') { + return {}; + } + + // If the prop is inteded to be overriden by the caller + if (prop in overrides) { + return overrides[prop].apply(undefined, params); + } + + return senderFn({ + method: `accessor:${namespace}:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { throw new Error(err.error) }); + }, + }, + ) as T; + + this.http = new Http(this.getReader(), this.getPersistence(), this.httpExtend, this.getSenderFn()); + this.notifier = new Notifier(this.getSenderFn()); + } + + public getSenderFn() { + return this.senderFn; + } + + public getEnvironmentRead(): IEnvironmentRead { + if (!this.environmentRead) { + this.environmentRead = { + getSettings: () => this.proxify('getEnvironmentRead:getSettings'), + getServerSettings: () => this.proxify('getEnvironmentRead:getServerSettings'), + getEnvironmentVariables: () => this.proxify('getEnvironmentRead:getEnvironmentVariables'), + }; + } + + return this.environmentRead; + } + + public getEnvironmentWrite() { + if (!this.environmentWriter) { + this.environmentWriter = { + getSettings: () => this.proxify('getEnvironmentWrite:getSettings'), + getServerSettings: () => this.proxify('getEnvironmentWrite:getServerSettings'), + }; + } + + return this.environmentWriter; + } + + public getConfigurationModify() { + if (!this.configModifier) { + this.configModifier = { + scheduler: this.proxify('getConfigurationModify:scheduler'), + slashCommands: { + _proxy: this.proxify('getConfigurationModify:slashCommands'), + modifySlashCommand(slashcommand: ISlashCommand) { + // Store the slashcommand instance to use when the Apps-Engine calls the slashcommand + AppObjectRegistry.set(`slashcommand:${slashcommand.command}`, slashcommand); + + return this._proxy.modifySlashCommand(slashcommand); + }, + disableSlashCommand(command: string) { + return this._proxy.disableSlashCommand(command); + }, + enableSlashCommand(command: string) { + return this._proxy.enableSlashCommand(command); + }, + }, + serverSettings: this.proxify('getConfigurationModify:serverSettings'), + }; + } + + return this.configModifier; + } + + public getConfigurationExtend() { + if (!this.configExtender) { + const senderFn = this.senderFn; + + this.configExtender = { + ui: this.proxify('getConfigurationExtend:ui'), + http: this.httpExtend, + settings: this.proxify('getConfigurationExtend:settings'), + externalComponents: this.proxify('getConfigurationExtend:externalComponents'), + api: { + _proxy: this.proxify('getConfigurationExtend:api'), + async provideApi(api: IApi) { + const apiEndpoints = AppObjectRegistry.get('apiEndpoints')!; + + api.endpoints.forEach((endpoint) => { + endpoint._availableMethods = httpMethods.filter((method) => typeof endpoint[method] === 'function'); + + // We need to keep a reference to the endpoint around for us to call the executor later + AppObjectRegistry.set(`api:${endpoint.path}`, endpoint); + }); + + const result = await this._proxy.provideApi(api); + + // Let's call the listApis method to cache the info from the endpoints + // Also, since this is a side-effect, we do it async so we can return to the caller + senderFn({ method: 'accessor:api:listApis' }) + .then((response) => apiEndpoints.push(...(response.result as IApiEndpointMetadata[]))) + .catch((err) => err.error); + + return result; + }, + }, + scheduler: { + _proxy: this.proxify('getConfigurationExtend:scheduler'), + registerProcessors(processors: IProcessor[]) { + // Store the processor instance to use when the Apps-Engine calls the processor + processors.forEach((processor) => { + AppObjectRegistry.set(`scheduler:${processor.id}`, processor); + }); + + return this._proxy.registerProcessors(processors); + }, + }, + videoConfProviders: { + _proxy: this.proxify('getConfigurationExtend:videoConfProviders'), + provideVideoConfProvider(provider: IVideoConfProvider) { + // Store the videoConfProvider instance to use when the Apps-Engine calls the videoConfProvider + AppObjectRegistry.set(`videoConfProvider:${provider.name}`, provider); + + return this._proxy.provideVideoConfProvider(provider); + }, + }, + slashCommands: { + _proxy: this.proxify('getConfigurationExtend:slashCommands'), + provideSlashCommand(slashcommand: ISlashCommand) { + // Store the slashcommand instance to use when the Apps-Engine calls the slashcommand + AppObjectRegistry.set(`slashcommand:${slashcommand.command}`, slashcommand); + + return this._proxy.provideSlashCommand(slashcommand); + }, + }, + }; + } + + return this.configExtender; + } + + public getDefaultAppAccessors() { + if (!this.defaultAppAccessors) { + this.defaultAppAccessors = { + environmentReader: this.getEnvironmentRead(), + environmentWriter: this.getEnvironmentWrite(), + reader: this.getReader(), + http: this.getHttp(), + providedApiEndpoints: AppObjectRegistry.get('apiEndpoints') as IApiEndpointMetadata[], + }; + } + + return this.defaultAppAccessors; + } + + public getReader() { + if (!this.reader) { + this.reader = { + getEnvironmentReader: () => ({ + getSettings: () => this.proxify('getReader:getEnvironmentReader:getSettings'), + getServerSettings: () => this.proxify('getReader:getEnvironmentReader:getServerSettings'), + getEnvironmentVariables: () => this.proxify('getReader:getEnvironmentReader:getEnvironmentVariables'), + }), + getMessageReader: () => this.proxify('getReader:getMessageReader'), + getPersistenceReader: () => this.proxify('getReader:getPersistenceReader'), + getRoomReader: () => this.proxify('getReader:getRoomReader'), + getUserReader: () => this.proxify('getReader:getUserReader'), + getNotifier: () => this.getNotifier(), + getLivechatReader: () => this.proxify('getReader:getLivechatReader'), + getUploadReader: () => this.proxify('getReader:getUploadReader'), + getCloudWorkspaceReader: () => this.proxify('getReader:getCloudWorkspaceReader'), + getVideoConferenceReader: () => this.proxify('getReader:getVideoConferenceReader'), + getOAuthAppsReader: () => this.proxify('getReader:getOAuthAppsReader'), + getThreadReader: () => this.proxify('getReader:getThreadReader'), + getRoleReader: () => this.proxify('getReader:getRoleReader'), + }; + } + + return this.reader; + } + + public getModifier() { + if (!this.modifier) { + this.modifier = { + getCreator: this.getCreator.bind(this), + getUpdater: this.getUpdater.bind(this), + getExtender: this.getExtender.bind(this), + getDeleter: () => this.proxify('getModifier:getDeleter'), + getNotifier: () => this.getNotifier(), + getUiController: () => this.proxify('getModifier:getUiController'), + getScheduler: () => this.proxify('getModifier:getScheduler'), + getOAuthAppsModifier: () => this.proxify('getModifier:getOAuthAppsModifier'), + getModerationModifier: () => this.proxify('getModifier:getModerationModifier'), + }; + } + + return this.modifier; + } + + public getPersistence() { + if (!this.persistence) { + this.persistence = this.proxify('getPersistence'); + } + + return this.persistence; + } + + public getHttp() { + return this.http; + } + + private getCreator() { + if (!this.creator) { + this.creator = new ModifyCreator(this.senderFn); + } + + return this.creator; + } + + private getUpdater() { + if (!this.updater) { + this.updater = new ModifyUpdater(this.senderFn); + } + + return this.updater; + } + + private getExtender() { + if (!this.extender) { + this.extender = new ModifyExtender(this.senderFn); + } + + return this.extender; + } + + private getNotifier() { + return this.notifier; + } +} + +export const AppAccessorsInstance = new AppAccessors(Messenger.sendRequest); diff --git a/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts new file mode 100644 index 000000000000..06797551a621 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts @@ -0,0 +1,344 @@ +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 { IEmailCreator } from '@rocket.chat/apps-engine/definition/accessors/IEmailCreator.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 type { UserType as _UserType } from '@rocket.chat/apps-engine/definition/users/UserType.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; +import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts'; +import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts'; +import type { IUserBuilder } from '@rocket.chat/apps-engine/definition/accessors/IUserBuilder.ts'; +import type { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceBuilder.ts'; +import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; +import type { ILivechatMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/ILivechatMessageBuilder.ts'; +import type { UIHelper as _UIHelper } from '@rocket.chat/apps-engine/server/misc/UIHelper.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'; +import { require } from '../../../lib/require.ts'; + +const { UIHelper } = require('@rocket.chat/apps-engine/server/misc/UIHelper.js') as { UIHelper: typeof _UIHelper }; +const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; +const { UserType } = require('@rocket.chat/apps-engine/definition/users/UserType.js') as { UserType: typeof _UserType }; +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class ModifyCreator implements IModifyCreator { + constructor(private readonly senderFn: typeof Messenger.sendRequest) { } + + getLivechatCreator(): ILivechatCreator { + return new Proxy( + { __kind: 'getLivechatCreator' }, + { + get: (_target: unknown, prop: string) => { + // It's not worthwhile to make an asynchronous request for such a simple method + if (prop === 'createToken') { + return () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); + } + + if (prop === 'toJSON') { + return () => ({}); + } + + return (...params: unknown[]) => + this.senderFn({ + method: `accessor:getModifier:getCreator:getLivechatCreator:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw new Error(err.error); + }); + }, + }, + ) as ILivechatCreator; + } + + getUploadCreator(): IUploadCreator { + return new Proxy( + { __kind: 'getUploadCreator' }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + prop === 'toJSON' + ? {} + : this.senderFn({ + method: `accessor:getModifier:getCreator:getUploadCreator:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw new Error(err.error); + }), + }, + ) as IUploadCreator; + } + + getEmailCreator(): IEmailCreator { + return new Proxy( + { __kind: 'getEmailCreator' }, + { + get: (_target: unknown, prop: string) => + (...params: unknown[]) => + prop === 'toJSON' + ? {} + : this.senderFn({ + method: `accessor:getModifier:getCreator:getEmailCreator:${prop}`, + params + }) + .then((response) => response.result) + .catch((err) => { + throw new Error(err.error); + }), + } + ) + } + + 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) { + if (data) { + delete data.id; + } + + return new DiscussionBuilder(data); + } + + startVideoConference(data?: Partial) { + return new VideoConferenceBuilder(data); + } + + startBotUser(data?: Partial) { + 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 { + 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 { + 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('id') || ''); + } + + const response = await this.senderFn({ + method: 'bridges:getMessageBridge:doCreate', + params: [result, AppObjectRegistry.get('id')], + }); + + return String(response.result); + } + + private async _finishLivechatMessage(builder: ILivechatMessageBuilder): Promise { + 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('id')], + }); + + return String(response.result); + } + + private async _finishRoom(builder: IRoomBuilder): Promise { + 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('id')], + }); + + return String(response.result); + } + + private async _finishDiscussion(builder: IDiscussionBuilder): Promise { + 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('id')], + }); + + return String(response.result); + } + + private async _finishVideoConference(builder: IVideoConferenceBuilder): Promise { + 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('id')], + }); + + return String(response.result); + } + + private async _finishUser(builder: IUserBuilder): Promise { + const user = builder.getUser(); + + const response = await this.senderFn({ + method: 'bridges:getUserBridge:doCreate', + params: [user, AppObjectRegistry.get('id')], + }); + + return String(response.result); + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyExtender.ts b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyExtender.ts new file mode 100644 index 000000000000..c0793d015c64 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyExtender.ts @@ -0,0 +1,93 @@ +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 type { RocketChatAssociationModel as _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'; +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +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('id')], + }); + + 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('id')], + }); + + 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('id')], + }); + + 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('id')], + }); + break; + case RocketChatAssociationModel.ROOM: + await this.senderFn({ + method: 'bridges:getRoomBridge:doUpdate', + params: [ + (extender as IRoomExtender).getRoom(), + (extender as IRoomExtender).getUsernamesOfMembersBeingAdded(), + AppObjectRegistry.get('id'), + ], + }); + break; + case RocketChatAssociationModel.VIDEO_CONFERENCE: + await this.senderFn({ + method: 'bridges:getVideoConferenceBridge:doUpdate', + params: [(extender as IVideoConferenceExtender).getVideoConference(), AppObjectRegistry.get('id')], + }); + break; + default: + throw new Error('Invalid extender passed to the ModifyExtender.finish function.'); + } + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyUpdater.ts b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyUpdater.ts new file mode 100644 index 000000000000..8befe7bfa983 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyUpdater.ts @@ -0,0 +1,153 @@ +import type { IModifyUpdater } from '@rocket.chat/apps-engine/definition/accessors/IModifyUpdater.ts'; +import type { ILivechatUpdater } from '@rocket.chat/apps-engine/definition/accessors/ILivechatUpdater.ts'; +import type { IUserUpdater } from '@rocket.chat/apps-engine/definition/accessors/IUserUpdater.ts'; +import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts'; +import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; + +import type { UIHelper as _UIHelper } from '@rocket.chat/apps-engine/server/misc/UIHelper.ts'; +import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; + +import * as Messenger from '../../messenger.ts'; + +import { MessageBuilder } from '../builders/MessageBuilder.ts'; +import { RoomBuilder } from '../builders/RoomBuilder.ts'; +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; + +import { require } from '../../../lib/require.ts'; + +const { UIHelper } = require('@rocket.chat/apps-engine/server/misc/UIHelper.js') as { UIHelper: typeof _UIHelper }; +const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class ModifyUpdater implements IModifyUpdater { + constructor(private readonly senderFn: typeof Messenger.sendRequest) { } + + public getLivechatUpdater(): ILivechatUpdater { + return new Proxy( + { __kind: 'getLivechatUpdater' }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + prop === 'toJSON' + ? {} + : this.senderFn({ + method: `accessor:getModifier:getUpdater:getLivechatUpdater:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw new Error(err.error); + }), + }, + ) as ILivechatUpdater; + } + + public getUserUpdater(): IUserUpdater { + return new Proxy( + { __kind: 'getUserUpdater' }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + prop === 'toJSON' + ? {} + : this.senderFn({ + method: `accessor:getModifier:getUpdater:getUserUpdater:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw new Error(err.error); + }), + }, + ) as IUserUpdater; + } + + public async message(messageId: string, _updater: IUser): Promise { + const response = await this.senderFn({ + method: 'bridges:getMessageBridge:doGetById', + params: [messageId, AppObjectRegistry.get('id')], + }); + + return new MessageBuilder(response.result as IMessage); + } + + public async room(roomId: string, _updater: IUser): Promise { + const response = await this.senderFn({ + method: 'bridges:getRoomBridge:doGetById', + params: [roomId, AppObjectRegistry.get('id')], + }); + + return new RoomBuilder(response.result as IRoom); + } + + public finish(builder: IMessageBuilder | IRoomBuilder): Promise { + switch (builder.kind) { + case RocketChatAssociationModel.MESSAGE: + return this._finishMessage(builder as IMessageBuilder); + case RocketChatAssociationModel.ROOM: + return this._finishRoom(builder as IRoomBuilder); + default: + throw new Error('Invalid builder passed to the ModifyUpdater.finish function.'); + } + } + + private async _finishMessage(builder: IMessageBuilder): Promise { + const result = builder.getMessage(); + + if (!result.id) { + throw new Error("Invalid message, can't update a message without an id."); + } + + if (!result.sender?.id) { + throw new Error('Invalid sender assigned to the message.'); + } + + if (result.blocks?.length) { + result.blocks = UIHelper.assignIds(result.blocks, AppObjectRegistry.get('id') || ''); + } + + await this.senderFn({ + method: 'bridges:getMessageBridge:doUpdate', + params: [result, AppObjectRegistry.get('id')], + }); + } + + private async _finishRoom(builder: IRoomBuilder): Promise { + const result = builder.getRoom(); + + if (!result.id) { + throw new Error("Invalid room, can't update a room without an id."); + } + + if (!result.type) { + throw new Error('Invalid type assigned to the room.'); + } + + if (result.type !== RoomType.LIVE_CHAT) { + if (!result.creator || !result.creator.id) { + throw new Error('Invalid creator assigned to the room.'); + } + + if (!result.slugifiedName || !result.slugifiedName.trim()) { + throw new Error('Invalid slugifiedName assigned to the room.'); + } + } + + if (!result.displayName || !result.displayName.trim()) { + throw new Error('Invalid displayName assigned to the room.'); + } + + await this.senderFn({ + method: 'bridges:getRoomBridge:doUpdate', + params: [result, builder.getMembersToBeAddedUsernames(), AppObjectRegistry.get('id')], + }); + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/notifier.ts b/packages/apps-engine/deno-runtime/lib/accessors/notifier.ts new file mode 100644 index 000000000000..625d68c1039f --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/notifier.ts @@ -0,0 +1,75 @@ +import type { IMessageBuilder, INotifier } from '@rocket.chat/apps-engine/definition/accessors'; +import type { ITypingOptions } from '@rocket.chat/apps-engine/definition/accessors/INotifier.ts'; +import type { _TypingScope } from '@rocket.chat/apps-engine/definition/accessors/INotifier.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import { MessageBuilder } from './builders/MessageBuilder.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import * as Messenger from '../messenger.ts'; +import { require } from "../require.ts"; + +const { TypingScope } = require('@rocket.chat/apps-engine/definition/accessors/INotifier.js') as { + TypingScope: typeof _TypingScope; +}; + +export class Notifier implements INotifier { + private senderFn: typeof Messenger.sendRequest; + + constructor(senderFn: typeof Messenger.sendRequest) { + this.senderFn = senderFn; + } + + public async notifyUser(user: IUser, message: IMessage): Promise { + if (!message.sender || !message.sender.id) { + const appUser = await this.getAppUser(); + + message.sender = appUser; + } + + await this.callMessageBridge('doNotifyUser', [user, message, AppObjectRegistry.get('id')]); + } + + public async notifyRoom(room: IRoom, message: IMessage): Promise { + if (!message.sender || !message.sender.id) { + const appUser = await this.getAppUser(); + + message.sender = appUser; + } + + await this.callMessageBridge('doNotifyRoom', [room, message, AppObjectRegistry.get('id')]); + } + + public async typing(options: ITypingOptions): Promise<() => Promise> { + options.scope = options.scope || TypingScope.Room; + + if (!options.username) { + const appUser = await this.getAppUser(); + options.username = (appUser && appUser.name) || ''; + } + + const appId = AppObjectRegistry.get('id'); + + await this.callMessageBridge('doTyping', [{ ...options, isTyping: true }, appId]); + + return async () => { + await this.callMessageBridge('doTyping', [{ ...options, isTyping: false }, appId]); + }; + } + + public getMessageBuilder(): IMessageBuilder { + return new MessageBuilder(); + } + + private async callMessageBridge(method: string, params: Array): Promise { + await this.senderFn({ + method: `bridges:getMessageBridge:${method}`, + params, + }); + } + + private async getAppUser(): Promise { + const response = await this.senderFn({ method: 'bridges:getUserBridge:doGetAppUser', params: [AppObjectRegistry.get('id')] }); + return response.result; + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/tests/AppAccessors.test.ts b/packages/apps-engine/deno-runtime/lib/accessors/tests/AppAccessors.test.ts new file mode 100644 index 000000000000..04592eadd3db --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/tests/AppAccessors.test.ts @@ -0,0 +1,122 @@ +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'; + +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); + }, + }); + + 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'); + + assertEquals(room, { + params: ['123'], + method: 'accessor:getReader:getRoomReader:getById', + }); + }); + + it('creates the correct format for IEnvironmentRead calls from IRead', async () => { + const reader = appAccessors.getReader().getEnvironmentReader().getEnvironmentVariables(); + const room = await reader.getValueByName('NODE_ENV'); + + assertEquals(room, { + params: ['NODE_ENV'], + method: 'accessor:getReader:getEnvironmentReader:getEnvironmentVariables:getValueByName', + }); + }); + + it('creates the correct format for IEvironmentRead calls', async () => { + const envRead = appAccessors.getEnvironmentRead(); + const env = await envRead.getServerSettings().getValueById('123'); + + assertEquals(env, { + params: ['123'], + method: 'accessor:getEnvironmentRead:getServerSettings:getValueById', + }); + }); + + it('creates the correct format for IEvironmentWrite calls', async () => { + const envRead = appAccessors.getEnvironmentWrite(); + const env = await envRead.getServerSettings().incrementValue('123', 6); + + assertEquals(env, { + params: ['123', 6], + method: 'accessor:getEnvironmentWrite:getServerSettings:incrementValue', + }); + }); + + it('creates the correct format for IConfigurationModify calls', async () => { + const configModify = appAccessors.getConfigurationModify(); + const command = await configModify.slashCommands.modifySlashCommand({ + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + }); + + assertEquals(command, { + params: [ + { + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + }, + ], + method: 'accessor:getConfigurationModify:slashCommands:modifySlashCommand', + }); + }); + + it('correctly stores a reference to a slashcommand object and sends a request via proxy', async () => { + const configExtend = appAccessors.getConfigurationExtend(); + + const slashcommand = { + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + executor() { + return Promise.resolve(); + }, + }; + + const result = await configExtend.slashCommands.provideSlashCommand(slashcommand); + + assertEquals(AppObjectRegistry.get('slashcommand:test'), slashcommand); + + // The function will not be serialized and sent to the main process + delete result.params[0].executor; + + assertEquals(result, { + method: 'accessor:getConfigurationExtend:slashCommands:provideSlashCommand', + params: [ + { + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + }, + ], + }); + }); +}); diff --git a/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts b/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts new file mode 100644 index 000000000000..5927869e6c84 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts @@ -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('id', '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, { + 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, { + 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}"`); + }); +}); diff --git a/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts b/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts new file mode 100644 index 000000000000..1ec056e02ce3 --- /dev/null +++ b/packages/apps-engine/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('id', '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/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts b/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts new file mode 100644 index 000000000000..313275c967cf --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts @@ -0,0 +1,128 @@ +// deno-lint-ignore-file no-explicit-any +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { assertSpyCall, spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; +import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod.ts'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { ModifyUpdater } from '../modify/ModifyUpdater.ts'; + +describe('ModifyUpdater', () => { + let modifyUpdater: ModifyUpdater; + + const senderFn = (r: any) => + Promise.resolve({ + id: Math.random().toString(36).substring(2), + jsonrpc: '2.0', + result: structuredClone(r), + serialize() { + return JSON.stringify(this); + }, + }); + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('id', 'deno-test'); + modifyUpdater = new ModifyUpdater(senderFn); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('correctly formats requests for the update message flow', async () => { + const _spy = spy(modifyUpdater, 'senderFn' as keyof ModifyUpdater); + + const messageBuilder = await modifyUpdater.message('123', { id: '456' } as any); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getMessageBridge:doGetById', + params: ['123', 'deno-test'], + }, + ], + }); + + messageBuilder.setUpdateData( + { + id: '123', + room: { id: '123' }, + sender: { id: '456' }, + text: 'Hello World', + }, + { + id: '456', + }, + ); + + await modifyUpdater.finish(messageBuilder); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getMessageBridge:doUpdate', + params: [messageBuilder.getMessage(), 'deno-test'], + }, + ], + }); + + _spy.restore(); + }); + + it('correctly formats requests for the update room flow', async () => { + const _spy = spy(modifyUpdater, 'senderFn' as keyof ModifyUpdater); + + const roomBuilder = await modifyUpdater.room('123', { id: '456' } as any); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getRoomBridge:doGetById', + params: ['123', 'deno-test'], + }, + ], + }); + + roomBuilder.setData({ + id: '123', + type: 'c', + displayName: 'Test Room', + slugifiedName: 'test-room', + creator: { id: '456' }, + }); + + roomBuilder.setMembersToBeAddedByUsernames(['username1', 'username2']); + + // We need to sneak in the id as the `modifyUpdater.room` call won't have legitimate data + roomBuilder.getRoom().id = '123'; + + await modifyUpdater.finish(roomBuilder); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getRoomBridge:doUpdate', + params: [roomBuilder.getRoom(), roomBuilder.getMembersToBeAddedUsernames(), 'deno-test'], + }, + ], + }); + }); + + it('correctly formats requests to UserUpdater methods', async () => { + const result = await modifyUpdater.getUserUpdater().updateStatusText({ id: '123' } as any, 'Hello World') as any; + + assertEquals(result, { + method: 'accessor:getModifier:getUpdater:getUserUpdater:updateStatusText', + params: [{ id: '123' }, 'Hello World'], + }); + }); + + it('correctly formats requests to LivechatUpdater methods', async () => { + const result = await modifyUpdater.getLivechatUpdater().closeRoom({ id: '123' } as any, 'close it!') as any; + + assertEquals(result, { + method: 'accessor:getModifier:getUpdater:getLivechatUpdater:closeRoom', + params: [{ id: '123' }, 'close it!'], + }); + }); +}); diff --git a/packages/apps-engine/deno-runtime/lib/ast/mod.ts b/packages/apps-engine/deno-runtime/lib/ast/mod.ts new file mode 100644 index 000000000000..09f4994f2bad --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/ast/mod.ts @@ -0,0 +1,64 @@ +import { generate } from "astring"; +// @deno-types="../../acorn.d.ts" +import { Program, parse } from "acorn"; +// @deno-types="../../acorn-walk.d.ts" +import { fullAncestor } from "acorn-walk"; + +import * as operations from "./operations.ts"; +import type { WalkerState } from "./operations.ts"; + +function fixAst(ast: Program): boolean { + const pendingOperations = [ + operations.fixLivechatIsOnlineCalls, + operations.checkReassignmentOfModifiedIdentifiers, + operations.fixRoomUsernamesCalls, + ]; + + // Have we touched the tree? + let isModified = false; + + while (pendingOperations.length) { + const ops = pendingOperations.splice(0); + const state: WalkerState = { + isModified: false, + functionIdentifiers: new Set(), + }; + + fullAncestor(ast, (node, state, ancestors, type) => { + ops.forEach(operation => operation(node, state, ancestors, type)); + }, undefined, state); + + if (state.isModified) { + isModified = true; + } + + if (state.functionIdentifiers.size) { + pendingOperations.push( + operations.buildFixModifiedFunctionsOperation(state.functionIdentifiers), + operations.checkReassignmentOfModifiedIdentifiers + ); + } + } + + return isModified; +} + +export function fixBrokenSynchronousAPICalls(appSource: string): string { + const astRootNode = parse(appSource, { + ecmaVersion: 2017, + // Allow everything, we don't want to complain if code is badly written + // Also, since the code itself has been transpiled, the chance of getting + // shenanigans is lower + allowReserved: true, + allowReturnOutsideFunction: true, + allowImportExportEverywhere: true, + allowAwaitOutsideFunction: true, + allowSuperOutsideMethod: true, + }); + + if (fixAst(astRootNode)) { + return generate(astRootNode); + } + + return appSource; +} diff --git a/packages/apps-engine/deno-runtime/lib/ast/operations.ts b/packages/apps-engine/deno-runtime/lib/ast/operations.ts new file mode 100644 index 000000000000..d3886348041f --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/ast/operations.ts @@ -0,0 +1,239 @@ +// @deno-types="../../acorn.d.ts" +import { AnyNode, AssignmentExpression, AwaitExpression, Expression, Function, Identifier, MethodDefinition, Property } from 'acorn'; +// @deno-types="../../acorn-walk.d.ts" +import { FullAncestorWalkerCallback } from 'acorn-walk'; + +export type WalkerState = { + isModified: boolean; + functionIdentifiers: Set; +}; + +export function getFunctionIdentifier(ancestors: AnyNode[], functionNodeIndex: number) { + const parent = ancestors[functionNodeIndex - 1]; + + // If there is a parent node and it's not a computed property, we can try to + // extract an identifier for our function from it. This needs to be done first + // because when functions are assigned to named symbols, this will be the only + // way to call it, even if the function itself has an identifier + // Consider the following block: + // + // const foo = function bar() {} + // + // Even though the function itself has a name, the only way to call it in the + // program is wiht `foo()` + if (parent && !(parent as Property | MethodDefinition).computed) { + // Several node types can have an id prop of type Identifier + const { id } = parent as unknown as { id?: Identifier }; + if (id?.type === 'Identifier') { + return id.name; + } + + // Usually assignments to object properties (MethodDefinition, Property) + const { key } = parent as MethodDefinition | Property; + if (key?.type === 'Identifier') { + return key.name; + } + + // Variable assignments have left hand side that can be used as Identifier + const { left } = parent as AssignmentExpression; + + // Simple assignment: `const fn = () => {}` + if (left?.type === 'Identifier') { + return left.name; + } + + // Object property assignment: `obj.fn = () => {}` + if (left?.type === 'MemberExpression' && !left.computed) { + return (left.property as Identifier).name; + } + } + + // nodeIndex needs to be the index of a Function node (either FunctionDeclaration or FunctionExpression) + const currentNode = ancestors[functionNodeIndex] as Function; + + // Function declarations or expressions can be directly named + if (currentNode.id?.type === 'Identifier') { + return currentNode.id.name; + } +} + +export function wrapWithAwait(node: Expression) { + if (!node.type.endsWith('Expression')) { + throw new Error(`Can't wrap "${node.type}" with await`); + } + + const innerNode: Expression = { ...node }; + + node.type = 'AwaitExpression'; + // starting here node has become an AwaitExpression + (node as AwaitExpression).argument = innerNode; + + Object.keys(node).forEach((key) => !['type', 'argument'].includes(key) && delete node[key as keyof AnyNode]); +} + +export function asyncifyScope(ancestors: AnyNode[], state: WalkerState) { + const functionNodeIndex = ancestors.findLastIndex((n) => 'async' in n); + if (functionNodeIndex === -1) return; + + // At this point this is a node with an "async" property, so it has to be + // of type Function - let TS know about that + const functionScopeNode = ancestors[functionNodeIndex] as Function; + + if (functionScopeNode.async) { + return; + } + + functionScopeNode.async = true; + + // If the parent of a function node is a call expression, we're talking about an IIFE + // Should we care about this case as well? + // const parentNode = ancestors[functionScopeIndex-1]; + // if (parentNode?.type === 'CallExpression' && ancestors[functionScopeIndex-2] && ancestors[functionScopeIndex-2].type !== 'AwaitExpression') { + // pendingOperations.push(buildFunctionPredicate(getFunctionIdentifier(ancestors, functionScopeIndex-2))); + // } + + const identifier = getFunctionIdentifier(ancestors, functionNodeIndex); + + // We can't fix calls of functions which name we can't determine at compile time + if (!identifier) return; + + state.functionIdentifiers.add(identifier); +} + +export function buildFixModifiedFunctionsOperation(functionIdentifiers: Set): FullAncestorWalkerCallback { + return function _fixModifiedFunctionsOperation(node, state, ancestors) { + if (node.type !== 'CallExpression') return; + + let isWrappable = false; + + // This node is a simple call to a function, like `fn()` + isWrappable = node.callee.type === 'Identifier' && functionIdentifiers.has(node.callee.name); + + // This node is a call to an object property or instance method, like `obj.fn()`, but not computed like `obj[fn]()` + isWrappable ||= + node.callee.type === 'MemberExpression' && + !node.callee.computed && + node.callee.property?.type === 'Identifier' && + functionIdentifiers.has(node.callee.property.name); + + // This is a weird dereferencing technique used by bundlers, and since we'll be dealing with bundled sources we have to check for it + // e.g. `r=(0,fn)(e)` + if (!isWrappable && node.callee.type === 'SequenceExpression') { + const [, secondExpression] = node.callee.expressions; + isWrappable = secondExpression?.type === 'Identifier' && functionIdentifiers.has(secondExpression.name); + isWrappable ||= + secondExpression?.type === 'MemberExpression' && + !secondExpression.computed && + secondExpression.property.type === 'Identifier' && + functionIdentifiers.has(secondExpression.property.name); + } + + if (!isWrappable) return; + + // ancestors[ancestors.length-1] === node, so here we're checking for parent node + const parentNode = ancestors[ancestors.length - 2]; + if (!parentNode || parentNode.type === 'AwaitExpression') return; + + wrapWithAwait(node); + asyncifyScope(ancestors, state); + + state.isModified = true; + }; +} + +export const checkReassignmentOfModifiedIdentifiers: FullAncestorWalkerCallback = (node, { functionIdentifiers }, _ancestors) => { + if (node.type === 'AssignmentExpression') { + if (node.operator !== '=') return; + + let identifier = ''; + + if (node.left.type === 'Identifier') identifier = node.left.name; + + if (node.left.type === 'MemberExpression' && !node.left.computed) { + identifier = (node.left.property as Identifier).name; + } + + if (!identifier || node.right.type !== 'Identifier' || !functionIdentifiers.has(node.right.name)) return; + + functionIdentifiers.add(identifier); + + return; + } + + if (node.type === 'VariableDeclarator') { + if (node.id.type !== 'Identifier' || functionIdentifiers.has(node.id.name)) return; + + if (node.init?.type !== 'Identifier' || !functionIdentifiers.has(node.init?.name)) return; + + functionIdentifiers.add(node.id.name); + + return; + } + + // "Property" is for plain objects, "PropertyDefinition" is for classes + // but both share the same structure + if (node.type === 'Property' || node.type === 'PropertyDefinition') { + if (node.key.type !== 'Identifier' || functionIdentifiers.has(node.key.name)) return; + + if (node.value?.type !== 'Identifier' || !functionIdentifiers.has(node.value.name)) return; + + functionIdentifiers.add(node.key.name); + + return; + } +}; + +export const fixLivechatIsOnlineCalls: FullAncestorWalkerCallback = (node, state, ancestors) => { + if (node.type !== 'MemberExpression' || node.computed) return; + + if ((node.property as Identifier).name !== 'isOnline') return; + + if (node.object.type !== 'CallExpression') return; + + if (node.object.callee.type !== 'MemberExpression') return; + + if ((node.object.callee.property as Identifier).name !== 'getLivechatReader') return; + + let parentIndex = ancestors.length - 2; + let targetNode = ancestors[parentIndex]; + + if (targetNode.type !== 'CallExpression') { + targetNode = node; + } else { + parentIndex--; + } + + // If we're already wrapped with an await, nothing to do + if (ancestors[parentIndex].type === 'AwaitExpression') return; + + // If we're in the middle of a chained member access, we can't wrap with await + if (ancestors[parentIndex].type === 'MemberExpression') return; + + wrapWithAwait(targetNode); + asyncifyScope(ancestors, state); + + state.isModified = true; +}; + +export const fixRoomUsernamesCalls: FullAncestorWalkerCallback = (node, state, ancestors) => { + if (node.type !== 'MemberExpression' || node.computed) return; + + if ((node.property as Identifier).name !== 'usernames') return; + + let parentIndex = ancestors.length - 2; + let targetNode = ancestors[parentIndex]; + + if (targetNode.type !== 'CallExpression') { + targetNode = node; + } else { + parentIndex--; + } + + // If we're already wrapped with an await, nothing to do + if (ancestors[parentIndex].type === 'AwaitExpression') return; + + wrapWithAwait(targetNode); + asyncifyScope(ancestors, state); + + state.isModified = true; +} diff --git a/packages/apps-engine/deno-runtime/lib/ast/tests/data/ast_blocks.ts b/packages/apps-engine/deno-runtime/lib/ast/tests/data/ast_blocks.ts new file mode 100644 index 000000000000..330d2bf52620 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/ast/tests/data/ast_blocks.ts @@ -0,0 +1,436 @@ +// @deno-types="../../../../acorn.d.ts" +import { AnyNode, ClassDeclaration, ExpressionStatement, FunctionDeclaration, VariableDeclaration } from 'acorn'; + +/** + * Partial AST blocks to support testing. + * `start` and `end` properties are omitted for brevity. + */ + +type TestNodeExcerpt = { + code: string; + node: N; +}; + +export const FunctionDeclarationFoo: TestNodeExcerpt = { + code: 'function foo() {}', + node: { + type: 'FunctionDeclaration', + id: { + type: 'Identifier', + name: 'foo', + }, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, +}; + +export const ConstFooAssignedFunctionExpression: TestNodeExcerpt = { + code: 'const foo = function() {}', + node: { + type: 'VariableDeclaration', + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: 'foo', + }, + init: { + type: 'FunctionExpression', + id: null, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, + }, + ], + }, +}; + +export const AssignmentExpressionOfArrowFunctionToFooIdentifier: TestNodeExcerpt = { + code: 'foo = () => {}', + node: { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'Identifier', + name: 'foo', + }, + right: { + type: 'ArrowFunctionExpression', + id: null, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, + }, + }, +}; + +export const AssignmentExpressionOfNamedFunctionToFooMemberExpression: TestNodeExcerpt = { + code: 'obj.foo = function bar() {}', + node: { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'a', + }, + property: { + type: 'Identifier', + name: 'foo', + }, + computed: false, + optional: false, + }, + right: { + type: 'FunctionExpression', + id: null, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, + }, + }, +}; + +export const MethodDefinitionOfFooInClassBar: TestNodeExcerpt = { + code: 'class Bar { foo() {} }', + node: { + type: 'ClassDeclaration', + id: { + type: 'Identifier', + name: 'Bar', + }, + superClass: null, + body: { + type: 'ClassBody', + body: [ + { + type: 'MethodDefinition', + key: { + type: 'Identifier', + name: 'foo', + }, + value: { + type: 'FunctionExpression', + id: null, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, + kind: 'method', + computed: false, + static: false, + }, + ], + }, + }, +}; + +export const SimpleCallExpressionOfFoo: TestNodeExcerpt = { + code: 'foo()', + node: { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'foo', + }, + arguments: [], + optional: false, + }, + }, +}; + +export const SyncFunctionDeclarationWithAsyncCallExpression: TestNodeExcerpt = { + // NOTE: this is invalid syntax, it won't be parsed by acorn + // but it can be an intermediary state of the AST after we run + // `wrapWithAwait` on "bar" call expressions, for instance + code: 'function foo() { return () => await bar() }', + node: { + type: 'FunctionDeclaration', + id: { + type: 'Identifier', + name: 'foo', + }, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [ + { + type: 'ReturnStatement', + argument: { + type: 'ArrowFunctionExpression', + id: null, + expression: true, + generator: false, + async: false, + params: [], + body: { + type: 'AwaitExpression', + argument: { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'bar', + }, + arguments: [], + optional: false, + }, + }, + }, + }, + ], + }, + }, +}; + +export const AssignmentOfFooToBar: TestNodeExcerpt = { + code: 'bar = foo', + node: { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'Identifier', + name: 'bar', + }, + right: { + type: 'Identifier', + name: 'foo', + }, + }, + }, +}; + +export const AssignmentOfFooToBarMemberExpression: TestNodeExcerpt = { + code: 'obj.bar = foo', + node: { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + computed: false, + optional: false, + object: { + type: 'Identifier', + name: 'obj', + }, + property: { + type: 'Identifier', + name: 'bar', + }, + }, + right: { + type: 'Identifier', + name: 'foo', + }, + }, + }, +}; + +export const AssignmentOfFooToBarVariableDeclarator: TestNodeExcerpt = { + code: 'const bar = foo', + node: { + type: 'VariableDeclaration', + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: 'bar', + }, + init: { + type: 'Identifier', + name: 'foo', + }, + }, + ], + }, +}; + +export const AssignmentOfFooToBarPropertyDefinition: TestNodeExcerpt = { + code: 'class baz { bar = foo }', + node: { + type: 'ClassDeclaration', + id: { + type: 'Identifier', + name: 'baz', + }, + superClass: null, + body: { + type: 'ClassBody', + body: [ + { + type: 'PropertyDefinition', + static: false, + computed: false, + key: { + type: 'Identifier', + name: 'bar', + }, + value: { + type: 'Identifier', + name: 'foo', + }, + }, + ], + }, + }, +}; + +const fixSimpleCallExpressionCode = ` +function bar() { + const a = foo(); + + return a; +}`; + +export const FixSimpleCallExpression: TestNodeExcerpt = { + code: fixSimpleCallExpressionCode, + node: { + type: 'FunctionDeclaration', + id: { + type: 'Identifier', + name: 'bar', + }, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [ + { + type: 'VariableDeclaration', + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: 'a', + }, + init: { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'foo', + }, + arguments: [], + optional: false, + }, + }, + ], + }, + { + type: 'ReturnStatement', + argument: { + type: 'Identifier', + name: 'a', + }, + }, + ], + }, + }, +}; + +export const ArrowFunctionDerefCallExpression: TestNodeExcerpt = { + // NOTE: this call strategy is widely used by bundlers; it's used to sever the `this` + // reference in the method from the object that contains it. This is mostly because + // the bundler wants to ensure that it does not messes up the bindings in the code it + // generates. + // + // This would be similar to doing `foo.call(undefined)` + code: 'const bar = () => (0, e.foo)();', + node: { + type: 'VariableDeclaration', + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: 'bar', + }, + init: { + type: 'ArrowFunctionExpression', + id: null, + expression: true, + generator: false, + async: false, + params: [], + body: { + type: 'CallExpression', + optional: false, + arguments: [], + callee: { + type: 'SequenceExpression', + expressions: [ + { + type: 'Literal', + value: 0, + }, + { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'e', + }, + property: { + type: 'Identifier', + name: 'foo', + }, + computed: false, + optional: false, + }, + ], + }, + }, + }, + }, + ], + }, +}; diff --git a/packages/apps-engine/deno-runtime/lib/ast/tests/operations.test.ts b/packages/apps-engine/deno-runtime/lib/ast/tests/operations.test.ts new file mode 100644 index 000000000000..2b00c271f730 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/ast/tests/operations.test.ts @@ -0,0 +1,245 @@ +import { assertEquals, assertThrows } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; + +import { WalkerState, asyncifyScope, buildFixModifiedFunctionsOperation, checkReassignmentOfModifiedIdentifiers, getFunctionIdentifier, wrapWithAwait } from '../operations.ts'; +import { + ArrowFunctionDerefCallExpression, + AssignmentExpressionOfArrowFunctionToFooIdentifier, + AssignmentExpressionOfNamedFunctionToFooMemberExpression, + AssignmentOfFooToBar, + AssignmentOfFooToBarMemberExpression, + AssignmentOfFooToBarPropertyDefinition, + AssignmentOfFooToBarVariableDeclarator, + ConstFooAssignedFunctionExpression, + FixSimpleCallExpression, + FunctionDeclarationFoo, + MethodDefinitionOfFooInClassBar, + SimpleCallExpressionOfFoo, + SyncFunctionDeclarationWithAsyncCallExpression, +} from './data/ast_blocks.ts'; +import { AnyNode, ArrowFunctionExpression, AssignmentExpression, AwaitExpression, Expression, MethodDefinition, ReturnStatement, VariableDeclaration } from '../../../acorn.d.ts'; +import { assertNotEquals } from 'https://deno.land/std@0.203.0/assert/assert_not_equals.ts'; + +describe('getFunctionIdentifier', () => { + it(`identifies the name "foo" for the code \`${FunctionDeclarationFoo.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [FunctionDeclarationFoo.node]; + const functionNodeIndex = 0; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); + + it(`identifies the name "foo" for the code \`${ConstFooAssignedFunctionExpression.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [ + ConstFooAssignedFunctionExpression.node, // VariableDeclaration + ConstFooAssignedFunctionExpression.node.declarations[0], // VariableDeclarator + ConstFooAssignedFunctionExpression.node.declarations[0].init! // FunctionExpression + ]; + const functionNodeIndex = 2; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); + + it(`identifies the name "foo" for the code \`${AssignmentExpressionOfArrowFunctionToFooIdentifier.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [ + AssignmentExpressionOfArrowFunctionToFooIdentifier.node, // ExpressionStatement + AssignmentExpressionOfArrowFunctionToFooIdentifier.node.expression, // AssignmentExpression + (AssignmentExpressionOfArrowFunctionToFooIdentifier.node.expression as AssignmentExpression).right, // ArrowFunctionExpression + ]; + const functionNodeIndex = 2; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); + + it(`identifies the name "foo" for the code \`${AssignmentExpressionOfNamedFunctionToFooMemberExpression.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [ + AssignmentExpressionOfNamedFunctionToFooMemberExpression.node, // ExpressionStatement + AssignmentExpressionOfNamedFunctionToFooMemberExpression.node.expression, // AssignmentExpression + (AssignmentExpressionOfNamedFunctionToFooMemberExpression.node.expression as AssignmentExpression).right, // FunctionExpression + ]; + const functionNodeIndex = 2; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); + + it(`identifies the name "foo" for the code \`${MethodDefinitionOfFooInClassBar.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [ + MethodDefinitionOfFooInClassBar.node, // ClassDeclaration + MethodDefinitionOfFooInClassBar.node.body, // ClassBody + MethodDefinitionOfFooInClassBar.node.body!.body[0], // MethodDefinition + (MethodDefinitionOfFooInClassBar.node.body!.body[0] as MethodDefinition).value, // FunctionExpression + ]; + const functionNodeIndex = 3; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); +}); + +describe('wrapWithAwait', () => { + it('wraps a call expression with await', () => { + const node = structuredClone(SimpleCallExpressionOfFoo.node.expression); + wrapWithAwait(node); + + assertEquals('AwaitExpression', node.type); + assertNotEquals(SimpleCallExpressionOfFoo.node.expression.type, node.type); + assertEquals(SimpleCallExpressionOfFoo.node.expression, (node as AwaitExpression).argument); + }); + + it('throws if node is not an expression', () => { + const node = structuredClone(SimpleCallExpressionOfFoo.node); + assertThrows(() => wrapWithAwait(node as unknown as Expression)); + }) +}); + +describe('asyncifyScope', () => { + it('makes only the first function scope async', () => { + const node = structuredClone(SyncFunctionDeclarationWithAsyncCallExpression.node); + const ancestors: AnyNode[] = [ + node, // FunctionDeclaration + node.body, // BlockStatement + node.body!.body[0], // ReturnStatement + (node.body!.body[0] as ReturnStatement).argument!, // ArrowFunctionExpression + ((node.body!.body[0] as ReturnStatement).argument! as ArrowFunctionExpression).body, // AwaitExpression + (((node.body!.body[0] as ReturnStatement).argument! as ArrowFunctionExpression).body as AwaitExpression).argument, // CallExpression + ]; + const state: WalkerState = { + isModified: false, + functionIdentifiers: new Set(), + } + + asyncifyScope(ancestors, state); + + // Assert the function did indeed change the expression to async + assertEquals(((node.body.body[0] as ReturnStatement).argument as ArrowFunctionExpression).async, true) + + // Assert the function did NOT change all ancestors in the chain + assertEquals(node.async, false); + + // Assert it couldn't find a function identifier + assertEquals(state.functionIdentifiers.size, 0); + }); +}); + +describe('checkReassignmentofModifiedIdentifiers', () => { + it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBar.code}"`, () => { + const node = structuredClone(AssignmentOfFooToBar.node); + const ancestors: AnyNode[] = [ + node, // ExpressionStatement + node.expression, // AssignmentExpression + (node.expression as AssignmentExpression).right, // Identifier + ]; + const state: WalkerState = { + isModified: true, + functionIdentifiers: new Set(['foo']), + } + + checkReassignmentOfModifiedIdentifiers(node.expression, state, ancestors, ''); + + assertEquals(state.functionIdentifiers.has('bar'), true); + }); + + it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBarMemberExpression.code}"`, () => { + const node = structuredClone(AssignmentOfFooToBarMemberExpression.node); + const ancestors: AnyNode[] = [ + node, // ExpressionStatement + node.expression, // AssignmentExpression + (node.expression as AssignmentExpression).right, // Identifier + ]; + const state: WalkerState = { + isModified: true, + functionIdentifiers: new Set(['foo']), + } + + checkReassignmentOfModifiedIdentifiers(node.expression, state, ancestors, ''); + + assertEquals(state.functionIdentifiers.has('bar'), true); + }); + + it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBarVariableDeclarator.code}"`, () => { + const node = structuredClone(AssignmentOfFooToBarVariableDeclarator.node); + const ancestors: AnyNode[] = [ + node, // VariableDeclaration + node.declarations[0], // VariableDeclarator + ]; + const state: WalkerState = { + isModified: true, + functionIdentifiers: new Set(['foo']), + } + + checkReassignmentOfModifiedIdentifiers(node.declarations[0], state, ancestors, ''); + + assertEquals(state.functionIdentifiers.has('bar'), true); + }); + + it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBarPropertyDefinition.code}"`, () => { + const node = structuredClone(AssignmentOfFooToBarPropertyDefinition.node); + const ancestors: AnyNode[] = [ + node, // ClassDeclaration + node.body, // ClassBody + node.body.body[0], // PropertyDefinition + ]; + const state: WalkerState = { + isModified: true, + functionIdentifiers: new Set(['foo']), + } + + checkReassignmentOfModifiedIdentifiers(node.body.body[0], state, ancestors, ''); + + assertEquals(state.functionIdentifiers.has('bar'), true); + }); +}); + +describe('buildFixModifiedFunctionsOperation', function() { + const state: WalkerState = { + isModified: false, + functionIdentifiers: new Set(['foo']), + }; + + const fixFunction = buildFixModifiedFunctionsOperation(state.functionIdentifiers); + + beforeEach(() => { + state.isModified = false; + state.functionIdentifiers = new Set(['foo']); + }); + + it(`fixes calls of "foo" in the code "${FixSimpleCallExpression.code}"`, () => { + const node = structuredClone(FixSimpleCallExpression.node); + const ancestors: AnyNode[] = [ + node, // FunctionDeclaration + node.body, // BlockStatement + node.body.body[0], // VariableDeclaration + (node.body.body[0] as VariableDeclaration).declarations[0], // VariableDeclarator + (node.body.body[0] as VariableDeclaration).declarations[0].init!, // CallExpression + ]; + + fixFunction(ancestors[4], state, ancestors, ''); + + assertEquals(state.isModified, true); + assertEquals(state.functionIdentifiers.has('bar'), true); + assertNotEquals(FixSimpleCallExpression.node, node); + assertEquals(node.async, true); + assertEquals(ancestors[4].type, 'AwaitExpression'); + }); + + it(`fixes calls of "foo" in the code "${ArrowFunctionDerefCallExpression.code}"`, () => { + const node = structuredClone(ArrowFunctionDerefCallExpression.node); + const ancestors: AnyNode[] = [ + node, // VariableDeclaration + node.declarations[0], // VariableDeclarator + node.declarations[0].init!, // ArrowFunctionExpression + (node.declarations[0].init as ArrowFunctionExpression).body, // CallExpression + ]; + + fixFunction(ancestors[3], state, ancestors, ''); + + // Recorded that a modification has been made + assertEquals(state.isModified, true); + // Recorded that the enclosing scope of the call also requires fixing + assertEquals(state.functionIdentifiers.has('bar'), true); + // Original node and fixed node are different + assertNotEquals(ArrowFunctionDerefCallExpression.node, node); + // The function call is now await'ed + assertEquals(ancestors[3].type, 'AwaitExpression'); + // The parent function of the call is now marked as async + assertEquals((ancestors[2] as ArrowFunctionExpression).async, true); + }); +}) diff --git a/packages/apps-engine/deno-runtime/lib/codec.ts b/packages/apps-engine/deno-runtime/lib/codec.ts new file mode 100644 index 000000000000..288db46169dc --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/codec.ts @@ -0,0 +1,43 @@ +import { Buffer } from 'node:buffer'; +import { Decoder, Encoder, ExtensionCodec } from '@msgpack/msgpack'; + +import type { App as _App } from '@rocket.chat/apps-engine/definition/App.ts'; +import { require } from "./require.ts"; + +const { App } = require('@rocket.chat/apps-engine/definition/App.js') as { + App: typeof _App; +}; + +const extensionCodec = new ExtensionCodec(); + +extensionCodec.register({ + type: 0, + encode: (object: unknown) => { + // We don't care about functions, but also don't want to throw an error + if (typeof object === 'function' || object instanceof App) { + return new Uint8Array(0); + } + + return null; + }, + decode: (_data: Uint8Array) => undefined, +}); + +// Since Deno doesn't have Buffer by default, we need to use Uint8Array +extensionCodec.register({ + type: 1, + encode: (object: unknown) => { + if (object instanceof Buffer) { + return new Uint8Array(object.buffer, object.byteOffset, object.byteLength); + } + + return null; + }, + // msgpack will reuse the Uint8Array instance, so WE NEED to copy it instead of simply creating a view + decode: (data: Uint8Array) => { + return Buffer.from(data); + }, +}); + +export const encoder = new Encoder({ extensionCodec }); +export const decoder = new Decoder({ extensionCodec }); diff --git a/packages/apps-engine/deno-runtime/lib/logger.ts b/packages/apps-engine/deno-runtime/lib/logger.ts new file mode 100644 index 000000000000..ea2701c70230 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/logger.ts @@ -0,0 +1,142 @@ +import stackTrace from 'stack-trace'; +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; + +export interface StackFrame { + getTypeName(): string; + getFunctionName(): string; + getMethodName(): string; + getFileName(): string; + getLineNumber(): number; + getColumnNumber(): number; + isNative(): boolean; + isConstructor(): boolean; +} + +enum LogMessageSeverity { + DEBUG = 'debug', + INFORMATION = 'info', + LOG = 'log', + WARNING = 'warning', + ERROR = 'error', + SUCCESS = 'success', +} + +type Entry = { + caller: string; + severity: LogMessageSeverity; + method: string; + timestamp: Date; + args: Array; +}; + +interface ILoggerStorageEntry { + appId: string; + method: string; + entries: Array; + startTime: Date; + endTime: Date; + totalTime: number; + _createdAt: Date; +} + +export class Logger { + private entries: Array; + private start: Date; + private method: string; + + constructor(method: string) { + this.method = method; + this.entries = []; + this.start = new Date(); + } + + public debug(...args: Array): void { + this.addEntry(LogMessageSeverity.DEBUG, this.getStack(stackTrace.get()), ...args); + } + + public info(...args: Array): void { + this.addEntry(LogMessageSeverity.INFORMATION, this.getStack(stackTrace.get()), ...args); + } + + public log(...args: Array): void { + this.addEntry(LogMessageSeverity.LOG, this.getStack(stackTrace.get()), ...args); + } + + public warn(...args: Array): void { + this.addEntry(LogMessageSeverity.WARNING, this.getStack(stackTrace.get()), ...args); + } + + public error(...args: Array): void { + this.addEntry(LogMessageSeverity.ERROR, this.getStack(stackTrace.get()), ...args); + } + + public success(...args: Array): void { + this.addEntry(LogMessageSeverity.SUCCESS, this.getStack(stackTrace.get()), ...args); + } + + private addEntry(severity: LogMessageSeverity, caller: string, ...items: Array): void { + const i = items.map((args) => { + if (args instanceof Error) { + return JSON.stringify(args, Object.getOwnPropertyNames(args)); + } + if (typeof args === 'object' && args !== null && 'stack' in args) { + return JSON.stringify(args, Object.getOwnPropertyNames(args)); + } + if (typeof args === 'object' && args !== null && 'message' in args) { + return JSON.stringify(args, Object.getOwnPropertyNames(args)); + } + const str = JSON.stringify(args, null, 2); + return str ? JSON.parse(str) : str; // force call toJSON to prevent circular references + }); + + this.entries.push({ + caller, + severity, + method: this.method, + timestamp: new Date(), + args: i, + }); + } + + private getStack(stack: Array): string { + let func = 'anonymous'; + + if (stack.length === 1) { + return func; + } + + const frame = stack[1]; + + if (frame.getMethodName() === null) { + func = 'anonymous OR constructor'; + } else { + func = frame.getMethodName(); + } + + if (frame.getFunctionName() !== null) { + func = `${func} -> ${frame.getFunctionName()}`; + } + + return func; + } + + private getTotalTime(): number { + return new Date().getTime() - this.start.getTime(); + } + + public hasEntries(): boolean { + return this.entries.length > 0; + } + + public getLogs(): ILoggerStorageEntry { + return { + appId: AppObjectRegistry.get('id')!, + method: this.method, + entries: this.entries, + startTime: this.start, + endTime: new Date(), + totalTime: this.getTotalTime(), + _createdAt: new Date(), + }; + } +} diff --git a/packages/apps-engine/deno-runtime/lib/messenger.ts b/packages/apps-engine/deno-runtime/lib/messenger.ts new file mode 100644 index 000000000000..1e9ffe05c6c5 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/messenger.ts @@ -0,0 +1,199 @@ +import { writeAll } from "https://deno.land/std@0.216.0/io/write_all.ts"; + +import * as jsonrpc from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; +import type { Logger } from './logger.ts'; +import { encoder } from './codec.ts'; + +export type RequestDescriptor = Pick; + +export type NotificationDescriptor = Pick; + +export type SuccessResponseDescriptor = Pick; + +export type ErrorResponseDescriptor = Pick; + +export type JsonRpcRequest = jsonrpc.IParsedObjectRequest | jsonrpc.IParsedObjectNotification; +export type JsonRpcResponse = jsonrpc.IParsedObjectSuccess | jsonrpc.IParsedObjectError; + +export function isRequest(message: jsonrpc.IParsedObject): message is JsonRpcRequest { + return message.type === 'request' || message.type === 'notification'; +} + +export function isResponse(message: jsonrpc.IParsedObject): message is JsonRpcResponse { + return message.type === 'success' || message.type === 'error'; +} + +export function isErrorResponse(message: jsonrpc.JsonRpc): message is jsonrpc.ErrorObject { + return message instanceof jsonrpc.ErrorObject; +} + +const COMMAND_PONG = '_zPONG'; + +export const RPCResponseObserver = new EventTarget(); + +export const Queue = new (class Queue { + private queue: Uint8Array[] = []; + private isProcessing = false; + + private async processQueue() { + if (this.isProcessing) { + return; + } + + this.isProcessing = true; + + while (this.queue.length) { + const message = this.queue.shift(); + + if (message) { + await Transport.send(message); + } + } + + this.isProcessing = false; + } + + public enqueue(message: jsonrpc.JsonRpc | typeof COMMAND_PONG) { + this.queue.push(encoder.encode(message)); + this.processQueue(); + } +}); + +export const Transport = new (class Transporter { + private selectedTransport: Transporter['stdoutTransport'] | Transporter['noopTransport']; + + constructor() { + this.selectedTransport = this.stdoutTransport.bind(this); + } + + private async stdoutTransport(message: Uint8Array): Promise { + await writeAll(Deno.stdout, message); + } + + private async noopTransport(_message: Uint8Array): Promise {} + + public selectTransport(transport: 'stdout' | 'noop'): void { + switch (transport) { + case 'stdout': + this.selectedTransport = this.stdoutTransport.bind(this); + break; + case 'noop': + this.selectedTransport = this.noopTransport.bind(this); + break; + } + } + + public send(message: Uint8Array): Promise { + return this.selectedTransport(message); + } +})(); + +export function parseMessage(message: string | Record) { + let parsed: jsonrpc.IParsedObject | jsonrpc.IParsedObject[]; + + if (typeof message === 'string') { + parsed = jsonrpc.parse(message); + } else { + parsed = jsonrpc.parseObject(message); + } + + if (Array.isArray(parsed)) { + throw jsonrpc.error(null, jsonrpc.JsonRpcError.invalidRequest(null)); + } + + if (parsed.type === 'invalid') { + throw jsonrpc.error(null, parsed.payload); + } + + return parsed; +} + +export async function sendInvalidRequestError(): Promise { + const rpc = jsonrpc.error(null, jsonrpc.JsonRpcError.invalidRequest(null)); + + await Queue.enqueue(rpc); +} + +export async function sendInvalidParamsError(id: jsonrpc.ID): Promise { + const rpc = jsonrpc.error(id, jsonrpc.JsonRpcError.invalidParams(null)); + + await Queue.enqueue(rpc); +} + +export async function sendParseError(): Promise { + const rpc = jsonrpc.error(null, jsonrpc.JsonRpcError.parseError(null)); + + await Queue.enqueue(rpc); +} + +export async function sendMethodNotFound(id: jsonrpc.ID): Promise { + const rpc = jsonrpc.error(id, jsonrpc.JsonRpcError.methodNotFound(null)); + + await Queue.enqueue(rpc); +} + +export async function errorResponse({ error: { message, code = -32000, data = {} }, id }: ErrorResponseDescriptor): Promise { + const logger = AppObjectRegistry.get('logger'); + + if (logger?.hasEntries()) { + data.logs = logger.getLogs(); + } + + const rpc = jsonrpc.error(id, new jsonrpc.JsonRpcError(message, code, data)); + + await Queue.enqueue(rpc); +} + +export async function successResponse({ id, result }: SuccessResponseDescriptor): Promise { + const payload = { value: result } as Record; + const logger = AppObjectRegistry.get('logger'); + + if (logger?.hasEntries()) { + payload.logs = logger.getLogs(); + } + + const rpc = jsonrpc.success(id, payload); + + await Queue.enqueue(rpc); +} + +export function pongResponse(): Promise { + return Promise.resolve(Queue.enqueue(COMMAND_PONG)); +} + +export async function sendRequest(requestDescriptor: RequestDescriptor): Promise { + const request = jsonrpc.request(Math.random().toString(36).slice(2), requestDescriptor.method, requestDescriptor.params); + + // TODO: add timeout to this + const responsePromise = new Promise((resolve, reject) => { + const handler = (event: Event) => { + if (event instanceof ErrorEvent) { + reject(event.error); + } + + if (event instanceof CustomEvent) { + resolve(event.detail); + } + + RPCResponseObserver.removeEventListener(`response:${request.id}`, handler); + }; + + RPCResponseObserver.addEventListener(`response:${request.id}`, handler); + }); + + await Queue.enqueue(request); + + return responsePromise as Promise; +} + +export function sendNotification({ method, params }: NotificationDescriptor) { + const request = jsonrpc.notification(method, params); + + Queue.enqueue(request); +} + +export function log(params: jsonrpc.RpcParams) { + sendNotification({ method: 'log', params }); +} diff --git a/packages/apps-engine/deno-runtime/lib/require.ts b/packages/apps-engine/deno-runtime/lib/require.ts new file mode 100644 index 000000000000..3288ecf67ffa --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/require.ts @@ -0,0 +1,14 @@ +import { createRequire } from 'node:module'; + +const _require = createRequire(import.meta.url); + +export const require = (mod: string) => { + // When we try to import something from the apps-engine, we resolve the path using import maps from Deno + // However, the import maps are configured to look at the source folder for typescript files, but during + // runtime those files are not available + if (mod.startsWith('@rocket.chat/apps-engine')) { + mod = import.meta.resolve(mod).replace('file://', '').replace('src/', ''); + } + + return _require(mod); +} diff --git a/packages/apps-engine/deno-runtime/lib/room.ts b/packages/apps-engine/deno-runtime/lib/room.ts new file mode 100644 index 000000000000..b7423cdd31ff --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/room.ts @@ -0,0 +1,104 @@ +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; +import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager.ts'; + +const PrivateManager = Symbol('RoomPrivateManager'); + +export class Room { + public id: string | undefined; + + public displayName?: string; + + public slugifiedName: string | undefined; + + public type: RoomType | undefined; + + public creator: IUser | undefined; + + public isDefault?: boolean; + + public isReadOnly?: boolean; + + public displaySystemMessages?: boolean; + + public messageCount?: number; + + public createdAt?: Date; + + public updatedAt?: Date; + + public lastModifiedAt?: Date; + + public customFields?: { [key: string]: unknown }; + + public userIds?: Array; + + private _USERNAMES: Promise> | undefined; + + private [PrivateManager]: AppManager | undefined; + + /** + * @deprecated + */ + public get usernames(): Promise> { + if (!this._USERNAMES) { + this._USERNAMES = this[PrivateManager]?.getBridges().getInternalBridge().doGetUsernamesOfRoomById(this.id); + } + + return this._USERNAMES || Promise.resolve([]); + } + + public set usernames(usernames) {} + + public constructor(room: IRoom, manager: AppManager) { + Object.assign(this, room); + + Object.defineProperty(this, PrivateManager, { + configurable: false, + enumerable: false, + writable: false, + value: manager, + }); + } + + get value(): object { + return { + id: this.id, + displayName: this.displayName, + slugifiedName: this.slugifiedName, + type: this.type, + creator: this.creator, + isDefault: this.isDefault, + isReadOnly: this.isReadOnly, + displaySystemMessages: this.displaySystemMessages, + messageCount: this.messageCount, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + lastModifiedAt: this.lastModifiedAt, + customFields: this.customFields, + userIds: this.userIds, + }; + } + + public async getUsernames(): Promise> { + // Get usernames + if (!this._USERNAMES) { + this._USERNAMES = await this[PrivateManager]?.getBridges().getInternalBridge().doGetUsernamesOfRoomById(this.id); + } + + return this._USERNAMES || []; + } + + public toJSON() { + return this.value; + } + + public toString() { + return this.value; + } + + public valueOf() { + return this.value; + } +} diff --git a/packages/apps-engine/deno-runtime/lib/roomFactory.ts b/packages/apps-engine/deno-runtime/lib/roomFactory.ts new file mode 100644 index 000000000000..8c270eeb86b9 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/roomFactory.ts @@ -0,0 +1,27 @@ +import type { IRoom } from "@rocket.chat/apps-engine/definition/rooms/IRoom.ts"; +import type { AppManager } from "@rocket.chat/apps-engine/server/AppManager.ts"; + +import { AppAccessors } from "./accessors/mod.ts"; +import { Room } from "./room.ts"; +import { JsonRpcError } from "jsonrpc-lite"; + +const getMockAppManager = (senderFn: AppAccessors['senderFn']) => ({ + getBridges: () => ({ + getInternalBridge: () => ({ + doGetUsernamesOfRoomById: (roomId: string) => { + return senderFn({ + method: 'bridges:getInternalBridge:doGetUsernamesOfRoomById', + params: [roomId], + }).then((result) => result.result).catch((err) => { + throw new JsonRpcError(`Error getting usernames of room: ${err}`, -32000); + }) + }, + }), + }), +}); + +export default function createRoom(room: IRoom, senderFn: AppAccessors['senderFn']) { + const mockAppManager = getMockAppManager(senderFn); + + return new Room(room, mockAppManager as unknown as AppManager); +} diff --git a/packages/apps-engine/deno-runtime/lib/sanitizeDeprecatedUsage.ts b/packages/apps-engine/deno-runtime/lib/sanitizeDeprecatedUsage.ts new file mode 100644 index 000000000000..91cf6587e741 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/sanitizeDeprecatedUsage.ts @@ -0,0 +1,20 @@ +import { fixBrokenSynchronousAPICalls } from "./ast/mod.ts"; + +function hasPotentialDeprecatedUsage(source: string) { + return ( + // potential usage of Room.usernames getter + source.includes('.usernames') || + // potential usage of LivechatRead.isOnline method + source.includes('.isOnline(') || + // potential usage of LivechatCreator.createToken method + source.includes('.createToken(') + ) +} + +export function sanitizeDeprecatedUsage(source: string) { + if (!hasPotentialDeprecatedUsage(source)) { + return source; + } + + return fixBrokenSynchronousAPICalls(source); +} diff --git a/packages/apps-engine/deno-runtime/lib/tests/logger.test.ts b/packages/apps-engine/deno-runtime/lib/tests/logger.test.ts new file mode 100644 index 000000000000..f69a728e79af --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/tests/logger.test.ts @@ -0,0 +1,111 @@ +import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { Logger } from "../logger.ts"; + +describe('Logger', () => { + it('getLogs should return an array of entries', () => { + const logger = new Logger('test'); + logger.info('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.method, 'test'); + }) + + it('should be able to add entries of different severity', () => { + const logger = new Logger('test'); + logger.info('test'); + logger.debug('test'); + logger.error('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 3); + assertEquals(logs.entries[0].severity, 'info'); + assertEquals(logs.entries[1].severity, 'debug'); + assertEquals(logs.entries[2].severity, 'error'); + }) + + it('should be able to add an info entry', () => { + const logger = new Logger('test'); + logger.info('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'info'); + }); + + it('should be able to add an debug entry', () => { + const logger = new Logger('test'); + logger.debug('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'debug'); + }); + + it('should be able to add an error entry', () => { + const logger = new Logger('test'); + logger.error('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'error'); + }); + + it('should be able to add an success entry', () => { + const logger = new Logger('test'); + logger.success('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'success'); + }); + + it('should be able to add an warning entry', () => { + const logger = new Logger('test'); + logger.warn('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'warning'); + }); + + it('should be able to add an log entry', () => { + const logger = new Logger('test'); + logger.log('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'log'); + }); + + it('should be able to add an entry with multiple arguments', () => { + const logger = new Logger('test'); + logger.log('test', 'test', 'test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].args[1], 'test'); + assertEquals(logs.entries[0].args[2], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'log'); + }); + + it('should be able to add an entry with multiple arguments of different types', () => { + const logger = new Logger('test'); + logger.log('test', 1, true, { foo: 'bar' }); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].args[1], 1); + assertEquals(logs.entries[0].args[2], true); + assertEquals(logs.entries[0].args[3], { foo: 'bar' }); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'log'); + }); + +}) diff --git a/packages/apps-engine/deno-runtime/lib/tests/messenger.test.ts b/packages/apps-engine/deno-runtime/lib/tests/messenger.test.ts new file mode 100644 index 000000000000..9b4f128380bc --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/tests/messenger.test.ts @@ -0,0 +1,96 @@ +import { assertEquals, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; + +import * as Messenger from '../messenger.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { Logger } from '../logger.ts'; + +describe('Messenger', () => { + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('logger', new Logger('test')); + AppObjectRegistry.set('id', 'test'); + Messenger.Transport.selectTransport('noop'); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + Messenger.Transport.selectTransport('stdout'); + }); + + it('should add logs to success responses', async () => { + const theSpy = spy(Messenger.Queue, 'enqueue'); + + const logger = AppObjectRegistry.get('logger') as Logger; + + logger.info('test'); + + await Messenger.successResponse({ id: 'test', result: 'test' }); + + assertEquals(theSpy.calls.length, 1); + + const [responseArgument] = theSpy.calls[0].args; + + assertObjectMatch(responseArgument, { + jsonrpc: '2.0', + id: 'test', + result: { + value: 'test', + logs: { + appId: 'test', + method: 'test', + entries: [ + { + severity: 'info', + method: 'test', + args: ['test'], + caller: 'anonymous OR constructor', + }, + ], + }, + }, + }); + + theSpy.restore(); + }); + + it('should add logs to error responses', async () => { + const theSpy = spy(Messenger.Queue, 'enqueue'); + + const logger = AppObjectRegistry.get('logger') as Logger; + + logger.info('test'); + + await Messenger.errorResponse({ id: 'test', error: { code: -32000, message: 'test' } }); + + assertEquals(theSpy.calls.length, 1); + + const [responseArgument] = theSpy.calls[0].args; + + assertObjectMatch(responseArgument, { + jsonrpc: '2.0', + id: 'test', + error: { + code: -32000, + message: 'test', + data: { + logs: { + appId: 'test', + method: 'test', + entries: [ + { + severity: 'info', + method: 'test', + args: ['test'], + caller: 'anonymous OR constructor', + }, + ], + }, + }, + }, + }); + + theSpy.restore(); + }); +}); diff --git a/packages/apps-engine/deno-runtime/main.ts b/packages/apps-engine/deno-runtime/main.ts new file mode 100644 index 000000000000..09be5258ecd0 --- /dev/null +++ b/packages/apps-engine/deno-runtime/main.ts @@ -0,0 +1,129 @@ +if (!Deno.args.includes('--subprocess')) { + Deno.stderr.writeSync( + new TextEncoder().encode(` + This is a Deno wrapper for Rocket.Chat Apps. It is not meant to be executed stand-alone; + It is instead meant to be executed as a subprocess by the Apps-Engine framework. + `), + ); + Deno.exit(1001); +} + +import { JsonRpcError } from 'jsonrpc-lite'; +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import * as Messenger from './lib/messenger.ts'; +import { decoder } from './lib/codec.ts'; +import { AppObjectRegistry } from './AppObjectRegistry.ts'; +import { Logger } from './lib/logger.ts'; + +import slashcommandHandler from './handlers/slashcommand-handler.ts'; +import videoConferenceHandler from './handlers/videoconference-handler.ts'; +import apiHandler from './handlers/api-handler.ts'; +import handleApp from './handlers/app/handler.ts'; +import handleScheduler from './handlers/scheduler-handler.ts'; + +type Handlers = { + app: typeof handleApp; + api: typeof apiHandler; + slashcommand: typeof slashcommandHandler; + videoconference: typeof videoConferenceHandler; + scheduler: typeof handleScheduler; + ping: (method: string, params: unknown) => 'pong'; +}; + +const COMMAND_PING = '_zPING'; + +async function requestRouter({ type, payload }: Messenger.JsonRpcRequest): Promise { + const methodHandlers: Handlers = { + app: handleApp, + api: apiHandler, + slashcommand: slashcommandHandler, + videoconference: videoConferenceHandler, + scheduler: handleScheduler, + ping: (_method, _params) => 'pong', + }; + + // We're not handling notifications at the moment + if (type === 'notification') { + return Messenger.sendInvalidRequestError(); + } + + const { id, method, params } = payload; + + const logger = new Logger(method); + AppObjectRegistry.set('logger', logger); + + const app = AppObjectRegistry.get('app'); + + if (app) { + // Same logic as applied in the ProxiedApp class previously + (app as unknown as Record).logger = logger; + } + + const [methodPrefix] = method.split(':') as [keyof Handlers]; + const handler = methodHandlers[methodPrefix]; + + if (!handler) { + return Messenger.errorResponse({ + error: { message: 'Method not found', code: -32601 }, + id, + }); + } + + const result = await handler(method, params); + + if (result instanceof JsonRpcError) { + return Messenger.errorResponse({ id, error: result }); + } + + return Messenger.successResponse({ id, result }); +} + +function handleResponse(response: Messenger.JsonRpcResponse): void { + let event: Event; + + if (response.type === 'error') { + event = new ErrorEvent(`response:${response.payload.id}`, { + error: response.payload, + }); + } else { + event = new CustomEvent(`response:${response.payload.id}`, { + detail: response.payload, + }); + } + + Messenger.RPCResponseObserver.dispatchEvent(event); +} + +async function main() { + Messenger.sendNotification({ method: 'ready' }); + + for await (const message of decoder.decodeStream(Deno.stdin.readable)) { + try { + // Process PING command first as it is not JSON RPC + if (message === COMMAND_PING) { + Messenger.pongResponse(); + continue; + } + + const JSONRPCMessage = Messenger.parseMessage(message as Record); + + if (Messenger.isRequest(JSONRPCMessage)) { + void requestRouter(JSONRPCMessage); + continue; + } + + if (Messenger.isResponse(JSONRPCMessage)) { + handleResponse(JSONRPCMessage); + } + } catch (error) { + if (Messenger.isErrorResponse(error)) { + await Messenger.errorResponse(error); + } else { + await Messenger.sendParseError(); + } + } + } +} + +main(); diff --git a/packages/apps-engine/package.json b/packages/apps-engine/package.json new file mode 100644 index 000000000000..c229ff447000 --- /dev/null +++ b/packages/apps-engine/package.json @@ -0,0 +1,132 @@ +{ + "name": "@rocket.chat/apps-engine", + "version": "1.47.0-alpha", + "description": "The engine code for the Rocket.Chat Apps which manages, runs, translates, coordinates and all of that.", + "main": "index", + "typings": "index", + "scripts": { + "start": "run-s .:build:clean .:build:watch", + "testunit": "run-p .:test:node .:test:deno", + ".:test:node": "NODE_ENV=test ts-node ./tests/runner.ts", + ".:test:deno": "cd deno-runtime && deno task test", + "lint": "run-p .:lint:eslint .:lint:deno", + ".:lint:eslint": "eslint .", + ".:lint:deno": "deno lint --ignore=deno-runtime/.deno deno-runtime/", + "fix-lint": "eslint . --fix", + "build": "run-s .:build:clean .:build:default .:build:deno-cache", + ".:build:clean": "rimraf client definition server", + ".:build:default": "tsc -p tsconfig.json", + ".:build:deno-cache": "node scripts/deno-cache.js", + ".:build:watch": "yarn .:build:default --watch", + "typecheck": "tsc -p tsconfig.json --noEmit", + "bundle": "node scripts/bundle.js", + "gen-doc": "typedoc", + "prepack": "yarn bundle" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/RocketChat/Rocket.Chat.Apps-engine.git" + }, + "keywords": [ + "rocket.chat", + "team chat", + "apps engine" + ], + "files": [ + "client/**", + "definition/**", + "deno-runtime/**", + "lib/**", + "scripts/**", + "server/**" + ], + "publishConfig": { + "access": "public" + }, + "author": { + "name": "Rocket.Chat", + "url": "https://rocket.chat/" + }, + "contributors": [ + { + "name": "Bradley Hilton", + "email": "bradley.hilton@rocket.chat" + }, + { + "name": "Rodrigo Nascimento", + "email": "rodrigo.nascimento@rocket.chat" + }, + { + "name": "Douglas Gubert", + "email": "douglas.gubert@rocket.chat" + } + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/RocketChat/Rocket.Chat.Apps-engine/issues" + }, + "homepage": "https://github.com/RocketChat/Rocket.Chat.Apps-engine#readme", + "devDependencies": { + "@rocket.chat/eslint-config": "workspace:~", + "@rocket.chat/ui-kit": "workspace:~", + "@types/adm-zip": "^0.5.0", + "@types/debug": "^4.1.12", + "@types/lodash.clonedeep": "^4.5.7", + "@types/nedb": "^1.8.12", + "@types/node": "^18.0.0", + "@types/semver": "^5.5.0", + "@types/stack-trace": "0.0.29", + "@types/uuid": "~8.3.4", + "@typescript-eslint/eslint-plugin": "~5.60.1", + "@typescript-eslint/parser": "~5.60.1", + "alsatian": "^2.4.0", + "browserify": "^16.5.2", + "eslint": "~8.45.0", + "nedb": "^1.8.0", + "npm-run-all": "^4.1.5", + "nyc": "^14.1.1", + "rimraf": "^6.0.1", + "tap-bark": "^1.0.0", + "ts-node": "^6.2.0", + "typedoc": "~0.24.8", + "typescript": "~5.1.6", + "uglify-es": "^3.3.9" + }, + "dependencies": { + "@msgpack/msgpack": "3.0.0-beta2", + "adm-zip": "^0.5.9", + "cryptiles": "^4.1.3", + "debug": "^4.3.4", + "esbuild": "^0.20.2", + "jose": "^4.11.1", + "jsonrpc-lite": "^2.2.0", + "lodash.clonedeep": "^4.5.0", + "semver": "^5.7.1", + "stack-trace": "0.0.10", + "uuid": "~8.3.2" + }, + "peerDependencies": { + "@rocket.chat/ui-kit": "workspace:^" + }, + "nyc": { + "include": [ + "src/*.ts", + "src/server/**/*.ts" + ], + "extension": [ + ".ts" + ], + "reporter": [ + "lcov", + "json", + "html" + ], + "all": true + }, + "volta": { + "extends": "../../package.json" + }, + "installConfig": { + "hoistingLimits": "workspaces" + } +} diff --git a/packages/apps-engine/scripts/bundle.js b/packages/apps-engine/scripts/bundle.js new file mode 100644 index 000000000000..a7f8932bec12 --- /dev/null +++ b/packages/apps-engine/scripts/bundle.js @@ -0,0 +1,35 @@ +const fs = require('fs'); +const path = require('path'); +const { Readable } = require('stream'); + +const browserify = require('browserify'); +const { minify } = require('uglify-es'); + +const targetDir = path.join(__dirname, '..', 'client'); + +// browserify accepts either a stream or a file path +const glue = new Readable({ + read() { + console.log('read'); + this.push("window.AppsEngineUIClient = require('./AppsEngineUIClient').AppsEngineUIClient;"); + this.push(null); + }, +}); + +async function main() { + const bundle = await new Promise((resolve, reject) => + browserify(glue, { + basedir: targetDir, + }).bundle((err, bundle) => { + if (err) return reject(err); + + resolve(bundle.toString()); + }), + ); + + const result = minify(bundle); + + fs.writeFileSync(path.join(targetDir, 'AppsEngineUIClient.min.js'), result.code); +} + +main(); diff --git a/packages/apps-engine/scripts/deno-cache.js b/packages/apps-engine/scripts/deno-cache.js new file mode 100644 index 000000000000..acf2f4b977b4 --- /dev/null +++ b/packages/apps-engine/scripts/deno-cache.js @@ -0,0 +1,25 @@ +const childProcess = require('child_process'); +const path = require('path'); + +try { + childProcess.execSync('deno info'); +} catch (e) { + console.error( + 'Could not execute "deno" in the system. It is now a requirement for the Apps-Engine framework, and Rocket.Chat apps will not work without it.\n', + 'Make sure to install Deno and run the installation process for the Apps-Engine again. More info on https://docs.deno.com/runtime/manual/getting_started/installation', + ); + process.exit(1); +} + +const rootPath = path.join(__dirname, '..'); +const denoRuntimePath = path.join(rootPath, 'deno-runtime'); +const DENO_DIR = process.env.DENO_DIR ?? path.join(rootPath, '.deno-cache'); + +childProcess.execSync('deno cache main.ts', { + cwd: denoRuntimePath, + env: { + DENO_DIR, + PATH: process.env.PATH, + }, + stdio: 'inherit', +}); diff --git a/packages/apps-engine/src/client/AppClientManager.ts b/packages/apps-engine/src/client/AppClientManager.ts new file mode 100644 index 000000000000..5d409f148f5f --- /dev/null +++ b/packages/apps-engine/src/client/AppClientManager.ts @@ -0,0 +1,28 @@ +import type { IAppInfo } from '../definition/metadata'; +import { AppServerCommunicator } from './AppServerCommunicator'; +import { AppsEngineUIHost } from './AppsEngineUIHost'; + +export class AppClientManager { + private apps: Array; + + constructor(private readonly appsEngineUIHost: AppsEngineUIHost, private readonly communicator?: AppServerCommunicator) { + if (!(appsEngineUIHost instanceof AppsEngineUIHost)) { + throw new Error('The appClientUIHost must extend appClientUIHost'); + } + + if (communicator && !(communicator instanceof AppServerCommunicator)) { + throw new Error('The communicator must extend AppServerCommunicator'); + } + + this.apps = []; + } + + public async load(): Promise { + this.apps = await this.communicator.getEnabledApps(); + console.log('Enabled apps:', this.apps); + } + + public async initialize(): Promise { + this.appsEngineUIHost.initialize(); + } +} diff --git a/packages/apps-engine/src/client/AppServerCommunicator.ts b/packages/apps-engine/src/client/AppServerCommunicator.ts new file mode 100644 index 000000000000..fae400bc7ff7 --- /dev/null +++ b/packages/apps-engine/src/client/AppServerCommunicator.ts @@ -0,0 +1,16 @@ +import type { IAppInfo } from '../definition/metadata'; + +export abstract class AppServerCommunicator { + public abstract getEnabledApps(): Promise>; + + public abstract getDisabledApps(): Promise>; + + // Map> + public abstract getLanguageAdditions(): Promise>>; + + // Map> + public abstract getSlashCommands(): Promise>>; + + // Map> + public abstract getContextualBarButtons(): Promise>>; +} diff --git a/packages/apps-engine/src/client/AppsEngineUIClient.ts b/packages/apps-engine/src/client/AppsEngineUIClient.ts new file mode 100644 index 000000000000..620be5e21d01 --- /dev/null +++ b/packages/apps-engine/src/client/AppsEngineUIClient.ts @@ -0,0 +1,70 @@ +import { ACTION_ID_LENGTH, MESSAGE_ID } from './constants'; +import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from './definition'; +import { AppsEngineUIMethods } from './definition/AppsEngineUIMethods'; +import { randomString } from './utils'; + +/** + * Represents the SDK provided to the external component. + */ +export class AppsEngineUIClient { + private listener: (this: Window, ev: MessageEvent) => any; + + private callbacks: Map any>; + + constructor() { + this.listener = () => console.log('init'); + this.callbacks = new Map(); + } + + /** + * Get the current user's information. + * + * @return the information of the current user. + */ + public getUserInfo(): Promise { + return this.call(AppsEngineUIMethods.GET_USER_INFO); + } + + /** + * Get the current room's information. + * + * @return the information of the current room. + */ + public getRoomInfo(): Promise { + return this.call(AppsEngineUIMethods.GET_ROOM_INFO); + } + + /** + * Initialize the app SDK for communicating with Rocket.Chat + */ + public init(): void { + this.listener = ({ data }) => { + if (!data?.hasOwnProperty(MESSAGE_ID)) { + return; + } + + const { + [MESSAGE_ID]: { id, payload }, + } = data; + + if (this.callbacks.has(id)) { + const resolve = this.callbacks.get(id); + + if (typeof resolve === 'function') { + resolve(payload); + } + this.callbacks.delete(id); + } + }; + window.addEventListener('message', this.listener); + } + + private call(action: string, payload?: any): Promise { + return new Promise((resolve) => { + const id = randomString(ACTION_ID_LENGTH); + + window.parent.postMessage({ [MESSAGE_ID]: { action, payload, id } }, '*'); + this.callbacks.set(id, resolve); + }); + } +} diff --git a/packages/apps-engine/src/client/AppsEngineUIHost.ts b/packages/apps-engine/src/client/AppsEngineUIHost.ts new file mode 100644 index 000000000000..02f82f236ed2 --- /dev/null +++ b/packages/apps-engine/src/client/AppsEngineUIHost.ts @@ -0,0 +1,78 @@ +import { MESSAGE_ID } from './constants'; +import type { IAppsEngineUIResponse, IExternalComponentRoomInfo, IExternalComponentUserInfo } from './definition'; +import { AppsEngineUIMethods } from './definition'; + +type HandleActionData = IExternalComponentUserInfo | IExternalComponentRoomInfo; + +/** + * Represents the host which handles API calls from external components. + */ +export abstract class AppsEngineUIHost { + /** + * The message emitter who calling the API. + */ + private responseDestination!: Window; + + constructor() { + this.initialize(); + } + + /** + * initialize the AppClientUIHost by registering window `message` listener + */ + public initialize() { + window.addEventListener('message', async ({ data, source }) => { + if (!data?.hasOwnProperty(MESSAGE_ID)) { + return; + } + + this.responseDestination = source as Window; + + const { + [MESSAGE_ID]: { action, id }, + } = data; + + switch (action) { + case AppsEngineUIMethods.GET_USER_INFO: + this.handleAction(action, id, await this.getClientUserInfo()); + break; + case AppsEngineUIMethods.GET_ROOM_INFO: + this.handleAction(action, id, await this.getClientRoomInfo()); + break; + } + }); + } + + /** + * Get the current user's information. + */ + public abstract getClientUserInfo(): Promise; + + /** + * Get the opened room's information. + */ + public abstract getClientRoomInfo(): Promise; + + /** + * Handle the action sent from the external component. + * @param action the name of the action + * @param id the unique id of the API call + * @param data The data that will return to the caller + */ + private async handleAction(action: AppsEngineUIMethods, id: string, data: HandleActionData): Promise { + if (this.responseDestination instanceof MessagePort || this.responseDestination instanceof ServiceWorker) { + return; + } + + this.responseDestination.postMessage( + { + [MESSAGE_ID]: { + id, + action, + payload: data, + } as IAppsEngineUIResponse, + }, + '*', + ); + } +} diff --git a/packages/apps-engine/src/client/constants/index.ts b/packages/apps-engine/src/client/constants/index.ts new file mode 100644 index 000000000000..bd7f2e779ca1 --- /dev/null +++ b/packages/apps-engine/src/client/constants/index.ts @@ -0,0 +1,6 @@ +/** + * The id length of each action. + */ +export const ACTION_ID_LENGTH = 80; + +export const MESSAGE_ID = 'rc-apps-engine-ui'; diff --git a/packages/apps-engine/src/client/definition/AppsEngineUIMethods.ts b/packages/apps-engine/src/client/definition/AppsEngineUIMethods.ts new file mode 100644 index 000000000000..df150f9ce62b --- /dev/null +++ b/packages/apps-engine/src/client/definition/AppsEngineUIMethods.ts @@ -0,0 +1,7 @@ +/** + * The actions provided by the AppClientSDK. + */ +export enum AppsEngineUIMethods { + GET_USER_INFO = 'getUserInfo', + GET_ROOM_INFO = 'getRoomInfo', +} diff --git a/packages/apps-engine/src/client/definition/IAppsEngineUIResponse.ts b/packages/apps-engine/src/client/definition/IAppsEngineUIResponse.ts new file mode 100644 index 000000000000..dff7289226a4 --- /dev/null +++ b/packages/apps-engine/src/client/definition/IAppsEngineUIResponse.ts @@ -0,0 +1,19 @@ +import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from './index'; + +/** + * The response to the AppClientSDK's API call. + */ +export interface IAppsEngineUIResponse { + /** + * The name of the action + */ + action: string; + /** + * The unique id of the API call + */ + id: string; + /** + * The data that will return to the caller + */ + payload: IExternalComponentUserInfo | IExternalComponentRoomInfo; +} diff --git a/packages/apps-engine/src/client/definition/IExternalComponentRoomInfo.ts b/packages/apps-engine/src/client/definition/IExternalComponentRoomInfo.ts new file mode 100644 index 000000000000..32ca449e9650 --- /dev/null +++ b/packages/apps-engine/src/client/definition/IExternalComponentRoomInfo.ts @@ -0,0 +1,16 @@ +import type { IRoom } from '../../definition/rooms'; +import type { IExternalComponentUserInfo } from './IExternalComponentUserInfo'; + +type ClientRoomInfo = Pick; + +/** + * Represents the room's information returned to the + * external component. + */ +export interface IExternalComponentRoomInfo extends ClientRoomInfo { + /** + * the list that contains all the users belonging + * to this room. + */ + members: Array; +} diff --git a/packages/apps-engine/src/client/definition/IExternalComponentUserInfo.ts b/packages/apps-engine/src/client/definition/IExternalComponentUserInfo.ts new file mode 100644 index 000000000000..9212f5b39876 --- /dev/null +++ b/packages/apps-engine/src/client/definition/IExternalComponentUserInfo.ts @@ -0,0 +1,14 @@ +import type { IUser } from '../../definition/users'; + +type ClientUserInfo = Pick; + +/** + * Represents the user's information returned to + * the external component. + */ +export interface IExternalComponentUserInfo extends ClientUserInfo { + /** + * the avatar URL of the Rocket.Chat user + */ + avatarUrl: string; +} diff --git a/packages/apps-engine/src/client/definition/index.ts b/packages/apps-engine/src/client/definition/index.ts new file mode 100644 index 000000000000..70a1fe884a6a --- /dev/null +++ b/packages/apps-engine/src/client/definition/index.ts @@ -0,0 +1,4 @@ +export * from './AppsEngineUIMethods'; +export * from './IExternalComponentUserInfo'; +export * from './IExternalComponentRoomInfo'; +export * from './IAppsEngineUIResponse'; diff --git a/packages/apps-engine/src/client/index.ts b/packages/apps-engine/src/client/index.ts new file mode 100644 index 000000000000..2ebfee0264d2 --- /dev/null +++ b/packages/apps-engine/src/client/index.ts @@ -0,0 +1,4 @@ +import { AppClientManager } from './AppClientManager'; +import { AppServerCommunicator } from './AppServerCommunicator'; + +export { AppClientManager, AppServerCommunicator }; diff --git a/packages/apps-engine/src/client/utils/index.ts b/packages/apps-engine/src/client/utils/index.ts new file mode 100644 index 000000000000..f5e851e7d50f --- /dev/null +++ b/packages/apps-engine/src/client/utils/index.ts @@ -0,0 +1,18 @@ +/** + * Generate a random string with the specified length. + * @param length the length for the generated random string. + */ +export function randomString(length: number): string { + const buffer: Array = []; + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + for (let i = 0; i < length; i++) { + buffer.push(chars[getRandomInt(chars.length)]); + } + + return buffer.join(''); +} + +function getRandomInt(max: number): number { + return Math.floor(Math.random() * Math.floor(max)); +} diff --git a/packages/apps-engine/src/definition/App.ts b/packages/apps-engine/src/definition/App.ts new file mode 100644 index 000000000000..0044f35134b6 --- /dev/null +++ b/packages/apps-engine/src/definition/App.ts @@ -0,0 +1,236 @@ +import { AppStatus } from './AppStatus'; +import type { IApp } from './IApp'; +import type { + IAppAccessors, + IAppInstallationContext, + IAppUninstallationContext, + IConfigurationExtend, + IConfigurationModify, + IEnvironmentRead, + IHttp, + ILogger, + IModify, + IPersistence, + IRead, + IAppUpdateContext, +} from './accessors'; +import type { IAppAuthorInfo } from './metadata/IAppAuthorInfo'; +import type { IAppInfo } from './metadata/IAppInfo'; +import type { ISetting } from './settings'; +import type { ISettingUpdateContext } from './settings/ISettingUpdateContext'; + +export abstract class App implements IApp { + private status: AppStatus = AppStatus.UNKNOWN; + + /** + * Create a new App, this is called whenever the server starts up and initiates the Apps. + * Note, your implementation of this class should call `super(name, id, version)` so we have it. + * Also, please use the `initialize()` method to do items instead of the constructor as the constructor + * *might* be called more than once but the `initialize()` will only be called once. + */ + public constructor(private readonly info: IAppInfo, private readonly logger: ILogger, private readonly accessors?: IAppAccessors) { + this.logger.debug( + `Constructed the App ${this.info.name} (${this.info.id})`, + `v${this.info.version} which depends on the API v${this.info.requiredApiVersion}!`, + `Created by ${this.info.author.name}`, + ); + + this.setStatus(AppStatus.CONSTRUCTED); + } + + public async getStatus(): Promise { + return this.status; + } + + /** + * Get the name of this App. + * + * @return {string} the name + */ + public getName(): string { + return this.info.name; + } + + /** + * Gets the sluggified name of this App. + * + * @return {string} the name slugged + */ + public getNameSlug(): string { + return this.info.nameSlug; + } + + /** + * Gets the username of this App's app user. + * + * @return {string} the username of the app user + * + * @deprecated This method will be removed in the next major version. + * Please use read.getUserReader().getAppUser() instead. + */ + public getAppUserUsername(): string { + return `${this.info.nameSlug}.bot`; + } + + /** + * Get the ID of this App, please see for how to obtain an ID for your App. + * + * @return {number} the ID + */ + public getID(): string { + return this.info.id; + } + + /** + * Get the version of this App, using http://semver.org/. + * + * @return {string} the version + */ + public getVersion(): string { + return this.info.version; + } + + /** + * Get the description of this App, mostly used to show to the clients/administrators. + * + * @return {string} the description + */ + public getDescription(): string { + return this.info.description; + } + + /** + * Gets the API Version which this App depends on (http://semver.org/). + * This property is used for the dependency injections. + * + * @return {string} the required api version + */ + public getRequiredApiVersion(): string { + return this.info.requiredApiVersion; + } + + /** + * Gets the information regarding the author/maintainer of this App. + * + * @return author information + */ + public getAuthorInfo(): IAppAuthorInfo { + return this.info.author; + } + + /** + * Gets the entirity of the App's information. + * + * @return App information + */ + public getInfo(): IAppInfo { + return this.info; + } + + /** + * Gets the ILogger instance for this App. + * + * @return the logger instance + */ + public getLogger(): ILogger { + return this.logger; + } + + public getAccessors(): IAppAccessors { + return this.accessors; + } + + /** + * Method which will be called when the App is initialized. This is the recommended place + * to add settings and slash commands. If an error is thrown, all commands will be unregistered. + */ + public async initialize(configurationExtend: IConfigurationExtend, environmentRead: IEnvironmentRead): Promise { + await this.extendConfiguration(configurationExtend, environmentRead); + } + + /** + * Method which is called when this App is enabled and can be called several + * times during this instance's life time. Once after the `initialize()` is called, + * pending it doesn't throw an error, and then anytime the App is enabled by the user. + * If this method, `onEnable()`, returns false, then this App will not + * actually be enabled (ex: a setting isn't configured). + * + * @return whether the App should be enabled or not + */ + public async onEnable(environment: IEnvironmentRead, configurationModify: IConfigurationModify): Promise { + return true; + } + + /** + * Method which is called when this App is disabled and it can be called several times. + * If this App was enabled and then the user disabled it, this method will be called. + */ + public async onDisable(configurationModify: IConfigurationModify): Promise {} + + /** + * Method which is called when the App is uninstalled and it is called one single time. + * + * This method will NOT be called when an App is getting disabled manually, ONLY when + * it's being uninstalled from Rocket.Chat. + */ + public async onUninstall(context: IAppUninstallationContext, read: IRead, http: IHttp, persistence: IPersistence, modify: IModify): Promise {} + + /** + * Method which is called when the App is installed and it is called one single time. + * + * This method is NOT called when the App is updated. + */ + public async onInstall(context: IAppInstallationContext, read: IRead, http: IHttp, persistence: IPersistence, modify: IModify): Promise {} + + /** + * Method which is called when the App is updated and it is called one single time. + * + * This method is NOT called when the App is installed. + */ + public async onUpdate(context: IAppUpdateContext, read: IRead, http: IHttp, persistence: IPersistence, modify: IModify): Promise {} + + /** + * Method which is called whenever a setting which belongs to this App has been updated + * by an external system and not this App itself. The setting passed is the newly updated one. + * + * @param setting the setting which was updated + * @param configurationModify the accessor to modifiy the system + * @param reader the reader accessor + * @param http an accessor to the outside world + */ + public async onSettingUpdated(setting: ISetting, configurationModify: IConfigurationModify, read: IRead, http: IHttp): Promise {} + + /** + * Method which is called before a setting which belongs to this App is going to be updated + * by an external system and not this App itself. The setting passed is the newly updated one. + * + * @param setting the setting which is going to be updated + * @param configurationModify the accessor to modifiy the system + * @param reader the reader accessor + * @param http an accessor to the outside world + */ + public async onPreSettingUpdate(context: ISettingUpdateContext, configurationModify: IConfigurationModify, read: IRead, http: IHttp): Promise { + return context.newSetting; + } + + /** + * Method will be called during initialization. It allows for adding custom configuration options and defaults + * @param configuration + */ + protected async extendConfiguration(configuration: IConfigurationExtend, environmentRead: IEnvironmentRead): Promise {} + + /** + * Sets the status this App is now at, use only when 100% true (it's protected for a reason). + * + * @param status the new status of this App + */ + protected async setStatus(status: AppStatus): Promise { + this.logger.debug(`The status is now: ${status}`); + this.status = status; + } + + // Avoid leaking references if object is serialized (e.g. to be sent over IPC) + public toJSON(): Record { + return this.info; + } +} diff --git a/packages/apps-engine/src/definition/AppStatus.ts b/packages/apps-engine/src/definition/AppStatus.ts new file mode 100644 index 000000000000..31638a8d0f1c --- /dev/null +++ b/packages/apps-engine/src/definition/AppStatus.ts @@ -0,0 +1,65 @@ +export enum AppStatus { + /** The status is known, aka not been constructed the proper way. */ + UNKNOWN = 'unknown', + /** The App has been constructed but that's it. */ + CONSTRUCTED = 'constructed', + /** The App's `initialize()` was called and returned true. */ + INITIALIZED = 'initialized', + /** The App's `onEnable()` was called, returned true, and this was done automatically (system start up). */ + AUTO_ENABLED = 'auto_enabled', + /** The App's `onEnable()` was called, returned true, and this was done by the user such as installing a new one. */ + MANUALLY_ENABLED = 'manually_enabled', + /** + * The App was disabled due to an error while attempting to compile it. + * An attempt to enable it again will fail, as it needs to be updated. + */ + COMPILER_ERROR_DISABLED = 'compiler_error_disabled', + /** + * The App was disable due to its license being invalid + */ + INVALID_LICENSE_DISABLED = 'invalid_license_disabled', + /** + * The app was disabled due to an invalid installation or validation in its signature. + */ + INVALID_INSTALLATION_DISABLED = 'invalid_installation_disabled', + /** The App was disabled due to an unrecoverable error being thrown. */ + ERROR_DISABLED = 'error_disabled', + /** The App was manually disabled by a user. */ + MANUALLY_DISABLED = 'manually_disabled', + INVALID_SETTINGS_DISABLED = 'invalid_settings_disabled', + /** The App was disabled due to other circumstances. */ + DISABLED = 'disabled', +} + +export class AppStatusUtilsDef { + public isEnabled(status: AppStatus): boolean { + switch (status) { + case AppStatus.AUTO_ENABLED: + case AppStatus.MANUALLY_ENABLED: + return true; + default: + return false; + } + } + + public isDisabled(status: AppStatus): boolean { + switch (status) { + case AppStatus.COMPILER_ERROR_DISABLED: + case AppStatus.ERROR_DISABLED: + case AppStatus.MANUALLY_DISABLED: + case AppStatus.INVALID_SETTINGS_DISABLED: + case AppStatus.INVALID_LICENSE_DISABLED: + case AppStatus.INVALID_INSTALLATION_DISABLED: + case AppStatus.DISABLED: + return true; + default: + return false; + } + } + + public isError(status: AppStatus): boolean { + return [AppStatus.ERROR_DISABLED, AppStatus.COMPILER_ERROR_DISABLED].includes(status); + } +} + +export const AppStatusUtils = new AppStatusUtilsDef(); diff --git a/packages/apps-engine/src/definition/IApp.ts b/packages/apps-engine/src/definition/IApp.ts new file mode 100644 index 000000000000..53faff9647f4 --- /dev/null +++ b/packages/apps-engine/src/definition/IApp.ts @@ -0,0 +1,90 @@ +import type { AppStatus } from './AppStatus'; +import type { IAppAccessors } from './accessors'; +import type { ILogger } from './accessors/ILogger'; +import type { IAppAuthorInfo } from './metadata/IAppAuthorInfo'; +import type { IAppInfo } from './metadata/IAppInfo'; + +export interface IApp { + /** + * Gets the status of this App. + * + * @return {AppStatus} the status/state of the App + */ + getStatus(): Promise; + + /** + * Get the name of this App. + * + * @return {string} the name + */ + getName(): string; + + /** + * Gets the sluggified name of this App. + * + * @return {string} the name slugged + */ + getNameSlug(): string; + + /** + * Gets the username of this App's app user. + * + * @return {string} the username of the app user + * + * @deprecated This method will be removed in the next major version. + * Please use read.getAppUser instead. + */ + getAppUserUsername(): string; + + /** + * Get the ID of this App, please see for how to obtain an ID for your App. + * + * @return {number} the ID + */ + getID(): string; + + /** + * Get the version of this App, using http://semver.org/. + * + * @return {string} the version + */ + getVersion(): string; + + /** + * Get the description of this App, mostly used to show to the clients/administrators. + * + * @return {string} the description + */ + getDescription(): string; + + /** + * Gets the API Version which this App depends on (http://semver.org/). + * This property is used for the dependency injections. + * + * @return {string} the required api version + */ + getRequiredApiVersion(): string; + + /** + * Gets the information regarding the author/maintainer of this App. + * + * @return author information + */ + getAuthorInfo(): IAppAuthorInfo; + + /** + * Gets the entirity of the App's information. + * + * @return App information + */ + getInfo(): IAppInfo; + + /** + * Gets the ILogger instance for this App. + * + * @return the logger instance + */ + getLogger(): ILogger; + + getAccessors(): IAppAccessors; +} diff --git a/packages/apps-engine/src/definition/LICENSE b/packages/apps-engine/src/definition/LICENSE new file mode 100644 index 000000000000..42ea81dc8cdb --- /dev/null +++ b/packages/apps-engine/src/definition/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015-2023 Rocket.Chat Technologies Corp. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/apps-engine/src/definition/accessors/IApiExtend.ts b/packages/apps-engine/src/definition/accessors/IApiExtend.ts new file mode 100644 index 000000000000..ab3210105e9c --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IApiExtend.ts @@ -0,0 +1,16 @@ +import type { IApi } from '../api'; + +/** + * This accessor provides methods for adding a custom api. + * It is provided during the initialization of your App + */ + +export interface IApiExtend { + /** + * Adds an api which can be called by external services lateron. + * Should an api already exists an error will be thrown. + * + * @param api the command information + */ + provideApi(api: IApi): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IAppAccessors.ts b/packages/apps-engine/src/definition/accessors/IAppAccessors.ts new file mode 100644 index 000000000000..c2ea3bfcacea --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IAppAccessors.ts @@ -0,0 +1,11 @@ +import type { IEnvironmentRead, IHttp, IRead } from '.'; +import type { IApiEndpointMetadata } from '../api'; +import type { IEnvironmentWrite } from './IEnvironmentWrite'; + +export interface IAppAccessors { + readonly environmentReader: IEnvironmentRead; + readonly environmentWriter: IEnvironmentWrite; + readonly reader: IRead; + readonly http: IHttp; + readonly providedApiEndpoints: Array; +} diff --git a/packages/apps-engine/src/definition/accessors/IAppInstallationContext.ts b/packages/apps-engine/src/definition/accessors/IAppInstallationContext.ts new file mode 100644 index 000000000000..0ca1c08ba0dc --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IAppInstallationContext.ts @@ -0,0 +1,5 @@ +import type { IUser } from '../users'; + +export interface IAppInstallationContext { + user: IUser; +} diff --git a/packages/apps-engine/src/definition/accessors/IAppUninstallationContext.ts b/packages/apps-engine/src/definition/accessors/IAppUninstallationContext.ts new file mode 100644 index 000000000000..96ddbfa03298 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IAppUninstallationContext.ts @@ -0,0 +1,5 @@ +import type { IUser } from '../users'; + +export interface IAppUninstallationContext { + user: IUser; +} diff --git a/packages/apps-engine/src/definition/accessors/IAppUpdateContext.ts b/packages/apps-engine/src/definition/accessors/IAppUpdateContext.ts new file mode 100644 index 000000000000..d0bcf7ea280b --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IAppUpdateContext.ts @@ -0,0 +1,6 @@ +import type { IUser } from '../users'; + +export interface IAppUpdateContext { + user?: IUser; + oldAppVersion: string; +} diff --git a/packages/apps-engine/src/definition/accessors/ICloudWorkspaceRead.ts b/packages/apps-engine/src/definition/accessors/ICloudWorkspaceRead.ts new file mode 100644 index 000000000000..c78749fae59b --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ICloudWorkspaceRead.ts @@ -0,0 +1,24 @@ +import type { IWorkspaceToken } from '../cloud/IWorkspaceToken'; + +/** + * Accessor that enables apps to read information + * related to the Cloud connectivity of the workspace. + * + * Methods in this accessor will usually connect to the + * Rocket.Chat Cloud, which means they won't work properly + * in air-gapped environment. + * + * This accessor available via `IRead` object, which is + * usually received as a parameter wherever it's available. + */ +export interface ICloudWorkspaceRead { + /** + * Returns an access token that can be used to access + * Cloud Services on the workspace's behalf. + * + * @param scope The scope that the token should be authorized with + * + * @RequiresPermission cloud.workspace-token; scopes: Array + */ + getWorkspaceToken(scope: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IConfigurationExtend.ts b/packages/apps-engine/src/definition/accessors/IConfigurationExtend.ts new file mode 100644 index 000000000000..a58f127a0421 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IConfigurationExtend.ts @@ -0,0 +1,36 @@ +import type { IApiExtend } from './IApiExtend'; +import type { IExternalComponentsExtend } from './IExternalComponentsExtend'; +import type { IHttpExtend } from './IHttp'; +import type { ISchedulerExtend } from './ISchedulerExtend'; +import type { ISettingsExtend } from './ISettingsExtend'; +import type { ISlashCommandsExtend } from './ISlashCommandsExtend'; +import type { IUIExtend } from './IUIExtend'; +import type { IVideoConfProvidersExtend } from './IVideoConfProvidersExtend'; + +/** + * This accessor provides methods for declaring the configuration + * of your App. It is provided during initialization of your App. + */ +export interface IConfigurationExtend { + /** Accessor for customing the handling of IHttp requests and responses your App causes. */ + readonly http: IHttpExtend; + + /** Accessor for declaring the settings your App provides. */ + readonly settings: ISettingsExtend; + + /** Accessor for declaring the commands which your App provides. */ + readonly slashCommands: ISlashCommandsExtend; + + /** Accessor for declaring api endpoints. */ + readonly api: IApiExtend; + + readonly externalComponents: IExternalComponentsExtend; + + /** Accessor for declaring tasks that can be scheduled (like cron) */ + readonly scheduler: ISchedulerExtend; + /** Accessor for registering different elements in the host UI */ + readonly ui: IUIExtend; + + /** Accessor for declaring the videoconf providers which your App provides. */ + readonly videoConfProviders: IVideoConfProvidersExtend; +} diff --git a/packages/apps-engine/src/definition/accessors/IConfigurationModify.ts b/packages/apps-engine/src/definition/accessors/IConfigurationModify.ts new file mode 100644 index 000000000000..d0f818e2e028 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IConfigurationModify.ts @@ -0,0 +1,18 @@ +import type { ISchedulerModify } from './ISchedulerModify'; +import type { IServerSettingsModify } from './IServerSettingsModify'; +import type { ISlashCommandsModify } from './ISlashCommandsModify'; + +/** + * This accessor provides methods for modifying the configuration + * of Rocket.Chat. It is provided during "onEnable" of your App. + */ +export interface IConfigurationModify { + /** Accessor for modifying the settings inside of Rocket.Chat. */ + readonly serverSettings: IServerSettingsModify; + + /** Accessor for modifying the slash commands inside of Rocket.Chat. */ + readonly slashCommands: ISlashCommandsModify; + + /** Accessor for modifying schedulers */ + readonly scheduler: ISchedulerModify; +} diff --git a/packages/apps-engine/src/definition/accessors/IDiscussionBuilder.ts b/packages/apps-engine/src/definition/accessors/IDiscussionBuilder.ts new file mode 100644 index 000000000000..51dc0c2c4f92 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IDiscussionBuilder.ts @@ -0,0 +1,25 @@ +import type { IRoomBuilder } from '.'; +import type { IMessage } from '../messages'; +import type { RocketChatAssociationModel } from '../metadata'; +import type { IRoom } from '../rooms'; + +/** + * Interface for building out a room. + * Please note, a room creator, name, and type must be set otherwise you will NOT + * be able to successfully save the room object. + */ +export interface IDiscussionBuilder extends IRoomBuilder { + kind: RocketChatAssociationModel.DISCUSSION; + + setParentRoom(parentRoom: IRoom): IDiscussionBuilder; + + getParentRoom(): IRoom; + + setParentMessage(parentMessage: IMessage): IDiscussionBuilder; + + getParentMessage(): IMessage; + + setReply(reply: string): IDiscussionBuilder; + + getReply(): string; +} diff --git a/packages/apps-engine/src/definition/accessors/IEmailCreator.ts b/packages/apps-engine/src/definition/accessors/IEmailCreator.ts new file mode 100644 index 000000000000..d5d051bc2dff --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IEmailCreator.ts @@ -0,0 +1,10 @@ +import type { IEmail } from '../email'; + +export interface IEmailCreator { + /** + * Sends an email through Rocket.Chat + * + * @param email the email data + */ + send(email: IEmail): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IEnvironmentRead.ts b/packages/apps-engine/src/definition/accessors/IEnvironmentRead.ts new file mode 100644 index 000000000000..81b50bee77c4 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IEnvironmentRead.ts @@ -0,0 +1,27 @@ +import type { IEnvironmentalVariableRead } from './IEnvironmentalVariableRead'; +import type { IServerSettingRead } from './IServerSettingRead'; +import type { ISettingRead } from './ISettingRead'; + +/** + * Allows read-access to the App's settings, + * the certain server's settings along with environmental + * variables all of which are not user created. + */ +export interface IEnvironmentRead { + /** Gets an instance of the App's settings reader. */ + getSettings(): ISettingRead; + + /** + * Gets an instance of the Server's Settings reader. + * Please note: Due to security concerns, only a subset of settings + * are accessible. + */ + getServerSettings(): IServerSettingRead; + + /** + * Gets an instance of the Environmental Variables reader. + * Please note: Due to security concerns, only a subset of + * them are readable. + */ + getEnvironmentVariables(): IEnvironmentalVariableRead; +} diff --git a/packages/apps-engine/src/definition/accessors/IEnvironmentWrite.ts b/packages/apps-engine/src/definition/accessors/IEnvironmentWrite.ts new file mode 100644 index 000000000000..ab1869ab67cb --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IEnvironmentWrite.ts @@ -0,0 +1,10 @@ +import type { IServerSettingUpdater } from './IServerSettingUpdater'; +import type { ISettingUpdater } from './ISettingUpdater'; + +/** + * Allows write-access to the App's settings, + */ +export interface IEnvironmentWrite { + getSettings(): ISettingUpdater; + getServerSettings(): IServerSettingUpdater; +} diff --git a/packages/apps-engine/src/definition/accessors/IEnvironmentalVariableRead.ts b/packages/apps-engine/src/definition/accessors/IEnvironmentalVariableRead.ts new file mode 100644 index 000000000000..3bb77b033e83 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IEnvironmentalVariableRead.ts @@ -0,0 +1,11 @@ +/** A reader for reading the Environmental Variables. */ +export interface IEnvironmentalVariableRead { + /** Gets the value for a variable. */ + getValueByName(envVarName: string): Promise; + + /** Checks to see if Apps can access the given variable name. */ + isReadable(envVarName: string): Promise; + + /** Checks to see if any value is set for the given variable name. */ + isSet(envVarName: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IExternalComponentsExtend.ts b/packages/apps-engine/src/definition/accessors/IExternalComponentsExtend.ts new file mode 100644 index 000000000000..6a3dc781056f --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IExternalComponentsExtend.ts @@ -0,0 +1,17 @@ +import type { IExternalComponent } from '../externalComponent'; + +/** + * This accessor provides a method for registering external + * components. This is provided during the initialization of your App. + */ +export interface IExternalComponentsExtend { + /** + * Register an external component to the system. + * If you call this method twice and the component + * has the same name as before, the first one will be + * overwritten as the names provided **must** be unique. + * + * @param externalComponent the external component to be registered + */ + register(externalComponent: IExternalComponent): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IHttp.ts b/packages/apps-engine/src/definition/accessors/IHttp.ts new file mode 100644 index 000000000000..8c5eeb9a4d55 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IHttp.ts @@ -0,0 +1,202 @@ +import type { IPersistence } from './IPersistence'; +import type { IRead } from './IRead'; + +/** + * The Http package allows users to call out to an external web service. + * Based off of: https://github.com/meteor-typings/meteor/blob/master/1.4/main.d.ts#L869 + */ +export interface IHttp { + get(url: string, options?: IHttpRequest): Promise; + + post(url: string, options?: IHttpRequest): Promise; + + put(url: string, options?: IHttpRequest): Promise; + + del(url: string, options?: IHttpRequest): Promise; + + patch(url: string, options?: IHttpRequest): Promise; +} + +export enum RequestMethod { + GET = 'get', + POST = 'post', + PUT = 'put', + DELETE = 'delete', + HEAD = 'head', + OPTIONS = 'options', + PATCH = 'patch', +} + +export interface IHttpRequest { + content?: string; + data?: any; + query?: string; + params?: { + [key: string]: string; + }; + auth?: string; + headers?: { + [key: string]: string; + }; + timeout?: number; + /** + * The encoding to be used on response data. + * + * If null, the body is returned as a Buffer. Anything else (including the default value of undefined) + * will be passed as the encoding parameter to toString() (meaning this is effectively 'utf8' by default). + * (Note: if you expect binary data, you should set encoding: null.) + */ + encoding?: string | null; + /** + * if `true`, requires SSL certificates be valid. + * + * Defaul: `true`; + */ + strictSSL?: boolean; + /** + * If `true`, the server certificate is verified against the list of supplied CAs. + * + * Default: `true`. + * + * https://nodejs.org/api/tls.html#tls_tls_connect_options_callback + */ + rejectUnauthorized?: boolean; +} + +export interface IHttpResponse { + url: string; + method: RequestMethod; + statusCode: number; + headers?: { + [key: string]: string; + }; + content?: string; + data?: any; +} + +export interface IHttpExtend { + /** + * A method for providing a single header which is added to every request. + * + * @param key the name of the header + * @param value the header's content + */ + provideDefaultHeader(key: string, value: string): void; + + /** + * A method for providing more than one header which are added to every request. + * + * @param headers an object with strings as the keys (header name) and strings as values (header content) + */ + provideDefaultHeaders(headers: { [key: string]: string }): void; + + /** + * A method for providing a single query parameter which is added to every request. + * + * @param key the name of the query parameter + * @param value the query parameter's content + */ + provideDefaultParam(key: string, value: string): void; + + /** + * A method for providing more than one query parameters which are added to every request. + * + * @param headers an object with strings as the keys (parameter name) and strings as values (parameter content) + */ + provideDefaultParams(params: { [key: string]: string }): void; + + /** + * Method for providing a function which is called before every request is called out to the final destination. + * This can be called more than once which means there can be more than one handler. The order provided is the order called. + * Note: if this handler throws an error when it is executed then the request will be aborted. + * + * @param handler the instance of the IHttpPreRequestHandler + */ + providePreRequestHandler(handler: IHttpPreRequestHandler): void; + + /** + * Method for providing a function which is called after every response is got from the url and before the result is returned. + * This can be called more than once which means there can be more than one handler. The order provided is the order called. + * Note: if this handler throws an error when it is executed then the respone will not be returned + * + * @param handler the instance of the IHttpPreResponseHandler + */ + providePreResponseHandler(handler: IHttpPreResponseHandler): void; + + /** + * A method for getting all of the default headers provided, the value is a readonly and any modifications done will be ignored. + * Please use the provider methods for adding them. + */ + getDefaultHeaders(): Map; + + /** + * A method for getting all of the default parameters provided, the value is a readonly and any modifications done will be ignored. + * Please use the provider methods for adding them. + */ + getDefaultParams(): Map; + + /** + * A method for getting all of the pre-request handlers provided, the value is a readonly and any modifications done will be ignored. + * Please use the provider methods for adding them. + */ + getPreRequestHandlers(): Array; + + /** + * A method for getting all of the pre-response handlers provided, the value is a readonly and any modifications done will be ignored. + * Please use the provider methods for adding them. + */ + getPreResponseHandlers(): Array; +} + +export interface IHttpPreRequestHandler { + executePreHttpRequest(url: string, request: IHttpRequest, read: IRead, persistence: IPersistence): Promise; +} + +export interface IHttpPreResponseHandler { + executePreHttpResponse(response: IHttpResponse, read: IRead, persistence: IPersistence): Promise; +} + +export enum HttpStatusCode { + CONTINUE = 100, + SWITCHING_PROTOCOLS = 101, + OK = 200, + CREATED = 201, + ACCEPTED = 202, + NON_AUTHORITATIVE_INFORMATION = 203, + NO_CONTENT = 204, + RESET_CONTENT = 205, + PARTIAL_CONTENT = 206, + MULTIPLE_CHOICES = 300, + MOVED_PERMANENTLY = 301, + FOUND = 302, + SEE_OTHER = 303, + NOT_MODIFIED = 304, + USE_PROXY = 305, + TEMPORARY_REDIRECT = 307, + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + PAYMENT_REQUIRED = 402, + FORBIDDEN = 403, + NOT_FOUND = 404, + METHOD_NOT_ALLOWED = 405, + NOT_ACCEPTABLE = 406, + PROXY_AUTHENTICATION_REQUIRED = 407, + REQUEST_TIMEOUT = 408, + CONFLICT = 409, + GONE = 410, + LENGTH_REQUIRED = 411, + PRECONDITION_FAILED = 412, + REQUEST_ENTITY_TOO_LARGE = 413, + REQUEST_URI_TOO_LONG = 414, + UNSUPPORTED_MEDIA_TYPE = 415, + REQUESTED_RANGE_NOT_SATISFIABLE = 416, + EXPECTATION_FAILED = 417, + UNPROCESSABLE_ENTITY = 422, + TOO_MANY_REQUESTS = 429, + INTERNAL_SERVER_ERROR = 500, + NOT_IMPLEMENTED = 501, + BAD_GATEWAY = 502, + SERVICE_UNAVAILABLE = 503, + GATEWAY_TIMEOUT = 504, + HTTP_VERSION_NOT_SUPPORTED = 505, +} diff --git a/packages/apps-engine/src/definition/accessors/ILivechatCreator.ts b/packages/apps-engine/src/definition/accessors/ILivechatCreator.ts new file mode 100644 index 000000000000..56a3ec17ec27 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ILivechatCreator.ts @@ -0,0 +1,43 @@ +import type { ILivechatRoom, IVisitor } from '../livechat'; +import type { IUser } from '../users'; + +export interface IExtraRoomParams { + source?: ILivechatRoom['source']; + customFields?: { + [key: string]: unknown; + }; +} + +export interface ILivechatCreator { + /** + * Creates a room to connect the `visitor` to an `agent`. + * + * This method uses the Livechat routing method configured + * in the server + * + * @param visitor The Livechat Visitor that started the conversation + * @param agent The agent responsible for the room + */ + createRoom(visitor: IVisitor, agent: IUser, extraParams?: IExtraRoomParams): Promise; + + /** + * @deprecated Use `createAndReturnVisitor` instead. + * Creates a Livechat visitor + * + * @param visitor Data of the visitor to be created + */ + createVisitor(visitor: IVisitor): Promise; + + /** + * Creates a Livechat visitor + * + * @param visitor Data of the visitor to be created + */ + createAndReturnVisitor(visitor: IVisitor): Promise; + + /** + * Creates a token to be used when + * creating a new livechat visitor + */ + createToken(): string; +} diff --git a/packages/apps-engine/src/definition/accessors/ILivechatMessageBuilder.ts b/packages/apps-engine/src/definition/accessors/ILivechatMessageBuilder.ts new file mode 100644 index 000000000000..c36b078f53a6 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ILivechatMessageBuilder.ts @@ -0,0 +1,219 @@ +import type { ILivechatMessage, IVisitor } from '../livechat'; +import type { IMessageAttachment } from '../messages'; +import type { RocketChatAssociationModel } from '../metadata'; +import type { IRoom } from '../rooms'; +import type { IUser } from '../users'; +import type { IMessageBuilder } from './IMessageBuilder'; + +/** + * Interface for building out a livechat message. + * Please note, that a room and sender must be associated otherwise you will NOT + * be able to successfully save the message object. + */ +export interface ILivechatMessageBuilder { + kind: RocketChatAssociationModel.LIVECHAT_MESSAGE; + + /** + * Provides a convient way to set the data for the message. + * Note: Providing an "id" field here will be ignored. + * + * @param message the message data to set + */ + setData(message: ILivechatMessage): ILivechatMessageBuilder; + + /** + * Sets the room where this message should be sent to. + * + * @param room the room where to send + */ + setRoom(room: IRoom): ILivechatMessageBuilder; + + /** + * Gets the room where this message was sent to. + */ + getRoom(): IRoom; + + /** + * Sets the sender of this message. + * + * @param sender the user sending the message + */ + setSender(sender: IUser): ILivechatMessageBuilder; + + /** + * Gets the User which sent the message. + */ + getSender(): IUser; + + /** + * Sets the text of the message. + * + * @param text the actual text + */ + setText(text: string): ILivechatMessageBuilder; + + /** + * Gets the message text. + */ + getText(): string; + + /** + * Sets the emoji to use for the avatar, this overwrites the current avatar + * whether it be the user's or the avatar url provided. + * + * @param emoji the emoji code + */ + setEmojiAvatar(emoji: string): ILivechatMessageBuilder; + + /** + * Gets the emoji used for the avatar. + */ + getEmojiAvatar(): string; + + /** + * Sets the url which to display for the avatar, this overwrites the current + * avatar whether it be the user's or an emoji one. + * + * @param avatarUrl image url to use as the avatar + */ + setAvatarUrl(avatarUrl: string): ILivechatMessageBuilder; + + /** + * Gets the url used for the avatar. + */ + getAvatarUrl(): string; + + /** + * Sets the display text of the sender's username that is visible. + * + * @param alias the username alias to display + */ + setUsernameAlias(alias: string): ILivechatMessageBuilder; + + /** + * Gets the display text of the sender's username that is visible. + */ + getUsernameAlias(): string; + + /** + * Adds one attachment to the message's list of attachments, this will not + * overwrite any existing ones but just adds. + * + * @param attachment the attachment to add + */ + addAttachment(attachment: IMessageAttachment): ILivechatMessageBuilder; + + /** + * Sets the attachments for the message, replacing and destroying all of the current attachments. + * + * @param attachments array of the attachments + */ + setAttachments(attachments: Array): ILivechatMessageBuilder; + + /** + * Gets the attachments array for the message + */ + getAttachments(): Array; + + /** + * Replaces an attachment at the given position (index). + * If there is no attachment at that position, there will be an error thrown. + * + * @param position the index of the attachment to replace + * @param attachment the attachment to replace with + */ + replaceAttachment(position: number, attachment: IMessageAttachment): ILivechatMessageBuilder; + + /** + * Removes an attachment at the given position (index). + * If there is no attachment at that position, there will be an error thrown. + * + * @param position the index of the attachment to remove + */ + removeAttachment(position: number): ILivechatMessageBuilder; + + /** + * Sets the user who is editing this message. + * This is required if you are modifying an existing message. + * + * @param user the editor + */ + setEditor(user: IUser): ILivechatMessageBuilder; + + /** + * Gets the user who edited the message + */ + getEditor(): IUser; + + /** + * Sets whether this message can group with others. + * This is desirable if you want to avoid confusion with other integrations. + * + * @param groupable whether this message can group with others + */ + setGroupable(groupable: boolean): ILivechatMessageBuilder; + + /** + * Gets whether this message can group with others. + */ + getGroupable(): boolean; + + /** + * Sets whether this message should have any URLs in the text + * parsed by Rocket.Chat and get the details added to the message's + * attachments. + * + * @param parseUrls whether URLs should be parsed in this message + */ + setParseUrls(parseUrls: boolean): ILivechatMessageBuilder; + + /** + * Gets whether this message should have its URLs parsed + */ + getParseUrls(): boolean; + + /** + * Set the token of the livechat visitor that + * sent the message + * + * @param token The Livechat visitor's token + */ + setToken(token: string): ILivechatMessageBuilder; + + /** + * Gets the token of the livechat visitor that + * sent the message + */ + getToken(): string; + + /** + * If the sender of the message is a Livechat Visitor, + * set the visitor who sent the message. + * + * If you set the visitor property of a message, the + * sender will be emptied + * + * @param visitor The visitor who sent the message + */ + setVisitor(visitor: IVisitor): ILivechatMessageBuilder; + + /** + * Get the visitor who sent the message, + * if any + */ + getVisitor(): IVisitor; + + /** + * Gets the resulting message that has been built up to the point of calling it. + * + * *Note:* This will error out if the Room has not been defined OR if the room + * is not of type RoomType.LIVE_CHAT. + */ + getMessage(): ILivechatMessage; + + /** + * Returns a message builder based on the + * livechat message of this builder + */ + getMessageBuilder(): IMessageBuilder; +} diff --git a/packages/apps-engine/src/definition/accessors/ILivechatRead.ts b/packages/apps-engine/src/definition/accessors/ILivechatRead.ts new file mode 100644 index 000000000000..a756d162c337 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ILivechatRead.ts @@ -0,0 +1,38 @@ +import type { IDepartment } from '../livechat'; +import type { ILivechatRoom } from '../livechat/ILivechatRoom'; +import type { IVisitor } from '../livechat/IVisitor'; +import type { IMessage } from '../messages'; + +export interface ILivechatRead { + /** + * Gets online status of the livechat. + * @param departmentId (optional) the id of the livechat department + * @deprecated use `isOnlineAsync` instead + */ + isOnline(departmentId?: string): boolean; + /** + * Gets online status of the livechat. + * @param departmentId (optional) the id of the livechat department + */ + isOnlineAsync(departmentId?: string): Promise; + getDepartmentsEnabledWithAgents(): Promise>; + getLivechatRooms(visitor: IVisitor, departmentId?: string): Promise>; + getLivechatOpenRoomsByAgentId(agentId: string): Promise>; + getLivechatTotalOpenRoomsByAgentId(agentId: string): Promise; + /** + * @deprecated This method does not adhere to the conversion practices applied + * elsewhere in the Apps-Engine and will be removed in the next major version. + * Prefer the alternative methods to fetch visitors. + */ + getLivechatVisitors(query: object): Promise>; + getLivechatVisitorById(id: string): Promise; + getLivechatVisitorByEmail(email: string): Promise; + getLivechatVisitorByToken(token: string): Promise; + getLivechatVisitorByPhoneNumber(phoneNumber: string): Promise; + getLivechatDepartmentByIdOrName(value: string): Promise; + /** + * @experimental we do not encourage the wider usage of this method, + * as we're evaluating its performance and fit for the API. + */ + _fetchLivechatRoomMessages(roomId: string): Promise>; +} diff --git a/packages/apps-engine/src/definition/accessors/ILivechatUpdater.ts b/packages/apps-engine/src/definition/accessors/ILivechatUpdater.ts new file mode 100644 index 000000000000..fb75cf9ecf3b --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ILivechatUpdater.ts @@ -0,0 +1,33 @@ +import type { ILivechatTransferData, IVisitor } from '../livechat'; +import type { IRoom } from '../rooms'; +import type { IUser } from '../users'; + +export interface ILivechatUpdater { + /** + * Transfer a Livechat visitor to another room + * + * @param visitor Visitor to be transferred + * @param transferData The data to execute the transferring + */ + transferVisitor(visitor: IVisitor, transferData: ILivechatTransferData): Promise; + + /** + * Closes a Livechat room + * + * @param room The room to be closed + * @param comment The comment explaining the reason for closing the room + * @param closer The user that closes the room + */ + closeRoom(room: IRoom, comment: string, closer?: IUser): Promise; + + /** + * Set a livechat visitor's custom fields by its token + * @param token The visitor's token + * @param key The key in the custom fields + * @param value The value to be set + * @param overwrite Whether overwrite or not + * + * @returns Promise to whether success or not + */ + setCustomFields(token: IVisitor['token'], key: string, value: string, overwrite: boolean): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/ILogEntry.ts b/packages/apps-engine/src/definition/accessors/ILogEntry.ts new file mode 100644 index 000000000000..4dc46693b6d9 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ILogEntry.ts @@ -0,0 +1,22 @@ +export enum LogMessageSeverity { + DEBUG = 'debug', + INFORMATION = 'info', + LOG = 'log', + WARNING = 'warning', + ERROR = 'error', + SUCCESS = 'success', +} + +/** + * Message which will be passed to a UI (either in a log or in the application's UI) + */ +export interface ILogEntry { + /** The function name who did this logging, this is automatically added (can be null). */ + caller?: string; + /** The severity rate, this is automatically added. */ + severity: LogMessageSeverity; + /** When this entry was made. */ + timestamp: Date; + /** The items which were logged. */ + args: Array; +} diff --git a/packages/apps-engine/src/definition/accessors/ILogger.ts b/packages/apps-engine/src/definition/accessors/ILogger.ts new file mode 100644 index 000000000000..eac6e531802d --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ILogger.ts @@ -0,0 +1,29 @@ +import type { AppMethod } from '../metadata/AppMethod'; +import type { ILogEntry } from './ILogEntry'; + +/** + * This logger provides a way to log various levels to the entire system. + * When used, the items passed in will be logged to the database. This will + * allow people to easily see what happened (users) or debug what went wrong. + */ +export interface ILogger { + method: `${AppMethod}`; + + debug(...items: Array): void; + info(...items: Array): void; + log(...items: Array): void; + warn(...items: Array): void; + error(...items: Array): void; + success(...items: Array): void; + + /** Gets the entries logged. */ + getEntries(): Array; + /** Gets the method which this logger is for. */ + getMethod(): `${AppMethod}`; + /** Gets when this logger was constructed. */ + getStartTime(): Date; + /** Gets the end time, usually Date.now(). */ + getEndTime(): Date; + /** Gets the amount of time this was a logger, start - Date.now(). */ + getTotalTime(): number; +} diff --git a/packages/apps-engine/src/definition/accessors/IMessageBuilder.ts b/packages/apps-engine/src/definition/accessors/IMessageBuilder.ts new file mode 100644 index 000000000000..024a4b123ff8 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IMessageBuilder.ts @@ -0,0 +1,236 @@ +import type { LayoutBlock } from '@rocket.chat/ui-kit'; + +import type { IMessage, IMessageAttachment } from '../messages'; +import type { RocketChatAssociationModel } from '../metadata'; +import type { IRoom } from '../rooms'; +import type { BlockBuilder, IBlock } from '../uikit'; +import type { IUser } from '../users'; + +/** + * Interface for building out a message. + * Please note, that a room and sender must be associated otherwise you will NOT + * be able to successfully save the message object. + */ +export interface IMessageBuilder { + kind: RocketChatAssociationModel.MESSAGE; + + /** + * Provides a convenient way to set the data for the message. + * Note: Providing an "id" field here will be ignored. + * + * @param message the message data to set + */ + setData(message: IMessage): IMessageBuilder; + + /** + * Provides a convenient way to set the data for the message + * keeping the "id" field so as to update the message later. + * + * @param message the message data to set + * @param editor the user who edited the updated message + */ + setUpdateData(message: IMessage, editor: IUser): IMessageBuilder; + + /** + * Sets the thread to which this message belongs, if any. + * + * @param threadId The id of the thread + */ + setThreadId(threadId: string): IMessageBuilder; + + /** + * Retrieves the threadId to which this message belongs, + * if any. + * + * If you would like to retrieve the actual message that + * the thread originated from, you can use the + * `IMessageRead.getById()` method + */ + getThreadId(): string; + + /** + * Sets the room where this message should be sent to. + * + * @param room the room where to send + */ + setRoom(room: IRoom): IMessageBuilder; + + /** + * Gets the room where this message was sent to. + */ + getRoom(): IRoom; + + /** + * Sets the sender of this message. + * + * @param sender the user sending the message + */ + setSender(sender: IUser): IMessageBuilder; + + /** + * Gets the User which sent the message. + */ + getSender(): IUser; + + /** + * Sets the text of the message. + * + * @param text the actual text + */ + setText(text: string): IMessageBuilder; + + /** + * Gets the message text. + */ + getText(): string; + + /** + * Sets the emoji to use for the avatar, this overwrites the current avatar + * whether it be the user's or the avatar url provided. + * + * @param emoji the emoji code + */ + setEmojiAvatar(emoji: string): IMessageBuilder; + + /** + * Gets the emoji used for the avatar. + */ + getEmojiAvatar(): string; + + /** + * Sets the url which to display for the avatar, this overwrites the current + * avatar whether it be the user's or an emoji one. + * + * @param avatarUrl image url to use as the avatar + */ + setAvatarUrl(avatarUrl: string): IMessageBuilder; + + /** + * Gets the url used for the avatar. + */ + getAvatarUrl(): string; + + /** + * Sets the display text of the sender's username that is visible. + * + * @param alias the username alias to display + */ + setUsernameAlias(alias: string): IMessageBuilder; + + /** + * Gets the display text of the sender's username that is visible. + */ + getUsernameAlias(): string; + + /** + * Adds one attachment to the message's list of attachments, this will not + * overwrite any existing ones but just adds. + * + * @param attachment the attachment to add + */ + addAttachment(attachment: IMessageAttachment): IMessageBuilder; + + /** + * Sets the attachments for the message, replacing and destroying all of the current attachments. + * + * @param attachments array of the attachments + */ + setAttachments(attachments: Array): IMessageBuilder; + + /** + * Gets the attachments array for the message + */ + getAttachments(): Array; + + /** + * Replaces an attachment at the given position (index). + * If there is no attachment at that position, there will be an error thrown. + * + * @param position the index of the attachment to replace + * @param attachment the attachment to replace with + */ + replaceAttachment(position: number, attachment: IMessageAttachment): IMessageBuilder; + + /** + * Removes an attachment at the given position (index). + * If there is no attachment at that position, there will be an error thrown. + * + * @param position the index of the attachment to remove + */ + removeAttachment(position: number): IMessageBuilder; + + /** + * Sets the user who is editing this message. + * This is required if you are modifying an existing message. + * + * @param user the editor + */ + setEditor(user: IUser): IMessageBuilder; + + /** + * Gets the user who edited the message + */ + getEditor(): IUser; + + /** + * Sets whether this message can group with others. + * This is desirable if you want to avoid confusion with other integrations. + * + * @param groupable whether this message can group with others + */ + setGroupable(groupable: boolean): IMessageBuilder; + + /** + * Gets whether this message can group with others. + */ + getGroupable(): boolean; + + /** + * Sets whether this message should have any URLs in the text + * parsed by Rocket.Chat and get the details added to the message's + * attachments. + * + * @param parseUrls whether URLs should be parsed in this message + */ + setParseUrls(parseUrls: boolean): IMessageBuilder; + + /** + * Gets whether this message should have its URLs parsed + */ + getParseUrls(): boolean; + + /** + * Gets the resulting message that has been built up to the point of calling it. + * + * *Note:* This will error out if the Room has not been defined. + */ + getMessage(): IMessage; + + /** + * Adds a block collection to the message's + * own collection + */ + addBlocks(blocks: BlockBuilder | Array): IMessageBuilder; + + /** + * Sets the block collection of the message + * + * @param blocks + */ + setBlocks(blocks: BlockBuilder | Array): IMessageBuilder; + + /** + * Gets the block collection of the message + */ + getBlocks(): Array; + + /** + * Adds a custom field to the message. + * Note: This key can not already exist or it will throw an error. + * Note: The key must not contain a period in it, an error will be thrown. + * + * @param key the name of the custom field + * @param value the value of this custom field + */ + addCustomField(key: string, value: any): IMessageBuilder; +} diff --git a/packages/apps-engine/src/definition/accessors/IMessageExtender.ts b/packages/apps-engine/src/definition/accessors/IMessageExtender.ts new file mode 100644 index 000000000000..7db010bae081 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IMessageExtender.ts @@ -0,0 +1,36 @@ +import type { IMessage, IMessageAttachment } from '../messages'; +import type { RocketChatAssociationModel } from '../metadata'; + +export interface IMessageExtender { + kind: RocketChatAssociationModel.MESSAGE; + + /** + * Adds a custom field to the message. + * Note: This key can not already exist or it will throw an error. + * Note: The key must not contain a period in it, an error will be thrown. + * + * @param key the name of the custom field + * @param value the value of this custom field + */ + addCustomField(key: string, value: any): IMessageExtender; + + /** + * Adds a single attachment to the message. + * + * @param attachment the item to add + */ + addAttachment(attachment: IMessageAttachment): IMessageExtender; + + /** + * Adds all of the provided attachments to the message. + * + * @param attachments an array of attachments + */ + addAttachments(attachments: Array): IMessageExtender; + + /** + * Gets the resulting message that has been extended at the point of calling it. + * Note: modifying the returned value will have no effect. + */ + getMessage(): IMessage; +} diff --git a/packages/apps-engine/src/definition/accessors/IMessageRead.ts b/packages/apps-engine/src/definition/accessors/IMessageRead.ts new file mode 100644 index 000000000000..10c99d26388b --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IMessageRead.ts @@ -0,0 +1,15 @@ +import type { IMessage } from '../messages/index'; +import type { IRoom } from '../rooms/IRoom'; +import type { IUser } from '../users/IUser'; + +/** + * This accessor provides methods for accessing + * messages in a read-only-fashion. + */ +export interface IMessageRead { + getById(id: string): Promise; + + getSenderUser(messageId: string): Promise; + + getRoom(messageId: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IMessageUpdater.ts b/packages/apps-engine/src/definition/accessors/IMessageUpdater.ts new file mode 100644 index 000000000000..b21baacae04f --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IMessageUpdater.ts @@ -0,0 +1,21 @@ +import type { Reaction } from '../messages'; + +export interface IMessageUpdater { + /** + * Add a reaction to a message + * + * @param messageId the id of the message + * @param userId the id of the user + * @param reaction the reaction + */ + addReaction(messageId: string, userId: string, reaction: Reaction): Promise; + + /** + * Remove a reaction from a message + * + * @param messageId the id of the message + * @param userId the id of the user + * @param reaction the reaction + */ + removeReaction(messageId: string, userId: string, reaction: Reaction): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IModerationModify.ts b/packages/apps-engine/src/definition/accessors/IModerationModify.ts new file mode 100644 index 000000000000..6b0f54968a04 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IModerationModify.ts @@ -0,0 +1,27 @@ +import type { IMessage } from '../messages'; +import type { IUser } from '../users'; + +export interface IModerationModify { + /** + * Provides a way for Apps to report a message. + * @param messageId the messageId to report + * @param description the description of the report + * @param userId the userId to be reported + * @param appId the app id + */ + report(messageId: string, description: string, userId: string, appId: string): Promise; + + /** + * Provides a way for Apps to dismiss reports by message id. + * @param messageId the messageId to dismiss reports + * @param appId the app id + */ + dismissReportsByMessageId(messageId: IMessage['id'], reason: string, action: string, appId: string): Promise; + + /** + * Provides a way for Apps to dismiss reports by user id. + * @param userId the userId to dismiss reports + * @param appId the app id + */ + dismissReportsByUserId(userId: IUser['id'], reason: string, action: string, appId: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IModify.ts b/packages/apps-engine/src/definition/accessors/IModify.ts new file mode 100644 index 000000000000..e76e4cc76824 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IModify.ts @@ -0,0 +1,45 @@ +import type { IModerationModify } from './IModerationModify'; +import type { IModifyCreator } from './IModifyCreator'; +import type { IModifyDeleter } from './IModifyDeleter'; +import type { IModifyExtender } from './IModifyExtender'; +import type { IModifyUpdater } from './IModifyUpdater'; +import type { INotifier } from './INotifier'; +import type { IOAuthAppsModify } from './IOAuthAppsModify'; +import type { ISchedulerModify } from './ISchedulerModify'; +import type { IUIController } from './IUIController'; + +export interface IModify { + getCreator(): IModifyCreator; + + getDeleter(): IModifyDeleter; + + getExtender(): IModifyExtender; + + getUpdater(): IModifyUpdater; + + /** + * Gets the accessor for sending notifications to a user or users in a room. + * + * @returns the notifier accessor + */ + getNotifier(): INotifier; + /** + * Gets the accessor for interacting with the UI + */ + getUiController(): IUIController; + + /** + * Gets the accessor for creating scheduled jobs + */ + getScheduler(): ISchedulerModify; + + /** + * Gets the accessor for creating OAuth apps + */ + getOAuthAppsModifier(): IOAuthAppsModify; + /** + * Gets the accessor for modifying moderation + * @returns the moderation accessor + */ + getModerationModifier(): IModerationModify; +} diff --git a/packages/apps-engine/src/definition/accessors/IModifyCreator.ts b/packages/apps-engine/src/definition/accessors/IModifyCreator.ts new file mode 100644 index 000000000000..6c2acd50493c --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IModifyCreator.ts @@ -0,0 +1,100 @@ +import type { ILivechatMessage } from '../livechat'; +import type { IMessage } from '../messages'; +import type { IRoom } from '../rooms'; +import type { BlockBuilder } from '../uikit'; +import type { IBotUser } from '../users/IBotUser'; +import type { AppVideoConference } from '../videoConferences'; +import type { IDiscussionBuilder } from './IDiscussionBuilder'; +import type { IEmailCreator } from './IEmailCreator'; +import type { ILivechatCreator } from './ILivechatCreator'; +import type { ILivechatMessageBuilder } from './ILivechatMessageBuilder'; +import type { IMessageBuilder } from './IMessageBuilder'; +import type { IRoomBuilder } from './IRoomBuilder'; +import type { IUploadCreator } from './IUploadCreator'; +import type { IUserBuilder } from './IUserBuilder'; +import type { IVideoConferenceBuilder } from './IVideoConferenceBuilder'; + +export interface IModifyCreator { + /** + * Get the creator object responsible for the + * Livechat integrations + */ + getLivechatCreator(): ILivechatCreator; + + /** + * Get the creator object responsible for the upload. + */ + getUploadCreator(): IUploadCreator; + + /** + * Gets the creator object responsible for email sending + */ + getEmailCreator(): IEmailCreator; + + /** + * @deprecated please prefer the rocket.chat/ui-kit components + * + * Gets a new instance of a BlockBuilder + */ + getBlockBuilder(): BlockBuilder; + /** + * Starts the process for building a new message object. + * + * @param data (optional) the initial data to pass into the builder, + * the `id` property will be ignored + * @return an IMessageBuilder instance + */ + startMessage(data?: IMessage): IMessageBuilder; + + /** + * Starts the process for building a new livechat message object. + * + * @param data (optional) the initial data to pass into the builder, + * the `id` property will be ignored + * @return an IMessageBuilder instance + */ + startLivechatMessage(data?: ILivechatMessage): ILivechatMessageBuilder; + + /** + * Starts the process for building a new room. + * + * @param data (optional) the initial data to pass into the builder, + * the `id` property will be ignored + * @return an IRoomBuilder instance + */ + startRoom(data?: IRoom): IRoomBuilder; + + /** + * Starts the process for building a new discussion. + * + * @param data (optional) the initial data to pass into the builder, + * the `id` property will be ignored + * @return an IDiscussionBuilder instance + */ + startDiscussion(data?: Partial): IDiscussionBuilder; + + /** + * Starts the process for building a new video conference. + * + * @param data (optional) the initial data to pass into the builder, + * @return an IVideoConferenceBuilder instance + */ + startVideoConference(data?: Partial): IVideoConferenceBuilder; + + /** + * Starts the process for building a new bot user. + * + * @param data (optional) the initial data to pass into the builder, + * the `id` property will be ignored + * @return an IUserBuilder instance + */ + startBotUser(data?: Partial): IUserBuilder; + + /** + * Finishes the creating process, saving the object to the database. + * + * @param builder the builder instance + * @return the resulting `id` of the resulting object + */ + finish(builder: IMessageBuilder | ILivechatMessageBuilder | IRoomBuilder | IDiscussionBuilder | IVideoConferenceBuilder | IUserBuilder): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IModifyDeleter.ts b/packages/apps-engine/src/definition/accessors/IModifyDeleter.ts new file mode 100644 index 000000000000..7d1103ba13f3 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IModifyDeleter.ts @@ -0,0 +1,12 @@ +import type { IMessage } from '../messages'; +import type { IUser, UserType } from '../users'; + +export interface IModifyDeleter { + deleteRoom(roomId: string): Promise; + + deleteUsers(appId: Exclude, userType: UserType.APP | UserType.BOT): Promise; + + deleteMessage(message: IMessage, user: IUser): Promise; + + removeUsersFromRoom(roomId: string, usernames: Array): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IModifyExtender.ts b/packages/apps-engine/src/definition/accessors/IModifyExtender.ts new file mode 100644 index 000000000000..786b23975407 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IModifyExtender.ts @@ -0,0 +1,40 @@ +import type { IUser } from '../users'; +import type { IMessageExtender } from './IMessageExtender'; +import type { IRoomExtender } from './IRoomExtender'; +import type { IVideoConferenceExtender } from './IVideoConferenceExtend'; + +export interface IModifyExtender { + /** + * Modifies a message in a non-destructive way: Properties can be added to it, + * but existing properties cannot be changed. + * + * @param messageId the id of the message to be extended + * @param updater the user who is updating/extending the message + * @return the extender instance for the message + */ + extendMessage(messageId: string, updater: IUser): Promise; + + /** + * Modifies a room in a non-destructive way: Properties can be added to it, + * but existing properties cannot be changed. + * + * @param roomId the id of the room to be extended + * @param updater the user who is updating/extending the room + * @return the extender instance for the room + */ + extendRoom(roomId: string, updater: IUser): Promise; + + /** + * Modifies a video conference in a non-destructive way: Properties can be added to it, + * but existing properties cannot be changed. + */ + extendVideoConference(id: string): Promise; + + /** + * Finishes the extending process, saving the object to the database. + * Note: If there is an issue or error while updating, this will throw an error. + * + * @param extender the extender instance + */ + finish(extender: IRoomExtender | IMessageExtender | IVideoConferenceExtender): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IModifyUpdater.ts b/packages/apps-engine/src/definition/accessors/IModifyUpdater.ts new file mode 100644 index 000000000000..60dcf90b2df7 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IModifyUpdater.ts @@ -0,0 +1,52 @@ +import type { IUser } from '../users'; +import type { ILivechatUpdater } from './ILivechatUpdater'; +import type { IMessageBuilder } from './IMessageBuilder'; +import type { IMessageUpdater } from './IMessageUpdater'; +import type { IRoomBuilder } from './IRoomBuilder'; +import type { IUserUpdater } from './IUserUpdater'; + +export interface IModifyUpdater { + /** + * Get the updater object responsible for the + * Livechat integrations + */ + getLivechatUpdater(): ILivechatUpdater; + + /** + * Gets the update object responsible for + * methods that update users + */ + getUserUpdater(): IUserUpdater; + + /** + * Get the updater object responsible for + * methods that update messages + */ + getMessageUpdater(): IMessageUpdater; + + /** + * Modifies an existing message. + * Raises an exception if a non-existent messageId is supplied + * + * @param messageId the id of the existing message to modfiy and build + * @param updater the user who is updating the message + */ + message(messageId: string, updater: IUser): Promise; + + /** + * Modifies an existing room. + * Raises an exception if a non-existent roomId is supplied + * + * @param roomId the id of the existing room to modify and build + * @param updater the user who is updating the room + */ + room(roomId: string, updater: IUser): Promise; + + /** + * Finishes the updating process, saving the object to the database. + * Note: If there is an issue or error while updating, this will throw an error. + * + * @param builder the builder instance + */ + finish(builder: IMessageBuilder | IRoomBuilder): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/INotifier.ts b/packages/apps-engine/src/definition/accessors/INotifier.ts new file mode 100644 index 000000000000..a41fb22c4ff6 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/INotifier.ts @@ -0,0 +1,63 @@ +import type { IMessage } from '../messages'; +import type { IRoom } from '../rooms'; +import type { IUser } from '../users'; +import type { IMessageBuilder } from './IMessageBuilder'; + +export enum TypingScope { + Room = 'room', +} + +export interface ITypingOptions { + /** + * The typing scope where the typing message should be presented, + * TypingScope.Room by default. + */ + scope?: TypingScope; + /** + * The id of the typing scope + * + * TypingScope.Room <-> room.id + */ + id: string; + /** + * The name of the user who is typing the message + * + * **Note**: If not provided, it will use app assigned + * user's name by default. + */ + username?: string; +} + +export interface INotifier { + /** + * Notifies the provided user of the provided message. + * + * **Note**: Notifications only are shown to the user if they are + * online and it only stays around for the duration of their session. + * + * @param user The user who should be notified + * @param message The message with the content to notify the user about + */ + notifyUser(user: IUser, message: IMessage): Promise; + + /** + * Notifies all of the users in the provided room. + * + * **Note**: Notifications only are shown to those online + * and it only stays around for the duration of their session. + * + * @param room The room which to notify the users in + * @param message The message content to notify users about + */ + notifyRoom(room: IRoom, message: IMessage): Promise; + + /** + * Notifies all of the users a typing indicator in the provided scope. + * + * @returns a cancellation function to stop typing + */ + typing(options: ITypingOptions): Promise<() => Promise>; + + /** Gets a new message builder for building a notification message. */ + getMessageBuilder(): IMessageBuilder; +} diff --git a/packages/apps-engine/src/definition/accessors/IOAuthApp.ts b/packages/apps-engine/src/definition/accessors/IOAuthApp.ts new file mode 100644 index 000000000000..1c2edbe19c95 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IOAuthApp.ts @@ -0,0 +1,13 @@ +export interface IOAuthApp { + id: string; + name: string; + active: boolean; + clientId?: string; + clientSecret?: string; + redirectUri: string; + createdAt?: string; + updatedAt?: string; + createdBy: { username: string; id: string }; +} + +export type IOAuthAppParams = Omit; diff --git a/packages/apps-engine/src/definition/accessors/IOAuthAppsModify.ts b/packages/apps-engine/src/definition/accessors/IOAuthAppsModify.ts new file mode 100644 index 000000000000..72d0a4ddce5a --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IOAuthAppsModify.ts @@ -0,0 +1,23 @@ +import type { IOAuthAppParams } from './IOAuthApp'; + +export interface IOAuthAppsModify { + /** + * Create an OAuthApp + * @param OAuthApp - the OAuth app to create, in case the clientId and the clientSecret is not sent it will generate automatically + * @param appId - the app id + */ + createOAuthApp(OAuthApp: IOAuthAppParams, appId: string): Promise; + /** + * Update the OAuth app info + * @param OAuthApp - OAuth data that will be updated + * @param id - OAuth app id + * @param appId - the app id + */ + updateOAuthApp(OAuthApp: IOAuthAppParams, id: string, appId: string): Promise; + /** + * Deletes the OAuth app + * @param id - OAuth app id + * @param appId - the app id + */ + deleteOAuthApp(id: string, appId: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IOAuthAppsReader.ts b/packages/apps-engine/src/definition/accessors/IOAuthAppsReader.ts new file mode 100644 index 000000000000..97d544cd9366 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IOAuthAppsReader.ts @@ -0,0 +1,16 @@ +import type { IOAuthApp } from './IOAuthApp'; + +export interface IOAuthAppsReader { + /** + * Returns the OAuth app info by its id + * @param id - OAuth app id + * @param appId - the app id + */ + getOAuthAppById(id: string, appId: string): Promise; + /** + * Returns the OAuth app info by its name + * @param name - OAuth app name + * @param appId - the app id + */ + getOAuthAppByName(name: string, appId: string): Promise>; +} diff --git a/packages/apps-engine/src/definition/accessors/IPersistence.ts b/packages/apps-engine/src/definition/accessors/IPersistence.ts new file mode 100644 index 000000000000..30f1d676539e --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IPersistence.ts @@ -0,0 +1,97 @@ +import type { RocketChatAssociationRecord } from '../metadata'; + +/** + * Provides an accessor write data to the App's persistent storage. + * A App only has access to its own persistent storage and does not + * have access to any other App's. + */ +export interface IPersistence { + /** + * Creates a new record in the App's persistent storage, returning the resulting "id". + * + * @param data the actual data to store, must be an object otherwise it will error out. + * @return the resulting record's id + */ + create(data: object): Promise; + + /** + * Creates a new record in the App's persistent storage with the associated information + * being provided. + * + * @param data the actual data to store, must be an object otherwise it will error out + * @param association the association data which includes the model and record id + * @return the resulting record's id + */ + createWithAssociation(data: object, association: RocketChatAssociationRecord): Promise; + + /** + * Creates a new record in the App's persistent storage with the data being + * associated with more than one Rocket.Chat record. + * + * @param data the actual data to store, must be an object otherwise it will error out + * @param associations an array of association data which includes the model and record id + * @return the resulting record's id + */ + createWithAssociations(data: object, associations: Array): Promise; + + /** + * Updates an existing record with the data provided in the App's persistent storage. + * This will throw an error if the record doesn't currently exist or if the data is not an object. + * + * @param id the data record's id + * @param data the actual data to store, must be an object otherwise it will error out + * @param upsert whether a record should be created if the id to be updated does not exist + * @return the id of the updated/upserted record + */ + update(id: string, data: object, upsert?: boolean): Promise; + + /** + * Updates an existing record with the data provided in the App's persistent storage which are + * associated with provided information. + * This will throw an error if the record doesn't currently exist or if the data is not an object. + * + * @param association the association record + * @param data the actual data to store, must be an object otherwise it will error out + * @param upsert whether a record should be created if the id to be updated does not exist + * @return the id of the updated/upserted record + */ + updateByAssociation(association: RocketChatAssociationRecord, data: object, upsert?: boolean): Promise; + + /** + * Updates an existing record with the data provided in the App's persistent storage which are + * associated with more than one Rocket.Chat record. + * This will throw an error if the record doesn't currently exist or if the data is not an object. + * + * @param associations an array of association data which includes the model and record id + * @param data the actual data to store, must be an object otherwise it will error out + * @param upsert whether a record should be created if the id to be updated does not exist + * @return the id of the updated/upserted record + */ + updateByAssociations(associations: Array, data: object, upsert?: boolean): Promise; + + /** + * Removes a record by the provided id and returns the removed record. + * + * @param id of the record to remove + * @return the data record which was removed + */ + remove(id: string): Promise; + + /** + * Removes all of the records in persistent storage which are associated with the provided information. + * + * @param association the information about the association for the records to be removed + * @return the data of the removed records + */ + removeByAssociation(association: RocketChatAssociationRecord): Promise>; + + /** + * Removes all of the records in persistent storage which are associated with the provided information. + * More than one association acts like an AND which means a record in persistent storage must have all + * of the associations to be considered a match. + * + * @param associations the information about the associations for the records to be removed + * @return the data of the removed records + */ + removeByAssociations(associations: Array): Promise>; +} diff --git a/packages/apps-engine/src/definition/accessors/IPersistenceRead.ts b/packages/apps-engine/src/definition/accessors/IPersistenceRead.ts new file mode 100644 index 000000000000..d0d1178a44d0 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IPersistenceRead.ts @@ -0,0 +1,40 @@ +import type { RocketChatAssociationRecord } from '../metadata'; + +/** + * Provides a read-only accessor for the App's persistent storage. + * A App only has access to its own persistent storage and does not + * have access to any other App's. + */ +export interface IPersistenceRead { + /** + * Retrieves a record from the App's persistent storage by the provided id. + * A "falsey" value (undefined or null or false) is returned should nothing exist + * in the storage by the provided id. + * + * @param id the record to get's id + * @return the record if it exists, falsey if not + */ + read(id: string): Promise; + + /** + * Retrieves a record from the App's persistent storage by the provided id. + * An empty array is returned should there be no records associated with the + * data provided. + * + * @param association the association record to query the persistent storage for + * @return array of the records if any exists, empty array if none exist + */ + readByAssociation(association: RocketChatAssociationRecord): Promise>; + + /** + * Retrieves a record from the App's persistent storage by the provided id. + * Providing more than one association record acts like an AND which means a record + * in persistent storage must have all of the associations to be considered a match. + * An empty array is returned should there be no records associated with the + * data provided. + * + * @param associations the association records to query the persistent storage for + * @return array of the records if any exists, empty array if none exist + */ + readByAssociations(associations: Array): Promise>; +} diff --git a/packages/apps-engine/src/definition/accessors/IRead.ts b/packages/apps-engine/src/definition/accessors/IRead.ts new file mode 100644 index 000000000000..17f66d6218dc --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IRead.ts @@ -0,0 +1,52 @@ +import type { ICloudWorkspaceRead } from './ICloudWorkspaceRead'; +import type { IEnvironmentRead } from './IEnvironmentRead'; +import type { ILivechatRead } from './ILivechatRead'; +import type { IMessageRead } from './IMessageRead'; +import type { INotifier } from './INotifier'; +import type { IOAuthAppsReader } from './IOAuthAppsReader'; +import type { IPersistenceRead } from './IPersistenceRead'; +import type { IRoleRead } from './IRoleRead'; +import type { IRoomRead } from './IRoomRead'; +import type { IThreadRead } from './IThreadRead'; +import type { IUploadRead } from './IUploadRead'; +import type { IUserRead } from './IUserRead'; +import type { IVideoConferenceRead } from './IVideoConferenceRead'; + +/** + * The IRead accessor provides methods for accessing the + * Rocket.Chat's environment in a read-only-fashion. + * It is safe to be injected in multiple places, idempotent and extensible + */ +export interface IRead { + /** Gets the IEnvironmentRead instance, contains settings and environmental variables. */ + getEnvironmentReader(): IEnvironmentRead; + + /** Gets the IThreadRead instance */ + + getThreadReader(): IThreadRead; + + /** Gets the IMessageRead instance. */ + getMessageReader(): IMessageRead; + + /** Gets the IPersistenceRead instance. */ + getPersistenceReader(): IPersistenceRead; + + /** Gets the IRoomRead instance. */ + getRoomReader(): IRoomRead; + + /** Gets the IUserRead instance. */ + getUserReader(): IUserRead; + + /** Gets the INotifier for notifying users/rooms. */ + getNotifier(): INotifier; + + getLivechatReader(): ILivechatRead; + getUploadReader(): IUploadRead; + getCloudWorkspaceReader(): ICloudWorkspaceRead; + + getVideoConferenceReader(): IVideoConferenceRead; + + getOAuthAppsReader(): IOAuthAppsReader; + + getRoleReader(): IRoleRead; +} diff --git a/packages/apps-engine/src/definition/accessors/IRoleRead.ts b/packages/apps-engine/src/definition/accessors/IRoleRead.ts new file mode 100644 index 000000000000..fb56ed306a32 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IRoleRead.ts @@ -0,0 +1,27 @@ +import type { IRole } from '../roles'; + +/** + * Interface for reading roles. + */ +export interface IRoleRead { + /** + * Retrieves a role by its id or name. + * @param idOrName The id or name of the role to retrieve. + * @param appId The id of the app. + * @returns The role, if found. + * @returns null if no role is found. + * @throws If there is an error while retrieving the role. + */ + getOneByIdOrName(idOrName: IRole['id'] | IRole['name'], appId: string): Promise; + + /** + * Retrieves all custom roles. + * @param appId The id of the app. + * @returns All custom roles. + * @throws If there is an error while retrieving the roles. + * @throws If the app does not have the necessary permissions. + * @see IRole.protected + * @see AppPermissions.role.read + */ + getCustomRoles(appId: string): Promise>; +} diff --git a/packages/apps-engine/src/definition/accessors/IRoomBuilder.ts b/packages/apps-engine/src/definition/accessors/IRoomBuilder.ts new file mode 100644 index 000000000000..b92955896380 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IRoomBuilder.ts @@ -0,0 +1,186 @@ +import type { RocketChatAssociationModel } from '../metadata'; +import type { IRoom, RoomType } from '../rooms'; +import type { IUser } from '../users'; + +/** + * Interface for building out a room. + * Please note, a room creator, name, and type must be set otherwise you will NOT + * be able to successfully save the room object. + */ +export interface IRoomBuilder { + kind: RocketChatAssociationModel.ROOM | RocketChatAssociationModel.DISCUSSION; + + /** + * Provides a convient way to set the data for the room. + * Note: Providing an "id" field here will be ignored. + * + * @param room the room data to set + */ + setData(room: Partial): IRoomBuilder; + + /** + * Sets the display name of this room. + * + * @param name the display name of the room + */ + setDisplayName(name: string): IRoomBuilder; + + /** + * Gets the display name of this room. + */ + getDisplayName(): string; + + /** + * Sets the slugified name of this room, it must align to the rules of Rocket.Chat room + * names otherwise there will be an error thrown (no spaces, special characters, etc). + * + * @param name the slugified name + */ + setSlugifiedName(name: string): IRoomBuilder; + + /** + * Gets the slugified name of this room. + */ + getSlugifiedName(): string; + + /** + * Sets the room's type. + * + * @param type the room type + */ + setType(type: RoomType): IRoomBuilder; + + /** + * Gets the room's type. + */ + getType(): RoomType; + + /** + * Sets the creator of the room. + * + * @param creator the user who created the room + */ + setCreator(creator: IUser): IRoomBuilder; + + /** + * Gets the room's creator. + */ + getCreator(): IUser; + + /** + * Adds a user to the room, these are by username until further notice. + * + * @param username the user's username to add to the room + * @deprecated in favor of `addMemberToBeAddedByUsername`. This method will be removed on version 2.0.0 + */ + addUsername(username: string): IRoomBuilder; + + /** + * Sets the usernames of who are joined to the room. + * + * @param usernames the list of usernames + * @deprecated in favor of `setMembersByUsernames`. This method will be removed on version 2.0.0 + */ + setUsernames(usernames: Array): IRoomBuilder; + + /** + * Gets the usernames of users in the room. + * @deprecated in favor of `getMembersUsernames`. This method will be removed on version 2.0.0 + */ + getUsernames(): Array; + + /** + * Adds a member to the room by username + * + * @param username the user's username to add to the room + */ + addMemberToBeAddedByUsername(username: string): IRoomBuilder; + + /** + * Sets a list of members to the room by usernames + * + * @param usernames the list of usernames + */ + setMembersToBeAddedByUsernames(usernames: Array): IRoomBuilder; + + /** + * Gets the list of usernames of the members who are been added to the room + */ + getMembersToBeAddedUsernames(): Array; + + /** + * Sets whether this room should be a default room or not. + * This means that new users will automatically join this room + * when they join the server. + * + * @param isDefault room should be default or not + */ + setDefault(isDefault: boolean): IRoomBuilder; + + /** + * Gets whether this room is a default room or not. + */ + getIsDefault(): boolean; + + /** + * Sets whether this room should be in read only state or not. + * This means that users without the required permission to talk when + * a room is muted will not be able to talk but instead will only be + * able to read the contents of the room. + * + * @param isReadOnly whether it should be read only or not + */ + setReadOnly(isReadOnly: boolean): IRoomBuilder; + + /** + * Gets whether this room is on read only state or not. + */ + getIsReadOnly(): boolean; + + /** + * Sets whether this room should display the system messages (like user join, etc) + * or not. This means that whenever a system event, such as joining or leaving, happens + * then Rocket.Chat won't send the message to the channel. + * + * @param displaySystemMessages whether the messages should display or not + */ + setDisplayingOfSystemMessages(displaySystemMessages: boolean): IRoomBuilder; + + /** + * Gets whether this room should display the system messages or not. + */ + getDisplayingOfSystemMessages(): boolean; + + /** + * Adds a custom field to the room. + * Note: This will replace an existing field with the same key should it exist already. + * + * @param key the name of the key + * @param value the value of the custom field + */ + addCustomField(key: string, value: object): IRoomBuilder; + + /** + * Sets the entire custom field property to an object provided. This will overwrite + * every existing key/values which are unrecoverable. + * + * @param fields the data to set + */ + setCustomFields(fields: { [key: string]: object }): IRoomBuilder; + + /** + * Gets the custom field property of the room. + */ + getCustomFields(): { [key: string]: object }; + + /** + * Gets user ids of members from a direct message + */ + getUserIds(): Array; + + /** + * Gets the resulting room that has been built up to the point of calling this method. + * Note: modifying the returned value will have no effect. + */ + getRoom(): IRoom; +} diff --git a/packages/apps-engine/src/definition/accessors/IRoomExtender.ts b/packages/apps-engine/src/definition/accessors/IRoomExtender.ts new file mode 100644 index 000000000000..4135c63edd13 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IRoomExtender.ts @@ -0,0 +1,40 @@ +import type { RocketChatAssociationModel } from '../metadata'; +import type { IRoom } from '../rooms'; +import type { IUser } from '../users'; + +export interface IRoomExtender { + kind: RocketChatAssociationModel.ROOM; + + /** + * Adds a custom field to the room. + * Note: This key can not already exist or it will throw an error. + * Note: The key must not contain a period in it, an error will be thrown. + * + * @param key the name of the custom field + * @param value the value of this custom field + */ + addCustomField(key: string, value: any): IRoomExtender; + + /** + * Adds a user to the room. + * + * @param user the user which is to be added to the room + */ + addMember(user: IUser): IRoomExtender; + + /** + * Get a list of users being added to the room. + */ + getMembersBeingAdded(): Array; + + /** + * Get a list of usernames of users being added to the room. + */ + getUsernamesOfMembersBeingAdded(): Array; + + /** + * Gets the resulting room that has been extended at the point of calling this. + * Note: modifying the returned value will have no effect. + */ + getRoom(): IRoom; +} diff --git a/packages/apps-engine/src/definition/accessors/IRoomRead.ts b/packages/apps-engine/src/definition/accessors/IRoomRead.ts new file mode 100644 index 000000000000..f4e0df33239d --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IRoomRead.ts @@ -0,0 +1,93 @@ +import type { GetMessagesOptions } from '../../server/bridges/RoomBridge'; +import type { IMessageRaw } from '../messages/index'; +import type { IRoom } from '../rooms/index'; +import type { IUser } from '../users/index'; + +/** + * This accessor provides methods for accessing + * rooms in a read-only-fashion. + */ +export interface IRoomRead { + /** + * Gets a room by an id. + * + * @param id the id of the room + * @returns the room + */ + getById(id: string): Promise; + + /** + * Gets just the creator of the room by the room's id. + * + * @param id the id of the room + * @returns the creator of the room + */ + getCreatorUserById(id: string): Promise; + + /** + * Gets a room by its name. + * + * @param name the name of the room + * @returns the room + */ + getByName(name: string): Promise; + + /** + * Gets just the creator of the room by the room's name. + * + * @param name the name of the room + * @returns the creator of the room + */ + getCreatorUserByName(name: string): Promise; + + /** + * Retrieves an array of messages from the specified room. + * + * @param roomId The unique identifier of the room from which to retrieve messages. + * @param options Optional parameters for retrieving messages: + * - limit: The maximum number of messages to retrieve. Maximum 100 + * - skip: The number of messages to skip (for pagination). + * - sort: An object defining the sorting order of the messages. Each key is a field to sort by, and the value is either "asc" for ascending order or "desc" for descending order. + * @returns A Promise that resolves to an array of IMessage objects representing the messages in the room. + */ + getMessages(roomId: string, options?: Partial): Promise>; + + /** + * Gets an iterator for all of the users in the provided room. + * + * @param roomId the room's id + * @returns an iterator for the users in the room + */ + getMembers(roomId: string): Promise>; + + /** + * Gets a direct room with all usernames + * @param usernames all usernames belonging to the direct room + * @returns the room + */ + getDirectByUsernames(usernames: Array): Promise; + + /** + * Get a list of the moderators of a given room + * + * @param roomId the room's id + * @returns a list of the users with the moderator role in the room + */ + getModerators(roomId: string): Promise>; + + /** + * Get a list of the owners of a given room + * + * @param roomId the room's id + * @returns a list of the users with the owner role in the room + */ + getOwners(roomId: string): Promise>; + + /** + * Get a list of the leaders of a given room + * + * @param roomId the room's id + * @returns a list of the users with the leader role in the room + */ + getLeaders(roomId: string): Promise>; +} diff --git a/packages/apps-engine/src/definition/accessors/ISchedulerExtend.ts b/packages/apps-engine/src/definition/accessors/ISchedulerExtend.ts new file mode 100644 index 000000000000..fc003e34b587 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ISchedulerExtend.ts @@ -0,0 +1,11 @@ +import type { IProcessor } from '../scheduler'; + +export interface ISchedulerExtend { + /** + * Register processors that can be scheduled to run + * + * @param {Array} processors An array of processors + * @returns List of task ids run at startup, or void no startup run is set + */ + registerProcessors(processors: Array): Promise>; +} diff --git a/packages/apps-engine/src/definition/accessors/ISchedulerModify.ts b/packages/apps-engine/src/definition/accessors/ISchedulerModify.ts new file mode 100644 index 000000000000..04faa8700799 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ISchedulerModify.ts @@ -0,0 +1,31 @@ +import type { IOnetimeSchedule, IRecurringSchedule } from '../scheduler'; + +/** + * This accessor provides methods to work with the Job Scheduler + */ +export interface ISchedulerModify { + /** + * Schedules a registered processor to run _once_. + * + * @param {IOnetimeSchedule} job + * @returns jobid as string + */ + scheduleOnce(job: IOnetimeSchedule): Promise; + /** + * Schedules a registered processor to run in recurrencly according to a given interval + * + * @param {IRecurringSchedule} job + * @returns jobid as string + */ + scheduleRecurring(job: IRecurringSchedule): Promise; + /** + * Cancels a running job given its jobId + * + * @param {string} jobId + */ + cancelJob(jobId: string): Promise; + /** + * Cancels all the running jobs from the app + */ + cancelAllJobs(): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IServerSettingRead.ts b/packages/apps-engine/src/definition/accessors/IServerSettingRead.ts new file mode 100644 index 000000000000..ecb7af279241 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IServerSettingRead.ts @@ -0,0 +1,43 @@ +import type { ISetting } from '../settings/ISetting'; + +/** + * Reader for the settings inside of the server (Rocket.Chat). + * Only a subset of them are exposed to Apps. + */ +export interface IServerSettingRead { + /** + * Gets a server setting by id. + * Please note: a error will be thrown if not found + * or trying to access one that isn't exposed. + * + * @param id the id of the setting to get + * @return the setting + */ + getOneById(id: string): Promise; + + /** + * Gets a server setting's value by id. + * Please note: a error will be thrown if not found + * or trying to access one that isn't exposed. + * + * @param id the id of the setting to get + * @return the setting's value + */ + getValueById(id: string): Promise; + + /** + * Gets all of the server settings which are exposed + * to the Apps. + * + * @return an iterator of the exposed settings + */ + getAll(): Promise>; + + /** + * Checks if the server setting for the id provided is readable, + * will return true or false and won't throw an error. + * + * @param id the server setting id + */ + isReadableById(id: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IServerSettingUpdater.ts b/packages/apps-engine/src/definition/accessors/IServerSettingUpdater.ts new file mode 100644 index 000000000000..766285c25953 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IServerSettingUpdater.ts @@ -0,0 +1,6 @@ +import type { ISetting } from '../settings/ISetting'; + +export interface IServerSettingUpdater { + updateOne(setting: ISetting): Promise; + incrementValue(id: ISetting['id'], value?: number): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IServerSettingsModify.ts b/packages/apps-engine/src/definition/accessors/IServerSettingsModify.ts new file mode 100644 index 000000000000..400bd5ae211f --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IServerSettingsModify.ts @@ -0,0 +1,40 @@ +import type { ISetting } from '../settings'; + +/** + * This accessor provides methods to change default setting options + * of Rocket.Chat in a compatible way. It is provided during + * your App's "onEnable". + */ +export interface IServerSettingsModify { + /** + * Hides an existing settings group. + * + * @param name The technical name of the group + */ + hideGroup(name: string): Promise; + + /** + * Hides a setting. This does not influence the actual functionality (the setting will still + * have its value and can be programatically read), but the administrator will not be able to see it anymore + * + * @param id the id of the setting to hide + */ + hideSetting(id: string): Promise; + + /** + * Modifies the configured value of another setting, please use it with caution as an invalid + * setting configuration could cause a Rocket.Chat instance to become unstable. + * + * @param setting the modified setting (id must be provided) + */ + modifySetting(setting: ISetting): Promise; + + /** + * Increases the setting value by the specified amount. + * To be used only with statistic settings that track the amount of times an action has been performed + * + * @param id the id of the existing Rocket.Chat setting + * @param value how much should the count be increased by. Defaults to 1. + */ + incrementValue(id: ISetting['id'], value?: number): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/ISettingRead.ts b/packages/apps-engine/src/definition/accessors/ISettingRead.ts new file mode 100644 index 000000000000..142ee895b161 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ISettingRead.ts @@ -0,0 +1,23 @@ +import type { ISetting } from '../settings/index'; + +/** + * This accessor provides methods for accessing + * App settings in a read-only-fashion. + */ +export interface ISettingRead { + /** + * Gets the App's setting by the provided id. + * Does not throw an error but instead will return undefined it doesn't exist. + * + * @param id the id of the setting + */ + getById(id: string): Promise; + + /** + * Gets the App's setting value by the provided id. + * Note: this will throw an error if the setting doesn't exist + * + * @param id the id of the setting value to get + */ + getValueById(id: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/ISettingUpdater.ts b/packages/apps-engine/src/definition/accessors/ISettingUpdater.ts new file mode 100644 index 000000000000..3826286df6c9 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ISettingUpdater.ts @@ -0,0 +1,5 @@ +import type { ISetting } from '../settings/ISetting'; + +export interface ISettingUpdater { + updateValue(id: ISetting['id'], value: ISetting['value']): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/ISettingsExtend.ts b/packages/apps-engine/src/definition/accessors/ISettingsExtend.ts new file mode 100644 index 000000000000..249776379645 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ISettingsExtend.ts @@ -0,0 +1,17 @@ +import type { ISetting } from '../settings/index'; + +/** + * This accessor provides methods for adding custom settings, + * which are displayed on your App's page. + * This is provided on initialization of your App. + */ +export interface ISettingsExtend { + /** + * Adds a setting which can be configured by an administrator. + * Settings can only be added to groups which have been provided by this App earlier + * and if a group is not provided, the setting will appear outside of a group. + * + * @param setting the setting + */ + provideSetting(setting: ISetting): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/ISlashCommandsExtend.ts b/packages/apps-engine/src/definition/accessors/ISlashCommandsExtend.ts new file mode 100644 index 000000000000..4895e61bf96f --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ISlashCommandsExtend.ts @@ -0,0 +1,16 @@ +import type { ISlashCommand } from '../slashcommands'; + +/** + * This accessor provides methods for adding custom slash commands. + * It is provided during the initialization of your App + */ + +export interface ISlashCommandsExtend { + /** + * Adds a slash command which can be used during conversations lateron. + * Should a command already exists an error will be thrown. + * + * @param slashCommand the command information + */ + provideSlashCommand(slashCommand: ISlashCommand): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/ISlashCommandsModify.ts b/packages/apps-engine/src/definition/accessors/ISlashCommandsModify.ts new file mode 100644 index 000000000000..b9e3d4c3e615 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ISlashCommandsModify.ts @@ -0,0 +1,30 @@ +import type { ISlashCommand } from '../slashcommands'; + +/** + * This accessor provides methods for modifying existing Rocket.Chat slash commands. + * It is provided during "onEnable" of your App. + */ +export interface ISlashCommandsModify { + /** + * Modifies an existing command. The command must either be your App's + * own command or a system command. One App can not modify another + * App's command. + * + * @param slashCommand the modified slash command + */ + modifySlashCommand(slashCommand: ISlashCommand): Promise; + + /** + * Renders an existing slash command un-usable. + * + * @param command the command's usage without the slash + */ + disableSlashCommand(command: string): Promise; + + /** + * Enables an existing slash command to be usable again. + * + * @param command the command's usage without the slash + */ + enableSlashCommand(command: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IThreadRead.ts b/packages/apps-engine/src/definition/accessors/IThreadRead.ts new file mode 100644 index 000000000000..72ceae996eec --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IThreadRead.ts @@ -0,0 +1,9 @@ +import type { IMessage } from '../messages/index'; + +/** + * This accessor provides methods for accessing + * Thread messages in a read-only-fashion. + */ +export interface IThreadRead { + getThreadById(id: string): Promise | undefined>; +} diff --git a/packages/apps-engine/src/definition/accessors/IUIController.ts b/packages/apps-engine/src/definition/accessors/IUIController.ts new file mode 100644 index 000000000000..be7e91f5a05b --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IUIController.ts @@ -0,0 +1,31 @@ +import type { Omit } from '../../lib/utils'; +import type { IUIKitErrorInteraction, IUIKitInteraction, IUIKitSurface } from '../uikit'; +import type { IUIKitContextualBarViewParam, IUIKitModalViewParam } from '../uikit/UIKitInteractionResponder'; +import type { IUser } from '../users'; + +export type IUIKitInteractionParam = Omit; +export type IUIKitErrorInteractionParam = Omit; + +export type IUIKitSurfaceViewParam = Omit & Partial>; + +export interface IUIController { + /** + * @deprecated please prefer the `openSurfaceView` method + */ + openModalView(view: IUIKitModalViewParam, context: IUIKitInteractionParam, user: IUser): Promise; + /** + * @deprecated please prefer the `updateSurfaceView` method + */ + updateModalView(view: IUIKitModalViewParam, context: IUIKitInteractionParam, user: IUser): Promise; + /** + * @deprecated please prefer the `openSurfaceView` method + */ + openContextualBarView(view: IUIKitContextualBarViewParam, context: IUIKitInteractionParam, user: IUser): Promise; + /** + * @deprecated please prefer the `updateSurfaceView` method + */ + updateContextualBarView(view: IUIKitContextualBarViewParam, context: IUIKitInteractionParam, user: IUser): Promise; + setViewError(errorInteraction: IUIKitErrorInteractionParam, context: IUIKitInteractionParam, user: IUser): Promise; + openSurfaceView(view: IUIKitSurfaceViewParam, context: IUIKitInteractionParam, user: IUser): Promise; + updateSurfaceView(view: IUIKitSurfaceViewParam, context: IUIKitInteractionParam, user: IUser): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IUIExtend.ts b/packages/apps-engine/src/definition/accessors/IUIExtend.ts new file mode 100644 index 000000000000..3dca2e32809b --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IUIExtend.ts @@ -0,0 +1,5 @@ +import type { IUIActionButtonDescriptor } from '../ui'; + +export interface IUIExtend { + registerButton(button: IUIActionButtonDescriptor): void; +} diff --git a/packages/apps-engine/src/definition/accessors/IUploadCreator.ts b/packages/apps-engine/src/definition/accessors/IUploadCreator.ts new file mode 100644 index 000000000000..25262b42e7d3 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IUploadCreator.ts @@ -0,0 +1,12 @@ +import type { IUpload } from '../uploads'; +import type { IUploadDescriptor } from '../uploads/IUploadDescriptor'; + +export interface IUploadCreator { + /** + * Create an upload to a room + * + * @param buffer A Buffer with the file's content (See [here](https://nodejs.org/api/buffer.html) + * @param descriptor The metadata about the upload + */ + uploadBuffer(buffer: Buffer, descriptor: IUploadDescriptor): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IUploadRead.ts b/packages/apps-engine/src/definition/accessors/IUploadRead.ts new file mode 100644 index 000000000000..ce4029a8c0c8 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IUploadRead.ts @@ -0,0 +1,7 @@ +import type { IUpload } from '../uploads'; + +export interface IUploadRead { + getById(id: string): Promise; + getBufferById(id: string): Promise; + getBuffer(upload: IUpload): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IUserBuilder.ts b/packages/apps-engine/src/definition/accessors/IUserBuilder.ts new file mode 100644 index 000000000000..4e0c52aa893a --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IUserBuilder.ts @@ -0,0 +1,60 @@ +import type { RocketChatAssociationModel } from '../metadata'; +import type { IUser, IUserEmail } from '../users'; + +/** + * Interface for creating a user. + * Please note, a username and email provided must be unique else you will NOT + * be able to successfully save the user object. + */ +export interface IUserBuilder { + kind: RocketChatAssociationModel.USER; + + /** + * Provides a convient way to set the data for the user. + * Note: Providing an "id" field here will be ignored. + * + * @param user the user data to set + */ + setData(user: Partial): IUserBuilder; + + /** + * Sets emails of the user + * + * @param emails the array of email addresses of the user + */ + setEmails(emails: Array): IUserBuilder; + + /** + * Gets emails of the user + */ + getEmails(): Array; + + /** + * Sets the display name of this user. + * + * @param name the display name of the user + */ + setDisplayName(name: string): IUserBuilder; + + /** + * Gets the display name of this user. + */ + getDisplayName(): string; + + /** + * Sets the username for the user + * + * @param username username of the user + */ + setUsername(username: string): IUserBuilder; + + /** + * Gets the username of this user + */ + getUsername(): string; + + /** + * Gets the user + */ + getUser(): Partial; +} diff --git a/packages/apps-engine/src/definition/accessors/IUserRead.ts b/packages/apps-engine/src/definition/accessors/IUserRead.ts new file mode 100644 index 000000000000..33c4c6e455e4 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IUserRead.ts @@ -0,0 +1,22 @@ +import type { IUser } from '../users/index'; + +/** + * This accessor provides methods for accessing + * users in a read-only-fashion. + */ +export interface IUserRead { + getById(id: string): Promise; + + getByUsername(username: string): Promise; + + /** + * Gets the app user of this app. + */ + getAppUser(appId?: string): Promise; + + /** + * Gets the user's badge count (unread messages count). + * @param uid user's id + */ + getUserUnreadMessageCount(uid: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IUserUpdater.ts b/packages/apps-engine/src/definition/accessors/IUserUpdater.ts new file mode 100644 index 000000000000..8c57b4dadfa8 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IUserUpdater.ts @@ -0,0 +1,18 @@ +import type { IUser } from '../users/IUser'; + +/** + * Updating a user is a more granular approach, since + * it is one of the more sensitive aspects of Rocket.Chat - + * or any other system for that matter. + * + * Allowing apps to modify _all_ the aspects of a user + * would open a critical surface for them to abuse such + * power and "take hold" of a server, for instance. + */ +export interface IUserUpdater { + updateStatusText(user: IUser, statusText: IUser['statusText']): Promise; + updateStatus(user: IUser, statusText: IUser['statusText'], status: IUser['status']): Promise; + updateBio(user: IUser, bio: IUser['bio']): Promise; + updateCustomFields(user: IUser, customFields: IUser['customFields']): Promise; + deactivate(userId: IUser['id'], confirmRelinquish: boolean): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IVideoConfProvidersExtend.ts b/packages/apps-engine/src/definition/accessors/IVideoConfProvidersExtend.ts new file mode 100644 index 000000000000..c61224893e11 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IVideoConfProvidersExtend.ts @@ -0,0 +1,15 @@ +import type { IVideoConfProvider } from '../videoConfProviders'; + +/** + * This accessor provides methods for adding videoconf providers. + * It is provided during the initialization of your App + */ + +export interface IVideoConfProvidersExtend { + /** + * Adds a videoconf provider + * + * @param provider the provider information + */ + provideVideoConfProvider(provider: IVideoConfProvider): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IVideoConferenceBuilder.ts b/packages/apps-engine/src/definition/accessors/IVideoConferenceBuilder.ts new file mode 100644 index 000000000000..11b96da0e4ef --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IVideoConferenceBuilder.ts @@ -0,0 +1,34 @@ +import type { RocketChatAssociationModel } from '../metadata'; +import type { AppVideoConference } from '../videoConferences'; + +export interface IVideoConferenceBuilder { + kind: RocketChatAssociationModel.VIDEO_CONFERENCE; + + setData(call: Partial): IVideoConferenceBuilder; + + setRoomId(rid: string): IVideoConferenceBuilder; + + getRoomId(): string; + + setCreatedBy(userId: string): IVideoConferenceBuilder; + + getCreatedBy(): string; + + setProviderName(name: string): IVideoConferenceBuilder; + + getProviderName(): string; + + setProviderData(data: Record): IVideoConferenceBuilder; + + getProviderData(): Record; + + setTitle(name: string): IVideoConferenceBuilder; + + getTitle(): string; + + setDiscussionRid(rid: string | undefined): IVideoConferenceBuilder; + + getDiscussionRid(): string | undefined; + + getVideoConference(): AppVideoConference; +} diff --git a/packages/apps-engine/src/definition/accessors/IVideoConferenceExtend.ts b/packages/apps-engine/src/definition/accessors/IVideoConferenceExtend.ts new file mode 100644 index 000000000000..d9b7e5838368 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IVideoConferenceExtend.ts @@ -0,0 +1,21 @@ +import type { RocketChatAssociationModel } from '../metadata'; +import type { IVideoConferenceUser, VideoConference } from '../videoConferences'; +import type { VideoConferenceMember } from '../videoConferences/IVideoConference'; + +export interface IVideoConferenceExtender { + kind: RocketChatAssociationModel.VIDEO_CONFERENCE; + + setProviderData(value: Record): IVideoConferenceExtender; + + setStatus(value: VideoConference['status']): IVideoConferenceExtender; + + setEndedBy(value: IVideoConferenceUser['_id']): IVideoConferenceExtender; + + setEndedAt(value: VideoConference['endedAt']): IVideoConferenceExtender; + + addUser(userId: VideoConferenceMember['_id'], ts?: VideoConferenceMember['ts']): IVideoConferenceExtender; + + setDiscussionRid(rid: VideoConference['discussionRid']): IVideoConferenceExtender; + + getVideoConference(): VideoConference; +} diff --git a/packages/apps-engine/src/definition/accessors/IVideoConferenceRead.ts b/packages/apps-engine/src/definition/accessors/IVideoConferenceRead.ts new file mode 100644 index 000000000000..aa2d53d70590 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IVideoConferenceRead.ts @@ -0,0 +1,15 @@ +import type { VideoConference } from '../videoConferences/IVideoConference'; + +/** + * This accessor provides methods for accessing + * video conferences in a read-only-fashion. + */ +export interface IVideoConferenceRead { + /** + * Gets a video conference by an id. + * + * @param id the id of the video conference + * @returns the video conference + */ + getById(id: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/index.ts b/packages/apps-engine/src/definition/accessors/index.ts new file mode 100644 index 000000000000..e98a4208fe13 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/index.ts @@ -0,0 +1,58 @@ +export * from './IApiExtend'; +export * from './IAppAccessors'; +export * from './IAppInstallationContext'; +export * from './IAppUpdateContext'; +export * from './IAppUninstallationContext'; +export * from './ICloudWorkspaceRead'; +export * from './IConfigurationExtend'; +export * from './IConfigurationModify'; +export * from './IDiscussionBuilder'; +export * from './IEnvironmentalVariableRead'; +export * from './IEnvironmentRead'; +export * from './IEnvironmentWrite'; +export * from './IExternalComponentsExtend'; +export * from './IHttp'; +export * from './ILivechatCreator'; +export * from './ILivechatMessageBuilder'; +export * from './ILivechatRead'; +export * from './ILivechatUpdater'; +export * from './ILogEntry'; +export * from './ILogger'; +export * from './IMessageBuilder'; +export * from './IMessageExtender'; +export * from './IMessageRead'; +export * from './IMessageUpdater'; +export * from './IModify'; +export * from './IModifyCreator'; +export * from './IModifyDeleter'; +export * from './IModifyExtender'; +export * from './IModifyUpdater'; +export * from './INotifier'; +export * from './IPersistence'; +export * from './IPersistenceRead'; +export * from './IRead'; +export * from './IRoleRead'; +export * from './IRoomBuilder'; +export * from './IRoomExtender'; +export * from './IRoomRead'; +export * from './ISchedulerExtend'; +export * from './ISchedulerModify'; +export * from './IServerSettingRead'; +export * from './IServerSettingsModify'; +export * from './IServerSettingUpdater'; +export * from './ISettingRead'; +export * from './ISettingsExtend'; +export * from './ISettingUpdater'; +export * from './ISlashCommandsExtend'; +export * from './ISlashCommandsModify'; +export * from './IUIController'; +export * from './IUIExtend'; +export * from './IUploadCreator'; +export * from './IUploadRead'; +export * from './IUserBuilder'; +export * from './IUserRead'; +export * from './IVideoConferenceBuilder'; +export * from './IVideoConferenceExtend'; +export * from './IVideoConferenceRead'; +export * from './IVideoConfProvidersExtend'; +export * from './IModerationModify'; diff --git a/packages/apps-engine/src/definition/api/ApiEndpoint.ts b/packages/apps-engine/src/definition/api/ApiEndpoint.ts new file mode 100644 index 000000000000..8ab7610c9b4f --- /dev/null +++ b/packages/apps-engine/src/definition/api/ApiEndpoint.ts @@ -0,0 +1,40 @@ +import type { IApp } from '../IApp'; +import { HttpStatusCode } from '../accessors'; +import type { IApiEndpoint } from './IApiEndpoint'; +import type { IApiResponse, IApiResponseJSON } from './IResponse'; + +/** Represents an api endpoint that is being provided. */ +export abstract class ApiEndpoint implements IApiEndpoint { + /** + * The last part of the api URL. Example: https://{your-server-address}/api/apps/public/{your-app-id}/{path} + * or https://{your-server-address}/api/apps/private/{your-app-id}/{private-hash}/{path} + */ + public path: string; + + constructor(public app: IApp) {} + + /** + * Return response with status 200 (OK) and a optional content + * @param content + */ + protected success(content?: any): IApiResponse { + return { + status: HttpStatusCode.OK, + content, + }; + } + + /** + * Return a json response adding Content Type header as + * application/json if not already provided + * @param reponse + */ + protected json(response: IApiResponseJSON): IApiResponse { + if (!response.headers || !response.headers['content-type']) { + response.headers = response.headers || {}; + response.headers['content-type'] = 'application/json'; + } + + return response; + } +} diff --git a/packages/apps-engine/src/definition/api/IApi.ts b/packages/apps-engine/src/definition/api/IApi.ts new file mode 100644 index 000000000000..9ad2f42ed5a8 --- /dev/null +++ b/packages/apps-engine/src/definition/api/IApi.ts @@ -0,0 +1,58 @@ +import type { IApiEndpoint } from './IApiEndpoint'; + +/** + * Represents an api that is being provided. + */ +export interface IApi { + /** + * Provides the visibility method of the URL, see the ApiVisibility descriptions for more information + */ + visibility: ApiVisibility; + /** + * Provides the visibility method of the URL, see the ApiSecurity descriptions for more information + */ + security: ApiSecurity; + /** + * Provide enpoints for this api registry + */ + endpoints: Array; +} + +export enum ApiVisibility { + /** + * A public Api has a fixed format for a url. Using it enables an + * easy to remember structure, however, it also means the url is + * intelligently guessed. As a result, we recommend having some + * sort of security setup if you must have a public api.Whether + * you use the provided security, ApiSecurity, or implement your own. + * Url format: + * `https://{your-server-address}/api/apps/public/{your-app-id}/{path}` + */ + PUBLIC, + /** + * Private Api's contain a random value in the url format, + * making them harder go guess by default. The random value + * will be generated whenever the App is installed on a server. + * This means that the URL will not be the same on any server, + * but will remain the same throughout the lifecycle of an App + * including updates. As a result, if a user uninstalls the App + * and reinstalls the App, then the random value will change. + * Url format: + * `https://{your-server-address}/api/apps/private/{your-app-id}/{random-hash}/{path}` + */ + PRIVATE, +} + +export enum ApiSecurity { + /** + * No security check will be executed agains the calls made to this URL + */ + UNSECURE, + /** + * Only calls containing a valid token will be able to execute the api + * Mutiple tokens can be generated to access the api, by default one + * will be generated automatically. + * @param `X-Auth-Token` + */ + // CHECKSUM_SECRET, +} diff --git a/packages/apps-engine/src/definition/api/IApiEndpoint.ts b/packages/apps-engine/src/definition/api/IApiEndpoint.ts new file mode 100644 index 000000000000..b369fc175dc2 --- /dev/null +++ b/packages/apps-engine/src/definition/api/IApiEndpoint.ts @@ -0,0 +1,47 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import type { IApiEndpointInfo } from './IApiEndpointInfo'; +import type { IApiExample } from './IApiExample'; +import type { IApiRequest } from './IRequest'; +import type { IApiResponse } from './IResponse'; + +/** + * Represents an api endpoint that is being provided. + */ +export interface IApiEndpoint { + /** + * The last part of the api URL. Example: https://{your-server-address}/api/apps/public/{your-app-id}/{path} + * or https://{your-server-address}/api/apps/private/{your-app-id}/{private-hash}/{path} + */ + path: string; + examples?: { [key: string]: IApiExample }; + /** + * Whether this endpoint requires an authenticated user to access it. + * + * The authentication will be done by the host server using its own + * authentication system. + * + * If no authentication is provided, the request will be automatically + * rejected with a 401 status code. + */ + authRequired?: boolean; + + /** + * The methods that are available for this endpoint. + * This property is provided by the Runtime and should not be set manually. + * + * Its values are used on the Apps-Engine to validate the request method. + */ + _availableMethods?: string[]; + + /** + * Called whenever the publically accessible url for this App is called, + * if you handle the methods differently then split it out so your code doesn't get too big. + */ + get?(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise; + post?(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise; + put?(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise; + delete?(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise; + head?(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise; + options?(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise; + patch?(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise; +} diff --git a/packages/apps-engine/src/definition/api/IApiEndpointInfo.ts b/packages/apps-engine/src/definition/api/IApiEndpointInfo.ts new file mode 100644 index 000000000000..de9144784b34 --- /dev/null +++ b/packages/apps-engine/src/definition/api/IApiEndpointInfo.ts @@ -0,0 +1,6 @@ +export interface IApiEndpointInfo { + basePath: string; + fullPath: string; + appId: string; + hash?: string; +} diff --git a/packages/apps-engine/src/definition/api/IApiEndpointMetadata.ts b/packages/apps-engine/src/definition/api/IApiEndpointMetadata.ts new file mode 100644 index 000000000000..0ede26045f79 --- /dev/null +++ b/packages/apps-engine/src/definition/api/IApiEndpointMetadata.ts @@ -0,0 +1,10 @@ +import type { IApiExample } from './IApiExample'; + +export interface IApiEndpointMetadata { + path: string; + computedPath: string; + methods: Array; + examples?: { + [key: string]: IApiExample; + }; +} diff --git a/packages/apps-engine/src/definition/api/IApiExample.ts b/packages/apps-engine/src/definition/api/IApiExample.ts new file mode 100644 index 000000000000..f870d59e643d --- /dev/null +++ b/packages/apps-engine/src/definition/api/IApiExample.ts @@ -0,0 +1,19 @@ +/** + * Represents the parameters of an api example. + */ +export interface IApiExample { + params?: { [key: string]: string }; + query?: { [key: string]: string }; + headers?: { [key: string]: string }; + content?: any; +} + +/** + * Decorator to describe api examples + */ +export function example(options: IApiExample) { + return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { + target.examples = target.examples || {}; + target.examples[propertyKey] = options; + }; +} diff --git a/packages/apps-engine/src/definition/api/IRequest.ts b/packages/apps-engine/src/definition/api/IRequest.ts new file mode 100644 index 000000000000..027de11b2026 --- /dev/null +++ b/packages/apps-engine/src/definition/api/IRequest.ts @@ -0,0 +1,16 @@ +import type { RequestMethod } from '../accessors'; +import type { IUser } from '../users'; + +export interface IApiRequest { + method: RequestMethod; + headers: { [key: string]: string }; + query: { [key: string]: string }; + params: { [key: string]: string }; + content: any; + privateHash?: string; + /** + * The user that is making the request, as + * authenticated by Rocket.Chat's strategy. + */ + user?: IUser; +} diff --git a/packages/apps-engine/src/definition/api/IResponse.ts b/packages/apps-engine/src/definition/api/IResponse.ts new file mode 100644 index 000000000000..8f394b8a93b0 --- /dev/null +++ b/packages/apps-engine/src/definition/api/IResponse.ts @@ -0,0 +1,13 @@ +import type { HttpStatusCode } from '../accessors'; + +export interface IApiResponse { + status: HttpStatusCode; + headers?: { [key: string]: string }; + content?: any; +} + +export interface IApiResponseJSON { + status: HttpStatusCode; + headers?: { [key: string]: string }; + content?: { [key: string]: any }; +} diff --git a/packages/apps-engine/src/definition/api/index.ts b/packages/apps-engine/src/definition/api/index.ts new file mode 100644 index 000000000000..41e4482f2ec6 --- /dev/null +++ b/packages/apps-engine/src/definition/api/index.ts @@ -0,0 +1,8 @@ +export { ApiEndpoint } from './ApiEndpoint'; +export { IApi, ApiVisibility, ApiSecurity } from './IApi'; +export { IApiEndpoint } from './IApiEndpoint'; +export { IApiEndpointInfo } from './IApiEndpointInfo'; +export { IApiExample, example } from './IApiExample'; +export { IApiRequest } from './IRequest'; +export { IApiResponse } from './IResponse'; +export { IApiEndpointMetadata } from './IApiEndpointMetadata'; diff --git a/packages/apps-engine/src/definition/app-schema.json b/packages/apps-engine/src/definition/app-schema.json new file mode 100644 index 000000000000..68c2e0c19edc --- /dev/null +++ b/packages/apps-engine/src/definition/app-schema.json @@ -0,0 +1,75 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Rocket.Chat App", + "description": "A Rocket.Chat App declaration for usage inside of Rocket.Chat.", + "type": "object", + "properties": { + "id": { + "description": "The App's unique identifier in uuid v4 format. This is optional, although recommended, however if you are going to publish on the App store, you will be assigned one.", + "type": "string", + "pattern": "^[0-9a-fA-f]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$", + "minLength": 36, + "maxLength": 36 + }, + "name": { + "description": "The public visible name of this App.", + "type": "string" + }, + "nameSlug": { + "description": "A url friendly slugged version of your App's name.", + "type": "string", + "pattern": "^([a-z]|\\-)+$", + "minLength": 3 + }, + "version": { + "description": "The version of this App which will be used for display publicly and letting users know there is an update. This uses the semver format.", + "type": "string", + "pattern": "^(?:\\d*)\\.(?:\\d*)\\.(?:\\d*)$", + "minLength": 5 + }, + "description": { + "description": "A description of this App, used to explain what this App does and provides for the user.", + "type": "string" + }, + "requiredApiVersion": { + "description": "The required version of the App's API which this App depends on. This uses the semver format.", + "type": "string", + "pattern": "^(?:\\^|~)?(?:\\d*)\\.(?:\\d*)\\.(?:\\d*)$", + "minLength": 5 + }, + "author": { + "type": "object", + "properties": { + "name": { + "description": "The author's name who created this App.", + "type": "string" + }, + "support": { + "description": "The place where people can get support for this App, whether email or website.", + "type": "string" + }, + "homepage": { + "description": "The homepage for this App, it can be a Github or the author's website.", + "type": "string", + "format": "uri" + } + }, + "required": ["name", "support"] + }, + "classFile": { + "type": "string", + "description": "The name of the file which contains your App TypeScript source code.", + "pattern": "^.*\\.(ts)$" + }, + "iconFile": { + "type": "string", + "description": "The name of the file to use as the icon.", + "pattern": "^.*\\.(png|jpg|jpeg|gif)$" + }, + "assetsFolder": { + "type": "string", + "description": "The name of the folder which contains all of your resources, it should not start with a period." + } + }, + "required": ["id", "name", "nameSlug", "version", "description", "requiredApiVersion", "author", "classFile", "iconFile"] +} diff --git a/packages/apps-engine/src/definition/assets/IAsset.ts b/packages/apps-engine/src/definition/assets/IAsset.ts new file mode 100644 index 000000000000..30c54ae6565d --- /dev/null +++ b/packages/apps-engine/src/definition/assets/IAsset.ts @@ -0,0 +1,6 @@ +export interface IAsset { + name: string; + path: string; + type: string; + public: boolean; +} diff --git a/packages/apps-engine/src/definition/assets/IAssetProvider.ts b/packages/apps-engine/src/definition/assets/IAssetProvider.ts new file mode 100644 index 000000000000..4e1da50222fa --- /dev/null +++ b/packages/apps-engine/src/definition/assets/IAssetProvider.ts @@ -0,0 +1,5 @@ +import type { IAsset } from './IAsset'; + +export interface IAssetProvider { + getAssets(): Array; +} diff --git a/packages/apps-engine/src/definition/assets/index.ts b/packages/apps-engine/src/definition/assets/index.ts new file mode 100644 index 000000000000..98abab64ddf5 --- /dev/null +++ b/packages/apps-engine/src/definition/assets/index.ts @@ -0,0 +1,4 @@ +import { IAsset } from './IAsset'; +import { IAssetProvider } from './IAssetProvider'; + +export { IAsset, IAssetProvider }; diff --git a/packages/apps-engine/src/definition/cloud/IWorkspaceToken.ts b/packages/apps-engine/src/definition/cloud/IWorkspaceToken.ts new file mode 100644 index 000000000000..40a46bf7e37f --- /dev/null +++ b/packages/apps-engine/src/definition/cloud/IWorkspaceToken.ts @@ -0,0 +1,4 @@ +export interface IWorkspaceToken { + token: string; + expiresAt: Date; +} diff --git a/packages/apps-engine/src/definition/email/IEmail.ts b/packages/apps-engine/src/definition/email/IEmail.ts new file mode 100644 index 000000000000..ca81b23e5bcc --- /dev/null +++ b/packages/apps-engine/src/definition/email/IEmail.ts @@ -0,0 +1,9 @@ +export interface IEmail { + to: string | string[]; + from: string; + replyTo?: string; + subject: string; + html?: string; + text?: string; + headers?: string; +} diff --git a/packages/apps-engine/src/definition/email/IEmailDescriptor.ts b/packages/apps-engine/src/definition/email/IEmailDescriptor.ts new file mode 100644 index 000000000000..168bae039168 --- /dev/null +++ b/packages/apps-engine/src/definition/email/IEmailDescriptor.ts @@ -0,0 +1,11 @@ +export interface IEmailDescriptor { + from?: string | undefined; + to?: string | Array | undefined; + cc?: string | Array | undefined; + bcc?: string | Array | undefined; + replyTo?: string | Array | undefined; + subject?: string | undefined; + text?: string | undefined; + html?: string | undefined; + headers?: Record | undefined; +} diff --git a/packages/apps-engine/src/definition/email/IPreEmailSent.ts b/packages/apps-engine/src/definition/email/IPreEmailSent.ts new file mode 100644 index 000000000000..2d5e40c92851 --- /dev/null +++ b/packages/apps-engine/src/definition/email/IPreEmailSent.ts @@ -0,0 +1,25 @@ +import type { IEmailDescriptor, IPreEmailSentContext } from '.'; +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; + +/** + * Event interface that allows apps to + * register as a handler of of the `IPreEmailSent` + * event. + * + * This event is trigger before the mailer sends + * an email. + * + * To prevent the email from being sent, you can + * throw an error with a message specifying the + * reason for rejection. + */ +export interface IPreEmailSent { + [AppMethod.EXECUTE_PRE_EMAIL_SENT]( + context: IPreEmailSentContext, + read: IRead, + http: IHttp, + persis: IPersistence, + modify: IModify, + ): Promise; +} diff --git a/packages/apps-engine/src/definition/email/IPreEmailSentContext.ts b/packages/apps-engine/src/definition/email/IPreEmailSentContext.ts new file mode 100644 index 000000000000..7427424f88eb --- /dev/null +++ b/packages/apps-engine/src/definition/email/IPreEmailSentContext.ts @@ -0,0 +1,6 @@ +import type { IEmailDescriptor } from './IEmailDescriptor'; + +export interface IPreEmailSentContext { + context: unknown; + email: IEmailDescriptor; +} diff --git a/packages/apps-engine/src/definition/email/index.ts b/packages/apps-engine/src/definition/email/index.ts new file mode 100644 index 000000000000..6074ebaec4c3 --- /dev/null +++ b/packages/apps-engine/src/definition/email/index.ts @@ -0,0 +1,4 @@ +export * from './IEmailDescriptor'; +export * from './IPreEmailSent'; +export * from './IPreEmailSentContext'; +export * from './IEmail'; diff --git a/packages/apps-engine/src/definition/example-app.json b/packages/apps-engine/src/definition/example-app.json new file mode 100644 index 000000000000..e78048d1d316 --- /dev/null +++ b/packages/apps-engine/src/definition/example-app.json @@ -0,0 +1,13 @@ +{ + //This is an example of how a app.json file will look like + "name": "Testing", + "nameSlug": "testing", + "description": "Testing description", + "version": "1.0.0", + "requiredApiVersion": "0.0.1", + "author": { + "name": "Bradley Hilton", + "support": "https://github.com/RocketChat/Rocket.Chat.Apps-engine" + }, + "classFile": "ExampleApp.ts" +} diff --git a/packages/apps-engine/src/definition/exceptions/AppsEngineException.ts b/packages/apps-engine/src/definition/exceptions/AppsEngineException.ts new file mode 100644 index 000000000000..a3e802aa69d8 --- /dev/null +++ b/packages/apps-engine/src/definition/exceptions/AppsEngineException.ts @@ -0,0 +1,32 @@ +/** + * The internal exception from the framework + * + * It's used to signal to the outside world that + * a _known_ exception has happened during the execution + * of the apps. + * + * It's the base exception for other known classes + * such as UserNotAllowedException, which is used + * to inform the host that an app identified + * that a user cannot perform some action, e.g. + * join a room + */ +export class AppsEngineException extends Error { + public name = 'AppsEngineException'; + + public static JSONRPC_ERROR_CODE = -32070; + + public message: string; + + constructor(message?: string) { + super(); + this.message = message; + } + + public getErrorInfo() { + return { + name: this.name, + message: this.message, + }; + } +} diff --git a/packages/apps-engine/src/definition/exceptions/EssentialAppDisabledException.ts b/packages/apps-engine/src/definition/exceptions/EssentialAppDisabledException.ts new file mode 100644 index 000000000000..e5043d93e336 --- /dev/null +++ b/packages/apps-engine/src/definition/exceptions/EssentialAppDisabledException.ts @@ -0,0 +1,16 @@ +import { AppsEngineException } from '.'; + +/** + * This exception informs the host system that an + * app essential to the execution of a system action + * is disabled, so the action should be halted. + * + * Apps can register to be considered essential to + * the execution of internal events of the framework + * such as `IPreMessageSentPrevent`, `IPreRoomUserJoined`, + * etc. + * + * This is used interally by the framework and is not + * intended to be thrown manually by apps. + */ +export class EssentialAppDisabledException extends AppsEngineException {} diff --git a/packages/apps-engine/src/definition/exceptions/FileUploadNotAllowedException.ts b/packages/apps-engine/src/definition/exceptions/FileUploadNotAllowedException.ts new file mode 100644 index 000000000000..0ae9d98edb3f --- /dev/null +++ b/packages/apps-engine/src/definition/exceptions/FileUploadNotAllowedException.ts @@ -0,0 +1,12 @@ +import { AppsEngineException } from './AppsEngineException'; + +/** + * This exception informs the host system that an + * app has determined that a file upload is not + * allowed to be completed. + * + * Currently it is expected to be thrown by the + * following events: + * - IPreFileUpload + */ +export class FileUploadNotAllowedException extends AppsEngineException {} diff --git a/packages/apps-engine/src/definition/exceptions/InvalidSettingValueException.ts b/packages/apps-engine/src/definition/exceptions/InvalidSettingValueException.ts new file mode 100644 index 000000000000..2b1a193accb2 --- /dev/null +++ b/packages/apps-engine/src/definition/exceptions/InvalidSettingValueException.ts @@ -0,0 +1,8 @@ +import { AppsEngineException } from './AppsEngineException'; + +/** + * This exception informs the host system that an + * app has determined that an invalid setting value + * is passed. + */ +export class InvalidSettingValueException extends AppsEngineException {} diff --git a/packages/apps-engine/src/definition/exceptions/UserNotAllowedException.ts b/packages/apps-engine/src/definition/exceptions/UserNotAllowedException.ts new file mode 100644 index 000000000000..d81969d8f62a --- /dev/null +++ b/packages/apps-engine/src/definition/exceptions/UserNotAllowedException.ts @@ -0,0 +1,14 @@ +import { AppsEngineException } from '.'; + +/** + * This exception informs the host system that an + * app has determined that an user is not allowed + * to perform a specific action. + * + * Currently it is expected to be thrown by the + * following events: + * - IPreRoomCreatePrevent + * - IPreRoomUserJoined + * - IPreRoomUserLeave + */ +export class UserNotAllowedException extends AppsEngineException {} diff --git a/packages/apps-engine/src/definition/exceptions/index.ts b/packages/apps-engine/src/definition/exceptions/index.ts new file mode 100644 index 000000000000..6129978c2f89 --- /dev/null +++ b/packages/apps-engine/src/definition/exceptions/index.ts @@ -0,0 +1,5 @@ +export * from './AppsEngineException'; +export * from './EssentialAppDisabledException'; +export * from './UserNotAllowedException'; +export * from './FileUploadNotAllowedException'; +export * from './InvalidSettingValueException'; diff --git a/packages/apps-engine/src/definition/externalComponent/IExternalComponent.ts b/packages/apps-engine/src/definition/externalComponent/IExternalComponent.ts new file mode 100644 index 000000000000..7c750e1c7e22 --- /dev/null +++ b/packages/apps-engine/src/definition/externalComponent/IExternalComponent.ts @@ -0,0 +1,51 @@ +import type { IExternalComponentOptions } from './IExternalComponentOptions'; +import type { IExternalComponentState } from './IExternalComponentState'; +/** + * Represents an external component that is being provided. + */ +export interface IExternalComponent { + /** + * Provides the appId of the app which the external component belongs to. + */ + appId: string; + /** + * Provides the name of the external component. This key must be unique. + */ + name: string; + /** + * Provides the description of the external component. + */ + description: string; + /** + * Provides the icon's url or base64 string. + */ + icon: string; + /** + * Provides the location which external component needs + * to register, see the ExternalComponentLocation descriptions + * for the more information. + */ + location: ExternalComponentLocation; + /** + * Provides the url that external component will load. + */ + url: string; + /** + * Provides options for the external component. + */ + options?: IExternalComponentOptions; + /** + * Represents the current state of the external component. + * The value is *null* until the ExternalComponentOpened + * event is triggered. It doesn't make sense to get its value in + * PreExternalComponentOpenedPrevent, PreExternalComponentOpenedModify + * and PreExternalComponentOpenedExtend handlers. + */ + state?: IExternalComponentState; +} + +export enum ExternalComponentLocation { + CONTEXTUAL_BAR = 'CONTEXTUAL_BAR', + + MODAL = 'MODAL', +} diff --git a/packages/apps-engine/src/definition/externalComponent/IExternalComponentOptions.ts b/packages/apps-engine/src/definition/externalComponent/IExternalComponentOptions.ts new file mode 100644 index 000000000000..2581c047ab43 --- /dev/null +++ b/packages/apps-engine/src/definition/externalComponent/IExternalComponentOptions.ts @@ -0,0 +1,10 @@ +export interface IExternalComponentOptions { + /** + * The width of the external component + */ + width?: number; + /** + * The height of the external component + */ + height?: number; +} diff --git a/packages/apps-engine/src/definition/externalComponent/IExternalComponentState.ts b/packages/apps-engine/src/definition/externalComponent/IExternalComponentState.ts new file mode 100644 index 000000000000..4c401f3e28d1 --- /dev/null +++ b/packages/apps-engine/src/definition/externalComponent/IExternalComponentState.ts @@ -0,0 +1,16 @@ +import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from '../../client/definition'; + +/** + * The state of an external component, which contains the + * current user's information and the current room's information. + */ +export interface IExternalComponentState { + /** + * The user who opened this external component + */ + currentUser: IExternalComponentUserInfo; + /** + * The room where the external component belongs to + */ + currentRoom: IExternalComponentRoomInfo; +} diff --git a/packages/apps-engine/src/definition/externalComponent/IPostExternalComponentClosed.ts b/packages/apps-engine/src/definition/externalComponent/IPostExternalComponentClosed.ts new file mode 100644 index 000000000000..24d224ba2913 --- /dev/null +++ b/packages/apps-engine/src/definition/externalComponent/IPostExternalComponentClosed.ts @@ -0,0 +1,16 @@ +import type { IHttp, IPersistence, IRead } from '../accessors'; +import type { IExternalComponent } from './IExternalComponent'; + +/** + * Handler called after an external component is closed. + */ +export interface IPostExternalComponentClosed { + /** + * Method called after an external component is closed. + * + * @param externalComponent The external component which was closed + * @param read An accessor to the environment + * @param http An accessor to the outside world + */ + executePostExternalComponentClosed(externalComponent: IExternalComponent, read: IRead, http: IHttp, persistence: IPersistence): Promise; +} diff --git a/packages/apps-engine/src/definition/externalComponent/IPostExternalComponentOpened.ts b/packages/apps-engine/src/definition/externalComponent/IPostExternalComponentOpened.ts new file mode 100644 index 000000000000..8a09ebe711d2 --- /dev/null +++ b/packages/apps-engine/src/definition/externalComponent/IPostExternalComponentOpened.ts @@ -0,0 +1,16 @@ +import type { IHttp, IPersistence, IRead } from '../accessors'; +import type { IExternalComponent } from './IExternalComponent'; + +/** + * Handler called after an external component is opened. + */ +export interface IPostExternalComponentOpened { + /** + * Method called after an external component is opened. + * + * @param externalComponent The external component which was opened + * @param read An accessor to the environment + * @param http An accessor to the outside world + */ + executePostExternalComponentOpened(externalComponent: IExternalComponent, read: IRead, http: IHttp, persistence: IPersistence): Promise; +} diff --git a/packages/apps-engine/src/definition/externalComponent/index.ts b/packages/apps-engine/src/definition/externalComponent/index.ts new file mode 100644 index 000000000000..acd4bbf44982 --- /dev/null +++ b/packages/apps-engine/src/definition/externalComponent/index.ts @@ -0,0 +1,5 @@ +import { IExternalComponent } from './IExternalComponent'; +import { IPostExternalComponentClosed } from './IPostExternalComponentClosed'; +import { IPostExternalComponentOpened } from './IPostExternalComponentOpened'; + +export { IExternalComponent, IPostExternalComponentClosed, IPostExternalComponentOpened }; diff --git a/packages/apps-engine/src/definition/livechat/IDepartment.ts b/packages/apps-engine/src/definition/livechat/IDepartment.ts new file mode 100644 index 000000000000..1a59c9835612 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IDepartment.ts @@ -0,0 +1,17 @@ +export interface IDepartment { + id: string; + name?: string; + email?: string; + description?: string; + offlineMessageChannelName?: string; + requestTagBeforeClosingChat?: false; + chatClosingTags?: Array; + abandonedRoomsCloseCustomMessage?: string; + waitingQueueMessage?: string; + departmentsAllowedToForward?: string; + enabled: boolean; + updatedAt: Date; + numberOfAgents: number; + showOnOfflineForm: boolean; + showOnRegistration: boolean; +} diff --git a/packages/apps-engine/src/definition/livechat/ILivechatEventContext.ts b/packages/apps-engine/src/definition/livechat/ILivechatEventContext.ts new file mode 100644 index 000000000000..b94f07ef0250 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/ILivechatEventContext.ts @@ -0,0 +1,7 @@ +import type { IUser } from '../users'; +import type { ILivechatRoom } from './ILivechatRoom'; + +export interface ILivechatEventContext { + agent: IUser; + room: ILivechatRoom; +} diff --git a/packages/apps-engine/src/definition/livechat/ILivechatMessage.ts b/packages/apps-engine/src/definition/livechat/ILivechatMessage.ts new file mode 100644 index 000000000000..d7cc5497d70e --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/ILivechatMessage.ts @@ -0,0 +1,7 @@ +import type { IMessage } from '../messages/IMessage'; +import type { IVisitor } from './IVisitor'; + +export interface ILivechatMessage extends IMessage { + visitor?: IVisitor; + token?: string; +} diff --git a/packages/apps-engine/src/definition/livechat/ILivechatRoom.ts b/packages/apps-engine/src/definition/livechat/ILivechatRoom.ts new file mode 100644 index 000000000000..e3f55142331a --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/ILivechatRoom.ts @@ -0,0 +1,55 @@ +import { RoomType } from '../rooms'; +import type { IRoom } from '../rooms/IRoom'; +import type { IUser } from '../users'; +import type { IDepartment } from './IDepartment'; +import type { IVisitor } from './IVisitor'; + +export enum OmnichannelSourceType { + WIDGET = 'widget', + EMAIL = 'email', + SMS = 'sms', + APP = 'app', + OTHER = 'other', +} + +interface IOmnichannelSourceApp { + type: 'app'; + id: string; + // A human readable alias that goes with the ID, for post analytical purposes + alias?: string; + // A label to be shown in the room info + label?: string; + sidebarIcon?: string; + defaultIcon?: string; +} +type OmnichannelSource = + | { + type: Exclude; + } + | IOmnichannelSourceApp; + +export interface IVisitorChannelInfo { + lastMessageTs?: Date; + phone?: string; +} + +export interface ILivechatRoom extends IRoom { + visitor: IVisitor; + visitorChannelInfo?: IVisitorChannelInfo; + department?: IDepartment; + closer: 'user' | 'visitor' | 'bot'; + closedBy?: IUser; + servedBy?: IUser; + responseBy?: IUser; + isWaitingResponse: boolean; + isOpen: boolean; + closedAt?: Date; + source?: OmnichannelSource; +} + +export const isLivechatRoom = (room: IRoom): room is ILivechatRoom => { + return room.type === RoomType.LIVE_CHAT; +}; +export const isLivechatFromApp = (room: ILivechatRoom): room is ILivechatRoom & { source: IOmnichannelSourceApp } => { + return room.source && room.source.type === 'app'; +}; diff --git a/packages/apps-engine/src/definition/livechat/ILivechatRoomClosedHandler.ts b/packages/apps-engine/src/definition/livechat/ILivechatRoomClosedHandler.ts new file mode 100644 index 000000000000..dba672ad391b --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/ILivechatRoomClosedHandler.ts @@ -0,0 +1,19 @@ +import type { IHttp, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { ILivechatRoom } from './ILivechatRoom'; + +/** + * Handler called after a livechat room is closed. + * @deprecated please prefer the IPostLivechatRoomClosed event + */ +export interface ILivechatRoomClosedHandler { + /** + * Method called *after* a livechat room is closed. + * + * @param livechatRoom The livechat room which is closed. + * @param read An accessor to the environment + * @param http An accessor to the outside world + * @param persistence An accessor to the App's persistence + */ + [AppMethod.EXECUTE_LIVECHAT_ROOM_CLOSED_HANDLER](data: ILivechatRoom, read: IRead, http: IHttp, persistence: IPersistence): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/ILivechatTransferData.ts b/packages/apps-engine/src/definition/livechat/ILivechatTransferData.ts new file mode 100644 index 000000000000..988d0a2ec5fd --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/ILivechatTransferData.ts @@ -0,0 +1,8 @@ +import type { IUser } from '../users'; +import type { ILivechatRoom } from './ILivechatRoom'; + +export interface ILivechatTransferData { + currentRoom: ILivechatRoom; + targetAgent?: IUser; + targetDepartment?: string; +} diff --git a/packages/apps-engine/src/definition/livechat/ILivechatTransferEventContext.ts b/packages/apps-engine/src/definition/livechat/ILivechatTransferEventContext.ts new file mode 100644 index 000000000000..74db472751d4 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/ILivechatTransferEventContext.ts @@ -0,0 +1,15 @@ +import type { IRoom } from '../rooms'; +import type { IUser } from '../users'; +import type { IDepartment } from './IDepartment'; + +export enum LivechatTransferEventType { + AGENT = 'agent', + DEPARTMENT = 'department', +} + +export interface ILivechatTransferEventContext { + type: LivechatTransferEventType; + room: IRoom; + from: IUser | IDepartment; + to: IUser | IDepartment; +} diff --git a/packages/apps-engine/src/definition/livechat/IPostLivechatAgentAssigned.ts b/packages/apps-engine/src/definition/livechat/IPostLivechatAgentAssigned.ts new file mode 100644 index 000000000000..5c322534c9ff --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IPostLivechatAgentAssigned.ts @@ -0,0 +1,25 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { ILivechatEventContext } from './ILivechatEventContext'; + +/** + * Handler called after the assignment of a livechat agent. + */ +export interface IPostLivechatAgentAssigned { + /** + * Handler called *after* the assignment of a livechat agent. + * + * @param data the livechat context data which contains agent's info and room's info. + * @param read An accessor to the environment + * @param http An accessor to the outside world + * @param persis An accessor to the App's persistence + * @param modify An accessor to the modifier + */ + [AppMethod.EXECUTE_POST_LIVECHAT_AGENT_ASSIGNED]( + context: ILivechatEventContext, + read: IRead, + http: IHttp, + persis: IPersistence, + modify?: IModify, + ): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/IPostLivechatAgentUnassigned.ts b/packages/apps-engine/src/definition/livechat/IPostLivechatAgentUnassigned.ts new file mode 100644 index 000000000000..2884cfa94b83 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IPostLivechatAgentUnassigned.ts @@ -0,0 +1,25 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { ILivechatEventContext } from './ILivechatEventContext'; + +/** + * Handler called after the unassignment of a livechat agent. + */ +export interface IPostLivechatAgentUnassigned { + /** + * Handler called *after* the unassignment of a livechat agent. + * + * @param data the livechat context data which contains agent's info and room's info. + * @param read An accessor to the environment + * @param http An accessor to the outside world + * @param persis An accessor to the App's persistence + * @param modify An accessor to the modifier + */ + [AppMethod.EXECUTE_POST_LIVECHAT_AGENT_UNASSIGNED]( + context: ILivechatEventContext, + read: IRead, + http: IHttp, + persis: IPersistence, + modify?: IModify, + ): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/IPostLivechatGuestSaved.ts b/packages/apps-engine/src/definition/livechat/IPostLivechatGuestSaved.ts new file mode 100644 index 000000000000..2c5edc0b9692 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IPostLivechatGuestSaved.ts @@ -0,0 +1,19 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { IVisitor } from './IVisitor'; + +/** + * Handler called after the guest's info get saved. + */ +export interface IPostLivechatGuestSaved { + /** + * Handler called *after* the guest's info get saved. + * + * @param data the livechat context data which contains guest's info and room's info. + * @param read An accessor to the environment + * @param http An accessor to the outside world + * @param persis An accessor to the App's persistence + * @param modify An accessor to the modifier + */ + [AppMethod.EXECUTE_POST_LIVECHAT_GUEST_SAVED](context: IVisitor, read: IRead, http: IHttp, persis: IPersistence, modify: IModify): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/IPostLivechatRoomClosed.ts b/packages/apps-engine/src/definition/livechat/IPostLivechatRoomClosed.ts new file mode 100644 index 000000000000..072e02e8c721 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IPostLivechatRoomClosed.ts @@ -0,0 +1,19 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { ILivechatRoom } from './ILivechatRoom'; + +/** + * Handler called after a livechat room is closed. + */ +export interface IPostLivechatRoomClosed { + /** + * Method called *after* a livechat room is closed. + * + * @param livechatRoom The livechat room which is closed. + * @param read An accessor to the environment + * @param http An accessor to the outside world + * @param persis An accessor to the App's persistence + * @param modify An accessor to the modifier + */ + [AppMethod.EXECUTE_POST_LIVECHAT_ROOM_CLOSED](room: ILivechatRoom, read: IRead, http: IHttp, persis: IPersistence, modify?: IModify): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/IPostLivechatRoomSaved.ts b/packages/apps-engine/src/definition/livechat/IPostLivechatRoomSaved.ts new file mode 100644 index 000000000000..13b6d1cb2e4b --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IPostLivechatRoomSaved.ts @@ -0,0 +1,19 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { ILivechatRoom } from './ILivechatRoom'; + +/** + * Handler called after the room's info get saved. + */ +export interface IPostLivechatRoomSaved { + /** + * Handler called *after* the room's info get saved. + * + * @param data the livechat context data which contains room's info. + * @param read An accessor to the environment + * @param http An accessor to the outside world + * @param persis An accessor to the App's persistence + * @param modify An accessor to the modifier + */ + [AppMethod.EXECUTE_POST_LIVECHAT_ROOM_SAVED](context: ILivechatRoom, read: IRead, http: IHttp, persis: IPersistence, modify: IModify): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/IPostLivechatRoomStarted.ts b/packages/apps-engine/src/definition/livechat/IPostLivechatRoomStarted.ts new file mode 100644 index 000000000000..237dcd9566e9 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IPostLivechatRoomStarted.ts @@ -0,0 +1,19 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { ILivechatRoom } from './ILivechatRoom'; + +/** + * Handler called after a livechat room is started. + */ +export interface IPostLivechatRoomStarted { + /** + * Method called *after* a livechat room is started. + * + * @param livechatRoom The livechat room which is started. + * @param read An accessor to the environment + * @param http An accessor to the outside world + * @param persis An accessor to the App's persistence + * @param modify An accessor to the modifier + */ + [AppMethod.EXECUTE_POST_LIVECHAT_ROOM_STARTED](room: ILivechatRoom, read: IRead, http: IHttp, persis: IPersistence, modify?: IModify): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/IPostLivechatRoomTransferred.ts b/packages/apps-engine/src/definition/livechat/IPostLivechatRoomTransferred.ts new file mode 100644 index 000000000000..aa86f8d358d3 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IPostLivechatRoomTransferred.ts @@ -0,0 +1,13 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { ILivechatTransferEventContext } from './ILivechatTransferEventContext'; + +export interface IPostLivechatRoomTransferred { + [AppMethod.EXECUTE_POST_LIVECHAT_ROOM_TRANSFERRED]( + context: ILivechatTransferEventContext, + read: IRead, + http: IHttp, + persis: IPersistence, + modify: IModify, + ): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/IVisitor.ts b/packages/apps-engine/src/definition/livechat/IVisitor.ts new file mode 100644 index 000000000000..db5876dd912d --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IVisitor.ts @@ -0,0 +1,16 @@ +import type { IVisitorEmail } from './IVisitorEmail'; +import type { IVisitorPhone } from './IVisitorPhone'; + +export interface IVisitor { + id?: string; + token: string; + username: string; + updatedAt?: Date; + name: string; + department?: string; + phone?: Array; + visitorEmails?: Array; + status?: string; + customFields?: { [key: string]: any }; + livechatData?: { [key: string]: any }; +} diff --git a/packages/apps-engine/src/definition/livechat/IVisitorEmail.ts b/packages/apps-engine/src/definition/livechat/IVisitorEmail.ts new file mode 100644 index 000000000000..a1e35380666e --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IVisitorEmail.ts @@ -0,0 +1,3 @@ +export interface IVisitorEmail { + address: string; +} diff --git a/packages/apps-engine/src/definition/livechat/IVisitorPhone.ts b/packages/apps-engine/src/definition/livechat/IVisitorPhone.ts new file mode 100644 index 000000000000..fe112777e7d7 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IVisitorPhone.ts @@ -0,0 +1,3 @@ +export interface IVisitorPhone { + phoneNumber: string; +} diff --git a/packages/apps-engine/src/definition/livechat/index.ts b/packages/apps-engine/src/definition/livechat/index.ts new file mode 100644 index 000000000000..5ea8d9f42885 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/index.ts @@ -0,0 +1,38 @@ +import { IDepartment } from './IDepartment'; +import { ILivechatEventContext } from './ILivechatEventContext'; +import { ILivechatMessage } from './ILivechatMessage'; +import { ILivechatRoom } from './ILivechatRoom'; +import { ILivechatRoomClosedHandler } from './ILivechatRoomClosedHandler'; +import { ILivechatTransferData } from './ILivechatTransferData'; +import { ILivechatTransferEventContext, LivechatTransferEventType } from './ILivechatTransferEventContext'; +import { IPostLivechatAgentAssigned } from './IPostLivechatAgentAssigned'; +import { IPostLivechatAgentUnassigned } from './IPostLivechatAgentUnassigned'; +import { IPostLivechatGuestSaved } from './IPostLivechatGuestSaved'; +import { IPostLivechatRoomClosed } from './IPostLivechatRoomClosed'; +import { IPostLivechatRoomSaved } from './IPostLivechatRoomSaved'; +import { IPostLivechatRoomStarted } from './IPostLivechatRoomStarted'; +import { IPostLivechatRoomTransferred } from './IPostLivechatRoomTransferred'; +import { IVisitor } from './IVisitor'; +import { IVisitorEmail } from './IVisitorEmail'; +import { IVisitorPhone } from './IVisitorPhone'; + +export { + ILivechatEventContext, + ILivechatMessage, + ILivechatRoom, + IPostLivechatAgentAssigned, + IPostLivechatAgentUnassigned, + IPostLivechatGuestSaved, + IPostLivechatRoomStarted, + IPostLivechatRoomClosed, + IPostLivechatRoomSaved, + IPostLivechatRoomTransferred, + ILivechatRoomClosedHandler, + ILivechatTransferData, + ILivechatTransferEventContext, + IDepartment, + IVisitor, + IVisitorEmail, + IVisitorPhone, + LivechatTransferEventType, +}; diff --git a/packages/apps-engine/src/definition/messages/IMessage.ts b/packages/apps-engine/src/definition/messages/IMessage.ts new file mode 100644 index 000000000000..d7ea6357497a --- /dev/null +++ b/packages/apps-engine/src/definition/messages/IMessage.ts @@ -0,0 +1,34 @@ +import type { LayoutBlock } from '@rocket.chat/ui-kit'; + +import type { IRoom } from '../rooms'; +import type { IBlock } from '../uikit'; +import type { IUser, IUserLookup } from '../users'; +import type { IMessageAttachment } from './IMessageAttachment'; +import type { IMessageFile } from './IMessageFile'; +import type { IMessageReactions } from './IMessageReaction'; + +export interface IMessage { + id?: string; + threadId?: string; + room: IRoom; + sender: IUser; + text?: string; + createdAt?: Date; + updatedAt?: Date; + editor?: IUser; + editedAt?: Date; + emoji?: string; + avatarUrl?: string; + alias?: string; + file?: IMessageFile; + attachments?: Array; + reactions?: IMessageReactions; + groupable?: boolean; + parseUrls?: boolean; + customFields?: { [key: string]: any }; + blocks?: Array; + starred?: Array<{ _id: string }>; + pinned?: boolean; + pinnedAt?: Date; + pinnedBy?: IUserLookup; +} diff --git a/packages/apps-engine/src/definition/messages/IMessageAction.ts b/packages/apps-engine/src/definition/messages/IMessageAction.ts new file mode 100644 index 000000000000..3f32e4aa781d --- /dev/null +++ b/packages/apps-engine/src/definition/messages/IMessageAction.ts @@ -0,0 +1,17 @@ +import type { MessageActionType } from './MessageActionType'; +import type { MessageProcessingType } from './MessageProcessingType'; + +/** + * Interface which represents an action which can be added to a message. + */ +export interface IMessageAction { + type: MessageActionType; + text?: string; + url?: string; + image_url?: string; + is_webview?: boolean; + webview_height_ratio?: string; + msg?: string; + msg_in_chat_window?: boolean; + msg_processing_type?: MessageProcessingType; +} diff --git a/packages/apps-engine/src/definition/messages/IMessageAttachment.ts b/packages/apps-engine/src/definition/messages/IMessageAttachment.ts new file mode 100644 index 000000000000..96dd8aa1fe34 --- /dev/null +++ b/packages/apps-engine/src/definition/messages/IMessageAttachment.ts @@ -0,0 +1,43 @@ +import type { IMessageAction } from './IMessageAction'; +import type { IMessageAttachmentAuthor } from './IMessageAttachmentAuthor'; +import type { IMessageAttachmentField } from './IMessageAttachmentField'; +import type { IMessageAttachmentTitle } from './IMessageAttachmentTitle'; +import type { MessageActionButtonsAlignment } from './MessageActionButtonsAlignment'; + +/** + * Interface which represents an attachment which can be added to a message. + */ +export interface IMessageAttachment { + /** Causes the image, audio, and video sections to be hidding when this is true. */ + collapsed?: boolean; + /** The color you want the order on the left side to be, supports any valid background-css value. */ + color?: string; // TODO: Maybe we change this to a Color class which has helper methods? + /** The text to display for this attachment. */ + text?: string; + /** Displays the time next to the text portion. */ + timestamp?: Date; + /** Only applicable if the timestamp is provided, as it makes the time clickable to this link. */ + timestampLink?: string; + /** An image that displays to the left of the text, looks better when this is relatively small. */ + thumbnailUrl?: string; + /** Author portion of the attachment. */ + author?: IMessageAttachmentAuthor; + /** Title portion of the attachment. */ + title?: IMessageAttachmentTitle; + /** The image to display, will be "big" and easy to see. */ + imageUrl?: string; + /** Audio file to play, only supports what html's