diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 534f23be0100..a3ae6a000b42 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,3 +9,4 @@ /_templates/ @RocketChat/chat-engine /apps/meteor/client/ @RocketChat/frontend /apps/meteor/tests/ @RocketChat/chat-engine +/apps/meteor/app/apps/ @RocketChat/apps diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index af571a2c861a..ecfb34c5b1e8 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -53,12 +53,6 @@ jobs: # run: | # npx package-lock-check - - name: Cache cypress - id: cache-cypress - uses: actions/cache@v2 - with: - path: /home/runner/.cache/Cypress - key: ${{ runner.OS }}-cache-cypress-${{ hashFiles('**/package-lock.json', '.github/workflows/build_and_test.yml') }} - uses: c-hive/gha-yarn-cache@v2 - name: Cache turbo id: cache-turbo @@ -118,7 +112,6 @@ jobs: git version - name: yarn install - # if: steps.cache-nodemodules.outputs.cache-hit != 'true' || steps.cache-cypress.outputs.cache-hit != 'true' run: yarn - run: yarn lint @@ -212,15 +205,6 @@ jobs: - uses: actions/checkout@v3 - - name: Cache cypress - id: cache-cypress - uses: actions/cache@v2 - with: - path: /home/runner/.cache/Cypress - key: ${{ runner.OS }}-cache-cypress-${{ hashFiles('**/package-lock.json', '.github/workflows/build_and_test.yml') }} - restore-keys: | - ${{ runner.os }}-cache-cypress- - ${{ runner.os }}- - uses: c-hive/gha-yarn-cache@v2 - name: Cache turbo id: cache-turbo @@ -256,17 +240,6 @@ jobs: Xvfb -screen 0 1024x768x24 :99 & for i in $(seq 1 5); do (docker exec mongo mongo rocketchat --eval 'db.dropDatabase()') && npm run testci -- --test=testapi && s=0 && break || s=$? && sleep 1; done; (exit $s) - - name: E2E Test UI (Legacy - Cypress) - env: - TEST_MODE: 'true' - MONGO_URL: mongodb://localhost:27017/rocketchat - MONGO_OPLOG_URL: mongodb://localhost:27017/local - run: | - cd ./apps/meteor - echo -e 'pcm.!default {\n type hw\n card 0\n}\n\nctl.!default {\n type hw\n card 0\n}' > ~/.asoundrc - Xvfb -screen 0 1024x768x24 :99 & - for i in $(seq 1 2); do (docker exec mongo mongo rocketchat --eval 'db.dropDatabase()') && npm run testci -- --test=testui && s=0 && break || s=$? && ([ ! -w tests/cypress/screenshots ] || mv tests/cypress/screenshots tests/cypress/screenshots-$i) && ([ ! -w tests/cypress/videos ] || mv tests/cypress/videos tests/cypress/videos-$i) && sleep 1; done; (exit $s) - - name: E2E Test UI env: TEST_MODE: 'true' @@ -285,20 +258,6 @@ jobs: name: playwright-test-trace path: ./apps/meteor/tests/e2e/test-failures* - - name: Store cypress test screenshots - uses: actions/upload-artifact@v2 - if: failure() - with: - name: cypress-test-screenshots - path: ./apps/meteor/tests/cypress/screenshots* - - - name: Store cypress test videos - uses: actions/upload-artifact@v2 - if: failure() - with: - name: cypress-test-videos - path: ./apps/meteor/tests/cypress/videos* - test-ee: runs-on: ubuntu-20.04 needs: build @@ -345,12 +304,6 @@ jobs: - uses: actions/checkout@v3 - - name: Cache cypress - id: cache-cypress - uses: actions/cache@v2 - with: - path: /home/runner/.cache/Cypress - key: ${{ runner.OS }}-cache-cypress-${{ hashFiles('**/package-lock.json', '.github/workflows/build_and_test.yml') }} - uses: c-hive/gha-yarn-cache@v2 - name: Cache turbo id: cache-turbo @@ -364,7 +317,6 @@ jobs: ${{ runner.os }}- - name: Yarn install - # if: steps.cache-nodemodules.outputs.cache-hit != 'true' || steps.cache-cypress.outputs.cache-hit != 'true' run: yarn - name: Build micro services @@ -386,25 +338,6 @@ jobs: for i in $(seq 1 5); do (docker exec mongo mongo rocketchat --eval 'db.dropDatabase()') && npm run testci -- --enterprise --test=testapi && s=0 && break || s=$? && sleep 1; done; (exit $s) - - name: E2E Test UI (Legacy - Cypress) - env: - TEST_MODE: 'true' - MONGO_URL: mongodb://localhost:27017/rocketchat - MONGO_OPLOG_URL: mongodb://localhost:27017/local - ENTERPRISE_LICENSE: ${{ secrets.ENTERPRISE_LICENSE }} - TRANSPORTER: nats://localhost:4222 - CYPRESS_BASE_URL: http://localhost:4000 - CYPRESS_TEST_API_URL: http://localhost:4000 - OVERWRITE_SETTING_Site_Url: http://localhost:4000 - SKIP_PROCESS_EVENT_REGISTRATION: 'true' - run: | - echo -e 'pcm.!default {\n type hw\n card 0\n}\n\nctl.!default {\n type hw\n card 0\n}' > ~/.asoundrc - Xvfb -screen 0 1024x768x24 :99 & - - cd ./apps/meteor/ - - for i in $(seq 1 2); do (docker exec mongo mongo rocketchat --eval 'db.dropDatabase()') && npm run testci -- --enterprise --test=testui && s=0 && break || s=$? && ([ ! -w tests/cypress/screenshots ] || mv tests/cypress/screenshots tests/cypress/screenshots-$i) && ([ ! -w tests/cypress/videos ] || mv tests/cypress/videos tests/cypress/videos-$i) && sleep 1; done; (exit $s) - - name: Install Playwright run: | cd ./apps/meteor/ @@ -417,8 +350,7 @@ jobs: MONGO_OPLOG_URL: mongodb://localhost:27017/local ENTERPRISE_LICENSE: ${{ secrets.ENTERPRISE_LICENSE }} TRANSPORTER: nats://localhost:4222 - CYPRESS_BASE_URL: http://localhost:4000 - CYPRESS_TEST_API_URL: http://localhost:4000 + TEST_API_URL: http://localhost:4000 OVERWRITE_SETTING_Site_Url: http://localhost:4000 SKIP_PROCESS_EVENT_REGISTRATION: 'true' run: | @@ -436,19 +368,6 @@ jobs: name: ee-playwright-test-trace path: ./apps/meteor/tests/e2e/test-failures* - - name: Store cypress test screenshots - uses: actions/upload-artifact@v2 - if: failure() - with: - name: ee-cypress-test-screenshots - path: ./apps/meteor/tests/cypress/screenshots* - - - name: Store cypress test videos - uses: actions/upload-artifact@v2 - if: failure() - with: - name: ee-cypress-test-videos - path: ./apps/meteor/tests/cypress/videos* # notification: # runs-on: ubuntu-20.04 # needs: test diff --git a/.gitignore b/.gitignore index 816c05071f59..310be7ef939d 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,5 @@ yarn-error.log* !.yarn/sdks !.yarn/versions -.nvmrc \ No newline at end of file +.nvmrc +.idea/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 3d13987bf7ec..4bc6251d4020 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,5 @@ } ], "typescript.tsdk": "./node_modules/typescript/lib", - "cSpell.words": ["photoswipe"] + "cSpell.words": ["photoswipe", "tmid"] } diff --git a/.yarnrc.yml b/.yarnrc.yml index 8abafc3d6310..404765a22773 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -11,5 +11,3 @@ plugins: spec: '@yarnpkg/plugin-typescript' yarnPath: .yarn/releases/yarn-3.2.0.cjs - -checksumBehavior: 'update' diff --git a/apps/meteor/.gitignore b/apps/meteor/.gitignore index f99fb3d2ccd9..3fc755f6bb5b 100644 --- a/apps/meteor/.gitignore +++ b/apps/meteor/.gitignore @@ -78,8 +78,6 @@ tests/end-to-end/temporary_staged_test .screenshots /private/livechat /storybook-static -/tests/cypress/screenshots -/tests/cypress/videos /tests/e2e/test-failures coverage .nyc_output diff --git a/apps/meteor/app/api/server/api.d.ts b/apps/meteor/app/api/server/api.d.ts index fc9519b87ea5..98fcefe501e1 100644 --- a/apps/meteor/app/api/server/api.d.ts +++ b/apps/meteor/app/api/server/api.d.ts @@ -98,7 +98,7 @@ type ActionThis; - getLoggedInUser(): IUser | undefined; + getLoggedInUser(): TOptions extends { authRequired: true } ? IUser : IUser | undefined; getPaginationItems(): { readonly offset: number; readonly count: number; diff --git a/apps/meteor/app/api/server/lib/custom-sounds.js b/apps/meteor/app/api/server/lib/custom-sounds.js deleted file mode 100644 index 0579e99f38e7..000000000000 --- a/apps/meteor/app/api/server/lib/custom-sounds.js +++ /dev/null @@ -1,20 +0,0 @@ -import { CustomSounds } from '../../../models/server/raw'; - -export async function findCustomSounds({ query = {}, pagination: { offset, count, sort } }) { - const cursor = await CustomSounds.find(query, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - }); - - const total = await cursor.count(); - - const sounds = await cursor.toArray(); - - return { - sounds, - count: sounds.length, - offset, - total, - }; -} diff --git a/apps/meteor/app/api/server/lib/custom-user-status.js b/apps/meteor/app/api/server/lib/custom-user-status.js deleted file mode 100644 index 6108af3c6a72..000000000000 --- a/apps/meteor/app/api/server/lib/custom-user-status.js +++ /dev/null @@ -1,20 +0,0 @@ -import { CustomUserStatus } from '../../../models/server/raw'; - -export async function findCustomUserStatus({ query = {}, pagination: { offset, count, sort } }) { - const cursor = await CustomUserStatus.find(query, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - }); - - const total = await cursor.count(); - - const statuses = await cursor.toArray(); - - return { - statuses, - count: statuses.length, - offset, - total, - }; -} diff --git a/apps/meteor/app/api/server/v1/commands.js b/apps/meteor/app/api/server/v1/commands.ts similarity index 74% rename from apps/meteor/app/api/server/v1/commands.js rename to apps/meteor/app/api/server/v1/commands.ts index 9a5cde50ef00..75423636a1bf 100644 --- a/apps/meteor/app/api/server/v1/commands.js +++ b/apps/meteor/app/api/server/v1/commands.ts @@ -29,10 +29,22 @@ API.v1.addRoute( }, ); -// TODO: replace with something like client/lib/minimongo -const processQueryOptionsOnResult = (result, options = {}) => { +/* @deprecated */ +const processQueryOptionsOnResult = , F extends keyof T>( + result: T[], + options: { + fields?: { + [key in F]?: 1 | 0; + }; + sort?: { + [key: string]: 1 | -1; + }; + limit?: number; + skip?: number; + } = {}, +): Pick[] => { if (result === undefined || result === null) { - return undefined; + return []; } if (Array.isArray(result)) { @@ -74,66 +86,50 @@ const processQueryOptionsOnResult = (result, options = {}) => { } } - if (!options.fields) { - options.fields = {}; - } - - const fieldsToRemove = []; - const fieldsToGet = []; + const fieldsToRemove: F[] = []; + const fieldsToGet: F[] = []; - for (const field in options.fields) { - if (options.fields.hasOwnProperty(field)) { - if (options.fields[field] === 0) { - fieldsToRemove.push(field); - } else if (options.fields[field] === 1) { - fieldsToGet.push(field); + if (options.fields) { + for (const field in Object.keys(options.fields)) { + if (options.fields.hasOwnProperty(field as F)) { + if (options.fields[field as F] === 0) { + fieldsToRemove.push(field as F); + } else if (options.fields[field as F] === 1) { + fieldsToGet.push(field as F); + } } } } - if (fieldsToRemove.length > 0 && fieldsToGet.length > 0) { - console.warn("Can't mix remove and get fields"); - fieldsToRemove.splice(0, fieldsToRemove.length); - } - - if (fieldsToGet.length > 0 && fieldsToGet.indexOf('_id') === -1) { - fieldsToGet.push('_id'); + if (fieldsToGet.length > 0 && fieldsToGet.indexOf('_id' as F) === -1) { + fieldsToGet.push('_id' as F); } - const pickFields = (obj, fields) => { - const picked = {}; - fields.forEach((field) => { - if (field.indexOf('.') !== -1) { - objectPath.set(picked, field, objectPath.get(obj, field)); + const pickFields = (obj: T, fields: F[]): Pick => { + const picked: Partial = {}; + fields.forEach((field: F) => { + if (String(field).indexOf('.') !== -1) { + objectPath.set(picked, String(field), objectPath.get(obj, String(field))); } else { picked[field] = obj[field]; } }); - return picked; + return picked as Pick; }; - if (fieldsToRemove.length > 0 || fieldsToGet.length > 0) { - if (Array.isArray(result)) { - result = result.map((record) => { - if (fieldsToRemove.length > 0) { - return Object.fromEntries(Object.entries(record).filter(([key]) => !fieldsToRemove.includes(key))); - } - - if (fieldsToGet.length > 0) { - return pickFields(record, fieldsToGet); - } + if (fieldsToRemove.length > 0 && fieldsToGet.length > 0) { + console.warn("Can't mix remove and get fields"); + fieldsToRemove.splice(0, fieldsToRemove.length); + } - return null; - }); - } else { + if (fieldsToRemove.length > 0 || fieldsToGet.length > 0) { + return result.map((record) => { if (fieldsToRemove.length > 0) { - return Object.fromEntries(Object.entries(result).filter(([key]) => !fieldsToRemove.includes(key))); + return Object.fromEntries(Object.entries(record).filter(([key]) => !fieldsToRemove.includes(key as F))) as Pick; } - if (fieldsToGet.length > 0) { - return pickFields(result, fieldsToGet); - } - } + return pickFields(record, fieldsToGet); + }); } return result; @@ -149,20 +145,23 @@ API.v1.addRoute( let commands = Object.values(slashCommands.commands); - if (query && query.command) { + if (query?.command) { commands = commands.filter((command) => command.command === query.command); } const totalCount = commands.length; - commands = processQueryOptionsOnResult(commands, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - fields, - }); + + if (fields) { + console.warn('commands.list -> fields is deprecated and will be removed in 5.0.0'); + } return API.v1.success({ - commands, + commands: processQueryOptionsOnResult(commands, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + fields, + }), offset, count: commands.length, total: totalCount, @@ -178,7 +177,6 @@ API.v1.addRoute( { post() { const body = this.bodyParams; - const user = this.getLoggedInUser(); if (typeof body.command !== 'string') { return API.v1.failure('You must provide a command to run.'); @@ -201,28 +199,28 @@ API.v1.addRoute( return API.v1.failure('The command provided does not exist (or is disabled).'); } - if (!canAccessRoomId(body.roomId, user._id)) { + if (!canAccessRoomId(body.roomId, this.userId)) { return API.v1.unauthorized(); } const params = body.params ? body.params : ''; - const message = { - _id: Random.id(), - rid: body.roomId, - msg: `/${cmd} ${params}`, - }; - - if (body.tmid) { + if (typeof body.tmid === 'string') { const thread = Messages.findOneById(body.tmid); if (!thread || thread.rid !== body.roomId) { return API.v1.failure('Invalid thread.'); } - message.tmid = body.tmid; } + const message = { + _id: Random.id(), + rid: body.roomId, + msg: `/${cmd} ${params}`, + ...(body.tmid && { tmid: body.tmid }), + }; + const { triggerId } = body; - const result = Meteor.runAsUser(user._id, () => slashCommands.run(cmd, params, message, triggerId)); + const result = slashCommands.run(cmd, params, message, triggerId); return API.v1.success({ result }); }, @@ -234,7 +232,7 @@ API.v1.addRoute( { authRequired: true }, { // Expects these query params: command: 'giphy', params: 'mine', roomId: 'value' - get() { + async get() { const query = this.queryParams; const user = this.getLoggedInUser(); @@ -261,21 +259,18 @@ API.v1.addRoute( const params = query.params ? query.params : ''; - let preview; - Meteor.runAsUser(user._id, () => { - preview = Meteor.call('getSlashCommandPreviews', { - cmd, - params, - msg: { rid: query.roomId }, - }); + const preview = Meteor.call('getSlashCommandPreviews', { + cmd, + params, + msg: { rid: query.roomId }, }); return API.v1.success({ preview }); }, + // Expects a body format of: { command: 'giphy', params: 'mine', roomId: 'value', tmid: 'value', triggerId: 'value', previewItem: { id: 'sadf8' type: 'image', value: 'https://dev.null/gif' } } post() { const body = this.bodyParams; - const user = this.getLoggedInUser(); if (typeof body.command !== 'string') { return API.v1.failure('You must provide a command to run the preview item on.'); @@ -310,35 +305,33 @@ API.v1.addRoute( return API.v1.failure('The command provided does not exist (or is disabled).'); } - if (!canAccessRoomId(body.roomId, user._id)) { + if (!canAccessRoomId(body.roomId, this.userId)) { return API.v1.unauthorized(); } - const params = body.params ? body.params : ''; - const message = { - rid: body.roomId, - }; - + const { params = '' } = body; if (body.tmid) { const thread = Messages.findOneById(body.tmid); if (!thread || thread.rid !== body.roomId) { return API.v1.failure('Invalid thread.'); } - message.tmid = body.tmid; } - Meteor.runAsUser(user._id, () => { - Meteor.call( - 'executeSlashCommandPreview', - { - cmd, - params, - msg: { rid: body.roomId, tmid: body.tmid }, - }, - body.previewItem, - body.triggerId, - ); - }); + const msg = { + rid: body.roomId, + ...(body.tmid && { tmid: body.tmid }), + }; + + Meteor.call( + 'executeSlashCommandPreview', + { + cmd, + params, + msg, + }, + body.previewItem, + body.triggerId, + ); return API.v1.success(); }, diff --git a/apps/meteor/app/api/server/v1/custom-sounds.js b/apps/meteor/app/api/server/v1/custom-sounds.js deleted file mode 100644 index 57c5ac9a0c7f..000000000000 --- a/apps/meteor/app/api/server/v1/custom-sounds.js +++ /dev/null @@ -1,26 +0,0 @@ -import { API } from '../api'; -import { findCustomSounds } from '../lib/custom-sounds'; - -API.v1.addRoute( - 'custom-sounds.list', - { authRequired: true }, - { - get() { - const { offset, count } = this.getPaginationItems(); - const { sort, query } = this.parseJsonQuery(); - - return API.v1.success( - Promise.await( - findCustomSounds({ - query, - pagination: { - offset, - count, - sort, - }, - }), - ), - ); - }, - }, -); diff --git a/apps/meteor/app/api/server/v1/custom-sounds.ts b/apps/meteor/app/api/server/v1/custom-sounds.ts new file mode 100644 index 000000000000..f6294c8a5ec1 --- /dev/null +++ b/apps/meteor/app/api/server/v1/custom-sounds.ts @@ -0,0 +1,29 @@ +import { CustomSounds } from '../../../models/server/raw'; +import { API } from '../api'; + +API.v1.addRoute( + 'custom-sounds.list', + { authRequired: true }, + { + async get() { + const { offset, count } = this.getPaginationItems(); + const { sort, query } = this.parseJsonQuery(); + const cursor = await CustomSounds.find(query, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + }); + + const total = await cursor.count(); + + const sounds = await cursor.toArray(); + + return API.v1.success({ + sounds, + count: sounds.length, + offset, + total, + }); + }, + }, +); diff --git a/apps/meteor/app/api/server/v1/custom-user-status.js b/apps/meteor/app/api/server/v1/custom-user-status.ts similarity index 55% rename from apps/meteor/app/api/server/v1/custom-user-status.js rename to apps/meteor/app/api/server/v1/custom-user-status.ts index b53583a4dc0b..e86df11a7380 100644 --- a/apps/meteor/app/api/server/v1/custom-user-status.js +++ b/apps/meteor/app/api/server/v1/custom-user-status.ts @@ -1,30 +1,33 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { CustomUserStatus } from '../../../models'; +import { CustomUserStatus } from '../../../models/server/raw'; import { API } from '../api'; -import { findCustomUserStatus } from '../lib/custom-user-status'; API.v1.addRoute( 'custom-user-status.list', { authRequired: true }, { - get() { + async get() { const { offset, count } = this.getPaginationItems(); const { sort, query } = this.parseJsonQuery(); - return API.v1.success( - Promise.await( - findCustomUserStatus({ - query, - pagination: { - offset, - count, - sort, - }, - }), - ), - ); + const cursor = await CustomUserStatus.find(query, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + }); + + const total = await cursor.count(); + + const statuses = await cursor.toArray(); + + return API.v1.success({ + statuses, + count: statuses.length, + offset, + total, + }); }, }, ); @@ -33,7 +36,7 @@ API.v1.addRoute( 'custom-user-status.create', { authRequired: true }, { - post() { + async post() { check(this.bodyParams, { name: String, statusType: Match.Maybe(String), @@ -44,12 +47,15 @@ API.v1.addRoute( statusType: this.bodyParams.statusType, }; - Meteor.runAsUser(this.userId, () => { - Meteor.call('insertOrUpdateUserStatus', userStatusData); - }); + Meteor.call('insertOrUpdateUserStatus', userStatusData); + + const customUserStatus = await CustomUserStatus.findOneByName(userStatusData.name); + if (!customUserStatus) { + throw new Meteor.Error('error-creating-custom-user-status', 'Error creating custom user status'); + } return API.v1.success({ - customUserStatus: CustomUserStatus.findOneByName(userStatusData.name), + customUserStatus, }); }, }, @@ -65,7 +71,7 @@ API.v1.addRoute( return API.v1.failure('The "customUserStatusId" params is required!'); } - Meteor.runAsUser(this.userId, () => Meteor.call('deleteCustomUserStatus', customUserStatusId)); + Meteor.call('deleteCustomUserStatus', customUserStatusId); return API.v1.success(); }, @@ -76,7 +82,7 @@ API.v1.addRoute( 'custom-user-status.update', { authRequired: true }, { - post() { + async post() { check(this.bodyParams, { _id: String, name: String, @@ -89,19 +95,23 @@ API.v1.addRoute( statusType: this.bodyParams.statusType, }; - const customUserStatus = CustomUserStatus.findOneById(userStatusData._id); + const customUserStatusToUpdate = await CustomUserStatus.findOneById(userStatusData._id); // Ensure the message exists - if (!customUserStatus) { + if (!customUserStatusToUpdate) { return API.v1.failure(`No custom user status found with the id of "${userStatusData._id}".`); } - Meteor.runAsUser(this.userId, () => { - Meteor.call('insertOrUpdateUserStatus', userStatusData); - }); + Meteor.call('insertOrUpdateUserStatus', userStatusData); + + const customUserStatus = await CustomUserStatus.findOneById(userStatusData._id); + + if (!customUserStatus) { + throw new Meteor.Error('error-updating-custom-user-status', 'Error updating custom user status'); + } return API.v1.success({ - customUserStatus: CustomUserStatus.findOneById(userStatusData._id), + customUserStatus, }); }, }, diff --git a/apps/meteor/app/api/server/v1/im.ts b/apps/meteor/app/api/server/v1/im.ts index 76542f1e5582..0236d350b212 100644 --- a/apps/meteor/app/api/server/v1/im.ts +++ b/apps/meteor/app/api/server/v1/im.ts @@ -2,7 +2,14 @@ * Docs: https://github.com/RocketChat/developer-docs/blob/master/reference/api/rest-api/endpoints/team-collaboration-endpoints/im-endpoints */ import type { IMessage, IRoom, ISetting, ISubscription, IUpload, IUser } from '@rocket.chat/core-typings'; -import { isDmDeleteProps, isDmFileProps, isDmMemberProps, isDmMessagesProps, isDmCreateProps } from '@rocket.chat/rest-typings'; +import { + isDmDeleteProps, + isDmFileProps, + isDmMemberProps, + isDmMessagesProps, + isDmCreateProps, + isDmHistoryProps, +} from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; @@ -240,7 +247,7 @@ API.v1.addRoute( API.v1.addRoute( ['dm.history', 'im.history'], - { authRequired: true }, + { authRequired: true, validateParams: isDmHistoryProps }, { async get() { const { offset = 0, count = 20 } = this.getPaginationItems(); @@ -351,7 +358,7 @@ API.v1.addRoute( sort: sortObj, skip: offset, limit: count, - ...(fields && { fields }), + ...(fields && { projection: fields }), }).toArray(); return API.v1.success({ @@ -403,7 +410,7 @@ API.v1.addRoute( sort: sort || { ts: -1 }, skip: offset, limit: count, - fields, + projection: fields, }).toArray(); if (!msgs) { diff --git a/apps/meteor/app/apps/server/bridges/commands.ts b/apps/meteor/app/apps/server/bridges/commands.ts index 8a6726e3925a..9f6448794e4d 100644 --- a/apps/meteor/app/apps/server/bridges/commands.ts +++ b/apps/meteor/app/apps/server/bridges/commands.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { SlashCommandContext, ISlashCommand, ISlashCommandPreviewItem } from '@rocket.chat/apps-engine/definition/slashcommands'; import { CommandBridge } from '@rocket.chat/apps-engine/server/bridges/CommandBridge'; -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, RequiredField, SlashCommand } from '@rocket.chat/core-typings'; import { slashCommands } from '../../../utils/server'; import { Utilities } from '../../lib/misc/Utilities'; @@ -114,7 +114,7 @@ export class AppCommandsBridge extends CommandBridge { previewCallback: (!command.executePreviewItem ? undefined : this._appCommandPreviewExecutor.bind(this)) as | typeof slashCommands.commands[string]['previewCallback'] | undefined, - }; + } as SlashCommand; slashCommands.commands[command.command.toLowerCase()] = item; this.orch.getNotifier().commandAdded(command.command.toLowerCase()); @@ -160,7 +160,12 @@ export class AppCommandsBridge extends CommandBridge { } } - private _appCommandExecutor(command: string, parameters: any, message: IMessage, triggerId: string): void { + private _appCommandExecutor( + command: string, + parameters: any, + message: RequiredField, 'rid'>, + triggerId?: string, + ): void { const user = this.orch.getConverters()?.get('users').convertById(Meteor.userId()); const room = this.orch.getConverters()?.get('rooms').convertById(message.rid); const threadId = message.tmid; @@ -195,6 +200,6 @@ export class AppCommandsBridge extends CommandBridge { const context = new SlashCommandContext(Object.freeze(user), Object.freeze(room), Object.freeze(params), threadId, triggerId); - Promise.await(this.orch.getManager()?.getCommandManager().executePreview(command, preview, context)); + await this.orch.getManager()?.getCommandManager().executePreview(command, preview, context); } } diff --git a/apps/meteor/app/emoji/client/emojiPicker.js b/apps/meteor/app/emoji/client/emojiPicker.js index 393124a3fbef..5995ed8bd746 100644 --- a/apps/meteor/app/emoji/client/emojiPicker.js +++ b/apps/meteor/app/emoji/client/emojiPicker.js @@ -5,7 +5,6 @@ import { FlowRouter } from 'meteor/kadira:flow-router'; import { Template } from 'meteor/templating'; import { escapeRegExp } from '@rocket.chat/string-helpers'; -import '../../theme/client/imports/components/emojiPicker.css'; import { t } from '../../utils/client'; import { EmojiPicker } from './lib/EmojiPicker'; import { emoji } from '../lib/rocketchat'; diff --git a/apps/meteor/app/federation-v2/server/application/RoomServiceReceiver.ts b/apps/meteor/app/federation-v2/server/application/RoomServiceReceiver.ts new file mode 100644 index 000000000000..49898c6ec2a7 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/RoomServiceReceiver.ts @@ -0,0 +1,217 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { FederatedRoom } from '../domain/FederatedRoom'; +import { FederatedUser } from '../domain/FederatedUser'; +import { EVENT_ORIGIN, IFederationBridge } from '../domain/IFederationBridge'; +import { RocketChatMessageAdapter } from '../infrastructure/rocket-chat/adapters/Message'; +import { RocketChatRoomAdapter } from '../infrastructure/rocket-chat/adapters/Room'; +import { RocketChatSettingsAdapter } from '../infrastructure/rocket-chat/adapters/Settings'; +import { RocketChatUserAdapter } from '../infrastructure/rocket-chat/adapters/User'; +import { + FederationRoomCreateInputDto, + FederationRoomChangeMembershipDto, + FederationRoomSendInternalMessageDto, + FederationRoomChangeJoinRulesDto, + FederationRoomChangeNameDto, + FederationRoomChangeTopicDto, +} from './input/RoomReceiverDto'; + +export class FederationRoomServiceReceiver { + constructor( + private rocketRoomAdapter: RocketChatRoomAdapter, + private rocketUserAdapter: RocketChatUserAdapter, + private rocketMessageAdapter: RocketChatMessageAdapter, + private rocketSettingsAdapter: RocketChatSettingsAdapter, + private bridge: IFederationBridge, + ) {} // eslint-disable-line no-empty-function + + public async createRoom(roomCreateInput: FederationRoomCreateInputDto): Promise { + const { + externalRoomId, + externalInviterId, + normalizedInviterId, + externalRoomName, + normalizedRoomId, + roomType, + wasInternallyProgramaticallyCreated = false, + } = roomCreateInput; + + if ((await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId)) || wasInternallyProgramaticallyCreated) { + return; + } + + if (!(await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviterId))) { + const externalUserProfileInformation = await this.bridge.getUserProfileInformation(externalInviterId); + const name = externalUserProfileInformation?.displayname || normalizedInviterId; + const federatedCreatorUser = FederatedUser.createInstance(externalInviterId, { + name, + username: normalizedInviterId, + existsOnlyOnProxyServer: false, + }); + + await this.rocketUserAdapter.createFederatedUser(federatedCreatorUser); + } + const creator = await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviterId); + const newFederatedRoom = FederatedRoom.createInstance( + externalRoomId, + normalizedRoomId, + creator as FederatedUser, + roomType || RoomType.CHANNEL, + externalRoomName, + ); + await this.rocketRoomAdapter.createFederatedRoom(newFederatedRoom); + } + + public async changeRoomMembership(roomChangeMembershipInput: FederationRoomChangeMembershipDto): Promise { + const { + externalRoomId, + normalizedInviteeId, + normalizedRoomId, + normalizedInviterId, + externalRoomName, + externalInviteeId, + externalInviterId, + inviteeUsernameOnly, + inviterUsernameOnly, + eventOrigin, + roomType, + leave, + } = roomChangeMembershipInput; + const affectedFederatedRoom = await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!affectedFederatedRoom && eventOrigin === EVENT_ORIGIN.LOCAL) { + throw new Error(`Could not find room with external room id: ${externalRoomId}`); + } + const isInviterFromTheSameHomeServer = await this.bridge.isUserIdFromTheSameHomeserver( + externalInviterId, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + const isInviteeFromTheSameHomeServer = await this.bridge.isUserIdFromTheSameHomeserver( + externalInviteeId, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + + if (!(await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviterId))) { + const externalUserProfileInformation = await this.bridge.getUserProfileInformation(externalInviterId); + const name = externalUserProfileInformation.displayname || normalizedInviterId; + const username = isInviterFromTheSameHomeServer ? inviterUsernameOnly : normalizedInviterId; + const federatedInviterUser = FederatedUser.createInstance(externalInviterId, { + name, + username, + existsOnlyOnProxyServer: isInviterFromTheSameHomeServer, + }); + + await this.rocketUserAdapter.createFederatedUser(federatedInviterUser); + } + + if (!(await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviteeId))) { + const externalUserProfileInformation = await this.bridge.getUserProfileInformation(externalInviteeId); + const name = externalUserProfileInformation.displayname || normalizedInviteeId; + const username = isInviteeFromTheSameHomeServer ? inviteeUsernameOnly : normalizedInviteeId; + const federatedInviteeUser = FederatedUser.createInstance(externalInviteeId, { + name, + username, + existsOnlyOnProxyServer: isInviteeFromTheSameHomeServer, + }); + + await this.rocketUserAdapter.createFederatedUser(federatedInviteeUser); + } + + const federatedInviteeUser = await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviteeId); + const federatedInviterUser = await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviterId); + + if (!affectedFederatedRoom && eventOrigin === EVENT_ORIGIN.REMOTE) { + const members = [federatedInviterUser, federatedInviteeUser] as any[]; + const newFederatedRoom = FederatedRoom.createInstance( + externalRoomId, + normalizedRoomId, + federatedInviterUser as FederatedUser, + roomType, + externalRoomName, + members, + ); + + await this.rocketRoomAdapter.createFederatedRoom(newFederatedRoom); + await this.bridge.joinRoom(externalRoomId, externalInviteeId); + } + const federatedRoom = affectedFederatedRoom || (await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId)); + + if (leave) { + return this.rocketRoomAdapter.removeUserFromRoom( + federatedRoom as FederatedRoom, + federatedInviteeUser as FederatedUser, + federatedInviterUser as FederatedUser, + ); + } + await this.rocketRoomAdapter.addUserToRoom( + federatedRoom as FederatedRoom, + federatedInviteeUser as FederatedUser, + federatedInviterUser as FederatedUser, + ); + } + + public async receiveExternalMessage(roomSendInternalMessageInput: FederationRoomSendInternalMessageDto): Promise { + const { externalRoomId, externalSenderId, text } = roomSendInternalMessageInput; + + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!federatedRoom) { + return; + } + + const senderUser = await this.rocketUserAdapter.getFederatedUserByExternalId(externalSenderId); + if (!senderUser) { + return; + } + + await this.rocketMessageAdapter.sendMessage(senderUser, text, federatedRoom); + } + + public async changeJoinRules(roomJoinRulesChangeInput: FederationRoomChangeJoinRulesDto): Promise { + const { externalRoomId, roomType } = roomJoinRulesChangeInput; + + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!federatedRoom) { + return; + } + + if (federatedRoom.isDirectMessage()) { + return; + } + + federatedRoom.setRoomType(roomType); + await this.rocketRoomAdapter.updateRoomType(federatedRoom); + } + + public async changeRoomName(roomChangeNameInput: FederationRoomChangeNameDto): Promise { + const { externalRoomId, normalizedRoomName } = roomChangeNameInput; + + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!federatedRoom) { + return; + } + + if (federatedRoom.isDirectMessage()) { + return; + } + + federatedRoom.changeRoomName(normalizedRoomName); + + await this.rocketRoomAdapter.updateRoomName(federatedRoom); + } + + public async changeRoomTopic(roomChangeTopicInput: FederationRoomChangeTopicDto): Promise { + const { externalRoomId, roomTopic } = roomChangeTopicInput; + + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!federatedRoom) { + return; + } + + if (federatedRoom.isDirectMessage()) { + return; + } + + federatedRoom.changeRoomTopic(roomTopic); + + await this.rocketRoomAdapter.updateRoomTopic(federatedRoom); + } +} diff --git a/apps/meteor/app/federation-v2/server/application/RoomServiceSender.ts b/apps/meteor/app/federation-v2/server/application/RoomServiceSender.ts new file mode 100644 index 000000000000..2be2283ddd86 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/RoomServiceSender.ts @@ -0,0 +1,128 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; + +import { FederatedRoom } from '../domain/FederatedRoom'; +import { FederatedUser } from '../domain/FederatedUser'; +import { IFederationBridge } from '../domain/IFederationBridge'; +import { RocketChatNotificationAdapter } from '../infrastructure/rocket-chat/adapters/Notification'; +import { RocketChatRoomAdapter } from '../infrastructure/rocket-chat/adapters/Room'; +import { RocketChatSettingsAdapter } from '../infrastructure/rocket-chat/adapters/Settings'; +import { RocketChatUserAdapter } from '../infrastructure/rocket-chat/adapters/User'; +import { FederationRoomInviteUserDto, FederationRoomSendExternalMessageDto } from './input/RoomSenderDto'; + +export class FederationRoomServiceSender { + constructor( + private rocketRoomAdapter: RocketChatRoomAdapter, + private rocketUserAdapter: RocketChatUserAdapter, + private rocketSettingsAdapter: RocketChatSettingsAdapter, + private rocketNotificationAdapter: RocketChatNotificationAdapter, + private bridge: IFederationBridge, + ) {} // eslint-disable-line no-empty-function + + public async inviteUserToAFederatedRoom(roomInviteUserInput: FederationRoomInviteUserDto): Promise { + const { normalizedInviteeId, rawInviteeId, internalInviterId, inviteeUsernameOnly, internalRoomId } = roomInviteUserInput; + + if (!(await this.rocketUserAdapter.getFederatedUserByInternalId(internalInviterId))) { + const internalUser = (await this.rocketUserAdapter.getInternalUserById(internalInviterId)) as IUser; + const externalInviterId = await this.bridge.createUser( + internalUser.username as string, + internalUser.name as string, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + const federatedInviterUser = FederatedUser.createInstance(externalInviterId, { + name: internalUser.name as string, + username: internalUser.username as string, + existsOnlyOnProxyServer: true, + }); + await this.rocketUserAdapter.createFederatedUser(federatedInviterUser); + } + + if (!(await this.rocketUserAdapter.getFederatedUserByInternalUsername(normalizedInviteeId))) { + const externalUserProfileInformation = await this.bridge.getUserProfileInformation(rawInviteeId); + const name = externalUserProfileInformation?.displayname || normalizedInviteeId; + const federatedInviteeUser = FederatedUser.createInstance(rawInviteeId, { + name, + username: normalizedInviteeId, + existsOnlyOnProxyServer: false, + }); + + await this.rocketUserAdapter.createFederatedUser(federatedInviteeUser); + } + + const federatedInviterUser = (await this.rocketUserAdapter.getFederatedUserByInternalId(internalInviterId)) as FederatedUser; + const federatedInviteeUser = (await this.rocketUserAdapter.getFederatedUserByInternalUsername(normalizedInviteeId)) as FederatedUser; + const isInviteeFromTheSameHomeServer = await this.bridge.isUserIdFromTheSameHomeserver( + rawInviteeId, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + + if (!(await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId))) { + const internalRoom = (await this.rocketRoomAdapter.getInternalRoomById(internalRoomId)) as IRoom; + const roomName = (internalRoom.fname || internalRoom.name) as string; + const externalRoomId = await this.bridge.createRoom( + federatedInviterUser.externalId, + federatedInviteeUser.externalId, + internalRoom.t as RoomType, + roomName, + internalRoom.topic, + ); + const newFederatedRoom = FederatedRoom.createInstance( + externalRoomId, + externalRoomId, + federatedInviterUser, + internalRoom.t as RoomType, + roomName, + ); + await this.rocketRoomAdapter.updateFederatedRoomByInternalRoomId(internalRoom._id, newFederatedRoom); + } + + const federatedRoom = (await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId)) as FederatedRoom; + const wasInvitedWhenTheRoomWasCreated = federatedRoom.isDirectMessage(); + if (isInviteeFromTheSameHomeServer) { + await this.bridge.createUser( + inviteeUsernameOnly, + federatedInviteeUser.internalReference.name as string, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + await this.bridge.inviteToRoom(federatedRoom.externalId, federatedInviterUser.externalId, federatedInviteeUser.externalId); + await this.bridge.joinRoom(federatedRoom.externalId, federatedInviteeUser.externalId); + } else if (!wasInvitedWhenTheRoomWasCreated) { + this.bridge.inviteToRoom(federatedRoom.externalId, federatedInviterUser.externalId, federatedInviteeUser.externalId).catch(() => { + this.rocketNotificationAdapter.notifyWithEphemeralMessage( + 'Federation_Matrix_only_owners_can_invite_users', + federatedInviterUser?.internalReference?._id, + internalRoomId, + federatedInviterUser?.internalReference?.language, + ); + }); + } + await this.rocketRoomAdapter.addUserToRoom(federatedRoom, federatedInviteeUser, federatedInviterUser); + } + + public async sendMessageFromRocketChat(roomSendExternalMessageInput: FederationRoomSendExternalMessageDto): Promise { + const { internalRoomId, internalSenderId, message } = roomSendExternalMessageInput; + + const federatedSender = await this.rocketUserAdapter.getFederatedUserByInternalId(internalSenderId); + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId); + + if (!federatedSender) { + throw new Error(`Could not find user id for ${internalSenderId}`); + } + if (!federatedRoom) { + throw new Error(`Could not find room id for ${internalRoomId}`); + } + + await this.bridge.sendMessage(federatedRoom.externalId, federatedSender.externalId, message.msg); + + return message; + } + + public async isAFederatedRoom(internalRoomId: string): Promise { + if (!internalRoomId) { + return false; + } + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId); + + return Boolean(federatedRoom?.isFederated()); + } +} diff --git a/apps/meteor/app/federation-v2/server/application/input/RoomReceiverDto.ts b/apps/meteor/app/federation-v2/server/application/input/RoomReceiverDto.ts new file mode 100644 index 000000000000..ed85b194d67d --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/input/RoomReceiverDto.ts @@ -0,0 +1,63 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { EVENT_ORIGIN } from '../../domain/IFederationBridge'; + +class BaseRoom { + externalRoomId: string; + + normalizedRoomId: string; +} + +export class FederationRoomCreateInputDto extends BaseRoom { + externalInviterId: string; + + normalizedInviterId: string; + + wasInternallyProgramaticallyCreated?: boolean; + + externalRoomName?: string; + + roomType?: RoomType; +} + +export class FederationRoomChangeMembershipDto extends BaseRoom { + externalInviterId: string; + + normalizedInviterId: string; + + inviterUsernameOnly: string; + + externalInviteeId: string; + + normalizedInviteeId: string; + + inviteeUsernameOnly: string; + + roomType: RoomType; + + eventOrigin: EVENT_ORIGIN; + + leave?: boolean; + + externalRoomName?: string; +} + +export class FederationRoomSendInternalMessageDto extends BaseRoom { + externalSenderId: string; + + normalizedSenderId: string; + + text: string; +} + +export class FederationRoomChangeJoinRulesDto extends BaseRoom { + roomType: RoomType; +} + +export class FederationRoomChangeNameDto extends BaseRoom { + normalizedRoomName: string; +} + +export class FederationRoomChangeTopicDto extends BaseRoom { + roomTopic: string; +} diff --git a/apps/meteor/app/federation-v2/server/application/input/RoomSenderDto.ts b/apps/meteor/app/federation-v2/server/application/input/RoomSenderDto.ts new file mode 100644 index 000000000000..700216105866 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/input/RoomSenderDto.ts @@ -0,0 +1,21 @@ +import { IMessage } from '@rocket.chat/core-typings'; + +export class FederationRoomInviteUserDto { + internalInviterId: string; + + internalRoomId: string; + + rawInviteeId: string; + + normalizedInviteeId: string; + + inviteeUsernameOnly: string; +} + +export class FederationRoomSendExternalMessageDto { + internalRoomId: string; + + internalSenderId: string; + + message: IMessage; +} diff --git a/apps/meteor/app/federation-v2/server/bridge.ts b/apps/meteor/app/federation-v2/server/bridge.ts deleted file mode 100644 index 895e472de021..000000000000 --- a/apps/meteor/app/federation-v2/server/bridge.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { Bridge as MatrixBridge } from '@rocket.chat/forked-matrix-appservice-bridge'; - -import { settings } from '../../settings/server'; -import { Settings } from '../../models/server/raw'; -import type { IMatrixEvent } from './definitions/IMatrixEvent'; -import type { MatrixEventType } from './definitions/MatrixEventType'; -import { addToQueue } from './queue'; -import { getRegistrationInfo } from './config'; -import { bridgeLogger } from './logger'; - -class Bridge { - private bridgeInstance: MatrixBridge; - - private isRunning = false; - - public async start(): Promise { - try { - await this.stop(); - await this.createInstance(); - - if (!this.isRunning) { - await this.bridgeInstance.run(this.getBridgePort()); - this.isRunning = true; - } - } catch (e) { - bridgeLogger.error('Failed to initialize the matrix-appservice-bridge.', e); - - bridgeLogger.error('Disabling Matrix Bridge. Please resolve error and try again'); - Settings.updateValueById('Federation_Matrix_enabled', false); - } - } - - public async stop(): Promise { - if (!this.isRunning) { - return; - } - // the http server can take some minutes to shutdown and this promise to be resolved - await this.bridgeInstance?.close(); - this.isRunning = false; - } - - public async getRoomStateByRoomId(userId: string, roomId: string): Promise[]> { - return Array.from(((await this.getInstance().getIntent(userId).roomState(roomId)) as IMatrixEvent[]) || []); - } - - public getInstance(): MatrixBridge { - return this.bridgeInstance; - } - - private async createInstance(): Promise { - bridgeLogger.info('Performing Dynamic Import of matrix-appservice-bridge'); - - // Dynamic import to prevent Rocket.Chat from loading the module until needed and then handle if that fails - const { Bridge: MatrixBridge, AppServiceRegistration } = await import('@rocket.chat/forked-matrix-appservice-bridge'); - - this.bridgeInstance = new MatrixBridge({ - homeserverUrl: settings.get('Federation_Matrix_homeserver_url'), - domain: settings.get('Federation_Matrix_homeserver_domain'), - registration: AppServiceRegistration.fromObject(getRegistrationInfo()), - disableStores: true, - controller: { - onAliasQuery: (alias, matrixRoomId): void => { - console.log('onAliasQuery', alias, matrixRoomId); - }, - onEvent: async (request /* , context*/): Promise => { - // Get the event - const event = request.getData() as unknown as IMatrixEvent; - - addToQueue(event); - }, - onLog: async (line, isError): Promise => { - console.log(line, isError); - }, - }, - }); - } - - private getBridgePort(): number { - const [, , port] = settings.get('Federation_Matrix_bridge_url').split(':'); - - return parseInt(port); - } -} - -export const matrixBridge = new Bridge(); diff --git a/apps/meteor/app/federation-v2/server/config.ts b/apps/meteor/app/federation-v2/server/config.ts deleted file mode 100644 index d1bad2455d80..000000000000 --- a/apps/meteor/app/federation-v2/server/config.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { AppServiceOutput } from '@rocket.chat/forked-matrix-appservice-bridge'; - -import { settings } from '../../settings/server'; - -export type bridgeUrlTuple = [string, string, number]; - -export function getRegistrationInfo(): AppServiceOutput { - /* eslint-disable @typescript-eslint/camelcase */ - return { - id: settings.get('Federation_Matrix_id'), - hs_token: settings.get('Federation_Matrix_hs_token'), - as_token: settings.get('Federation_Matrix_as_token'), - url: settings.get('Federation_Matrix_bridge_url'), - sender_localpart: settings.get('Federation_Matrix_bridge_localpart'), - namespaces: { - users: [ - { - exclusive: false, - // Reserve these MXID's (usernames) - regex: `.*`, - }, - ], - aliases: [ - { - exclusive: false, - // Reserve these room aliases - regex: `.*`, - }, - ], - rooms: [ - { - exclusive: false, - // This regex is used to define which rooms we listen to with the bridge. - // This does not reserve the rooms like the other namespaces. - regex: '.*', - }, - ], - }, - rate_limited: false, - protocols: null, - }; - /* eslint-enable @typescript-eslint/camelcase */ -} diff --git a/apps/meteor/app/federation-v2/server/data-interface/index.ts b/apps/meteor/app/federation-v2/server/data-interface/index.ts deleted file mode 100644 index 18e2fbf7020f..000000000000 --- a/apps/meteor/app/federation-v2/server/data-interface/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as message from './message'; -import * as room from './room'; -import * as user from './user'; - -export const dataInterface = { - message: message.normalize, - room: room.normalize, - user: user.normalize, -}; diff --git a/apps/meteor/app/federation-v2/server/data-interface/message.ts b/apps/meteor/app/federation-v2/server/data-interface/message.ts deleted file mode 100644 index 7d27732f93e6..000000000000 --- a/apps/meteor/app/federation-v2/server/data-interface/message.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { IMessage, IUser } from '@rocket.chat/core-typings'; - -import { dataInterface } from '.'; - -interface INormalizedMessage extends IMessage { - u: Required>; -} - -export const normalize = async (message: IMessage): Promise => { - // TODO: normalize the entire payload (if needed) - const normalizedMessage: INormalizedMessage = message as INormalizedMessage; - - // Normalize the user - normalizedMessage.u = (await dataInterface.user(message.u._id)) as Required>; - - return normalizedMessage; -}; diff --git a/apps/meteor/app/federation-v2/server/data-interface/room.ts b/apps/meteor/app/federation-v2/server/data-interface/room.ts deleted file mode 100644 index df1d2163badf..000000000000 --- a/apps/meteor/app/federation-v2/server/data-interface/room.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IRoom } from '@rocket.chat/core-typings'; - -import { Rooms } from '../../../models/server'; - -export const normalize = async (roomId: string): Promise => { - // Normalize the user - return Rooms.findOneById(roomId); -}; diff --git a/apps/meteor/app/federation-v2/server/data-interface/user.ts b/apps/meteor/app/federation-v2/server/data-interface/user.ts deleted file mode 100644 index 15fb48843428..000000000000 --- a/apps/meteor/app/federation-v2/server/data-interface/user.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IUser } from '@rocket.chat/core-typings'; - -import { Users } from '../../../models/server'; - -export const normalize = async (userId: string): Promise => { - // Normalize the user - return Users.findOneById(userId); -}; diff --git a/apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts b/apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts new file mode 100644 index 000000000000..c82b078c49e7 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts @@ -0,0 +1,78 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import { IRoom, IUser } from '@rocket.chat/core-typings'; + +import { FederatedUser } from './FederatedUser'; + +export class FederatedRoom { + public externalId: string; + + public members?: FederatedUser[]; + + public internalReference: IRoom; + + // eslint-disable-next-line + private constructor() {} + + private static generateTemporaryName(normalizedExternalId: string): string { + return `Federation-${normalizedExternalId}`; + } + + public static createInstance( + externalId: string, + normalizedExternalId: string, + creator: FederatedUser, + type: RoomType, + name?: string, + members?: IUser[], + ): FederatedRoom { + const roomName = name || FederatedRoom.generateTemporaryName(normalizedExternalId); + return Object.assign(new FederatedRoom(), { + externalId, + ...(type === RoomType.DIRECT_MESSAGE ? { members } : {}), + internalReference: { + t: type, + name: roomName, + fname: roomName, + u: creator.internalReference, + }, + }); + } + + public static build(): FederatedRoom { + return new FederatedRoom(); + } + + public isDirectMessage(): boolean { + return this.internalReference?.t === RoomType.DIRECT_MESSAGE; + } + + public setRoomType(type: RoomType): void { + if (this.isDirectMessage()) { + throw new Error('Its not possible to change a direct message type'); + } + this.internalReference.t = type; + } + + public changeRoomName(name: string): void { + if (this.isDirectMessage()) { + throw new Error('Its not possible to change a direct message name'); + } + this.internalReference.name = name; + this.internalReference.fname = name; + } + + public changeRoomTopic(topic: string): void { + if (this.isDirectMessage()) { + throw new Error('Its not possible to change a direct message topic'); + } + this.internalReference.description = topic; + } + + public getMembers(): IUser[] { + return this.isDirectMessage() && this.members && this.members.length > 0 ? this.members.map((user) => user.internalReference) : []; + } + + public isFederated(): boolean { + return this.internalReference?.federated === true; + } +} diff --git a/apps/meteor/app/federation-v2/server/domain/FederatedUser.ts b/apps/meteor/app/federation-v2/server/domain/FederatedUser.ts new file mode 100644 index 000000000000..225b0fabc003 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/domain/FederatedUser.ts @@ -0,0 +1,38 @@ +import { IUser } from '@rocket.chat/core-typings'; + +export interface IFederatedUserCreationParams { + name: string; + username: string; + existsOnlyOnProxyServer: boolean; +} + +export class FederatedUser { + public externalId: string; + + public internalReference: IUser; + + public existsOnlyOnProxyServer: boolean; + + // eslint-disable-next-line + private constructor() {} + + public static createInstance(externalId: string, params: IFederatedUserCreationParams): FederatedUser { + return Object.assign(new FederatedUser(), { + externalId, + existsOnlyOnProxyServer: params.existsOnlyOnProxyServer, + internalReference: { + username: params.username, + name: params.name, + type: 'user', + status: 'online', + active: true, + roles: ['user'], + requirePasswordChange: false, + }, + }); + } + + public static build(): FederatedUser { + return new FederatedUser(); + } +} diff --git a/apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts b/apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts new file mode 100644 index 000000000000..86310ec30f17 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts @@ -0,0 +1,25 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +export interface IFederationBridge { + start(): Promise; + stop(): Promise; + onFederationAvailabilityChanged(enabled: boolean): Promise; + getUserProfileInformation(externalUserId: string): Promise; + joinRoom(externalRoomId: string, externalUserId: string): Promise; + createRoom( + externalCreatorId: string, + externalInviteeId: string, + roomType: RoomType, + roomName: string, + roomTopic?: string, + ): Promise; + inviteToRoom(externalRoomId: string, externalInviterId: string, externalInviteeId: string): Promise; + sendMessage(externalRoomId: string, externaSenderId: string, text: string): Promise; + createUser(username: string, name: string, domain: string): Promise; + isUserIdFromTheSameHomeserver(externalUserId: string, domain: string): boolean; +} + +export enum EVENT_ORIGIN { + LOCAL = 'LOCAL', + REMOTE = 'REMOTE', +} diff --git a/apps/meteor/app/federation-v2/server/eventHandler.ts b/apps/meteor/app/federation-v2/server/eventHandler.ts deleted file mode 100644 index 166ed1199adc..000000000000 --- a/apps/meteor/app/federation-v2/server/eventHandler.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { IMatrixEvent } from './definitions/IMatrixEvent'; -import { MatrixEventType } from './definitions/MatrixEventType'; -import { handleRoomMembership, handleCreateRoom, handleSendMessage, setRoomJoinRules, setRoomName, setRoomTopic } from './events'; - -export const eventHandler = async (event: IMatrixEvent): Promise => { - console.log(`Processing ${event.type}...`, JSON.stringify(event, null, 2)); - - switch (event.type) { - case MatrixEventType.CREATE_ROOM: { - await handleCreateRoom(event as IMatrixEvent); - - break; - } - case MatrixEventType.ROOM_MEMBERSHIP: { - await handleRoomMembership(event as IMatrixEvent); - - break; - } - case MatrixEventType.SET_ROOM_JOIN_RULES: { - await setRoomJoinRules(event as IMatrixEvent); - - break; - } - case MatrixEventType.SET_ROOM_NAME: { - await setRoomName(event as IMatrixEvent); - - break; - } - case MatrixEventType.SET_ROOM_TOPIC: { - await setRoomTopic(event as IMatrixEvent); - - break; - } - case MatrixEventType.SEND_MESSAGE: { - await handleSendMessage(event as IMatrixEvent); - - break; - } - // case MatrixEventType.SET_ROOM_POWER_LEVELS: - // case MatrixEventType.SET_ROOM_CANONICAL_ALIAS: - // case MatrixEventType.SET_ROOM_HISTORY_VISIBILITY: - // case MatrixEventType.SET_ROOM_GUEST_ACCESS: { - // console.log(`Ignoring ${event.type}`); - // - // break; - // } - default: - console.log(`Could not find handler for ${event.type}`, event); - } -}; diff --git a/apps/meteor/app/federation-v2/server/events/createRoom.ts b/apps/meteor/app/federation-v2/server/events/createRoom.ts deleted file mode 100644 index 5cca3032c455..000000000000 --- a/apps/meteor/app/federation-v2/server/events/createRoom.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { IRoom, RoomType } from '@rocket.chat/apps-engine/definition/rooms'; -import { ICreatedRoom } from '@rocket.chat/core-typings'; -import { IUser } from '@rocket.chat/apps-engine/definition/users'; - -import { MatrixBridgedRoom, MatrixBridgedUser, Users } from '../../../models/server'; -import { Rooms } from '../../../models/server/raw'; -import { createRoom } from '../../../lib/server'; -import { IMatrixEvent } from '../definitions/IMatrixEvent'; -import { MatrixEventType } from '../definitions/MatrixEventType'; -import { checkBridgedRoomExists } from '../methods/checkBridgedRoomExists'; -import { matrixClient } from '../matrix-client'; -import { SetRoomJoinRules } from '../definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules'; -import { matrixBridge } from '../bridge'; -import { setRoomJoinRules } from './setRoomJoinRules'; -import { setRoomName } from './setRoomName'; -import { handleRoomMembership } from './roomMembership'; - -const removeUselessCharacterFromMatrixRoomId = (matrixRoomId: string): string => { - const prefixedRoomIdOnly = matrixRoomId.split(':')[0]; - const prefix = '!'; - - return prefixedRoomIdOnly?.replace(prefix, ''); -}; - -const generateRoomNameForLocalServer = (matrixRoomId: string, matrixRoomName?: string): string => { - return matrixRoomName || `Federation-${removeUselessCharacterFromMatrixRoomId(matrixRoomId)}`; -}; - -const createLocalRoomAsync = async (roomType: RoomType, roomName: string, creator: IUser, members: IUser[] = []): Promise => { - return new Promise((resolve) => resolve(createRoom(roomType, roomName, creator.username, members as any[]) as ICreatedRoom)); -}; - -const createBridgedRecordRoom = async (roomId: IRoom['id'], matrixRoomId: string): Promise => - new Promise((resolve) => resolve(MatrixBridgedRoom.insert({ rid: roomId, mri: matrixRoomId }))); - -const createLocalUserIfNecessary = async (matrixUserId: string): Promise => { - const { uid } = await matrixClient.user.createLocal(matrixUserId); - - return uid; -}; - -const applyRoomStateIfNecessary = async (matrixRoomId: string, roomState?: IMatrixEvent[]): Promise => { - // TODO: this should be better - /* eslint-disable no-await-in-loop */ - for (const state of roomState || []) { - switch (state.type) { - case 'm.room.create': - continue; - case 'm.room.join_rules': { - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/camelcase - await setRoomJoinRules({ room_id: matrixRoomId, ...state }); - - break; - } - case 'm.room.name': { - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/camelcase - await setRoomName({ room_id: matrixRoomId, ...state }); - - break; - } - case 'm.room.member': { - // @ts-ignore - if (state.content.membership === 'join') { - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/camelcase,@typescript-eslint/no-use-before-define - await handleRoomMembership({ room_id: matrixRoomId, ...state }); - } - - break; - } - } - } - /* eslint-enable no-await-in-loop */ -}; - -const mapLocalAndExternal = async (roomId: string, matrixRoomId: string): Promise => { - await createBridgedRecordRoom(roomId, matrixRoomId); - await Rooms.setAsBridged(roomId); -}; - -const tryToGetDataFromExternalRoom = async ( - senderMatrixUserId: string, - matrixRoomId: string, - roomState: IMatrixEvent[] = [], -): Promise> => { - const finalRoomState = - roomState && roomState?.length > 0 ? roomState : await matrixBridge.getRoomStateByRoomId(senderMatrixUserId, matrixRoomId); - const externalRoomName = finalRoomState.find((stateEvent: Record) => stateEvent.type === MatrixEventType.SET_ROOM_NAME) - ?.content?.name; - const externalRoomJoinRule = finalRoomState.find( - (stateEvent: Record) => stateEvent.type === MatrixEventType.SET_ROOM_JOIN_RULES, - )?.content?.join_rule; - - return { - externalRoomName, - externalRoomJoinRule, - }; -}; - -export const createLocalDirectMessageRoom = async (matrixRoomId: string, creator: IUser, affectedUser: IUser): Promise => { - const { _id: roomId } = await createLocalRoomAsync(RoomType.DIRECT_MESSAGE, generateRoomNameForLocalServer(matrixRoomId), creator, [ - creator, - affectedUser, - ]); - await mapLocalAndExternal(roomId, matrixRoomId); - - return roomId; -}; - -export const getLocalRoomType = (matrixJoinRule = '', matrixRoomIsDirect = false): RoomType => { - const mapping: Record = { - [SetRoomJoinRules.JOIN]: RoomType.CHANNEL, - [SetRoomJoinRules.INVITE]: RoomType.PRIVATE_GROUP, - }; - const roomType = mapping[matrixJoinRule] || RoomType.CHANNEL; - - return roomType === RoomType.PRIVATE_GROUP && matrixRoomIsDirect ? RoomType.DIRECT_MESSAGE : roomType; -}; - -export const createLocalChannelsRoom = async ( - matrixRoomId: string, - senderMatrixUserId: string, - creator: IUser, - roomState?: IMatrixEvent[], -): Promise => { - let roomName = ''; - let joinRule; - - try { - const { externalRoomName, externalRoomJoinRule } = await tryToGetDataFromExternalRoom(senderMatrixUserId, matrixRoomId, roomState); - roomName = externalRoomName; - joinRule = externalRoomJoinRule; - } catch (err) { - // no-op - } - const { rid: roomId } = await createLocalRoomAsync( - getLocalRoomType(joinRule), - generateRoomNameForLocalServer(matrixRoomId, roomName), - creator, - ); - await mapLocalAndExternal(roomId, matrixRoomId); - - return roomId; -}; - -export const processFirstAccessFromExternalServer = async ( - matrixRoomId: string, - senderMatrixUserId: string, - affectedMatrixUserId: string, - senderUser: IUser, - affectedUser: IUser, - isDirect = false, - roomState: IMatrixEvent[], -): Promise => { - let roomId; - if (isDirect) { - roomId = await createLocalDirectMessageRoom(matrixRoomId, senderUser, affectedUser); - } else { - roomId = await createLocalChannelsRoom(matrixRoomId, senderMatrixUserId, senderUser, roomState); - } - - await applyRoomStateIfNecessary(matrixRoomId, roomState); - await matrixBridge.getInstance().getIntent(affectedMatrixUserId).join(matrixRoomId); - - return roomId; -}; - -export const handleCreateRoom = async (event: IMatrixEvent): Promise => { - const { - room_id: matrixRoomId, - sender, - content: { was_programatically_created: wasProgramaticallyCreated = false }, - } = event; - - // Check if the room already exists and if so, ignore - const roomExists = await checkBridgedRoomExists(matrixRoomId); - if (roomExists || wasProgramaticallyCreated) { - return; - } - - const bridgedUserId = await MatrixBridgedUser.getId(sender); - const creator = await Users.findOneById(bridgedUserId || (await createLocalUserIfNecessary(sender))); - - await createLocalChannelsRoom(matrixRoomId, sender, creator); -}; diff --git a/apps/meteor/app/federation-v2/server/events/index.ts b/apps/meteor/app/federation-v2/server/events/index.ts deleted file mode 100644 index ef403e8e78cd..000000000000 --- a/apps/meteor/app/federation-v2/server/events/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './createRoom'; -export * from './roomMembership'; -export * from './sendMessage'; -export * from './setRoomJoinRules'; -export * from './setRoomName'; -export * from './setRoomTopic'; diff --git a/apps/meteor/app/federation-v2/server/events/roomMembership.ts b/apps/meteor/app/federation-v2/server/events/roomMembership.ts deleted file mode 100644 index d51233c1f14e..000000000000 --- a/apps/meteor/app/federation-v2/server/events/roomMembership.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { IUser } from '@rocket.chat/apps-engine/definition/users'; - -import { MatrixBridgedUser, MatrixBridgedRoom, Users } from '../../../models/server'; -import { addUserToRoom, removeUserFromRoom } from '../../../lib/server'; -import { IMatrixEvent } from '../definitions/IMatrixEvent'; -import { MatrixEventType } from '../definitions/MatrixEventType'; -import { AddMemberToRoomMembership } from '../definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom'; -import { matrixClient } from '../matrix-client'; -import { processFirstAccessFromExternalServer } from './createRoom'; - -const extractServerNameFromMatrixUserId = (matrixRoomId = ''): string => matrixRoomId.split(':')[1]; - -const addUserToRoomAsync = async (roomId: string, affectedUser: IUser, senderUser?: IUser): Promise => { - new Promise((resolve) => resolve(addUserToRoom(roomId, affectedUser as any, senderUser as any))); -}; - -export const handleRoomMembership = async (event: IMatrixEvent): Promise => { - const { - room_id: matrixRoomId, - sender: senderMatrixUserId, - state_key: affectedMatrixUserId, - content: { membership, is_direct: isDirect = false }, - invite_room_state: roomState, - } = event; - - // Find the bridged room id - let roomId = await MatrixBridgedRoom.getId(matrixRoomId); - const fromADifferentServer = - extractServerNameFromMatrixUserId(senderMatrixUserId) !== extractServerNameFromMatrixUserId(affectedMatrixUserId); - - // If there is no room id, throw error - if (!roomId && !fromADifferentServer) { - throw new Error(`Could not find room with matrixRoomId: ${matrixRoomId}`); - } - - // Find the sender user - const senderUserId = await MatrixBridgedUser.getId(senderMatrixUserId); - let senderUser = await Users.findOneById(senderUserId); - // If the sender user does not exist, it means we need to create it - if (!senderUser) { - const { uid } = await matrixClient.user.createLocal(senderMatrixUserId); - - senderUser = Users.findOneById(uid); - } - - // Find the affected user - const affectedUserId = await MatrixBridgedUser.getId(affectedMatrixUserId); - let affectedUser = await Users.findOneById(affectedUserId); - // If the affected user does not exist, it means we need to create it - if (!affectedUser) { - const { uid } = await matrixClient.user.createLocal(affectedMatrixUserId); - - affectedUser = Users.findOneById(uid); - } - - if (!roomId && fromADifferentServer) { - roomId = await processFirstAccessFromExternalServer( - matrixRoomId, - senderMatrixUserId, - affectedMatrixUserId, - senderUser, - affectedUser, - isDirect, - roomState as IMatrixEvent[], - ); - } - - if (!roomId) { - return; - } - - switch (membership) { - case AddMemberToRoomMembership.JOIN: - await addUserToRoomAsync(roomId, affectedUser); - break; - case AddMemberToRoomMembership.INVITE: - // TODO: this should be a local invite - await addUserToRoomAsync(roomId, affectedUser, senderUser); - break; - case AddMemberToRoomMembership.LEAVE: - await removeUserFromRoom(roomId, affectedUser, { - byUser: senderUser, - }); - break; - } -}; diff --git a/apps/meteor/app/federation-v2/server/events/sendMessage.ts b/apps/meteor/app/federation-v2/server/events/sendMessage.ts deleted file mode 100644 index c70577d1e2af..000000000000 --- a/apps/meteor/app/federation-v2/server/events/sendMessage.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MatrixBridgedRoom, MatrixBridgedUser, Users } from '../../../models/server'; -import { IMatrixEvent } from '../definitions/IMatrixEvent'; -import { MatrixEventType } from '../definitions/MatrixEventType'; -import { sendMessage } from '../../../lib/server'; -import { Rooms } from '../../../models/server/raw'; - -export const sendMessageAsync = async (user: any, msg: any, room: any): Promise => - new Promise((resolve) => resolve(sendMessage(user, msg, room))); - -export const handleSendMessage = async (event: IMatrixEvent): Promise => { - const { room_id: matrixRoomId, sender } = event; - - // Find the bridged room id - const roomId = await MatrixBridgedRoom.getId(matrixRoomId); - if (!roomId) { - return; - } - - // Find the bridged user id - const userId = await MatrixBridgedUser.getId(sender); - - // Find the user - const user = await Users.findOneById(userId); - - const room = await Rooms.findOneById(roomId); - - await sendMessageAsync(user, { msg: event.content.body }, room); -}; diff --git a/apps/meteor/app/federation-v2/server/events/setRoomJoinRules.ts b/apps/meteor/app/federation-v2/server/events/setRoomJoinRules.ts deleted file mode 100644 index e95bf691bf43..000000000000 --- a/apps/meteor/app/federation-v2/server/events/setRoomJoinRules.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; - -import { Rooms, Subscriptions } from '../../../models/server/raw'; -import { MatrixBridgedRoom } from '../../../models/server'; -import { IMatrixEvent } from '../definitions/IMatrixEvent'; -import { MatrixEventType } from '../definitions/MatrixEventType'; -import { SetRoomJoinRules } from '../definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules'; - -export const setRoomJoinRules = async (event: IMatrixEvent): Promise => { - const { - room_id: matrixRoomId, - content: { join_rule: joinRule }, - } = event; - - // Find the bridged room id - const roomId = await MatrixBridgedRoom.getId(matrixRoomId); - if (!roomId) { - return; - } - - const localRoom = await Rooms.findOneById(roomId); - - if (!localRoom || localRoom?.t === RoomType.DIRECT_MESSAGE) { - return; - } - - let type; - - switch (joinRule) { - case SetRoomJoinRules.INVITE: - type = RoomType.PRIVATE_GROUP; - break; - case SetRoomJoinRules.JOIN: - default: - type = RoomType.CHANNEL; - } - - await Rooms.update( - { _id: roomId }, - { - $set: { - t: type, - }, - }, - ); - - await Subscriptions.update( - { rid: roomId }, - { - $set: { - t: type, - }, - }, - { multi: true }, - ); -}; diff --git a/apps/meteor/app/federation-v2/server/events/setRoomName.ts b/apps/meteor/app/federation-v2/server/events/setRoomName.ts deleted file mode 100644 index 243791841d70..000000000000 --- a/apps/meteor/app/federation-v2/server/events/setRoomName.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Rooms, Subscriptions } from '../../../models/server/raw'; -import { MatrixBridgedRoom } from '../../../models/server'; -import { IMatrixEvent } from '../definitions/IMatrixEvent'; -import { MatrixEventType } from '../definitions/MatrixEventType'; - -export const setRoomName = async (event: IMatrixEvent): Promise => { - const { - room_id: matrixRoomId, - content: { name }, - } = event; - - // Normalize room name - const normalizedName = name.replace('@', ''); - - // Find the bridged room id - const roomId = await MatrixBridgedRoom.getId(matrixRoomId); - - if (!roomId) { - return; - } - - await Rooms.update( - { _id: roomId }, - { - $set: { - name: normalizedName, - fname: normalizedName, - }, - }, - ); - - await Subscriptions.update( - { rid: roomId }, - { - $set: { - name: normalizedName, - fname: normalizedName, - }, - }, - { multi: true }, - ); -}; diff --git a/apps/meteor/app/federation-v2/server/events/setRoomTopic.ts b/apps/meteor/app/federation-v2/server/events/setRoomTopic.ts deleted file mode 100644 index d75d38df651e..000000000000 --- a/apps/meteor/app/federation-v2/server/events/setRoomTopic.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { MatrixBridgedRoom, Rooms } from '../../../models/server'; -import { IMatrixEvent } from '../definitions/IMatrixEvent'; -import { MatrixEventType } from '../definitions/MatrixEventType'; - -export const setRoomTopic = async (event: IMatrixEvent): Promise => { - const { - room_id: matrixRoomId, - content: { topic }, - } = event; - - // Find the bridged room id - const roomId = await MatrixBridgedRoom.getId(matrixRoomId); - - Rooms.update( - { _id: roomId }, - { - $set: { - description: topic, - }, - }, - ); -}; diff --git a/apps/meteor/app/federation-v2/server/index.ts b/apps/meteor/app/federation-v2/server/index.ts index e331badfd006..d2d90966d4d0 100644 --- a/apps/meteor/app/federation-v2/server/index.ts +++ b/apps/meteor/app/federation-v2/server/index.ts @@ -1,4 +1,35 @@ -import './settings'; -import { startBridge } from './startup'; +import { FederationFactory } from './infrastructure/Factory'; -startBridge(); +const PROCESSING_CONCURRENCY = 1; + +const rocketSettingsAdapter = FederationFactory.buildRocketSettingsAdapter(); +rocketSettingsAdapter.initialize(); +const queueInstance = FederationFactory.buildQueue(); +const federation = FederationFactory.buildBridge(rocketSettingsAdapter, queueInstance); +const rocketRoomAdapter = FederationFactory.buildRocketRoomAdapter(); +const rocketUserAdapter = FederationFactory.buildRocketUserAdapter(); +const rocketMessageAdapter = FederationFactory.buildRocketMessageAdapter(); +const rocketNotificationAdapter = FederationFactory.buildRocketNotificationdapter(); + +const federationRoomServiceReceiver = FederationFactory.buildRoomServiceReceiver( + rocketRoomAdapter, + rocketUserAdapter, + rocketMessageAdapter, + rocketSettingsAdapter, + federation, +); +const federationEventsHandler = FederationFactory.buildEventHandlers(federationRoomServiceReceiver); + +export const federationRoomServiceSender = FederationFactory.buildRoomServiceSender( + rocketRoomAdapter, + rocketUserAdapter, + rocketSettingsAdapter, + rocketNotificationAdapter, + federation, +); + +(async (): Promise => { + queueInstance.setHandler(federationEventsHandler.handleEvent.bind(federationEventsHandler), PROCESSING_CONCURRENCY); + await federation.start(); + await rocketSettingsAdapter.onFederationEnabledStatusChanged(federation.onFederationAvailabilityChanged.bind(federation)); +})(); diff --git a/apps/meteor/app/federation-v2/server/infrastructure/Factory.ts b/apps/meteor/app/federation-v2/server/infrastructure/Factory.ts new file mode 100644 index 000000000000..36a20f827c10 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/Factory.ts @@ -0,0 +1,90 @@ +import { FederationRoomServiceReceiver } from '../application/RoomServiceReceiver'; +import { FederationRoomServiceSender } from '../application/RoomServiceSender'; +import { MatrixBridge } from './matrix/Bridge'; +import { MatrixEventsHandler } from './matrix/handlers'; +import { + MatrixRoomCreatedHandler, + MatrixRoomJoinRulesChangedHandler, + MatrixRoomMembershipChangedHandler, + MatrixRoomMessageSentHandler, + MatrixRoomNameChangedHandler, + MatrixRoomTopicChangedHandler, +} from './matrix/handlers/Room'; +import { InMemoryQueue } from './queue/InMemoryQueue'; +import { RocketChatMessageAdapter } from './rocket-chat/adapters/Message'; +import { RocketChatRoomAdapter } from './rocket-chat/adapters/Room'; +import { RocketChatSettingsAdapter } from './rocket-chat/adapters/Settings'; +import { RocketChatUserAdapter } from './rocket-chat/adapters/User'; +import { IFederationBridge } from '../domain/IFederationBridge'; +import { RocketChatNotificationAdapter } from './rocket-chat/adapters/Notification'; + +export class FederationFactory { + public static buildRocketSettingsAdapter(): RocketChatSettingsAdapter { + return new RocketChatSettingsAdapter(); + } + + public static buildRocketRoomAdapter(): RocketChatRoomAdapter { + return new RocketChatRoomAdapter(); + } + + public static buildRocketUserAdapter(): RocketChatUserAdapter { + return new RocketChatUserAdapter(); + } + + public static buildRocketMessageAdapter(): RocketChatMessageAdapter { + return new RocketChatMessageAdapter(); + } + + public static buildRocketNotificationdapter(): RocketChatNotificationAdapter { + return new RocketChatNotificationAdapter(); + } + + public static buildQueue(): InMemoryQueue { + return new InMemoryQueue(); + } + + public static buildRoomServiceReceiver( + rocketRoomAdapter: RocketChatRoomAdapter, + rocketUserAdapter: RocketChatUserAdapter, + rocketMessageAdapter: RocketChatMessageAdapter, + rocketSettingsAdapter: RocketChatSettingsAdapter, + bridge: IFederationBridge, + ): FederationRoomServiceReceiver { + return new FederationRoomServiceReceiver(rocketRoomAdapter, rocketUserAdapter, rocketMessageAdapter, rocketSettingsAdapter, bridge); + } + + public static buildRoomServiceSender( + rocketRoomAdapter: RocketChatRoomAdapter, + rocketUserAdapter: RocketChatUserAdapter, + rocketSettingsAdapter: RocketChatSettingsAdapter, + rocketNotificationAdapter: RocketChatNotificationAdapter, + bridge: IFederationBridge, + ): FederationRoomServiceSender { + return new FederationRoomServiceSender(rocketRoomAdapter, rocketUserAdapter, rocketSettingsAdapter, rocketNotificationAdapter, bridge); + } + + public static buildBridge(rocketSettingsAdapter: RocketChatSettingsAdapter, queue: InMemoryQueue): IFederationBridge { + return new MatrixBridge( + rocketSettingsAdapter.getApplicationServiceId(), + rocketSettingsAdapter.getHomeServerUrl(), + rocketSettingsAdapter.getHomeServerDomain(), + rocketSettingsAdapter.getBridgeUrl(), + rocketSettingsAdapter.getBridgePort(), + rocketSettingsAdapter.generateRegistrationFileObject(), + queue.addToQueue.bind(queue), + ); + } + + public static buildEventHandlers(roomServiceReceive: FederationRoomServiceReceiver): MatrixEventsHandler { + const EVENT_HANDLERS = [ + new MatrixRoomCreatedHandler(roomServiceReceive), + new MatrixRoomMembershipChangedHandler(roomServiceReceive), + new MatrixRoomJoinRulesChangedHandler(roomServiceReceive), + new MatrixRoomNameChangedHandler(roomServiceReceive), + new MatrixRoomTopicChangedHandler(roomServiceReceive), + new MatrixRoomMessageSentHandler(roomServiceReceive), + ]; + + return new MatrixEventsHandler(EVENT_HANDLERS); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts new file mode 100644 index 000000000000..acb2824f6903 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts @@ -0,0 +1,177 @@ +import { AppServiceOutput, Bridge } from '@rocket.chat/forked-matrix-appservice-bridge'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { IFederationBridge } from '../../domain/IFederationBridge'; +import { bridgeLogger } from '../rocket-chat/adapters/logger'; +import { IMatrixEvent } from './definitions/IMatrixEvent'; +import { MatrixEventType } from './definitions/MatrixEventType'; + +export class MatrixBridge implements IFederationBridge { + private bridgeInstance: Bridge; + + private isRunning = false; + + constructor( + private appServiceId: string, + private homeServerUrl: string, + private homeServerDomain: string, + private bridgeUrl: string, + private bridgePort: number, + private homeServerRegistrationFile: Record, + private eventHandler: Function, + ) { + this.logInfo(); + } + + public async onFederationAvailabilityChanged(enabled: boolean): Promise { + if (!enabled) { + await this.stop(); + return; + } + await this.start(); + } + + public async start(): Promise { + try { + await this.stop(); + await this.createInstance(); + + if (!this.isRunning) { + await this.bridgeInstance.run(this.bridgePort); + this.isRunning = true; + } + } catch (e) { + bridgeLogger.error('Failed to initialize the matrix-appservice-bridge.', e); + bridgeLogger.error('Disabling Matrix Bridge. Please resolve error and try again'); + + // await this.settingsAdapter.disableFederation(); + } + } + + public async stop(): Promise { + if (!this.isRunning) { + return; + } + // the http server might take some minutes to shutdown, and this promise can take some time to be resolved + await this.bridgeInstance?.close(); + this.isRunning = false; + } + + public async getUserProfileInformation(externalUserId: string): Promise { + try { + return this.bridgeInstance.getIntent(externalUserId).getProfileInfo(externalUserId); + } catch (err) { + // no-op + } + } + + public async joinRoom(externalRoomId: string, externalUserId: string): Promise { + await this.bridgeInstance.getIntent(externalUserId).join(externalRoomId); + } + + public async inviteToRoom(externalRoomId: string, externalInviterId: string, externalInviteeId: string): Promise { + await this.bridgeInstance.getIntent(externalInviterId).invite(externalRoomId, externalInviteeId); + } + + public async createUser(username: string, name: string, domain: string): Promise { + const matrixUserId = `@${username?.toLowerCase()}:${domain}`; + const intent = this.bridgeInstance.getIntent(matrixUserId); + + await intent.ensureProfile(name); + await intent.setDisplayName(`${username} (${name})`); + + return matrixUserId; + } + + public async createRoom( + externalCreatorId: string, + externalInviteeId: string, + roomType: RoomType, + roomName: string, + roomTopic?: string, + ): Promise { + const intent = this.bridgeInstance.getIntent(externalCreatorId); + + const visibility = roomType === 'p' || roomType === 'd' ? 'invite' : 'public'; + const preset = roomType === 'p' || roomType === 'd' ? 'private_chat' : 'public_chat'; + + // Create the matrix room + const matrixRoom = await intent.createRoom({ + createAsClient: true, + options: { + name: roomName, + topic: roomTopic, + visibility, + preset, + ...this.parametersForDirectMessagesIfNecessary(roomType, externalInviteeId), + // eslint-disable-next-line @typescript-eslint/camelcase + creation_content: { + // eslint-disable-next-line @typescript-eslint/camelcase + was_internally_programatically_created: true, + }, + }, + }); + + return matrixRoom.room_id; + } + + public async sendMessage(externalRoomId: string, externaSenderId: string, text: string): Promise { + await this.bridgeInstance.getIntent(externaSenderId).sendText(externalRoomId, text); + } + + public isUserIdFromTheSameHomeserver(externalUserId: string, domain: string): boolean { + const userDomain = externalUserId.includes(':') ? externalUserId.split(':').pop() : ''; + + return userDomain === domain; + } + + public getInstance(): IFederationBridge { + return this; + } + + private parametersForDirectMessagesIfNecessary = (roomType: RoomType, invitedUserId: string): Record => { + return roomType === RoomType.DIRECT_MESSAGE + ? { + // eslint-disable-next-line @typescript-eslint/camelcase + is_direct: true, + invite: [invitedUserId], + } + : {}; + }; + + private logInfo(): void { + bridgeLogger.info(`Running Federation V2: + id: ${this.appServiceId} + bridgeUrl: ${this.bridgeUrl} + homeserverURL: ${this.homeServerUrl} + homeserverDomain: ${this.homeServerDomain} + `); + } + + private async createInstance(): Promise { + bridgeLogger.info('Performing Dynamic Import of matrix-appservice-bridge'); + + // Dynamic import to prevent Rocket.Chat from loading the module until needed and then handle if that fails + const { Bridge, AppServiceRegistration } = await import('@rocket.chat/forked-matrix-appservice-bridge'); + + this.bridgeInstance = new Bridge({ + homeserverUrl: this.homeServerUrl, + domain: this.homeServerDomain, + registration: AppServiceRegistration.fromObject(this.homeServerRegistrationFile as AppServiceOutput), + disableStores: true, + controller: { + onAliasQuery: (alias, matrixRoomId): void => { + console.log('onAliasQuery', alias, matrixRoomId); + }, + onEvent: async (request /* , context*/): Promise => { + // Get the event + const event = request.getData() as unknown as IMatrixEvent; + this.eventHandler(event); + }, + onLog: async (line, isError): Promise => { + console.log(line, isError); + }, + }, + }); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver.ts new file mode 100644 index 000000000000..9d845516cd75 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver.ts @@ -0,0 +1,152 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { + FederationRoomChangeJoinRulesDto, + FederationRoomChangeMembershipDto, + FederationRoomChangeNameDto, + FederationRoomChangeTopicDto, + FederationRoomCreateInputDto, + FederationRoomSendInternalMessageDto, +} from '../../../application/input/RoomReceiverDto'; +import { EVENT_ORIGIN } from '../../../domain/IFederationBridge'; +import { IMatrixEvent } from '../definitions/IMatrixEvent'; +import { AddMemberToRoomMembership } from '../definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom'; +import { RoomJoinRules } from '../definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules'; +import { MatrixEventType } from '../definitions/MatrixEventType'; + +export class MatrixRoomReceiverConverter { + public static toRoomCreateDto(externalEvent: IMatrixEvent): FederationRoomCreateInputDto { + return Object.assign(new FederationRoomCreateInputDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + ...MatrixRoomReceiverConverter.tryToGetExternalInfoFromTheRoomState( + externalEvent.invite_room_state || externalEvent.unsigned?.invite_room_state, + ), + externalInviterId: externalEvent.sender, + normalizedInviterId: MatrixRoomReceiverConverter.convertMatrixUserIdFormatToRCFormat(externalEvent.sender), + wasInternallyProgramaticallyCreated: externalEvent.content?.was_internally_programatically_created || false, + }); + } + + public static toChangeRoomMembershipDto( + externalEvent: IMatrixEvent, + ): FederationRoomChangeMembershipDto { + return Object.assign(new FederationRoomChangeMembershipDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + ...MatrixRoomReceiverConverter.tryToGetExternalInfoFromTheRoomState( + externalEvent.invite_room_state || externalEvent.unsigned?.invite_room_state, + externalEvent.content?.is_direct, + ), + externalInviterId: externalEvent.sender, + normalizedInviterId: MatrixRoomReceiverConverter.convertMatrixUserIdFormatToRCFormat(externalEvent.sender), + externalInviteeId: externalEvent.state_key, + normalizedInviteeId: MatrixRoomReceiverConverter.convertMatrixUserIdFormatToRCFormat(externalEvent.state_key), + inviteeUsernameOnly: MatrixRoomReceiverConverter.formatMatrixUserIdToRCUsernameFormat(externalEvent.state_key), + inviterUsernameOnly: MatrixRoomReceiverConverter.formatMatrixUserIdToRCUsernameFormat(externalEvent.sender), + eventOrigin: MatrixRoomReceiverConverter.getEventOrigin(externalEvent.sender, externalEvent.state_key), + leave: externalEvent.content?.membership === AddMemberToRoomMembership.LEAVE, + }); + } + + public static toSendRoomMessageDto(externalEvent: IMatrixEvent): FederationRoomSendInternalMessageDto { + return Object.assign(new FederationRoomSendInternalMessageDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + externalSenderId: externalEvent.sender, + normalizedSenderId: MatrixRoomReceiverConverter.convertMatrixUserIdFormatToRCFormat(externalEvent.sender), + text: externalEvent.content?.body, + }); + } + + public static toRoomChangeJoinRulesDto( + externalEvent: IMatrixEvent, + ): FederationRoomChangeJoinRulesDto { + return Object.assign(new FederationRoomChangeJoinRulesDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + roomType: MatrixRoomReceiverConverter.convertMatrixJoinRuleToRCRoomType(externalEvent.content?.join_rule), + }); + } + + public static toRoomChangeNameDto(externalEvent: IMatrixEvent): FederationRoomChangeNameDto { + return Object.assign(new FederationRoomChangeNameDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + normalizedRoomName: MatrixRoomReceiverConverter.normalizeRoomNameToRCFormat(externalEvent.content?.name), + }); + } + + public static toRoomChangeTopicDto(externalEvent: IMatrixEvent): FederationRoomChangeTopicDto { + return Object.assign(new FederationRoomChangeTopicDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + roomTopic: externalEvent.content?.topic, + }); + } + + private static convertMatrixUserIdFormatToRCFormat(matrixUserId = ''): string { + return matrixUserId.replace('@', ''); + } + + private static convertMatrixRoomIdFormatToRCFormat(matrixRoomId = ''): string { + const prefixedRoomIdOnly = matrixRoomId.split(':')[0]; + const prefix = '!'; + + return prefixedRoomIdOnly?.replace(prefix, ''); + } + + private static normalizeRoomNameToRCFormat(matrixRoomName = ''): string { + return matrixRoomName.replace('@', ''); + } + + private static formatMatrixUserIdToRCUsernameFormat(matrixUserId = ''): string { + return matrixUserId.split(':')[0]?.replace('@', ''); + } + + private static getEventOrigin(inviterId = '', inviteeId = ''): EVENT_ORIGIN { + const fromADifferentServer = + MatrixRoomReceiverConverter.extractServerNameFromMatrixUserId(inviterId) !== + MatrixRoomReceiverConverter.extractServerNameFromMatrixUserId(inviteeId); + + return fromADifferentServer ? EVENT_ORIGIN.REMOTE : EVENT_ORIGIN.LOCAL; + } + + private static extractServerNameFromMatrixUserId(matrixUserId = ''): string { + const splitted = matrixUserId.split(':'); + + return splitted.length > 1 ? splitted[1] : ''; + } + + private static getBasicRoomsFields(externalRoomId: string): Record { + return { + externalRoomId, + normalizedRoomId: MatrixRoomReceiverConverter.convertMatrixRoomIdFormatToRCFormat(externalRoomId), + }; + } + + private static convertMatrixJoinRuleToRCRoomType(matrixJoinRule: RoomJoinRules, matrixRoomIsDirect = false): RoomType { + const mapping: Record = { + [RoomJoinRules.JOIN]: RoomType.CHANNEL, + [RoomJoinRules.INVITE]: RoomType.PRIVATE_GROUP, + }; + const roomType = mapping[matrixJoinRule] || RoomType.CHANNEL; + + return roomType === RoomType.PRIVATE_GROUP && matrixRoomIsDirect ? RoomType.DIRECT_MESSAGE : roomType; + } + + private static tryToGetExternalInfoFromTheRoomState( + roomState: Record[] = [], + matrixRoomIsDirect = false, + ): Record { + if (roomState.length === 0) { + return {}; + } + const externalRoomName = roomState.find((stateEvent: Record) => stateEvent.type === MatrixEventType.ROOM_NAME_CHANGED) + ?.content?.name; + const externalRoomJoinRule = roomState.find( + (stateEvent: Record) => stateEvent.type === MatrixEventType.ROOM_JOIN_RULES_CHANGED, + )?.content?.join_rule; + + return { + ...(externalRoomName ? { externalRoomName } : {}), + ...(externalRoomJoinRule + ? { roomType: MatrixRoomReceiverConverter.convertMatrixJoinRuleToRCRoomType(externalRoomJoinRule, matrixRoomIsDirect) } + : {}), + }; + } +} diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEvent.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEvent.ts similarity index 84% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEvent.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEvent.ts index 7111057ec55e..3470ab6481bc 100644 --- a/apps/meteor/app/federation-v2/server/definitions/IMatrixEvent.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEvent.ts @@ -11,6 +11,6 @@ export interface IMatrixEvent { sender: string; state_key: string; type: T; - unsigned: { age: number }; + unsigned: { age: number; invite_room_state: Record[] }; user_id: string; } diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom.ts similarity index 100% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom.ts diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts similarity index 64% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts index 6180c0356200..f9e80f615808 100644 --- a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts @@ -1,5 +1,5 @@ export interface IMatrixEventContentCreateRoom { creator: string; room_version: string; - was_programatically_created?: boolean; + was_internally_programatically_created?: boolean; } diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSendMessage.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSendMessage.ts similarity index 100% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSendMessage.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSendMessage.ts diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts similarity index 61% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts index 920f9bb53777..f97fa09d8aa7 100644 --- a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts @@ -1,8 +1,8 @@ -export enum SetRoomJoinRules { +export enum RoomJoinRules { JOIN = 'public', INVITE = 'invite', } export interface IMatrixEventContentSetRoomJoinRules { - join_rule: SetRoomJoinRules; + join_rule: RoomJoinRules; } diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomName.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomName.ts similarity index 100% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomName.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomName.ts diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomTopic.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomTopic.ts similarity index 100% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomTopic.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomTopic.ts diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/index.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/index.ts similarity index 57% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/index.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/index.ts index 7615779282e1..3c1d5b52f076 100644 --- a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/index.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/index.ts @@ -7,10 +7,10 @@ import { IMatrixEventContentSetRoomName } from './IMatrixEventContentSetRoomName import { IMatrixEventContentSetRoomTopic } from './IMatrixEventContentSetRoomTopic'; export type EventContent = { - [MatrixEventType.CREATE_ROOM]: IMatrixEventContentCreateRoom; - [MatrixEventType.ROOM_MEMBERSHIP]: IMatrixEventContentAddMemberToRoom; - [MatrixEventType.SET_ROOM_JOIN_RULES]: IMatrixEventContentSetRoomJoinRules; - [MatrixEventType.SET_ROOM_NAME]: IMatrixEventContentSetRoomName; - [MatrixEventType.SET_ROOM_TOPIC]: IMatrixEventContentSetRoomTopic; - [MatrixEventType.SEND_MESSAGE]: IMatrixEventContentSendMessage; + [MatrixEventType.ROOM_CREATED]: IMatrixEventContentCreateRoom; + [MatrixEventType.ROOM_MEMBERSHIP_CHANGED]: IMatrixEventContentAddMemberToRoom; + [MatrixEventType.ROOM_JOIN_RULES_CHANGED]: IMatrixEventContentSetRoomJoinRules; + [MatrixEventType.ROOM_NAME_CHANGED]: IMatrixEventContentSetRoomName; + [MatrixEventType.ROOM_TOPIC_CHANGED]: IMatrixEventContentSetRoomTopic; + [MatrixEventType.ROOM_MESSAGE_SENT]: IMatrixEventContentSendMessage; }; diff --git a/apps/meteor/app/federation-v2/server/definitions/MatrixEventType.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType.ts similarity index 51% rename from apps/meteor/app/federation-v2/server/definitions/MatrixEventType.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType.ts index 14d4f0bb0ecb..bb58a0d71825 100644 --- a/apps/meteor/app/federation-v2/server/definitions/MatrixEventType.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType.ts @@ -1,12 +1,12 @@ export enum MatrixEventType { - CREATE_ROOM = 'm.room.create', - ROOM_MEMBERSHIP = 'm.room.member', + ROOM_CREATED = 'm.room.create', + ROOM_MEMBERSHIP_CHANGED = 'm.room.member', // SET_ROOM_POWER_LEVELS = 'm.room.power_levels', // SET_ROOM_CANONICAL_ALIAS = 'm.room.canonical_alias', - SET_ROOM_JOIN_RULES = 'm.room.join_rules', + ROOM_JOIN_RULES_CHANGED = 'm.room.join_rules', // SET_ROOM_HISTORY_VISIBILITY = 'm.room.history_visibility', // SET_ROOM_GUEST_ACCESS = 'm.room.guest_access', - SET_ROOM_NAME = 'm.room.name', - SET_ROOM_TOPIC = 'm.room.topic', - SEND_MESSAGE = 'm.room.message', + ROOM_NAME_CHANGED = 'm.room.name', + ROOM_TOPIC_CHANGED = 'm.room.topic', + ROOM_MESSAGE_SENT = 'm.room.message', } diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/BaseEvent.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/BaseEvent.ts new file mode 100644 index 000000000000..25e179bfae7e --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/BaseEvent.ts @@ -0,0 +1,16 @@ +import { IMatrixEvent } from '../definitions/IMatrixEvent'; +import { MatrixEventType } from '../definitions/MatrixEventType'; + +export abstract class MatrixBaseEventHandler { + private type: T; + + public abstract handle(externalEvent: IMatrixEvent): Promise; + + protected constructor(type: T) { + this.type = type; + } + + public equals(type: MatrixEventType): boolean { + return this.type === type; + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Room.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Room.ts new file mode 100644 index 000000000000..47d10be8fb73 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Room.ts @@ -0,0 +1,65 @@ +import { FederationRoomServiceReceiver } from '../../../application/RoomServiceReceiver'; +import { MatrixRoomReceiverConverter } from '../converters/RoomReceiver'; +import { IMatrixEvent } from '../definitions/IMatrixEvent'; +import { MatrixEventType } from '../definitions/MatrixEventType'; +import { MatrixBaseEventHandler } from './BaseEvent'; + +export class MatrixRoomCreatedHandler extends MatrixBaseEventHandler { + constructor(private roomService: FederationRoomServiceReceiver) { + super(MatrixEventType.ROOM_CREATED); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.createRoom(MatrixRoomReceiverConverter.toRoomCreateDto(externalEvent)); + } +} + +export class MatrixRoomMembershipChangedHandler extends MatrixBaseEventHandler { + constructor(private roomService: FederationRoomServiceReceiver) { + super(MatrixEventType.ROOM_MEMBERSHIP_CHANGED); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.changeRoomMembership(MatrixRoomReceiverConverter.toChangeRoomMembershipDto(externalEvent)); + } +} + +export class MatrixRoomJoinRulesChangedHandler extends MatrixBaseEventHandler { + constructor(private roomService: FederationRoomServiceReceiver) { + super(MatrixEventType.ROOM_JOIN_RULES_CHANGED); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.changeJoinRules(MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto(externalEvent)); + } +} + +export class MatrixRoomNameChangedHandler extends MatrixBaseEventHandler { + constructor(private roomService: FederationRoomServiceReceiver) { + super(MatrixEventType.ROOM_NAME_CHANGED); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.changeRoomName(MatrixRoomReceiverConverter.toRoomChangeNameDto(externalEvent)); + } +} + +export class MatrixRoomTopicChangedHandler extends MatrixBaseEventHandler { + constructor(private roomService: FederationRoomServiceReceiver) { + super(MatrixEventType.ROOM_TOPIC_CHANGED); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.changeRoomTopic(MatrixRoomReceiverConverter.toRoomChangeTopicDto(externalEvent)); + } +} + +export class MatrixRoomMessageSentHandler extends MatrixBaseEventHandler { + constructor(private roomService: FederationRoomServiceReceiver) { + super(MatrixEventType.ROOM_MESSAGE_SENT); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.receiveExternalMessage(MatrixRoomReceiverConverter.toSendRoomMessageDto(externalEvent)); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/index.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/index.ts new file mode 100644 index 000000000000..67b361681492 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/index.ts @@ -0,0 +1,16 @@ +import { IMatrixEvent } from '../definitions/IMatrixEvent'; +import { MatrixEventType } from '../definitions/MatrixEventType'; +import { MatrixBaseEventHandler } from './BaseEvent'; + +export class MatrixEventsHandler { + // eslint-disable-next-line no-empty-function + constructor(private handlers: MatrixBaseEventHandler[]) {} + + public async handleEvent(event: IMatrixEvent): Promise { + const handler = this.handlers.find((handler) => handler.equals(event.type)); + if (!handler) { + return console.log(`Could not find handler for ${event.type}`, event); + } + return handler?.handle(event); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/queue/InMemoryQueue.ts b/apps/meteor/app/federation-v2/server/infrastructure/queue/InMemoryQueue.ts new file mode 100644 index 000000000000..fc4ea1106d26 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/queue/InMemoryQueue.ts @@ -0,0 +1,16 @@ +import * as fastq from 'fastq'; + +export class InMemoryQueue { + private instance: any; + + public setHandler(handler: Function, concurrency: number): void { + this.instance = fastq.promise(handler as any, concurrency); + } + + public addToQueue(task: Record): void { + if (!this.instance) { + throw new Error('You need to set the handler first'); + } + this.instance.push(task).catch(console.error); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Message.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Message.ts new file mode 100644 index 000000000000..3f82f77a6a19 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Message.ts @@ -0,0 +1,9 @@ +import { sendMessage } from '../../../../../lib/server'; +import { FederatedRoom } from '../../../domain/FederatedRoom'; +import { FederatedUser } from '../../../domain/FederatedUser'; + +export class RocketChatMessageAdapter { + public async sendMessage(user: FederatedUser, text: string, room: FederatedRoom): Promise { + new Promise((resolve) => resolve(sendMessage(user.internalReference, { msg: text }, room.internalReference))); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Notification.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Notification.ts new file mode 100644 index 000000000000..8e07d94cf12a --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Notification.ts @@ -0,0 +1,14 @@ +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; + +import { api } from '../../../../../../server/sdk/api'; + +export class RocketChatNotificationAdapter { + public notifyWithEphemeralMessage(i18nMessageKey: string, userId: string, roomId: string, language = 'en'): void { + api.broadcast('notify.ephemeralMessage', userId, roomId, { + msg: TAPi18n.__(i18nMessageKey, { + postProcess: 'sprintf', + lng: language, + }), + }); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Room.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Room.ts new file mode 100644 index 000000000000..44ec3ee5e5ad --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Room.ts @@ -0,0 +1,103 @@ +import { ICreatedRoom, IRoom } from '@rocket.chat/core-typings'; + +import { MatrixBridgedRoom } from '../../../../../models/server'; +import { FederatedRoom } from '../../../domain/FederatedRoom'; +import { createRoom, addUserToRoom, removeUserFromRoom } from '../../../../../lib/server'; +import { Rooms, Subscriptions } from '../../../../../models/server/raw'; +import { FederatedUser } from '../../../domain/FederatedUser'; + +export class RocketChatRoomAdapter { + public async getFederatedRoomByExternalId(externalRoomId: string): Promise { + const internalBridgedRoomId = MatrixBridgedRoom.getId(externalRoomId); + if (!internalBridgedRoomId) { + return; + } + const room = await Rooms.findOneById(internalBridgedRoomId); + + return this.createFederatedRoomInstance(externalRoomId, room); + } + + public async getFederatedRoomByInternalId(internalRoomId: string): Promise { + const externalRoomId = MatrixBridgedRoom.getMatrixId(internalRoomId); + if (!externalRoomId) { + return; + } + const room = await Rooms.findOneById(internalRoomId); + + return this.createFederatedRoomInstance(externalRoomId, room); + } + + public async getInternalRoomById(internalRoomId: string): Promise { + return Rooms.findOneById(internalRoomId); + } + + public async createFederatedRoom(federatedRoom: FederatedRoom): Promise { + const members = federatedRoom.getMembers(); + const { rid, _id } = createRoom( + federatedRoom.internalReference.t, + federatedRoom.internalReference.name, + federatedRoom.internalReference.u.username as string, + members, + ) as ICreatedRoom; + const roomId = rid || _id; + MatrixBridgedRoom.insert({ rid: roomId, mri: federatedRoom.externalId }); + await Rooms.setAsFederated(roomId); + } + + public async updateFederatedRoomByInternalRoomId(internalRoomId: string, federatedRoom: FederatedRoom): Promise { + MatrixBridgedRoom.upsert({ rid: internalRoomId }, { rid: internalRoomId, mri: federatedRoom.externalId }); + await Rooms.setAsFederated(internalRoomId); + } + + public async addUserToRoom(federatedRoom: FederatedRoom, inviteeUser: FederatedUser, inviterUser?: FederatedUser): Promise { + return new Promise((resolve) => + resolve(addUserToRoom(federatedRoom.internalReference._id, inviteeUser.internalReference, inviterUser?.internalReference) as any), + ); + } + + public async removeUserFromRoom(federatedRoom: FederatedRoom, affectedUser: FederatedUser, byUser: FederatedUser): Promise { + return new Promise((resolve) => + resolve( + removeUserFromRoom(federatedRoom.internalReference._id, affectedUser.internalReference, { + byUser: byUser.internalReference, + }) as any, + ), + ); + } + + public async updateRoomType(federatedRoom: FederatedRoom): Promise { + await Rooms.update({ _id: federatedRoom.internalReference._id }, { $set: { t: federatedRoom.internalReference.t } }); + await Subscriptions.update( + { rid: federatedRoom.internalReference._id }, + { $set: { t: federatedRoom.internalReference.t } }, + { multi: true }, + ); + } + + public async updateRoomName(federatedRoom: FederatedRoom): Promise { + await Rooms.update( + { _id: federatedRoom.internalReference._id }, + { $set: { name: federatedRoom.internalReference.name, fname: federatedRoom.internalReference.fname } }, + ); + await Subscriptions.update( + { rid: federatedRoom.internalReference._id }, + { $set: { name: federatedRoom.internalReference.name, fname: federatedRoom.internalReference.fname } }, + { multi: true }, + ); + } + + public async updateRoomTopic(federatedRoom: FederatedRoom): Promise { + await Rooms.update( + { _id: federatedRoom.internalReference._id }, + { $set: { description: federatedRoom.internalReference.description } }, + ); + } + + private createFederatedRoomInstance(externalRoomId: string, room: IRoom): FederatedRoom { + const federatedRoom = FederatedRoom.build(); + federatedRoom.externalId = externalRoomId; + federatedRoom.internalReference = room; + + return federatedRoom; + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings.ts new file mode 100644 index 000000000000..71ff9af73461 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings.ts @@ -0,0 +1,192 @@ +import yaml from 'js-yaml'; +import { SHA256 } from 'meteor/sha'; + +import { Settings } from '../../../../../models/server/raw'; +import { settings, settingsRegistry } from '../../../../../settings/server'; + +const EVERYTHING_REGEX = '.*'; +const LISTEN_RULES = EVERYTHING_REGEX; + +export class RocketChatSettingsAdapter { + public initialize(): void { + this.addFederationSettings(); + this.watchChangesAndUpdateRegistrationFile(); + } + + public getApplicationServiceId(): string { + return settings.get('Federation_Matrix_id'); + } + + public getApplicationHomeServerToken(): string { + return settings.get('Federation_Matrix_hs_token'); + } + + public getApplicationApplicationServiceToken(): string { + return settings.get('Federation_Matrix_as_token'); + } + + public getBridgeUrl(): string { + return settings.get('Federation_Matrix_bridge_url'); + } + + public getBridgePort(): number { + const [, , port] = this.getBridgeUrl().split(':'); + + return parseInt(port); + } + + public getHomeServerUrl(): string { + return settings.get('Federation_Matrix_homeserver_url'); + } + + public getHomeServerDomain(): string { + return settings.get('Federation_Matrix_homeserver_domain'); + } + + public getBridgeBotUsername(): string { + return settings.get('Federation_Matrix_bridge_localpart'); + } + + public async disableFederation(): Promise { + await Settings.updateValueById('Federation_Matrix_enabled', false); + } + + public onFederationEnabledStatusChanged(callback: Function): void { + settings.watchMultiple( + [ + 'Federation_Matrix_enabled', + 'Federation_Matrix_id', + 'Federation_Matrix_hs_token', + 'Federation_Matrix_as_token', + 'Federation_Matrix_homeserver_url', + 'Federation_Matrix_homeserver_domain', + 'Federation_Matrix_bridge_url', + 'Federation_Matrix_bridge_localpart', + ], + ([enabled]) => callback(enabled), + ); + } + + public generateRegistrationFileObject(): Record { + /* eslint-disable @typescript-eslint/camelcase */ + return { + id: this.getApplicationServiceId(), + hs_token: this.getApplicationHomeServerToken(), + as_token: this.getApplicationApplicationServiceToken(), + url: this.getBridgeUrl(), + sender_localpart: this.getBridgeBotUsername(), + namespaces: { + users: [ + { + exclusive: false, + regex: LISTEN_RULES, + }, + ], + rooms: [ + { + exclusive: false, + regex: LISTEN_RULES, + }, + ], + aliases: [ + { + exclusive: false, + regex: LISTEN_RULES, + }, + ], + }, + }; + /* eslint-enable @typescript-eslint/camelcase */ + } + + private async updateRegistrationFile(): Promise { + await Settings.updateValueById('Federation_Matrix_registration_file', yaml.dump(this.generateRegistrationFileObject())); + } + + private watchChangesAndUpdateRegistrationFile(): void { + settings.watchMultiple( + [ + 'Federation_Matrix_id', + 'Federation_Matrix_hs_token', + 'Federation_Matrix_as_token', + 'Federation_Matrix_homeserver_url', + 'Federation_Matrix_homeserver_domain', + 'Federation_Matrix_bridge_url', + 'Federation_Matrix_bridge_localpart', + ], + this.updateRegistrationFile.bind(this), + ); + } + + private addFederationSettings(): void { + settingsRegistry.addGroup('Federation', function () { + this.section('Matrix Bridge', function () { + this.add('Federation_Matrix_enabled', false, { + readonly: false, + type: 'boolean', + i18nLabel: 'Federation_Matrix_enabled', + i18nDescription: 'Federation_Matrix_enabled_desc', + alert: 'Federation_Matrix_Enabled_Alert', + }); + + const uniqueId = settings.get('uniqueID'); + const hsToken = SHA256(`hs_${uniqueId}`); + const asToken = SHA256(`as_${uniqueId}`); + + this.add('Federation_Matrix_id', `rocketchat_${uniqueId}`, { + readonly: true, + type: 'string', + i18nLabel: 'Federation_Matrix_id', + i18nDescription: 'Federation_Matrix_id_desc', + }); + + this.add('Federation_Matrix_hs_token', hsToken, { + readonly: true, + type: 'string', + i18nLabel: 'Federation_Matrix_hs_token', + i18nDescription: 'Federation_Matrix_hs_token_desc', + }); + + this.add('Federation_Matrix_as_token', asToken, { + readonly: true, + type: 'string', + i18nLabel: 'Federation_Matrix_as_token', + i18nDescription: 'Federation_Matrix_as_token_desc', + }); + + this.add('Federation_Matrix_homeserver_url', 'http://localhost:8008', { + type: 'string', + i18nLabel: 'Federation_Matrix_homeserver_url', + i18nDescription: 'Federation_Matrix_homeserver_url_desc', + alert: 'Federation_Matrix_homeserver_url_alert', + }); + + this.add('Federation_Matrix_homeserver_domain', 'local.rocket.chat', { + type: 'string', + i18nLabel: 'Federation_Matrix_homeserver_domain', + i18nDescription: 'Federation_Matrix_homeserver_domain_desc', + alert: 'Federation_Matrix_homeserver_domain_alert', + }); + + this.add('Federation_Matrix_bridge_url', 'http://host.docker.internal:3300', { + type: 'string', + i18nLabel: 'Federation_Matrix_bridge_url', + i18nDescription: 'Federation_Matrix_bridge_url_desc', + }); + + this.add('Federation_Matrix_bridge_localpart', 'rocket.cat', { + type: 'string', + i18nLabel: 'Federation_Matrix_bridge_localpart', + i18nDescription: 'Federation_Matrix_bridge_localpart_desc', + }); + + this.add('Federation_Matrix_registration_file', '', { + readonly: true, + type: 'code', + i18nLabel: 'Federation_Matrix_registration_file', + i18nDescription: 'Federation_Matrix_registration_file_desc', + }); + }); + }); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/User.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/User.ts new file mode 100644 index 000000000000..a2f2c26f8e6c --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/User.ts @@ -0,0 +1,85 @@ +import { IUser } from '@rocket.chat/core-typings'; + +import { MatrixBridgedUser, Users } from '../../../../../models/server'; +import { FederatedUser } from '../../../domain/FederatedUser'; + +export class RocketChatUserAdapter { + public async getFederatedUserByExternalId(externalUserId: string): Promise { + const internalBridgedUserId = MatrixBridgedUser.getId(externalUserId); + if (!internalBridgedUserId) { + return; + } + + const user = await Users.findOneById(internalBridgedUserId); + + return this.createFederatedUserInstance(externalUserId, user); + } + + public async getFederatedUserByInternalId(internalUserId: string): Promise { + const internalBridgedUserId = MatrixBridgedUser.getById(internalUserId); + if (!internalBridgedUserId) { + return; + } + const { uid: userId, mui: externalUserId } = internalBridgedUserId; + const user = await Users.findOneById(userId); + + return this.createFederatedUserInstance(externalUserId, user); + } + + public async getFederatedUserByInternalUsername(username: string): Promise { + const user = await Users.findOneByUsername(username); + if (!user) { + return; + } + const internalBridgedUserId = MatrixBridgedUser.getById(user._id); + if (!internalBridgedUserId) { + return; + } + const { mui: externalUserId } = internalBridgedUserId; + + return this.createFederatedUserInstance(externalUserId, user); + } + + public async getInternalUserById(userId: string): Promise { + return Users.findOneById(userId); + } + + public async createFederatedUser(federatedUser: FederatedUser): Promise { + const existingLocalUser = await Users.findOneByUsername(federatedUser.internalReference.username); + if (existingLocalUser) { + return MatrixBridgedUser.upsert( + { uid: existingLocalUser._id }, + { + uid: existingLocalUser._id, + mui: federatedUser.externalId, + remote: !federatedUser.existsOnlyOnProxyServer, + }, + ); + } + const newLocalUserId = await Users.create({ + username: federatedUser.internalReference.username, + type: federatedUser.internalReference.type, + status: federatedUser.internalReference.status, + active: federatedUser.internalReference.active, + roles: federatedUser.internalReference.roles, + name: federatedUser.internalReference.name, + requirePasswordChange: federatedUser.internalReference.requirePasswordChange, + }); + MatrixBridgedUser.upsert( + { uid: newLocalUserId }, + { + uid: newLocalUserId, + mui: federatedUser.externalId, + remote: !federatedUser.existsOnlyOnProxyServer, + }, + ); + } + + private createFederatedUserInstance(externalUserId: string, user: IUser): FederatedUser { + const federatedUser = FederatedUser.build(); + federatedUser.externalId = externalUserId; + federatedUser.internalReference = user; + + return federatedUser; + } +} diff --git a/apps/meteor/app/federation-v2/server/logger.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/logger.ts similarity index 73% rename from apps/meteor/app/federation-v2/server/logger.ts rename to apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/logger.ts index 0b88f48bfde6..331ed9f5f4b4 100644 --- a/apps/meteor/app/federation-v2/server/logger.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/logger.ts @@ -1,4 +1,4 @@ -import { Logger } from '../../logger/server'; +import { Logger } from '../../../../../logger/server'; const logger = new Logger('Federation_Matrix'); diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/converters/RoomSender.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/converters/RoomSender.ts new file mode 100644 index 000000000000..76d6937f206b --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/converters/RoomSender.ts @@ -0,0 +1,34 @@ +import { IMessage } from '@rocket.chat/core-typings'; + +import { FederationRoomInviteUserDto, FederationRoomSendExternalMessageDto } from '../../../application/input/RoomSenderDto'; + +export class FederationRoomSenderConverter { + public static toRoomInviteUserDto( + internalInviterId: string, + internalRoomId: string, + externalInviteeId: string, + ): FederationRoomInviteUserDto { + const normalizedInviteeId = externalInviteeId.replace('@', ''); + const inviteeUsernameOnly = externalInviteeId.split(':')[0]?.replace('@', ''); + + return Object.assign(new FederationRoomInviteUserDto(), { + internalInviterId, + internalRoomId, + rawInviteeId: externalInviteeId, + normalizedInviteeId, + inviteeUsernameOnly, + }); + } + + public static toSendExternalMessageDto( + internalSenderId: string, + internalRoomId: string, + message: IMessage, + ): FederationRoomSendExternalMessageDto { + return Object.assign(new FederationRoomSendExternalMessageDto(), { + internalRoomId, + internalSenderId, + message, + }); + } +} diff --git a/apps/meteor/app/federation-v2/server/matrix-client/index.ts b/apps/meteor/app/federation-v2/server/matrix-client/index.ts deleted file mode 100644 index 68664d6e9cdf..000000000000 --- a/apps/meteor/app/federation-v2/server/matrix-client/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as message from './message'; -import * as room from './room'; -import * as user from './user'; - -export const matrixClient = { - message, - room, - user, -}; diff --git a/apps/meteor/app/federation-v2/server/matrix-client/message.ts b/apps/meteor/app/federation-v2/server/matrix-client/message.ts deleted file mode 100644 index a6a9d8626632..000000000000 --- a/apps/meteor/app/federation-v2/server/matrix-client/message.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { IMessage } from '@rocket.chat/core-typings'; - -import { MatrixBridgedRoom, MatrixBridgedUser } from '../../../models/server'; -import { matrixBridge } from '../bridge'; - -export const send = async (message: IMessage): Promise => { - // Retrieve the matrix user - const userMatrixId = MatrixBridgedUser.getMatrixId(message.u._id); - - // Retrieve the matrix room - const roomMatrixId = MatrixBridgedRoom.getMatrixId(message.rid); - - if (!userMatrixId) { - throw new Error(`Could not find user matrix id for ${message.u._id}`); - } - - if (!roomMatrixId) { - throw new Error(`Could not find room matrix id for ${message.rid}`); - } - - const intent = matrixBridge.getInstance().getIntent(userMatrixId); - await intent.sendText(roomMatrixId, message.msg || '...not-supported...'); - - return message; -}; diff --git a/apps/meteor/app/federation-v2/server/matrix-client/room.ts b/apps/meteor/app/federation-v2/server/matrix-client/room.ts deleted file mode 100644 index e031de4e4b2b..000000000000 --- a/apps/meteor/app/federation-v2/server/matrix-client/room.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; -import { IRoom, IUser } from '@rocket.chat/core-typings'; - -import { MatrixBridgedRoom, MatrixBridgedUser } from '../../../models/server'; -import { matrixBridge } from '../bridge'; -import { Rooms } from '../../../models/server/raw'; - -interface ICreateRoomResult { - rid: string; - mri: string; -} - -const parametersForDirectMessagesIfNecessary = (room: IRoom, invitedUserId: string): Record => { - return room.t === RoomType.DIRECT_MESSAGE - ? { - // eslint-disable-next-line @typescript-eslint/camelcase - is_direct: true, - invite: [invitedUserId], - } - : {}; -}; - -export const create = async (inviterUser: IUser, room: IRoom, invitedUserId: string): Promise => { - // Check if this room already exists (created by another method) - // and if so, ignore the callback - const roomMatrixId = MatrixBridgedRoom.getMatrixId(room._id); - if (roomMatrixId) { - return { rid: room._id, mri: roomMatrixId }; - } - - // Retrieve the matrix user - const userMatrixId = MatrixBridgedUser.getMatrixId(inviterUser._id); - - if (!userMatrixId) { - throw new Error(`Could not find user matrix id for ${inviterUser._id}`); - } - - const intent = matrixBridge.getInstance().getIntent(userMatrixId); - - const visibility = room.t === 'p' || room.t === 'd' ? 'invite' : 'public'; - const preset = room.t === 'p' || room.t === 'd' ? 'private_chat' : 'public_chat'; - - // Create the matrix room - const matrixRoom = await intent.createRoom({ - createAsClient: true, - options: { - name: room.fname || room.name, - topic: room.topic, - visibility, - preset, - ...parametersForDirectMessagesIfNecessary(room, invitedUserId), - // eslint-disable-next-line @typescript-eslint/camelcase - creation_content: { - // eslint-disable-next-line @typescript-eslint/camelcase - was_programatically_created: true, - }, - }, - }); - // Add to the map - MatrixBridgedRoom.insert({ rid: room._id, mri: matrixRoom.room_id }); - - await Rooms.setAsBridged(room._id); - - // Add our user TODO: Doing this I think is un-needed since our user is the creator of the room. With it in.. there were errors - // await intent.invite(matrixRoom.room_id, userMatrixId); - - return { rid: room._id, mri: matrixRoom.room_id }; -}; diff --git a/apps/meteor/app/federation-v2/server/matrix-client/user.ts b/apps/meteor/app/federation-v2/server/matrix-client/user.ts deleted file mode 100644 index 9e6ba092e9b4..000000000000 --- a/apps/meteor/app/federation-v2/server/matrix-client/user.ts +++ /dev/null @@ -1,174 +0,0 @@ -import type { MatrixProfileInfo } from '@rocket.chat/forked-matrix-bot-sdk'; -import { IUser } from '@rocket.chat/core-typings'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; - -import { matrixBridge } from '../bridge'; -import { MatrixBridgedUser, MatrixBridgedRoom, Users } from '../../../models/server'; -import { addUserToRoom } from '../../../lib/server/functions'; -import { matrixClient } from '.'; -import { dataInterface } from '../data-interface'; -import { settings } from '../../../settings/server'; -import { api } from '../../../../server/sdk/api'; - -interface ICreateUserResult { - uid: string; - mui: string; - remote: boolean; -} - -const removeUselessCharsFromMatrixId = (matrixUserId = ''): string => matrixUserId.replace('@', ''); -const formatUserIdAsRCUsername = (userId = ''): string => removeUselessCharsFromMatrixId(userId.split(':')[0]); - -export const invite = async (inviterId: string, roomId: string, invitedId: string): Promise => { - console.log(`[${inviterId}-${invitedId}-${roomId}] Inviting user ${invitedId} to ${roomId}...`); - - // Find the inviter user - let bridgedInviterUser = MatrixBridgedUser.getById(inviterId); - // Get the user - const inviterUser = await dataInterface.user(inviterId); - - // Determine if the user is local or remote - let invitedUserMatrixId = invitedId; - const invitedUserDomain = invitedId.includes(':') ? invitedId.split(':').pop() : ''; - const invitedUserIsRemote = invitedUserDomain && invitedUserDomain !== settings.get('Federation_Matrix_homeserver_domain'); - - // Find the invited user in Rocket.Chats users - // TODO: this should be refactored asap, since these variable value changes lead us to confusion - let invitedUser = Users.findOneByUsername(removeUselessCharsFromMatrixId(invitedId)); - - if (!invitedUser) { - // Create the invited user - const { uid } = await matrixClient.user.createLocal(invitedUserMatrixId); - invitedUser = Users.findOneById(uid); - } - - // The inviters user doesn't yet exist in matrix - if (!bridgedInviterUser) { - console.log(`[${inviterId}-${invitedId}-${roomId}] Creating remote inviter user...`); - - // Create the missing user - bridgedInviterUser = await matrixClient.user.createRemote(inviterUser); - - console.log(`[${inviterId}-${invitedId}-${roomId}] Inviter user created as ${bridgedInviterUser.mui}...`); - } - - // Find the bridged room id - let matrixRoomId = await MatrixBridgedRoom.getMatrixId(roomId); - - // Get the room - const room = await dataInterface.room(roomId); - - if (!matrixRoomId) { - console.log(`[${inviterId}-${invitedId}-${roomId}] Creating remote room...`); - - // Create the missing room - const { mri } = await matrixClient.room.create({ _id: inviterId } as IUser, room, invitedId); - - matrixRoomId = mri; - - console.log(`[${inviterId}-${invitedId}-${roomId}] Remote room created as ${matrixRoomId}...`); - } - - // If the invited user is not remote, let's ensure it exists remotely - if (!invitedUserIsRemote) { - console.log(`[${inviterId}-${invitedId}-${roomId}] Creating remote invited user...`); - - // Check if we already have a matrix id for that user - const existingMatrixId = MatrixBridgedUser.getMatrixId(invitedUser._id); - - if (!existingMatrixId) { - const { mui } = await matrixClient.user.createRemote(invitedUser); - - invitedUserMatrixId = mui; - } else { - invitedUserMatrixId = existingMatrixId; - } - - console.log(`[${inviterId}-${invitedId}-${roomId}] Invited user created as ${invitedUserMatrixId}...`); - } - - console.log(`[${inviterId}-${invitedId}-${roomId}] Inviting the user to the room...`); - // Invite && Auto-join if the user is Rocket.Chat controlled - if (!invitedUserIsRemote) { - // Invite the user to the room - await matrixBridge.getInstance().getIntent(bridgedInviterUser.mui).invite(matrixRoomId, invitedUserMatrixId); - - console.log(`[${inviterId}-${invitedId}-${roomId}] Auto-join room...`); - - await matrixBridge.getInstance().getIntent(invitedUserMatrixId).join(matrixRoomId); - } else if (room.t !== 'd') { - // Invite the user to the room but don't wait as this is dependent on the user accepting the invite because we don't control this user - matrixBridge - .getInstance() - .getIntent(bridgedInviterUser.mui) - .invite(matrixRoomId, invitedUserMatrixId) - .catch(() => { - api.broadcast('notify.ephemeralMessage', inviterId, roomId, { - msg: TAPi18n.__('Federation_Matrix_only_owners_can_invite_users', { - postProcess: 'sprintf', - lng: settings.get('Language') || 'en', - }), - }); - }); - } - - // Add the matrix user to the invited room - addUserToRoom(roomId, invitedUser, inviterUser, false); -}; - -export const createRemote = async (u: IUser): Promise => { - const matrixUserId = `@${u.username?.toLowerCase()}:${settings.get('Federation_Matrix_homeserver_domain')}`; - - console.log(`Creating remote user ${matrixUserId}...`); - - const intent = matrixBridge.getInstance().getIntent(matrixUserId); - - await intent.ensureProfile(u.name); - - await intent.setDisplayName(`${u.username} (${u.name})`); - - const payload = { uid: u._id, mui: matrixUserId, remote: true }; - - MatrixBridgedUser.upsert({ uid: u._id }, payload); - - return payload; -}; - -const createLocalUserIfNotExists = async (userId = '', profileInfo: MatrixProfileInfo = {}): Promise => { - const existingUser = await Users.findOneByUsername(formatUserIdAsRCUsername(userId)); - - if (existingUser) { - return existingUser._id; - } - - return Users.create({ - username: removeUselessCharsFromMatrixId(userId), - type: 'user', - status: 'online', - active: true, - roles: ['user'], - name: profileInfo.displayname, - requirePasswordChange: false, - }); -}; - -export const createLocal = async (matrixUserId: string): Promise => { - console.log(`Creating local user ${matrixUserId}...`); - - const intent = matrixBridge.getInstance().getIntent(matrixUserId); - - let currentProfile: MatrixProfileInfo = {}; - - try { - currentProfile = await intent.getProfileInfo(matrixUserId); - } catch (err) { - // no-op - } - - const uid = await createLocalUserIfNotExists(matrixUserId, currentProfile); - const payload = { uid, mui: matrixUserId, remote: false }; - - MatrixBridgedUser.upsert({ uid }, payload); - - return payload; -}; diff --git a/apps/meteor/app/federation-v2/server/methods/checkBridgedRoomExists.ts b/apps/meteor/app/federation-v2/server/methods/checkBridgedRoomExists.ts deleted file mode 100644 index 7db759e1cc38..000000000000 --- a/apps/meteor/app/federation-v2/server/methods/checkBridgedRoomExists.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { MatrixBridgedRoom } from '../../../models/server'; - -export const checkBridgedRoomExists = async (matrixRoomId: string): Promise => { - const existingRoomId = MatrixBridgedRoom.getId(matrixRoomId); - - return !!existingRoomId; -}; diff --git a/apps/meteor/app/federation-v2/server/queue.ts b/apps/meteor/app/federation-v2/server/queue.ts deleted file mode 100644 index f1f0ea02061e..000000000000 --- a/apps/meteor/app/federation-v2/server/queue.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Create the queue -import { queueAsPromised } from 'fastq'; -import * as fastq from 'fastq'; - -import { IMatrixEvent } from './definitions/IMatrixEvent'; -import { MatrixEventType } from './definitions/MatrixEventType'; -import { eventHandler } from './eventHandler'; - -export const matrixEventQueue: queueAsPromised> = fastq.promise(eventHandler, 1); - -export const addToQueue = (event: IMatrixEvent): void => { - console.log(`Queueing ${event.type}...`); - - // TODO: Handle error - matrixEventQueue.push(event).catch((err) => console.error(err)); -}; diff --git a/apps/meteor/app/federation-v2/server/settings.ts b/apps/meteor/app/federation-v2/server/settings.ts deleted file mode 100644 index f10264a3ea37..000000000000 --- a/apps/meteor/app/federation-v2/server/settings.ts +++ /dev/null @@ -1,136 +0,0 @@ -import yaml from 'js-yaml'; -import { SHA256 } from 'meteor/sha'; - -import { getRegistrationInfo } from './config'; -import { Settings } from '../../models/server/raw'; -import { settings, settingsRegistry } from '../../settings/server'; - -settingsRegistry.addGroup('Federation', function () { - this.section('Matrix Bridge', async function () { - this.add('Federation_Matrix_enabled', false, { - readonly: false, - type: 'boolean', - i18nLabel: 'Federation_Matrix_enabled', - i18nDescription: 'Federation_Matrix_enabled_desc', - alert: 'Federation_Matrix_Enabled_Alert', - }); - - const uniqueId = await settings.get('uniqueID'); - const hsToken = SHA256(`hs_${uniqueId}`); - const asToken = SHA256(`as_${uniqueId}`); - - this.add('Federation_Matrix_id', `rocketchat_${uniqueId}`, { - readonly: true, - type: 'string', - i18nLabel: 'Federation_Matrix_id', - i18nDescription: 'Federation_Matrix_id_desc', - }); - - this.add('Federation_Matrix_hs_token', hsToken, { - readonly: true, - type: 'string', - i18nLabel: 'Federation_Matrix_hs_token', - i18nDescription: 'Federation_Matrix_hs_token_desc', - }); - - this.add('Federation_Matrix_as_token', asToken, { - readonly: true, - type: 'string', - i18nLabel: 'Federation_Matrix_as_token', - i18nDescription: 'Federation_Matrix_as_token_desc', - }); - - this.add('Federation_Matrix_homeserver_url', 'http://localhost:8008', { - type: 'string', - i18nLabel: 'Federation_Matrix_homeserver_url', - i18nDescription: 'Federation_Matrix_homeserver_url_desc', - alert: 'Federation_Matrix_homeserver_url_alert', - }); - - this.add('Federation_Matrix_homeserver_domain', 'local.rocket.chat', { - type: 'string', - i18nLabel: 'Federation_Matrix_homeserver_domain', - i18nDescription: 'Federation_Matrix_homeserver_domain_desc', - alert: 'Federation_Matrix_homeserver_domain_alert', - }); - - this.add('Federation_Matrix_bridge_url', 'http://host.docker.internal:3300', { - type: 'string', - i18nLabel: 'Federation_Matrix_bridge_url', - i18nDescription: 'Federation_Matrix_bridge_url_desc', - }); - - this.add('Federation_Matrix_bridge_localpart', 'rocket.cat', { - type: 'string', - i18nLabel: 'Federation_Matrix_bridge_localpart', - i18nDescription: 'Federation_Matrix_bridge_localpart_desc', - }); - - this.add('Federation_Matrix_registration_file', '', { - readonly: true, - type: 'code', - i18nLabel: 'Federation_Matrix_registration_file', - i18nDescription: 'Federation_Matrix_registration_file_desc', - }); - }); -}); - -let registrationFile = {}; - -const updateRegistrationFile = async function (): Promise { - const registrationInfo = getRegistrationInfo(); - - // eslint-disable-next-line @typescript-eslint/camelcase - const { id, hs_token, as_token, sender_localpart } = registrationInfo; - let { url } = registrationInfo; - - if (!url || !url.includes(':')) { - url = `${url}:3300`; - } - - /* eslint-disable @typescript-eslint/camelcase */ - registrationFile = { - id, - hs_token, - as_token, - url, - sender_localpart, - namespaces: { - users: [ - { - exclusive: false, - regex: '.*', - }, - ], - rooms: [ - { - exclusive: false, - regex: '.*', - }, - ], - aliases: [ - { - exclusive: false, - regex: '.*', - }, - ], - }, - }; - /* eslint-enable @typescript-eslint/camelcase */ - - // Update the registration file - await Settings.updateValueById('Federation_Matrix_registration_file', yaml.dump(registrationFile)); -}; - -settings.watchMultiple( - [ - 'Federation_Matrix_id', - 'Federation_Matrix_hs_token', - 'Federation_Matrix_as_token', - 'Federation_Matrix_homeserver_url', - 'Federation_Matrix_homeserver_domain', - 'Federation_Matrix_bridge_url', - 'Federation_Matrix_bridge_localpart', - ], - updateRegistrationFile, -); diff --git a/apps/meteor/app/federation-v2/server/startup.ts b/apps/meteor/app/federation-v2/server/startup.ts deleted file mode 100644 index a1495878788c..000000000000 --- a/apps/meteor/app/federation-v2/server/startup.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { settings } from '../../settings/server'; -import { matrixBridge } from './bridge'; -import { bridgeLogger, setupLogger } from './logger'; - -const watchChanges = (): void => { - settings.watchMultiple( - [ - 'Federation_Matrix_enabled', - 'Federation_Matrix_id', - 'Federation_Matrix_hs_token', - 'Federation_Matrix_as_token', - 'Federation_Matrix_homeserver_url', - 'Federation_Matrix_homeserver_domain', - 'Federation_Matrix_bridge_url', - 'Federation_Matrix_bridge_localpart', - ], - async ([enabled]) => { - setupLogger.info(`Federation Matrix is ${enabled ? 'enabled' : 'disabled'}`); - if (!enabled) { - await matrixBridge.stop(); - return; - } - await matrixBridge.start(); - }, - ); -}; - -export const startBridge = (): void => { - watchChanges(); - - bridgeLogger.info(`Running Federation V2: - id: ${settings.get('Federation_Matrix_id')} - bridgeUrl: ${settings.get('Federation_Matrix_bridge_url')} - homeserverURL: ${settings.get('Federation_Matrix_homeserver_url')} - homeserverDomain: ${settings.get('Federation_Matrix_homeserver_domain')} - `); -}; diff --git a/apps/meteor/app/katex/client/index.js b/apps/meteor/app/katex/client/index.ts similarity index 55% rename from apps/meteor/app/katex/client/index.js rename to apps/meteor/app/katex/client/index.ts index 139b420af26b..a2cee08f78e4 100644 --- a/apps/meteor/app/katex/client/index.js +++ b/apps/meteor/app/katex/client/index.ts @@ -1,52 +1,71 @@ import { Random } from 'meteor/random'; -import katex from 'katex'; +import KatexPackage from 'katex'; import { unescapeHTML, escapeHTML } from '@rocket.chat/string-helpers'; - import 'katex/dist/katex.min.css'; import './style.css'; +import { IMessage } from '@rocket.chat/core-typings'; class Boundary { - length() { + start: number; + + end: number; + + length(): number { return this.end - this.start; } - extract(str) { + extract(str: string): string { return str.substr(this.start, this.length()); } } +type Delimiter = { + opener: string; + closer: string; + displayMode: boolean; + enabled: () => boolean; +}; + +type OpeningDelimiter = { options: Delimiter; pos: number }; + +type LatexBoundary = { outer: Boundary; inner: Boundary }; + class Katex { - constructor(katex, { dollarSyntax, parenthesisSyntax }) { + katex: KatexPackage; + + delimitersMap: Delimiter[]; + + constructor(katex: KatexPackage, { dollarSyntax, parenthesisSyntax }: { dollarSyntax: boolean; parenthesisSyntax: boolean }) { this.katex = katex; this.delimitersMap = [ { opener: '\\[', closer: '\\]', displayMode: true, - enabled: () => parenthesisSyntax, + enabled: (): boolean => parenthesisSyntax, }, { opener: '\\(', closer: '\\)', displayMode: false, - enabled: () => parenthesisSyntax, + enabled: (): boolean => parenthesisSyntax, }, { opener: '$$', closer: '$$', displayMode: true, - enabled: () => dollarSyntax, + enabled: (): boolean => dollarSyntax, }, { opener: '$', closer: '$', displayMode: false, - enabled: () => dollarSyntax, + enabled: (): boolean => dollarSyntax, }, ]; } - findOpeningDelimiter(str, start) { + findOpeningDelimiter(str: string, start: number): OpeningDelimiter | null { const matches = this.delimitersMap .filter((options) => options.enabled()) .map((options) => ({ @@ -70,7 +89,7 @@ class Katex { return match; } - getLatexBoundaries(str, { options: { closer }, pos }) { + getLatexBoundaries(str: string, { options: { closer }, pos }: OpeningDelimiter): LatexBoundary | null { const closerIndex = str.substr(pos + closer.length).indexOf(closer); if (closerIndex < 0) { return null; @@ -92,15 +111,17 @@ class Katex { } // Searches for the first latex block in the given string - findLatex(str) { + findLatex(str: string): (LatexBoundary & { options: Delimiter }) | null { let start = 0; let openingDelimiterMatch; while ((openingDelimiterMatch = this.findOpeningDelimiter(str, start++)) != null) { const match = this.getLatexBoundaries(str, openingDelimiterMatch); - if (match && match.inner.extract(str).trim().length) { - match.options = openingDelimiterMatch.options; - return match; + if (match?.inner.extract(str).trim().length) { + return { + ...match, + options: openingDelimiterMatch.options, + }; } } @@ -109,7 +130,7 @@ class Katex { // Breaks a message to what comes before, after and to the content of a // matched latex block - extractLatex(str, match) { + extractLatex(str: string, match: LatexBoundary): { before: string; latex: string; after: string } { const before = str.substr(0, match.outer.start); const after = str.substr(match.outer.end); let latex = match.inner.extract(str); @@ -123,9 +144,9 @@ class Katex { // Takes a latex math string and the desired display mode and renders it // to HTML using the KaTeX library - renderLatex = (latex, displayMode) => { + renderLatex = (latex: string, displayMode: Delimiter['displayMode']): string => { try { - return this.katex.renderToString(latex, { + return KatexPackage.renderToString(latex, { displayMode, macros: { '\\href': '\\@secondoftwo', @@ -137,11 +158,15 @@ class Katex { }; // Takes a string and renders all latex blocks inside it - render(str, renderFunction) { + render(str: string, renderFunction: (latex: string, displayMode: Delimiter['displayMode']) => string): string { let result = ''; while (this.findLatex(str) != null) { // Find the first latex block in the string const match = this.findLatex(str); + if (!match) { + continue; + } + const parts = this.extractLatex(str, match); // Add to the reuslt what comes before the latex block as well as @@ -155,7 +180,11 @@ class Katex { return result; } - renderMessage = (message) => { + public renderMessage(message: string): string; + + public renderMessage(message: IMessage): IMessage; + + public renderMessage(message: string | IMessage): string | IMessage { if (typeof message === 'string') { return this.render(message, this.renderLatex); } @@ -170,7 +199,7 @@ class Katex { message.html = this.render(message.html, (latex, displayMode) => { const token = `=!=${Random.id()}=!=`; - message.tokens.push({ + message.tokens?.push({ token, text: this.renderLatex(latex, displayMode), }); @@ -178,14 +207,40 @@ class Katex { }); return message; - }; + } } -export const createKatexMessageRendering = (options) => { - const instance = new Katex(katex, options); - return (message) => instance.renderMessage(message); -}; +export function createKatexMessageRendering( + options: { + dollarSyntax: boolean; + parenthesisSyntax: boolean; + }, + _isMessage: true, +): (message: IMessage) => IMessage; +export function createKatexMessageRendering( + options: { + dollarSyntax: boolean; + parenthesisSyntax: boolean; + }, + _isMessage: false, +): (message: string) => string; +export function createKatexMessageRendering( + options: { + dollarSyntax: boolean; + parenthesisSyntax: boolean; + }, + _isMessage: true | false, +): ((message: string) => string) | ((message: IMessage) => IMessage) { + const instance = new Katex(KatexPackage, options); + if (_isMessage) { + return (message: IMessage): IMessage => instance.renderMessage(message); + } + return (message: string): string => instance.renderMessage(message); +} -export const getKatexHtml = (text, katex) => { - return createKatexMessageRendering({ dollarSyntax: katex.dollarSyntaxEnabled, parenthesisSyntax: katex.parenthesisSyntaxEnabled })(text); +export const getKatexHtml = (text: string, katex: { dollarSyntaxEnabled: boolean; parenthesisSyntaxEnabled: boolean }): string => { + return createKatexMessageRendering( + { dollarSyntax: katex.dollarSyntaxEnabled, parenthesisSyntax: katex.parenthesisSyntaxEnabled }, + false, + )(text); }; diff --git a/apps/meteor/app/katex/server/index.js b/apps/meteor/app/katex/server/index.ts similarity index 100% rename from apps/meteor/app/katex/server/index.js rename to apps/meteor/app/katex/server/index.ts diff --git a/apps/meteor/app/lib/client/methods/sendMessage.js b/apps/meteor/app/lib/client/methods/sendMessage.js index 8dfafeeb5f94..8d3d9bd9532e 100644 --- a/apps/meteor/app/lib/client/methods/sendMessage.js +++ b/apps/meteor/app/lib/client/methods/sendMessage.js @@ -32,9 +32,9 @@ Meteor.methods({ message.unread = true; } - // If the room is bridged, send the message to matrix only - const { bridged } = Rooms.findOne({ _id: message.rid }, { fields: { bridged: 1 } }); - if (bridged) { + // If the room is federated, send the message to matrix only + const { federated } = Rooms.findOne({ _id: message.rid }, { fields: { federated: 1 } }); + if (federated) { return; } diff --git a/apps/meteor/app/lib/server/methods/sendMessage.js b/apps/meteor/app/lib/server/methods/sendMessage.js index 91e223322b36..3cf002ba4e0b 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.js +++ b/apps/meteor/app/lib/server/methods/sendMessage.js @@ -7,13 +7,14 @@ import { hasPermission } from '../../../authorization'; import { metrics } from '../../../metrics'; import { settings } from '../../../settings'; import { messageProperties } from '../../../ui-utils'; -import { Users, Messages, Rooms } from '../../../models'; +import { Users, Messages } from '../../../models'; import { sendMessage } from '../functions'; import { RateLimiter } from '../lib'; import { canSendMessage } from '../../../authorization/server'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { api } from '../../../../server/sdk/api'; -import { matrixClient } from '../../../federation-v2/server/matrix-client'; +import { federationRoomServiceSender } from '../../../federation-v2/server'; +import { FederationRoomSenderConverter } from '../../../federation-v2/server/infrastructure/rocket-chat/converters/RoomSender'; export function executeSendMessage(uid, message) { if (message.tshow && !message.tmid) { @@ -106,10 +107,10 @@ Meteor.methods({ } try { - // If the room is bridged, send the message to matrix only - const { bridged } = Rooms.findOne({ _id: message.rid }, { fields: { bridged: 1 } }); - if (bridged) { - return matrixClient.message.send({ ...message, u: { _id: uid } }); + if (Promise.await(federationRoomServiceSender.isAFederatedRoom(message.rid))) { + return federationRoomServiceSender.sendMessageFromRocketChat( + FederationRoomSenderConverter.toSendExternalMessageDto(uid, message.rid, message), + ); } return executeSendMessage(uid, message); diff --git a/apps/meteor/app/livechat/client/index.js b/apps/meteor/app/livechat/client/index.js index 645d89170532..19a5118b809f 100644 --- a/apps/meteor/app/livechat/client/index.js +++ b/apps/meteor/app/livechat/client/index.js @@ -1,5 +1,4 @@ import '../lib/messageTypes'; -import './route'; import './voip'; import './ui'; import './tabBar'; diff --git a/apps/meteor/app/livechat/client/route.js b/apps/meteor/app/livechat/client/route.js deleted file mode 100644 index c7370c0e60e0..000000000000 --- a/apps/meteor/app/livechat/client/route.js +++ /dev/null @@ -1,61 +0,0 @@ -import { FlowRouter } from 'meteor/kadira:flow-router'; - -import { AccountBox } from '../../ui-utils'; -import '../../../client/views/omnichannel/routes'; - -export const livechatManagerRoutes = FlowRouter.group({ - prefix: '/omnichannel', - name: 'omnichannel', -}); - -export const load = () => import('./views/admin'); - -AccountBox.addRoute( - { - name: 'livechat-dashboard', - path: '/dashboard', - sideNav: 'omnichannelFlex', - i18nPageTitle: 'Livechat_Dashboard', - pageTemplate: 'livechatDashboard', - }, - livechatManagerRoutes, - load, -); - -AccountBox.addRoute( - { - name: 'livechat-departments', - path: '/departments', - sideNav: 'omnichannelFlex', - i18nPageTitle: 'Departments', - pageTemplate: 'livechatDepartments', - }, - livechatManagerRoutes, - load, -); - -AccountBox.addRoute( - { - name: 'livechat-department-edit', - path: '/departments/:_id/edit', - sideNav: 'omnichannelFlex', - i18nPageTitle: 'Edit_Department', - pageTemplate: 'livechatDepartmentForm', - customContainer: true, - }, - livechatManagerRoutes, - load, -); - -AccountBox.addRoute( - { - name: 'livechat-department-new', - path: '/departments/new', - sideNav: 'omnichannelFlex', - i18nPageTitle: 'New_Department', - pageTemplate: 'livechatDepartmentForm', - customContainer: true, - }, - livechatManagerRoutes, - load, -); diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index ab8ee8a66e42..e501782744d4 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -1,3 +1,4 @@ +import { isLivechatDepartmentProps } from '@rocket.chat/rest-typings'; import { Match, check } from 'meteor/check'; import { API } from '../../../../api/server'; @@ -14,9 +15,9 @@ import { API.v1.addRoute( 'livechat/department', - { authRequired: true }, + { authRequired: true, validateParams: isLivechatDepartmentProps }, { - get() { + async get() { const { offset, count } = this.getPaginationItems(); const { sort } = this.parseJsonQuery(); @@ -26,7 +27,7 @@ API.v1.addRoute( findDepartments({ userId: this.userId, text, - enabled, + enabled: enabled === 'true', onlyMyDepartments: onlyMyDepartments === 'true', excludeDepartmentId, pagination: { diff --git a/apps/meteor/app/models/server/raw/CustomUserStatus.ts b/apps/meteor/app/models/server/raw/CustomUserStatus.ts index be94faa2833a..d23dc8fd5a13 100644 --- a/apps/meteor/app/models/server/raw/CustomUserStatus.ts +++ b/apps/meteor/app/models/server/raw/CustomUserStatus.ts @@ -9,8 +9,11 @@ export class CustomUserStatusRaw extends BaseRaw { } // find one by name - async findOneByName(name: string, options: WithoutProjection>): Promise { - return this.findOne({ name }, options); + + async findOneByName(name: string, options?: undefined): Promise; + + async findOneByName(name: string, options?: WithoutProjection>): Promise { + return options ? this.findOne({ name }, options) : this.findOne({ name }); } // find diff --git a/apps/meteor/app/models/server/raw/Messages.js b/apps/meteor/app/models/server/raw/Messages.ts similarity index 50% rename from apps/meteor/app/models/server/raw/Messages.js rename to apps/meteor/app/models/server/raw/Messages.ts index 7f84cac25420..ee81c791180a 100644 --- a/apps/meteor/app/models/server/raw/Messages.js +++ b/apps/meteor/app/models/server/raw/Messages.ts @@ -1,10 +1,25 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; +import type { IMessage, IRoom, IUser, MessageTypesValues, ILivechatDepartment } from '@rocket.chat/core-typings'; +import type { PaginatedRequest } from '@rocket.chat/rest-typings'; +import type { + AggregationCursor, + Cursor, + FilterQuery, + FindOneOptions, + WithoutProjection, + Collection, + CollectionAggregationOptions, +} from 'mongodb'; import { BaseRaw } from './BaseRaw'; -export class MessagesRaw extends BaseRaw { - findVisibleByMentionAndRoomId(username, rid, options) { - const query = { +export class MessagesRaw extends BaseRaw { + findVisibleByMentionAndRoomId( + username: IUser['username'], + rid: IRoom['_id'], + options: WithoutProjection>, + ): Cursor { + const query: FilterQuery = { '_hidden': { $ne: true }, 'mentions.username': username, rid, @@ -13,8 +28,12 @@ export class MessagesRaw extends BaseRaw { return this.find(query, options); } - findStarredByUserAtRoom(userId, roomId, options) { - const query = { + findStarredByUserAtRoom( + userId: IUser['_id'], + roomId: IRoom['_id'], + options: WithoutProjection>, + ): Cursor { + const query: FilterQuery = { '_hidden': { $ne: true }, 'starred._id': userId, 'rid': roomId, @@ -23,21 +42,21 @@ export class MessagesRaw extends BaseRaw { return this.find(query, options); } - findByRoomIdAndType(roomId, type, options) { - const query = { + findByRoomIdAndType( + roomId: IRoom['_id'], + type: IMessage['t'], + options: WithoutProjection> = {}, + ): Cursor { + const query: FilterQuery = { rid: roomId, t: type, }; - if (options == null) { - options = {}; - } - return this.find(query, options); } - findSnippetedByRoom(roomId, options) { - const query = { + findSnippetedByRoom(roomId: IRoom['_id'], options: WithoutProjection>): Cursor { + const query: FilterQuery = { _hidden: { $ne: true }, snippeted: true, rid: roomId, @@ -46,14 +65,15 @@ export class MessagesRaw extends BaseRaw { return this.find(query, options); } - findDiscussionsByRoom(rid, options) { - const query = { rid, drid: { $exists: true } }; + // TODO: do we need this? currently not used anywhere + findDiscussionsByRoom(rid: IRoom['_id'], options: WithoutProjection>): Cursor { + const query: FilterQuery = { rid, drid: { $exists: true } }; return this.find(query, options); } - findDiscussionsByRoomAndText(rid, text, options) { - const query = { + findDiscussionsByRoomAndText(rid: IRoom['_id'], text: string, options: WithoutProjection>): Cursor { + const query: FilterQuery = { rid, drid: { $exists: true }, msg: new RegExp(escapeRegExp(text), 'i'), @@ -62,7 +82,20 @@ export class MessagesRaw extends BaseRaw { return this.find(query, options); } - findAllNumberOfTransferredRooms({ start, end, departmentId, onlyCount = false, options = {} }) { + findAllNumberOfTransferredRooms({ + start, + end, + departmentId, + onlyCount = false, + options = {}, + }: { + start: string; + end: string; + departmentId: ILivechatDepartment['_id']; + onlyCount: boolean; + options: PaginatedRequest; + }): AggregationCursor { + // FIXME: aggregation type definitions const match = { $match: { t: 'livechat_transfer_history', @@ -98,7 +131,7 @@ export class MessagesRaw extends BaseRaw { numberOfTransferredRooms: 1, }, }; - const firstParams = [match, lookup, unwind]; + const firstParams: Exclude['aggregate']>[0], undefined> = [match, lookup, unwind]; if (departmentId) { firstParams.push({ $match: { @@ -121,8 +154,8 @@ export class MessagesRaw extends BaseRaw { return this.col.aggregate(params, { allowDiskUse: true }); } - getTotalOfMessagesSentByDate({ start, end, options = {} }) { - const params = [ + getTotalOfMessagesSentByDate({ start, end, options = {} }: { start: Date; end: Date; options?: PaginatedRequest }): Promise { + const params: Exclude['aggregate']>[0], undefined> = [ { $match: { t: { $exists: false }, ts: { $gte: start, $lte: end } } }, { $lookup: { @@ -179,7 +212,7 @@ export class MessagesRaw extends BaseRaw { return this.col.aggregate(params).toArray(); } - findLivechatClosedMessages(rid, options) { + findLivechatClosedMessages(rid: IRoom['_id'], options: WithoutProjection>): Cursor { return this.find( { rid, @@ -189,32 +222,53 @@ export class MessagesRaw extends BaseRaw { ); } - async countRoomsWithStarredMessages(options) { - const [queryResult] = await this.col - .aggregate( - [{ $match: { 'starred._id': { $exists: true } } }, { $group: { _id: '$rid' } }, { $group: { _id: null, total: { $sum: 1 } } }], + async countRoomsWithStarredMessages(options: CollectionAggregationOptions): Promise { + const queryResult = await this.col + .aggregate<{ _id: null; total: number }>( + [ + { $match: { 'starred._id': { $exists: true } } }, + { $group: { _id: '$rid' } }, + { + $group: { + _id: null, + total: { $sum: 1 }, + }, + }, + ], options, ) - .toArray(); + .next(); return queryResult?.total || 0; } - async countRoomsWithPinnedMessages(options) { - const [queryResult] = await this.col - .aggregate([{ $match: { pinned: true } }, { $group: { _id: '$rid' } }, { $group: { _id: null, total: { $sum: 1 } } }], options) - .toArray(); + async countRoomsWithPinnedMessages(options: CollectionAggregationOptions): Promise { + const queryResult = await this.col + .aggregate<{ _id: null; total: number }>( + [ + { $match: { pinned: true } }, + { $group: { _id: '$rid' } }, + { + $group: { + _id: null, + total: { $sum: 1 }, + }, + }, + ], + options, + ) + .next(); return queryResult?.total || 0; } - async countE2EEMessages(options) { + async countE2EEMessages(options: WithoutProjection>): Promise { return this.find({ t: 'e2e' }, options).count(); } - findPinned(options) { - const query = { - t: { $ne: 'rm' }, + findPinned(options: WithoutProjection>): Cursor { + const query: FilterQuery = { + t: { $ne: 'rm' as MessageTypesValues }, _hidden: { $ne: true }, pinned: true, }; @@ -222,8 +276,8 @@ export class MessagesRaw extends BaseRaw { return this.find(query, options); } - findStarred(options) { - const query = { + findStarred(options: WithoutProjection>): Cursor { + const query: FilterQuery = { '_hidden': { $ne: true }, 'starred._id': { $exists: true }, }; diff --git a/apps/meteor/app/models/server/raw/Rooms.js b/apps/meteor/app/models/server/raw/Rooms.js index 0d86223b42a8..807b3d365aea 100644 --- a/apps/meteor/app/models/server/raw/Rooms.js +++ b/apps/meteor/app/models/server/raw/Rooms.js @@ -461,8 +461,8 @@ export class RoomsRaw extends BaseRaw { ]); } - setAsBridged(roomId) { - return this.updateOne({ _id: roomId }, { $set: { bridged: true } }); + setAsFederated(roomId) { + return this.updateOne({ _id: roomId }, { $set: { federated: true } }); } findByE2E(options) { diff --git a/apps/meteor/app/slashcommand-asciiarts/lib/gimme.ts b/apps/meteor/app/slashcommand-asciiarts/lib/gimme.ts index a057a5faa542..2f7e92cd53c8 100644 --- a/apps/meteor/app/slashcommand-asciiarts/lib/gimme.ts +++ b/apps/meteor/app/slashcommand-asciiarts/lib/gimme.ts @@ -1,5 +1,5 @@ import { Meteor } from 'meteor/meteor'; -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, RequiredField } from '@rocket.chat/core-typings'; import { slashCommands } from '../../utils/lib/slashCommand'; /* @@ -7,7 +7,7 @@ import { slashCommands } from '../../utils/lib/slashCommand'; * @param {Object} message - The message object */ -function Gimme(_command: 'gimme', params: string, item: IMessage): void { +function Gimme(_command: 'gimme', params: string, item: RequiredField, 'rid'>): void { const msg = item; msg.msg = `༼ つ ◕_◕ ༽つ ${params}`; Meteor.call('sendMessage', msg); diff --git a/apps/meteor/app/slashcommand-asciiarts/lib/lenny.ts b/apps/meteor/app/slashcommand-asciiarts/lib/lenny.ts index 135090952227..01f22523e416 100644 --- a/apps/meteor/app/slashcommand-asciiarts/lib/lenny.ts +++ b/apps/meteor/app/slashcommand-asciiarts/lib/lenny.ts @@ -1,4 +1,4 @@ -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, RequiredField } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { slashCommands } from '../../utils/lib/slashCommand'; @@ -7,7 +7,7 @@ import { slashCommands } from '../../utils/lib/slashCommand'; * @param {Object} message - The message object */ -function LennyFace(_command: 'lennyface', params: string, item: IMessage): void { +function LennyFace(_command: 'lennyface', params: string, item: RequiredField, 'rid'>): void { const msg = item; msg.msg = `${params} ( ͡° ͜ʖ ͡°)`; Meteor.call('sendMessage', msg); diff --git a/apps/meteor/app/slashcommand-asciiarts/lib/shrug.ts b/apps/meteor/app/slashcommand-asciiarts/lib/shrug.ts index b18e1fd1b039..89d97e6da225 100644 --- a/apps/meteor/app/slashcommand-asciiarts/lib/shrug.ts +++ b/apps/meteor/app/slashcommand-asciiarts/lib/shrug.ts @@ -1,4 +1,3 @@ -import type { IMessage } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { slashCommands } from '../../utils/lib/slashCommand'; @@ -7,13 +6,15 @@ import { slashCommands } from '../../utils/lib/slashCommand'; * @param {Object} message - The message object */ -function Shrug(_command: 'shrug', params: string, item: IMessage): void { - const msg = item; - msg.msg = `${params} ¯\\_(ツ)_/¯`; - Meteor.call('sendMessage', msg); -} - -slashCommands.add('shrug', Shrug, { - description: 'Slash_Shrug_Description', - params: 'your_message_optional', -}); +slashCommands.add( + 'shrug', + (_command: 'shrug', params, item): void => { + const msg = item; + msg.msg = `${params} ¯\\_(ツ)_/¯`; + Meteor.call('sendMessage', msg); + }, + { + description: 'Slash_Shrug_Description', + params: 'your_message_optional', + }, +); diff --git a/apps/meteor/app/slashcommand-asciiarts/lib/tableflip.ts b/apps/meteor/app/slashcommand-asciiarts/lib/tableflip.ts index c2663ec487b3..f71872060dc0 100644 --- a/apps/meteor/app/slashcommand-asciiarts/lib/tableflip.ts +++ b/apps/meteor/app/slashcommand-asciiarts/lib/tableflip.ts @@ -1,4 +1,3 @@ -import type { IMessage } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { slashCommands } from '../../utils/lib/slashCommand'; @@ -7,13 +6,15 @@ import { slashCommands } from '../../utils/lib/slashCommand'; * @param {Object} message - The message object */ -function Tableflip(_command: 'tableflip', params: string, item: IMessage): void { - const msg = item; - msg.msg = `${params} (╯°□°)╯︵ ┻━┻`; - Meteor.call('sendMessage', msg); -} - -slashCommands.add('tableflip', Tableflip, { - description: 'Slash_Tableflip_Description', - params: 'your_message_optional', -}); +slashCommands.add( + 'tableflip', + (_command, params, item): void => { + const msg = item; + msg.msg = `${params} (╯°□°)╯︵ ┻━┻`; + Meteor.call('sendMessage', msg); + }, + { + description: 'Slash_Tableflip_Description', + params: 'your_message_optional', + }, +); diff --git a/apps/meteor/app/slashcommand-asciiarts/lib/unflip.ts b/apps/meteor/app/slashcommand-asciiarts/lib/unflip.ts index 5f66b2c8c85c..bc8525103450 100644 --- a/apps/meteor/app/slashcommand-asciiarts/lib/unflip.ts +++ b/apps/meteor/app/slashcommand-asciiarts/lib/unflip.ts @@ -1,4 +1,3 @@ -import type { IMessage } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { slashCommands } from '../../utils/lib/slashCommand'; @@ -7,13 +6,15 @@ import { slashCommands } from '../../utils/lib/slashCommand'; * @param {Object} message - The message object */ -function Unflip(_command: 'unflip', params: string, item: IMessage): void { - const msg = item; - msg.msg = `${params} ┬─┬ ノ( ゜-゜ノ)`; - Meteor.call('sendMessage', msg); -} - -slashCommands.add('unflip', Unflip, { - description: 'Slash_TableUnflip_Description', - params: 'your_message_optional', -}); +slashCommands.add( + 'unflip', + (_command: 'unflip', params, item): void => { + const msg = item; + msg.msg = `${params} ┬─┬ ノ( ゜-゜ノ)`; + Meteor.call('sendMessage', msg); + }, + { + description: 'Slash_TableUnflip_Description', + params: 'your_message_optional', + }, +); diff --git a/apps/meteor/app/slashcommands-archiveroom/server/server.ts b/apps/meteor/app/slashcommands-archiveroom/server/server.ts index ae5f12ac8506..b6be536a2ce2 100644 --- a/apps/meteor/app/slashcommands-archiveroom/server/server.ts +++ b/apps/meteor/app/slashcommands-archiveroom/server/server.ts @@ -1,71 +1,72 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { IMessage } from '@rocket.chat/core-typings'; import { Rooms, Messages } from '../../models/server'; import { slashCommands } from '../../utils/lib/slashCommand'; import { api } from '../../../server/sdk/api'; import { settings } from '../../settings/server'; -function Archive(_command: 'archive', params: string, item: IMessage): void { - let channel = params.trim(); +slashCommands.add( + 'archive', + function Archive(_command, params, item): void { + let channel = params.trim(); - let room; + let room; - if (channel === '') { - room = Rooms.findOneById(item.rid); - channel = room.name; - } else { - channel = channel.replace('#', ''); - room = Rooms.findOneByName(channel); - } + if (channel === '') { + room = Rooms.findOneById(item.rid); + channel = room.name; + } else { + channel = channel.replace('#', ''); + room = Rooms.findOneByName(channel); + } - const userId = Meteor.userId(); + const userId = Meteor.userId(); - if (!userId) { - return; - } + if (!userId) { + return; + } - if (!room) { - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Channel_doesnt_exist', { - postProcess: 'sprintf', - sprintf: [channel], - lng: settings.get('Language') || 'en', - }), - }); - return; - } + if (!room) { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Channel_doesnt_exist', { + postProcess: 'sprintf', + sprintf: [channel], + lng: settings.get('Language') || 'en', + }), + }); + return; + } + + // You can not archive direct messages. + if (room.t === 'd') { + return; + } - // You can not archive direct messages. - if (room.t === 'd') { - return; - } + if (room.archived) { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Duplicate_archived_channel_name', { + postProcess: 'sprintf', + sprintf: [channel], + lng: settings.get('Language') || 'en', + }), + }); + return; + } + Meteor.call('archiveRoom', room._id); - if (room.archived) { + Messages.createRoomArchivedByRoomIdAndUser(room._id, Meteor.user()); api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Duplicate_archived_channel_name', { + msg: TAPi18n.__('Channel_Archived', { postProcess: 'sprintf', sprintf: [channel], lng: settings.get('Language') || 'en', }), }); - return; - } - Meteor.call('archiveRoom', room._id); - - Messages.createRoomArchivedByRoomIdAndUser(room._id, Meteor.user()); - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Channel_Archived', { - postProcess: 'sprintf', - sprintf: [channel], - lng: settings.get('Language') || 'en', - }), - }); -} - -slashCommands.add('archive', Archive, { - description: 'Archive', - params: '#channel', - permission: 'archive-room', -}); + }, + { + description: 'Archive', + params: '#channel', + permission: 'archive-room', + }, +); diff --git a/apps/meteor/app/slashcommands-bridge/server/index.ts b/apps/meteor/app/slashcommands-bridge/server/index.ts index c364aca53720..b9396a596312 100644 --- a/apps/meteor/app/slashcommands-bridge/server/index.ts +++ b/apps/meteor/app/slashcommands-bridge/server/index.ts @@ -1,11 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { IMessage } from '@rocket.chat/core-typings'; import { slashCommands } from '../../utils/lib/slashCommand'; -import { matrixClient } from '../../federation-v2/server/matrix-client'; +import { federationRoomServiceSender } from '../../federation-v2/server'; +import { FederationRoomSenderConverter } from '../../federation-v2/server/infrastructure/rocket-chat/converters/RoomSender'; -function Bridge(_command: 'bridge', stringParams: string, item: IMessage): void { +function Bridge(_command: 'bridge', stringParams: string | undefined, item: Record): void { if (_command !== 'bridge' || !Match.test(stringParams, String)) { return; } @@ -23,7 +23,12 @@ function Bridge(_command: 'bridge', stringParams: string, item: IMessage): void const currentUserId = Meteor.userId(); if (currentUserId) { - Promise.await(matrixClient.user.invite(currentUserId, roomId, `@${userId.replace('@', '')}`)); + const invitee = `@${userId.replace('@', '')}`; + Promise.await( + federationRoomServiceSender.inviteUserToAFederatedRoom( + FederationRoomSenderConverter.toRoomInviteUserDto(currentUserId, roomId, invitee), + ), + ); } break; diff --git a/apps/meteor/app/slashcommands-create/server/server.ts b/apps/meteor/app/slashcommands-create/server/server.ts index 6d342951b0dc..d47b84da6787 100644 --- a/apps/meteor/app/slashcommands-create/server/server.ts +++ b/apps/meteor/app/slashcommands-create/server/server.ts @@ -1,61 +1,62 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { IMessage } from '@rocket.chat/core-typings'; import { settings } from '../../settings/server'; import { Rooms } from '../../models/server'; import { slashCommands } from '../../utils/lib/slashCommand'; import { api } from '../../../server/sdk/api'; -function Create(_command: 'create', params: string, item: IMessage): void { - function getParams(str: string): string[] { - const regex = /(--(\w+))+/g; - const result = []; - let m; - while ((m = regex.exec(str)) !== null) { - if (m.index === regex.lastIndex) { - regex.lastIndex++; +slashCommands.add( + 'create', + function Create(_command: 'create', params, item): void { + function getParams(str: string): string[] { + const regex = /(--(\w+))+/g; + const result = []; + let m; + while ((m = regex.exec(str)) !== null) { + if (m.index === regex.lastIndex) { + regex.lastIndex++; + } + result.push(m[2]); } - result.push(m[2]); + return result; } - return result; - } - - const regexp = new RegExp(settings.get('UTF8_Channel_Names_Validation') as string); - - const channel = regexp.exec(params.trim()); - - if (!channel) { - return; - } - - const channelStr: string = channel ? channel[0] : ''; - if (channelStr === '') { - return; - } - const userId = Meteor.userId() as string; - - const room = Rooms.findOneByName(channelStr); - if (room != null) { - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Channel_already_exist', { - postProcess: 'sprintf', - sprintf: [channelStr], - lng: settings.get('Language') || 'en', - }), - }); - return; - } - - if (getParams(params).indexOf('private') > -1) { - return Meteor.call('createPrivateGroup', channelStr, []); - } - - Meteor.call('createChannel', channelStr, []); -} - -slashCommands.add('create', Create, { - description: 'Create_A_New_Channel', - params: '#channel', - permission: ['create-c', 'create-p'], -}); + + const regexp = new RegExp(settings.get('UTF8_Channel_Names_Validation') as string); + + const channel = regexp.exec(params.trim()); + + if (!channel) { + return; + } + + const channelStr: string = channel ? channel[0] : ''; + if (channelStr === '') { + return; + } + const userId = Meteor.userId() as string; + + const room = Rooms.findOneByName(channelStr); + if (room != null) { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Channel_already_exist', { + postProcess: 'sprintf', + sprintf: [channelStr], + lng: settings.get('Language') || 'en', + }), + }); + return; + } + + if (getParams(params).indexOf('private') > -1) { + return Meteor.call('createPrivateGroup', channelStr, []); + } + + Meteor.call('createChannel', channelStr, []); + }, + { + description: 'Create_A_New_Channel', + params: '#channel', + permission: ['create-c', 'create-p'], + }, +); diff --git a/apps/meteor/app/slashcommands-help/server/server.ts b/apps/meteor/app/slashcommands-help/server/server.ts index b1d6fea68969..00bd14934e9d 100644 --- a/apps/meteor/app/slashcommands-help/server/server.ts +++ b/apps/meteor/app/slashcommands-help/server/server.ts @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import type { IMessage } from '@rocket.chat/core-typings'; import { settings } from '../../settings/server'; import { slashCommands } from '../../utils/lib/slashCommand'; @@ -17,55 +16,57 @@ interface IHelpCommand { command: string; } -function Help(_command: 'help', _params: string, item: IMessage): void { - const userId = Meteor.userId() as string; - const user = Users.findOneById(userId); +slashCommands.add( + 'help', + function Help(_command, _params, item): void { + const userId = Meteor.userId() as string; + const user = Users.findOneById(userId); - const keys: IHelpCommand[] = [ - { - key: 'Open_channel_user_search', - command: 'Command (or Ctrl) + p OR Command (or Ctrl) + k', - }, - { - key: 'Mark_all_as_read', - command: 'Shift (or Ctrl) + ESC', - }, - { - key: 'Edit_previous_message', - command: 'Up Arrow', - }, - { - key: 'Move_beginning_message', - command: 'Command (or Alt) + Left Arrow', - }, - { - key: 'Move_beginning_message', - command: 'Command (or Alt) + Up Arrow', - }, - { - key: 'Move_end_message', - command: 'Command (or Alt) + Right Arrow', - }, - { - key: 'Move_end_message', - command: 'Command (or Alt) + Down Arrow', - }, - { - key: 'New_line_message_compose_input', - command: 'Shift + Enter', - }, - ]; - keys.forEach((key) => { - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__(key.key, { - postProcess: 'sprintf', - sprintf: [key.command], - lng: user?.language || settings.get('Language') || 'en', - }), + const keys: IHelpCommand[] = [ + { + key: 'Open_channel_user_search', + command: 'Command (or Ctrl) + p OR Command (or Ctrl) + k', + }, + { + key: 'Mark_all_as_read', + command: 'Shift (or Ctrl) + ESC', + }, + { + key: 'Edit_previous_message', + command: 'Up Arrow', + }, + { + key: 'Move_beginning_message', + command: 'Command (or Alt) + Left Arrow', + }, + { + key: 'Move_beginning_message', + command: 'Command (or Alt) + Up Arrow', + }, + { + key: 'Move_end_message', + command: 'Command (or Alt) + Right Arrow', + }, + { + key: 'Move_end_message', + command: 'Command (or Alt) + Down Arrow', + }, + { + key: 'New_line_message_compose_input', + command: 'Shift + Enter', + }, + ]; + keys.forEach((key) => { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__(key.key, { + postProcess: 'sprintf', + sprintf: [key.command], + lng: user?.language || settings.get('Language') || 'en', + }), + }); }); - }); -} - -slashCommands.add('help', Help, { - description: 'Show_the_keyboard_shortcut_list', -}); + }, + { + description: 'Show_the_keyboard_shortcut_list', + }, +); diff --git a/apps/meteor/app/slashcommands-hide/server/hide.ts b/apps/meteor/app/slashcommands-hide/server/hide.ts index 203f304c050d..398344a8c70e 100644 --- a/apps/meteor/app/slashcommands-hide/server/hide.ts +++ b/apps/meteor/app/slashcommands-hide/server/hide.ts @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import type { IMessage } from '@rocket.chat/core-typings'; import { settings } from '../../settings/server'; import { Rooms, Subscriptions, Users } from '../../models/server'; @@ -12,63 +11,65 @@ import { api } from '../../../server/sdk/api'; * @param {Object} message - The message object */ -function Hide(_command: 'hide', param: string, item: IMessage): void { - const room = param.trim(); - const userId = Meteor.userId(); - if (!userId) { - return; - } +slashCommands.add( + 'hide', + (_command: 'hide', param, item): void => { + const room = param.trim(); + const userId = Meteor.userId(); + if (!userId) { + return; + } - const user = Users.findOneById(userId); + const user = Users.findOneById(userId); - if (!user) { - return; - } + if (!user) { + return; + } - const lng = user.language || settings.get('Language') || 'en'; + const lng = user.language || settings.get('Language') || 'en'; - // if there is not a param, hide the current room - let { rid } = item; - if (room !== '') { - const [strippedRoom] = room.replace(/#|@/, '').split(' '); + // if there is not a param, hide the current room + let { rid } = item; + if (room !== '') { + const [strippedRoom] = room.replace(/#|@/, '').split(' '); - const [type] = room; + const [type] = room; - const roomObject = - type === '#' - ? Rooms.findOneByName(strippedRoom) - : Rooms.findOne({ - t: 'd', - usernames: { $all: [user.username, strippedRoom] }, - }); - if (!roomObject) { - api.broadcast('notify.ephemeralMessage', user._id, item.rid, { - msg: TAPi18n.__('Channel_doesnt_exist', { - postProcess: 'sprintf', - sprintf: [room], - lng, - }), - }); + const roomObject = + type === '#' + ? Rooms.findOneByName(strippedRoom) + : Rooms.findOne({ + t: 'd', + usernames: { $all: [user.username, strippedRoom] }, + }); + if (!roomObject) { + api.broadcast('notify.ephemeralMessage', user._id, item.rid, { + msg: TAPi18n.__('Channel_doesnt_exist', { + postProcess: 'sprintf', + sprintf: [room], + lng, + }), + }); + } + if (!Subscriptions.findOneByRoomIdAndUserId(roomObject._id, user._id, { fields: { _id: 1 } })) { + api.broadcast('notify.ephemeralMessage', user._id, item.rid, { + msg: TAPi18n.__('error-logged-user-not-in-room', { + postProcess: 'sprintf', + sprintf: [room], + lng, + }), + }); + return; + } + rid = roomObject._id; } - if (!Subscriptions.findOneByRoomIdAndUserId(roomObject._id, user._id, { fields: { _id: 1 } })) { - api.broadcast('notify.ephemeralMessage', user._id, item.rid, { - msg: TAPi18n.__('error-logged-user-not-in-room', { - postProcess: 'sprintf', - sprintf: [room], - lng, - }), - }); - return; - } - rid = roomObject._id; - } - Meteor.call('hideRoom', rid, (error: string) => { - if (error) { - return api.broadcast('notify.ephemeralMessage', user._id, item.rid, { - msg: TAPi18n.__(error, { lng }), - }); - } - }); -} - -slashCommands.add('hide', Hide, { description: 'Hide_room', params: '#room' }); + Meteor.call('hideRoom', rid, (error: string) => { + if (error) { + return api.broadcast('notify.ephemeralMessage', user._id, item.rid, { + msg: TAPi18n.__(error, { lng }), + }); + } + }); + }, + { description: 'Hide_room', params: '#room' }, +); diff --git a/apps/meteor/app/slashcommands-invite/server/server.ts b/apps/meteor/app/slashcommands-invite/server/server.ts index d54b319646a8..1bc427536e83 100644 --- a/apps/meteor/app/slashcommands-invite/server/server.ts +++ b/apps/meteor/app/slashcommands-invite/server/server.ts @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { IMessage } from '@rocket.chat/core-typings'; import { settings } from '../../settings/server'; import { slashCommands } from '../../utils/lib/slashCommand'; @@ -11,74 +10,75 @@ import { api } from '../../../server/sdk/api'; * Invite is a named function that will replace /invite commands * @param {Object} message - The message object */ - -function Invite(_command: 'invite', params: string, item: IMessage): void { - const usernames = params - .split(/[\s,]/) - .map((username) => username.replace(/(^@)|( @)/, '')) - .filter((a) => a !== ''); - if (usernames.length === 0) { - return; - } - const users = Meteor.users.find({ - username: { - $in: usernames, - }, - }); - const userId = Meteor.userId() as string; - if (users.count() === 0) { - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('User_doesnt_exist', { - postProcess: 'sprintf', - sprintf: [usernames.join(' @')], - lng: settings.get('Language') || 'en', - }), - }); - return; - } - const usersFiltered = users.fetch().filter(function (user) { - const subscription = Subscriptions.findOneByRoomIdAndUserId(item.rid, user._id, { - fields: { _id: 1 }, - }); - if (subscription == null) { - return true; +slashCommands.add( + 'invite', + (_command: 'invite', params, item): void => { + const usernames = params + .split(/[\s,]/) + .map((username) => username.replace(/(^@)|( @)/, '')) + .filter((a) => a !== ''); + if (usernames.length === 0) { + return; } - const usernameStr = user.username as string; - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Username_is_already_in_here', { - postProcess: 'sprintf', - sprintf: [usernameStr], - lng: settings.get('Language') || 'en', - }), + const users = Meteor.users.find({ + username: { + $in: usernames, + }, }); - return false; - }); - - usersFiltered.forEach(function (user) { - try { - return Meteor.call('addUserToRoom', { - rid: item.rid, - username: user.username, + const userId = Meteor.userId() as string; + if (users.count() === 0) { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('User_doesnt_exist', { + postProcess: 'sprintf', + sprintf: [usernames.join(' @')], + lng: settings.get('Language') || 'en', + }), }); - } catch ({ error }) { - if (typeof error !== 'string') { - return; + return; + } + const usersFiltered = users.fetch().filter(function (user) { + const subscription = Subscriptions.findOneByRoomIdAndUserId(item.rid, user._id, { + fields: { _id: 1 }, + }); + if (subscription == null) { + return true; } - if (error === 'cant-invite-for-direct-room') { - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Cannot_invite_users_to_direct_rooms', { lng: settings.get('Language') || 'en' }), - }); - } else { - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__(error, { lng: settings.get('Language') || 'en' }), + const usernameStr = user.username as string; + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Username_is_already_in_here', { + postProcess: 'sprintf', + sprintf: [usernameStr], + lng: settings.get('Language') || 'en', + }), + }); + return false; + }); + + usersFiltered.forEach(function (user) { + try { + return Meteor.call('addUserToRoom', { + rid: item.rid, + username: user.username, }); + } catch ({ error }) { + if (typeof error !== 'string') { + return; + } + if (error === 'cant-invite-for-direct-room') { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Cannot_invite_users_to_direct_rooms', { lng: settings.get('Language') || 'en' }), + }); + } else { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__(error, { lng: settings.get('Language') || 'en' }), + }); + } } - } - }); -} - -slashCommands.add('invite', Invite, { - description: 'Invite_user_to_join_channel', - params: '@username', - permission: 'add-user-to-joined-room', -}); + }); + }, + { + description: 'Invite_user_to_join_channel', + params: '@username', + permission: 'add-user-to-joined-room', + }, +); diff --git a/apps/meteor/app/slashcommands-inviteall/server/server.ts b/apps/meteor/app/slashcommands-inviteall/server/server.ts index a56b2c83c769..4c05b3e1b19e 100644 --- a/apps/meteor/app/slashcommands-inviteall/server/server.ts +++ b/apps/meteor/app/slashcommands-inviteall/server/server.ts @@ -5,15 +5,15 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; +import type { ISubscription, SlashCommand } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions, Users } from '../../models/server'; import { slashCommands } from '../../utils/lib/slashCommand'; import { settings } from '../../settings/server'; import { api } from '../../../server/sdk/api'; -function inviteAll(type: string): typeof slashCommands.commands[string]['callback'] { - return function inviteAll(command: string, params: string, item: IMessage): void { +function inviteAll(type: T): SlashCommand['callback'] { + return function inviteAll(command: T, params: string, item): void { if (!/invite\-all-(to|from)/.test(command)) { return; } diff --git a/apps/meteor/app/slashcommands-join/server/server.ts b/apps/meteor/app/slashcommands-join/server/server.ts index 25737a969dd4..1d65feb1f979 100644 --- a/apps/meteor/app/slashcommands-join/server/server.ts +++ b/apps/meteor/app/slashcommands-join/server/server.ts @@ -1,53 +1,54 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { IMessage } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions } from '../../models/server'; import { settings } from '../../settings/server'; import { slashCommands } from '../../utils/lib/slashCommand'; import { api } from '../../../server/sdk/api'; -function Join(_command: 'join', params: string, item: IMessage): void { - let channel = params.trim(); - if (channel === '') { - return; - } - - channel = channel.replace('#', ''); - - const userId = Meteor.userId() as string; - const user = Meteor.users.findOne(userId); - const room = Rooms.findOneByNameAndType(channel, 'c'); - - if (!user) { - return; - } - - if (!room) { - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Channel_doesnt_exist', { - postProcess: 'sprintf', - sprintf: [channel], - lng: settings.get('Language') || 'en', - }), +slashCommands.add( + 'join', + (_command: 'join', params, item): void => { + let channel = params.trim(); + if (channel === '') { + return; + } + + channel = channel.replace('#', ''); + + const userId = Meteor.userId() as string; + const user = Meteor.users.findOne(userId); + const room = Rooms.findOneByNameAndType(channel, 'c'); + + if (!user) { + return; + } + + if (!room) { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Channel_doesnt_exist', { + postProcess: 'sprintf', + sprintf: [channel], + lng: settings.get('Language') || 'en', + }), + }); + } + + const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { + fields: { _id: 1 }, }); - } - const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { - fields: { _id: 1 }, - }); - - if (subscription) { - throw new Meteor.Error('error-user-already-in-room', 'You are already in the channel', { - method: 'slashCommands', - }); - } - - Meteor.call('joinRoom', room._id); -} - -slashCommands.add('join', Join, { - description: 'Join_the_given_channel', - params: '#channel', - permission: 'view-c-room', -}); + if (subscription) { + throw new Meteor.Error('error-user-already-in-room', 'You are already in the channel', { + method: 'slashCommands', + }); + } + + Meteor.call('joinRoom', room._id); + }, + { + description: 'Join_the_given_channel', + params: '#channel', + permission: 'view-c-room', + }, +); diff --git a/apps/meteor/app/slashcommands-kick/server/server.ts b/apps/meteor/app/slashcommands-kick/server/server.ts index 5a08e53fbacd..70d7c44db543 100644 --- a/apps/meteor/app/slashcommands-kick/server/server.ts +++ b/apps/meteor/app/slashcommands-kick/server/server.ts @@ -1,54 +1,55 @@ // Kick is a named function that will replace /kick commands import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import type { IMessage } from '@rocket.chat/core-typings'; import { Users, Subscriptions } from '../../models/server'; import { settings } from '../../settings/server'; import { slashCommands } from '../../utils/lib/slashCommand'; import { api } from '../../../server/sdk/api'; -const Kick = function (_command: 'kick', params: string, item: IMessage): void { - const username = params.trim().replace('@', ''); - if (username === '') { - return; - } - const userId = Meteor.userId() as string; - const user = Users.findOneById(userId); - const lng = user?.language || settings.get('Language') || 'en'; +slashCommands.add( + 'kick', + function (_command: 'kick', params, item): void { + const username = params.trim().replace('@', ''); + if (username === '') { + return; + } + const userId = Meteor.userId() as string; + const user = Users.findOneById(userId); + const lng = user?.language || settings.get('Language') || 'en'; - const kickedUser = Users.findOneByUsernameIgnoringCase(username); + const kickedUser = Users.findOneByUsernameIgnoringCase(username); - if (kickedUser == null) { - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Username_doesnt_exist', { - postProcess: 'sprintf', - sprintf: [username], - lng, - }), - }); - return; - } + if (kickedUser == null) { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Username_doesnt_exist', { + postProcess: 'sprintf', + sprintf: [username], + lng, + }), + }); + return; + } - const subscription = Subscriptions.findOneByRoomIdAndUserId(item.rid, userId, { - fields: { _id: 1 }, - }); - if (!subscription) { - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Username_is_not_in_this_room', { - postProcess: 'sprintf', - sprintf: [username], - lng, - }), + const subscription = Subscriptions.findOneByRoomIdAndUserId(item.rid, userId, { + fields: { _id: 1 }, }); - return; - } - const { rid } = item; - Meteor.call('removeUserFromRoom', { rid, username }); -}; - -slashCommands.add('kick', Kick, { - description: 'Remove_someone_from_room', - params: '@username', - permission: 'remove-user', -}); + if (!subscription) { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Username_is_not_in_this_room', { + postProcess: 'sprintf', + sprintf: [username], + lng, + }), + }); + return; + } + const { rid } = item; + Meteor.call('removeUserFromRoom', { rid, username }); + }, + { + description: 'Remove_someone_from_room', + params: '@username', + permission: 'remove-user', + }, +); diff --git a/apps/meteor/app/slashcommands-leave/server/leave.ts b/apps/meteor/app/slashcommands-leave/server/leave.ts index 21aed9574caf..d99def1db777 100644 --- a/apps/meteor/app/slashcommands-leave/server/leave.ts +++ b/apps/meteor/app/slashcommands-leave/server/leave.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import type { IMessage } from '@rocket.chat/core-typings'; +import type { SlashCommand } from '@rocket.chat/core-typings'; import { slashCommands } from '../../utils/lib/slashCommand'; import { settings } from '../../settings/server'; @@ -11,7 +11,7 @@ import { Users } from '../../models/server'; * Leave is a named function that will replace /leave commands * @param {Object} message - The message object */ -function Leave(_command: string, _params: string, item: IMessage): void { +const Leave: SlashCommand<'leave'>['callback'] = function Leave(_command, _params, item): void { try { Meteor.call('leaveRoom', item.rid); } catch ({ error }) { @@ -24,7 +24,7 @@ function Leave(_command: string, _params: string, item: IMessage): void { msg: TAPi18n.__(error, { lng: user?.language || settings.get('Language') || 'en' }), }); } -} +}; slashCommands.add('leave', Leave, { description: 'Leave_the_current_channel', diff --git a/apps/meteor/app/slashcommands-me/server/me.ts b/apps/meteor/app/slashcommands-me/server/me.ts index 300be2ad353a..9907474dbf15 100644 --- a/apps/meteor/app/slashcommands-me/server/me.ts +++ b/apps/meteor/app/slashcommands-me/server/me.ts @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; import s from 'underscore.string'; -import { IMessage } from '@rocket.chat/core-typings'; import { slashCommands } from '../../utils/lib/slashCommand'; @@ -10,7 +9,7 @@ import { slashCommands } from '../../utils/lib/slashCommand'; */ slashCommands.add( 'me', - function Me(_command: 'me', params: string, item: IMessage): void { + function Me(_command: 'me', params, item): void { if (s.trim(params)) { const msg = item; msg.msg = `_${params}_`; diff --git a/apps/meteor/app/slashcommands-msg/server/server.ts b/apps/meteor/app/slashcommands-msg/server/server.ts index bea98e24e2ca..d1d4cc60ede0 100644 --- a/apps/meteor/app/slashcommands-msg/server/server.ts +++ b/apps/meteor/app/slashcommands-msg/server/server.ts @@ -1,7 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import type { IMessage } from '@rocket.chat/core-typings'; import { slashCommands } from '../../utils/lib/slashCommand'; import { settings } from '../../settings/server'; @@ -12,42 +11,44 @@ import { api } from '../../../server/sdk/api'; * Msg is a named function that will replace /msg commands */ -function Msg(_command: 'msg', params: string, item: IMessage): void { - const trimmedParams = params.trim(); - const separator = trimmedParams.indexOf(' '); - const userId = Meteor.userId() as string; - if (separator === -1) { - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Username_and_message_must_not_be_empty', { lng: settings.get('Language') || 'en' }), - }); - return; - } - const message = trimmedParams.slice(separator + 1); - const targetUsernameOrig = trimmedParams.slice(0, separator); - const targetUsername = targetUsernameOrig.replace('@', ''); - const targetUser = Users.findOneByUsernameIgnoringCase(targetUsername); - if (targetUser == null) { - const user = Users.findOneById(userId, { fields: { language: 1 } }); - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Username_doesnt_exist', { - postProcess: 'sprintf', - sprintf: [targetUsernameOrig], - lng: user?.language || settings.get('Language') || 'en', - }), - }); - return; - } - const { rid } = Meteor.call('createDirectMessage', targetUsername); - const msgObject = { - _id: Random.id(), - rid, - msg: message, - }; - Meteor.call('sendMessage', msgObject); -} - -slashCommands.add('msg', Msg, { - description: 'Direct_message_someone', - params: '@username ', - permission: 'create-d', -}); +slashCommands.add( + 'msg', + function Msg(_command: 'msg', params, item): void { + const trimmedParams = params.trim(); + const separator = trimmedParams.indexOf(' '); + const userId = Meteor.userId() as string; + if (separator === -1) { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Username_and_message_must_not_be_empty', { lng: settings.get('Language') || 'en' }), + }); + return; + } + const message = trimmedParams.slice(separator + 1); + const targetUsernameOrig = trimmedParams.slice(0, separator); + const targetUsername = targetUsernameOrig.replace('@', ''); + const targetUser = Users.findOneByUsernameIgnoringCase(targetUsername); + if (targetUser == null) { + const user = Users.findOneById(userId, { fields: { language: 1 } }); + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Username_doesnt_exist', { + postProcess: 'sprintf', + sprintf: [targetUsernameOrig], + lng: user?.language || settings.get('Language') || 'en', + }), + }); + return; + } + const { rid } = Meteor.call('createDirectMessage', targetUsername); + const msgObject = { + _id: Random.id(), + rid, + msg: message, + }; + Meteor.call('sendMessage', msgObject); + }, + { + description: 'Direct_message_someone', + params: '@username ', + permission: 'create-d', + }, +); diff --git a/apps/meteor/app/slashcommands-mute/server/mute.ts b/apps/meteor/app/slashcommands-mute/server/mute.ts index ed4fd5312893..0353337a162d 100644 --- a/apps/meteor/app/slashcommands-mute/server/mute.ts +++ b/apps/meteor/app/slashcommands-mute/server/mute.ts @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { IMessage } from '@rocket.chat/core-typings'; import { slashCommands } from '../../utils/lib/slashCommand'; import { settings } from '../../settings/server'; @@ -11,44 +10,46 @@ import { api } from '../../../server/sdk/api'; * Mute is a named function that will replace /mute commands */ -function Mute(_command: 'mute', params: string, item: IMessage): void { - const username = params.trim().replace('@', ''); - if (username === '') { - return; - } +slashCommands.add( + 'mute', + function Mute(_command: 'mute', params, item): void { + const username = params.trim().replace('@', ''); + if (username === '') { + return; + } - const userId = Meteor.userId() as string; - const mutedUser = Users.findOneByUsernameIgnoringCase(username); - if (mutedUser == null) { - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Username_doesnt_exist', { - postProcess: 'sprintf', - sprintf: [username], - lng: settings.get('Language') || 'en', - }), + const userId = Meteor.userId() as string; + const mutedUser = Users.findOneByUsernameIgnoringCase(username); + if (mutedUser == null) { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Username_doesnt_exist', { + postProcess: 'sprintf', + sprintf: [username], + lng: settings.get('Language') || 'en', + }), + }); + } + const subscription = Subscriptions.findOneByRoomIdAndUserId(item.rid, mutedUser._id, { + fields: { _id: 1 }, }); - } - const subscription = Subscriptions.findOneByRoomIdAndUserId(item.rid, mutedUser._id, { - fields: { _id: 1 }, - }); - if (!subscription) { - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Username_is_not_in_this_room', { - postProcess: 'sprintf', - sprintf: [username], - lng: settings.get('Language') || 'en', - }), + if (!subscription) { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Username_is_not_in_this_room', { + postProcess: 'sprintf', + sprintf: [username], + lng: settings.get('Language') || 'en', + }), + }); + return; + } + Meteor.call('muteUserInRoom', { + rid: item.rid, + username, }); - return; - } - Meteor.call('muteUserInRoom', { - rid: item.rid, - username, - }); -} - -slashCommands.add('mute', Mute, { - description: 'Mute_someone_in_room', - params: '@username', - permission: 'mute-user', -}); + }, + { + description: 'Mute_someone_in_room', + params: '@username', + permission: 'mute-user', + }, +); diff --git a/apps/meteor/app/slashcommands-mute/server/unmute.ts b/apps/meteor/app/slashcommands-mute/server/unmute.ts index 38e9229b95d0..82d513302478 100644 --- a/apps/meteor/app/slashcommands-mute/server/unmute.ts +++ b/apps/meteor/app/slashcommands-mute/server/unmute.ts @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { IMessage } from '@rocket.chat/core-typings'; import { slashCommands } from '../../utils/lib/slashCommand'; import { Users, Subscriptions } from '../../models/server'; @@ -11,43 +10,45 @@ import { api } from '../../../server/sdk/api'; * Unmute is a named function that will replace /unmute commands */ -function Unmute(_command: 'unmute', params: string, item: IMessage): void | Promise { - const username = params.trim().replace('@', ''); - if (username === '') { - return; - } - const userId = Meteor.userId() as string; - const unmutedUser = Users.findOneByUsernameIgnoringCase(username); - if (unmutedUser == null) { - return api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Username_doesnt_exist', { - postProcess: 'sprintf', - sprintf: [username], - lng: settings.get('Language') || 'en', - }), - }); - } +slashCommands.add( + 'unmute', + function Unmute(_command, params, item): void | Promise { + const username = params.trim().replace('@', ''); + if (username === '') { + return; + } + const userId = Meteor.userId() as string; + const unmutedUser = Users.findOneByUsernameIgnoringCase(username); + if (unmutedUser == null) { + return api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Username_doesnt_exist', { + postProcess: 'sprintf', + sprintf: [username], + lng: settings.get('Language') || 'en', + }), + }); + } - const subscription = Subscriptions.findOneByRoomIdAndUserId(item.rid, unmutedUser._id, { - fields: { _id: 1 }, - }); - if (!subscription) { - return api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Username_is_not_in_this_room', { - postProcess: 'sprintf', - sprintf: [username], - lng: settings.get('Language') || 'en', - }), + const subscription = Subscriptions.findOneByRoomIdAndUserId(item.rid, unmutedUser._id, { + fields: { _id: 1 }, }); - } - Meteor.call('unmuteUserInRoom', { - rid: item.rid, - username, - }); -} - -slashCommands.add('unmute', Unmute, { - description: 'Unmute_someone_in_room', - params: '@username', - permission: 'mute-user', -}); + if (!subscription) { + return api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Username_is_not_in_this_room', { + postProcess: 'sprintf', + sprintf: [username], + lng: settings.get('Language') || 'en', + }), + }); + } + Meteor.call('unmuteUserInRoom', { + rid: item.rid, + username, + }); + }, + { + description: 'Unmute_someone_in_room', + params: '@username', + permission: 'mute-user', + }, +); diff --git a/apps/meteor/app/slashcommands-open/client/client.ts b/apps/meteor/app/slashcommands-open/client/client.ts index ce0342b20c6b..14dd0ff9fb09 100644 --- a/apps/meteor/app/slashcommands-open/client/client.ts +++ b/apps/meteor/app/slashcommands-open/client/client.ts @@ -1,46 +1,47 @@ import { Meteor } from 'meteor/meteor'; import { FlowRouter } from 'meteor/kadira:flow-router'; -import type { IMessage } from '@rocket.chat/core-typings'; import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; import { slashCommands } from '../../utils/lib/slashCommand'; import { Subscriptions, ChatSubscription } from '../../models/client'; -function Open(_command: 'open', params: string, _item: IMessage): void { - const dict: Record = { - '#': ['c', 'p'], - '@': ['d'], - }; +slashCommands.add( + 'open', + function Open(_command, params): void { + const dict: Record = { + '#': ['c', 'p'], + '@': ['d'], + }; - const room = params.trim().replace(/#|@/, ''); - const type = dict[params.trim()[0]] || []; + const room = params.trim().replace(/#|@/, ''); + const type = dict[params.trim()[0]] || []; - const query = { - name: room, - ...(type && { t: { $in: type } }), - }; + const query = { + name: room, + ...(type && { t: { $in: type } }), + }; - const subscription = ChatSubscription.findOne(query); + const subscription = ChatSubscription.findOne(query); - if (subscription) { - roomCoordinator.openRouteLink(subscription.t, subscription, FlowRouter.current().queryParams); - } + if (subscription) { + roomCoordinator.openRouteLink(subscription.t, subscription, FlowRouter.current().queryParams); + } - if (type && type.indexOf('d') === -1) { - return; - } - return Meteor.call('createDirectMessage', room, function (err: Meteor.Error) { - if (err) { + if (type && type.indexOf('d') === -1) { return; } - const subscription = Subscriptions.findOne(query); - roomCoordinator.openRouteLink(subscription.t, subscription, FlowRouter.current().queryParams); - }); -} - -slashCommands.add('open', Open, { - description: 'Opens_a_channel_group_or_direct_message', - params: 'room_name', - clientOnly: true, - permission: ['view-c-room', 'view-c-room', 'view-d-room', 'view-joined-room', 'create-d'], -}); + return Meteor.call('createDirectMessage', room, function (err: Meteor.Error) { + if (err) { + return; + } + const subscription = Subscriptions.findOne(query); + roomCoordinator.openRouteLink(subscription.t, subscription, FlowRouter.current().queryParams); + }); + }, + { + description: 'Opens_a_channel_group_or_direct_message', + params: 'room_name', + clientOnly: true, + permission: ['view-c-room', 'view-c-room', 'view-d-room', 'view-joined-room', 'create-d'], + }, +); diff --git a/apps/meteor/app/slashcommands-status/client/status.ts b/apps/meteor/app/slashcommands-status/client/status.ts index 69b1dd11241e..2a47886488af 100644 --- a/apps/meteor/app/slashcommands-status/client/status.ts +++ b/apps/meteor/app/slashcommands-status/client/status.ts @@ -1,26 +1,27 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import type { IMessage } from '@rocket.chat/core-typings'; import { slashCommands } from '../../utils/lib/slashCommand'; import { settings } from '../../settings/server'; import { api } from '../../../server/sdk/api'; import { handleError } from '../../../client/lib/utils/handleError'; -function Status(_command: 'status', params: string, item: IMessage): void { - const userId = Meteor.userId() as string; +slashCommands.add( + 'status', + function Status(_command, params, item): void { + const userId = Meteor.userId() as string; - Meteor.call('setUserStatus', null, params, (err: Meteor.Error) => { - if (err) { - return handleError(err); - } - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('StatusMessage_Changed_Successfully', { lng: settings.get('Language') || 'en' }), + Meteor.call('setUserStatus', null, params, (err: Meteor.Error) => { + if (err) { + return handleError(err); + } + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('StatusMessage_Changed_Successfully', { lng: settings.get('Language') || 'en' }), + }); }); - }); -} - -slashCommands.add('status', Status, { - description: 'Slash_Status_Description', - params: 'Slash_Status_Params', -}); + }, + { + description: 'Slash_Status_Description', + params: 'Slash_Status_Params', + }, +); diff --git a/apps/meteor/app/slashcommands-status/server/status.ts b/apps/meteor/app/slashcommands-status/server/status.ts index 5bbee67baf8c..33ae7f77e1e8 100644 --- a/apps/meteor/app/slashcommands-status/server/status.ts +++ b/apps/meteor/app/slashcommands-status/server/status.ts @@ -1,36 +1,37 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import type { IMessage } from '@rocket.chat/core-typings'; import { slashCommands } from '../../utils/lib/slashCommand'; import { settings } from '../../settings/server'; import { api } from '../../../server/sdk/api'; import { Users } from '../../models/server'; -function Status(_command: 'status', params: string, item: IMessage): void { - const userId = Meteor.userId() as string; +slashCommands.add( + 'status', + function Status(_command: 'status', params, item): void { + const userId = Meteor.userId() as string; - Meteor.call('setUserStatus', null, params, (err: Meteor.Error) => { - const user = userId && Users.findOneById(userId, { fields: { language: 1 } }); - const lng = user?.language || settings.get('Language') || 'en'; + Meteor.call('setUserStatus', null, params, (err: Meteor.Error) => { + const user = userId && Users.findOneById(userId, { fields: { language: 1 } }); + const lng = user?.language || settings.get('Language') || 'en'; - if (err) { - if (err.error === 'error-not-allowed') { + if (err) { + if (err.error === 'error-not-allowed') { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('StatusMessage_Change_Disabled', { lng }), + }); + } + + throw err; + } else { api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('StatusMessage_Change_Disabled', { lng }), + msg: TAPi18n.__('StatusMessage_Changed_Successfully', { lng }), }); } - - throw err; - } else { - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('StatusMessage_Changed_Successfully', { lng }), - }); - } - }); -} - -slashCommands.add('status', Status, { - description: 'Slash_Status_Description', - params: 'Slash_Status_Params', -}); + }); + }, + { + description: 'Slash_Status_Description', + params: 'Slash_Status_Params', + }, +); diff --git a/apps/meteor/app/slashcommands-topic/client/topic.ts b/apps/meteor/app/slashcommands-topic/client/topic.ts index 8b0e121ee013..3c30011fb6ea 100644 --- a/apps/meteor/app/slashcommands-topic/client/topic.ts +++ b/apps/meteor/app/slashcommands-topic/client/topic.ts @@ -1,5 +1,4 @@ import { Meteor } from 'meteor/meteor'; -import type { IMessage } from '@rocket.chat/core-typings'; import { slashCommands } from '../../utils/lib/slashCommand'; import { ChatRoom } from '../../models/client/models/ChatRoom'; @@ -7,25 +6,27 @@ import { callbacks } from '../../../lib/callbacks'; import { hasPermission } from '../../authorization/client'; import { handleError } from '../../../client/lib/utils/handleError'; -function Topic(_command: 'topic', params: string, item: IMessage): void { - if (Meteor.isClient && hasPermission('edit-room', item.rid)) { - Meteor.call('saveRoomSettings', item.rid, 'roomTopic', params, (err: Meteor.Error) => { - if (err) { - if (Meteor.isClient) { - handleError(err); +slashCommands.add( + 'topic', + function Topic(_command: 'topic', params, item): void { + if (Meteor.isClient && hasPermission('edit-room', item.rid)) { + Meteor.call('saveRoomSettings', item.rid, 'roomTopic', params, (err: Meteor.Error) => { + if (err) { + if (Meteor.isClient) { + handleError(err); + } + throw err; } - throw err; - } - - if (Meteor.isClient) { - callbacks.run('roomTopicChanged', ChatRoom.findOne(item.rid)); - } - }); - } -} -slashCommands.add('topic', Topic, { - description: 'Slash_Topic_Description', - params: 'Slash_Topic_Params', - permission: 'edit-room', -}); + if (Meteor.isClient) { + callbacks.run('roomTopicChanged', ChatRoom.findOne(item.rid)); + } + }); + } + }, + { + description: 'Slash_Topic_Description', + params: 'Slash_Topic_Params', + permission: 'edit-room', + }, +); diff --git a/apps/meteor/app/slashcommands-topic/server/topic.ts b/apps/meteor/app/slashcommands-topic/server/topic.ts index 763861d2d20b..0256b6bb7fae 100644 --- a/apps/meteor/app/slashcommands-topic/server/topic.ts +++ b/apps/meteor/app/slashcommands-topic/server/topic.ts @@ -1,21 +1,22 @@ import { Meteor } from 'meteor/meteor'; -import type { IMessage } from '@rocket.chat/core-typings'; import { slashCommands } from '../../utils/lib/slashCommand'; import { hasPermission } from '../../authorization/server/functions/hasPermission'; -function Topic(_command: 'topic', params: string, item: IMessage): void { - if (Meteor.isServer && hasPermission(Meteor.userId() as string, 'edit-room', item.rid)) { - Meteor.call('saveRoomSettings', item.rid, 'roomTopic', params, (err: Meteor.Error) => { - if (err) { - throw err; - } - }); - } -} - -slashCommands.add('topic', Topic, { - description: 'Slash_Topic_Description', - params: 'Slash_Topic_Params', - permission: 'edit-room', -}); +slashCommands.add( + 'topic', + function Topic(_command: 'topic', params, item): void { + if (Meteor.isServer && hasPermission(Meteor.userId() as string, 'edit-room', item.rid)) { + Meteor.call('saveRoomSettings', item.rid, 'roomTopic', params, (err: Meteor.Error) => { + if (err) { + throw err; + } + }); + } + }, + { + description: 'Slash_Topic_Description', + params: 'Slash_Topic_Params', + permission: 'edit-room', + }, +); diff --git a/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts b/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts index ad3c999f7b38..2bc7318d75aa 100644 --- a/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts +++ b/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { IMessage } from '@rocket.chat/core-typings'; import { Rooms, Messages } from '../../models/server'; import { slashCommands } from '../../utils/lib/slashCommand'; @@ -9,61 +8,63 @@ import { api } from '../../../server/sdk/api'; import { roomCoordinator } from '../../../server/lib/rooms/roomCoordinator'; import { RoomMemberActions } from '../../../definition/IRoomTypeConfig'; -function Unarchive(_command: 'unarchive', params: string, item: IMessage): void { - let channel = params.trim(); - let room; +slashCommands.add( + 'unarchive', + function Unarchive(_command: 'unarchive', params, item): void { + let channel = params.trim(); + let room; - if (channel === '') { - room = Rooms.findOneById(item.rid); - channel = room.name; - } else { - channel = channel.replace('#', ''); - room = Rooms.findOneByName(channel); - } + if (channel === '') { + room = Rooms.findOneById(item.rid); + channel = room.name; + } else { + channel = channel.replace('#', ''); + room = Rooms.findOneByName(channel); + } - const userId = Meteor.userId() as string; + const userId = Meteor.userId() as string; - if (!room) { - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Channel_doesnt_exist', { - postProcess: 'sprintf', - sprintf: [channel], - lng: settings.get('Language') || 'en', - }), - }); - return; - } + if (!room) { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Channel_doesnt_exist', { + postProcess: 'sprintf', + sprintf: [channel], + lng: settings.get('Language') || 'en', + }), + }); + return; + } - // You can not archive direct messages. - if (!roomCoordinator.getRoomDirectives(room.t)?.allowMemberAction(room, RoomMemberActions.ARCHIVE)) { - return; - } + // You can not archive direct messages. + if (!roomCoordinator.getRoomDirectives(room.t)?.allowMemberAction(room, RoomMemberActions.ARCHIVE)) { + return; + } - if (!room.archived) { + if (!room.archived) { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Channel_already_Unarchived', { + postProcess: 'sprintf', + sprintf: [channel], + lng: settings.get('Language') || 'en', + }), + }); + return; + } + + Meteor.call('unarchiveRoom', room._id); + + Messages.createRoomUnarchivedByRoomIdAndUser(room._id, Meteor.user()); api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Channel_already_Unarchived', { + msg: TAPi18n.__('Channel_Unarchived', { postProcess: 'sprintf', sprintf: [channel], lng: settings.get('Language') || 'en', }), }); - return; - } - - Meteor.call('unarchiveRoom', room._id); - - Messages.createRoomUnarchivedByRoomIdAndUser(room._id, Meteor.user()); - api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Channel_Unarchived', { - postProcess: 'sprintf', - sprintf: [channel], - lng: settings.get('Language') || 'en', - }), - }); -} - -slashCommands.add('unarchive', Unarchive, { - description: 'Unarchive', - params: '#channel', - permission: 'unarchive-room', -}); + }, + { + description: 'Unarchive', + params: '#channel', + permission: 'unarchive-room', + }, +); diff --git a/apps/meteor/app/ui-sidenav/client/roomList.js b/apps/meteor/app/ui-sidenav/client/roomList.js index 902441e0b0fa..3dfdf1189230 100644 --- a/apps/meteor/app/ui-sidenav/client/roomList.js +++ b/apps/meteor/app/ui-sidenav/client/roomList.js @@ -172,7 +172,7 @@ const mergeSubRoom = (subscription) => { departmentId: 1, source: 1, queuedAt: 1, - bridged: 1, + federated: 1, }, }; @@ -214,7 +214,7 @@ const mergeSubRoom = (subscription) => { ts, source, queuedAt, - bridged, + federated, } = room; subscription.lm = subscription.lr ? new Date(Math.max(subscription.lr, lastRoomUpdate)) : lastRoomUpdate; @@ -253,7 +253,7 @@ const mergeSubRoom = (subscription) => { ts, source, queuedAt, - bridged, + federated, }); }; @@ -297,7 +297,7 @@ const mergeRoomSub = (room) => { ts, source, queuedAt, - bridged, + federated, } = room; Subscriptions.update( @@ -338,7 +338,7 @@ const mergeRoomSub = (room) => { ts, source, queuedAt, - bridged, + federated, ...getLowerCaseNames(room, sub.name, sub.fname), }, }, diff --git a/apps/meteor/app/ui-utils/client/lib/AccountBox.ts b/apps/meteor/app/ui-utils/client/lib/AccountBox.ts index dddf7636a361..a0b2e8a6ac43 100644 --- a/apps/meteor/app/ui-utils/client/lib/AccountBox.ts +++ b/apps/meteor/app/ui-utils/client/lib/AccountBox.ts @@ -2,11 +2,8 @@ import { IUActionButtonWhen, IUIActionButton } from '@rocket.chat/apps-engine/de import { ReactiveVar } from 'meteor/reactive-var'; import { Tracker } from 'meteor/tracker'; import { Meteor } from 'meteor/meteor'; -import { FlowRouter, Router } from 'meteor/kadira:flow-router'; -import { Session } from 'meteor/session'; import { SideNav } from './SideNav'; -import { appLayout } from '../../../../client/lib/appLayout'; import { applyDropdownActionButtonFilters } from '../../../ui-message/client/actionButtons/lib/applyButtonFilters'; export interface IAccountBoxItem extends Omit { @@ -71,44 +68,6 @@ export class AccountBoxBase { public getItems(): IAccountBoxItem[] { return this.items.get().filter((item: IAccountBoxItem) => applyDropdownActionButtonFilters(item)); } - - public addRoute(newRoute: any, router: any, wait = async (): Promise => null): Router { - if (router == null) { - router = FlowRouter; - } - const container = newRoute.customContainer ? 'pageCustomContainer' : 'pageContainer'; - const routeConfig = { - center: container, - pageTemplate: newRoute.pageTemplate, - i18nPageTitle: '', - pageTitle: '', - }; - - if (newRoute.i18nPageTitle != null) { - routeConfig.i18nPageTitle = newRoute.i18nPageTitle; - } - - if (newRoute.pageTitle != null) { - routeConfig.pageTitle = newRoute.pageTitle; - } - - return router.route(newRoute.path, { - name: newRoute.name, - async action() { - await wait(); - Session.set('openedRoom', null); - appLayout.renderMainLayout(routeConfig); - }, - triggersEnter: [ - (): void => { - if (newRoute.sideNav != null) { - SideNav.setFlex(newRoute.sideNav); - SideNav.openFlex(); - } - }, - ], - }); - } } export const AccountBox = new AccountBoxBase(); diff --git a/apps/meteor/app/ui-utils/client/lib/openRoom.js b/apps/meteor/app/ui-utils/client/lib/openRoom.js index 03bd35c48a67..d5d9bbd79d96 100644 --- a/apps/meteor/app/ui-utils/client/lib/openRoom.js +++ b/apps/meteor/app/ui-utils/client/lib/openRoom.js @@ -1,3 +1,4 @@ +import React from 'react'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import { FlowRouter } from 'meteor/kadira:flow-router'; @@ -16,6 +17,8 @@ import { RoomManager as NewRoomManager } from '../../../../client/lib/RoomManage import { Rooms, Subscriptions } from '../../../models/client'; import { fireGlobalEvent } from '../../../../client/lib/utils/fireGlobalEvent'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; +import MainLayout from '../../../../client/views/root/MainLayout'; +import BlazeTemplate from '../../../../client/views/root/BlazeTemplate'; window.currentTracker = undefined; @@ -32,7 +35,7 @@ export const openRoom = async function (type, name, render = true) { window.currentTracker = Tracker.autorun(async function (c) { const user = Meteor.user(); if ((user && user.username == null) || (user == null && settings.get('Accounts_AllowAnonymousRead') === false)) { - appLayout.renderMainLayout(); + appLayout.render(); return; } @@ -53,7 +56,11 @@ export const openRoom = async function (type, name, render = true) { RoomManager.open({ typeName: type + name, rid: room._id }); if (render) { - appLayout.renderMainLayout({ center: 'room' }); + appLayout.render( + + + , + ); } c.stop(); @@ -104,7 +111,11 @@ export const openRoom = async function (type, name, render = true) { } } Session.set('roomNotFound', { type, name, error }); - appLayout.renderMainLayout({ center: 'roomNotFound' }); + appLayout.render( + + + , + ); } }); }; diff --git a/apps/meteor/app/ui/client/index.ts b/apps/meteor/app/ui/client/index.ts index 37fb76779b62..2a16ec295fa7 100644 --- a/apps/meteor/app/ui/client/index.ts +++ b/apps/meteor/app/ui/client/index.ts @@ -8,8 +8,6 @@ import './lib/textarea-cursor'; import './views/app/burger.html'; import './views/app/home.html'; import './views/app/notAuthorized.html'; -import './views/app/pageContainer.html'; -import './views/app/pageCustomContainer.html'; import './views/app/roomSearch.html'; import './views/app/userSearch.html'; import './views/app/burger'; diff --git a/apps/meteor/app/ui/client/views/app/pageContainer.html b/apps/meteor/app/ui/client/views/app/pageContainer.html deleted file mode 100644 index 253092e954b2..000000000000 --- a/apps/meteor/app/ui/client/views/app/pageContainer.html +++ /dev/null @@ -1,12 +0,0 @@ - \ No newline at end of file diff --git a/apps/meteor/app/ui/client/views/app/pageCustomContainer.html b/apps/meteor/app/ui/client/views/app/pageCustomContainer.html deleted file mode 100644 index d4f59f354e06..000000000000 --- a/apps/meteor/app/ui/client/views/app/pageCustomContainer.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/apps/meteor/app/utils/lib/slashCommand.ts b/apps/meteor/app/utils/lib/slashCommand.ts index ef8175d9c5d0..9c1ed3b2741d 100644 --- a/apps/meteor/app/utils/lib/slashCommand.ts +++ b/apps/meteor/app/utils/lib/slashCommand.ts @@ -1,59 +1,23 @@ import { Meteor } from 'meteor/meteor'; -import type { IMessage } from '@rocket.chat/core-typings'; - -type SlashCommandCallback = (command: T, params: string, message: IMessage, triggerId: string) => void; - -type SlashCommandPreviewItem = { - id: string; - type: 'image' | 'video' | 'audio' | 'text' | 'other'; - value: string; -}; - -type SlashCommandPreviews = { - i18nTitle: string; - items: SlashCommandPreviewItem[]; -}; - -type SlashCommandPreviewer = (command: string, params: string, message: IMessage) => SlashCommandPreviews | undefined; - -type SlashCommandPreviewCallback = ( - command: string, - params: string, - message: IMessage, - preview: SlashCommandPreviewItem, - triggerId: string, -) => void; - -type SlashCommandOptions = { - params?: string; - description?: string; - permission?: string | string[]; - clientOnly?: boolean; -}; - -type SlashCommand = { - command: T; - callback?: SlashCommandCallback; - params: SlashCommandOptions['params']; - description: SlashCommandOptions['description']; - permission: SlashCommandOptions['permission']; - clientOnly?: SlashCommandOptions['clientOnly']; - result?: (err: Meteor.Error, result: never, data: { cmd: T; params: string; msg: IMessage }) => void; - providesPreview: boolean; - previewer?: SlashCommandPreviewer; - previewCallback?: SlashCommandPreviewCallback; -}; +import type { + IMessage, + SlashCommand, + SlashCommandOptions, + RequiredField, + SlashCommandPreviewItem, + SlashCommandPreviews, +} from '@rocket.chat/core-typings'; export const slashCommands = { - commands: {} as Record>, + commands: {} as Record, add( - command: T, + command: string, callback?: SlashCommand['callback'], options: SlashCommandOptions = {}, - result?: SlashCommand['result'], + result?: SlashCommand['result'], providesPreview = false, - previewer?: SlashCommand['previewer'], - previewCallback?: SlashCommand['previewCallback'], + previewer?: SlashCommand['previewer'], + previewCallback?: SlashCommand['previewCallback'], ): void { this.commands[command] = { command, @@ -66,9 +30,9 @@ export const slashCommands = { providesPreview, previewer, previewCallback, - } as SlashCommand; + } as SlashCommand; }, - run(command: string, params: string, message: IMessage, triggerId: string): void { + run(command: string, params: string, message: RequiredField, 'rid'>, triggerId?: string | undefined): void { const cmd = this.commands[command]; if (typeof cmd?.callback !== 'function') { return; diff --git a/apps/meteor/client/components/Katex.tsx b/apps/meteor/client/components/Katex.tsx index 6a66a1157191..a7cb3f279f4b 100644 --- a/apps/meteor/client/components/Katex.tsx +++ b/apps/meteor/client/components/Katex.tsx @@ -11,7 +11,7 @@ type KatexProps = { }; const Katex = ({ text, options }: KatexProps): ReactElement => ( - + ); export default memo(Katex); diff --git a/apps/meteor/client/components/Message/MessageBodyRender/BigEmoji.tsx b/apps/meteor/client/components/Message/MessageBodyRender/BigEmoji.tsx deleted file mode 100644 index 65b09cf9aa54..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/BigEmoji.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { BigEmoji as ASTBigEmoji } from '@rocket.chat/message-parser'; -import React, { ReactElement } from 'react'; - -import MessageEmoji from '../MessageEmoji'; - -type BigEmojiProps = { - value: ASTBigEmoji['value']; - isThreadPreview?: boolean; -}; - -const BigEmoji = ({ value, isThreadPreview }: BigEmojiProps): ReactElement => ( - <> - {value.map((block, index) => ( - - ))} - -); - -export default BigEmoji; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/Bold.tsx b/apps/meteor/client/components/Message/MessageBodyRender/Bold.tsx deleted file mode 100644 index 085cb203ea39..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/Bold.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Bold as ASTBold } from '@rocket.chat/message-parser'; -import React, { FC } from 'react'; - -import Italic from './Italic'; -import Link from './Link'; -import PlainText from './PlainText'; -import Strike from './Strike'; - -const Bold: FC<{ value: ASTBold['value'] }> = ({ value = [] }) => ( - - {value.map((block, index) => { - switch (block.type) { - case 'LINK': - return ; - case 'PLAIN_TEXT': - return ; - case 'STRIKE': - return <Strike key={index} value={block.value} />; - case 'ITALIC': - return <Italic key={index} value={block.value} />; - default: - return null; - } - })} - </strong> -); - -export default Bold; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/CodeLine.tsx b/apps/meteor/client/components/Message/MessageBodyRender/CodeLine.tsx deleted file mode 100644 index 275d970db306..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/CodeLine.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { CodeLine as ASTCodeLine } from '@rocket.chat/message-parser'; -import React, { FC } from 'react'; - -const CodeLine: FC<{ value: ASTCodeLine['value'] }> = ({ value }) => <div>{value.type === 'PLAIN_TEXT' && value.value}</div>; - -export default CodeLine; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/Heading.tsx b/apps/meteor/client/components/Message/MessageBodyRender/Heading.tsx deleted file mode 100644 index 0c67e5252e10..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/Heading.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Heading as ASTHeading } from '@rocket.chat/message-parser'; -import React, { FC } from 'react'; - -import PlainText from './PlainText'; - -const Heading: FC<{ value: ASTHeading['value']; level: ASTHeading['level'] }> = ({ value = [], level = 1 }) => { - const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements; - - return ( - <HeadingTag> - {value.map((block, index) => { - switch (block.type) { - case 'PLAIN_TEXT': - return <PlainText key={index} value={block.value} />; - default: - return null; - } - })} - </HeadingTag> - ); -}; - -export default Heading; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/Image.tsx b/apps/meteor/client/components/Message/MessageBodyRender/Image.tsx deleted file mode 100644 index d98db0d46b93..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/Image.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Image as ASTImage } from '@rocket.chat/message-parser'; -import React, { FC } from 'react'; - -type ImageProps = { - value: ASTImage['value']; -}; - -const style = { - maxWidth: '100%', -}; - -const Image: FC<ImageProps> = ({ value }) => { - const { src, label } = value; - return ( - <a href={src.value} target='_blank' rel='noopener noreferrer'> - <img src={src.value} data-title={src.value} alt={String(label.value)} style={style} /> - </a> - ); -}; - -export default Image; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/Inline.tsx b/apps/meteor/client/components/Message/MessageBodyRender/Inline.tsx deleted file mode 100644 index caf183f4802b..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/Inline.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Paragraph as ASTParagraph } from '@rocket.chat/message-parser'; -import React, { FC } from 'react'; - -import MessageEmoji from '../MessageEmoji'; -import Bold from './Bold'; -import Image from './Image'; -import InlineCode from './InlineCode'; -import Italic from './Italic'; -import Link from './Link'; -import Mention from './Mention'; -import MentionChannel from './MentionChannel'; -import PlainText from './PlainText'; -import Strike from './Strike'; -import { useMessageBodyIsThreadPreview } from './contexts/MessageBodyContext'; - -const Inline: FC<{ value: ASTParagraph['value'] }> = ({ value = [] }) => { - const isThreadPreview = useMessageBodyIsThreadPreview(); - return ( - <> - {value.map((block, index) => { - switch (block.type) { - case 'IMAGE': - return <Image key={index} value={block.value} />; - case 'PLAIN_TEXT': - return <PlainText key={index} value={block.value} />; - case 'BOLD': - return <Bold key={index} value={block.value} />; - case 'STRIKE': - return <Strike key={index} value={block.value} />; - case 'ITALIC': - return <Italic key={index} value={block.value} />; - case 'LINK': - return <Link key={index} value={block.value} />; - case 'MENTION_USER': - return <Mention key={index} value={block.value} />; - case 'MENTION_CHANNEL': - return <MentionChannel key={index} value={block.value} />; - case 'EMOJI': - return <MessageEmoji isThreadPreview={isThreadPreview} key={index} emojiHandle={`:${block.value.value}:`} />; - case 'INLINE_CODE': - return <InlineCode key={index} value={block.value} />; - default: - return null; - } - })} - </> - ); -}; - -export default Inline; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/InlineCode.tsx b/apps/meteor/client/components/Message/MessageBodyRender/InlineCode.tsx deleted file mode 100644 index c4bceb55adc8..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/InlineCode.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { InlineCode as ASTInlineCode } from '@rocket.chat/message-parser'; -import React, { ReactElement, FC } from 'react'; - -import PlainText from './PlainText'; - -const InlineCode: FC<{ value: ASTInlineCode['value'] }> = ({ value }) => ( - <code className='code-colors inline'> - {((block): ReactElement | null => { - switch (block.type) { - case 'PLAIN_TEXT': - return <PlainText value={block.value} />; - default: - return null; - } - })(value)} - </code> -); - -export default InlineCode; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/Italic.tsx b/apps/meteor/client/components/Message/MessageBodyRender/Italic.tsx deleted file mode 100644 index 3d98a4300776..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/Italic.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Italic as ASTItalic } from '@rocket.chat/message-parser'; -import React, { FC } from 'react'; - -import Bold from './Bold'; -import Link from './Link'; -import PlainText from './PlainText'; -import Strike from './Strike'; - -const Italic: FC<{ value: ASTItalic['value'] }> = ({ value = [] }) => ( - <i> - {value.map((block, index) => { - switch (block.type) { - case 'LINK': - return <Link key={index} value={block.value} />; - case 'PLAIN_TEXT': - return <PlainText key={index} value={block.value} />; - case 'STRIKE': - return <Strike key={index} value={block.value} />; - case 'BOLD': - return <Bold key={index} value={block.value} />; - - default: - return null; - } - })} - </i> -); - -export default Italic; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/Link.tsx b/apps/meteor/client/components/Message/MessageBodyRender/Link.tsx deleted file mode 100644 index edf528ba98f5..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/Link.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Link as ASTLink } from '@rocket.chat/message-parser'; -import React, { FC } from 'react'; - -import { baseURI } from '../../../lib/baseURI'; -import Bold from './Bold'; -import Italic from './Italic'; -import PlainText from './PlainText'; -import Strike from './Strike'; - -type LinkProps = { - value: ASTLink['value']; -}; - -const Link: FC<LinkProps> = ({ value }) => { - const { src, label } = value; - const target = src.value.indexOf(baseURI) === 0 ? '' : '_blank'; - return ( - <a href={src.value} data-title={src.value} target={target} rel='noopener noreferrer'> - {((block: ASTLink['value']['label']): JSX.Element | string | null => { - switch (block.type) { - case 'PLAIN_TEXT': - return <PlainText value={block.value} />; - case 'STRIKE': - return <Strike value={block.value} />; - case 'ITALIC': - return <Italic value={block.value} />; - case 'BOLD': - return <Bold value={block.value} />; - default: - return null; - } - })(label)} - </a> - ); -}; - -export default Link; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/Mention.tsx b/apps/meteor/client/components/Message/MessageBodyRender/Mention.tsx deleted file mode 100644 index b1a539e24cbe..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/Mention.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { UserMention as ASTUserMention } from '@rocket.chat/message-parser'; -import { useUserId } from '@rocket.chat/ui-contexts'; -import React, { FC, memo } from 'react'; - -import { useMessageBodyUserMentions, useMessageBodyMentionClick } from './contexts/MessageBodyContext'; - -const Mention: FC<{ value: ASTUserMention['value'] }> = ({ value: { value: mention } }) => { - const uid = useUserId(); - const mentions = useMessageBodyUserMentions(); - const mentioned = mentions.find((mentioned) => mentioned.username === mention); - const onUserMentionClick = useMessageBodyMentionClick(); - const classNames = ['mention-link']; - if (mention === 'all') { - classNames.push('mention-link--all'); - classNames.push('mention-link--group'); - } else if (mention === 'here') { - classNames.push('mention-link--here'); - classNames.push('mention-link--group'); - } else if (mentioned && mentioned._id === uid) { - classNames.push('mention-link--me'); - classNames.push('mention-link--user'); - } else { - classNames.push('mention-link--user'); - } - return ( - <> - {mentioned && ( - <span - onClick={classNames.includes('mention-link--user') ? onUserMentionClick(mention) : undefined} - className={classNames.join(' ')} - > - {mentioned.name || mention} - </span> - )} - {!mentioned && `@${mention}`} - </> - ); -}; - -export default memo(Mention); diff --git a/apps/meteor/client/components/Message/MessageBodyRender/MentionChannel.tsx b/apps/meteor/client/components/Message/MessageBodyRender/MentionChannel.tsx deleted file mode 100644 index 40e39a9beb51..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/MentionChannel.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { UserMention as ASTUserMention } from '@rocket.chat/message-parser'; -import React, { FC, memo } from 'react'; - -import { useMessageBodyChannelMentions, useMessageBodyChannelMentionClick } from './contexts/MessageBodyContext'; - -const Mention: FC<{ value: ASTUserMention['value'] }> = ({ value: { value: mention } }) => { - const mentions = useMessageBodyChannelMentions(); - const mentioned = mentions.find((mentioned) => mentioned.name === mention); - const onChannelMentionClick = useMessageBodyChannelMentionClick(); - - return ( - <> - {mentioned && ( - <span onClick={onChannelMentionClick(mentioned._id)} className='mention-link mention-link--room'> - #{mention} - </span> - )} - {!mentioned && `#${mention}`} - </> - ); -}; - -export default memo(Mention); diff --git a/apps/meteor/client/components/Message/MessageBodyRender/OrderedList.tsx b/apps/meteor/client/components/Message/MessageBodyRender/OrderedList.tsx deleted file mode 100644 index 742abd1dca9d..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/OrderedList.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { OrderedList as ASTOrderedList } from '@rocket.chat/message-parser'; -import React, { FC } from 'react'; - -import Inline from './Inline'; - -const OrderedList: FC<{ value: ASTOrderedList['value'] }> = ({ value }) => ( - <ol> - {value.map(({ value, number }, index) => ( - <li key={index} value={Number(number)}> - <Inline value={value} /> - </li> - ))} - </ol> -); - -export default OrderedList; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/Paragraph.tsx b/apps/meteor/client/components/Message/MessageBodyRender/Paragraph.tsx deleted file mode 100644 index 75fbe9cf62dc..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/Paragraph.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Paragraph as ASTParagraph } from '@rocket.chat/message-parser'; -import React, { FC } from 'react'; - -import Inline from './Inline'; - -const Paragraph: FC<{ value: ASTParagraph['value'] }> = ({ value = [] }) => ( - <p> - <Inline value={value} /> - </p> -); - -export default Paragraph; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/Quote.tsx b/apps/meteor/client/components/Message/MessageBodyRender/Quote.tsx deleted file mode 100644 index 5250816825bf..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/Quote.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { css } from '@rocket.chat/css-in-js'; -import { Box } from '@rocket.chat/fuselage'; -import colors from '@rocket.chat/fuselage-tokens/colors'; -import { Quote as ASTQuote } from '@rocket.chat/message-parser'; -import React, { FC } from 'react'; - -import Paragraph from './Paragraph'; - -const hover = css` - &:hover, - &:focus { - background: ${colors.n200} !important; - border-color: ${colors.n300} !important; - border-inline-start-color: ${colors.n600} !important; - } -`; - -const Quote: FC<{ value: ASTQuote['value'] }> = ({ value }) => ( - <Box - is='blockquote' - className={hover} - pi='x8' - borderRadius='x2' - borderWidth='x2' - borderStyle='solid' - backgroundColor='neutral-100' - borderColor='neutral-200' - borderInlineStartColor='neutral-600' - > - {value.map((item, index) => ( - <Paragraph key={index} value={item.value} /> - ))} - </Box> -); - -export default Quote; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/Strike.tsx b/apps/meteor/client/components/Message/MessageBodyRender/Strike.tsx deleted file mode 100644 index 497917565222..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/Strike.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Strike as ASTStrike } from '@rocket.chat/message-parser'; -import React, { FC } from 'react'; - -import Bold from './Bold'; -import Italic from './Italic'; -import Link from './Link'; -import PlainText from './PlainText'; - -const Strike: FC<{ value: ASTStrike['value'] }> = ({ value = [] }) => ( - <del> - {value.map((block, index) => { - switch (block.type) { - case 'LINK': - return <Link key={index} value={block.value} />; - case 'PLAIN_TEXT': - return <PlainText key={index} value={block.value} />; - case 'BOLD': - return <Bold key={index} value={block.value} />; - case 'ITALIC': - return <Italic key={index} value={block.value} />; - default: - return null; - } - })} - </del> -); - -export default Strike; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/TaskList.tsx b/apps/meteor/client/components/Message/MessageBodyRender/TaskList.tsx deleted file mode 100644 index f35be1beb974..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/TaskList.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { CheckBox } from '@rocket.chat/fuselage'; -import { Tasks as ASTTasks } from '@rocket.chat/message-parser'; -import React, { FC } from 'react'; - -import Inline from './Inline'; - -const TaksList: FC<{ value: ASTTasks['value'] }> = ({ value }) => ( - <ul - style={{ - listStyle: 'none', - marginLeft: 0, - paddingLeft: 0, - }} - > - {value.map((item) => ( - <li> - <CheckBox checked={item.status} /> <Inline value={item.value} /> - </li> - ))} - </ul> -); - -export default TaksList; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/UnorderedList.tsx b/apps/meteor/client/components/Message/MessageBodyRender/UnorderedList.tsx deleted file mode 100644 index e2a3076fa057..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/UnorderedList.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { UnorderedList as ASTUnorderedList } from '@rocket.chat/message-parser'; -import React, { FC } from 'react'; - -import Inline from './Inline'; - -const UnorderedList: FC<{ value: ASTUnorderedList['value'] }> = ({ value }) => ( - <ul> - {value.map((item, index) => ( - <li key={index}> - <Inline value={item.value} /> - </li> - ))} - </ul> -); - -export default UnorderedList; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/definitions/ChannelMention.ts b/apps/meteor/client/components/Message/MessageBodyRender/definitions/ChannelMention.ts deleted file mode 100644 index e2213cbf0df6..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/definitions/ChannelMention.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { IRoom } from '@rocket.chat/core-typings'; - -export type ChannelMention = Pick<IRoom, '_id' | 'name'>; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/definitions/UserMention.ts b/apps/meteor/client/components/Message/MessageBodyRender/definitions/UserMention.ts deleted file mode 100644 index 3fdabc0446a9..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/definitions/UserMention.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; - -export type UserMention = Pick<IUser, '_id' | 'name' | 'username'>; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/index.tsx b/apps/meteor/client/components/Message/MessageBodyRender/index.tsx deleted file mode 100644 index 96e78d4daf2c..000000000000 --- a/apps/meteor/client/components/Message/MessageBodyRender/index.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { BigEmoji as ASTBigEmoji, MarkdownAST } from '@rocket.chat/message-parser'; -import React, { FC, memo, MouseEvent } from 'react'; - -import BigEmoji from './BigEmoji'; -import Code from './Code'; -import Heading from './Heading'; -import OrderedList from './OrderedList'; -import Paragraph from './Paragraph'; -import Quote from './Quote'; -import TaskList from './TaskList'; -import UnorderedList from './UnorderedList'; -import { MessageBodyContext } from './contexts/MessageBodyContext'; -import { ChannelMention } from './definitions/ChannelMention'; -import { UserMention } from './definitions/UserMention'; - -type BodyProps = { - tokens: MarkdownAST; - mentions: UserMention[]; - channels: ChannelMention[]; - onUserMentionClick?: (username: string) => (e: MouseEvent<HTMLDivElement>) => void; - onChannelMentionClick?: (id: string) => (e: MouseEvent<HTMLDivElement>) => void; - isThreadPreview?: boolean; -}; - -const isBigEmoji = (tokens: MarkdownAST): tokens is [ASTBigEmoji] => tokens.length === 1 && tokens[0].type === 'BIG_EMOJI'; - -const MessageBodyRender: FC<BodyProps> = ({ - tokens, - mentions = [], - channels = [], - onUserMentionClick, - onChannelMentionClick, - isThreadPreview, -}) => { - if (isBigEmoji(tokens)) { - return <BigEmoji value={tokens[0].value} isThreadPreview={isThreadPreview} />; - } - - return ( - <MessageBodyContext.Provider value={{ mentions, channels, onUserMentionClick, onChannelMentionClick, isThreadPreview }}> - {tokens.map((block, index) => { - if (block.type === 'UNORDERED_LIST') { - return <UnorderedList value={block.value} key={index} />; - } - - if (block.type === 'QUOTE') { - return <Quote value={block.value} key={index} />; - } - if (block.type === 'TASKS') { - return <TaskList value={block.value} key={index} />; - } - - if (block.type === 'ORDERED_LIST') { - return <OrderedList value={block.value} key={index} />; - } - - if (block.type === 'PARAGRAPH') { - return <Paragraph value={block.value} key={index} />; - } - - if (block.type === 'CODE') { - return <Code {...block} key={index} />; - } - - if (block.type === 'HEADING') { - return <Heading value={block.value} level={block.level} key={index} />; - } - - if (block.type === 'LINE_BREAK') { - return <br key={index} />; - } - - return null; - })} - </MessageBodyContext.Provider> - ); -}; - -export default memo(MessageBodyRender); diff --git a/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts b/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts index a80ea72447ab..1d87a9f6e6da 100644 --- a/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts +++ b/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts @@ -27,9 +27,8 @@ export const useDepartmentsList = ( const t = useTranslation(); const [itemsList, setItemsList] = useState(() => new RecordList<ILivechatDepartmentRecord>()); const reload = useCallback(() => setItemsList(new RecordList<ILivechatDepartmentRecord>()), []); - const endpoint = 'livechat/department'; - const getDepartments = useEndpoint('GET', endpoint); + const getDepartments = useEndpoint('GET', 'livechat/department'); useComponentDidUpdate(() => { options && reload(); @@ -44,7 +43,7 @@ export const useDepartmentsList = ( count: end + start, sort: `{ "name": 1 }`, excludeDepartmentId: options.excludeDepartmentId, - enabled: options.enabled, + enabled: options.enabled ? 'true' : 'false', }); const items = departments diff --git a/apps/meteor/client/components/UserStatusMenu.tsx b/apps/meteor/client/components/UserStatusMenu.tsx index c435f26cc8e1..5df7dd7c33cc 100644 --- a/apps/meteor/client/components/UserStatusMenu.tsx +++ b/apps/meteor/client/components/UserStatusMenu.tsx @@ -42,7 +42,7 @@ const UserStatusMenu = ({ ]; if (allowInvisibleStatus) { - statuses.push([UserStatusType.OFFLINE, renderOption(UserStatusType.OFFLINE, t('Invisible'))]); + statuses.push([UserStatusType.OFFLINE, renderOption(UserStatusType.OFFLINE, t('Offline'))]); } return statuses; diff --git a/apps/meteor/client/components/gazzodown/Markup.tsx b/apps/meteor/client/components/gazzodown/Markup.tsx new file mode 100644 index 000000000000..1bd557c0ae55 --- /dev/null +++ b/apps/meteor/client/components/gazzodown/Markup.tsx @@ -0,0 +1,55 @@ +import * as MessageParser from '@rocket.chat/message-parser'; +import React, { memo, ReactElement } from 'react'; + +import BigEmojiBlock from './blocks/BigEmojiBlock'; +import CodeBlock from './blocks/CodeBlock'; +import HeadingBlock from './blocks/HeadingBlock'; +import OrderedListBlock from './blocks/OrderedListBlock'; +import ParagraphBlock from './blocks/ParagraphBlock'; +import QuoteBlock from './blocks/QuoteBlock'; +import TaskList from './blocks/TaskListBlock'; +import UnorderedListBlock from './blocks/UnorderedListBlock'; + +type MarkupProps = { + tokens: MessageParser.MarkdownAST; +}; + +const Markup = ({ tokens }: MarkupProps): ReactElement => ( + <> + {tokens.map((block, index) => { + switch (block.type) { + case 'BIG_EMOJI': + return <BigEmojiBlock key={index} emojis={block.value} />; + + case 'PARAGRAPH': + return <ParagraphBlock key={index} children={block.value} />; + + case 'HEADING': + return <HeadingBlock key={index} level={block.level} children={block.value} />; + + case 'UNORDERED_LIST': + return <UnorderedListBlock key={index} items={block.value} />; + + case 'ORDERED_LIST': + return <OrderedListBlock key={index} items={block.value} />; + + case 'TASKS': + return <TaskList key={index} tasks={block.value} />; + + case 'QUOTE': + return <QuoteBlock key={index} children={block.value} />; + + case 'CODE': + return <CodeBlock key={index} language={block.language} lines={block.value} />; + + case 'LINE_BREAK': + return <br key={index} />; + + default: + return null; + } + })} + </> +); + +export default memo(Markup); diff --git a/apps/meteor/client/components/Message/MessageBodyRender/contexts/MessageBodyContext.ts b/apps/meteor/client/components/gazzodown/MarkupInteractionContext.ts similarity index 62% rename from apps/meteor/client/components/Message/MessageBodyRender/contexts/MessageBodyContext.ts rename to apps/meteor/client/components/gazzodown/MarkupInteractionContext.ts index f257d210299e..70d6cb103d27 100644 --- a/apps/meteor/client/components/Message/MessageBodyRender/contexts/MessageBodyContext.ts +++ b/apps/meteor/client/components/gazzodown/MarkupInteractionContext.ts @@ -1,38 +1,35 @@ +import { IRoom, IUser } from '@rocket.chat/core-typings'; import { createContext, useContext, MouseEvent } from 'react'; -import { ChannelMention } from '../definitions/ChannelMention'; -import { UserMention } from '../definitions/UserMention'; +type UserMention = Pick<IUser, '_id' | 'name' | 'username'>; +type ChannelMention = Pick<IRoom, '_id' | 'name'>; -type MessageBodyContextType = { +type MarkupInteractionContextValue = { mentions?: UserMention[]; channels?: ChannelMention[]; - isThreadPreview?: boolean; onUserMentionClick?: (username: string) => (e: MouseEvent<HTMLDivElement>) => void; onChannelMentionClick?: (id: string) => (e: MouseEvent<HTMLDivElement>) => void; }; -export const MessageBodyContext = createContext<MessageBodyContextType>({ +export const MarkupInteractionContext = createContext<MarkupInteractionContextValue>({ mentions: [], channels: [], }); -export const useMessageBodyContext = (): MessageBodyContextType => useContext(MessageBodyContext); - -export const useMessageBodyIsThreadPreview = (): MessageBodyContextType['isThreadPreview'] => - useContext(MessageBodyContext).isThreadPreview; +export const useMarkupInteractionContext = (): MarkupInteractionContextValue => useContext(MarkupInteractionContext); export const useMessageBodyUserMentions = (): UserMention[] => { - const { mentions = [] } = useMessageBodyContext(); + const { mentions = [] } = useMarkupInteractionContext(); return mentions; }; export const useMessageBodyChannelMentions = (): ChannelMention[] => { - const { channels = [] } = useMessageBodyContext(); + const { channels = [] } = useMarkupInteractionContext(); return channels; }; export const useMessageBodyMentionClick = (): ((username: string) => (e: MouseEvent<HTMLDivElement>) => void) => { - const { onUserMentionClick } = useMessageBodyContext(); + const { onUserMentionClick } = useMarkupInteractionContext(); if (!onUserMentionClick) { console.warn('onUserMentionClick is not defined'); return (username: string) => (): void => { @@ -43,7 +40,7 @@ export const useMessageBodyMentionClick = (): ((username: string) => (e: MouseEv }; export const useMessageBodyChannelMentionClick = (): ((id: string) => (e: MouseEvent<HTMLDivElement>) => void) => { - const { onChannelMentionClick } = useMessageBodyContext(); + const { onChannelMentionClick } = useMarkupInteractionContext(); if (!onChannelMentionClick) { console.warn('onChannelMentionClick is not defined'); return (username: string) => (): void => { diff --git a/apps/meteor/client/components/gazzodown/PreviewMarkup.tsx b/apps/meteor/client/components/gazzodown/PreviewMarkup.tsx new file mode 100644 index 000000000000..0ed7fa7a64f8 --- /dev/null +++ b/apps/meteor/client/components/gazzodown/PreviewMarkup.tsx @@ -0,0 +1,74 @@ +import * as MessageParser from '@rocket.chat/message-parser'; +import React, { memo, ReactElement } from 'react'; + +import PreviewBigEmojiBlock from './blocks/PreviewBigEmojiBlock'; +import PreviewInlineElements from './elements/PreviewInlineElements'; + +const isOnlyBigEmojiBlock = (tokens: MessageParser.MarkdownAST): tokens is [MessageParser.BigEmoji] => + tokens.length === 1 && tokens[0].type === 'BIG_EMOJI'; + +type PreviewMarkupProps = { + tokens: MessageParser.MarkdownAST; +}; + +const PreviewMarkup = ({ tokens }: PreviewMarkupProps): ReactElement | null => { + if (isOnlyBigEmojiBlock(tokens)) { + return <PreviewBigEmojiBlock emojis={tokens[0].value} />; + } + + const firstBlock = tokens.find((block) => block.type !== 'LINE_BREAK'); + + if (!firstBlock) { + return null; + } + + switch (firstBlock.type) { + case 'PARAGRAPH': + return <PreviewInlineElements children={firstBlock.value} />; + + case 'HEADING': + return <>{firstBlock.value.map((plain) => plain.value).join('')}</>; + + case 'UNORDERED_LIST': + case 'ORDERED_LIST': { + const firstItem = firstBlock.value[0]; + + return ( + <> + {firstItem.number ? `${firstItem.number}.` : '-'} <PreviewInlineElements children={firstItem.value} /> + </> + ); + } + + case 'TASKS': { + const firstTask = firstBlock.value[0]; + + return ( + <> + {firstTask.status ? '\u2611' : '\u2610'} <PreviewInlineElements children={firstTask.value} /> + </> + ); + } + + case 'QUOTE': { + const firstParagraph = firstBlock.value[0]; + + return ( + <> + &gt; <PreviewInlineElements children={firstParagraph.value} /> + </> + ); + } + + case 'CODE': { + const firstLine = firstBlock.value.find((line) => line.value.value.trim()); + + return firstLine ? <>{firstLine.value.value.trim()}</> : null; + } + + default: + return null; + } +}; + +export default memo(PreviewMarkup); diff --git a/apps/meteor/client/components/gazzodown/blocks/BigEmojiBlock.tsx b/apps/meteor/client/components/gazzodown/blocks/BigEmojiBlock.tsx new file mode 100644 index 000000000000..4c5262fd4ab4 --- /dev/null +++ b/apps/meteor/client/components/gazzodown/blocks/BigEmojiBlock.tsx @@ -0,0 +1,18 @@ +import * as MessageParser from '@rocket.chat/message-parser'; +import React, { ReactElement } from 'react'; + +import BigEmojiElement from '../elements/BigEmojiElement'; + +type BigEmojiBlockProps = { + emojis: MessageParser.Emoji[]; +}; + +const BigEmojiBlock = ({ emojis }: BigEmojiBlockProps): ReactElement => ( + <> + {emojis.map((emoji, index) => ( + <BigEmojiElement key={index} handle={emoji.value.value} /> + ))} + </> +); + +export default BigEmojiBlock; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/Code.tsx b/apps/meteor/client/components/gazzodown/blocks/CodeBlock.tsx similarity index 59% rename from apps/meteor/client/components/Message/MessageBodyRender/Code.tsx rename to apps/meteor/client/components/gazzodown/blocks/CodeBlock.tsx index 49402f74666a..886ec71f7188 100644 --- a/apps/meteor/client/components/Message/MessageBodyRender/Code.tsx +++ b/apps/meteor/client/components/gazzodown/blocks/CodeBlock.tsx @@ -1,8 +1,7 @@ -import { Code as ASTCode } from '@rocket.chat/message-parser'; -import React, { FC, useEffect, useState } from 'react'; +import * as MessageParser from '@rocket.chat/message-parser'; +import React, { ReactElement, useEffect, useState } from 'react'; import hljs, { register } from '../../../../app/markdown/lib/hljs'; -import CodeLine from './CodeLine'; type hljsResult = { language: string; @@ -12,24 +11,31 @@ type hljsResult = { const isHljsResult = (result: any): result is hljsResult => result?.value; -const Code: FC<ASTCode> = ({ value = [], language }) => { +type CodeBlockProps = { + language?: string; + lines: MessageParser.CodeLine[]; +}; + +const CodeBlock = ({ lines = [], language }: CodeBlockProps): ReactElement => { const [code, setCode] = useState<(JSX.Element | null)[] | { language: string; code: string }>(() => - value.map((block, index) => { + lines.map((block, index) => { switch (block.type) { case 'CODE_LINE': - return <CodeLine key={index} value={block.value} />; + return <div key={index}>{block.value.type === 'PLAIN_TEXT' ? block.value.value : null}</div>; + default: return null; } }), ); + useEffect(() => { !language || language === 'none' - ? setCode(hljs.highlightAuto(value.map((line) => line.value.value).join('\n'))) + ? setCode(hljs.highlightAuto(lines.map((line) => line.value.value).join('\n'))) : register(language).then(() => { - setCode(hljs.highlight(language, value.map((line) => line.value.value).join('\n'))); + setCode(hljs.highlight(language, lines.map((line) => line.value.value).join('\n'))); }); - }, [language, value]); + }, [language, lines]); return ( <code className={`code-colors hljs ${language}`}> @@ -46,4 +52,4 @@ const Code: FC<ASTCode> = ({ value = [], language }) => { ); }; -export default Code; +export default CodeBlock; diff --git a/apps/meteor/client/components/gazzodown/blocks/HeadingBlock.tsx b/apps/meteor/client/components/gazzodown/blocks/HeadingBlock.tsx new file mode 100644 index 000000000000..1d21893f8a12 --- /dev/null +++ b/apps/meteor/client/components/gazzodown/blocks/HeadingBlock.tsx @@ -0,0 +1,23 @@ +import * as MessageParser from '@rocket.chat/message-parser'; +import React, { ReactElement } from 'react'; + +import PlainSpan from '../elements/PlainSpan'; + +type HeadingBlockProps = { + children?: MessageParser.Plain[]; + level?: 1 | 2 | 3 | 4; +}; + +const HeadingBlock = ({ children = [], level = 1 }: HeadingBlockProps): ReactElement => { + const HeadingTag = `h${level}` as const; + + return ( + <HeadingTag> + {children.map((block, index) => ( + <PlainSpan key={index} text={block.value} /> + ))} + </HeadingTag> + ); +}; + +export default HeadingBlock; diff --git a/apps/meteor/client/components/gazzodown/blocks/OrderedListBlock.tsx b/apps/meteor/client/components/gazzodown/blocks/OrderedListBlock.tsx new file mode 100644 index 000000000000..2da9078bbcc6 --- /dev/null +++ b/apps/meteor/client/components/gazzodown/blocks/OrderedListBlock.tsx @@ -0,0 +1,20 @@ +import * as MessageParser from '@rocket.chat/message-parser'; +import React, { ReactElement } from 'react'; + +import InlineElements from '../elements/InlineElements'; + +type OrderedListBlockProps = { + items: MessageParser.ListItem[]; +}; + +const OrderedListBlock = ({ items }: OrderedListBlockProps): ReactElement => ( + <ol> + {items.map(({ value, number }, index) => ( + <li key={index} value={number}> + <InlineElements children={value} /> + </li> + ))} + </ol> +); + +export default OrderedListBlock; diff --git a/apps/meteor/client/components/gazzodown/blocks/ParagraphBlock.tsx b/apps/meteor/client/components/gazzodown/blocks/ParagraphBlock.tsx new file mode 100644 index 000000000000..084f4808a390 --- /dev/null +++ b/apps/meteor/client/components/gazzodown/blocks/ParagraphBlock.tsx @@ -0,0 +1,16 @@ +import * as MessageParser from '@rocket.chat/message-parser'; +import React, { ReactElement } from 'react'; + +import InlineElements from '../elements/InlineElements'; + +type ParagraphBlockProps = { + children: MessageParser.Inlines[]; +}; + +const ParagraphBlock = ({ children }: ParagraphBlockProps): ReactElement => ( + <p> + <InlineElements children={children} /> + </p> +); + +export default ParagraphBlock; diff --git a/apps/meteor/client/components/gazzodown/blocks/PreviewBigEmojiBlock.tsx b/apps/meteor/client/components/gazzodown/blocks/PreviewBigEmojiBlock.tsx new file mode 100644 index 000000000000..1ba958fc9aa2 --- /dev/null +++ b/apps/meteor/client/components/gazzodown/blocks/PreviewBigEmojiBlock.tsx @@ -0,0 +1,18 @@ +import * as MessageParser from '@rocket.chat/message-parser'; +import React, { ReactElement } from 'react'; + +import PreviewEmojiElement from '../elements/PreviewEmojiElement'; + +type PreviewBigEmojiBlockProps = { + emojis: MessageParser.Emoji[]; +}; + +const PreviewBigEmojiBlock = ({ emojis }: PreviewBigEmojiBlockProps): ReactElement => ( + <> + {emojis.map((emoji, index) => ( + <PreviewEmojiElement key={index} handle={emoji.value.value} /> + ))} + </> +); + +export default PreviewBigEmojiBlock; diff --git a/apps/meteor/client/components/gazzodown/blocks/QuoteBlock.tsx b/apps/meteor/client/components/gazzodown/blocks/QuoteBlock.tsx new file mode 100644 index 000000000000..aa2f4c44fec2 --- /dev/null +++ b/apps/meteor/client/components/gazzodown/blocks/QuoteBlock.tsx @@ -0,0 +1,18 @@ +import * as MessageParser from '@rocket.chat/message-parser'; +import React, { ReactElement } from 'react'; + +import ParagraphBlock from './ParagraphBlock'; + +type QuoteBlockProps = { + children: MessageParser.Paragraph[]; +}; + +const QuoteBlock = ({ children }: QuoteBlockProps): ReactElement => ( + <blockquote> + {children.map((paragraph, index) => ( + <ParagraphBlock key={index} children={paragraph.value} /> + ))} + </blockquote> +); + +export default QuoteBlock; diff --git a/apps/meteor/client/components/gazzodown/blocks/TaskListBlock.tsx b/apps/meteor/client/components/gazzodown/blocks/TaskListBlock.tsx new file mode 100644 index 000000000000..1f0cfe8196fe --- /dev/null +++ b/apps/meteor/client/components/gazzodown/blocks/TaskListBlock.tsx @@ -0,0 +1,21 @@ +import { CheckBox } from '@rocket.chat/fuselage'; +import * as MessageParser from '@rocket.chat/message-parser'; +import React, { ReactElement } from 'react'; + +import InlineElements from '../elements/InlineElements'; + +type TaskListBlockProps = { + tasks: MessageParser.Task[]; +}; + +const TaksListBlock = ({ tasks }: TaskListBlockProps): ReactElement => ( + <ul className='task-list'> + {tasks.map((item, index) => ( + <li key={index}> + <CheckBox checked={item.status} /> <InlineElements children={item.value} /> + </li> + ))} + </ul> +); + +export default TaksListBlock; diff --git a/apps/meteor/client/components/gazzodown/blocks/UnorderedListBlock.tsx b/apps/meteor/client/components/gazzodown/blocks/UnorderedListBlock.tsx new file mode 100644 index 000000000000..42ba4425abff --- /dev/null +++ b/apps/meteor/client/components/gazzodown/blocks/UnorderedListBlock.tsx @@ -0,0 +1,20 @@ +import * as MessageParser from '@rocket.chat/message-parser'; +import React, { ReactElement } from 'react'; + +import InlineElements from '../elements/InlineElements'; + +type UnorderedListBlockProps = { + items: MessageParser.ListItem[]; +}; + +const UnorderedListBlock = ({ items }: UnorderedListBlockProps): ReactElement => ( + <ul> + {items.map((item, index) => ( + <li key={index}> + <InlineElements children={item.value} /> + </li> + ))} + </ul> +); + +export default UnorderedListBlock; diff --git a/apps/meteor/client/components/gazzodown/elements/BigEmojiElement.tsx b/apps/meteor/client/components/gazzodown/elements/BigEmojiElement.tsx new file mode 100644 index 000000000000..0387d1532ca8 --- /dev/null +++ b/apps/meteor/client/components/gazzodown/elements/BigEmojiElement.tsx @@ -0,0 +1,32 @@ +import { MessageEmoji } from '@rocket.chat/fuselage'; +import React, { ReactElement, useMemo } from 'react'; + +import { getEmojiClassNameAndDataTitle } from '../../../lib/utils/renderEmoji'; + +type BigEmojiElementProps = { + handle: string; +}; + +const BigEmojiElement = ({ handle }: BigEmojiElementProps): ReactElement => { + const emojiProps = useMemo(() => { + const props = getEmojiClassNameAndDataTitle(`:${handle}:`); + + if (!props.className && !props.name) { + return undefined; + } + + return props; + }, [handle]); + + if (!emojiProps) { + return <>:${handle}:</>; + } + + return ( + <MessageEmoji big {...emojiProps}> + :{handle}: + </MessageEmoji> + ); +}; + +export default BigEmojiElement; diff --git a/apps/meteor/client/components/gazzodown/elements/BoldSpan.tsx b/apps/meteor/client/components/gazzodown/elements/BoldSpan.tsx new file mode 100644 index 000000000000..14e079ebe6b2 --- /dev/null +++ b/apps/meteor/client/components/gazzodown/elements/BoldSpan.tsx @@ -0,0 +1,36 @@ +import * as MessageParser from '@rocket.chat/message-parser'; +import React, { ReactElement } from 'react'; + +import ItalicSpan from './ItalicSpan'; +import LinkSpan from './LinkSpan'; +import PlainSpan from './PlainSpan'; +import StrikeSpan from './StrikeSpan'; + +type BoldSpanProps = { + children: (MessageParser.Link | MessageParser.MarkupExcluding<MessageParser.Bold>)[]; +}; + +const BoldSpan = ({ children }: BoldSpanProps): ReactElement => ( + <strong> + {children.map((block, index) => { + switch (block.type) { + case 'LINK': + return <LinkSpan key={index} href={block.value.src.value} label={block.value.label} />; + + case 'PLAIN_TEXT': + return <PlainSpan key={index} text={block.value} />; + + case 'STRIKE': + return <StrikeSpan key={index} children={block.value} />; + + case 'ITALIC': + return <ItalicSpan key={index} children={block.value} />; + + default: + return null; + } + })} + </strong> +); + +export default BoldSpan; diff --git a/apps/meteor/client/components/gazzodown/elements/ChannelMentionElement.tsx b/apps/meteor/client/components/gazzodown/elements/ChannelMentionElement.tsx new file mode 100644 index 000000000000..2d17fbddc952 --- /dev/null +++ b/apps/meteor/client/components/gazzodown/elements/ChannelMentionElement.tsx @@ -0,0 +1,25 @@ +import React, { memo, ReactElement } from 'react'; + +import { useMessageBodyChannelMentions, useMessageBodyChannelMentionClick } from '../MarkupInteractionContext'; + +type ChannelMentionElementProps = { + mention: string; +}; + +const ChannelMentionElement = ({ mention }: ChannelMentionElementProps): ReactElement => { + const mentions = useMessageBodyChannelMentions(); + const mentioned = mentions.find((mentioned) => mentioned.name === mention); + const onChannelMentionClick = useMessageBodyChannelMentionClick(); + + if (!mentioned) { + return <>#{mention}</>; + } + + return ( + <span className='mention-link mention-link--room' onClick={onChannelMentionClick(mentioned._id)}> + #{mention} + </span> + ); +}; + +export default memo(ChannelMentionElement); diff --git a/apps/meteor/client/components/gazzodown/elements/CodeElement.tsx b/apps/meteor/client/components/gazzodown/elements/CodeElement.tsx new file mode 100644 index 000000000000..3a0b8e5cd1c5 --- /dev/null +++ b/apps/meteor/client/components/gazzodown/elements/CodeElement.tsx @@ -0,0 +1,15 @@ +import React, { ReactElement } from 'react'; + +import PlainSpan from './PlainSpan'; + +type CodeElementProps = { + code: string; +}; + +const CodeElement = ({ code }: CodeElementProps): ReactElement => ( + <code className='code-colors inline'> + <PlainSpan text={code} /> + </code> +); + +export default CodeElement; diff --git a/apps/meteor/client/components/gazzodown/elements/ColorElement.tsx b/apps/meteor/client/components/gazzodown/elements/ColorElement.tsx new file mode 100644 index 000000000000..152486b01f7d --- /dev/null +++ b/apps/meteor/client/components/gazzodown/elements/ColorElement.tsx @@ -0,0 +1,26 @@ +import React, { ReactElement } from 'react'; + +type ColorElementProps = { + r: number; + g: number; + b: number; + a: number; +}; + +const ColorElement = ({ r, g, b, a }: ColorElementProps): ReactElement => ( + <span> + <span + style={{ + backgroundColor: `rgba(${r}, ${g}, ${b}, ${(a / 255) * 100}%)`, + display: 'inline-block', + width: '1em', + height: '1em', + verticalAlign: 'middle', + marginInlineEnd: '0.5em', + }} + /> + rgba({r}, {g}, {b}, {(a / 255) * 100}%) + </span> +); + +export default ColorElement; diff --git a/apps/meteor/client/components/gazzodown/elements/EmojiElement.tsx b/apps/meteor/client/components/gazzodown/elements/EmojiElement.tsx new file mode 100644 index 000000000000..a8c5f15db5bf --- /dev/null +++ b/apps/meteor/client/components/gazzodown/elements/EmojiElement.tsx @@ -0,0 +1,28 @@ +import { MessageEmoji } from '@rocket.chat/fuselage'; +import React, { ReactElement, useMemo } from 'react'; + +import { getEmojiClassNameAndDataTitle } from '../../../lib/utils/renderEmoji'; + +type EmojiElementProps = { + handle: string; +}; + +const EmojiElement = ({ handle }: EmojiElementProps): ReactElement => { + const emojiProps = useMemo(() => { + const props = getEmojiClassNameAndDataTitle(`:${handle}:`); + + if (!props.className && !props.name) { + return undefined; + } + + return props; + }, [handle]); + + if (!emojiProps) { + return <>:${handle}:</>; + } + + return <MessageEmoji {...emojiProps}>:{handle}:</MessageEmoji>; +}; + +export default EmojiElement; diff --git a/apps/meteor/client/components/gazzodown/elements/ImageElement.tsx b/apps/meteor/client/components/gazzodown/elements/ImageElement.tsx new file mode 100644 index 000000000000..de1c4e501046 --- /dev/null +++ b/apps/meteor/client/components/gazzodown/elements/ImageElement.tsx @@ -0,0 +1,45 @@ +import * as MessageParser from '@rocket.chat/message-parser'; +import React, { ReactElement, useMemo } from 'react'; + +const flattenMarkup = (markup: MessageParser.Markup | MessageParser.Link): string => { + switch (markup.type) { + case 'PLAIN_TEXT': + return markup.value; + + case 'ITALIC': + case 'BOLD': + case 'STRIKE': + return markup.value.map(flattenMarkup).join(''); + + case 'LINK': { + const label = flattenMarkup(markup.value.label); + const href = markup.value.src.value; + + return label ? `${label} (${href})` : href; + } + + default: + return ''; + } +}; + +const style = { + maxWidth: '100%', +}; + +type ImageElementProps = { + src: string; + alt: MessageParser.Markup; +}; + +const ImageElement = ({ src, alt }: ImageElementProps): ReactElement => { + const plainAlt = useMemo(() => flattenMarkup(alt), [alt]); + + return ( + <a href={src} target='_blank' rel='noopener noreferrer' title={plainAlt}> + <img src={src} data-title={src} alt={plainAlt} style={style} /> + </a> + ); +}; + +export default ImageElement; diff --git a/apps/meteor/client/components/gazzodown/elements/InlineElements.tsx b/apps/meteor/client/components/gazzodown/elements/InlineElements.tsx new file mode 100644 index 000000000000..c60a3c6c1d57 --- /dev/null +++ b/apps/meteor/client/components/gazzodown/elements/InlineElements.tsx @@ -0,0 +1,64 @@ +import * as MessageParser from '@rocket.chat/message-parser'; +import React, { ReactElement } from 'react'; + +import BoldSpan from './BoldSpan'; +import ChannelMentionElement from './ChannelMentionElement'; +import CodeElement from './CodeElement'; +import ColorElement from './ColorElement'; +import EmojiElement from './EmojiElement'; +import ImageElement from './ImageElement'; +import ItalicSpan from './ItalicSpan'; +import LinkSpan from './LinkSpan'; +import PlainSpan from './PlainSpan'; +import StrikeSpan from './StrikeSpan'; +import UserMentionElement from './UserMentionElement'; + +type InlineElementsProps = { + children: MessageParser.Inlines[]; +}; + +const InlineElements = ({ children }: InlineElementsProps): ReactElement => ( + <> + {children.map((child, index) => { + switch (child.type) { + case 'BOLD': + return <BoldSpan key={index} children={child.value} />; + + case 'STRIKE': + return <StrikeSpan key={index} children={child.value} />; + + case 'ITALIC': + return <ItalicSpan key={index} children={child.value} />; + + case 'LINK': + return <LinkSpan key={index} href={child.value.src.value} label={child.value.label} />; + + case 'PLAIN_TEXT': + return <PlainSpan key={index} text={child.value} />; + + case 'IMAGE': + return <ImageElement key={index} src={child.value.src.value} alt={child.value.label} />; + + case 'MENTION_USER': + return <UserMentionElement key={index} mention={child.value.value} />; + + case 'MENTION_CHANNEL': + return <ChannelMentionElement key={index} mention={child.value.value} />; + + case 'INLINE_CODE': + return <CodeElement key={index} code={child.value.value} />; + + case 'EMOJI': + return <EmojiElement key={index} handle={child.value.value} />; + + case 'COLOR': + return <ColorElement key={index} {...child.value} />; + + default: + return null; + } + })} + </> +); + +export default InlineElements; diff --git a/apps/meteor/client/components/gazzodown/elements/ItalicSpan.tsx b/apps/meteor/client/components/gazzodown/elements/ItalicSpan.tsx new file mode 100644 index 000000000000..f1e06950ca0b --- /dev/null +++ b/apps/meteor/client/components/gazzodown/elements/ItalicSpan.tsx @@ -0,0 +1,36 @@ +import * as MessageParser from '@rocket.chat/message-parser'; +import React, { ReactElement } from 'react'; + +import BoldSpan from './BoldSpan'; +import LinkSpan from './LinkSpan'; +import PlainSpan from './PlainSpan'; +import StrikeSpan from './StrikeSpan'; + +type ItalicSpanProps = { + children: (MessageParser.Link | MessageParser.MarkupExcluding<MessageParser.Italic>)[]; +}; + +const ItalicSpan = ({ children }: ItalicSpanProps): ReactElement => ( + <i> + {children.map((block, index) => { + switch (block.type) { + case 'LINK': + return <LinkSpan key={index} href={block.value.src.value} label={block.value.label} />; + + case 'PLAIN_TEXT': + return <PlainSpan key={index} text={block.value} />; + + case 'STRIKE': + return <StrikeSpan key={index} children={block.value} />; + + case 'BOLD': + return <BoldSpan key={index} children={block.value} />; + + default: + return null; + } + })} + </i> +); + +export default ItalicSpan; diff --git a/apps/meteor/client/components/gazzodown/elements/LinkSpan.tsx b/apps/meteor/client/components/gazzodown/elements/LinkSpan.tsx new file mode 100644 index 000000000000..581ddc9970dc --- /dev/null +++ b/apps/meteor/client/components/gazzodown/elements/LinkSpan.tsx @@ -0,0 +1,42 @@ +import * as MessageParser from '@rocket.chat/message-parser'; +import React, { ReactElement } from 'react'; + +import { baseURI } from '../../../lib/baseURI'; +import BoldSpan from './BoldSpan'; +import ItalicSpan from './ItalicSpan'; +import PlainSpan from './PlainSpan'; +import StrikeSpan from './StrikeSpan'; + +type LinkSpanProps = { + href: string; + label: MessageParser.Markup; +}; + +const LinkSpan = ({ href, label }: LinkSpanProps): ReactElement => { + const attrs = href.indexOf(baseURI) !== 0 ? { rel: 'noopener noreferrer', target: '_blank' } : {}; + + return ( + <a href={href} data-title={href} {...attrs}> + {((block: MessageParser.Markup): JSX.Element | string | null => { + switch (block.type) { + case 'PLAIN_TEXT': + return <PlainSpan text={block.value} />; + + case 'STRIKE': + return <StrikeSpan children={block.value} />; + + case 'ITALIC': + return <ItalicSpan children={block.value} />; + + case 'BOLD': + return <BoldSpan children={block.value} />; + + default: + return null; + } + })(label)} + </a> + ); +}; + +export default LinkSpan; diff --git a/apps/meteor/client/components/Message/MessageBodyRender/PlainText.tsx b/apps/meteor/client/components/gazzodown/elements/PlainSpan.tsx similarity index 61% rename from apps/meteor/client/components/Message/MessageBodyRender/PlainText.tsx rename to apps/meteor/client/components/gazzodown/elements/PlainSpan.tsx index 5c0cd2c4011e..a82d13b9cbb6 100644 --- a/apps/meteor/client/components/Message/MessageBodyRender/PlainText.tsx +++ b/apps/meteor/client/components/gazzodown/elements/PlainSpan.tsx @@ -1,14 +1,13 @@ -import { Plain as ASTPlain } from '@rocket.chat/message-parser'; -import React, { FC, memo } from 'react'; +import React, { memo, ReactElement } from 'react'; import { useMessageListHighlights, useMessageListKatex } from '../../../views/room/MessageList/contexts/MessageListContext'; import CustomText from '../../CustomText'; -type PlainTextType = { - value: ASTPlain['value']; +type PlainSpanProps = { + text: string; }; -const PlainText: FC<PlainTextType> = ({ value: text }) => { +const PlainSpan = ({ text }: PlainSpanProps): ReactElement => { const highlights = useMessageListHighlights(); const katex = useMessageListKatex(); @@ -19,4 +18,4 @@ const PlainText: FC<PlainTextType> = ({ value: text }) => { return <>{text}</>; }; -export default memo(PlainText); +export default memo(PlainSpan); diff --git a/apps/meteor/client/components/gazzodown/elements/PreviewColorElement.tsx b/apps/meteor/client/components/gazzodown/elements/PreviewColorElement.tsx new file mode 100644 index 000000000000..10f36d4f2075 --- /dev/null +++ b/apps/meteor/client/components/gazzodown/elements/PreviewColorElement.tsx @@ -0,0 +1,33 @@ +import React, { ReactElement } from 'react'; + +const toHexByte = (value: number): string => value.toString(16).padStart(2, '0'); + +type PreviewColorElementProps = { + r: number; + g: number; + b: number; + a: number; +}; + +const PreviewColorElement = ({ r, g, b, a }: PreviewColorElementProps): ReactElement => { + if (a === 255) { + return ( + <> + #{toHexByte(r)} + {toHexByte(g)} + {toHexByte(b)} + </> + ); + } + + return ( + <> + #{toHexByte(r)} + {toHexByte(g)} + {toHexByte(b)} + {toHexByte(a)} + </> + ); +}; + +export default PreviewColorElement; diff --git a/apps/meteor/client/components/gazzodown/elements/PreviewEmojiElement.tsx b/apps/meteor/client/components/gazzodown/elements/PreviewEmojiElement.tsx new file mode 100644 index 000000000000..4976666f4707 --- /dev/null +++ b/apps/meteor/client/components/gazzodown/elements/PreviewEmojiElement.tsx @@ -0,0 +1,28 @@ +import { ThreadMessageEmoji } from '@rocket.chat/fuselage'; +import React, { ReactElement, useMemo } from 'react'; + +import { getEmojiClassNameAndDataTitle } from '../../../lib/utils/renderEmoji'; + +type PreviewEmojiElementProps = { + handle: string; +}; + +const PreviewEmojiElement = ({ handle }: PreviewEmojiElementProps): ReactElement => { + const emojiProps = useMemo(() => { + const props = getEmojiClassNameAndDataTitle(`:${handle}:`); + + if (!props.className && !props.name) { + return undefined; + } + + return props; + }, [handle]); + + if (!emojiProps) { + return <>:${handle}:</>; + } + + return <ThreadMessageEmoji {...emojiProps}>:{handle}:</ThreadMessageEmoji>; +}; + +export default PreviewEmojiElement; diff --git a/apps/meteor/client/components/gazzodown/elements/PreviewInlineElements.tsx b/apps/meteor/client/components/gazzodown/elements/PreviewInlineElements.tsx new file mode 100644 index 000000000000..096fa7438de5 --- /dev/null +++ b/apps/meteor/client/components/gazzodown/elements/PreviewInlineElements.tsx @@ -0,0 +1,51 @@ +import * as MessageParser from '@rocket.chat/message-parser'; +import React, { Fragment, ReactElement } from 'react'; + +import PreviewColorElement from './PreviewColorElement'; +import PreviewEmojiElement from './PreviewEmojiElement'; + +type PreviewInlineElementsProps = { + children: MessageParser.Inlines[]; +}; + +const PreviewInlineElements = ({ children }: PreviewInlineElementsProps): ReactElement => ( + <> + {children.map((child, index) => { + switch (child.type) { + case 'BOLD': + case 'ITALIC': + case 'STRIKE': + return <PreviewInlineElements key={index} children={child.value} />; + + case 'LINK': + return <PreviewInlineElements key={index} children={[child.value.label]} />; + + case 'PLAIN_TEXT': + return <Fragment key={index} children={child.value} />; + + case 'IMAGE': + return <PreviewInlineElements key={index} children={[child.value.label]} />; + + case 'MENTION_USER': + return <Fragment key={index}>@{child.value.value}</Fragment>; + + case 'MENTION_CHANNEL': + return <Fragment key={index}>#{child.value.value}</Fragment>; + + case 'INLINE_CODE': + return <Fragment key={index} children={child.value.value} />; + + case 'EMOJI': + return <PreviewEmojiElement key={index} handle={child.value.value} />; + + case 'COLOR': + return <PreviewColorElement key={index} {...child.value} />; + + default: + return null; + } + })} + </> +); + +export default PreviewInlineElements; diff --git a/apps/meteor/client/components/gazzodown/elements/StrikeSpan.tsx b/apps/meteor/client/components/gazzodown/elements/StrikeSpan.tsx new file mode 100644 index 000000000000..b70ad7ec1baa --- /dev/null +++ b/apps/meteor/client/components/gazzodown/elements/StrikeSpan.tsx @@ -0,0 +1,36 @@ +import * as MessageParser from '@rocket.chat/message-parser'; +import React, { ReactElement } from 'react'; + +import BoldSpan from './BoldSpan'; +import ItalicSpan from './ItalicSpan'; +import LinkSpan from './LinkSpan'; +import PlainSpan from './PlainSpan'; + +type StrikeSpanProps = { + children: (MessageParser.Link | MessageParser.MarkupExcluding<MessageParser.Strike>)[]; +}; + +const StrikeSpan = ({ children }: StrikeSpanProps): ReactElement => ( + <del> + {children.map((block, index) => { + switch (block.type) { + case 'LINK': + return <LinkSpan key={index} href={block.value.src.value} label={block.value.label} />; + + case 'PLAIN_TEXT': + return <PlainSpan key={index} text={block.value} />; + + case 'BOLD': + return <BoldSpan key={index} children={block.value} />; + + case 'ITALIC': + return <ItalicSpan key={index} children={block.value} />; + + default: + return null; + } + })} + </del> +); + +export default StrikeSpan; diff --git a/apps/meteor/client/components/gazzodown/elements/UserMentionElement.tsx b/apps/meteor/client/components/gazzodown/elements/UserMentionElement.tsx new file mode 100644 index 000000000000..8a0b63033c25 --- /dev/null +++ b/apps/meteor/client/components/gazzodown/elements/UserMentionElement.tsx @@ -0,0 +1,43 @@ +import { useUserId } from '@rocket.chat/ui-contexts'; +import React, { memo, ReactElement, useMemo } from 'react'; + +import { useMessageBodyUserMentions, useMessageBodyMentionClick } from '../MarkupInteractionContext'; + +type UserMentionElementProps = { + mention: string; +}; + +const UserMentionElement = ({ mention }: UserMentionElementProps): ReactElement => { + const uid = useUserId(); + const mentions = useMessageBodyUserMentions(); + const mentioned = mentions.find((mentioned) => mentioned.username === mention); + const onUserMentionClick = useMessageBodyMentionClick(); + + const classNames = useMemo(() => { + if (mention === 'all') { + return 'mention-link mention-link--all mention-link--group'; + } + + if (mention === 'here') { + return 'mention-link mention-link--here mention-link--group'; + } + + if (mentioned && mentioned._id === uid) { + return 'mention-link mention-link--me mention-link--user'; + } + + return 'mention-link mention-link--user'; + }, [mention, mentioned, uid]); + + if (!mentioned) { + return <>@{mention}</>; + } + + return ( + <span className={classNames} onClick={mention !== 'all' && mention !== 'here' ? onUserMentionClick(mention) : undefined}> + {mentioned.name || mention} + </span> + ); +}; + +export default memo(UserMentionElement); diff --git a/apps/meteor/client/components/Message/Attachments/ActionAttachtment.tsx b/apps/meteor/client/components/message/Attachments/ActionAttachtment.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/ActionAttachtment.tsx rename to apps/meteor/client/components/message/Attachments/ActionAttachtment.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/Action.tsx b/apps/meteor/client/components/message/Attachments/Attachment/Action.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/Action.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/Action.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/Attachment.tsx b/apps/meteor/client/components/message/Attachments/Attachment/Attachment.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/Attachment.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/Attachment.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/Author.tsx b/apps/meteor/client/components/message/Attachments/Attachment/Author.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/Author.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/Author.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/AuthorAvatar.tsx b/apps/meteor/client/components/message/Attachments/Attachment/AuthorAvatar.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/AuthorAvatar.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/AuthorAvatar.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/AuthorName.tsx b/apps/meteor/client/components/message/Attachments/Attachment/AuthorName.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/AuthorName.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/AuthorName.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/Block.tsx b/apps/meteor/client/components/message/Attachments/Attachment/Block.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/Block.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/Block.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/Collapse.tsx b/apps/meteor/client/components/message/Attachments/Attachment/Collapse.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/Collapse.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/Collapse.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/Content.tsx b/apps/meteor/client/components/message/Attachments/Attachment/Content.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/Content.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/Content.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/Description.tsx b/apps/meteor/client/components/message/Attachments/Attachment/Description.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/Description.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/Description.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/Details.tsx b/apps/meteor/client/components/message/Attachments/Attachment/Details.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/Details.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/Details.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/Download.tsx b/apps/meteor/client/components/message/Attachments/Attachment/Download.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/Download.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/Download.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/Inner.tsx b/apps/meteor/client/components/message/Attachments/Attachment/Inner.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/Inner.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/Inner.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/Row.tsx b/apps/meteor/client/components/message/Attachments/Attachment/Row.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/Row.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/Row.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/Size.tsx b/apps/meteor/client/components/message/Attachments/Attachment/Size.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/Size.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/Size.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/Text.tsx b/apps/meteor/client/components/message/Attachments/Attachment/Text.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/Text.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/Text.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/Thumb.tsx b/apps/meteor/client/components/message/Attachments/Attachment/Thumb.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/Thumb.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/Thumb.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/Title.tsx b/apps/meteor/client/components/message/Attachments/Attachment/Title.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/Title.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/Title.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/TitleLink.tsx b/apps/meteor/client/components/message/Attachments/Attachment/TitleLink.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/TitleLink.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/TitleLink.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachment/index.tsx b/apps/meteor/client/components/message/Attachments/Attachment/index.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachment/index.tsx rename to apps/meteor/client/components/message/Attachments/Attachment/index.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachments.stories.tsx b/apps/meteor/client/components/message/Attachments/Attachments.stories.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachments.stories.tsx rename to apps/meteor/client/components/message/Attachments/Attachments.stories.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Attachments.tsx b/apps/meteor/client/components/message/Attachments/Attachments.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Attachments.tsx rename to apps/meteor/client/components/message/Attachments/Attachments.tsx diff --git a/apps/meteor/client/components/Message/Attachments/DefaultAttachment.tsx b/apps/meteor/client/components/message/Attachments/DefaultAttachment.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/DefaultAttachment.tsx rename to apps/meteor/client/components/message/Attachments/DefaultAttachment.tsx diff --git a/apps/meteor/client/components/Message/Attachments/FieldsAttachment/Field.tsx b/apps/meteor/client/components/message/Attachments/FieldsAttachment/Field.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/FieldsAttachment/Field.tsx rename to apps/meteor/client/components/message/Attachments/FieldsAttachment/Field.tsx diff --git a/apps/meteor/client/components/Message/Attachments/FieldsAttachment/ShortField.tsx b/apps/meteor/client/components/message/Attachments/FieldsAttachment/ShortField.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/FieldsAttachment/ShortField.tsx rename to apps/meteor/client/components/message/Attachments/FieldsAttachment/ShortField.tsx diff --git a/apps/meteor/client/components/Message/Attachments/FieldsAttachment/index.tsx b/apps/meteor/client/components/message/Attachments/FieldsAttachment/index.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/FieldsAttachment/index.tsx rename to apps/meteor/client/components/message/Attachments/FieldsAttachment/index.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Files/AudioAttachment.tsx b/apps/meteor/client/components/message/Attachments/Files/AudioAttachment.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Files/AudioAttachment.tsx rename to apps/meteor/client/components/message/Attachments/Files/AudioAttachment.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Files/GenericFileAttachment.tsx b/apps/meteor/client/components/message/Attachments/Files/GenericFileAttachment.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Files/GenericFileAttachment.tsx rename to apps/meteor/client/components/message/Attachments/Files/GenericFileAttachment.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Files/ImageAttachment.tsx b/apps/meteor/client/components/message/Attachments/Files/ImageAttachment.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Files/ImageAttachment.tsx rename to apps/meteor/client/components/message/Attachments/Files/ImageAttachment.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Files/VideoAttachment.tsx b/apps/meteor/client/components/message/Attachments/Files/VideoAttachment.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Files/VideoAttachment.tsx rename to apps/meteor/client/components/message/Attachments/Files/VideoAttachment.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Files/index.tsx b/apps/meteor/client/components/message/Attachments/Files/index.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Files/index.tsx rename to apps/meteor/client/components/message/Attachments/Files/index.tsx diff --git a/apps/meteor/client/components/Message/Attachments/Item.tsx b/apps/meteor/client/components/message/Attachments/Item.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/Item.tsx rename to apps/meteor/client/components/message/Attachments/Item.tsx diff --git a/apps/meteor/client/components/Message/Attachments/QuoteAttachment.tsx b/apps/meteor/client/components/message/Attachments/QuoteAttachment.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/QuoteAttachment.tsx rename to apps/meteor/client/components/message/Attachments/QuoteAttachment.tsx diff --git a/apps/meteor/client/components/Message/Attachments/components/Image.tsx b/apps/meteor/client/components/message/Attachments/components/Image.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/components/Image.tsx rename to apps/meteor/client/components/message/Attachments/components/Image.tsx diff --git a/apps/meteor/client/components/Message/Attachments/components/ImageBox.tsx b/apps/meteor/client/components/message/Attachments/components/ImageBox.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/components/ImageBox.tsx rename to apps/meteor/client/components/message/Attachments/components/ImageBox.tsx diff --git a/apps/meteor/client/components/Message/Attachments/components/Load.tsx b/apps/meteor/client/components/message/Attachments/components/Load.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/components/Load.tsx rename to apps/meteor/client/components/message/Attachments/components/Load.tsx diff --git a/apps/meteor/client/components/Message/Attachments/components/Retry.stories.tsx b/apps/meteor/client/components/message/Attachments/components/Retry.stories.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/components/Retry.stories.tsx rename to apps/meteor/client/components/message/Attachments/components/Retry.stories.tsx diff --git a/apps/meteor/client/components/Message/Attachments/components/Retry.tsx b/apps/meteor/client/components/message/Attachments/components/Retry.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/components/Retry.tsx rename to apps/meteor/client/components/message/Attachments/components/Retry.tsx diff --git a/apps/meteor/client/components/Message/Attachments/hooks/useCollapse.tsx b/apps/meteor/client/components/message/Attachments/hooks/useCollapse.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/hooks/useCollapse.tsx rename to apps/meteor/client/components/message/Attachments/hooks/useCollapse.tsx diff --git a/apps/meteor/client/components/Message/Attachments/hooks/useLoadImage.tsx b/apps/meteor/client/components/message/Attachments/hooks/useLoadImage.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/hooks/useLoadImage.tsx rename to apps/meteor/client/components/message/Attachments/hooks/useLoadImage.tsx diff --git a/apps/meteor/client/components/Message/Attachments/index.tsx b/apps/meteor/client/components/message/Attachments/index.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/index.tsx rename to apps/meteor/client/components/message/Attachments/index.tsx diff --git a/apps/meteor/client/components/Message/Attachments/providers/AttachmentProvider.tsx b/apps/meteor/client/components/message/Attachments/providers/AttachmentProvider.tsx similarity index 100% rename from apps/meteor/client/components/Message/Attachments/providers/AttachmentProvider.tsx rename to apps/meteor/client/components/message/Attachments/providers/AttachmentProvider.tsx diff --git a/apps/meteor/client/components/Message/MessageActions/Action.tsx b/apps/meteor/client/components/message/MessageActions/Action.tsx similarity index 100% rename from apps/meteor/client/components/Message/MessageActions/Action.tsx rename to apps/meteor/client/components/message/MessageActions/Action.tsx diff --git a/apps/meteor/client/components/Message/MessageActions/Actions.tsx b/apps/meteor/client/components/message/MessageActions/Actions.tsx similarity index 100% rename from apps/meteor/client/components/Message/MessageActions/Actions.tsx rename to apps/meteor/client/components/message/MessageActions/Actions.tsx diff --git a/apps/meteor/client/components/Message/MessageActions/index.tsx b/apps/meteor/client/components/message/MessageActions/index.tsx similarity index 100% rename from apps/meteor/client/components/Message/MessageActions/index.tsx rename to apps/meteor/client/components/message/MessageActions/index.tsx diff --git a/apps/meteor/client/components/Message/MessageEmoji.tsx b/apps/meteor/client/components/message/MessageEmoji.tsx similarity index 60% rename from apps/meteor/client/components/Message/MessageEmoji.tsx rename to apps/meteor/client/components/message/MessageEmoji.tsx index bdb65c9655d8..4a2c226c1cd1 100644 --- a/apps/meteor/client/components/Message/MessageEmoji.tsx +++ b/apps/meteor/client/components/message/MessageEmoji.tsx @@ -4,25 +4,26 @@ import React, { ReactElement } from 'react'; import { getEmojiClassNameAndDataTitle } from '../../lib/utils/renderEmoji'; type MessageEmojiProps = { - emojiHandle: string; // :emoji: + handle: string; big?: boolean; isThreadPreview?: boolean; }; -function MessageEmoji({ emojiHandle, big, isThreadPreview }: MessageEmojiProps): ReactElement { - const emojiProps = getEmojiClassNameAndDataTitle(emojiHandle); +function MessageEmoji({ handle, big, isThreadPreview }: MessageEmojiProps): ReactElement { + handle = `:${handle}:`; + const emojiProps = getEmojiClassNameAndDataTitle(handle); if (!emojiProps.className && !emojiProps.name) { - return <>{emojiHandle}</>; + return <>{handle}</>; } if (isThreadPreview) { - return <ThreadMessageEmoji {...emojiProps}>{emojiHandle}</ThreadMessageEmoji>; + return <ThreadMessageEmoji {...emojiProps}>{handle}</ThreadMessageEmoji>; } return ( <MessageEmojiBase big={big} {...emojiProps}> - {emojiHandle} + {handle} </MessageEmojiBase> ); } diff --git a/apps/meteor/client/components/Message/Metrics/Broadcast.tsx b/apps/meteor/client/components/message/Metrics/Broadcast.tsx similarity index 100% rename from apps/meteor/client/components/Message/Metrics/Broadcast.tsx rename to apps/meteor/client/components/message/Metrics/Broadcast.tsx diff --git a/apps/meteor/client/components/Message/Metrics/Content.tsx b/apps/meteor/client/components/message/Metrics/Content.tsx similarity index 100% rename from apps/meteor/client/components/Message/Metrics/Content.tsx rename to apps/meteor/client/components/message/Metrics/Content.tsx diff --git a/apps/meteor/client/components/Message/Metrics/ContentItem.tsx b/apps/meteor/client/components/message/Metrics/ContentItem.tsx similarity index 100% rename from apps/meteor/client/components/Message/Metrics/ContentItem.tsx rename to apps/meteor/client/components/message/Metrics/ContentItem.tsx diff --git a/apps/meteor/client/components/Message/Metrics/Discussion.tsx b/apps/meteor/client/components/message/Metrics/Discussion.tsx similarity index 100% rename from apps/meteor/client/components/Message/Metrics/Discussion.tsx rename to apps/meteor/client/components/message/Metrics/Discussion.tsx diff --git a/apps/meteor/client/components/Message/Metrics/Metrics.stories.tsx b/apps/meteor/client/components/message/Metrics/Metrics.stories.tsx similarity index 100% rename from apps/meteor/client/components/Message/Metrics/Metrics.stories.tsx rename to apps/meteor/client/components/message/Metrics/Metrics.stories.tsx diff --git a/apps/meteor/client/components/Message/Metrics/Metrics.tsx b/apps/meteor/client/components/message/Metrics/Metrics.tsx similarity index 100% rename from apps/meteor/client/components/Message/Metrics/Metrics.tsx rename to apps/meteor/client/components/message/Metrics/Metrics.tsx diff --git a/apps/meteor/client/components/Message/Metrics/MetricsFollowing.tsx b/apps/meteor/client/components/message/Metrics/MetricsFollowing.tsx similarity index 100% rename from apps/meteor/client/components/Message/Metrics/MetricsFollowing.tsx rename to apps/meteor/client/components/message/Metrics/MetricsFollowing.tsx diff --git a/apps/meteor/client/components/Message/Metrics/MetricsItem.tsx b/apps/meteor/client/components/message/Metrics/MetricsItem.tsx similarity index 100% rename from apps/meteor/client/components/Message/Metrics/MetricsItem.tsx rename to apps/meteor/client/components/message/Metrics/MetricsItem.tsx diff --git a/apps/meteor/client/components/Message/Metrics/MetricsItemIcon.tsx b/apps/meteor/client/components/message/Metrics/MetricsItemIcon.tsx similarity index 100% rename from apps/meteor/client/components/Message/Metrics/MetricsItemIcon.tsx rename to apps/meteor/client/components/message/Metrics/MetricsItemIcon.tsx diff --git a/apps/meteor/client/components/Message/Metrics/MetricsItemLabel.tsx b/apps/meteor/client/components/message/Metrics/MetricsItemLabel.tsx similarity index 100% rename from apps/meteor/client/components/Message/Metrics/MetricsItemLabel.tsx rename to apps/meteor/client/components/message/Metrics/MetricsItemLabel.tsx diff --git a/apps/meteor/client/components/Message/Metrics/Reply.tsx b/apps/meteor/client/components/message/Metrics/Reply.tsx similarity index 100% rename from apps/meteor/client/components/Message/Metrics/Reply.tsx rename to apps/meteor/client/components/message/Metrics/Reply.tsx diff --git a/apps/meteor/client/components/Message/Metrics/Thread.tsx b/apps/meteor/client/components/message/Metrics/Thread.tsx similarity index 100% rename from apps/meteor/client/components/Message/Metrics/Thread.tsx rename to apps/meteor/client/components/message/Metrics/Thread.tsx diff --git a/apps/meteor/client/components/Message/Metrics/index.tsx b/apps/meteor/client/components/message/Metrics/index.tsx similarity index 100% rename from apps/meteor/client/components/Message/Metrics/index.tsx rename to apps/meteor/client/components/message/Metrics/index.tsx diff --git a/apps/meteor/client/components/Message/NotificationStatus/All.tsx b/apps/meteor/client/components/message/NotificationStatus/All.tsx similarity index 100% rename from apps/meteor/client/components/Message/NotificationStatus/All.tsx rename to apps/meteor/client/components/message/NotificationStatus/All.tsx diff --git a/apps/meteor/client/components/Message/NotificationStatus/Me.tsx b/apps/meteor/client/components/message/NotificationStatus/Me.tsx similarity index 100% rename from apps/meteor/client/components/Message/NotificationStatus/Me.tsx rename to apps/meteor/client/components/message/NotificationStatus/Me.tsx diff --git a/apps/meteor/client/components/Message/NotificationStatus/NotificationStatus.tsx b/apps/meteor/client/components/message/NotificationStatus/NotificationStatus.tsx similarity index 100% rename from apps/meteor/client/components/Message/NotificationStatus/NotificationStatus.tsx rename to apps/meteor/client/components/message/NotificationStatus/NotificationStatus.tsx diff --git a/apps/meteor/client/components/Message/NotificationStatus/Unread.tsx b/apps/meteor/client/components/message/NotificationStatus/Unread.tsx similarity index 100% rename from apps/meteor/client/components/Message/NotificationStatus/Unread.tsx rename to apps/meteor/client/components/message/NotificationStatus/Unread.tsx diff --git a/apps/meteor/client/components/Message/NotificationStatus/index.ts b/apps/meteor/client/components/message/NotificationStatus/index.ts similarity index 100% rename from apps/meteor/client/components/Message/NotificationStatus/index.ts rename to apps/meteor/client/components/message/NotificationStatus/index.ts diff --git a/apps/meteor/client/components/Message/Oembed/definitions.ts b/apps/meteor/client/components/message/Oembed/definitions.ts similarity index 100% rename from apps/meteor/client/components/Message/Oembed/definitions.ts rename to apps/meteor/client/components/message/Oembed/definitions.ts diff --git a/apps/meteor/client/components/Message/StatusMessage.tsx b/apps/meteor/client/components/message/StatusMessage.tsx similarity index 100% rename from apps/meteor/client/components/Message/StatusMessage.tsx rename to apps/meteor/client/components/message/StatusMessage.tsx diff --git a/apps/meteor/client/components/Message/helpers/followSyle.ts b/apps/meteor/client/components/message/helpers/followSyle.ts similarity index 100% rename from apps/meteor/client/components/Message/helpers/followSyle.ts rename to apps/meteor/client/components/message/helpers/followSyle.ts diff --git a/apps/meteor/client/components/Message/hooks/useBlockRendered.ts b/apps/meteor/client/components/message/hooks/useBlockRendered.ts similarity index 100% rename from apps/meteor/client/components/Message/hooks/useBlockRendered.ts rename to apps/meteor/client/components/message/hooks/useBlockRendered.ts diff --git a/apps/meteor/client/lib/appLayout.ts b/apps/meteor/client/lib/appLayout.ts index 770706194547..4f2f3a519b7b 100644 --- a/apps/meteor/client/lib/appLayout.ts +++ b/apps/meteor/client/lib/appLayout.ts @@ -1,9 +1,7 @@ import { Emitter } from '@rocket.chat/emitter'; -import { ComponentProps, createElement, lazy, ReactElement } from 'react'; +import { ReactElement } from 'react'; import { Subscription, Unsubscribe } from 'use-subscription'; -const MainLayout = lazy(() => import('../views/root/MainLayout')); - type AppLayoutDescriptor = ReactElement | null; class AppLayoutSubscription extends Emitter<{ update: void }> implements Subscription<AppLayoutDescriptor> { @@ -18,10 +16,6 @@ class AppLayoutSubscription extends Emitter<{ update: void }> implements Subscri this.emit('update'); } - renderMainLayout(props: ComponentProps<typeof MainLayout> = {}): void { - this.setCurrentValue(createElement(MainLayout, props)); - } - render(element: ReactElement): void { this.setCurrentValue(element); } diff --git a/apps/meteor/client/lib/createRouteGroup.ts b/apps/meteor/client/lib/createRouteGroup.tsx similarity index 62% rename from apps/meteor/client/lib/createRouteGroup.ts rename to apps/meteor/client/lib/createRouteGroup.tsx index 6952a430ab82..264935af7a4e 100644 --- a/apps/meteor/client/lib/createRouteGroup.ts +++ b/apps/meteor/client/lib/createRouteGroup.tsx @@ -1,38 +1,24 @@ import { FlowRouter, Group, RouteOptions } from 'meteor/kadira:flow-router'; import { ReactiveVar } from 'meteor/reactive-var'; import { Tracker } from 'meteor/tracker'; -import { ComponentType, createElement, lazy, ReactNode } from 'react'; +import React, { ElementType, ReactNode } from 'react'; +import MainLayout from '../views/root/MainLayout'; import { appLayout } from './appLayout'; -import { createTemplateForComponent } from './portals/createTemplateForComponent'; - -type RouteRegister = { - ( - path: string, - options: RouteOptions & { - lazyRouteComponent: () => Promise<{ default: ComponentType }>; - props?: Record<string, unknown>; - ready?: boolean; - }, - ): [register: () => void, unregister: () => void]; - (path: string, options: RouteOptions): void; -}; const registerLazyComponentRoute = ( routeGroup: Group, - importRouter: () => Promise<{ - default: ComponentType<{ - renderRoute?: () => ReactNode; - }>; + RouterComponent: ElementType<{ + children?: ReactNode; }>, path: string, { - lazyRouteComponent, + component: RouteComponent, props, ready = true, ...rest }: RouteOptions & { - lazyRouteComponent: () => Promise<{ default: ComponentType }>; + component: ElementType; props?: Record<string, unknown>; ready?: boolean; }, @@ -60,23 +46,18 @@ const registerLazyComponentRoute = ( computation?.stop(); }; - const RouteComponent = lazy(lazyRouteComponent); - const renderRoute = (): ReactNode => createElement(RouteComponent, props); - routeGroup.route(path, { ...rest, triggersEnter: [handleEnter, ...(rest.triggersEnter ?? [])], triggersExit: [handleExit, ...(rest.triggersExit ?? [])], action() { - const center = createTemplateForComponent( - Tracker.nonreactive(() => FlowRouter.getRouteName()), - importRouter, - { - attachment: 'at-parent', - props: () => ({ renderRoute }), - }, + appLayout.render( + <MainLayout> + <RouterComponent> + <RouteComponent {...props} /> + </RouterComponent> + </MainLayout>, ); - appLayout.renderMainLayout({ center }); }, }); @@ -86,12 +67,20 @@ const registerLazyComponentRoute = ( export const createRouteGroup = ( name: string, prefix: string, - importRouter: () => Promise<{ - default: ComponentType<{ - renderRoute?: () => ReactNode; - }>; + RouterComponent: ElementType<{ + children?: ReactNode; }>, -): RouteRegister => { +): { + ( + path: string, + options: RouteOptions & { + component: ElementType; + props?: Record<string, unknown>; + ready?: boolean; + }, + ): [register: () => void, unregister: () => void]; + (path: string, options: RouteOptions): void; +} => { const routeGroup = FlowRouter.group({ name, prefix, @@ -100,7 +89,7 @@ export const createRouteGroup = ( function registerRoute( path: string, options: RouteOptions & { - lazyRouteComponent: () => Promise<{ default: ComponentType }>; + component: ElementType; props?: Record<string, unknown>; ready?: boolean; }, @@ -111,13 +100,13 @@ export const createRouteGroup = ( options: | RouteOptions | (RouteOptions & { - lazyRouteComponent: () => Promise<{ default: ComponentType }>; + component: ElementType; props?: Record<string, unknown>; ready?: boolean; }), ): [register: () => void, unregister: () => void] | void { - if ('lazyRouteComponent' in options) { - return registerLazyComponentRoute(routeGroup, importRouter, path, options); + if ('component' in options) { + return registerLazyComponentRoute(routeGroup, RouterComponent, path, options); } routeGroup.route(path, options); @@ -126,10 +115,11 @@ export const createRouteGroup = ( registerRoute('/', { name: `${name}-index`, action() { - const center = createTemplateForComponent(`${name}-index`, importRouter, { - attachment: 'at-parent', - }); - appLayout.renderMainLayout({ center }); + appLayout.render( + <MainLayout> + <RouterComponent /> + </MainLayout>, + ); }, }); diff --git a/apps/meteor/client/providers/MeteorProvider.tsx b/apps/meteor/client/providers/MeteorProvider.tsx index 89aef340fdae..8504a59b8b15 100644 --- a/apps/meteor/client/providers/MeteorProvider.tsx +++ b/apps/meteor/client/providers/MeteorProvider.tsx @@ -1,6 +1,6 @@ import React, { FC } from 'react'; -import AttachmentProvider from '../components/Message/Attachments/providers/AttachmentProvider'; +import AttachmentProvider from '../components/message/Attachments/providers/AttachmentProvider'; import AuthorizationProvider from './AuthorizationProvider'; import AvatarUrlProvider from './AvatarUrlProvider'; import { CallProvider } from './CallProvider'; diff --git a/apps/meteor/client/startup/renderMessage/katex.ts b/apps/meteor/client/startup/renderMessage/katex.ts index 005181c9b92e..68b76006c117 100644 --- a/apps/meteor/client/startup/renderMessage/katex.ts +++ b/apps/meteor/client/startup/renderMessage/katex.ts @@ -19,7 +19,7 @@ Meteor.startup(() => { }; import('../../../app/katex/client').then(({ createKatexMessageRendering }) => { - const renderMessage = createKatexMessageRendering(options); + const renderMessage = createKatexMessageRendering(options, true); callbacks.remove('renderMessage', 'katex'); callbacks.add('renderMessage', renderMessage, callbacks.priority.HIGH + 1, 'katex'); }); diff --git a/apps/meteor/client/startup/routes.tsx b/apps/meteor/client/startup/routes.tsx index b73c5969d6f3..3a64b75caed5 100644 --- a/apps/meteor/client/startup/routes.tsx +++ b/apps/meteor/client/startup/routes.tsx @@ -11,9 +11,10 @@ import toastr from 'toastr'; import { KonchatNotification } from '../../app/ui/client'; import { APIClient } from '../../app/utils/client'; import { appLayout } from '../lib/appLayout'; -import { createTemplateForComponent } from '../lib/portals/createTemplateForComponent'; import { dispatchToastMessage } from '../lib/toast'; import { handleError } from '../lib/utils/handleError'; +import BlazeTemplate from '../views/root/BlazeTemplate'; +import MainLayout from '../views/root/MainLayout'; const InvitePage = lazy(() => import('../views/invite/InvitePage')); const SecretURLPage = lazy(() => import('../views/invite/SecretURLPage')); @@ -23,13 +24,22 @@ const SetupWizardRoute = lazy(() => import('../views/setupWizard/SetupWizardRout const MailerUnsubscriptionPage = lazy(() => import('../views/mailer/MailerUnsubscriptionPage')); const NotFoundPage = lazy(() => import('../views/notFound/NotFoundPage')); const MeetPage = lazy(() => import('../views/meet/MeetPage')); +const DirectoryPage = lazy(() => import('../views/directory/DirectoryPage')); +const OmnichannelDirectoryPage = lazy(() => import('../views/omnichannel/directory/OmnichannelDirectoryPage')); +const OmnichannelQueueList = lazy(() => import('../views/omnichannel/queueList')); +const AccountRoute = lazy(() => import('../views/account/AccountRoute')); FlowRouter.wait(); FlowRouter.route('/', { name: 'index', action() { - appLayout.renderMainLayout({ center: 'loading' }); + appLayout.render( + <MainLayout> + <BlazeTemplate template='loading' /> + </MainLayout>, + ); + if (!Meteor.userId()) { return FlowRouter.go('home'); } @@ -104,55 +114,65 @@ FlowRouter.route('/home', { } } - appLayout.renderMainLayout({ center: 'home' }); + appLayout.render( + <MainLayout> + <BlazeTemplate template='home' /> + </MainLayout>, + ); }); return; } - appLayout.renderMainLayout({ center: 'home' }); + appLayout.render( + <MainLayout> + <BlazeTemplate template='home' /> + </MainLayout>, + ); }, }); FlowRouter.route('/directory/:tab?', { name: 'directory', action: () => { - const DirectoryPage = createTemplateForComponent('DirectoryPage', () => import('../views/directory/DirectoryPage'), { - attachment: 'at-parent', - }); - appLayout.renderMainLayout({ center: DirectoryPage }); + appLayout.render( + <MainLayout> + <DirectoryPage /> + </MainLayout>, + ); }, }); FlowRouter.route('/omnichannel-directory/:page?/:bar?/:id?/:tab?/:context?', { name: 'omnichannel-directory', action: () => { - const OmnichannelDirectoryPage = createTemplateForComponent( - 'OmnichannelDirectoryPage', - () => import('../views/omnichannel/directory/OmnichannelDirectoryPage'), - { attachment: 'at-parent' }, + appLayout.render( + <MainLayout> + <OmnichannelDirectoryPage /> + </MainLayout>, ); - appLayout.renderMainLayout({ center: OmnichannelDirectoryPage }); }, }); FlowRouter.route('/livechat-queue', { name: 'livechat-queue', action: () => { - const OmnichannelQueueList = createTemplateForComponent('QueueList', () => import('../views/omnichannel/queueList'), { - attachment: 'at-parent', - }); - appLayout.renderMainLayout({ center: OmnichannelQueueList }); + appLayout.render( + <MainLayout> + <OmnichannelQueueList /> + </MainLayout>, + ); }, }); FlowRouter.route('/account/:group?', { name: 'account', action: () => { - const AccountRoute = createTemplateForComponent('AccountRoute', () => import('../views/account/AccountRoute'), { - attachment: 'at-parent', - }); - appLayout.renderMainLayout({ center: AccountRoute }); + appLayout.render( + <MainLayout> + <AccountRoute /> + </MainLayout>, + ); }, }); @@ -181,7 +201,11 @@ FlowRouter.route('/room-not-found/:type/:name', { name: 'room-not-found', action: ({ type, name } = {}) => { Session.set('roomNotFound', { type, name }); - appLayout.renderMainLayout({ center: 'roomNotFound' }); + appLayout.render( + <MainLayout> + <BlazeTemplate template='roomNotFound' /> + </MainLayout>, + ); }, }); @@ -240,35 +264,33 @@ FlowRouter.route('/reset-password/:token', { FlowRouter.route('/snippet/:snippetId/:snippetName', { name: 'snippetView', action() { - appLayout.renderMainLayout({ center: 'snippetPage' }); + appLayout.render( + <MainLayout> + <BlazeTemplate template='snippetPage' /> + </MainLayout>, + ); }, }); FlowRouter.route('/oauth/authorize', { name: 'oauth/authorize', - action(_params, queryParams) { - appLayout.renderMainLayout({ - center: 'authorize', - modal: true, - // eslint-disable-next-line @typescript-eslint/camelcase - client_id: queryParams?.client_id, - // eslint-disable-next-line @typescript-eslint/camelcase - redirect_uri: queryParams?.redirect_uri, - // eslint-disable-next-line @typescript-eslint/camelcase - response_type: queryParams?.response_type, - state: queryParams?.state, - }); + action() { + appLayout.render( + <MainLayout> + <BlazeTemplate template='authorize' /> + </MainLayout>, + ); }, }); FlowRouter.route('/oauth/error/:error', { name: 'oauth/error', - action(params) { - appLayout.renderMainLayout({ - center: 'oauth404', - modal: true, - error: params?.error, - }); + action() { + appLayout.render( + <MainLayout> + <BlazeTemplate template='oauth404' /> + </MainLayout>, + ); }, }); diff --git a/apps/meteor/client/templates.ts b/apps/meteor/client/templates.ts index 279507d1764c..eecf160432d4 100644 --- a/apps/meteor/client/templates.ts +++ b/apps/meteor/client/templates.ts @@ -2,18 +2,18 @@ import { HTML } from 'meteor/htmljs'; import { createTemplateForComponent } from './lib/portals/createTemplateForComponent'; -createTemplateForComponent('MessageActions', () => import('./components/Message/MessageActions')); +createTemplateForComponent('MessageActions', () => import('./components/message/MessageActions')); -createTemplateForComponent('reactAttachments', () => import('./components/Message/Attachments')); +createTemplateForComponent('reactAttachments', () => import('./components/message/Attachments')); -createTemplateForComponent('ThreadMetric', () => import('./components/Message/Metrics/Thread'), { +createTemplateForComponent('ThreadMetric', () => import('./components/message/Metrics/Thread'), { renderContainerView: () => HTML.DIV({ style: 'min-height: 36px;', }), }); -createTemplateForComponent('DiscussionMetric', () => import('./components/Message/Metrics/Discussion'), { +createTemplateForComponent('DiscussionMetric', () => import('./components/message/Metrics/Discussion'), { renderContainerView: () => HTML.DIV({ style: 'min-height: 36px;', @@ -21,9 +21,8 @@ createTemplateForComponent('DiscussionMetric', () => import('./components/Messag }); createTemplateForComponent('MessageList', () => import('./views/room/MessageList/MessageList')); -createTemplateForComponent('MessageBody', () => import('./components/Message/MessageBodyRender')); -createTemplateForComponent('BroadCastMetric', () => import('./components/Message/Metrics/Broadcast')); +createTemplateForComponent('BroadCastMetric', () => import('./components/message/Metrics/Broadcast')); createTemplateForComponent( 'Checkbox', @@ -55,14 +54,6 @@ createTemplateForComponent('omnichannelFlex', () => import('./views/omnichannel/ renderContainerView: () => HTML.DIV({ style: 'height: 100%; position: relative;' }), }); -createTemplateForComponent('auditPage', () => import('../ee/client/audit/AuditPage'), { - attachment: 'at-parent', -}); - -createTemplateForComponent('auditLogPage', () => import('../ee/client/audit/AuditLogPage'), { - attachment: 'at-parent', -}); - createTemplateForComponent('DiscussionMessageList', () => import('./views/room/contextualBar/Discussions'), { renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), }); @@ -127,8 +118,6 @@ createTemplateForComponent('channelFilesList', () => import('./views/room/contex renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), }); -createTemplateForComponent('RoomAnnouncement', () => import('./views/room/Announcement')); - createTemplateForComponent('PruneMessages', () => import('./views/room/contextualBar/PruneMessages'), { renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), }); diff --git a/apps/meteor/client/views/admin/AdministrationRouter.tsx b/apps/meteor/client/views/admin/AdministrationRouter.tsx index 6a9095a83617..1d4050d85283 100644 --- a/apps/meteor/client/views/admin/AdministrationRouter.tsx +++ b/apps/meteor/client/views/admin/AdministrationRouter.tsx @@ -1,12 +1,16 @@ import { useCurrentRoute, useRoute } from '@rocket.chat/ui-contexts'; -import React, { Suspense, ReactElement, useEffect } from 'react'; +import React, { Suspense, ReactElement, useEffect, ReactNode } from 'react'; import PageSkeleton from '../../components/PageSkeleton'; import SettingsProvider from '../../providers/SettingsProvider'; import { useUpgradeTabParams } from '../hooks/useUpgradeTabParams'; import AdministrationLayout from './AdministrationLayout'; -const AdministrationRouter = ({ renderRoute }: { renderRoute: () => ReactElement }): ReactElement => { +type AdministrationRouterProps = { + children?: ReactNode; +}; + +const AdministrationRouter = ({ children }: AdministrationRouterProps): ReactElement => { const { tabType, trialEndDate, isLoading } = useUpgradeTabParams(); const [routeName] = useCurrentRoute(); const defaultRoute = useRoute('admin-info'); @@ -28,7 +32,7 @@ const AdministrationRouter = ({ renderRoute }: { renderRoute: () => ReactElement return ( <AdministrationLayout> <SettingsProvider privileged> - {renderRoute ? <Suspense fallback={<PageSkeleton />}>{renderRoute()}</Suspense> : <PageSkeleton />} + {children ? <Suspense fallback={<PageSkeleton />}>{children}</Suspense> : <PageSkeleton />} </SettingsProvider> </AdministrationLayout> ); diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx index 89695f9f648d..87c78ad2193f 100644 --- a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx @@ -1,6 +1,6 @@ import { Button, Icon } from '@rocket.chat/fuselage'; import { useRoute, useRouteParameter, usePermission, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useCallback, ReactNode, useRef } from 'react'; +import React, { useCallback, useRef, ReactElement } from 'react'; import Page from '../../../components/Page'; import VerticalBar from '../../../components/VerticalBar'; @@ -8,7 +8,7 @@ import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import CustomUserStatusFormWithData from './CustomUserStatusFormWithData'; import CustomUserStatusTable from './CustomUserStatusTable'; -const CustomUserStatusRoute = (): ReactNode => { +const CustomUserStatusRoute = (): ReactElement => { const t = useTranslation(); const route = useRoute('custom-user-status'); const context = useRouteParameter('context'); diff --git a/apps/meteor/client/views/admin/routes.js b/apps/meteor/client/views/admin/routes.tsx similarity index 54% rename from apps/meteor/client/views/admin/routes.js rename to apps/meteor/client/views/admin/routes.tsx index 1af09369eef7..b1946f331949 100644 --- a/apps/meteor/client/views/admin/routes.js +++ b/apps/meteor/client/views/admin/routes.tsx @@ -1,135 +1,147 @@ +import React, { lazy } from 'react'; + import { appLayout } from '../../lib/appLayout'; import { createRouteGroup } from '../../lib/createRouteGroup'; +import BlazeTemplate from '../root/BlazeTemplate'; +import MainLayout from '../root/MainLayout'; -export const registerAdminRoute = createRouteGroup('admin', '/admin', () => import('./AdministrationRouter')); +export const registerAdminRoute = createRouteGroup( + 'admin', + '/admin', + lazy(() => import('./AdministrationRouter')), +); registerAdminRoute('/custom-sounds/:context?/:id?', { name: 'custom-sounds', - lazyRouteComponent: () => import('./customSounds/AdminSoundsRoute'), + component: lazy(() => import('./customSounds/AdminSoundsRoute')), }); registerAdminRoute('/apps/what-is-it', { name: 'admin-apps-disabled', - lazyRouteComponent: () => import('./apps/AppsWhatIsIt'), + component: lazy(() => import('./apps/AppsWhatIsIt')), }); registerAdminRoute('/marketplace/:context?/:id?/:version?', { name: 'admin-marketplace', - lazyRouteComponent: () => import('./apps/AppsRoute'), + component: lazy(() => import('./apps/AppsRoute')), }); registerAdminRoute('/apps/:context?/:id?/:version?/:tab?', { name: 'admin-apps', - lazyRouteComponent: () => import('./apps/AppsRoute'), + component: lazy(() => import('./apps/AppsRoute')), }); registerAdminRoute('/info', { name: 'admin-info', - lazyRouteComponent: () => import('./info/InformationRoute'), + component: lazy(() => import('./info/InformationRoute')), }); registerAdminRoute('/import', { name: 'admin-import', - lazyRouteComponent: () => import('./import/ImportRoute'), + component: lazy(() => import('./import/ImportRoute')), props: { page: 'history' }, }); registerAdminRoute('/import/new/:importerKey?', { name: 'admin-import-new', - lazyRouteComponent: () => import('./import/ImportRoute'), + component: lazy(() => import('./import/ImportRoute')), props: { page: 'new' }, }); registerAdminRoute('/import/prepare', { name: 'admin-import-prepare', - lazyRouteComponent: () => import('./import/ImportRoute'), + component: lazy(() => import('./import/ImportRoute')), props: { page: 'prepare' }, }); registerAdminRoute('/import/progress', { name: 'admin-import-progress', - lazyRouteComponent: () => import('./import/ImportRoute'), + component: lazy(() => import('./import/ImportRoute')), props: { page: 'progress' }, }); registerAdminRoute('/mailer', { name: 'admin-mailer', - lazyRouteComponent: () => import('./mailer/MailerRoute'), + component: lazy(() => import('./mailer/MailerRoute')), }); registerAdminRoute('/oauth-apps/:context?/:id?', { name: 'admin-oauth-apps', - lazyRouteComponent: () => import('./oauthApps/OAuthAppsRoute'), + component: lazy(() => import('./oauthApps/OAuthAppsRoute')), }); registerAdminRoute('/integrations/:context?/:type?/:id?', { name: 'admin-integrations', - lazyRouteComponent: () => import('./integrations/IntegrationsRoute'), + component: lazy(() => import('./integrations/IntegrationsRoute')), }); registerAdminRoute('/custom-user-status/:context?/:id?', { name: 'custom-user-status', - lazyRouteComponent: () => import('./customUserStatus/CustomUserStatusRoute'), + component: lazy(() => import('./customUserStatus/CustomUserStatusRoute')), }); registerAdminRoute('/emoji-custom/:context?/:id?', { name: 'emoji-custom', - lazyRouteComponent: () => import('./customEmoji/CustomEmojiRoute'), + component: lazy(() => import('./customEmoji/CustomEmojiRoute')), }); registerAdminRoute('/users/:context?/:id?', { name: 'admin-users', - lazyRouteComponent: () => import('./users/UsersRoute'), + component: lazy(() => import('./users/UsersRoute')), }); registerAdminRoute('/rooms/:context?/:id?', { name: 'admin-rooms', - lazyRouteComponent: () => import('./rooms/RoomsRoute'), + component: lazy(() => import('./rooms/RoomsRoute')), }); registerAdminRoute('/invites', { name: 'invites', - lazyRouteComponent: () => import('./invites/InvitesRoute'), + component: lazy(() => import('./invites/InvitesRoute')), }); registerAdminRoute('/cloud/:page?', { name: 'cloud', - lazyRouteComponent: () => import('./cloud/CloudRoute'), + component: lazy(() => import('./cloud/CloudRoute')), }); registerAdminRoute('/view-logs', { name: 'admin-view-logs', - lazyRouteComponent: () => import('./viewLogs/ViewLogsRoute'), + component: lazy(() => import('./viewLogs/ViewLogsRoute')), }); registerAdminRoute('/federation-dashboard', { name: 'federation-dashboard', - lazyRouteComponent: () => import('./federationDashboard/FederationDashboardRoute'), + component: lazy(() => import('./federationDashboard/FederationDashboardRoute')), }); registerAdminRoute('/permissions/:context?/:_id?', { name: 'admin-permissions', - lazyRouteComponent: () => import('./permissions/PermissionsRouter'), + component: lazy(() => import('./permissions/PermissionsRouter')), }); registerAdminRoute('/email-inboxes/:context?/:_id?', { name: 'admin-email-inboxes', - lazyRouteComponent: () => import('./emailInbox/EmailInboxRoute'), + component: lazy(() => import('./emailInbox/EmailInboxRoute')), }); registerAdminRoute('/settings/:group?', { name: 'admin-settings', - lazyRouteComponent: () => import('./settings/SettingsRoute'), + component: lazy(() => import('./settings/SettingsRoute')), }); registerAdminRoute('/chatpal', { name: 'chatpal-admin', action() { - appLayout.renderMainLayout({ center: 'ChatpalAdmin' }); + appLayout.render( + <MainLayout> + <BlazeTemplate template='ChatpalAdmin' /> + </MainLayout>, + ); }, }); registerAdminRoute('/upgrade/:type?', { name: 'upgrade', - lazyRouteComponent: () => import('./upgrade/UpgradePage'), + component: lazy(() => import('./upgrade/UpgradePage')), }); diff --git a/apps/meteor/client/views/blocks/MessageBlock.js b/apps/meteor/client/views/blocks/MessageBlock.js index 035f09eb1582..85e6fc577ab1 100644 --- a/apps/meteor/client/views/blocks/MessageBlock.js +++ b/apps/meteor/client/views/blocks/MessageBlock.js @@ -3,7 +3,7 @@ import { UiKitMessage, UiKitComponent, kitContext, messageParser } from '@rocket import React from 'react'; import * as ActionManager from '../../../app/ui-message/client/ActionManager'; -import { useBlockRendered } from '../../components/Message/hooks/useBlockRendered'; +import { useBlockRendered } from '../../components/message/hooks/useBlockRendered'; import { renderMessageBody } from '../../lib/utils/renderMessageBody'; import './textParsers'; diff --git a/apps/meteor/client/views/notFound/NotFoundPage.js b/apps/meteor/client/views/notFound/NotFoundPage.js deleted file mode 100644 index fd78d24879c1..000000000000 --- a/apps/meteor/client/views/notFound/NotFoundPage.js +++ /dev/null @@ -1,63 +0,0 @@ -import { Box, Button, ButtonGroup, Flex, Margins } from '@rocket.chat/fuselage'; -import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; - -function NotFoundPage() { - const t = useTranslation(); - const homeRoute = useRoute('home'); - - const handleGoToPreviousPageClick = () => { - window.history.back(); - }; - - const handleGoHomeClick = () => { - homeRoute.push(); - }; - - return ( - <Flex.Container direction='column' justifyContent='center' alignItems='center'> - <Box - is='section' - width='full' - minHeight='sh' - textAlign='center' - backgroundColor='neutral-800' - style={{ - backgroundImage: "url('/images/404.svg')", - backgroundRepeat: 'no-repeat', - backgroundPosition: 'center', - backgroundSize: 'cover', - }} - > - <Flex.Item> - <Box> - <Margins all='x12'> - <Box fontWeight='p2m' fontSize='x64' color='alternative'> - 404 - </Box> - - <Box role='heading' aria-level='1' fontScale='h2' color='alternative'> - {t('Oops_page_not_found')} - </Box> - - <Box role='status' aria-label='Sorry_page_you_requested_does_not_exist_or_was_deleted' fontScale='p2' color='alternative'> - {t('Sorry_page_you_requested_does_not_exist_or_was_deleted')} - </Box> - </Margins> - - <ButtonGroup align='center' margin='x64'> - <Button type='button' primary onClick={handleGoToPreviousPageClick}> - {t('Return_to_previous_page')} - </Button> - <Button type='button' primary onClick={handleGoHomeClick}> - {t('Return_to_home')} - </Button> - </ButtonGroup> - </Box> - </Flex.Item> - </Box> - </Flex.Container> - ); -} - -export default NotFoundPage; diff --git a/apps/meteor/client/views/omnichannel/OmnichannelRouter.tsx b/apps/meteor/client/views/omnichannel/OmnichannelRouter.tsx index 025fa0472aad..d7c477b86390 100644 --- a/apps/meteor/client/views/omnichannel/OmnichannelRouter.tsx +++ b/apps/meteor/client/views/omnichannel/OmnichannelRouter.tsx @@ -1,14 +1,14 @@ import { useCurrentRoute, useRoute } from '@rocket.chat/ui-contexts'; -import React, { ReactNode, Suspense, useEffect, FC } from 'react'; +import React, { ReactNode, Suspense, useEffect, ReactElement } from 'react'; import { SideNav } from '../../../app/ui-utils/client'; import PageSkeleton from '../../components/PageSkeleton'; type OmnichannelRouterProps = { - renderRoute?: () => ReactNode; + children?: ReactNode; }; -const OmnichannelRouter: FC<OmnichannelRouterProps> = ({ renderRoute }) => { +const OmnichannelRouter = ({ children }: OmnichannelRouterProps): ReactElement => { const [routeName] = useCurrentRoute(); const defaultRoute = useRoute('omnichannel-current-chats'); useEffect(() => { @@ -22,7 +22,7 @@ const OmnichannelRouter: FC<OmnichannelRouterProps> = ({ renderRoute }) => { SideNav.openFlex(() => undefined); }, []); - return renderRoute ? <Suspense fallback={<PageSkeleton />}>{renderRoute()}</Suspense> : <PageSkeleton />; + return children ? <Suspense fallback={<PageSkeleton />}>{children}</Suspense> : <PageSkeleton />; }; export default OmnichannelRouter; diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentsAgentsTable.js b/apps/meteor/client/views/omnichannel/departments/DepartmentsAgentsTable.js index f62fd6931838..09cef271a255 100644 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentsAgentsTable.js +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentsAgentsTable.js @@ -16,7 +16,7 @@ function DepartmentsAgentsTable({ agents, setAgentListFinal, setAgentsAdded, set return ( <> - <AddAgent agentList={agentList} setAgentList={setAgentList} setAgentsAdded={setAgentsAdded} /> + <AddAgent agentList={agentList} data-qa='DepartmentSelect-AgentsTable' setAgentList={setAgentList} setAgentsAdded={setAgentsAdded} /> <GenericTable header={ <> diff --git a/apps/meteor/client/views/omnichannel/departments/EditDepartment.js b/apps/meteor/client/views/omnichannel/departments/EditDepartment.js index e1f3826c13da..f14f849ce062 100644 --- a/apps/meteor/client/views/omnichannel/departments/EditDepartment.js +++ b/apps/meteor/client/views/omnichannel/departments/EditDepartment.js @@ -280,7 +280,7 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { <Page.ScrollableContentWithShadow> <FieldGroup w='full' alignSelf='center' maxWidth='x600' id={formId} is='form' autoComplete='off' onSubmit={handleSubmit}> <Field> - <Box display='flex' flexDirection='row'> + <Box display='flex' data-qa='DepartmentEditToggle-Enabled' flexDirection='row'> <Field.Label>{t('Enabled')}</Field.Label> <Field.Row> <ToggleSwitch flexGrow={1} checked={enabled} onChange={handleEnabled} /> @@ -290,17 +290,30 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { <Field> <Field.Label>{t('Name')}*</Field.Label> <Field.Row> - <TextInput flexGrow={1} error={nameError} value={name} onChange={handleName} placeholder={t('Name')} /> + <TextInput + data-qa='DepartmentEditTextInput-Name' + flexGrow={1} + error={nameError} + value={name} + onChange={handleName} + placeholder={t('Name')} + /> </Field.Row> </Field> <Field> <Field.Label>{t('Description')}</Field.Label> <Field.Row> - <TextAreaInput flexGrow={1} value={description} onChange={handleDescription} placeholder={t('Description')} /> + <TextAreaInput + data-qa='DepartmentEditTextInput-Description' + flexGrow={1} + value={description} + onChange={handleDescription} + placeholder={t('Description')} + /> </Field.Row> </Field> <Field> - <Box display='flex' flexDirection='row'> + <Box data-qa='DepartmentEditToggle-ShowOnRegistrationPage' display='flex' flexDirection='row'> <Field.Label>{t('Show_on_registration_page')}</Field.Label> <Field.Row> <ToggleSwitch flexGrow={1} checked={showOnRegistration} onChange={handleShowOnRegistration} /> @@ -311,6 +324,7 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { <Field.Label>{t('Email')}*</Field.Label> <Field.Row> <TextInput + data-qa='DepartmentEditTextInput-Email' flexGrow={1} error={emailError} value={email} @@ -321,7 +335,7 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { </Field.Row> </Field> <Field> - <Box display='flex' flexDirection='row'> + <Box display='flex' data-qa='DepartmentEditToggle-ShowOnOfflinePage' flexDirection='row'> <Field.Label>{t('Show_on_offline_page')}</Field.Label> <Field.Row> <ToggleSwitch flexGrow={1} checked={showOnOfflineForm} onChange={handleShowOnOfflineForm} /> @@ -332,6 +346,7 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { <Field.Label>{t('Livechat_DepartmentOfflineMessageToChannel')}</Field.Label> <Field.Row> <PaginatedSelectFiltered + data-qa='DepartmentSelect-LivechatDepartmentOfflineMessageToChannel' value={offlineMessageChannelName} onChange={handleOfflineMessageChannelName} flexShrink={0} @@ -404,10 +419,15 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { </Field> )} <Field> - <Box display='flex' flexDirection='row'> + <Box display='flex' data-qa='DiscussionToggle-RequestTagBeforeCLosingChat' flexDirection='row'> <Field.Label>{t('Request_tag_before_closing_chat')}</Field.Label> <Field.Row> - <ToggleSwitch flexGrow={1} checked={requestTagBeforeClosingChat} onChange={handleRequestTagBeforeClosingChat} /> + <ToggleSwitch + data-qa='DiscussionToggle-RequestTagBeforeCLosingChat' + flexGrow={1} + checked={requestTagBeforeClosingChat} + onChange={handleRequestTagBeforeClosingChat} + /> </Field.Row> </Box> </Field> @@ -415,7 +435,13 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { <Field> <Field.Label alignSelf='stretch'>{t('Conversation_closing_tags')}*</Field.Label> <Field.Row> - <TextInput error={tagError} value={tagsText} onChange={handleTagTextChange} placeholder={t('Enter_a_tag')} /> + <TextInput + data-qa='DepartmentEditTextInput-ConversationClosingTags' + error={tagError} + value={tagsText} + onChange={handleTagTextChange} + placeholder={t('Enter_a_tag')} + /> <Button mis='x8' title={t('add')} onClick={handleTagTextSubmit}> {t('Add')} </Button> diff --git a/apps/meteor/client/views/omnichannel/directory/calls/CallTable.tsx b/apps/meteor/client/views/omnichannel/directory/calls/CallTable.tsx index 2c9485b74e4d..7d964af4c16a 100644 --- a/apps/meteor/client/views/omnichannel/directory/calls/CallTable.tsx +++ b/apps/meteor/client/views/omnichannel/directory/calls/CallTable.tsx @@ -22,7 +22,7 @@ const useQuery = ( userIdLoggedIn: string | null, ): { sort: string; - open: boolean; + open: 'false'; roomName: string; agents: string[]; count?: number; @@ -31,7 +31,7 @@ const useQuery = ( useMemo( () => ({ sort: JSON.stringify({ [column]: direction === 'asc' ? 1 : -1 }), - open: false, + open: 'false', roomName: text || '', agents: userIdLoggedIn ? [userIdLoggedIn] : [], ...(itemsPerPage && { count: itemsPerPage }), diff --git a/apps/meteor/client/views/omnichannel/queueList/hooks/useQuery.ts b/apps/meteor/client/views/omnichannel/queueList/hooks/useQuery.ts index 880c157455d4..76a5fe26dfcb 100644 --- a/apps/meteor/client/views/omnichannel/queueList/hooks/useQuery.ts +++ b/apps/meteor/client/views/omnichannel/queueList/hooks/useQuery.ts @@ -12,7 +12,7 @@ type useQueryType = ( debouncedSort: [string, 'asc' | 'desc'], ) => { agentId?: ILivechatAgent['_id']; - includeOfflineAgents?: boolean; + includeOfflineAgents?: 'true' | 'false'; departmentId?: ILivechatAgent['_id']; offset: number; count: number; @@ -25,7 +25,7 @@ export const useQuery: useQueryType = ({ servedBy, status, departmentId, itemsPe useMemo(() => { const query: { agentId?: string; - includeOflineAgents?: boolean; + includeOflineAgents?: 'true' | 'false'; departmentId?: string; sort: string; count: number; @@ -39,7 +39,7 @@ export const useQuery: useQueryType = ({ servedBy, status, departmentId, itemsPe }; if (status !== 'online') { - query.includeOflineAgents = true; + query.includeOflineAgents = 'true'; } if (servedBy) { query.agentId = servedBy; diff --git a/apps/meteor/client/views/omnichannel/routes.js b/apps/meteor/client/views/omnichannel/routes.ts similarity index 53% rename from apps/meteor/client/views/omnichannel/routes.js rename to apps/meteor/client/views/omnichannel/routes.ts index 8fd4c1e5fa45..d7ab9012aca7 100644 --- a/apps/meteor/client/views/omnichannel/routes.js +++ b/apps/meteor/client/views/omnichannel/routes.ts @@ -1,83 +1,89 @@ +import { lazy } from 'react'; + import { createRouteGroup } from '../../lib/createRouteGroup'; -export const registerOmnichannelRoute = createRouteGroup('omnichannel', '/omnichannel', () => import('./OmnichannelRouter')); +export const registerOmnichannelRoute = createRouteGroup( + 'omnichannel', + '/omnichannel', + lazy(() => import('./OmnichannelRouter')), +); registerOmnichannelRoute('/installation', { name: 'omnichannel-installation', - lazyRouteComponent: () => import('./installation/Installation'), + component: lazy(() => import('./installation/Installation')), }); registerOmnichannelRoute('/managers', { name: 'omnichannel-managers', - lazyRouteComponent: () => import('./managers/ManagersRoute'), + component: lazy(() => import('./managers/ManagersRoute')), }); registerOmnichannelRoute('/agents/:context?/:id?', { name: 'omnichannel-agents', - lazyRouteComponent: () => import('./agents/AgentsRoute'), + component: lazy(() => import('./agents/AgentsRoute')), }); registerOmnichannelRoute('/webhooks', { name: 'omnichannel-webhooks', - lazyRouteComponent: () => import('./webhooks/WebhooksPageContainer'), + component: lazy(() => import('./webhooks/WebhooksPageContainer')), }); registerOmnichannelRoute('/customfields/:context?/:id?', { name: 'omnichannel-customfields', - lazyRouteComponent: () => import('./customFields/CustomFieldsRoute'), + component: lazy(() => import('./customFields/CustomFieldsRoute')), }); registerOmnichannelRoute('/appearance', { name: 'omnichannel-appearance', - lazyRouteComponent: () => import('./appearance/AppearancePageContainer'), + component: lazy(() => import('./appearance/AppearancePageContainer')), }); registerOmnichannelRoute('/businessHours/:context?/:type?/:id?', { name: 'omnichannel-businessHours', - lazyRouteComponent: () => import('./businessHours/BusinessHoursRouter'), + component: lazy(() => import('./businessHours/BusinessHoursRouter')), }); registerOmnichannelRoute('/units/:context?/:id?', { name: 'omnichannel-units', - lazyRouteComponent: () => import('../../../ee/client/omnichannel/units/UnitsRoute'), + component: lazy(() => import('../../../ee/client/omnichannel/units/UnitsRoute')), }); registerOmnichannelRoute('/tags/:context?/:id?', { name: 'omnichannel-tags', - lazyRouteComponent: () => import('../../../ee/client/omnichannel/tags/TagsRoute'), + component: lazy(() => import('../../../ee/client/omnichannel/tags/TagsRoute')), }); registerOmnichannelRoute('/priorities/:context?/:id?', { name: 'omnichannel-priorities', - lazyRouteComponent: () => import('../../../ee/client/omnichannel/priorities/PrioritiesRoute'), + component: lazy(() => import('../../../ee/client/omnichannel/priorities/PrioritiesRoute')), }); registerOmnichannelRoute('/triggers/:context?/:id?', { name: 'omnichannel-triggers', - lazyRouteComponent: () => import('./triggers/TriggersPage'), + component: lazy(() => import('./triggers/TriggersPage')), }); registerOmnichannelRoute('/facebook', { name: 'omnichannel-facebook', - lazyRouteComponent: () => import('./facebook/FacebookPageContainer'), + component: lazy(() => import('./facebook/FacebookPageContainer')), }); registerOmnichannelRoute('/current/:id?/:tab?/:context?', { name: 'omnichannel-current-chats', - lazyRouteComponent: () => import('./currentChats/CurrentChatsRoute'), + component: lazy(() => import('./currentChats/CurrentChatsRoute')), }); registerOmnichannelRoute('/departments/:context?/:id?/:tab?', { name: 'omnichannel-departments', - lazyRouteComponent: () => import('./departments/DepartmentsRoute'), + component: lazy(() => import('./departments/DepartmentsRoute')), }); registerOmnichannelRoute('/realtime-monitoring', { name: 'omnichannel-realTime', - lazyRouteComponent: () => import('./realTimeMonitoring/RealTimeMonitoringPage'), + component: lazy(() => import('./realTimeMonitoring/RealTimeMonitoringPage')), }); registerOmnichannelRoute('/analytics', { name: 'omnichannel-analytics', - lazyRouteComponent: () => import('./analytics/AnalyticsPage'), + component: lazy(() => import('./analytics/AnalyticsPage')), }); diff --git a/apps/meteor/client/views/room/Announcement/Announcement.tsx b/apps/meteor/client/views/room/Announcement/Announcement.tsx index 6c2bc0c81436..2691ea9b7a7c 100644 --- a/apps/meteor/client/views/room/Announcement/Announcement.tsx +++ b/apps/meteor/client/views/room/Announcement/Announcement.tsx @@ -1,10 +1,11 @@ +import { Box } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal } from '@rocket.chat/ui-contexts'; +import { useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; import React, { FC, MouseEvent } from 'react'; +import GenericModal from '../../../components/GenericModal'; import MarkdownText from '../../../components/MarkdownText'; import AnnouncementComponent from './AnnouncementComponent'; -import AnnouncementModal from './AnnouncementModal'; type AnnouncementParams = { announcement: string; @@ -12,6 +13,7 @@ type AnnouncementParams = { }; const Announcement: FC<AnnouncementParams> = ({ announcement, announcementDetails }) => { + const t = useTranslation(); const setModal = useSetModal(); const closeModal = useMutableCallback(() => setModal(null)); const handleClick = (e: MouseEvent<HTMLAnchorElement>): void => { @@ -23,7 +25,15 @@ const Announcement: FC<AnnouncementParams> = ({ announcement, announcementDetail return; } - announcementDetails ? announcementDetails() : setModal(<AnnouncementModal onClose={closeModal}>{announcement}</AnnouncementModal>); + announcementDetails + ? announcementDetails() + : setModal( + <GenericModal icon={null} title={t('Announcement')} confirmText={t('Close')} onConfirm={closeModal} onClose={closeModal}> + <Box> + <MarkdownText content={announcement} /> + </Box> + </GenericModal>, + ); }; return announcement ? ( diff --git a/apps/meteor/client/views/room/Announcement/AnnouncementModal.tsx b/apps/meteor/client/views/room/Announcement/AnnouncementModal.tsx deleted file mode 100644 index f5003b06358d..000000000000 --- a/apps/meteor/client/views/room/Announcement/AnnouncementModal.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Button, ButtonGroup, Box, Modal } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { FC } from 'react'; - -import MarkdownText from '../../../components/MarkdownText'; - -type AnnouncementModalParams = { - onClose: () => void; - confirmLabel?: string; - children?: string; -}; - -const AnnouncementModal: FC<AnnouncementModalParams> = ({ onClose, confirmLabel = 'Close', children, ...props }) => { - const t = useTranslation(); - - return ( - <Modal {...props}> - <Modal.Header> - <Modal.Title>{t('Announcement')}</Modal.Title> - <Modal.Close onClick={onClose} /> - </Modal.Header> - <Modal.Content> - <Box> - <MarkdownText content={children} /> - </Box> - </Modal.Content> - <Modal.Footer> - <ButtonGroup align='end'> - <Button onClick={onClose}>{confirmLabel}</Button> - </ButtonGroup> - </Modal.Footer> - </Modal> - ); -}; - -export default AnnouncementModal; diff --git a/apps/meteor/client/views/room/MessageList/components/Message.tsx b/apps/meteor/client/views/room/MessageList/components/Message.tsx index 8c27a72b2bdb..4fcf6c8325ea 100644 --- a/apps/meteor/client/views/room/MessageList/components/Message.tsx +++ b/apps/meteor/client/views/room/MessageList/components/Message.tsx @@ -38,6 +38,7 @@ const Message: FC<{ message: IMessage; sequential: boolean; subscription?: ISubs isSelected={isSelected} isEditing={isMessageHighlight} isPending={message.temp} + sequential={sequential} data-qa-editing={isMessageHighlight} data-qa-selected={isSelected} > diff --git a/apps/meteor/client/views/room/MessageList/components/MessageContent.tsx b/apps/meteor/client/views/room/MessageList/components/MessageContent.tsx index 4bcb365d6536..edddebb5ae16 100644 --- a/apps/meteor/client/views/room/MessageList/components/MessageContent.tsx +++ b/apps/meteor/client/views/room/MessageList/components/MessageContent.tsx @@ -5,11 +5,11 @@ import { useTranslation, useUserId, TranslationKey } from '@rocket.chat/ui-conte import React, { FC, memo } from 'react'; import { isE2EEMessage } from '../../../../../lib/isE2EEMessage'; -import Attachments from '../../../../components/Message/Attachments'; -import MessageActions from '../../../../components/Message/MessageActions'; -import BroadcastMetric from '../../../../components/Message/Metrics/Broadcast'; -import DiscussionMetric from '../../../../components/Message/Metrics/Discussion'; -import ThreadMetric from '../../../../components/Message/Metrics/Thread'; +import Attachments from '../../../../components/message/Attachments'; +import MessageActions from '../../../../components/message/MessageActions'; +import BroadcastMetric from '../../../../components/message/Metrics/Broadcast'; +import DiscussionMetric from '../../../../components/message/Metrics/Discussion'; +import ThreadMetric from '../../../../components/message/Metrics/Thread'; import { useUserData } from '../../../../hooks/useUserData'; import { UserPresence } from '../../../../lib/presence'; import MessageBlockUiKit from '../../../blocks/MessageBlock'; diff --git a/apps/meteor/client/views/room/MessageList/components/MessageContentBody.tsx b/apps/meteor/client/views/room/MessageList/components/MessageContentBody.tsx index bace0b3af3e8..30e91f803638 100644 --- a/apps/meteor/client/views/room/MessageList/components/MessageContentBody.tsx +++ b/apps/meteor/client/views/room/MessageList/components/MessageContentBody.tsx @@ -1,28 +1,72 @@ -/* eslint-disable complexity */ import { IMessage } from '@rocket.chat/core-typings'; -import React, { FC, memo } from 'react'; +import { css } from '@rocket.chat/css-in-js'; +import { Box } from '@rocket.chat/fuselage'; +import colors from '@rocket.chat/fuselage-tokens/colors'; +import React, { ReactElement } from 'react'; -import MessageBodyRender from '../../../../components/Message/MessageBodyRender'; +import Markup from '../../../../components/gazzodown/Markup'; +import { MarkupInteractionContext } from '../../../../components/gazzodown/MarkupInteractionContext'; import { useMessageActions } from '../../contexts/MessageContext'; import { useParsedMessage } from '../hooks/useParsedMessage'; -const MessageContentBody: FC<{ message: IMessage; isThreadPreview?: boolean }> = ({ message, isThreadPreview }) => { +type MessageContentBodyProps = { + message: IMessage; +}; + +const MessageContentBody = ({ message }: MessageContentBodyProps): ReactElement => { + const tokens = useParsedMessage(message); + const { actions: { openRoom, openUserCard }, } = useMessageActions(); - const tokens = useParsedMessage(message); - return ( - <MessageBodyRender - onUserMentionClick={openUserCard} - onChannelMentionClick={openRoom} - mentions={message?.mentions || []} - channels={message?.channels || []} - tokens={tokens} - isThreadPreview={isThreadPreview} - /> + <Box + className={css` + > blockquote { + padding-inline: 8px; + border-radius: 2px; + border-width: 2px; + border-style: solid; + background-color: var(--rcx-color-neutral-100, ${colors.n100}); + border-color: var(--rcx-color-neutral-200, ${colors.n200}); + border-inline-start-color: var(--rcx-color-neutral-600, ${colors.n600}); + + &:hover, + &:focus { + background-color: var(--rcx-color-neutral-200, ${colors.n200}); + border-color: var(--rcx-color-neutral-300, ${colors.n300}); + border-inline-start-color: var(--rcx-color-neutral-600, ${colors.n600}); + } + } + + > ul.task-list { + > li::before { + display: none; + } + + > li > .rcx-check-box > .rcx-check-box__input:focus + .rcx-check-box__fake { + z-index: 1; + } + + list-style: none; + margin-inline-start: 0; + padding-inline-start: 0; + } + `} + > + <MarkupInteractionContext.Provider + value={{ + mentions: message?.mentions ?? [], + channels: message?.channels ?? [], + onUserMentionClick: openUserCard, + onChannelMentionClick: openRoom, + }} + > + <Markup tokens={tokens} /> + </MarkupInteractionContext.Provider> + </Box> ); }; -export default memo(MessageContentBody); +export default MessageContentBody; diff --git a/apps/meteor/client/views/room/MessageList/components/MessageSystem.tsx b/apps/meteor/client/views/room/MessageList/components/MessageSystem.tsx index 0f67fbd36bb3..651ca7715bad 100644 --- a/apps/meteor/client/views/room/MessageList/components/MessageSystem.tsx +++ b/apps/meteor/client/views/room/MessageList/components/MessageSystem.tsx @@ -14,9 +14,9 @@ import { TranslationKey, useTranslation } from '@rocket.chat/ui-contexts'; import React, { FC, memo } from 'react'; import { MessageTypes } from '../../../../../app/ui-utils/client'; -import Attachments from '../../../../components/Message/Attachments'; -import MessageActions from '../../../../components/Message/MessageActions'; import UserAvatar from '../../../../components/avatar/UserAvatar'; +import Attachments from '../../../../components/message/Attachments'; +import MessageActions from '../../../../components/message/MessageActions'; import { useUserData } from '../../../../hooks/useUserData'; import { getUserDisplayName } from '../../../../lib/getUserDisplayName'; import { UserPresence } from '../../../../lib/presence'; diff --git a/apps/meteor/client/views/room/MessageList/components/ThreadMessagePreview.tsx b/apps/meteor/client/views/room/MessageList/components/ThreadMessagePreview.tsx index 72808129e822..8ab8cf934b9c 100644 --- a/apps/meteor/client/views/room/MessageList/components/ThreadMessagePreview.tsx +++ b/apps/meteor/client/views/room/MessageList/components/ThreadMessagePreview.tsx @@ -20,7 +20,7 @@ import { useMessageActions } from '../../contexts/MessageContext'; import { useIsSelecting, useToggleSelect, useIsSelectedMessage, useCountSelected } from '../contexts/SelectedMessagesContext'; import { useMessageBody } from '../hooks/useMessageBody'; import { useParentMessage } from '../hooks/useParentMessage'; -import MessageContentBody from './MessageContentBody'; +import ThreadMessagePreviewBody from './ThreadMessagePreviewBody'; export const ThreadMessagePreview: FC<{ message: IThreadMessage; sequential: boolean }> = ({ message, sequential, ...props }) => { const { @@ -48,7 +48,13 @@ export const ThreadMessagePreview: FC<{ message: IThreadMessage; sequential: boo <ThreadMessageIconThread /> </ThreadMessageLeftContainer> <ThreadMessageContainer> - <ThreadMessageOrigin>{parentMessage.phase === AsyncStatePhase.RESOLVED ? body : <Skeleton />}</ThreadMessageOrigin> + <ThreadMessageOrigin> + {parentMessage.phase === AsyncStatePhase.RESOLVED ? ( + <ThreadMessagePreviewBody message={{ ...parentMessage.value, msg: body }} /> + ) : ( + <Skeleton /> + )} + </ThreadMessageOrigin> <ThreadMessageUnfollow /> </ThreadMessageContainer> </ThreadMessageRow> @@ -59,9 +65,7 @@ export const ThreadMessagePreview: FC<{ message: IThreadMessage; sequential: boo {isSelecting && <CheckBox checked={isSelected} onChange={toggleSelected} />} </ThreadMessageLeftContainer> <ThreadMessageContainer> - <ThreadMessageBody> - {message.ignored ? t('Message_Ignored') : <MessageContentBody isThreadPreview message={message} />} - </ThreadMessageBody> + <ThreadMessageBody>{message.ignored ? t('Message_Ignored') : <ThreadMessagePreviewBody message={message} />}</ThreadMessageBody> </ThreadMessageContainer> </ThreadMessageRow> </ThreadMessageTemplate> diff --git a/apps/meteor/client/views/room/MessageList/components/ThreadMessagePreviewBody.tsx b/apps/meteor/client/views/room/MessageList/components/ThreadMessagePreviewBody.tsx new file mode 100644 index 000000000000..8909d3a606a4 --- /dev/null +++ b/apps/meteor/client/views/room/MessageList/components/ThreadMessagePreviewBody.tsx @@ -0,0 +1,17 @@ +import { IMessage } from '@rocket.chat/core-typings'; +import React, { ReactElement } from 'react'; + +import PreviewMarkup from '../../../../components/gazzodown/PreviewMarkup'; +import { useParsedMessage } from '../hooks/useParsedMessage'; + +type ThreadMessagePreviewBodyProps = { + message: IMessage; +}; + +const ThreadMessagePreviewBody = ({ message }: ThreadMessagePreviewBodyProps): ReactElement => { + const tokens = useParsedMessage(message); + + return <PreviewMarkup tokens={tokens} />; +}; + +export default ThreadMessagePreviewBody; diff --git a/apps/meteor/client/views/room/MessageList/components/UrlPreview/OEmbedCollapseable.tsx b/apps/meteor/client/views/room/MessageList/components/UrlPreview/OEmbedCollapseable.tsx index 3033c1901e8f..359336862f4c 100644 --- a/apps/meteor/client/views/room/MessageList/components/UrlPreview/OEmbedCollapseable.tsx +++ b/apps/meteor/client/views/room/MessageList/components/UrlPreview/OEmbedCollapseable.tsx @@ -2,7 +2,7 @@ import { MessageGenericPreview, Box } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { ReactElement, ReactNode } from 'react'; -import { useCollapse } from '../../../../../components/Message/Attachments/hooks/useCollapse'; +import { useCollapse } from '../../../../../components/message/Attachments/hooks/useCollapse'; import OEmbedPreviewContent from './OEmbedPreviewContent'; import type { PreviewMetadata } from './PreviewList'; diff --git a/apps/meteor/client/views/room/MessageList/components/UrlPreview/UrlPreview.tsx b/apps/meteor/client/views/room/MessageList/components/UrlPreview/UrlPreview.tsx index 6f2b37efb20b..bfd57b453349 100644 --- a/apps/meteor/client/views/room/MessageList/components/UrlPreview/UrlPreview.tsx +++ b/apps/meteor/client/views/room/MessageList/components/UrlPreview/UrlPreview.tsx @@ -2,7 +2,7 @@ import { Box } from '@rocket.chat/fuselage'; import { useAttachmentAutoLoadEmbedMedia, useTranslation } from '@rocket.chat/ui-contexts'; import React, { ReactElement } from 'react'; -import { useCollapse } from '../../../../../components/Message/Attachments/hooks/useCollapse'; +import { useCollapse } from '../../../../../components/message/Attachments/hooks/useCollapse'; import type { UrlPreview as UrlPreviewType } from './PreviewList'; import UrlPreviewResolver from './UrlPreviewResolver'; diff --git a/apps/meteor/client/views/room/MessageList/hooks/useParsedMessage.ts b/apps/meteor/client/views/room/MessageList/hooks/useParsedMessage.ts index 2d6fa52ba639..cbde9ae437f5 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useParsedMessage.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useParsedMessage.ts @@ -2,7 +2,7 @@ import { IMessage } from '@rocket.chat/core-typings'; import { MarkdownAST, parser } from '@rocket.chat/message-parser'; import { useMemo } from 'react'; -export function useParsedMessage(message: IMessage): MarkdownAST { +export function useParsedMessage(message: Pick<IMessage, 'md' | 'msg'>): MarkdownAST { return useMemo(() => { if (message.md) { return message.md; diff --git a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx index 30b815fbb098..e3c5b743c7df 100644 --- a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx +++ b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx @@ -1,20 +1,27 @@ import { FieldGroup, Field, ToggleSwitch, Select } from '@rocket.chat/fuselage'; import type { SelectOption } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; +import React, { ReactElement, ChangeEvent } from 'react'; import VerticalBar from '../../../../components/VerticalBar'; type AutoTranslateProps = { language: string; languages: SelectOption[]; - handleSwitch: (event?: any) => void; + handleSwitch: (e: ChangeEvent<HTMLInputElement>) => void; translateEnable: boolean | undefined; handleChangeLanguage: (value: string) => void; handleClose?: () => void; }; -const AutoTranslate = ({ language, languages, handleSwitch, translateEnable, handleChangeLanguage, handleClose }: AutoTranslateProps) => { +const AutoTranslate = ({ + language, + languages, + handleSwitch, + translateEnable, + handleChangeLanguage, + handleClose, +}: AutoTranslateProps): ReactElement => { const t = useTranslation(); return ( @@ -24,17 +31,20 @@ const AutoTranslate = ({ language, languages, handleSwitch, translateEnable, han <VerticalBar.Text>{t('Auto_Translate')}</VerticalBar.Text> {handleClose && <VerticalBar.Close onClick={handleClose} />} </VerticalBar.Header> - <VerticalBar.Content> + <VerticalBar.Content pbs='x24'> <FieldGroup> - <Field.Label htmlFor='automatic-translation'>{t('Automatic_Translation')}</Field.Label> - <Field.Row> - <ToggleSwitch id='automatic-translation' onChange={handleSwitch} defaultChecked={translateEnable} /> - </Field.Row> - - <Field.Label htmlFor='language'>{t('Language')}</Field.Label> - <Field.Row verticalAlign='middle'> - <Select id='language' value={language} disabled={!translateEnable} onChange={handleChangeLanguage} options={languages} /> - </Field.Row> + <Field> + <Field.Row> + <ToggleSwitch id='automatic-translation' onChange={handleSwitch} defaultChecked={translateEnable} /> + <Field.Label htmlFor='automatic-translation'>{t('Automatic_Translation')}</Field.Label> + </Field.Row> + </Field> + <Field> + <Field.Label htmlFor='language'>{t('Language')}</Field.Label> + <Field.Row verticalAlign='middle'> + <Select id='language' value={language} disabled={!translateEnable} onChange={handleChangeLanguage} options={languages} /> + </Field.Row> + </Field> </FieldGroup> </VerticalBar.Content> </> diff --git a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.js b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.tsx similarity index 65% rename from apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.js rename to apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.tsx index cf8efdfe2773..f34171b39422 100644 --- a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.js +++ b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.tsx @@ -1,26 +1,25 @@ +import { IRoom } from '@rocket.chat/core-typings'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useUserSubscription, useLanguage } from '@rocket.chat/ui-contexts'; -import React, { useMemo, useEffect, useState, memo } from 'react'; +import React, { useMemo, useEffect, useState, memo, ReactElement } from 'react'; import { useEndpointActionExperimental } from '../../../../hooks/useEndpointActionExperimental'; import { useEndpointData } from '../../../../hooks/useEndpointData'; import { useTabBarClose } from '../../providers/ToolboxProvider'; import AutoTranslate from './AutoTranslate'; -const AutoTranslateWithData = ({ rid }) => { - const close = useTabBarClose(); +const AutoTranslateWithData = ({ rid }: { rid: IRoom['_id'] }): ReactElement => { + const handleClose = useTabBarClose(); const userLanguage = useLanguage(); const subscription = useUserSubscription(rid); + const [currentLanguage, setCurrentLanguage] = useState(subscription?.autoTranslateLanguage ?? ''); + const saveSettings = useEndpointActionExperimental('POST', 'autotranslate.saveSettings'); - const { value: data } = useEndpointData( + const { value: translateData } = useEndpointData( 'autotranslate.getSupportedLanguages', useMemo(() => ({ targetLanguage: userLanguage }), [userLanguage]), ); - const [currentLanguage, setCurrentLanguage] = useState(subscription.autoTranslateLanguage); - - const saveSettings = useEndpointActionExperimental('POST', 'autotranslate.saveSettings'); - const handleChangeLanguage = useMutableCallback((value) => { setCurrentLanguage(value); @@ -40,23 +39,23 @@ const AutoTranslateWithData = ({ rid }) => { }); useEffect(() => { - if (!subscription.autoTranslate) { + if (!subscription?.autoTranslate) { return; } - if (!subscription.autoTranslateLanguage) { + if (!subscription?.autoTranslateLanguage) { handleChangeLanguage(userLanguage); } - }, [subscription.autoTranslate, subscription.autoTranslateLanguage, handleChangeLanguage, userLanguage]); + }, [subscription?.autoTranslate, subscription?.autoTranslateLanguage, handleChangeLanguage, userLanguage]); return ( <AutoTranslate language={currentLanguage} - languages={data ? data.languages.map((value) => [value.language, value.name]) : []} + languages={translateData ? translateData.languages.map((language) => [language.language, language.name]) : []} handleSwitch={handleSwitch} handleChangeLanguage={handleChangeLanguage} - translateEnable={!!subscription.autoTranslate} - handleClose={close} + translateEnable={!!subscription?.autoTranslate} + handleClose={handleClose} /> ); }; diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutSection.tsx b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutSection.tsx new file mode 100644 index 000000000000..29ebdf68db66 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutSection.tsx @@ -0,0 +1,19 @@ +import { Box, Divider } from '@rocket.chat/fuselage'; +import React, { ReactElement } from 'react'; + +type KeyboardShortcutSectionProps = { + title: string; + command: string; +}; + +const KeyboardShortcutSection = ({ title, command }: KeyboardShortcutSectionProps): ReactElement => ( + <Box is='section' mb='x16'> + <Box fontScale='p2m' fontWeight='700'> + {title} + </Box> + <Divider /> + <Box fontScale='p2'>{command}</Box> + </Box> +); + +export default KeyboardShortcutSection; diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.js b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.js deleted file mode 100644 index 452f4b706855..000000000000 --- a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.js +++ /dev/null @@ -1,31 +0,0 @@ -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; - -import VerticalBar from '../../../../components/VerticalBar'; -import ShortcutSection from './ShortcutSection'; - -const KeyboardShortcuts = ({ handleClose }) => { - const t = useTranslation(); - - return ( - <> - <VerticalBar.Header> - <VerticalBar.Icon name='keyboard' /> - <VerticalBar.Text>{t('Keyboard_Shortcuts_Title')}</VerticalBar.Text> - {handleClose && <VerticalBar.Close onClick={handleClose} />} - </VerticalBar.Header> - <VerticalBar.ScrollableContent> - <ShortcutSection title={t('Keyboard_Shortcuts_Open_Channel_Slash_User_Search')} command={t('Keyboard_Shortcuts_Keys_1')} /> - <ShortcutSection title={t('Keyboard_Shortcuts_Mark_all_as_read')} command={t('Keyboard_Shortcuts_Keys_8')} /> - <ShortcutSection title={t('Keyboard_Shortcuts_Edit_Previous_Message')} command={t('Keyboard_Shortcuts_Keys_2')} /> - <ShortcutSection title={t('Keyboard_Shortcuts_Move_To_Beginning_Of_Message')} command={t('Keyboard_Shortcuts_Keys_3')} /> - <ShortcutSection title={t('Keyboard_Shortcuts_Move_To_Beginning_Of_Message')} command={t('Keyboard_Shortcuts_Keys_4')} /> - <ShortcutSection title={t('Keyboard_Shortcuts_Move_To_End_Of_Message')} command={t('Keyboard_Shortcuts_Keys_5')} /> - <ShortcutSection title={t('Keyboard_Shortcuts_Move_To_End_Of_Message')} command={t('Keyboard_Shortcuts_Keys_6')} /> - <ShortcutSection title={t('Keyboard_Shortcuts_New_Line_In_Message')} command={t('Keyboard_Shortcuts_Keys_7')} /> - </VerticalBar.ScrollableContent> - </> - ); -}; - -export default KeyboardShortcuts; diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.stories.tsx b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.stories.tsx index 65b69378e337..5ed77d36f85f 100644 --- a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.stories.tsx @@ -2,16 +2,16 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; import VerticalBar from '../../../../components/VerticalBar'; -import KeyboardShortcutsWithClose from './KeyboardShortcutsWithClose'; +import KeyboardShortcutsWithData from './KeyboardShortcutsWithData'; export default { title: 'Room/Contextual Bar/KeyboardShortcut', - component: KeyboardShortcutsWithClose, + component: KeyboardShortcutsWithData, parameters: { layout: 'fullscreen', }, decorators: [(fn) => <VerticalBar height='100vh'>{fn()}</VerticalBar>], -} as ComponentMeta<typeof KeyboardShortcutsWithClose>; +} as ComponentMeta<typeof KeyboardShortcutsWithData>; -export const Default: ComponentStory<typeof KeyboardShortcutsWithClose> = (args) => <KeyboardShortcutsWithClose {...args} />; +export const Default: ComponentStory<typeof KeyboardShortcutsWithData> = (args) => <KeyboardShortcutsWithData {...args} />; Default.storyName = 'KeyboardShortcuts'; diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.tsx b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.tsx new file mode 100644 index 000000000000..500becc48764 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.tsx @@ -0,0 +1,31 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { memo, ReactElement } from 'react'; + +import VerticalBar from '../../../../components/VerticalBar'; +import KeyboardShortcutSection from './KeyboardShortcutSection'; + +const KeyboardShortcuts = ({ handleClose }: { handleClose: () => void }): ReactElement => { + const t = useTranslation(); + + return ( + <> + <VerticalBar.Header> + <VerticalBar.Icon name='keyboard' /> + <VerticalBar.Text>{t('Keyboard_Shortcuts_Title')}</VerticalBar.Text> + {handleClose && <VerticalBar.Close onClick={handleClose} />} + </VerticalBar.Header> + <VerticalBar.ScrollableContent> + <KeyboardShortcutSection title={t('Keyboard_Shortcuts_Open_Channel_Slash_User_Search')} command={t('Keyboard_Shortcuts_Keys_1')} /> + <KeyboardShortcutSection title={t('Keyboard_Shortcuts_Mark_all_as_read')} command={t('Keyboard_Shortcuts_Keys_8')} /> + <KeyboardShortcutSection title={t('Keyboard_Shortcuts_Edit_Previous_Message')} command={t('Keyboard_Shortcuts_Keys_2')} /> + <KeyboardShortcutSection title={t('Keyboard_Shortcuts_Move_To_Beginning_Of_Message')} command={t('Keyboard_Shortcuts_Keys_3')} /> + <KeyboardShortcutSection title={t('Keyboard_Shortcuts_Move_To_Beginning_Of_Message')} command={t('Keyboard_Shortcuts_Keys_4')} /> + <KeyboardShortcutSection title={t('Keyboard_Shortcuts_Move_To_End_Of_Message')} command={t('Keyboard_Shortcuts_Keys_5')} /> + <KeyboardShortcutSection title={t('Keyboard_Shortcuts_Move_To_End_Of_Message')} command={t('Keyboard_Shortcuts_Keys_6')} /> + <KeyboardShortcutSection title={t('Keyboard_Shortcuts_New_Line_In_Message')} command={t('Keyboard_Shortcuts_Keys_7')} /> + </VerticalBar.ScrollableContent> + </> + ); +}; + +export default memo(KeyboardShortcuts); diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutsWithClose.js b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutsWithClose.js deleted file mode 100644 index 94111937261c..000000000000 --- a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutsWithClose.js +++ /dev/null @@ -1,11 +0,0 @@ -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import React, { memo } from 'react'; - -import KeyboardShortcuts from './KeyboardShortcuts'; - -const KeyboardShortcutsWithClose = ({ tabBar }) => { - const handleClose = useMutableCallback(() => tabBar && tabBar.close()); - return <KeyboardShortcuts handleClose={handleClose} />; -}; - -export default memo(KeyboardShortcutsWithClose); diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutsWithData.tsx b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutsWithData.tsx new file mode 100644 index 000000000000..afee820bb10a --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutsWithData.tsx @@ -0,0 +1,12 @@ +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import React, { ReactElement } from 'react'; + +import { ToolboxContextValue } from '../../lib/Toolbox/ToolboxContext'; +import KeyboardShortcuts from './KeyboardShortcuts'; + +const KeyboardShortcutsWithData = ({ tabBar }: { tabBar: ToolboxContextValue['tabBar'] }): ReactElement => { + const handleClose = useMutableCallback(() => tabBar?.close()); + return <KeyboardShortcuts handleClose={handleClose} />; +}; + +export default KeyboardShortcutsWithData; diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/ShortcutSection.js b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/ShortcutSection.js deleted file mode 100644 index a540a22161aa..000000000000 --- a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/ShortcutSection.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Box, Divider } from '@rocket.chat/fuselage'; -import React from 'react'; - -const ShortcutSection = ({ title, command }) => ( - <Box is='section' mb='x16'> - <Box fontScale='p2m' fontWeight='700'> - {title} - </Box> - <Divider /> - <Box fontScale='p2'>{command}</Box> - </Box> -); - -export default ShortcutSection; diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/index.ts b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/index.ts index dd578fbd1237..5d1134a343aa 100644 --- a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/index.ts +++ b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/index.ts @@ -1 +1 @@ -export { default } from './KeyboardShortcutsWithClose'; +export { default } from './KeyboardShortcutsWithData'; diff --git a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.stories.tsx b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.stories.tsx index 6bbe382b39b0..557baddcb9cf 100644 --- a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.stories.tsx @@ -20,6 +20,6 @@ export const Default = Template.bind({}); export const WithCallout = Template.bind({}); WithCallout.args = { - pinned: true, + values: { pinned: true }, callOutText: 'This is a callout', }; diff --git a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.js b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.tsx similarity index 66% rename from apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.js rename to apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.tsx index 3bb7dae29d2e..5010a838a6b3 100644 --- a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.js +++ b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.tsx @@ -1,36 +1,51 @@ +import { IUser } from '@rocket.chat/core-typings'; import { Field, ButtonGroup, Button, CheckBox, Callout } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; +import React, { ReactElement } from 'react'; import UserAutoCompleteMultiple from '../../../../components/UserAutoCompleteMultiple'; import VerticalBar from '../../../../components/VerticalBar'; -import DateTimeRow from './DateTimeRow'; +import PruneMessagesDateTimeRow from './PruneMessagesDateTimeRow'; +import { initialValues } from './PruneMessagesWithData'; + +type PruneMessagesProps = { + callOutText?: string; + validateText?: string; + users: IUser['username'][]; + values: Record<string, unknown>; + handlers: Record<string, (eventOrValue: unknown) => void>; + onClickClose: () => void; + onClickPrune: () => void; + onChangeUsers: (value: IUser['username'], action?: string) => void; +}; const PruneMessages = ({ callOutText, validateText, - newerDateTime, - handleNewerDateTime, - olderDateTime, - handleOlderDateTime, - users, - inclusive, - pinned, - discussion, - threads, - attached, - handleInclusive, - handlePinned, - handleDiscussion, - handleThreads, - handleAttached, + values, + handlers, onClickClose, onClickPrune, onChangeUsers, -}) => { +}: PruneMessagesProps): ReactElement => { const t = useTranslation(); + const { newerDate, newerTime, olderDate, olderTime, users, inclusive, pinned, discussion, threads, attached } = + values as typeof initialValues; + + const { + handleNewerDate, + handleNewerTime, + handleOlderDate, + handleOlderTime, + handleInclusive, + handlePinned, + handleDiscussion, + handleThreads, + handleAttached, + } = handlers; + const inclusiveCheckboxId = useUniqueId(); const pinnedCheckboxId = useUniqueId(); const discussionCheckboxId = useUniqueId(); @@ -45,55 +60,56 @@ const PruneMessages = ({ {onClickClose && <VerticalBar.Close onClick={onClickClose} />} </VerticalBar.Header> <VerticalBar.ScrollableContent> - <DateTimeRow label={t('Newer_than')} dateTime={newerDateTime} handleDateTime={handleNewerDateTime} /> - <DateTimeRow label={t('Older_than')} dateTime={olderDateTime} handleDateTime={handleOlderDateTime} /> - + <PruneMessagesDateTimeRow + label={t('Newer_than')} + dateTime={{ date: newerDate, time: newerTime }} + handleDateTime={{ date: handleNewerDate, time: handleNewerTime }} + /> + <PruneMessagesDateTimeRow + label={t('Older_than')} + dateTime={{ date: olderDate, time: olderTime }} + handleDateTime={{ date: handleOlderDate, time: handleOlderTime }} + /> <Field> <Field.Label flexGrow={0}>{t('Only_from_users')}</Field.Label> <UserAutoCompleteMultiple value={users} onChange={onChangeUsers} placeholder={t('Please_enter_usernames')} /> </Field> - <Field> <Field.Row> <CheckBox id={inclusiveCheckboxId} checked={inclusive} onChange={handleInclusive} /> <Field.Label htmlFor={inclusiveCheckboxId}>{t('Inclusive')}</Field.Label> </Field.Row> </Field> - <Field> <Field.Row> <CheckBox id={pinnedCheckboxId} checked={pinned} onChange={handlePinned} /> <Field.Label htmlFor={pinnedCheckboxId}>{t('RetentionPolicy_DoNotPrunePinned')}</Field.Label> </Field.Row> </Field> - <Field> <Field.Row> <CheckBox id={discussionCheckboxId} checked={discussion} onChange={handleDiscussion} /> <Field.Label htmlFor={discussionCheckboxId}>{t('RetentionPolicy_DoNotPruneDiscussion')}</Field.Label> </Field.Row> </Field> - <Field> <Field.Row> <CheckBox id={threadsCheckboxId} checked={threads} onChange={handleThreads} /> <Field.Label htmlFor={threadsCheckboxId}>{t('RetentionPolicy_DoNotPruneThreads')}</Field.Label> </Field.Row> </Field> - <Field> <Field.Row> <CheckBox id={attachedCheckboxId} checked={attached} onChange={handleAttached} /> <Field.Label htmlFor={attachedCheckboxId}>{t('Files_only')}</Field.Label> </Field.Row> </Field> - {callOutText && !validateText && <Callout type='warning'>{callOutText}</Callout>} {validateText && <Callout type='warning'>{validateText}</Callout>} </VerticalBar.ScrollableContent> <VerticalBar.Footer> <ButtonGroup stretch> - <Button primary danger disabled={validateText && true} onClick={onClickPrune}> + <Button primary danger disabled={Boolean(validateText)} onClick={onClickPrune}> {t('Prune')} </Button> </ButtonGroup> diff --git a/apps/meteor/client/views/room/contextualBar/PruneMessages/DateTimeRow.js b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesDateTimeRow.tsx similarity index 51% rename from apps/meteor/client/views/room/contextualBar/PruneMessages/DateTimeRow.js rename to apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesDateTimeRow.tsx index f4cc00c44aa5..464aa82f0461 100644 --- a/apps/meteor/client/views/room/contextualBar/PruneMessages/DateTimeRow.js +++ b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesDateTimeRow.tsx @@ -1,7 +1,19 @@ import { Field, InputBox, Box, Margins } from '@rocket.chat/fuselage'; -import React from 'react'; +import React, { ReactElement } from 'react'; -const DateTimeRow = ({ label, dateTime, handleDateTime }) => ( +type PruneMessagesDateTimeRowProps = { + label: string; + dateTime: { + date: string; + time: string; + }; + handleDateTime: { + date: (eventOrValue: unknown) => void; + time: (eventOrValue: unknown) => void; + }; +}; + +const PruneMessagesDateTimeRow = ({ label, dateTime, handleDateTime }: PruneMessagesDateTimeRowProps): ReactElement => ( <Field> <Field.Label flexGrow={0}>{label}</Field.Label> <Box display='flex' mi='neg-x4'> @@ -13,4 +25,4 @@ const DateTimeRow = ({ label, dateTime, handleDateTime }) => ( </Field> ); -export default DateTimeRow; +export default PruneMessagesDateTimeRow; diff --git a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.js b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.tsx similarity index 58% rename from apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.js rename to apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.tsx index 3717d0ba9215..dbf3f00b4cd3 100644 --- a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.js +++ b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.tsx @@ -1,24 +1,26 @@ +import { IRoom, IUser, isDirectMessageRoom } from '@rocket.chat/core-typings'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useSetModal, useToastMessageDispatch, useUserRoom, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import moment from 'moment'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState, ReactElement } from 'react'; import GenericModal from '../../../../components/GenericModal'; import { useForm } from '../../../../hooks/useForm'; +import { ToolboxContextValue } from '../../lib/Toolbox/ToolboxContext'; import PruneMessages from './PruneMessages'; -const getTimeZoneOffset = function () { +const getTimeZoneOffset = (): string => { const offset = new Date().getTimezoneOffset(); const absOffset = Math.abs(offset); return `${offset < 0 ? '+' : '-'}${`00${Math.floor(absOffset / 60)}`.slice(-2)}:${`00${absOffset % 60}`.slice(-2)}`; }; -const initialValues = { +export const initialValues = { newerDate: '', newerTime: '', olderDate: '', olderTime: '', - users: [], + users: [] as IUser['username'][], inclusive: false, pinned: false, discussion: false, @@ -26,94 +28,88 @@ const initialValues = { attached: false, }; -const PruneMessagesWithData = ({ rid, tabBar }) => { +const DEFAULT_PRUNE_LIMIT = 2000; + +const PruneMessagesWithData = ({ rid, tabBar }: { rid: IRoom['_id']; tabBar: ToolboxContextValue['tabBar'] }): ReactElement => { const t = useTranslation(); const room = useUserRoom(rid); - room.type = room.t; - room.rid = rid; - const { name, usernames } = room; - const setModal = useSetModal(); - const onClickClose = useMutableCallback(() => tabBar && tabBar.close()); + const onClickClose = useMutableCallback(() => tabBar?.close()); const closeModal = useCallback(() => setModal(null), [setModal]); const dispatchToastMessage = useToastMessageDispatch(); - const pruneMessages = useEndpoint('POST', 'rooms.cleanHistory'); + const pruneMessagesAction = useEndpoint('POST', 'rooms.cleanHistory'); const [fromDate, setFromDate] = useState(new Date('0001-01-01T00:00:00Z')); const [toDate, setToDate] = useState(new Date('9999-12-31T23:59:59Z')); - const [callOutText, setCallOutText] = useState(); - const [validateText, setValidateText] = useState(); + const [callOutText, setCallOutText] = useState<string | undefined>(); + const [validateText, setValidateText] = useState<string | undefined>(); const [counter, setCounter] = useState(0); const { values, handlers, reset } = useForm(initialValues); - const { newerDate, newerTime, olderDate, olderTime, users, inclusive, pinned, discussion, threads, attached } = values; - - const { - handleNewerDate, - handleNewerTime, - handleOlderDate, - handleOlderTime, - handleUsers, - handleInclusive, - handlePinned, - handleDiscussion, - handleThreads, - handleAttached, - } = handlers; - - const onChangeUsers = useMutableCallback((value, action) => { + const { newerDate, newerTime, olderDate, olderTime, users, inclusive, pinned, discussion, threads, attached } = + values as typeof initialValues; + const { handleUsers } = handlers; + + const onChangeUsers = useMutableCallback((value: IUser['username'], action?: string) => { if (!action) { if (users.includes(value)) { return; } return handleUsers([...users, value]); } - handleUsers(users.filter((current) => current !== value)); - }); - - const handlePrune = useMutableCallback(async () => { - const limit = 2000; - - try { - if (counter === limit) { - return; - } - const { count } = await pruneMessages({ - roomId: rid, - latest: toDate, - oldest: fromDate, - inclusive, - limit, - excludePinned: pinned, - filesOnly: attached, - ignoreDiscussion: discussion, - ignoreThreads: threads, - users, - }); - - setCounter(count); - - if (count < 1) { - throw new Error(t('No_messages_found_to_prune')); - } - - dispatchToastMessage({ type: 'success', message: `${count} ${t('messages_pruned')}` }); - closeModal(); - reset(); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error.message }); - closeModal(); - } + return handleUsers(users.filter((current) => current !== value)); }); - const handleModal = () => { - setModal( - <GenericModal variant='danger' onClose={closeModal} onCancel={closeModal} onConfirm={handlePrune} confirmText={t('Yes_prune_them')}> + const handlePrune = useMutableCallback((): void => { + const handlePruneAction = async (): Promise<void> => { + const limit = DEFAULT_PRUNE_LIMIT; + + try { + if (counter === limit) { + return; + } + + const { count } = await pruneMessagesAction({ + roomId: rid, + latest: toDate.toISOString(), + oldest: fromDate.toISOString(), + inclusive, + limit, + excludePinned: pinned, + filesOnly: attached, + ignoreDiscussion: discussion, + ignoreThreads: threads, + users, + }); + + setCounter(count); + + if (count < 1) { + throw new Error(t('No_messages_found_to_prune')); + } + + dispatchToastMessage({ type: 'success', message: `${count} ${t('messages_pruned')}` }); + closeModal(); + reset(); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error.message }); + closeModal(); + } + }; + + return setModal( + <GenericModal + variant='danger' + onClose={closeModal} + onCancel={closeModal} + onConfirm={handlePruneAction} + confirmText={t('Yes_prune_them')} + > {t('Prune_Modal')} </GenericModal>, ); - }; + }); useEffect(() => { if (newerDate) { @@ -166,7 +162,7 @@ const PruneMessagesWithData = ({ rid, tabBar }) => { setCallOutText( t('Prune_Warning_all', { postProcess: 'sprintf', - sprintf: [filesOrMessages, name || usernames?.join(' x ')], + sprintf: [filesOrMessages, room && isDirectMessageRoom(room) && (room.name || room.usernames?.join(' x '))], }) + exceptPinned + ifFrom, @@ -190,31 +186,19 @@ const PruneMessagesWithData = ({ rid, tabBar }) => { ); } - setValidateText(); - }, [newerDate, olderDate, fromDate, toDate, attached, name, t, pinned, users, usernames]); + setValidateText(undefined); + }, [newerDate, olderDate, fromDate, toDate, attached, t, pinned, users, room]); return ( <PruneMessages callOutText={callOutText} validateText={validateText} - newerDateTime={{ date: newerDate, time: newerTime }} - handleNewerDateTime={{ date: handleNewerDate, time: handleNewerTime }} - olderDateTime={{ date: olderDate, time: olderTime }} - handleOlderDateTime={{ date: handleOlderDate, time: handleOlderTime }} users={users} - inclusive={inclusive} - pinned={pinned} - discussion={discussion} - threads={threads} - attached={attached} - handleInclusive={handleInclusive} - handlePinned={handlePinned} - handleDiscussion={handleDiscussion} - handleThreads={handleThreads} - handleAttached={handleAttached} - onClickClose={onClickClose} - onClickPrune={handleModal} onChangeUsers={onChangeUsers} + values={values} + handlers={handlers} + onClickClose={onClickClose} + onClickPrune={handlePrune} /> ); }; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/Message.js b/apps/meteor/client/views/room/contextualBar/Threads/components/Message.js index 35cae5855b8c..7658bf1e13a3 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/Message.js +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/Message.js @@ -1,10 +1,10 @@ import { Button, Icon, Message, Box } from '@rocket.chat/fuselage'; import React from 'react'; -import * as NotificationStatus from '../../../../../components/Message/NotificationStatus'; -import { followStyle, anchor } from '../../../../../components/Message/helpers/followSyle'; import RawText from '../../../../../components/RawText'; import UserAvatar from '../../../../../components/avatar/UserAvatar'; +import * as NotificationStatus from '../../../../../components/message/NotificationStatus'; +import { followStyle, anchor } from '../../../../../components/message/helpers/followSyle'; function isIterable(obj) { // checks for null and undefined diff --git a/apps/meteor/client/views/root/MainLayout/MainLayout.tsx b/apps/meteor/client/views/root/MainLayout/MainLayout.tsx index 84fe7f8b1838..43fe2e8e1441 100644 --- a/apps/meteor/client/views/root/MainLayout/MainLayout.tsx +++ b/apps/meteor/client/views/root/MainLayout/MainLayout.tsx @@ -1,20 +1,21 @@ -import React, { ReactElement, useMemo } from 'react'; +import React, { ReactElement, ReactNode, Suspense } from 'react'; -import BlazeTemplate from '../BlazeTemplate'; import AuthenticationCheck from './AuthenticationCheck'; import Preload from './Preload'; import { useCustomScript } from './useCustomScript'; type MainLayoutProps = { - center?: string; + children?: ReactNode; } & Record<string, unknown>; -const MainLayout = ({ center }: MainLayoutProps): ReactElement => { +const MainLayout = ({ children = null }: MainLayoutProps): ReactElement => { useCustomScript(); return ( <Preload> - <AuthenticationCheck>{useMemo(() => (center ? <BlazeTemplate template={center} /> : null), [center])}</AuthenticationCheck> + <AuthenticationCheck> + <Suspense fallback={null}>{children}</Suspense> + </AuthenticationCheck> </Preload> ); }; diff --git a/apps/meteor/client/views/root/MainLayout/TwoFactorAuthSetupCheck.tsx b/apps/meteor/client/views/root/MainLayout/TwoFactorAuthSetupCheck.tsx index 42b6072961a5..a11f715eb78d 100644 --- a/apps/meteor/client/views/root/MainLayout/TwoFactorAuthSetupCheck.tsx +++ b/apps/meteor/client/views/root/MainLayout/TwoFactorAuthSetupCheck.tsx @@ -19,7 +19,7 @@ const TwoFactorAuthSetupCheck = ({ children }: { children: ReactNode }): ReactEl return false; } - const mandatoryRole = Roles.findOne({ _id: { $in: user.roles }, mandatory2fa: true }); + const mandatoryRole = Roles.findOne({ _id: { $in: user.roles ?? [] }, mandatory2fa: true }); return mandatoryRole !== undefined && tfaEnabled; }, [tfaEnabled, user]), ); diff --git a/apps/meteor/client/views/teams/contextualBar/channels/BaseTeamsChannels.js b/apps/meteor/client/views/teams/contextualBar/channels/BaseTeamsChannels.js index b21e0337afe6..9cf70c819021 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/BaseTeamsChannels.js +++ b/apps/meteor/client/views/teams/contextualBar/channels/BaseTeamsChannels.js @@ -83,20 +83,22 @@ const BaseTeamsChannels = ({ )} </VerticalBar.Content> - <VerticalBar.Footer> - <ButtonGroup stretch> - {onClickAddExisting && ( - <Button onClick={onClickAddExisting} width='50%'> - {t('Team_Add_existing')} - </Button> - )} - {onClickCreateNew && ( - <Button onClick={onClickCreateNew} width='50%'> - {t('Create_new')} - </Button> - )} - </ButtonGroup> - </VerticalBar.Footer> + {(onClickAddExisting || onClickCreateNew) && ( + <VerticalBar.Footer> + <ButtonGroup stretch> + {onClickAddExisting && ( + <Button onClick={onClickAddExisting} width='50%'> + {t('Team_Add_existing')} + </Button> + )} + {onClickCreateNew && ( + <Button onClick={onClickCreateNew} width='50%'> + {t('Create_new')} + </Button> + )} + </ButtonGroup> + </VerticalBar.Footer> + )} </> ); }; diff --git a/apps/meteor/cypress.json b/apps/meteor/cypress.json deleted file mode 100644 index 28f2c5dfe960..000000000000 --- a/apps/meteor/cypress.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "baseUrl": "http://localhost:3000", - "defaultCommandTimeout": 10000, - "video": false, - "trashAssetsBeforeRuns": false, - "retries": 2, - "projectId": "99zfrs", - "screenshotsFolder": "tests/cypress/screenshots", - "videosFolder": "tests/cypress/videos", - "fixturesFolder": "tests/cypress/fixtures", - "pluginsFile": "tests/cypress/plugins", - "supportFile": "tests/cypress/support", - "integrationFolder": "tests/cypress/integration" -} diff --git a/apps/meteor/ee/app/livechat-enterprise/client/index.js b/apps/meteor/ee/app/livechat-enterprise/client/index.js index 7b739d7319ef..4a89ad01dbcd 100644 --- a/apps/meteor/ee/app/livechat-enterprise/client/index.js +++ b/apps/meteor/ee/app/livechat-enterprise/client/index.js @@ -1,6 +1,5 @@ import { hasLicense } from '../../license/client'; import '../lib/messageTypes'; -import './route'; import './startup'; hasLicense('livechat-enterprise').then((enabled) => { diff --git a/apps/meteor/ee/app/livechat-enterprise/client/route.js b/apps/meteor/ee/app/livechat-enterprise/client/route.js deleted file mode 100644 index 8e7252e2f28b..000000000000 --- a/apps/meteor/ee/app/livechat-enterprise/client/route.js +++ /dev/null @@ -1,70 +0,0 @@ -import { AccountBox } from '../../../../app/ui-utils'; -import { livechatManagerRoutes, load } from '../../../../app/livechat/client/route'; - -AccountBox.addRoute( - { - name: 'livechat-tag-edit', - path: '/tags/:_id/edit', - sideNav: 'omnichannelFlex', - i18nPageTitle: 'Edit_Tag', - pageTemplate: 'livechatTagForm', - }, - livechatManagerRoutes, -); - -AccountBox.addRoute( - { - name: 'livechat-tag-new', - path: '/tags/new', - sideNav: 'omnichannelFlex', - i18nPageTitle: 'New_Tag', - pageTemplate: 'livechatTagForm', - }, - livechatManagerRoutes, -); - -AccountBox.addRoute( - { - name: 'livechat-priority-edit', - path: '/priorities/:_id/edit', - sideNav: 'omnichannelFlex', - i18nPageTitle: 'Edit_Priority', - pageTemplate: 'livechatPriorityForm', - }, - livechatManagerRoutes, -); - -AccountBox.addRoute( - { - name: 'livechat-priority-new', - path: '/priorities/new', - sideNav: 'omnichannelFlex', - i18nPageTitle: 'New_Priority', - pageTemplate: 'livechatPriorityForm', - }, - livechatManagerRoutes, -); - -AccountBox.addRoute( - { - name: 'livechat-business-hour-edit', - path: '/business-hours/:_id/:type/edit', - sideNav: 'omnichannelFlex', - i18nPageTitle: 'Edit_Business_Hour', - pageTemplate: 'livechatBusinessHoursForm', - }, - livechatManagerRoutes, - load, -); - -AccountBox.addRoute( - { - name: 'livechat-business-hour-new', - path: '/business-hours/new', - sideNav: 'omnichannelFlex', - i18nPageTitle: 'New_Business_Hour', - pageTemplate: 'livechatBusinessHoursForm', - }, - livechatManagerRoutes, - load, -); diff --git a/apps/meteor/ee/client/omnichannel/routes.js b/apps/meteor/ee/client/omnichannel/routes.ts similarity index 60% rename from apps/meteor/ee/client/omnichannel/routes.js rename to apps/meteor/ee/client/omnichannel/routes.ts index 66bbc9a39861..0cbe9a787bbf 100644 --- a/apps/meteor/ee/client/omnichannel/routes.js +++ b/apps/meteor/ee/client/omnichannel/routes.ts @@ -1,16 +1,18 @@ +import { lazy } from 'react'; + import { registerOmnichannelRoute } from '../../../client/views/omnichannel/routes'; registerOmnichannelRoute('/monitors', { name: 'omnichannel-monitors', - lazyRouteComponent: () => import('./monitors/MonitorsPageContainer'), + component: lazy(() => import('./monitors/MonitorsPageContainer')), }); registerOmnichannelRoute('/priorities/:context?/:id?', { name: 'omnichannel-priorities', - lazyRouteComponent: () => import('./priorities/PrioritiesRoute'), + component: lazy(() => import('./priorities/PrioritiesRoute')), }); registerOmnichannelRoute('/canned-responses/:context?/:id?', { name: 'omnichannel-canned-responses', - lazyRouteComponent: () => import('./cannedResponses/CannedResponsesRoute'), + component: lazy(() => import('./cannedResponses/CannedResponsesRoute')), }); diff --git a/apps/meteor/ee/client/startup/engagementDashboard.ts b/apps/meteor/ee/client/startup/engagementDashboard.ts index 56da53c2ec50..b6391495bee1 100644 --- a/apps/meteor/ee/client/startup/engagementDashboard.ts +++ b/apps/meteor/ee/client/startup/engagementDashboard.ts @@ -1,4 +1,5 @@ import { Meteor } from 'meteor/meteor'; +import { lazy } from 'react'; import { hasAllPermission } from '../../../app/authorization/client'; import { registerAdminRoute, registerAdminSidebarItem, unregisterAdminSidebarItem } from '../../../client/views/admin'; @@ -6,7 +7,7 @@ import { onToggledFeature } from '../lib/onToggledFeature'; const [registerRoute, unregisterRoute] = registerAdminRoute('/engagement-dashboard/:tab?', { name: 'engagement-dashboard', - lazyRouteComponent: () => import('../views/admin/engagementDashboard/EngagementDashboardRoute'), + component: lazy(() => import('../views/admin/engagementDashboard/EngagementDashboardRoute')), ready: false, }); diff --git a/apps/meteor/ee/client/startup/routes.ts b/apps/meteor/ee/client/startup/routes.ts deleted file mode 100644 index 7037b9645fe4..000000000000 --- a/apps/meteor/ee/client/startup/routes.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { FlowRouter } from 'meteor/kadira:flow-router'; - -import { appLayout } from '../../../client/lib/appLayout'; - -FlowRouter.route('/audit', { - name: 'audit-home', - action() { - appLayout.renderMainLayout({ center: 'auditPage' }); - }, -}); - -FlowRouter.route('/audit-log', { - name: 'audit-log', - action() { - appLayout.renderMainLayout({ center: 'auditLogPage' }); - }, -}); diff --git a/apps/meteor/ee/client/startup/routes.tsx b/apps/meteor/ee/client/startup/routes.tsx new file mode 100644 index 000000000000..cb5a01ecad73 --- /dev/null +++ b/apps/meteor/ee/client/startup/routes.tsx @@ -0,0 +1,30 @@ +import { FlowRouter } from 'meteor/kadira:flow-router'; +import React, { lazy } from 'react'; + +import { appLayout } from '../../../client/lib/appLayout'; +import MainLayout from '../../../client/views/root/MainLayout'; + +const AuditPage = lazy(() => import('../audit/AuditPage')); +const AuditLogPage = lazy(() => import('../audit/AuditLogPage')); + +FlowRouter.route('/audit', { + name: 'audit-home', + action() { + appLayout.render( + <MainLayout> + <AuditPage /> + </MainLayout>, + ); + }, +}); + +FlowRouter.route('/audit-log', { + name: 'audit-log', + action() { + appLayout.render( + <MainLayout> + <AuditLogPage /> + </MainLayout>, + ); + }, +}); diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 3effa60ed996..aabd707580e3 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -28,16 +28,12 @@ "obj:dev": "TEST_MODE=true yarn dev", "stylelint": "stylelint \"app/**/*.css\" \"client/**/*.css\" \"app/**/*.less\" \"client/**/*.less\" \"ee/**/*.less\"", "stylelint:fix": "stylelint --fix \"app/**/*.css\" \"client/**/*.css\" \"app/**/*.less\" \"client/**/*.less\" \"ee/**/*.less\"", - "typecheck": "cross-env NODE_OPTIONS=\"--max-old-space-size=4092\" tsc --noEmit --skipLibCheck", + "typecheck": "cross-env NODE_OPTIONS=\"--max-old-space-size=8184\" tsc --noEmit --skipLibCheck", "deploy": "npm run build && pm2 startOrRestart pm2.json", "coverage": "nyc -r html mocha --config ./.mocharc.js", "testci": "node .scripts/start.js", "test:playwright": "playwright test", "test:playwright:ee": "cross-env ENTERPRISE=true yarn test:playwright", - "testui": "cypress run", - "testui-pass": "cypress run --spec ./tests/cypress/integration/01-pass/**/*.spec.js", - "testui-intermittent": "cypress run --spec ./tests/cypress/integration/02-intermittent/**/*.spec.js", - "testui-skip": "cypress run --spec ./tests/cypress/integration/03-skip/**/*.spec.js", "testapi": "mocha --config ./.mocharc.api.js", "testunit": "npm run .testunit:definition && npm run .testunit:client && npm run .testunit:server", ".testunit:server": "mocha --config ./.mocharc.js", @@ -125,6 +121,7 @@ "@types/rewire": "^2.5.28", "@types/semver": "^7.3.9", "@types/sharp": "^0.30.2", + "@types/sinon": "^10.0.11", "@types/string-strip-html": "^5.0.0", "@types/supertest": "^2.0.11", "@types/toastr": "^2.1.39", @@ -145,7 +142,6 @@ "chai-dom": "^1.11.0", "chai-spies": "^1.0.0", "cross-env": "^7.0.3", - "cypress": "^7.7.0", "emojione-assets": "^4.5.0", "eslint": "^6.8.0", "eslint-config-prettier": "^8.5.0", @@ -170,6 +166,7 @@ "postcss-url": "^10.1.3", "prettier": "2.6.2", "rewire": "^6.0.0", + "sinon": "^14.0.0", "source-map": "^0.7.3", "stylelint": "^13.13.1", "stylelint-order": "^4.1.0", @@ -212,8 +209,10 @@ "@slack/client": "^4.12.0", "@slack/rtm-api": "^6.0.0", "@types/cookie": "^0.5.1", + "@types/katex": "^0.14.0", "@types/lodash": "^4.14.182", "@types/lodash.debounce": "^4.0.6", + "@types/object-path": "^0.11.1", "@types/proxy-from-env": "^1.0.1", "@types/speakeasy": "^2.0.7", "adm-zip": "0.5.9", @@ -244,8 +243,6 @@ "cors": "^2.8.5", "css-vars-ponyfill": "^2.4.7", "csv-parse": "^5.0.4", - "cypress-real-events": "^1.7.0", - "cypress-wait-until": "^1.7.2", "date-fns": "^2.28.0", "dompurify": "^2.3.6", "ejson": "^2.2.2", @@ -293,7 +290,7 @@ "mailparser": "^3.4.0", "marked": "^0.7.0", "mem": "^8.1.1", - "meteor-node-stubs": "^1.2.1", + "meteor-node-stubs": "^1.2.3", "mime-db": "^1.52.0", "mime-type": "^4.0.0", "mkdirp": "^1.0.4", diff --git a/apps/meteor/playwright.config.ts b/apps/meteor/playwright.config.ts index b37124707b01..744accb7eb41 100644 --- a/apps/meteor/playwright.config.ts +++ b/apps/meteor/playwright.config.ts @@ -1,21 +1,31 @@ import { PlaywrightTestConfig } from '@playwright/test'; +import { verifyTestBaseUrl } from './tests/e2e/utils/configs/verifyTestBaseUrl'; + +const { isLocal, baseURL } = verifyTestBaseUrl(); + +const localInserts = isLocal + ? { + globalSetup: require.resolve('./tests/e2e/utils/configs/setup.ts'), + globalTeardown: require.resolve('./tests/e2e/utils/configs/teardown.ts'), + } + : { testIgnore: '00-wizard.spec.ts' }; + const config: PlaywrightTestConfig = { - outputDir: 'tests/e2e/test-failures', - reporter: [['list']], - workers: 1, - globalSetup: require.resolve('./tests/e2e/utils/configs/setup.ts'), - globalTeardown: require.resolve('./tests/e2e/utils/configs/teardown.ts'), + ...localInserts, use: { - baseURL: process.env.ENTERPRISE ? 'http://localhost:4000' : 'http://localhost:3000', headless: true, viewport: { width: 1368, height: 768 }, ignoreHTTPSErrors: true, video: 'retain-on-failure', screenshot: 'only-on-failure', trace: 'retain-on-failure', + baseURL, }, + outputDir: 'tests/e2e/test-failures', + reporter: [['list']], testDir: 'tests/e2e', retries: 3, + workers: 1, }; export default config; diff --git a/apps/meteor/server/modules/watchers/publishFields.ts b/apps/meteor/server/modules/watchers/publishFields.ts index bede67ffd03f..6a0ff581ef50 100644 --- a/apps/meteor/server/modules/watchers/publishFields.ts +++ b/apps/meteor/server/modules/watchers/publishFields.ts @@ -105,7 +105,7 @@ export const roomFields = { queuedAt: 1, // Federation fields - bridged: 1, + federated: 1, // fields used by DMs usernames: 1, diff --git a/apps/meteor/server/modules/watchers/watchers.module.ts b/apps/meteor/server/modules/watchers/watchers.module.ts index b4914d742181..f3567981dc14 100644 --- a/apps/meteor/server/modules/watchers/watchers.module.ts +++ b/apps/meteor/server/modules/watchers/watchers.module.ts @@ -122,7 +122,7 @@ export function initWatchers(models: IModelsParam, broadcast: BroadcastCallback, switch (clientAction) { case 'inserted': case 'updated': - const message: IMessage | undefined = data ?? (await Messages.findOne({ _id: id })); + const message: IMessage | null = data ?? (await Messages.findOne({ _id: id })); if (!message) { return; } diff --git a/apps/meteor/server/startup/migrations/index.ts b/apps/meteor/server/startup/migrations/index.ts index 045c08a18f4e..f293f36a4640 100644 --- a/apps/meteor/server/startup/migrations/index.ts +++ b/apps/meteor/server/startup/migrations/index.ts @@ -89,4 +89,5 @@ import './v262'; import './v263'; import './v264'; import './v265'; +import './v266'; import './xrun'; diff --git a/apps/meteor/server/startup/migrations/v266.ts b/apps/meteor/server/startup/migrations/v266.ts new file mode 100644 index 000000000000..a2c016b1c571 --- /dev/null +++ b/apps/meteor/server/startup/migrations/v266.ts @@ -0,0 +1,19 @@ +import { addMigration } from '../../lib/migrations'; +import { Rooms } from '../../../app/models/server/raw'; + +addMigration({ + version: 266, + async up() { + await Rooms.updateMany( + { bridged: true }, + { + $set: { + federated: true, + }, + $unset: { + bridged: 1, + }, + }, + ); + }, +}); diff --git a/apps/meteor/tests/.eslintrc b/apps/meteor/tests/.eslintrc index 72c84bef583b..95b73a453271 100644 --- a/apps/meteor/tests/.eslintrc +++ b/apps/meteor/tests/.eslintrc @@ -4,8 +4,6 @@ }, "globals": { "browser": false, - "Cypress": false, - "cy": false, "expect": false } } diff --git a/apps/meteor/tests/cypress/fixtures/example.json b/apps/meteor/tests/cypress/fixtures/example.json deleted file mode 100644 index da18d9352a17..000000000000 --- a/apps/meteor/tests/cypress/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} \ No newline at end of file diff --git a/apps/meteor/tests/cypress/integration/00-login.spec.js b/apps/meteor/tests/cypress/integration/00-login.spec.js deleted file mode 100644 index 895f75669298..000000000000 --- a/apps/meteor/tests/cypress/integration/00-login.spec.js +++ /dev/null @@ -1,158 +0,0 @@ -import loginPage from '../pageobjects/login.page'; -import setupWizard from '../pageobjects/setup-wizard.page'; - -describe('[Login]', () => { - before(() => { - localStorage.clear(); - loginPage.open(); - }); - - describe('[Render]', () => { - it('it should show email / username field', () => { - loginPage.emailOrUsernameField.should('be.visible'); - }); - - it('it should show password field', () => { - loginPage.passwordField.should('be.visible'); - }); - - it('it should show submit button', () => { - loginPage.submitButton.should('be.visible'); - }); - - it('it should show register button', () => { - loginPage.registerButton.should('be.visible'); - }); - - it('it should show forgot password button', () => { - loginPage.forgotPasswordButton.should('be.visible'); - }); - - it('it should not show name field', () => { - loginPage.nameField.should('not.exist'); - }); - - it('it should not show email field', () => { - loginPage.emailField.should('not.exist'); - }); - - it('it should not show confirm password field', () => { - loginPage.confirmPasswordField.should('not.exist'); - }); - - it('it should not show back to login button', () => { - loginPage.backToLoginButton.should('not.exist'); - }); - }); - - describe('[Required Fields]', () => { - before(() => { - loginPage.submit(); - }); - - describe('email / username: ', () => { - it('it should be required', () => { - loginPage.emailOrUsernameField.should('have.class', 'error'); - loginPage.emailOrUsernameInvalidText.get('text').should('not.be.empty'); - }); - }); - - describe('password: ', () => { - it('it should be required', () => { - loginPage.passwordField.should('have.class', 'error'); - loginPage.passwordInvalidText.get('text').should('not.be.empty'); - }); - }); - }); -}); - -describe('[Setup Wizard]', () => { - before(() => { - loginPage.open(); - setupWizard.login(); - }); - - describe('[Render - Step 2]', () => { - it('it should show organization name', () => { - setupWizard.organizationName.should('be.visible'); - }); - - it('it should show organization type', () => { - setupWizard.organizationType.should('be.visible'); - }); - - it('it should show industry', () => { - setupWizard.industry.should('be.visible'); - }); - - it('it should show size', () => { - setupWizard.size.should('be.visible'); - }); - - it('it should show country', () => { - setupWizard.country.should('be.visible'); - }); - - it('it should fill the form', () => { - setupWizard.organizationName.type('Org Name'); - setupWizard.size.click().wait(100); - cy.get('.rcx-options .rcx-option:first-child').click(); - cy.get('.rcx-options').should('not.exist'); - setupWizard.industry.click().wait(100); - cy.get('.rcx-options .rcx-option:first-child').click(); - cy.get('.rcx-options').should('not.exist'); - setupWizard.country.click().wait(100); - cy.get('.rcx-options .rcx-option:first-child').click(); - cy.get('.rcx-options').should('not.exist'); - }); - - after(() => { - setupWizard.goNext(); - }); - }); - - describe('[Render - Step 3]', () => { - it('it should have email field to register the server', () => { - setupWizard.registeredServer.should('be.visible'); - }); - - it('it should start with "Register" button disabled', () => { - setupWizard.registerButton.should('be.disabled'); - }); - - it('it should show an error on invalid email', () => { - setupWizard.registeredServer.type('a'); - setupWizard.registeredServer.clear(); - cy.get('.rcx-field__error:contains("This field is required")').should('be.visible'); - }); - - it('it should enable "Register" button when email is valid and terms checked', () => { - setupWizard.registeredServer.type('email@email.com'); - setupWizard.agreementField.click(); - setupWizard.registerButton.should('be.enabled'); - }); - - it('it should have option for standalone server', () => { - setupWizard.standaloneServer.should('be.visible'); - }); - - it('it should continue when clicking on "Continue as standalone"', () => { - setupWizard.standaloneServer.click(); - }); - }); - - describe('[Render - Final Step]', () => { - it('it should confirm the standalone option', () => { - setupWizard.goToWorkspace.should('be.visible'); - setupWizard.standaloneConfirmText.should('be.visible'); - }); - - it('it should confirm standalone', () => { - setupWizard.goToWorkspace.click(); - }); - }); - - after(() => { - cy.logout(); - }); -}); diff --git a/apps/meteor/tests/cypress/integration/01-register.spec.js b/apps/meteor/tests/cypress/integration/01-register.spec.js deleted file mode 100644 index c859a8cd3095..000000000000 --- a/apps/meteor/tests/cypress/integration/01-register.spec.js +++ /dev/null @@ -1,107 +0,0 @@ -import loginPage from '../pageobjects/login.page'; - -describe('[Register]', () => { - before(() => { - loginPage.open(); - loginPage.gotToRegister(); - }); - - describe('render:', () => { - it('it should show name field', () => { - loginPage.nameField.should('be.visible'); - }); - - it('it should show email field', () => { - loginPage.emailField.should('be.visible'); - }); - - it('it should show password field', () => { - loginPage.passwordField.should('be.visible'); - }); - - it('it should show confirm password field', () => { - loginPage.confirmPasswordField.should('be.visible'); - }); - - it('it should not show email / username field', () => { - loginPage.emailOrUsernameField.should('not.exist'); - }); - - it('it should show submit button', () => { - loginPage.submitButton.should('be.visible'); - }); - - it('it should not show register button', () => { - loginPage.registerButton.should('not.exist'); - }); - - it('it should not show forgot password button', () => { - loginPage.forgotPasswordButton.should('not.exist'); - }); - - it('it should show back to login button', () => { - loginPage.backToLoginButton.should('be.visible'); - }); - }); - - describe('name:', () => { - it('it should be required', () => { - loginPage.submit(); - loginPage.nameField.should('have.class', 'error'); - loginPage.nameInvalidText.get('text').should('not.be.empty'); - }); - }); - - describe('email:', () => { - it('it should be required', () => { - loginPage.submit(); - loginPage.emailField.should('have.class', 'error'); - loginPage.emailInvalidText.get('text').should('not.be.empty'); - }); - - it('it should be invalid for email without domain', () => { - loginPage.emailField.type('invalid-email'); - loginPage.submit(); - loginPage.emailField.should('have.class', 'error'); - loginPage.emailInvalidText.get('text').should('not.be.empty'); - }); - - it('it should be invalid for email with invalid domain', () => { - loginPage.emailField.type('invalid-email@mail'); - loginPage.submit(); - loginPage.emailField.should('have.class', 'error'); - loginPage.emailInvalidText.get('text').should('not.be.empty'); - }); - - it.skip('it should be invalid for email space', () => { - loginPage.emailField.type('invalid email@mail.com'); - loginPage.submit(); - loginPage.emailField.should('have.class', 'error'); - loginPage.emailInvalidText.get('text').should('not.be.empty'); - }); - }); - - describe('password:', () => { - it('it should be required', () => { - loginPage.submit(); - loginPage.passwordField.should('have.class', 'error'); - loginPage.passwordInvalidText.get('text').should('not.be.empty'); - }); - }); - - describe('confirm-password:', () => { - it('it should be invalid if different from password', () => { - loginPage.passwordField.type('password'); - loginPage.submit(); - loginPage.confirmPasswordField.should('have.class', 'error'); - loginPage.confirmPasswordInvalidText.get('text').should('not.be.empty'); - }); - - it('it should be valid if equal to password', () => { - loginPage.confirmPasswordField.type('password'); - loginPage.submit(); - loginPage.passwordField.should('not.have.class', 'error'); - loginPage.passwordInvalidText.should('not.have.text'); - }); - }); -}); diff --git a/apps/meteor/tests/cypress/integration/02-forgot-password.spec.js b/apps/meteor/tests/cypress/integration/02-forgot-password.spec.js deleted file mode 100644 index c9069ee34d4b..000000000000 --- a/apps/meteor/tests/cypress/integration/02-forgot-password.spec.js +++ /dev/null @@ -1,75 +0,0 @@ -import loginPage from '../pageobjects/login.page'; - -describe('[Forgot Password]', () => { - before(() => { - loginPage.open(); - loginPage.gotToForgotPassword(); - }); - - describe('render:', () => { - it('it should not show name field', () => { - loginPage.nameField.should('not.exist'); - }); - - it('it should show email field', () => { - loginPage.emailField.should('be.visible'); - }); - - it('it should not show password field', () => { - loginPage.passwordField.should('not.exist'); - }); - - it('it should not show confirm password field', () => { - loginPage.confirmPasswordField.should('not.exist'); - }); - - it('it should not show email / username field', () => { - loginPage.emailOrUsernameField.should('not.exist'); - }); - - it('it should show submit button', () => { - loginPage.submitButton.should('be.visible'); - }); - - it('it should not show register button', () => { - loginPage.registerButton.should('not.exist'); - }); - - it('it should not show forgot password button', () => { - loginPage.forgotPasswordButton.should('not.exist'); - }); - - it('it should show back to login button', () => { - loginPage.backToLoginButton.should('be.visible'); - }); - }); - - describe('email:', () => { - it('it should be required', () => { - loginPage.submit(); - loginPage.emailField.should('have.class', 'error'); - loginPage.emailInvalidText.get('text').should('not.be.empty'); - }); - - it('it should be invalid for email without domain', () => { - loginPage.emailField.type('invalid-email'); - loginPage.submit(); - loginPage.emailField.should('have.class', 'error'); - loginPage.emailInvalidText.get('text').should('not.be.empty'); - }); - - it('it should be invalid for email with invalid domain', () => { - loginPage.emailField.type('invalid-email@mail'); - loginPage.submit(); - loginPage.emailField.should('have.class', 'error'); - loginPage.emailInvalidText.get('text').should('not.be.empty'); - }); - - it.skip('it should be invalid for email space', () => { - loginPage.emailField.type('invalid email@mail.com'); - loginPage.submit(); - loginPage.emailField.should('have.class', 'error'); - loginPage.emailInvalidText.get('text').should('not.be.empty'); - }); - }); -}); diff --git a/apps/meteor/tests/cypress/integration/03-user-creation.spec.js b/apps/meteor/tests/cypress/integration/03-user-creation.spec.js deleted file mode 100644 index bb64a3292ec3..000000000000 --- a/apps/meteor/tests/cypress/integration/03-user-creation.spec.js +++ /dev/null @@ -1,19 +0,0 @@ -import loginPage from '../pageobjects/login.page'; -import { username, email, password } from '../../data/user.js'; - -// Basic usage test start -describe('[User Creation]', function () { - before(() => { - loginPage.open(); - }); - - it('it should create user', () => { - loginPage.gotToRegister(); - - loginPage.registerNewUser({ username, email, password }); - - loginPage.usernameField.should('be.visible'); - - loginPage.submitButton.click(); - }); -}); diff --git a/apps/meteor/tests/cypress/integration/04-main-elements-render.spec.js b/apps/meteor/tests/cypress/integration/04-main-elements-render.spec.js deleted file mode 100644 index 4d8d5d67e6b1..000000000000 --- a/apps/meteor/tests/cypress/integration/04-main-elements-render.spec.js +++ /dev/null @@ -1,285 +0,0 @@ -import flexTab from '../pageobjects/flex-tab.page'; -import mainContent from '../pageobjects/main-content.page'; -import sideNav from '../pageobjects/side-nav.page'; -import { checkIfUserIsValid } from '../../data/checks'; -import { username, email, password } from '../../data/user.js'; - -describe('[Main Elements Render]', function () { - before(() => { - checkIfUserIsValid(username, email, password); - sideNav.spotlightSearchIcon.click(); - sideNav.searchChannel('general'); - }); - - describe('[Side Nav Bar]', () => { - describe('render:', () => { - it('it should show the new channel button', () => { - sideNav.newChannelBtnToolbar.should('be.visible'); - }); - - it('it should show "general" channel', () => { - sideNav.general.should('be.visible'); - }); - }); - - describe('spotlight search render:', () => { - after(() => { - mainContent.messageInput.click(); - }); - it('it should show spotlight search bar', () => { - sideNav.spotlightSearchIcon.click(); - sideNav.spotlightSearch.should('be.visible'); - }); - - it('it should click the spotlight and show the channel list', () => { - sideNav.spotlightSearch.click('center'); - sideNav.spotlightSearchPopUp.should('be.visible'); - }); - - it.skip('it should remove the list when the spotlight loses focus', () => { - sideNav.spotlightSearchPopUp.should('be.visible'); - mainContent.messageInput.click(); - mainContent.lastMessage.click(); - sideNav.spotlightSearchPopUp.should('not.exist'); - }); - - it('it should add text to the spotlight and show the channel list', () => { - sideNav.spotlightSearch.type('rocket.cat'); - sideNav.spotlightSearchPopUp.should('be.visible'); - }); - - it.skip('it should remove the text on the spotlight and the list when lost focus', () => { - sideNav.spotlightSearchPopUp.should('be.visible'); - mainContent.messageInput.click(); - sideNav.spotlightSearchPopUp.should('not.exist'); - sideNav.spotlightSearch.should('have.text', ''); - }); - }); - }); - - describe('[User Options]', () => { - describe('render:', () => { - before(() => { - sideNav.sidebarUserMenu.click(); - }); - - after(() => { - sideNav.sidebarUserMenu.click(); - }); - - it('it should show online button', () => { - sideNav.statusOnline.should('be.visible'); - }); - - it('it should show away button', () => { - sideNav.statusAway.should('be.visible'); - }); - - it('it should show busy button', () => { - sideNav.statusBusy.should('be.visible'); - }); - - it('it should show offline button', () => { - sideNav.statusOffline.should('be.visible'); - }); - - it('it should show my account button', () => { - sideNav.account.should('be.visible'); - }); - - it('it should show logout button', () => { - sideNav.logout.should('be.visible'); - }); - }); - }); - - describe('[Main Content]', () => { - describe('render:', () => { - before(() => { - sideNav.openChannel('general'); - }); - - it('it should show the title of the channel', () => { - mainContent.channelTitle.contains('general').should('be.visible'); - }); - - it('it should show the empty favorite star', () => { - mainContent.emptyFavoriteStar.should('be.visible'); - }); - - it('it should click the star', () => { - mainContent.emptyFavoriteStar.click(); - }); - - it('it should show the filled favorite star', () => { - mainContent.favoriteStar.should('be.visible'); - }); - - it('it should click the star', () => { - mainContent.favoriteStar.click(); - }); - - it('it should show the empty favorite star', () => { - mainContent.emptyFavoriteStar.should('be.visible'); - }); - - it('it should show the message input bar', () => { - mainContent.messageInput.should('be.visible'); - }); - - it('it should show the message box actions button', () => { - mainContent.messageBoxActions.should('be.visible'); - }); - - // issues with the new message box action button and the no animations on tests - - it('it should show the audio recording button', () => { - mainContent.recordBtn.should('be.visible'); - }); - - it('it should show the emoji button', () => { - mainContent.emojiBtn.should('be.visible'); - }); - }); - }); - - describe('[Flextab]', () => { - describe('[Render]', () => { - before(() => { - sideNav.openChannel('general'); - }); - - describe('Room Info Tab:', () => { - before(() => { - flexTab.operateFlexTab('info', true); - }); - - after(() => { - flexTab.operateFlexTab('info', false); - }); - - it('it should show the room info button', () => { - flexTab.channelTab.should('be.visible'); - }); - - it('it should show the room info tab content', () => { - flexTab.channelSettings.should('be.visible'); - }); - - it.skip('it should show the room name', () => { - flexTab.channelSettingName.should('have.attr', 'title', 'general'); - }); - }); - - describe('Search Tab:', () => { - before(() => { - flexTab.operateFlexTab('search', true); - }); - - after(() => { - flexTab.operateFlexTab('search', false); - }); - - it('it should show the message search button', () => { - flexTab.searchTab.should('be.visible'); - }); - - it('it should show the message tab content', () => { - flexTab.searchTabContent.should('be.visible'); - }); - }); - - describe('Members Tab:', () => { - before(() => { - flexTab.operateFlexTab('members', true); - }); - - after(() => { - flexTab.operateFlexTab('members', false); - }); - - it('it should show the members tab button', () => { - flexTab.membersTab.should('be.visible'); - }); - - it('it should show the members content', () => { - flexTab.membersTabContent.should('be.visible'); - }); - }); - - describe('Notifications Tab:', () => { - before(() => { - flexTab.operateFlexTab('notifications', true); - }); - - after(() => { - flexTab.operateFlexTab('notifications', false); - }); - - it('it should not show the notifications button', () => { - flexTab.notificationsTab.should('not.exist'); - }); - - it('it should show the notifications Tab content', () => { - flexTab.notificationsSettings.should('be.visible'); - }); - }); - - describe('Files Tab:', () => { - before(() => { - flexTab.operateFlexTab('files', true); - }); - - after(() => { - flexTab.operateFlexTab('files', false); - }); - - it('it should show the files Tab content', () => { - flexTab.filesTabContent.should('be.visible'); - }); - }); - - describe('Mentions Tab:', () => { - before(() => { - flexTab.operateFlexTab('mentions', true); - }); - - after(() => { - flexTab.operateFlexTab('mentions', false); - }); - - it('it should show the mentions Tab content', () => { - flexTab.mentionsTabContent.should('be.visible'); - }); - }); - - describe('Starred Messages Tab:', () => { - before(() => { - flexTab.operateFlexTab('starred', true); - }); - - after(() => { - flexTab.operateFlexTab('starred', false); - }); - - it('it should show the starred messages Tab content', () => { - flexTab.starredTabContent.should('be.visible'); - }); - }); - - describe('Pinned Messages Tab:', () => { - before(() => { - flexTab.operateFlexTab('pinned', true); - }); - - after(() => { - flexTab.operateFlexTab('pinned', false); - }); - - it('it should show the pinned messages Tab content', () => { - flexTab.pinnedTabContent.should('be.visible'); - }); - }); - }); - }); -}); diff --git a/apps/meteor/tests/cypress/integration/05-channel-creation.spec.js b/apps/meteor/tests/cypress/integration/05-channel-creation.spec.js deleted file mode 100644 index 89f906d72bf2..000000000000 --- a/apps/meteor/tests/cypress/integration/05-channel-creation.spec.js +++ /dev/null @@ -1,34 +0,0 @@ -import sideNav from '../pageobjects/side-nav.page'; -import { publicChannelName, privateChannelName } from '../../data/channel.js'; -import { targetUser } from '../../data/interactions.js'; -import { checkIfUserIsValid, setPublicChannelCreated, setPrivateChannelCreated, setDirectMessageCreated } from '../../data/checks'; -import { username, email, password } from '../../data/user.js'; - -// Basic usage test start -describe('[Channel creation]', function () { - before(() => { - checkIfUserIsValid(username, email, password); - }); - - describe('public channel:', function () { - it('it should create a public channel', function () { - sideNav.createChannel(publicChannelName, false, false); - setPublicChannelCreated(true); - }); - }); - - describe('private channel:', function () { - it('it should create a private channel', function () { - sideNav.createChannel(privateChannelName, true, false); - setPrivateChannelCreated(true); - }); - }); - - describe('direct message:', function () { - it('it should start a direct message with rocket.cat', function () { - sideNav.spotlightSearchIcon.click(); - sideNav.searchChannel(targetUser); - setDirectMessageCreated(true); - }); - }); -}); diff --git a/apps/meteor/tests/cypress/integration/06-messaging.spec.js b/apps/meteor/tests/cypress/integration/06-messaging.spec.js deleted file mode 100644 index 79d4aaf7d1f6..000000000000 --- a/apps/meteor/tests/cypress/integration/06-messaging.spec.js +++ /dev/null @@ -1,423 +0,0 @@ -import mainContent from '../pageobjects/main-content.page'; -import sideNav from '../pageobjects/side-nav.page'; -import flexTab from '../pageobjects/flex-tab.page'; -import Global from '../pageobjects/global'; -import { username, email, password } from '../../data/user.js'; -import { publicChannelName, privateChannelName } from '../../data/channel.js'; -import { targetUser, imgURL } from '../../data/interactions.js'; -import { - checkIfUserIsValid, - publicChannelCreated, - privateChannelCreated, - directMessageCreated, - setPublicChannelCreated, - setPrivateChannelCreated, - setDirectMessageCreated, -} from '../../data/checks'; -import { updatePermission } from '../../data/permissions.helper'; -import { api, getCredentials, credentials, request } from '../../data/api-data'; -import { createUser, login } from '../../data/users.helper'; - -// Test data -const message = `message from ${username}`; -let testDMUsername; - -function messagingTest(currentTest) { - describe('Normal message:', () => { - it('it should send a message', () => { - mainContent.sendMessage(message); - }); - - it('it should show the last message', () => { - mainContent.lastMessage.should('be.visible'); - }); - - if (currentTest !== 'direct') { - it('it should be that the last message is from the logged user', () => { - mainContent.lastMessage.should('contain', username); - }); - } - - if (currentTest === 'general') { - it('it should not show the Admin tag', () => { - mainContent.lastMessageUserTag.should('not.exist'); - }); - } - }); - - describe.skip('fileUpload:', () => { - after(() => {}); - it('it should send a attachment', () => { - mainContent.fileUpload(imgURL); - }); - - it('it should show the confirm button', () => { - Global.modalConfirm.should('be.visible'); - }); - - it('it should show the cancel button', () => { - Global.modalCancel.should('be.visible'); - }); - - it('it should show the file preview', () => { - Global.modalFilePreview.should('be.visible'); - }); - - it('it should show the confirm button', () => { - Global.modalConfirm.should('be.visible'); - }); - - it('it should show the file title', () => { - Global.modalFileTitle.should('be.visible'); - }); - - it('it should show the file name input', () => { - Global.modalFileName.should('be.visible'); - }); - - it('it should fill the file name input', () => { - Global.modalFileName.type('File Name'); - }); - - it('it should show the file name input', () => { - Global.modalFileDescription.should('be.visible'); - }); - - it('it should fill the file name input', () => { - Global.modalFileDescription.type('File Description'); - }); - - it('it should click the confirm', () => { - Global.modalConfirm.click(); - Global.modalConfirm.waitForVisible(5000, true); - }); - - it('it should show the file in the message', () => { - mainContent.lastMessageDesc.waitForVisible(10000); - mainContent.lastMessageDesc.getText().should.equal('File Description'); - }); - }); -} - -function grantCreateDPermission() { - return new Promise((resolve) => { - getCredentials(() => { - updatePermission('create-d', ['user']).then(resolve); - }); - }); -} - -function revokeCreateDPermission() { - return new Promise((resolve) => { - getCredentials(() => { - updatePermission('create-d', []).then(resolve); - }); - }); -} - -function toggleOpenMessageActionMenu() { - mainContent.closeMessageActionMenu(); - mainContent.openMessageActionMenu(); -} - -function createDMUserAndPost(testChannel, done) { - getCredentials(() => { - createUser().then((createdUser) => { - testDMUsername = createdUser.username; - - request - .post(api('users.setActiveStatus')) - .set(credentials) - .send({ - activeStatus: true, - userId: createdUser._id, - }) - .then(() => { - login(testDMUsername, password).then((userCredentials) => { - request - .post(api('chat.postMessage')) - .set(userCredentials) - .send({ - channel: testChannel, - text: 'Message from Test DM user', - }) - .end(done); - }); - }); - }); - }); -} - -function leaveTestDM() { - // Leave the existing DM - const dmElement = sideNav.getChannelFromList(testDMUsername).scrollIntoView().rightclick().wait(800); - dmElement.closest('.rcx-sidebar-item--clickable').find('.rcx-sidebar-item__menu-wrapper > button').click(); - sideNav.popOverHideOption.click(); - - Global.modal.should('be.visible'); - Global.modalConfirm.click(); -} - -function messageActionsTest(currentTest, testChannel) { - describe('[Actions]', () => { - before(() => { - mainContent.sendMessage('Message for Message Actions Tests'); - }); - describe('Render:', () => { - before(() => { - mainContent.openMessageActionMenu(); - }); - - after(() => { - mainContent.closeMessageActionMenu(); - }); - - it('it should show the message action menu', () => { - mainContent.messageActionMenu.should('be.visible'); - }); - - it('it should show the edit action', () => { - mainContent.messageEdit.scrollIntoView().should('be.visible'); - }); - - it('it should show the delete action', () => { - mainContent.messageDelete.scrollIntoView().should('be.visible'); - }); - - it('it should show the permalink action', () => { - mainContent.messagePermalink.scrollIntoView().should('be.visible'); - }); - - it('it should show the copy action', () => { - mainContent.messageCopy.scrollIntoView().should('be.visible'); - }); - - it('it should show the quote the action', () => { - mainContent.messageQuote.scrollIntoView().should('be.visible'); - }); - - it('it should show the star action', () => { - mainContent.messageStar.scrollIntoView().should('be.visible'); - }); - - if (currentTest === 'general') { - it('it should not show the pin action', () => { - mainContent.messagePin.should('not.exist'); - }); - } - - it('it should not show the mark as unread action', () => { - mainContent.messageUnread.should('not.exist'); - }); - - if (currentTest === 'direct') { - it('it should not show the Reply to DM action', () => { - mainContent.messageReplyInDM.should('not.exist'); - }); - } else if (currentTest !== 'private') { - context('when the channel last message was posted by someone else', () => { - before((done) => { - revokeCreateDPermission().then(() => { - createDMUserAndPost(testChannel, done); - }); - }); - - it('it should not show the Reply to DM action', () => { - toggleOpenMessageActionMenu(); - // We don't have the test DM user in a DM channel or have the `create-d` permission - mainContent.messageReplyInDM.should('not.exist'); - }); - - context('when the user has permission to create DMs', () => { - before(() => grantCreateDPermission()); - after(() => revokeCreateDPermission()); - - it('it should show the Reply to DM action', () => { - toggleOpenMessageActionMenu(); - - mainContent.messageReplyInDM.should('be.visible'); - }); - }); - - context('when the user already has a created DM', () => { - // Grant Create DM permission, create a DM, then revoke the permission - before(() => grantCreateDPermission()); - - before(() => { - mainContent.closeMessageActionMenu(); - sideNav.spotlightSearchIcon.click(); - sideNav.searchChannel(testDMUsername); - }); - - before(() => revokeCreateDPermission()); - - before(() => { - sideNav.openChannel(testChannel); - mainContent.openMessageActionMenu(); - }); - - after(() => { - mainContent.closeMessageActionMenu(); - leaveTestDM(); - }); - - it('it should show the Reply to DM action', () => { - mainContent.messageReplyInDM.should('be.visible'); - }); - }); - }); - } - }); - - describe('[Usage]', () => { - describe('Reply:', () => { - it('it should reply the message', () => { - toggleOpenMessageActionMenu(); - - mainContent.selectAction('reply'); - flexTab.sendBtn.click(); - }); - - it('it should check if the message was replied', () => { - mainContent.beforeLastMessageQuote.then(($el) => { - const text = $el.data('id'); - mainContent.lastMessageQuote.should('has.attr', 'data-tmid', text); - }); - flexTab.threadTab.click(); - }); - }); - - describe('Edit:', () => { - before(() => { - mainContent.sendMessage('Message for Message edit Tests'); - mainContent.openMessageActionMenu(); - }); - - it('it should edit the message', () => { - mainContent.selectAction('edit'); - mainContent.sendBtn.click(); - }); - }); - - describe('Delete:', () => { - before(() => { - mainContent.sendMessage('Message for Message Delete Tests'); - mainContent.openMessageActionMenu(); - }); - - it('it should delete the message', () => { - mainContent.selectAction('delete'); - Global.modalConfirm.click(); - Global.modalOverlay.should('not.exist'); - }); - - it('it should not show the deleted message', () => { - mainContent.lastMessage.should('not.contain', 'Message for Message Delete Tests'); - }); - }); - - describe('Quote:', () => { - const message = `Message for quote Tests - ${Date.now()}`; - - before(() => { - mainContent.sendMessage(message); - mainContent.openMessageActionMenu(); - }); - - it('it should quote the message', () => { - mainContent.selectAction('quote'); - mainContent.sendBtn.click(); - mainContent.waitForLastMessageTextAttachmentEqualsText(message); - }); - }); - - describe('Star:', () => { - before(() => { - mainContent.sendMessage('Message for star Tests'); - mainContent.openMessageActionMenu(); - }); - - it('it should star the message', () => { - mainContent.selectAction('star'); - }); - }); - - describe('Copy:', () => { - before(() => { - mainContent.sendMessage('Message for copy Tests'); - mainContent.openMessageActionMenu(); - }); - - it('it should copy the message', () => { - mainContent.selectAction('copy'); - }); - }); - - describe('Permalink:', () => { - before(() => { - mainContent.sendMessage('Message for permalink Tests'); - mainContent.openMessageActionMenu(); - }); - - it('it should permalink the message', () => { - mainContent.selectAction('permalink'); - }); - }); - }); - }); -} - -describe('[Message]', () => { - before(() => { - checkIfUserIsValid(username, email, password); - }); - - describe('[GENERAL Channel]', () => { - before(() => { - sideNav.spotlightSearchIcon.click(); - sideNav.searchChannel('general'); - }); - messagingTest('general'); - messageActionsTest('general', 'general'); - }); - - describe('[Public Channel]', () => { - before(() => { - if (!publicChannelCreated) { - sideNav.createChannel(publicChannelName, false, false); - setPublicChannelCreated(true); - console.log(' public channel not found, creating one...'); - } - sideNav.openChannel(publicChannelName); - }); - messagingTest('public'); - messageActionsTest('public', publicChannelName); - }); - - describe('[Private Channel]', () => { - before(() => { - if (!privateChannelCreated) { - sideNav.createChannel(privateChannelName, true, false); - setPrivateChannelCreated(true); - console.log(' private channel not found, creating one...'); - } - sideNav.openChannel(privateChannelName); - }); - messagingTest('private'); - messageActionsTest('private', privateChannelName); - }); - - describe('[Direct Message]', () => { - before(() => { - if (!directMessageCreated) { - sideNav.spotlightSearchIcon.click(); - sideNav.searchChannel(targetUser); - setDirectMessageCreated(true); - console.log(' Direct message not found, creating one...'); - } - sideNav.openChannel(targetUser); - }); - messagingTest('direct'); - messageActionsTest('direct'); - }); -}); diff --git a/apps/meteor/tests/cypress/integration/07-emoji.spec.js b/apps/meteor/tests/cypress/integration/07-emoji.spec.js deleted file mode 100644 index 6d52bc7fc478..000000000000 --- a/apps/meteor/tests/cypress/integration/07-emoji.spec.js +++ /dev/null @@ -1,147 +0,0 @@ -import mainContent from '../pageobjects/main-content.page'; -import sideNav from '../pageobjects/side-nav.page'; -import { username, email, password } from '../../data/user.js'; -import { checkIfUserIsValid } from '../../data/checks'; - -describe('[Emoji]', () => { - before(() => { - checkIfUserIsValid(username, email, password); - sideNav.spotlightSearchIcon.click(); - sideNav.searchChannel('general'); - }); - - describe('Render:', () => { - before(() => { - mainContent.emojiBtn.click(); - }); - - after(() => { - mainContent.emojiSmile.first().click(); - mainContent.setTextToInput(''); - }); - - it('it should show the emoji picker menu', () => { - mainContent.emojiPickerMainScreen.should('be.visible'); - }); - - it('it should click the emoji picker people tab', () => { - mainContent.emojiPickerPeopleIcon.click(); - }); - - it('it should show the emoji picker people tab', () => { - mainContent.emojiPickerPeopleIcon.should('be.visible'); - }); - - it('it should show the emoji picker nature tab', () => { - mainContent.emojiPickerNatureIcon.should('be.visible'); - }); - - it('it should show the emoji picker food tab', () => { - mainContent.emojiPickerFoodIcon.should('be.visible'); - }); - - it('it should show the emoji picker activity tab', () => { - mainContent.emojiPickerActivityIcon.should('be.visible'); - }); - - it('it should show the emoji picker travel tab', () => { - mainContent.emojiPickerTravelIcon.should('be.visible'); - }); - - it('it should show the emoji picker objects tab', () => { - mainContent.emojiPickerObjectsIcon.should('be.visible'); - }); - - it('it should show the emoji picker symbols tab', () => { - mainContent.emojiPickerSymbolsIcon.should('be.visible'); - }); - - it('it should show the emoji picker flags tab', () => { - mainContent.emojiPickerFlagsIcon.should('be.visible'); - }); - - it('it should show the emoji picker custom tab', () => { - mainContent.emojiPickerCustomIcon.should('be.visible'); - }); - - it('it should show the emoji picker change tone button', () => { - mainContent.emojiPickerChangeTone.should('be.visible'); - }); - - it('it should show the emoji picker search bar', () => { - mainContent.emojiPickerFilter.should('be.visible'); - }); - }); - - describe('[Usage]', () => { - describe('send emoji via screen:', () => { - before(() => { - mainContent.emojiBtn.click(); - mainContent.emojiPickerPeopleIcon.click(); - }); - - it('it should select a grinning emoji', () => { - mainContent.emojiGrinning.first().click(); - }); - - it('it should be that the value on the message input is the same as the emoji clicked', () => { - mainContent.messageInput.should('have.value', ':grinning: '); - }); - - it('it should send the emoji', () => { - mainContent.addTextToInput(' '); - mainContent.sendBtn.click(); - }); - - it('it should be that the value on the message is the same as the emoji clicked', () => { - mainContent.lastMessage.should('contain', '😀'); - }); - }); - - describe('send emoji via text:', () => { - it('it should add emoji text to the message input', () => { - mainContent.addTextToInput(':smile'); - }); - - it('it should show the emoji popup bar', () => { - mainContent.messagePopUp.should('be.visible'); - }); - - it('it should be that the emoji popup bar title is emoji', () => { - mainContent.messagePopUpTitle.should('contain', 'Emoji'); - }); - - it('it should show the emoji popup bar items', () => { - mainContent.messagePopUpItems.should('be.visible'); - }); - - it('it should click the first emoji on the popup list', () => { - mainContent.messagePopUpFirstItem.click(); - }); - - it('it should be that the value on the message input is the same as the emoji clicked', () => { - mainContent.messageInput.should('have.value', ':smile: '); - }); - - it('it should send the emoji', () => { - mainContent.sendBtn.click(); - }); - - it('it should be that the value on the message is the same as the emoji clicked', () => { - mainContent.lastMessage.should('contain', '😄'); - }); - }); - - describe("send texts and make sure they're not converted to emojis:", () => { - it('should render numbers', () => { - mainContent.sendMessage('0 1 2 3 4 5 6 7 8 9'); - mainContent.waitForLastMessageEqualsHtml('0 1 2 3 4 5 6 7 8 9'); - }); - - it('should render special characters', () => { - mainContent.sendMessage('® * © ™ #'); - mainContent.waitForLastMessageEqualsHtml('® * © ™ #'); - }); - }); - }); -}); diff --git a/apps/meteor/tests/cypress/integration/08-resolutions.spec.js b/apps/meteor/tests/cypress/integration/08-resolutions.spec.js deleted file mode 100644 index 3c5b431d8bea..000000000000 --- a/apps/meteor/tests/cypress/integration/08-resolutions.spec.js +++ /dev/null @@ -1,92 +0,0 @@ -import Global from '../pageobjects/global'; -import mainContent from '../pageobjects/main-content.page'; -import sideNav from '../pageobjects/side-nav.page'; -import { username, email, password } from '../../data/user.js'; -import { checkIfUserIsValid } from '../../data/checks'; - -// skipping this since the main content its not moved anymore, instead there is a overlay of the side nav over the main content -describe('[Resolution]', () => { - describe('[Mobile Render]', () => { - before(() => { - checkIfUserIsValid(username, email, password); - }); - - beforeEach(() => { - Global.setWindowSize(650, 800); - cy.wait(500); - }); - - after(() => { - Global.setWindowSize(1600, 1600); - // cy.wait(500); - sideNav.spotlightSearchIcon.should('be.visible'); - }); - - it('it should close the sidenav', () => { - mainContent.mainContent.should('be.visible').getLocation().its('x').should('be.equal', 0); - sideNav.sideNavBar.should('not.have.attr', 'data-qa-opened', 'false'); - }); - - describe('moving elements:', () => { - beforeEach(() => { - sideNav.sideNavBar - .parent() - .find('.sidebar') - .then((el) => { - if (!el[0].hasAttribute('data-qa-opened')) { - sideNav.burgerBtn.click({ force: true }); - } - }); - - cy.waitUntil(() => { - return browser.element('.menu-opened').then((el) => el.length); - }); - }); - - it('it should open the sidenav', () => { - cy.waitUntil(() => { - return browser.element('.menu-opened').then((el) => el.length); - }); - mainContent.mainContent.should('be.visible').getLocation().its('x').should('be.equal', 0); - sideNav.sideNavBar.should('have.attr', 'data-qa-opened', 'true'); - }); - - it('it should not close sidebar on pressing the sidebar item menu', () => { - sideNav.firstSidebarItemMenu.click({ force: true }); - cy.wait(800); - mainContent.mainContent.should('be.visible').getLocation().its('x').should('be.equal', 0); - sideNav.sideNavBar.should('have.attr', 'data-qa-opened', 'true'); - sideNav.firstSidebarItemMenu.click({ force: true }); - cy.wait(800); - }); - - it('it should close the sidenav when open general channel', () => { - sideNav.openChannel('general'); - cy.wait(1200); - sideNav.sideNavBar.should('not.have.attr', 'data-qa-opened'); - }); - - // Skipped because it's not closing sidebar after opening an item - describe.skip('Preferences', () => { - it('it should open the user preferences screen', () => { - sideNav.sidebarUserMenu.click(); - sideNav.account.click(); - }); - - it('it should close the sidenav when press the preferences link', () => { - sideNav.preferences.click(); - sideNav.sideNavBar.should('not.have.attr', 'data-qa-opened'); - }); - - it('it should close the sidenav when press the profile link', () => { - sideNav.profile.click(); - sideNav.sideNavBar.should('not.have.attr', 'data-qa-opened'); - }); - - it('it should close the preferences nav', () => { - sideNav.preferencesClose.click(); - }); - }); - }); - }); -}); diff --git a/apps/meteor/tests/cypress/integration/09-channel.spec.js b/apps/meteor/tests/cypress/integration/09-channel.spec.js deleted file mode 100644 index 7a88f0b0cb23..000000000000 --- a/apps/meteor/tests/cypress/integration/09-channel.spec.js +++ /dev/null @@ -1,359 +0,0 @@ -import flexTab from '../pageobjects/flex-tab.page'; -import mainContent from '../pageobjects/main-content.page'; -import sideNav from '../pageobjects/side-nav.page'; -import Global from '../pageobjects/global'; -import { username, email, password } from '../../data/user.js'; -import { checkIfUserIsValid, publicChannelCreated, setPublicChannelCreated } from '../../data/checks'; -import { publicChannelName } from '../../data/channel.js'; -import { targetUser } from '../../data/interactions.js'; - -describe('[Channel]', () => { - before(() => { - checkIfUserIsValid(username, email, password); - if (!publicChannelCreated) { - sideNav.createChannel(publicChannelName, false, false); - setPublicChannelCreated(true); - console.log('public channel not found, creating one...'); - } - sideNav.openChannel('general'); - }); - describe('[Search]', () => { - describe('[SpotlightSearch]', () => { - describe('rocket.cat:', () => { - it('it should search rocket cat', () => { - sideNav.spotlightSearchIcon.click(); - sideNav.searchChannel('rocket.cat'); - }); - - it('it should start a direct message with rocket.cat', () => { - mainContent.channelTitle.should('contain', 'rocket.cat'); - }); - }); - - describe('general:', () => { - it('it should search general', () => { - sideNav.spotlightSearchIcon.click(); - sideNav.searchChannel('general'); - }); - - it('it should go to general', () => { - mainContent.channelTitle.should('contain', 'general'); - }); - }); - - describe('user created channel:', () => { - it('it should search the user created channel', () => { - sideNav.spotlightSearchIcon.click(); - sideNav.searchChannel(publicChannelName); - }); - - it('it should go to the user created channel', () => { - mainContent.channelTitle.should('contain', publicChannelName); - }); - }); - }); - - describe('[SideNav Channel List]', () => { - before(() => { - mainContent.messageInput.click(); - }); - describe('rocket.cat:', () => { - it('it should show the rocket cat in the direct messages list', () => { - sideNav.getChannelFromList('rocket.cat').scrollIntoView().should('be.visible'); - }); - - it('it should go to the rocket cat direct message', () => { - sideNav.openChannel('rocket.cat'); - }); - }); - - describe('general:', () => { - it('it should show the general in the channel list', () => { - sideNav.getChannelFromList('general').scrollIntoView().should('be.visible'); - }); - - it('it should go to the general channel', () => { - sideNav.openChannel('general'); - }); - }); - - describe('user created channel:', () => { - it('it should show the user created channel in the channel list', () => { - sideNav.getChannelFromList(publicChannelName).scrollIntoView().should('be.visible'); - }); - - it('it should go to the user created channel', () => { - sideNav.openChannel(publicChannelName); - }); - }); - }); - }); - - describe.skip('[Usage]', () => { - before(() => { - sideNav.openChannel(publicChannelName); - }); - - describe('Adding a user to the room:', () => { - before(() => { - if (Global.toastAlert.isVisible()) { - Global.dismissToast(); - } - flexTab.operateFlexTab('members', true); - }); - - after(() => { - if (Global.toastAlert.isVisible()) { - Global.dismissToast(); - } - flexTab.operateFlexTab('members', false); - }); - - it('it should add people to the room', () => { - flexTab.addPeopleToChannel(targetUser); - }); - }); - - describe('Channel settings:', () => { - describe('Channel name edit', () => { - before(() => { - if (Global.toastAlert.isVisible()) { - Global.dismissToast(); - } - flexTab.operateFlexTab('info', true); - }); - - after(() => { - if (Global.toastAlert.isVisible()) { - Global.dismissToast(); - } - flexTab.operateFlexTab('info', false); - }); - - it('it should show the old name', () => { - flexTab.firstSetting.getText().should.equal(publicChannelName); - }); - - it('it should click the edit name', () => { - flexTab.editNameBtn.click(); - }); - - it('it should edit the name input', () => { - flexTab.editNameTextInput.type(`NAME-EDITED-${publicChannelName}`); - }); - - it('it should save the name', () => { - flexTab.editNameSave.click(); - }); - - it('it should show the new name', () => { - const channelName = sideNav.getChannelFromList(`NAME-EDITED-${publicChannelName}`); - channelName.getText().should.equal(`NAME-EDITED-${publicChannelName}`); - }); - }); - - describe('Channel topic edit', () => { - before(() => { - flexTab.operateFlexTab('info', true); - }); - - after(() => { - if (Global.toastAlert.isVisible()) { - Global.dismissToast(); - } - flexTab.operateFlexTab('info', false); - }); - - it('it should click the edit topic', () => { - flexTab.editTopicBtn.click(); - }); - - it('it should edit the topic input', () => { - flexTab.editTopicTextInput.type('TOPIC EDITED'); - }); - - it('it should save the topic', () => { - flexTab.editNameSave.click(); - }); - - it('it should show the new topic', () => { - flexTab.secondSetting.getText().should.equal('TOPIC EDITED'); - }); - }); - - describe('Channel announcement edit', () => { - before(() => { - flexTab.operateFlexTab('info', true); - }); - - after(() => { - if (Global.toastAlert.isVisible()) { - Global.dismissToast(); - } - flexTab.operateFlexTab('info', false); - }); - - it('it should click the edit announcement', () => { - flexTab.editAnnouncementBtn.click(); - }); - - it('it should edit the announcement input', () => { - flexTab.editAnnouncementTextInput.type('ANNOUNCEMENT EDITED'); - }); - - it('it should save the announcement', () => { - flexTab.editNameSave.click(); - }); - - it('it should show the new announcement', () => { - flexTab.thirdSetting.getText().should.equal('ANNOUNCEMENT EDITED'); - }); - }); - - describe('Channel description edit', () => { - before(() => { - flexTab.operateFlexTab('info', true); - }); - - after(() => { - if (Global.toastAlert.isVisible()) { - Global.dismissToast(); - } - flexTab.operateFlexTab('info', false); - }); - - it('it should click the edit description', () => { - flexTab.editDescriptionBtn.click(); - }); - - it('it should edit the description input', () => { - flexTab.editDescriptionTextInput.type('DESCRIPTION EDITED'); - }); - - it('it should save the description', () => { - flexTab.editNameSave.click(); - }); - - it('it should show the new description', () => { - flexTab.fourthSetting.getText().should.equal('DESCRIPTION EDITED'); - }); - }); - }); - - describe('Members tab usage:', () => { - describe('User muted', () => { - before(() => { - flexTab.operateFlexTab('members', true); - }); - - after(() => { - flexTab.operateFlexTab('members', false); - }); - - it('it should mute rocket cat', () => { - flexTab.muteUser(targetUser); - }); - }); - - describe('Owner added', () => { - before(() => { - if (Global.toastAlert.isVisible()) { - Global.dismissToast(); - } - flexTab.operateFlexTab('members', true); - }); - - after(() => { - if (Global.toastAlert.isVisible()) { - Global.dismissToast(); - } - flexTab.operateFlexTab('members', false); - }); - - it('it should set rocket cat as owner', () => { - flexTab.setUserOwner(targetUser); - }); - - it('it should dismiss the toast', () => { - if (Global.toastAlert.isVisible()) { - Global.dismissToast(); - } - }); - - it('it should the last message should be a subscription role added', () => { - mainContent.lastMessageRoleAdded.should('be.visible'); - }); - - it('it should show the target username in owner add message', () => { - mainContent.lastMessage.getText().should.have.string(targetUser); - }); - }); - - describe('Moderator added', () => { - before(() => { - if (Global.toastAlert.isVisible()) { - Global.dismissToast(); - } - flexTab.operateFlexTab('members', true); - }); - - after(() => { - if (Global.toastAlert.isVisible()) { - Global.dismissToast(); - } - flexTab.operateFlexTab('members', false); - }); - - it('it should set rocket cat as moderator', () => { - flexTab.setUserModerator(targetUser); - }); - - it('it should dismiss the toast', () => { - if (Global.toastAlert.isVisible()) { - Global.dismissToast(); - } - }); - - it('it should be that the last message is a subscription role added', () => { - mainContent.lastMessageRoleAdded.should('be.visible'); - }); - - it('it should show the target username in moderator add message', () => { - mainContent.lastMessage.getText().should.have.string(targetUser); - }); - }); - - // no channel quit at the moment - describe.skip('channel quit and enter', () => { - it('it should leave the channel', () => { - const channel = sideNav.getChannelFromList(`NAME-EDITED-${publicChannelName}`); - channel.click(); - channel.moveToObject(); - sideNav.channelLeave.click(); - }); - - it('it should show the modal alert popup', () => { - Global.modal.should('be.visible'); - Global.modalConfirm.should('be.visible'); - }); - - it('it should close the popup', () => { - Global.confirmPopup(); - }); - - it('it should not show the channel on the list', () => { - sideNav.getChannelFromList(`NAME-EDITED-${publicChannelName}`).should('not.exist'); - }); - - it('it should search and enter the channel with the spotlight', () => { - sideNav.searchChannel(`NAME-EDITED-${publicChannelName}`); - mainContent.joinChannelBtn.click(); - }); - - it('it should show the channel on the list', () => { - sideNav.getChannelFromList(`NAME-EDITED-${publicChannelName}`).should('be.visible'); - }); - }); - }); - }); -}); diff --git a/apps/meteor/tests/cypress/integration/10-user-preferences.spec.js b/apps/meteor/tests/cypress/integration/10-user-preferences.spec.js deleted file mode 100644 index f8461080c73f..000000000000 --- a/apps/meteor/tests/cypress/integration/10-user-preferences.spec.js +++ /dev/null @@ -1,148 +0,0 @@ -import flexTab from '../pageobjects/flex-tab.page'; -import mainContent from '../pageobjects/main-content.page'; -import sideNav from '../pageobjects/side-nav.page'; -import preferencesMainContent from '../pageobjects/preferences-main-content.page'; -import admin from '../pageobjects/administration.page'; -import { username, password, email, adminUsername, adminEmail, adminPassword } from '../../data/user.js'; -import { checkIfUserIsValid } from '../../data/checks'; - -describe('[User Preferences]', () => { - describe('default', () => { - before(() => { - checkIfUserIsValid(username, email, password); - sideNav.sidebarUserMenu.click(); - sideNav.account.click(); - }); - - describe('render:', () => { - it('it should show the preferences link', () => { - sideNav.preferences.should('be.visible'); - }); - - it('it should show the profile link', () => { - sideNav.profile.should('be.visible'); - }); - - it('it should click on the profile link', () => { - sideNav.profile.click(); - }); - - it('it should show the username input', () => { - preferencesMainContent.userNameTextInput.should('be.visible'); - }); - - it('it should show the real name input', () => { - preferencesMainContent.realNameTextInput.should('be.visible'); - }); - - it('it should show the email input', () => { - preferencesMainContent.emailTextInput.scrollIntoView().should('be.visible'); - }); - - it('it should show the password input', () => { - preferencesMainContent.passwordTextInput.scrollIntoView().should('be.visible'); - }); - - it('it should show the submit button', () => { - preferencesMainContent.submitBtn.should('be.visible').should('be.disabled'); - }); - }); - - describe('user info change:', () => { - it('it should click on the profile link', () => { - sideNav.profile.click(); - }); - - it('it should change the name field', () => { - preferencesMainContent.changeRealName(`EditedRealName${username}`); - }); - - it('it should change the Username field', () => { - preferencesMainContent.changeUsername(`EditedUserName${username}`); - }); - - it.skip('it should change the email field', () => { - preferencesMainContent.changeEmail(`EditedUserEmail${username}@gmail.com`); - }); - - it.skip('it should put the password in the modal input', () => { - preferencesMainContent.acceptPasswordOverlay(password); - }); - - it('it should save the settings', () => { - preferencesMainContent.saveChanges(); - }); - - it.skip('it should put the password in the modal input', () => { - preferencesMainContent.acceptPasswordOverlay(password); - }); - - it('it should close the preferences menu', () => { - sideNav.preferencesClose.click(); - sideNav.getChannelFromList('general').scrollIntoView().click(); - }); - - it('it should send a message to be tested', () => { - mainContent.sendMessage('HI'); - mainContent.waitForLastMessageEqualsText('HI'); - }); - - it.skip('it should be that the name on the last message is the edited one', () => { - mainContent.waitForLastMessageUserEqualsText(`EditedUserName${username}`); - mainContent.lastMessageUser.getText().should.equal(`EditedUserName${username}`); - }); - - it.skip('it should be that the user name on the members flex tab is the edited one', () => { - mainContent.lastMessageUser.click(); - flexTab.memberUserName.getText().should.equal(`EditedUserName${username}`); - }); - - it.skip('it should that the real name on the members flex tab is the edited one', () => { - flexTab.memberRealName.getText().should.equal(`EditedRealName${username}`); - }); - }); - }); - - describe('admin', () => { - describe.skip('user info change forbidden:', () => { - before(() => { - checkIfUserIsValid(adminUsername, adminEmail, adminPassword); - admin.open('admin/Accounts'); - admin.accountsRealNameChangeFalse.click(); - admin.adminSaveChanges(); - admin.accountsUsernameChangeFalse.click(); - admin.adminSaveChanges(); - admin.settingsSearch.type(''); - sideNav.preferencesClose.click(); - }); - - after(() => { - admin.open('admin/Accounts'); - admin.accountsRealNameChangeTrue.click(); - admin.adminSaveChanges(); - admin.accountsUsernameChangeTrue.click(); - admin.adminSaveChanges(); - admin.settingsSearch.type(''); - sideNav.preferencesClose.click(); - }); - - it('it should open profile', () => { - sideNav.accountMenu.click(); - sideNav.account.click(); - sideNav.profile.click(); - }); - - it('it should be that the name field is disabled', () => { - preferencesMainContent.realNameTextInputEnabled().should.be.false; - }); - - it('it should be that the Username field is disabled', () => { - preferencesMainContent.userNameTextInputEnabled().should.be.false; - }); - - it('it should close profile', () => { - sideNav.preferencesClose.click(); - }); - }); - }); -}); diff --git a/apps/meteor/tests/cypress/integration/11-admin.spec.js b/apps/meteor/tests/cypress/integration/11-admin.spec.js deleted file mode 100644 index edc654836036..000000000000 --- a/apps/meteor/tests/cypress/integration/11-admin.spec.js +++ /dev/null @@ -1,838 +0,0 @@ -import sideNav from '../pageobjects/side-nav.page'; -import flexTab from '../pageobjects/flex-tab.page'; -import admin from '../pageobjects/administration.page'; -import { checkIfUserIsValid } from '../../data/checks'; -import { adminUsername, adminEmail, adminPassword } from '../../data/user.js'; - -describe('[Administration]', () => { - before(() => { - checkIfUserIsValid(adminUsername, adminEmail, adminPassword); - }); - - after(() => { - sideNav.preferencesClose.click(); - }); - - describe('[Admin View]', () => { - before(() => { - sideNav.sidebarUserMenu.click(); - }); - - it('it should enter the admin view', () => { - sideNav.admin.click(); - }); - - describe('info:', () => { - before(() => { - admin.infoLink.click(); - }); - - it('should show Deployment box', () => { - admin.infoDeployment.should('be.visible'); - }); - - it('should show License box', () => { - admin.infoLicense.should('be.visible'); - }); - - it('should show Usage box', () => { - admin.infoUsage.should('be.visible'); - }); - - it('should show Federation box', () => { - admin.infoFederation.scrollIntoView().should('be.visible'); - }); - }); - - describe.skip('[Rooms]', () => { - before(() => { - admin.roomsLink.click(); - }); - - after(() => { - admin.infoLink.click(); - }); - - describe('render:', () => { - it('it should show the search form', () => { - admin.roomsSearchForm.should('be.visible'); - }); - - it('it should show the rooms Filter', () => { - admin.roomsFilter.should('be.visible'); - }); - - it('it should show the channel checkbox', () => { - admin.roomsChannelsCheckbox.should('be.visible'); - }); - - it('it should show the direct message checkbox', () => { - admin.roomsDirectCheckbox.should('be.visible'); - }); - - it('it should show the Private channel checkbox', () => { - admin.roomsPrivateCheckbox.should('be.visible'); - }); - - it('it should show the general channel', () => { - admin.roomsGeneralChannel.should('be.visible'); - }); - }); - - describe('filter text:', () => { - before(() => { - admin.roomsFilter.click(); - admin.roomsFilter.type('general'); - }); - - after(() => { - admin.roomsFilter.click(); - admin.roomsFilter.type(''); - }); - - it('it should show the general channel', () => { - admin.roomsGeneralChannel.should('be.visible'); - }); - }); - - describe('filter text with wrong channel:', () => { - before(() => { - admin.roomsFilter.click(); - admin.roomsFilter.type('something else'); - }); - - after(() => { - admin.roomsFilter.click(); - admin.roomsFilter.type(''); - }); - - it('it should not show the general channel', () => { - admin.roomsGeneralChannel.should('not.exist'); - }); - }); - - describe('filter checkbox:', () => { - let checkbox = 1; - before(() => { - admin.roomsFilter.type(''); - // add value triggers a key event that changes search±±±±±±±±± - admin.roomsFilter.addValue(' '); - admin.roomsGeneralChannel.waitForVisible(5000); - }); - beforeEach(() => { - switch (checkbox) { - case 1: - admin.roomsChannelsCheckbox.click(); - break; - case 2: - admin.roomsDirectCheckbox.click(); - break; - case 3: - admin.roomsPrivateCheckbox.click(); - break; - } - }); - - afterEach(() => { - switch (checkbox) { - case 1: - admin.roomsChannelsCheckbox.click(); - checkbox++; - break; - case 2: - admin.roomsDirectCheckbox.click(); - checkbox++; - break; - case 3: - admin.roomsPrivateCheckbox.click(); - break; - } - }); - - it('it should show the general channel', () => { - admin.roomsGeneralChannel.should('be.visible'); - }); - - it('it should not show the general channel', () => { - admin.roomsGeneralChannel.should('not.exist'); - }); - - it('it should not show the general channel', () => { - admin.roomsGeneralChannel.should('not.exist'); - }); - }); - }); - - describe.skip('[Users]', () => { - before(() => { - admin.usersLink.waitForVisible(5000); - admin.usersLink.click(); - admin.usersFilter.waitForVisible(5000); - }); - - after(() => { - admin.infoLink.click(); - }); - - it('it should show the search form', () => { - admin.usersFilter.should('be.visible'); - }); - - it('it should show rocket.cat', () => { - // it cant find the user if there is too many users - admin.usersRocketCat.should('be.visible'); - }); - - describe('filter text:', () => { - before(() => { - admin.usersFilter.click(); - admin.usersFilter.type('Rocket.Cat'); - }); - - after(() => { - admin.usersFilter.click(); - admin.usersFilter.type(''); - }); - - it('it should show rocket.cat', () => { - admin.usersRocketCat.waitForVisible(); - admin.usersRocketCat.should('be.visible'); - }); - }); - - describe('filter text with wrong user:', () => { - before(() => { - admin.usersFilter.click(); - admin.usersFilter.type('something else'); - }); - - after(() => { - admin.usersFilter.click(); - admin.usersFilter.type(''); - }); - - it('it should not show rocket.cat', () => { - admin.usersRocketCat.should('not.exist'); - }); - }); - - describe('[Flex Tab] ', () => { - describe('send invitation:', () => { - before(() => { - flexTab.usersSendInvitationTab.waitForVisible(5000); - flexTab.usersSendInvitationTab.click(); - flexTab.usersSendInvitationTextArea.waitForVisible(5000); - }); - - after(() => { - flexTab.usersSendInvitationTab.waitForVisible(5000); - flexTab.usersSendInvitationTab.click(); - flexTab.usersSendInvitationTextArea.waitForVisible(5000, true); - }); - - it('it should show the send invitation text area', () => { - flexTab.usersSendInvitationTextArea.should('be.visible'); - }); - - it('it should show the cancel button', () => { - flexTab.usersButtonCancel.should('be.visible'); - }); - - it('it should show the send button', () => { - flexTab.usersSendInvitationSend.should('be.visible'); - }); - }); - - describe('create user:', () => { - before(() => { - flexTab.usersAddUserTab.waitForVisible(5000); - flexTab.usersAddUserTab.click(); - flexTab.usersAddUserName.waitForVisible(5000); - }); - - after(() => { - flexTab.usersAddUserTab.waitForVisible(5000); - flexTab.usersAddUserTab.click(); - flexTab.usersAddUserName.waitForVisible(5000, true); - }); - - it('it should show the name field', () => { - flexTab.usersAddUserName.should('be.visible'); - }); - - it('it should show the username field', () => { - flexTab.usersAddUserUsername.should('be.visible'); - }); - - it('it should show the email field', () => { - flexTab.usersAddUserEmail.should('be.visible'); - }); - - it('it should show the verified checkbox', () => { - flexTab.usersAddUserVerifiedCheckbox.should('be.visible'); - }); - - it('it should show the password field', () => { - flexTab.usersAddUserPassword.should('be.visible'); - }); - - it('it should show the random password button', () => { - flexTab.usersAddUserRandomPassword.should('be.visible'); - }); - - it('it should show the require password change button', () => { - flexTab.usersAddUserChangePasswordCheckbox.should('be.visible'); - }); - - it('it should show the role dropdown', () => { - flexTab.usersAddUserRoleList.waitForVisible(5000); - flexTab.usersAddUserRoleList.should('be.visible'); - }); - - it('ít should show the add role button', () => { - flexTab.usersAddUserRoleButton.waitForVisible(5000); - flexTab.usersAddUserRoleButton.should('be.visible'); - }); - - it('it should show the join default channel checkbox', () => { - flexTab.usersAddUserDefaultChannelCheckbox.should('be.visible'); - }); - - it('it should show the send welcome checkbox', () => { - flexTab.usersAddUserWelcomeEmailCheckbox.should('be.visible'); - }); - - it('it should show the save button', () => { - flexTab.usersButtonSave.should('be.visible'); - }); - - it('it should show the cancel button', () => { - flexTab.usersButtonCancel.should('be.visible'); - }); - }); - }); - }); - - describe('[Roles]', () => { - before(() => { - admin.permissionsLink.click(); - }); - - after(() => { - admin.infoLink.click(); - }); - - it('it should show the permissions grid', () => { - admin.rolesPermissionGrid.should('be.visible'); - }); - - it('it should show the new role button', () => { - admin.rolesNewRolesButton.should('be.visible'); - }); - - it('it should show the admin link', () => { - admin.rolesAdmin.should('be.visible'); - }); - - describe('new role:', () => { - before(() => { - admin.rolesNewRolesButton.click(); - }); - - after(() => { - admin.rolesReturnLink.first().click(); - }); - - it('it should show the return to permissions', () => { - admin.rolesReturnLink.should('be.visible'); - }); - - it('it should show the new role name field', () => { - admin.rolesNewRoleName.should('be.visible'); - }); - - it('it should show the new role description field', () => { - admin.rolesNewRoleDesc.should('be.visible'); - }); - - it('it should show the new role scope', () => { - admin.rolesNewRoleScope.should('be.visible'); - }); - }); - - describe('admin role:', () => { - before(() => { - admin.rolesAdmin.click(); - admin.usersInRole.click(); - }); - - after(() => { - admin.rolesReturnLink.first().click(); - }); - - it('it should show internal admin', () => { - admin.usersInternalAdmin.should('be.visible'); - }); - }); - }); - - describe('[General Settings]', () => { - before(() => { - admin.settingsLink.click(); - admin.settingsSearch.type('general'); - admin.generalSettingsButton.click(); - }); - - describe('general:', () => { - it('it should show site url field', () => { - admin.generalSiteUrl.should('be.visible'); - }); - - it('it should change site url field', () => { - admin.generalSiteUrl.type('something'); - }); - - it('it should show the reset button', () => { - admin.generalSiteUrlReset.scrollIntoView().should('be.visible'); - }); - - it('it should click the reset button', () => { - admin.generalSiteUrlReset.click(); - }); - - it('it should that the site url field is different from the last input', () => { - admin.generalSiteUrl.should('not.contain', 'something'); - }); - - it('it should show site name field', () => { - admin.generalSiteName.should('be.visible'); - }); - - it('it should change site name field', () => { - admin.generalSiteName.type('something'); - }); - - it('it should show the reset button', () => { - admin.generalSiteNameReset.scrollIntoView().should('be.visible'); - }); - - it('it should click the reset button', () => { - admin.generalSiteNameReset.click(); - }); - - it('it should be that the name field is different from the last input', () => { - admin.generalSiteName.should('not.contain', 'something'); - }); - - it('it should show language field', () => { - admin.generalLanguage.should('be.visible'); - }); - - it('it should change the language ', () => { - admin.generalLanguage.click(); - cy.get('.rcx-option__content:contains("English")').scrollIntoView().click(); - }); - - it('it should show the reset button', () => { - admin.generalLanguageReset.scrollIntoView().should('be.visible'); - }); - - it('it should click the reset button', () => { - admin.generalLanguageReset.click(); - }); - - it('it should show invalid self signed certs toggle', () => { - admin.generalSelfSignedCerts.parent().should('be.visible'); - }); - - it('it should change the invalid self signed certs toggle', () => { - admin.generalSelfSignedCerts.parent().click(); - }); - - it('it should show the reset button', () => { - admin.generalSelfSignedCertsReset.should('be.visible'); - }); - - it('it should click the reset button', () => { - admin.generalSelfSignedCertsReset.click(); - }); - - it('it should show favorite rooms checkboxes', () => { - admin.generalFavoriteRoom.scrollIntoView().parent().should('be.visible'); - }); - - it('it should change the favorite rooms toggle', () => { - admin.generalFavoriteRoom.parent().click(); - }); - - it('it should show the reset button', () => { - admin.generalFavoriteRoomReset.scrollIntoView().should('be.visible'); - }); - - it('it should click the reset button', () => { - admin.generalFavoriteRoomReset.click(); - }); - - it('it should show open first channel field', () => { - admin.generalOpenFirstChannel.should('be.visible'); - }); - - it('it should change open first channel field', () => { - admin.generalOpenFirstChannel.type('something'); - }); - - it('it should show the reset button', () => { - admin.generalOpenFirstChannelReset.scrollIntoView().should('be.visible'); - }); - - it('it should click the reset button', () => { - admin.generalOpenFirstChannelReset.click(); - }); - - it('it should show cdn prefix field', () => { - admin.generalCdnPrefix.should('be.visible'); - }); - - it('it should change site url field', () => { - admin.generalCdnPrefix.type('something'); - }); - - it('it should show the reset button', () => { - admin.generalCdnPrefixReset.scrollIntoView().should('be.visible'); - }); - - it('it should click the reset button', () => { - admin.generalCdnPrefixReset.click(); - }); - - it('it should show the force SSL toggle', () => { - admin.generalForceSSL.parent().should('be.visible'); - }); - - it('it should change the force ssl toggle', () => { - admin.generalForceSSL.parent().click(); - }); - - it('it should show the reset button', () => { - admin.generalForceSSLReset.scrollIntoView().should('be.visible'); - }); - - it('it should click the reset button', () => { - admin.generalForceSSLReset.click(); - }); - - it('it should show google tag id field', () => { - admin.generalGoogleTagId.should('be.visible'); - }); - - it('it should change google tag id field', () => { - admin.generalGoogleTagId.type('something'); - }); - - it('it should show the reset button', () => { - admin.generalGoogleTagIdReset.scrollIntoView().should('be.visible'); - }); - - it('it should click the reset button', () => { - admin.generalGoogleTagIdReset.click(); - }); - - it.skip('it should show bugsnag key field', () => { - admin.generalBugsnagKey.should('be.visible'); - }); - - it.skip('it should change bugsnag key id field', () => { - admin.generalBugsnagKey.type('something'); - }); - - it.skip('it should show the reset button', () => { - admin.generalBugsnagKeyReset.scrollIntoView().should('be.visible'); - }); - - it.skip('it should click the reset button', () => { - admin.generalBugsnagKeyReset.click(); - }); - }); - - describe('iframe:', () => { - before(() => { - admin.generalSectionIframeIntegration.find('[aria-expanded="false"]').click(); - admin.generalIframeSend.parent().scrollIntoView(); - }); - - it('it should show iframe send toggle', () => { - admin.generalIframeSend.parent().should('be.visible'); - }); - - it('it should show send origin field', () => { - admin.generalIframeSendTargetOrigin.should('be.visible'); - }); - - it('it should show iframe send toggle', () => { - admin.generalIframeRecieve.parent().should('be.visible'); - }); - - it('it should show send origin field', () => { - admin.generalIframeRecieveOrigin.should('be.visible'); - }); - }); - - describe('notifications:', () => { - before(() => { - admin.generalSectionNotifications.find('[aria-expanded="false"]').click(); - admin.generalNotificationsMaxRoomMembers.scrollIntoView(); - }); - - it('it should show the max room members field', () => { - admin.generalNotificationsMaxRoomMembers.should('be.visible'); - }); - }); - - describe('rest api:', () => { - before(() => { - admin.generalSectionRestApi.find('[aria-expanded="false"]').click(); - admin.generalRestApiUserLimit.scrollIntoView(); - }); - - it('it should show the API user add limit field', () => { - admin.generalRestApiUserLimit.should('be.visible'); - }); - }); - - describe('reporting:', () => { - before(() => { - admin.generalSectionReporting.find('[aria-expanded="false"]').click(); - admin.generalReporting.parent().scrollIntoView(); - }); - - it('it should show the report to rocket.chat toggle', () => { - admin.generalReporting.parent().should('be.visible'); - }); - }); - - describe('stream cast:', () => { - before(() => { - admin.generalSectionStreamCast.find('[aria-expanded="false"]').click(); - admin.generalStreamCastAdress.scrollIntoView(); - }); - - it('it should show the stream cast adress field', () => { - admin.generalStreamCastAdress.should('be.visible'); - }); - }); - - describe('utf8:', () => { - before(() => { - admin.generalSectionUTF8.find('[aria-expanded="false"]').click(); - }); - - it('it should show the usernames utf8 regex field', () => { - admin.generalUTF8UsernamesRegex.scrollIntoView().should('be.visible'); - }); - - it('it should show the channels utf8 regex field', () => { - admin.generalUTF8ChannelsRegex.scrollIntoView().should('be.visible'); - }); - - it('it should show the utf8 names slug checkboxes', () => { - admin.generalUTF8NamesSlug.parent().should('be.visible'); - }); - }); - }); - - describe('[Accounts]', () => { - before(() => { - admin.groupSettingsPageBack.click(); - admin.settingsSearch.type('accounts'); - admin.accountsSettingsButton.click(); - }); - - describe('default user preferences', () => { - before(() => { - admin.accountsSectionDefaultUserPreferences.find('[aria-expanded="false"]').click(); - }); - - it('it should show the enable auto away field', () => { - admin.accountsEnableAutoAway.parent().scrollIntoView(); - admin.accountsEnableAutoAway.parent().should('be.visible'); - }); - - it('the enable auto away field value should be true', () => { - admin.accountsEnableAutoAway.find('input').should('be.checked'); - }); - - it('it should show the idle timeout limit field', () => { - admin.accountsidleTimeLimit.click(); - admin.accountsidleTimeLimit.should('be.visible'); - }); - - it('the idle timeout limit field value should be 300', () => { - admin.accountsidleTimeLimit.should('have.value', '300'); - }); - - it('it should show the desktop audio notifications select field', () => { - admin.accountsDesktopNotifications.scrollIntoView(); - admin.accountsDesktopNotifications.should('be.visible'); - }); - - it('the desktop audio notifications field value should be all', () => { - admin.accountsDesktopNotifications.find('.rcx-select__item').should('have.text', 'All messages'); - }); - - it('it should show the mobile notifications select field', () => { - admin.accountsMobileNotifications.scrollIntoView(); - admin.accountsMobileNotifications.should('be.visible'); - }); - - it('the mobile notifications field value should be all', () => { - admin.accountsMobileNotifications.find('.rcx-select__item').should('have.text', 'All messages'); - }); - - it('it should show the unread tray icon alert field', () => { - admin.accountsUnreadAlert.parent().scrollIntoView(); - admin.accountsUnreadAlert.parent().should('be.visible'); - }); - - it('the unread tray icon alert field value should be true', () => { - admin.accountsUnreadAlert.find('input').should('be.checked'); - }); - - it('it should show the use emojis field', () => { - admin.accountsUseEmojis.parent().scrollIntoView(); - admin.accountsUseEmojis.parent().should('be.visible'); - }); - - it('the use emojis field value should be true', () => { - admin.accountsUseEmojis.find('input').should('be.checked'); - }); - - it('it should show the convert ascii to emoji field', () => { - admin.accountsConvertAsciiEmoji.parent().scrollIntoView(); - admin.accountsConvertAsciiEmoji.parent().should('be.visible'); - }); - - it('the convert ascii to emoji field value should be true', () => { - admin.accountsConvertAsciiEmoji.find('input').should('be.checked'); - }); - - it('it should show the auto load images field', () => { - admin.accountsAutoImageLoad.parent().scrollIntoView(); - admin.accountsAutoImageLoad.parent().should('be.visible'); - }); - - it('the auto load images field value should be true', () => { - admin.accountsAutoImageLoad.find('input').should('be.checked'); - }); - - it('it should show the save mobile bandwidth field', () => { - admin.accountsSaveMobileBandwidth.parent().scrollIntoView(); - admin.accountsSaveMobileBandwidth.parent().should('be.visible'); - }); - - it('the save mobile bandwidth field value should be true', () => { - admin.accountsSaveMobileBandwidth.find('input').should('be.checked'); - }); - - it('it should show the collapse embedded media by default field', () => { - admin.accountsCollapseMediaByDefault.parent().scrollIntoView(); - admin.accountsCollapseMediaByDefault.parent().should('be.visible'); - }); - - it('the collapse embedded media by default field value should be false', () => { - admin.accountsCollapseMediaByDefault.should('not.be.checked'); - }); - - it('it should show the hide usernames field', () => { - admin.accountsHideUsernames.parent().scrollIntoView(); - admin.accountsHideUsernames.parent().should('be.visible'); - }); - - it('the hide usernames field value should be false', () => { - admin.accountsHideUsernames.should('not.be.checked'); - }); - - it('it should show the hide roles field', () => { - admin.accountsHideRoles.parent().scrollIntoView(); - admin.accountsHideRoles.parent().should('be.visible'); - }); - - it('the hide roles field value should be false', () => { - admin.accountsHideRoles.should('not.be.checked'); - }); - - it('it should show the hide right sidebar with click field', () => { - admin.accountsHideFlexTab.parent().scrollIntoView(); - admin.accountsHideFlexTab.parent().should('be.visible'); - }); - - it('the hide right sidebar with click field value should be false', () => { - admin.accountsHideFlexTab.should('not.be.checked'); - }); - - it('it should show the display avatars field', () => { - admin.accountsDisplayAvatars.parent().scrollIntoView(); - admin.accountsDisplayAvatars.parent().should('be.visible'); - }); - - it('the display avatars field value should be true', () => { - admin.accountsDisplayAvatars.find('input').should('be.checked'); - }); - - it('it should show the enter key behavior field', () => { - admin.accountsSendOnEnter.scrollIntoView(); - admin.accountsSendOnEnter.should('be.visible'); - }); - - it('the enter key behavior field value should be normal', () => { - admin.accountsSendOnEnter.find('.rcx-select__item').should('have.text', 'Normal mode (send with Enter)'); - }); - - it('it should show the messagebox view mode field', () => { - admin.accountsMessageViewMode.scrollIntoView(); - admin.accountsMessageViewMode.should('be.visible'); - }); - - // Not working on CI. May be because the setting is a select with a integer value - it.skip('the view mode field value should be 0', () => { - admin.accountsMessageViewMode.find('.rcx-select__item').should('have.text', 'Normal'); - }); - - it('it should show the offline email notification field', () => { - admin.accountsEmailNotificationMode.scrollIntoView(); - admin.accountsEmailNotificationMode.should('be.visible'); - }); - - it('the offline email notification field value should be all', () => { - admin.accountsEmailNotificationMode.find('.rcx-select__item').should('have.text', 'Every Mention/DM'); - }); - - it('it should show the new room notification field', () => { - admin.accountsNewRoomNotification.scrollIntoView(); - admin.accountsNewRoomNotification.should('be.visible'); - }); - - it('the new room notification field value should be door', () => { - admin.accountsNewRoomNotification.find('.rcx-select__item').should('have.text', 'Default'); - }); - - it('it should show the new message notification field', () => { - admin.accountsNewMessageNotification.scrollIntoView(); - admin.accountsNewMessageNotification.should('be.visible'); - }); - - it('the new message notification field value should be chime', () => { - admin.accountsNewMessageNotification.find('.rcx-select__item').should('have.text', 'Default'); - }); - - it('it should show the notification sound volume field', () => { - admin.accountsNotificationsSoundVolume.scrollIntoView(); - admin.accountsNotificationsSoundVolume.should('be.visible'); - }); - - it('the notification sound volume field value should be 100', () => { - admin.accountsNotificationsSoundVolume.should('have.value', '100'); - }); - }); - }); - }); -}); diff --git a/apps/meteor/tests/cypress/integration/12-settings.spec.js b/apps/meteor/tests/cypress/integration/12-settings.spec.js deleted file mode 100644 index 13018d558252..000000000000 --- a/apps/meteor/tests/cypress/integration/12-settings.spec.js +++ /dev/null @@ -1,505 +0,0 @@ -import supertest from 'supertest'; - -import loginPage from '../pageobjects/login.page'; -import flexTab from '../pageobjects/flex-tab.page'; -import mainContent from '../pageobjects/main-content.page'; -import sideNav from '../pageobjects/side-nav.page'; -import admin from '../pageobjects/administration.page'; -import { checkIfUserIsValid } from '../../data/checks'; -import { adminUsername, adminEmail, adminPassword, username, email, password, reason } from '../../data/user.js'; -import { wait } from '../../data/api-data'; - -const apiUrl = (typeof Cypress !== 'undefined' && Cypress.env('TEST_API_URL')) || process.env.TEST_API_URL || 'http://localhost:3000'; - -const request = supertest(apiUrl); -const prefix = '/api/v1/'; - -function api(path) { - return prefix + path; -} - -const credentials = { - 'X-Auth-Token': undefined, - 'X-User-Id': undefined, -}; - -const login = { - user: adminUsername, - password: adminPassword, -}; - -const settingUserPrefix = `setting${Date.now()}`; - -describe('[Api Settings Change]', () => { - before((done) => { - checkIfUserIsValid(username, email, password).then(() => { - request - .post(api('login')) - .send(login) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - credentials['X-Auth-Token'] = res.body.data.authToken; - credentials['X-User-Id'] = res.body.data.userId; - }) - .end(done); - }); - }); - - after(() => { - sideNav.preferencesClose.click(); - }); - - it('/login', () => { - expect(credentials).to.have.property('X-Auth-Token').with.lengthOf.at.least(1); - expect(credentials).to.have.property('X-User-Id').with.lengthOf.at.least(1); - }); - - describe('message edit:', () => { - it('it should change the message editing via api', (done) => { - request - .post(api('settings/Message_AllowEditing')) - .set(credentials) - .send({ value: false }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - - it.skip('it should not show the edit messages', () => { - // the page needs a refresh to show the changes in the client - mainContent.sendMessage('Message for Message Edit Block'); - mainContent.openMessageActionMenu(); - mainContent.messageEdit.should('not.exist'); - }); - - it('it should change the message editing via api', (done) => { - request - .post(api('settings/Message_AllowEditing')) - .set(credentials) - .send({ value: true }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - }); - - describe('message delete:', () => { - it('it should change the message deleting via api', (done) => { - request - .post(api('settings/Message_AllowDeleting')) - .set(credentials) - .send({ value: false }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - - it.skip('it should not show the delete messages', () => { - // the page needs a refresh to show the changes in the client - mainContent.sendMessage('Message for Message delete Block'); - mainContent.openMessageActionMenu(); - mainContent.messageDelete.should('not.exist'); - }); - - it('it should change the message deleting via api', (done) => { - request - .post(api('settings/Message_AllowDeleting')) - .set(credentials) - .send({ value: true }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - }); - - describe('block audio files:', () => { - it('it should change the message audio files via api', (done) => { - request - .post(api('settings/Message_AudioRecorderEnabled')) - .set(credentials) - .send({ value: false }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - - it.skip('it should not show the audo file button', () => { - // the page needs a refresh to show the changes in the client - mainContent.recordBtn.should('not.exist'); - }); - - it('it should change the message audio files via api', (done) => { - request - .post(api('settings/Message_AudioRecorderEnabled')) - .set(credentials) - .send({ value: true }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - }); - - describe('block video files:', () => { - it('it should change the message video files via api', (done) => { - request - .post(api('settings/Message_VideoRecorderEnabled')) - .set(credentials) - .send({ value: false }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - - it('it should change the message video files via api', (done) => { - request - .post(api('settings/Message_VideoRecorderEnabled')) - .set(credentials) - .send({ value: true }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - }); - - describe('bad words filter:', () => { - it('it should change the bad words filter via api', (done) => { - request - .post(api('settings/Message_AllowBadWordsFilter')) - .set(credentials) - .send({ value: true }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - - it('it should add bad words to the filter via api', (done) => { - request - .post(api('settings/Message_BadWordsFilterList')) - .set(credentials) - .send({ value: 'badword' }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - - it('it should send a bad word', () => { - sideNav.general.click(); - mainContent.setTextToInput('badword'); - mainContent.sendBtn.click(); - mainContent.waitForLastMessageEqualsText('*******'); - }); - - it('it should change the bad words filter via api', (done) => { - request - .post(api('settings/Message_AllowBadWordsFilter')) - .set(credentials) - .send({ value: false }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - }); - - describe('block message pin:', () => { - it('it should change the message pin via api', (done) => { - request - .post(api('settings/Message_AllowPinning')) - .set(credentials) - .send({ value: false }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - - it.skip('it should not show the pinned tab button', () => { - // the page needs a refresh to show the changes in the client - flexTab.pinnedTab.should('not.exist'); - }); - - it.skip('it should not show the pin message action', () => { - // the page needs a refresh to show the changes in the client - mainContent.sendMessage('Message for Message pin Block'); - mainContent.openMessageActionMenu(); - mainContent.pinMessage.should('not.exist'); - }); - - it('it should change the message pin via api', (done) => { - request - .post(api('settings/Message_AllowPinning')) - .set(credentials) - .send({ value: true }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - }); - - describe('block message star:', () => { - it('it should change the message star via api', (done) => { - request - .post(api('settings/Message_AllowStarring')) - .set(credentials) - .send({ value: false }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - - it.skip('it should not show the starred tab button', () => { - // the page needs a refresh to show the changes in the client - flexTab.starredTab.should('not.exist'); - }); - - it.skip('it should not show the star message action', () => { - // the page needs a refresh to show the changes in the client - mainContent.sendMessage('Message for Message pin Block'); - mainContent.openMessageActionMenu(); - mainContent.starMessage.should('not.exist'); - }); - - it('it should change the message star via api', (done) => { - request - .post(api('settings/Message_AllowStarring')) - .set(credentials) - .send({ value: true }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - }); - - describe.skip('block file upload:', () => { - it('it should change the file upload via api', (done) => { - request - .post(api('settings/FileUpload_Enabled')) - .set(credentials) - .send({ value: false }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - - it('it should not show file upload icon', () => { - mainContent.fileAttachment.should('not.exist'); - }); - - it('it should change the file upload via api', (done) => { - request - .post(api('settings/FileUpload_Enabled')) - .set(credentials) - .send({ value: true }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - }); - - describe.skip('profile changes:', () => { - before(() => { - sideNav.sidebarUserMenu.click(); - sideNav.account.click(); - }); - - after(() => { - sideNav.preferencesClose.click(); - sideNav.searchChannel('general'); - }); - describe('block profile change', () => { - it('it should change the allow user profile change via api', (done) => { - request - .post(api('settings/Accounts_AllowUserProfileChange')) - .set(credentials) - .send({ value: false }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - - it('it should not show profile link', () => { - sideNav.profile.should('not.exist'); - }); - - it('it should change the allow user profile change via api', (done) => { - request - .post(api('settings/Accounts_AllowUserProfileChange')) - .set(credentials) - .send({ value: true }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - }); - - describe('block avatar change', () => { - it('it should change the allow user avatar change via api', (done) => { - request - .post(api('settings/Accounts_AllowUserAvatarChange')) - .set(credentials) - .send({ value: false }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - - it('it should not show avatar link', () => { - sideNav.avatar.should('not.exist'); - }); - - it('it should change the allow user avatar change via api', (done) => { - request - .post(api('settings/Accounts_AllowUserAvatarChange')) - .set(credentials) - .send({ value: true }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - }); - }); - - describe('Manually Approve New Users:', () => { - before(() => { - sideNav.sidebarUserMenu.click(); - sideNav.logout.click(); - - // loginPage.open(); - }); - - it('it should change the Manually Approve New Users via api', (done) => { - request - .post(api('settings/Accounts_ManuallyApproveNewUsers')) - .set(credentials) - .send({ value: true }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - - it('register the user', () => { - loginPage.registerButton.click(); - loginPage.nameField.type(`${settingUserPrefix}${username}`); - loginPage.emailField.type(`${settingUserPrefix}${email}`); - loginPage.passwordField.type(password); - loginPage.confirmPasswordField.type(password); - loginPage.reasonField.type(reason); - - loginPage.submit(); - - loginPage.registrationSucceededCard.should('have.attr', 'data-i18n', 'Registration_Succeeded'); - loginPage.backToLoginButton.click(); - }); - - it('login as admin and go to users', () => { - checkIfUserIsValid(adminUsername, adminEmail, adminPassword); - sideNav.sidebarUserMenu.click(); - sideNav.admin.click(); - admin.usersLink.click(); - cy.get('h2:contains("Users")').should('be.visible'); - }); - - it('search the user', () => { - admin.usersFilter.click(); - admin.usersFilter.type(`${settingUserPrefix}${username}`); - cy.wait(1000); - }); - - it('opens the user', () => { - admin.getUserFromList(`${settingUserPrefix}${username}`).click().wait(100); - flexTab.usersView.should('be.visible'); - }); - - it('it should show the activate user btn', () => { - flexTab.moreActions.click().wait(200); - flexTab.usersActivate.should('be.visible'); - }); - - it('it should activate the user', () => { - flexTab.usersActivate.click().wait(200); - }); - - it('it should show the deactivate btn', () => { - flexTab.moreActions.click().wait(200); - flexTab.usersDeactivate.should('be.visible'); - }); - - it('it should change the Manually Approve New Users via api', (done) => { - request - .post(api('settings/Accounts_ManuallyApproveNewUsers')) - .set(credentials) - .send({ value: false }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(wait(done, 100)); - }); - }); -}); diff --git a/apps/meteor/tests/cypress/integration/13-permissions.spec.js b/apps/meteor/tests/cypress/integration/13-permissions.spec.js deleted file mode 100644 index 6d2a90e06e24..000000000000 --- a/apps/meteor/tests/cypress/integration/13-permissions.spec.js +++ /dev/null @@ -1,155 +0,0 @@ -import sideNav from '../pageobjects/side-nav.page'; -import flexTab from '../pageobjects/flex-tab.page'; -import admin from '../pageobjects/administration.page'; -import mainContent from '../pageobjects/main-content.page'; -import { checkIfUserIsValid } from '../../data/checks'; -import { username, email, password, adminUsername, adminEmail, adminPassword } from '../../data/user.js'; - -describe.skip('[Permissions]', () => { - before(() => { - checkIfUserIsValid(adminUsername, adminEmail, adminPassword); - sideNav.general.click(); - sideNav.accountMenu.click(); - sideNav.admin.click(); - }); - - after(() => { - checkIfUserIsValid(adminUsername, adminEmail, adminPassword); - sideNav.general.click(); - sideNav.accountMenu.click(); - sideNav.admin.click(); - admin.permissionsLink.click(); - - if (!admin.rolesUserCreateC.isSelected()) { - admin.rolesUserCreateC.scrollIntoView(); - admin.rolesUserCreateC.click(); - } - - if (!admin.rolesUserCreateD.isSelected()) { - admin.rolesUserCreateD.scrollIntoView(); - admin.rolesUserCreateD.click(); - } - - if (!admin.rolesUserCreateP.isSelected()) { - admin.rolesUserCreateP.scrollIntoView(); - admin.rolesUserCreateP.click(); - } - - if (!admin.rolesUserMentionAll.isSelected()) { - admin.rolesUserMentionAll.scrollIntoView(); - admin.rolesUserMentionAll.click(); - } - - if (!admin.rolesOwnerDeleteMessage.isSelected()) { - admin.rolesOwnerDeleteMessage.scrollIntoView(); - admin.rolesOwnerDeleteMessage.click(); - } - - if (!admin.rolesOwnerEditMessage.isSelected()) { - admin.rolesOwnerEditMessage.scrollIntoView(); - admin.rolesOwnerEditMessage.click(); - } - }); - - describe('user creation via admin view:', () => { - before(() => { - admin.usersLink.click(); - flexTab.usersAddUserTab.click(); - }); - - after(() => { - admin.infoLink.click(); - }); - - it('it should create a user', () => { - flexTab.usersAddUserName.type(`adminCreated${username}`); - flexTab.usersAddUserUsername.type(`adminCreated${username}`); - flexTab.usersAddUserEmail.type(`adminCreated${email}`); - flexTab.usersAddUserVerifiedCheckbox.click(); - flexTab.usersAddUserPassword.type(password); - flexTab.usersAddUserChangePasswordCheckbox.click(); - flexTab.addRole('user'); - flexTab.usersButtonSave.click(); - }); - - it('it should show the user in the list', () => { - admin.checkUserList(username).should.be.true; - }); - }); - - describe('change the permissions:', () => { - before(() => { - admin.permissionsLink.click(); - }); - - it('it should change the create c room permission', () => { - if (admin.rolesUserCreateC.isSelected()) { - admin.rolesUserCreateC.scrollIntoView(); - admin.rolesUserCreateC.click(); - } - }); - - it('it should change the create d room permission', () => { - if (admin.rolesUserCreateD.isSelected()) { - admin.rolesUserCreateD.scrollIntoView(); - admin.rolesUserCreateD.click(); - } - }); - - it('it should change the create p room permission', () => { - if (admin.rolesUserCreateP.isSelected()) { - admin.rolesUserCreateP.scrollIntoView(); - admin.rolesUserCreateP.click(); - } - }); - - it('it should change the mention all permission', () => { - if (admin.rolesUserMentionAll.isSelected()) { - admin.rolesUserMentionAll.scrollIntoView(); - admin.rolesUserMentionAll.click(); - } - }); - - it('it should change the delete message all permission for owners', () => { - if (admin.rolesOwnerDeleteMessage.isSelected()) { - admin.rolesOwnerDeleteMessage.scrollIntoView(); - admin.rolesOwnerDeleteMessage.click(); - } - }); - - it('it should change the edit message all permission for owners', () => { - if (admin.rolesOwnerEditMessage.isSelected()) { - admin.rolesOwnerEditMessage.scrollIntoView(); - admin.rolesOwnerEditMessage.click(); - } - }); - }); - - describe('test the permissions:', () => { - before(() => { - sideNav.preferencesClose.click(); - - checkIfUserIsValid(`adminCreated${username}`, `adminCreated${email}`, password); - }); - - it('it should not show the plus icon on toolbar ', () => { - sideNav.newChannelIcon.should('not.exist'); - }); - - it('it should go to general', () => { - sideNav.searchChannel('general'); - }); - - it('it should try to use @all and should be warned by rocket.cat ', () => { - mainContent.tryToMentionAll(); - }); - - it.skip('it should not be able to delete own message ', () => { - // waiting for changes in the delete-message permission - }); - - it.skip('it should not be able to edit own message ', () => { - // waiting for changes in the edit-message permission - }); - }); -}); diff --git a/apps/meteor/tests/cypress/integration/14-setting-permissions.spec.js b/apps/meteor/tests/cypress/integration/14-setting-permissions.spec.js deleted file mode 100644 index 07895380e3be..000000000000 --- a/apps/meteor/tests/cypress/integration/14-setting-permissions.spec.js +++ /dev/null @@ -1,118 +0,0 @@ -import { assert } from 'chai'; - -import { adminUsername, adminEmail, adminPassword, username, email, password } from '../../data/user.js'; -import admin from '../pageobjects/administration.page'; -import { checkIfUserIsValid } from '../../data/checks'; -import sideNav from '../pageobjects/side-nav.page'; - -function openAdminView() { - admin.open('admin/Layout'); -} - -function logoutRocketchat() { - // sideNav.sidebarUserMenu.waitForVisible(5000); - sideNav.sidebarUserMenu.click(); - // sideNav.logout.waitForVisible(5000); - sideNav.logout.click(); -} - -describe.skip('[Rocket.Chat Settings based permissions]', function () { - const newTitle = 'Testtitle'; - - describe('Give User Permissions', function () { - before(() => { - try { - // If the tests run as a suite,a user may already be logged-in - logoutRocketchat(); - } catch (e) { - // most possibly already logged off since started seperately => try to continue - } - checkIfUserIsValid(adminUsername, adminEmail, adminPassword); - openAdminView(); - admin.permissionsLink.click(); - }); - - it('Set permission for user to manage settings', function (done) { - if (!admin.rolesManageSettingsPermissions.isSelected()) { - admin.rolesManageSettingsPermissions.click(); - } - admin.rolesManageSettingsPermissions.isSelected().should.equal(true); - done(); - }); - - it('Set Permission for user to change titlepage title', function (done) { - admin.rolesSettingsTab.click(); - admin.rolesSettingsFindInput.type('Layout'); - if (!admin.rolesSettingLayoutTitle.isSelected()) { - admin.rolesSettingLayoutTitle.click(); - } - admin.rolesSettingLayoutTitle.isSelected().should.equal(true); - done(); - }); - - after(() => { - sideNav.preferencesClose.click(); - logoutRocketchat(); - }); - }); - - describe('Test new user setting permissions', function () { - before(() => { - try { - checkIfUserIsValid(username, email, password); - } catch (e) { - console.log(' User could not be logged in - trying again'); - checkIfUserIsValid(username, email, password); - } - openAdminView(); - }); - - it('Change titlepage title is allowed', function (done) { - admin.layoutLink.click(); - admin.generalLayoutTitle.type(newTitle); - browser.pause(2000); - admin.buttonSave.click(); - done(); - }); - - after(() => { - sideNav.preferencesClose.click(); - logoutRocketchat(); - }); - }); - - describe('Verify settings change and cleanup', function () { - before(() => { - console.log('Switching back to Admin'); - checkIfUserIsValid(adminUsername, adminEmail, adminPassword); - openAdminView(); - }); - - it('New settings value visible for admin as well', function (done) { - admin.layoutLink.click(); - admin.layoutButtonExpandContent.click(); - assert(admin.generalLayoutTitle.getValue() === newTitle, 'Title setting value not changed properly'); - browser.pause(2000); - admin.buttonSave.click(); - done(); - }); - - it('Cleanup permissions', function (done) { - admin.permissionsLink.click(); - - admin.rolesManageSettingsPermissions.click(); - admin.rolesManageSettingsPermissions.isSelected().should.equal(false); - - admin.rolesSettingsTab.click(); - admin.rolesSettingsFindInput.type('Layout'); - admin.rolesSettingLayoutTitle.click(); - admin.rolesSettingLayoutTitle.isSelected().should.equal(false); - done(); - }); - - after(() => { - sideNav.preferencesClose.click(); - logoutRocketchat(); - }); - }); -}); diff --git a/apps/meteor/tests/cypress/integration/15-message-popup.spec.js b/apps/meteor/tests/cypress/integration/15-message-popup.spec.js deleted file mode 100644 index 964bf3fe491d..000000000000 --- a/apps/meteor/tests/cypress/integration/15-message-popup.spec.js +++ /dev/null @@ -1,119 +0,0 @@ -import { adminEmail, adminPassword } from '../../data/user.js'; -import { api, request, getCredentials, credentials } from '../../data/api-data.js'; -import loginPage from '../pageobjects/login.page'; -import sideNav from '../pageobjects/side-nav.page'; -import mainContent from '../pageobjects/main-content.page'; - -const users = new Array(10) - .fill(null) - .map(() => `${Date.now()}.${Math.random().toString(36).slice(2)}`) - .map((uniqueId, i) => ({ - name: `User #${uniqueId}`, - username: `user.test.mentions.${uniqueId}`, - email: `user.test.mentions.${uniqueId}@rocket.chat`, - password: 'rocket.chat', - isMentionable: i % 2 === 0, - })); - -const createTestUser = async ({ email, name, username, password, isMentionable }) => { - await new Promise((done) => getCredentials(done)); - - await new Promise((done) => - request - .post(api('users.create')) - .set(credentials) - .send({ - email, - name, - username, - password, - active: true, - roles: ['user'], - joinDefaultChannels: true, - verified: true, - }) - .end(done), - ); - - if (isMentionable) { - const userCredentials = {}; - - await new Promise((done) => - request - .post(api('login')) - .send({ user: username, password }) - .expect((res) => { - userCredentials['X-Auth-Token'] = res.body.data.authToken; - userCredentials['X-User-Id'] = res.body.data.userId; - }) - .end(done), - ); - - await new Promise((done) => - request - .post(api('chat.postMessage')) - .set(userCredentials) - .send({ - channel: 'general', - text: 'Test', - }) - .end(done), - ); - } -}; - -describe('[Message Popup]', () => { - before(() => { - loginPage.open(); - - cy.logout(); - - cy.window().then({ timeout: 10000 }, async () => { - for (const user of users) { - await createTestUser(user); // eslint-disable-line no-await-in-loop - } - }); - - // loginPage.open(); - loginPage.login({ email: adminEmail, password: adminPassword }); - - sideNav.general.click(); - }); - - after(() => { - cy.logout(); - }); - - describe('test user mentions in message popup', () => { - it('should add "@" to the message input', () => { - mainContent.setTextToInput('@'); - }); - - it('should show the message popup', () => { - mainContent.messagePopUp.should('be.visible'); - }); - - it('should be that the message popup bar title is people', () => { - mainContent.messagePopUpTitle.should('contain', 'People'); - }); - - it('should show the message popup bar items', () => { - mainContent.messagePopUpItems.should('be.visible'); - }); - - const mentionableUsers = users.filter(({ isMentionable }) => isMentionable); - for (let i = 1; i <= 5; ++i) { - it(`should show mentionable user #${5 - i + 1} as message popup bar item #${i}`, () => { - mainContent.messagePopUpItems.find(`.popup-item:nth-child(${i}) strong`).should('contain', mentionableUsers[5 - i].username); - }); - } - - it('should show "all" as message popup bar item #6', () => { - mainContent.messagePopUpItems.find('.popup-item:nth-child(6) strong').should('contain', 'all'); - }); - - it('should show "here" as message popup bar item #7', () => { - mainContent.messagePopUpItems.find('.popup-item:nth-child(7) strong').should('contain', 'here'); - }); - }); -}); diff --git a/apps/meteor/tests/cypress/integration/16-discussion.spec.js b/apps/meteor/tests/cypress/integration/16-discussion.spec.js deleted file mode 100644 index 4276e01cd14e..000000000000 --- a/apps/meteor/tests/cypress/integration/16-discussion.spec.js +++ /dev/null @@ -1,62 +0,0 @@ -/* eslint-disable func-names, prefer-arrow-callback, no-var, space-before-function-paren, -quotes, prefer-template, no-undef, no-unused-vars*/ - -import mainContent from '../pageobjects/main-content.page'; -import sideNav from '../pageobjects/side-nav.page'; -import { sendEscape } from '../pageobjects/keyboard'; -import { discussion } from '../pageobjects/discussion.page'; -import { username, email, password } from '../../data/user.js'; -import { checkIfUserIsValid } from '../../data/checks'; - -const parentChannelName = 'unit-testing-' + Date.now(); -const discussionName = 'Lorem ipsum dolor sit amet'; -const message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'; - -describe('[Discussion]', function () { - before(function () { - checkIfUserIsValid(username, email, password); - sideNav.createChannel(parentChannelName, true, false); - cy.wait(1000); - }); - - describe('via creation screen', function () { - it('Create a discussion', function () { - discussion.createDiscussion(parentChannelName, discussionName, message); - cy.wait(1000); - }); - }); - - describe('from context menu', function () { - before(() => { - mainContent.sendMessage(message); - cy.wait(1000); - }); - - it('it should show a dialog for starting a discussion', () => { - cy.wait(1000); - mainContent.openMessageActionMenu(); - cy.waitUntil(() => { - return discussion.startDiscussionContextItem.then((el) => el.length); - }); - discussion.startDiscussionContextItem.click(); - discussion.saveDiscussionButton.should('be.enabled'); - discussion.saveDiscussionButton.click(); - cy.wait(1000); - }); - - it('it should have create a new room', function () { - mainContent.channelTitle.should('contain', message); - }); - - it('The message should be copied', function () { - cy.wait(1200); - mainContent.waitForLastMessageQuoteEqualsText(message); - }); - }); - - after(function () { - it('remove parent channel', () => { - discussion.deleteRoom(parentChannelName); - }); - }); -}); diff --git a/apps/meteor/tests/cypress/pageobjects/Page.js b/apps/meteor/tests/cypress/pageobjects/Page.js deleted file mode 100644 index e9c0b3f3dff3..000000000000 --- a/apps/meteor/tests/cypress/pageobjects/Page.js +++ /dev/null @@ -1,10 +0,0 @@ -class Page { - get body() { - return browser.element('body'); - } - - open(path) { - cy.visit(`/${path}`); - } -} -export default Page; diff --git a/apps/meteor/tests/cypress/pageobjects/administration.page.js b/apps/meteor/tests/cypress/pageobjects/administration.page.js deleted file mode 100644 index eea2a11dd0d2..000000000000 --- a/apps/meteor/tests/cypress/pageobjects/administration.page.js +++ /dev/null @@ -1,657 +0,0 @@ -import Page from './Page'; - -class Administration extends Page { - get flexNav() { - return browser.element('.flex-nav'); - } - - get flexNavContent() { - return browser.element('.flex-nav'); - } - - get settingsSearch() { - return browser.element('input[type=search]'); - } - - get settingsLink() { - return browser.element('.flex-nav [href="/admin/settings"]'); - } - - get groupSettingsPageBack() { - return browser.element('button[title="Back"]'); - } - - get layoutLink() { - return browser.element('.flex-nav [href="/admin/Layout"]'); - } - - get infoLink() { - return browser.element('.flex-nav [href="/admin/info"]'); - } - - get roomsLink() { - return browser.element('.flex-nav [href="/admin/rooms"]'); - } - - get usersLink() { - return browser.element('.flex-nav [href="/admin/users"]'); - } - - get accountsSettingsButton() { - return browser.element('[data-qa-id="Accounts"] button:contains("Open")'); - } - - get generalSettingsButton() { - return browser.element('[data-qa-id="General"] button:contains("Open")'); - } - - get permissionsLink() { - return browser.element('.flex-nav [href="/admin/permissions"]'); - } - - get customScriptBtn() { - return browser.element('.section:nth-of-type(6) .collapse'); - } - - get customScriptLoggedOutTextArea() { - return browser.element('.section:nth-of-type(6) .CodeMirror-scroll'); - } - - get customScriptLoggedInTextArea() { - return browser.element('.CodeMirror.cm-s-default:nth-of-type(2)'); - } - - get infoDeployment() { - return browser.element('[data-qa-id="deployment-card"]'); - } - - get infoLicense() { - return browser.element('[data-qa-id="license-card"]'); - } - - get infoUsage() { - return browser.element('[data-qa-id="usage-card"]'); - } - - get infoFederation() { - return browser.element('[data-qa-id="federation-card"]'); - } - - get roomsSearchForm() { - return browser.element('.content .search'); - } - - get roomsFilter() { - return browser.element('#rooms-filter'); - } - - get roomsChannelsCheckbox() { - return browser.element('label:nth-of-type(1) input[name="room-type"]'); - } - - get roomsDirectCheckbox() { - return browser.element('label:nth-of-type(2) input[name="room-type"]'); - } - - get roomsPrivateCheckbox() { - return browser.element('label:nth-of-type(3) input[name="room-type"]'); - } - - get roomsGeneralChannel() { - return browser.element('td=general'); - } - - get usersRocketCat() { - return browser.element('td=Rocket.Cat'); - } - - get usersInternalAdmin() { - return browser.element('.rcx-table__cell:contains("@rocketchat.internal.admin.test")'); - } - - get usersInRole() { - return browser.element('button:contains("Users in role")'); - } - - get usersFilter() { - return browser.element('input[placeholder="Search Users"]'); - } - - get rolesNewRolesButton() { - return browser.element('button[aria-label="New"]'); - } - - get rolesPermissionGrid() { - return browser.element('[role=tab]:contains("Permission")'); - } - - get rolesAdmin() { - return browser.element('.rcx-table__cell--header:contains("Admin")'); - } - - get rolesModerator() { - return browser.element('[title="Moderator"]'); - } - - get rolesOwner() { - return browser.element('[title="Owner"]'); - } - - get rolesReturnLink() { - return browser.element('[href="/admin/permissions"]'); - } - - get rolesNewRoleName() { - return browser.element('input[placeholder="Role"]'); - } - - get rolesNewRoleDesc() { - return browser.element('input[placeholder="Description"]'); - } - - get rolesNewRoleScope() { - return browser.element('label:contains("Scope")'); - } - - get rolesAddBtn() { - return browser.element('button.add'); - } - - get rolesRoomsSearchForm() { - return browser.element('.search [name="room"]'); - } - - get rolesSettingsFindInput() { - return browser.element('input#permissions-filter'); - } - - get rolesSettingsTab() { - return browser.element('button[data-value="settings"]'); - } - - get rolesPermissionsTab() { - return browser.element('button[data-value="permissions"]'); - } - - // permissions grids checkboxes - - get rolesUserCreateC() { - return browser.element('[name="perm[user][create-c]"]'); - } - - get rolesUserCreateP() { - return browser.element('[name="perm[user][create-p]"]'); - } - - get rolesUserCreateD() { - return browser.element('[name="perm[user][create-d]"]'); - } - - get rolesUserMentionAll() { - return browser.element('[name="perm[user][mention-all]"]'); - } - - get rolesUserPreviewC() { - return browser.element('[name="perm[user][preview-c-room]"]'); - } - - get rolesUserViewC() { - return browser.element('[name="perm[user][view-c-room]"]'); - } - - get rolesUserViewD() { - return browser.element('[name="perm[user][view-d-room]"]'); - } - - get rolesUserViewP() { - return browser.element('[name="perm[user][view-p-room]"]'); - } - - get rolesUserHistory() { - return browser.element('[name="perm[user][view-history]"]'); - } - - get rolesOwnerDeleteMessage() { - return browser.element('[name="perm[owner][delete-message]"]'); - } - - get rolesOwnerEditMessage() { - return browser.element('[name="perm[owner][edit-message]"]'); - } - - get rolesManageSettingsPermissions() { - return browser.element('[name="perm[user][manage-selected-settings]"]'); - } - - get rolesSettingLayoutTitle() { - return browser.element('[name="perm[user][change-setting-Layout_Home_Title]"'); - } - - get emojiFilter() { - return browser.element('#emoji-filter'); - } - - // settings - get buttonSave() { - return browser.element('button.save'); - } - - get generalSectionIframeIntegration() { - return browser.element('[data-qa-section="Iframe_Integration"]'); - } - - get generalSectionNotifications() { - return browser.element('[data-qa-section="Notifications"]'); - } - - get generalSectionRestApi() { - return browser.element('[data-qa-section="REST API"]'); - } - - get generalSectionReporting() { - return browser.element('[data-qa-section="Reporting"]'); - } - - get generalSectionStreamCast() { - return browser.element('[data-qa-section="Stream_Cast"]'); - } - - get generalSectionUTF8() { - return browser.element('[data-qa-section="UTF8"]'); - } - - get generalSiteUrl() { - return browser.element('[data-qa-setting-id="Site_Url"]'); - } - - get generalSiteUrlReset() { - return browser.element('[data-qa-reset-setting-id="Site_Url"]'); - } - - get generalSiteName() { - return browser.element('[data-qa-setting-id="Site_Name"]'); - } - - get generalSiteNameReset() { - return browser.element('[data-qa-reset-setting-id="Site_Name"]'); - } - - get generalLanguage() { - return browser.element('[data-qa-setting-id="Language"]'); - } - - get generalLanguagePtOption() { - return browser.element('[value="pt"]'); - } - - get generalLanguageReset() { - return browser.element('[data-qa-reset-setting-id="Language"]'); - } - - get generalSelfSignedCerts() { - return browser.element('[data-qa-setting-id="Allow_Invalid_SelfSigned_Certs"]'); - } - - get generalSelfSignedCertsReset() { - return browser.element('[data-qa-reset-setting-id="Allow_Invalid_SelfSigned_Certs"]'); - } - - get generalFavoriteRoom() { - return browser.element('[data-qa-setting-id="Favorite_Rooms"]'); - } - - get generalFavoriteRoomReset() { - return browser.element('[data-qa-reset-setting-id="Favorite_Rooms"]'); - } - - get generalOpenFirstChannel() { - return browser.element('[data-qa-setting-id="First_Channel_After_Login"]'); - } - - get generalOpenFirstChannelReset() { - return browser.element('[data-qa-reset-setting-id="First_Channel_After_Login"]'); - } - - get generalCdnPrefix() { - return browser.element('[data-qa-setting-id="CDN_PREFIX"]'); - } - - get generalCdnPrefixReset() { - return browser.element('[data-qa-reset-setting-id="CDN_PREFIX"]'); - } - - get generalForceSSL() { - return browser.element('[data-qa-setting-id="Force_SSL"]'); - } - - get generalForceSSLReset() { - return browser.element('[data-qa-reset-setting-id="Force_SSL"]'); - } - - get generalGoogleTagId() { - return browser.element('[data-qa-setting-id="GoogleTagManager_id"]'); - } - - get generalGoogleTagIdReset() { - return browser.element('[data-qa-reset-setting-id="GoogleTagManager_id"]'); - } - - get generalBugsnagKey() { - return browser.element('[data-qa-setting-id="Bugsnag_api_key"]'); - } - - get generalBugsnagKeyReset() { - return browser.element('[data-qa-reset-setting-id="Bugsnag_api_key"]'); - } - - get generalIframeSend() { - return browser.element('[data-qa-setting-id="Iframe_Integration_send_enable"]'); - } - - get generalIframeSendReset() { - return browser.element('[data-qa-reset-setting-id="Iframe_Integration_send_enable"]'); - } - - get generalIframeSendTargetOrigin() { - return browser.element('[data-qa-setting-id="Iframe_Integration_send_target_origin"]'); - } - - get generalIframeSendTargetOriginReset() { - return browser.element('[data-qa-reset-setting-id="Iframe_Integration_send_target_origin"]'); - } - - get generalIframeRecieve() { - return browser.element('[data-qa-setting-id="Iframe_Integration_receive_enable"]'); - } - - get generalIframeRecieveOrigin() { - return browser.element('[data-qa-setting-id="Iframe_Integration_receive_origin"]'); - } - - get generalIframeRecieveOriginReset() { - return browser.element('[data-qa-reset-setting-id="Iframe_Integration_receive_origin"]'); - } - - get generalNotificationsMaxRoomMembers() { - return browser.element('[data-qa-setting-id="Notifications_Max_Room_Members"]'); - } - - get generalNotificationsMaxRoomMembersReset() { - return browser.element('[data-qa-reset-setting-id="Notifications_Max_Room_Members"]'); - } - - get generalRestApiUserLimit() { - return browser.element('[data-qa-setting-id="API_User_Limit"]'); - } - - get generalRestApiUserLimitReset() { - return browser.element('[data-qa-reset-setting-id="API_User_Limit"]'); - } - - get generalReporting() { - return browser.element('[data-qa-setting-id="Statistics_reporting"]'); - } - - get generalReportingReset() { - return browser.element('[data-qa-reset-setting-id="Statistics_reporting"]'); - } - - get generalStreamCastAdress() { - return browser.element('[data-qa-setting-id="Stream_Cast_Address"]'); - } - - get generalStreamCastAdressReset() { - return browser.element('[data-qa-reset-setting-id="Stream_Cast_Address"]'); - } - - get generalUTF8UsernamesRegex() { - return browser.element('[data-qa-setting-id="UTF8_User_Names_Validation"]'); - } - - get generalUTF8ChannelsRegex() { - return browser.element('[data-qa-setting-id="UTF8_Channel_Names_Validation"]'); - } - - get generalUTF8RegexReset() { - return browser.element('[data-qa-reset-setting-id="UTF8_User_Names_Validation"]'); - } - - get generalUTF8NamesSlug() { - return browser.element('[data-qa-setting-id="UTF8_Names_Slugify"]'); - } - - get generalUTF8NamesSlugReset() { - return browser.element('[data-qa-reset-setting-id="UTF8_Names_Slugify"]'); - } - - get generalLayoutTitle() { - return browser.element('[data-qa-setting-id="Layout_Home_Title"]'); - } - - // accounts - get accountsSectionDefaultUserPreferences() { - return browser.element('[data-qa-section="Accounts_Default_User_Preferences"]'); - } - - get accountsEnableAutoAway() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_enableAutoAway"]'); - } - - get accountsEnableAutoAwayReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_enableAutoAway"]'); - } - - get accountsidleTimeLimit() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_idleTimeLimit"]'); - } - - get accountsidleTimeLimitReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_idleTimeLimit"]'); - } - - get accountsDesktopNotifications() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_desktopNotifications"]'); - } - - get accountsDesktopNotificationsReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_desktopNotifications"]'); - } - - get accountsMobileNotifications() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_pushNotifications"]'); - } - - get accountsMobileNotificationsReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_pushNotifications"]'); - } - - get accountsUnreadAlert() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_unreadAlert"]'); - } - - get accountsUnreadAlertReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_unreadAlert"]'); - } - - get accountsUseEmojis() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_useEmojis"]'); - } - - get accountsUseEmojisReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_useEmojis"]'); - } - - get accountsConvertAsciiEmoji() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_convertAsciiEmoji"]'); - } - - get accountsConvertAsciiEmojiReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_convertAsciiEmoji"]'); - } - - get accountsAutoImageLoad() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_autoImageLoad"]'); - } - - get accountsAutoImageLoadReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_autoImageLoad"]'); - } - - get accountsSaveMobileBandwidth() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_saveMobileBandwidth"]'); - } - - get accountsSaveMobileBandwidthReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_saveMobileBandwidth"]'); - } - - get accountsCollapseMediaByDefault() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_collapseMediaByDefault"]'); - } - - get accountsCollapseMediaByDefaultReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_collapseMediaByDefault"]'); - } - - get accountsHideUsernames() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_hideUsernames"]'); - } - - get accountsHideUsernamesReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_hideUsernames"]'); - } - - get accountsHideRoles() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_hideRoles"]'); - } - - get accountsHideRolesReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_hideRoles"]'); - } - - get accountsHideFlexTab() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_hideFlexTab"]'); - } - - get accountsHideFlexTabReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_hideFlexTab"]'); - } - - get DisplayAvatars() { - return browser.element('[data-qa-setting-id="displayAvatars"]'); - } - - get DisplayAvatarsReset() { - return browser.element('[data-qa-reset-setting-id="displayAvatars"]'); - } - - get accountsDisplayAvatars() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_displayAvatars"]'); - } - - get accountsMergeChannels() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_mergeChannels"]'); - } - - get accountsMergeChannelsReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_mergeChannels"]'); - } - - get accountsSendOnEnter() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_sendOnEnter"]'); - } - - get accountsSendOnEnterReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_sendOnEnter"]'); - } - - get accountsMessageViewMode() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_messageViewMode"]'); - } - - get accountsMessageViewModeReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_messageViewMode"]'); - } - - get accountsEmailNotificationMode() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_emailNotificationMode"]'); - } - - get accountsEmailNotificationModeReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_emailNotificationMode"]'); - } - - get accountsNewRoomNotification() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_newRoomNotification"]'); - } - - get accountsNewRoomNotificationReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_newRoomNotification"]'); - } - - get accountsNewMessageNotification() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_newMessageNotification"]'); - } - - get accountsNewMessageNotificationReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_newMessageNotification"]'); - } - - get accountsMuteFocusedConversations() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_muteFocusedConversations"]'); - } - - get accountsMuteFocusedConversationsReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_muteFocusedConversations"]'); - } - - get accountsNotificationsSoundVolume() { - return browser.element('[data-qa-setting-id="Accounts_Default_User_Preferences_notificationsSoundVolume"]'); - } - - get accountsNotificationsSoundVolumeReset() { - return browser.element('[data-qa-reset-setting-id="Accounts_Default_User_Preferences_notificationsSoundVolume"]'); - } - - get accountsRealNameChange() { - return browser.element('[data-qa-setting-id="Accounts_AllowRealNameChange"]'); - } - - get accountsUserStatusMessageChange() { - return browser.element('[data-qa-setting-id="Accounts_AllowUserStatusMessageChange"]'); - } - - get accountsUsernameChange() { - return browser.element('[data-qa-setting-id="Accounts_AllowUsernameChange"]'); - } - - get layoutButtonExpandContent() { - return browser.element('.section:nth-of-type(2) .rc-button.rc-button--nude'); - } - - checkUserList(user) { - const element = browser.element(`td=adminCreated${user}`); - element.waitForVisible(5000); - browser.pause(500); - const result = element.isVisible(); - if (Array.isArray(result)) { - return result[0]; - } - - return result; - } - - getUserFromList(user) { - return browser.element(`.rcx-table__cell:first-child:contains(${user}) figure`).should('be.visible'); - } - - adminSaveChanges() { - this.buttonSave.waitForVisible(5000); - browser.waitUntil(function () { - return browser.isEnabled('button.save'); - }, 5000); - this.buttonSave.click(); - } -} - -export default new Administration(); diff --git a/apps/meteor/tests/cypress/pageobjects/discussion.page.js b/apps/meteor/tests/cypress/pageobjects/discussion.page.js deleted file mode 100644 index 02131e9ad573..000000000000 --- a/apps/meteor/tests/cypress/pageobjects/discussion.page.js +++ /dev/null @@ -1,69 +0,0 @@ -import Page from './Page'; -import sideNav from './side-nav.page'; -import flexTab from './flex-tab.page'; -import global from './global'; - -class Discussion extends Page { - // Sidebar - this should actually be part of the sidebar-file - leaving it here for mergability - get newDiscussionButton() { - return browser.element('.menu-nav .js-create-discussion'); - } - - // Global - this should actually be part of the global-file - leaving it here for mergability - deleteRoom(roomName) { - if (roomName) { - sideNav.openChannel(roomName); - } - flexTab.operateFlexTab('info', true); - flexTab.deleteBtn.click(); - global.modal.waitForVisible(5000); - global.confirmPopup(); - } - - // Action Menu - get startDiscussionContextItem() { - return browser.element('[data-qa-id="start-discussion"][data-qa-type="message-action"]'); - } - - // Modal - get createDiscussionModal() { - return browser.element('#create-discussion'); - } - - get discussionName() { - return browser.element('.rcx-field:contains("Discussion name") input'); - } - - get discussionMessage() { - return browser.element('.rcx-field:contains("Your message") textarea'); - } - - get parentChannelName() { - return browser.element('.rcx-field:contains("Parent channel or group") input'); - } - - get saveDiscussionButton() { - return browser.element('button:contains("Create")'); - } - - // Sequences - - createDiscussion(parentChannelName, name, message) { - sideNav.newChannelBtnToolbar.click(); - sideNav.newDiscussionBtn.click(); - this.discussionName.type(name); - this.discussionMessage.type(message); - - this.parentChannelName.type(parentChannelName); - - browser.element('.rcx-options .rcx-option:first-child').click(); - - this.saveDiscussionButton.should('be.enabled'); - - this.saveDiscussionButton.click(); - } -} - -const discussion = new Discussion(); - -export { discussion }; diff --git a/apps/meteor/tests/cypress/pageobjects/flex-tab.page.js b/apps/meteor/tests/cypress/pageobjects/flex-tab.page.js deleted file mode 100644 index aa4c61368f36..000000000000 --- a/apps/meteor/tests/cypress/pageobjects/flex-tab.page.js +++ /dev/null @@ -1,478 +0,0 @@ -import Page from './Page'; -import Global from './global'; - -class FlexTab extends Page { - get headerMoreActions() { - return browser.element('.rcx-room-header .rcx-button-group__item:not(.hidden) .rcx-icon--name-kebab'); - } - - get moreActions() { - return browser.element('.rcx-button-group__item:not(.hidden) .rcx-icon--name-kebab'); - } - - get sendBtn() { - return browser.element('.rcx-vertical-bar .rc-message-box__icon.js-send'); - } - - get messageInput() { - return browser.element('.rcx-vertical-bar .js-input-message'); - } - - get threadTab() { - return browser.element('.rcx-room-header .rcx-button-group__item:not(.hidden) .rcx-icon--name-thread'); - } - - // Channel Info Tab - get channelTab() { - return browser.element('.rcx-room-header .rcx-button-group__item:not(.hidden) .rcx-icon--name-info-circled'); - } - - get channelSettings() { - return browser.element('aside > h3 > div > i.rcx-box--full.rcx-icon--name-info-circled'); - } - - get channelSettingName() { - return browser.element('.channel-settings .rc-user-info__name'); - } - - get archiveBtn() { - return browser.element('.clearfix:last-child .icon-pencil'); - } - - get archiveRadio() { - return browser.element('.editing'); - } - - get archiveSave() { - return browser.element('.save'); - } - - get editNameBtn() { - return browser.element('[data-edit="name"]'); - } - - get editTopicBtn() { - return browser.element('[data-edit="topic"]'); - } - - get editAnnouncementBtn() { - return browser.element('[data-edit="announcement"]'); - } - - get editDescriptionBtn() { - return browser.element('[data-edit="description"]'); - } - - get editNotificationBtn() { - return browser.element('[data-edit="desktopNotifications"]'); - } - - get editMobilePushBtn() { - return browser.element('[data-edit="mobilePushNotifications"]'); - } - - get editEmailNotificationBtn() { - return browser.element('[data-edit="emailNotifications"]'); - } - - get editUnreadAlertBtn() { - return browser.element('[data-edit="unreadAlert"]'); - } - - get editNameTextInput() { - return browser.element('.channel-settings input[name="name"]'); - } - - get editTopicTextInput() { - return browser.element('.channel-settings input[name="topic"]'); - } - - get editAnnouncementTextInput() { - return browser.element('.channel-settings input[name="announcement"]'); - } - - get editDescriptionTextInput() { - return browser.element('.channel-settings input[name="description"]'); - } - - get editNameSave() { - return browser.element('.channel-settings .save'); - } - - get deleteBtn() { - return browser.element('.channel-settings .js-delete'); - } - - // Members Tab - get membersTab() { - return browser.element('.rcx-room-header .rcx-button-group__item:not(.hidden) .rcx-icon--name-members'); - } - - get membersTabContent() { - return browser.element('aside > h3 > div > i.rcx-box--full.rcx-icon--name-members'); - } - - get userSearchBar() { - return browser.element('#user-add-search'); - } - - get removeUserBtn() { - return browser.element('.remove-user'); - } - - get setOwnerBtn() { - return browser.element('.set-owner'); - } - - get setModeratorBtn() { - return browser.element('.set-moderator'); - } - - get muteUserBtn() { - return browser.element('.mute-user'); - } - - get viewAllBtn() { - return browser.element('.button.back'); - } - - get startVideoCall() { - return browser.element('.start-video-call'); - } - - get startAudioCall() { - return browser.element('.start-audio-call'); - } - - get showAll() { - return browser.element('.see-all'); - } - - get membersUserInfo() { - return browser.element('.flex-tab-container .info'); - } - - get avatarImage() { - return browser.element('aside.rcx-vertical-bar .rcx-avatar'); - } - - get memberUserName() { - return browser.element('.info h3'); - } - - get memberRealName() { - return browser.element('.info p'); - } - - // Search Tab - get searchTab() { - return browser.element('.rcx-room-header .rcx-button-group__item:not(.hidden) .rcx-icon--name-magnifier'); - } - - get searchTabContent() { - return browser.element('.rocket-search-result'); - } - - get messageSearchBar() { - return browser.element('#message-search'); - } - - get searchResult() { - return browser.element('.new-day'); - } - - // Notifications Tab - get notificationsTab() { - return browser.element('.rcx-option__content:contains("Notifications Preferences")'); - } - - get notificationsSettings() { - return browser.element('aside > h3 > div > i.rcx-box--full.rcx-icon--name-bell'); - } - - // Files Tab - get filesTab() { - return browser.element('.rcx-room-header .rcx-button-group__item:not(.hidden) .rcx-icon--name-clip'); - } - - get fileItem() { - return browser.element('.uploaded-files-list ul:first-child'); - } - - get filesTabContent() { - return browser.element('aside > h3 > div > i.rcx-icon--name-attachment'); - } - - get fileDelete() { - return browser.element('.uploaded-files-list ul:first-child .file-delete'); - } - - get fileDownload() { - return browser.element('.uploaded-files-list ul:first-child .file-download'); - } - - get fileName() { - return browser.element('.uploaded-files-list ul:first-child .room-file-item'); - } - - // Mentions Tab - get mentionsTab() { - return browser.element('.rcx-option__content:contains("Mentions")'); - } - - get mentionsTabContent() { - return browser.element('aside > h3 > div > i.rcx-icon--name-at'); - } - - // Starred Tab - get starredTab() { - return browser.element('.rcx-option__content:contains("Starred Messages")'); - } - - get starredTabContent() { - return browser.element('aside > h3 > div > i.rcx-icon--name-star'); - } - - // Pinned Tab - get pinnedTab() { - return browser.element('.rcx-option__content:contains("Pinned Messages")'); - } - - get pinnedTabContent() { - return browser.element('aside > h3 > div > i.rcx-icon--name-pin'); - } - - get firstSetting() { - return browser.element('.clearfix li:nth-child(1) .current-setting'); - } - - get secondSetting() { - return browser.element('.clearfix li:nth-child(2) .current-setting'); - } - - get thirdSetting() { - return browser.element('.clearfix li:nth-child(3) .current-setting'); - } - - get fourthSetting() { - return browser.element('.clearfix li:nth-child(4) .current-setting'); - } - - // admin view flextab items - get usersSendInvitationTab() { - return browser.element('.tab-button:not(.hidden) .tab-button-icon--send'); - } - - get usersAddUserTab() { - return browser.element('.tab-button:not(.hidden) .tab-button-icon--plus'); - } - - get usersSendInvitationTextArea() { - return browser.element('#inviteEmails'); - } - - get usersButtonCancel() { - return browser.element('.button.cancel'); - } - - get usersSendInvitationSend() { - return browser.element('.button.send'); - } - - get usersButtonSave() { - return browser.element('.button.save'); - } - - get usersAddUserName() { - return browser.element('#name'); - } - - get usersAddUserUsername() { - return browser.element('#username'); - } - - get usersAddUserEmail() { - return browser.element('#email'); - } - - get usersAddUserRoleList() { - return browser.element('#roleSelect'); - } - - get usersAddUserPassword() { - return browser.element('#password'); - } - - get usersAddUserRoleButton() { - return browser.element('#addRole'); - } - - get usersAddUserVerifiedCheckbox() { - return browser.element('#verified'); - } - - get usersAddUserChangePasswordCheckbox() { - return browser.element('#changePassword'); - } - - get usersAddUserDefaultChannelCheckbox() { - return browser.element('#joinDefaultChannels'); - } - - get usersAddUserWelcomeEmailCheckbox() { - return browser.element('#sendWelcomeEmail'); - } - - get usersAddUserRandomPassword() { - return browser.element('#randomPassword'); - } - - get emojiNewAliases() { - return browser.element('#aliases'); - } - - get emojiNewImageInput() { - return browser.element('#image'); - } - - get usersView() { - return browser.element('.rcx-vertical-bar:contains("User Info")'); - } - - get usersActivate() { - return browser.element('.rcx-option__content:contains("Activate")'); - } - - get usersDeactivate() { - return browser.element('.rcx-option__content:contains("Deactivate")'); - } - - getUserEl(username) { - return browser.element(`.flex-tab button[title="${username}"] > p`); - } - - archiveChannel() { - this.archiveBtn.waitForVisible(); - this.archiveBtn.click(); - this.archiveRadio.waitForVisible(); - this.archiveRadio.click(); - this.archiveSave.click(); - } - - addPeopleToChannel(user) { - this.userSearchBar.waitForVisible(); - this.userSearchBar.setValue(user); - browser.waitForVisible('.-autocomplete-item', 5000); - browser.click('.-autocomplete-item'); - } - - removePeopleFromChannel(user) { - this.enterUserView(user); - this.removeUserBtn.waitForVisible(5000); - this.removeUserBtn.click(); - } - - addRole(role) { - this.usersAddUserRoleList.waitForVisible(5000); - this.usersAddUserRoleList.click(); - browser.waitForVisible(`option[value=${role}]`, 5000); - browser.click(`option[value=${role}]`); - this.usersAddUserRoleButton.waitForVisible(5000); - this.usersAddUserRoleButton.click(); - browser.waitForVisible(`.remove-role=${role}`); - } - - operateFlexTab(desiredTab, desiredState) { - // desiredState true=open false=closed - - const operate = (tab, panel, more) => { - this[panel].should(!desiredState ? 'be.visible' : 'not.exist'); - - if (more) { - this.headerMoreActions.click(); - } - - this[tab].click(); - - // The button "more" keeps the focus when popover is closed from a click - // on an item, need to click again to change the status to unselected and - // allow the next click to open the popover again - if (more) { - this.headerMoreActions.click(); - } - - this[panel].should(desiredState ? 'be.visible' : 'not.exist'); - }; - - const tabs = { - info() { - operate('channelTab', 'channelSettings'); - }, - - search() { - operate('searchTab', 'messageSearchBar'); - }, - - members() { - operate('membersTab', 'avatarImage'); - }, - - notifications() { - operate('notificationsTab', 'notificationsSettings', true); - }, - - files() { - operate('filesTab', 'filesTabContent'); - }, - - mentions() { - operate('mentionsTab', 'mentionsTabContent', true); - }, - - starred() { - operate('starredTab', 'starredTabContent', true); - }, - - pinned() { - operate('pinnedTab', 'pinnedTabContent', true); - }, - }; - - tabs[desiredTab].call(this); - } - - setUserOwner(user) { - this.enterUserView(user); - this.setOwnerBtn.waitForVisible(5000); - this.setOwnerBtn.click(); - this.viewAllBtn.click(); - browser.pause(100); - } - - setUserModerator(user) { - this.enterUserView(user); - this.setModeratorBtn.waitForVisible(); - this.setModeratorBtn.click(); - this.viewAllBtn.click(); - browser.pause(100); - } - - muteUser(user) { - this.enterUserView(user); - this.muteUserBtn.waitForVisible(5000); - this.muteUserBtn.click(); - Global.confirmPopup(); - this.viewAllBtn.click(); - browser.pause(100); - } - - enterUserView(user) { - if (!this.membersUserInfo.isVisible()) { - const userEl = this.getUserEl(user); - userEl.waitForVisible(); - userEl.click(); - } - } -} - -export default new FlexTab(); diff --git a/apps/meteor/tests/cypress/pageobjects/global.js b/apps/meteor/tests/cypress/pageobjects/global.js deleted file mode 100644 index ad4d8dbfb55c..000000000000 --- a/apps/meteor/tests/cypress/pageobjects/global.js +++ /dev/null @@ -1,59 +0,0 @@ -class Global { - // Modal - get modalOverlay() { - return browser.element('.rc-modal-wrapper'); - } - - get modal() { - return browser.element('.rcx-modal'); - } - - get modalConfirm() { - return browser.element('.rcx-modal .rcx-button--primary-danger'); - } - - get modalCancel() { - return browser.element('.rc-modal .js-modal'); - } - - get modalPasswordField() { - return browser.element('.rc-modal [type="password"]'); - } - - get modalFileName() { - return browser.element('.rc-modal #file-name'); - } - - get modalFileDescription() { - return browser.element('.rc-modal #file-description'); - } - - get modalFilePreview() { - return browser.element('.rc-modal .upload-preview-file'); - } - - get modalFileTitle() { - return browser.element('.rc-modal .upload-preview-title'); - } - - get toastAlert() { - return browser.element('.toast'); - } - - confirmPopup() { - this.modalConfirm.waitForVisible(5000); - browser.pause(500); - this.modalConfirm.click(); - this.modal.waitForVisible(5000, true); - } - - setWindowSize(width, height) { - cy.viewport(width, height); - } - - dismissToast() { - this.toastAlert.click(); - } -} - -export default new Global(); diff --git a/apps/meteor/tests/cypress/pageobjects/keyboard.js b/apps/meteor/tests/cypress/pageobjects/keyboard.js deleted file mode 100644 index 1fd552a838d6..000000000000 --- a/apps/meteor/tests/cypress/pageobjects/keyboard.js +++ /dev/null @@ -1,19 +0,0 @@ -const Keys = { - TAB: '\uE004', - ENTER: '\uE007', - ESCAPE: 'u\ue00c', -}; - -const sendEnter = function () { - browser.keys(Keys.ENTER); -}; - -const sendEscape = function () { - browser.keys(Keys.ESCAPE); -}; - -const sendTab = function () { - browser.keys(Keys.TAB); -}; - -export { sendEnter, sendEscape, sendTab }; diff --git a/apps/meteor/tests/cypress/pageobjects/login.page.js b/apps/meteor/tests/cypress/pageobjects/login.page.js deleted file mode 100644 index eac892d6ace8..000000000000 --- a/apps/meteor/tests/cypress/pageobjects/login.page.js +++ /dev/null @@ -1,124 +0,0 @@ -import Page from './Page'; - -class LoginPage extends Page { - get registerButton() { - return cy.get('button.register'); - } - - get forgotPasswordButton() { - return cy.get('.forgot-password'); - } - - get backToLoginButton() { - return cy.get('.back-to-login'); - } - - get submitButton() { - return cy.get('.login'); - } - - get emailOrUsernameField() { - return cy.get('[name=emailOrUsername]'); - } - - get nameField() { - return cy.get('[name=name]'); - } - - get usernameField() { - return cy.get('[name=username]'); - } - - get emailField() { - return cy.get('[name=email]'); - } - - get passwordField() { - return cy.get('[name=pass]'); - } - - get confirmPasswordField() { - return cy.get('[name=confirm-pass]'); - } - - get reasonField() { - return cy.get('[name=reason]'); - } - - get inputUsername() { - return cy.get('form#login-card input#username'); - } - - get emailOrUsernameInvalidText() { - return cy.get('[name=emailOrUsername]~.input-error'); - } - - get nameInvalidText() { - return cy.get('[name=name]~.input-error'); - } - - get emailInvalidText() { - return cy.get('[name=email]~.input-error'); - } - - get passwordInvalidText() { - return cy.get('[name=pass]~.input-error'); - } - - get confirmPasswordInvalidText() { - return cy.get('[name=confirm-pass]~.input-error'); - } - - get registrationSucceededCard() { - return cy.get('#login-card h2'); - } - - open() { - localStorage.clear(); - super.open(''); - cy.wait(1000); - } - - gotToRegister() { - this.registerButton.click(); - } - - gotToForgotPassword() { - this.forgotPasswordButton.click(); - } - - registerNewUser({ username, email, password }) { - this.nameField.type(username); - this.emailField.type(email); - this.passwordField.type(password); - this.confirmPasswordField.type(password); - - this.submit(); - } - - registerNewAdmin({ adminUsername, adminEmail, adminPassword }) { - this.nameField.type(adminUsername); - this.emailField.type(adminEmail); - this.passwordField.type(adminPassword); - this.confirmPasswordField.type(adminPassword); - - this.submit(); - } - - login({ email, password }) { - this.emailOrUsernameField.type(email); - this.passwordField.type(password); - - this.submit(); - } - - loginSucceded({ email, password }) { - this.login({ email, password }); - } - - submit() { - this.submitButton.click(); - } -} - -export default new LoginPage(); diff --git a/apps/meteor/tests/cypress/pageobjects/main-content.page.js b/apps/meteor/tests/cypress/pageobjects/main-content.page.js deleted file mode 100644 index b9ab46a52211..000000000000 --- a/apps/meteor/tests/cypress/pageobjects/main-content.page.js +++ /dev/null @@ -1,361 +0,0 @@ -import Page from './Page'; -import flexTab from './flex-tab.page'; - -class MainContent extends Page { - get mainContent() { - return browser.element('.main-content'); - } - - // Main Content Header (Channel Title Area) - get emptyFavoriteStar() { - return browser.element('.rcx-room-header .rcx-icon--name-star'); - } - - get favoriteStar() { - return browser.element('.rcx-room-header .rcx-icon--name-star-filled'); - } - - get channelTitle() { - return browser.element('.rcx-room-header'); - } - - // Main Content Footer (Message Input Area) - get messageInput() { - return browser.element('.js-input-message'); - } - - get sendBtn() { - return browser.element('.rc-message-box__icon.js-send'); - } - - get messageBoxActions() { - return browser.element('.rc-message-box__icon'); - } - - get recordBtn() { - return browser.element('.js-audio-message-record'); - } - - get emojiBtn() { - return browser.element('.rc-message-box__icon.emoji-picker-icon'); - } - - get messagePopUp() { - return browser.element('.message-popup'); - } - - get messagePopUpTitle() { - return browser.element('.message-popup-title'); - } - - get messagePopUpItems() { - return browser.element('.message-popup-items'); - } - - get messagePopUpFirstItem() { - return browser.element('.popup-item.selected'); - } - - get mentionAllPopUp() { - return browser.element('.popup-item[data-id="all"]'); - } - - get joinChannelBtn() { - return browser.element('.button.join'); - } - - // Messages - get lastMessageUser() { - return browser.element('[data-qa-type="message"]:last-child [data-username]'); - } - - get lastMessage() { - return browser.element('[data-qa-type="message"]:last-child'); - } - - get lastMessageDesc() { - return browser.element('[data-qa-type="message"]:last-child .body .attachment-description'); - } - - get lastMessageRoleAdded() { - return browser.element('[data-qa-type="message"]:last-child.subscription-role-added .body'); - } - - get beforeLastMessage() { - return browser.element('[data-qa-type="message"]:nth-last-child(2) [data-qa-type="message-body"]'); - } - - get lastMessageUserTag() { - return browser.element('[data-qa-type="message"]:last-child .role-tag'); - } - - get lastMessageImg() { - return browser.element('[data-qa-type="message"]:last-child .attachment-image img'); - } - - get lastMessageTextAttachment() { - return browser.element('[data-qa-type="message"]:last-child .attachment-text'); - } - - get beforeLastMessageQuote() { - return browser.element('[data-qa-type="message"]:nth-last-child(2)'); - } - - get lastMessageQuote() { - return browser.element('[data-qa-type="message"]:last-child'); - } - - get messageOptionsBtn() { - return browser.element('[data-qa-type="message"]:last-child [data-qa-type="message-action-menu"][data-qa-id="menu"]'); - } - - get messageOptionsBtns() { - return browser.element('[data-qa-type="message"]:last-child [data-qa-type="message-action-menu"]'); - } - - get messageActionMenu() { - return browser.element('[data-qa-type="message-action-menu-options"]'); - } - - get messageActionMenuBtns() { - return browser.element('[data-qa-type="message-action-menu-options"] [data-qa-type="message-action"]'); - } - - get messageReply() { - return browser.element('[data-qa-type="message-action"][data-qa-id="reply-in-thread"]'); - } - - get messageEdit() { - return browser.element('[data-qa-id="edit-message"][data-qa-type="message-action"]'); - } - - get messageDelete() { - return browser.element('[data-qa-id="delete-message"][data-qa-type="message-action"]'); - } - - get messagePermalink() { - return browser.element('[data-qa-id="permalink"][data-qa-type="message-action"]'); - } - - get messageCopy() { - return browser.element('[data-qa-id="copy"][data-qa-type="message-action"]'); - } - - get messageQuote() { - return browser.element('[data-qa-id="quote-message"][data-qa-type="message-action"]'); - } - - get messageStar() { - return browser.element('[data-qa-id="star-message"][data-qa-type="message-action"]'); - } - - get messageUnread() { - return browser.element('[data-qa-id="mark-message-as-unread"][data-qa-type="message-action"]'); - } - - get messageReplyInDM() { - return browser.element('[data-qa-id="reply-directly"][data-qa-type="message-action"]'); - } - - get messagePin() { - return browser.element('[data-qa-id="pin-message"][data-qa-type="message-action"]'); - } - - // Emojis - get emojiPickerMainScreen() { - return browser.element('.emoji-picker'); - } - - get emojiPickerPeopleIcon() { - return browser.element('.emoji-picker .icon-people'); - } - - get emojiPickerNatureIcon() { - return browser.element('.emoji-picker .icon-nature'); - } - - get emojiPickerFoodIcon() { - return browser.element('.emoji-picker .icon-food'); - } - - get emojiPickerActivityIcon() { - return browser.element('.emoji-picker .icon-activity'); - } - - get emojiPickerTravelIcon() { - return browser.element('.emoji-picker .icon-travel'); - } - - get emojiPickerObjectsIcon() { - return browser.element('.emoji-picker .icon-objects'); - } - - get emojiPickerSymbolsIcon() { - return browser.element('.emoji-picker .icon-symbols'); - } - - get emojiPickerFlagsIcon() { - return browser.element('.emoji-picker .icon-flags'); - } - - get emojiPickerModifierIcon() { - return browser.element('.emoji-picker .icon-symbols'); - } - - get emojiPickerChangeTone() { - return browser.element('.emoji-picker .change-tone'); - } - - get emojiPickerCustomIcon() { - return browser.element('.emoji-picker .icon-rocket'); - } - - get emojiPickerRecentIcon() { - return browser.element('.emoji-picker .icon-recent'); - } - - get emojiPickerFilter() { - return browser.element('.emoji-picker .js-emojipicker-search'); - } - - get emojiPickerEmojiContainer() { - return browser.element('.emoji-picker .emojis'); - } - - get emojiGrinning() { - return browser.element('.emoji-picker .emoji-grinning'); - } - - get emojiSmile() { - return browser.element('.emoji-picker .emoji-smile'); - } - - // Popover - get popoverWrapper() { - return browser.element('.rc-popover'); - } - - // Sends a message and wait for the message to equal the text sent - sendMessage(text) { - this.setTextToInput(text); - this.sendBtn.click(); - cy.wait(300); - this.lastMessage.should('be.visible'); - cy.get('[data-qa-type="message"]:last-child [data-qa-type="message-body"]').should('contain', text); - } - - // adds text to the input - addTextToInput(text) { - this.messageInput.type(text); - } - - // Clear and sets the text to the input - setTextToInput(text) { - cy.wait(400); - this.messageInput.clear(text); - if (text) { - this.messageInput.type(text); - } - } - - // uploads a file in the given filepath (url). - fileUpload(filePath) { - this.sendMessage('Prepare for the file'); - this.fileAttachment.chooseFile(filePath); - } - - waitForLastMessageEqualsText(text) { - cy.get('[data-qa-type="message"]:last-child [data-qa-type="message-body"]').should('contain', text); - } - - waitForLastMessageQuoteEqualsText(text) { - cy.get('[data-qa-type="message"]:last-child .rcx-attachment__details').should('contain', text); - } - - waitForLastMessageEqualsHtml(text) { - cy.wait(200); - cy.get('[data-qa-type="message"]:last-child [data-qa-type="message-body"]').should('contain.html', text); - } - - waitForLastMessageTextAttachmentEqualsText(text) { - return cy.get('[data-qa-type="message"]:last-child .rcx-attachment__details .rcx-box--with-inline-elements').should('contain', text); - } - - openMessageActionMenu() { - this.lastMessage.realHover().should('be.visible'); - - cy.waitUntil(() => { - return this.messageOptionsBtns.then((el) => el.length); - }); - - this.messageOptionsBtns.should('be.visible'); - - this.messageOptionsBtn.click().wait(100); - - cy.waitUntil(() => { - return this.messageActionMenuBtns.then((el) => el.length); - }); - - this.messageActionMenuBtns.should('be.visible'); - } - - closeMessageActionMenu() { - // Old popover closes only on click outside - cy.get('body').realHover({ position: 'topLeft' }).click(); - } - - setLanguageToEnglish() { - this.settingLanguageSelect.click(); - this.settingLanguageEnglish.click(); - this.settingSaveBtn.click(); - } - - tryToMentionAll() { - this.addTextToInput('@all'); - this.sendBtn.click(); - this.waitForLastMessageEqualsText('Notify all in this room is not allowed'); - } - - // Do one of the message actions, based on the "action" parameter inserted. - selectAction(action) { - switch (action) { - case 'edit': - this.messageEdit.click().wait(100); - this.messageInput.type('this message was edited'); - break; - case 'reply': - this.messageReply.click().wait(100); - flexTab.messageInput.type('this is a reply message'); - break; - case 'delete': - this.messageDelete.click(); - break; - case 'permalink': - this.messagePermalink.click(); - break; - case 'copy': - this.messageCopy.click(); - break; - case 'quote': - this.messageQuote.click().wait(100); - this.messageInput.type('this is a quote message'); - break; - case 'star': - this.messageStar.click(); - break; - case 'unread': - this.messageUnread.click(); - break; - case 'reaction': - this.messageReply.click(); - this.emojiPickerPeopleIcon.click(); - this.emojiGrinning.click(); - break; - case 'close': - this.messageClose.click(); - break; - } - } -} - -export default new MainContent(); diff --git a/apps/meteor/tests/cypress/pageobjects/preferences-main-content.page.js b/apps/meteor/tests/cypress/pageobjects/preferences-main-content.page.js deleted file mode 100644 index a5f32c9d48bc..000000000000 --- a/apps/meteor/tests/cypress/pageobjects/preferences-main-content.page.js +++ /dev/null @@ -1,71 +0,0 @@ -import Page from './Page'; - -class PreferencesMainContent extends Page { - get formTextInput() { - return browser.element('.rocket-form'); - } - - get realNameTextInput() { - return browser.element('label:contains("Name")').closest('.rcx-field').find('input'); - } - - get userNameTextInput() { - return browser.element('label:contains("Username")').closest('.rcx-field').find('input'); - } - - get emailTextInput() { - return browser.element('label:contains("Email")').closest('.rcx-field').find('input'); - } - - get passwordTextInput() { - return browser.element('label:contains("Password")').closest('.rcx-field').find('input'); - } - - get resendVerificationEmailBtn() { - return browser.element('#resend-verification-email'); - } - - get avatarFileInput() { - return browser.element('.avatar-file-input'); - } - - get useUploadedAvatar() { - return browser.element('.avatar-suggestion-item:nth-of-type(2) .select-service'); - } - - get submitBtn() { - return browser.element('button:contains("Save changes")'); - } - - realNameTextInputEnabled() { - return browser.isEnabled('input[name="realname"]'); - } - - userNameTextInputEnabled() { - return browser.isEnabled('input[name="username"]'); - } - - changeUsername(userName) { - this.userNameTextInput.clear().type(userName); - } - - changeRealName(realName) { - this.realNameTextInput.clear().type(realName); - } - - changeEmail(email) { - this.emailTextInput.clear().type(email); - } - - saveChanges() { - this.submitBtn.should('be.enabled'); - this.submitBtn.click(); - } - - changeAvatarUpload(url) { - this.avatarFileInput.chooseFile(url); - this.useUploadedAvatar.click(); - } -} - -export default new PreferencesMainContent(); diff --git a/apps/meteor/tests/cypress/pageobjects/settings.js b/apps/meteor/tests/cypress/pageobjects/settings.js deleted file mode 100644 index d62825e1b278..000000000000 --- a/apps/meteor/tests/cypress/pageobjects/settings.js +++ /dev/null @@ -1,42 +0,0 @@ -import supertest from 'supertest'; - -import { adminUsername, adminPassword } from '../../data/user.js'; - -const testUrl = (typeof Cypress !== 'undefined' && Cypress.env('TEST_API_URL')) || process.env.TEST_API_URL || 'http://localhost:3000'; - -const request = supertest(testUrl); -const prefix = '/api/v1/'; - -const login = { - user: adminUsername, - password: adminPassword, -}; - -function api(path) { - return prefix + path; -} - -export async function getSettingValue(name) { - let credentials = { - 'X-Auth-Token': undefined, - 'X-User-Id': undefined, - }; - - // login - const reponseLogin = await request.post(api('login')).send(login).expect('Content-Type', 'application/json').expect(200); - - credentials = { - 'X-Auth-Token': reponseLogin.body.data.authToken, - 'X-User-Id': reponseLogin.body.data.userId, - }; - - const responseGetSetting = await request - .get(api(`settings/${name}`)) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(200); - - await request.post(api('logout')).set(credentials).expect('Content-Type', 'application/json').expect(200); - - return responseGetSetting.body.value; -} diff --git a/apps/meteor/tests/cypress/pageobjects/setup-wizard.page.js b/apps/meteor/tests/cypress/pageobjects/setup-wizard.page.js deleted file mode 100644 index 70db966e757a..000000000000 --- a/apps/meteor/tests/cypress/pageobjects/setup-wizard.page.js +++ /dev/null @@ -1,66 +0,0 @@ -import Page from './Page'; -import { adminEmail, adminPassword } from '../../data/user'; - -class SetupWizard extends Page { - get nextStep() { - return browser.element('button:contains("Next"):visible'); - } - - get goToWorkspace() { - return browser.element('button:contains("Confirm")'); - } - - get organizationType() { - return browser.element('[name="organizationType"]'); - } - - get organizationName() { - return browser.element('[name="organizationName"]'); - } - - get industry() { - return browser.element('[name="organizationIndustry"]'); - } - - get size() { - return browser.element('[name="organizationSize"]'); - } - - get country() { - return browser.element('[name="country"]'); - } - - get registeredServer() { - return browser.element('input[name=email]'); - } - - get registerButton() { - return browser.element('button:contains("Register")'); - } - - get agreementField() { - return browser.element('input[name=agreement]').closest('.rcx-check-box'); - } - - get standaloneServer() { - return browser.element('button:contains("Continue as standalone")'); - } - - get standaloneConfirmText() { - return browser.element('.rcx-box:contains("Standalone Server Confirmation")'); - } - - login() { - cy.login(adminEmail, adminPassword); - } - - goNext() { - this.nextStep.click(); - } - - goToHome() { - this.goToWorkspace.click(); - } -} - -export default new SetupWizard(); diff --git a/apps/meteor/tests/cypress/pageobjects/side-nav.page.js b/apps/meteor/tests/cypress/pageobjects/side-nav.page.js deleted file mode 100644 index 2d7d3546821b..000000000000 --- a/apps/meteor/tests/cypress/pageobjects/side-nav.page.js +++ /dev/null @@ -1,222 +0,0 @@ -import Page from './Page'; -import mainContent from './main-content.page'; - -class SideNav extends Page { - // New channel - get channelType() { - return browser.element('#modal-root .rcx-field:contains("Private") .rcx-toggle-switch__fake'); - } - - get channelReadOnly() { - return browser.elements('.create-channel__switches .rc-switch__button').value[1]; - } - - get channelName() { - return browser.element('#modal-root [placeholder="Channel Name"]'); - } - - get saveChannelBtn() { - return browser.element('#modal-root button:contains("Create")'); - } - - // Account box - getPopOverContent() { - return browser.element('.rc-popover__content'); - } - - get accountBoxUserName() { - return browser.element('.sidebar__account-username'); - } - - get accountBoxUserAvatar() { - return browser.element('.sidebar__account .avatar-image'); - } - - get accountMenu() { - return browser.element('.sidebar__account'); - } - - get sidebarHeader() { - return browser.element('.sidebar__header'); - } - - get sidebarUserMenu() { - return browser.element('[data-qa="sidebar-avatar-button"]'); - } - - get sidebarMenu() { - return browser.element('.sidebar__toolbar-button-icon--menu'); - } - - get popOverContent() { - return browser.element('.rc-popover__content'); - } - - get popOverHideOption() { - return browser.element('.rcx-option__content:contains("Hide")'); - } - - get statusOnline() { - return browser.element('.rcx-box--with-inline-elements:contains("online")'); - } - - get statusAway() { - return browser.element('.rcx-box--with-inline-elements:contains("away")'); - } - - get statusBusy() { - return browser.element('.rcx-box--with-inline-elements:contains("busy")'); - } - - get statusOffline() { - return browser.element('.rcx-box--with-inline-elements:contains("offline")'); - } - - get account() { - return browser.element('.rcx-option__content:contains("My Account")'); - } - - get admin() { - return browser.element('.rcx-option__content:contains("Administration")'); - } - - get logout() { - return browser.element('.rcx-option__content:contains("Logout")'); - } - - get sideNavBar() { - return browser.element('.sidebar'); - } - - // Toolbar - get spotlightSearchIcon() { - return browser.element('[data-qa="sidebar-search"]'); - } - - get spotlightSearch() { - return browser.element('[data-qa="sidebar-search-input"]'); - } - - get spotlightSearchPopUp() { - return browser.element('[data-qa="sidebar-search-result"]'); - } - - get newChannelBtnToolbar() { - return browser.element('[data-qa="sidebar-create"]'); - } - - get newChannelBtn() { - return browser.element('.rcx-option__content:contains("Channel")'); - } - - get newDiscussionBtn() { - return browser.element('.rcx-option__content:contains("Discussion")'); - } - - get newChannelIcon() { - return browser.element('[data-qa="sidebar-create-channel"]'); - } - - // Rooms List - get general() { - return this.getChannelFromList('general'); - } - - get channelLeave() { - return browser.element('.leave-room'); - } - - get channelHoverIcon() { - return browser.element('.rooms-list > .wrapper > ul [title="general"] .icon-eye-off'); - } - - // Account - get preferences() { - return browser.element('[href="/account/preferences"]'); - } - - get profile() { - return browser.element('[href="/account/profile"]'); - } - - get avatar() { - return browser.element('[href="/changeavatar"]'); - } - - get preferencesClose() { - return browser.element('.flex-nav i.rcx-icon--name-cross'); - } - - get burgerBtn() { - return browser.element('.burger, [aria-label="Open_menu"]'); - } - - get sidebarWrap() { - return browser.element('.sidebar-wrap'); - } - - get firstSidebarItem() { - return browser.element('.sidebar-item'); - } - - get firstSidebarItemMenu() { - return browser.element('[data-qa=sidebar-avatar-button]'); - } - - get popoverOverlay() { - return browser.element('.rc-popover.rc-popover--sidebar-item'); - } - - // Opens a channel via rooms list - openChannel(channelName) { - cy.contains('[data-qa="sidebar-item-title"]', channelName).scrollIntoView().click(); - cy.get('.rcx-room-header').should('contain', channelName); - } - - // Opens a channel via spotlight search - searchChannel(channelName) { - this.spotlightSearch.should('be.visible'); - - // Should have focus automatically, but some times it's not happening - this.spotlightSearch.click(); - - this.spotlightSearch.should('have.focus'); - this.spotlightSearch.type(channelName); - cy.wait(500); - - cy.get( - `[data-qa="sidebar-search-result"] .rcx-sidebar-item--clickable:contains("${channelName}"), [data-qa="sidebar-search-result"] .rcx-sidebar-item[aria-label='${channelName}']`, - ).click(); - - cy.get('.rcx-room-header').should('contain', channelName); - } - - // Gets a channel from the rooms list - getChannelFromList(channelName) { - return cy.contains('[data-qa="sidebar-item-title"]', channelName).scrollIntoView(); - } - - createChannel(channelName, isPrivate /* isReadOnly*/) { - this.newChannelBtnToolbar.click(); - - this.newChannelBtn.click(); - - if (!isPrivate) { - this.channelType.click({ multiple: true }); - } - - this.channelName.type(channelName); - - this.saveChannelBtn.should('be.enabled'); - - // if (isReadOnly) { - // this.channelReadOnly.click(); - // } - - this.saveChannelBtn.click(); - this.channelType.should('not.exist'); - mainContent.messageInput.should('be.focused'); - } -} - -export default new SideNav(); diff --git a/apps/meteor/tests/cypress/plugins/index.js b/apps/meteor/tests/cypress/plugins/index.js deleted file mode 100644 index f2c89a867a7e..000000000000 --- a/apps/meteor/tests/cypress/plugins/index.js +++ /dev/null @@ -1,17 +0,0 @@ -// *********************************************************** -// This example plugins/index.js can be used to load plugins -// -// You can change the location of this file or turn off loading -// the plugins file with the 'pluginsFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/plugins-guide -// *********************************************************** - -// This function is called when a project is opened or re-opened (e.g. due to -// the project's config changing) - -module.exports = (/* on, config*/) => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config -}; diff --git a/apps/meteor/tests/cypress/support/commands.js b/apps/meteor/tests/cypress/support/commands.js deleted file mode 100644 index df259223850b..000000000000 --- a/apps/meteor/tests/cypress/support/commands.js +++ /dev/null @@ -1,62 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -import 'cypress-wait-until'; - -// -- This is a parent command -- -Cypress.Commands.add('login', (email, password) => - cy.window().then( - ({ Meteor }) => - new Promise((resolve) => { - Meteor.loginWithPassword(email, password, resolve); - }), - ), -); - -Cypress.Commands.add('logout', () => - cy.window().then( - ({ Meteor, FlowRouter }) => - new Promise((resolve) => { - Meteor.startup(() => { - setTimeout(() => { - const user = Meteor.user(); - if (!user) { - return resolve(); - } - - Meteor.logout(() => { - Meteor.call('logoutCleanUp', user); - FlowRouter.go('home'); - resolve(); - }); - }, 500); - }); - }), - ), -); - -// -// -- This is a child command -- -Cypress.Commands.add('getLocation', { prevSubject: 'element' }, (subject) => subject.get(0).getBoundingClientRect()); -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) - -global.browser = { - element(attr) { - return cy.get(attr); - }, -}; diff --git a/apps/meteor/tests/cypress/support/index.js b/apps/meteor/tests/cypress/support/index.js deleted file mode 100644 index 7d3c23ed6a40..000000000000 --- a/apps/meteor/tests/cypress/support/index.js +++ /dev/null @@ -1,47 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -import 'cypress-real-events/support'; - -// Import commands.js using ES2015 syntax: -import './commands'; - -// Cypress.Cookies.debug(true); - -Cypress.Cookies.defaults({ - preserve: ['rc_uid', 'rc_token'], -}); - -Cypress.LocalStorage.clear = function () {}; - -Cypress.on('uncaught:exception', (error) => { - console.error(error); - return false; -}); - -// Disable CSS animations -// Cypress.on('window:load', (win) => { -// win.document.querySelector('head').insertAdjacentHTML( -// 'beforeend', -// ` -// <style> -// * { transition-duration: 0.001s !important; animation-duration: 0.001s !important;} -// </style> -// `, -// ); -// }); - -// Alternatively you can use CommonJS syntax: -// require('./commands') diff --git a/apps/meteor/tests/data/api-data.js b/apps/meteor/tests/data/api-data.js index 92e45852cac4..701d2bbfac4f 100644 --- a/apps/meteor/tests/data/api-data.js +++ b/apps/meteor/tests/data/api-data.js @@ -4,7 +4,7 @@ import { publicChannelName, privateChannelName } from './channel.js'; import { roleNameUsers, roleNameSubscriptions, roleScopeUsers, roleScopeSubscriptions, roleDescription } from './role.js'; import { username, email, adminUsername, adminPassword } from './user.js'; -const apiUrl = (typeof Cypress !== 'undefined' && Cypress.env('TEST_API_URL')) || process.env.TEST_API_URL || 'http://localhost:3000'; +const apiUrl = process.env.TEST_API_URL || 'http://localhost:3000'; export const request = supertest(apiUrl); const prefix = '/api/v1/'; diff --git a/apps/meteor/tests/data/checks.js b/apps/meteor/tests/data/checks.js index 4895530d23a1..abfe171915ab 100644 --- a/apps/meteor/tests/data/checks.js +++ b/apps/meteor/tests/data/checks.js @@ -1,5 +1,3 @@ -import loginPage from '../cypress/pageobjects/login.page'; - export let publicChannelCreated = false; export let privateChannelCreated = false; export let directMessageCreated = false; @@ -15,28 +13,3 @@ export function setPrivateChannelCreated(status) { export function setDirectMessageCreated(status) { directMessageCreated = status; } - -export function checkIfUserIsValid(username, email, password) { - loginPage.open(); - - return cy.window().then(({ Meteor }) => { - const user = Meteor.user(); - if (!user || user.username !== username) { - return new Promise((resolve) => { - Meteor.loginWithPassword(email, password, (error) => { - if (error && error.error === 403) { - Meteor.logout(() => { - loginPage.gotToRegister(); - loginPage.registerNewUser({ username, email, password }); - cy.get('form#login-card input#username').should('be.visible'); - cy.get('#login-card button.login').click(); - resolve(); - }); - } else { - resolve(); - } - }); - }); - } - }); -} diff --git a/apps/meteor/tests/e2e/10-user-preferences.spec.ts b/apps/meteor/tests/e2e/10-user-preferences.spec.ts index b0cfa3801fd8..69df180bee2f 100644 --- a/apps/meteor/tests/e2e/10-user-preferences.spec.ts +++ b/apps/meteor/tests/e2e/10-user-preferences.spec.ts @@ -8,10 +8,14 @@ import FlexTab from './utils/pageobjects/FlexTab'; import PreferencesMainContent from './utils/pageobjects/PreferencesMainContent'; import { adminLogin } from './utils/mocks/userAndPasswordMock'; import { clearMessages } from './utils/helpers/clearMessages'; +import { verifyTestBaseUrl } from './utils/configs/verifyTestBaseUrl'; test.describe('[User Preferences]', () => { test.beforeAll(async () => { - await clearMessages(['GENERAL']); + const { isLocal } = verifyTestBaseUrl(); + if (isLocal) { + await clearMessages(['GENERAL']); + } }); test.describe('default', () => { let flexTab: FlexTab; diff --git a/apps/meteor/tests/e2e/16-discussion.spec.ts b/apps/meteor/tests/e2e/16-discussion.spec.ts new file mode 100644 index 000000000000..2f7f7698c70c --- /dev/null +++ b/apps/meteor/tests/e2e/16-discussion.spec.ts @@ -0,0 +1,55 @@ +import { test, Page } from '@playwright/test'; +import { faker } from '@faker-js/faker'; +import { v4 as uuid } from 'uuid'; + +import Discussion from './utils/pageobjects/Discussion'; +import LoginPage from './utils/pageobjects/LoginPage'; +import SideNav from './utils/pageobjects/SideNav'; +import MainContent from './utils/pageobjects/MainContent'; +import { adminLogin } from './utils/mocks/userAndPasswordMock'; + +test.describe('[Discussion]', () => { + let page: Page; + let loginPage: LoginPage; + let discussion: Discussion; + let sideNav: SideNav; + let mainContent: MainContent; + + let discussionName: string; + let message: string; + + test.beforeAll(async ({ browser }) => { + page = await browser.newPage(); + await page.goto('/'); + await page.waitForLoadState('load'); + loginPage = new LoginPage(page); + discussion = new Discussion(page); + sideNav = new SideNav(page); + mainContent = new MainContent(page); + + await loginPage.login(adminLogin); + }); + + test.describe('[Create discussion from screen]', () => { + test('expect discussion is created', async () => { + discussionName = faker.animal.type(); + message = faker.animal.type(); + await sideNav.newChannelBtnToolbar().click(); + await discussion.createDiscussion('public channel', discussionName, message); + }); + }); + + test.describe.skip('[Create discussion from context menu]', () => { + test.beforeAll(async () => { + message = faker.animal.type() + uuid(); + await sideNav.findForChat('public channel'); + await mainContent.sendMessage(message); + }); + + test('expect show a dialog for starting a discussion', async () => { + await mainContent.getPage().waitForLoadState('domcontentloaded', { timeout: 3000 }); + await mainContent.openMessageActionMenu(); + await discussion.createDiscussionInContext(message); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts b/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts new file mode 100644 index 000000000000..697a06ee9bd1 --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts @@ -0,0 +1,73 @@ +import { test, Page, expect } from '@playwright/test'; + +import LoginPage from './utils/pageobjects/LoginPage'; +import SideNav from './utils/pageobjects/SideNav'; +import Departments from './utils/pageobjects/Departments'; +import { adminLogin } from './utils/mocks/userAndPasswordMock'; + +test.describe('[Department]', () => { + let loginPage: LoginPage; + let sideNav: SideNav; + let departments: Departments; + let page: Page; + test.beforeAll(async ({ browser }) => { + page = await browser.newPage(); + const basePath = '/'; + + await page.goto(basePath); + loginPage = new LoginPage(page); + sideNav = new SideNav(page); + departments = new Departments(page); + + await loginPage.login(adminLogin); + await sideNav.sidebarUserMenu().click(); + await sideNav.omnichannel().click(); + }); + + test.describe('[Render]', async () => { + test.beforeEach(async () => { + await departments.departmentsLink.click(); + await departments.btnNewDepartment.click(); + }); + test('expect show all inputs', async () => { + await departments.getAddScreen(); + }); + }); + test.describe('[Actions]', async () => { + test.beforeEach(async () => { + await departments.departmentsLink.click(); + }); + test.describe('[Create and Edit]', async () => { + test.afterEach(async () => { + await departments.toastSuccess.click(); + }); + + test('expect new department is created', async () => { + await departments.btnNewDepartment.click(); + await departments.doAddDepartments(); + await expect(departments.departmentAdded).toBeVisible(); + }); + + test('expect department is edited', async () => { + await departments.departmentAdded.click(); + await departments.doEditDepartments(); + await expect(departments.departmentAdded).toHaveText('any_name_edit'); + }); + }); + test.describe('[Delete department]', () => { + test.beforeEach(async () => { + await departments.btnTableDeleteDepartment.click(); + }); + test('expect dont show dialog on cancel delete department', async () => { + await departments.btnModalCancelDeleteDepartment.click(); + await expect(departments.modalDepartment).not.toBeVisible(); + await expect(departments.departmentAdded).toBeVisible(); + }); + test('expect delete departments', async () => { + await departments.btnModalDeleteDepartment.click(); + await expect(departments.modalDepartment).not.toBeVisible(); + await expect(departments.departmentAdded).not.toBeVisible(); + }); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/utils/configs/verifyTestBaseUrl.ts b/apps/meteor/tests/e2e/utils/configs/verifyTestBaseUrl.ts new file mode 100644 index 000000000000..32e42d340566 --- /dev/null +++ b/apps/meteor/tests/e2e/utils/configs/verifyTestBaseUrl.ts @@ -0,0 +1,18 @@ +const getBaseUrl = (): string => { + if (process.env.BASE_URL) { + return process.env.BASE_URL; + } + if (process.env.ENTERPRISE) { + return 'http://localhost:4000'; + } + + return 'http://localhost:3000'; +}; + +export const verifyTestBaseUrl = (): { baseURL: string; isLocal: boolean } => { + const baseURL = getBaseUrl(); + return { + baseURL, + isLocal: baseURL.startsWith('http://localhost'), + }; +}; diff --git a/apps/meteor/tests/e2e/utils/pageobjects/Administration.ts b/apps/meteor/tests/e2e/utils/pageobjects/Administration.ts index d61ba6c522b8..3df941244c7a 100644 --- a/apps/meteor/tests/e2e/utils/pageobjects/Administration.ts +++ b/apps/meteor/tests/e2e/utils/pageobjects/Administration.ts @@ -675,10 +675,6 @@ export default class Administration extends BasePage { return this.getPage().locator('.section:nth-of-type(2) .rc-button.rc-button--nude'); } - public toastSuccess(): Locator { - return this.getPage().locator('.toast-success'); - } - public modalCancel(): Locator { return this.getPage().locator('//button[text()="Cancel"]'); } diff --git a/apps/meteor/tests/e2e/utils/pageobjects/BasePage.ts b/apps/meteor/tests/e2e/utils/pageobjects/BasePage.ts index 6b2f85211cf1..d920c3fcc5cd 100644 --- a/apps/meteor/tests/e2e/utils/pageobjects/BasePage.ts +++ b/apps/meteor/tests/e2e/utils/pageobjects/BasePage.ts @@ -1,4 +1,4 @@ -import { Page } from '@playwright/test'; +import { Page, Locator } from '@playwright/test'; class BasePage { private page: Page; @@ -22,5 +22,9 @@ class BasePage { public async keyboardPress(key: string): Promise<void> { await this.getPage().keyboard.press(key); } + + get toastSuccess(): Locator { + return this.getPage().locator('#toast-container'); + } } export default BasePage; diff --git a/apps/meteor/tests/e2e/utils/pageobjects/Departments.ts b/apps/meteor/tests/e2e/utils/pageobjects/Departments.ts new file mode 100644 index 000000000000..09b6af0c147c --- /dev/null +++ b/apps/meteor/tests/e2e/utils/pageobjects/Departments.ts @@ -0,0 +1,130 @@ +import { Locator, expect } from '@playwright/test'; + +import BasePage from './BasePage'; + +export default class Departments extends BasePage { + get departmentsLink(): Locator { + return this.getPage().locator('a[href="omnichannel/departments"]'); + } + + get btnNewDepartment(): Locator { + return this.getPage().locator('button.rcx-button >> text="New"'); + } + + get btnSaveDepartment(): Locator { + return this.getPage().locator('button.rcx-button--primary.rcx-button >> text="Save"'); + } + + get btnBack(): Locator { + return this.getPage().locator('button.rcx-button >> text="Back"'); + } + + get enabledToggle(): Locator { + // temporary selector + return this.getPage().locator('[data-qa="DepartmentEditToggle-Enabled"] span label'); + } + + get nameInput(): Locator { + return this.getPage().locator('[data-qa="DepartmentEditTextInput-Name"]'); + } + + get descriptionInput(): Locator { + return this.getPage().locator('[data-qa="DepartmentEditTextInput-Description"]'); + } + + get showOnRegistrationPage(): Locator { + return this.getPage().locator('[data-qa="DepartmentEditToggle-ShowOnRegistrationPage"] span label'); + } + + get emailInput(): Locator { + return this.getPage().locator('[data-qa="DepartmentEditTextInput-Email"]'); + } + + get showOnOfflinePageToggle(): Locator { + return this.getPage().locator('[data-qa="DepartmentEditToggle-ShowOnOfflinePage"] span label'); + } + + get selectLiveChatDepartmentOfflineMessageToChannel(): Locator { + return this.getPage().locator('[data-qa="DepartmentSelect-LivechatDepartmentOfflineMessageToChannel"]'); + } + + get requestTagBeforeClosingChatToggle(): Locator { + return this.getPage().locator('[data-qa="DiscussionToggle-RequestTagBeforeCLosingChat"] span label'); + } + + get selectAgentsTable(): Locator { + return this.getPage().locator('[data-qa="DepartmentSelect-AgentsTable"]'); + } + + get btnAddAgent(): Locator { + return this.getPage().locator('button.rcx-button--primary.rcx-button >> text="Add"'); + } + + public virtuosoOptions(option: string): Locator { + return this.getPage().locator(`[data-test-id="virtuoso-scroller"] .rcx-option >> text="${option}"`); + } + + get departmentAdded(): Locator { + return this.getPage().locator('table tr:first-child td:first-child '); + } + + get btnTableDeleteDepartment(): Locator { + return this.getPage().locator('table tr:first-child td:nth-child(6) button'); + } + + get btnModalCancelDeleteDepartment(): Locator { + return this.getPage().locator('#modal-root .rcx-modal .rcx-modal__footer .rcx-button--ghost'); + } + + get btnModalDeleteDepartment(): Locator { + return this.getPage().locator('#modal-root .rcx-modal .rcx-modal__footer .rcx-button--primary-danger'); + } + + get modalDepartment(): Locator { + return this.getPage().locator('#modal-root'); + } + + public async getAddScreen(): Promise<void> { + const textInputs = [this.nameInput, this.descriptionInput, this.emailInput]; + const toggleButtons = [this.enabledToggle, this.showOnOfflinePageToggle, this.requestTagBeforeClosingChatToggle]; + const selects = [this.selectLiveChatDepartmentOfflineMessageToChannel, this.selectAgentsTable]; + const actionsButtons = [this.btnSaveDepartment, this.btnBack, this.btnAddAgent]; + const addScreenSelectors = [...textInputs, ...toggleButtons, ...actionsButtons, ...selects]; + + await Promise.all(addScreenSelectors.map((addScreenSelector) => expect(addScreenSelector).toBeVisible())); + } + + public async doAddAgent(): Promise<void> { + await this.enabledToggle.click(); + await this.nameInput.type('rocket.cat'); + } + + public async doAddDepartments(): Promise<void> { + await this.enabledToggle.click(); + await this.nameInput.type('any_name'); + await this.descriptionInput.type('any_description'); + await this.showOnOfflinePageToggle.click(); + await this.emailInput.type('any_email@mail.com'); + await this.showOnRegistrationPage.click(); + await this.selectLiveChatDepartmentOfflineMessageToChannel.click(); + await this.virtuosoOptions('general').click(); + await this.selectAgentsTable.click(); + await this.btnSaveDepartment.click(); + } + + public async doEditDepartments(): Promise<void> { + await this.enabledToggle.click(); + await this.nameInput.click({ clickCount: 3 }); + await this.keyboardPress('Backspace'); + await this.nameInput.fill('any_name_edit'); + await this.descriptionInput.click({ clickCount: 3 }); + await this.keyboardPress('Backspace'); + await this.descriptionInput.fill('any_description_edited'); + await this.btnSaveDepartment.click(); + } + + public async doBackToPrincipalScreen(): Promise<void> { + await this.departmentAdded.click(); + await this.btnBack.click(); + } +} diff --git a/apps/meteor/tests/e2e/utils/pageobjects/Discussion.ts b/apps/meteor/tests/e2e/utils/pageobjects/Discussion.ts new file mode 100644 index 000000000000..afe9a22ab328 --- /dev/null +++ b/apps/meteor/tests/e2e/utils/pageobjects/Discussion.ts @@ -0,0 +1,54 @@ +import { Locator, expect } from '@playwright/test'; + +import BasePage from './BasePage'; + +export default class Discussion extends BasePage { + public startDiscussionContextItem(): Locator { + return this.getPage().locator('[data-qa-id="start-discussion"][data-qa-type="message-action"]'); + } + + public createDiscussionBtn(): Locator { + return this.getPage().locator('.rcx-option__content >> text="Discussion"'); + } + + public channelName(): Locator { + return this.getPage().locator('.rcx-input-box--undecorated.rcx-input-box').first(); + } + + public discussionName(): Locator { + return this.getPage().locator('[placeholder="A meaningful name for the discussion room"]'); + } + + public discussionMessage(): Locator { + return this.getPage().locator('textarea.rcx-input-box'); + } + + public buttonCreateDiscussion(): Locator { + return this.getPage().locator('button.rcx-button--primary.rcx-button >> text="Create"'); + } + + public discussionCreated(discussionName: string): Locator { + return this.getPage().locator(`[data-qa="sidebar-item-title"] >> text='${discussionName}'`); + } + + async createDiscussion(channelName: string, discussionName: string, message: string): Promise<void> { + await this.createDiscussionBtn().click(); + await this.channelName().type(channelName); + await this.getPage().keyboard.press('Enter'); + + await this.discussionName().type(discussionName); + await this.discussionMessage().type(message); + + await this.buttonCreateDiscussion().click(); + + await expect(this.discussionCreated(discussionName)).toBeVisible(); + } + + async createDiscussionInContext(message: string): Promise<void> { + await this.startDiscussionContextItem().waitFor(); + await this.getPage().pause(); + await this.startDiscussionContextItem().click(); + await this.buttonCreateDiscussion().click(); + await expect(this.discussionCreated(message)).toBeVisible(); + } +} diff --git a/apps/meteor/tests/e2e/utils/pageobjects/Global.ts b/apps/meteor/tests/e2e/utils/pageobjects/Global.ts index 64a3c4a29779..f6750773844c 100644 --- a/apps/meteor/tests/e2e/utils/pageobjects/Global.ts +++ b/apps/meteor/tests/e2e/utils/pageobjects/Global.ts @@ -50,10 +50,6 @@ class Global extends BasePage { await this.modalConfirm().click(); } - // public async setWindowSize(width, height): Promise<void> { - // cy.viewport(width, height); - // } - // public async dismissToast(): Promise<void> { await this.toastAlert().click(); } diff --git a/apps/meteor/tests/e2e/utils/pageobjects/MainContent.ts b/apps/meteor/tests/e2e/utils/pageobjects/MainContent.ts index 5008609aaf8f..be835d133c45 100644 --- a/apps/meteor/tests/e2e/utils/pageobjects/MainContent.ts +++ b/apps/meteor/tests/e2e/utils/pageobjects/MainContent.ts @@ -412,10 +412,6 @@ export default class MainContent extends BasePage { await this.modalDeleteMessageButton().click(); } - public toastSuccess(): Locator { - return this.getPage().locator('.toast-message'); - } - public getQuotedMessage(): Locator { return this.getPage().locator('//li[@data-username="rocketchat.internal.admin.test"][last()]//blockquote//div[2]'); } diff --git a/apps/meteor/tests/e2e/utils/pageobjects/SideNav.ts b/apps/meteor/tests/e2e/utils/pageobjects/SideNav.ts index 685417245dee..5781b67f8ca2 100644 --- a/apps/meteor/tests/e2e/utils/pageobjects/SideNav.ts +++ b/apps/meteor/tests/e2e/utils/pageobjects/SideNav.ts @@ -85,6 +85,10 @@ class SideNav extends BasePage { return this.getPage().locator('//li[@class="rcx-option"]//div[contains(text(), "Administration")]'); } + public omnichannel(): Locator { + return this.getPage().locator('li.rcx-option >> text="Omnichannel"'); + } + public logout(): Locator { return this.getPage().locator('//*[contains(@class, "rcx-option__content") and contains(text(), "Logout")]'); } @@ -111,11 +115,11 @@ class SideNav extends BasePage { } public newChannelBtn(): Locator { - return this.getPage().locator('//*[contains(@class, "rcx-option__content")]', { hasText: 'Channel' }); + return this.getPage().locator('li.rcx-option >> text="Channel"'); } public newDiscussionBtn(): Locator { - return this.getPage().locator('//*[contains(@class, "rcx-option__content")]', { hasText: 'Discussion' }); + return this.getPage().locator('li.rcx-option >> text="Discussion"'); } public newChannelIcon(): Locator { @@ -242,7 +246,7 @@ class SideNav extends BasePage { public async findForChat(target: string): Promise<void> { await this.searchUser().click(); - await this.searchInput().type(target, { delay: 300 }); + await this.searchInput().type(target, { delay: 100 }); await this.getPage().keyboard.press(ENTER); } diff --git a/apps/meteor/tests/end-to-end/api/04-direct-message.js b/apps/meteor/tests/end-to-end/api/04-direct-message.js index d1f6e82dc75e..9eb450539ab9 100644 --- a/apps/meteor/tests/end-to-end/api/04-direct-message.js +++ b/apps/meteor/tests/end-to-end/api/04-direct-message.js @@ -170,7 +170,6 @@ describe('[Direct Messages]', function () { .set(credentials) .query({ roomId: directMessage._id, - userId: 'rocket.cat', }) .expect('Content-Type', 'application/json') .expect(200) diff --git a/apps/meteor/tests/mocks/client/jsdom.ts b/apps/meteor/tests/mocks/client/jsdom.ts index ead125473f78..3398a619a932 100644 --- a/apps/meteor/tests/mocks/client/jsdom.ts +++ b/apps/meteor/tests/mocks/client/jsdom.ts @@ -1,6 +1,6 @@ import globalJsdom from 'jsdom-global'; -const testUrl = process.env.CYPRESS_BASE_URL || 'http://localhost:3000'; +const testUrl = process.env.TEST_API_URL || 'http://localhost:3000'; export const enableJsdom = (): void => { globalJsdom('<!doctype html><html><head><meta charset="utf-8"></head><body></body></html>', { diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceReceiver.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceReceiver.spec.ts new file mode 100644 index 000000000000..11050f2681fb --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceReceiver.spec.ts @@ -0,0 +1,436 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import '../../../lib/server.mocks'; + +import { FederationRoomServiceReceiver } from '../../../../../../app/federation-v2/server/application/RoomServiceReceiver'; +import { FederatedUser } from '../../../../../../app/federation-v2/server/domain/FederatedUser'; +import { FederatedRoom } from '../../../../../../app/federation-v2/server/domain/FederatedRoom'; +import { EVENT_ORIGIN } from '../../../../../../app/federation-v2/server/domain/IFederationBridge'; + +describe('Federation - Application - FederationRoomServiceReceiver', () => { + let service: FederationRoomServiceReceiver; + const roomAdapter = { + getFederatedRoomByExternalId: sinon.stub(), + createFederatedRoom: sinon.stub(), + removeUserFromRoom: sinon.stub(), + addUserToRoom: sinon.stub(), + updateRoomType: sinon.stub(), + updateRoomName: sinon.stub(), + updateRoomTopic: sinon.stub(), + }; + const userAdapter = { + getFederatedUserByExternalId: sinon.stub(), + createFederatedUser: sinon.stub(), + }; + const messageAdapter = { + sendMessage: sinon.stub(), + }; + const settingsAdapter = { + getHomeServerDomain: sinon.stub(), + }; + const bridge = { + getUserProfileInformation: sinon.stub().resolves({}), + isUserIdFromTheSameHomeserver: sinon.stub(), + joinRoom: sinon.stub(), + }; + + beforeEach(() => { + service = new FederationRoomServiceReceiver( + roomAdapter as any, + userAdapter as any, + messageAdapter as any, + settingsAdapter as any, + bridge as any, + ); + }); + + afterEach(() => { + roomAdapter.getFederatedRoomByExternalId.reset(); + roomAdapter.createFederatedRoom.reset(); + roomAdapter.removeUserFromRoom.reset(); + roomAdapter.addUserToRoom.reset(); + roomAdapter.updateRoomType.reset(); + roomAdapter.updateRoomName.reset(); + roomAdapter.updateRoomTopic.reset(); + userAdapter.getFederatedUserByExternalId.reset(); + userAdapter.createFederatedUser.reset(); + messageAdapter.sendMessage.reset(); + settingsAdapter.getHomeServerDomain.reset(); + bridge.isUserIdFromTheSameHomeserver.reset(); + bridge.joinRoom.reset(); + }); + + describe('#createRoom()', () => { + it('should NOT create users nor room if the room already exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.resolves({} as any); + await service.createRoom({} as any); + + expect(roomAdapter.createFederatedRoom.called).to.be.false; + }); + + it('should NOT create users nor room if the room was created internally and programatically', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.createRoom({ wasInternallyProgramaticallyCreated: true } as any); + + expect(roomAdapter.createFederatedRoom.called).to.be.false; + }); + + it('should NOT create the creator if it already exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.resolves({} as any); + await service.createRoom({} as any); + + expect(userAdapter.createFederatedUser.called).to.be.false; + }); + + it('should create the creator if it does not exists yet', async () => { + const creator = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: false, + }); + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves(undefined); + userAdapter.getFederatedUserByExternalId.onCall(1).resolves(creator); + await service.createRoom({ externalInviterId: 'externalInviterId', normalizedInviterId: 'normalizedInviterId' } as any); + + expect(userAdapter.createFederatedUser.calledWith(creator)).to.be.true; + }); + + it('should create the room if it does not exists yet', async () => { + const creator = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: false, + }); + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves(undefined); + userAdapter.getFederatedUserByExternalId.onCall(1).resolves(creator); + await service.createRoom({ + externalInviterId: 'externalInviterId', + normalizedInviterId: 'normalizedInviterId', + externalRoomId: 'externalRoomId', + normalizedRoomId: 'normalizedRoomId', + externalRoomName: 'externalRoomName', + } as any); + + const room = FederatedRoom.createInstance( + 'externalRoomId', + 'normalizedRoomId', + creator as FederatedUser, + RoomType.CHANNEL, + 'externalRoomName', + ); + expect(roomAdapter.createFederatedRoom.calledWith(room)).to.be.true; + }); + }); + + describe('#changeRoomMembership()', () => { + it('should throw an error if the room does not exists AND event origin is equal to LOCAL', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + try { + await service.changeRoomMembership({ externalRoomId: 'externalRoomId', eventOrigin: EVENT_ORIGIN.LOCAL } as any); + } catch (e: any) { + expect(e.message).to.be.equal('Could not find room with external room id: externalRoomId'); + } + }); + + it('should NOT throw an error if the room already exists AND event origin is equal to LOCAL', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.changeRoomMembership({ externalRoomId: 'externalRoomId', eventOrigin: EVENT_ORIGIN.LOCAL } as any); + + expect(bridge.isUserIdFromTheSameHomeserver.called).to.be.true; + }); + + it('should NOT throw an error if the room already exists AND event origin is equal to REMOTE', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.changeRoomMembership({ externalRoomId: 'externalRoomId', eventOrigin: EVENT_ORIGIN.REMOTE } as any); + + expect(bridge.isUserIdFromTheSameHomeserver.called).to.be.true; + }); + + it('should NOT create the inviter if it already exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(1).resolves({} as any); + await service.changeRoomMembership({ externalRoomId: 'externalRoomId', eventOrigin: EVENT_ORIGIN.LOCAL } as any); + + expect(userAdapter.createFederatedUser.called).to.be.false; + }); + + it('should create the inviter if it does not exists', async () => { + const inviter = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: false, + }); + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves(undefined); + bridge.isUserIdFromTheSameHomeserver.onCall(0).resolves(false); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + eventOrigin: EVENT_ORIGIN.LOCAL, + externalInviterId: 'externalInviterId', + normalizedInviterId: 'normalizedInviterId', + } as any); + + expect(userAdapter.createFederatedUser.calledWith(inviter)).to.be.true; + }); + + it('should NOT create the invitee if it already exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(1).resolves(undefined); + await service.changeRoomMembership({ externalRoomId: 'externalRoomId', eventOrigin: EVENT_ORIGIN.LOCAL } as any); + + expect(userAdapter.createFederatedUser.calledOnce).to.be.true; + }); + + it('should create the invitee if it does not exists', async () => { + const invitee = FederatedUser.createInstance('externalInviteeId', { + name: 'normalizedInviteeId', + username: 'normalizedInviteeId', + existsOnlyOnProxyServer: false, + }); + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(1).resolves(undefined); + bridge.isUserIdFromTheSameHomeserver.onCall(1).resolves(false); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + eventOrigin: EVENT_ORIGIN.LOCAL, + externalInviteeId: 'externalInviteeId', + normalizedInviteeId: 'normalizedInviteeId', + } as any); + + expect(userAdapter.createFederatedUser.calledWith(invitee)).to.be.true; + }); + + it('should create the room if it does not exists yet AND the event origin is REMOTE', async () => { + const inviter = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: false, + }); + const invitee = inviter; + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + bridge.isUserIdFromTheSameHomeserver.onCall(1).resolves(false); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves(undefined); + userAdapter.getFederatedUserByExternalId.onCall(1).resolves(undefined); + userAdapter.getFederatedUserByExternalId.onCall(2).resolves(inviter); + userAdapter.getFederatedUserByExternalId.onCall(3).resolves(invitee); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + normalizedRoomId: 'normalizedRoomId', + eventOrigin: EVENT_ORIGIN.REMOTE, + roomType: RoomType.CHANNEL, + externalInviteeId: 'externalInviteeId', + normalizedInviteeId: 'normalizedInviteeId', + } as any); + + const room = FederatedRoom.createInstance('externalRoomId', 'normalizedRoomId', inviter as FederatedUser, RoomType.CHANNEL); + expect(roomAdapter.createFederatedRoom.calledWith(room)).to.be.true; + expect(bridge.joinRoom.calledWith('externalRoomId', 'externalInviteeId')).to.be.true; + }); + + it('should NOT create the room if it already exists yet AND the event origin is REMOTE', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + normalizedRoomId: 'normalizedRoomId', + eventOrigin: EVENT_ORIGIN.REMOTE, + roomType: RoomType.CHANNEL, + externalInviteeId: 'externalInviteeId', + normalizedInviteeId: 'normalizedInviteeId', + } as any); + + expect(roomAdapter.createFederatedRoom.called).to.be.false; + }); + + it('should NOT create the room if it already exists yet AND the event origin is REMOTE', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + normalizedRoomId: 'normalizedRoomId', + eventOrigin: EVENT_ORIGIN.LOCAL, + roomType: RoomType.CHANNEL, + externalInviteeId: 'externalInviteeId', + normalizedInviteeId: 'normalizedInviteeId', + } as any); + + expect(roomAdapter.createFederatedRoom.called).to.be.false; + }); + + it('should remove the user from room if its a LEAVE event', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + normalizedRoomId: 'normalizedRoomId', + eventOrigin: EVENT_ORIGIN.LOCAL, + roomType: RoomType.CHANNEL, + externalInviteeId: 'externalInviteeId', + leave: true, + normalizedInviteeId: 'normalizedInviteeId', + } as any); + + expect(roomAdapter.removeUserFromRoom.called).to.be.true; + expect(roomAdapter.addUserToRoom.called).to.be.false; + }); + + it('should add the user from room if its NOT a LEAVE event', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + normalizedRoomId: 'normalizedRoomId', + eventOrigin: EVENT_ORIGIN.LOCAL, + roomType: RoomType.CHANNEL, + externalInviteeId: 'externalInviteeId', + leave: false, + normalizedInviteeId: 'normalizedInviteeId', + } as any); + + expect(roomAdapter.removeUserFromRoom.called).to.be.false; + expect(roomAdapter.addUserToRoom.called).to.be.true; + }); + }); + + describe('#receiveExternalMessage()', () => { + it('should NOT send a message if the room does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + await service.receiveExternalMessage({ + text: 'text', + } as any); + + expect(messageAdapter.sendMessage.called).to.be.false; + }); + + it('should NOT send a message if the sender does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.resolves(undefined); + await service.receiveExternalMessage({ + text: 'text', + } as any); + + expect(messageAdapter.sendMessage.called).to.be.false; + }); + + it('should send a message if the room and the sender already exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.resolves({} as any); + await service.receiveExternalMessage({ + text: 'text', + } as any); + + expect(messageAdapter.sendMessage.calledWith({}, 'text', {})).to.be.true; + }); + }); + + describe('#changeJoinRules()', () => { + it('should NOT change the room type if the room does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + await service.changeJoinRules({ + roomType: RoomType.CHANNEL, + } as any); + + expect(roomAdapter.updateRoomType.called).to.be.false; + }); + + it('should NOT change the room type if it exists and is a direct message', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.t = RoomType.DIRECT_MESSAGE; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + await service.changeJoinRules({ + roomType: RoomType.CHANNEL, + } as any); + + expect(roomAdapter.updateRoomType.called).to.be.false; + }); + + it('should change the room type if it exists and is NOT a direct message', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.t = RoomType.PRIVATE_GROUP; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + await service.changeJoinRules({ + roomType: RoomType.CHANNEL, + } as any); + room.internalReference.t = RoomType.CHANNEL; + expect(roomAdapter.updateRoomType.calledWith(room)).to.be.true; + }); + }); + + describe('#changeRoomName()', () => { + it('should NOT change the room name if the room does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + await service.changeRoomName({ + normalizedRoomName: 'normalizedRoomName', + } as any); + + expect(roomAdapter.updateRoomName.called).to.be.false; + }); + + it('should NOT change the room name if it exists and is a direct message', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.t = RoomType.DIRECT_MESSAGE; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + await service.changeRoomName({ + normalizedRoomName: 'normalizedRoomName', + } as any); + + expect(roomAdapter.updateRoomName.called).to.be.false; + }); + + it('should change the room name if it exists and is NOT a direct message', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.t = RoomType.PRIVATE_GROUP; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + await service.changeRoomName({ + roomnormalizedRoomNameType: 'normalizedRoomName', + } as any); + room.internalReference.name = 'normalizedRoomName'; + room.internalReference.fname = 'normalizedRoomName'; + expect(roomAdapter.updateRoomName.calledWith(room)).to.be.true; + }); + }); + + describe('#changeRoomTopic()', () => { + it('should NOT change the room topic if the room does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + await service.changeRoomTopic({ + roomTopic: 'roomTopic', + } as any); + + expect(roomAdapter.updateRoomTopic.called).to.be.false; + }); + + it('should NOT change the room topic if it exists and is a direct message', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.t = RoomType.DIRECT_MESSAGE; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + await service.changeRoomTopic({ + roomTopic: 'roomTopic', + } as any); + + expect(roomAdapter.updateRoomTopic.called).to.be.false; + }); + + it('should change the room topic if it exists and is NOT a direct message', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.t = RoomType.PRIVATE_GROUP; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + await service.changeRoomTopic({ + roomTopic: 'roomTopic', + } as any); + room.internalReference.description = 'roomTopic'; + expect(roomAdapter.updateRoomTopic.calledWith(room)).to.be.true; + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceSender.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceSender.spec.ts new file mode 100644 index 000000000000..8fa7719fb2ad --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceSender.spec.ts @@ -0,0 +1,311 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { FederationRoomServiceSender } from '../../../../../../app/federation-v2/server/application/RoomServiceSender'; +import { FederatedUser } from '../../../../../../app/federation-v2/server/domain/FederatedUser'; +import { FederatedRoom } from '../../../../../../app/federation-v2/server/domain/FederatedRoom'; + +describe('Federation - Application - FederationRoomServiceSender', () => { + let service: FederationRoomServiceSender; + const roomAdapter = { + getFederatedRoomByExternalId: sinon.stub(), + getFederatedRoomByInternalId: sinon.stub(), + createFederatedRoom: sinon.stub(), + updateFederatedRoomByInternalRoomId: sinon.stub(), + removeUserFromRoom: sinon.stub(), + addUserToRoom: sinon.stub(), + getInternalRoomById: sinon.stub(), + }; + const userAdapter = { + getFederatedUserByExternalId: sinon.stub(), + getFederatedUserByInternalId: sinon.stub(), + createFederatedUser: sinon.stub(), + getInternalUserById: sinon.stub(), + getFederatedUserByInternalUsername: sinon.stub(), + }; + const settingsAdapter = { + getHomeServerDomain: sinon.stub(), + }; + const bridge = { + getUserProfileInformation: sinon.stub().resolves({}), + isUserIdFromTheSameHomeserver: sinon.stub(), + sendMessage: sinon.stub(), + createUser: sinon.stub(), + inviteToRoom: sinon.stub().returns(new Promise((resolve) => resolve({}))), + createRoom: sinon.stub(), + joinRoom: sinon.stub(), + }; + const notificationAdapter = {}; + const room = FederatedRoom.build(); + const user = FederatedRoom.build(); + + beforeEach(() => { + service = new FederationRoomServiceSender( + roomAdapter as any, + userAdapter as any, + settingsAdapter as any, + notificationAdapter as any, + bridge as any, + ); + }); + + afterEach(() => { + roomAdapter.getFederatedRoomByExternalId.reset(); + roomAdapter.getFederatedRoomByInternalId.reset(); + roomAdapter.createFederatedRoom.reset(); + roomAdapter.updateFederatedRoomByInternalRoomId.reset(); + roomAdapter.addUserToRoom.reset(); + roomAdapter.getInternalRoomById.reset(); + userAdapter.getFederatedUserByExternalId.reset(); + userAdapter.getFederatedUserByInternalId.reset(); + userAdapter.getInternalUserById.reset(); + userAdapter.createFederatedUser.reset(); + userAdapter.getFederatedUserByInternalUsername.reset(); + settingsAdapter.getHomeServerDomain.reset(); + bridge.isUserIdFromTheSameHomeserver.reset(); + bridge.sendMessage.reset(); + bridge.createUser.reset(); + bridge.createRoom.reset(); + bridge.inviteToRoom.reset(); + bridge.joinRoom.reset(); + }); + + describe('#inviteUserToAFederatedRoom()', () => { + it('should NOT create the inviter user if the user already exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves(user); + userAdapter.getFederatedUserByInternalUsername.resolves(user); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({} as any); + + expect(userAdapter.createFederatedUser.called).to.be.false; + }); + + it('should create the inviter user both externally and internally if it does not exists', async () => { + userAdapter.getFederatedUserByInternalUsername.resolves(user); + userAdapter.getFederatedUserByInternalId.onCall(0).resolves(undefined); + userAdapter.getFederatedUserByInternalId.onCall(1).resolves(user); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + settingsAdapter.getHomeServerDomain.returns('domain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + bridge.createUser.resolves('externalInviterId'); + await service.inviteUserToAFederatedRoom({ externalInviterId: 'externalInviterId' } as any); + const inviter = FederatedUser.createInstance('externalInviterId', { + name: 'name', + username: 'username', + existsOnlyOnProxyServer: true, + }); + expect(bridge.createUser.calledWith('username', 'name', 'domain')).to.be.true; + expect(userAdapter.createFederatedUser.calledWith(inviter)).to.be.true; + }); + + it('should NOT create the invitee user if the user already exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves(user); + userAdapter.getFederatedUserByInternalUsername.resolves({} as any); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + settingsAdapter.getHomeServerDomain.returns('domain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ normalizedInviteeId: 'normalizedInviteeId', rawInviteeId: 'rawInviteeId' } as any); + + expect(userAdapter.createFederatedUser.called).to.be.false; + }); + + it('should create the invitee user internally if it does not exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves(user); + userAdapter.getFederatedUserByInternalUsername.onCall(0).resolves(undefined); + userAdapter.getFederatedUserByInternalUsername.onCall(1).resolves({} as any); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + settingsAdapter.getHomeServerDomain.returns('domain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + await service.inviteUserToAFederatedRoom({ normalizedInviteeId: 'normalizedInviteeId', rawInviteeId: 'rawInviteeId' } as any); + const invitee = FederatedUser.createInstance('rawInviteeId', { + name: 'normalizedInviteeId', + username: 'normalizedInviteeId', + existsOnlyOnProxyServer: false, + }); + + expect(userAdapter.createFederatedUser.calledWith(invitee)).to.be.true; + }); + + it('should NOT create the room if it already exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves(user); + userAdapter.getFederatedUserByInternalUsername.resolves({} as any); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + settingsAdapter.getHomeServerDomain.returns('domain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ normalizedInviteeId: 'normalizedInviteeId', rawInviteeId: 'rawInviteeId' } as any); + + expect(roomAdapter.createFederatedRoom.called).to.be.false; + }); + + it('should create the room both externally and internally if it does not exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves({ externalId: 'externalInviterId' } as any); + userAdapter.getFederatedUserByInternalUsername.resolves({ externalId: 'externalInviteeId' } as any); + roomAdapter.getInternalRoomById.resolves({ _id: 'internalRoomId', t: RoomType.CHANNEL, name: 'roomName', topic: 'topic' } as any); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + settingsAdapter.getHomeServerDomain.returns('domain'); + bridge.createUser.resolves('externalInviterId'); + bridge.createRoom.resolves('externalRoomId'); + roomAdapter.getFederatedRoomByInternalId.onCall(0).resolves(undefined); + roomAdapter.getFederatedRoomByInternalId.onCall(1).resolves(room); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ normalizedInviteeId: 'normalizedInviteeId', rawInviteeId: 'rawInviteeId' } as any); + const roomResult = FederatedRoom.createInstance('externalRoomId', 'externalRoomId', user as any, RoomType.CHANNEL, 'roomName'); + + expect(bridge.createRoom.calledWith('externalInviterId', 'externalInviteeId', RoomType.CHANNEL, 'roomName', 'topic')).to.be.true; + expect(roomAdapter.updateFederatedRoomByInternalRoomId.calledWith('internalRoomId', roomResult)).to.be.true; + }); + + it('should create, invite and join the user to the room in the proxy home server if the invitee is from the same homeserver', async () => { + userAdapter.getFederatedUserByInternalId.resolves({ externalId: 'externalInviterId' } as any); + userAdapter.getFederatedUserByInternalUsername.resolves({ + externalId: 'externalInviteeId', + internalReference: { name: 'usernameInvitee' }, + } as any); + roomAdapter.getInternalRoomById.resolves({ _id: 'internalRoomId', t: RoomType.CHANNEL, name: 'roomName', topic: 'topic' } as any); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + room.externalId = 'externalRoomId'; + roomAdapter.getFederatedRoomByInternalId.resolves(room); + settingsAdapter.getHomeServerDomain.returns('domain'); + bridge.isUserIdFromTheSameHomeserver.resolves(true); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ + normalizedInviteeId: 'normalizedInviteeId', + rawInviteeId: 'rawInviteeId', + inviteeUsernameOnly: 'inviteeUsernameOnly', + } as any); + + expect(bridge.createUser.calledWith('inviteeUsernameOnly', 'usernameInvitee', 'domain')).to.be.true; + expect(bridge.inviteToRoom.calledWith('externalRoomId', 'externalInviterId', 'externalInviteeId')).to.be.true; + expect(bridge.joinRoom.calledWith('externalRoomId', 'externalInviteeId')).to.be.true; + }); + + it('should invite the user to an external room if the room is NOT direct message(on DMs, they are invited during the creational process)', async () => { + userAdapter.getFederatedUserByInternalId.resolves({ externalId: 'externalInviterId' } as any); + userAdapter.getFederatedUserByInternalUsername.resolves({ + externalId: 'externalInviteeId', + internalReference: { name: 'usernameInvitee' }, + } as any); + roomAdapter.getInternalRoomById.resolves({ + _id: 'internalRoomId', + t: RoomType.DIRECT_MESSAGE, + name: 'roomName', + topic: 'topic', + } as any); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + room.externalId = 'externalRoomId'; + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.isUserIdFromTheSameHomeserver.resolves(false); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ + normalizedInviteeId: 'normalizedInviteeId', + rawInviteeId: 'rawInviteeId', + inviteeUsernameOnly: 'inviteeUsernameOnly', + } as any); + + expect(bridge.inviteToRoom.calledWith('externalRoomId', 'externalInviterId', 'externalInviteeId')).to.be.true; + }); + + it('should NOT invite any user externally if the user is not from the same home server AND it was already invited when creating the room', async () => { + userAdapter.getFederatedUserByInternalId.resolves({ externalId: 'externalInviterId' } as any); + userAdapter.getFederatedUserByInternalUsername.resolves({ + externalId: 'externalInviteeId', + internalReference: { name: 'usernameInvitee' }, + } as any); + room.internalReference = {} as any; + room.internalReference.t = RoomType.DIRECT_MESSAGE; + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.isUserIdFromTheSameHomeserver.resolves(false); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ + normalizedInviteeId: 'normalizedInviteeId', + rawInviteeId: 'rawInviteeId', + inviteeUsernameOnly: 'inviteeUsernameOnly', + } as any); + + expect(bridge.inviteToRoom.called).to.be.false; + expect(bridge.createUser.called).to.be.false; + expect(bridge.joinRoom.called).to.be.false; + }); + + it('should always add the user to the internal room', async () => { + userAdapter.getFederatedUserByInternalId.resolves({ externalId: 'externalInviterId' } as any); + userAdapter.getFederatedUserByInternalUsername.resolves({ + externalId: 'externalInviteeId', + internalReference: { name: 'usernameInvitee' }, + } as any); + room.internalReference = {} as any; + room.internalReference.t = RoomType.DIRECT_MESSAGE; + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.isUserIdFromTheSameHomeserver.resolves(false); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ + normalizedInviteeId: 'normalizedInviteeId', + rawInviteeId: 'rawInviteeId', + inviteeUsernameOnly: 'inviteeUsernameOnly', + } as any); + + expect(roomAdapter.addUserToRoom.called).to.be.true; + }); + }); + + describe('#sendMessageFromRocketChat()', () => { + it('should throw an error if the sender does not exists ', async () => { + userAdapter.getFederatedUserByInternalId.resolves(undefined); + try { + await service.sendMessageFromRocketChat({ internalSenderId: 'internalSenderId' } as any); + } catch (e: any) { + expect(e.message).to.be.equal('Could not find user id for internalSenderId'); + } + }); + + it('should throw an error if the room does not exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves({} as any); + roomAdapter.getFederatedRoomByInternalId.resolves(undefined); + try { + await service.sendMessageFromRocketChat({ internalRoomId: 'internalRoomId' } as any); + } catch (e: any) { + expect(e.message).to.be.equal('Could not find room id for internalRoomId'); + } + }); + + it('should send the message through the bridge', async () => { + userAdapter.getFederatedUserByInternalId.resolves({ externalId: 'externalId' } as any); + roomAdapter.getFederatedRoomByInternalId.resolves({ externalId: 'externalId' } as any); + await service.sendMessageFromRocketChat({ message: { msg: 'text' } } as any); + expect(bridge.sendMessage.calledWith('externalId', 'externalId', 'text')).to.be.true; + }); + }); + + describe('#isAFederatedRoom()', () => { + it('should return false if internalRoomId is undefined', async () => { + expect(await service.isAFederatedRoom('')).to.be.false; + }); + + it('should return false if the room does not exist', async () => { + roomAdapter.getFederatedRoomByInternalId.resolves(undefined); + expect(await service.isAFederatedRoom('')).to.be.false; + }); + + it('should return true if the room is NOT federated', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.federated = false; + roomAdapter.getFederatedRoomByInternalId.resolves(room); + expect(await service.isAFederatedRoom('internalRoomId')).to.be.false; + }); + + it('should return true if the room is federated', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.federated = true; + roomAdapter.getFederatedRoomByInternalId.resolves(room); + expect(await service.isAFederatedRoom('internalRoomId')).to.be.true; + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedRoom.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedRoom.spec.ts new file mode 100644 index 000000000000..b6d7f0011368 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedRoom.spec.ts @@ -0,0 +1,169 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import { expect } from 'chai'; + +import { FederatedRoom } from '../../../../../../app/federation-v2/server/domain/FederatedRoom'; + +describe('Federation - Domain - FederatedRoom', () => { + const members = [{ internalReference: { id: 'userId' } }, { internalReference: { id: 'userId2' } }] as any; + + describe('#createInstance()', () => { + it('should set the internal room name when it was provided', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, 'p' as any, 'myRoomName'); + expect(federatedRoom.internalReference.name).to.be.equal('myRoomName'); + expect(federatedRoom.internalReference.fname).to.be.equal('myRoomName'); + }); + + it('should generate automatically a room name when it was not provided', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, 'p' as any); + expect(federatedRoom.internalReference.name).to.be.equal('Federation-externalId'); + expect(federatedRoom.internalReference.fname).to.be.equal('Federation-externalId'); + }); + + it('should set the members property when the room is a direct message one', () => { + const federatedRoom = FederatedRoom.createInstance( + '!externalId@id', + 'externalId', + { id: 'userId' } as any, + RoomType.DIRECT_MESSAGE, + '', + members, + ); + expect(federatedRoom.members).to.be.eql(members); + }); + + it('should NOT set the members property when the room is NOT a direct message one', () => { + const federatedRoom = FederatedRoom.createInstance( + '!externalId@id', + 'externalId', + { id: 'userId' } as any, + RoomType.CHANNEL, + '', + members, + ); + expect(federatedRoom.members).to.be.undefined; + }); + + it('should return an instance of FederatedRoom', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.CHANNEL); + expect(federatedRoom).to.be.instanceOf(FederatedRoom); + }); + }); + + describe('#isDirectMessage()', () => { + it('should return true if its a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.DIRECT_MESSAGE); + expect(federatedRoom.isDirectMessage()).to.be.true; + }); + + it('should return false if its NOT a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.CHANNEL); + expect(federatedRoom.isDirectMessage()).to.be.false; + }); + }); + + describe('#setRoomType()', () => { + it('should set the Room type if its not a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.PRIVATE_GROUP); + federatedRoom.setRoomType(RoomType.CHANNEL); + expect(federatedRoom.internalReference.t).to.be.equal(RoomType.CHANNEL); + }); + + it('should throw an error when trying to set the room type if its a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.DIRECT_MESSAGE); + expect(() => federatedRoom.setRoomType(RoomType.CHANNEL)).to.be.throw('Its not possible to change a direct message type'); + }); + }); + + describe('#changeRoomName()', () => { + it('should change the Room name if its not a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.PRIVATE_GROUP); + federatedRoom.changeRoomName('newName'); + expect(federatedRoom.internalReference.name).to.be.equal('newName'); + expect(federatedRoom.internalReference.fname).to.be.equal('newName'); + }); + + it('should throw an error when trying to change the room name if its a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.DIRECT_MESSAGE); + expect(() => federatedRoom.changeRoomName('newName')).to.be.throw('Its not possible to change a direct message name'); + }); + }); + + describe('#changeRoomTopic()', () => { + it('should change the Room topic if its not a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.PRIVATE_GROUP); + federatedRoom.changeRoomTopic('newName'); + expect(federatedRoom.internalReference.description).to.be.equal('newName'); + }); + + it('should throw an error when trying to change the room topic if its a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.DIRECT_MESSAGE); + expect(() => federatedRoom.changeRoomTopic('newName')).to.be.throw('Its not possible to change a direct message topic'); + }); + }); + + describe('#changeRoomTopic()', () => { + it('should change the Room topic if its not a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.PRIVATE_GROUP); + federatedRoom.changeRoomTopic('newName'); + expect(federatedRoom.internalReference.description).to.be.equal('newName'); + }); + + it('should throw an error when trying to change the room topic if its a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.DIRECT_MESSAGE); + expect(() => federatedRoom.changeRoomTopic('newName')).to.be.throw('Its not possible to change a direct message topic'); + }); + }); + + describe('#getMembers()', () => { + it('should return the internalReference members if the room is a direct message', () => { + const federatedRoom = FederatedRoom.createInstance( + '!externalId@id', + 'externalId', + { id: 'userId' } as any, + RoomType.DIRECT_MESSAGE, + '', + members, + ); + expect(federatedRoom.getMembers()).to.be.eql(members.map((user: any) => user.internalReference)); + }); + + it('should return an empty array if the room is not a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance( + '!externalId@id', + 'externalId', + { id: 'userId' } as any, + RoomType.CHANNEL, + '', + members, + ); + expect(federatedRoom.getMembers()).to.be.eql([]); + }); + }); + + describe('#isFederated()', () => { + it('should return true if the room is federated', () => { + const federatedRoom = FederatedRoom.createInstance( + '!externalId@id', + 'externalId', + { id: 'userId' } as any, + RoomType.DIRECT_MESSAGE, + '', + members, + ); + federatedRoom.internalReference.federated = true; + expect(federatedRoom.isFederated()).to.be.true; + }); + + it('should return false if the room is NOT federated', () => { + const federatedRoom = FederatedRoom.createInstance( + '!externalId@id', + 'externalId', + { id: 'userId' } as any, + RoomType.DIRECT_MESSAGE, + '', + members, + ); + expect(federatedRoom.isFederated()).to.be.false; + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedUser.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedUser.spec.ts new file mode 100644 index 000000000000..386877bbd858 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedUser.spec.ts @@ -0,0 +1,43 @@ +import { expect } from 'chai'; + +import { FederatedUser } from '../../../../../../app/federation-v2/server/domain/FederatedUser'; + +describe('Federation - Domain - FederatedUser', () => { + describe('#createInstance()', () => { + it('should set the internal user name when it was provided', () => { + const federatedUser = FederatedUser.createInstance('@marcos:matrix.org', { + name: '', + username: 'username', + existsOnlyOnProxyServer: false, + }); + expect(federatedUser.internalReference.username).to.be.equal('username'); + }); + + it('should set the internal name when it was provided', () => { + const federatedUser = FederatedUser.createInstance('@marcos:matrix.org', { + name: 'name', + username: '', + existsOnlyOnProxyServer: false, + }); + expect(federatedUser.internalReference.name).to.be.equal('name'); + }); + + it('should set the existsOnlyOnProxyServer it was provided', () => { + const federatedUser = FederatedUser.createInstance('@marcos:matrix.org', { + name: '', + username: 'username', + existsOnlyOnProxyServer: true, + }); + expect(federatedUser.existsOnlyOnProxyServer).to.be.true; + }); + + it('should return an instance of FederatedUser', () => { + const federatedUser = FederatedUser.createInstance('@marcos:matrix.org', { + name: '', + username: 'username', + existsOnlyOnProxyServer: false, + }); + expect(federatedUser).to.be.instanceOf(FederatedUser); + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/converters/RoomReceiver.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/converters/RoomReceiver.spec.ts new file mode 100644 index 000000000000..31a67945fa15 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/converters/RoomReceiver.spec.ts @@ -0,0 +1,328 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { expect } from 'chai'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { MatrixRoomReceiverConverter } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver'; +import { + FederationRoomCreateInputDto, + FederationRoomChangeMembershipDto, + FederationRoomSendInternalMessageDto, + FederationRoomChangeJoinRulesDto, + FederationRoomChangeNameDto, + FederationRoomChangeTopicDto, +} from '../../../../../../../../app/federation-v2/server/application/input/RoomReceiverDto'; +import { MatrixEventType } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType'; +import { RoomJoinRules } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules'; +import { AddMemberToRoomMembership } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom'; +import { EVENT_ORIGIN } from '../../../../../../../../app/federation-v2/server/domain/IFederationBridge'; + +describe('Federation - Infrastructure - Matrix - MatrixRoomReceiverConverter', () => { + describe('#toRoomCreateDto()', () => { + const event = { + content: { was_internally_programatically_created: true, name: 'roomName' }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + }; + + it('should return an instance of FederationRoomCreateInputDto', () => { + expect(MatrixRoomReceiverConverter.toRoomCreateDto({} as any)).to.be.instanceOf(FederationRoomCreateInputDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toRoomCreateDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should return the external room name and room type when the room state is present on the event and it has the correct events', () => { + const state = [ + { type: MatrixEventType.ROOM_NAME_CHANGED, content: { name: event.content.name } }, + { type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.JOIN } }, + ]; + const result = MatrixRoomReceiverConverter.toRoomCreateDto({ unsigned: { invite_room_state: state } } as any); + expect(result.externalRoomName).to.be.equal(event.content.name); + expect(result.roomType).to.be.equal(RoomType.CHANNEL); + }); + + it('should convert to the expected (private) room type when the join rule is equal to INVITE', () => { + const state = [ + { type: MatrixEventType.ROOM_NAME_CHANGED, content: { name: event.content.name } }, + { type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.INVITE } }, + ]; + const result = MatrixRoomReceiverConverter.toRoomCreateDto({ unsigned: { invite_room_state: state } } as any); + expect(result.externalRoomName).to.be.equal(event.content.name); + expect(result.roomType).to.be.equal(RoomType.PRIVATE_GROUP); + }); + + it('should convert to the expected (channel) room type when the join rule is equal to JOIN', () => { + const state = [{ type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.JOIN } }]; + const result = MatrixRoomReceiverConverter.toRoomCreateDto({ invite_room_state: state } as any); + expect(result.roomType).to.be.equal(RoomType.CHANNEL); + }); + + it('should convert the inviter id to the a rc-format like (without any @ in it)', () => { + const result = MatrixRoomReceiverConverter.toRoomCreateDto({ sender: event.sender } as any); + expect(result.normalizedInviterId).to.be.equal('marcos.defendi:matrix.org'); + }); + + it('should set wasInternallyProgramaticallyCreated accordingly to the event', () => { + const result = MatrixRoomReceiverConverter.toRoomCreateDto({ content: event.content } as any); + expect(result.wasInternallyProgramaticallyCreated).to.be.true; + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toRoomCreateDto(event as any); + expect(result).to.be.eql({ + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + externalInviterId: '@marcos.defendi:matrix.org', + normalizedInviterId: 'marcos.defendi:matrix.org', + wasInternallyProgramaticallyCreated: true, + }); + }); + }); + + describe('#toChangeRoomMembershipDto()', () => { + const event = { + content: { name: 'roomName' }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + state_key: '@marcos.defendi2:matrix.org', + }; + + it('should return an instance of FederationRoomChangeMembershipDto', () => { + expect(MatrixRoomReceiverConverter.toChangeRoomMembershipDto({} as any)).to.be.instanceOf(FederationRoomChangeMembershipDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should return the external room name and room type when the room state is present on the event and it has the correct events', () => { + const state = [ + { type: MatrixEventType.ROOM_NAME_CHANGED, content: { name: event.content.name } }, + { type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.JOIN } }, + ]; + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ unsigned: { invite_room_state: state } } as any); + expect(result.externalRoomName).to.be.equal(event.content.name); + expect(result.roomType).to.be.equal(RoomType.CHANNEL); + }); + + it('should convert to the expected (private) room type when the join rule is equal to INVITE', () => { + const state = [ + { type: MatrixEventType.ROOM_NAME_CHANGED, content: { name: event.content.name } }, + { type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.INVITE } }, + ]; + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ unsigned: { invite_room_state: state } } as any); + expect(result.externalRoomName).to.be.equal(event.content.name); + expect(result.roomType).to.be.equal(RoomType.PRIVATE_GROUP); + }); + + it('should convert to the expected (channel) room type when the join rule is equal to JOIN', () => { + const state = [{ type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.JOIN } }]; + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ invite_room_state: state } as any); + expect(result.roomType).to.be.equal(RoomType.CHANNEL); + }); + + it('should convert to the expected (direct) room type when the join rule is equal to INVITE and its a direct message', () => { + const state = [{ type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.INVITE } }]; + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ + invite_room_state: state, + content: { is_direct: true }, + } as any); + expect(result.roomType).to.be.equal(RoomType.DIRECT_MESSAGE); + }); + + it('should convert the inviter id to the a rc-format like (without any @ in it)', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ sender: event.sender } as any); + expect(result.normalizedInviterId).to.be.equal('marcos.defendi:matrix.org'); + }); + + it('should convert the invitee id to the a rc-format like (without any @ in it)', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ state_key: event.sender } as any); + expect(result.normalizedInviteeId).to.be.equal('marcos.defendi:matrix.org'); + }); + + it('should convert the inviter id to the a rc-format username like (without any @ in it and just the part before the ":")', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ sender: event.sender } as any); + expect(result.inviterUsernameOnly).to.be.equal('marcos.defendi'); + }); + + it('should convert the invitee id to the a rc-format username like (without any @ in it and just the part before the ":")', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ state_key: event.sender } as any); + expect(result.inviteeUsernameOnly).to.be.equal('marcos.defendi'); + }); + + it('should set leave to true if its a LEAVE event', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ + content: { membership: AddMemberToRoomMembership.LEAVE }, + } as any); + expect(result.leave).to.be.true; + }); + + it('should set leave to false if its NOT a LEAVE event', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ + content: { membership: AddMemberToRoomMembership.JOIN }, + } as any); + expect(result.leave).to.be.false; + }); + + it('should set the event origin as REMOTE if the users are from different home servers', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ sender: 'a:matrix.org', state_key: 'a:matrix2.org' } as any); + expect(result.eventOrigin).to.be.equal(EVENT_ORIGIN.REMOTE); + }); + + it('should set the event origin as LOCAL if the users are from different home servers', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ sender: 'a:matrix.org', state_key: 'a:matrix.org' } as any); + expect(result.eventOrigin).to.be.equal(EVENT_ORIGIN.LOCAL); + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto(event as any); + expect(result).to.be.eql({ + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + externalInviterId: '@marcos.defendi:matrix.org', + normalizedInviterId: 'marcos.defendi:matrix.org', + externalInviteeId: '@marcos.defendi2:matrix.org', + normalizedInviteeId: 'marcos.defendi2:matrix.org', + inviteeUsernameOnly: 'marcos.defendi2', + inviterUsernameOnly: 'marcos.defendi', + eventOrigin: EVENT_ORIGIN.LOCAL, + leave: false, + }); + }); + }); + + describe('#toSendRoomMessageDto()', () => { + const event = { + content: { body: 'msg' }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + }; + + it('should return an instance of FederationRoomSendInternalMessageDto', () => { + expect(MatrixRoomReceiverConverter.toSendRoomMessageDto({} as any)).to.be.instanceOf(FederationRoomSendInternalMessageDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toSendRoomMessageDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should convert the sender id to the a rc-format like (without any @ in it)', () => { + const result = MatrixRoomReceiverConverter.toSendRoomMessageDto({ sender: event.sender } as any); + expect(result.normalizedSenderId).to.be.equal('marcos.defendi:matrix.org'); + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toSendRoomMessageDto(event as any); + expect(result).to.be.eql({ + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + externalSenderId: '@marcos.defendi:matrix.org', + normalizedSenderId: 'marcos.defendi:matrix.org', + text: 'msg', + }); + }); + }); + + describe('#toRoomChangeJoinRulesDto()', () => { + const event = { + content: { join_rule: RoomJoinRules.JOIN }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + }; + + it('should return an instance of FederationRoomChangeJoinRulesDto', () => { + expect(MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto({} as any)).to.be.instanceOf(FederationRoomChangeJoinRulesDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should convert to the expected (private) room type when the join rule is equal to INVITE', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto({ content: { join_rule: RoomJoinRules.INVITE } } as any); + expect(result.roomType).to.be.equal(RoomType.PRIVATE_GROUP); + }); + + it('should convert to the expected (channel) room type when the join rule is equal to JOIN', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto({ content: { join_rule: RoomJoinRules.JOIN } } as any); + expect(result.roomType).to.be.equal(RoomType.CHANNEL); + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto(event as any); + expect(result).to.be.eql({ + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + roomType: RoomType.CHANNEL, + }); + }); + }); + + describe('#toRoomChangeNameDto()', () => { + const event = { + content: { name: '@roomName' }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + }; + + it('should return an instance of toRoomChangeNameDto', () => { + expect(MatrixRoomReceiverConverter.toRoomChangeNameDto({} as any)).to.be.instanceOf(FederationRoomChangeNameDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeNameDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should convert the roomName to a normalized version without starting with @', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeNameDto({ content: event.content } as any); + expect(result.normalizedRoomName).to.be.equal('roomName'); + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeNameDto(event as any); + expect(result).to.be.eql({ + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + normalizedRoomName: 'roomName', + }); + }); + }); + + describe('#toRoomChangeTopicDto()', () => { + const event = { + content: { topic: 'room topic' }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + }; + + it('should return an instance of FederationRoomChangeTopicDto', () => { + expect(MatrixRoomReceiverConverter.toRoomChangeTopicDto({} as any)).to.be.instanceOf(FederationRoomChangeTopicDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeTopicDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeTopicDto(event as any); + expect(result).to.be.eql({ + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + roomTopic: 'room topic', + }); + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/BaseEvent.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/BaseEvent.spec.ts new file mode 100644 index 000000000000..a8ff82b762f2 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/BaseEvent.spec.ts @@ -0,0 +1,45 @@ +import { expect, spy } from 'chai'; + +import { MatrixBaseEventHandler } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/handlers/BaseEvent'; + +describe('Federation - Infrastructure - Matrix - MatrixBaseEventHandler', () => { + describe('#equals()', () => { + class MyHandler extends MatrixBaseEventHandler<any> { + public constructor(type: any) { + super(type); + } + + public handle(): Promise<void> { + throw new Error('Method not implemented.'); + } + } + const myHandler = new MyHandler('type' as any); + + it('should return true if the type is equals to the provided one', () => { + expect(myHandler.equals('type' as any)).to.be.true; + }); + + it('should return false if the type is different to the provided one', () => { + expect(myHandler.equals('different' as any)).to.be.false; + }); + }); + + describe('#handle()', () => { + const spyFn = spy(); + class MyHandler extends MatrixBaseEventHandler<any> { + public constructor(type: any) { + super(type); + } + + public async handle(): Promise<void> { + spyFn(); + } + } + const myHandler = new MyHandler('type' as any); + + it('should call the handler fn in the implementated class', () => { + myHandler.handle(); + expect(spyFn).to.be.called; + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/MatrixEventsHandler.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/MatrixEventsHandler.spec.ts new file mode 100644 index 000000000000..bbdde4978286 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/MatrixEventsHandler.spec.ts @@ -0,0 +1,25 @@ +import { expect, spy } from 'chai'; + +import { MatrixEventsHandler } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/handlers'; + +describe('Federation - Infrastructure - Matrix - MatrixEventsHandler', () => { + describe('#handleEvent()', () => { + const spyFn = spy(); + const myHandler = new MatrixEventsHandler([ + { + equals: (eventType: string): boolean => eventType === 'eventType', + handle: spyFn, + }, + ] as any); + + it('should call the handler fn properly', async () => { + await myHandler.handleEvent({ type: 'eventType' } as any); + expect(spyFn).to.have.been.called.with({ type: 'eventType' }); + }); + + it('should NOT call the handler if there is no handler for the event', async () => { + await myHandler.handleEvent({ type: 'eventType2' } as any); + expect(spyFn).to.not.be.called; + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/queue/InMemoryQueue.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/queue/InMemoryQueue.spec.ts new file mode 100644 index 000000000000..a79437b3494d --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/queue/InMemoryQueue.spec.ts @@ -0,0 +1,28 @@ +import { expect, spy } from 'chai'; +import mock from 'mock-require'; + +import { InMemoryQueue } from '../../../../../../../app/federation-v2/server/infrastructure/queue/InMemoryQueue'; + +mock('fastq', { + promise: (handler: Function) => ({ + push: async (task: any): Promise<void> => handler(task), + }), +}); + +describe('Federation - Infrastructure - Queue - InMemoryQueue', () => { + const queue = new InMemoryQueue(); + + describe('#addToQueue()', () => { + it('should throw an error if the instance was not set beforehand', () => { + expect(() => queue.addToQueue({})).to.throw('You need to set the handler first'); + }); + + it('should push the task to the queue instance to be handled when the instance was properly defined', () => { + const spiedCb = spy(); + const concurrency = 1; + queue.setHandler(spiedCb, concurrency); + queue.addToQueue({ task: 'my-task' }); + expect(spiedCb).to.have.been.called.with({ task: 'my-task' }); + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Room.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Room.spec.ts new file mode 100644 index 000000000000..19edac2efe00 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Room.spec.ts @@ -0,0 +1,3 @@ +// describe('Federation - Infrastructure - RocketChat - RocketChatRoomAdapter', () => { + +// }); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Settings.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Settings.spec.ts new file mode 100644 index 000000000000..f5d7859a0c1c --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Settings.spec.ts @@ -0,0 +1,3 @@ +// describe('Federation - Infrastructure - RocketChat - RocketChatSettingsAdapter', () => { + +// }); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/User.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/User.spec.ts new file mode 100644 index 000000000000..29bf5f30b0aa --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/User.spec.ts @@ -0,0 +1,5 @@ +// // import { expect } from 'chai'; + +// describe('Federation - Infrastructure - RocketChat - RocketChatUserAdapter', () => { +// +// }); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/converters/RoomSender.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/converters/RoomSender.spec.ts new file mode 100644 index 000000000000..575f51ec3a92 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/converters/RoomSender.spec.ts @@ -0,0 +1,72 @@ +import { expect } from 'chai'; +import { IMessage } from '@rocket.chat/core-typings'; + +import { FederationRoomSenderConverter } from '../../../../../../../../app/federation-v2/server/infrastructure/rocket-chat/converters/RoomSender'; +import { + FederationRoomInviteUserDto, + FederationRoomSendExternalMessageDto, +} from '../../../../../../../../app/federation-v2/server/application/input/RoomSenderDto'; + +describe('Federation - Infrastructure - RocketChat - FederationRoomSenderConverter', () => { + describe('#toRoomInviteUserDto()', () => { + it('should return an instance of FederationRoomInviteUserDto', () => { + expect( + FederationRoomSenderConverter.toRoomInviteUserDto('internalInviterId', 'internalRoomId', 'externalInviteeId'), + ).to.be.instanceOf(FederationRoomInviteUserDto); + }); + + it('should return the normalizedInviteeId property without any @ if any', () => { + expect( + FederationRoomSenderConverter.toRoomInviteUserDto('internalInviterId', 'internalRoomId', '@externalInviteeId:server-name.com') + .normalizedInviteeId, + ).to.be.equal('externalInviteeId:server-name.com'); + }); + + it('should return the inviteeUsernameOnly property without any @ if any and only the first part before ":"', () => { + expect( + FederationRoomSenderConverter.toRoomInviteUserDto('internalInviterId', 'internalRoomId', '@externalInviteeId:server-name.com') + .inviteeUsernameOnly, + ).to.be.equal('externalInviteeId'); + }); + + it('should return the normalizedInviteeId AND inviteeUsernameOnly equals to the rawInviteeId if it does not have any special chars', () => { + const result = FederationRoomSenderConverter.toRoomInviteUserDto('internalInviterId', 'internalRoomId', 'externalInviteeId'); + expect(result.rawInviteeId).to.be.equal('externalInviteeId'); + expect(result.normalizedInviteeId).to.be.equal('externalInviteeId'); + expect(result.inviteeUsernameOnly).to.be.equal('externalInviteeId'); + }); + + it('should have all the properties set', () => { + const internalInviterId = 'internalInviterId'; + const internalRoomId = 'internalRoomId'; + const externalInviteeId = 'externalInviteeId'; + const result: any = FederationRoomSenderConverter.toRoomInviteUserDto(internalInviterId, internalRoomId, externalInviteeId); + expect(result).to.be.eql({ + internalInviterId, + internalRoomId, + rawInviteeId: externalInviteeId, + normalizedInviteeId: externalInviteeId, + inviteeUsernameOnly: externalInviteeId, + }); + }); + }); + describe('#toSendExternalMessageDto()', () => { + it('should return an instance of FederationRoomSendExternalMessageDto', () => { + expect( + FederationRoomSenderConverter.toSendExternalMessageDto('internalSenderId', 'internalRoomId', { msg: 'text' } as IMessage), + ).to.be.instanceOf(FederationRoomSendExternalMessageDto); + }); + + it('should have all the properties set', () => { + const internalSenderId = 'internalSenderId'; + const internalRoomId = 'internalRoomId'; + const msg = { msg: 'text' } as IMessage; + const result: any = FederationRoomSenderConverter.toSendExternalMessageDto(internalSenderId, internalRoomId, msg); + expect(result).to.be.eql({ + internalSenderId, + internalRoomId, + message: msg, + }); + }); + }); +}); diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index dc09fe72a312..647fbe290171 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -84,6 +84,7 @@ export interface IRoom extends IRocketChatRecord { description?: string; createdOTR?: boolean; e2eKeyId?: string; + federated?: boolean; channel?: { _id: string }; } diff --git a/packages/core-typings/src/ISubscription.ts b/packages/core-typings/src/ISubscription.ts index b105402605d9..e0f07d277eba 100644 --- a/packages/core-typings/src/ISubscription.ts +++ b/packages/core-typings/src/ISubscription.ts @@ -53,7 +53,7 @@ export interface ISubscription extends IRocketChatRecord { blocked?: unknown; blocker?: unknown; autoTranslate?: unknown; - autoTranslateLanguage?: unknown; + autoTranslateLanguage?: string; disableNotifications?: unknown; muteGroupMentions?: unknown; ignored?: unknown; diff --git a/packages/core-typings/src/SlashCommands/index.ts b/packages/core-typings/src/SlashCommands/index.ts new file mode 100644 index 000000000000..b8e0d5d9a7f9 --- /dev/null +++ b/packages/core-typings/src/SlashCommands/index.ts @@ -0,0 +1,50 @@ +import type { IMessage } from '../IMessage'; +import type { RequiredField } from '../utils'; + +type SlashCommandCallback<T extends string = string> = ( + command: T, + params: string, + message: RequiredField<Partial<IMessage>, 'rid'>, + triggerId?: string, +) => void; + +export type SlashCommandPreviewItem = { + id: string; + type: 'image' | 'video' | 'audio' | 'text' | 'other'; + value: string; +}; + +export type SlashCommandPreviews = { + i18nTitle: string; + items: SlashCommandPreviewItem[]; +}; + +type SlashCommandPreviewer = (command: string, params: string, message: IMessage) => SlashCommandPreviews | undefined; + +type SlashCommandPreviewCallback = ( + command: string, + params: string, + message: IMessage, + preview: SlashCommandPreviewItem, + triggerId: string, +) => void; + +export type SlashCommandOptions = { + params?: string; + description?: string; + permission?: string | string[]; + clientOnly?: boolean; +}; + +export type SlashCommand<T extends string = string> = { + command: string; + callback?: SlashCommandCallback<T>; + params: SlashCommandOptions['params']; + description: SlashCommandOptions['description']; + permission: SlashCommandOptions['permission']; + clientOnly?: SlashCommandOptions['clientOnly']; + result?: (err: Meteor.Error, result: never, data: { cmd: string; params: string; msg: IMessage }) => void; + providesPreview: boolean; + previewer?: SlashCommandPreviewer; + previewCallback?: SlashCommandPreviewCallback; +}; diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 99c80fb49721..44045c4613d4 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -27,6 +27,7 @@ export * from './IServerEvent'; export * from './ICronJobs'; export * from './IPushToken'; export * from './IPushNotificationConfig'; +export * from './SlashCommands'; export * from './IUserDataFile'; export * from './IUserSession'; diff --git a/packages/core-typings/src/utils.ts b/packages/core-typings/src/utils.ts index 40c11706d8e3..2f4eb3981324 100644 --- a/packages/core-typings/src/utils.ts +++ b/packages/core-typings/src/utils.ts @@ -21,3 +21,5 @@ export type Jsonify<T> = T extends Date : T; export type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>; + +export type RequiredField<T, K extends keyof T> = T & Required<Pick<T, K>>; diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index 04b4a05c0b4a..0a9dbb202fe3 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -2,6 +2,7 @@ import type { KeyOfEach } from '@rocket.chat/core-typings'; import type { AppsEndpoints } from './apps'; +import type { AutoTranslateEndpoints } from './v1/autoTranslate'; import type { ReplacePlaceholders } from './helpers/ReplacePlaceholders'; import type { BannersEndpoints } from './v1/banners'; import type { ChannelsEndpoints } from './v1/channels'; @@ -34,6 +35,7 @@ import type { VoipEndpoints } from './v1/voip'; import type { EmailInboxEndpoints } from './v1/email-inbox'; import type { WebdavEndpoints } from './v1/webdav'; import type { OAuthAppsEndpoint } from './v1/oauthapps'; +import type { CommandsEndpoints } from './v1/commands'; // eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/interface-name-prefix export interface Endpoints @@ -41,6 +43,7 @@ export interface Endpoints BannersEndpoints, ChatEndpoints, CloudEndpoints, + CommandsEndpoints, CustomUserStatusEndpoints, DmEndpoints, DnsEndpoints, @@ -70,7 +73,7 @@ export interface Endpoints EmailInboxEndpoints, WebdavEndpoints, OAuthAppsEndpoint, - AppsEndpoints {} + AutoTranslateEndpoints {} type OperationsByPathPattern<TPathPattern extends keyof Endpoints> = TPathPattern extends any ? OperationsByPathPatternAndMethod<TPathPattern> @@ -155,7 +158,9 @@ export * from './v1/channels/ChannelsConvertToTeamProps'; export * from './v1/channels/ChannelsSetReadOnlyProps'; export * from './v1/channels/ChannelsDeleteProps'; export * from './v1/dm'; +export * from './v1/dm/DmHistoryProps'; export * from './v1/integrations'; +export * from './v1/omnichannel'; export * from './v1/oauthapps'; export * from './helpers/PaginatedRequest'; export * from './helpers/PaginatedResult'; diff --git a/packages/rest-typings/src/v1/autoTranslate.ts b/packages/rest-typings/src/v1/autoTranslate.ts new file mode 100644 index 000000000000..9f265c9aa6d4 --- /dev/null +++ b/packages/rest-typings/src/v1/autoTranslate.ts @@ -0,0 +1,13 @@ +import type { ISupportedLanguage } from '@rocket.chat/core-typings'; + +export type AutoTranslateEndpoints = { + 'autotranslate.getSupportedLanguages': { + GET: (params: { targetLanguage: string }) => { languages: ISupportedLanguage[] }; + }; + 'autotranslate.saveSettings': { + POST: (params: { roomId: string; field: string; value: boolean; defaultLanguage?: string }) => void; + }; + 'autotranslate.translateMessage': { + POST: (params: { messageId: string; targetLanguage?: string }) => void; + }; +}; diff --git a/packages/rest-typings/src/v1/banners.ts b/packages/rest-typings/src/v1/banners.ts index 2af16f69620d..7045e2383a14 100644 --- a/packages/rest-typings/src/v1/banners.ts +++ b/packages/rest-typings/src/v1/banners.ts @@ -1,26 +1,104 @@ +import Ajv from 'ajv'; import type { BannerPlatform, IBanner } from '@rocket.chat/core-typings'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type BannersGetNew = { + platform: BannerPlatform; + bid: IBanner['_id']; +}; + +const BannersGetNewSchema = { + type: 'object', + properties: { + platform: { + type: 'string', + enum: ['1', '2'], + }, + bid: { + type: 'string', + }, + }, + required: ['platform', 'bid'], + additionalProperties: false, +}; + +export const isBannersGetNewProps = ajv.compile<BannersGetNew>(BannersGetNewSchema); + +type BannersId = { + platform: BannerPlatform; +}; + +const BannersIdSchema = { + type: 'object', + properties: { + platform: { + type: 'string', + }, + }, + required: ['platform'], + additionalProperties: false, +}; + +export const isBannersIdProps = ajv.compile<BannersId>(BannersIdSchema); + +type Banners = { + platform: BannerPlatform; +}; + +const BannersSchema = { + type: 'object', + properties: { + platform: { + type: 'string', + }, + }, + required: ['platform'], + additionalProperties: false, +}; + +export const isBannersProps = ajv.compile<Banners>(BannersSchema); + +type BannersDismiss = { + bannerId: string; +}; + +const BannersDismissSchema = { + type: 'object', + properties: { + bannerId: { + type: 'string', + }, + }, + required: ['bannerId'], + additionalProperties: false, +}; + +export const isBannersDismissProps = ajv.compile<BannersDismiss>(BannersDismissSchema); + export type BannersEndpoints = { /* @deprecated */ 'banners.getNew': { - GET: (params: { platform: BannerPlatform; bid: IBanner['_id'] }) => { + GET: (params: BannersGetNew) => { banners: IBanner[]; }; }; 'banners/:id': { - GET: (params: { platform: BannerPlatform }) => { + GET: (params: BannersId) => { banners: IBanner[]; }; }; 'banners': { - GET: (params: { platform: BannerPlatform }) => { + GET: (params: Banners) => { banners: IBanner[]; }; }; 'banners.dismiss': { - POST: (params: { bannerId: string }) => void; + POST: (params: BannersDismiss) => void; }; }; diff --git a/packages/rest-typings/src/v1/chat.ts b/packages/rest-typings/src/v1/chat.ts index e22ab320c99f..1754c302f5a9 100644 --- a/packages/rest-typings/src/v1/chat.ts +++ b/packages/rest-typings/src/v1/chat.ts @@ -1,46 +1,432 @@ import type { IMessage, IRoom, ReadReceipt } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type ChatFollowMessage = { + mid: IMessage['_id']; +}; + +const chatFollowMessageSchema = { + type: 'object', + properties: { + mid: { + type: 'string', + }, + }, + required: ['mid'], + additionalProperties: false, +}; + +export const isChatFollowMessageProps = ajv.compile<ChatFollowMessage>(chatFollowMessageSchema); + +type ChatUnfollowMessage = { + mid: IMessage['_id']; +}; + +const chatUnfollowMessageSchema = { + type: 'object', + properties: { + mid: { + type: 'string', + }, + }, + required: ['mid'], + additionalProperties: false, +}; + +export const isChatUnfollowMessageProps = ajv.compile<ChatUnfollowMessage>(chatUnfollowMessageSchema); + +type ChatGetMessage = { + msgId: IMessage['_id']; +}; + +const ChatGetMessageSchema = { + type: 'object', + properties: { + msgId: { + type: 'string', + }, + }, + required: ['msgId'], + additionalProperties: false, +}; + +export const isChatGetMessageProps = ajv.compile<ChatGetMessage>(ChatGetMessageSchema); + +type ChatStarMessage = { + msgId: IMessage['_id']; +}; + +const ChatStarMessageSchema = { + type: 'object', + properties: { + msgId: { + type: 'string', + }, + }, + required: ['msgId'], + additionalProperties: false, +}; + +export const isChatStarMessageProps = ajv.compile<ChatStarMessage>(ChatStarMessageSchema); + +type ChatUnstarMessage = { + msgId: IMessage['_id']; +}; + +const ChatUnstarMessageSchema = { + type: 'object', + properties: { + msgId: { + type: 'string', + }, + }, + required: ['msgId'], + additionalProperties: false, +}; + +export const isChatUnstarMessageProps = ajv.compile<ChatUnstarMessage>(ChatUnstarMessageSchema); + +type ChatPinMessage = { + msgId: IMessage['_id']; +}; + +const ChatPinMessageSchema = { + type: 'object', + properties: { + msgId: { + type: 'string', + }, + }, + required: ['msgId'], + additionalProperties: false, +}; + +export const isChatPinMessageProps = ajv.compile<ChatPinMessage>(ChatPinMessageSchema); + +type ChatUnpinMessage = { + messageId: IMessage['_id']; +}; + +const ChatUnpinMessageSchema = { + type: 'object', + properties: { + messageId: { + type: 'string', + }, + }, + required: ['messageId'], + additionalProperties: false, +}; + +export const isChatUnpinMessageProps = ajv.compile<ChatUnpinMessage>(ChatUnpinMessageSchema); + +type ChatGetDiscussions = { + roomId: IRoom['_id']; + text?: string; + offset: number; + count: number; +}; + +const ChatGetDiscussionsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + text: { + type: 'string', + nullable: true, + }, + offset: { + type: 'number', + }, + count: { + type: 'number', + }, + }, + required: ['roomId', 'offset', 'count'], + additionalProperties: false, +}; + +export const isChatGetDiscussionsProps = ajv.compile<ChatGetDiscussions>(ChatGetDiscussionsSchema); + +type ChatReportMessage = { + messageId: IMessage['_id']; + description: string; +}; + +const ChatReportMessageSchema = { + type: 'object', + properties: { + messageId: { + type: 'string', + }, + description: { + type: 'string', + }, + }, + required: ['messageId', 'description'], + additionalProperties: false, +}; + +export const isChatReportMessageProps = ajv.compile<ChatReportMessage>(ChatReportMessageSchema); + +type ChatGetThreadsList = { + rid: IRoom['_id']; + type: 'unread' | 'following' | 'all'; + text?: string; + offset: number; + count: number; +}; + +const ChatGetThreadsListSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + type: { + type: 'string', + }, + text: { + type: 'string', + nullable: true, + }, + offset: { + type: 'number', + }, + count: { + type: 'number', + }, + }, + required: ['rid', 'type', 'offset', 'count'], + additionalProperties: false, +}; + +export const isChatGetThreadsListProps = ajv.compile<ChatGetThreadsList>(ChatGetThreadsListSchema); + +type ChatSyncThreadsList = { + rid: IRoom['_id']; + updatedSince: string; +}; + +const ChatSyncThreadsListSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + updatedSince: { + type: 'string', + }, + }, + required: ['rid', 'updatedSince'], + additionalProperties: false, +}; + +export const isChatSyncThreadsListProps = ajv.compile<ChatSyncThreadsList>(ChatSyncThreadsListSchema); + +type ChatDelete = { + msgId: IMessage['_id']; + roomId: IRoom['_id']; +}; + +const ChatDeleteSchema = { + type: 'object', + properties: { + msgId: { + type: 'string', + }, + roomId: { + type: 'string', + }, + }, + required: ['msgId', 'roomId'], + additionalProperties: false, +}; + +export const isChatDeleteProps = ajv.compile<ChatDelete>(ChatDeleteSchema); + +type ChatReact = { emoji: string; messageId: IMessage['_id'] } | { reaction: string; messageId: IMessage['_id'] }; + +const ChatReactSchema = { + oneOf: [ + { + type: 'object', + properties: { + emoji: { + type: 'string', + }, + messageId: { + type: 'string', + }, + }, + required: ['emoji', 'messageId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + reaction: { + type: 'string', + }, + messageId: { + type: 'string', + }, + }, + required: ['reaction', 'messageId'], + additionalProperties: false, + }, + ], +}; + +export const isChatReactProps = ajv.compile<ChatReact>(ChatReactSchema); + +/** + * The param `ignore` cannot be boolean, since this is a GET method. Use strings 'true' or 'false' instead. + * @param {string} ignore + */ +type ChatIgnoreUser = { + rid: string; + userId: string; + ignore: string; +}; + +const ChatIgnoreUserSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + userId: { + type: 'string', + }, + ignore: { + type: 'string', + }, + }, + required: ['rid', 'userId', 'ignore'], + additionalProperties: false, +}; + +export const isChatIgnoreUserProps = ajv.compile<ChatIgnoreUser>(ChatIgnoreUserSchema); + +type ChatSearch = { + roomId: IRoom['_id']; + searchText: string; + count: number; + offset: number; +}; + +const ChatSearchSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + searchText: { + type: 'string', + }, + count: { + type: 'number', + }, + offset: { + type: 'number', + }, + }, + required: ['roomId', 'searchText', 'count', 'offset'], + additionalProperties: false, +}; + +export const isChatSearchProps = ajv.compile<ChatSearch>(ChatSearchSchema); + +type ChatUpdate = { + roomId: IRoom['_id']; + msgId: string; + text: string; +}; + +const ChatUpdateSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + msgId: { + type: 'string', + }, + text: { + type: 'string', + }, + }, + required: ['roomId', 'msgId', 'text'], + additionalProperties: false, +}; + +export const isChatUpdateProps = ajv.compile<ChatUpdate>(ChatUpdateSchema); + +type ChatGetMessageReadReceipts = { + messageId: IMessage['_id']; +}; + +const ChatGetMessageReadReceiptsSchema = { + type: 'object', + properties: { + messageId: { + type: 'string', + }, + }, + required: ['messageId'], + additionalProperties: false, +}; + +export const isChatGetMessageReadReceiptsProps = ajv.compile<ChatGetMessageReadReceipts>(ChatGetMessageReadReceiptsSchema); export type ChatEndpoints = { 'chat.getMessage': { - GET: (params: { msgId: IMessage['_id'] }) => { + GET: (params: ChatGetMessage) => { message: IMessage; }; }; 'chat.followMessage': { - POST: (params: { mid: IMessage['_id'] }) => void; + POST: (params: ChatFollowMessage) => void; }; 'chat.unfollowMessage': { - POST: (params: { mid: IMessage['_id'] }) => void; + POST: (params: ChatUnfollowMessage) => void; }; 'chat.starMessage': { - POST: (params: { messageId: IMessage['_id'] }) => void; + POST: (params: ChatStarMessage) => void; }; 'chat.unStarMessage': { - POST: (params: { messageId: IMessage['_id'] }) => void; + POST: (params: ChatUnstarMessage) => void; }; 'chat.pinMessage': { - POST: (params: { messageId: IMessage['_id'] }) => void; + POST: (params: ChatPinMessage) => void; }; 'chat.unPinMessage': { - POST: (params: { messageId: IMessage['_id'] }) => void; + POST: (params: ChatUnpinMessage) => void; }; 'chat.reportMessage': { - POST: (params: { messageId: IMessage['_id']; description: string }) => void; + POST: (params: ChatReportMessage) => void; }; 'chat.getDiscussions': { - GET: (params: { roomId: IRoom['_id']; text?: string; offset: number; count: number }) => { + GET: (params: ChatGetDiscussions) => { messages: IMessage[]; total: number; }; }; 'chat.getThreadsList': { - GET: (params: { rid: IRoom['_id']; type: 'unread' | 'following' | 'all'; text?: string; offset: number; count: number }) => { + GET: (params: ChatGetThreadsList) => { threads: IMessage[]; total: number; }; }; 'chat.syncThreadsList': { - GET: (params: { rid: IRoom['_id']; updatedSince: string }) => { + GET: (params: ChatSyncThreadsList) => { threads: { update: IMessage[]; remove: IMessage[]; @@ -48,29 +434,29 @@ export type ChatEndpoints = { }; }; 'chat.delete': { - POST: (params: { msgId: string; roomId: string }) => { + POST: (params: ChatDelete) => { _id: string; ts: string; message: Pick<IMessage, '_id' | 'rid' | 'u'>; }; }; 'chat.react': { - POST: (params: { emoji: string; messageId: string } | { reaction: string; messageId: string }) => void; + POST: (params: ChatReact) => void; }; 'chat.ignoreUser': { - GET: (params: { rid: string; userId: string; ignore: boolean }) => {}; + GET: (params: ChatIgnoreUser) => {}; }; 'chat.search': { - GET: (params: { roomId: IRoom['_id']; searchText: string; count: number; offset: number }) => { + GET: (params: ChatSearch) => { messages: IMessage[]; }; }; 'chat.update': { - POST: (params: { roomId: IRoom['_id']; msgId: string; text: string }) => { + POST: (params: ChatUpdate) => { messages: IMessage; }; }; 'chat.getMessageReadReceipts': { - GET: (params: { messageId: string }) => { receipts: ReadReceipt[] }; + GET: (params: ChatGetMessageReadReceipts) => { receipts: ReadReceipt[] }; }; }; diff --git a/packages/rest-typings/src/v1/cloud.ts b/packages/rest-typings/src/v1/cloud.ts index c5f56e26f097..e27289566711 100644 --- a/packages/rest-typings/src/v1/cloud.ts +++ b/packages/rest-typings/src/v1/cloud.ts @@ -1,16 +1,82 @@ import type { CloudRegistrationIntentData, CloudConfirmationPollData, CloudRegistrationStatus } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type CloudManualRegister = { + cloudBlob: string; +}; + +const CloudManualRegisterSchema = { + type: 'object', + properties: { + cloudBlob: { + type: 'string', + }, + }, + required: ['cloudBlob'], + additionalProperties: false, +}; + +export const isCloudManualRegisterProps = ajv.compile<CloudManualRegister>(CloudManualRegisterSchema); + +type CloudCreateRegistrationIntent = { + resend: boolean; + email: string; +}; + +const CloudCreateRegistrationIntentSchema = { + type: 'object', + properties: { + resend: { + type: 'boolean', + }, + email: { + type: 'string', + }, + }, + required: ['resend', 'email'], + additionalProperties: false, +}; + +export const isCloudCreateRegistrationIntentProps = ajv.compile<CloudCreateRegistrationIntent>(CloudCreateRegistrationIntentSchema); + +type CloudConfirmationPoll = { + deviceCode: string; + resend?: string; +}; + +const CloudConfirmationPollSchema = { + type: 'object', + properties: { + deviceCode: { + type: 'string', + }, + resend: { + type: 'string', + nullable: true, + }, + }, + required: ['deviceCode'], + optionalProperties: ['resend'], + additionalProperties: false, +}; + +export const isCloudConfirmationPollProps = ajv.compile<CloudConfirmationPoll>(CloudConfirmationPollSchema); export type CloudEndpoints = { 'cloud.manualRegister': { - POST: (params: { cloudBlob: string }) => void; + POST: (params: CloudManualRegister) => void; }; 'cloud.createRegistrationIntent': { - POST: (params: { resend: boolean; email: string }) => { + POST: (params: CloudCreateRegistrationIntent) => { intentData: CloudRegistrationIntentData; }; }; 'cloud.confirmationPoll': { - GET: (params: { deviceCode: string; resend?: boolean }) => { + GET: (params: CloudConfirmationPoll) => { pollData: CloudConfirmationPollData; }; }; diff --git a/packages/rest-typings/src/v1/commands.ts b/packages/rest-typings/src/v1/commands.ts new file mode 100644 index 000000000000..32e14b1c0393 --- /dev/null +++ b/packages/rest-typings/src/v1/commands.ts @@ -0,0 +1,42 @@ +import type { SlashCommand, SlashCommandPreviews } from '../../../core-typings/dist'; +import type { PaginatedRequest } from '../helpers/PaginatedRequest'; +import type { PaginatedResult } from '../helpers/PaginatedResult'; + +export type CommandsEndpoints = { + 'commands.get': { + GET: (params: { command: string }) => { + command: SlashCommand; + }; + }; + 'commands.list': { + GET: ( + params: PaginatedRequest<{ + fields?: string; + }>, + ) => PaginatedResult<{ + commands: SlashCommand[]; + }>; + }; + 'commands.run': { + POST: (params: { command: string; params?: string; roomId: string; tmid?: string; triggerId: string }) => { + result: unknown; + }; + }; + 'commands.preview': { + GET: (params: { command: string; params?: string; roomId: string }) => { + preview: SlashCommandPreviews; + }; + POST: (params: { + command: string; + params?: string; + roomId: string; + previewItem: { + id: string; + type: string; + value: string; + }; + triggerId: string; + tmid?: string; + }) => void; + }; +}; diff --git a/packages/rest-typings/src/v1/customSounds.ts b/packages/rest-typings/src/v1/customSounds.ts index 4a8f75a68089..84efd66ecb60 100644 --- a/packages/rest-typings/src/v1/customSounds.ts +++ b/packages/rest-typings/src/v1/customSounds.ts @@ -1,14 +1,44 @@ +import type { ICustomSound } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type CustomSoundsList = PaginatedRequest<{ query: string }>; + +const CustomSoundsListSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + }, + }, + required: ['query'], + additionalProperties: false, +}; + +export const isCustomSoundsListProps = ajv.compile<CustomSoundsList>(CustomSoundsListSchema); + export type CustomSoundEndpoint = { 'custom-sounds.list': { - GET: (params: PaginatedRequest<{ query: string }>) => PaginatedResult<{ - sounds: { - _id: string; - name: string; - extension: string; - }[]; + GET: (params: CustomSoundsList) => PaginatedResult<{ + sounds: ICustomSound[]; }>; }; }; diff --git a/packages/rest-typings/src/v1/customUserStatus.ts b/packages/rest-typings/src/v1/customUserStatus.ts index 9f9f8350180f..dfd30aa9a117 100644 --- a/packages/rest-typings/src/v1/customUserStatus.ts +++ b/packages/rest-typings/src/v1/customUserStatus.ts @@ -1,12 +1,57 @@ -import type { IUserStatus } from '@rocket.chat/core-typings'; +import type { ICustomUserStatus, IUserStatus } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type CustomUserStatusListProps = PaginatedRequest<{ query: string }>; + +const CustomUserStatusListSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + }, + }, + required: ['query'], + additionalProperties: false, +}; + +export const isCustomUserStatusListProps = ajv.compile<CustomUserStatusListProps>(CustomUserStatusListSchema); + export type CustomUserStatusEndpoints = { 'custom-user-status.list': { - GET: (params: PaginatedRequest<{ query: string }>) => PaginatedResult<{ + GET: (params: CustomUserStatusListProps) => PaginatedResult<{ statuses: IUserStatus[]; }>; }; + 'custom-user-status.create': { + POST: (params: { name: string; statusType?: string }) => { + customUserStatus: ICustomUserStatus; + }; + }; + 'custom-user-status.delete': { + POST: (params: { customUserStatusId: string }) => void; + }; + 'custom-user-status.update': { + POST: (params: { id: string; name?: string; statusType?: string }) => { + customUserStatus: ICustomUserStatus; + }; + }; }; diff --git a/packages/rest-typings/src/v1/directory.ts b/packages/rest-typings/src/v1/directory.ts index fda67ddf9810..af0ad5be6d8d 100644 --- a/packages/rest-typings/src/v1/directory.ts +++ b/packages/rest-typings/src/v1/directory.ts @@ -1,14 +1,42 @@ import type { IRoom } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; +import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type DirectoryProps = PaginatedRequest<{}>; + +const DirectorySchema = { + type: 'object', + properties: { + query: { + type: 'string', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isDirectoryProps = ajv.compile<DirectoryProps>(DirectorySchema); + export type DirectoryEndpoint = { directory: { - GET: (params: { - query: { [key: string]: string }; - count: number; - offset: number; - sort: { [key: string]: number }; - }) => PaginatedResult<{ result: IRoom[] }>; + GET: (params: DirectoryProps) => PaginatedResult<{ result: IRoom[] }>; }; }; diff --git a/packages/rest-typings/src/v1/dm/DmCloseProps.ts b/packages/rest-typings/src/v1/dm/DmCloseProps.ts new file mode 100644 index 000000000000..1a1ea759680a --- /dev/null +++ b/packages/rest-typings/src/v1/dm/DmCloseProps.ts @@ -0,0 +1,20 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +const ajv = new Ajv(); + +export type DmCloseProps = { + roomId: string; +}; + +const DmClosePropsSchema: JSONSchemaType<DmCloseProps> = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isDmCloseProps = ajv.compile(DmClosePropsSchema); diff --git a/packages/rest-typings/src/v1/dm/DmHistoryProps.ts b/packages/rest-typings/src/v1/dm/DmHistoryProps.ts new file mode 100644 index 000000000000..0a836961f366 --- /dev/null +++ b/packages/rest-typings/src/v1/dm/DmHistoryProps.ts @@ -0,0 +1,56 @@ +import type { PaginatedRequest } from '@rocket.chat/rest-typings/src/helpers/PaginatedRequest'; +import Ajv from 'ajv'; + +const ajv = new Ajv(); + +export type DmHistoryProps = PaginatedRequest<{ + roomId: string; + latest?: string; + oldest?: string; + inclusive?: 'false' | 'true'; + unreads?: 'true' | 'false'; + showThreadMessages?: 'false' | 'true'; +}>; + +const DmHistoryPropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + minLength: 1, + }, + latest: { + type: 'string', + minLength: 1, + }, + showThreadMessages: { + type: 'string', + enum: ['false', 'true'], + }, + oldest: { + type: 'string', + minLength: 1, + }, + inclusive: { + type: 'string', + enum: ['false', 'true'], + }, + unreads: { + type: 'string', + enum: ['true', 'false'], + }, + count: { + type: 'number', + }, + offset: { + type: 'number', + }, + sort: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isDmHistoryProps = ajv.compile<DmHistoryProps>(DmHistoryPropsSchema); diff --git a/packages/rest-typings/src/v1/dm/DmKickProps.ts b/packages/rest-typings/src/v1/dm/DmKickProps.ts new file mode 100644 index 000000000000..beca20ef5dc8 --- /dev/null +++ b/packages/rest-typings/src/v1/dm/DmKickProps.ts @@ -0,0 +1,24 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +const ajv = new Ajv(); + +type DmKickProps = { + roomId: string; + userId: string; +}; + +const DmKickPropsSchema: JSONSchemaType<DmKickProps> = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + userId: { + type: 'string', + }, + }, + required: ['roomId', 'userId'], + additionalProperties: false, +}; + +export const isDmKickProps = ajv.compile(DmKickPropsSchema); diff --git a/packages/rest-typings/src/v1/dm/DmLeaveProps.ts b/packages/rest-typings/src/v1/dm/DmLeaveProps.ts new file mode 100644 index 000000000000..a91ee8ba9a91 --- /dev/null +++ b/packages/rest-typings/src/v1/dm/DmLeaveProps.ts @@ -0,0 +1,20 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +const ajv = new Ajv(); + +export type DmLeaveProps = { + roomId: string; +}; + +const DmLeavePropsSchema: JSONSchemaType<DmLeaveProps> = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isDmLeaveProps = ajv.compile(DmLeavePropsSchema); diff --git a/packages/rest-typings/src/v1/dm/im.ts b/packages/rest-typings/src/v1/dm/im.ts index 927f7ac3458f..87c4adfe4fa7 100644 --- a/packages/rest-typings/src/v1/dm/im.ts +++ b/packages/rest-typings/src/v1/dm/im.ts @@ -2,9 +2,12 @@ import type { IMessage, IRoom, IUser, IUpload } from '@rocket.chat/core-typings' import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; import type { PaginatedResult } from '../../helpers/PaginatedResult'; +import type { DmCloseProps } from './DmCloseProps'; import type { DmCreateProps } from './DmCreateProps'; import type { DmDeleteProps } from './DmDeleteProps'; import type { DmFileProps } from './DmFileProps'; +import type { DmHistoryProps } from './DmHistoryProps'; +import type { DmLeaveProps } from './DmLeaveProps'; import type { DmMemberProps } from './DmMembersProps'; import type { DmMessagesProps } from './DmMessagesProps'; @@ -18,7 +21,13 @@ export type ImEndpoints = { POST: (params: DmDeleteProps) => void; }; 'im.close': { - POST: (params: { roomId: string }) => void; + POST: (params: DmCloseProps) => void; + }; + 'im.kick': { + POST: (params: DmCloseProps) => void; + }; + 'im.leave': { + POST: (params: DmLeaveProps) => void; }; 'im.counters': { GET: (params: { roomId: string; userId?: string }) => { @@ -37,16 +46,7 @@ export type ImEndpoints = { }>; }; 'im.history': { - GET: ( - params: PaginatedRequest<{ - roomId: string; - latest?: string; - oldest?: string; - inclusive?: string; - unreads?: string; - showThreadMessages?: string; - }>, - ) => { + GET: (params: DmHistoryProps) => { messages: Pick<IMessage, '_id' | 'rid' | 'msg' | 'ts' | '_updatedAt' | 'u'>[]; }; }; diff --git a/packages/rest-typings/src/v1/dns.ts b/packages/rest-typings/src/v1/dns.ts index b2d553e036f2..28a630ea3352 100644 --- a/packages/rest-typings/src/v1/dns.ts +++ b/packages/rest-typings/src/v1/dns.ts @@ -1,11 +1,51 @@ +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type DnsResolveTxtProps = { + url: string; +}; + +const dnsResolveTxtPropsSchema = { + type: 'object', + properties: { + url: { + type: 'string', + }, + }, + required: ['url'], + additionalProperties: false, +}; + +export const isDnsResolveTxtProps = ajv.compile<DnsResolveTxtProps>(dnsResolveTxtPropsSchema); + +type DnsResolveSrvProps = { + url: string; +}; + +const DnsResolveSrvSchema = { + type: 'object', + properties: { + url: { + type: 'string', + }, + }, + required: ['url'], + additionalProperties: false, +}; + +export const isDnsResolveSrvProps = ajv.compile<DnsResolveSrvProps>(DnsResolveSrvSchema); + export type DnsEndpoints = { 'dns.resolve.srv': { - GET: (params: { url: string }) => { + GET: (params: DnsResolveSrvProps) => { resolved: Record<string, string | number>; }; }; 'dns.resolve.txt': { - POST: (params: { url: string }) => { + POST: (params: DnsResolveTxtProps) => { resolved: string; // resolved: Record<string, string | number>; }; diff --git a/packages/rest-typings/src/v1/e2e.ts b/packages/rest-typings/src/v1/e2e.ts index d8bf5f77312c..62ce4110ddbb 100644 --- a/packages/rest-typings/src/v1/e2e.ts +++ b/packages/rest-typings/src/v1/e2e.ts @@ -1,19 +1,107 @@ +/* eslint-disable @typescript-eslint/camelcase */ import type { IUser } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type E2eSetUserPublicAndPrivateKeysProps = { + public_key: string; + private_key: string; +}; + +const E2eSetUserPublicAndPrivateKeysSchema = { + type: 'object', + properties: { + public_key: { + type: 'string', + }, + private_key: { + type: 'string', + }, + }, + required: ['public_key', 'private_key'], + additionalProperties: false, +}; + +export const isE2eSetUserPublicAndPrivateKeysProps = ajv.compile<E2eSetUserPublicAndPrivateKeysProps>(E2eSetUserPublicAndPrivateKeysSchema); + +type E2eGetUsersOfRoomWithoutKeyProps = { rid: string }; + +const E2eGetUsersOfRoomWithoutKeySchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + }, + required: ['rid'], + additionalProperties: false, +}; + +export const isE2eGetUsersOfRoomWithoutKeyProps = ajv.compile<E2eGetUsersOfRoomWithoutKeyProps>(E2eGetUsersOfRoomWithoutKeySchema); + +type E2eUpdateGroupKeyProps = { + uid: string; + rid: string; + key: string; +}; + +const E2eUpdateGroupKeySchema = { + type: 'object', + properties: { + uid: { + type: 'string', + }, + rid: { + type: 'string', + }, + key: { + type: 'string', + }, + }, + required: ['uid', 'rid', 'key'], + additionalProperties: false, +}; + +export const isE2eUpdateGroupKeyProps = ajv.compile<E2eUpdateGroupKeyProps>(E2eUpdateGroupKeySchema); + +type E2eSetRoomKeyIdProps = { + rid: string; + keyID: string; +}; + +const E2eSetRoomKeyIdSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + keyID: { + type: 'string', + }, + }, + required: ['rid', 'keyID'], + additionalProperties: false, +}; + +export const isE2eSetRoomKeyIdProps = ajv.compile<E2eSetRoomKeyIdProps>(E2eSetRoomKeyIdSchema); export type E2eEndpoints = { 'e2e.setUserPublicAndPrivateKeys': { - POST: (params: { public_key: string; private_key: string }) => void; + POST: (params: E2eSetUserPublicAndPrivateKeysProps) => void; }; 'e2e.getUsersOfRoomWithoutKey': { - GET: (params: { rid: string }) => { + GET: (params: E2eGetUsersOfRoomWithoutKeyProps) => { users: Pick<IUser, '_id' | 'e2e'>[]; }; }; 'e2e.updateGroupKey': { - POST: (params: { uid: string; rid: string; key: string }) => {}; + POST: (params: E2eUpdateGroupKeyProps) => {}; }; 'e2e.setRoomKeyID': { - POST: (params: { rid: string; keyID: string }) => {}; + POST: (params: E2eSetRoomKeyIdProps) => {}; }; 'e2e.fetchMyKeys': { GET: () => { public_key: string; private_key: string }; diff --git a/packages/rest-typings/src/v1/email-inbox.ts b/packages/rest-typings/src/v1/email-inbox.ts index afa4745ed86d..d0347fe7e829 100644 --- a/packages/rest-typings/src/v1/email-inbox.ts +++ b/packages/rest-typings/src/v1/email-inbox.ts @@ -1,43 +1,174 @@ import type { IEmailInbox } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type EmailInboxListProps = PaginatedRequest<{ query?: string }>; + +const EmailInboxListPropsSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isEmailInboxList = ajv.compile<EmailInboxListProps>(EmailInboxListPropsSchema); + +type EmailInboxProps = { + _id?: string; + name: string; + email: string; + active: boolean; // POST method + description: string; + senderInfo: string; + department: string; + smtp: { + password: string; + port: number; + secure: boolean; + server: string; + username: string; + }; + imap: { + password: string; + port: number; + secure: boolean; + server: string; + username: string; + }; +}; + +const EmailInboxPropsSchema = { + type: 'object', + properties: { + _id: { + type: 'string', + nullable: true, + }, + name: { + type: 'string', + }, + email: { + type: 'string', + }, + active: { + type: 'boolean', + }, + description: { + type: 'string', + }, + senderInfo: { + type: 'string', + }, + department: { + type: 'string', + }, + + smtp: { + type: 'object', + properties: { + password: { + type: 'string', + }, + port: { + type: 'number', + }, + secure: { + type: 'boolean', + }, + server: { + type: 'string', + }, + username: { + type: 'string', + }, + }, + required: ['password', 'port', 'secure', 'server', 'username'], + additionalProperties: false, + }, + + imap: { + type: 'object', + properties: { + password: { + type: 'string', + }, + port: { + type: 'number', + }, + secure: { + type: 'boolean', + }, + server: { + type: 'string', + }, + username: { + type: 'string', + }, + }, + required: ['password', 'port', 'secure', 'server', 'username'], + additionalProperties: false, + }, + }, + + required: ['name', 'email', 'active', 'description', 'senderInfo', 'department', 'smtp', 'imap'], + additionalProperties: false, +}; + +export const isEmailInbox = ajv.compile<EmailInboxProps>(EmailInboxPropsSchema); + +type EmailInboxSearchProps = { + email: string; +}; + +const EmailInboxSearchPropsSchema = { + type: 'object', + properties: { + email: { + type: 'string', + }, + }, + required: ['email'], + additionalProperties: false, +}; + +export const isEmailInboxSearch = ajv.compile<EmailInboxSearchProps>(EmailInboxSearchPropsSchema); + export type EmailInboxEndpoints = { 'email-inbox.list': { - GET: (params: PaginatedRequest<{ query?: string }>) => PaginatedResult<{ emailInboxes: IEmailInbox[] }>; + GET: (params: EmailInboxListProps) => PaginatedResult<{ emailInboxes: IEmailInbox[] }>; }; 'email-inbox': { - POST: (params: { - _id?: string; - name: string; - email: string; - active: boolean; - description: string; - senderInfo: string; - department: string; - smtp: { - password: string; - port: number; - secure: boolean; - server: string; - username: string; - }; - imap: { - password: string; - port: number; - secure: boolean; - server: string; - username: string; - }; - }) => { _id: string }; + POST: (params: EmailInboxProps) => { _id: string }; }; 'email-inbox/:_id': { GET: (params: void) => IEmailInbox | null; DELETE: (params: void) => { _id: string }; }; 'email-inbox.search': { - GET: (params: { email: string }) => { emailInbox: IEmailInbox | null }; + GET: (params: EmailInboxSearchProps) => { emailInbox: IEmailInbox | null }; }; 'email-inbox.send-test/:_id': { POST: (params: void) => { _id: string }; diff --git a/packages/rest-typings/src/v1/emojiCustom.ts b/packages/rest-typings/src/v1/emojiCustom.ts index 0a52dddc940e..2532879dd9a9 100644 --- a/packages/rest-typings/src/v1/emojiCustom.ts +++ b/packages/rest-typings/src/v1/emojiCustom.ts @@ -1,8 +1,47 @@ import type { ICustomEmojiDescriptor } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type emojiCustomDeleteProps = { + emojiId: ICustomEmojiDescriptor['_id']; +}; + +const emojiCustomDeletePropsSchema = { + type: 'object', + properties: { + emojiId: { + type: 'string', + }, + }, + required: ['emojiId'], + additionalProperties: false, +}; + +export const isEmojiCustomDelete = ajv.compile<emojiCustomDeleteProps>(emojiCustomDeletePropsSchema); + +type emojiCustomList = { + query: string; +}; + +const emojiCustomListSchema = { + type: 'object', + properties: { + query: { + type: 'string', + }, + }, + required: ['query'], + additionalProperties: false, +}; + +export const isemojiCustomList = ajv.compile<emojiCustomList>(emojiCustomListSchema); + export type EmojiCustomEndpoints = { 'emoji-custom.all': { GET: (params: PaginatedRequest<{ query: string }, 'name'>) => { @@ -10,13 +49,13 @@ export type EmojiCustomEndpoints = { } & PaginatedResult; }; 'emoji-custom.list': { - GET: (params: { query: string }) => { + GET: (params: emojiCustomList) => { emojis?: { update: ICustomEmojiDescriptor[]; }; }; }; 'emoji-custom.delete': { - POST: (params: { emojiId: ICustomEmojiDescriptor['_id'] }) => void; + POST: (params: emojiCustomDeleteProps) => void; }; }; diff --git a/packages/rest-typings/src/v1/groups.ts b/packages/rest-typings/src/v1/groups.ts index 5cb5db378ffc..10540a3ecd62 100644 --- a/packages/rest-typings/src/v1/groups.ts +++ b/packages/rest-typings/src/v1/groups.ts @@ -1,16 +1,330 @@ import type { IMessage, IRoom, ITeam, IGetRoomRoles, IUser, IUpload } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type GroupsFilesProps = { + roomId: IRoom['_id']; + count: number; + sort: string; + query: string; +}; + +const GroupsFilesPropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + count: { + type: 'number', + }, + sort: { + type: 'string', + }, + query: { + type: 'string', + }, + }, + required: ['roomId', 'count', 'sort', 'query'], + additionalProperties: false, +}; + +export const isGroupsFilesProps = ajv.compile<GroupsFilesProps>(GroupsFilesPropsSchema); + +type GroupsMembersProps = { + roomId: IRoom['_id']; + offset?: number; + count?: number; + filter?: string; + status?: string[]; +}; + +const GroupsMembersPropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + offset: { + type: 'number', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + filter: { + type: 'string', + nullable: true, + }, + status: { + type: 'array', + items: { type: 'string' }, + nullable: true, + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isGroupsMembersProps = ajv.compile<GroupsMembersProps>(GroupsMembersPropsSchema); + +type GroupsArchiveProps = { + roomId: IRoom['_id']; +}; + +const GroupsArchivePropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isGroupsArchiveProps = ajv.compile<GroupsArchiveProps>(GroupsArchivePropsSchema); + +type GroupsUnarchiveProps = { + roomId: IRoom['_id']; +}; + +const GroupsUnarchivePropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isGroupsUnarchiveProps = ajv.compile<GroupsUnarchiveProps>(GroupsUnarchivePropsSchema); + +type GroupsCreateProps = { + name: string; + members: string[]; + readOnly: boolean; + extraData: { + broadcast: boolean; + encrypted: boolean; + teamId?: string; + }; +}; + +const GroupsCreatePropsSchema = { + type: 'object', + properties: { + name: { + type: 'string', + }, + members: { + type: 'array', + items: { type: 'string' }, + }, + readOnly: { + type: 'boolean', + }, + extraData: { + type: 'object', + properties: { + broadcast: { + type: 'boolean', + }, + encrypted: { + type: 'boolean', + }, + teamId: { + type: 'string', + nullable: true, + }, + }, + required: ['broadcast', 'encrypted'], + additionalProperties: false, + }, + }, + required: ['name', 'members', 'readOnly', 'extraData'], + additionalProperties: false, +}; + +export const isGroupsCreateProps = ajv.compile<GroupsCreateProps>(GroupsCreatePropsSchema); + +type GroupsConvertToTeamProps = { + roomId: string; + roomName: string; +}; + +const GroupsConvertToTeamPropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + roomName: { + type: 'string', + }, + }, + required: ['roomId', 'roomName'], + additionalProperties: false, +}; + +export const isGroupsConvertToTeamProps = ajv.compile<GroupsConvertToTeamProps>(GroupsConvertToTeamPropsSchema); + +type GroupsCountersProps = { + roomId: string; +}; + +const GroupsCountersPropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isGroupsCountersProps = ajv.compile<GroupsCountersProps>(GroupsCountersPropsSchema); + +type GroupsCloseProps = { + roomId: string; +}; + +const GroupsClosePropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isGroupsCloseProps = ajv.compile<GroupsCloseProps>(GroupsClosePropsSchema); + +type GroupsDeleteProps = { + roomId: string; +}; + +const GroupsDeletePropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isGroupsDeleteProps = ajv.compile<GroupsDeleteProps>(GroupsDeletePropsSchema); + +type GroupsLeaveProps = { + roomId: string; +}; + +const GroupsLeavePropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isGroupsLeaveProps = ajv.compile<GroupsLeaveProps>(GroupsLeavePropsSchema); + +type GroupsRolesProps = { + roomId: string; +}; + +const GroupsRolesPropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isGroupsRolesProps = ajv.compile<GroupsRolesProps>(GroupsRolesPropsSchema); + +type GroupsKickProps = { + roomId: string; + userId: string; +}; + +const GroupsKickPropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + userId: { + type: 'string', + }, + }, + required: ['roomId', 'userId'], + additionalProperties: false, +}; + +export const isGroupsKickProps = ajv.compile<GroupsKickProps>(GroupsKickPropsSchema); + +type GroupsMessageProps = PaginatedRequest<{ + roomId: IRoom['_id']; +}>; + +const GroupsMessagePropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isGroupsMessageProps = ajv.compile<GroupsMessageProps>(GroupsMessagePropsSchema); + export type GroupsEndpoints = { 'groups.files': { - GET: (params: PaginatedRequest<{ roomId: IRoom['_id']; query: string }>) => PaginatedResult<{ + GET: (params: GroupsFilesProps) => PaginatedResult<{ files: IUpload[]; }>; }; 'groups.members': { - GET: (params: { roomId: IRoom['_id']; offset?: number; count?: number; filter?: string; status?: string[] }) => { + GET: (params: GroupsMembersProps) => { count: number; offset: number; members: IUser[]; @@ -23,30 +337,21 @@ export type GroupsEndpoints = { }>; }; 'groups.archive': { - POST: (params: { roomId: string }) => void; + POST: (params: GroupsArchiveProps) => void; }; 'groups.unarchive': { - POST: (params: { roomId: string }) => void; + POST: (params: GroupsUnarchiveProps) => void; }; 'groups.create': { - POST: (params: { - name: string; - members: string[]; - readOnly: boolean; - extraData: { - broadcast: boolean; - encrypted: boolean; - teamId?: string; - }; - }) => { + POST: (params: GroupsCreateProps) => { group: Partial<IRoom>; }; }; 'groups.convertToTeam': { - POST: (params: { roomId: string; roomName: string }) => { team: ITeam }; + POST: (params: GroupsConvertToTeamProps) => { team: ITeam }; }; 'groups.counters': { - GET: (params: { roomId: string }) => { + GET: (params: GroupsCountersProps) => { joined: boolean; members: number; unreads: number; @@ -57,28 +362,23 @@ export type GroupsEndpoints = { }; }; 'groups.close': { - POST: (params: { roomId: string }) => {}; + POST: (params: GroupsCloseProps) => {}; }; 'groups.kick': { - POST: (params: { roomId: string; userId: string }) => {}; + POST: (params: GroupsKickProps) => {}; }; 'groups.delete': { - POST: (params: { roomId: string }) => {}; + POST: (params: GroupsDeleteProps) => {}; }; 'groups.leave': { - POST: (params: { roomId: string }) => {}; + POST: (params: GroupsLeaveProps) => {}; }; 'groups.roles': { - GET: (params: { roomId: string }) => { roles: IGetRoomRoles[] }; + GET: (params: GroupsRolesProps) => { roles: IGetRoomRoles[] }; }; 'groups.messages': { - GET: (params: { - roomId: IRoom['_id']; - query: { 'mentions._id': { $in: string[] } } | { 'starred._id': { $in: string[] } } | { pinned: boolean }; - offset: number; - sort: { ts: number }; - }) => { + GET: (params: GroupsMessageProps) => PaginatedResult<{ messages: IMessage[]; - }; + }>; }; }; diff --git a/packages/rest-typings/src/v1/invites.ts b/packages/rest-typings/src/v1/invites.ts index 200302c99711..fda658af2267 100644 --- a/packages/rest-typings/src/v1/invites.ts +++ b/packages/rest-typings/src/v1/invites.ts @@ -1,4 +1,43 @@ import type { IInvite, IRoom } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type v1UseInviteTokenProps = { + token: string; +}; + +const v1UseInviteTokenPropsSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + }, + required: ['token'], + additionalProperties: false, +}; + +export const isV1UseInviteTokenProps = ajv.compile<v1UseInviteTokenProps>(v1UseInviteTokenPropsSchema); + +type v1ValidateInviteTokenProps = { + token: string; +}; + +const v1ValidateInviteTokenPropsSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + }, + required: ['token'], + additionalProperties: false, +}; + +export const isV1ValidateInviteTokenProps = ajv.compile<v1ValidateInviteTokenProps>(v1ValidateInviteTokenPropsSchema); export type InvitesEndpoints = { 'listInvites': { @@ -8,7 +47,7 @@ export type InvitesEndpoints = { DELETE: () => void; }; '/v1/useInviteToken': { - POST: (params: { token: string }) => { + POST: (params: v1UseInviteTokenProps) => { room: { rid: IRoom['_id']; prid: IRoom['prid']; @@ -19,6 +58,6 @@ export type InvitesEndpoints = { }; }; '/v1/validateInviteToken': { - POST: (params: { token: string }) => { valid: boolean }; + POST: (params: v1ValidateInviteTokenProps) => { valid: boolean }; }; }; diff --git a/packages/rest-typings/src/v1/ldap.ts b/packages/rest-typings/src/v1/ldap.ts index 45c482c0957d..3fd343cf4c03 100644 --- a/packages/rest-typings/src/v1/ldap.ts +++ b/packages/rest-typings/src/v1/ldap.ts @@ -1,3 +1,26 @@ +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type ldapTestSearchProps = { + username: string; +}; + +const ldapTestSearchPropsSchema = { + type: 'object', + properties: { + username: { + type: 'string', + }, + }, + required: ['username'], + additionalProperties: false, +}; + +export const isLdapTestSearch = ajv.compile<ldapTestSearchProps>(ldapTestSearchPropsSchema); + export type LDAPEndpoints = { 'ldap.testConnection': { POST: () => { @@ -5,7 +28,7 @@ export type LDAPEndpoints = { }; }; 'ldap.testSearch': { - POST: (params: { username: string }) => { + POST: (params: ldapTestSearchProps) => { message: string; }; }; diff --git a/packages/rest-typings/src/v1/licenses.ts b/packages/rest-typings/src/v1/licenses.ts index f1bb124a956b..de477d37a813 100644 --- a/packages/rest-typings/src/v1/licenses.ts +++ b/packages/rest-typings/src/v1/licenses.ts @@ -1,11 +1,33 @@ import type { ILicense } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type licensesAddProps = { + license: string; +}; + +const licensesAddPropsSchema = { + type: 'object', + properties: { + license: { + type: 'string', + }, + }, + required: ['license'], + additionalProperties: false, +}; + +export const isLicensesAddProps = ajv.compile<licensesAddProps>(licensesAddPropsSchema); export type LicensesEndpoints = { 'licenses.get': { GET: () => { licenses: Array<ILicense> }; }; 'licenses.add': { - POST: (params: { license: string }) => void; + POST: (params: licensesAddProps) => void; }; 'licenses.maxActiveUsers': { GET: () => { maxActiveUsers: number | null; activeUsers: number }; diff --git a/packages/rest-typings/src/v1/oauthapps.ts b/packages/rest-typings/src/v1/oauthapps.ts index 89f993d50b21..4928257efd1e 100644 --- a/packages/rest-typings/src/v1/oauthapps.ts +++ b/packages/rest-typings/src/v1/oauthapps.ts @@ -1,7 +1,9 @@ import type { IOAuthApps, IUser } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; -const ajv = new Ajv(); +const ajv = new Ajv({ + coerceTypes: true, +}); export type OauthAppsGetParams = { clientId: string } | { appId: string }; diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 3b29ad0b9274..fc2ab9e23fd9 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -13,12 +13,770 @@ import type { IRoom, ISetting, } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; type booleanString = 'true' | 'false'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type LivechatVisitorsInfo = { + visitorId: string; +}; + +const LivechatVisitorsInfoSchema = { + type: 'object', + properties: { + visitorId: { + type: 'string', + }, + }, + required: ['visitorId'], + additionalProperties: false, +}; + +export const isLivechatVisitorsInfoProps = ajv.compile<LivechatVisitorsInfo>(LivechatVisitorsInfoSchema); + +type LivechatRoomOnHold = { + roomId: IRoom['_id']; +}; + +const LivechatRoomOnHoldSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isLivechatRoomOnHoldProps = ajv.compile<LivechatRoomOnHold>(LivechatRoomOnHoldSchema); + +type LivechatDepartmentId = { + onlyMyDepartments?: booleanString; + includeAgents?: booleanString; +}; + +const LivechatDepartmentIdSchema = { + type: 'object', + properties: { + onlyMyDepartments: { + type: 'string', + nullable: true, + }, + includeAgents: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isLivechatDepartmentIdProps = ajv.compile<LivechatDepartmentId>(LivechatDepartmentIdSchema); + +type LivechatDepartmentAutocomplete = { + selector: string; + onlyMyDepartments: booleanString; +}; + +const LivechatDepartmentAutocompleteSchema = { + type: 'object', + properties: { + selector: { + type: 'string', + }, + onlyMyDepartments: { + type: 'string', + }, + }, + required: ['selector', 'onlyMyDepartments'], + additionalProperties: false, +}; + +export const isLivechatDepartmentAutocompleteProps = ajv.compile<LivechatDepartmentAutocomplete>(LivechatDepartmentAutocompleteSchema); + +type LivechatDepartmentDepartmentIdAgentsGET = { + sort: string; +}; + +const LivechatDepartmentDepartmentIdAgentsGETSchema = { + type: 'object', + properties: { + sort: { + type: 'string', + }, + }, + required: ['sort'], + additionalProperties: false, +}; + +export const isLivechatDepartmentDepartmentIdAgentsGETProps = ajv.compile<LivechatDepartmentDepartmentIdAgentsGET>( + LivechatDepartmentDepartmentIdAgentsGETSchema, +); + +type LivechatDepartmentDepartmentIdAgentsPOST = { + upsert: string[]; + remove: string[]; +}; + +const LivechatDepartmentDepartmentIdAgentsPOSTSchema = { + type: 'object', + properties: { + upsert: { + type: 'array', + items: { + type: 'string', + }, + }, + remove: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + required: ['upsert', 'remove'], + additionalProperties: false, +}; + +export const isLivechatDepartmentDepartmentIdAgentsPOSTProps = ajv.compile<LivechatDepartmentDepartmentIdAgentsPOST>( + LivechatDepartmentDepartmentIdAgentsPOSTSchema, +); + +type LivechatVisitorTokenGet = { + token: string; +}; + +const LivechatVisitorTokenGetSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + }, + required: ['token'], + additionalProperties: false, +}; + +export const isLivechatVisitorTokenGetProps = ajv.compile<LivechatVisitorTokenGet>(LivechatVisitorTokenGetSchema); + +type LivechatVisitorTokenDelete = { + token: string; +}; + +const LivechatVisitorTokenDeleteSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + }, + required: ['token'], + additionalProperties: false, +}; + +export const isLivechatVisitorTokenDeleteProps = ajv.compile<LivechatVisitorTokenDelete>(LivechatVisitorTokenDeleteSchema); + +type LivechatVisitorTokenRoom = { + token: string; +}; + +const LivechatVisitorTokenRoomSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + }, + required: ['token'], + additionalProperties: false, +}; + +export const isLivechatVisitorTokenRoomProps = ajv.compile<LivechatVisitorTokenRoom>(LivechatVisitorTokenRoomSchema); + +type LivechatVisitorCallStatus = { + token: string; + callStatus: string; + rid: string; + callId: string; +}; + +const LivechatVisitorCallStatusSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + callStatus: { + type: 'string', + }, + rid: { + type: 'string', + }, + callId: { + type: 'string', + }, + }, + required: ['token', 'callStatus', 'rid', 'callId'], + additionalProperties: false, +}; + +export const isLivechatVisitorCallStatusProps = ajv.compile<LivechatVisitorCallStatus>(LivechatVisitorCallStatusSchema); + +type LivechatVisitorStatus = { + token: string; + status: string; +}; + +const LivechatVisitorStatusSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + status: { + type: 'string', + }, + }, + required: ['token', 'status'], + additionalProperties: false, +}; + +export const isLivechatVisitorStatusProps = ajv.compile<LivechatVisitorStatus>(LivechatVisitorStatusSchema); + +type LiveChatRoomJoin = { + roomId: string; +}; + +const LiveChatRoomJoinSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isLiveChatRoomJoinProps = ajv.compile<LiveChatRoomJoin>(LiveChatRoomJoinSchema); + +type LivechatMonitorsListProps = PaginatedRequest<{ text: string }>; + +const LivechatMonitorsListSchema = { + type: 'object', + properties: { + text: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['text'], + additionalProperties: false, +}; + +export const isLivechatMonitorsListProps = ajv.compile<LivechatMonitorsListProps>(LivechatMonitorsListSchema); + +type LivechatTagsListProps = PaginatedRequest<{ text: string }, 'name'>; + +const LivechatTagsListSchema = { + type: 'object', + properties: { + text: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['text'], + additionalProperties: false, +}; + +export const isLivechatTagsListProps = ajv.compile<LivechatTagsListProps>(LivechatTagsListSchema); + +type LivechatDepartmentProps = PaginatedRequest<{ + text: string; + onlyMyDepartments?: booleanString; + enabled?: booleanString; + excludeDepartmentId?: string; +}>; + +const LivechatDepartmentSchema = { + type: 'object', + properties: { + text: { + type: 'string', + nullable: true, + }, + onlyMyDepartments: { + type: 'string', + enum: ['true', 'false'], + nullable: true, + }, + enabled: { + type: 'string', + nullable: true, + }, + excludeDepartmentId: { + type: 'string', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + fields: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isLivechatDepartmentProps = ajv.compile<LivechatDepartmentProps>(LivechatDepartmentSchema); + +type LivechatDepartmentsAvailableByUnitIdProps = PaginatedRequest<{ text: string }>; + +const LivechatDepartmentsAvailableByUnitIdSchema = { + type: 'object', + properties: { + text: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['text'], + additionalProperties: false, +}; + +export const isLivechatDepartmentsAvailableByUnitIdProps = ajv.compile<LivechatDepartmentsAvailableByUnitIdProps>( + LivechatDepartmentsAvailableByUnitIdSchema, +); + +type LivechatDepartmentsByUnitProps = PaginatedRequest<{ text: string }>; + +const LivechatDepartmentsByUnitSchema = { + type: 'object', + properties: { + text: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['text'], + additionalProperties: false, +}; + +export const isLivechatDepartmentsByUnitProps = ajv.compile<LivechatDepartmentsByUnitProps>(LivechatDepartmentsByUnitSchema); + +type LivechatDepartmentsByUnitIdProps = PaginatedRequest<{ text: string }>; + +const LivechatDepartmentsByUnitIdSchema = { + type: 'object', + properties: { + text: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['text'], + additionalProperties: false, +}; + +export const isLivechatDepartmentsByUnitIdProps = ajv.compile<LivechatDepartmentsByUnitIdProps>(LivechatDepartmentsByUnitIdSchema); + +type LivechatUsersManagerGETProps = PaginatedRequest<{ text?: string }>; + +const LivechatUsersManagerGETSchema = { + type: 'object', + properties: { + text: { + type: 'string', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isLivechatUsersManagerGETProps = ajv.compile<LivechatUsersManagerGETProps>(LivechatUsersManagerGETSchema); + +type LivechatUsersManagerPOSTProps = PaginatedRequest<{ username: string }>; + +const LivechatUsersManagerPOSTSchema = { + type: 'object', + properties: { + username: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['username'], + additionalProperties: false, +}; + +export const isLivechatUsersManagerPOSTProps = ajv.compile<LivechatUsersManagerPOSTProps>(LivechatUsersManagerPOSTSchema); + +type LivechatQueueProps = { + agentId?: string; + includeOfflineAgents?: booleanString; + departmentId?: string; + offset: number; + count: number; + sort: string; +}; + +const LivechatQueuePropsSchema = { + type: 'object', + properties: { + agentId: { + type: 'string', + nullable: true, + }, + includeOfflineAgents: { + type: 'string', + nullable: true, + }, + departmentId: { + type: 'string', + nullable: true, + }, + count: { + type: 'number', + }, + offset: { + type: 'number', + }, + sort: { + type: 'string', + }, + }, + required: ['count', 'offset', 'sort'], + additionalProperties: false, +}; + +export const isLivechatQueueProps = ajv.compile<LivechatQueueProps>(LivechatQueuePropsSchema); + +type CannedResponsesProps = PaginatedRequest<{ + scope?: string; + departmentId?: string; + text?: string; +}>; + +const CannedResponsesPropsSchema = { + type: 'object', + properties: { + scope: { + type: 'string', + nullable: true, + }, + departmentId: { + type: 'string', + nullable: true, + }, + text: { + type: 'string', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isCannedResponsesProps = ajv.compile<CannedResponsesProps>(CannedResponsesPropsSchema); + +type LivechatCustomFieldsProps = PaginatedRequest<{ text: string }>; + +const LivechatCustomFieldsSchema = { + type: 'object', + properties: { + text: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['text'], + additionalProperties: false, +}; + +export const isLivechatCustomFieldsProps = ajv.compile<LivechatCustomFieldsProps>(LivechatCustomFieldsSchema); + +type LivechatRoomsProps = { + guest: string; + fname: string; + servedBy: string[]; + status: string; + department: string; + from: string; + to: string; + customFields: any; + current: number; + itemsPerPage: number; + tags: string[]; +}; + +const LivechatRoomsSchema = { + type: 'object', + properties: { + guest: { + type: 'string', + }, + fname: { + type: 'string', + }, + servedBy: { + type: 'array', + items: { + type: 'string', + }, + }, + status: { + type: 'string', + }, + department: { + type: 'string', + }, + from: { + type: 'string', + }, + to: { + type: 'string', + }, + customFields: { + type: 'object', + nullable: true, + }, + current: { + type: 'number', + }, + itemsPerPage: { + type: 'number', + }, + tags: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + required: ['guest', 'fname', 'servedBy', 'status', 'department', 'from', 'to', 'current', 'itemsPerPage'], + additionalProperties: false, +}; + +export const isLivechatRoomsProps = ajv.compile<LivechatRoomsProps>(LivechatRoomsSchema); + +type LivechatRidMessagesProps = PaginatedRequest<{ query: string }>; + +const LivechatRidMessagesSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + }, + }, + required: ['query'], + additionalProperties: false, +}; + +export const isLivechatRidMessagesProps = ajv.compile<LivechatRidMessagesProps>(LivechatRidMessagesSchema); + +type LivechatUsersAgentProps = PaginatedRequest<{ text?: string }>; + +const LivechatUsersAgentSchema = { + type: 'object', + properties: { + text: { + type: 'string', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isLivechatUsersAgentProps = ajv.compile<LivechatUsersAgentProps>(LivechatUsersAgentSchema); + export type OmnichannelEndpoints = { 'livechat/appearance': { GET: () => { @@ -26,7 +784,7 @@ export type OmnichannelEndpoints = { }; }; 'livechat/visitors.info': { - GET: (params: { visitorId: string }) => { + GET: (params: LivechatVisitorsInfo) => { visitor: { visitorEmails: Array<{ address: string; @@ -35,30 +793,23 @@ export type OmnichannelEndpoints = { }; }; 'livechat/room.onHold': { - POST: (params: { roomId: IRoom['_id'] }) => void; + POST: (params: LivechatRoomOnHold) => void; }; 'livechat/room.join': { - GET: (params: { roomId: IRoom['_id'] }) => { success: boolean }; + GET: (params: LiveChatRoomJoin) => { success: boolean }; }; 'livechat/monitors.list': { - GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ + GET: (params: LivechatMonitorsListProps) => PaginatedResult<{ monitors: ILivechatMonitor[]; }>; }; 'livechat/tags.list': { - GET: (params: PaginatedRequest<{ text: string }, 'name'>) => PaginatedResult<{ + GET: (params: LivechatTagsListProps) => PaginatedResult<{ tags: ILivechatTag[]; }>; }; 'livechat/department': { - GET: ( - params: PaginatedRequest<{ - text: string; - onlyMyDepartments?: booleanString; - enabled?: boolean; - excludeDepartmentId?: string; - }>, - ) => PaginatedResult<{ + GET: (params: LivechatDepartmentProps) => PaginatedResult<{ departments: ILivechatDepartment[]; }>; POST: (params: { department: Partial<ILivechatDepartment>; agents: string[] }) => { @@ -67,7 +818,7 @@ export type OmnichannelEndpoints = { }; }; 'livechat/department/:_id': { - GET: (params: { onlyMyDepartments?: booleanString; includeAgents?: booleanString }) => { + GET: (params: LivechatDepartmentId) => { department: ILivechatDepartmentRecord | null; agents?: ILivechatDepartmentAgents[]; }; @@ -78,27 +829,27 @@ export type OmnichannelEndpoints = { DELETE: () => void; }; 'livechat/department.autocomplete': { - GET: (params: { selector: string; onlyMyDepartments: booleanString }) => { + GET: (params: LivechatDepartmentAutocomplete) => { items: ILivechatDepartment[]; }; }; 'livechat/department/:departmentId/agents': { - GET: (params: { sort: string }) => PaginatedResult<{ agents: ILivechatDepartmentAgents[] }>; - POST: (params: { upsert: string[]; remove: string[] }) => void; + GET: (params: LivechatDepartmentDepartmentIdAgentsGET) => PaginatedResult<{ agents: ILivechatDepartmentAgents[] }>; + POST: (params: LivechatDepartmentDepartmentIdAgentsPOST) => void; }; 'livechat/departments.available-by-unit/:id': { - GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ + GET: (params: LivechatDepartmentsAvailableByUnitIdProps) => PaginatedResult<{ departments: ILivechatDepartment[]; }>; }; 'livechat/departments.by-unit/': { - GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ + GET: (params: LivechatDepartmentsByUnitProps) => PaginatedResult<{ departments: ILivechatDepartment[]; }>; }; 'livechat/departments.by-unit/:id': { - GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ + GET: (params: LivechatDepartmentsByUnitIdProps) => PaginatedResult<{ departments: ILivechatDepartment[]; }>; }; @@ -110,7 +861,7 @@ export type OmnichannelEndpoints = { }; 'livechat/custom-fields': { - GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ + GET: (params: LivechatCustomFieldsProps) => PaginatedResult<{ customFields: [ { _id: string; @@ -120,28 +871,23 @@ export type OmnichannelEndpoints = { }>; }; 'livechat/rooms': { - GET: (params: { - guest: string; - fname: string; - servedBy: string[]; - status: string; - department: string; - from: string; - to: string; - customFields: any; - current: number; - itemsPerPage: number; - tags: string[]; - }) => PaginatedResult<{ + GET: (params: LivechatRoomsProps) => PaginatedResult<{ rooms: IOmnichannelRoom[]; }>; }; 'livechat/:rid/messages': { - GET: (params: PaginatedRequest<{ query: string }>) => PaginatedResult<{ + GET: (params: LivechatRidMessagesProps) => PaginatedResult<{ messages: IMessage[]; }>; }; + 'livechat/users/manager': { + GET: (params: LivechatUsersManagerGETProps) => PaginatedResult<{ + users: ILivechatAgent[]; + }>; + POST: (params: { username: string }) => { success: boolean }; + }; + 'livechat/users/manager/:_id': { GET: ( params: PaginatedRequest<{ @@ -151,11 +897,11 @@ export type OmnichannelEndpoints = { DELETE: () => void; }; - 'livechat/users/manager': { + 'livechat/users/agent': { GET: (params: PaginatedRequest<{ text?: string }>) => PaginatedResult<{ users: ILivechatAgent[]; }>; - POST: (params: { username: string }) => { success: boolean }; + POST: (params: LivechatUsersManagerPOSTProps) => { success: boolean }; }; 'livechat/users/agent/:_id': { @@ -167,13 +913,6 @@ export type OmnichannelEndpoints = { DELETE: () => { success: boolean }; }; - 'livechat/users/agent': { - GET: (params: PaginatedRequest<{ text?: string }>) => PaginatedResult<{ - users: ILivechatAgent[]; - }>; - POST: (params: { username: string }) => { success: boolean }; - }; - 'livechat/visitor': { POST: (params: { visitor: ILivechatVisitorDTO }) => { visitor: ILivechatVisitor; @@ -181,39 +920,32 @@ export type OmnichannelEndpoints = { }; 'livechat/visitor/:token': { - GET: (params: { token: string }) => { visitor: ILivechatVisitor }; - DELETE: (params: { token: string }) => { + GET: (params: LivechatVisitorTokenGet) => { visitor: ILivechatVisitor }; + DELETE: (params: LivechatVisitorTokenDelete) => { visitor: { _id: string; ts: string }; }; }; 'livechat/visitor/:token/room': { - GET: (params: { token: string }) => { rooms: IOmnichannelRoom[] }; + GET: (params: LivechatVisitorTokenRoom) => { rooms: IOmnichannelRoom[] }; }; 'livechat/visitor.callStatus': { - POST: (params: { token: string; callStatus: string; rid: string; callId: string }) => { + POST: (params: LivechatVisitorCallStatus) => { token: string; callStatus: string; }; }; 'livechat/visitor.status': { - POST: (params: { token: string; status: string }) => { + POST: (params: LivechatVisitorStatus) => { token: string; status: string; }; }; 'livechat/queue': { - GET: (params: { - agentId?: ILivechatAgent['_id']; - includeOfflineAgents?: boolean; - departmentId?: ILivechatAgent['_id']; - offset: number; - count: number; - sort: string; - }) => { + GET: (params: LivechatQueueProps) => { queue: { chats: number; department: { _id: string; name: string }; @@ -229,13 +961,7 @@ export type OmnichannelEndpoints = { }; 'canned-responses': { - GET: ( - params: PaginatedRequest<{ - scope?: string; - departmentId?: string; - text?: string; - }>, - ) => PaginatedResult<{ + GET: (params: CannedResponsesProps) => PaginatedResult<{ cannedResponses: IOmnichannelCannedResponse[]; }>; }; diff --git a/packages/rest-typings/src/v1/permissions.ts b/packages/rest-typings/src/v1/permissions.ts index 85506bf1aff8..3128db4ec259 100644 --- a/packages/rest-typings/src/v1/permissions.ts +++ b/packages/rest-typings/src/v1/permissions.ts @@ -1,13 +1,33 @@ -import Ajv, { JSONSchemaType } from 'ajv'; +import Ajv from 'ajv'; import type { IPermission } from '@rocket.chat/core-typings'; -const ajv = new Ajv(); +const ajv = new Ajv({ + coerceTypes: true, +}); + +type PermissionsListAllProps = { + updatedSince?: string; +}; + +const permissionListAllSchema = { + type: 'object', + properties: { + updatedSince: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isPermissionsListAll = ajv.compile<PermissionsListAllProps>(permissionListAllSchema); type PermissionsUpdateProps = { permissions: { _id: string; roles: string[] }[]; }; -const permissionUpdatePropsSchema: JSONSchemaType<PermissionsUpdateProps> = { +const permissionUpdatePropsSchema = { type: 'object', properties: { permissions: { @@ -31,11 +51,11 @@ const permissionUpdatePropsSchema: JSONSchemaType<PermissionsUpdateProps> = { additionalProperties: false, }; -export const isBodyParamsValidPermissionUpdate = ajv.compile(permissionUpdatePropsSchema); +export const isBodyParamsValidPermissionUpdate = ajv.compile<PermissionsUpdateProps>(permissionUpdatePropsSchema); export type PermissionsEndpoints = { 'permissions.listAll': { - GET: (params: { updatedSince?: string }) => { + GET: (params: PermissionsListAllProps) => { update: IPermission[]; remove: IPermission[]; }; diff --git a/packages/rest-typings/src/v1/push.ts b/packages/rest-typings/src/v1/push.ts index d0b07c6afbca..54568328bc42 100644 --- a/packages/rest-typings/src/v1/push.ts +++ b/packages/rest-typings/src/v1/push.ts @@ -1,12 +1,64 @@ import type { IMessage, IPushNotificationConfig, IPushTokenTypes, IPushToken } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type PushTokenProps = { + id?: string; + type: IPushTokenTypes; + value: string; + appName: string; +}; + +const PushTokenPropsSchema = { + type: 'object', + properties: { + id: { + type: 'string', + nullable: true, + }, + type: { + type: 'string', + }, + value: { + type: 'string', + }, + appName: { + type: 'string', + }, + }, + required: ['type', 'value', 'appName'], + additionalProperties: false, +}; + +export const isPushTokenProps = ajv.compile<PushTokenProps>(PushTokenPropsSchema); + +type PushGetProps = { + id: string; +}; + +const PushGetPropsSchema = { + type: 'object', + properties: { + id: { + type: 'string', + }, + }, + required: ['id'], + additionalProperties: false, +}; + +export const isPushGetProps = ajv.compile<PushGetProps>(PushGetPropsSchema); export type PushEndpoints = { 'push.token': { - POST: (payload: { id?: string; type: IPushTokenTypes; value: string; appName: string }) => { result: IPushToken }; + POST: (payload: PushTokenProps) => { result: IPushToken }; DELETE: (payload: { token: string }) => void; }; 'push.get': { - GET: (params: { id: string }) => { + GET: (params: PushGetProps) => { data: { message: IMessage; notification: IPushNotificationConfig; diff --git a/packages/rest-typings/src/v1/roles.ts b/packages/rest-typings/src/v1/roles.ts index 6e3d729a0222..8c4135fc4b5b 100644 --- a/packages/rest-typings/src/v1/roles.ts +++ b/packages/rest-typings/src/v1/roles.ts @@ -1,11 +1,15 @@ -import Ajv, { JSONSchemaType } from 'ajv'; +import Ajv from 'ajv'; import type { RocketChatRecordDeleted, IRole, IUserInRole } from '@rocket.chat/core-typings'; -const ajv = new Ajv(); +import type { PaginatedRequest } from '../helpers/PaginatedRequest'; + +const ajv = new Ajv({ + coerceTypes: true, +}); type RoleCreateProps = Pick<IRole, 'name'> & Partial<Pick<IRole, 'description' | 'scope' | 'mandatory2fa'>>; -const roleCreatePropsSchema: JSONSchemaType<RoleCreateProps> = { +const roleCreatePropsSchema = { type: 'object', properties: { name: { @@ -29,14 +33,14 @@ const roleCreatePropsSchema: JSONSchemaType<RoleCreateProps> = { additionalProperties: false, }; -export const isRoleCreateProps = ajv.compile(roleCreatePropsSchema); +export const isRoleCreateProps = ajv.compile<RoleCreateProps>(roleCreatePropsSchema); type RoleUpdateProps = { roleId: IRole['_id']; name: IRole['name']; } & Partial<RoleCreateProps>; -const roleUpdatePropsSchema: JSONSchemaType<RoleUpdateProps> = { +const roleUpdatePropsSchema = { type: 'object', properties: { roleId: { @@ -63,11 +67,11 @@ const roleUpdatePropsSchema: JSONSchemaType<RoleUpdateProps> = { additionalProperties: false, }; -export const isRoleUpdateProps = ajv.compile(roleUpdatePropsSchema); +export const isRoleUpdateProps = ajv.compile<RoleUpdateProps>(roleUpdatePropsSchema); type RoleDeleteProps = { roleId: IRole['_id'] }; -const roleDeletePropsSchema: JSONSchemaType<RoleDeleteProps> = { +const roleDeletePropsSchema = { type: 'object', properties: { roleId: { @@ -78,7 +82,7 @@ const roleDeletePropsSchema: JSONSchemaType<RoleDeleteProps> = { additionalProperties: false, }; -export const isRoleDeleteProps = ajv.compile(roleDeletePropsSchema); +export const isRoleDeleteProps = ajv.compile<RoleDeleteProps>(roleDeletePropsSchema); type RoleAddUserToRoleProps = { username: string; @@ -88,7 +92,7 @@ type RoleAddUserToRoleProps = { roomId?: string; }; -const roleAddUserToRolePropsSchema: JSONSchemaType<RoleAddUserToRoleProps> = { +const roleAddUserToRolePropsSchema = { type: 'object', properties: { username: { @@ -111,7 +115,7 @@ const roleAddUserToRolePropsSchema: JSONSchemaType<RoleAddUserToRoleProps> = { additionalProperties: false, }; -export const isRoleAddUserToRoleProps = ajv.compile(roleAddUserToRolePropsSchema); +export const isRoleAddUserToRoleProps = ajv.compile<RoleAddUserToRoleProps>(roleAddUserToRolePropsSchema); type RoleRemoveUserFromRoleProps = { username: string; @@ -122,7 +126,7 @@ type RoleRemoveUserFromRoleProps = { scope?: string; }; -const roleRemoveUserFromRolePropsSchema: JSONSchemaType<RoleRemoveUserFromRoleProps> = { +const roleRemoveUserFromRolePropsSchema = { type: 'object', properties: { username: { @@ -149,7 +153,45 @@ const roleRemoveUserFromRolePropsSchema: JSONSchemaType<RoleRemoveUserFromRolePr additionalProperties: false, }; -export const isRoleRemoveUserFromRoleProps = ajv.compile(roleRemoveUserFromRolePropsSchema); +export const isRoleRemoveUserFromRoleProps = ajv.compile<RoleRemoveUserFromRoleProps>(roleRemoveUserFromRolePropsSchema); + +type RolesGetUsersInRoleProps = PaginatedRequest<{ + roomId?: string; + role: string; +}>; + +const RolesGetUsersInRolePropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + nullable: true, + }, + role: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['role'], + additionalProperties: false, +}; + +export const isRolesGetUsersInRoleProps = ajv.compile<RolesGetUsersInRoleProps>(RolesGetUsersInRolePropsSchema); type RoleSyncProps = { updatedSince?: string; @@ -182,7 +224,7 @@ export type RolesEndpoints = { }; 'roles.getUsersInRole': { - GET: (params: { roomId?: string; role: string; offset?: number; count?: number }) => { + GET: (params: RolesGetUsersInRoleProps) => { users: IUserInRole[]; total: number; }; diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index 647127c4b469..a57757a8476d 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -1,16 +1,406 @@ +/* eslint-disable @typescript-eslint/camelcase */ import type { IMessage, IRoom, IUser, RoomAdminFieldsType } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type RoomsAutoCompleteChannelAndPrivateProps = { selector: string }; + +const RoomsAutoCompleteChannelAndPrivateSchema = { + type: 'object', + properties: { + selector: { + type: 'string', + }, + }, + required: ['selector'], + additionalProperties: false, +}; + +export const isRoomsAutoCompleteChannelAndPrivateProps = ajv.compile<RoomsAutoCompleteChannelAndPrivateProps>( + RoomsAutoCompleteChannelAndPrivateSchema, +); + +type RoomsAutocompleteChannelAndPrivateWithPaginationProps = PaginatedRequest<{ selector: string }>; + +const RoomsAutocompleteChannelAndPrivateWithPaginationSchema = { + type: 'object', + properties: { + selector: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: ['selector'], + additionalProperties: false, +}; + +export const isRoomsAutocompleteChannelAndPrivateWithPaginationProps = ajv.compile<RoomsAutocompleteChannelAndPrivateWithPaginationProps>( + RoomsAutocompleteChannelAndPrivateWithPaginationSchema, +); + +type RoomsAutocompleteAvailableForTeamsProps = { name: string }; + +const RoomsAutocompleteAvailableForTeamsSchema = { + type: 'object', + properties: { + name: { + type: 'string', + }, + }, + required: ['name'], + additionalProperties: false, +}; + +export const isRoomsAutocompleteAvailableForTeamsProps = ajv.compile<RoomsAutocompleteAvailableForTeamsProps>( + RoomsAutocompleteAvailableForTeamsSchema, +); + +type RoomsInfoProps = { roomId: string } | { roomName: string }; + +const RoomsInfoSchema = { + oneOf: [ + { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + roomName: { + type: 'string', + }, + }, + required: ['roomName'], + additionalProperties: false, + }, + ], +}; + +export const isRoomsInfoProps = ajv.compile<RoomsInfoProps>(RoomsInfoSchema); + +type RoomsCreateDiscussionProps = { + prid: IRoom['_id']; + pmid?: IMessage['_id']; + t_name: string; // IRoom['fname'] + users?: IUser['username'][]; + encrypted?: boolean; + reply?: string; +}; + +const RoomsCreateDiscussionSchema = { + type: 'object', + properties: { + prid: { + type: 'string', + }, + pmid: { + type: 'string', + nullable: true, + }, + t_name: { + type: 'string', + nullable: true, + }, + users: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + encrypted: { + type: 'boolean', + nullable: true, + }, + reply: { + type: 'string', + nullable: true, + }, + }, + required: ['prid', 't_name'], + additionalProperties: false, +}; + +export const isRoomsCreateDiscussionProps = ajv.compile<RoomsCreateDiscussionProps>(RoomsCreateDiscussionSchema); + +type RoomsExportProps = { + rid: IRoom['_id']; + type: 'email' | 'file'; + toUsers?: IUser['username'][]; + toEmails?: string[]; + additionalEmails?: string; + subject?: string; + messages?: IMessage['_id'][]; + dateFrom?: string; + dateTo?: string; + format?: 'html' | 'json'; +}; + +const RoomsExportSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + type: { + type: 'string', + nullable: true, + }, + toUsers: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + toEmails: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + additionalEmails: { + type: 'string', + nullable: true, + }, + subject: { + type: 'string', + nullable: true, + }, + messages: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + dateFrom: { + type: 'string', + nullable: true, + }, + dateTo: { + type: 'string', + nullable: true, + }, + format: { + type: 'string', + nullable: true, + }, + }, + required: ['rid'], + additionalProperties: false, +}; + +export const isRoomsExportProps = ajv.compile<RoomsExportProps>(RoomsExportSchema); + +type RoomsAdminRoomsProps = PaginatedRequest<{ + filter?: string; + types?: string[]; +}>; + +const RoomsAdminRoomsSchema = { + type: 'object', + properties: { + filter: { + type: 'string', + nullable: true, + }, + types: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isRoomsAdminRoomsProps = ajv.compile<RoomsAdminRoomsProps>(RoomsAdminRoomsSchema); + +type RoomsAdminRoomsGetRoomProps = { rid?: string }; + +const RoomsAdminRoomsGetRoomSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isRoomsAdminRoomsGetRoomProps = ajv.compile<RoomsAdminRoomsGetRoomProps>(RoomsAdminRoomsGetRoomSchema); + +type RoomsChangeArchivationStateProps = { rid: string; action?: string }; + +const RoomsChangeArchivationStateSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + action: { + type: 'string', + nullable: true, + }, + }, + required: ['rid'], + additionalProperties: false, +}; + +export const isRoomsChangeArchivationStateProps = ajv.compile<RoomsChangeArchivationStateProps>(RoomsChangeArchivationStateSchema); + +type RoomsSaveRoomSettingsProps = { + rid: string; + roomAvatar?: string; + featured?: boolean; + roomName?: string; + roomTopic?: string; + roomAnnouncement?: string; + roomDescription?: string; + roomType?: IRoom['t']; + readOnly?: boolean; + reactWhenReadOnly?: boolean; + default?: boolean; + tokenpass?: string; + encrypted?: boolean; + favorite?: { + defaultValue?: boolean; + favorite?: boolean; + }; +}; + +const RoomsSaveRoomSettingsSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + roomAvatar: { + type: 'string', + nullable: true, + }, + featured: { + type: 'boolean', + nullable: true, + }, + roomName: { + type: 'string', + nullable: true, + }, + roomTopic: { + type: 'string', + nullable: true, + }, + roomAnnouncement: { + type: 'string', + nullable: true, + }, + roomDescription: { + type: 'string', + nullable: true, + }, + roomType: { + type: 'string', + nullable: true, + }, + readOnly: { + type: 'boolean', + nullable: true, + }, + reactWhenReadOnly: { + type: 'boolean', + nullable: true, + }, + default: { + type: 'boolean', + nullable: true, + }, + tokenpass: { + type: 'string', + nullable: true, + }, + encrypted: { + type: 'boolean', + nullable: true, + }, + favorite: { + type: 'object', + properties: { + defaultValue: { + type: 'boolean', + nullable: true, + }, + favorite: { + type: 'boolean', + nullable: true, + }, + }, + nullable: true, + }, + }, + required: ['rid'], + additionalProperties: false, +}; + +export const isRoomsSaveRoomSettingsProps = ajv.compile<RoomsSaveRoomSettingsProps>(RoomsSaveRoomSettingsSchema); + export type RoomsEndpoints = { 'rooms.autocomplete.channelAndPrivate': { - GET: (params: { selector: string }) => { + GET: (params: RoomsAutoCompleteChannelAndPrivateProps) => { items: IRoom[]; }; }; 'rooms.autocomplete.channelAndPrivate.withPagination': { - GET: (params: { selector: string; offset?: number; count?: number; sort?: string }) => { + GET: (params: RoomsAutocompleteChannelAndPrivateWithPaginationProps) => { items: IRoom[]; count: number; offset: number; @@ -18,81 +408,54 @@ export type RoomsEndpoints = { }; }; 'rooms.autocomplete.availableForTeams': { - GET: (params: { name: string }) => { + GET: (params: RoomsAutocompleteAvailableForTeamsProps) => { items: IRoom[]; }; }; 'rooms.info': { - GET: (params: { roomId: string } | { roomName: string }) => { + GET: (params: RoomsInfoProps) => { room: IRoom; }; }; - 'rooms.createDiscussion': { + 'rooms.cleanHistory': { POST: (params: { - prid: IRoom['_id']; - pmid?: IMessage['_id']; - t_name: IRoom['fname']; + roomId: IRoom['_id']; + latest: string; + oldest: string; + inclusive?: boolean; + excludePinned?: boolean; + filesOnly?: boolean; users?: IUser['username'][]; - encrypted?: boolean; - reply?: string; - }) => { + limit?: number; + ignoreDiscussion?: boolean; + ignoreThreads?: boolean; + }) => { _id: IRoom['_id']; count: number; success: boolean }; + }; + 'rooms.createDiscussion': { + POST: (params: RoomsCreateDiscussionProps) => { discussion: IRoom; }; }; 'rooms.export': { - POST: (params: { - rid: IRoom['_id']; - type: 'email' | 'file'; - toUsers?: IUser['username'][]; - toEmails?: string[]; - additionalEmails?: string; - subject?: string; - messages?: IMessage['_id'][]; - dateFrom?: string; - dateTo?: string; - format?: 'html' | 'json'; - }) => { + POST: (params: RoomsExportProps) => { missing?: []; success: boolean; }; }; 'rooms.adminRooms': { - GET: ( - params: PaginatedRequest<{ - filter?: string; - types?: string[]; - }>, - ) => PaginatedResult<{ rooms: Pick<IRoom, RoomAdminFieldsType>[] }>; + GET: (params: RoomsAdminRoomsProps) => PaginatedResult<{ rooms: Pick<IRoom, RoomAdminFieldsType>[] }>; }; 'rooms.adminRooms.getRoom': { - GET: (params: { rid?: string }) => Pick<IRoom, RoomAdminFieldsType>; + GET: (params: RoomsAdminRoomsGetRoomProps) => Pick<IRoom, RoomAdminFieldsType>; }; 'rooms.saveRoomSettings': { - POST: (params: { - rid: string; - roomAvatar?: string; - featured?: boolean; - roomName?: string; - roomTopic?: string; - roomAnnouncement?: string; - roomDescription?: string; - roomType?: IRoom['t']; - readOnly?: boolean; - reactWhenReadOnly?: boolean; - default?: boolean; - tokenpass?: string; - encrypted?: boolean; - favorite?: { - defaultValue?: boolean; - favorite?: boolean; - }; - }) => { + POST: (params: RoomsSaveRoomSettingsProps) => { success: boolean; rid: string; }; }; 'rooms.changeArchivationState': { - POST: (params: { rid: string; action?: string }) => { + POST: (params: RoomsChangeArchivationStateProps) => { success: boolean; }; }; diff --git a/packages/rest-typings/src/v1/statistics.ts b/packages/rest-typings/src/v1/statistics.ts index b3fd2fb723c3..837bbf1afa0e 100644 --- a/packages/rest-typings/src/v1/statistics.ts +++ b/packages/rest-typings/src/v1/statistics.ts @@ -1,4 +1,7 @@ import type { IStats } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +import type { PaginatedRequest } from '../helpers/PaginatedRequest'; type OTREnded = { rid: string }; @@ -18,12 +21,64 @@ export type TelemetryPayload = { params: Param[]; }; +const ajv = new Ajv({ + coerceTypes: true, +}); + +type StatisticsProps = { refresh?: 'true' | 'false' }; + +const StatisticsSchema = { + type: 'object', + properties: { + refresh: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isStatisticsProps = ajv.compile<StatisticsProps>(StatisticsSchema); + +type StatisticsListProps = PaginatedRequest<{ fields?: string }>; + +const StatisticsListSchema = { + type: 'object', + properties: { + fields: { + type: 'string', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isStatisticsListProps = ajv.compile<StatisticsListProps>(StatisticsListSchema); + export type StatisticsEndpoints = { 'statistics': { - GET: (params: { refresh?: 'true' | 'false' }) => IStats; + GET: (params: StatisticsProps) => IStats; }; 'statistics.list': { - GET: (params: { offset?: number; count?: number; sort?: string; fields?: string; query?: string }) => { + GET: (params: StatisticsListProps) => { statistics: IStats[]; count: number; offset: number; diff --git a/packages/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts index 0f9951f4dfec..4b311d1af3d2 100644 --- a/packages/rest-typings/src/v1/users.ts +++ b/packages/rest-typings/src/v1/users.ts @@ -1,26 +1,140 @@ import type { ITeam, IUser } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type UsersInfo = { userId?: IUser['_id']; userName?: IUser['username'] }; + +const UsersInfoSchema = { + type: 'object', + properties: { + userId: { + type: 'string', + nullable: true, + }, + userName: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isUsersInfoProps = ajv.compile<UsersInfo>(UsersInfoSchema); + +type Users2faSendEmailCode = { emailOrUsername: string }; + +const Users2faSendEmailCodeSchema = { + type: 'object', + properties: { + emailOrUsername: { + type: 'string', + }, + }, + required: ['emailOrUsername'], + additionalProperties: false, +}; + +export const isUsers2faSendEmailCodeProps = ajv.compile<Users2faSendEmailCode>(Users2faSendEmailCodeSchema); + +type UsersAutocomplete = { selector: string }; + +const UsersAutocompleteSchema = { + type: 'object', + properties: { + selector: { + type: 'string', + }, + }, + required: ['selector'], + additionalProperties: false, +}; + +export const isUsersAutocompleteProps = ajv.compile<UsersAutocomplete>(UsersAutocompleteSchema); + +type UsersListTeams = { userId: IUser['_id'] }; + +const UsersListTeamsSchema = { + type: 'object', + properties: { + userId: { + type: 'string', + }, + }, + required: ['userId'], + additionalProperties: false, +}; + +export const isUsersListTeamsProps = ajv.compile<UsersListTeams>(UsersListTeamsSchema); + +type UsersSetAvatar = { userId?: IUser['_id']; username?: IUser['username']; avatarUrl?: string }; + +const UsersSetAvatarSchema = { + type: 'object', + properties: { + userId: { + type: 'string', + nullable: true, + }, + username: { + type: 'string', + nullable: true, + }, + avatarUrl: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isUsersSetAvatarProps = ajv.compile<UsersSetAvatar>(UsersSetAvatarSchema); + +type UsersResetAvatar = { userId?: IUser['_id']; username?: IUser['username'] }; + +const UsersResetAvatarSchema = { + type: 'object', + properties: { + userId: { + type: 'string', + nullable: true, + }, + username: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isUsersResetAvatarProps = ajv.compile<UsersResetAvatar>(UsersResetAvatarSchema); export type UsersEndpoints = { 'users.info': { - GET: (params: { userId?: IUser['_id']; userName?: IUser['username'] }) => { + GET: (params: UsersInfo) => { user: IUser; }; }; 'users.2fa.sendEmailCode': { - POST: (params: { emailOrUsername: string }) => void; + POST: (params: Users2faSendEmailCode) => void; }; 'users.autocomplete': { - GET: (params: { selector: string }) => { + GET: (params: UsersAutocomplete) => { items: Required<Pick<IUser, '_id' | 'name' | 'username' | 'nickname' | 'status' | 'avatarETag'>>[]; }; }; 'users.listTeams': { - GET: (params: { userId: IUser['_id'] }) => { teams: Array<ITeam> }; + GET: (params: UsersListTeams) => { teams: Array<ITeam> }; }; 'users.setAvatar': { - POST: (params: { userId?: IUser['_id']; username?: IUser['username']; avatarUrl?: string }) => void; + POST: (params: UsersSetAvatar) => void; }; 'users.resetAvatar': { - POST: (params: { userId?: IUser['_id']; username?: IUser['username'] }) => void; + POST: (params: UsersResetAvatar) => void; }; }; diff --git a/packages/rest-typings/src/v1/videoConference.ts b/packages/rest-typings/src/v1/videoConference.ts index 4030043e2f02..c314e43deb6c 100644 --- a/packages/rest-typings/src/v1/videoConference.ts +++ b/packages/rest-typings/src/v1/videoConference.ts @@ -1,8 +1,34 @@ import type { IRoom } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type VideoConferenceJitsiUpdateTimeout = { roomId: IRoom['_id']; joiningNow?: boolean }; + +const VideoConferenceJitsiUpdateTimeoutSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + joiningNow: { + type: 'boolean', + nullable: true, + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isVideoConferenceJitsiUpdateTimeoutProps = ajv.compile<VideoConferenceJitsiUpdateTimeout>( + VideoConferenceJitsiUpdateTimeoutSchema, +); export type VideoConferenceEndpoints = { 'video-conference/jitsi.update-timeout': { - POST: (params: { roomId: IRoom['_id']; joiningNow?: boolean }) => { + POST: (params: VideoConferenceJitsiUpdateTimeout) => { jitsiTimeout: number; }; }; diff --git a/packages/rest-typings/src/v1/voip.ts b/packages/rest-typings/src/v1/voip.ts index 5398586927a3..fbdeafc23fef 100644 --- a/packages/rest-typings/src/v1/voip.ts +++ b/packages/rest-typings/src/v1/voip.ts @@ -8,72 +8,534 @@ import type { IVoipExtensionWithAgentInfo, IManagementServerConnectionStatus, IRegistrationInfo, - VoipClientEvents, } from '@rocket.chat/core-typings'; +import { VoipClientEvents } from '@rocket.chat/core-typings'; +import Ajv, { JSONSchemaType } from 'ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; +const ajv = new Ajv({ + coerceTypes: true, +}); + +/** *************************************************/ +type CustomSoundsList = PaginatedRequest<{ query: string }>; + +const CustomSoundsListSchema: JSONSchemaType<CustomSoundsList> = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isCustomSoundsListProps = ajv.compile<CustomSoundsList>(CustomSoundsListSchema); + +type ConnectorExtensionGetRegistrationInfoByUserId = { id: string }; + +const ConnectorExtensionGetRegistrationInfoByUserIdSchema: JSONSchemaType<ConnectorExtensionGetRegistrationInfoByUserId> = { + type: 'object', + properties: { + id: { + type: 'string', + }, + }, + required: ['id'], + additionalProperties: false, +}; + +export const isConnectorExtensionGetRegistrationInfoByUserIdProps = ajv.compile<ConnectorExtensionGetRegistrationInfoByUserId>( + ConnectorExtensionGetRegistrationInfoByUserIdSchema, +); + +type VoipQueuesGetQueuedCallsForThisExtension = { extension: string }; + +const VoipQueuesGetQueuedCallsForThisExtensionSchema: JSONSchemaType<VoipQueuesGetQueuedCallsForThisExtension> = { + type: 'object', + properties: { + extension: { + type: 'string', + }, + }, + required: ['extension'], + additionalProperties: false, +}; + +export const isVoipQueuesGetQueuedCallsForThisExtensionProps = ajv.compile<VoipQueuesGetQueuedCallsForThisExtension>( + VoipQueuesGetQueuedCallsForThisExtensionSchema, +); + +type VoipQueuesGetMembershipSubscription = { extension: string }; + +const VoipQueuesGetMembershipSubscriptionSchema: JSONSchemaType<VoipQueuesGetMembershipSubscription> = { + type: 'object', + properties: { + extension: { + type: 'string', + }, + }, + required: ['extension'], + additionalProperties: false, +}; + +export const isVoipQueuesGetMembershipSubscriptionProps = ajv.compile<VoipQueuesGetMembershipSubscription>( + VoipQueuesGetMembershipSubscriptionSchema, +); + +type OmnichannelExtensions = PaginatedRequest<{ + status?: string; + agentId?: string; + queues?: string[]; + extension?: string; +}>; + +const OmnichannelExtensionsSchema: JSONSchemaType<OmnichannelExtensions> = { + type: 'object', + properties: { + status: { + type: 'string', + nullable: true, + }, + agentId: { + type: 'string', + nullable: true, + }, + queues: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + extension: { + type: 'string', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isOmnichannelExtensionsProps = ajv.compile<OmnichannelExtensions>(OmnichannelExtensionsSchema); + +type OmnichannelExtension = + | { + userId: string; + type: 'free' | 'allocated' | 'available'; + } + | { + username: string; + type: 'free' | 'allocated' | 'available'; + }; + +const OmnichannelExtensionSchema: JSONSchemaType<OmnichannelExtension> = { + oneOf: [ + { + type: 'object', + properties: { + userId: { + type: 'string', + }, + type: { + type: 'string', + enum: ['free', 'allocated', 'available'], + }, + }, + required: ['userId', 'type'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + username: { + type: 'string', + }, + type: { + type: 'string', + enum: ['free', 'allocated', 'available'], + }, + }, + required: ['username', 'type'], + additionalProperties: false, + }, + ], +}; + +export const isOmnichannelExtensionProps = ajv.compile<OmnichannelExtension>(OmnichannelExtensionSchema); + +type OmnichannelAgentExtensionGET = { username: string }; + +const OmnichannelAgentExtensionGETSchema: JSONSchemaType<OmnichannelAgentExtensionGET> = { + type: 'object', + properties: { + username: { + type: 'string', + }, + }, + required: ['username'], + additionalProperties: false, +}; + +export const isOmnichannelAgentExtensionGETProps = ajv.compile<OmnichannelAgentExtensionGET>(OmnichannelAgentExtensionGETSchema); + +type OmnichannelAgentExtensionPOST = { userId: string; extension: string } | { username: string; extension: string }; + +const OmnichannelAgentExtensionPOSTSchema: JSONSchemaType<OmnichannelAgentExtensionPOST> = { + oneOf: [ + { + type: 'object', + properties: { + userId: { + type: 'string', + }, + extension: { + type: 'string', + }, + }, + required: ['userId', 'extension'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + username: { + type: 'string', + }, + extension: { + type: 'string', + }, + }, + required: ['username', 'extension'], + additionalProperties: false, + }, + ], +}; + +export const isOmnichannelAgentExtensionPOSTProps = ajv.compile<OmnichannelAgentExtensionPOST>(OmnichannelAgentExtensionPOSTSchema); + +type OmnichannelAgentExtensionDELETE = { username: string }; + +const OmnichannelAgentExtensionDELETESchema: JSONSchemaType<OmnichannelAgentExtensionDELETE> = { + type: 'object', + properties: { + username: { + type: 'string', + }, + }, + required: ['username'], + additionalProperties: false, +}; + +export const isOmnichannelAgentExtensionDELETEProps = ajv.compile<OmnichannelAgentExtensionDELETE>(OmnichannelAgentExtensionDELETESchema); + +type OmnichannelAgentsAvailable = PaginatedRequest<{ text?: string; includeExtension?: string }>; + +const OmnichannelAgentsAvailableSchema: JSONSchemaType<OmnichannelAgentsAvailable> = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + text: { + type: 'string', + nullable: true, + }, + includeExtension: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isOmnichannelAgentsAvailableProps = ajv.compile<OmnichannelAgentsAvailable>(OmnichannelAgentsAvailableSchema); + +type VoipEvents = { event: VoipClientEvents; rid: string; comment?: string }; + +const VoipEventsSchema: JSONSchemaType<VoipEvents> = { + type: 'object', + properties: { + event: { + type: 'string', + enum: Object.values(VoipClientEvents), + }, + rid: { + type: 'string', + }, + comment: { + type: 'string', + nullable: true, + }, + }, + required: ['event', 'rid'], + additionalProperties: false, +}; + +export const isVoipEventsProps = ajv.compile<VoipEvents>(VoipEventsSchema); + +type VoipRoom = { token: string; agentId: ILivechatAgent['_id'] } | { rid: string; token: string }; + +const VoipRoomSchema: JSONSchemaType<VoipRoom> = { + oneOf: [ + { + type: 'object', + properties: { + token: { + type: 'string', + }, + agentId: { + type: 'string', + }, + }, + required: ['token', 'agentId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + rid: { + type: 'string', + }, + token: { + type: 'string', + }, + }, + required: ['rid', 'token'], + additionalProperties: false, + }, + ], +}; + +export const isVoipRoomProps = ajv.compile<VoipRoom>(VoipRoomSchema); + +type VoipManagementServerCheckConnection = { host: string; port: string; username: string; password: string }; + +const VoipManagementServerCheckConnectionSchema: JSONSchemaType<VoipManagementServerCheckConnection> = { + type: 'object', + properties: { + host: { + type: 'string', + }, + port: { + type: 'string', + }, + username: { + type: 'string', + }, + password: { + type: 'string', + }, + }, + required: ['host', 'port', 'username', 'password'], + additionalProperties: false, +}; + +export const isVoipManagementServerCheckConnectionProps = ajv.compile<VoipManagementServerCheckConnection>( + VoipManagementServerCheckConnectionSchema, +); + +type VoipCallServerCheckConnection = { websocketUrl: string; host: string; port: string; path: string }; + +const VoipCallServerCheckConnectionSchema: JSONSchemaType<VoipCallServerCheckConnection> = { + type: 'object', + properties: { + websocketUrl: { + type: 'string', + }, + host: { + type: 'string', + }, + port: { + type: 'string', + }, + path: { + type: 'string', + }, + }, + required: ['websocketUrl', 'host', 'port', 'path'], + additionalProperties: false, +}; + +export const isVoipCallServerCheckConnectionProps = ajv.compile<VoipCallServerCheckConnection>(VoipCallServerCheckConnectionSchema); + +type VoipRooms = { + agents?: string[]; + open?: 'true' | 'false'; + createdAt?: string; + closedAt?: string; + tags?: string[]; + queue?: string; + visitorId?: string; +}; + +const VoipRoomsSchema: JSONSchemaType<VoipRooms> = { + type: 'object', + properties: { + agents: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + open: { + type: 'string', + enum: ['true', 'false'], + nullable: true, + }, + createdAt: { + type: 'string', + nullable: true, + }, + closedAt: { + type: 'string', + nullable: true, + }, + tags: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + queue: { + type: 'string', + nullable: true, + }, + visitorId: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isVoipRoomsProps = ajv.compile<VoipRooms>(VoipRoomsSchema); + +type VoipRoomClose = { rid: string; token: string; comment: string; tags?: string[] }; + +const VoipRoomCloseSchema: JSONSchemaType<VoipRoomClose> = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + token: { + type: 'string', + }, + comment: { + type: 'string', + }, + tags: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + }, + required: ['rid', 'token', 'comment'], + additionalProperties: false, +}; + +export const isVoipRoomCloseProps = ajv.compile<VoipRoomClose>(VoipRoomCloseSchema); + export type VoipEndpoints = { 'connector.extension.getRegistrationInfoByUserId': { - GET: (params: { id: string }) => IRegistrationInfo | { result: string }; + GET: (params: ConnectorExtensionGetRegistrationInfoByUserId) => IRegistrationInfo | { result: string }; }; 'voip/queues.getSummary': { GET: () => { summary: IQueueSummary[] }; }; 'voip/queues.getQueuedCallsForThisExtension': { - GET: (params: { extension: string }) => IQueueMembershipDetails; + GET: (params: VoipQueuesGetQueuedCallsForThisExtension) => IQueueMembershipDetails; }; 'voip/queues.getMembershipSubscription': { - GET: (params: { extension: string }) => IQueueMembershipSubscription; + GET: (params: VoipQueuesGetMembershipSubscription) => IQueueMembershipSubscription; }; 'omnichannel/extensions': { - GET: ( - params: PaginatedRequest<{ status?: string; agentId?: string; queues?: string[]; extension?: string }>, - ) => PaginatedResult<{ extensions: IVoipExtensionWithAgentInfo[] }>; + GET: (params: OmnichannelExtensions) => PaginatedResult<{ extensions: IVoipExtensionWithAgentInfo[] }>; }; 'omnichannel/extension': { - GET: ( - params: { userId: string; type: 'free' | 'allocated' | 'available' } | { username: string; type: 'free' | 'allocated' | 'available' }, - ) => { + GET: (params: OmnichannelExtension) => { extensions: string[]; }; }; 'omnichannel/agent/extension': { - GET: (params: { username: string }) => { extension: Pick<IUser, '_id' | 'username' | 'extension'> }; - POST: (params: { userId: string; extension: string } | { username: string; extension: string }) => void; - DELETE: (params: { username: string }) => void; + GET: (params: OmnichannelAgentExtensionGET) => { extension: Pick<IUser, '_id' | 'username' | 'extension'> }; + POST: (params: OmnichannelAgentExtensionPOST) => void; + DELETE: (params: OmnichannelAgentExtensionDELETE) => void; }; 'omnichannel/agents/available': { - GET: (params: PaginatedRequest<{ text?: string; includeExtension?: string }>) => PaginatedResult<{ agents: ILivechatAgent[] }>; + GET: (params: OmnichannelAgentsAvailable) => PaginatedResult<{ agents: ILivechatAgent[] }>; }; 'voip/events': { - POST: (params: { event: VoipClientEvents; rid: string; comment?: string }) => void; + POST: (params: VoipEvents) => void; }; 'voip/room': { - GET: (params: { token: string; agentId: ILivechatAgent['_id'] } | { rid: string; token: string }) => { + GET: (params: VoipRoom) => { room: IVoipRoom; newRoom: boolean; }; }; 'voip/managementServer/checkConnection': { - GET: (params: { host: string; port: string; username: string; password: string }) => IManagementServerConnectionStatus; + GET: (params: VoipManagementServerCheckConnection) => IManagementServerConnectionStatus; }; 'voip/callServer/checkConnection': { - GET: (params: { websocketUrl: string; host: string; port: string; path: string }) => IManagementServerConnectionStatus; + GET: (params: VoipCallServerCheckConnection) => IManagementServerConnectionStatus; }; 'voip/rooms': { - GET: (params: { - agents?: string[]; - open?: boolean; - createdAt?: string; - closedAt?: string; - tags?: string[]; - queue?: string; - visitorId?: string; - }) => PaginatedResult<{ rooms: IVoipRoom[] }>; + GET: (params: VoipRooms) => PaginatedResult<{ rooms: IVoipRoom[] }>; }; 'voip/room.close': { - POST: (params: { rid: string; token: string; comment: string; tags?: string[] }) => { rid: string }; + POST: (params: VoipRoomClose) => { rid: string }; }; }; diff --git a/yarn.lock b/yarn.lock index e4a840710703..bd6380a7cf07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2411,42 +2411,6 @@ __metadata: languageName: node linkType: hard -"@cypress/request@npm:^2.88.5": - version: 2.88.10 - resolution: "@cypress/request@npm:2.88.10" - dependencies: - aws-sign2: ~0.7.0 - aws4: ^1.8.0 - caseless: ~0.12.0 - combined-stream: ~1.0.6 - extend: ~3.0.2 - forever-agent: ~0.6.1 - form-data: ~2.3.2 - http-signature: ~1.3.6 - is-typedarray: ~1.0.0 - isstream: ~0.1.2 - json-stringify-safe: ~5.0.1 - mime-types: ~2.1.19 - performance-now: ^2.1.0 - qs: ~6.5.2 - safe-buffer: ^5.1.2 - tough-cookie: ~2.5.0 - tunnel-agent: ^0.6.0 - uuid: ^8.3.2 - checksum: 69c3e3b332e9be4866a900f6bcca5d274d8cea6c99707fbcce061de8dbab11c9b1e39f4c017f6e83e6e682717781d4f6106fd6b7cf9546580fcfac353b6676cf - languageName: node - linkType: hard - -"@cypress/xvfb@npm:^1.2.4": - version: 1.2.4 - resolution: "@cypress/xvfb@npm:1.2.4" - dependencies: - debug: ^3.1.0 - lodash.once: ^4.1.1 - checksum: 7bdcdaeb1bb692ec9d9bf8ec52538aa0bead6764753f4a067a171a511807a43fab016f7285a56bef6a606c2467ff3f1365e1ad2d2d583b81beed849ee1573fd1 - languageName: node - linkType: hard - "@dabh/diagnostics@npm:^2.0.2": version: 2.0.3 resolution: "@dabh/diagnostics@npm:2.0.3" @@ -4835,6 +4799,7 @@ __metadata: "@types/jsdom": ^16.2.12 "@types/jsdom-global": ^3.0.2 "@types/jsrsasign": ^9.0.3 + "@types/katex": ^0.14.0 "@types/ldapjs": ^2.2.1 "@types/less": ^3.0.2 "@types/lodash": ^4.14.182 @@ -4851,6 +4816,7 @@ __metadata: "@types/node": ^14.18.15 "@types/node-rsa": ^1.1.1 "@types/nodemailer": ^6.4.4 + "@types/object-path": ^0.11.1 "@types/parseurl": ^1.3.1 "@types/photoswipe": ^4.1.2 "@types/proxy-from-env": ^1.0.1 @@ -4860,6 +4826,7 @@ __metadata: "@types/rewire": ^2.5.28 "@types/semver": ^7.3.9 "@types/sharp": ^0.30.2 + "@types/sinon": ^10.0.11 "@types/speakeasy": ^2.0.7 "@types/string-strip-html": ^5.0.0 "@types/supertest": ^2.0.11 @@ -4909,9 +4876,6 @@ __metadata: cross-env: ^7.0.3 css-vars-ponyfill: ^2.4.7 csv-parse: ^5.0.4 - cypress: ^7.7.0 - cypress-real-events: ^1.7.0 - cypress-wait-until: ^1.7.2 date-fns: ^2.28.0 dompurify: ^2.3.6 ejson: ^2.2.2 @@ -4970,7 +4934,7 @@ __metadata: mailparser: ^3.4.0 marked: ^0.7.0 mem: ^8.1.1 - meteor-node-stubs: ^1.2.1 + meteor-node-stubs: ^1.2.3 mime-db: ^1.52.0 mime-type: ^4.0.0 mkdirp: ^1.0.4 @@ -5021,6 +4985,7 @@ __metadata: rewire: ^6.0.0 semver: ^7.3.7 sharp: ^0.30.4 + sinon: ^14.0.0 sip.js: ^0.20.0 sodium-native: ^3.3.0 sodium-plus: ^0.9.0 @@ -5285,7 +5250,7 @@ __metadata: languageName: node linkType: hard -"@sinonjs/commons@npm:^1.7.0": +"@sinonjs/commons@npm:^1.6.0, @sinonjs/commons@npm:^1.7.0, @sinonjs/commons@npm:^1.8.3": version: 1.8.3 resolution: "@sinonjs/commons@npm:1.8.3" dependencies: @@ -5294,6 +5259,15 @@ __metadata: languageName: node linkType: hard +"@sinonjs/fake-timers@npm:>=5, @sinonjs/fake-timers@npm:^9.1.2": + version: 9.1.2 + resolution: "@sinonjs/fake-timers@npm:9.1.2" + dependencies: + "@sinonjs/commons": ^1.7.0 + checksum: 7d3aef54e17c1073101cb64d953157c19d62a40e261a30923fa1ee337b049c5f29cc47b1f0c477880f42b5659848ba9ab897607ac8ea4acd5c30ddcfac57fca6 + languageName: node + linkType: hard + "@sinonjs/fake-timers@npm:^8.0.1": version: 8.1.0 resolution: "@sinonjs/fake-timers@npm:8.1.0" @@ -5303,6 +5277,24 @@ __metadata: languageName: node linkType: hard +"@sinonjs/samsam@npm:^6.1.1": + version: 6.1.1 + resolution: "@sinonjs/samsam@npm:6.1.1" + dependencies: + "@sinonjs/commons": ^1.6.0 + lodash.get: ^4.4.2 + type-detect: ^4.0.8 + checksum: a09b0914bf573f0da82bd03c64ba413df81a7c173818dc3f0a90c2652240ac835ef583f4d52f0b215e626633c91a4095c255e0669f6ead97241319f34f05e7fc + languageName: node + linkType: hard + +"@sinonjs/text-encoding@npm:^0.7.1": + version: 0.7.1 + resolution: "@sinonjs/text-encoding@npm:0.7.1" + checksum: 130de0bb568c5f8a611ec21d1a4e3f80ab0c5ec333010f49cfc1adc5cba6d8808699c8a587a46b0f0b016a1f4c1389bc96141e773e8460fcbb441875b2e91ba7 + languageName: node + linkType: hard + "@slack/client@npm:^4.12.0": version: 4.12.0 resolution: "@slack/client@npm:4.12.0" @@ -7315,6 +7307,13 @@ __metadata: languageName: node linkType: hard +"@types/katex@npm:^0.14.0": + version: 0.14.0 + resolution: "@types/katex@npm:0.14.0" + checksum: 330e0d0337ba48c87f5b793965fbad673653789bf6e50dfe8d726a7b0cbefd37195055e31503aae629814aa79447e4f23a4b87ad1ac565c0d9a9d9978836f39b + languageName: node + linkType: hard + "@types/keyv@npm:^3.1.1": version: 3.1.4 resolution: "@types/keyv@npm:3.1.4" @@ -7545,7 +7544,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^14.0.10, @types/node@npm:^14.0.26, @types/node@npm:^14.14.31, @types/node@npm:^14.14.37, @types/node@npm:^14.18.12": +"@types/node@npm:^14.0.10, @types/node@npm:^14.0.26, @types/node@npm:^14.14.37, @types/node@npm:^14.18.12": version: 14.18.12 resolution: "@types/node@npm:14.18.12" checksum: 8a0273caa0584020adb8802784fc7d4f18f05e6c205335b7f3818a91d6b0c22736b9f51da3428d5bc54076ad47f1a4d6d57990a3ce8489a520ac66b2b3ff24bc @@ -7582,6 +7581,13 @@ __metadata: languageName: node linkType: hard +"@types/object-path@npm:^0.11.1": + version: 0.11.1 + resolution: "@types/object-path@npm:0.11.1" + checksum: 007e819d1d9dc830491b60023b1502ef1e421416d9953d6fefcda7d06eb91548eef8ee30073a9cfb6a834ac977042f6e1a761cde2d6a7973b06ddca753be91e3 + languageName: node + linkType: hard + "@types/overlayscrollbars@npm:^1.12.0": version: 1.12.1 resolution: "@types/overlayscrollbars@npm:1.12.1" @@ -7830,14 +7836,23 @@ __metadata: languageName: node linkType: hard -"@types/sinonjs__fake-timers@npm:^6.0.2": - version: 6.0.4 - resolution: "@types/sinonjs__fake-timers@npm:6.0.4" - checksum: 200cb24235409964269465e8a94ad735ec8bab98f3b2405cd6351fa6f6399be268cbbd4e824c9d361d9431ae11070cff4c3b6400b18aff03cb7933985853c0c9 +"@types/sinon@npm:^10.0.11": + version: 10.0.11 + resolution: "@types/sinon@npm:10.0.11" + dependencies: + "@types/sinonjs__fake-timers": "*" + checksum: 196f3e26985dca5dfb593592e4b64463e536c047a9f43aa2b328b16024a3b0e3fb27b7a3f3972c6ef75749f55012737eb6c63a1c2e9782b7fe5cbbd25f75fd62 + languageName: node + linkType: hard + +"@types/sinonjs__fake-timers@npm:*": + version: 8.1.2 + resolution: "@types/sinonjs__fake-timers@npm:8.1.2" + checksum: bbc73a5ab6c0ec974929392f3d6e1e8db4ebad97ec506d785301e1c3d8a4f98a35b1aa95b97035daef02886fd8efd7788a2fa3ced2ec7105988bfd8dce61eedd languageName: node linkType: hard -"@types/sizzle@npm:*, @types/sizzle@npm:^2.3.2": +"@types/sizzle@npm:*": version: 2.3.3 resolution: "@types/sizzle@npm:2.3.3" checksum: 586a9fb1f6ff3e325e0f2cc1596a460615f0bc8a28f6e276ac9b509401039dd242fa8b34496d3a30c52f5b495873922d09a9e76c50c2ab2bcc70ba3fb9c4e160 @@ -8113,15 +8128,6 @@ __metadata: languageName: node linkType: hard -"@types/yauzl@npm:^2.9.1": - version: 2.9.2 - resolution: "@types/yauzl@npm:2.9.2" - dependencies: - "@types/node": "*" - checksum: dfb49abe82605615712fc694eaa4f7068fe30aa03f38c085e2c2e74408beaad30471d36da9654a811482ece2ea4405575fd99b19c0aa327ed2a9736b554bbf43 - languageName: node - linkType: hard - "@types/yoga-layout@npm:1.9.2": version: 1.9.2 resolution: "@types/yoga-layout@npm:1.9.2" @@ -8689,7 +8695,7 @@ __metadata: human-interval: ~1.0.0 moment-timezone: ~0.5.27 mongodb: ~3.5.0 - checksum: f5f68008298f9482631f1f494e392cd6b8ba7971a3b0ece81ae2abe60f53d67973ff4476156fa5c9c41b8b58c4ccd284e95c545e0523996dfd05f9a80b843e07 + checksum: acb4ebb7e7356f6e53e810d821eb6aa3d88bbfb9e85183e707517bee6d1eea1f189f38bdf0dd2b91360492ab7643134d510c320d2523d86596498ab98e59735b languageName: node linkType: hard @@ -8904,7 +8910,7 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.0": +"ansi-escapes@npm:^4.2.1": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2" dependencies: @@ -9076,7 +9082,7 @@ __metadata: languageName: node linkType: hard -"arch@npm:^2.1.0, arch@npm:^2.2.0": +"arch@npm:^2.1.0": version: 2.2.0 resolution: "arch@npm:2.2.0" checksum: e21b7635029fe8e9cdd5a026f9a6c659103e63fff423834323cdf836a1bb240a72d0c39ca8c470f84643385cf581bd8eda2cad8bf493e27e54bd9783abe9101f @@ -10454,13 +10460,6 @@ __metadata: languageName: node linkType: hard -"blob-util@npm:^2.0.2": - version: 2.0.2 - resolution: "blob-util@npm:2.0.2" - checksum: d543e6b92e4ca715ca33c78e89a07a2290d43e5b2bc897d7ec588c5c7bbf59df93e45225ac0c9258aa6ce4320358990f99c9288f1c48280f8ec5d7a2e088d19b - languageName: node - linkType: hard - "blockstack@npm:19.3.0": version: 19.3.0 resolution: "blockstack@npm:19.3.0" @@ -10487,7 +10486,7 @@ __metadata: languageName: node linkType: hard -"bluebird@npm:^3.1.5, bluebird@npm:^3.3.5, bluebird@npm:^3.5.0, bluebird@npm:^3.5.5, bluebird@npm:^3.7.2": +"bluebird@npm:^3.1.5, bluebird@npm:^3.3.5, bluebird@npm:^3.5.0, bluebird@npm:^3.5.5": version: 3.7.2 resolution: "bluebird@npm:3.7.2" checksum: 869417503c722e7dc54ca46715f70e15f4d9c602a423a02c825570862d12935be59ed9c7ba34a9b31f186c017c23cac6b54e35446f8353059c101da73eac22ef @@ -11137,13 +11136,6 @@ __metadata: languageName: node linkType: hard -"cachedir@npm:^2.3.0": - version: 2.3.0 - resolution: "cachedir@npm:2.3.0" - checksum: ec90cb0f2e6336e266aa748dbadf3da9e0b20e843e43f1591acab7a3f1451337dc2f26cb9dd833ae8cfefeffeeb43ef5b5ff62782a685f4e3c2305dd98482fcb - languageName: node - linkType: hard - "call-bind@npm:^1.0.0, call-bind@npm:^1.0.2": version: 1.0.2 resolution: "call-bind@npm:1.0.2" @@ -11573,13 +11565,6 @@ __metadata: languageName: node linkType: hard -"check-more-types@npm:^2.24.0": - version: 2.24.0 - resolution: "check-more-types@npm:2.24.0" - checksum: b09080ec3404d20a4b0ead828994b2e5913236ef44ed3033a27062af0004cf7d2091fbde4b396bf13b7ce02fb018bc9960b48305e6ab2304cd82d73ed7a51ef4 - languageName: node - linkType: hard - "cheerio-select@npm:^1.5.0": version: 1.6.0 resolution: "cheerio-select@npm:1.6.0" @@ -11832,7 +11817,7 @@ __metadata: languageName: node linkType: hard -"cli-table3@npm:^0.6.1, cli-table3@npm:~0.6.0": +"cli-table3@npm:^0.6.1": version: 0.6.1 resolution: "cli-table3@npm:0.6.1" dependencies: @@ -12209,13 +12194,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^5.1.0": - version: 5.1.0 - resolution: "commander@npm:5.1.0" - checksum: 0b7fec1712fbcc6230fcb161d8d73b4730fa91a21dc089515489402ad78810547683f058e2a9835929c212fead1d6a6ade70db28bbb03edbc2829a9ab7d69447 - languageName: node - linkType: hard - "commander@npm:^6.1.0, commander@npm:^6.2.1": version: 6.2.1 resolution: "commander@npm:6.2.1" @@ -12230,13 +12208,6 @@ __metadata: languageName: node linkType: hard -"common-tags@npm:^1.8.0": - version: 1.8.2 - resolution: "common-tags@npm:1.8.2" - checksum: 767a6255a84bbc47df49a60ab583053bb29a7d9687066a18500a516188a062c4e4cd52de341f22de0b07062e699b1b8fe3cfa1cb55b241cb9301aeb4f45b4dff - languageName: node - linkType: hard - "commondir@npm:^1.0.1": version: 1.0.1 resolution: "commondir@npm:1.0.1" @@ -13277,73 +13248,6 @@ __metadata: languageName: node linkType: hard -"cypress-real-events@npm:^1.7.0": - version: 1.7.0 - resolution: "cypress-real-events@npm:1.7.0" - peerDependencies: - cypress: ^4.x || ^5.x || ^6.x || ^7.x || ^8.x || ^9.x - checksum: 92981b05c44faa72e9785bc6d748a29a77373c3c784261b4e3b0e9e67f055a1d502c7573e786a4e25df040fc50961e5897f6d55ffa33ae08246f8a9518ca9a4b - languageName: node - linkType: hard - -"cypress-wait-until@npm:^1.7.2": - version: 1.7.2 - resolution: "cypress-wait-until@npm:1.7.2" - checksum: e3fe3c35ef8cfda39fb8919ae63a238bd580a98f5c02120306b32d6502ddfa9bfc3afde733cd9b282035b9f67e8386bd6c58bd59ca5fd2ea65291e6d9bac1ed7 - languageName: node - linkType: hard - -"cypress@npm:^7.7.0": - version: 7.7.0 - resolution: "cypress@npm:7.7.0" - dependencies: - "@cypress/request": ^2.88.5 - "@cypress/xvfb": ^1.2.4 - "@types/node": ^14.14.31 - "@types/sinonjs__fake-timers": ^6.0.2 - "@types/sizzle": ^2.3.2 - arch: ^2.2.0 - blob-util: ^2.0.2 - bluebird: ^3.7.2 - cachedir: ^2.3.0 - chalk: ^4.1.0 - check-more-types: ^2.24.0 - cli-cursor: ^3.1.0 - cli-table3: ~0.6.0 - commander: ^5.1.0 - common-tags: ^1.8.0 - dayjs: ^1.10.4 - debug: ^4.3.2 - enquirer: ^2.3.6 - eventemitter2: ^6.4.3 - execa: 4.1.0 - executable: ^4.1.1 - extract-zip: 2.0.1 - figures: ^3.2.0 - fs-extra: ^9.1.0 - getos: ^3.2.1 - is-ci: ^3.0.0 - is-installed-globally: ~0.4.0 - lazy-ass: ^1.6.0 - listr2: ^3.8.3 - lodash: ^4.17.21 - log-symbols: ^4.0.0 - minimist: ^1.2.5 - ospath: ^1.2.2 - pretty-bytes: ^5.6.0 - ramda: ~0.27.1 - request-progress: ^3.0.0 - supports-color: ^8.1.1 - tmp: ~0.2.1 - untildify: ^4.0.0 - url: ^0.11.0 - yauzl: ^2.10.0 - bin: - cypress: bin/cypress - checksum: 8ad8700d448949941801b6ff25549ef93e593b659b1605a936a8021032acb50f1849c9b8f8d1ba7df713de803b8203a1978231ccd0443f2e819bd9cf1b23d562 - languageName: node - linkType: hard - "d3-array@npm:2, d3-array@npm:^2.3.0": version: 2.12.1 resolution: "d3-array@npm:2.12.1" @@ -13548,7 +13452,7 @@ __metadata: languageName: node linkType: hard -"dayjs@npm:^1.10.4, dayjs@npm:^1.8.29": +"dayjs@npm:^1.8.29": version: 1.11.0 resolution: "dayjs@npm:1.11.0" checksum: 2d36f6d71345114cdcd89147adf9e05b4f8fe81684e08c8bf1f86b140aa0b86ecc3cae661a9348d96feb7fbefd03e1bc3697303688e95209670abcb36b4ece15 @@ -14085,6 +13989,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^5.0.0": + version: 5.1.0 + resolution: "diff@npm:5.1.0" + checksum: c7bf0df7c9bfbe1cf8a678fd1b2137c4fb11be117a67bc18a0e03ae75105e8533dbfb1cda6b46beb3586ef5aed22143ef9d70713977d5fb1f9114e21455fba90 + languageName: node + linkType: hard + "diffie-hellman@npm:^5.0.0": version: 5.0.3 resolution: "diffie-hellman@npm:5.0.3" @@ -14740,7 +14651,7 @@ __metadata: languageName: node linkType: hard -"enquirer@npm:2.3.6, enquirer@npm:^2.3.5, enquirer@npm:^2.3.6": +"enquirer@npm:2.3.6, enquirer@npm:^2.3.5": version: 2.3.6 resolution: "enquirer@npm:2.3.6" dependencies: @@ -15644,7 +15555,7 @@ __metadata: languageName: node linkType: hard -"eventemitter2@npm:^6.3.1, eventemitter2@npm:^6.4.3, eventemitter2@npm:^6.4.5": +"eventemitter2@npm:^6.3.1, eventemitter2@npm:^6.4.5": version: 6.4.5 resolution: "eventemitter2@npm:6.4.5" checksum: 84504f9cf0cc30205cdd46783fe9df3733435e5097f13070b678023110b5ef07847651808ae280cd94c42cd5976880211c7a40321a8ff8fa56f7c5f9c5c11960 @@ -15726,23 +15637,6 @@ __metadata: languageName: node linkType: hard -"execa@npm:4.1.0": - version: 4.1.0 - resolution: "execa@npm:4.1.0" - dependencies: - cross-spawn: ^7.0.0 - get-stream: ^5.0.0 - human-signals: ^1.1.1 - is-stream: ^2.0.0 - merge-stream: ^2.0.0 - npm-run-path: ^4.0.0 - onetime: ^5.1.0 - signal-exit: ^3.0.2 - strip-final-newline: ^2.0.0 - checksum: e30d298934d9c52f90f3847704fd8224e849a081ab2b517bbc02f5f7732c24e56a21f14cb96a08256deffeb2d12b2b7cb7e2b014a12fb36f8d3357e06417ed55 - languageName: node - linkType: hard - "execa@npm:^0.10.0": version: 0.10.0 resolution: "execa@npm:0.10.0" @@ -15844,7 +15738,7 @@ __metadata: languageName: node linkType: hard -"executable@npm:^4.1.0, executable@npm:^4.1.1": +"executable@npm:^4.1.0": version: 4.1.1 resolution: "executable@npm:4.1.1" dependencies: @@ -16040,23 +15934,6 @@ __metadata: languageName: node linkType: hard -"extract-zip@npm:2.0.1": - version: 2.0.1 - resolution: "extract-zip@npm:2.0.1" - dependencies: - "@types/yauzl": ^2.9.1 - debug: ^4.1.1 - get-stream: ^5.1.0 - yauzl: ^2.10.0 - dependenciesMeta: - "@types/yauzl": - optional: true - bin: - extract-zip: cli.js - checksum: 8cbda9debdd6d6980819cc69734d874ddd71051c9fe5bde1ef307ebcedfe949ba57b004894b585f758b7c9eeeea0e3d87f2dda89b7d25320459c2c9643ebb635 - languageName: node - linkType: hard - "extsprintf@npm:1.3.0": version: 1.3.0 resolution: "extsprintf@npm:1.3.0" @@ -16318,7 +16195,7 @@ __metadata: languageName: node linkType: hard -"figures@npm:^3.0.0, figures@npm:^3.2.0": +"figures@npm:^3.0.0": version: 3.2.0 resolution: "figures@npm:3.2.0" dependencies: @@ -16966,7 +16843,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^9.0.0, fs-extra@npm:^9.0.1, fs-extra@npm:^9.1.0": +"fs-extra@npm:^9.0.0, fs-extra@npm:^9.0.1": version: 9.1.0 resolution: "fs-extra@npm:9.1.0" dependencies: @@ -17358,15 +17235,6 @@ __metadata: languageName: node linkType: hard -"get-stream@npm:^5.0.0, get-stream@npm:^5.1.0": - version: 5.2.0 - resolution: "get-stream@npm:5.2.0" - dependencies: - pump: ^3.0.0 - checksum: 8bc1a23174a06b2b4ce600df38d6c98d2ef6d84e020c1ddad632ad75bac4e092eeb40e4c09e0761c35fc2dbc5e7fff5dab5e763a383582c4a167dd69a905bd12 - languageName: node - linkType: hard - "get-stream@npm:^6.0.0": version: 6.0.1 resolution: "get-stream@npm:6.0.1" @@ -17405,15 +17273,6 @@ __metadata: languageName: node linkType: hard -"getos@npm:^3.2.1": - version: 3.2.1 - resolution: "getos@npm:3.2.1" - dependencies: - async: ^3.2.0 - checksum: 42fd78a66d47cebd3e09de5566cc0044e034b08f4a000a310dbd89a77b02c65d8f4002554bfa495ea5bdc4fa9d515f5ac785a7cc474ba45383cc697f865eeaf1 - languageName: node - linkType: hard - "getpass@npm:^0.1.1": version: 0.1.7 resolution: "getpass@npm:0.1.7" @@ -17618,15 +17477,6 @@ __metadata: languageName: node linkType: hard -"global-dirs@npm:^3.0.0": - version: 3.0.0 - resolution: "global-dirs@npm:3.0.0" - dependencies: - ini: 2.0.0 - checksum: 953c17cf14bf6ee0e2100ae82a0d779934eed8a3ec5c94a7a4f37c5b3b592c31ea015fb9a15cf32484de13c79f4a814f3015152f3e1d65976cfbe47c1bfe4a88 - languageName: node - linkType: hard - "global-modules@npm:^1.0.0": version: 1.0.0 resolution: "global-modules@npm:1.0.0" @@ -18820,17 +18670,6 @@ __metadata: languageName: node linkType: hard -"http-signature@npm:~1.3.6": - version: 1.3.6 - resolution: "http-signature@npm:1.3.6" - dependencies: - assert-plus: ^1.0.0 - jsprim: ^2.0.2 - sshpk: ^1.14.1 - checksum: 10be2af4764e71fee0281392937050201ee576ac755c543f570d6d87134ce5e858663fe999a7adb3e4e368e1e356d0d7fec6b9542295b875726ff615188e7a0c - languageName: node - linkType: hard - "http2@https://github.com/node-apn/node-http2/archive/apn-2.1.4.tar.gz": version: 3.3.6 resolution: "http2@https://github.com/node-apn/node-http2/archive/apn-2.1.4.tar.gz" @@ -18872,13 +18711,6 @@ __metadata: languageName: node linkType: hard -"human-signals@npm:^1.1.1": - version: 1.1.1 - resolution: "human-signals@npm:1.1.1" - checksum: d587647c9e8ec24e02821b6be7de5a0fc37f591f6c4e319b3054b43fd4c35a70a94c46fc74d8c1a43c47fde157d23acd7421f375e1c1365b09a16835b8300205 - languageName: node - linkType: hard - "human-signals@npm:^2.1.0": version: 2.1.0 resolution: "human-signals@npm:2.1.0" @@ -19411,13 +19243,6 @@ __metadata: languageName: node linkType: hard -"ini@npm:2.0.0": - version: 2.0.0 - resolution: "ini@npm:2.0.0" - checksum: e7aadc5fb2e4aefc666d74ee2160c073995a4061556b1b5b4241ecb19ad609243b9cceafe91bae49c219519394bbd31512516cb22a3b1ca6e66d869e0447e84e - languageName: node - linkType: hard - "ini@npm:^1.3.4, ini@npm:^1.3.5, ini@npm:~1.3.0": version: 1.3.8 resolution: "ini@npm:1.3.8" @@ -19784,17 +19609,6 @@ __metadata: languageName: node linkType: hard -"is-ci@npm:^3.0.0": - version: 3.0.1 - resolution: "is-ci@npm:3.0.1" - dependencies: - ci-info: ^3.2.0 - bin: - is-ci: bin.js - checksum: 192c66dc7826d58f803ecae624860dccf1899fc1f3ac5505284c0a5cf5f889046ffeb958fa651e5725d5705c5bcb14f055b79150ea5fcad7456a9569de60260e - languageName: node - linkType: hard - "is-color-stop@npm:^1.0.0": version: 1.1.0 resolution: "is-color-stop@npm:1.1.0" @@ -20029,16 +19843,6 @@ __metadata: languageName: node linkType: hard -"is-installed-globally@npm:~0.4.0": - version: 0.4.0 - resolution: "is-installed-globally@npm:0.4.0" - dependencies: - global-dirs: ^3.0.0 - is-path-inside: ^3.0.2 - checksum: 3359840d5982d22e9b350034237b2cda2a12bac1b48a721912e1ab8e0631dd07d45a2797a120b7b87552759a65ba03e819f1bd63f2d7ab8657ec0b44ee0bf399 - languageName: node - linkType: hard - "is-jpg@npm:^2.0.0": version: 2.0.0 resolution: "is-jpg@npm:2.0.0" @@ -20191,13 +19995,6 @@ __metadata: languageName: node linkType: hard -"is-path-inside@npm:^3.0.2": - version: 3.0.3 - resolution: "is-path-inside@npm:3.0.3" - checksum: abd50f06186a052b349c15e55b182326f1936c89a78bf6c8f2b707412517c097ce04bc49a0ca221787bc44e1049f51f09a2ffb63d22899051988d3a618ba13e9 - languageName: node - linkType: hard - "is-plain-obj@npm:^1.0.0, is-plain-obj@npm:^1.1.0": version: 1.1.0 resolution: "is-plain-obj@npm:1.1.0" @@ -21554,18 +21351,6 @@ __metadata: languageName: node linkType: hard -"jsprim@npm:^2.0.2": - version: 2.0.2 - resolution: "jsprim@npm:2.0.2" - dependencies: - assert-plus: 1.0.0 - extsprintf: 1.3.0 - json-schema: 0.4.0 - verror: 1.10.0 - checksum: d175f6b1991e160cb0aa39bc857da780e035611986b5492f32395411879fdaf4e513d98677f08f7352dac93a16b66b8361c674b86a3fa406e2e7af6b26321838 - languageName: node - linkType: hard - "jsrsasign@npm:^10.5.19": version: 10.5.22 resolution: "jsrsasign@npm:10.5.22" @@ -21605,6 +21390,13 @@ __metadata: languageName: node linkType: hard +"just-extend@npm:^4.0.2": + version: 4.2.1 + resolution: "just-extend@npm:4.2.1" + checksum: ff9fdede240fad313efeeeb68a660b942e5586d99c0058064c78884894a2690dc09bba44c994ad4e077e45d913fef01a9240c14a72c657b53687ac58de53b39c + languageName: node + linkType: hard + "jwa@npm:^1.4.1": version: 1.4.1 resolution: "jwa@npm:1.4.1" @@ -21797,13 +21589,6 @@ __metadata: languageName: node linkType: hard -"lazy-ass@npm:^1.6.0": - version: 1.6.0 - resolution: "lazy-ass@npm:1.6.0" - checksum: 5a3ebb17915b03452320804466345382a6c25ac782ec4874fecdb2385793896cd459be2f187dc7def8899180c32ee0ab9a1aa7fe52193ac3ff3fe29bb0591729 - languageName: node - linkType: hard - "lazy-universal-dotenv@npm:^3.0.1": version: 3.0.1 resolution: "lazy-universal-dotenv@npm:3.0.1" @@ -22055,27 +21840,6 @@ __metadata: languageName: node linkType: hard -"listr2@npm:^3.8.3": - version: 3.14.0 - resolution: "listr2@npm:3.14.0" - dependencies: - cli-truncate: ^2.1.0 - colorette: ^2.0.16 - log-update: ^4.0.0 - p-map: ^4.0.0 - rfdc: ^1.3.0 - rxjs: ^7.5.1 - through: ^2.3.8 - wrap-ansi: ^7.0.0 - peerDependencies: - enquirer: ">= 2.3.0 < 3" - peerDependenciesMeta: - enquirer: - optional: true - checksum: fdb8b2d6bdf5df9371ebd5082bee46c6d0ca3d1e5f2b11fbb5a127839855d5f3da9d4968fce94f0a5ec67cac2459766abbb1faeef621065ebb1829b11ef9476d - languageName: node - linkType: hard - "load-json-file@npm:^1.0.0": version: 1.1.0 resolution: "load-json-file@npm:1.1.0" @@ -22340,7 +22104,7 @@ __metadata: languageName: node linkType: hard -"lodash.once@npm:^4.0.0, lodash.once@npm:^4.1.1": +"lodash.once@npm:^4.0.0": version: 4.1.1 resolution: "lodash.once@npm:4.1.1" checksum: d768fa9f9b4e1dc6453be99b753906f58990e0c45e7b2ca5a3b40a33111e5d17f6edf2f768786e2716af90a8e78f8f91431ab8435f761fef00f9b0c256f6d245 @@ -22438,7 +22202,7 @@ __metadata: languageName: node linkType: hard -"log-symbols@npm:4.1.0, log-symbols@npm:^4.0.0, log-symbols@npm:^4.1.0": +"log-symbols@npm:4.1.0, log-symbols@npm:^4.1.0": version: 4.1.0 resolution: "log-symbols@npm:4.1.0" dependencies: @@ -22459,18 +22223,6 @@ __metadata: languageName: node linkType: hard -"log-update@npm:^4.0.0": - version: 4.0.0 - resolution: "log-update@npm:4.0.0" - dependencies: - ansi-escapes: ^4.3.0 - cli-cursor: ^3.1.0 - slice-ansi: ^4.0.0 - wrap-ansi: ^6.2.0 - checksum: ae2f85bbabc1906034154fb7d4c4477c79b3e703d22d78adee8b3862fa913942772e7fa11713e3d96fb46de4e3cabefbf5d0a544344f03b58d3c4bff52aa9eb2 - languageName: node - linkType: hard - "logalot@npm:^2.0.0, logalot@npm:^2.1.0": version: 2.1.0 resolution: "logalot@npm:2.1.0" @@ -23301,7 +23053,7 @@ __metadata: languageName: node linkType: hard -"meteor-node-stubs@npm:^1.2.1": +"meteor-node-stubs@npm:^1.2.3": version: 1.2.3 resolution: "meteor-node-stubs@npm:1.2.3" dependencies: @@ -24395,6 +24147,19 @@ __metadata: languageName: node linkType: hard +"nise@npm:^5.1.1": + version: 5.1.1 + resolution: "nise@npm:5.1.1" + dependencies: + "@sinonjs/commons": ^1.8.3 + "@sinonjs/fake-timers": ">=5" + "@sinonjs/text-encoding": ^0.7.1 + just-extend: ^4.0.2 + path-to-regexp: ^1.7.0 + checksum: d8be29e84a014743c9a10f428fac86f294ac5f92bed1f606fe9b551e935f494d8e0ce1af8a12673c6014010ec7f771f2d48aa5c8e116f223eb4f40c5e1ab44b3 + languageName: node + linkType: hard + "nkeys.js@npm:^1.0.0-9": version: 1.0.0-9 resolution: "nkeys.js@npm:1.0.0-9" @@ -24836,7 +24601,7 @@ __metadata: languageName: node linkType: hard -"npm-run-path@npm:^4.0.0, npm-run-path@npm:^4.0.1": +"npm-run-path@npm:^4.0.1": version: 4.0.1 resolution: "npm-run-path@npm:4.0.1" dependencies: @@ -25353,13 +25118,6 @@ __metadata: languageName: node linkType: hard -"ospath@npm:^1.2.2": - version: 1.2.2 - resolution: "ospath@npm:1.2.2" - checksum: 505f48a4f4f1c557d6c656ec985707726e3714721680139be037613e903aa8c8fa4ddd8d1342006f9b2dc0065e6e20f8b7bea2ee05354f31257044790367b347 - languageName: node - linkType: hard - "outdent@npm:~0.8.0": version: 0.8.0 resolution: "outdent@npm:0.8.0" @@ -26064,6 +25822,15 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:^1.7.0": + version: 1.8.0 + resolution: "path-to-regexp@npm:1.8.0" + dependencies: + isarray: 0.0.1 + checksum: 709f6f083c0552514ef4780cb2e7e4cf49b0cc89a97439f2b7cc69a608982b7690fb5d1720a7473a59806508fc2dae0be751ba49f495ecf89fd8fbc62abccbcd + languageName: node + linkType: hard + "path-to-regexp@npm:^6.2.0": version: 6.2.0 resolution: "path-to-regexp@npm:6.2.0" @@ -27400,13 +27167,6 @@ __metadata: languageName: node linkType: hard -"pretty-bytes@npm:^5.6.0": - version: 5.6.0 - resolution: "pretty-bytes@npm:5.6.0" - checksum: 9c082500d1e93434b5b291bd651662936b8bd6204ec9fa17d563116a192d6d86b98f6d328526b4e8d783c07d5499e2614a807520249692da9ec81564b2f439cd - languageName: node - linkType: hard - "pretty-error@npm:^2.1.1": version: 2.1.2 resolution: "pretty-error@npm:2.1.2" @@ -27974,13 +27734,6 @@ __metadata: languageName: node linkType: hard -"ramda@npm:~0.27.1": - version: 0.27.2 - resolution: "ramda@npm:0.27.2" - checksum: 28d6735dd1eea1a796c56cf6111f3673c6105bbd736e521cdd7826c46a18eeff337c2dba4668f6eed990d539b9961fd6db19aa46ccc1530ba67a396c0a9f580d - languageName: node - linkType: hard - "randexp@npm:0.4.6": version: 0.4.6 resolution: "randexp@npm:0.4.6" @@ -29211,15 +28964,6 @@ __metadata: languageName: node linkType: hard -"request-progress@npm:^3.0.0": - version: 3.0.0 - resolution: "request-progress@npm:3.0.0" - dependencies: - throttleit: ^1.0.0 - checksum: 6ea1761dcc8a8b7b5894afd478c0286aa31bd69438d7050294bd4fd0d0b3e09b5cde417d38deef9c49809039c337d8744e4bb49d8632b0c3e4ffa5e8a687e0fd - languageName: node - linkType: hard - "request-promise-core@npm:1.1.4": version: 1.1.4 resolution: "request-promise-core@npm:1.1.4" @@ -29729,15 +29473,6 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.5.1": - version: 7.5.5 - resolution: "rxjs@npm:7.5.5" - dependencies: - tslib: ^2.1.0 - checksum: e034f60805210cce756dd2f49664a8108780b117cf5d0e2281506e9e6387f7b4f1532d974a8c8b09314fa7a16dd2f6cff3462072a5789672b5dcb45c4173f3c6 - languageName: node - linkType: hard - "safe-buffer@npm:5.1.1": version: 5.1.1 resolution: "safe-buffer@npm:5.1.1" @@ -30452,6 +30187,20 @@ __metadata: languageName: node linkType: hard +"sinon@npm:^14.0.0": + version: 14.0.0 + resolution: "sinon@npm:14.0.0" + dependencies: + "@sinonjs/commons": ^1.8.3 + "@sinonjs/fake-timers": ^9.1.2 + "@sinonjs/samsam": ^6.1.1 + diff: ^5.0.0 + nise: ^5.1.1 + supports-color: ^7.2.0 + checksum: b2aeeb0cdc2cd30f904ccbcd60bae4e1b3dcf3aeeface09c1832db0336be0dbaa461f3b91b769bed84f05c83d45d5072a9da7ee14bc7289daeda2a1214fe173c + languageName: node + linkType: hard + "sip.js@npm:^0.20.0": version: 0.20.0 resolution: "sip.js@npm:0.20.0" @@ -30947,7 +30696,7 @@ __metadata: languageName: node linkType: hard -"sshpk@npm:^1.14.1, sshpk@npm:^1.7.0": +"sshpk@npm:^1.7.0": version: 1.17.0 resolution: "sshpk@npm:1.17.0" dependencies: @@ -31769,7 +31518,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:8.1.1, supports-color@npm:^8.0.0, supports-color@npm:^8.1.1": +"supports-color@npm:8.1.1, supports-color@npm:^8.0.0": version: 8.1.1 resolution: "supports-color@npm:8.1.1" dependencies: @@ -31803,7 +31552,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0": +"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0, supports-color@npm:^7.2.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" dependencies: @@ -32234,13 +31983,6 @@ __metadata: languageName: node linkType: hard -"throttleit@npm:^1.0.0": - version: 1.0.0 - resolution: "throttleit@npm:1.0.0" - checksum: 1b2db4d2454202d589e8236c07a69d2fab838876d370030ebea237c34c0a7d1d9cf11c29f994531ebb00efd31e9728291042b7754f2798a8352ec4463455b659 - languageName: node - linkType: hard - "through2@npm:^2.0.0, through2@npm:~2.0.3": version: 2.0.5 resolution: "through2@npm:2.0.5" @@ -32358,7 +32100,7 @@ __metadata: languageName: node linkType: hard -"tmp@npm:^0.2.1, tmp@npm:~0.2.1": +"tmp@npm:^0.2.1": version: 0.2.1 resolution: "tmp@npm:0.2.1" dependencies: @@ -32753,7 +32495,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:~2.3.1": +"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.2.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:~2.3.1": version: 2.3.1 resolution: "tslib@npm:2.3.1" checksum: de17a98d4614481f7fcb5cd53ffc1aaf8654313be0291e1bfaee4b4bb31a20494b7d218ff2e15017883e8ea9626599b3b0e0229c18383ba9dce89da2adf15cb9 @@ -33019,7 +32761,7 @@ __metadata: languageName: node linkType: hard -"type-detect@npm:4.0.8, type-detect@npm:^4.0.0, type-detect@npm:^4.0.5": +"type-detect@npm:4.0.8, type-detect@npm:^4.0.0, type-detect@npm:^4.0.5, type-detect@npm:^4.0.8": version: 4.0.8 resolution: "type-detect@npm:4.0.8" checksum: 62b5628bff67c0eb0b66afa371bd73e230399a8d2ad30d852716efcc4656a7516904570cd8631a49a3ce57c10225adf5d0cbdcb47f6b0255fe6557c453925a15 @@ -33546,13 +33288,6 @@ __metadata: languageName: node linkType: hard -"untildify@npm:^4.0.0": - version: 4.0.0 - resolution: "untildify@npm:4.0.0" - checksum: 39ced9c418a74f73f0a56e1ba4634b4d959422dff61f4c72a8e39f60b99380c1b45ed776fbaa0a4101b157e4310d873ad7d114e8534ca02609b4916bb4187fb9 - languageName: node - linkType: hard - "unzip-response@npm:^2.0.1": version: 2.0.1 resolution: "unzip-response@npm:2.0.1"